返回 DeepSeek-Reasonix
termination_policy.go
根目录 / internal / control / termination_policy.go
1 package control
2
3 import (
4 "reasonix/internal/agent"
5 "reasonix/internal/provider"
6 "strings"
7 "time"
8 )
9
10 // planCancelledMessages preserves the existing provider replay policy without
11 // mutating the executor, storage, or UI. Its result belongs to the caller.
12 func planCancelledMessages(msgs []provider.Message, idx int, fallback provider.Message, startedAt time.Time, canReplay func(provider.Message) bool, evidence *interruptedTailEvidence) []provider.Message {
13 if start, ok := resolveInterruptedTurnStart(msgs, idx, true, startedAt, fallback); ok {
14 idx = start
15 }
16 if idx < 0 {
17 idx = 0
18 }
19 if idx > len(msgs) {
20 idx = len(msgs)
21 }
22 next := append([]provider.Message{}, msgs[:idx]...)
23 keptUser := false
24 userEnd := idx
25 for i, m := range msgs[idx:] {
26 if !agent.IsUserAuthoredTurnMessage(m) {
27 continue
28 }
29 m.Content = StripComposePrefixes(m.Content)
30 next = append(next, m)
31 keptUser = true
32 userEnd = idx + i + 1
33 break
34 }
35 if !keptUser && agent.IsUserAuthoredTurnMessage(fallback) {
36 fallback.Content = StripComposePrefixes(fallback.Content)
37 if strings.TrimSpace(fallback.Content) != "" {
38 fallback.Images = append([]string(nil), fallback.Images...)
39 next = append(next, fallback)
40 keptUser = true
41 userEnd = idx
42 }
43 }
44 if !keptUser && len(msgs) <= idx {
45 return nil
46 }
47 recovery := &provider.InterruptedTurnRecovery{Pending: true}
48 localIndexes := make([]int, 0, 1)
49 for i := userEnd; i < len(msgs); {
50 m := msgs[i]
51 if m.LocalOnly {
52 m.Role = provider.RoleTool
53 m.ToolCallID = provider.LocalOnlyToolID
54 m.Name = provider.LocalOnlyToolName
55 previousRecovery := m.InterruptedTurn
56 m.InterruptedTurn = nil
57 next = append(next, m)
58 localIndexes = append(localIndexes, len(next)-1)
59 recovery.DroppedPartialText = recovery.DroppedPartialText || strings.TrimSpace(m.Content) != ""
60 recovery.DroppedPartialReasoning = recovery.DroppedPartialReasoning || strings.TrimSpace(m.ReasoningContent) != ""
61 if previousRecovery != nil {
62 recovery.CompletedTools = append(recovery.CompletedTools, previousRecovery.CompletedTools...)
63 recovery.InterruptedTools = append(recovery.InterruptedTools, previousRecovery.InterruptedTools...)
64 recovery.NotStartedTools = append(recovery.NotStartedTools, previousRecovery.NotStartedTools...)
65 recovery.UnknownTools = append(recovery.UnknownTools, previousRecovery.UnknownTools...)
66 } else {
67 for _, call := range m.ToolCalls {
68 provider.RecordToolRecovery(recovery, interruptedToolSummary(call), provider.ToolRunUnknown)
69 }
70 }
71 i++
72 continue
73 }
74 // Keep compaction digests between the pinned input and recent tool tail;
75 // their summarized work is no longer available verbatim.
76 if agent.IsCompactionSummary(m) {
77 next = append(next, m)
78 i++
79 continue
80 }
81 if m.Role == provider.RoleAssistant {
82 recordInterruptedAssistantRecovery(recovery, msgs, i, evidence)
83 }
84 if end, ok := completeToolTurnEnd(msgs, i); ok && canReplay(m) {
85 next = append(next, msgs[i:end]...)
86 i = end
87 continue
88 }
89 switch m.Role {
90 case provider.RoleAssistant:
91 local := m
92 local.Role = provider.RoleTool
93 local.LocalOnly = true
94 local.ToolCallID = provider.LocalOnlyToolID
95 local.Name = provider.LocalOnlyToolName
96 local.InterruptedTurn = nil
97 next = append(next, local)
98 localIndexes = append(localIndexes, len(next)-1)
99 recovery.DroppedPartialText = recovery.DroppedPartialText || strings.TrimSpace(local.Content) != ""
100 recovery.DroppedPartialReasoning = recovery.DroppedPartialReasoning || strings.TrimSpace(local.ReasoningContent) != ""
101 case provider.RoleTool:
102 local := m
103 local.LocalOnly = true
104 local.ToolCalls = []provider.ToolCall{{ID: m.ToolCallID, Name: m.Name}}
105 local.ToolCallID = provider.LocalOnlyToolID
106 local.Name = provider.LocalOnlyToolName
107 next = append(next, local)
108 localIndexes = append(localIndexes, len(next)-1)
109 }
110 i++
111 }
112 if len(localIndexes) == 0 {
113 next = append(next, provider.Message{
114 Role: provider.RoleTool, ToolCallID: provider.LocalOnlyToolID,
115 Name: provider.LocalOnlyToolName, LocalOnly: true,
116 })
117 localIndexes = append(localIndexes, len(next)-1)
118 }
119 if evidence != nil {
120 recovery.Cause = "runtime_restart"
121 recovery.TurnID = evidence.turnID
122 recovery.SilentInterruption = len(recovery.ToolCalls) == 0 && len(recovery.CompletedTools) == 0 && !recovery.DroppedPartialText && !recovery.DroppedPartialReasoning
123 }
124 next[localIndexes[len(localIndexes)-1]].InterruptedTurn = recovery
125 return next
126 }
127
127 lines GO