refactor(event): remove the readiness and audit channels nothing sends to

With the final-readiness gate gone, FinalReadinessError, the readiness audit
and the structured readiness on TurnDone have no producer, and neither do the
contract-shadow, completion-report, evidence-bundle, outcome-progress,
protocol-recovery, verification-drift and project-check-probe audits. Removes
the types, the sink capabilities and their forwarding in every wrapper, the
trajectory and metrics fields, the final_readiness outcome and notice code,
and the delivery-recovery submit port.

The ACP status shape and the desktop reader of old sessions keep their
readiness fields: external editors and recorded sessions still carry them.
This commit is contained in:
esengine committed 2026-11-02 05:59:36 -08:00
1 parent 8c785fa4b0
commit 3d0fe85e7e
65 files changed
+61 -1991

No files matched your search

+2 -2
View File
@@ -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)
}
-2
View File
@@ -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"`
+2 -2
View File
@@ -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;
@@ -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),
+1 -6
View File
@@ -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
}
@@ -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"))
@@ -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)
}
@@ -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)
}
@@ -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)
-56
View File
@@ -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()
+2 -19
View File
@@ -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)
}
}
-18
View File
@@ -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
-39
View File
@@ -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) {
-1
View File
@@ -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"
@@ -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)
}
}
-182
View File
@@ -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)
}
}
@@ -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
}
@@ -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)
}
}
-64
View File
@@ -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()
@@ -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"})
@@ -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)
}
}
-9
View File
@@ -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"`
+2 -19
View File
@@ -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)
@@ -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
}
-55
View File
@@ -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{
-16
View File
@@ -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)}
}
-13
View File
@@ -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)}
+1 -20
View File
@@ -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()
+2 -5
View File
@@ -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),
@@ -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)
}
}
+4 -93
View File
@@ -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,
-113
View File
@@ -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.
+1 -5
View File
@@ -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,
})
-11
View File
@@ -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.
+1 -21
View File
@@ -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
-3
View File
@@ -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
@@ -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})
-5
View File
@@ -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:
+11 -24
View File
@@ -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)
}
}
@@ -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
-22
View File
@@ -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
-9
View File
@@ -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, ""},
@@ -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 {
@@ -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)
}
}
@@ -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)
}
}
})
}
}
+2 -9
View File
@@ -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
@@ -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
}
@@ -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 ()
@@ -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 ()
+2 -19
View File
@@ -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
@@ -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:
@@ -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)
}
}
@@ -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")
-24
View File
@@ -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)
}
@@ -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++ }
@@ -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)
}
}
-1
View File
@@ -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)
-4
View File
@@ -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.
-5
View File
@@ -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
-10
View File
@@ -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.
+2 -11
View File
@@ -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,
+3 -3
View File
@@ -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)
-137
View File
@@ -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)
+5 -47
View File
@@ -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)
}
}
+1 -4
View File
@@ -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
},