| 1 | import type { Item } from "./useController"; |
| 2 | import type { HistoryContentRef } from "./types"; |
| 3 | import type { TranscriptBackend, SessionTranscript } from "./transcriptStoreTypes"; |
| 4 | import { itemIdForToolCall } from "./transcriptRecordProjection"; |
| 5 | import { fileDiffFromWire } from "./tools"; |
| 6 | type ToolContentOwner = { sessions: Map<string, SessionTranscript>; backend: TranscriptBackend; requestFullContent(tabId: string, entryId: string, field: string): Promise<string | undefined> }; |
| 7 | export async function readTranscriptToolContent(owner: ToolContentOwner, tabId: string, item: Extract<Item, { kind: "tool" }>, value: Record<string, unknown>): Promise<string | undefined> { |
| 8 | const session = [...owner.sessions.values()].find(session => session.tabId === tabId && |
| 9 | [...session.contributions.values()].some(items => items.some(candidate => candidate.id === item.id))); |
| 10 | if (!session) return undefined; |
| 11 | const entryId = [...session.contributions].find(([, items]) => items.some(candidate => candidate.id === item.id))?.[0]; |
| 12 | const record = entryId && session.byId.get(entryId); |
| 13 | if (!record) return undefined; |
| 14 | let calls = record.message.toolCalls ?? []; |
| 15 | let callIndex = calls.findIndex((call, index) => itemIdForToolCall(call.id, `he:${record.entryId}:tc${index}`) === item.id); |
| 16 | let call = calls[callIndex]; |
| 17 | const resultId = session.matchTables.get(record.entryId)?.get(callIndex); |
| 18 | let result = resultId ? session.byId.get(resultId) : record.message.role === "tool" ? record : undefined; |
| 19 | if (record.refs.some(ref => ref.field === "canonicalMessage")) { |
| 20 | await owner.requestFullContent(tabId, record.entryId, "content"); |
| 21 | calls = record.message.toolCalls ?? []; |
| 22 | callIndex = calls.findIndex((candidate, index) => itemIdForToolCall(candidate.id, `he:${record.entryId}:tc${index}`) === item.id); |
| 23 | call = calls[callIndex]; |
| 24 | result = resultId ? session.byId.get(resultId) : record.message.role === "tool" ? record : undefined; |
| 25 | } |
| 26 | if (result?.refs.some(ref => ref.field === "canonicalMessage")) { |
| 27 | await owner.requestFullContent(tabId, result.entryId, "content"); |
| 28 | } |
| 29 | const generation = session.generation; |
| 30 | const refs = [ |
| 31 | ...record.refs.filter(ref => call && ref.toolCallId === call.id && (ref.field === "toolArguments" || ref.field === "toolDiff")), |
| 32 | ...(result?.refs.filter(ref => ref.field === "content" || ref.field === "toolResultError") ?? []), |
| 33 | ]; |
| 34 | if (refs.some(ref => ref.field === "toolArguments" || ref.field === "toolDiff") && !call?.id && calls.filter(call => !call.id).length > 1) throw new Error("Ambiguous legacy tool reference"); |
| 35 | const full: Record<string, unknown> = { ...value, execution: result?.message.execution ?? value.execution }; |
| 36 | if (call) full.args = call.arguments; |
| 37 | if (!result && call?.resultObservation?.contentRef) { |
| 38 | const contentRef = call.resultObservation.contentRef; |
| 39 | const ref: HistoryContentRef = { entryId: `m:${call.resultObservation.messageId}`, field: "canonicalMessage", size: contentRef.bytes, chunks: Math.ceil(contentRef.bytes / (1 << 20)), canonicalRef: contentRef, revision: session.revision, revKnown: true, digest: contentRef.digest }; |
| 40 | let bytes = ""; |
| 41 | for (let index = 0; index < ref.chunks; index++) { |
| 42 | const chunk = await owner.backend.HistoryContentForTab(tabId, ref, index); |
| 43 | if (owner.sessions.get(session.key) !== session || generation !== session.generation || chunk.stale) throw new Error("Tool reference expired; retry"); |
| 44 | bytes += chunk.data ?? ""; |
| 45 | } |
| 46 | if (bytes.length !== contentRef.bytes) throw new Error("Incomplete tool result content"); |
| 47 | const decoded = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(bytes, char => char.charCodeAt(0)))); |
| 48 | full.output = decoded.raw_content || decoded.content || ""; |
| 49 | full.execution = decoded.tool_execution ?? full.execution; |
| 50 | } |
| 51 | |
| 52 | for (const ref of refs) { |
| 53 | let data = ""; |
| 54 | for (let index = 0; index < Math.max(1, ref.chunks); index++) { |
| 55 | const chunk = await owner.backend.HistoryContentForTab(tabId, ref, index); |
| 56 | if (owner.sessions.get(session.key) !== session || generation !== session.generation || chunk.stale) throw new Error("Tool reference expired; retry"); |
| 57 | data += chunk.data ?? ""; |
| 58 | if (chunk.done) break; |
| 59 | } |
| 60 | if (new TextEncoder().encode(data).byteLength !== ref.size) throw new Error("Incomplete tool content"); |
| 61 | if (ref.field === "toolArguments") full.args = data; |
| 62 | else if (ref.field === "content") full.output = data; |
| 63 | else if (ref.field === "toolResultError") full.error = data; |
| 64 | else full.diff = call ? fileDiffFromWire({ ...call, diff: data }) ?? data : data; |
| 65 | } |
| 66 | return JSON.stringify(full, null, 2); |
| 67 | } |
| 68 |