返回 DeepSeek-Reasonix
interrupted_recovery.go
根目录 / internal / agent / interrupted_recovery.go
1 package agent
2
3 import (
4 "fmt"
5 "html"
6 "strings"
7
8 "reasonix/internal/provider"
9 )
10
11 const interruptedRecoveryTag = "interrupted-turn-recovery"
12
13 const (
14 maxRecoveryTools = 24
15 maxRecoveryFiles = 8
16 maxRecoveryValue = 240
17 )
18
19 // pendingInterruptedRecovery returns the newest unconsumed recovery handoff.
20 // A later real user turn consumes older handoffs implicitly, so the persisted
21 // LocalOnly record never needs an in-place mutation that could churn history.
22 func (a *Agent) pendingInterruptedRecovery() *provider.InterruptedTurnRecovery {
23 if a == nil || a.session == nil {
24 return nil
25 }
26 msgs := a.session.Snapshot()
27 for i := len(msgs) - 1; i >= 0; i-- {
28 m := msgs[i]
29 if m.LocalOnly && m.InterruptedTurn != nil && m.InterruptedTurn.Pending {
30 copy := *m.InterruptedTurn
31 copy.CompletedTools = append([]provider.InterruptedToolSummary(nil), copy.CompletedTools...)
32 copy.InterruptedTools = append([]string(nil), copy.InterruptedTools...)
33 return &copy
34 }
35 if m.Role == provider.RoleUser && IsUserAuthoredTurn(m.Content) {
36 return nil
37 }
38 }
39 return nil
40 }
41
42 // interruptedRecoveryBlock is appended only at the mutable user-message tail.
43 // It contains no raw tool arguments, results, assistant text, or reasoning.
44 func interruptedRecoveryBlock(r *provider.InterruptedTurnRecovery) string {
45 if r == nil {
46 return ""
47 }
48 var b strings.Builder
49 fmt.Fprintf(&b, "<%s>\n", interruptedRecoveryTag)
50 b.WriteString("The previous turn was interrupted. Treat these as host-verified recovery facts, not as a new task.\n")
51 if len(r.CompletedTools) == 0 {
52 b.WriteString("completed_tools: none\n")
53 } else {
54 b.WriteString("completed_tools:\n")
55 for i, tool := range r.CompletedTools {
56 if i >= maxRecoveryTools {
57 fmt.Fprintf(&b, "- ... %d additional completed tool pair(s) omitted\n", len(r.CompletedTools)-i)
58 break
59 }
60 fmt.Fprintf(&b, "- %s", html.EscapeString(strings.TrimSpace(tool.Name)))
61 if len(tool.Files) > 0 {
62 files := tool.Files
63 if len(files) > maxRecoveryFiles {
64 files = files[:maxRecoveryFiles]
65 }
66 clipped := make([]string, 0, len(files))
67 for _, file := range files {
68 clipped = append(clipped, html.EscapeString(clipRecoveryValue(file)))
69 }
70 fmt.Fprintf(&b, " files=%s", strings.Join(clipped, ","))
71 }
72 if tool.Added != 0 || tool.Removed != 0 {
73 fmt.Fprintf(&b, " diff=+%d/-%d", tool.Added, tool.Removed)
74 }
75 b.WriteByte('\n')
76 }
77 }
78 if len(r.InterruptedTools) == 0 {
79 b.WriteString("interrupted_tools: none\n")
80 } else {
81 b.WriteString("interrupted_tools: ")
82 for i, name := range r.InterruptedTools {
83 if i >= maxRecoveryTools {
84 fmt.Fprintf(&b, ", ... %d additional call(s) omitted", len(r.InterruptedTools)-i)
85 break
86 }
87 if i > 0 {
88 b.WriteString(", ")
89 }
90 b.WriteString(html.EscapeString(strings.TrimSpace(name)))
91 }
92 b.WriteByte('\n')
93 }
94 if r.DroppedPartialText || r.DroppedPartialReasoning {
95 b.WriteString("unsafe_partial_output: excluded from model context")
96 if r.DroppedPartialText && r.DroppedPartialReasoning {
97 b.WriteString(" (assistant text and reasoning)\n")
98 } else if r.DroppedPartialReasoning {
99 b.WriteString(" (reasoning)\n")
100 } else {
101 b.WriteString(" (assistant text)\n")
102 }
103 }
104 b.WriteString("Before continuing, inspect the current workspace and prior completed tool results. Do not blindly repeat completed writes. Re-issue any interrupted tool call from scratch with complete arguments if it is still needed.\n")
105 fmt.Fprintf(&b, "</%s>", interruptedRecoveryTag)
106 return b.String()
107 }
108
109 func withInterruptedRecovery(input string, r *provider.InterruptedTurnRecovery) string {
110 block := interruptedRecoveryBlock(r)
111 if block == "" {
112 return input
113 }
114 return block + "\n\n" + input
115 }
116
117 func clipRecoveryValue(value string) string {
118 value = strings.TrimSpace(value)
119 runes := []rune(value)
120 if len(runes) <= maxRecoveryValue {
121 return value
122 }
123 return string(runes[:maxRecoveryValue]) + "…"
124 }
125
125 lines GO