返回 DeepSeek-Reasonix
decision.go
根目录 / internal / recovery / decision.go
1 package recovery
2
3 // Route is the pure decision outcome for a proposed action.
4 type Route int
5
6 const (
7 // RouteBypass leaves the call to the ordinary Ask/YOLO approval path.
8 RouteBypass Route = iota
9 // RouteAllow lets Auto execute without a human card or reviewer call.
10 RouteAllow
11 // RouteReview hands an ambiguous recovery mutation to the isolated reviewer.
12 RouteReview
13 // RouteStop blocks one exact operation after repeated technical failure.
14 // Other operations in the same Episode may still proceed.
15 RouteStop
16 // RouteStopTurn blocks further execution after an Episode-level hard limit.
17 // Host-proven read-only diagnosis remains available.
18 RouteStopTurn
19 )
20
21 // String returns a stable route name for tests and diagnostics.
22 func (r Route) String() string {
23 switch r {
24 case RouteBypass:
25 return "bypass"
26 case RouteAllow:
27 return "allow"
28 case RouteReview:
29 return "review"
30 case RouteStop:
31 return "stop"
32 case RouteStopTurn:
33 return "stop_turn"
34 default:
35 return "unknown"
36 }
37 }
38
39 // Facts are the host-observed inputs for the pure decision engine.
40 // The engine never locks, calls a model, shows UI, or mutates state.
41 type Facts struct {
42 // AutoMode is true only when tool-approval mode is Auto.
43 AutoMode bool
44
45 // Proposal classification.
46 ReadOnly bool
47 Mutates bool
48 Verification bool
49 HighRisk bool
50 // PlanTransition is a host-observed structural rewrite of an active plan.
51 PlanTransition bool
52
53 // Active failure context (zero values when none).
54 HasActiveFailure bool
55 SameFailedOperation bool
56 ExpandedScope bool
57 StrategyChanged bool
58 SafeRetryAvailable bool
59 // FailureCount is the exact-operation failure count (1 = first failure).
60 FailureCount uint8
61 // EpisodeFailureCount is the Task's total qualifying failures since last
62 // real progress inside the current Episode.
63 EpisodeFailureCount uint8
64 // ReviewRejects is the cumulative reviewer rejection count for the Episode.
65 ReviewRejects uint8
66 // OperationAlreadyStopped is true when this exact operation already hit its
67 // per-operation limit earlier in the Episode.
68 OperationAlreadyStopped bool
69 // EpisodeStopped is true when a previous decision already exhausted the
70 // Episode for this Task.
71 EpisodeStopped bool
72 // StopReason is the reason the Episode was stopped (when EpisodeStopped).
73 StopReason StopReason
74 }
75
76 // DecisionResult is the pure routing result.
77 type DecisionResult struct {
78 Route Route
79 // ConsumeSafeRetry is set when RouteAllow was chosen because this is the
80 // first safe verification retry; the coordinator must spend the budget.
81 ConsumeSafeRetry bool
82 // StopReason is set for RouteStop / RouteStopTurn.
83 StopReason StopReason
84 }
85
86 // Decide is the pure Auto Guard decision engine.
87 //
88 // Order is fixed by product policy:
89 // 1. non-Auto → bypass ordinary approval
90 // 2. Episode already stopped → allow read-only diagnosis, stop execution
91 // 3. structured plan transition → reviewer
92 // 4. read-only diagnosis → allow
93 // 5. no active failure → allow ordinary mutations
94 // 6. operations other than the exact failed operation → allow
95 // 7. first safe verification retry → allow (+ consume budget)
96 // 8. already-stopped operation re-proposal is handled by the gate (retries)
97 // 9. three consecutive failures of the same operation → stop that operation
98 // 10. remaining exact-operation retries → reviewer
99 //
100 // Episode-level totals (6 failures / 3 review rejects / 3 stopped-op retries)
101 // are enforced by the gate before or after Decide; Decide focuses on pure
102 // routing of one proposal given Facts.
103 func Decide(f Facts) DecisionResult {
104 if !f.AutoMode {
105 return DecisionResult{Route: RouteBypass}
106 }
107 if f.EpisodeStopped {
108 if f.ReadOnly && !f.Mutates && !f.Verification && !f.PlanTransition {
109 return DecisionResult{Route: RouteAllow}
110 }
111 reason := f.StopReason
112 if reason == StopReasonNone {
113 reason = StopReasonEpisodeFailures
114 }
115 return DecisionResult{Route: RouteStopTurn, StopReason: reason}
116 }
117 if f.PlanTransition {
118 return DecisionResult{Route: RouteReview}
119 }
120 // Non-mutating, non-verification calls (and host-proven read-only tools)
121 // always continue so diagnosis can proceed without cards.
122 if f.ReadOnly && !f.Mutates {
123 return DecisionResult{Route: RouteAllow}
124 }
125 if !f.Mutates && !f.Verification {
126 return DecisionResult{Route: RouteAllow}
127 }
128 if !f.HasActiveFailure {
129 return DecisionResult{Route: RouteAllow}
130 }
131 if f.SafeRetryAvailable {
132 return DecisionResult{Route: RouteAllow, ConsumeSafeRetry: true}
133 }
134 // A failure is an execution-reliability signal, not a task-wide safety
135 // boundary. Keep unrelated work on the zero-confirmation path; permission,
136 // sandbox, and the Episode ceiling still apply independently.
137 if !f.SameFailedOperation {
138 return DecisionResult{Route: RouteAllow}
139 }
140 // Already-stopped exact operation: gate escalates retries; Decide stops the
141 // operation so the agent cannot re-run it.
142 if f.OperationAlreadyStopped && f.SameFailedOperation {
143 return DecisionResult{Route: RouteStop, StopReason: StopReasonOperationFailures}
144 }
145 // Repeated technical failure of the same exact operation is not a
146 // user-owned product decision. Stop only that operation.
147 if f.FailureCount >= MaxOperationFailures && f.SameFailedOperation {
148 return DecisionResult{Route: RouteStop, StopReason: StopReasonOperationFailures}
149 }
150 return DecisionResult{Route: RouteReview}
151 }
152
152 lines GO