返回 DeepSeek-Reasonix
history.go
根目录 / internal / transcript / history.go
1 package transcript
2
3 import (
4 "fmt"
5 "strings"
6
7 "reasonix/internal/agent"
8 "reasonix/internal/event"
9 "reasonix/internal/provider"
10 )
11
12 // HistoryOptions supplies surface-specific presentation without allowing a
13 // frontend to invent record identity or event coverage.
14 type HistoryOptions struct {
15 UserContent func(provider.Message) string
16 SubmitContent func(provider.Message) string
17 ToolCall func(provider.ToolCall) ToolCall
18 LegacyTurns []LegacyDisplayTurn
19 CheckpointTurns map[int]int
20 }
21
22 // History converts the persisted message identities into display records.
23 // Derived notices are keyed by their owning message, never by their body.
24 func History(messages []provider.Message, opts HistoryOptions) []Message {
25 out := make([]Message, 0, len(messages))
26 byUser := legacyTurnsByUser(opts.LegacyTurns)
27 canonicalMessages := canonicalMessageIDs(messages)
28 suppressCanonical := false
29 todoArgs := completedTodoArguments(messages)
30 for messageIndex, m := range messages {
31 if suppressCanonical && m.DecisionReceipt == nil && !independentLocalMessage(m) {
32 if !agent.IsUserAuthoredTurnMessage(m) {
33 continue
34 }
35 suppressCanonical = false
36 }
37 rows := historyRows(m, messageIndex, opts, todoArgs)
38 stampHistoryRows(rows, m)
39 out = append(out, rows...)
40 if m.Role == provider.RoleUser && !m.LocalOnly && agent.IsUserAuthoredTurnMessage(m) {
41 if appendLegacyTurnRows(&out, byUser, canonicalMessages, m) {
42 suppressCanonical = true
43 }
44 }
45 }
46 return out
47 }
48
49 func canonicalMessageIDs(messages []provider.Message) map[string]bool {
50 canonicalMessages := make(map[string]bool)
51 for _, message := range messages {
52 if message.ID != "" {
53 canonicalMessages[message.ID] = true
54 }
55 }
56 return canonicalMessages
57 }
58
59 func legacyTurnsByUser(turns []LegacyDisplayTurn) map[string][]LegacyDisplayTurn {
60 byUser := make(map[string][]LegacyDisplayTurn)
61 for _, turn := range turns {
62 key := turn.UserMessageID
63 if key == "" {
64 key = "legacy:" + turn.UserHash
65 }
66 byUser[key] = append(byUser[key], turn)
67 }
68 return byUser
69 }
70
71 func independentLocalMessage(m provider.Message) bool {
72 _, steer := agent.ReplaySteerText(m.Content)
73 return m.LocalOnly && (m.ReadPause != nil || m.ReadCompletion != nil || len(m.ProtocolRecovery) > 0 || (m.FinalReadinessRecovery != nil && m.FinalReadinessRecovery.Pending) || steer)
74 }
75
76 func historyRows(m provider.Message, messageIndex int, opts HistoryOptions, todoArgs map[string]string) []Message {
77 switch {
78 case m.Role == provider.RoleSystem:
79 return nil
80 case m.DecisionReceipt != nil:
81 return []Message{{Role: "notice", Code: event.NoticeCodeDecisionReceipt, Level: "info", DecisionReceipt: m.DecisionReceipt}}
82 case m.LocalOnly && m.ReadPause != nil:
83 return []Message{{Role: "notice", Code: event.TurnOutcomeIncompleteRead, Level: "info", ReadPause: m.ReadPause}}
84 case m.LocalOnly && m.ReadCompletion != nil:
85 return []Message{readCompletionMessage(m.ReadCompletion)}
86 case m.LocalOnly && len(m.ProtocolRecovery) > 0:
87 if recovery, ok := provider.DecodeProtocolRecovery(m.ProtocolRecovery); ok && recovery.State == "pending" {
88 return []Message{{Role: "notice", Code: "protocol_recovery", Level: "info", Pending: true, ProtocolRecovery: &provider.ProtocolRecoveryAction{ID: recovery.ID}}}
89 }
90 return nil
91 case m.LocalOnly && m.FinalReadinessRecovery != nil && m.FinalReadinessRecovery.Pending:
92 return []Message{{Role: "notice", Code: agent.HistoricalChecksNoticeCode, Level: "info",
93 Content: agent.HistoricalChecksNoticeText,
94 Readiness: agent.HistoricalChecks(m.FinalReadinessRecovery)}}
95 default:
96 return defaultHistoryRows(m, messageIndex, opts, todoArgs)
97 }
98 }
99
100 func defaultHistoryRows(m provider.Message, messageIndex int, opts HistoryOptions, todoArgs map[string]string) []Message {
101 if text, handled := agent.ReplaySteerText(m.Content); handled {
102 if text == "" {
103 return nil
104 }
105 row := Message{Role: "notice", Content: "↪ " + text}
106 if m.LocalOnly {
107 row.Content, row.Code, row.Level = agent.UnappliedSteerNotice(text), event.NoticeCodeUnappliedSteer, "warn"
108 }
109 return []Message{row}
110 }
111 if m.Role == provider.RoleUser && agent.IsHostGeneratedUserMessage(m) {
112 return nil
113 }
114 row := Message{MessageID: m.ID, Role: string(m.Role), Content: m.Content, CreatedAt: m.CreatedAt,
115 WorkDurationMs: m.WorkDurationMs, MemoryCitations: m.MemoryCitations, Execution: m.ToolExecution,
116 PresentedFiles: provider.PresentedFileList(m.PresentedFiles)}
117 if m.LocalOnly {
118 row.Role = "assistant"
119 }
120 if m.Role == provider.RoleUser {
121 applyUserHistoryContent(&row, m, messageIndex, opts)
122 }
123 if row.Role == "assistant" {
124 applyAssistantHistoryFields(&row, m, opts, todoArgs)
125 }
126 if m.Role == provider.RoleTool && !m.LocalOnly {
127 row.ToolCallID, row.ToolName = m.ToolCallID, m.Name
128 if toolFailed(row.Content) {
129 row.ToolResultError = row.Content
130 }
131 }
132 var rows []Message
133 if !m.LocalOnly || strings.TrimSpace(row.Content+row.Reasoning) != "" || len(row.ToolCalls) > 0 {
134 rows = append(rows, row)
135 }
136 for _, receipt := range m.DecisionReceipts {
137 if receipt != nil {
138 rows = append(rows, Message{Role: "notice", Code: event.NoticeCodeDecisionReceipt, Level: "info", DecisionReceipt: receipt})
139 }
140 }
141 if m.LocalOnly && m.InterruptedTurn != nil {
142 rows = append(rows, interruptedNotice(m.InterruptedTurn))
143 }
144 return rows
145 }
146
147 func applyUserHistoryContent(row *Message, m provider.Message, messageIndex int, opts HistoryOptions) {
148 if turn, ok := opts.CheckpointTurns[messageIndex]; ok {
149 row.CheckpointTurn = &turn
150 }
151 row.Content = agent.UserMessageText(m)
152 if opts.UserContent != nil {
153 row.Content = opts.UserContent(m)
154 }
155 row.Content = CollapseExpandedPaste(row.Content)
156 if opts.SubmitContent != nil && row.Content != m.Content {
157 replay := opts.SubmitContent(m)
158 if replay != row.Content && (!agent.ContainsMemoryCompilerExecution(m.Content) || strings.HasPrefix(strings.TrimSpace(replay), "/")) {
159 row.SubmitText = replay
160 }
161 }
162 }
163
164 func applyAssistantHistoryFields(row *Message, m provider.Message, opts HistoryOptions, todoArgs map[string]string) {
165 row.Reasoning = m.ReasoningContent
166 for _, call := range m.ToolCalls {
167 converted := ToolCall{ID: call.ID, Name: call.Name, Arguments: call.Arguments, ResolvedName: call.ResolvedName,
168 CapabilityID: call.CapabilityID, ResolvedReadOnly: call.ResolvedReadOnly, Diff: call.Diff, Added: call.Added, Removed: call.Removed}
169 if opts.ToolCall != nil {
170 converted = opts.ToolCall(call)
171 }
172 if args, ok := todoArgs[call.ID]; ok && call.Name == "todo_write" {
173 converted.Arguments = args
174 }
175 row.ToolCalls = append(row.ToolCalls, converted)
176 }
177 for _, search := range m.ServerSearch {
178 search.Raw = nil
179 row.ServerSearch = append(row.ServerSearch, search)
180 }
181 }
182
183 func stampHistoryRows(rows []Message, m provider.Message) {
184 for i := range rows {
185 row := &rows[i]
186 switch {
187 case row.Role == "tool" && row.ToolCallID != "":
188 row.RecordID = "tool:" + row.ToolCallID
189 case row.MessageID != "":
190 row.RecordID = "m:" + row.MessageID
191 default:
192 row.RecordID = fmt.Sprintf("m:%s:notice:%d", m.ID, i)
193 }
194 if row.CreatedAt == 0 {
195 row.CreatedAt = m.CreatedAt
196 }
197 }
198 }
199
200 // appendLegacyTurnRows reports whether the replayed turn carried a
201 // cancelled-turn notice, which suppresses its canonical successor messages.
202 func appendLegacyTurnRows(out *[]Message, byUser map[string][]LegacyDisplayTurn, canonicalMessages map[string]bool, m provider.Message) bool {
203 key := m.ID
204 if len(byUser[key]) == 0 {
205 key = "legacy:" + LegacyDisplayKey(agent.UserMessageText(m))
206 }
207 turns := byUser[key]
208 if len(turns) == 0 {
209 return false
210 }
211 turn := turns[0]
212 byUser[key] = turns[1:]
213 cancelled := false
214 for _, legacy := range turn.Messages {
215 if legacy.Role == "notice" && legacy.Code == event.NoticeCodeCancelledTurn {
216 cancelled = true
217 }
218 }
219 sawCancelled := false
220 for i, legacy := range turn.Messages {
221 if !cancelled && legacy.MessageID != "" && canonicalMessages[legacy.MessageID] {
222 continue
223 }
224 if legacy.RecordID == "" {
225 if legacy.MessageID != "" {
226 legacy.RecordID = "m:" + legacy.MessageID
227 } else {
228 legacy.RecordID = fmt.Sprintf("m:%s:legacy:%s:%d", m.ID, turn.TurnID, i)
229 }
230 }
231 if legacy.Role == "notice" && legacy.Code == event.NoticeCodeCancelledTurn {
232 sawCancelled = true
233 }
234 *out = append(*out, legacy)
235 }
236 return sawCancelled
237 }
238
238 lines GO