/** * Session tool-result guard — wraps a pi `SessionManager.appendMessage` to: * 1. cap oversized tool-result text + details so they cannot blow up the next * LLM request (size + persistence limits). * 2. track pending tool-call IDs so missing tool results can be synthesised * (some providers refuse a turn that has an orphan tool_use block). * 3. drop assistant `toolCall` blocks whose tool name is not in the allowlist * (these would also trigger provider 400s). * 4. broadcast `xopc:transcript-row` updates so the gateway UI can stream them. * * Previously this module shipped with three sibling files * (`session-tool-result-state.ts`, `session-raw-append-message.ts`, * `session-tool-result-guard-wrapper.ts`). They are now consolidated here as * private constructs around the `ToolResultGuard` class. Pi-coding-agent owns * the `SessionManager` instance and calls `appendMessage` from inside the * runtime, so we still need to monkey-patch that method — but the patched * implementation is just `guard.guardedAppend.bind(guard)` and all state lives * on the class. */ import type { AgentMessage } from '@earendil-works/pi-agent-core'; import type { SessionManager } from '@earendil-works/pi-coding-agent'; import type { Config } from '../../config/schema.js'; export type BeforeMessageWriteHookEvent = { message: AgentMessage; }; export type BeforeMessageWriteHookResult = { block?: boolean; message?: AgentMessage; } | undefined; export interface ToolResultGuardOptions { /** Optional session key for transcript update broadcasts. */ sessionKey?: string; /** Optional transform applied to any message before persistence. */ transformMessageForPersistence?: (message: AgentMessage) => AgentMessage; /** * Optional, synchronous transform applied to toolResult messages *before* they are * persisted to the session transcript. */ transformToolResultForPersistence?: (message: AgentMessage, meta: { toolCallId?: string; toolName?: string; isSynthetic?: boolean; }) => AgentMessage; /** * Whether to synthesize missing tool results to satisfy strict providers. * Defaults to true. */ allowSyntheticToolResults?: boolean; missingToolResultText?: string; /** * Optional set/list of tool names accepted for assistant toolCall/toolUse blocks. * When set, tool calls with unknown names are dropped before persistence. */ allowedToolNames?: Iterable; /** * Synchronous hook invoked before any message is written to the session JSONL. * If the hook returns { block: true }, the message is silently dropped. * If it returns { message }, the modified message is written instead. */ beforeMessageWriteHook?: (event: BeforeMessageWriteHookEvent) => BeforeMessageWriteHookResult; maxToolResultChars?: number; } export interface InstallSessionToolResultGuardResult { flushPendingToolResults: () => void; clearPendingToolResults: () => void; getPendingIds: () => string[]; setActiveTurnId: (turnId: string | null) => void; } /** Idempotent wrapper that also adds the helper methods consumers expect. */ export type GuardedPiTranscriptManager = SessionManager & { flushPendingToolResults?: () => void; clearPendingToolResults?: () => void; setActiveTurnId?: (turnId: string | null) => void; }; /** * Install the guard on a SessionManager and return its control API. * Subsequent assistant/toolResult writes by pi-coding-agent flow through the * guard transparently. */ export declare function installSessionToolResultGuard(sessionManager: SessionManager, opts?: ToolResultGuardOptions): InstallSessionToolResultGuardResult; /** * Convenience wrapper used by the embedded runner pool: install the guard * (idempotent), pin the size cap from the model context window, and expose * `flushPendingToolResults` / `clearPendingToolResults` directly on the * SessionManager instance so callers do not need to keep the install result. */ export declare function guardSessionManager(sessionManager: SessionManager, opts?: { agentId?: string; sessionKey?: string; config?: Config; contextWindowTokens?: number; allowSyntheticToolResults?: boolean; missingToolResultText?: string; allowedToolNames?: Iterable; transformMessageForPersistence?: (message: AgentMessage) => AgentMessage; }): GuardedPiTranscriptManager; /** * Recover the original (un-guarded) appendMessage for a session manager. * Useful for callers that need a low-level "bypass the guard" write path. */ export declare function getRawSessionAppendMessage(sessionManager: SessionManager): SessionManager['appendMessage'];