mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
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:
1 parent
8c785fa4b0
commit
3d0fe85e7e
65 files changed
+61
-1991
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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)}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 ()
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user