/** * Agentic tool-execution loop for the memory-flush reflection turn. * * Extracted from {@link runMemoryFlush} so the loop primitive is * testable in isolation and reusable from any reflection-style phase * that wants "invoke → execute tool_calls → feed results back → repeat". * * Design: * - Bounded iterations — caller passes `maxIterations` (default * {@link DEFAULT_MAX_FLUSH_ITERATIONS}). The loop exits as soon as * the model stops emitting `tool_calls`, hits the cap, or the reply * matches {@link SILENT_REPLY_TOKEN}. * - Rich result — returns iteration count, attempted/successful * appends, per-tool errors, silent-reply flag, and raw final text. * The caller logs this; nothing is swallowed. * - Self-contained tool execution — does not depend on LangGraph's * ToolNode or any graph runtime. The only contract is the * LangChain v0.3 `StructuredToolInterface.invoke(args)` shape. * - Tool errors are captured as {@link ToolMessage}s with the raw * JSON error, so the model can read them and self-correct (e.g. * retry with a different path if the schema rejected its first * attempt). */ import { AIMessage, type BaseMessage } from '@langchain/core/messages'; import type { StructuredToolInterface } from '@langchain/core/tools'; /** Minimal model contract the loop needs. */ export interface InvokableModel { invoke(messages: BaseMessage[], options?: any): Promise; } export interface FlushLoopParams { model: InvokableModel; tools: StructuredToolInterface[]; /** Initial messages — typically [SystemMessage, HumanMessage]. */ initialMessages: BaseMessage[]; /** Upper bound on model.invoke() calls. Default 8. */ maxIterations?: number; /** * Optional debug hook — called once per iteration with `{ i, ai, toolCalls }`. * Use for INFO-level tracing during rollout; pass `undefined` in prod. */ onIteration?: (event: { i: number; ai: AIMessage; toolCalls: ToolCallLike[]; }) => void; } /** Shape LangChain emits on AIMessage.tool_calls (duck-typed). */ export interface ToolCallLike { name: string; args: Record; id?: string; } export interface ToolErrorRecord { iteration: number; toolName: string; toolCallId?: string; error: string; /** Raw args the model passed — useful for diagnosing schema drift. */ args?: Record; } export interface FlushLoopResult { /** Number of model.invoke() calls actually made. */ iterations: number; /** Every `memory_append` tool_call the model emitted across all iterations. */ appendsAttempted: number; /** How many of those returned `{ ok: true, ... }`. */ appendsSucceeded: number; /** Tool errors the model saw (and may have reacted to). */ toolErrors: ToolErrorRecord[]; /** True if the final AIMessage text matched {@link SILENT_REPLY_TOKEN}. */ silentReply: boolean; /** Whether the loop stopped because it hit `maxIterations`. */ hitIterationCap: boolean; /** Final AIMessage content as plain text (best-effort). */ finalText: string; /** Full message transcript — useful for debugging / tests. */ messages: BaseMessage[]; } /** * Extract a flat text string from any AIMessage content shape * (string, array of content blocks, etc). */ export declare function extractText(message: AIMessage): string; /** * Parse a tool result string. Memory tools return JSON with * `{ ok: boolean, error?: string, path?: string }`. Non-JSON payloads * are treated as opaque success strings. */ export declare function parseToolResult(raw: string): { ok: boolean; error?: string; path?: string; }; /** * Bind a tool array to a model if `bindTools` exists on the model. * Exported so the caller (runMemoryFlush) can do it once and pass the * bound model in, keeping this loop free of LangChain model-specific * extensions. */ export declare function bindToolsIfSupported(model: any, tools: StructuredToolInterface[]): InvokableModel; /** * Run the agentic reflection loop until the model stops emitting * tool_calls, the iteration cap is hit, or the final reply matches * {@link SILENT_REPLY_TOKEN}. */ export declare function runFlushLoop(params: FlushLoopParams): Promise;