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