返回 DeepSeek-Reasonix
history_replay.go
根目录 / internal / provider / history_replay.go
1 package provider
2
3 import (
4 "encoding/json"
5 "strings"
6 )
7
8 // RepairHistoryForReplay turns unusable historical tool batches into ordinary
9 // records, never executable calls. Unlike replacing unknown arguments with {},
10 // this preserves uncertainty about side effects and keeps recorded results.
11 // Use only for host history, before extensions and the final transcript gate.
12 // Canonical messages are not mutated; healthy wire bytes remain unchanged.
13 func RepairHistoryForReplay(msgs []Message) []Message {
14 out := RepairRejectedArguments(ProjectionMessages(msgs))
15 for i := 0; i < len(out); {
16 m := out[i]
17 if len(m.ToolCalls) == 0 {
18 // Unpaired results are still observations, even when their call was
19 // lost at an interrupted boundary. Do not silently drop the facts.
20 if m.Role == RoleTool {
21 out[i] = archivedToolResult(m)
22 }
23 i++
24 continue
25 }
26 end := i + 1
27 for end < len(out) && out[end].Role == RoleTool {
28 end++
29 }
30 if !replayableToolBatch(m) {
31 out[i] = archivedToolCalls(m)
32 for j := i + 1; j < end; j++ {
33 out[j] = archivedToolResult(out[j])
34 }
35 }
36 i = end
37 }
38 return out
39 }
40
41 func replayableToolBatch(m Message) bool {
42 if m.Role != RoleAssistant {
43 return false
44 }
45 seen := make(map[string]bool, len(m.ToolCalls))
46 for _, call := range m.ToolCalls {
47 if strings.TrimSpace(call.Name) == "" {
48 return false
49 }
50 // Empty IDs retain the existing positional compatibility path.
51 if call.ID != "" {
52 if strings.TrimSpace(call.ID) == "" || seen[call.ID] {
53 return false
54 }
55 seen[call.ID] = true
56 }
57 var args map[string]json.RawMessage
58 if json.Unmarshal([]byte(call.Arguments), &args) != nil || args == nil {
59 return false
60 }
61 }
62 return true
63 }
64
65 func archivedToolCalls(m Message) Message {
66 type record struct {
67 ID string `json:"id"`
68 Name string `json:"name"`
69 Arguments string `json:"original_arguments"`
70 }
71 calls := make([]record, 0, len(m.ToolCalls))
72 for _, call := range m.ToolCalls {
73 calls = append(calls, record{call.ID, call.Name, call.Arguments})
74 }
75 data, _ := json.Marshal(calls) // strings only; no local recovery metadata
76 return archivedMessage(m, "[Historical tool record: the original call format cannot be replayed. These are archived proposals, not new tool calls. Execution is unconfirmed unless the recorded results establish it. Do not repeat actions solely because this record was recovered; verify their effects first.]\n"+string(data))
77 }
78
79 func archivedToolResult(m Message) Message {
80 data, _ := json.Marshal(struct {
81 CallID string `json:"call_id"`
82 Name string `json:"name"`
83 State ToolRunState `json:"recorded_execution_state,omitempty"`
84 }{m.ToolCallID, m.Name, m.ToolRunState})
85 // Content is the existing provider-visible observation, never RawContent.
86 return archivedMessage(m, "[Historical tool observation; pairing may be incomplete. This record does not request a new execution.]\n"+string(data))
87 }
88
89 func archivedMessage(m Message, note string) Message {
90 content := note
91 if m.Content != "" {
92 data, _ := json.Marshal(struct {
93 Content string `json:"original_message_content"`
94 }{m.Content})
95 content += "\n" + string(data)
96 }
97 // Drop native tool replay metadata. Host-authored user data preserves images
98 // and avoids assistant prefill, which thinking providers may reject.
99 return Message{ID: m.ID, Role: RoleUser, Origin: MessageOriginHost, Content: content, Images: m.Images, ImageInputs: m.ImageInputs}
100 }
101
101 lines GO