| 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 |