返回 DeepSeek-Reasonix
transcriptRecordProjection.ts
根目录 / desktop / frontend / src / lib / transcriptRecordProjection.ts
1 import { isShellToolName } from "./shellToolIdentity";
2 import { historyToolStatus } from "./historyToolStatus";
3 import { asArray } from "./array";
4 import { canonicalMessage } from "./canonicalTranscriptBackend";
5 import { historicalResultNotice } from "./completionResultState";
6 import { historyNoticeItems } from "./controllerNotices";
7 import { appendHistoryAttachmentRefs } from "./historyAttachmentRefs";
8 import { historySearchAndAnswer } from "./searchTranscript";
9 import { sessionOperationFromHistory, sessionOperationItem } from "./sessionMaintenanceOperation";
10 import { fileDiffFromWire, summarizeFileDiff } from "./tools";
11 import { historyToolError, isReadOnlyTool, type Item } from "./useController";
12 import type { HistoryContentRef, HistoryEntry, HistoryMessage, MemoryCitation } from "./types";
13 import { recordBytes } from "./transcriptRecordBytes";
14
15 export interface TranscriptRecord {
16 entryId: string;
17 turn: number;
18 order: number;
19 message: HistoryMessage;
20 refs: HistoryContentRef[];
21 resolved?: Record<string, string>;
22 previewMessage?: HistoryMessage;
23 staleRefs?: Record<string, true>;
24 bytes: number;
25 }
26
27 export interface RecordConversion {
28 items: Item[];
29 claims: string[];
30 unresolvedIds: string[];
31 pendingPositional: number[];
32 matches: Map<number, string>;
33 }
34
35 export interface ToolProjectionView {
36 records: TranscriptRecord[];
37 indexOf: Map<string, number>;
38 toolResultOwners: Map<string, string>;
39 toolCallOwners: Map<string, string>;
40 toolCallDisplayIds: Map<string, string>;
41 toolDisplayIds: Map<string, string>;
42 toolIdentityConflicts: Set<string>;
43 suppressedToolResults: Set<string>;
44 claimedToolResults: Set<string>;
45 }
46
47 export function toolCallKey(entryId: string, callIndex: number): string {
48 return `${entryId}\u0000${callIndex}`;
49 }
50
51 export function entryToRecord(entry: HistoryEntry): TranscriptRecord {
52 return {
53 entryId: entry.entryId,
54 turn: entry.turn,
55 order: entry.order,
56 message: entry.message,
57 refs: asArray<HistoryContentRef>(entry.refs),
58 bytes: recordBytes(entry.message),
59 };
60 }
61
62 export function itemIdForToolCall(toolCallId: string, fallback: string): string {
63 return toolCallId || fallback;
64 }
65
66 /** Converts one record against the complete resident window. */
67 export function convertRecord(
68 rec: TranscriptRecord,
69 view: ToolProjectionView,
70 consumed: Set<string>,
71 priorMatches?: Map<number, string>,
72 ): RecordConversion {
73 if (view.suppressedToolResults.has(rec.entryId) || view.claimedToolResults.has(rec.entryId)) {
74 return { items: [], claims: [], unresolvedIds: [], pendingPositional: [], matches: new Map() };
75 }
76 const converted = convertRecordBody(rec, view, consumed, priorMatches);
77 if (rec.message.turnId) converted.items = converted.items.map(item => ({ ...item, turnId: rec.message.turnId }));
78 return converted;
79 }
80
81 function convertRecordBody(
82 rec: TranscriptRecord,
83 view: ToolProjectionView,
84 consumed: Set<string>,
85 priorMatches?: Map<number, string>,
86 ): RecordConversion {
87 const items: Item[] = [];
88 const claims: string[] = [];
89 const unresolvedIds: string[] = [];
90 const pendingPositional: number[] = [];
91 const matches = new Map<number, string>(priorMatches);
92 const message = rec.message;
93 const id = message.messageId && (message.role === "assistant" || message.role === "user") ? `m:${message.messageId}` : `he:${rec.entryId}`;
94
95 if (message.role === "system") return { items, claims, unresolvedIds, pendingPositional, matches };
96 if (message.role === "phase") {
97 if (message.content.trim() !== "") items.push({ kind: "phase", id, text: message.content });
98 return { items, claims, unresolvedIds, pendingPositional, matches };
99 }
100 if (message.role === "notice") {
101 if (message.completionReceipt || message.completionSummary) {
102 const result = historicalResultNotice(message, id);
103 if (result) items.push(result);
104 return { items, claims, unresolvedIds, pendingPositional, matches };
105 }
106 return { items: historyNoticeItems(message, id), claims, unresolvedIds, pendingPositional, matches };
107 }
108 if (message.role === "compaction") {
109 const operation = sessionOperationFromHistory(message);
110 if (operation) {
111 items.push(sessionOperationItem(operation, rec.entryId));
112 return { items, claims, unresolvedIds, pendingPositional, matches };
113 }
114 items.push({
115 kind: "compaction", id, pending: Boolean(message.pending), trigger: message.trigger ?? "",
116 messages: message.messages ?? 0, summary: message.summary ?? "", archive: message.archive ?? "",
117 operationId: message.operationId, operationKind: message.operationKind,
118 status: message.operationStatus, activity: message.operationActivity,
119 errorCode: message.errorCode, detail: message.detail, applied: message.applied,
120 inputTokens: message.inputTokens, resultTokens: message.resultTokens,
121 });
122 return { items, claims, unresolvedIds, pendingPositional, matches };
123 }
124 if (message.role === "user") {
125 if (message.content.trim() !== "") {
126 items.push({ kind: "user", id, messageId: message.messageId, submissionId: message.submissionId,
127 text: appendHistoryAttachmentRefs(message.content, message.attachments), submitText: message.submitText, createdAt: message.createdAt,
128 checkpointTurn: message.checkpointTurn, historyTurn: rec.turn > 0 ? rec.turn : undefined });
129 }
130 return { items, claims, unresolvedIds, pendingPositional, matches };
131 }
132 if (message.role === "assistant") {
133 const memoryCitations = asArray<MemoryCitation>(message.memoryCitations);
134 items.push(...historySearchAndAnswer(id, {
135 content: message.content, reasoning: message.reasoning, workDurationMs: message.workDurationMs,
136 turnFinal: message.turnFinal, samplingCount: message.samplingCount, toolCount: message.toolCount, turnDurationMs: message.turnDurationMs, turnUsage: message.turnUsage, createdAt: message.createdAt,
137 memoryCitations: memoryCitations.length > 0 ? memoryCitations : undefined, serverSearch: message.serverSearch,
138 }, rec.refs.length > 0));
139 const toolCalls = message.toolCalls ?? [];
140 let scan = (view.indexOf.get(rec.entryId) ?? -1) + 1;
141 for (let callIndex = 0; callIndex < toolCalls.length; callIndex += 1) {
142 const toolCall = toolCalls[callIndex];
143 let result: HistoryMessage | undefined;
144 let resultEntryId: string | undefined;
145 const prior = matches.get(callIndex);
146 if (prior) {
147 resultEntryId = prior;
148 result = view.records[view.indexOf.get(prior) ?? -1]?.message;
149 } else if (toolCall.id) {
150 const owner = view.toolCallOwners.get(toolCallKey(rec.entryId, callIndex))
151 ?? (toolCall.resultObservation?.messageId ? undefined : view.toolResultOwners.get(toolCall.id));
152 if (owner) {
153 resultEntryId = owner;
154 result = view.records[view.indexOf.get(owner) ?? -1]?.message;
155 } else unresolvedIds.push(toolCall.id);
156 } else {
157 while (scan < view.records.length) {
158 const candidate = view.records[scan];
159 if (candidate.message.role !== "tool") break;
160 scan += 1;
161 if (candidate.message.toolCallId || consumed.has(candidate.entryId)) continue;
162 resultEntryId = candidate.entryId;
163 result = candidate.message;
164 break;
165 }
166 if (!resultEntryId) pendingPositional.push(callIndex);
167 }
168 if (resultEntryId) {
169 matches.set(callIndex, resultEntryId);
170 claims.push(resultEntryId);
171 consumed.add(resultEntryId);
172 }
173 const archived = Boolean(toolCall.argumentsArchived || result?.toolResultArchived || (!result && toolCall.resultObservation?.contentRef));
174 const output = result?.toolResultArchived ? undefined : result?.content ?? "";
175 const error = result?.toolResultError || (output ? historyToolError(output) : undefined);
176 const fileDiff = fileDiffFromWire(toolCall);
177 items.push({
178 kind: "tool", id: view.toolCallDisplayIds.get(toolCallKey(rec.entryId, callIndex))
179 ?? itemIdForToolCall(toolCall.id, `he:${rec.entryId}:tc${callIndex}`), name: toolCall.name,
180 args: toolCall.arguments ?? "", readOnly: typeof toolCall.resolvedReadOnly === "boolean" ? toolCall.resolvedReadOnly : isReadOnlyTool(toolCall.name),
181 resolvedName: toolCall.resolvedName, capabilityId: toolCall.capabilityId, parentId: toolCall.parentId,
182 status: historyToolStatus(result, toolCall, error), contentState: !result || archived ? "unloaded" : "ready",
183 resultMissing: !result && !toolCall.resultObservation?.messageId || undefined,
184 resultEvidence: result ? "formal" : toolCall.resultObservation ? "observation" : "missing",
185 sourceEntryId: rec.entryId,
186 identityConflict: view.toolIdentityConflicts.has(toolCallKey(rec.entryId, callIndex)) || undefined,
187 output, error, dataArchived: archived || undefined,
188 subject: toolCall.subject, summary: summarizeFileDiff(fileDiff) || toolCall.summary, fileDiff,
189 isShell: isShellToolName(toolCall.name) || (toolCall.id || "").startsWith("shell-"), execution: result?.execution,
190 presentedFiles: result?.presentedFiles,
191 });
192 }
193 return { items, claims, unresolvedIds, pendingPositional, matches };
194 }
195 if (message.role === "tool") {
196 if (consumed.has(rec.entryId)) return { items, claims, unresolvedIds, pendingPositional, matches };
197 const output = message.toolResultArchived ? undefined : message.content;
198 const error = message.toolResultError || (output ? historyToolError(output) : undefined);
199 items.push({
200 kind: "tool", id: view.toolDisplayIds.get(rec.entryId) ?? itemIdForToolCall(message.toolCallId ?? "", id), name: message.toolName || "tool", args: "",
201 readOnly: isReadOnlyTool(message.toolName || "tool"), status: error ? "error" : "done", output, error,
202 resultEvidence: "formal", sourceEntryId: rec.entryId,
203 identityConflict: view.toolIdentityConflicts.has(rec.entryId) || undefined,
204 dataArchived: message.toolResultArchived || undefined, isShell: isShellToolName(message.toolName || "") || (message.toolCallId || "").startsWith("shell-"),
205 execution: message.execution, presentedFiles: message.presentedFiles,
206 });
207 }
208 return { items, claims, unresolvedIds, pendingPositional, matches };
209 }
210
211 export function applyResolvedField(rec: TranscriptRecord, ref: HistoryContentRef, data: string): boolean {
212 const message = rec.message;
213 switch (ref.field) {
214 case "canonicalMessage": {
215 const bytes = Uint8Array.from(data, character => character.charCodeAt(0));
216 const decoded = canonicalMessage(
217 { messageId: rec.entryId, submissionId: message.submissionId, position: rec.turn, version: 1, role: message.role, eventSequence: 0, visibleTurn: rec.turn, turnFinal: message.turnFinal, toolObservations: Object.fromEntries((message.toolCalls ?? []).flatMap(call => call.resultObservation ? [[call.id, call.resultObservation]] : [])), turnDurationMs: message.turnDurationMs, samplingCount: message.samplingCount, toolCount: message.toolCount },
218 JSON.parse(new TextDecoder().decode(bytes)),
219 );
220 rec.message = { ...message, ...decoded };
221 return true;
222 }
223 case "content": rec.message = { ...message, content: data }; return true;
224 case "reasoning": rec.message = { ...message, reasoning: data }; return true;
225 case "submitText": rec.message = { ...message, submitText: data }; return true;
226 case "detail": rec.message = { ...message, detail: data }; return true;
227 case "code": rec.message = { ...message, code: data }; return true;
228 case "summary": rec.message = { ...message, summary: data }; return true;
229 case "archive": rec.message = { ...message, archive: data }; return true;
230 case "toolResultError": rec.message = { ...message, toolResultError: data }; return true;
231 case "toolArguments":
232 case "toolSubject":
233 case "toolSummary":
234 case "toolDiff": {
235 rec.message = { ...message, toolCalls: (message.toolCalls ?? []).map((toolCall) => {
236 if (toolCall.id !== ref.toolCallId) return toolCall;
237 if (ref.field === "toolArguments") return { ...toolCall, arguments: data };
238 if (ref.field === "toolSubject") return { ...toolCall, subject: data };
239 if (ref.field === "toolSummary") return { ...toolCall, summary: data };
240 return { ...toolCall, diff: data };
241 }) };
242 return true;
243 }
244 default: return false;
245 }
246 }
247
247 lines TYPESCRIPT