返回 DeepSeek-Reasonix
transcriptToolAssociation.ts
根目录 / desktop / frontend / src / lib / transcriptToolAssociation.ts
1 import { toolCallKey, type ToolProjectionView, type TranscriptRecord } from "./transcriptRecordProjection";
2
3 /** Build the bounded resident window's explicit call/result association. */
4 export function buildToolProjectionView(records: TranscriptRecord[], stableDisplayIds = new Map<string, string>(),
5 stableCallDisplayIds = new Map<string, string>()): ToolProjectionView {
6 const indexOf = new Map<string, number>();
7 const resultsByMessageId = new Map<string, TranscriptRecord>();
8 const observedCallIds = new Map<string, Set<string>>();
9 const callsById = new Map<string, Array<{ record: TranscriptRecord; index: number }>>();
10 const toolResultOwners = new Map<string, string>();
11 const toolCallOwners = new Map<string, string>();
12 const toolCallDisplayIds = new Map(stableCallDisplayIds);
13 const toolDisplayIds = new Map(stableDisplayIds);
14 const toolIdentityConflicts = new Set<string>();
15 const suppressedToolResults = new Set<string>();
16 const claimedToolResults = new Set<string>();
17 records.forEach((record, recordIndex) => {
18 indexOf.set(record.entryId, recordIndex);
19 if (record.message.role === "tool" && record.message.messageId) {
20 const current = resultsByMessageId.get(record.message.messageId);
21 if (!current || record.entryId === `m:${record.message.messageId}`) resultsByMessageId.set(record.message.messageId, record);
22 }
23 if (record.message.role !== "assistant") return;
24 (record.message.toolCalls ?? []).forEach((call, callIndex) => {
25 if (call.id) {
26 const calls = callsById.get(call.id) ?? [];
27 calls.push({ record, index: callIndex });
28 callsById.set(call.id, calls);
29 }
30 const messageId = call.resultObservation?.messageId;
31 if (!messageId || !call.id) return;
32 const ids = observedCallIds.get(messageId) ?? new Set<string>();
33 ids.add(call.id);
34 observedCallIds.set(messageId, ids);
35 });
36 });
37 const usedCallDisplayIds = new Set(toolCallDisplayIds.values());
38 for (const [callId, calls] of callsById) {
39 for (const { record, index } of calls) {
40 const key = toolCallKey(record.entryId, index);
41 if (toolCallDisplayIds.has(key)) continue;
42 const displayId = !usedCallDisplayIds.has(callId)
43 ? callId
44 : `${callId}:call:${encodeURIComponent(record.message.messageId || record.entryId)}:${index}`;
45 toolCallDisplayIds.set(key, displayId);
46 usedCallDisplayIds.add(displayId);
47 }
48 }
49
50 const compatible = (call: TranscriptRecord, result: TranscriptRecord): boolean => {
51 if (call.message.turnId && result.message.turnId && call.message.turnId !== result.message.turnId) return false;
52 return !(call.turn > 0 && result.turn > 0 && call.turn !== result.turn);
53 };
54 const effectiveCallId = (result: TranscriptRecord): string | undefined => {
55 if (result.message.role !== "tool") return undefined;
56 if (result.message.toolCallId) return result.message.toolCallId;
57 const observed = result.message.messageId ? observedCallIds.get(result.message.messageId) : undefined;
58 return observed?.size === 1 ? [...observed][0] : undefined;
59 };
60 const resultsByCall = new Map<string, TranscriptRecord[]>();
61 for (const result of records) {
62 const callId = effectiveCallId(result);
63 if (!callId) continue;
64 const group = resultsByCall.get(callId) ?? [];
65 group.push(result);
66 resultsByCall.set(callId, group);
67 }
68
69 const canonicalResultsByCall = new Map<string, TranscriptRecord[]>();
70 for (const [callId, group] of resultsByCall) {
71 const formalByMessage = new Map<string, TranscriptRecord>();
72 for (const result of group) {
73 const messageId = result.message.messageId;
74 if (!messageId) continue;
75 const current = formalByMessage.get(messageId);
76 if (!current) { formalByMessage.set(messageId, result); continue; }
77 const preferResult = result.entryId === `m:${messageId}` && current.entryId !== `m:${messageId}`;
78 const kept = preferResult ? result : current;
79 const duplicate = preferResult ? current : result;
80 formalByMessage.set(messageId, kept);
81 suppressedToolResults.add(duplicate.entryId);
82 }
83 const formal = [...formalByMessage.values()];
84 const aliases = group.filter(result => !result.message.messageId);
85 canonicalResultsByCall.set(callId, [...formal, ...aliases]);
86 const calls = callsById.get(callId) ?? [];
87 if (formal.length === 1) toolResultOwners.set(callId, formal[0].entryId);
88 else if (formal.length === 0 && aliases.length === 1) toolResultOwners.set(callId, aliases[0].entryId);
89
90 for (const alias of formal.length > 0 ? aliases : []) {
91 if (formal.some(owner => (!alias.turn || !owner.turn || alias.turn === owner.turn)
92 && (!alias.message.turnId || !owner.message.turnId || alias.message.turnId === owner.message.turnId))) {
93 suppressedToolResults.add(alias.entryId);
94 }
95 }
96
97 const ambiguousCalls = calls.filter(({ record, index }) => {
98 const observation = record.message.toolCalls?.[index]?.resultObservation?.messageId;
99 return !observation && formal.filter(result => compatible(record, result)).length > 1;
100 });
101 const identityConflict = formal.length > 1 && (calls.length === 0 || ambiguousCalls.length > 0);
102 if (identityConflict) {
103 for (const result of formal) toolIdentityConflicts.add(result.entryId);
104 for (const call of ambiguousCalls) toolIdentityConflicts.add(toolCallKey(call.record.entryId, call.index));
105 }
106
107 for (let index = 0; index < formal.length; index += 1) {
108 const result = formal[index];
109 let displayId = toolDisplayIds.get(result.entryId);
110 const callWillOwnBaseId = identityConflict && calls.length > 0;
111 if (!displayId || (callWillOwnBaseId && displayId === callId)) {
112 displayId = formal.length > 1 && (callWillOwnBaseId || index > 0)
113 ? `${callId}:conflict:${encodeURIComponent(result.message.messageId || result.entryId)}`
114 : callId;
115 }
116 toolDisplayIds.set(result.entryId, displayId);
117 }
118 for (const alias of aliases) if (!toolDisplayIds.has(alias.entryId)) toolDisplayIds.set(alias.entryId, callId);
119 }
120
121 for (const [callId, calls] of callsById) {
122 const group = canonicalResultsByCall.get(callId) ?? [];
123 for (const { record, index } of calls) {
124 const call = record.message.toolCalls?.[index];
125 if (!call) continue;
126 let owner: TranscriptRecord | undefined;
127 if (call.resultObservation?.messageId) {
128 const explicit = resultsByMessageId.get(call.resultObservation.messageId);
129 if (explicit && compatible(record, explicit)) owner = explicit;
130 } else {
131 const candidates = group.filter(result => compatible(record, result));
132 const formal = candidates.filter(result => Boolean(result.message.messageId));
133 if (formal.length === 1) owner = formal[0];
134 else if (formal.length === 0 && candidates.length === 1) owner = candidates[0];
135 }
136 if (owner) {
137 toolCallOwners.set(toolCallKey(record.entryId, index), owner.entryId);
138 claimedToolResults.add(owner.entryId);
139 }
140 }
141 }
142 for (const entryId of toolDisplayIds.keys()) if (!indexOf.has(entryId)) toolDisplayIds.delete(entryId);
143 const callKeys = new Set([...callsById.values()].flatMap(calls => calls.map(call => toolCallKey(call.record.entryId, call.index))));
144 for (const key of toolCallDisplayIds.keys()) if (!callKeys.has(key)) toolCallDisplayIds.delete(key);
145 return { records, indexOf, toolResultOwners, toolCallOwners, toolCallDisplayIds, toolDisplayIds,
146 toolIdentityConflicts, suppressedToolResults, claimedToolResults };
147 }
148
148 lines TYPESCRIPT