/** * Per-turn tool-call journal — derived (not separately recorded) from the * existing run-event ledger. Borrowed from Flue's durable-execution journal: * when an agent run resumes after an interruption (gateway/transport drop, cold * start, soft-timeout auto-continue), we classify the tool calls already in the * ledger so the resumed model does NOT re-execute side effects it already * completed (re-sending an email, re-creating a ticket) and so it is explicitly * told about any tool call that started but whose outcome was never recorded. * * IMPORTANT — this is a pure read-over-the-ledger view. There is no new * recording hook anywhere in the hot path. Classification uses input-aware * `tool_start` → `tool_done` matching when a `tool_done` event carries its * input, while preserving the legacy positional fallback that older ledger * events and `thread-data-builder.ts` rely on: * * - `tool_start` events carry `{ tool, input }` (no tool-call id at this * layer; the ledger event stream omits it). * - `tool_done` events carry `{ tool, input?, result }`. * - A `tool_done` with input is matched to the open start with the same tool * name and deep-canonicalized input. * - A legacy `tool_done` without input is matched to the OLDEST still-open * `tool_start` for the same tool name (FIFO per tool). * * A `tool_start` with no matching `tool_done` is the dangerous case: the call * began, its side effect may or may not have landed, and the interruption ate * the result. We surface those as "interrupted / unknown outcome" so the model * decides rather than blindly re-running. * * Two layers of protection are built on this: (1) a prompt-level note on resume * (see `run-loop-with-resume.ts`) telling the model what already completed, and * (2) tool-layer enforcement in `production-agent.ts`/`runToolCall`, which uses * `findCompletedJournalEntry(...)` to refuse re-executing a journaled-complete * write tool — returning the journaled result instead of running the side * effect again. Layer 2 is the stronger guarantee. */ import type { AgentChatEvent, AgentToolInput } from "./types.js"; /** A single recorded tool-call ledger entry, classified by outcome. */ export interface ToolCallJournalEntry { /** * Stable-ish identity for this tool call within the turn. The ledger event * stream has no tool-call id, so we key by tool name + a short input * signature + positional order, exactly enough to disambiguate repeats of the * same tool within one turn for human/model-readable reporting. */ key: string; /** Tool / action name (from the `tool_start` event). */ tool: string; /** Tool input captured at `tool_start`, if any. */ input?: AgentToolInput; /** 0-based position of the `tool_start` among all tool calls in the turn. */ order: number; /** Result text from the matched `tool_done`, when the call completed. */ result?: string; } /** Result of classifying one turn's ledger events. */ export interface ToolCallJournal { /** Tool calls with a matching `tool_done` — already ran, do NOT re-run. */ completed: ToolCallJournalEntry[]; /** * Tool calls with a `tool_start` but no matching `tool_done` — interrupted, * outcome unknown. The model must decide whether re-running is safe. */ interrupted: ToolCallJournalEntry[]; } /** * Classify a single turn's recorded events into completed vs interrupted tool * calls. Pure and side-effect free — given the same events it always returns * the same journal. A `clear` event resets the per-turn tally exactly as the * thread rebuild does, so partial streamed output that was discarded on resume * doesn't leave phantom open tool calls. */ export declare function classifyToolCallJournal(events: readonly AgentChatEvent[]): ToolCallJournal; /** True when the journal has nothing worth telling a resuming model about. */ export declare function isJournalEmpty(journal: ToolCallJournal): boolean; /** * Find a COMPLETED journal entry that matches a tool call about to be * dispatched, by tool name + input signature (position-independent — a resumed * call may sit at a different order than the original). Used by the tool-layer * hard-block in production-agent.ts/runToolCall to skip re-executing a side * effect that already completed in a prior interrupted chunk: when this returns * an entry, the loop returns `entry.result` instead of running the action. * * Returns the FIRST unmatched completed entry for that (name + input); the * caller is expected to claim it (mark it consumed) so two identical fresh * calls in the same turn don't both short-circuit on one journaled completion. * Returns undefined when there is no completed entry for this exact call — * including every fresh call, which must execute normally. */ export declare function findCompletedJournalEntry(journal: ToolCallJournal, toolName: string, input: unknown, consumedKeys?: Set): ToolCallJournalEntry | undefined; /** * Build the structured resume note injected on auto-continue. Returns `null` * when there are no completed or interrupted tool calls to report, so the * caller can preserve the exact pre-existing "continue from where you left off" * behavior on normal resumes (no regression for turns with no tool activity). * * The note is intentionally a flat, model-readable block: a list of already-run * tool calls (with short results) the model must NOT re-run, and a separate list * of interrupted calls whose outcome is unknown for the model to decide on. */ export declare function buildResumeJournalNote(journal: ToolCallJournal): string | null; //# sourceMappingURL=tool-call-journal.d.ts.map