返回 DeepSeek-Reasonix
reasoning_replay_recovery.go
根目录 / internal / agent / reasoning_replay_recovery.go
1 package agent
2
3 import (
4 "reasonix/internal/event"
5 "reasonix/internal/i18n"
6 "reasonix/internal/provider"
7 )
8
9 // reasoningReplayRecoveryBudget bounds the thinking-400 catch-and-repair to one
10 // retry per model round and records the repaired provider-message prefix.
11 type reasoningReplayRecoveryBudget struct {
12 persisted bool
13 retries int
14 local bool
15 cutoff int
16 anchor string
17 }
18
19 // recoverReasoningReplay400 applies the vendor-documented self-heal for a
20 // provider that rejected replayed thinking history with HTTP 400: rebuild the
21 // frozen request's messages through the strong projection and retry exactly
22 // once. Everything except Messages stays byte-identical to the rejected
23 // request; the original history boundary bounds future projection, including
24 // a trailing tool pair that disappears from the repaired view.
25 func (a *Agent) recoverReasoningReplay400(frozen samplingRequest, err error, budget *reasoningReplayRecoveryBudget) (samplingRequest, bool) {
26 if a == nil || budget == nil || budget.retries > 0 || a.protocolRecoverySpent() {
27 return samplingRequest{}, false
28 }
29 if provider.AsReasoningReplayError(err) == nil {
30 return samplingRequest{}, false
31 }
32 return a.recoverReasoningReplayHistory(frozen, budget)
33 }
34
35 func (a *Agent) recoverReasoningReplayHistory(frozen samplingRequest, budget *reasoningReplayRecoveryBudget) (samplingRequest, bool) {
36 if a == nil || budget == nil || budget.retries > 0 || a.protocolRecoverySpent() {
37 return samplingRequest{}, false
38 }
39 repaired, changed := provider.ProjectReasoningStrippedMessages(a.svc.prov, frozen.req.Messages)
40 if !changed {
41 return samplingRequest{}, false
42 }
43 budget.retries++
44 budget.cutoff = len(frozen.req.Messages)
45 if budget.cutoff > 0 {
46 budget.anchor = reasoningReplayMessageFingerprint(frozen.req.Messages[budget.cutoff-1])
47 }
48 next := frozen.req
49 next.Messages = a.replayRecoveryFacts(frozen.req.Messages, repaired)
50 return samplingRequest{req: next}, true
51 }
52
53 // tryRecoverReasoningReplay400 is the streamWithSamplingRecovery branch for a
54 // thinking-400: the frozen history's replayed reasoning is stale for this
55 // provider, so repair the projection once and retry; every other 400 falls
56 // through to the terminal path. On a repair the speculative attempt's buffered
57 // events are discarded and the attempt is audited before the caller replays.
58 func (a *Agent) tryRecoverReasoningReplay400(streamSink *deferredStreamSink, frozen samplingRequest, attemptID string, attempt int, err error, budget *reasoningReplayRecoveryBudget) (samplingRequest, bool) {
59 next, ok := a.recoverReasoningReplay400(frozen, err, budget)
60 if !ok {
61 return samplingRequest{}, false
62 }
63 if streamSink != nil {
64 streamSink.Discard()
65 }
66 a.emitStreamAttempt(attemptID, event.StreamAttemptDiscard, attempt, "reasoning_replay_400", err)
67 event.RecordProtocolRecovery(a.svc.sink, event.ProtocolRecoveryAudit{Kind: event.ProtocolRecoveryReasoningReplay400Detected})
68 return next, true
69 }
70
71 // activateReasoningReplayStrongProjection records the repaired history prefix.
72 // Messages appended after it keep their normal reasoning/tool replay.
73 func (a *Agent) activateReasoningReplayStrongProjection(budget reasoningReplayRecoveryBudget) {
74 if a == nil {
75 return
76 }
77 a.sess.reasoningReplayStrongProjection = budget.cutoff
78 a.sess.reasoningReplayStrongProjectionAnchor = budget.anchor
79 kind := event.ProtocolRecoveryReasoningReplay400Recovered
80 if budget.local {
81 kind = event.ProtocolRecoveryHistoryRepaired
82 }
83 event.RecordProtocolRecovery(a.svc.sink, event.ProtocolRecoveryAudit{Kind: kind})
84 a.emitReasoningReplayRepairNotice()
85 }
86
87 func (a *Agent) emitReasoningReplayRepairNotice() {
88 if a == nil || a.svc.sink == nil {
89 return
90 }
91 a.svc.sink.Emit(event.Event{
92 Kind: event.Notice,
93 Level: event.LevelWarn,
94 Code: event.NoticeCodeReasoningReplayRepair,
95 Text: i18n.M.ReasoningReplayRepair,
96 Detail: "reasoning replay recovery regenerated the response once from a repaired history with completed-tool facts preserved",
97 })
98 }
99
99 lines GO