diff --git a/cmd/e2ebench/diff.go b/cmd/e2ebench/diff.go index d49bc88a9f..c8f0650902 100644 --- a/cmd/e2ebench/diff.go +++ b/cmd/e2ebench/diff.go @@ -243,11 +243,11 @@ func renderDiff(r diffReport) string { fmt.Fprintf(&b, "| Tokens (prompt / completion) | %s / %s |\n", comma(r.m.PromptTokens), comma(r.m.CompletionTokens)) fmt.Fprintf(&b, "| Model calls | %d |\n", r.m.Steps) fmt.Fprintf(&b, "| Cost | %s%.4f |\n", currencySym(r.m.Currency), r.m.Cost) - if r.m.CapabilityRoutes > 0 || r.m.CapabilitySkillInvocations > 0 || r.m.CapabilityMCPCall > 0 || r.m.ReadinessChecks > 0 { + if r.m.CapabilityRoutes > 0 || r.m.CapabilitySkillInvocations > 0 || r.m.CapabilityMCPCall > 0 { fmt.Fprintf(&b, "| Capability routes (semantic) | %d (%d) |\n", r.m.CapabilityRoutes, r.m.CapabilitySemanticRoutes) fmt.Fprintf(&b, "| Routed candidates (require / prefer / suggest / declined) | %d (%d / %d / %d / %d) |\n", r.m.CapabilityRoutedCandidates, r.m.CapabilityRoutedRequire, r.m.CapabilityRoutedPrefer, r.m.CapabilityRoutedSuggest, r.m.CapabilityDeclines) fmt.Fprintf(&b, "| Skill invocations / MCP proxy calls | %d / %d |\n", r.m.CapabilitySkillInvocations, r.m.CapabilityMCPCall) - fmt.Fprintf(&b, "| Review blocks / readiness recoveries | %d / %d |\n", r.m.CapabilityReviewBlocks, r.m.ReadinessRecoveries) + fmt.Fprintf(&b, "| Review blocks | %d |\n", r.m.CapabilityReviewBlocks) if r.m.CapabilityRouterCost > 0 || r.m.CapabilityRouterLatencyMs > 0 { fmt.Fprintf(&b, "| Capability-router cost / latency | %s%.4f / %dms |\n", currencySym(r.m.Currency), r.m.CapabilityRouterCost, r.m.CapabilityRouterLatencyMs) } diff --git a/cmd/e2ebench/main.go b/cmd/e2ebench/main.go index e117abcd0d..eb5558513a 100644 --- a/cmd/e2ebench/main.go +++ b/cmd/e2ebench/main.go @@ -85,8 +85,6 @@ type runMetrics struct { // graph, which is where a scheduling wait is recorded and nowhere else. Fanout *fanoutMetrics `json:"fanout,omitempty"` // Optional Delivery capability counters (omitempty for baseline/old metrics). - ReadinessChecks int `json:"readiness_checks,omitempty"` - ReadinessRecoveries int `json:"readiness_recoveries,omitempty"` CapabilityRoutes int `json:"capability_routes,omitempty"` CapabilityRoutedCandidates int `json:"capability_routed_candidates,omitempty"` CapabilityRoutedRequire int `json:"capability_routed_require,omitempty"` diff --git a/desktop/frontend-next/src/port/wire.ts b/desktop/frontend-next/src/port/wire.ts index 35b5d09343..c40ba799d4 100644 --- a/desktop/frontend-next/src/port/wire.ts +++ b/desktop/frontend-next/src/port/wire.ts @@ -763,8 +763,8 @@ export interface WireEvent { attachments?: StoredAttachment[]; // steer: the host wrote this guidance, so it is never drawn as the person's. hostAuthored?: boolean; - // recovery_paused is no longer emitted; sessions recorded before the retry - // budgets were removed still carry it, so a reader has to render it. + // final_readiness and recovery_paused are no longer emitted; sessions recorded + // before they were removed still carry them, so a reader has to render them. outcome?: "final_readiness" | "recovery_paused" | "no_progress"; phase?: string; retryAttempt?: number; diff --git a/internal/assembly/boot/audit_forwarding_test.go b/internal/assembly/boot/audit_forwarding_test.go index 7cce77bf3b..30430928c7 100644 --- a/internal/assembly/boot/audit_forwarding_test.go +++ b/internal/assembly/boot/audit_forwarding_test.go @@ -18,17 +18,12 @@ import ( // silently, with the tests still green. Pure forwarders should embed // event.AuditForwarder instead of repeating the nine methods. func TestAuditCapabilitiesAreForwardedByEveryWrapper(t *testing.T) { - reference := reflect.TypeFor[event.CompletionReportAuditSink]() + reference := reflect.TypeFor[event.DelegationAuditSink]() required := map[string]reflect.Type{ - "DelegationAuditSink": reflect.TypeFor[event.DelegationAuditSink](), - "ReadinessAuditSink": reflect.TypeFor[event.ReadinessAuditSink](), - "ContractShadowAuditSink": reflect.TypeFor[event.ContractShadowAuditSink](), - "EvidenceBundleAuditSink": reflect.TypeFor[event.EvidenceBundleAuditSink](), - "MemoryRecallSink": reflect.TypeFor[event.MemoryRecallSink](), - "OutcomeProgressSink": reflect.TypeFor[event.OutcomeProgressSink](), - "ProtocolRecoveryAuditSink": reflect.TypeFor[event.ProtocolRecoveryAuditSink](), - "TurnCompletionSink": reflect.TypeFor[event.TurnCompletionSink](), - "SubagentHandoffSink": reflect.TypeFor[event.SubagentHandoffSink](), + "DelegationAuditSink": reflect.TypeFor[event.DelegationAuditSink](), + "MemoryRecallSink": reflect.TypeFor[event.MemoryRecallSink](), + "TurnCompletionSink": reflect.TypeFor[event.TurnCompletionSink](), + "SubagentHandoffSink": reflect.TypeFor[event.SubagentHandoffSink](), } wrappers := []event.Sink{ event.Sync(event.Discard), diff --git a/internal/assembly/boot/bestof.go b/internal/assembly/boot/bestof.go index 96ab85c65c..8b086dae1f 100644 --- a/internal/assembly/boot/bestof.go +++ b/internal/assembly/boot/bestof.go @@ -2,7 +2,6 @@ package boot import ( "context" - "errors" "io" "os" "path/filepath" @@ -110,11 +109,7 @@ func runUnattended(ctx context.Context, parent Options, sink event.Sink, mode, r } defer ctrl.Close() if err := ctrl.Run(agent.WithoutRunTimeSink(ctx), prompt); err != nil { - var unready *agent.FinalReadinessError - if !errors.As(err, &unready) { - return "", "", err - } - unverified = unready.Reason + return "", "", err } return finalAnswer(ctrl), unverified, nil } diff --git a/internal/assembly/boot/effect_default_mode_test.go b/internal/assembly/boot/effect_default_mode_test.go index 4ee0b78b88..0c6607241d 100644 --- a/internal/assembly/boot/effect_default_mode_test.go +++ b/internal/assembly/boot/effect_default_mode_test.go @@ -3,7 +3,6 @@ package boot import ( "context" "encoding/json" - "errors" "os" "path/filepath" "strings" @@ -11,7 +10,6 @@ import ( "testing" "reasonix/internal/contract/provider" - "reasonix/internal/runtime/agent" ) // writingProvider asks to write one file, then ends once it has seen the result. @@ -86,10 +84,7 @@ model = "x" t.Fatalf("Build: %v", err) } defer ctrl.Close() - // A write nobody verified ends the turn with a readiness pause, which is - // the turn doing its job here, not a failure of the build. - var paused *agent.FinalReadinessError - if err := ctrl.Run(context.Background(), "write the file"); err != nil && !errors.As(err, &paused) { + if err := ctrl.Run(context.Background(), "write the file"); err != nil { t.Fatalf("Run: %v", err) } _, statErr := os.Stat(filepath.Join(dir, "out.txt")) diff --git a/internal/assembly/boot/effect_refused_calls_test.go b/internal/assembly/boot/effect_refused_calls_test.go index f346207d2d..0e09fe4aff 100644 --- a/internal/assembly/boot/effect_refused_calls_test.go +++ b/internal/assembly/boot/effect_refused_calls_test.go @@ -32,7 +32,7 @@ func TestEffectRefusedCallReachesTheNextRequestOnce(t *testing.T) { testutil.Turn{Text: "third"}, ) setBootTokenProfileTestProvider(t, prov) - ctrl, err := Build(context.Background(), Options{Home: home, WorkspaceRoot: dir, AgentPreset: AgentPresetBalanced, Sink: &bundleAuditSink{}, HeadlessApprovalMode: control.ToolApprovalDontAsk}) + ctrl, err := Build(context.Background(), Options{Home: home, WorkspaceRoot: dir, AgentPreset: AgentPresetBalanced, HeadlessApprovalMode: control.ToolApprovalDontAsk}) if err != nil { t.Fatal(err) } diff --git a/internal/assembly/boot/evidence_seal_effect_test.go b/internal/assembly/boot/evidence_seal_effect_test.go deleted file mode 100644 index dc6d8047d0..0000000000 --- a/internal/assembly/boot/evidence_seal_effect_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package boot - -import ( - "sync" - - "reasonix/internal/contract/event" -) - -type bundleAuditSink struct { - mu sync.Mutex - got []event.EvidenceBundleAudit -} - -func (s *bundleAuditSink) Emit(event.Event) {} - -func (s *bundleAuditSink) RecordEvidenceBundle(a event.EvidenceBundleAudit) { - s.mu.Lock() - defer s.mu.Unlock() - s.got = append(s.got, a) -} diff --git a/internal/contract/event/audit_forwarder.go b/internal/contract/event/audit_forwarder.go index c2ca26cfd8..e471323081 100644 --- a/internal/contract/event/audit_forwarder.go +++ b/internal/contract/event/audit_forwarder.go @@ -13,36 +13,12 @@ type AuditForwarder struct{ Inner Sink } func (f AuditForwarder) CaptureUsageScope() any { return CaptureUsageScope(f.Inner) } -func (f AuditForwarder) RecordReadinessAudit(a hostaudit.ReadinessAudit) { - RecordReadinessAudit(f.Inner, a) -} - func (f AuditForwarder) RecordTurnCompletion() { RecordTurnCompletion(f.Inner) } -func (f AuditForwarder) RecordContractShadow(a ContractShadowAudit) { - RecordContractShadow(f.Inner, a) -} - -func (f AuditForwarder) RecordEvidenceBundle(a EvidenceBundleAudit) { - RecordEvidenceBundle(f.Inner, a) -} - -func (f AuditForwarder) RecordCompletionReport(a CompletionReportAudit) { - RecordCompletionReport(f.Inner, a) -} - func (f AuditForwarder) RecordMemoryRecall(a MemoryRecallAudit) { RecordMemoryRecall(f.Inner, a) } -func (f AuditForwarder) RecordOutcomeProgress(s hostaudit.OutcomeSample) { - RecordOutcomeProgress(f.Inner, s) -} - -func (f AuditForwarder) RecordProtocolRecovery(a ProtocolRecoveryAudit) { - RecordProtocolRecovery(f.Inner, a) -} - func (f AuditForwarder) RecordDelegationAudit(a hostaudit.DelegationAudit) { RecordDelegationAudit(f.Inner, a) } @@ -55,18 +31,10 @@ func (f AuditForwarder) RecordRunBudget(s RunBudgetSample) { RecordRunBudget(f.Inner, s) } -func (f AuditForwarder) RecordProjectCheckProbe(p ProjectCheckProbe) { - RecordProjectCheckProbe(f.Inner, p) -} - func (f AuditForwarder) RecordSubagentHandoff(a SubagentHandoffAudit) { RecordSubagentHandoff(f.Inner, a) } -func (f AuditForwarder) RecordVerificationContractDrift(d VerificationContractDrift) { - RecordVerificationContractDrift(f.Inner, d) -} - // DelegationAuditSink receives one receipt per completed sub-agent run. type DelegationAuditSink interface { RecordDelegationAudit(a hostaudit.DelegationAudit) diff --git a/internal/contract/event/coalesce.go b/internal/contract/event/coalesce.go index 2d2bb67ad2..471fe77356 100644 --- a/internal/contract/event/coalesce.go +++ b/internal/contract/event/coalesce.go @@ -185,20 +185,6 @@ func (c *coalescer) RecordDelegationAudit(a hostaudit.DelegationAudit) { RecordDelegationAudit(c.inner, a) } -func (c *coalescer) RecordReadinessAudit(a hostaudit.ReadinessAudit) { - c.mu.Lock() - c.enqueueFlushLocked() - c.drainAndUnlock() - RecordReadinessAudit(c.inner, a) -} - -func (c *coalescer) RecordProjectCheckProbe(p ProjectCheckProbe) { - c.mu.Lock() - c.enqueueFlushLocked() - c.drainAndUnlock() - RecordProjectCheckProbe(c.inner, p) -} - func (c *coalescer) RecordSubagentHandoff(a SubagentHandoffAudit) { c.mu.Lock() c.enqueueFlushLocked() @@ -206,13 +192,6 @@ func (c *coalescer) RecordSubagentHandoff(a SubagentHandoffAudit) { RecordSubagentHandoff(c.inner, a) } -func (c *coalescer) RecordVerificationContractDrift(d VerificationContractDrift) { - c.mu.Lock() - c.enqueueFlushLocked() - c.drainAndUnlock() - RecordVerificationContractDrift(c.inner, d) -} - func (c *coalescer) RecordTurnCompletion() { c.mu.Lock() c.enqueueFlushLocked() @@ -220,41 +199,6 @@ func (c *coalescer) RecordTurnCompletion() { RecordTurnCompletion(c.inner) } -func (c *coalescer) RecordProtocolRecovery(a ProtocolRecoveryAudit) { - c.mu.Lock() - c.enqueueFlushLocked() - c.drainAndUnlock() - RecordProtocolRecovery(c.inner, a) -} - -func (c *coalescer) RecordContractShadow(a ContractShadowAudit) { - c.mu.Lock() - c.enqueueFlushLocked() - c.drainAndUnlock() - RecordContractShadow(c.inner, a) -} - -func (c *coalescer) RecordCompletionReport(a CompletionReportAudit) { - c.mu.Lock() - c.enqueueFlushLocked() - c.drainAndUnlock() - RecordCompletionReport(c.inner, a) -} - -func (c *coalescer) RecordEvidenceBundle(a EvidenceBundleAudit) { - c.mu.Lock() - c.enqueueFlushLocked() - c.drainAndUnlock() - RecordEvidenceBundle(c.inner, a) -} - -func (c *coalescer) RecordOutcomeProgress(sample hostaudit.OutcomeSample) { - c.mu.Lock() - c.enqueueFlushLocked() - c.drainAndUnlock() - RecordOutcomeProgress(c.inner, sample) -} - func (c *coalescer) RecordMemoryRecall(a MemoryRecallAudit) { c.mu.Lock() c.enqueueFlushLocked() diff --git a/internal/contract/event/coalesce_test.go b/internal/contract/event/coalesce_test.go index cff2eaf47a..35cf624571 100644 --- a/internal/contract/event/coalesce_test.go +++ b/internal/contract/event/coalesce_test.go @@ -1,7 +1,6 @@ package event import ( - "reasonix/internal/contract/hostaudit" "strings" "sync" "testing" @@ -11,9 +10,7 @@ import ( type coalesceRecordSink struct { mu sync.Mutex events []Event - readiness int turns int - recovery int workspace int } @@ -23,24 +20,12 @@ func (s *coalesceRecordSink) Emit(e Event) { s.events = append(s.events, e) } -func (s *coalesceRecordSink) RecordReadinessAudit(hostaudit.ReadinessAudit) { - s.mu.Lock() - defer s.mu.Unlock() - s.readiness++ -} - func (s *coalesceRecordSink) RecordTurnCompletion() { s.mu.Lock() defer s.mu.Unlock() s.turns++ } -func (s *coalesceRecordSink) RecordProtocolRecovery(ProtocolRecoveryAudit) { - s.mu.Lock() - defer s.mu.Unlock() - s.recovery++ -} - func (s *coalesceRecordSink) RecordWorkspaceMutation(WorkspaceMutation) { s.mu.Lock() defer s.mu.Unlock() @@ -141,17 +126,15 @@ func TestCoalesceCapabilitiesFlushFirstAndForward(t *testing.T) { c := Coalesce(inner, time.Hour) c.Emit(Event{Kind: Text, Text: "lead"}) c.Emit(Event{Kind: Text, Text: "tail"}) - c.(ReadinessAuditSink).RecordReadinessAudit(hostaudit.ReadinessAudit{}) c.(TurnCompletionSink).RecordTurnCompletion() - c.(ProtocolRecoveryAuditSink).RecordProtocolRecovery(ProtocolRecoveryAudit{}) c.(WorkspaceMutationSink).RecordWorkspaceMutation(WorkspaceMutation{Content: true}) got := inner.snapshot() if len(got) != 2 || got[1].Text != "tail" { t.Fatalf("capability call must flush the buffered delta first: %+v", got) } - if inner.readiness != 1 || inner.turns != 1 || inner.recovery != 1 || inner.workspace != 1 { - t.Fatalf("capabilities not forwarded: %d/%d/%d/%d", inner.readiness, inner.turns, inner.recovery, inner.workspace) + if inner.turns != 1 || inner.workspace != 1 { + t.Fatalf("capabilities not forwarded: %d/%d", inner.turns, inner.workspace) } } diff --git a/internal/contract/event/event.go b/internal/contract/event/event.go index a34ec67626..ac54bace9b 100644 --- a/internal/contract/event/event.go +++ b/internal/contract/event/event.go @@ -14,7 +14,6 @@ package event import ( "encoding/json" "reasonix/internal/contract/delivery" - "reasonix/internal/contract/hostaudit" "reasonix/internal/contract/pricing" "reasonix/internal/contract/agentgraph" @@ -209,8 +208,6 @@ type StreamAttemptInfo struct { FirstTokenMs int64 } -const TurnOutcomeFinalReadiness = "final_readiness" - // TurnOutcomeNoProgress marks a run the user's progress-watch setting paused. const TurnOutcomeNoProgress = "no_progress" @@ -542,14 +539,6 @@ type AskAnswer struct { Value json.RawMessage `json:",omitempty"` } -// FinalReadiness carries machine-readable recovery requirements on TurnDone. -// Missing values are stable category ids; user-facing detail stays localized in -// the frontend instead of scraping the diagnostic error string. -type FinalReadiness struct { - Attempts int - Missing []string -} - const ( UsageSourceExecutor = "executor" UsageSourcePlanner = "planner" @@ -610,7 +599,6 @@ type Event struct { Err error // TurnDone: non-nil on failure Cancelled bool // TurnDone: Cancel was requested while the turn was active Outcome string // TurnDone: optional machine-readable recoverable outcome - Readiness *FinalReadiness // TurnDone: structured final-readiness recovery state Receipt *CompletionReceipt // TurnDone: what the host verified, and what it could not CheckpointTurn *int // TurnDone: authoritative checkpoint for this turn's visible user message // TurnDone: wall-clock ms the turn's runs took up to their final answers, @@ -690,12 +678,6 @@ type WorkspaceChangedPayload struct { WatchState WorkspaceWatchState } -// ReadinessAuditSink is an optional sink capability. Sinks that do not care -// about readiness audit receipts can implement only Sink and will ignore them. -type ReadinessAuditSink interface { - RecordReadinessAudit(hostaudit.ReadinessAudit) -} - // TurnCompletionSink is an optional sink capability for synchronous controller // entry points that do not publish a TurnDone UI event. It keeps accounting // independent from frontend event lifecycles without synthesizing an event that diff --git a/internal/contract/event/event_test.go b/internal/contract/event/event_test.go index a01c0d114a..deb82ac257 100644 --- a/internal/contract/event/event_test.go +++ b/internal/contract/event/event_test.go @@ -1,7 +1,6 @@ package event import ( - "reasonix/internal/contract/hostaudit" "sync" "testing" @@ -79,22 +78,12 @@ func TestSyncTreatsTypedNilSinkAsDiscard(t *testing.T) { } type readinessAuditRecorder struct { - events []hostaudit.ReadinessAudit - recovery []ProtocolRecoveryAudit workspace []WorkspaceMutation turns int } func (r *readinessAuditRecorder) Emit(Event) {} -func (r *readinessAuditRecorder) RecordReadinessAudit(a hostaudit.ReadinessAudit) { - r.events = append(r.events, a) -} - -func (r *readinessAuditRecorder) RecordProtocolRecovery(a ProtocolRecoveryAudit) { - r.recovery = append(r.recovery, a) -} - func (r *readinessAuditRecorder) RecordTurnCompletion() { r.turns++ } func (r *readinessAuditRecorder) RecordWorkspaceMutation(m WorkspaceMutation) { @@ -118,34 +107,6 @@ func TestSyncForwardsWorkspaceMutationWithoutUIEvent(t *testing.T) { } } -func TestSyncForwardsReadinessAuditReceipts(t *testing.T) { - rec := &readinessAuditRecorder{} - sink := Sync(rec) - - RecordReadinessAudit(sink, hostaudit.ReadinessAudit{ - Result: hostaudit.ReadinessBlocked, - MissingProjectChecks: 1, - }) - - if len(rec.events) != 1 { - t.Fatalf("readiness audit events = %d, want 1", len(rec.events)) - } - if rec.events[0].Result != hostaudit.ReadinessBlocked || rec.events[0].MissingProjectChecks != 1 { - t.Fatalf("readiness audit not forwarded through Sync: %+v", rec.events[0]) - } -} - -func TestSyncForwardsProtocolRecoveryWithoutEmittingUIEvent(t *testing.T) { - rec := &readinessAuditRecorder{} - sink := Sync(rec) - - RecordProtocolRecovery(sink, ProtocolRecoveryAudit{Kind: ProtocolRecoveryKind("test")}) - - if len(rec.recovery) != 1 || rec.recovery[0].Kind != ProtocolRecoveryKind("test") { - t.Fatalf("protocol recovery not forwarded through Sync: %+v", rec.recovery) - } -} - // Discard func TestDiscardSink(t *testing.T) { diff --git a/internal/contract/event/notice_codes.go b/internal/contract/event/notice_codes.go index 741b6c5bbf..45142ceb23 100644 --- a/internal/contract/event/notice_codes.go +++ b/internal/contract/event/notice_codes.go @@ -11,7 +11,6 @@ const ( NoticeCodeCheckpointRecordingFailed = "checkpoint_recording_failed" NoticeCodeTurnIdentityMismatch = "turn_identity_mismatch" NoticeCodePlanAutoApproved = "plan_auto_approved" - NoticeCodeFinalReadiness = "final_readiness" NoticeCodeEmptyFinal = "empty_final" NoticeCodeExecutorHandoff = "executor_handoff" NoticeCodeToolBudget = "tool_budget" diff --git a/internal/contract/event/project_check_probe.go b/internal/contract/event/project_check_probe.go deleted file mode 100644 index 77e391773b..0000000000 --- a/internal/contract/event/project_check_probe.go +++ /dev/null @@ -1,72 +0,0 @@ -// project_check_probe.go — the two project-check derivations, compared. -package event - -import "reasonix/internal/base/nilutil" - -// Divergence classes. Each names the one explanation that accounts for a -// disagreement, in the order they are tried: an earlier class rules out the -// ones below it, so a diff never carries a weaker reason than the true one. -const ( - // ProjectCheckBaselinePreservation is a criterion the task began requiring - // that the current declaration no longer names. The gate cannot see it at - // all, so this class is the protection working, never a regression. - ProjectCheckBaselinePreservation = "baseline_preservation" - // ProjectCheckMutationIndex is a disagreement the two sides' baselines - // explain: they measure "since the change" from different receipts. - ProjectCheckMutationIndex = "mutation_index" - // ProjectCheckIdentityNormalization is a disagreement canonicalisation - // explains: the raw command and its identity match different receipts. - ProjectCheckIdentityNormalization = "identity_normalization" - // ProjectCheckCandidateOnly and ProjectCheckLegacyOnly are the unexplained - // remainders — the only two classes that are candidate defects. - ProjectCheckCandidateOnly = "candidate_only" - ProjectCheckLegacyOnly = "legacy_only" -) - -// ProjectCheckDiff is one criterion the two derivations disagree about, and the -// reason they do. Identity is the project's own declared command, canonicalised -// — the same text the readiness reason already hands the model verbatim. -type ProjectCheckDiff struct { - Identity string `json:"identity"` - Class string `json:"class"` -} - -// ProjectCheckProbe is one comparison between the readiness gate's project-check -// derivation and the ledger's obligations. It observes and decides nothing, so a -// class here is a question to answer, never a block that happened. One probe per -// readiness evaluation, not per turn: a goal turn evaluates readiness more than -// once, and the denominator has to be what was actually compared. -type ProjectCheckProbe struct { - // Declared and Baseline are how many criteria each declaration names, so a - // run with nothing declared is not counted as agreement. - Declared int - Baseline int - // LegacyBlocked and CandidateBlocked are the two stop decisions. Parity - // between them is the headline; Diffs is why it was or was not reached. - LegacyBlocked bool - CandidateBlocked bool - // AgreedMissing counts criteria both derivations call unmet. - AgreedMissing int - Diffs []ProjectCheckDiff - // LegacyAfter and CandidateAfter are the receipt indices each side measured - // after. They differ by construction, so a divergence they explain is not a - // defect in either derivation. - LegacyAfter int - CandidateAfter int -} - -// ProjectCheckProbeSink is an optional sink capability for the shadow -// comparison; implementations must not let it reach a user-facing channel. -type ProjectCheckProbeSink interface { - RecordProjectCheckProbe(ProjectCheckProbe) -} - -// RecordProjectCheckProbe forwards one comparison to sinks that opt in. -func RecordProjectCheckProbe(s Sink, p ProjectCheckProbe) { - if nilutil.IsNil(s) { - return - } - if ps, ok := s.(ProjectCheckProbeSink); ok { - ps.RecordProjectCheckProbe(p) - } -} diff --git a/internal/contract/event/record.go b/internal/contract/event/record.go index 3a0460c967..9ad5fa04af 100644 --- a/internal/contract/event/record.go +++ b/internal/contract/event/record.go @@ -2,7 +2,6 @@ package event import ( "reasonix/internal/base/nilutil" - "reasonix/internal/contract/hostaudit" ) // Forwarding a turn's audit signals to sinks that opt into them. Each is a @@ -20,151 +19,6 @@ func RecordTurnCompletion(s Sink) { } } -// RecordReadinessAudit forwards a readiness audit receipt to sinks that opt in. -func RecordReadinessAudit(s Sink, a hostaudit.ReadinessAudit) { - if nilutil.IsNil(s) { - return - } - if rs, ok := s.(ReadinessAuditSink); ok { - rs.RecordReadinessAudit(a) - } -} - -// ProtocolRecoveryKind is a content-free internal observation about a provider -// protocol repair. It is deliberately separate from Event/Notice so recovery -// stays invisible in chat transcripts and frontends do not need to understand -// provider implementation details. -type ProtocolRecoveryKind string - -type ProtocolRecoveryAudit struct { - Kind ProtocolRecoveryKind - // ChildID names the delegated run this repair happened inside, stamped by - // the nesting sink that already knows it; empty is the parent's own loop. - ChildID string -} - -// ContractShadowAudit is the shadow task-contract's end-of-turn summary: -// counts and enums only, never requirement text. Shadow means observed, not -// enforced — the old control logic still decides behavior. -type ContractShadowAudit struct { - Intent string - Requirements int - RequirementsSatisfied int - Checks int - ChecksSatisfied int - Epoch uint64 - Verdict string - Complete bool - ReadyToFinalize bool -} - -// ContractShadowAuditSink is an optional sink capability; implementations -// must keep it content-free, like every other audit channel. -type ContractShadowAuditSink interface { - RecordContractShadow(ContractShadowAudit) -} - -// RecordContractShadow forwards the shadow contract summary only to sinks -// that explicitly opt in. Ordinary UI sinks receive nothing. -func RecordContractShadow(s Sink, a ContractShadowAudit) { - if nilutil.IsNil(s) { - return - } - if cs, ok := s.(ContractShadowAuditSink); ok { - cs.RecordContractShadow(a) - } -} - -// CompletionReportAudit is the host-authored completion report's end-of-turn -// summary: counts, enums, and gap kinds only, never paths or command text. -// The gap counters carry the point — what the turn left unproven. -type CompletionReportAudit struct { - Verdict string - Risk string - Criteria int - CriteriaSatisfied int - Changes int - ChangesUnreviewed int - Verifications int - VerificationsFailed int - VerificationsStale int - // VerificationsInconclusive counts checks that ran behind a shell stage - // that decided the exit status, so neither outcome was readable. - VerificationsInconclusive int - Gaps int - GapKinds []string - // ClaimsVerified counts the turn's own asserted verifications; - // ClaimsUnbacked is how many of them the ledger did not support. - ClaimsVerified int - ClaimsUnbacked int - // OriginRecorded counts the external results the host kept the text of this - // turn, and OriginMatched the calls (not read-only) whose arguments carried - // that text verbatim. Counts only: they measure a signal, nothing acts on it. - OriginRecorded int - OriginMatched int -} - -// CompletionReportAuditSink is an optional sink capability; implementations -// must keep it content-free, like every other audit channel. -type CompletionReportAuditSink interface { - RecordCompletionReport(CompletionReportAudit) -} - -// RecordCompletionReport forwards the completion summary only to sinks that -// explicitly opt in. Ordinary UI sinks receive nothing. -func RecordCompletionReport(s Sink, a CompletionReportAudit) { - if nilutil.IsNil(s) { - return - } - if cs, ok := s.(CompletionReportAuditSink); ok { - cs.RecordCompletionReport(a) - } -} - -// EvidenceBundleAudit is the end-of-turn shadow bundle's seal: its record -// identity in Trusted Host State, never the bundle's content. FailureCode is -// the store's typed cause when nothing was sealed. -type EvidenceBundleAudit struct { - Sealed bool - Record string - Generation uint64 - Integrity string - Receipts int - FailureCode string - // SnapshotComplete says the turn-end workspace snapshot saw the whole tree. - SnapshotComplete bool - // UnobservedChanges counts paths that changed between the previous sealed - // turn and this one, when UnobservedCompared says that was established. - UnobservedCompared bool - UnobservedChanges int - // Outcome is the host-evidence outcome; DivergenceClass and its reason - // codes say how it differs from the completion report's verdict. - Outcome string - DivergenceClass string - DivergenceReasons []string - // ContractRevision is the accepted contract revision in force, and - // ContractDecision what host policy did with this turn's derivation. - ContractRevision int - ContractDecision string - ContractFailure string -} - -// EvidenceBundleAuditSink is an optional sink capability; implementations must -// keep it content-free, like every other audit channel. -type EvidenceBundleAuditSink interface { - RecordEvidenceBundle(EvidenceBundleAudit) -} - -// RecordEvidenceBundle forwards the seal only to sinks that opt in. -func RecordEvidenceBundle(s Sink, a EvidenceBundleAudit) { - if nilutil.IsNil(s) { - return - } - if es, ok := s.(EvidenceBundleAuditSink); ok { - es.RecordEvidenceBundle(a) - } -} - // MemoryRecallAudit summarizes one automatic-recall decision: identifiers, // scores, and budget numbers only — never the query or fact text. type MemoryRecallAudit struct { @@ -206,39 +60,3 @@ func RecordMemoryRecall(s Sink, a MemoryRecallAudit) { mr.RecordMemoryRecall(a) } } - -// OutcomeProgressSink is an optional sink capability for the shadow outcome -// scorer's per-round samples: counts only, never paths or commands. Shadow -// means observed, not enforced — the novelty guard still decides behavior. -type OutcomeProgressSink interface { - RecordOutcomeProgress(hostaudit.OutcomeSample) -} - -// RecordOutcomeProgress forwards a shadow outcome sample only to sinks that -// explicitly opt in. Ordinary UI sinks receive nothing. -func RecordOutcomeProgress(s Sink, sample hostaudit.OutcomeSample) { - if nilutil.IsNil(s) { - return - } - if op, ok := s.(OutcomeProgressSink); ok { - op.RecordOutcomeProgress(sample) - } -} - -// ProtocolRecoveryAuditSink is an optional sink capability. Implementations -// must keep it content-free; prompts, responses, endpoints, model names, and -// tool arguments do not belong in this audit channel. -type ProtocolRecoveryAuditSink interface { - RecordProtocolRecovery(ProtocolRecoveryAudit) -} - -// RecordProtocolRecovery forwards a content-free recovery observation only to -// sinks that explicitly opt in. Ordinary UI sinks receive nothing. -func RecordProtocolRecovery(s Sink, a ProtocolRecoveryAudit) { - if nilutil.IsNil(s) { - return - } - if rs, ok := s.(ProtocolRecoveryAuditSink); ok { - rs.RecordProtocolRecovery(a) - } -} diff --git a/internal/contract/event/sink_capabilities.go b/internal/contract/event/sink_capabilities.go index de701e28ec..9ae70654c8 100644 --- a/internal/contract/event/sink_capabilities.go +++ b/internal/contract/event/sink_capabilities.go @@ -1,52 +1,19 @@ package event import ( - "maps" "reflect" - "slices" ) // capabilityContracts is the single authority on the optional capabilities a // wrapper must forward. Delivery is by type assertion, so a wrapper missing one // silently drops that channel for every sink behind it. Register new ones here. var capabilityContracts = map[string]reflect.Type{ - "CompletionReport": reflect.TypeFor[CompletionReportAuditSink](), - "ContractShadow": reflect.TypeFor[ContractShadowAuditSink](), "DelegationAudit": reflect.TypeFor[DelegationAuditSink](), - "EvidenceBundle": reflect.TypeFor[EvidenceBundleAuditSink](), "MemoryRecall": reflect.TypeFor[MemoryRecallSink](), - "OutcomeProgress": reflect.TypeFor[OutcomeProgressSink](), - "ProjectCheckProbe": reflect.TypeFor[ProjectCheckProbeSink](), - "ProtocolRecovery": reflect.TypeFor[ProtocolRecoveryAuditSink](), - "ReadinessAudit": reflect.TypeFor[ReadinessAuditSink](), "RunBudget": reflect.TypeFor[RunBudgetSink](), "SubagentHandoff": reflect.TypeFor[SubagentHandoffSink](), "TurnCompletion": reflect.TypeFor[TurnCompletionSink](), "UsageQuote": reflect.TypeFor[UsageQuoter](), - "VerificationDrift": reflect.TypeFor[VerificationContractDriftSink](), "WorkspaceMutation": reflect.TypeFor[WorkspaceMutationSink](), "UsageScope": reflect.TypeFor[UsageScopeSource](), } - -// CapabilityNames lists every optional capability in stable order. -func CapabilityNames() []string { - out := slices.Sorted(maps.Keys(capabilityContracts)) - return out -} - -// MissingCapabilities names the optional capabilities s does not implement. -// A pass-through wrapper must report none: anything it omits is an audit -// channel that dies at that link. -func MissingCapabilities(s Sink) []string { - if s == nil { - return CapabilityNames() - } - t := reflect.TypeOf(s) - var out []string - for _, name := range CapabilityNames() { - if !t.Implements(capabilityContracts[name]) { - out = append(out, name) - } - } - return out -} diff --git a/internal/contract/event/sink_capabilities_test.go b/internal/contract/event/sink_capabilities_test.go index 9acf540209..fd0165d022 100644 --- a/internal/contract/event/sink_capabilities_test.go +++ b/internal/contract/event/sink_capabilities_test.go @@ -25,65 +25,21 @@ func (s *capturingSink) QuoteUsage(e Event) *pricing.CostQuote { func (s *capturingSink) RecordDelegationAudit(hostaudit.DelegationAudit) { s.recorded = append(s.recorded, "DelegationAudit") } -func (s *capturingSink) RecordReadinessAudit(hostaudit.ReadinessAudit) { - s.recorded = append(s.recorded, "ReadinessAudit") -} func (s *capturingSink) RecordTurnCompletion() { s.recorded = append(s.recorded, "TurnCompletion") } -func (s *capturingSink) RecordProtocolRecovery(ProtocolRecoveryAudit) { - s.recorded = append(s.recorded, "ProtocolRecovery") -} -func (s *capturingSink) RecordContractShadow(ContractShadowAudit) { - s.recorded = append(s.recorded, "ContractShadow") -} -func (s *capturingSink) RecordCompletionReport(CompletionReportAudit) { - s.recorded = append(s.recorded, "CompletionReport") -} -func (s *capturingSink) RecordEvidenceBundle(EvidenceBundleAudit) { - s.recorded = append(s.recorded, "EvidenceBundle") -} func (s *capturingSink) RecordMemoryRecall(MemoryRecallAudit) { s.recorded = append(s.recorded, "MemoryRecall") } -func (s *capturingSink) RecordOutcomeProgress(hostaudit.OutcomeSample) { - s.recorded = append(s.recorded, "OutcomeProgress") -} func (s *capturingSink) RecordWorkspaceMutation(WorkspaceMutation) { s.recorded = append(s.recorded, "WorkspaceMutation") } func (s *capturingSink) RecordRunBudget(RunBudgetSample) { s.recorded = append(s.recorded, "RunBudget") } -func (s *capturingSink) RecordProjectCheckProbe(ProjectCheckProbe) { - s.recorded = append(s.recorded, "ProjectCheckProbe") -} func (s *capturingSink) RecordSubagentHandoff(SubagentHandoffAudit) { s.recorded = append(s.recorded, "SubagentHandoff") } -func (s *capturingSink) RecordVerificationContractDrift(VerificationContractDrift) { - s.recorded = append(s.recorded, "VerificationDrift") -} - -// recordAll drives every optional capability once through s. -func recordAll(s Sink) { - QuoteUsage(s, Event{}) - RecordDelegationAudit(s, hostaudit.DelegationAudit{}) - RecordReadinessAudit(s, hostaudit.ReadinessAudit{}) - RecordTurnCompletion(s) - RecordProtocolRecovery(s, ProtocolRecoveryAudit{}) - RecordContractShadow(s, ContractShadowAudit{}) - RecordCompletionReport(s, CompletionReportAudit{}) - RecordEvidenceBundle(s, EvidenceBundleAudit{}) - RecordMemoryRecall(s, MemoryRecallAudit{}) - RecordOutcomeProgress(s, hostaudit.OutcomeSample{}) - RecordWorkspaceMutation(s, WorkspaceMutation{}) - RecordRunBudget(s, RunBudgetSample{}) - RecordProjectCheckProbe(s, ProjectCheckProbe{}) - RecordSubagentHandoff(s, SubagentHandoffAudit{}) - RecordVerificationContractDrift(s, VerificationContractDrift{}) - CaptureUsageScope(s) -} // AuditForwarder is why a new capability does not have to be repeated at every // layer, so it must itself be complete. It reads the registry rather than a @@ -99,51 +55,16 @@ func TestAuditForwarderCoversEveryCapability(t *testing.T) { } } -func TestCapturingSinkCoversEveryCapability(t *testing.T) { - if missing := MissingCapabilities(&capturingSink{}); len(missing) > 0 { - t.Fatalf("test fixture is stale — implement %v so chain tests stay honest", missing) - } -} - -// TestWrappersForwardEveryCapability is the guard the real chain lacked: -// CostQuoteSink forwarded Emit only, so every audit channel died one link -// before the trajectory recorder with nothing failing. -func TestWrappersForwardEveryCapability(t *testing.T) { - inner := &capturingSink{} - wrappers := map[string]Sink{ - "Sync": Sync(inner), - "Coalesce": Coalesce(inner, DefaultStreamDeltaWindow), - "CostQuoteSink": NewCostQuoteSink(inner, nil), - "NoticeWindow": NewNoticeLedger().Begin(inner), - } - for name, w := range wrappers { - t.Run(name, func(t *testing.T) { - if missing := MissingCapabilities(w); len(missing) > 0 { - t.Fatalf("%s drops %v — embed AuditForwarder or forward them by hand", name, missing) - } - inner.recorded = nil - recordAll(w) - want := len(CapabilityNames()) - if _, quotes := w.(*CostQuoteSink); quotes { - want-- - } - if got := len(inner.recorded); got != want { - t.Fatalf("%s delivered %d of %d capabilities: %v", name, got, want, inner.recorded) - } - }) - } -} - // TestSessionChainDeliversAudits mirrors boot's assembly order // (Coalesce → Sync → CostQuote → recorder) end to end. func TestSessionChainDeliversAudits(t *testing.T) { recorder := &capturingSink{} chain := Coalesce(Sync(NewCostQuoteSink(recorder, nil)), 5*time.Millisecond) - RecordProtocolRecovery(chain, ProtocolRecoveryAudit{Kind: ProtocolRecoveryKind("test")}) + RecordMemoryRecall(chain, MemoryRecallAudit{}) - if len(recorder.recorded) != 1 || recorder.recorded[0] != "ProtocolRecovery" { - t.Fatalf("protocol recovery never reached the recorder: %v", recorder.recorded) + if len(recorder.recorded) != 1 || recorder.recorded[0] != "MemoryRecall" { + t.Fatalf("memory recall never reached the recorder: %v", recorder.recorded) } } diff --git a/internal/contract/event/sync.go b/internal/contract/event/sync.go index 6438d2c3bf..602c959326 100644 --- a/internal/contract/event/sync.go +++ b/internal/contract/event/sync.go @@ -44,22 +44,6 @@ func (s *syncSink) RecordDelegationAudit(a hostaudit.DelegationAudit) { RecordDelegationAudit(s.inner, a) } -func (s *syncSink) RecordReadinessAudit(a hostaudit.ReadinessAudit) { - s.mu.Lock() - defer s.mu.Unlock() - if rs, ok := s.inner.(ReadinessAuditSink); ok { - rs.RecordReadinessAudit(a) - } -} - -func (s *syncSink) RecordProjectCheckProbe(p ProjectCheckProbe) { - s.mu.Lock() - defer s.mu.Unlock() - if ps, ok := s.inner.(ProjectCheckProbeSink); ok { - ps.RecordProjectCheckProbe(p) - } -} - func (s *syncSink) RecordSubagentHandoff(a SubagentHandoffAudit) { s.mu.Lock() defer s.mu.Unlock() @@ -68,14 +52,6 @@ func (s *syncSink) RecordSubagentHandoff(a SubagentHandoffAudit) { } } -func (s *syncSink) RecordVerificationContractDrift(d VerificationContractDrift) { - s.mu.Lock() - defer s.mu.Unlock() - if ds, ok := s.inner.(VerificationContractDriftSink); ok { - ds.RecordVerificationContractDrift(d) - } -} - func (s *syncSink) RecordTurnCompletion() { s.mu.Lock() defer s.mu.Unlock() @@ -84,46 +60,6 @@ func (s *syncSink) RecordTurnCompletion() { } } -func (s *syncSink) RecordProtocolRecovery(a ProtocolRecoveryAudit) { - s.mu.Lock() - defer s.mu.Unlock() - if rs, ok := s.inner.(ProtocolRecoveryAuditSink); ok { - rs.RecordProtocolRecovery(a) - } -} - -func (s *syncSink) RecordContractShadow(a ContractShadowAudit) { - s.mu.Lock() - defer s.mu.Unlock() - if rs, ok := s.inner.(ContractShadowAuditSink); ok { - rs.RecordContractShadow(a) - } -} - -func (s *syncSink) RecordCompletionReport(a CompletionReportAudit) { - s.mu.Lock() - defer s.mu.Unlock() - if rs, ok := s.inner.(CompletionReportAuditSink); ok { - rs.RecordCompletionReport(a) - } -} - -func (s *syncSink) RecordEvidenceBundle(a EvidenceBundleAudit) { - s.mu.Lock() - defer s.mu.Unlock() - if es, ok := s.inner.(EvidenceBundleAuditSink); ok { - es.RecordEvidenceBundle(a) - } -} - -func (s *syncSink) RecordOutcomeProgress(sample hostaudit.OutcomeSample) { - s.mu.Lock() - defer s.mu.Unlock() - if op, ok := s.inner.(OutcomeProgressSink); ok { - op.RecordOutcomeProgress(sample) - } -} - func (s *syncSink) RecordMemoryRecall(a MemoryRecallAudit) { s.mu.Lock() defer s.mu.Unlock() diff --git a/internal/contract/event/usage_scope_test.go b/internal/contract/event/usage_scope_test.go index d0d3cf1c08..e4c88c7d50 100644 --- a/internal/contract/event/usage_scope_test.go +++ b/internal/contract/event/usage_scope_test.go @@ -1,11 +1,7 @@ package event import ( - "encoding/json" "testing" - "time" - - "reasonix/internal/contract/provider" ) type usageScopeTestSink struct { @@ -17,60 +13,6 @@ type usageScopeTestSink struct { func (s *usageScopeTestSink) CaptureUsageScope() any { return s.current } func (s *usageScopeTestSink) Emit(e Event) { s.events = append(s.events, e) } -func TestUsageScopePreservesCapturedOwnerAndWire(t *testing.T) { - old, next := make(chan struct{}), make(chan struct{}) - sink := &usageScopeTestSink{current: old} - var quotes int - owner := NewCostQuoteSink(sink, &QuoteContext{ - Now: func() time.Time { - quotes++ - return time.Date(2026, 8, 19, 2, 0, 0, 0, time.UTC) - }, - CatalogProviderForModel: func(string) string { return "deepseek" }, - }) - chain := Coalesce(Sync(owner), DefaultStreamDeltaWindow) - bound := BindUsageScope(chain) - if missing := MissingCapabilities(bound); len(missing) != 0 { - t.Fatalf("bound wrapper drops %v", missing) - } - sink.current = next - nested := BindUsageScope(bound) - u := Event{Kind: Usage, UsageSource: UsageSourceSubagent, ModelRef: "deepseek/deepseek-v4-pro", - Pricing: &provider.Pricing{CacheHit: .15, Input: 4.5, Output: 13.5, Currency: "CNY"}, - Usage: &provider.Usage{PromptTokens: 1_000_000, CompletionTokens: 1_000_000, RequestCount: 1}} - u.CostQuote = QuoteUsage(nested, u) - if u.CostQuote == nil || u.CostQuote.Original.Amount != "36" || quotes != 1 { - t.Fatalf("bound usage lost the quote owner: quote=%+v calls=%d", u.CostQuote, quotes) - } - nested.Emit(u) - if len(sink.events) != 1 || UsageScope(sink.events[0]) != old { - t.Fatal("nested capture adopted a later owner") - } - chain.Emit(u) - if quotes != 1 || sink.events[0].CostQuote != u.CostQuote || sink.events[1].CostQuote != u.CostQuote { - t.Fatal("scope binding changed or repeated the occurrence quote") - } - want, err := json.Marshal(sink.events[1]) - if err != nil { - t.Fatal(err) - } - got, err := json.Marshal(sink.events[0]) - if err != nil || string(got) != string(want) { - t.Fatalf("scope changed JSON: %s %v", got, err) - } - if UsageScope(u) != nil { - t.Fatal("capture mutated the producer event") - } - var replay Event - if err := json.Unmarshal(got, &replay); err != nil || UsageScope(replay) != nil { - t.Fatal("replay acquired process-local ownership") - } - bound.Emit(Event{Kind: Notice, Text: "unchanged"}) - if len(sink.events) != 3 || UsageScope(sink.events[2]) != nil { - t.Fatal("non-usage event acquired attribution") - } -} - func TestUsageScopeKeepsEarlierEventOwner(t *testing.T) { sink := &usageScopeTestSink{current: "new"} BindUsageScope(sink).Emit(Event{Kind: Usage, usageScope: "old"}) diff --git a/internal/contract/event/verification_drift.go b/internal/contract/event/verification_drift.go deleted file mode 100644 index 8dbc2ff961..0000000000 --- a/internal/contract/event/verification_drift.go +++ /dev/null @@ -1,35 +0,0 @@ -// verification_drift.go — the declaration a Goal was accepted under, beside the -// one this process loaded. -package event - -import "reasonix/internal/base/nilutil" - -// VerificationContractDrift is one comparison between the contract a resumed -// Goal was frozen under and the declaration the current process read. It -// observes and decides nothing. It does not say the frozen contract should be -// kept, that the current declaration supersedes it, or that the Goal owes -// anything: which of the two governs is a decision no observation may make. -type VerificationContractDrift struct { - ScopeID string - Epoch uint64 - // Frozen and Current are canonical criterion identities — the project's own - // declared commands, the same text a readiness reason already shows. - Frozen []string - Current []string - Drift bool -} - -// VerificationContractDriftSink is an optional sink capability. -type VerificationContractDriftSink interface { - RecordVerificationContractDrift(VerificationContractDrift) -} - -// RecordVerificationContractDrift forwards one comparison to sinks that opt in. -func RecordVerificationContractDrift(s Sink, d VerificationContractDrift) { - if nilutil.IsNil(s) { - return - } - if ds, ok := s.(VerificationContractDriftSink); ok { - ds.RecordVerificationContractDrift(d) - } -} diff --git a/internal/contract/eventwire/wire.go b/internal/contract/eventwire/wire.go index 520b11a2ac..7581b13c08 100644 --- a/internal/contract/eventwire/wire.go +++ b/internal/contract/eventwire/wire.go @@ -41,7 +41,6 @@ type Event struct { Err string `json:"err,omitempty" externalizable:"true"` Cancelled bool `json:"cancelled,omitempty"` // TurnDone: the user asked to stop Outcome string `json:"outcome,omitempty"` - Readiness *FinalReadiness `json:"readiness,omitempty"` Receipt *CompletionReceipt `json:"receipt,omitempty"` CheckpointTurn *int `json:"checkpointTurn,omitempty"` WorkDurationMs int64 `json:"workDurationMs,omitempty"` // TurnDone: run time up to the final answer @@ -202,9 +201,6 @@ func ToWire(e event.Event) Event { w.Cancelled, w.Outcome = e.Cancelled, e.Outcome w.CheckpointTurn, w.WorkDurationMs, w.WorkStartedAt = e.CheckpointTurn, e.WorkDurationMs, e.WorkStartedAt w.Receipt = completionReceiptWire(e.Receipt) - if e.Readiness != nil { - w.Readiness = &FinalReadiness{Attempts: e.Readiness.Attempts, Missing: append([]string(nil), e.Readiness.Missing...)} - } if e.Err != nil { w.Err = e.Err.Error() } @@ -316,11 +312,6 @@ func ToWireVia(in *provider.Via) *Via { return &Via{Device: in.Device, Ordinal: in.Ordinal} } -type FinalReadiness struct { - Attempts int `json:"attempts,omitempty"` - Missing []string `json:"missing,omitempty"` -} - // MemoryCitation is the JSON form of provider.MemoryCitation. type MemoryCitation struct { ID string `json:"id,omitempty"` diff --git a/internal/contract/eventwire/wire_test.go b/internal/contract/eventwire/wire_test.go index eb62f99bfb..6ef98f240e 100644 --- a/internal/contract/eventwire/wire_test.go +++ b/internal/contract/eventwire/wire_test.go @@ -106,12 +106,12 @@ func TestToWireContextMaintenanceJSON(t *testing.T) { } func TestToWireNoticeCarriesCode(t *testing.T) { - w := ToWire(event.Event{Kind: event.Notice, Level: event.LevelInfo, Code: event.NoticeCodeFinalReadiness, Text: "readiness copy"}) + w := ToWire(event.Event{Kind: event.Notice, Level: event.LevelInfo, Code: event.NoticeCodeEmptyFinal, Text: "empty copy"}) b, err := json.Marshal(w) if err != nil { t.Fatalf("marshal: %v", err) } - if !strings.Contains(string(b), `"code":"final_readiness"`) { + if !strings.Contains(string(b), `"code":"empty_final"`) { t.Fatalf("notice JSON = %s, want a stable code field", b) } @@ -265,23 +265,6 @@ func TestToWireToolOmitsHostOnlyWorkspaceMutationMetadata(t *testing.T) { } func TestToWireTurnOutcomeIsOptionalAndMachineReadable(t *testing.T) { - readiness := ToWire(event.Event{ - Kind: event.TurnDone, - Err: errors.New("final-answer readiness failed 3 times: missing verification"), - Outcome: event.TurnOutcomeFinalReadiness, - Readiness: &event.FinalReadiness{Attempts: 3, Missing: []string{"verification", "review"}}, - }) - if readiness.Outcome != event.TurnOutcomeFinalReadiness || readiness.Err == "" || readiness.Readiness == nil || readiness.Readiness.Attempts != 3 { - t.Fatalf("readiness wire event = %+v", readiness) - } - b, err := json.Marshal(readiness) - if err != nil { - t.Fatalf("marshal readiness: %v", err) - } - if !strings.Contains(string(b), `"outcome":"final_readiness"`) || !strings.Contains(string(b), `"missing":["verification","review"]`) { - t.Fatalf("readiness JSON = %s, want structured outcome", b) - } - ordinary, err := json.Marshal(ToWire(event.Event{Kind: event.TurnDone, Err: errors.New("provider failed")})) if err != nil { t.Fatalf("marshal ordinary error: %v", err) diff --git a/internal/contract/hostaudit/readiness_audit.go b/internal/contract/hostaudit/readiness_audit.go deleted file mode 100644 index 0557ae4683..0000000000 --- a/internal/contract/hostaudit/readiness_audit.go +++ /dev/null @@ -1,30 +0,0 @@ -package hostaudit - -// ReadinessAuditResult classifies one host final-answer readiness audit receipt. -type ReadinessAuditResult string - -const ( - ReadinessAllowed ReadinessAuditResult = "allowed" - ReadinessBlocked ReadinessAuditResult = "blocked" - ReadinessErrored ReadinessAuditResult = "errored" -) - -// ReadinessAudit is a structured, non-rendered receipt for the final-answer -// readiness gate. It lets metrics sinks count why the host blocked a final -// answer without scraping Notice text or adding model-visible state. -type ReadinessAudit struct { - Result ReadinessAuditResult - Recovered bool - MissingProjectChecks int - IncompleteTodos int - MissingAcceptanceCriteria int - MissingVerification int - // Structured review and changed-path inspection are different debts read - // from different indices, and one counter could not say which was owed. - MissingStructuredReview int - MissingPathInspection int - MissingSignoff int - MissingMutation int - MissingCapabilities int - MissingRender int -} diff --git a/internal/frontend/acp/server_test.go b/internal/frontend/acp/server_test.go index f80c2d07fb..76a48627a5 100644 --- a/internal/frontend/acp/server_test.go +++ b/internal/frontend/acp/server_test.go @@ -2213,61 +2213,6 @@ func TestServePromptFailureIsAJSONRPCErrorWithTypedCode(t *testing.T) { } } -func TestServeReadinessPauseEndsTheTurnWithAReceiptAndTypedOutcome(t *testing.T) { - factory := &fakeFactory{behavior: func(_ context.Context, sink event.Sink, _ string) error { - sink.Emit(event.Event{Kind: event.Text, Text: "all done"}) - return &agent.FinalReadinessError{Attempts: 1, Reason: "run a relevant verification command", Missing: []string{"verification"}} - }} - client, stop := startServer(t, factory) - defer stop() - - client.call(t, "initialize", InitializeParams{ProtocolVersion: 1}) - newResp := client.call(t, "session/new", SessionNewParams{}) - var nr SessionNewResult - json.Unmarshal(newResp.Result, &nr) - - promptCh := client.callAsync("session/prompt", SessionPromptParams{ - SessionID: nr.SessionID, - Prompt: []ContentBlock{{Type: "text", Text: "write something"}}, - }) - notifs, resp := drainPrompt(t, client, promptCh) - if resp.Error != nil { - t.Fatalf("a readiness pause is not a failure: %+v", resp.Error) - } - var raw map[string]json.RawMessage - if err := json.Unmarshal(resp.Result, &raw); err != nil { - t.Fatalf("prompt result: %v", err) - } - if string(raw["stopReason"]) != `"end_turn"` { - t.Errorf("stopReason = %s, want end_turn", raw["stopReason"]) - } - var meta struct { - Reasonix struct { - TurnOutcome ReasonixTurnOutcome `json:"turnOutcome"` - } `json:"reasonix.io"` - } - if err := json.Unmarshal(raw["_meta"], &meta); err != nil || meta.Reasonix.TurnOutcome.Kind != "paused" { - t.Errorf("_meta = %s (%v), want turnOutcome kind paused", raw["_meta"], err) - } - var text strings.Builder - for _, n := range notifs { - var p struct { - Update struct { - SessionUpdate string `json:"sessionUpdate"` - Content struct { - Text string `json:"text"` - } `json:"content"` - } `json:"update"` - } - if json.Unmarshal(n.Params, &p) == nil && p.Update.SessionUpdate == "agent_message_chunk" { - text.WriteString(p.Update.Content.Text) - } - } - if got := text.String(); !strings.Contains(got, "all done") || !strings.Contains(got, "[paused]") || !strings.Contains(got, "verification") { - t.Errorf("streamed text = %q, want the answer followed by a [paused] receipt naming the missing verification", got) - } -} - // statusUpdatesCaps is the initialize declaration that opts a test client in to // the unsolicited vendor status notifications. var statusUpdatesCaps = ClientCapabilities{Meta: map[string]any{"reasonix.io": map[string]any{ diff --git a/internal/frontend/acp/session.go b/internal/frontend/acp/session.go index bb2d695a28..3732464169 100644 --- a/internal/frontend/acp/session.go +++ b/internal/frontend/acp/session.go @@ -17,7 +17,6 @@ import ( "time" "reasonix/internal/contract/provider" - "reasonix/internal/runtime/agent" "reasonix/internal/session/control" "reasonix/internal/state/sessioninbox" ) @@ -367,14 +366,9 @@ func (s *service) sessionPrompt(ctx context.Context, raw json.RawMessage) (any, stop := StopEndTurn vendor := map[string]any{} if runErr != nil { - var readinessErr *agent.FinalReadinessError switch { case runCtx.Err() != nil: stop = StopCancelled - case errors.As(runErr, &readinessErr): - outcome := ReasonixTurnOutcome{Kind: "paused", Reason: clipStatusText(readinessErr.Error(), 2_048)} - sess.sink.send(readinessReceipt(outcome.Reason, readinessErr.Missing)) - vendor["turnOutcome"] = outcome default: return nil, &RPCError{ Code: ErrInternal, @@ -735,13 +729,3 @@ func sessionInfoMatchesCwd(info SessionInfo, filter string) bool { } return filepath.Clean(info.Cwd) == filepath.Clean(filter) } - -// readinessReceipt is the host-written line a text-only client sees when the -// turn stopped before delivery was verified; the typed outcome rides _meta. -func readinessReceipt(reason string, missing []string) messageChunk { - text := "\n\n[paused] Not verified, the work may be incomplete: " + reason - if len(missing) > 0 { - text += " (missing: " + strings.Join(redactStatusTexts(missing, 256), ", ") + ")" - } - return messageChunk{SessionUpdate: "agent_message_chunk", Content: textBlock(text)} -} diff --git a/internal/frontend/acp/status.go b/internal/frontend/acp/status.go index a28c1e36a8..0588be4f7d 100644 --- a/internal/frontend/acp/status.go +++ b/internal/frontend/acp/status.go @@ -3,7 +3,6 @@ package acp import ( "context" "encoding/json" - "errors" "math" "reasonix/internal/contract/pricing" "runtime" @@ -15,7 +14,6 @@ import ( "reasonix/internal/base/secrets" "reasonix/internal/contract/event" "reasonix/internal/contract/provider" - "reasonix/internal/runtime/agent" "reasonix/internal/session/control" ) @@ -351,11 +349,6 @@ func (t *statusTelemetry) onEvent(e event.Event) (string, bool) { return "phase", true case event.ToolDispatch: t.mutate(func(t *statusTelemetry) { t.phase = "implementing" }) - case event.Notice: - if e.Code == event.NoticeCodeFinalReadiness { - t.mutate(func(t *statusTelemetry) { t.phase = "checking_readiness" }) - return "phase", true - } } return "", false } @@ -386,13 +379,7 @@ func (t *statusTelemetry) finishTurn(runErr error, cancelled bool, goalStatus, s t.turnOutcome = ReasonixTurnOutcome{Kind: "paused", Reason: "goal blocked"} eventName = "pause" default: - var readinessErr *agent.FinalReadinessError switch { - case errors.As(runErr, &readinessErr): - t.phase = "readiness_paused" - t.turnOutcome = ReasonixTurnOutcome{Kind: "paused", Reason: clipStatusText(readinessErr.Error(), 2_048)} - t.finalReadiness.Risks = redactStatusTexts(readinessErr.Missing, 2_048) - eventName = "pause" case runErr != nil: t.phase = "error" t.turnOutcome = ReasonixTurnOutcome{Kind: "error", Reason: clipStatusText(runErr.Error(), 2_048)} diff --git a/internal/frontend/acp/status_test.go b/internal/frontend/acp/status_test.go index bbdd484450..eeb104be1e 100644 --- a/internal/frontend/acp/status_test.go +++ b/internal/frontend/acp/status_test.go @@ -13,7 +13,6 @@ import ( "reasonix/internal/base/testenv" "reasonix/internal/contract/event" "reasonix/internal/contract/provider" - "reasonix/internal/runtime/agent" ) type statusFactory struct { @@ -207,19 +206,8 @@ func TestStatusNormalizesPhaseAndRedactsPublicText(t *testing.T) { if got := telemetry.snapshot().phase; got != "working" { t.Fatalf("unknown phase = %q, want working", got) } - telemetry.finishTurn(&agent.FinalReadinessError{ - Attempts: 1, - Reason: "token=secret-reason", - Missing: []string{"api_key=secret-risk"}, - }, false, "running", "authorization: bearer secret-summary") + telemetry.finishTurn(errors.New("token=secret-reason"), false, "running", "authorization: bearer secret-summary") snapshot := telemetry.snapshot() - encoded, err := json.Marshal(snapshot.finalReadiness) - if err != nil { - t.Fatal(err) - } - if strings.Contains(string(encoded), "secret-") || !strings.Contains(string(encoded), "[redacted]") { - t.Fatalf("status text was not redacted: %s", encoded) - } if strings.Contains(snapshot.turnOutcome.Reason, "secret-") { t.Fatalf("turn outcome was not redacted: %q", snapshot.turnOutcome.Reason) } @@ -308,13 +296,6 @@ func TestStatusRecomputesPlannerModeAfterWorkModeSwitch(t *testing.T) { func TestStatusClassifiesPauseAndError(t *testing.T) { telemetry := newStatusTelemetry() - telemetry.beginTurn() - pauseEvent := telemetry.finishTurn(&agent.FinalReadinessError{Attempts: 3, Reason: "missing verification", Missing: []string{"verify"}}, false, "running", "partial") - paused := telemetry.snapshot() - if pauseEvent != "pause" || paused.turnOutcome.Kind != "paused" || len(paused.finalReadiness.Risks) != 1 { - t.Fatalf("pause classification = event %q snapshot %+v", pauseEvent, paused) - } - telemetry.beginTurn() errorEvent := telemetry.finishTurn(errors.New("provider failed"), false, "running", "") failed := telemetry.snapshot() diff --git a/internal/frontend/cli/run_completion.go b/internal/frontend/cli/run_completion.go index 38ec2dbfdf..446c3d3e69 100644 --- a/internal/frontend/cli/run_completion.go +++ b/internal/frontend/cli/run_completion.go @@ -41,8 +41,8 @@ type runCompletion struct { class string isError bool exitCode int - // unverified: the model finished and the host's final-readiness judgement - // stayed unmet. That is a verdict on the answer, not a failed run. + // unverified: the model finished but the host refused the work it was asked + // for. That is a verdict on the answer, not a failed run. unverified bool } @@ -50,9 +50,6 @@ func classifyRunCompletion(err error) runCompletion { if err == nil { return runCompletion{subtype: "success", class: "success"} } - if runReadiness(err) != nil { - return runCompletion{subtype: "success", class: runFailureClass(err), unverified: true} - } return runCompletion{ subtype: "error_during_execution", class: runFailureClass(err), diff --git a/internal/frontend/cli/run_completion_test.go b/internal/frontend/cli/run_completion_test.go index 24b8970011..fecb63448d 100644 --- a/internal/frontend/cli/run_completion_test.go +++ b/internal/frontend/cli/run_completion_test.go @@ -5,8 +5,6 @@ import ( "errors" "fmt" "testing" - - "reasonix/internal/runtime/agent" ) func TestClassifyRunCompletionNamesTheFailureClass(t *testing.T) { @@ -15,7 +13,6 @@ func TestClassifyRunCompletionNamesTheFailureClass(t *testing.T) { want string }{ {nil, "success"}, - {&agent.FinalReadinessError{Attempts: 3, Reason: "incomplete todos"}, "final_readiness"}, {fmt.Errorf("run: %w", context.DeadlineExceeded), "timeout"}, {context.Canceled, "cancelled"}, {errors.New("provider 500"), "error_during_execution"}, @@ -25,12 +22,3 @@ func TestClassifyRunCompletionNamesTheFailureClass(t *testing.T) { } } } - -// An unmet final-readiness judgement is a verdict on a finished run: it keeps -// its benchmark class and exits 0. -func TestFinalReadinessIsAVerdictNotAFailure(t *testing.T) { - readiness := classifyRunCompletion(&agent.FinalReadinessError{Attempts: 3}) - if readiness.isError || readiness.exitCode != 0 || !readiness.unverified || readiness.subtype != "success" { - t.Fatalf("final-readiness completion = %+v, want a successful, unverified run", readiness) - } -} diff --git a/internal/frontend/cli/run_metrics.go b/internal/frontend/cli/run_metrics.go index a2c63d21f5..48e1641519 100644 --- a/internal/frontend/cli/run_metrics.go +++ b/internal/frontend/cli/run_metrics.go @@ -59,23 +59,10 @@ type RunMetrics struct { // CompletionVerdict and CompletionGapKinds are the turn's own verdict on // itself. Outcome answers only whether the run errored, so without these a // reader is told "success" by a file the same run warned a human about. - CompletionVerdict string `json:"completion_verdict,omitempty"` - CompletionGapKinds []string `json:"completion_gap_kinds,omitempty"` - Estimated bool `json:"estimated,omitempty"` - Compactions int `json:"compactions"` - ReadinessChecks int `json:"readiness_checks"` - ReadinessAllowed int `json:"readiness_allowed"` - ReadinessBlocks int `json:"readiness_blocks"` - ReadinessRecoveries int `json:"readiness_recoveries"` - ReadinessErrors int `json:"readiness_errors"` - ReadinessMissingProjectChecks int `json:"readiness_missing_project_checks"` - ReadinessIncompleteTodos int `json:"readiness_incomplete_todos"` - ReadinessMissingAcceptance int `json:"readiness_missing_acceptance_criteria"` - ReadinessMissingVerification int `json:"readiness_missing_verification"` - ReadinessMissingStructuredReview int `json:"readiness_missing_structured_review"` - ReadinessMissingPathInspection int `json:"readiness_missing_path_inspection"` - ReadinessMissingSignoff int `json:"readiness_missing_signoff"` - ReadinessMissingMutation int `json:"readiness_missing_mutation"` + CompletionVerdict string `json:"completion_verdict,omitempty"` + CompletionGapKinds []string `json:"completion_gap_kinds,omitempty"` + Estimated bool `json:"estimated,omitempty"` + Compactions int `json:"compactions"` // Project-check shadow: the two derivations compared, by what explains a // disagreement. Only the last two classes are candidate defects. ProjectCheckProbes int `json:"project_check_probes"` @@ -498,48 +485,6 @@ func (s *metricsSink) RecordDelegationAudit(a hostaudit.DelegationAudit) { } } -func (s *metricsSink) RecordReadinessAudit(a hostaudit.ReadinessAudit) { - if s == nil { - return - } - defer event.RecordReadinessAudit(s.inner, a) - s.mu.Lock() - defer s.mu.Unlock() - s.m.ReadinessChecks++ - switch a.Result { - case hostaudit.ReadinessAllowed: - s.m.ReadinessAllowed++ - case hostaudit.ReadinessBlocked: - s.m.ReadinessBlocks++ - case hostaudit.ReadinessErrored: - s.m.ReadinessErrors++ - } - if a.Recovered { - s.m.ReadinessRecoveries++ - } - s.m.ReadinessMissingProjectChecks += a.MissingProjectChecks - s.m.ReadinessIncompleteTodos += a.IncompleteTodos - s.m.ReadinessMissingAcceptance += a.MissingAcceptanceCriteria - s.m.ReadinessMissingVerification += a.MissingVerification - s.m.ReadinessMissingStructuredReview += a.MissingStructuredReview - s.m.ReadinessMissingPathInspection += a.MissingPathInspection - s.m.ReadinessMissingSignoff += a.MissingSignoff - s.m.ReadinessMissingMutation += a.MissingMutation -} - -func (s *metricsSink) RecordVerificationContractDrift(d event.VerificationContractDrift) { - if s == nil { - return - } - defer event.RecordVerificationContractDrift(s.inner, d) - s.mu.Lock() - defer s.mu.Unlock() - s.m.GoalResumesWithContract++ - if d.Drift { - s.m.GoalVerificationDrifts++ - } -} - func (s *metricsSink) RecordSubagentHandoff(a event.SubagentHandoffAudit) { if s == nil { return @@ -586,40 +531,6 @@ func (s *metricsSink) RecordSubagentHandoff(a event.SubagentHandoffAudit) { s.m.SubagentHandoffLowered += a.LoweredClaims } -func (s *metricsSink) RecordProjectCheckProbe(p event.ProjectCheckProbe) { - if s == nil { - return - } - defer event.RecordProjectCheckProbe(s.inner, p) - s.mu.Lock() - defer s.mu.Unlock() - s.m.ProjectCheckProbes++ - if p.LegacyBlocked == p.CandidateBlocked { - s.m.ProjectCheckStopParity++ - } - for _, d := range p.Diffs { - switch d.Class { - case event.ProjectCheckBaselinePreservation: - s.m.ProjectCheckBaselinePreserved++ - case event.ProjectCheckMutationIndex: - s.m.ProjectCheckMutationIndex++ - case event.ProjectCheckIdentityNormalization: - s.m.ProjectCheckIdentityNorm++ - case event.ProjectCheckCandidateOnly: - s.m.ProjectCheckCandidateOnly++ - case event.ProjectCheckLegacyOnly: - s.m.ProjectCheckLegacyOnly++ - } - } -} - -func (s *metricsSink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - if a.ChildID != "" { - s.m.SubagentRecoveries++ - } - event.RecordProtocolRecovery(s.inner, a) -} - // MergeCapabilityAuditCounters copies capability counters into RunMetrics. func (m *RunMetrics) MergeCapabilityAuditCounters( routes, routedCandidates, routedRequire, routedPrefer, routedSuggest, declines int, diff --git a/internal/frontend/cli/run_metrics_test.go b/internal/frontend/cli/run_metrics_test.go index 612ca30189..372cb63981 100644 --- a/internal/frontend/cli/run_metrics_test.go +++ b/internal/frontend/cli/run_metrics_test.go @@ -1,13 +1,8 @@ package cli import ( - "encoding/json" - "os" - "path/filepath" - "reasonix/internal/contract/hostaudit" "testing" - "reasonix/internal/base/testenv" "reasonix/internal/contract/event" "reasonix/internal/contract/provider" ) @@ -37,60 +32,6 @@ func TestMetricsSinkUsesProviderCacheWriteCost(t *testing.T) { } } -func TestMetricsSinkAccumulatesReadinessAudit(t *testing.T) { - s := &metricsSink{inner: event.Discard} - - s.RecordReadinessAudit(hostaudit.ReadinessAudit{ - Result: hostaudit.ReadinessBlocked, - MissingProjectChecks: 2, - IncompleteTodos: 3, - MissingAcceptanceCriteria: 1, - MissingVerification: 1, - MissingStructuredReview: 1, - MissingPathInspection: 1, - MissingSignoff: 1, - MissingMutation: 1, - }) - s.RecordReadinessAudit(hostaudit.ReadinessAudit{ - Result: hostaudit.ReadinessAllowed, - Recovered: true, - }) - s.RecordReadinessAudit(hostaudit.ReadinessAudit{ - Result: hostaudit.ReadinessErrored, - }) - - if s.m.ReadinessChecks != 3 { - t.Fatalf("readiness checks = %d, want 3", s.m.ReadinessChecks) - } - if s.m.ReadinessAllowed != 1 { - t.Fatalf("readiness allowed = %d, want 1", s.m.ReadinessAllowed) - } - if s.m.ReadinessBlocks != 1 { - t.Fatalf("readiness blocks = %d, want 1", s.m.ReadinessBlocks) - } - if s.m.ReadinessRecoveries != 1 { - t.Fatalf("readiness recoveries = %d, want 1", s.m.ReadinessRecoveries) - } - if s.m.ReadinessErrors != 1 { - t.Fatalf("readiness errors = %d, want 1", s.m.ReadinessErrors) - } - if s.m.ReadinessMissingProjectChecks != 2 { - t.Fatalf("missing project checks = %d, want 2", s.m.ReadinessMissingProjectChecks) - } - if s.m.ReadinessIncompleteTodos != 3 { - t.Fatalf("incomplete todos = %d, want 3", s.m.ReadinessIncompleteTodos) - } - if s.m.ReadinessMissingAcceptance != 1 || s.m.ReadinessMissingVerification != 1 || - s.m.ReadinessMissingStructuredReview != 1 || s.m.ReadinessMissingPathInspection != 1 || s.m.ReadinessMissingSignoff != 1 { - t.Fatalf("delivery readiness misses = acceptance %d verification %d structured-review %d path-inspection %d signoff %d, want 1/1/1/1/1", - s.m.ReadinessMissingAcceptance, s.m.ReadinessMissingVerification, - s.m.ReadinessMissingStructuredReview, s.m.ReadinessMissingPathInspection, s.m.ReadinessMissingSignoff) - } - if s.m.ReadinessMissingMutation != 1 { - t.Fatalf("delivery work misses = mutation %d, want 1", s.m.ReadinessMissingMutation) - } -} - func TestMetricsSinkAccountsToolCallsAndRetries(t *testing.T) { s := &metricsSink{inner: event.Discard} @@ -127,60 +68,6 @@ func TestMetricsSinkAccountsToolCallsAndRetries(t *testing.T) { } } -func TestWriteMetricsIncludesReadinessFields(t *testing.T) { - path := filepath.Join(testenv.TempDir(t), "metrics.json") - if err := writeMetrics(path, RunMetrics{ - PromptTokens: 10, - CompletionTokens: 3, - CacheHitTokens: 7, - CacheMissTokens: 3, - Steps: 2, - ReadinessChecks: 1, - ReadinessAllowed: 1, - ReadinessBlocks: 0, - ReadinessRecoveries: 1, - ReadinessErrors: 0, - ReadinessMissingProjectChecks: 0, - ReadinessIncompleteTodos: 0, - ReadinessMissingAcceptance: 0, - ReadinessMissingVerification: 0, - ReadinessMissingStructuredReview: 0, - ReadinessMissingPathInspection: 0, - ReadinessMissingSignoff: 0, - ReadinessMissingMutation: 0, - }); err != nil { - t.Fatalf("writeMetrics: %v", err) - } - - b, err := os.ReadFile(path) - if err != nil { - t.Fatalf("ReadFile: %v", err) - } - var got map[string]any - if err := json.Unmarshal(b, &got); err != nil { - t.Fatalf("Unmarshal: %v", err) - } - for _, key := range []string{ - "readiness_checks", - "readiness_allowed", - "readiness_blocks", - "readiness_recoveries", - "readiness_errors", - "readiness_missing_project_checks", - "readiness_incomplete_todos", - "readiness_missing_acceptance_criteria", - "readiness_missing_verification", - "readiness_missing_structured_review", - "readiness_missing_path_inspection", - "readiness_missing_signoff", - "readiness_missing_mutation", - } { - if _, ok := got[key]; !ok { - t.Fatalf("metrics JSON missing %q: %s", key, string(b)) - } - } -} - // Outcome answers only whether the run errored. A reader that stops there is // told "success" by the same run that warned its human about open gaps, so the // turn's own verdict has to reach the file too. diff --git a/internal/frontend/cli/run_output.go b/internal/frontend/cli/run_output.go index d0baebad02..3905635e37 100644 --- a/internal/frontend/cli/run_output.go +++ b/internal/frontend/cli/run_output.go @@ -99,7 +99,6 @@ type runResult struct { PermissionDenials []runPermissionDenial `json:"permission_denials"` // PermissionMode is the posture the run settled on, named or defaulted. PermissionMode string `json:"permission_mode,omitempty"` - Readiness *eventwire.FinalReadiness `json:"readiness,omitempty"` UnverifiedBy string `json:"unverified_by,omitempty"` Completion *eventwire.CompletionSummary `json:"completion,omitempty"` } @@ -156,8 +155,7 @@ type machineRunDone struct { NumTurns int `json:"num_turns"` Usage machineEventUsage `json:"usage"` // PermissionDenials counts refused calls; the list itself carries content. - PermissionDenials int `json:"permission_denials,omitempty"` - Readiness *eventwire.FinalReadiness `json:"readiness,omitempty"` + PermissionDenials int `json:"permission_denials,omitempty"` } type runOutputSink struct { @@ -339,7 +337,6 @@ func (s *runOutputSink) Finalize(sessionID string, started time.Time, runErr err Usage: machineEventUsage{InputTokens: s.usage.InputTokens, OutputTokens: s.usage.OutputTokens, CacheHitTokens: s.usage.CacheReadInputTokens, CacheMissTokens: s.usage.CacheCreationInputTokens}, PermissionDenials: len(s.permissions.denials), - Readiness: runReadiness(runErr), }) } resultText := s.final @@ -393,7 +390,6 @@ func (s *runOutputSink) Finalize(sessionID string, started time.Time, runErr err Usage: s.usage, PermissionDenials: append([]runPermissionDenial{}, s.permissions.denials...), PermissionMode: s.permissions.mode, - Readiness: runReadiness(runErr), UnverifiedBy: unverifiedBy(s.permissions.denials), Completion: s.verdict.completion, }) diff --git a/internal/frontend/cli/run_verdict.go b/internal/frontend/cli/run_verdict.go index 738ee22f5e..660f3bd0d1 100644 --- a/internal/frontend/cli/run_verdict.go +++ b/internal/frontend/cli/run_verdict.go @@ -1,7 +1,6 @@ package cli import ( - "errors" "fmt" "io" "strings" @@ -10,7 +9,6 @@ import ( "reasonix/internal/contract/config" "reasonix/internal/contract/event" "reasonix/internal/contract/eventwire" - "reasonix/internal/runtime/agent" "reasonix/internal/safety/permission" "reasonix/internal/session/control" ) @@ -122,15 +120,6 @@ func (t *runDenialTally) snapshot() []runPermissionDenial { return append([]runPermissionDenial(nil), t.denials...) } -// runReadiness is the unmet final-readiness judgement a run ended with, or nil. -func runReadiness(runErr error) *eventwire.FinalReadiness { - var readiness *agent.FinalReadinessError - if !errors.As(runErr, &readiness) || readiness == nil { - return nil - } - return &eventwire.FinalReadiness{Attempts: readiness.Attempts, Missing: append([]string(nil), readiness.Missing...)} -} - // withFolderRefusal marks a run whose edits and commands were refused because // the folder is untrusted as unverified: whatever the model then said, the work // it was asked for did not happen. diff --git a/internal/platform/notify/sink_test.go b/internal/platform/notify/sink_test.go index 253c61aec5..f0b05393ff 100644 --- a/internal/platform/notify/sink_test.go +++ b/internal/platform/notify/sink_test.go @@ -12,37 +12,17 @@ import ( var errTestFailure = errors.New("failed") type recordSink struct { - events []event.Kind - recovery []event.ProtocolRecoveryAudit + events []event.Kind } func (s *recordSink) Emit(e event.Event) { s.events = append(s.events, e.Kind) } -func (s *recordSink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - s.recovery = append(s.recovery, a) -} - type recordSender struct { messages []Message } -func TestSinkForwardsProtocolRecoveryWithoutNotification(t *testing.T) { - inner := &recordSink{} - sender := &recordSender{} - sink := NewSink(inner, sender, i18n.English, NewSettings(config.NotificationsConfig{Enabled: true, TurnDone: true})) - - event.RecordProtocolRecovery(sink, event.ProtocolRecoveryAudit{Kind: event.ProtocolRecoveryKind("test")}) - - if len(inner.recovery) != 1 || inner.recovery[0].Kind != event.ProtocolRecoveryKind("test") { - t.Fatalf("forwarded protocol recovery = %+v", inner.recovery) - } - if len(sender.messages) != 0 { - t.Fatalf("protocol recovery sent user notification: %+v", sender.messages) - } -} - func (s *recordSender) Send(m Message) error { s.messages = append(s.messages, m) return nil diff --git a/internal/platform/telemetry/client.go b/internal/platform/telemetry/client.go index a153ad9e02..d0bc13687d 100644 --- a/internal/platform/telemetry/client.go +++ b/internal/platform/telemetry/client.go @@ -40,9 +40,6 @@ var uploadSignals = map[string]bool{ // hold was contended or not, how long it waited, and how much of it was // spent holding and not writing. "lease_hold": true, "lease_wait": true, "lease_idle": true, - // Recorded by RecordProtocolRecovery since it shipped and dropped here ever - // since: the bucket is a ProtocolRecoveryKind, a closed enum, never content. - "tool_call_reasoning_recovery": true, } // queuedSurface reads a payload's surface. A queue predating the field can only diff --git a/internal/platform/telemetry/client_test.go b/internal/platform/telemetry/client_test.go index e30fd6f2c7..be85b27153 100644 --- a/internal/platform/telemetry/client_test.go +++ b/internal/platform/telemetry/client_test.go @@ -398,7 +398,6 @@ func TestEverySignalTheSinkRecordsSurvivesTheUploadAllowList(t *testing.T) { sink.Emit(event.Event{Kind: event.ToolResult, Tool: event.Tool{Err: "permission denied"}}) sink.Emit(event.Event{Kind: event.Notice, Code: event.NoticeCodeEmptyFinal}) sink.Emit(event.Event{Kind: event.CompactionStarted, Compaction: event.Compaction{Trigger: "auto"}}) - event.RecordProtocolRecovery(sink, event.ProtocolRecoveryAudit{Kind: event.ProtocolRecoveryKind("test")}) sink.Emit(event.Event{Kind: event.TurnDone}) reporter.RecordRecovery(hostaudit.RecoveryMetrics{FailureEvents: 1, ReviewLatencyMsSum: 400, ReviewLatencyCount: 1}) diff --git a/internal/platform/telemetry/sink.go b/internal/platform/telemetry/sink.go index 8613f25507..0cc7e5f6b6 100644 --- a/internal/platform/telemetry/sink.go +++ b/internal/platform/telemetry/sink.go @@ -157,11 +157,6 @@ func (s *sink) Emit(e event.Event) { s.inner.Emit(e) } -func (s *sink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - add(s.counts, "tool_call_reasoning_recovery", string(a.Kind), 1) - event.RecordProtocolRecovery(s.inner, a) -} - func (s *sink) observe(e event.Event) { switch e.Kind { case event.TurnStarted: diff --git a/internal/platform/telemetry/sink_test.go b/internal/platform/telemetry/sink_test.go index 066db0d957..078fd08ee0 100644 --- a/internal/platform/telemetry/sink_test.go +++ b/internal/platform/telemetry/sink_test.go @@ -5,7 +5,6 @@ import ( "errors" "os" "path/filepath" - "reasonix/internal/contract/hostaudit" "slices" "strings" "testing" @@ -16,19 +15,10 @@ import ( ) type readinessSink struct { - events int - audits int - recovery int + events int } func (s *readinessSink) Emit(event.Event) { s.events++ } -func (s *readinessSink) RecordReadinessAudit(hostaudit.ReadinessAudit) { - s.audits++ -} -func (s *readinessSink) RecordProtocolRecovery(event.ProtocolRecoveryAudit) { - s.recovery++ -} - func TestSinkWritesOnlyWhitelistedContentFreeCounters(t *testing.T) { home := testenv.TempDir(t) reporter := &Reporter{ @@ -51,11 +41,9 @@ func TestSinkWritesOnlyWhitelistedContentFreeCounters(t *testing.T) { sink.Emit(event.Event{Kind: event.ToolResult, Tool: event.Tool{ Name: secret, Args: secret, Output: secret, Err: "permission denied: " + secret, }}) - event.RecordProtocolRecovery(sink, event.ProtocolRecoveryAudit{Kind: event.ProtocolRecoveryKind("test")}) sink.Emit(event.Event{Kind: event.TurnDone, Err: &provider.APIError{ Provider: secret, Status: 429, Body: secret, TraceID: secret, }}) - event.RecordReadinessAudit(sink, hostaudit.ReadinessAudit{}) entries, err := os.ReadDir(filepath.Join(home, pendingDirName)) if err != nil || len(entries) != 1 { @@ -77,22 +65,21 @@ func TestSinkWritesOnlyWhitelistedContentFreeCounters(t *testing.T) { got[counter.Signal] = counter.Bucket } for signal, bucket := range map[string]string{ - "client_surface": "cli", - "cli_mode": "run", - "turns": "count", - "finish_reason": "stop", - "cache_hit": "90_100", - "tool_error": "permission", - "provider_error": "rate_limit", - "cli_exit": "error", - "tool_call_reasoning_recovery": "test", + "client_surface": "cli", + "cli_mode": "run", + "turns": "count", + "finish_reason": "stop", + "cache_hit": "90_100", + "tool_error": "permission", + "provider_error": "rate_limit", + "cli_exit": "error", } { if got[signal] != bucket { t.Errorf("%s bucket = %q, want %q", signal, got[signal], bucket) } } - if inner.events != 6 || inner.audits != 1 || inner.recovery != 1 { - t.Fatalf("forwarding events=%d audits=%d recovery=%d", inner.events, inner.audits, inner.recovery) + if inner.events != 6 { + t.Fatalf("forwarding events=%d", inner.events) } } diff --git a/internal/runtime/agent/complete_step_e2e_test.go b/internal/runtime/agent/complete_step_e2e_test.go index f06dc3a1b1..6969ace75f 100644 --- a/internal/runtime/agent/complete_step_e2e_test.go +++ b/internal/runtime/agent/complete_step_e2e_test.go @@ -3,7 +3,6 @@ package agent import ( "context" "encoding/json" - "errors" "fmt" "path/filepath" "reasonix/internal/state/sessionstore" @@ -66,11 +65,6 @@ func hostAdvances(sink *recordSink) int { return n } -func readinessBlocked(err error) bool { - var readinessErr *FinalReadinessError - return errors.As(err, &readinessErr) -} - // sessionContains reports whether any message body holds sub — used to assert a // tool's own result text (a complete_step "signed off" or its rejection reason), // since Run returns nil whether or not a tool call was rejected mid-turn. @@ -116,9 +110,6 @@ func TestE2ESerialPlanHostAdvancesAndAllowsFinalAnswer(t *testing.T) { if n := hostAdvances(sink); n < 2 { t.Fatalf("host advanced %d times, want >=2 (one per complete_step)", n) } - if readinessBlocked(runErr) { - t.Fatal("a correctly signed-off plan should not trip the readiness gate") - } } // A command cited with a different string than it ran under (#2917: the model diff --git a/internal/runtime/agent/errors.go b/internal/runtime/agent/errors.go index 0c43ccc26a..23275da438 100644 --- a/internal/runtime/agent/errors.go +++ b/internal/runtime/agent/errors.go @@ -25,10 +25,6 @@ func PauseClass(err error) string { if errors.As(err, &repeated) { return PauseKindPerseveration } - var readiness *FinalReadinessError - if errors.As(err, &readiness) { - return "final_readiness" - } return "" } @@ -64,24 +60,6 @@ func InspectRunPause(err error) (RunPauseInfo, bool) { return RunPauseInfo{}, false } -// FinalReadinessError reports that the model exhausted its recovery attempts -// before satisfying the host-observed delivery checks. -type FinalReadinessError struct { - Attempts int - Reason string - Missing []string - // Signature is the full unmet-requirement state. Missing names only the - // categories, which stay put while the counts inside them move. - Signature string -} - -func (e *FinalReadinessError) Error() string { - if e == nil { - return "final-answer readiness failed" - } - return fmt.Sprintf("final-answer readiness failed %d times: %s", e.Attempts, e.Reason) -} - // EmptyFinalError reports that the model ended its response with no visible // answer text on every one of the host's retries. FinishReason is the last // terminal the provider reported, so a caller can tell a response cut off at diff --git a/internal/runtime/agent/errors_test.go b/internal/runtime/agent/errors_test.go index 57d49a6a33..eba9ba9542 100644 --- a/internal/runtime/agent/errors_test.go +++ b/internal/runtime/agent/errors_test.go @@ -6,21 +6,12 @@ import ( "testing" ) -func TestFinalReadinessErrorPreservesDiagnosticText(t *testing.T) { - err := (&FinalReadinessError{Attempts: 3, Reason: "missing verification"}).Error() - want := "final-answer readiness failed 3 times: missing verification" - if err != want { - t.Fatalf("Error() = %q, want %q", err, want) - } -} - func TestPauseClassNamesEachGuard(t *testing.T) { for _, tc := range []struct { err error want string }{ {&maxStepsPause{steps: 40, key: "max_steps"}, "max_steps"}, - {&FinalReadinessError{Attempts: 3}, "final_readiness"}, {fmt.Errorf("wrapped: %w", &maxStepsPause{steps: 5}), "max_steps"}, {errors.New("provider 500"), ""}, {nil, ""}, diff --git a/internal/runtime/agent/evidence_flow_test.go b/internal/runtime/agent/evidence_flow_test.go index a58624912c..6e372018ff 100644 --- a/internal/runtime/agent/evidence_flow_test.go +++ b/internal/runtime/agent/evidence_flow_test.go @@ -2,7 +2,6 @@ package agent import ( "context" - "reasonix/internal/contract/hostaudit" "reasonix/internal/state/sessionstore" "reflect" "strings" @@ -99,16 +98,6 @@ func sessionHasUserMessageContaining(s *sessionstore.Session, needle string) boo return false } -type readinessAuditSink struct { - events []hostaudit.ReadinessAudit -} - -func (s *readinessAuditSink) Emit(event.Event) {} - -func (s *readinessAuditSink) RecordReadinessAudit(a hostaudit.ReadinessAudit) { - s.events = append(s.events, a) -} - // TestEvidenceFlowEndToEnd drives a full Run(): turn 1 runs bash then signs the // step off citing that exact command; complete_step must see the host receipt // recorded earlier in the same batch and report it host-verified. @@ -395,46 +384,6 @@ func TestFinalReadinessAllowsWriterWithoutChecksOrTodos(t *testing.T) { } } -func TestFinalReadinessAuditSkipsWhenGateDoesNotApply(t *testing.T) { - t.Run("no writer", func(t *testing.T) { - prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{ - {{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}}, - }} - sink := &readinessAuditSink{} - a := New(prov, tool.NewRegistry(), sessionstore.NewSession(""), Options{ - ProjectChecks: []instruction.VerifyCheck{{Command: "go test ./...", SourcePath: "AGENTS.md", Line: 3}}, - }, sink) - - if err := a.Run(context.Background(), "inspect only"); err != nil { - t.Fatalf("Run: %v", err) - } - if len(sink.events) != 0 { - t.Fatalf("readiness audit events = %d, want 0: %+v", len(sink.events), sink.events) - } - }) - - t.Run("writer without checks or todo", func(t *testing.T) { - reg := tool.NewRegistry() - reg.Add(fakeTool{name: "write_file", readOnly: false, writesPaths: true}) - prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{ - { - toolCallChunk("c1", "write_file", `{"path":"changed.go","content":"package main"}`), - {Type: provider.ChunkDone}, - }, - {{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}}, - }} - sink := &readinessAuditSink{} - a := New(prov, reg, sessionstore.NewSession(""), Options{}, sink) - - if err := a.Run(context.Background(), "simple edit"); err != nil { - t.Fatalf("Run: %v", err) - } - if len(sink.events) != 0 { - t.Fatalf("readiness audit events = %d, want 0: %+v", len(sink.events), sink.events) - } - }) -} - func TestTodoWriteOnlyTurnMayEndWithIncompleteTodos(t *testing.T) { todoWrite, ok := tool.LookupBuiltin("todo_write") if !ok { diff --git a/internal/runtime/agent/project_check_probe_test.go b/internal/runtime/agent/project_check_probe_test.go deleted file mode 100644 index 206f088461..0000000000 --- a/internal/runtime/agent/project_check_probe_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package agent - -import ( - "context" - "testing" - - "reasonix/internal/state/sessionstore" - - "reasonix/internal/contract/event" - "reasonix/internal/contract/provider" - "reasonix/internal/contract/tool" - "reasonix/internal/safety/evidence" - "reasonix/internal/state/instruction" -) - -type projectCheckProbeSink struct { - probes []event.ProjectCheckProbe -} - -func (s *projectCheckProbeSink) Emit(event.Event) {} - -func (s *projectCheckProbeSink) RecordProjectCheckProbe(p event.ProjectCheckProbe) { - s.probes = append(s.probes, p) -} - -// Outside a delivery scope the checkpoint is left over from whatever restored -// it, so its baseline is not this turn's to owe. -func TestProjectCheckBaselineIgnoredOutsideItsScope(t *testing.T) { - const began, replacement = "go test ./...", "go test ./internal/foo" - reg := tool.NewRegistry() - reg.Add(fakeTool{name: "write_file", readOnly: false, writesPaths: true}) - reg.Add(fakeTool{name: "bash", readOnly: false}) - prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{ - { - toolCallChunk("c1", "write_file", `{"path":"changed.go","content":"package main"}`), - {Type: provider.ChunkDone}, - }, - { - toolCallChunk("c2", "bash", `{"command":"`+replacement+`"}`), - {Type: provider.ChunkDone}, - }, - {{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}}, - }} - a := New(prov, reg, sessionstore.NewSession(""), Options{ - ProjectChecks: []instruction.VerifyCheck{{Command: replacement, SourcePath: "AGENTS.md", Line: 3}}, - }, &projectCheckProbeSink{}) - a.RestoreDeliveryCheckpoint(evidence.DeliveryCheckpoint{ScopeID: "goal-elsewhere", BaselineChecks: []string{began}}) - if err := a.Run(context.Background(), "edit"); err != nil { - t.Fatalf("unscoped turn err = %v, want a restored goal's baseline not to bind it", err) - } -} diff --git a/internal/runtime/agent/restart_equivalence_test.go b/internal/runtime/agent/restart_equivalence_test.go deleted file mode 100644 index cdb9ddd4be..0000000000 --- a/internal/runtime/agent/restart_equivalence_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package agent - -import ( - "context" - "encoding/json" - "errors" - "slices" - "strings" - "testing" - - "reasonix/internal/contract/event" - "reasonix/internal/contract/provider" - "reasonix/internal/contract/tool" - "reasonix/internal/safety/evidence" - "reasonix/internal/state/sessionstore" -) - -// Restart equivalence: resuming at a turn boundary must leave the host deciding -// what it would have decided uninterrupted. The restarted arm keeps only what a -// resume keeps: the transcript and the goal sidecar's DeliveryCheckpoint. - -// restartStep is one user turn and the model's replies to it, in order. A step -// that runs out of replies answers "done", so a divergence shows up as a -// verdict rather than as a hang. -type restartStep struct { - input string - replies [][]provider.Chunk -} - -type restartScenario struct { - scope string - task string - options Options - registry func() *tool.Registry - steps []restartStep -} - -// stepVerdict is what the host decided when a step ended. -type stepVerdict struct { - Ready bool - Missing []string -} - -func (v stepVerdict) String() string { - if v.Ready { - return "ready" - } - return "owes " + strings.Join(v.Missing, ",") -} - -// stepProvider replays the current step's replies and then says "done". -type stepProvider struct { - replies [][]provider.Chunk -} - -func (p *stepProvider) Name() string { return "restart-equivalence" } - -func (p *stepProvider) Stream(context.Context, provider.Request) (<-chan provider.Chunk, error) { - reply := []provider.Chunk{{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}} - if len(p.replies) > 0 { - reply, p.replies = p.replies[0], p.replies[1:] - } - ch := make(chan provider.Chunk, len(reply)) - for _, c := range reply { - ch <- c - } - close(ch) - return ch, nil -} - -// run plays the scenario, restarting after step restartAfter (1-based); zero -// never restarts. -func (s restartScenario) run(t *testing.T, restartAfter int) []stepVerdict { - t.Helper() - prov := &stepProvider{} - boot := func(session *sessionstore.Session) *Agent { - return New(prov, s.registry(), session, s.options, event.Discard) - } - a := boot(sessionstore.NewSession("")) - ctx := deliveryGoalContext(s.scope, s.task) - verdicts := make([]stepVerdict, 0, len(s.steps)) - for i, step := range s.steps { - if i > 0 && i == restartAfter { - a = restart(t, a, boot) - } - prov.replies = step.replies - verdicts = append(verdicts, verdictOf(t, a.Run(ctx, step.input))) - } - return verdicts -} - -// restart is what survives a process boundary: the transcript and the -// delivery checkpoint, serialized as the sidecar serializes it. Nothing else -// the running agent held is carried. -func restart(t *testing.T, running *Agent, boot func(*sessionstore.Session) *Agent) *Agent { - t.Helper() - raw, err := json.Marshal(running.DeliveryCheckpoint()) - if err != nil { - t.Fatal(err) - } - var persisted evidence.DeliveryCheckpoint - if err := json.Unmarshal(raw, &persisted); err != nil { - t.Fatal(err) - } - session := sessionstore.NewSession("") - session.Messages = slices.Clone(running.Session().Messages) - resumed := boot(session) - resumed.RestoreDeliveryCheckpoint(persisted) - return resumed -} - -func verdictOf(t *testing.T, err error) stepVerdict { - t.Helper() - if err == nil { - return stepVerdict{Ready: true} - } - var unready *FinalReadinessError - if !errors.As(err, &unready) { - t.Fatalf("step failed outside readiness: %v", err) - } - missing := slices.Clone(unready.Missing) - slices.Sort(missing) - return stepVerdict{Missing: missing} -} - -func deliveryRegistry() *tool.Registry { - reg := evidenceRegistry() - reg.Add(fakeTool{name: "read_file", readOnly: true}) - reg.Add(fakeTool{name: "review", readOnly: true}) - return reg -} - -// shipStep writes path, inspects and verifies it, and signs off. -func shipStep(path string) restartStep { - return restartStep{input: "implement it", replies: [][]provider.Chunk{ - {toolCallChunk("criteria", "todo_write", `{"todos":[{"content":"Ship it","status":"in_progress"}]}`), {Type: provider.ChunkDone}}, - {toolCallChunk("write", "write_file", `{"path":"`+path+`"}`), {Type: provider.ChunkDone}}, - {toolCallChunk("inspect", "read_file", `{"path":"`+path+`"}`), {Type: provider.ChunkDone}}, - {toolCallChunk("verify", "bash", `{"command":"go test ./..."}`), {Type: provider.ChunkDone}}, - {toolCallChunk("signoff", "complete_step", `{"step":"Ship it","result":"implemented","evidence":[{"kind":"verification","summary":"tests pass","command":"go test ./..."}]}`), {Type: provider.ChunkDone}}, - }} -} - -var finishStep = restartStep{input: "finish the goal"} - -// reverifyStep redoes everything a change owes except a review. -func reverifyStep(path string) restartStep { - return restartStep{input: "verify again and finish", replies: [][]provider.Chunk{ - {toolCallChunk("inspect-2", "read_file", `{"path":"`+path+`"}`), {Type: provider.ChunkDone}}, - {toolCallChunk("verify-2", "bash", `{"command":"go test ./..."}`), {Type: provider.ChunkDone}}, - {toolCallChunk("signoff-2", "complete_step", `{"step":"Ship it","result":"implemented","evidence":[{"kind":"verification","summary":"tests pass","command":"go test ./..."}]}`), {Type: provider.ChunkDone}}, - }} -} - -func TestRestartEquivalence(t *testing.T) { - cases := []struct { - name string - scenario restartScenario - restartAfter int - // settles is the step the uninterrupted arm must end ready on, so an - // agreement cannot be two arms stuck the same way. - settles int - // knownStricter pins what a restart is known to re-demand at a step: - // proof whose receipts did not cross the boundary. The day it stops, - // or grows, this case fails. - knownStricter map[int][]string - }{ - { - name: "signed-off ordinary change", - scenario: restartScenario{scope: "goal-plain", task: "ship main", registry: deliveryRegistry, - options: Options{DeliveryProfile: true}, - steps: []restartStep{shipStep("main.go"), finishStep}}, - restartAfter: 1, - settles: 2, - }, - { - // The review is owed through the checkpoint, and the proof already - // given rides it too, so a restart asks for exactly the review. - name: "high-risk change still owes review", - scenario: restartScenario{scope: "goal-auth", task: "change auth", registry: deliveryRegistry, - options: Options{DeliveryProfile: true, ProjectSensitivePaths: []string{"auth/**"}}, - steps: []restartStep{shipStep("auth/login.go"), finishStep, reverifyStep("auth/login.go")}}, - restartAfter: 1, - }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - uninterrupted := tc.scenario.run(t, 0) - restarted := tc.scenario.run(t, tc.restartAfter) - t.Logf("uninterrupted %v; restarted after step %d %v", uninterrupted, tc.restartAfter, restarted) - if tc.settles > 0 && !uninterrupted[tc.settles-1].Ready { - t.Fatalf("the scenario never settles: step %d %s", tc.settles, uninterrupted[tc.settles-1]) - } - for i := range uninterrupted { - step := i + 1 - for _, owed := range uninterrupted[i].Missing { - if !slices.Contains(restarted[i].Missing, owed) { - t.Fatalf("step %d: the restart dropped %s, which the uninterrupted run still owes", step, owed) - } - } - var extra []string - for _, owed := range restarted[i].Missing { - if !slices.Contains(uninterrupted[i].Missing, owed) { - extra = append(extra, owed) - } - } - if want := tc.knownStricter[step]; !slices.Equal(extra, want) { - t.Fatalf("step %d: the restart re-demands %v, known %v", step, extra, want) - } - } - }) - } -} diff --git a/internal/runtime/agent/retry_e2e_test.go b/internal/runtime/agent/retry_e2e_test.go index e6137e54fe..998e450ae6 100644 --- a/internal/runtime/agent/retry_e2e_test.go +++ b/internal/runtime/agent/retry_e2e_test.go @@ -17,9 +17,8 @@ import ( ) type recordSink struct { - mu sync.Mutex - evs []event.Event - recovery []event.ProtocolRecoveryAudit + mu sync.Mutex + evs []event.Event } func (s *recordSink) Emit(e event.Event) { @@ -40,12 +39,6 @@ func (s *recordSink) kinds(k event.Kind) []event.Event { return out } -func (s *recordSink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - s.mu.Lock() - defer s.mu.Unlock() - s.recovery = append(s.recovery, a) -} - // TestAgentEmitsRetryingThenStreams drives the whole chain end-to-end: a real // OpenAI-compatible provider hits an httptest server that returns 503 twice then // a valid SSE stream. The agent must emit a Retrying event per backoff (so the diff --git a/internal/runtime/coordinator/fixtures_test.go b/internal/runtime/coordinator/fixtures_test.go index 8e3bf43b29..061d893d82 100644 --- a/internal/runtime/coordinator/fixtures_test.go +++ b/internal/runtime/coordinator/fixtures_test.go @@ -40,9 +40,8 @@ func lastToolResult(s *sessionstore.Session, name string) string { } type recordSink struct { - mu sync.Mutex - evs []event.Event - recovery []event.ProtocolRecoveryAudit + mu sync.Mutex + evs []event.Event } func (s *recordSink) Emit(e event.Event) { @@ -63,12 +62,6 @@ func (s *recordSink) kinds(k event.Kind) []event.Event { return out } -func (s *recordSink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - s.mu.Lock() - defer s.mu.Unlock() - s.recovery = append(s.recovery, a) -} - type recordingAsker struct { questions []event.AskQuestion } diff --git a/internal/runtime/coordinator/planner_sink_test.go b/internal/runtime/coordinator/planner_sink_test.go index bcaec18ed5..788f9f3664 100644 --- a/internal/runtime/coordinator/planner_sink_test.go +++ b/internal/runtime/coordinator/planner_sink_test.go @@ -1,15 +1,3 @@ package coordinator -import ( - "testing" - - "reasonix/internal/contract/event" -) - -// A sink that implements only Emit answers no audit type assertion, so the -// planner's audits would die there. -func TestPlannerSinkForwardsEveryAuditCapability(t *testing.T) { - if missing := event.MissingCapabilities(plannerSink(event.Discard)); len(missing) > 0 { - t.Errorf("plannerSink drops %v; every audit the planner produces dies there", missing) - } -} +import () diff --git a/internal/runtime/delegation/child_sink_capabilities_test.go b/internal/runtime/delegation/child_sink_capabilities_test.go index 27cf0df166..5958a9a4d2 100644 --- a/internal/runtime/delegation/child_sink_capabilities_test.go +++ b/internal/runtime/delegation/child_sink_capabilities_test.go @@ -1,26 +1,3 @@ package delegation -import ( - "context" - "testing" - - "reasonix/internal/contract/event" -) - -// Every sink this package hands to a child agent has to forward audits: a -// wrapper implementing only Emit answers no type assertion, so the child's -// records die there and the run reads as one that never produced them. This -// package has dug that hole twice, which is why the check is a list. -func TestChildAgentSinksForwardEveryAuditCapability(t *testing.T) { - tracker := newSubagentProgressTracker(context.Background(), event.Discard) - defer tracker.finish(nil, nil) - - for name, sink := range map[string]event.Sink{ - "subSinkFor": subSinkFor("parent", event.Discard), - "subagentProgressTracker.wrap": tracker.wrap(), - } { - if missing := event.MissingCapabilities(sink); len(missing) > 0 { - t.Errorf("%s drops %v; every audit a child produces dies there", name, missing) - } - } -} +import () diff --git a/internal/runtime/delegation/fixtures_test.go b/internal/runtime/delegation/fixtures_test.go index 5ffd37ea40..9ab42d782c 100644 --- a/internal/runtime/delegation/fixtures_test.go +++ b/internal/runtime/delegation/fixtures_test.go @@ -112,9 +112,8 @@ func (p readOnlyBoundaryProxy) ResolveCall(context.Context, json.RawMessage) (to } type recordSink struct { - mu sync.Mutex - evs []event.Event - recovery []event.ProtocolRecoveryAudit + mu sync.Mutex + evs []event.Event } func (s *recordSink) Emit(e event.Event) { @@ -135,12 +134,6 @@ func (s *recordSink) kinds(k event.Kind) []event.Event { return out } -func (s *recordSink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - s.mu.Lock() - defer s.mu.Unlock() - s.recovery = append(s.recovery, a) -} - type recordingWriter struct { name string readOnly bool @@ -326,16 +319,6 @@ func (r *recordingAsker) Ask(_ context.Context, questions []event.AskQuestion) ( return []event.AskAnswer{{QuestionID: "q1", Selected: []string{"Keep going"}}}, nil } -type recoverySink struct { - audits []event.ProtocolRecoveryAudit -} - -func (s *recoverySink) Emit(event.Event) {} - -func (s *recoverySink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - s.audits = append(s.audits, a) -} - type workspaceSignalSink struct { mu sync.Mutex events []event.Event diff --git a/internal/runtime/delegation/nested_sink.go b/internal/runtime/delegation/nested_sink.go index 8faaabb2e1..aa1e82dfb9 100644 --- a/internal/runtime/delegation/nested_sink.go +++ b/internal/runtime/delegation/nested_sink.go @@ -21,16 +21,6 @@ type nestedSink struct { parent event.Sink } -// RecordProtocolRecovery attributes a repair to the child it happened in. The -// producers run inside the child's own loop and hold no id; this sink is the -// layer that knows which run it is wrapping, as it already does for tools. -func (s nestedSink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - if a.ChildID == "" { - a.ChildID = s.parentID - } - event.RecordProtocolRecovery(s.parent, a) -} - func (s nestedSink) Emit(e event.Event) { switch e.Kind { case event.ToolDispatch, event.ToolResult, event.ToolProgress: diff --git a/internal/runtime/delegation/recovery_attribution_test.go b/internal/runtime/delegation/recovery_attribution_test.go deleted file mode 100644 index 6c9b2467de..0000000000 --- a/internal/runtime/delegation/recovery_attribution_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package delegation - -import ( - "context" - "testing" - - "reasonix/internal/contract/event" -) - -// A repair inside a delegated run has to be told apart from one in the parent's -// own loop: "the child kept working" and "the host pulled the child back" are -// different answers, and the producers run where neither id is in hand. The -// attribution is asserted through the stack a child actually emits into, since -// that stack is where audits were being dropped entirely. -func TestProtocolRecoveryCarriesTheChildItHappenedIn(t *testing.T) { - parent := &recoverySink{} - tracker := newSubagentProgressTracker(context.Background(), subSinkFor("fleet-2", parent)) - defer tracker.finish(nil, nil) - - event.RecordProtocolRecovery(tracker.wrap(), event.ProtocolRecoveryAudit{ - Kind: event.ProtocolRecoveryKind("test"), - }) - event.RecordProtocolRecovery(parent, event.ProtocolRecoveryAudit{ - Kind: event.ProtocolRecoveryKind("test"), - }) - - if len(parent.audits) != 2 { - t.Fatalf("recovery audits = %d, want the child's and the parent's", len(parent.audits)) - } - if got := parent.audits[0].ChildID; got != "fleet-2" { - t.Errorf("child recovery ChildID = %q, want the delegated run's id", got) - } - if got := parent.audits[1].ChildID; got != "" { - t.Errorf("parent recovery ChildID = %q, want empty for the parent's own loop", got) - } -} diff --git a/internal/session/control/controller_test.go b/internal/session/control/controller_test.go index 13b022f219..b3ef001f9c 100644 --- a/internal/session/control/controller_test.go +++ b/internal/session/control/controller_test.go @@ -402,16 +402,6 @@ func TestReconcileCleanupPendingRemovesOrphanedArtifacts(t *testing.T) { } } -func TestTurnOutcomeClassifiesFinalReadiness(t *testing.T) { - err := &agent.FinalReadinessError{Attempts: 3, Reason: "missing verification"} - if got := turnOutcome(err); got != event.TurnOutcomeFinalReadiness { - t.Fatalf("turnOutcome() = %q, want %q", got, event.TurnOutcomeFinalReadiness) - } - if got := turnOutcome(errors.New("provider failed")); got != "" { - t.Fatalf("ordinary turn outcome = %q, want empty", got) - } -} - func TestRunTurnSnapshotsActivityWhenTranscriptChanges(t *testing.T) { dir := testenv.TempDir(t) sess := sessionstore.NewSession("sys") diff --git a/internal/session/control/extensions.go b/internal/session/control/extensions.go index 113cadc925..e364fc9bec 100644 --- a/internal/session/control/extensions.go +++ b/internal/session/control/extensions.go @@ -209,38 +209,14 @@ func extensionFailurePlugin(err error) string { // user-facing events, never to the content-free telemetry channels — without // these, enabling extensions severed every audit from the recorder. -func (s *frontendEventSink) RecordReadinessAudit(a hostaudit.ReadinessAudit) { - event.RecordReadinessAudit(s.inner, a) -} - -func (s *frontendEventSink) RecordContractShadow(a event.ContractShadowAudit) { - event.RecordContractShadow(s.inner, a) -} - func (s *frontendEventSink) RecordDelegationAudit(a hostaudit.DelegationAudit) { event.RecordDelegationAudit(s.inner, a) } -func (s *frontendEventSink) RecordCompletionReport(a event.CompletionReportAudit) { - event.RecordCompletionReport(s.inner, a) -} - -func (s *frontendEventSink) RecordEvidenceBundle(a event.EvidenceBundleAudit) { - event.RecordEvidenceBundle(s.inner, a) -} - -func (s *frontendEventSink) RecordOutcomeProgress(sample hostaudit.OutcomeSample) { - event.RecordOutcomeProgress(s.inner, sample) -} - func (s *frontendEventSink) RecordMemoryRecall(a event.MemoryRecallAudit) { event.RecordMemoryRecall(s.inner, a) } -func (s *frontendEventSink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - event.RecordProtocolRecovery(s.inner, a) -} - func (s *frontendEventSink) RecordTurnCompletion() { event.RecordTurnCompletion(s.inner) } diff --git a/internal/session/control/frontend_sink_capability_test.go b/internal/session/control/frontend_sink_capability_test.go index 54696d5ef3..42af04e70c 100644 --- a/internal/session/control/frontend_sink_capability_test.go +++ b/internal/session/control/frontend_sink_capability_test.go @@ -1,7 +1,6 @@ package control import ( - "reasonix/internal/contract/hostaudit" "testing" "reasonix/internal/contract/event" @@ -12,23 +11,15 @@ import ( func TestFrontendEventSinkForwardsAuditCapabilities(t *testing.T) { inner := &capabilityProbeSink{Sink: event.Discard} s := newFrontendEventSink(inner, nil) - event.RecordOutcomeProgress(s, hostaudit.OutcomeSample{Round: 1}) event.RecordMemoryRecall(s, event.MemoryRecallAudit{Suppressed: "probe"}) - event.RecordContractShadow(s, event.ContractShadowAudit{Verdict: "probe"}) - event.RecordCompletionReport(s, event.CompletionReportAudit{Verdict: "probe"}) - if inner.outcome != 1 || inner.recall != 1 || inner.contract != 1 || inner.completion != 1 { + if inner.recall != 1 { t.Fatalf("audits dropped by frontendEventSink: %+v", inner) } } type capabilityProbeSink struct { event.Sink - outcome, recall, contract, completion int + recall int } -func (p *capabilityProbeSink) RecordOutcomeProgress(hostaudit.OutcomeSample) { p.outcome++ } -func (p *capabilityProbeSink) RecordMemoryRecall(event.MemoryRecallAudit) { p.recall++ } -func (p *capabilityProbeSink) RecordContractShadow(event.ContractShadowAudit) { p.contract++ } -func (p *capabilityProbeSink) RecordCompletionReport(event.CompletionReportAudit) { - p.completion++ -} +func (p *capabilityProbeSink) RecordMemoryRecall(event.MemoryRecallAudit) { p.recall++ } diff --git a/internal/session/control/goal_runtime_test.go b/internal/session/control/goal_runtime_test.go index 42f9ac70f1..c131e237a0 100644 --- a/internal/session/control/goal_runtime_test.go +++ b/internal/session/control/goal_runtime_test.go @@ -11,7 +11,6 @@ import ( "reasonix/internal/base/testenv" "reasonix/internal/contract/event" "reasonix/internal/contract/provider" - "reasonix/internal/contract/tool" "reasonix/internal/runtime/agent" "reasonix/internal/runtime/goaleval" "reasonix/internal/safety/evidence" @@ -470,55 +469,3 @@ func (f minimalFakeTool) ReadOnly() bool { return f.readOnly } func (f minimalFakeTool) Execute(context.Context, json.RawMessage) (string, error) { return f.name + " done", nil } - -// TestGoalDeliveryWorkflowCompletesAfterVerifiedSignoff covers the -// Goal + Delivery combination: the model works (edit → verify → review → -// complete_step), reports complete via update_goal, and the goal completes — -// no user-facing recovery card. -func TestGoalDeliveryWorkflowCompletesAfterVerifiedSignoff(t *testing.T) { - todoWrite, _ := tool.LookupBuiltin("todo_write") - completeStep, _ := tool.LookupBuiltin("complete_step") - reg := goalRegistry() - reg.Add(todoWrite) - reg.Add(completeStep) - reg.Add(minimalFakeTool{name: "write_file"}) - reg.Add(minimalFakeTool{name: "read_file", readOnly: true}) - reg.Add(minimalFakeTool{name: "bash"}) - - prov := &scriptedTurns{turns: flattenTurns( - [][]provider.Chunk{ - {toolCallChunk("t0", "todo_write", `{"todos":[{"content":"Ship main","status":"in_progress"}]}`), {Type: provider.ChunkDone}}, - {toolCallChunk("w1", "write_file", `{"path":"main.go"}`), {Type: provider.ChunkDone}}, - {toolCallChunk("rv", "read_file", `{"path":"main.go"}`), {Type: provider.ChunkDone}}, - {toolCallChunk("vf", "bash", `{"command":"go test ./..."}`), {Type: provider.ChunkDone}}, - {toolCallChunk("sg", "complete_step", `{"step":"Ship main","result":"implemented","evidence":[{"kind":"verification","summary":"tests pass","command":"go test ./..."}]}`), {Type: provider.ChunkDone}}, - {toolCallChunk("ug", "update_goal", `{"status":"complete","reason":""}`), {Type: provider.ChunkDone}}, - textTurn("Ship main delivered."), - }, - )} - ag := agent.New(prov, reg, sessionstore.NewSession(""), agent.Options{DeliveryProfile: true}, event.Discard) - done := make(chan event.Event, 1) - var doneReadiness *event.FinalReadiness - c := New(Options{ - Runner: ag, - Executor: ag, - Sink: event.FuncSink(func(e event.Event) { - if e.Kind == event.TurnDone { - doneReadiness = e.Readiness - done <- e - } - }), - }) - c.Submit("/goal implement main") - <-done - - if got := c.GoalStatus(); got != GoalStatusComplete { - t.Fatalf("GoalStatus() = %q, want complete after verified sign-off", got) - } - if doneReadiness != nil { - t.Fatalf("TurnDone.Readiness = %+v, want nil (Goal absorbs readiness; no recovery card)", doneReadiness) - } - if got := c.Goal(); got != "" { - t.Fatalf("completed goal should be cleared, got %q", got) - } -} diff --git a/internal/session/control/port.go b/internal/session/control/port.go index 424521a22f..d47aefaa81 100644 --- a/internal/session/control/port.go +++ b/internal/session/control/port.go @@ -67,7 +67,6 @@ type TurnControl interface { // *Controller for tests. All of them now take display before input — they // are pairs of strings, and the odd one out was a swap no compiler catches. SubmitDisplay(display, input string) - SubmitDeliveryRecovery(display, input string) SubmitInvocationDisplay(display, input string, invocations []InvocationRequest) SubmitHTTPFormat(input, format string) Admission Send(input string) diff --git a/internal/session/control/turn_cancel.go b/internal/session/control/turn_cancel.go index aab0fe82b5..db8661f8f5 100644 --- a/internal/session/control/turn_cancel.go +++ b/internal/session/control/turn_cancel.go @@ -104,10 +104,6 @@ func (c *Controller) finishGuardedTurn(err error, completion *guardedTurnComplet WorkStartedAt: turnWorkStartedAt(err, cancelRequested, completion), ItemID: activeInboxID, } - var readinessErr *agent.FinalReadinessError - if errors.As(err, &readinessErr) { - done.Readiness = &event.FinalReadiness{Attempts: readinessErr.Attempts, Missing: append([]string(nil), readinessErr.Missing...)} - } // Ack active durable items before exposing TurnDone. Frontends commonly // refresh the inbox from that event and must not observe already-consumed // steers in the completed turn. Dispatch still waits for finishing to clear. diff --git a/internal/session/control/turn_entry.go b/internal/session/control/turn_entry.go index 7237534a67..e8df1a30b7 100644 --- a/internal/session/control/turn_entry.go +++ b/internal/session/control/turn_entry.go @@ -6,7 +6,6 @@ package control import ( "context" - "errors" "fmt" "reasonix/internal/state/sessionstore" "slices" @@ -23,10 +22,6 @@ import ( ) func turnOutcome(err error) string { - var readinessErr *agent.FinalReadinessError - if errors.As(err, &readinessErr) { - return event.TurnOutcomeFinalReadiness - } if info, ok := agent.InspectRunPause(err); ok && (info.Kind == agent.PauseKindNoProgress || info.Kind == agent.PauseKindPerseveration) { return event.TurnOutcomeNoProgress diff --git a/internal/session/control/turn_submit.go b/internal/session/control/turn_submit.go index 4d2f6bcffe..6d2febde81 100644 --- a/internal/session/control/turn_submit.go +++ b/internal/session/control/turn_submit.go @@ -84,16 +84,6 @@ func (c *Controller) SubmitDisplay(display, input string) { c.submit(input, display, "") } -// SubmitDeliveryRecovery runs the same visible prompt path as SubmitDisplay but -// first authorizes the executor to retain the immediately preceding exhausted -// delivery ledger. The agent consumes that authorization once; if the card came -// from an older/reloaded session this safely degrades to an ordinary turn. -func (c *Controller) SubmitDeliveryRecovery(display, input string) { - c.runGuarded(func(ctx context.Context) error { - return c.runTurnLoop(ctx, orchestratedTurn{input: input, raw: input, display: display}) - }) -} - // SubmitInvocationDisplay executes composer-selected invocation entities // independently of slash-command parsing. Plain string submit entry points keep // their existing behavior for CLI, HTTP, and backward-compatible clients. diff --git a/internal/state/stats/stats_test.go b/internal/state/stats/stats_test.go index 30d5d3c1ed..2353bf46f0 100644 --- a/internal/state/stats/stats_test.go +++ b/internal/state/stats/stats_test.go @@ -78,7 +78,7 @@ func TestRecorderCountsMergedProviderRequests(t *testing.T) { } } -func TestRecorderCapturesGuardianUsageAndPreservesProtocolAudit(t *testing.T) { +func TestRecorderCapturesGuardianUsage(t *testing.T) { dir := testenv.TempDir(t) inner := &auditSpySink{} r := NewRecorder(inner, dir, "desktop") @@ -87,7 +87,6 @@ func TestRecorderCapturesGuardianUsageAndPreservesProtocolAudit(t *testing.T) { ModelRef: "deepseek/deepseek-v4-flash", Guardian: event.GuardianResult{Usage: &provider.Usage{PromptTokens: 10, CompletionTokens: 5, TotalTokens: 15}}, }) - event.RecordProtocolRecovery(r, event.ProtocolRecoveryAudit{Kind: event.ProtocolRecoveryKind("test")}) flushRecorder(t, r) day := dayStart(time.Now()) @@ -98,9 +97,6 @@ func TestRecorderCapturesGuardianUsageAndPreservesProtocolAudit(t *testing.T) { if got.Tokens != 15 || got.TopModel != "deepseek/deepseek-v4-flash" { t.Fatalf("guardian usage = %+v", got) } - if len(inner.protocol) != 1 || inner.protocol[0].Kind != event.ProtocolRecoveryKind("test") { - t.Fatalf("protocol audit was not forwarded: %+v", inner.protocol) - } } func TestRecorderSkipsZeroUsage(t *testing.T) { @@ -470,15 +466,10 @@ type spySink struct{ events []event.Event } func (s *spySink) Emit(e event.Event) { s.events = append(s.events, e) } type auditSpySink struct { - events []event.Event - protocol []event.ProtocolRecoveryAudit + events []event.Event } func (s *auditSpySink) Emit(e event.Event) { s.events = append(s.events, e) } -func (s *auditSpySink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - s.protocol = append(s.protocol, a) -} - func usageEvent(model string, prompt, completion, reasoning, hit, miss, total int) event.Event { return event.Event{ Kind: event.Usage, diff --git a/internal/state/trajectory/coalesce_test.go b/internal/state/trajectory/coalesce_test.go index 8f176f7565..47f5f0cd7f 100644 --- a/internal/state/trajectory/coalesce_test.go +++ b/internal/state/trajectory/coalesce_test.go @@ -88,7 +88,7 @@ func TestNonDeltaEventFlushesTheRunInOrder(t *testing.T) { rec, path := recorderAt(t, "order.jsonl", &capabilitySink{}) rec.Emit(event.Event{Kind: event.Reasoning, Text: "a"}) rec.Emit(event.Event{Kind: event.Reasoning, Text: "b"}) - rec.RecordProtocolRecovery(event.ProtocolRecoveryAudit{Kind: "some_kind"}) + rec.RecordTurnCompletion() rec.Emit(event.Event{Kind: event.Reasoning, Text: "c"}) if err := rec.Close(); err != nil { t.Fatalf("Close: %v", err) @@ -100,8 +100,8 @@ func TestNonDeltaEventFlushesTheRunInOrder(t *testing.T) { if got[0].Event == nil || got[0].Event.Text != "ab" { t.Errorf("first record = %+v, want the merged \"ab\"", got[0].Event) } - if got[1].ProtocolRecovery != "some_kind" { - t.Errorf("second record = %+v, want the recovery", got[1]) + if !got[1].TurnCompletion { + t.Errorf("second record = %+v, want the turn completion", got[1]) } if got[2].Event == nil || got[2].Event.Text != "c" { t.Errorf("third record = %+v, want \"c\"", got[2].Event) diff --git a/internal/state/trajectory/recorder.go b/internal/state/trajectory/recorder.go index a697cb601e..9d17c4add1 100644 --- a/internal/state/trajectory/recorder.go +++ b/internal/state/trajectory/recorder.go @@ -28,7 +28,6 @@ type Record struct { TS int64 `json:"ts"` RunHeader *RunHeader `json:"run_header,omitempty"` Event *eventwire.Event `json:"event,omitempty"` - ReadinessAudit *ReadinessAudit `json:"readiness_audit,omitempty"` ProtocolRecovery string `json:"protocol_recovery,omitempty"` RecoveryChild string `json:"protocol_recovery_child,omitempty"` TurnCompletion bool `json:"turn_completion,omitempty"` @@ -216,22 +215,6 @@ type EvidenceBundle struct { ContractFailure string `json:"contract_failure,omitempty"` } -// ReadinessAudit mirrors hostaudit.ReadinessAudit with stable snake_case keys. -type ReadinessAudit struct { - Result string `json:"result"` - Recovered bool `json:"recovered,omitempty"` - MissingProjectChecks int `json:"missing_project_checks,omitempty"` - IncompleteTodos int `json:"incomplete_todos,omitempty"` - MissingAcceptanceCriteria int `json:"missing_acceptance_criteria,omitempty"` - MissingVerification int `json:"missing_verification,omitempty"` - MissingStructuredReview int `json:"missing_structured_review,omitempty"` - MissingPathInspection int `json:"missing_path_inspection,omitempty"` - MissingSignoff int `json:"missing_signoff,omitempty"` - MissingMutation int `json:"missing_mutation,omitempty"` - MissingCapabilities int `json:"missing_capabilities,omitempty"` - MissingRender int `json:"missing_render,omitempty"` -} - // Recorder is an event.Sink decorator: every event (and optional-capability // audit) is appended as one JSONL record, then forwarded to the inner sink. // Recording failures never block forwarding — the first error is kept and @@ -374,24 +357,6 @@ func (r *Recorder) RecordDelegationAudit(a hostaudit.DelegationAudit) { event.RecordDelegationAudit(r.inner, a) } -func (r *Recorder) RecordReadinessAudit(a hostaudit.ReadinessAudit) { - r.append(Record{ReadinessAudit: &ReadinessAudit{ - Result: string(a.Result), - Recovered: a.Recovered, - MissingProjectChecks: a.MissingProjectChecks, - IncompleteTodos: a.IncompleteTodos, - MissingAcceptanceCriteria: a.MissingAcceptanceCriteria, - MissingVerification: a.MissingVerification, - MissingStructuredReview: a.MissingStructuredReview, - MissingPathInspection: a.MissingPathInspection, - MissingSignoff: a.MissingSignoff, - MissingMutation: a.MissingMutation, - MissingCapabilities: a.MissingCapabilities, - MissingRender: a.MissingRender, - }}) - event.RecordReadinessAudit(r.inner, a) -} - func (r *Recorder) RecordSubagentHandoff(a event.SubagentHandoffAudit) { r.append(Record{SubagentHandoff: &SubagentHandoff{ Entrance: a.Entrance, @@ -417,103 +382,6 @@ func (r *Recorder) RecordSubagentHandoff(a event.SubagentHandoffAudit) { event.RecordSubagentHandoff(r.inner, a) } -func (r *Recorder) RecordProjectCheckProbe(p event.ProjectCheckProbe) { - rec := &ProjectCheckProbe{ - Declared: p.Declared, - Baseline: p.Baseline, - LegacyBlocked: p.LegacyBlocked, - CandidateBlocked: p.CandidateBlocked, - AgreedMissing: p.AgreedMissing, - LegacyAfter: p.LegacyAfter, - CandidateAfter: p.CandidateAfter, - } - for _, d := range p.Diffs { - rec.Diffs = append(rec.Diffs, ProjectCheckDiff{Identity: d.Identity, Class: d.Class}) - } - r.append(Record{ProjectCheck: rec}) - event.RecordProjectCheckProbe(r.inner, p) -} - -func (r *Recorder) RecordContractShadow(a event.ContractShadowAudit) { - r.append(Record{ContractShadow: &ContractShadowAudit{ - Intent: a.Intent, - Requirements: a.Requirements, - RequirementsSatisfied: a.RequirementsSatisfied, - Checks: a.Checks, - ChecksSatisfied: a.ChecksSatisfied, - Epoch: a.Epoch, - Verdict: a.Verdict, - Complete: a.Complete, - ReadyToFinalize: a.ReadyToFinalize, - }}) - event.RecordContractShadow(r.inner, a) -} - -func (r *Recorder) RecordEvidenceBundle(a event.EvidenceBundleAudit) { - r.append(Record{EvidenceBundle: &EvidenceBundle{ - Sealed: a.Sealed, - Record: a.Record, - Generation: a.Generation, - Integrity: a.Integrity, - Receipts: a.Receipts, - FailureCode: a.FailureCode, - - SnapshotComplete: a.SnapshotComplete, - UnobservedCompared: a.UnobservedCompared, - UnobservedChanges: a.UnobservedChanges, - - Outcome: a.Outcome, - DivergenceClass: a.DivergenceClass, - DivergenceReasons: a.DivergenceReasons, - - ContractRevision: a.ContractRevision, - ContractDecision: a.ContractDecision, - ContractFailure: a.ContractFailure, - }}) - event.RecordEvidenceBundle(r.inner, a) -} - -func (r *Recorder) RecordCompletionReport(a event.CompletionReportAudit) { - r.append(Record{CompletionReport: &CompletionReport{ - Verdict: a.Verdict, - Risk: a.Risk, - Criteria: a.Criteria, - CriteriaSatisfied: a.CriteriaSatisfied, - Changes: a.Changes, - ChangesUnreviewed: a.ChangesUnreviewed, - Verifications: a.Verifications, - VerificationsFailed: a.VerificationsFailed, - VerificationsStale: a.VerificationsStale, - VerificationsInconclusive: a.VerificationsInconclusive, - Gaps: a.Gaps, - GapKinds: a.GapKinds, - ClaimsVerified: a.ClaimsVerified, - ClaimsUnbacked: a.ClaimsUnbacked, - OriginRecorded: a.OriginRecorded, - OriginMatched: a.OriginMatched, - }}) - event.RecordCompletionReport(r.inner, a) -} - -func (r *Recorder) RecordOutcomeProgress(sample hostaudit.OutcomeSample) { - r.append(Record{OutcomeProgress: &OutcomeProgress{ - Round: sample.Round, - Exploration: sample.Exploration, - Verification: sample.Verification, - Objective: sample.Objective, - Regression: sample.Regression, - Churn: sample.Churn, - LegacyGain: sample.LegacyGain, - Discriminating: sample.Discriminating, - DebtAge: sample.DebtAge, - BlindMutations: sample.BlindMutations, - Stall: sample.Stall, - StallAge: sample.StallAge, - StallMutations: sample.StallMutations, - }}) - event.RecordOutcomeProgress(r.inner, sample) -} - func (r *Recorder) RecordMemoryRecall(a event.MemoryRecallAudit) { rec := &MemoryRecall{ CharBudget: a.CharBudget, UsedChars: a.UsedChars, Omitted: a.Omitted, @@ -541,11 +409,6 @@ func (r *Recorder) RecordRunHeader(h RunHeader) { r.append(Record{RunHeader: &h}) } -func (r *Recorder) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - r.append(Record{ProtocolRecovery: string(a.Kind), RecoveryChild: a.ChildID}) - event.RecordProtocolRecovery(r.inner, a) -} - func (r *Recorder) RecordTurnCompletion() { r.append(Record{TurnCompletion: true}) event.RecordTurnCompletion(r.inner) diff --git a/internal/state/trajectory/recorder_test.go b/internal/state/trajectory/recorder_test.go index cc666d2dba..d826792078 100644 --- a/internal/state/trajectory/recorder_test.go +++ b/internal/state/trajectory/recorder_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "os" "path/filepath" - "reasonix/internal/contract/hostaudit" "strings" "testing" "time" @@ -14,29 +13,12 @@ import ( ) type capabilitySink struct { - events []event.Event - readiness []hostaudit.ReadinessAudit - recoveries []event.ProtocolRecoveryAudit - outcomes []hostaudit.OutcomeSample - reports []event.CompletionReportAudit - turns int + events []event.Event + turns int } -func (s *capabilitySink) Emit(e event.Event) { s.events = append(s.events, e) } -func (s *capabilitySink) RecordReadinessAudit(a hostaudit.ReadinessAudit) { - s.readiness = append(s.readiness, a) -} -func (s *capabilitySink) RecordProtocolRecovery(a event.ProtocolRecoveryAudit) { - s.recoveries = append(s.recoveries, a) -} +func (s *capabilitySink) Emit(e event.Event) { s.events = append(s.events, e) } func (s *capabilitySink) RecordTurnCompletion() { s.turns++ } -func (s *capabilitySink) RecordOutcomeProgress(sample hostaudit.OutcomeSample) { - s.outcomes = append(s.outcomes, sample) -} -func (s *capabilitySink) RecordCompletionReport(a event.CompletionReportAudit) { - s.reports = append(s.reports, a) -} - func readRecords(t *testing.T, path string) []Record { t.Helper() data, err := os.ReadFile(path) @@ -117,38 +99,14 @@ func TestRecorderRecordsAndForwardsOptionalCapabilities(t *testing.T) { t.Fatalf("New: %v", err) } - r.RecordReadinessAudit(hostaudit.ReadinessAudit{Result: hostaudit.ReadinessBlocked, MissingVerification: 2}) - r.RecordProtocolRecovery(event.ProtocolRecoveryAudit{Kind: event.ProtocolRecoveryKind("test")}) r.RecordTurnCompletion() - r.RecordOutcomeProgress(hostaudit.OutcomeSample{Round: 3, Exploration: 2, Objective: 1, LegacyGain: 4}) - r.RecordCompletionReport(event.CompletionReportAudit{ - Verdict: "partial", Changes: 2, ChangesUnreviewed: 1, Gaps: 1, GapKinds: []string{"unreviewed_change"}, - }) if err := r.Close(); err != nil { t.Fatalf("Close: %v", err) } recs := readRecords(t, path) - if len(recs) != 5 { - t.Fatalf("got %d records, want 5", len(recs)) - } - if recs[0].ReadinessAudit == nil || recs[0].ReadinessAudit.Result != "blocked" || recs[0].ReadinessAudit.MissingVerification != 2 { - t.Errorf("readiness record = %+v", recs[0].ReadinessAudit) - } - if recs[1].ProtocolRecovery != string(event.ProtocolRecoveryKind("test")) { - t.Errorf("protocol recovery record = %q", recs[1].ProtocolRecovery) - } - if !recs[2].TurnCompletion { - t.Errorf("turn completion record = %+v", recs[2]) - } - if recs[3].OutcomeProgress == nil || recs[3].OutcomeProgress.Round != 3 || recs[3].OutcomeProgress.Objective != 1 || recs[3].OutcomeProgress.LegacyGain != 4 { - t.Errorf("outcome progress record = %+v", recs[3].OutcomeProgress) - } - if rec := recs[4].CompletionReport; rec == nil || rec.Verdict != "partial" || rec.ChangesUnreviewed != 1 || len(rec.GapKinds) != 1 { - t.Errorf("completion report record = %+v", recs[4].CompletionReport) - } - if len(inner.readiness) != 1 || len(inner.recoveries) != 1 || inner.turns != 1 || len(inner.outcomes) != 1 || len(inner.reports) != 1 { - t.Errorf("inner capabilities = %d/%d/%d/%d/%d, want 1/1/1/1/1", len(inner.readiness), len(inner.recoveries), inner.turns, len(inner.outcomes), len(inner.reports)) + if len(recs) != 1 || !recs[0].TurnCompletion { + t.Fatalf("turn completion records = %+v", recs) } } diff --git a/tools/repolint/baseline.json b/tools/repolint/baseline.json index 71ec1398bf..f5b67f35b4 100644 --- a/tools/repolint/baseline.json +++ b/tools/repolint/baseline.json @@ -13,7 +13,7 @@ "doc-prose": 2128, "error-text": 9, "essay": 2579, - "file-size": 18427, + "file-size": 18416, "flat-view": 0, "frontend-parity": 13, "function-size": 2485, @@ -671,9 +671,6 @@ "internal/frontend/acp/session_config.go": { "essay": 6 }, - "internal/frontend/acp/status.go": { - "file-size": 11 - }, "internal/frontend/acp/switch_recovery_test.go": { "essay": 7 },