UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

127 lines • 5.66 kB
/** * transcript — the conversation, read back out of the event stream. * * A maintainer opening a bug report reads the conversation first: what was * asked, what the model said, which tools ran, what came back. All of that is * already in the typed events — `agent.turn_start` carries the prompt, * `stream.llm_end` the model's content, `stream.tool_start` / `tool_end` the * call and its result, `agent.turn_end` the final answer — so `conversation.json` * is DERIVED, never a second copy the reporter has to keep in sync. * * ## Derivable, and when it is not * * Turn markers come from the Agent loop. A bare `LLMCall`, a `Sequence` of * them, a pattern — none of those fire `turn_start`, so the walk falls back to * ONE synthetic turn holding the model and tool steps in order, and says so on * the turn (`derived: 'no-turn-markers'`). A recording with no LLM and no tool * events yields no transcript at all, and the manifest carries a note rather * than an empty file that reads like an empty conversation. * * ## What it does NOT do * * It does not summarize, truncate or scrub. Whatever the events carry is what * this writes — including tool arguments and results. Redaction is upstream: * under an agent's `redact` every event was served before it was recorded (a * selected key at any depth is the placeholder), and the manifest lists the * keys that were scrubbed so the reporter can see it happened. The policy * selects by name, never by content — free text under a name it does not * select (a user's sentence, a model's answer) is written as it was. */ const asRecord = (value) => typeof value === 'object' && value !== null ? value : {}; const asString = (value) => typeof value === 'string' ? value : undefined; /** * Walk one conversation's events into turns. * * @returns the transcript, or `undefined` when nothing conversational * happened — which the manifest reports as a note. */ export function deriveTranscript(events) { const turns = []; let open; /** `tool_start` args, held until the matching `tool_end` names the result. */ const pendingTools = new Map(); /** Open a turn the Agent never announced — an LLMCall or a composition. */ const ensureTurn = () => { if (open) return open; open = { index: turns.length, steps: [], derived: 'no-turn-markers' }; turns.push(open); return open; }; for (const raw of events) { const event = raw; const type = asString(event?.type); if (!type) continue; const payload = asRecord(event.payload); switch (type) { case 'agentfootprint.agent.turn_start': { open = { index: typeof payload.turnIndex === 'number' ? payload.turnIndex : turns.length, steps: [], ...(asString(payload.userPrompt) !== undefined && { user: asString(payload.userPrompt), }), }; turns.push(open); break; } case 'agentfootprint.agent.turn_end': { const turn = ensureTurn(); const final = asString(payload.finalContent); if (final !== undefined) turn.final = final; open = undefined; break; } case 'agentfootprint.stream.llm_end': { const turn = ensureTurn(); turn.steps.push({ kind: 'assistant', content: asString(payload.content) ?? '', ...(asString(payload.stopReason) !== undefined && { stopReason: asString(payload.stopReason), }), ...(typeof payload.toolCallCount === 'number' && { toolCallCount: payload.toolCallCount, }), }); break; } case 'agentfootprint.stream.tool_start': { const id = asString(payload.toolCallId); const name = asString(payload.toolName) ?? 'unknown-tool'; if (id) pendingTools.set(id, { name, args: payload.args }); break; } case 'agentfootprint.stream.tool_end': { const turn = ensureTurn(); const id = asString(payload.toolCallId); const started = id ? pendingTools.get(id) : undefined; if (id) pendingTools.delete(id); turn.steps.push({ kind: 'tool', name: started?.name ?? 'unknown-tool', ...(id !== undefined && { toolCallId: id }), ...(started !== undefined && { args: started.args }), ...('result' in payload && { result: payload.result }), ...(payload.error === true && { error: true }), ...(payload.notDispatched !== undefined && { notDispatched: payload.notDispatched }), }); break; } default: break; } } if (turns.length === 0) return undefined; // A single inferred turn with nothing in it is not a conversation. if (turns.length === 1 && turns[0].steps.length === 0 && turns[0].user === undefined) { return undefined; } return { turns: turns.map((turn) => ({ ...turn, steps: [...turn.steps] })) }; } //# sourceMappingURL=transcript.js.map