返回 DeepSeek-Reasonix
interrupted_recovery.go
根目录 / internal / agent / interrupted_recovery.go
1 package agent
2
3 import (
4 "fmt"
5 "html"
6 "slices"
7 "strings"
8
9 "reasonix/internal/provider"
10 )
11
12 const interruptedRecoveryTag = "interrupted-turn-recovery"
13
14 const recoveryPrecedenceClause = "The user's new message takes precedence. If it is unrelated to the interrupted task, do not resume that task."
15
16 const (
17 maxRecoveryTools = 24
18 maxRecoveryFiles = 8
19 maxRecoveryValue = 240
20 )
21
22 // pendingInterruptedRecovery returns the newest unconsumed recovery handoff.
23 // A later real user turn consumes older handoffs implicitly, so the persisted
24 // LocalOnly record never needs an in-place mutation that could churn history.
25 func (a *Agent) transcriptInterruptedRecovery() *provider.InterruptedTurnRecovery {
26 if a == nil || a.sess.conversation == nil {
27 return nil
28 }
29 msgs := a.sess.conversation.Snapshot()
30 for _, v := range slices.Backward(msgs) {
31 m := v
32 if m.LocalOnly && m.InterruptedTurn != nil && m.InterruptedTurn.Pending {
33 copy := *m.InterruptedTurn
34 if copy.FailureDiagnostic != nil {
35 diagnostic := *copy.FailureDiagnostic
36 copy.FailureDiagnostic = &diagnostic
37 }
38 copy.WriteChecks = append([]provider.WriteRecoveryCheck(nil), copy.WriteChecks...)
39 copy.SatisfiedWrites = append([]provider.InterruptedToolSummary(nil), copy.SatisfiedWrites...)
40 copy.CompletedTools = append([]provider.InterruptedToolSummary(nil), copy.CompletedTools...)
41 copy.InterruptedTools = append([]string(nil), copy.InterruptedTools...)
42 copy.NotStartedTools = append([]provider.InterruptedToolSummary(nil), copy.NotStartedTools...)
43 copy.UnknownTools = append([]provider.InterruptedToolSummary(nil), copy.UnknownTools...)
44 return &copy
45 }
46 if IsUserAuthoredTurnMessage(m) {
47 return nil
48 }
49 }
50 return nil
51 }
52
53 // interruptedRecoveryBlock is appended only at the mutable user-message tail.
54 // It contains no raw tool arguments, results, assistant text, or reasoning.
55 func interruptedRecoveryBlock(r *provider.InterruptedTurnRecovery) string {
56 if r == nil {
57 return ""
58 }
59 var b strings.Builder
60 fmt.Fprintf(&b, "<%s>\n", interruptedRecoveryTag)
61 if len(r.UserConfirmedTools) > 0 {
62 b.WriteString("The previous turn was interrupted. Preserve the stated provenance: user-confirmed effects are attestations, not tool results.\n")
63 } else {
64 b.WriteString("The previous turn was interrupted. Treat these as host-verified recovery facts, not as a new task.\n")
65 }
66 if len(r.CompletedTools) == 0 {
67 b.WriteString("completed_tools: none\n")
68 } else {
69 b.WriteString("completed_tools:\n")
70 for i, tool := range r.CompletedTools {
71 if i >= maxRecoveryTools {
72 fmt.Fprintf(&b, "- ... %d additional completed tool pair(s) omitted\n", len(r.CompletedTools)-i)
73 break
74 }
75 fmt.Fprintf(&b, "- %s", html.EscapeString(strings.TrimSpace(tool.Name)))
76 if len(tool.Files) > 0 {
77 files := tool.Files
78 if len(files) > maxRecoveryFiles {
79 files = files[:maxRecoveryFiles]
80 }
81 clipped := make([]string, 0, len(files))
82 for _, file := range files {
83 clipped = append(clipped, html.EscapeString(clipRecoveryValue(file)))
84 }
85 fmt.Fprintf(&b, " files=%s", strings.Join(clipped, ","))
86 }
87 if tool.Added != 0 || tool.Removed != 0 {
88 fmt.Fprintf(&b, " diff=+%d/-%d", tool.Added, tool.Removed)
89 }
90 b.WriteByte('\n')
91 }
92 }
93 if len(r.InterruptedTools) == 0 {
94 b.WriteString("interrupted_tools: none\n")
95 } else {
96 b.WriteString("interrupted_tools: ")
97 for i, name := range r.InterruptedTools {
98 if i >= maxRecoveryTools {
99 fmt.Fprintf(&b, ", ... %d additional call(s) omitted", len(r.InterruptedTools)-i)
100 break
101 }
102 if i > 0 {
103 b.WriteString(", ")
104 }
105 b.WriteString(html.EscapeString(strings.TrimSpace(name)))
106 }
107 b.WriteByte('\n')
108 }
109 writeRecoveryChecks(&b, r.WriteChecks)
110 writeRecoveryCalls(&b, "write_postconditions_satisfied_do_not_repeat", r.SatisfiedWrites)
111 writeRecoveryCalls(&b, "not_started_tools", r.NotStartedTools)
112 writeRecoveryCalls(&b, "outcome_unknown_tools", r.UnknownTools)
113 writeRecoveryCalls(&b, "failed_tools", r.FailedTools)
114 writeRecoveryCalls(&b, "user_confirmed_effects_do_not_repeat", r.UserConfirmedTools)
115 if r.DroppedPartialText || r.DroppedPartialReasoning {
116 b.WriteString("unsafe_partial_output: excluded from model context")
117 if r.DroppedPartialText && r.DroppedPartialReasoning {
118 b.WriteString(" (assistant text and reasoning)\n")
119 } else if r.DroppedPartialReasoning {
120 b.WriteString(" (reasoning)\n")
121 } else {
122 b.WriteString(" (assistant text)\n")
123 }
124 }
125 b.WriteString("Use these facts when deciding the next action. Read-only or idempotent calls may be retried when useful. For outcome-unknown calls, inspect workspace or external state before retrying operations with side effects, and ask the user when the safe action cannot be inferred. Calls marked not_started may be planned again with complete arguments. " + recoveryPrecedenceClause + "\n")
126 fmt.Fprintf(&b, "</%s>", interruptedRecoveryTag)
127 return b.String()
128 }
129
130 func withInterruptedRecovery(input string, r *provider.InterruptedTurnRecovery) string {
131 block := interruptedRecoveryBlock(r)
132 if block == "" {
133 return input
134 }
135 return block + "\n\n" + input
136 }
137
138 func clipRecoveryValue(value string) string {
139 value = strings.TrimSpace(value)
140 runes := []rune(value)
141 if len(runes) <= maxRecoveryValue {
142 return value
143 }
144 return string(runes[:maxRecoveryValue]) + "…"
145 }
146
147 func writeRecoveryCalls(b *strings.Builder, label string, calls []provider.InterruptedToolSummary) {
148 if len(calls) == 0 {
149 return
150 }
151 fmt.Fprintf(b, "%s:\n", label)
152 for i, call := range calls {
153 if i >= maxRecoveryTools {
154 fmt.Fprintf(b, "- ... %d omitted\n", len(calls)-i)
155 break
156 }
157 fmt.Fprintf(b, "- %s id=%s\n", html.EscapeString(clipRecoveryValue(call.Name)), html.EscapeString(clipRecoveryValue(call.ID)))
158 }
159 }
160
161 func writeRecoveryChecks(b *strings.Builder, checks []provider.WriteRecoveryCheck) {
162 for i, check := range checks {
163 if i >= maxRecoveryTools*maxRecoveryFiles {
164 break
165 }
166 fmt.Fprintf(b, "write_postcondition: id=%s path=%s state=%s\n", html.EscapeString(clipRecoveryValue(check.CallID)), html.EscapeString(clipRecoveryValue(check.Path)), html.EscapeString(clipRecoveryValue(check.State)))
167 }
168 }
169
169 lines GO