返回 DeepSeek-Reasonix
missing_reasoning_watch.go
根目录 / internal / agent / missing_reasoning_watch.go
1 package agent
2
3 import (
4 "time"
5
6 "reasonix/internal/provider"
7 )
8
9 // missingReasoningWatch is this conversation's live view of one incident. Only
10 // observeMissingToolCallReasoning moves these, and they belong to the session:
11 // replacing the conversation ends the incident being watched.
12 type missingReasoningWatch struct {
13 active bool // gates the one automatic retry, not a user-visible warning
14 stateRecorded bool // avoids a file transaction on every healthy tool-call turn
15 healthyStreak int // anti-flapping when no cross-process state dir is configured
16 }
17
18 // unwrittenResolve is a resolve whose state write failed. It answers to the
19 // provider configuration rather than to any conversation, so it sits beside
20 // missingReasoningWarnState instead of in sessionRuntime — a new session
21 // inherits the debt because the retry it owes is still owed.
22 type unwrittenResolve struct {
23 at time.Time
24 }
25
26 // observeMissingToolCallReasoning classifies a thinking-mode tool-call turn and
27 // claims one recovery for a strict replay contract. Compatible protocols bypass
28 // this incident state; healthy strict rounds eventually re-arm recovery.
29 func (a *Agent) observeMissingToolCallReasoning(calls []provider.ToolCall, reasoning string) (missing, shouldRetry bool) {
30 return a.observeMissingAssistantReasoning(provider.Message{
31 Role: provider.RoleAssistant, ToolCalls: calls, ReasoningContent: reasoning,
32 }, true)
33 }
34
35 // observeMissingAssistantReasoning extends the legacy tool-call watcher to
36 // provider-executed tool activity while preserving its persisted incident and
37 // anti-flapping behavior.
38 func (a *Agent) observeMissingAssistantReasoning(message provider.Message, complete bool) (missing, shouldRetry bool) {
39 if provider.AllowsEmptyReasoningFallback(a.svc.prov) || !provider.RequiresAssistantReasoningReplay(a.svc.prov, message) {
40 return false, false
41 }
42 decision := provider.DecideReasoningReplay(a.svc.prov, message, complete)
43 replayable := decision == provider.ReplayDirect || decision == provider.ReplayCompatible
44 // Strict contracts retain their incident across manual continuation.
45 if !provider.WarnOnMissingToolCallReasoning(a.svc.prov) {
46 if replayable {
47 a.recordHealthyAssistantReasoning(provider.MissingToolCallReasoningWarningFingerprint(a.svc.prov), time.Now())
48 }
49 if !replayable && !provider.AllowsEmptyReasoningFallback(a.svc.prov) {
50 return true, a.claimMissingReasoningIncident(time.Now())
51 }
52 return false, false
53 }
54 fingerprint := provider.MissingToolCallReasoningWarningFingerprint(a.svc.prov)
55 observedAt := time.Now()
56 if replayable {
57 a.recordHealthyAssistantReasoning(fingerprint, observedAt)
58 return false, false
59 }
60 a.sess.missingReasoning.healthyStreak = 0
61 if s := a.svc.warnState; s != nil {
62 stateReady := true
63 alreadyActive := a.sess.missingReasoning.active
64 if pending := a.unwrittenResolve.at; !pending.IsZero() {
65 result := s.resolveAt(fingerprint, pending)
66 stateReady = result.Recorded
67 if result.Recorded {
68 a.unwrittenResolve.at = time.Time{}
69 if result.Resolved {
70 alreadyActive = false
71 a.sess.missingReasoning.active = false
72 }
73 }
74 }
75 claimed := stateReady && s.claimAt(fingerprint, observedAt)
76 if !claimed || alreadyActive {
77 // This exact configuration already attempted recovery for the active
78 // incident, so do not grant another regeneration.
79 a.sess.missingReasoning.active = true
80 a.sess.missingReasoning.stateRecorded = true
81 return true, false
82 }
83 if !stateReady {
84 a.sess.missingReasoning.stateRecorded = false
85 }
86 } else if a.sess.missingReasoning.active {
87 return true, false
88 }
89 a.sess.missingReasoning.active = true
90 if a.unwrittenResolve.at.IsZero() {
91 a.sess.missingReasoning.stateRecorded = true
92 }
93 return true, true
94 }
95
96 func (a *Agent) recordHealthyAssistantReasoning(fingerprint string, observedAt time.Time) {
97 if a.svc.warnState == nil {
98 if a.sess.missingReasoning.active {
99 a.sess.missingReasoning.healthyStreak++
100 if a.sess.missingReasoning.healthyStreak >= missingReasoningHealthyResolveStreak {
101 a.sess.missingReasoning.active = false
102 a.sess.missingReasoning.healthyStreak = 0
103 }
104 }
105 return
106 }
107 if a.sess.missingReasoning.stateRecorded && !a.sess.missingReasoning.active {
108 return
109 }
110 result := a.resolveHealthyAssistantReasoning(fingerprint, observedAt)
111 if !result.Recorded {
112 if observedAt.After(a.unwrittenResolve.at) {
113 a.unwrittenResolve.at = observedAt
114 }
115 a.sess.missingReasoning.active = true
116 a.sess.missingReasoning.stateRecorded = false
117 return
118 }
119 if result.Resolved {
120 a.sess.missingReasoning.active = false
121 a.sess.missingReasoning.stateRecorded = true
122 return
123 }
124 a.sess.missingReasoning.active = true
125 a.sess.missingReasoning.stateRecorded = false
126 }
127
128 func (a *Agent) resolveHealthyAssistantReasoning(fingerprint string, observedAt time.Time) missingReasoningResolveResult {
129 if pending := a.unwrittenResolve.at; !pending.IsZero() {
130 result := a.svc.warnState.resolveAt(fingerprint, pending)
131 if !result.Recorded {
132 return result
133 }
134 a.unwrittenResolve.at = time.Time{}
135 }
136 return a.svc.warnState.resolveAt(fingerprint, observedAt)
137 }
138
139 func (a *Agent) claimMissingReasoningIncident(observedAt time.Time) bool {
140 a.sess.missingReasoning.healthyStreak = 0
141 if s := a.svc.warnState; s != nil {
142 fingerprint := provider.MissingToolCallReasoningWarningFingerprint(a.svc.prov)
143 claimed := s.claimAt(fingerprint, observedAt)
144 a.sess.missingReasoning.active = true
145 a.sess.missingReasoning.stateRecorded = true
146 return claimed
147 }
148 if a.sess.missingReasoning.active {
149 return false
150 }
151 a.sess.missingReasoning.active = true
152 a.sess.missingReasoning.stateRecorded = true
153 return true
154 }
155
155 lines GO