返回 DeepSeek-Reasonix
errors.go
根目录 / internal / agent / errors.go
1 package agent
2
3 import (
4 "errors"
5 "fmt"
6 "strings"
7
8 "reasonix/internal/provider"
9 )
10
11 // ReasoningReplayFailure classifies why an assistant turn could not safely be
12 // committed to provider-visible history.
13 type ReasoningReplayFailure string
14
15 const (
16 ReasoningReplayMissing ReasoningReplayFailure = "missing_required_reasoning"
17 ReasoningReplayOverflow ReasoningReplayFailure = "reasoning_overflow"
18 ReasoningReplayIncomplete ReasoningReplayFailure = "incomplete_reasoning"
19 ReasoningReplayUnreplayable ReasoningReplayFailure = "unreplayable_history"
20 )
21
22 // ReasoningReplayError stops client tools before execution when their provider
23 // reasoning cannot be replayed. Completed work is retained as LocalOnly by the
24 // ordinary interrupted-turn recovery path.
25 type ReasoningReplayError struct {
26 Kind ReasoningReplayFailure
27 }
28
29 func (e *ReasoningReplayError) Error() string {
30 if e != nil && e.Kind == ReasoningReplayIncomplete {
31 return "The provider ended the response with unfinished reasoning. Reasonix kept existing work and did not run the requested tools; retry to continue safely."
32 }
33 if e != nil && e.Kind == ReasoningReplayOverflow {
34 return "The provider reasoning exceeded the client safety limit, so Reasonix did not run the requested tools. Existing work was kept; retry to continue safely."
35 }
36 return "The provider repeatedly omitted reasoning required to replay this tool turn. Reasonix exhausted its safe automatic recovery and did not run the requested tools. Existing work was kept; switch provider or protocol if this continues."
37 }
38
39 // PauseClass names the guard that deliberately ended a run, so a host can
40 // classify an outcome without reaching into the unexported pause types.
41 // Empty for ordinary provider/tool failures.
42 func PauseClass(err error) string {
43 var budgetPause *taskBudgetPause
44 if errors.As(err, &budgetPause) {
45 return "task_budget"
46 }
47 var maxSteps *maxStepsPause
48 if errors.As(err, &maxSteps) {
49 return "max_steps"
50 }
51 var readiness *FinalReadinessError
52 if errors.As(err, &readiness) {
53 return "final_readiness"
54 }
55 var recovery *RecoveryPauseError
56 if errors.As(err, &recovery) {
57 return "recovery_paused"
58 }
59 var completion *CompletionUncertainError
60 if errors.As(err, &completion) {
61 return "completion_uncertain"
62 }
63 var incompleteRead *IncompleteReadError
64 if errors.As(err, &incompleteRead) {
65 return "incomplete_read"
66 }
67 return ""
68 }
69
70 // IncompleteReadError is a recoverable run boundary: a read_file result was
71 // only partially visible and the host refused to let the model silently treat
72 // it as complete. It carries only routing/size metadata, never file contents.
73 type IncompleteReadError struct {
74 Pause *provider.ReadPause
75 Reason string
76 Path string
77 ToolCallID string
78 ResultRef string
79 NextOffset int
80 ConsumedBytes int
81 TotalBytes int
82 }
83
84 func (e *IncompleteReadError) Error() string {
85 if e == nil {
86 return "read_file did not complete"
87 }
88 detail := strings.TrimSpace(e.Reason)
89 if detail == "" {
90 detail = "the retained result still has unread content"
91 }
92 return "read_file did not complete safely: " + detail
93 }
94
95 // RunPauseInfo is the stable host-facing description of a deliberate Run
96 // boundary. It keeps unexported control-flow error types private while allowing
97 // Controller to distinguish task budgets from an explicit runtime max_steps.
98 type RunPauseInfo struct {
99 Kind string
100 Limit int
101 Key string
102 HostOwned bool
103 Reason string
104 }
105
106 // InspectRunPause unwraps a deliberate explicit run boundary.
107 func InspectRunPause(err error) (RunPauseInfo, bool) {
108 var maxSteps *maxStepsPause
109 if errors.As(err, &maxSteps) {
110 return RunPauseInfo{Kind: "max_steps", Limit: maxSteps.steps, Key: maxSteps.key}, true
111 }
112 var budget *taskBudgetPause
113 if errors.As(err, &budget) {
114 return RunPauseInfo{Kind: "task_budget", Key: budget.axis, HostOwned: true, Reason: budget.detail}, true
115 }
116 var incompleteRead *IncompleteReadError
117 if errors.As(err, &incompleteRead) {
118 return RunPauseInfo{Kind: "incomplete_read", HostOwned: true, Reason: incompleteRead.Reason}, true
119 }
120 return RunPauseInfo{}, false
121 }
122
123 // ReadinessContinuationClass is retained for compatibility with hosts that
124 // inspect FinalReadinessError. Ordinary Standard/Delivery turns never use it
125 // to schedule another model request; only Goal/approved-Plan orchestration may
126 // interpret the advisory class after the visible turn has ended.
127 type ReadinessContinuationClass string
128
129 const (
130 // ReadinessContinuationNone is also the zero value so older callers that
131 // construct FinalReadinessError directly never opt into another model turn.
132 ReadinessContinuationNone ReadinessContinuationClass = ""
133 // ReadinessContinuationGeneric covers ordinary post-write verification and
134 // review gaps for Goal/Plan diagnostics.
135 ReadinessContinuationGeneric ReadinessContinuationClass = "generic"
136 // ReadinessContinuationHighConfidence covers exact or strict, safely
137 // actionable readiness duties for Goal/Plan diagnostics.
138 ReadinessContinuationHighConfidence ReadinessContinuationClass = "high_confidence"
139 )
140
141 // FinalReadinessError reports that the model exhausted its recovery attempts
142 // before satisfying the host-observed delivery checks.
143 type FinalReadinessError struct {
144 Attempts int
145 Reason string
146 Missing []string
147 ContinuationClass ReadinessContinuationClass
148 ProgressKey string
149 // Operations names the concrete changes the host could not settle, so the
150 // report points at a real change with a real next action instead of a
151 // category the user has to map back onto their work themselves.
152 Operations []ReadinessOperationGap
153 }
154
155 // ReadinessOperationGap is one unsettled host-observed change in a readiness
156 // report. Action is the closed-set next step, never prose.
157 type ReadinessOperationGap struct {
158 OperationID string `json:"operation_id"`
159 Paths []string `json:"paths,omitempty"`
160 State string `json:"state"`
161 Action string `json:"action"`
162 }
163
164 func (e *FinalReadinessError) Error() string {
165 if e == nil {
166 return "final-answer readiness failed"
167 }
168 return fmt.Sprintf("final-answer readiness failed %d times: %s", e.Attempts, e.Reason)
169 }
170
171 // RecoveryPauseError reports that Auto recovery exhausted its Episode budget
172 // and the model either summarized or continued calling tools after the one-shot
173 // finalization round. It is a control-flow signal, not a provider failure:
174 // completed work is kept and the user can continue in the next message.
175 type RecoveryPauseError struct {
176 // Message is the user-facing English product copy for wire/CLI clients.
177 Message string
178 // StopReason is an internal classifier; never show it as product copy.
179 StopReason string
180 // Detail is optional expandable diagnostic text (last error / counts).
181 Detail string
182 }
183
184 func (e *RecoveryPauseError) Error() string {
185 if e == nil {
186 return "automatic retries paused"
187 }
188 if strings.TrimSpace(e.Message) != "" {
189 return e.Message
190 }
191 return "Automatic retries paused. Reasonix stopped repeated attempts and kept completed work. Send \"continue\" to start a fresh attempt, or add instructions to change direction."
192 }
193
194 // CompletionUncertainContextTool is the retained host-safety cause for a
195 // context-unavailable tool being called again after the repair instruction.
196 const CompletionUncertainContextTool = "context_tool_repeat"
197
198 // CompletionUncertainError reports that a host safety condition paused the
199 // current turn after completed work was retained. It is a control-flow signal,
200 // not a provider failure: the candidate answer, tool results, and completed
201 // work stay in the session, and the user can continue in the next message.
202 type CompletionUncertainError struct {
203 // Cause is the stable classifier naming why completion stayed unconfirmed.
204 Cause string
205 // Message is the user-facing English product copy for wire/CLI clients.
206 Message string
207 // Detail is optional expandable diagnostic text; never product copy.
208 Detail string
209 }
210
211 func (e *CompletionUncertainError) Error() string {
212 if e == nil {
213 return "completion could not be confirmed"
214 }
215 if strings.TrimSpace(e.Message) != "" {
216 return e.Message
217 }
218 return "Completion could not be confirmed. Reasonix kept the current result and all completed work. Send \"continue\" to resume, or restate what should change."
219 }
220
220 lines GO