返回 DeepSeek-Reasonix
context_receipt.go
根目录 / internal / agent / context_receipt.go
1 package agent
2
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "log/slog"
8 "time"
9
10 "reasonix/internal/event"
11 "reasonix/internal/provider"
12 )
13
14 func (a *Agent) contextMaintenanceInputHash(visible []provider.Message) string {
15 if a == nil {
16 return ""
17 }
18 seed := a.currentPromptCacheKey() + "\n" + providerVisibleFingerprint(modelInputMessages(visible))
19 sum := sha256.Sum256([]byte(seed))
20 return hex.EncodeToString(sum[:])
21 }
22
23 // The same-turn backoff lifts once the changed view outgrows the failed
24 // attempt by this share of the window. Growth is the only signal that a retry
25 // can reclaim more, and it bounds the retries one turn can pay to a handful.
26 const maintenanceRetryGrowthRatio = 0.05
27
28 // contextMaintenanceBlocked reports whether the last receipt still suppresses
29 // automatic maintenance of the view fingerprinted by inputHash. est is the
30 // view's current estimate; zero means the caller has none and keeps the backoff.
31 func (a *Agent) contextMaintenanceBlocked(inputHash string, est int) (bool, string) {
32 if a == nil {
33 return false, ""
34 }
35 a.sess.compactionMu.Lock()
36 defer a.sess.compactionMu.Unlock()
37 r := a.sess.compactionState.LastReceipt
38 if r == nil {
39 // Legacy sidecars may only have BlockedInputHash without a receipt.
40 if a.sess.compactionState.BlockedInputHash != "" &&
41 (inputHash == "" || a.sess.compactionState.BlockedInputHash == inputHash) {
42 return true, a.sess.compactionState.BlockedReason
43 }
44 return false, ""
45 }
46 if r.Status != "blocked" && r.Status != "failed" {
47 return false, ""
48 }
49 reason := firstNonEmpty(a.sess.compactionState.BlockedReason, r.Reason)
50 // A failed view stays blocked for this generation. Changed input may retry
51 // on a later turn, but not once per tool result in the same active turn.
52 if r.BlockedInputHash != "" && inputHash != "" && r.BlockedInputHash != inputHash {
53 turn := a.activeTurnCreatedAt.Load()
54 if turn != 0 && a.sess.compaction.failedTurn.Load() == turn && !a.maintenanceRetryDue(r, est) {
55 return true, reason
56 }
57 return false, ""
58 }
59 return true, reason
60 }
61
62 func (a *Agent) maintenanceRetryDue(r *ContextMaintenanceReceipt, est int) bool {
63 window := a.effectiveContextWindow()
64 if est <= 0 || window <= 0 || r.InputTokens <= 0 {
65 return false
66 }
67 return est >= r.InputTokens+int(float64(window)*maintenanceRetryGrowthRatio)
68 }
69
70 func (a *Agent) emitContextMaintenance(r *ContextMaintenanceReceipt) {
71 if a == nil || r == nil || a.svc.sink == nil {
72 return
73 }
74 a.svc.sink.Emit(event.Event{Kind: event.ContextMaintenanceEvent, Maintenance: &event.ContextMaintenance{
75 Status: r.Status, Action: r.Action, Trigger: r.Trigger, OperationID: r.OperationID,
76 InputTokens: r.InputTokens, ResultTokens: r.ResultTokens, SavedTokens: r.SavedTokens,
77 AffectedToolResults: r.AffectedToolResults, ProjectionVersion: r.ProjectionVersion,
78 CacheBreak: r.CacheBreak, Reason: r.Reason,
79 }})
80 }
81
82 // recordContextMaintenanceBlocked persists a generation-scoped blocked receipt.
83 func (a *Agent) recordContextMaintenanceBlocked(inputHash, trigger, action, reason string) {
84 a.recordContextMaintenanceOutcome(inputHash, trigger, action, "blocked", reason)
85 }
86
87 // recordContextMaintenanceOutcome records blocked or failed for the current
88 // generation. Automatic Prepare will not re-enter summary until the generation
89 // advances (successful install, manual compress, or lineage change).
90 func (a *Agent) recordContextMaintenanceOutcome(inputHash, trigger, action, status, reason string) {
91 if a == nil || a.sess.conversation == nil {
92 return
93 }
94 visible := a.modelVisibleMessages()
95 if inputHash == "" {
96 inputHash = a.contextMaintenanceInputHash(visible)
97 }
98 inputTokens := a.estimatedVisibleRequestTokens(visible)
99 if trigger == "" {
100 trigger = CompactionTriggerPressure
101 }
102 if action == "" {
103 action = "summary"
104 }
105 if status != "failed" {
106 status = "blocked"
107 }
108 _, transcriptVersion := a.sess.conversation.snapshotMessagesVersion()
109 promptCacheKey := a.currentPromptCacheKey()
110 a.sess.compactionMu.Lock()
111 state := a.sess.compactionState
112 previous := state
113 // Suppress only repeated failures of the same view; a failure on a new
114 // view must refresh the stored hash or later retries of that view are
115 // never backed off.
116 if state.LastReceipt != nil &&
117 (state.LastReceipt.Status == "blocked" || state.LastReceipt.Status == "failed") &&
118 state.LastReceipt.Action == action &&
119 state.LastReceipt.BlockedInputHash == inputHash {
120 a.sess.compactionMu.Unlock()
121 return
122 }
123 now := time.Now().UTC()
124 state.SchemaVersion = compactionStateSchemaCurrent
125 state.TranscriptVersion = transcriptVersion
126 state.PromptCacheKey = promptCacheKey
127 // Do not advance projection version on failure; generation still advances so
128 // CAS losers and concurrent writers cannot overwrite a newer success.
129 state.Generation++
130 // LastReceipt carries the blocked signal; clear legacy top-level mirrors.
131 state.BlockedInputHash = ""
132 state.BlockedReason = ""
133 state.LastTrigger = ""
134 state.LastMode = ""
135 state.LastSourceTokens = 0
136 state.LastResultTokens = 0
137 state.LastReceipt = &ContextMaintenanceReceipt{
138 OperationID: fmt.Sprintf("%s-%s-%d", status, action, state.Generation), Status: status, Action: action,
139 Trigger: trigger, SourceProjection: state.Projection.ProjectionVersion,
140 ProjectionVersion: state.Projection.ProjectionVersion, InputHash: inputHash,
141 InputTokens: inputTokens, BlockedInputHash: inputHash, Reason: reason, CreatedAt: now,
142 }
143 state.UpdatedAt = now
144 a.sess.compactionState = state
145 if err := a.persistCompactionStateLocked(); err != nil {
146 a.sess.compactionState = previous
147 a.sess.compactionMu.Unlock()
148 return
149 }
150 a.sess.compaction.failedTurn.Store(a.activeTurnCreatedAt.Load())
151 a.sess.compactionMu.Unlock()
152 a.emitContextMaintenance(state.LastReceipt)
153 }
154
155 func (a *Agent) emitCompactionTelemetry(t CompactionTelemetry) {
156 detail := fmt.Sprintf("trigger=%s mode=%s summary_input=%s cache=%s src=%d fold=%d spans=%d proj=%d in=%d out=%d hit=%d miss=%d write=%d reqs=%d user_kept=%d user_dropped=%d",
157 t.Trigger, t.Mode, t.SummaryInputMode, t.CacheState, t.SourceTokens, t.FoldTokens, t.Spans, t.ProjectionTokens,
158 t.InputTokens, t.OutputTokens, t.CacheHitTokens, t.CacheMissTokens, t.CacheWriteTokens, t.RequestCount,
159 t.UserTurnsKept, t.UserTurnsDropped)
160 if t.ProviderRequestID != "" {
161 detail += " provider_request_id=" + t.ProviderRequestID
162 }
163 if t.Error != "" {
164 // CompactionModeDegraded remains readable for legacy telemetry, although
165 // new summarizer failures never install a degraded projection.
166 if t.Mode != CompactionModeDegraded {
167 slog.Warn("agent: compaction failed", "detail", detail+" err_type="+t.Error)
168 return
169 }
170 detail += " err_type=" + t.Error
171 }
172 a.svc.sink.Emit(event.Event{Kind: event.Notice, Level: event.LevelInfo, Text: "compaction telemetry", Detail: detail})
173 }
174
175 func (a *Agent) emitCompactionAborted(trigger string) {
176 a.svc.sink.Emit(event.Event{Kind: event.CompactionDone, Compaction: event.Compaction{Trigger: trigger}})
177 }
178
178 lines GO