返回 DeepSeek-Reasonix
types.go
根目录 / internal / recovery / types.go
1 package recovery
2
3 import (
4 "encoding/json"
5 "time"
6
7 "reasonix/internal/agent"
8 "reasonix/internal/event"
9 )
10
11 // Phase is a derived view of recovery progress for compatibility snapshots.
12 // Runtime truth is "has failure" and "has waiter", not a stored phase enum.
13 type Phase string
14
15 const (
16 PhaseIdle Phase = "idle"
17 PhaseDiagnosing Phase = "diagnosing"
18 PhaseAwaitingDecision Phase = "awaiting_decision"
19 )
20
21 // ChangeKind classifies how the proposed recovery action differs from the
22 // original approach.
23 type ChangeKind string
24
25 const (
26 ChangeSameStrategy ChangeKind = "same_strategy"
27 ChangeStrategy ChangeKind = "strategy"
28 ChangeScope ChangeKind = "scope"
29 ChangeRisk ChangeKind = "risk"
30 ChangeUncertain ChangeKind = "uncertain"
31 )
32
33 // ReviewOutcome is the independent recovery reviewer's decision.
34 type ReviewOutcome string
35
36 const (
37 ReviewContinue ReviewOutcome = "continue"
38 ReviewConfirm ReviewOutcome = "confirm"
39 )
40
41 // FailureClass separates execution reliability from permission and product
42 // decisions. Permission/sandbox/user blocks never become FailureEvents.
43 type FailureClass string
44
45 const (
46 FailureClassExecution FailureClass = "execution"
47 FailureClassMutation FailureClass = "mutation"
48 FailureClassTransient FailureClass = "transient"
49 FailureClassVerification FailureClass = "verification"
50 )
51
52 // ReviewVerdict is the strict JSON shape the recovery reviewer must produce.
53 // Host already knows failure/diagnosis/proposed action; only outcome fields are
54 // required. Extra fields from older models are tolerated on parse.
55 type ReviewVerdict struct {
56 Outcome ReviewOutcome `json:"outcome"`
57 ChangeKind ChangeKind `json:"change_kind"`
58 Rationale string `json:"rationale"`
59
60 // Legacy optional fields kept for older model outputs and tests.
61 FailureSummary string `json:"failure_summary,omitempty"`
62 Diagnosis string `json:"diagnosis,omitempty"`
63 ProposedAction string `json:"proposed_action,omitempty"`
64 }
65
66 // FailureEvent records failure evidence for diagnosis and reviewer context.
67 // SafeRetryLeft/RepeatCount/DiagnosisNotes remain on the wire for old
68 // snapshots; runtime budgets live on taskRuntime.
69 type FailureEvent struct {
70 Class FailureClass `json:"class,omitempty"`
71 Tool string `json:"tool"`
72 ArgsSummary string `json:"args_summary,omitempty"`
73 Subject string `json:"subject,omitempty"`
74 ErrSummary string `json:"err_summary,omitempty"`
75 OutputExcerpt string `json:"output_excerpt,omitempty"`
76 SourceAgent string `json:"source_agent,omitempty"`
77 TaskID string `json:"task_id,omitempty"`
78 // TaskScopeID persists only stable goal scopes. Ordinary turn scopes are
79 // runtime-local and intentionally omitted so a restart cannot revive a stale
80 // technical latch for a new user turn. It never acts as the Episode budget key.
81 TaskScopeID string `json:"task_scope_id,omitempty"`
82 ReadOnly bool `json:"read_only,omitempty"`
83 Verification bool `json:"verification,omitempty"`
84 Mutates bool `json:"mutates,omitempty"`
85 RepeatCount int `json:"repeat_count,omitempty"`
86 CreatedAt time.Time `json:"created_at,omitempty"`
87 Args json.RawMessage `json:"args,omitempty"`
88 Fingerprint string `json:"fingerprint,omitempty"`
89 SafeRetryLeft int `json:"safe_retry_left,omitempty"`
90 DiagnosisNotes []string `json:"diagnosis_notes,omitempty"`
91 }
92
93 // PendingProposal is the mutation paused for user confirmation.
94 // It is held only in the temporary waiter table, never as durable task state.
95 type PendingProposal struct {
96 Tool string `json:"tool"`
97 Subject string `json:"subject,omitempty"`
98 Preview string `json:"preview,omitempty"`
99 Args json.RawMessage `json:"args,omitempty"`
100 Fingerprint string `json:"fingerprint,omitempty"`
101 SourceAgent string `json:"source_agent,omitempty"`
102 ChangeKind ChangeKind `json:"change_kind,omitempty"`
103 Rationale string `json:"rationale,omitempty"`
104 Diagnosis string `json:"diagnosis,omitempty"`
105 Failure string `json:"failure,omitempty"`
106 Proposed string `json:"proposed,omitempty"`
107 PlanBefore string `json:"plan_before,omitempty"`
108 PlanAfter string `json:"plan_after,omitempty"`
109 // TaskGrant fields are transient host-classified scope. They are deliberately
110 // omitted from snapshots and never supplied by the model or wire client.
111 TaskGrantKey string `json:"-"`
112 TaskGrantTaskScope string `json:"-"`
113 TaskGrantDisplay string `json:"-"`
114 }
115
116 // TaskState is the persistable / debug view of one task's recovery state.
117 // Runtime truth is taskRuntime; Snapshot/Restore project to and from this shape.
118 //
119 // Persistence projection writes only LastFailure as historical evidence.
120 // Failure / ConsecutiveFails / ReviewBlocks may still appear in live Snapshot()
121 // for debugging, and old on-disk values are migrated to evidence without re-arming.
122 type TaskState struct {
123 Phase Phase `json:"phase"`
124 Failure *FailureEvent `json:"failure,omitempty"`
125 LastFailure *FailureEvent `json:"last_failure,omitempty"`
126 Pending *PendingProposal `json:"pending,omitempty"`
127 ApprovalID string `json:"approval_id,omitempty"`
128 ConsecutiveFails int `json:"consecutive_fails,omitempty"`
129 ReviewBlocks int `json:"review_blocks,omitempty"`
130 TailInjected bool `json:"tail_injected,omitempty"`
131 // EpisodeID is runtime/debug only and never written by persistence projection.
132 EpisodeID string `json:"episode_id,omitempty"`
133 // EpisodeStopped / StopReason are live debug views only.
134 EpisodeStopped bool `json:"episode_stopped,omitempty"`
135 StopReason string `json:"stop_reason,omitempty"`
136 }
137
138 // Snapshot is the form of all task recovery state.
139 // Live Snapshot() includes debug fields; PersistenceSnapshot() strips temporary
140 // lock/budget state so disk never re-arms Auto blocks after restart.
141 type Snapshot struct {
142 Tasks map[string]*TaskState `json:"tasks,omitempty"`
143 }
144
145 // Metrics are content-free counters for release observation.
146 // They never record parameters, paths, or error bodies.
147 type Metrics struct {
148 FailureEvents int64
149 RuleContinues int64
150 ReviewContinues int64
151 HumanPrompts int64
152 HumanContinues int64
153 TaskGrantContinues int64
154 TaskGrantUses int64
155 HumanRevises int64
156 ReviewErrors int64
157 ReviewLatencyMsSum int64
158 ReviewLatencyCount int64
159 RepeatPrompts int64
160
161 // Episode / generation counters (content-free).
162 OperationStops int64
163 EpisodeFailureStops int64
164 ReviewStops int64
165 StoppedOpRetryStops int64
166 ModeResets int64
167 EpisodeRotations int64
168 StaleObservationsIgnored int64
169 }
170
171 // ApprovalKindRecovery is the Approval.Kind value for recovery cards.
172 const ApprovalKindRecovery = "recovery"
173
174 // ApprovalKindTool and ApprovalKindPlan keep ordinary approval kinds explicit.
175 const (
176 ApprovalKindTool = "tool"
177 ApprovalKindPlan = "plan"
178 )
179
180 // ToEventApproval builds the event payload for a recovery confirmation card.
181 func ToEventApproval(id string, pending PendingProposal, failure *FailureEvent) event.Approval {
182 rec := &event.RecoveryApproval{
183 SourceAgent: pending.SourceAgent,
184 FailedTool: "",
185 FailedSummary: pending.Failure,
186 Diagnosis: pending.Diagnosis,
187 NextTool: pending.Tool,
188 NextAction: firstNonEmpty(pending.Proposed, pending.Subject, pending.Preview),
189 ChangeKind: string(pending.ChangeKind),
190 ChangeRationale: pending.Rationale,
191 ReviewRationale: pending.Rationale,
192 PlanBefore: pending.PlanBefore,
193 PlanAfter: pending.PlanAfter,
194 CanGrantTask: pending.TaskGrantKey != "",
195 TaskGrantScope: pending.TaskGrantDisplay,
196 }
197 if failure != nil {
198 rec.FailedTool = failure.Tool
199 if rec.FailedSummary == "" {
200 rec.FailedSummary = failure.ErrSummary
201 }
202 if rec.SourceAgent == "" {
203 rec.SourceAgent = failure.SourceAgent
204 }
205 }
206 subject := firstNonEmpty(pending.Subject, pending.Preview, pending.Tool)
207 reason := firstNonEmpty(pending.Rationale, pending.Diagnosis, "Plan change requires confirmation")
208 return event.Approval{
209 ID: id,
210 Tool: pending.Tool,
211 Subject: subject,
212 Reason: reason,
213 Fresh: true,
214 Kind: ApprovalKindRecovery,
215 Recovery: rec,
216 }
217 }
218
219 // Observation aliases keep call sites readable when bridging agent types.
220 type Observation = agent.RecoveryObservation
221 type Proposal = agent.RecoveryProposal
222 type Decision = agent.RecoveryDecision
223 type Action = agent.RecoveryAction
224
225 const (
226 ActionContinue = agent.RecoveryActionContinue
227 ActionContinueTask = agent.RecoveryActionContinueTask
228 ActionRevise = agent.RecoveryActionRevise
229 )
230
231 // DefaultReviseFeedback is injected when the user chooses "try another approach"
232 // without optional free-text feedback.
233 const DefaultReviseFeedback = "The pending mutation was rejected. Do not retry the same action. Summarize the failure cause, narrow the scope, and propose a safer alternative before attempting another mutation."
234
235 func firstNonEmpty(vals ...string) string {
236 for _, v := range vals {
237 if v != "" {
238 return v
239 }
240 }
241 return ""
242 }
243
243 lines GO