返回 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 { fileDiffFromWire, summarizeFileDiff } from "./tools";
10 import { historyToolError, isReadOnlyTool, type Item } from "./useController";
11 import type { HistoryContentRef, HistoryEntry, HistoryMessage, MemoryCitation } from "./types";
12 import { recordBytes } from "./transcriptRecordBytes";
13
14 export interface TranscriptRecord {
15 entryId: string;
16 turn: number;
17 order: number;
18 message: HistoryMessage;
19 refs: HistoryContentRef[];
20 resolved?: Record<string, string>;
21 staleRefs?: Record<string, true>;
22 bytes: number;
23 }
24
25 export interface RecordConversion {
26 items: Item[];
27 claims: string[];
28 unresolvedIds: string[];
29 pendingPositional: number[];
30 matches: Map<number, string>;
31 }
32
33 export function entryToRecord(entry: HistoryEntry): TranscriptRecord {
34 return {
35 entryId: entry.entryId,
36 turn: entry.turn,
37 order: entry.order,
38 message: entry.message,
39 refs: asArray<HistoryContentRef>(entry.refs),
40 bytes: recordBytes(entry.message),
41 };
42 }
43
44 export function itemIdForToolCall(toolCallId: string, fallback: string): string {
45 return toolCallId || fallback;
46 }
47
48 /** Converts one record against the complete resident window. */
49 export function convertRecord(
50 rec: TranscriptRecord,
51 view: { records: TranscriptRecord[]; indexOf: Map<string, number>; toolResultOwners: Map<string, string> },
52 consumed: Set<string>,
53 priorMatches?: Map<number, string>,
54 ): RecordConversion {
55 const converted = convertRecordBody(rec, view, consumed, priorMatches);
56 if (rec.message.turnId) converted.items = converted.items.map(item => ({ ...item, turnId: rec.message.turnId }));
57 return converted;
58 }
59
60 function convertRecordBody(
61 rec: TranscriptRecord,
62 view: { records: TranscriptRecord[]; indexOf: Map<string, number>; toolResultOwners: Map<string, string> },
63 consumed: Set<string>,
64 priorMatches?: Map<number, string>,
65 ): RecordConversion {
66 const items: Item[] = [];
67 const claims: string[] = [];
68 const unresolvedIds: string[] = [];
69 const pendingPositional: number[] = [];
70 const matches = new Map<number, string>(priorMatches);
71 const message = rec.message;
72 const id = message.messageId && (message.role === "assistant" || message.role === "user") ? `m:${message.messageId}` : `he:${rec.entryId}`;
73
74 if (message.role === "system") return { items, claims, unresolvedIds, pendingPositional, matches };
75 if (message.role === "phase") {
76 if (message.content.trim() !== "") items.push({ kind: "phase", id, text: message.content });
77 return { items, claims, unresolvedIds, pendingPositional, matches };
78 }
79 if (message.role === "notice") {
80 if (message.completionReceipt || message.completionSummary) {
81 const result = historicalResultNotice(message, id);
82 if (result) items.push(result);
83 return { items, claims, unresolvedIds, pendingPositional, matches };
84 }
85 return { items: historyNoticeItems(message, id), claims, unresolvedIds, pendingPositional, matches };
86 }
87 if (message.role === "compaction") {
88 items.push({
89 kind: "compaction", id, pending: Boolean(message.pending), trigger: message.trigger ?? "",
90 messages: message.messages ?? 0, summary: message.summary ?? "", archive: message.archive ?? "",
91 });
92 return { items, claims, unresolvedIds, pendingPositional, matches };
93 }
94 if (message.role === "user") {
95 if (message.content.trim() !== "") {
96 items.push({ kind: "user", id, messageId: message.messageId, submissionId: message.submissionId,
97 text: appendHistoryAttachmentRefs(message.content, message.attachments), submitText: message.submitText, createdAt: message.createdAt,
98 checkpointTurn: message.checkpointTurn, historyTurn: rec.turn > 0 ? rec.turn : undefined });
99 }
100 return { items, claims, unresolvedIds, pendingPositional, matches };
101 }
102 if (message.role === "assistant") {
103 const memoryCitations = asArray<MemoryCitation>(message.memoryCitations);
104 items.push(...historySearchAndAnswer(id, {
105 content: message.content, reasoning: message.reasoning, workDurationMs: message.workDurationMs,
106 turnFinal: message.turnFinal, samplingCount: message.samplingCount, toolCount: message.toolCount, turnDurationMs: message.turnDurationMs, turnUsage: message.turnUsage, createdAt: message.createdAt,
107 memoryCitations: memoryCitations.length > 0 ? memoryCitations : undefined, serverSearch: message.serverSearch,
108 }, rec.refs.length > 0));
109 const toolCalls = message.toolCalls ?? [];
110 let scan = (view.indexOf.get(rec.entryId) ?? -1) + 1;
111 for (let callIndex = 0; callIndex < toolCalls.length; callIndex += 1) {
112 const toolCall = toolCalls[callIndex];
113 let result: HistoryMessage | undefined;
114 let resultEntryId: string | undefined;
115 const prior = matches.get(callIndex);
116 if (prior) {
117 resultEntryId = prior;
118 result = view.records[view.indexOf.get(prior) ?? -1]?.message;
119 } else if (toolCall.id) {
120 const owner = view.toolResultOwners.get(toolCall.id);
121 if (owner) {
122 resultEntryId = owner;
123 result = view.records[view.indexOf.get(owner) ?? -1]?.message;
124 } else unresolvedIds.push(toolCall.id);
125 } else {
126 while (scan < view.records.length) {
127 const candidate = view.records[scan];
128 if (candidate.message.role !== "tool") break;
129 scan += 1;
130 if (candidate.message.toolCallId || consumed.has(candidate.entryId)) continue;
131 resultEntryId = candidate.entryId;
132 result = candidate.message;
133 break;
134 }
135 if (!resultEntryId) pendingPositional.push(callIndex);
136 }
137 if (resultEntryId) {
138 matches.set(callIndex, resultEntryId);
139 claims.push(resultEntryId);
140 consumed.add(resultEntryId);
141 }
142 const archived = Boolean(toolCall.argumentsArchived || result?.toolResultArchived || (!result && toolCall.resultObservation?.contentRef));
143 const output = result?.toolResultArchived ? undefined : result?.content ?? "";
144 const error = result?.toolResultError || (output ? historyToolError(output) : undefined);
145 const fileDiff = fileDiffFromWire(toolCall);
146 items.push({
147 kind: "tool", id: itemIdForToolCall(toolCall.id, `he:${rec.entryId}:tc${callIndex}`), name: toolCall.name,
148 args: toolCall.arguments ?? "", readOnly: typeof toolCall.resolvedReadOnly === "boolean" ? toolCall.resolvedReadOnly : isReadOnlyTool(toolCall.name),
149 resolvedName: toolCall.resolvedName, capabilityId: toolCall.capabilityId,
150 status: historyToolStatus(result, toolCall, error), contentState: !result || archived ? "unloaded" : "ready", resultMissing: !result && !toolCall.resultObservation?.messageId || undefined, output, error, dataArchived: archived || undefined,
151 subject: toolCall.subject, summary: summarizeFileDiff(fileDiff) || toolCall.summary, fileDiff,
152 isShell: isShellToolName(toolCall.name) || (toolCall.id || "").startsWith("shell-"), execution: result?.execution,
153 presentedFiles: result?.presentedFiles,
154 });
155 }
156 return { items, claims, unresolvedIds, pendingPositional, matches };
157 }
158 if (message.role === "tool") {
159 if (consumed.has(rec.entryId)) return { items, claims, unresolvedIds, pendingPositional, matches };
160 const output = message.toolResultArchived ? undefined : message.content;
161 const error = message.toolResultError || (output ? historyToolError(output) : undefined);
162 items.push({
163 kind: "tool", id: itemIdForToolCall(message.toolCallId ?? "", id), name: message.toolName || "tool", args: "",
164 readOnly: isReadOnlyTool(message.toolName || "tool"), status: error ? "error" : "done", output, error,
165 dataArchived: message.toolResultArchived || undefined, isShell: isShellToolName(message.toolName || "") || (message.toolCallId || "").startsWith("shell-"),
166 execution: message.execution, presentedFiles: message.presentedFiles,
167 });
168 }
169 return { items, claims, unresolvedIds, pendingPositional, matches };
170 }
171
172 export function applyResolvedField(rec: TranscriptRecord, ref: HistoryContentRef, data: string): boolean {
173 const message = rec.message;
174 switch (ref.field) {
175 case "canonicalMessage": {
176 const bytes = Uint8Array.from(data, character => character.charCodeAt(0));
177 const decoded = canonicalMessage(
178 { 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 },
179 JSON.parse(new TextDecoder().decode(bytes)),
180 );
181 rec.message = { ...message, ...decoded };
182 return true;
183 }
184 case "content": rec.message = { ...message, content: data }; return true;
185 case "reasoning": rec.message = { ...message, reasoning: data }; return true;
186 case "submitText": rec.message = { ...message, submitText: data }; return true;
187 case "detail": rec.message = { ...message, detail: data }; return true;
188 case "code": rec.message = { ...message, code: data }; return true;
189 case "summary": rec.message = { ...message, summary: data }; return true;
190 case "archive": rec.message = { ...message, archive: data }; return true;
191 case "toolResultError": rec.message = { ...message, toolResultError: data }; return true;
192 case "toolArguments":
193 case "toolSubject":
194 case "toolSummary":
195 case "toolDiff": {
196 rec.message = { ...message, toolCalls: (message.toolCalls ?? []).map((toolCall) => {
197 if (toolCall.id !== ref.toolCallId) return toolCall;
198 if (ref.field === "toolArguments") return { ...toolCall, arguments: data };
199 if (ref.field === "toolSubject") return { ...toolCall, subject: data };
200 if (ref.field === "toolSummary") return { ...toolCall, summary: data };
201 return { ...toolCall, diff: data };
202 }) };
203 return true;
204 }
205 default: return false;
206 }
207 }
208
208 lines TYPESCRIPT