@caupulican/pi-agent-core 0.81.5 → 0.81.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGN,WAAW,EAIX,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EAIZ,QAAQ,EACR,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAiBzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAwBzC;AAED,wBAAsB,YAAY,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAiBzB","sourcesContent":["/**\n * Agent loop that works with AgentMessage throughout.\n * Transforms to Message[] only at the LLM call boundary.\n */\n\nimport {\n\ttype AssistantMessage,\n\ttype Context,\n\tEventStream,\n\tstreamSimple,\n\ttype ToolResultMessage,\n\tvalidateToolArguments,\n} from \"@caupulican/pi-ai\";\nimport type {\n\tAgentContext,\n\tAgentEvent,\n\tAgentLoopConfig,\n\tAgentMessage,\n\tAgentTool,\n\tAgentToolCall,\n\tAgentToolResult,\n\tStreamFn,\n} from \"./types.ts\";\nimport { DEFAULT_MAX_STALL_TURNS } from \"./types.ts\";\n\nexport type AgentEventSink = (event: AgentEvent) => Promise<void> | void;\n\n/**\n * Start an agent loop with a new prompt message.\n * The prompt is added to the context and events are emitted for it.\n */\nexport function agentLoop(\n\tprompts: AgentMessage[],\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): EventStream<AgentEvent, AgentMessage[]> {\n\tconst stream = createAgentStream();\n\n\tvoid runAgentLoop(\n\t\tprompts,\n\t\tcontext,\n\t\tconfig,\n\t\tasync (event) => {\n\t\t\tstream.push(event);\n\t\t},\n\t\tsignal,\n\t\tstreamFn,\n\t).then((messages) => {\n\t\tstream.end(messages);\n\t});\n\n\treturn stream;\n}\n\n/**\n * Continue an agent loop from the current context without adding a new message.\n * Used for retries - context already has user message or tool results.\n *\n * **Important:** The last message in context must convert to a `user` or `toolResult` message\n * via `convertToLlm`. If it doesn't, the LLM provider will reject the request.\n * This cannot be validated here since `convertToLlm` is only called once per turn.\n */\nexport function agentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): EventStream<AgentEvent, AgentMessage[]> {\n\tif (context.messages.length === 0) {\n\t\tthrow new Error(\"Cannot continue: no messages in context\");\n\t}\n\n\tif (context.messages[context.messages.length - 1].role === \"assistant\") {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst stream = createAgentStream();\n\n\tvoid runAgentLoopContinue(\n\t\tcontext,\n\t\tconfig,\n\t\tasync (event) => {\n\t\t\tstream.push(event);\n\t\t},\n\t\tsignal,\n\t\tstreamFn,\n\t).then((messages) => {\n\t\tstream.end(messages);\n\t});\n\n\treturn stream;\n}\n\nexport async function runAgentLoop(\n\tprompts: AgentMessage[],\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\temit: AgentEventSink,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): Promise<AgentMessage[]> {\n\tconst newMessages: AgentMessage[] = [...prompts];\n\tconst currentContext: AgentContext = {\n\t\t...context,\n\t\tmessages: [...context.messages, ...prompts],\n\t};\n\n\tawait emit({ type: \"agent_start\" });\n\tawait emit({ type: \"turn_start\" });\n\tfor (const prompt of prompts) {\n\t\tawait emit({ type: \"message_start\", message: prompt });\n\t\tawait emit({ type: \"message_end\", message: prompt });\n\t}\n\n\tawait runLoop(currentContext, newMessages, config, signal, emit, streamFn);\n\treturn newMessages;\n}\n\nexport async function runAgentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\temit: AgentEventSink,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): Promise<AgentMessage[]> {\n\tif (context.messages.length === 0) {\n\t\tthrow new Error(\"Cannot continue: no messages in context\");\n\t}\n\n\tif (context.messages[context.messages.length - 1].role === \"assistant\") {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst newMessages: AgentMessage[] = [];\n\tconst currentContext: AgentContext = { ...context };\n\n\tawait emit({ type: \"agent_start\" });\n\tawait emit({ type: \"turn_start\" });\n\n\tawait runLoop(currentContext, newMessages, config, signal, emit, streamFn);\n\treturn newMessages;\n}\n\nfunction createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {\n\treturn new EventStream<AgentEvent, AgentMessage[]>(\n\t\t(event: AgentEvent) => event.type === \"agent_end\",\n\t\t(event: AgentEvent) => (event.type === \"agent_end\" ? event.messages : []),\n\t);\n}\n\n/**\n * How many `stallLimit`-length periods the runaway-loop window spans. A window of `stallLimit * P`\n * turns lets the count-based detector catch oscillating cycles of period up to `P` (each signature in a\n * period-k cycle recurs ~window/k times), not just back-to-back repeats. Beyond this the cycle is loose\n * enough that it's indistinguishable from legitimate varied work, so we don't chase it.\n */\nconst STALL_WINDOW_PERIODS = 4;\n\n/**\n * Normalize a tool-call batch into a stable signature for runaway-loop detection. Volatile argument\n * tokens — epoch/timestamps, UUIDs, long hashes/nonces — are masked so a model retrying the SAME call\n * with a fresh timestamp/id each turn still collapses to one signature and is detected (bug #28). Only\n * clearly-volatile patterns are masked: short numbers (`file2.ts`, `line 42`, `count: 3`) are kept so\n * genuinely-distinct calls (reading numbered files, different line ranges) are NOT falsely merged.\n */\nfunction normalizeToolSignature(pairs: Array<[string, unknown]>): string {\n\treturn JSON.stringify(pairs)\n\t\t.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, \"<uuid>\")\n\t\t.replace(/\\d{4}-\\d{2}-\\d{2}[tT][0-9:.]+(?:z|[+-]\\d{2}:?\\d{2})?/gi, \"<ts>\")\n\t\t.replace(/\\b[0-9a-f]{16,}\\b/gi, \"<hex>\")\n\t\t.replace(/\\d{10,}/g, \"<num>\");\n}\n\n/**\n * Main loop logic shared by agentLoop and agentLoopContinue.\n */\nasync function runLoop(\n\tinitialContext: AgentContext,\n\tnewMessages: AgentMessage[],\n\tinitialConfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n): Promise<void> {\n\tlet currentContext = initialContext;\n\tlet config = initialConfig;\n\tlet firstTurn = true;\n\t// Runaway-loop backstop state: a sliding window of recent NORMALIZED tool-call signatures. A model\n\t// wedged repeating the same action makes no progress but keeps spending tokens; if one signature\n\t// recurs `stallLimit` times within the window we stop gracefully. Signatures are normalized so\n\t// volatile args (timestamps/UUIDs/nonces that change every call) can't disguise an otherwise-\n\t// identical call (bug #28). The window spans `stallLimit * STALL_WINDOW_PERIODS` turns so periodic\n\t// oscillation is caught too, not just back-to-back repeats: a cycle of period P repeats each\n\t// signature ~window/P times, so any P up to STALL_WINDOW_PERIODS reaches the threshold before the\n\t// window slides past it. Counts only turns that issued tool calls, so varied/long work never trips\n\t// it. `0` disables.\n\tconst stallLimit = config.maxStallTurns ?? DEFAULT_MAX_STALL_TURNS;\n\tconst stallWindow: string[] = [];\n\t// Check for steering messages at start (user may have typed while waiting)\n\tlet pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];\n\n\t// Outer loop: continues when queued follow-up messages arrive after agent would stop\n\twhile (true) {\n\t\tlet hasMoreToolCalls = true;\n\n\t\t// Inner loop: process tool calls and steering messages\n\t\twhile (hasMoreToolCalls || pendingMessages.length > 0) {\n\t\t\tif (!firstTurn) {\n\t\t\t\tawait emit({ type: \"turn_start\" });\n\t\t\t} else {\n\t\t\t\tfirstTurn = false;\n\t\t\t}\n\n\t\t\t// Process pending messages (inject before next assistant response)\n\t\t\tif (pendingMessages.length > 0) {\n\t\t\t\tfor (const message of pendingMessages) {\n\t\t\t\t\tawait emit({ type: \"message_start\", message });\n\t\t\t\t\tawait emit({ type: \"message_end\", message });\n\t\t\t\t\tcurrentContext.messages.push(message);\n\t\t\t\t\tnewMessages.push(message);\n\t\t\t\t}\n\t\t\t\tpendingMessages = [];\n\t\t\t}\n\n\t\t\t// Stream assistant response\n\t\t\tconst message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);\n\t\t\tnewMessages.push(message);\n\n\t\t\tif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\t\t\t\tawait emit({ type: \"turn_end\", message, toolResults: [] });\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for tool calls\n\t\t\tconst toolCalls = message.content.filter((c) => c.type === \"toolCall\");\n\n\t\t\tconst toolResults: ToolResultMessage[] = [];\n\t\t\thasMoreToolCalls = false;\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tconst executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);\n\t\t\t\ttoolResults.push(...executedToolBatch.messages);\n\t\t\t\thasMoreToolCalls = !executedToolBatch.terminate;\n\n\t\t\t\tfor (const result of toolResults) {\n\t\t\t\t\tcurrentContext.messages.push(result);\n\t\t\t\t\tnewMessages.push(result);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait emit({ type: \"turn_end\", message, toolResults });\n\n\t\t\t// Runaway-loop backstop (cost guard): detect a model stuck repeating one action.\n\t\t\tif (stallLimit > 0 && toolCalls.length > 0) {\n\t\t\t\tconst signature = normalizeToolSignature(toolCalls.map((c) => [c.name, c.arguments ?? null]));\n\t\t\t\tstallWindow.push(signature);\n\t\t\t\tif (stallWindow.length > stallLimit * STALL_WINDOW_PERIODS) stallWindow.shift();\n\t\t\t\tconst repeats = stallWindow.reduce((n, s) => (s === signature ? n + 1 : n), 0);\n\t\t\t\tif (repeats >= stallLimit) {\n\t\t\t\t\tconfig.onRunawayStop?.({ signature, repeats });\n\t\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst nextTurnContext = {\n\t\t\t\tmessage,\n\t\t\t\ttoolResults,\n\t\t\t\tcontext: currentContext,\n\t\t\t\tnewMessages,\n\t\t\t};\n\t\t\tconst nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);\n\t\t\tif (nextTurnSnapshot) {\n\t\t\t\tcurrentContext = nextTurnSnapshot.context ?? currentContext;\n\t\t\t\tconfig = {\n\t\t\t\t\t...config,\n\t\t\t\t\tmodel: nextTurnSnapshot.model ?? config.model,\n\t\t\t\t\treasoning:\n\t\t\t\t\t\tnextTurnSnapshot.thinkingLevel === undefined\n\t\t\t\t\t\t\t? config.reasoning\n\t\t\t\t\t\t\t: nextTurnSnapshot.thinkingLevel === \"off\"\n\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t: nextTurnSnapshot.thinkingLevel,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tawait config.shouldStopAfterTurn?.({\n\t\t\t\t\tmessage,\n\t\t\t\t\ttoolResults,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t\tnewMessages,\n\t\t\t\t})\n\t\t\t) {\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tpendingMessages = (await config.getSteeringMessages?.()) || [];\n\t\t}\n\n\t\t// Agent would stop here. Check for follow-up messages.\n\t\tconst followUpMessages = (await config.getFollowUpMessages?.()) || [];\n\t\tif (followUpMessages.length > 0) {\n\t\t\t// Set as pending so inner loop processes them\n\t\t\tpendingMessages = followUpMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// No more messages, exit\n\t\tbreak;\n\t}\n\n\tawait emit({ type: \"agent_end\", messages: newMessages });\n}\n\n/**\n * Stream an assistant response from the LLM.\n * This is where AgentMessage[] gets transformed to Message[] for the LLM.\n */\nasync function streamAssistantResponse(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\t// Apply context transform if configured (AgentMessage[] → AgentMessage[])\n\tlet messages = context.messages;\n\tif (config.transformContext) {\n\t\tmessages = await config.transformContext(messages, signal);\n\t}\n\n\t// Convert to LLM-compatible messages (AgentMessage[] → Message[])\n\tconst llmMessages = await config.convertToLlm(messages);\n\n\t// Build LLM context\n\tconst llmContext: Context = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: llmMessages,\n\t\ttools: context.tools,\n\t};\n\n\tconst streamFunction = streamFn || streamSimple;\n\n\t// Resolve API key (important for expiring tokens)\n\tconst resolvedApiKey =\n\t\t(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;\n\n\tconst response = await streamFunction(config.model, llmContext, {\n\t\t...config,\n\t\tapiKey: resolvedApiKey,\n\t\tsignal,\n\t});\n\n\tlet partialMessage: AssistantMessage | null = null;\n\tlet addedPartial = false;\n\n\tfor await (const event of response) {\n\t\tswitch (event.type) {\n\t\t\tcase \"start\":\n\t\t\t\tpartialMessage = event.partial;\n\t\t\t\tcontext.messages.push(partialMessage);\n\t\t\t\taddedPartial = true;\n\t\t\t\tawait emit({ type: \"message_start\", message: { ...partialMessage } });\n\t\t\t\tbreak;\n\n\t\t\tcase \"text_start\":\n\t\t\tcase \"text_delta\":\n\t\t\tcase \"text_end\":\n\t\t\tcase \"thinking_start\":\n\t\t\tcase \"thinking_delta\":\n\t\t\tcase \"thinking_end\":\n\t\t\tcase \"toolcall_start\":\n\t\t\tcase \"toolcall_delta\":\n\t\t\tcase \"toolcall_end\":\n\t\t\t\tif (partialMessage) {\n\t\t\t\t\tpartialMessage = event.partial;\n\t\t\t\t\tcontext.messages[context.messages.length - 1] = partialMessage;\n\t\t\t\t\tawait emit({\n\t\t\t\t\t\ttype: \"message_update\",\n\t\t\t\t\t\tassistantMessageEvent: event,\n\t\t\t\t\t\tmessage: { ...partialMessage },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"done\":\n\t\t\tcase \"error\": {\n\t\t\t\tconst finalMessage = await response.result();\n\t\t\t\tif (addedPartial) {\n\t\t\t\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t\t\t\t} else {\n\t\t\t\t\tcontext.messages.push(finalMessage);\n\t\t\t\t}\n\t\t\t\tif (!addedPartial) {\n\t\t\t\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t\t\t\t}\n\t\t\t\tawait emit({ type: \"message_end\", message: finalMessage });\n\t\t\t\treturn finalMessage;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst finalMessage = await response.result();\n\tif (addedPartial) {\n\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t} else {\n\t\tcontext.messages.push(finalMessage);\n\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t}\n\tawait emit({ type: \"message_end\", message: finalMessage });\n\treturn finalMessage;\n}\n\n/**\n * Execute tool calls from an assistant message.\n */\nasync function executeToolCalls(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst toolCalls = assistantMessage.content.filter((c) => c.type === \"toolCall\");\n\tconst hasSequentialToolCall = toolCalls.some(\n\t\t(tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === \"sequential\",\n\t);\n\tif (config.toolExecution === \"sequential\" || hasSequentialToolCall) {\n\t\treturn executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);\n\t}\n\treturn executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);\n}\n\ntype ExecutedToolCallBatch = {\n\tmessages: ToolResultMessage[];\n\tterminate: boolean;\n};\n\nasync function executeToolCallsSequential(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: AgentToolCall[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallOutcome[] = [];\n\tconst messages: ToolResultMessage[] = [];\n\n\tfor (const toolCall of toolCalls) {\n\t\tawait emit({\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: toolCall.id,\n\t\t\ttoolName: toolCall.name,\n\t\t\targs: toolCall.arguments,\n\t\t});\n\n\t\tconst preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);\n\t\tlet finalized: FinalizedToolCallOutcome;\n\t\tif (preparation.kind === \"immediate\") {\n\t\t\tfinalized = {\n\t\t\t\ttoolCall,\n\t\t\t\tresult: preparation.result,\n\t\t\t\tisError: preparation.isError,\n\t\t\t};\n\t\t} else {\n\t\t\tconst executed = await executePreparedToolCall(preparation, signal, emit);\n\t\t\tfinalized = await finalizeExecutedToolCall(\n\t\t\t\tcurrentContext,\n\t\t\t\tassistantMessage,\n\t\t\t\tpreparation,\n\t\t\t\texecuted,\n\t\t\t\tconfig,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t}\n\n\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\tconst toolResultMessage = createToolResultMessage(finalized);\n\t\tawait emitToolResultMessage(toolResultMessage, emit);\n\t\tfinalizedCalls.push(finalized);\n\t\tmessages.push(toolResultMessage);\n\n\t\tif (signal?.aborted) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn {\n\t\tmessages,\n\t\tterminate: shouldTerminateToolBatch(finalizedCalls),\n\t};\n}\n\nasync function executeToolCallsParallel(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: AgentToolCall[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallEntry[] = [];\n\n\tfor (const toolCall of toolCalls) {\n\t\tawait emit({\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: toolCall.id,\n\t\t\ttoolName: toolCall.name,\n\t\t\targs: toolCall.arguments,\n\t\t});\n\n\t\tconst preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);\n\t\tif (preparation.kind === \"immediate\") {\n\t\t\tconst finalized = {\n\t\t\t\ttoolCall,\n\t\t\t\tresult: preparation.result,\n\t\t\t\tisError: preparation.isError,\n\t\t\t} satisfies FinalizedToolCallOutcome;\n\t\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\t\tfinalizedCalls.push(finalized);\n\t\t\tif (signal?.aborted) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tfinalizedCalls.push(async () => {\n\t\t\tconst executed = await executePreparedToolCall(preparation, signal, emit);\n\t\t\tconst finalized = await finalizeExecutedToolCall(\n\t\t\t\tcurrentContext,\n\t\t\t\tassistantMessage,\n\t\t\t\tpreparation,\n\t\t\t\texecuted,\n\t\t\t\tconfig,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\t\treturn finalized;\n\t\t});\n\t\tif (signal?.aborted) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst orderedFinalizedCalls = await Promise.all(\n\t\tfinalizedCalls.map((entry) => (typeof entry === \"function\" ? entry() : Promise.resolve(entry))),\n\t);\n\tconst messages: ToolResultMessage[] = [];\n\tfor (const finalized of orderedFinalizedCalls) {\n\t\tconst toolResultMessage = createToolResultMessage(finalized);\n\t\tawait emitToolResultMessage(toolResultMessage, emit);\n\t\tmessages.push(toolResultMessage);\n\t}\n\n\treturn {\n\t\tmessages,\n\t\tterminate: shouldTerminateToolBatch(orderedFinalizedCalls),\n\t};\n}\n\ntype PreparedToolCall = {\n\tkind: \"prepared\";\n\ttoolCall: AgentToolCall;\n\ttool: AgentTool<any>;\n\targs: unknown;\n};\n\ntype ImmediateToolCallOutcome = {\n\tkind: \"immediate\";\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype ExecutedToolCallOutcome = {\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype FinalizedToolCallOutcome = {\n\ttoolCall: AgentToolCall;\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype FinalizedToolCallEntry = FinalizedToolCallOutcome | (() => Promise<FinalizedToolCallOutcome>);\n\nfunction shouldTerminateToolBatch(finalizedCalls: FinalizedToolCallOutcome[]): boolean {\n\treturn finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);\n}\n\nfunction prepareToolCallArguments(tool: AgentTool<any>, toolCall: AgentToolCall): AgentToolCall {\n\tif (!tool.prepareArguments) {\n\t\treturn toolCall;\n\t}\n\tconst preparedArguments = tool.prepareArguments(toolCall.arguments);\n\tif (preparedArguments === toolCall.arguments) {\n\t\treturn toolCall;\n\t}\n\treturn {\n\t\t...toolCall,\n\t\targuments: preparedArguments as Record<string, any>,\n\t};\n}\n\nasync function prepareToolCall(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCall: AgentToolCall,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n): Promise<PreparedToolCall | ImmediateToolCallOutcome> {\n\tconst tool = currentContext.tools?.find((t) => t.name === toolCall.name);\n\tif (!tool) {\n\t\treturn {\n\t\t\tkind: \"immediate\",\n\t\t\tresult: createErrorToolResult(`Tool ${toolCall.name} not found`),\n\t\t\tisError: true,\n\t\t};\n\t}\n\n\ttry {\n\t\tconst preparedToolCall = prepareToolCallArguments(tool, toolCall);\n\t\tconst validatedArgs = validateToolArguments(tool, preparedToolCall);\n\t\tif (config.beforeToolCall) {\n\t\t\tconst beforeResult = await config.beforeToolCall(\n\t\t\t\t{\n\t\t\t\t\tassistantMessage,\n\t\t\t\t\ttoolCall,\n\t\t\t\t\targs: validatedArgs,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (signal?.aborted) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"immediate\",\n\t\t\t\t\tresult: createErrorToolResult(\"Operation aborted\"),\n\t\t\t\t\tisError: true,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (beforeResult?.block) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"immediate\",\n\t\t\t\t\tresult: createErrorToolResult(beforeResult.reason || \"Tool execution was blocked\"),\n\t\t\t\t\tisError: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tif (signal?.aborted) {\n\t\t\treturn {\n\t\t\t\tkind: \"immediate\",\n\t\t\t\tresult: createErrorToolResult(\"Operation aborted\"),\n\t\t\t\tisError: true,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tkind: \"prepared\",\n\t\t\ttoolCall,\n\t\t\ttool,\n\t\t\targs: validatedArgs,\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\tkind: \"immediate\",\n\t\t\tresult: createErrorToolResult(error instanceof Error ? error.message : String(error)),\n\t\t\tisError: true,\n\t\t};\n\t}\n}\n\nasync function executePreparedToolCall(\n\tprepared: PreparedToolCall,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallOutcome> {\n\tconst updateEvents: Promise<void>[] = [];\n\n\ttry {\n\t\tconst result = await prepared.tool.execute(\n\t\t\tprepared.toolCall.id,\n\t\t\tprepared.args as never,\n\t\t\tsignal,\n\t\t\t(partialResult) => {\n\t\t\t\tupdateEvents.push(\n\t\t\t\t\tPromise.resolve(\n\t\t\t\t\t\temit({\n\t\t\t\t\t\t\ttype: \"tool_execution_update\",\n\t\t\t\t\t\t\ttoolCallId: prepared.toolCall.id,\n\t\t\t\t\t\t\ttoolName: prepared.toolCall.name,\n\t\t\t\t\t\t\targs: prepared.toolCall.arguments,\n\t\t\t\t\t\t\tpartialResult,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t},\n\t\t);\n\t\tawait Promise.all(updateEvents);\n\t\treturn { result, isError: false };\n\t} catch (error) {\n\t\tawait Promise.all(updateEvents);\n\t\treturn {\n\t\t\tresult: createErrorToolResult(error instanceof Error ? error.message : String(error)),\n\t\t\tisError: true,\n\t\t};\n\t}\n}\n\nasync function finalizeExecutedToolCall(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\tprepared: PreparedToolCall,\n\texecuted: ExecutedToolCallOutcome,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n): Promise<FinalizedToolCallOutcome> {\n\tlet result = executed.result;\n\tlet isError = executed.isError;\n\n\tif (config.afterToolCall) {\n\t\ttry {\n\t\t\tconst afterResult = await config.afterToolCall(\n\t\t\t\t{\n\t\t\t\t\tassistantMessage,\n\t\t\t\t\ttoolCall: prepared.toolCall,\n\t\t\t\t\targs: prepared.args,\n\t\t\t\t\tresult,\n\t\t\t\t\tisError,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (afterResult) {\n\t\t\t\tresult = {\n\t\t\t\t\tcontent: afterResult.content ?? result.content,\n\t\t\t\t\tdetails: afterResult.details ?? result.details,\n\t\t\t\t\tterminate: afterResult.terminate ?? result.terminate,\n\t\t\t\t};\n\t\t\t\tisError = afterResult.isError ?? isError;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tresult = createErrorToolResult(error instanceof Error ? error.message : String(error));\n\t\t\tisError = true;\n\t\t}\n\t}\n\n\treturn {\n\t\ttoolCall: prepared.toolCall,\n\t\tresult,\n\t\tisError,\n\t};\n}\n\nfunction createErrorToolResult(message: string): AgentToolResult<any> {\n\treturn {\n\t\tcontent: [{ type: \"text\", text: message }],\n\t\tdetails: {},\n\t};\n}\n\nasync function emitToolExecutionEnd(finalized: FinalizedToolCallOutcome, emit: AgentEventSink): Promise<void> {\n\tawait emit({\n\t\ttype: \"tool_execution_end\",\n\t\ttoolCallId: finalized.toolCall.id,\n\t\ttoolName: finalized.toolCall.name,\n\t\tresult: finalized.result,\n\t\tisError: finalized.isError,\n\t});\n}\n\nfunction createToolResultMessage(finalized: FinalizedToolCallOutcome): ToolResultMessage {\n\treturn {\n\t\trole: \"toolResult\",\n\t\ttoolCallId: finalized.toolCall.id,\n\t\ttoolName: finalized.toolCall.name,\n\t\tcontent: finalized.result.content,\n\t\tdetails: finalized.result.details,\n\t\tisError: finalized.isError,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nasync function emitToolResultMessage(toolResultMessage: ToolResultMessage, emit: AgentEventSink): Promise<void> {\n\tawait emit({ type: \"message_start\", message: toolResultMessage });\n\tawait emit({ type: \"message_end\", message: toolResultMessage });\n}\n"]}
1
+ {"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGN,WAAW,EAIX,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACX,YAAY,EACZ,UAAU,EACV,eAAe,EACf,YAAY,EAIZ,QAAQ,EACR,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,SAAS,CACxB,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CAuBzC;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,WAAW,CAAC,UAAU,EAAE,YAAY,EAAE,CAAC,CA8BzC;AAED,wBAAsB,YAAY,CACjC,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBzB;AAED,wBAAsB,oBAAoB,CACzC,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,cAAc,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAiBzB","sourcesContent":["/**\n * Agent loop that works with AgentMessage throughout.\n * Transforms to Message[] only at the LLM call boundary.\n */\n\nimport {\n\ttype AssistantMessage,\n\ttype Context,\n\tEventStream,\n\tstreamSimple,\n\ttype ToolResultMessage,\n\tvalidateToolArguments,\n} from \"@caupulican/pi-ai\";\nimport type {\n\tAgentContext,\n\tAgentEvent,\n\tAgentLoopConfig,\n\tAgentMessage,\n\tAgentTool,\n\tAgentToolCall,\n\tAgentToolResult,\n\tStreamFn,\n} from \"./types.ts\";\nimport { DEFAULT_MAX_STALL_TURNS } from \"./types.ts\";\n\nexport type AgentEventSink = (event: AgentEvent) => Promise<void> | void;\n\n/**\n * Start an agent loop with a new prompt message.\n * The prompt is added to the context and events are emitted for it.\n */\nexport function agentLoop(\n\tprompts: AgentMessage[],\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): EventStream<AgentEvent, AgentMessage[]> {\n\tconst stream = createAgentStream();\n\n\tvoid runAgentLoop(\n\t\tprompts,\n\t\tcontext,\n\t\tconfig,\n\t\tasync (event) => {\n\t\t\tstream.push(event);\n\t\t},\n\t\tsignal,\n\t\tstreamFn,\n\t)\n\t\t.catch(async (error) => {\n\t\t\tconst messages = [createLoopFailureMessage(error, config, signal?.aborted ?? false)];\n\t\t\tstream.push({ type: \"agent_end\", messages });\n\t\t\treturn messages;\n\t\t})\n\t\t.then((messages) => {\n\t\t\tstream.end(messages);\n\t\t});\n\n\treturn stream;\n}\n\n/**\n * Continue an agent loop from the current context without adding a new message.\n * Used for retries - context already has user message or tool results.\n *\n * **Important:** The last message in context must convert to a `user` or `toolResult` message\n * via `convertToLlm`. If it doesn't, the LLM provider will reject the request.\n * This cannot be validated here since `convertToLlm` is only called once per turn.\n */\nexport function agentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): EventStream<AgentEvent, AgentMessage[]> {\n\tif (context.messages.length === 0) {\n\t\tthrow new Error(\"Cannot continue: no messages in context\");\n\t}\n\n\tif (context.messages[context.messages.length - 1].role === \"assistant\") {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst stream = createAgentStream();\n\n\tvoid runAgentLoopContinue(\n\t\tcontext,\n\t\tconfig,\n\t\tasync (event) => {\n\t\t\tstream.push(event);\n\t\t},\n\t\tsignal,\n\t\tstreamFn,\n\t)\n\t\t.catch(async (error) => {\n\t\t\tconst messages = [createLoopFailureMessage(error, config, signal?.aborted ?? false)];\n\t\t\tstream.push({ type: \"agent_end\", messages });\n\t\t\treturn messages;\n\t\t})\n\t\t.then((messages) => {\n\t\t\tstream.end(messages);\n\t\t});\n\n\treturn stream;\n}\n\nexport async function runAgentLoop(\n\tprompts: AgentMessage[],\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\temit: AgentEventSink,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): Promise<AgentMessage[]> {\n\tconst newMessages: AgentMessage[] = [...prompts];\n\tconst currentContext: AgentContext = {\n\t\t...context,\n\t\tmessages: [...context.messages, ...prompts],\n\t};\n\n\tawait emit({ type: \"agent_start\" });\n\tawait emit({ type: \"turn_start\" });\n\tfor (const prompt of prompts) {\n\t\tawait emit({ type: \"message_start\", message: prompt });\n\t\tawait emit({ type: \"message_end\", message: prompt });\n\t}\n\n\tawait runLoop(currentContext, newMessages, config, signal, emit, streamFn);\n\treturn newMessages;\n}\n\nexport async function runAgentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\temit: AgentEventSink,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): Promise<AgentMessage[]> {\n\tif (context.messages.length === 0) {\n\t\tthrow new Error(\"Cannot continue: no messages in context\");\n\t}\n\n\tif (context.messages[context.messages.length - 1].role === \"assistant\") {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst newMessages: AgentMessage[] = [];\n\tconst currentContext: AgentContext = { ...context };\n\n\tawait emit({ type: \"agent_start\" });\n\tawait emit({ type: \"turn_start\" });\n\n\tawait runLoop(currentContext, newMessages, config, signal, emit, streamFn);\n\treturn newMessages;\n}\n\nfunction createLoopFailureMessage(error: unknown, config: AgentLoopConfig, aborted: boolean): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [{ type: \"text\", text: \"\" }],\n\t\tapi: config.model.api,\n\t\tprovider: config.model.provider,\n\t\tmodel: config.model.id,\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\tstopReason: aborted ? \"aborted\" : \"error\",\n\t\terrorMessage: error instanceof Error ? error.message : String(error),\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nfunction createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {\n\treturn new EventStream<AgentEvent, AgentMessage[]>(\n\t\t(event: AgentEvent) => event.type === \"agent_end\",\n\t\t(event: AgentEvent) => (event.type === \"agent_end\" ? event.messages : []),\n\t);\n}\n\n/**\n * How many `stallLimit`-length periods the runaway-loop window spans. A window of `stallLimit * P`\n * turns lets the count-based detector catch oscillating cycles of period up to `P` (each signature in a\n * period-k cycle recurs ~window/k times), not just back-to-back repeats. Beyond this the cycle is loose\n * enough that it's indistinguishable from legitimate varied work, so we don't chase it.\n */\nconst STALL_WINDOW_PERIODS = 4;\n\n/**\n * Normalize a tool-call batch into a stable signature for runaway-loop detection. Volatile argument\n * tokens — epoch/timestamps, UUIDs, long hashes/nonces — are masked so a model retrying the SAME call\n * with a fresh timestamp/id each turn still collapses to one signature and is detected (bug #28). Only\n * clearly-volatile patterns are masked: short numbers (`file2.ts`, `line 42`, `count: 3`) are kept so\n * genuinely-distinct calls (reading numbered files, different line ranges) are NOT falsely merged.\n */\nfunction normalizeToolSignature(pairs: Array<[string, unknown]>): string {\n\treturn JSON.stringify(pairs)\n\t\t.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, \"<uuid>\")\n\t\t.replace(/\\d{4}-\\d{2}-\\d{2}[tT][0-9:.]+(?:z|[+-]\\d{2}:?\\d{2})?/gi, \"<ts>\")\n\t\t.replace(/\\b[0-9a-f]{16,}\\b/gi, \"<hex>\")\n\t\t.replace(/\\d{10,}/g, \"<num>\");\n}\n\n/**\n * Main loop logic shared by agentLoop and agentLoopContinue.\n */\nasync function runLoop(\n\tinitialContext: AgentContext,\n\tnewMessages: AgentMessage[],\n\tinitialConfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n): Promise<void> {\n\tlet currentContext = initialContext;\n\tlet config = initialConfig;\n\tlet firstTurn = true;\n\t// Runaway-loop backstop state: a sliding window of recent NORMALIZED tool-call signatures. A model\n\t// wedged repeating the same action makes no progress but keeps spending tokens; if one signature\n\t// recurs `stallLimit` times within the window we stop gracefully. Signatures are normalized so\n\t// volatile args (timestamps/UUIDs/nonces that change every call) can't disguise an otherwise-\n\t// identical call (bug #28). The window spans `stallLimit * STALL_WINDOW_PERIODS` turns so periodic\n\t// oscillation is caught too, not just back-to-back repeats: a cycle of period P repeats each\n\t// signature ~window/P times, so any P up to STALL_WINDOW_PERIODS reaches the threshold before the\n\t// window slides past it. Counts only turns that issued tool calls, so varied/long work never trips\n\t// it. `0` disables.\n\tconst stallLimit = config.maxStallTurns ?? DEFAULT_MAX_STALL_TURNS;\n\tconst stallWindow: string[] = [];\n\t// Check for steering messages at start (user may have typed while waiting)\n\tlet pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];\n\n\t// Outer loop: continues when queued follow-up messages arrive after agent would stop\n\twhile (true) {\n\t\tlet hasMoreToolCalls = true;\n\n\t\t// Inner loop: process tool calls and steering messages\n\t\twhile (hasMoreToolCalls || pendingMessages.length > 0) {\n\t\t\tif (!firstTurn) {\n\t\t\t\tawait emit({ type: \"turn_start\" });\n\t\t\t} else {\n\t\t\t\tfirstTurn = false;\n\t\t\t}\n\n\t\t\t// Process pending messages (inject before next assistant response)\n\t\t\tif (pendingMessages.length > 0) {\n\t\t\t\tfor (const message of pendingMessages) {\n\t\t\t\t\tawait emit({ type: \"message_start\", message });\n\t\t\t\t\tawait emit({ type: \"message_end\", message });\n\t\t\t\t\tcurrentContext.messages.push(message);\n\t\t\t\t\tnewMessages.push(message);\n\t\t\t\t}\n\t\t\t\tpendingMessages = [];\n\t\t\t}\n\n\t\t\t// Stream assistant response\n\t\t\tconst message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);\n\t\t\tnewMessages.push(message);\n\n\t\t\tif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\t\t\t\tawait emit({ type: \"turn_end\", message, toolResults: [] });\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for tool calls\n\t\t\tconst toolCalls = message.content.filter((c) => c.type === \"toolCall\");\n\n\t\t\tconst toolResults: ToolResultMessage[] = [];\n\t\t\thasMoreToolCalls = false;\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tconst executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);\n\t\t\t\ttoolResults.push(...executedToolBatch.messages);\n\t\t\t\thasMoreToolCalls = !executedToolBatch.terminate;\n\n\t\t\t\tfor (const result of toolResults) {\n\t\t\t\t\tcurrentContext.messages.push(result);\n\t\t\t\t\tnewMessages.push(result);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait emit({ type: \"turn_end\", message, toolResults });\n\n\t\t\t// Runaway-loop backstop (cost guard): detect a model stuck repeating one action.\n\t\t\tif (stallLimit > 0 && toolCalls.length > 0) {\n\t\t\t\tconst signature = normalizeToolSignature(toolCalls.map((c) => [c.name, c.arguments ?? null]));\n\t\t\t\tstallWindow.push(signature);\n\t\t\t\tif (stallWindow.length > stallLimit * STALL_WINDOW_PERIODS) stallWindow.shift();\n\t\t\t\tconst repeats = stallWindow.reduce((n, s) => (s === signature ? n + 1 : n), 0);\n\t\t\t\tif (repeats >= stallLimit) {\n\t\t\t\t\tconfig.onRunawayStop?.({ signature, repeats });\n\t\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst nextTurnContext = {\n\t\t\t\tmessage,\n\t\t\t\ttoolResults,\n\t\t\t\tcontext: currentContext,\n\t\t\t\tnewMessages,\n\t\t\t};\n\t\t\tconst nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);\n\t\t\tif (nextTurnSnapshot) {\n\t\t\t\tcurrentContext = nextTurnSnapshot.context ?? currentContext;\n\t\t\t\tconfig = {\n\t\t\t\t\t...config,\n\t\t\t\t\tmodel: nextTurnSnapshot.model ?? config.model,\n\t\t\t\t\treasoning:\n\t\t\t\t\t\tnextTurnSnapshot.thinkingLevel === undefined\n\t\t\t\t\t\t\t? config.reasoning\n\t\t\t\t\t\t\t: nextTurnSnapshot.thinkingLevel === \"off\"\n\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t: nextTurnSnapshot.thinkingLevel,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tawait config.shouldStopAfterTurn?.({\n\t\t\t\t\tmessage,\n\t\t\t\t\ttoolResults,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t\tnewMessages,\n\t\t\t\t})\n\t\t\t) {\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tpendingMessages = (await config.getSteeringMessages?.()) || [];\n\t\t}\n\n\t\t// Agent would stop here. Check for follow-up messages.\n\t\tconst followUpMessages = (await config.getFollowUpMessages?.()) || [];\n\t\tif (followUpMessages.length > 0) {\n\t\t\t// Set as pending so inner loop processes them\n\t\t\tpendingMessages = followUpMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// No more messages, exit\n\t\tbreak;\n\t}\n\n\tawait emit({ type: \"agent_end\", messages: newMessages });\n}\n\n/**\n * Stream an assistant response from the LLM.\n * This is where AgentMessage[] gets transformed to Message[] for the LLM.\n */\nasync function streamAssistantResponse(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\t// Apply context transform if configured (AgentMessage[] → AgentMessage[])\n\tlet messages = context.messages;\n\tif (config.transformContext) {\n\t\tmessages = await config.transformContext(messages, signal);\n\t}\n\n\t// Convert to LLM-compatible messages (AgentMessage[] → Message[])\n\tconst llmMessages = await config.convertToLlm(messages);\n\n\t// Build LLM context\n\tconst llmContext: Context = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: llmMessages,\n\t\ttools: context.tools,\n\t};\n\n\tconst streamFunction = streamFn || streamSimple;\n\n\t// Resolve API key (important for expiring tokens)\n\tconst resolvedApiKey =\n\t\t(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;\n\n\tconst response = await streamFunction(config.model, llmContext, {\n\t\t...config,\n\t\tapiKey: resolvedApiKey,\n\t\tsignal,\n\t});\n\n\tlet partialMessage: AssistantMessage | null = null;\n\tlet addedPartial = false;\n\n\tfor await (const event of response) {\n\t\tswitch (event.type) {\n\t\t\tcase \"start\":\n\t\t\t\tpartialMessage = event.partial;\n\t\t\t\tcontext.messages.push(partialMessage);\n\t\t\t\taddedPartial = true;\n\t\t\t\tawait emit({ type: \"message_start\", message: { ...partialMessage } });\n\t\t\t\tbreak;\n\n\t\t\tcase \"text_start\":\n\t\t\tcase \"text_delta\":\n\t\t\tcase \"text_end\":\n\t\t\tcase \"thinking_start\":\n\t\t\tcase \"thinking_delta\":\n\t\t\tcase \"thinking_end\":\n\t\t\tcase \"toolcall_start\":\n\t\t\tcase \"toolcall_delta\":\n\t\t\tcase \"toolcall_end\":\n\t\t\t\tif (partialMessage) {\n\t\t\t\t\tpartialMessage = event.partial;\n\t\t\t\t\tcontext.messages[context.messages.length - 1] = partialMessage;\n\t\t\t\t\tawait emit({\n\t\t\t\t\t\ttype: \"message_update\",\n\t\t\t\t\t\tassistantMessageEvent: event,\n\t\t\t\t\t\tmessage: { ...partialMessage },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"done\":\n\t\t\tcase \"error\": {\n\t\t\t\tconst finalMessage = await response.result();\n\t\t\t\tif (addedPartial) {\n\t\t\t\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t\t\t\t} else {\n\t\t\t\t\tcontext.messages.push(finalMessage);\n\t\t\t\t}\n\t\t\t\tif (!addedPartial) {\n\t\t\t\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t\t\t\t}\n\t\t\t\tawait emit({ type: \"message_end\", message: finalMessage });\n\t\t\t\treturn finalMessage;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst finalMessage = await response.result();\n\tif (addedPartial) {\n\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t} else {\n\t\tcontext.messages.push(finalMessage);\n\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t}\n\tawait emit({ type: \"message_end\", message: finalMessage });\n\treturn finalMessage;\n}\n\n/**\n * Execute tool calls from an assistant message.\n */\nasync function executeToolCalls(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst toolCalls = assistantMessage.content.filter((c) => c.type === \"toolCall\");\n\tconst hasSequentialToolCall = toolCalls.some(\n\t\t(tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === \"sequential\",\n\t);\n\tif (config.toolExecution === \"sequential\" || hasSequentialToolCall) {\n\t\treturn executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);\n\t}\n\treturn executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);\n}\n\ntype ExecutedToolCallBatch = {\n\tmessages: ToolResultMessage[];\n\tterminate: boolean;\n};\n\nasync function executeToolCallsSequential(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: AgentToolCall[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallOutcome[] = [];\n\tconst messages: ToolResultMessage[] = [];\n\n\tfor (const toolCall of toolCalls) {\n\t\tawait emit({\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: toolCall.id,\n\t\t\ttoolName: toolCall.name,\n\t\t\targs: toolCall.arguments,\n\t\t});\n\n\t\tconst preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);\n\t\tlet finalized: FinalizedToolCallOutcome;\n\t\tif (preparation.kind === \"immediate\") {\n\t\t\tfinalized = {\n\t\t\t\ttoolCall,\n\t\t\t\tresult: preparation.result,\n\t\t\t\tisError: preparation.isError,\n\t\t\t};\n\t\t} else {\n\t\t\tconst executed = await executePreparedToolCall(preparation, signal, emit);\n\t\t\tfinalized = await finalizeExecutedToolCall(\n\t\t\t\tcurrentContext,\n\t\t\t\tassistantMessage,\n\t\t\t\tpreparation,\n\t\t\t\texecuted,\n\t\t\t\tconfig,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t}\n\n\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\tconst toolResultMessage = createToolResultMessage(finalized);\n\t\tawait emitToolResultMessage(toolResultMessage, emit);\n\t\tfinalizedCalls.push(finalized);\n\t\tmessages.push(toolResultMessage);\n\n\t\tif (signal?.aborted) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn {\n\t\tmessages,\n\t\tterminate: shouldTerminateToolBatch(finalizedCalls),\n\t};\n}\n\nasync function executeToolCallsParallel(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: AgentToolCall[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallEntry[] = [];\n\n\tfor (const toolCall of toolCalls) {\n\t\tawait emit({\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: toolCall.id,\n\t\t\ttoolName: toolCall.name,\n\t\t\targs: toolCall.arguments,\n\t\t});\n\n\t\tconst preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);\n\t\tif (preparation.kind === \"immediate\") {\n\t\t\tconst finalized = {\n\t\t\t\ttoolCall,\n\t\t\t\tresult: preparation.result,\n\t\t\t\tisError: preparation.isError,\n\t\t\t} satisfies FinalizedToolCallOutcome;\n\t\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\t\tfinalizedCalls.push(finalized);\n\t\t\tif (signal?.aborted) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tfinalizedCalls.push(async () => {\n\t\t\tconst executed = await executePreparedToolCall(preparation, signal, emit);\n\t\t\tconst finalized = await finalizeExecutedToolCall(\n\t\t\t\tcurrentContext,\n\t\t\t\tassistantMessage,\n\t\t\t\tpreparation,\n\t\t\t\texecuted,\n\t\t\t\tconfig,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\t\treturn finalized;\n\t\t});\n\t\tif (signal?.aborted) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst orderedFinalizedCalls = await Promise.all(\n\t\tfinalizedCalls.map((entry) => (typeof entry === \"function\" ? entry() : Promise.resolve(entry))),\n\t);\n\tconst messages: ToolResultMessage[] = [];\n\tfor (const finalized of orderedFinalizedCalls) {\n\t\tconst toolResultMessage = createToolResultMessage(finalized);\n\t\tawait emitToolResultMessage(toolResultMessage, emit);\n\t\tmessages.push(toolResultMessage);\n\t}\n\n\treturn {\n\t\tmessages,\n\t\tterminate: shouldTerminateToolBatch(orderedFinalizedCalls),\n\t};\n}\n\ntype PreparedToolCall = {\n\tkind: \"prepared\";\n\ttoolCall: AgentToolCall;\n\ttool: AgentTool<any>;\n\targs: unknown;\n};\n\ntype ImmediateToolCallOutcome = {\n\tkind: \"immediate\";\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype ExecutedToolCallOutcome = {\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype FinalizedToolCallOutcome = {\n\ttoolCall: AgentToolCall;\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype FinalizedToolCallEntry = FinalizedToolCallOutcome | (() => Promise<FinalizedToolCallOutcome>);\n\nfunction shouldTerminateToolBatch(finalizedCalls: FinalizedToolCallOutcome[]): boolean {\n\treturn finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);\n}\n\nfunction prepareToolCallArguments(tool: AgentTool<any>, toolCall: AgentToolCall): AgentToolCall {\n\tif (!tool.prepareArguments) {\n\t\treturn toolCall;\n\t}\n\tconst preparedArguments = tool.prepareArguments(toolCall.arguments);\n\tif (preparedArguments === toolCall.arguments) {\n\t\treturn toolCall;\n\t}\n\treturn {\n\t\t...toolCall,\n\t\targuments: preparedArguments as Record<string, any>,\n\t};\n}\n\nasync function prepareToolCall(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCall: AgentToolCall,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n): Promise<PreparedToolCall | ImmediateToolCallOutcome> {\n\tconst tool = currentContext.tools?.find((t) => t.name === toolCall.name);\n\tif (!tool) {\n\t\treturn {\n\t\t\tkind: \"immediate\",\n\t\t\tresult: createErrorToolResult(`Tool ${toolCall.name} not found`),\n\t\t\tisError: true,\n\t\t};\n\t}\n\n\ttry {\n\t\tconst preparedToolCall = prepareToolCallArguments(tool, toolCall);\n\t\tconst validatedArgs = validateToolArguments(tool, preparedToolCall);\n\t\tif (config.beforeToolCall) {\n\t\t\tconst beforeResult = await config.beforeToolCall(\n\t\t\t\t{\n\t\t\t\t\tassistantMessage,\n\t\t\t\t\ttoolCall,\n\t\t\t\t\targs: validatedArgs,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (signal?.aborted) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"immediate\",\n\t\t\t\t\tresult: createErrorToolResult(\"Operation aborted\"),\n\t\t\t\t\tisError: true,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (beforeResult?.block) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"immediate\",\n\t\t\t\t\tresult: createErrorToolResult(beforeResult.reason || \"Tool execution was blocked\"),\n\t\t\t\t\tisError: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tif (signal?.aborted) {\n\t\t\treturn {\n\t\t\t\tkind: \"immediate\",\n\t\t\t\tresult: createErrorToolResult(\"Operation aborted\"),\n\t\t\t\tisError: true,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tkind: \"prepared\",\n\t\t\ttoolCall,\n\t\t\ttool,\n\t\t\targs: validatedArgs,\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\tkind: \"immediate\",\n\t\t\tresult: createErrorToolResult(error instanceof Error ? error.message : String(error)),\n\t\t\tisError: true,\n\t\t};\n\t}\n}\n\nasync function executePreparedToolCall(\n\tprepared: PreparedToolCall,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallOutcome> {\n\tconst updateEvents: Promise<void>[] = [];\n\n\ttry {\n\t\tconst result = await prepared.tool.execute(\n\t\t\tprepared.toolCall.id,\n\t\t\tprepared.args as never,\n\t\t\tsignal,\n\t\t\t(partialResult) => {\n\t\t\t\tupdateEvents.push(\n\t\t\t\t\tPromise.resolve(\n\t\t\t\t\t\temit({\n\t\t\t\t\t\t\ttype: \"tool_execution_update\",\n\t\t\t\t\t\t\ttoolCallId: prepared.toolCall.id,\n\t\t\t\t\t\t\ttoolName: prepared.toolCall.name,\n\t\t\t\t\t\t\targs: prepared.toolCall.arguments,\n\t\t\t\t\t\t\tpartialResult,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t},\n\t\t);\n\t\tawait Promise.all(updateEvents);\n\t\treturn { result, isError: false };\n\t} catch (error) {\n\t\tawait Promise.all(updateEvents);\n\t\treturn {\n\t\t\tresult: createErrorToolResult(error instanceof Error ? error.message : String(error)),\n\t\t\tisError: true,\n\t\t};\n\t}\n}\n\nasync function finalizeExecutedToolCall(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\tprepared: PreparedToolCall,\n\texecuted: ExecutedToolCallOutcome,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n): Promise<FinalizedToolCallOutcome> {\n\tlet result = executed.result;\n\tlet isError = executed.isError;\n\n\tif (config.afterToolCall) {\n\t\ttry {\n\t\t\tconst afterResult = await config.afterToolCall(\n\t\t\t\t{\n\t\t\t\t\tassistantMessage,\n\t\t\t\t\ttoolCall: prepared.toolCall,\n\t\t\t\t\targs: prepared.args,\n\t\t\t\t\tresult,\n\t\t\t\t\tisError,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (afterResult) {\n\t\t\t\tresult = {\n\t\t\t\t\tcontent: afterResult.content ?? result.content,\n\t\t\t\t\tdetails: afterResult.details ?? result.details,\n\t\t\t\t\tterminate: afterResult.terminate ?? result.terminate,\n\t\t\t\t};\n\t\t\t\tisError = afterResult.isError ?? isError;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tresult = createErrorToolResult(error instanceof Error ? error.message : String(error));\n\t\t\tisError = true;\n\t\t}\n\t}\n\n\treturn {\n\t\ttoolCall: prepared.toolCall,\n\t\tresult,\n\t\tisError,\n\t};\n}\n\nfunction createErrorToolResult(message: string): AgentToolResult<any> {\n\treturn {\n\t\tcontent: [{ type: \"text\", text: message }],\n\t\tdetails: {},\n\t};\n}\n\nasync function emitToolExecutionEnd(finalized: FinalizedToolCallOutcome, emit: AgentEventSink): Promise<void> {\n\tawait emit({\n\t\ttype: \"tool_execution_end\",\n\t\ttoolCallId: finalized.toolCall.id,\n\t\ttoolName: finalized.toolCall.name,\n\t\tresult: finalized.result,\n\t\tisError: finalized.isError,\n\t});\n}\n\nfunction createToolResultMessage(finalized: FinalizedToolCallOutcome): ToolResultMessage {\n\treturn {\n\t\trole: \"toolResult\",\n\t\ttoolCallId: finalized.toolCall.id,\n\t\ttoolName: finalized.toolCall.name,\n\t\tcontent: finalized.result.content,\n\t\tdetails: finalized.result.details,\n\t\tisError: finalized.isError,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nasync function emitToolResultMessage(toolResultMessage: ToolResultMessage, emit: AgentEventSink): Promise<void> {\n\tawait emit({ type: \"message_start\", message: toolResultMessage });\n\tawait emit({ type: \"message_end\", message: toolResultMessage });\n}\n"]}
@@ -12,7 +12,13 @@ export function agentLoop(prompts, context, config, signal, streamFn) {
12
12
  const stream = createAgentStream();
13
13
  void runAgentLoop(prompts, context, config, async (event) => {
14
14
  stream.push(event);
15
- }, signal, streamFn).then((messages) => {
15
+ }, signal, streamFn)
16
+ .catch(async (error) => {
17
+ const messages = [createLoopFailureMessage(error, config, signal?.aborted ?? false)];
18
+ stream.push({ type: "agent_end", messages });
19
+ return messages;
20
+ })
21
+ .then((messages) => {
16
22
  stream.end(messages);
17
23
  });
18
24
  return stream;
@@ -35,7 +41,13 @@ export function agentLoopContinue(context, config, signal, streamFn) {
35
41
  const stream = createAgentStream();
36
42
  void runAgentLoopContinue(context, config, async (event) => {
37
43
  stream.push(event);
38
- }, signal, streamFn).then((messages) => {
44
+ }, signal, streamFn)
45
+ .catch(async (error) => {
46
+ const messages = [createLoopFailureMessage(error, config, signal?.aborted ?? false)];
47
+ stream.push({ type: "agent_end", messages });
48
+ return messages;
49
+ })
50
+ .then((messages) => {
39
51
  stream.end(messages);
40
52
  });
41
53
  return stream;
@@ -69,6 +81,26 @@ export async function runAgentLoopContinue(context, config, emit, signal, stream
69
81
  await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
70
82
  return newMessages;
71
83
  }
84
+ function createLoopFailureMessage(error, config, aborted) {
85
+ return {
86
+ role: "assistant",
87
+ content: [{ type: "text", text: "" }],
88
+ api: config.model.api,
89
+ provider: config.model.provider,
90
+ model: config.model.id,
91
+ usage: {
92
+ input: 0,
93
+ output: 0,
94
+ cacheRead: 0,
95
+ cacheWrite: 0,
96
+ totalTokens: 0,
97
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
98
+ },
99
+ stopReason: aborted ? "aborted" : "error",
100
+ errorMessage: error instanceof Error ? error.message : String(error),
101
+ timestamp: Date.now(),
102
+ };
103
+ }
72
104
  function createAgentStream() {
73
105
  return new EventStream((event) => event.type === "agent_end", (event) => (event.type === "agent_end" ? event.messages : []));
74
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"agent-loop.js","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Agent loop that works with AgentMessage throughout.\n * Transforms to Message[] only at the LLM call boundary.\n */\n\nimport {\n\ttype AssistantMessage,\n\ttype Context,\n\tEventStream,\n\tstreamSimple,\n\ttype ToolResultMessage,\n\tvalidateToolArguments,\n} from \"@caupulican/pi-ai\";\nimport type {\n\tAgentContext,\n\tAgentEvent,\n\tAgentLoopConfig,\n\tAgentMessage,\n\tAgentTool,\n\tAgentToolCall,\n\tAgentToolResult,\n\tStreamFn,\n} from \"./types.ts\";\nimport { DEFAULT_MAX_STALL_TURNS } from \"./types.ts\";\n\nexport type AgentEventSink = (event: AgentEvent) => Promise<void> | void;\n\n/**\n * Start an agent loop with a new prompt message.\n * The prompt is added to the context and events are emitted for it.\n */\nexport function agentLoop(\n\tprompts: AgentMessage[],\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): EventStream<AgentEvent, AgentMessage[]> {\n\tconst stream = createAgentStream();\n\n\tvoid runAgentLoop(\n\t\tprompts,\n\t\tcontext,\n\t\tconfig,\n\t\tasync (event) => {\n\t\t\tstream.push(event);\n\t\t},\n\t\tsignal,\n\t\tstreamFn,\n\t).then((messages) => {\n\t\tstream.end(messages);\n\t});\n\n\treturn stream;\n}\n\n/**\n * Continue an agent loop from the current context without adding a new message.\n * Used for retries - context already has user message or tool results.\n *\n * **Important:** The last message in context must convert to a `user` or `toolResult` message\n * via `convertToLlm`. If it doesn't, the LLM provider will reject the request.\n * This cannot be validated here since `convertToLlm` is only called once per turn.\n */\nexport function agentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): EventStream<AgentEvent, AgentMessage[]> {\n\tif (context.messages.length === 0) {\n\t\tthrow new Error(\"Cannot continue: no messages in context\");\n\t}\n\n\tif (context.messages[context.messages.length - 1].role === \"assistant\") {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst stream = createAgentStream();\n\n\tvoid runAgentLoopContinue(\n\t\tcontext,\n\t\tconfig,\n\t\tasync (event) => {\n\t\t\tstream.push(event);\n\t\t},\n\t\tsignal,\n\t\tstreamFn,\n\t).then((messages) => {\n\t\tstream.end(messages);\n\t});\n\n\treturn stream;\n}\n\nexport async function runAgentLoop(\n\tprompts: AgentMessage[],\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\temit: AgentEventSink,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): Promise<AgentMessage[]> {\n\tconst newMessages: AgentMessage[] = [...prompts];\n\tconst currentContext: AgentContext = {\n\t\t...context,\n\t\tmessages: [...context.messages, ...prompts],\n\t};\n\n\tawait emit({ type: \"agent_start\" });\n\tawait emit({ type: \"turn_start\" });\n\tfor (const prompt of prompts) {\n\t\tawait emit({ type: \"message_start\", message: prompt });\n\t\tawait emit({ type: \"message_end\", message: prompt });\n\t}\n\n\tawait runLoop(currentContext, newMessages, config, signal, emit, streamFn);\n\treturn newMessages;\n}\n\nexport async function runAgentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\temit: AgentEventSink,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): Promise<AgentMessage[]> {\n\tif (context.messages.length === 0) {\n\t\tthrow new Error(\"Cannot continue: no messages in context\");\n\t}\n\n\tif (context.messages[context.messages.length - 1].role === \"assistant\") {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst newMessages: AgentMessage[] = [];\n\tconst currentContext: AgentContext = { ...context };\n\n\tawait emit({ type: \"agent_start\" });\n\tawait emit({ type: \"turn_start\" });\n\n\tawait runLoop(currentContext, newMessages, config, signal, emit, streamFn);\n\treturn newMessages;\n}\n\nfunction createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {\n\treturn new EventStream<AgentEvent, AgentMessage[]>(\n\t\t(event: AgentEvent) => event.type === \"agent_end\",\n\t\t(event: AgentEvent) => (event.type === \"agent_end\" ? event.messages : []),\n\t);\n}\n\n/**\n * How many `stallLimit`-length periods the runaway-loop window spans. A window of `stallLimit * P`\n * turns lets the count-based detector catch oscillating cycles of period up to `P` (each signature in a\n * period-k cycle recurs ~window/k times), not just back-to-back repeats. Beyond this the cycle is loose\n * enough that it's indistinguishable from legitimate varied work, so we don't chase it.\n */\nconst STALL_WINDOW_PERIODS = 4;\n\n/**\n * Normalize a tool-call batch into a stable signature for runaway-loop detection. Volatile argument\n * tokens — epoch/timestamps, UUIDs, long hashes/nonces — are masked so a model retrying the SAME call\n * with a fresh timestamp/id each turn still collapses to one signature and is detected (bug #28). Only\n * clearly-volatile patterns are masked: short numbers (`file2.ts`, `line 42`, `count: 3`) are kept so\n * genuinely-distinct calls (reading numbered files, different line ranges) are NOT falsely merged.\n */\nfunction normalizeToolSignature(pairs: Array<[string, unknown]>): string {\n\treturn JSON.stringify(pairs)\n\t\t.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, \"<uuid>\")\n\t\t.replace(/\\d{4}-\\d{2}-\\d{2}[tT][0-9:.]+(?:z|[+-]\\d{2}:?\\d{2})?/gi, \"<ts>\")\n\t\t.replace(/\\b[0-9a-f]{16,}\\b/gi, \"<hex>\")\n\t\t.replace(/\\d{10,}/g, \"<num>\");\n}\n\n/**\n * Main loop logic shared by agentLoop and agentLoopContinue.\n */\nasync function runLoop(\n\tinitialContext: AgentContext,\n\tnewMessages: AgentMessage[],\n\tinitialConfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n): Promise<void> {\n\tlet currentContext = initialContext;\n\tlet config = initialConfig;\n\tlet firstTurn = true;\n\t// Runaway-loop backstop state: a sliding window of recent NORMALIZED tool-call signatures. A model\n\t// wedged repeating the same action makes no progress but keeps spending tokens; if one signature\n\t// recurs `stallLimit` times within the window we stop gracefully. Signatures are normalized so\n\t// volatile args (timestamps/UUIDs/nonces that change every call) can't disguise an otherwise-\n\t// identical call (bug #28). The window spans `stallLimit * STALL_WINDOW_PERIODS` turns so periodic\n\t// oscillation is caught too, not just back-to-back repeats: a cycle of period P repeats each\n\t// signature ~window/P times, so any P up to STALL_WINDOW_PERIODS reaches the threshold before the\n\t// window slides past it. Counts only turns that issued tool calls, so varied/long work never trips\n\t// it. `0` disables.\n\tconst stallLimit = config.maxStallTurns ?? DEFAULT_MAX_STALL_TURNS;\n\tconst stallWindow: string[] = [];\n\t// Check for steering messages at start (user may have typed while waiting)\n\tlet pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];\n\n\t// Outer loop: continues when queued follow-up messages arrive after agent would stop\n\twhile (true) {\n\t\tlet hasMoreToolCalls = true;\n\n\t\t// Inner loop: process tool calls and steering messages\n\t\twhile (hasMoreToolCalls || pendingMessages.length > 0) {\n\t\t\tif (!firstTurn) {\n\t\t\t\tawait emit({ type: \"turn_start\" });\n\t\t\t} else {\n\t\t\t\tfirstTurn = false;\n\t\t\t}\n\n\t\t\t// Process pending messages (inject before next assistant response)\n\t\t\tif (pendingMessages.length > 0) {\n\t\t\t\tfor (const message of pendingMessages) {\n\t\t\t\t\tawait emit({ type: \"message_start\", message });\n\t\t\t\t\tawait emit({ type: \"message_end\", message });\n\t\t\t\t\tcurrentContext.messages.push(message);\n\t\t\t\t\tnewMessages.push(message);\n\t\t\t\t}\n\t\t\t\tpendingMessages = [];\n\t\t\t}\n\n\t\t\t// Stream assistant response\n\t\t\tconst message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);\n\t\t\tnewMessages.push(message);\n\n\t\t\tif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\t\t\t\tawait emit({ type: \"turn_end\", message, toolResults: [] });\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for tool calls\n\t\t\tconst toolCalls = message.content.filter((c) => c.type === \"toolCall\");\n\n\t\t\tconst toolResults: ToolResultMessage[] = [];\n\t\t\thasMoreToolCalls = false;\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tconst executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);\n\t\t\t\ttoolResults.push(...executedToolBatch.messages);\n\t\t\t\thasMoreToolCalls = !executedToolBatch.terminate;\n\n\t\t\t\tfor (const result of toolResults) {\n\t\t\t\t\tcurrentContext.messages.push(result);\n\t\t\t\t\tnewMessages.push(result);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait emit({ type: \"turn_end\", message, toolResults });\n\n\t\t\t// Runaway-loop backstop (cost guard): detect a model stuck repeating one action.\n\t\t\tif (stallLimit > 0 && toolCalls.length > 0) {\n\t\t\t\tconst signature = normalizeToolSignature(toolCalls.map((c) => [c.name, c.arguments ?? null]));\n\t\t\t\tstallWindow.push(signature);\n\t\t\t\tif (stallWindow.length > stallLimit * STALL_WINDOW_PERIODS) stallWindow.shift();\n\t\t\t\tconst repeats = stallWindow.reduce((n, s) => (s === signature ? n + 1 : n), 0);\n\t\t\t\tif (repeats >= stallLimit) {\n\t\t\t\t\tconfig.onRunawayStop?.({ signature, repeats });\n\t\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst nextTurnContext = {\n\t\t\t\tmessage,\n\t\t\t\ttoolResults,\n\t\t\t\tcontext: currentContext,\n\t\t\t\tnewMessages,\n\t\t\t};\n\t\t\tconst nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);\n\t\t\tif (nextTurnSnapshot) {\n\t\t\t\tcurrentContext = nextTurnSnapshot.context ?? currentContext;\n\t\t\t\tconfig = {\n\t\t\t\t\t...config,\n\t\t\t\t\tmodel: nextTurnSnapshot.model ?? config.model,\n\t\t\t\t\treasoning:\n\t\t\t\t\t\tnextTurnSnapshot.thinkingLevel === undefined\n\t\t\t\t\t\t\t? config.reasoning\n\t\t\t\t\t\t\t: nextTurnSnapshot.thinkingLevel === \"off\"\n\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t: nextTurnSnapshot.thinkingLevel,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tawait config.shouldStopAfterTurn?.({\n\t\t\t\t\tmessage,\n\t\t\t\t\ttoolResults,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t\tnewMessages,\n\t\t\t\t})\n\t\t\t) {\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tpendingMessages = (await config.getSteeringMessages?.()) || [];\n\t\t}\n\n\t\t// Agent would stop here. Check for follow-up messages.\n\t\tconst followUpMessages = (await config.getFollowUpMessages?.()) || [];\n\t\tif (followUpMessages.length > 0) {\n\t\t\t// Set as pending so inner loop processes them\n\t\t\tpendingMessages = followUpMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// No more messages, exit\n\t\tbreak;\n\t}\n\n\tawait emit({ type: \"agent_end\", messages: newMessages });\n}\n\n/**\n * Stream an assistant response from the LLM.\n * This is where AgentMessage[] gets transformed to Message[] for the LLM.\n */\nasync function streamAssistantResponse(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\t// Apply context transform if configured (AgentMessage[] → AgentMessage[])\n\tlet messages = context.messages;\n\tif (config.transformContext) {\n\t\tmessages = await config.transformContext(messages, signal);\n\t}\n\n\t// Convert to LLM-compatible messages (AgentMessage[] → Message[])\n\tconst llmMessages = await config.convertToLlm(messages);\n\n\t// Build LLM context\n\tconst llmContext: Context = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: llmMessages,\n\t\ttools: context.tools,\n\t};\n\n\tconst streamFunction = streamFn || streamSimple;\n\n\t// Resolve API key (important for expiring tokens)\n\tconst resolvedApiKey =\n\t\t(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;\n\n\tconst response = await streamFunction(config.model, llmContext, {\n\t\t...config,\n\t\tapiKey: resolvedApiKey,\n\t\tsignal,\n\t});\n\n\tlet partialMessage: AssistantMessage | null = null;\n\tlet addedPartial = false;\n\n\tfor await (const event of response) {\n\t\tswitch (event.type) {\n\t\t\tcase \"start\":\n\t\t\t\tpartialMessage = event.partial;\n\t\t\t\tcontext.messages.push(partialMessage);\n\t\t\t\taddedPartial = true;\n\t\t\t\tawait emit({ type: \"message_start\", message: { ...partialMessage } });\n\t\t\t\tbreak;\n\n\t\t\tcase \"text_start\":\n\t\t\tcase \"text_delta\":\n\t\t\tcase \"text_end\":\n\t\t\tcase \"thinking_start\":\n\t\t\tcase \"thinking_delta\":\n\t\t\tcase \"thinking_end\":\n\t\t\tcase \"toolcall_start\":\n\t\t\tcase \"toolcall_delta\":\n\t\t\tcase \"toolcall_end\":\n\t\t\t\tif (partialMessage) {\n\t\t\t\t\tpartialMessage = event.partial;\n\t\t\t\t\tcontext.messages[context.messages.length - 1] = partialMessage;\n\t\t\t\t\tawait emit({\n\t\t\t\t\t\ttype: \"message_update\",\n\t\t\t\t\t\tassistantMessageEvent: event,\n\t\t\t\t\t\tmessage: { ...partialMessage },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"done\":\n\t\t\tcase \"error\": {\n\t\t\t\tconst finalMessage = await response.result();\n\t\t\t\tif (addedPartial) {\n\t\t\t\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t\t\t\t} else {\n\t\t\t\t\tcontext.messages.push(finalMessage);\n\t\t\t\t}\n\t\t\t\tif (!addedPartial) {\n\t\t\t\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t\t\t\t}\n\t\t\t\tawait emit({ type: \"message_end\", message: finalMessage });\n\t\t\t\treturn finalMessage;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst finalMessage = await response.result();\n\tif (addedPartial) {\n\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t} else {\n\t\tcontext.messages.push(finalMessage);\n\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t}\n\tawait emit({ type: \"message_end\", message: finalMessage });\n\treturn finalMessage;\n}\n\n/**\n * Execute tool calls from an assistant message.\n */\nasync function executeToolCalls(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst toolCalls = assistantMessage.content.filter((c) => c.type === \"toolCall\");\n\tconst hasSequentialToolCall = toolCalls.some(\n\t\t(tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === \"sequential\",\n\t);\n\tif (config.toolExecution === \"sequential\" || hasSequentialToolCall) {\n\t\treturn executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);\n\t}\n\treturn executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);\n}\n\ntype ExecutedToolCallBatch = {\n\tmessages: ToolResultMessage[];\n\tterminate: boolean;\n};\n\nasync function executeToolCallsSequential(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: AgentToolCall[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallOutcome[] = [];\n\tconst messages: ToolResultMessage[] = [];\n\n\tfor (const toolCall of toolCalls) {\n\t\tawait emit({\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: toolCall.id,\n\t\t\ttoolName: toolCall.name,\n\t\t\targs: toolCall.arguments,\n\t\t});\n\n\t\tconst preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);\n\t\tlet finalized: FinalizedToolCallOutcome;\n\t\tif (preparation.kind === \"immediate\") {\n\t\t\tfinalized = {\n\t\t\t\ttoolCall,\n\t\t\t\tresult: preparation.result,\n\t\t\t\tisError: preparation.isError,\n\t\t\t};\n\t\t} else {\n\t\t\tconst executed = await executePreparedToolCall(preparation, signal, emit);\n\t\t\tfinalized = await finalizeExecutedToolCall(\n\t\t\t\tcurrentContext,\n\t\t\t\tassistantMessage,\n\t\t\t\tpreparation,\n\t\t\t\texecuted,\n\t\t\t\tconfig,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t}\n\n\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\tconst toolResultMessage = createToolResultMessage(finalized);\n\t\tawait emitToolResultMessage(toolResultMessage, emit);\n\t\tfinalizedCalls.push(finalized);\n\t\tmessages.push(toolResultMessage);\n\n\t\tif (signal?.aborted) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn {\n\t\tmessages,\n\t\tterminate: shouldTerminateToolBatch(finalizedCalls),\n\t};\n}\n\nasync function executeToolCallsParallel(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: AgentToolCall[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallEntry[] = [];\n\n\tfor (const toolCall of toolCalls) {\n\t\tawait emit({\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: toolCall.id,\n\t\t\ttoolName: toolCall.name,\n\t\t\targs: toolCall.arguments,\n\t\t});\n\n\t\tconst preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);\n\t\tif (preparation.kind === \"immediate\") {\n\t\t\tconst finalized = {\n\t\t\t\ttoolCall,\n\t\t\t\tresult: preparation.result,\n\t\t\t\tisError: preparation.isError,\n\t\t\t} satisfies FinalizedToolCallOutcome;\n\t\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\t\tfinalizedCalls.push(finalized);\n\t\t\tif (signal?.aborted) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tfinalizedCalls.push(async () => {\n\t\t\tconst executed = await executePreparedToolCall(preparation, signal, emit);\n\t\t\tconst finalized = await finalizeExecutedToolCall(\n\t\t\t\tcurrentContext,\n\t\t\t\tassistantMessage,\n\t\t\t\tpreparation,\n\t\t\t\texecuted,\n\t\t\t\tconfig,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\t\treturn finalized;\n\t\t});\n\t\tif (signal?.aborted) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst orderedFinalizedCalls = await Promise.all(\n\t\tfinalizedCalls.map((entry) => (typeof entry === \"function\" ? entry() : Promise.resolve(entry))),\n\t);\n\tconst messages: ToolResultMessage[] = [];\n\tfor (const finalized of orderedFinalizedCalls) {\n\t\tconst toolResultMessage = createToolResultMessage(finalized);\n\t\tawait emitToolResultMessage(toolResultMessage, emit);\n\t\tmessages.push(toolResultMessage);\n\t}\n\n\treturn {\n\t\tmessages,\n\t\tterminate: shouldTerminateToolBatch(orderedFinalizedCalls),\n\t};\n}\n\ntype PreparedToolCall = {\n\tkind: \"prepared\";\n\ttoolCall: AgentToolCall;\n\ttool: AgentTool<any>;\n\targs: unknown;\n};\n\ntype ImmediateToolCallOutcome = {\n\tkind: \"immediate\";\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype ExecutedToolCallOutcome = {\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype FinalizedToolCallOutcome = {\n\ttoolCall: AgentToolCall;\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype FinalizedToolCallEntry = FinalizedToolCallOutcome | (() => Promise<FinalizedToolCallOutcome>);\n\nfunction shouldTerminateToolBatch(finalizedCalls: FinalizedToolCallOutcome[]): boolean {\n\treturn finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);\n}\n\nfunction prepareToolCallArguments(tool: AgentTool<any>, toolCall: AgentToolCall): AgentToolCall {\n\tif (!tool.prepareArguments) {\n\t\treturn toolCall;\n\t}\n\tconst preparedArguments = tool.prepareArguments(toolCall.arguments);\n\tif (preparedArguments === toolCall.arguments) {\n\t\treturn toolCall;\n\t}\n\treturn {\n\t\t...toolCall,\n\t\targuments: preparedArguments as Record<string, any>,\n\t};\n}\n\nasync function prepareToolCall(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCall: AgentToolCall,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n): Promise<PreparedToolCall | ImmediateToolCallOutcome> {\n\tconst tool = currentContext.tools?.find((t) => t.name === toolCall.name);\n\tif (!tool) {\n\t\treturn {\n\t\t\tkind: \"immediate\",\n\t\t\tresult: createErrorToolResult(`Tool ${toolCall.name} not found`),\n\t\t\tisError: true,\n\t\t};\n\t}\n\n\ttry {\n\t\tconst preparedToolCall = prepareToolCallArguments(tool, toolCall);\n\t\tconst validatedArgs = validateToolArguments(tool, preparedToolCall);\n\t\tif (config.beforeToolCall) {\n\t\t\tconst beforeResult = await config.beforeToolCall(\n\t\t\t\t{\n\t\t\t\t\tassistantMessage,\n\t\t\t\t\ttoolCall,\n\t\t\t\t\targs: validatedArgs,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (signal?.aborted) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"immediate\",\n\t\t\t\t\tresult: createErrorToolResult(\"Operation aborted\"),\n\t\t\t\t\tisError: true,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (beforeResult?.block) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"immediate\",\n\t\t\t\t\tresult: createErrorToolResult(beforeResult.reason || \"Tool execution was blocked\"),\n\t\t\t\t\tisError: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tif (signal?.aborted) {\n\t\t\treturn {\n\t\t\t\tkind: \"immediate\",\n\t\t\t\tresult: createErrorToolResult(\"Operation aborted\"),\n\t\t\t\tisError: true,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tkind: \"prepared\",\n\t\t\ttoolCall,\n\t\t\ttool,\n\t\t\targs: validatedArgs,\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\tkind: \"immediate\",\n\t\t\tresult: createErrorToolResult(error instanceof Error ? error.message : String(error)),\n\t\t\tisError: true,\n\t\t};\n\t}\n}\n\nasync function executePreparedToolCall(\n\tprepared: PreparedToolCall,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallOutcome> {\n\tconst updateEvents: Promise<void>[] = [];\n\n\ttry {\n\t\tconst result = await prepared.tool.execute(\n\t\t\tprepared.toolCall.id,\n\t\t\tprepared.args as never,\n\t\t\tsignal,\n\t\t\t(partialResult) => {\n\t\t\t\tupdateEvents.push(\n\t\t\t\t\tPromise.resolve(\n\t\t\t\t\t\temit({\n\t\t\t\t\t\t\ttype: \"tool_execution_update\",\n\t\t\t\t\t\t\ttoolCallId: prepared.toolCall.id,\n\t\t\t\t\t\t\ttoolName: prepared.toolCall.name,\n\t\t\t\t\t\t\targs: prepared.toolCall.arguments,\n\t\t\t\t\t\t\tpartialResult,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t},\n\t\t);\n\t\tawait Promise.all(updateEvents);\n\t\treturn { result, isError: false };\n\t} catch (error) {\n\t\tawait Promise.all(updateEvents);\n\t\treturn {\n\t\t\tresult: createErrorToolResult(error instanceof Error ? error.message : String(error)),\n\t\t\tisError: true,\n\t\t};\n\t}\n}\n\nasync function finalizeExecutedToolCall(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\tprepared: PreparedToolCall,\n\texecuted: ExecutedToolCallOutcome,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n): Promise<FinalizedToolCallOutcome> {\n\tlet result = executed.result;\n\tlet isError = executed.isError;\n\n\tif (config.afterToolCall) {\n\t\ttry {\n\t\t\tconst afterResult = await config.afterToolCall(\n\t\t\t\t{\n\t\t\t\t\tassistantMessage,\n\t\t\t\t\ttoolCall: prepared.toolCall,\n\t\t\t\t\targs: prepared.args,\n\t\t\t\t\tresult,\n\t\t\t\t\tisError,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (afterResult) {\n\t\t\t\tresult = {\n\t\t\t\t\tcontent: afterResult.content ?? result.content,\n\t\t\t\t\tdetails: afterResult.details ?? result.details,\n\t\t\t\t\tterminate: afterResult.terminate ?? result.terminate,\n\t\t\t\t};\n\t\t\t\tisError = afterResult.isError ?? isError;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tresult = createErrorToolResult(error instanceof Error ? error.message : String(error));\n\t\t\tisError = true;\n\t\t}\n\t}\n\n\treturn {\n\t\ttoolCall: prepared.toolCall,\n\t\tresult,\n\t\tisError,\n\t};\n}\n\nfunction createErrorToolResult(message: string): AgentToolResult<any> {\n\treturn {\n\t\tcontent: [{ type: \"text\", text: message }],\n\t\tdetails: {},\n\t};\n}\n\nasync function emitToolExecutionEnd(finalized: FinalizedToolCallOutcome, emit: AgentEventSink): Promise<void> {\n\tawait emit({\n\t\ttype: \"tool_execution_end\",\n\t\ttoolCallId: finalized.toolCall.id,\n\t\ttoolName: finalized.toolCall.name,\n\t\tresult: finalized.result,\n\t\tisError: finalized.isError,\n\t});\n}\n\nfunction createToolResultMessage(finalized: FinalizedToolCallOutcome): ToolResultMessage {\n\treturn {\n\t\trole: \"toolResult\",\n\t\ttoolCallId: finalized.toolCall.id,\n\t\ttoolName: finalized.toolCall.name,\n\t\tcontent: finalized.result.content,\n\t\tdetails: finalized.result.details,\n\t\tisError: finalized.isError,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nasync function emitToolResultMessage(toolResultMessage: ToolResultMessage, emit: AgentEventSink): Promise<void> {\n\tawait emit({ type: \"message_start\", message: toolResultMessage });\n\tawait emit({ type: \"message_end\", message: toolResultMessage });\n}\n"]}
1
+ {"version":3,"file":"agent-loop.js","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAGN,WAAW,EACX,YAAY,EAEZ,qBAAqB,GACrB,MAAM,mBAAmB,CAAC;AAW3B,OAAO,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAIrD;;;GAGG;AACH,MAAM,UAAU,SAAS,CACxB,OAAuB,EACvB,OAAqB,EACrB,MAAuB,EACvB,MAAoB,EACpB,QAAmB,EACuB;IAC1C,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IAEnC,KAAK,YAAY,CAChB,OAAO,EACP,OAAO,EACP,MAAM,EACN,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;QAChB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAAA,CACnB,EACD,MAAM,EACN,QAAQ,CACR;SACC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,CAAC,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC;QACrF,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7C,OAAO,QAAQ,CAAC;IAAA,CAChB,CAAC;SACD,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAAA,CACrB,CAAC,CAAC;IAEJ,OAAO,MAAM,CAAC;AAAA,CACd;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAChC,OAAqB,EACrB,MAAuB,EACvB,MAAoB,EACpB,QAAmB,EACuB;IAC1C,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC5D,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAC;IAEnC,KAAK,oBAAoB,CACxB,OAAO,EACP,MAAM,EACN,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;QAChB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAAA,CACnB,EACD,MAAM,EACN,QAAQ,CACR;SACC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,CAAC,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,IAAI,KAAK,CAAC,CAAC,CAAC;QACrF,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7C,OAAO,QAAQ,CAAC;IAAA,CAChB,CAAC;SACD,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;QACnB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAAA,CACrB,CAAC,CAAC;IAEJ,OAAO,MAAM,CAAC;AAAA,CACd;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,OAAuB,EACvB,OAAqB,EACrB,MAAuB,EACvB,IAAoB,EACpB,MAAoB,EACpB,QAAmB,EACO;IAC1B,MAAM,WAAW,GAAmB,CAAC,GAAG,OAAO,CAAC,CAAC;IACjD,MAAM,cAAc,GAAiB;QACpC,GAAG,OAAO;QACV,QAAQ,EAAE,CAAC,GAAG,OAAO,CAAC,QAAQ,EAAE,GAAG,OAAO,CAAC;KAC3C,CAAC;IAEF,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;IACpC,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;IACnC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC9B,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QACvD,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,OAAO,CAAC,cAAc,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3E,OAAO,WAAW,CAAC;AAAA,CACnB;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACzC,OAAqB,EACrB,MAAuB,EACvB,IAAoB,EACpB,MAAoB,EACpB,QAAmB,EACO;IAC1B,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC5D,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,WAAW,GAAmB,EAAE,CAAC;IACvC,MAAM,cAAc,GAAiB,EAAE,GAAG,OAAO,EAAE,CAAC;IAEpD,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,CAAC;IACpC,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;IAEnC,MAAM,OAAO,CAAC,cAAc,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC3E,OAAO,WAAW,CAAC;AAAA,CACnB;AAED,SAAS,wBAAwB,CAAC,KAAc,EAAE,MAAuB,EAAE,OAAgB,EAAoB;IAC9G,OAAO;QACN,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;QACrC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG;QACrB,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,QAAQ;QAC/B,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE;QACtB,KAAK,EAAE;YACN,KAAK,EAAE,CAAC;YACR,MAAM,EAAE,CAAC;YACT,SAAS,EAAE,CAAC;YACZ,UAAU,EAAE,CAAC;YACb,WAAW,EAAE,CAAC;YACd,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE;SACpE;QACD,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO;QACzC,YAAY,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QACpE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACrB,CAAC;AAAA,CACF;AAED,SAAS,iBAAiB,GAA4C;IACrE,OAAO,IAAI,WAAW,CACrB,CAAC,KAAiB,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,EACjD,CAAC,KAAiB,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CACzE,CAAC;AAAA,CACF;AAED;;;;;GAKG;AACH,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAE/B;;;;;;GAMG;AACH,SAAS,sBAAsB,CAAC,KAA+B,EAAU;IACxE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SAC1B,OAAO,CAAC,gEAAgE,EAAE,QAAQ,CAAC;SACnF,OAAO,CAAC,wDAAwD,EAAE,MAAM,CAAC;SACzE,OAAO,CAAC,qBAAqB,EAAE,OAAO,CAAC;SACvC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAAA,CAC/B;AAED;;GAEG;AACH,KAAK,UAAU,OAAO,CACrB,cAA4B,EAC5B,WAA2B,EAC3B,aAA8B,EAC9B,MAA+B,EAC/B,IAAoB,EACpB,QAAmB,EACH;IAChB,IAAI,cAAc,GAAG,cAAc,CAAC;IACpC,IAAI,MAAM,GAAG,aAAa,CAAC;IAC3B,IAAI,SAAS,GAAG,IAAI,CAAC;IACrB,mGAAmG;IACnG,iGAAiG;IACjG,+FAA+F;IAC/F,8FAA8F;IAC9F,mGAAmG;IACnG,6FAA6F;IAC7F,kGAAkG;IAClG,mGAAmG;IACnG,oBAAoB;IACpB,MAAM,UAAU,GAAG,MAAM,CAAC,aAAa,IAAI,uBAAuB,CAAC;IACnE,MAAM,WAAW,GAAa,EAAE,CAAC;IACjC,2EAA2E;IAC3E,IAAI,eAAe,GAAmB,CAAC,MAAM,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;IAEnF,qFAAqF;IACrF,OAAO,IAAI,EAAE,CAAC;QACb,IAAI,gBAAgB,GAAG,IAAI,CAAC;QAE5B,uDAAuD;QACvD,OAAO,gBAAgB,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvD,IAAI,CAAC,SAAS,EAAE,CAAC;gBAChB,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;YACpC,CAAC;iBAAM,CAAC;gBACP,SAAS,GAAG,KAAK,CAAC;YACnB,CAAC;YAED,mEAAmE;YACnE,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChC,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;oBACvC,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,CAAC;oBAC/C,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;oBAC7C,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;oBACtC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC3B,CAAC;gBACD,eAAe,GAAG,EAAE,CAAC;YACtB,CAAC;YAED,4BAA4B;YAC5B,MAAM,OAAO,GAAG,MAAM,uBAAuB,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;YAC9F,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAE1B,IAAI,OAAO,CAAC,UAAU,KAAK,OAAO,IAAI,OAAO,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;gBACxE,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAC;gBAC3D,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;gBACzD,OAAO;YACR,CAAC;YAED,uBAAuB;YACvB,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;YAEvE,MAAM,WAAW,GAAwB,EAAE,CAAC;YAC5C,gBAAgB,GAAG,KAAK,CAAC;YACzB,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1B,MAAM,iBAAiB,GAAG,MAAM,gBAAgB,CAAC,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;gBAChG,WAAW,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;gBAChD,gBAAgB,GAAG,CAAC,iBAAiB,CAAC,SAAS,CAAC;gBAEhD,KAAK,MAAM,MAAM,IAAI,WAAW,EAAE,CAAC;oBAClC,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;oBACrC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAC1B,CAAC;YACF,CAAC;YAED,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;YAEvD,iFAAiF;YACjF,IAAI,UAAU,GAAG,CAAC,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5C,MAAM,SAAS,GAAG,sBAAsB,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC9F,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBAC5B,IAAI,WAAW,CAAC,MAAM,GAAG,UAAU,GAAG,oBAAoB;oBAAE,WAAW,CAAC,KAAK,EAAE,CAAC;gBAChF,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC/E,IAAI,OAAO,IAAI,UAAU,EAAE,CAAC;oBAC3B,MAAM,CAAC,aAAa,EAAE,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;oBAC/C,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;oBACzD,OAAO;gBACR,CAAC;YACF,CAAC;YAED,MAAM,eAAe,GAAG;gBACvB,OAAO;gBACP,WAAW;gBACX,OAAO,EAAE,cAAc;gBACvB,WAAW;aACX,CAAC;YACF,MAAM,gBAAgB,GAAG,MAAM,MAAM,CAAC,eAAe,EAAE,CAAC,eAAe,CAAC,CAAC;YACzE,IAAI,gBAAgB,EAAE,CAAC;gBACtB,cAAc,GAAG,gBAAgB,CAAC,OAAO,IAAI,cAAc,CAAC;gBAC5D,MAAM,GAAG;oBACR,GAAG,MAAM;oBACT,KAAK,EAAE,gBAAgB,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK;oBAC7C,SAAS,EACR,gBAAgB,CAAC,aAAa,KAAK,SAAS;wBAC3C,CAAC,CAAC,MAAM,CAAC,SAAS;wBAClB,CAAC,CAAC,gBAAgB,CAAC,aAAa,KAAK,KAAK;4BACzC,CAAC,CAAC,SAAS;4BACX,CAAC,CAAC,gBAAgB,CAAC,aAAa;iBACnC,CAAC;YACH,CAAC;YAED,IACC,MAAM,MAAM,CAAC,mBAAmB,EAAE,CAAC;gBAClC,OAAO;gBACP,WAAW;gBACX,OAAO,EAAE,cAAc;gBACvB,WAAW;aACX,CAAC,EACD,CAAC;gBACF,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;gBACzD,OAAO;YACR,CAAC;YAED,eAAe,GAAG,CAAC,MAAM,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;QAChE,CAAC;QAED,uDAAuD;QACvD,MAAM,gBAAgB,GAAG,CAAC,MAAM,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC;QACtE,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACjC,8CAA8C;YAC9C,eAAe,GAAG,gBAAgB,CAAC;YACnC,SAAS;QACV,CAAC;QAED,yBAAyB;QACzB,MAAM;IACP,CAAC;IAED,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC;AAAA,CACzD;AAED;;;GAGG;AACH,KAAK,UAAU,uBAAuB,CACrC,OAAqB,EACrB,MAAuB,EACvB,MAA+B,EAC/B,IAAoB,EACpB,QAAmB,EACS;IAC5B,4EAA0E;IAC1E,IAAI,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAChC,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAC7B,QAAQ,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC5D,CAAC;IAED,oEAAkE;IAClE,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAExD,oBAAoB;IACpB,MAAM,UAAU,GAAY;QAC3B,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,QAAQ,EAAE,WAAW;QACrB,KAAK,EAAE,OAAO,CAAC,KAAK;KACpB,CAAC;IAEF,MAAM,cAAc,GAAG,QAAQ,IAAI,YAAY,CAAC;IAEhD,kDAAkD;IAClD,MAAM,cAAc,GACnB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC;IAEjG,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE;QAC/D,GAAG,MAAM;QACT,MAAM,EAAE,cAAc;QACtB,MAAM;KACN,CAAC,CAAC;IAEH,IAAI,cAAc,GAA4B,IAAI,CAAC;IACnD,IAAI,YAAY,GAAG,KAAK,CAAC;IAEzB,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QACpC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,OAAO;gBACX,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC;gBAC/B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;gBACtC,YAAY,GAAG,IAAI,CAAC;gBACpB,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,GAAG,cAAc,EAAE,EAAE,CAAC,CAAC;gBACtE,MAAM;YAEP,KAAK,YAAY,CAAC;YAClB,KAAK,YAAY,CAAC;YAClB,KAAK,UAAU,CAAC;YAChB,KAAK,gBAAgB,CAAC;YACtB,KAAK,gBAAgB,CAAC;YACtB,KAAK,cAAc,CAAC;YACpB,KAAK,gBAAgB,CAAC;YACtB,KAAK,gBAAgB,CAAC;YACtB,KAAK,cAAc;gBAClB,IAAI,cAAc,EAAE,CAAC;oBACpB,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC;oBAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,cAAc,CAAC;oBAC/D,MAAM,IAAI,CAAC;wBACV,IAAI,EAAE,gBAAgB;wBACtB,qBAAqB,EAAE,KAAK;wBAC5B,OAAO,EAAE,EAAE,GAAG,cAAc,EAAE;qBAC9B,CAAC,CAAC;gBACJ,CAAC;gBACD,MAAM;YAEP,KAAK,MAAM,CAAC;YACZ,KAAK,OAAO,EAAE,CAAC;gBACd,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;gBAC7C,IAAI,YAAY,EAAE,CAAC;oBAClB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,YAAY,CAAC;gBAC9D,CAAC;qBAAM,CAAC;oBACP,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBACrC,CAAC;gBACD,IAAI,CAAC,YAAY,EAAE,CAAC;oBACnB,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,GAAG,YAAY,EAAE,EAAE,CAAC,CAAC;gBACrE,CAAC;gBACD,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;gBAC3D,OAAO,YAAY,CAAC;YACrB,CAAC;QACF,CAAC;IACF,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;IAC7C,IAAI,YAAY,EAAE,CAAC;QAClB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,YAAY,CAAC;IAC9D,CAAC;SAAM,CAAC;QACP,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACpC,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,GAAG,YAAY,EAAE,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAC;IAC3D,OAAO,YAAY,CAAC;AAAA,CACpB;AAED;;GAEG;AACH,KAAK,UAAU,gBAAgB,CAC9B,cAA4B,EAC5B,gBAAkC,EAClC,MAAuB,EACvB,MAA+B,EAC/B,IAAoB,EACa;IACjC,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC;IAChF,MAAM,qBAAqB,GAAG,SAAS,CAAC,IAAI,CAC3C,CAAC,EAAE,EAAE,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,aAAa,KAAK,YAAY,CAC7F,CAAC;IACF,IAAI,MAAM,CAAC,aAAa,KAAK,YAAY,IAAI,qBAAqB,EAAE,CAAC;QACpE,OAAO,0BAA0B,CAAC,cAAc,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IACtG,CAAC;IACD,OAAO,wBAAwB,CAAC,cAAc,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AAAA,CACnG;AAOD,KAAK,UAAU,0BAA0B,CACxC,cAA4B,EAC5B,gBAAkC,EAClC,SAA0B,EAC1B,MAAuB,EACvB,MAA+B,EAC/B,IAAoB,EACa;IACjC,MAAM,cAAc,GAA+B,EAAE,CAAC;IACtD,MAAM,QAAQ,GAAwB,EAAE,CAAC;IAEzC,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QAClC,MAAM,IAAI,CAAC;YACV,IAAI,EAAE,sBAAsB;YAC5B,UAAU,EAAE,QAAQ,CAAC,EAAE;YACvB,QAAQ,EAAE,QAAQ,CAAC,IAAI;YACvB,IAAI,EAAE,QAAQ,CAAC,SAAS;SACxB,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,cAAc,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACtG,IAAI,SAAmC,CAAC;QACxC,IAAI,WAAW,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACtC,SAAS,GAAG;gBACX,QAAQ;gBACR,MAAM,EAAE,WAAW,CAAC,MAAM;gBAC1B,OAAO,EAAE,WAAW,CAAC,OAAO;aAC5B,CAAC;QACH,CAAC;aAAM,CAAC;YACP,MAAM,QAAQ,GAAG,MAAM,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YAC1E,SAAS,GAAG,MAAM,wBAAwB,CACzC,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,QAAQ,EACR,MAAM,EACN,MAAM,CACN,CAAC;QACH,CAAC;QAED,MAAM,oBAAoB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;QAC5C,MAAM,iBAAiB,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;QAC7D,MAAM,qBAAqB,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;QACrD,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAC/B,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAEjC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACrB,MAAM;QACP,CAAC;IACF,CAAC;IAED,OAAO;QACN,QAAQ;QACR,SAAS,EAAE,wBAAwB,CAAC,cAAc,CAAC;KACnD,CAAC;AAAA,CACF;AAED,KAAK,UAAU,wBAAwB,CACtC,cAA4B,EAC5B,gBAAkC,EAClC,SAA0B,EAC1B,MAAuB,EACvB,MAA+B,EAC/B,IAAoB,EACa;IACjC,MAAM,cAAc,GAA6B,EAAE,CAAC;IAEpD,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QAClC,MAAM,IAAI,CAAC;YACV,IAAI,EAAE,sBAAsB;YAC5B,UAAU,EAAE,QAAQ,CAAC,EAAE;YACvB,QAAQ,EAAE,QAAQ,CAAC,IAAI;YACvB,IAAI,EAAE,QAAQ,CAAC,SAAS;SACxB,CAAC,CAAC;QAEH,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,cAAc,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;QACtG,IAAI,WAAW,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACtC,MAAM,SAAS,GAAG;gBACjB,QAAQ;gBACR,MAAM,EAAE,WAAW,CAAC,MAAM;gBAC1B,OAAO,EAAE,WAAW,CAAC,OAAO;aACO,CAAC;YACrC,MAAM,oBAAoB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAC/B,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACrB,MAAM;YACP,CAAC;YACD,SAAS;QACV,CAAC;QAED,cAAc,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YAC/B,MAAM,QAAQ,GAAG,MAAM,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;YAC1E,MAAM,SAAS,GAAG,MAAM,wBAAwB,CAC/C,cAAc,EACd,gBAAgB,EAChB,WAAW,EACX,QAAQ,EACR,MAAM,EACN,MAAM,CACN,CAAC;YACF,MAAM,oBAAoB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC5C,OAAO,SAAS,CAAC;QAAA,CACjB,CAAC,CAAC;QACH,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACrB,MAAM;QACP,CAAC;IACF,CAAC;IAED,MAAM,qBAAqB,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9C,cAAc,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAC/F,CAAC;IACF,MAAM,QAAQ,GAAwB,EAAE,CAAC;IACzC,KAAK,MAAM,SAAS,IAAI,qBAAqB,EAAE,CAAC;QAC/C,MAAM,iBAAiB,GAAG,uBAAuB,CAAC,SAAS,CAAC,CAAC;QAC7D,MAAM,qBAAqB,CAAC,iBAAiB,EAAE,IAAI,CAAC,CAAC;QACrD,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;IAClC,CAAC;IAED,OAAO;QACN,QAAQ;QACR,SAAS,EAAE,wBAAwB,CAAC,qBAAqB,CAAC;KAC1D,CAAC;AAAA,CACF;AA4BD,SAAS,wBAAwB,CAAC,cAA0C,EAAW;IACtF,OAAO,cAAc,CAAC,MAAM,GAAG,CAAC,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC;AAAA,CAC7G;AAED,SAAS,wBAAwB,CAAC,IAAoB,EAAE,QAAuB,EAAiB;IAC/F,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC5B,OAAO,QAAQ,CAAC;IACjB,CAAC;IACD,MAAM,iBAAiB,GAAG,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACpE,IAAI,iBAAiB,KAAK,QAAQ,CAAC,SAAS,EAAE,CAAC;QAC9C,OAAO,QAAQ,CAAC;IACjB,CAAC;IACD,OAAO;QACN,GAAG,QAAQ;QACX,SAAS,EAAE,iBAAwC;KACnD,CAAC;AAAA,CACF;AAED,KAAK,UAAU,eAAe,CAC7B,cAA4B,EAC5B,gBAAkC,EAClC,QAAuB,EACvB,MAAuB,EACvB,MAA+B,EACwB;IACvD,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI,CAAC,CAAC;IACzE,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,OAAO;YACN,IAAI,EAAE,WAAW;YACjB,MAAM,EAAE,qBAAqB,CAAC,QAAQ,QAAQ,CAAC,IAAI,YAAY,CAAC;YAChE,OAAO,EAAE,IAAI;SACb,CAAC;IACH,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,gBAAgB,GAAG,wBAAwB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAClE,MAAM,aAAa,GAAG,qBAAqB,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;QACpE,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;YAC3B,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,cAAc,CAC/C;gBACC,gBAAgB;gBAChB,QAAQ;gBACR,IAAI,EAAE,aAAa;gBACnB,OAAO,EAAE,cAAc;aACvB,EACD,MAAM,CACN,CAAC;YACF,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;gBACrB,OAAO;oBACN,IAAI,EAAE,WAAW;oBACjB,MAAM,EAAE,qBAAqB,CAAC,mBAAmB,CAAC;oBAClD,OAAO,EAAE,IAAI;iBACb,CAAC;YACH,CAAC;YACD,IAAI,YAAY,EAAE,KAAK,EAAE,CAAC;gBACzB,OAAO;oBACN,IAAI,EAAE,WAAW;oBACjB,MAAM,EAAE,qBAAqB,CAAC,YAAY,CAAC,MAAM,IAAI,4BAA4B,CAAC;oBAClF,OAAO,EAAE,IAAI;iBACb,CAAC;YACH,CAAC;QACF,CAAC;QACD,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACrB,OAAO;gBACN,IAAI,EAAE,WAAW;gBACjB,MAAM,EAAE,qBAAqB,CAAC,mBAAmB,CAAC;gBAClD,OAAO,EAAE,IAAI;aACb,CAAC;QACH,CAAC;QACD,OAAO;YACN,IAAI,EAAE,UAAU;YAChB,QAAQ;YACR,IAAI;YACJ,IAAI,EAAE,aAAa;SACnB,CAAC;IACH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,OAAO;YACN,IAAI,EAAE,WAAW;YACjB,MAAM,EAAE,qBAAqB,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACrF,OAAO,EAAE,IAAI;SACb,CAAC;IACH,CAAC;AAAA,CACD;AAED,KAAK,UAAU,uBAAuB,CACrC,QAA0B,EAC1B,MAA+B,EAC/B,IAAoB,EACe;IACnC,MAAM,YAAY,GAAoB,EAAE,CAAC;IAEzC,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,CACzC,QAAQ,CAAC,QAAQ,CAAC,EAAE,EACpB,QAAQ,CAAC,IAAa,EACtB,MAAM,EACN,CAAC,aAAa,EAAE,EAAE,CAAC;YAClB,YAAY,CAAC,IAAI,CAChB,OAAO,CAAC,OAAO,CACd,IAAI,CAAC;gBACJ,IAAI,EAAE,uBAAuB;gBAC7B,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE;gBAChC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI;gBAChC,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,SAAS;gBACjC,aAAa;aACb,CAAC,CACF,CACD,CAAC;QAAA,CACF,CACD,CAAC;QACF,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACnC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QAChC,OAAO;YACN,MAAM,EAAE,qBAAqB,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACrF,OAAO,EAAE,IAAI;SACb,CAAC;IACH,CAAC;AAAA,CACD;AAED,KAAK,UAAU,wBAAwB,CACtC,cAA4B,EAC5B,gBAAkC,EAClC,QAA0B,EAC1B,QAAiC,EACjC,MAAuB,EACvB,MAA+B,EACK;IACpC,IAAI,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC7B,IAAI,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC;IAE/B,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;QAC1B,IAAI,CAAC;YACJ,MAAM,WAAW,GAAG,MAAM,MAAM,CAAC,aAAa,CAC7C;gBACC,gBAAgB;gBAChB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;gBAC3B,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,MAAM;gBACN,OAAO;gBACP,OAAO,EAAE,cAAc;aACvB,EACD,MAAM,CACN,CAAC;YACF,IAAI,WAAW,EAAE,CAAC;gBACjB,MAAM,GAAG;oBACR,OAAO,EAAE,WAAW,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO;oBAC9C,OAAO,EAAE,WAAW,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO;oBAC9C,SAAS,EAAE,WAAW,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS;iBACpD,CAAC;gBACF,OAAO,GAAG,WAAW,CAAC,OAAO,IAAI,OAAO,CAAC;YAC1C,CAAC;QACF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,MAAM,GAAG,qBAAqB,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACvF,OAAO,GAAG,IAAI,CAAC;QAChB,CAAC;IACF,CAAC;IAED,OAAO;QACN,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,MAAM;QACN,OAAO;KACP,CAAC;AAAA,CACF;AAED,SAAS,qBAAqB,CAAC,OAAe,EAAwB;IACrE,OAAO;QACN,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;QAC1C,OAAO,EAAE,EAAE;KACX,CAAC;AAAA,CACF;AAED,KAAK,UAAU,oBAAoB,CAAC,SAAmC,EAAE,IAAoB,EAAiB;IAC7G,MAAM,IAAI,CAAC;QACV,IAAI,EAAE,oBAAoB;QAC1B,UAAU,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE;QACjC,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,IAAI;QACjC,MAAM,EAAE,SAAS,CAAC,MAAM;QACxB,OAAO,EAAE,SAAS,CAAC,OAAO;KAC1B,CAAC,CAAC;AAAA,CACH;AAED,SAAS,uBAAuB,CAAC,SAAmC,EAAqB;IACxF,OAAO;QACN,IAAI,EAAE,YAAY;QAClB,UAAU,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE;QACjC,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,IAAI;QACjC,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,OAAO;QACjC,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,OAAO;QACjC,OAAO,EAAE,SAAS,CAAC,OAAO;QAC1B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACrB,CAAC;AAAA,CACF;AAED,KAAK,UAAU,qBAAqB,CAAC,iBAAoC,EAAE,IAAoB,EAAiB;IAC/G,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;IAClE,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;AAAA,CAChE","sourcesContent":["/**\n * Agent loop that works with AgentMessage throughout.\n * Transforms to Message[] only at the LLM call boundary.\n */\n\nimport {\n\ttype AssistantMessage,\n\ttype Context,\n\tEventStream,\n\tstreamSimple,\n\ttype ToolResultMessage,\n\tvalidateToolArguments,\n} from \"@caupulican/pi-ai\";\nimport type {\n\tAgentContext,\n\tAgentEvent,\n\tAgentLoopConfig,\n\tAgentMessage,\n\tAgentTool,\n\tAgentToolCall,\n\tAgentToolResult,\n\tStreamFn,\n} from \"./types.ts\";\nimport { DEFAULT_MAX_STALL_TURNS } from \"./types.ts\";\n\nexport type AgentEventSink = (event: AgentEvent) => Promise<void> | void;\n\n/**\n * Start an agent loop with a new prompt message.\n * The prompt is added to the context and events are emitted for it.\n */\nexport function agentLoop(\n\tprompts: AgentMessage[],\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): EventStream<AgentEvent, AgentMessage[]> {\n\tconst stream = createAgentStream();\n\n\tvoid runAgentLoop(\n\t\tprompts,\n\t\tcontext,\n\t\tconfig,\n\t\tasync (event) => {\n\t\t\tstream.push(event);\n\t\t},\n\t\tsignal,\n\t\tstreamFn,\n\t)\n\t\t.catch(async (error) => {\n\t\t\tconst messages = [createLoopFailureMessage(error, config, signal?.aborted ?? false)];\n\t\t\tstream.push({ type: \"agent_end\", messages });\n\t\t\treturn messages;\n\t\t})\n\t\t.then((messages) => {\n\t\t\tstream.end(messages);\n\t\t});\n\n\treturn stream;\n}\n\n/**\n * Continue an agent loop from the current context without adding a new message.\n * Used for retries - context already has user message or tool results.\n *\n * **Important:** The last message in context must convert to a `user` or `toolResult` message\n * via `convertToLlm`. If it doesn't, the LLM provider will reject the request.\n * This cannot be validated here since `convertToLlm` is only called once per turn.\n */\nexport function agentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): EventStream<AgentEvent, AgentMessage[]> {\n\tif (context.messages.length === 0) {\n\t\tthrow new Error(\"Cannot continue: no messages in context\");\n\t}\n\n\tif (context.messages[context.messages.length - 1].role === \"assistant\") {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst stream = createAgentStream();\n\n\tvoid runAgentLoopContinue(\n\t\tcontext,\n\t\tconfig,\n\t\tasync (event) => {\n\t\t\tstream.push(event);\n\t\t},\n\t\tsignal,\n\t\tstreamFn,\n\t)\n\t\t.catch(async (error) => {\n\t\t\tconst messages = [createLoopFailureMessage(error, config, signal?.aborted ?? false)];\n\t\t\tstream.push({ type: \"agent_end\", messages });\n\t\t\treturn messages;\n\t\t})\n\t\t.then((messages) => {\n\t\t\tstream.end(messages);\n\t\t});\n\n\treturn stream;\n}\n\nexport async function runAgentLoop(\n\tprompts: AgentMessage[],\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\temit: AgentEventSink,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): Promise<AgentMessage[]> {\n\tconst newMessages: AgentMessage[] = [...prompts];\n\tconst currentContext: AgentContext = {\n\t\t...context,\n\t\tmessages: [...context.messages, ...prompts],\n\t};\n\n\tawait emit({ type: \"agent_start\" });\n\tawait emit({ type: \"turn_start\" });\n\tfor (const prompt of prompts) {\n\t\tawait emit({ type: \"message_start\", message: prompt });\n\t\tawait emit({ type: \"message_end\", message: prompt });\n\t}\n\n\tawait runLoop(currentContext, newMessages, config, signal, emit, streamFn);\n\treturn newMessages;\n}\n\nexport async function runAgentLoopContinue(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\temit: AgentEventSink,\n\tsignal?: AbortSignal,\n\tstreamFn?: StreamFn,\n): Promise<AgentMessage[]> {\n\tif (context.messages.length === 0) {\n\t\tthrow new Error(\"Cannot continue: no messages in context\");\n\t}\n\n\tif (context.messages[context.messages.length - 1].role === \"assistant\") {\n\t\tthrow new Error(\"Cannot continue from message role: assistant\");\n\t}\n\n\tconst newMessages: AgentMessage[] = [];\n\tconst currentContext: AgentContext = { ...context };\n\n\tawait emit({ type: \"agent_start\" });\n\tawait emit({ type: \"turn_start\" });\n\n\tawait runLoop(currentContext, newMessages, config, signal, emit, streamFn);\n\treturn newMessages;\n}\n\nfunction createLoopFailureMessage(error: unknown, config: AgentLoopConfig, aborted: boolean): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [{ type: \"text\", text: \"\" }],\n\t\tapi: config.model.api,\n\t\tprovider: config.model.provider,\n\t\tmodel: config.model.id,\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\tstopReason: aborted ? \"aborted\" : \"error\",\n\t\terrorMessage: error instanceof Error ? error.message : String(error),\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nfunction createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {\n\treturn new EventStream<AgentEvent, AgentMessage[]>(\n\t\t(event: AgentEvent) => event.type === \"agent_end\",\n\t\t(event: AgentEvent) => (event.type === \"agent_end\" ? event.messages : []),\n\t);\n}\n\n/**\n * How many `stallLimit`-length periods the runaway-loop window spans. A window of `stallLimit * P`\n * turns lets the count-based detector catch oscillating cycles of period up to `P` (each signature in a\n * period-k cycle recurs ~window/k times), not just back-to-back repeats. Beyond this the cycle is loose\n * enough that it's indistinguishable from legitimate varied work, so we don't chase it.\n */\nconst STALL_WINDOW_PERIODS = 4;\n\n/**\n * Normalize a tool-call batch into a stable signature for runaway-loop detection. Volatile argument\n * tokens — epoch/timestamps, UUIDs, long hashes/nonces — are masked so a model retrying the SAME call\n * with a fresh timestamp/id each turn still collapses to one signature and is detected (bug #28). Only\n * clearly-volatile patterns are masked: short numbers (`file2.ts`, `line 42`, `count: 3`) are kept so\n * genuinely-distinct calls (reading numbered files, different line ranges) are NOT falsely merged.\n */\nfunction normalizeToolSignature(pairs: Array<[string, unknown]>): string {\n\treturn JSON.stringify(pairs)\n\t\t.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, \"<uuid>\")\n\t\t.replace(/\\d{4}-\\d{2}-\\d{2}[tT][0-9:.]+(?:z|[+-]\\d{2}:?\\d{2})?/gi, \"<ts>\")\n\t\t.replace(/\\b[0-9a-f]{16,}\\b/gi, \"<hex>\")\n\t\t.replace(/\\d{10,}/g, \"<num>\");\n}\n\n/**\n * Main loop logic shared by agentLoop and agentLoopContinue.\n */\nasync function runLoop(\n\tinitialContext: AgentContext,\n\tnewMessages: AgentMessage[],\n\tinitialConfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n): Promise<void> {\n\tlet currentContext = initialContext;\n\tlet config = initialConfig;\n\tlet firstTurn = true;\n\t// Runaway-loop backstop state: a sliding window of recent NORMALIZED tool-call signatures. A model\n\t// wedged repeating the same action makes no progress but keeps spending tokens; if one signature\n\t// recurs `stallLimit` times within the window we stop gracefully. Signatures are normalized so\n\t// volatile args (timestamps/UUIDs/nonces that change every call) can't disguise an otherwise-\n\t// identical call (bug #28). The window spans `stallLimit * STALL_WINDOW_PERIODS` turns so periodic\n\t// oscillation is caught too, not just back-to-back repeats: a cycle of period P repeats each\n\t// signature ~window/P times, so any P up to STALL_WINDOW_PERIODS reaches the threshold before the\n\t// window slides past it. Counts only turns that issued tool calls, so varied/long work never trips\n\t// it. `0` disables.\n\tconst stallLimit = config.maxStallTurns ?? DEFAULT_MAX_STALL_TURNS;\n\tconst stallWindow: string[] = [];\n\t// Check for steering messages at start (user may have typed while waiting)\n\tlet pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];\n\n\t// Outer loop: continues when queued follow-up messages arrive after agent would stop\n\twhile (true) {\n\t\tlet hasMoreToolCalls = true;\n\n\t\t// Inner loop: process tool calls and steering messages\n\t\twhile (hasMoreToolCalls || pendingMessages.length > 0) {\n\t\t\tif (!firstTurn) {\n\t\t\t\tawait emit({ type: \"turn_start\" });\n\t\t\t} else {\n\t\t\t\tfirstTurn = false;\n\t\t\t}\n\n\t\t\t// Process pending messages (inject before next assistant response)\n\t\t\tif (pendingMessages.length > 0) {\n\t\t\t\tfor (const message of pendingMessages) {\n\t\t\t\t\tawait emit({ type: \"message_start\", message });\n\t\t\t\t\tawait emit({ type: \"message_end\", message });\n\t\t\t\t\tcurrentContext.messages.push(message);\n\t\t\t\t\tnewMessages.push(message);\n\t\t\t\t}\n\t\t\t\tpendingMessages = [];\n\t\t\t}\n\n\t\t\t// Stream assistant response\n\t\t\tconst message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);\n\t\t\tnewMessages.push(message);\n\n\t\t\tif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\t\t\t\tawait emit({ type: \"turn_end\", message, toolResults: [] });\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Check for tool calls\n\t\t\tconst toolCalls = message.content.filter((c) => c.type === \"toolCall\");\n\n\t\t\tconst toolResults: ToolResultMessage[] = [];\n\t\t\thasMoreToolCalls = false;\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\tconst executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);\n\t\t\t\ttoolResults.push(...executedToolBatch.messages);\n\t\t\t\thasMoreToolCalls = !executedToolBatch.terminate;\n\n\t\t\t\tfor (const result of toolResults) {\n\t\t\t\t\tcurrentContext.messages.push(result);\n\t\t\t\t\tnewMessages.push(result);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait emit({ type: \"turn_end\", message, toolResults });\n\n\t\t\t// Runaway-loop backstop (cost guard): detect a model stuck repeating one action.\n\t\t\tif (stallLimit > 0 && toolCalls.length > 0) {\n\t\t\t\tconst signature = normalizeToolSignature(toolCalls.map((c) => [c.name, c.arguments ?? null]));\n\t\t\t\tstallWindow.push(signature);\n\t\t\t\tif (stallWindow.length > stallLimit * STALL_WINDOW_PERIODS) stallWindow.shift();\n\t\t\t\tconst repeats = stallWindow.reduce((n, s) => (s === signature ? n + 1 : n), 0);\n\t\t\t\tif (repeats >= stallLimit) {\n\t\t\t\t\tconfig.onRunawayStop?.({ signature, repeats });\n\t\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst nextTurnContext = {\n\t\t\t\tmessage,\n\t\t\t\ttoolResults,\n\t\t\t\tcontext: currentContext,\n\t\t\t\tnewMessages,\n\t\t\t};\n\t\t\tconst nextTurnSnapshot = await config.prepareNextTurn?.(nextTurnContext);\n\t\t\tif (nextTurnSnapshot) {\n\t\t\t\tcurrentContext = nextTurnSnapshot.context ?? currentContext;\n\t\t\t\tconfig = {\n\t\t\t\t\t...config,\n\t\t\t\t\tmodel: nextTurnSnapshot.model ?? config.model,\n\t\t\t\t\treasoning:\n\t\t\t\t\t\tnextTurnSnapshot.thinkingLevel === undefined\n\t\t\t\t\t\t\t? config.reasoning\n\t\t\t\t\t\t\t: nextTurnSnapshot.thinkingLevel === \"off\"\n\t\t\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t\t\t: nextTurnSnapshot.thinkingLevel,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tawait config.shouldStopAfterTurn?.({\n\t\t\t\t\tmessage,\n\t\t\t\t\ttoolResults,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t\tnewMessages,\n\t\t\t\t})\n\t\t\t) {\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tpendingMessages = (await config.getSteeringMessages?.()) || [];\n\t\t}\n\n\t\t// Agent would stop here. Check for follow-up messages.\n\t\tconst followUpMessages = (await config.getFollowUpMessages?.()) || [];\n\t\tif (followUpMessages.length > 0) {\n\t\t\t// Set as pending so inner loop processes them\n\t\t\tpendingMessages = followUpMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// No more messages, exit\n\t\tbreak;\n\t}\n\n\tawait emit({ type: \"agent_end\", messages: newMessages });\n}\n\n/**\n * Stream an assistant response from the LLM.\n * This is where AgentMessage[] gets transformed to Message[] for the LLM.\n */\nasync function streamAssistantResponse(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\t// Apply context transform if configured (AgentMessage[] → AgentMessage[])\n\tlet messages = context.messages;\n\tif (config.transformContext) {\n\t\tmessages = await config.transformContext(messages, signal);\n\t}\n\n\t// Convert to LLM-compatible messages (AgentMessage[] → Message[])\n\tconst llmMessages = await config.convertToLlm(messages);\n\n\t// Build LLM context\n\tconst llmContext: Context = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: llmMessages,\n\t\ttools: context.tools,\n\t};\n\n\tconst streamFunction = streamFn || streamSimple;\n\n\t// Resolve API key (important for expiring tokens)\n\tconst resolvedApiKey =\n\t\t(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;\n\n\tconst response = await streamFunction(config.model, llmContext, {\n\t\t...config,\n\t\tapiKey: resolvedApiKey,\n\t\tsignal,\n\t});\n\n\tlet partialMessage: AssistantMessage | null = null;\n\tlet addedPartial = false;\n\n\tfor await (const event of response) {\n\t\tswitch (event.type) {\n\t\t\tcase \"start\":\n\t\t\t\tpartialMessage = event.partial;\n\t\t\t\tcontext.messages.push(partialMessage);\n\t\t\t\taddedPartial = true;\n\t\t\t\tawait emit({ type: \"message_start\", message: { ...partialMessage } });\n\t\t\t\tbreak;\n\n\t\t\tcase \"text_start\":\n\t\t\tcase \"text_delta\":\n\t\t\tcase \"text_end\":\n\t\t\tcase \"thinking_start\":\n\t\t\tcase \"thinking_delta\":\n\t\t\tcase \"thinking_end\":\n\t\t\tcase \"toolcall_start\":\n\t\t\tcase \"toolcall_delta\":\n\t\t\tcase \"toolcall_end\":\n\t\t\t\tif (partialMessage) {\n\t\t\t\t\tpartialMessage = event.partial;\n\t\t\t\t\tcontext.messages[context.messages.length - 1] = partialMessage;\n\t\t\t\t\tawait emit({\n\t\t\t\t\t\ttype: \"message_update\",\n\t\t\t\t\t\tassistantMessageEvent: event,\n\t\t\t\t\t\tmessage: { ...partialMessage },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"done\":\n\t\t\tcase \"error\": {\n\t\t\t\tconst finalMessage = await response.result();\n\t\t\t\tif (addedPartial) {\n\t\t\t\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t\t\t\t} else {\n\t\t\t\t\tcontext.messages.push(finalMessage);\n\t\t\t\t}\n\t\t\t\tif (!addedPartial) {\n\t\t\t\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t\t\t\t}\n\t\t\t\tawait emit({ type: \"message_end\", message: finalMessage });\n\t\t\t\treturn finalMessage;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst finalMessage = await response.result();\n\tif (addedPartial) {\n\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t} else {\n\t\tcontext.messages.push(finalMessage);\n\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t}\n\tawait emit({ type: \"message_end\", message: finalMessage });\n\treturn finalMessage;\n}\n\n/**\n * Execute tool calls from an assistant message.\n */\nasync function executeToolCalls(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst toolCalls = assistantMessage.content.filter((c) => c.type === \"toolCall\");\n\tconst hasSequentialToolCall = toolCalls.some(\n\t\t(tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === \"sequential\",\n\t);\n\tif (config.toolExecution === \"sequential\" || hasSequentialToolCall) {\n\t\treturn executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit);\n\t}\n\treturn executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);\n}\n\ntype ExecutedToolCallBatch = {\n\tmessages: ToolResultMessage[];\n\tterminate: boolean;\n};\n\nasync function executeToolCallsSequential(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: AgentToolCall[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallOutcome[] = [];\n\tconst messages: ToolResultMessage[] = [];\n\n\tfor (const toolCall of toolCalls) {\n\t\tawait emit({\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: toolCall.id,\n\t\t\ttoolName: toolCall.name,\n\t\t\targs: toolCall.arguments,\n\t\t});\n\n\t\tconst preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);\n\t\tlet finalized: FinalizedToolCallOutcome;\n\t\tif (preparation.kind === \"immediate\") {\n\t\t\tfinalized = {\n\t\t\t\ttoolCall,\n\t\t\t\tresult: preparation.result,\n\t\t\t\tisError: preparation.isError,\n\t\t\t};\n\t\t} else {\n\t\t\tconst executed = await executePreparedToolCall(preparation, signal, emit);\n\t\t\tfinalized = await finalizeExecutedToolCall(\n\t\t\t\tcurrentContext,\n\t\t\t\tassistantMessage,\n\t\t\t\tpreparation,\n\t\t\t\texecuted,\n\t\t\t\tconfig,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t}\n\n\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\tconst toolResultMessage = createToolResultMessage(finalized);\n\t\tawait emitToolResultMessage(toolResultMessage, emit);\n\t\tfinalizedCalls.push(finalized);\n\t\tmessages.push(toolResultMessage);\n\n\t\tif (signal?.aborted) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn {\n\t\tmessages,\n\t\tterminate: shouldTerminateToolBatch(finalizedCalls),\n\t};\n}\n\nasync function executeToolCallsParallel(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: AgentToolCall[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallEntry[] = [];\n\n\tfor (const toolCall of toolCalls) {\n\t\tawait emit({\n\t\t\ttype: \"tool_execution_start\",\n\t\t\ttoolCallId: toolCall.id,\n\t\t\ttoolName: toolCall.name,\n\t\t\targs: toolCall.arguments,\n\t\t});\n\n\t\tconst preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);\n\t\tif (preparation.kind === \"immediate\") {\n\t\t\tconst finalized = {\n\t\t\t\ttoolCall,\n\t\t\t\tresult: preparation.result,\n\t\t\t\tisError: preparation.isError,\n\t\t\t} satisfies FinalizedToolCallOutcome;\n\t\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\t\tfinalizedCalls.push(finalized);\n\t\t\tif (signal?.aborted) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tfinalizedCalls.push(async () => {\n\t\t\tconst executed = await executePreparedToolCall(preparation, signal, emit);\n\t\t\tconst finalized = await finalizeExecutedToolCall(\n\t\t\t\tcurrentContext,\n\t\t\t\tassistantMessage,\n\t\t\t\tpreparation,\n\t\t\t\texecuted,\n\t\t\t\tconfig,\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tawait emitToolExecutionEnd(finalized, emit);\n\t\t\treturn finalized;\n\t\t});\n\t\tif (signal?.aborted) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst orderedFinalizedCalls = await Promise.all(\n\t\tfinalizedCalls.map((entry) => (typeof entry === \"function\" ? entry() : Promise.resolve(entry))),\n\t);\n\tconst messages: ToolResultMessage[] = [];\n\tfor (const finalized of orderedFinalizedCalls) {\n\t\tconst toolResultMessage = createToolResultMessage(finalized);\n\t\tawait emitToolResultMessage(toolResultMessage, emit);\n\t\tmessages.push(toolResultMessage);\n\t}\n\n\treturn {\n\t\tmessages,\n\t\tterminate: shouldTerminateToolBatch(orderedFinalizedCalls),\n\t};\n}\n\ntype PreparedToolCall = {\n\tkind: \"prepared\";\n\ttoolCall: AgentToolCall;\n\ttool: AgentTool<any>;\n\targs: unknown;\n};\n\ntype ImmediateToolCallOutcome = {\n\tkind: \"immediate\";\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype ExecutedToolCallOutcome = {\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype FinalizedToolCallOutcome = {\n\ttoolCall: AgentToolCall;\n\tresult: AgentToolResult<any>;\n\tisError: boolean;\n};\n\ntype FinalizedToolCallEntry = FinalizedToolCallOutcome | (() => Promise<FinalizedToolCallOutcome>);\n\nfunction shouldTerminateToolBatch(finalizedCalls: FinalizedToolCallOutcome[]): boolean {\n\treturn finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);\n}\n\nfunction prepareToolCallArguments(tool: AgentTool<any>, toolCall: AgentToolCall): AgentToolCall {\n\tif (!tool.prepareArguments) {\n\t\treturn toolCall;\n\t}\n\tconst preparedArguments = tool.prepareArguments(toolCall.arguments);\n\tif (preparedArguments === toolCall.arguments) {\n\t\treturn toolCall;\n\t}\n\treturn {\n\t\t...toolCall,\n\t\targuments: preparedArguments as Record<string, any>,\n\t};\n}\n\nasync function prepareToolCall(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCall: AgentToolCall,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n): Promise<PreparedToolCall | ImmediateToolCallOutcome> {\n\tconst tool = currentContext.tools?.find((t) => t.name === toolCall.name);\n\tif (!tool) {\n\t\treturn {\n\t\t\tkind: \"immediate\",\n\t\t\tresult: createErrorToolResult(`Tool ${toolCall.name} not found`),\n\t\t\tisError: true,\n\t\t};\n\t}\n\n\ttry {\n\t\tconst preparedToolCall = prepareToolCallArguments(tool, toolCall);\n\t\tconst validatedArgs = validateToolArguments(tool, preparedToolCall);\n\t\tif (config.beforeToolCall) {\n\t\t\tconst beforeResult = await config.beforeToolCall(\n\t\t\t\t{\n\t\t\t\t\tassistantMessage,\n\t\t\t\t\ttoolCall,\n\t\t\t\t\targs: validatedArgs,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (signal?.aborted) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"immediate\",\n\t\t\t\t\tresult: createErrorToolResult(\"Operation aborted\"),\n\t\t\t\t\tisError: true,\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (beforeResult?.block) {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"immediate\",\n\t\t\t\t\tresult: createErrorToolResult(beforeResult.reason || \"Tool execution was blocked\"),\n\t\t\t\t\tisError: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tif (signal?.aborted) {\n\t\t\treturn {\n\t\t\t\tkind: \"immediate\",\n\t\t\t\tresult: createErrorToolResult(\"Operation aborted\"),\n\t\t\t\tisError: true,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tkind: \"prepared\",\n\t\t\ttoolCall,\n\t\t\ttool,\n\t\t\targs: validatedArgs,\n\t\t};\n\t} catch (error) {\n\t\treturn {\n\t\t\tkind: \"immediate\",\n\t\t\tresult: createErrorToolResult(error instanceof Error ? error.message : String(error)),\n\t\t\tisError: true,\n\t\t};\n\t}\n}\n\nasync function executePreparedToolCall(\n\tprepared: PreparedToolCall,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallOutcome> {\n\tconst updateEvents: Promise<void>[] = [];\n\n\ttry {\n\t\tconst result = await prepared.tool.execute(\n\t\t\tprepared.toolCall.id,\n\t\t\tprepared.args as never,\n\t\t\tsignal,\n\t\t\t(partialResult) => {\n\t\t\t\tupdateEvents.push(\n\t\t\t\t\tPromise.resolve(\n\t\t\t\t\t\temit({\n\t\t\t\t\t\t\ttype: \"tool_execution_update\",\n\t\t\t\t\t\t\ttoolCallId: prepared.toolCall.id,\n\t\t\t\t\t\t\ttoolName: prepared.toolCall.name,\n\t\t\t\t\t\t\targs: prepared.toolCall.arguments,\n\t\t\t\t\t\t\tpartialResult,\n\t\t\t\t\t\t}),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t},\n\t\t);\n\t\tawait Promise.all(updateEvents);\n\t\treturn { result, isError: false };\n\t} catch (error) {\n\t\tawait Promise.all(updateEvents);\n\t\treturn {\n\t\t\tresult: createErrorToolResult(error instanceof Error ? error.message : String(error)),\n\t\t\tisError: true,\n\t\t};\n\t}\n}\n\nasync function finalizeExecutedToolCall(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\tprepared: PreparedToolCall,\n\texecuted: ExecutedToolCallOutcome,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n): Promise<FinalizedToolCallOutcome> {\n\tlet result = executed.result;\n\tlet isError = executed.isError;\n\n\tif (config.afterToolCall) {\n\t\ttry {\n\t\t\tconst afterResult = await config.afterToolCall(\n\t\t\t\t{\n\t\t\t\t\tassistantMessage,\n\t\t\t\t\ttoolCall: prepared.toolCall,\n\t\t\t\t\targs: prepared.args,\n\t\t\t\t\tresult,\n\t\t\t\t\tisError,\n\t\t\t\t\tcontext: currentContext,\n\t\t\t\t},\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (afterResult) {\n\t\t\t\tresult = {\n\t\t\t\t\tcontent: afterResult.content ?? result.content,\n\t\t\t\t\tdetails: afterResult.details ?? result.details,\n\t\t\t\t\tterminate: afterResult.terminate ?? result.terminate,\n\t\t\t\t};\n\t\t\t\tisError = afterResult.isError ?? isError;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tresult = createErrorToolResult(error instanceof Error ? error.message : String(error));\n\t\t\tisError = true;\n\t\t}\n\t}\n\n\treturn {\n\t\ttoolCall: prepared.toolCall,\n\t\tresult,\n\t\tisError,\n\t};\n}\n\nfunction createErrorToolResult(message: string): AgentToolResult<any> {\n\treturn {\n\t\tcontent: [{ type: \"text\", text: message }],\n\t\tdetails: {},\n\t};\n}\n\nasync function emitToolExecutionEnd(finalized: FinalizedToolCallOutcome, emit: AgentEventSink): Promise<void> {\n\tawait emit({\n\t\ttype: \"tool_execution_end\",\n\t\ttoolCallId: finalized.toolCall.id,\n\t\ttoolName: finalized.toolCall.name,\n\t\tresult: finalized.result,\n\t\tisError: finalized.isError,\n\t});\n}\n\nfunction createToolResultMessage(finalized: FinalizedToolCallOutcome): ToolResultMessage {\n\treturn {\n\t\trole: \"toolResult\",\n\t\ttoolCallId: finalized.toolCall.id,\n\t\ttoolName: finalized.toolCall.name,\n\t\tcontent: finalized.result.content,\n\t\tdetails: finalized.result.details,\n\t\tisError: finalized.isError,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nasync function emitToolResultMessage(toolResultMessage: ToolResultMessage, emit: AgentEventSink): Promise<void> {\n\tawait emit({ type: \"message_start\", message: toolResultMessage });\n\tawait emit({ type: \"message_end\", message: toolResultMessage });\n}\n"]}
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { Model } from "@caupulican/pi-ai";
8
8
  import type { ReadonlySessionManager, SessionEntry } from "../session/session-manager.ts";
9
- import type { AgentMessage } from "../types.ts";
9
+ import type { AgentMessage, StreamFn } from "../types.ts";
10
10
  import { type FileOperations } from "./utils.ts";
11
11
  export interface BranchSummaryResult {
12
12
  summary?: string;
@@ -50,6 +50,8 @@ export interface GenerateBranchSummaryOptions {
50
50
  replaceInstructions?: boolean;
51
51
  /** Tokens reserved for prompt + LLM response (default 16384) */
52
52
  reserveTokens?: number;
53
+ /** Optional wrapped stream function (watchdog/retry-capable host path). */
54
+ streamFn?: StreamFn;
53
55
  }
54
56
  /**
55
57
  * Collect entries that should be summarized when navigating from one position to another.
@@ -78,11 +80,5 @@ export declare function collectEntriesForBranchSummary(session: ReadonlySessionM
78
80
  * @param tokenBudget - Maximum tokens to include (0 = no limit)
79
81
  */
80
82
  export declare function prepareBranchEntries(entries: SessionEntry[], tokenBudget?: number): BranchPreparation;
81
- /**
82
- * Generate a summary of abandoned branch entries.
83
- *
84
- * @param entries - Session entries to summarize (chronological order)
85
- * @param options - Generation options
86
- */
87
83
  export declare function generateBranchSummary(entries: SessionEntry[], options: GenerateBranchSummaryOptions): Promise<BranchSummaryResult>;
88
84
  //# sourceMappingURL=branch-summarization.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"branch-summarization.d.ts","sourceRoot":"","sources":["../../src/compaction/branch-summarization.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAQ/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC1F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAIN,KAAK,cAAc,EAInB,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,mBAAmB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IACjC,mEAAmE;IACnE,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,gDAAgD;IAChD,OAAO,EAAE,cAAc,CAAC;IACxB,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACpC,mDAAmD;IACnD,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,4BAA4B;IAC5C,qCAAqC;IACrC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oCAAoC;IACpC,MAAM,EAAE,WAAW,CAAC;IACpB,qDAAqD;IACrD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wFAAwF;IACxF,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAC7C,OAAO,EAAE,sBAAsB,EAC/B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,MAAM,GACd,oBAAoB,CAkCtB;AAoCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,iBAAiB,CAoDxG;AAwCD;;;;;GAKG;AACH,wBAAsB,qBAAqB,CAC1C,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,4BAA4B,GACnC,OAAO,CAAC,mBAAmB,CAAC,CAqE9B","sourcesContent":["/**\n * Branch summarization for tree navigation.\n *\n * When navigating to a different point in the session tree, this generates\n * a summary of the branch being left so context isn't lost.\n */\n\nimport type { Model } from \"@caupulican/pi-ai\";\nimport { completeSimple } from \"@caupulican/pi-ai\";\nimport {\n\tconvertToLlm,\n\tcreateBranchSummaryMessage,\n\tcreateCompactionSummaryMessage,\n\tcreateCustomMessage,\n} from \"../messages.ts\";\nimport type { ReadonlySessionManager, SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage } from \"../types.ts\";\nimport { estimateTokens } from \"./compaction.ts\";\nimport {\n\tcomputeFileLists,\n\tcreateFileOps,\n\textractFileOpsFromMessage,\n\ttype FileOperations,\n\tformatFileOperations,\n\tSUMMARIZATION_SYSTEM_PROMPT,\n\tserializeConversation,\n} from \"./utils.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BranchSummaryResult {\n\tsummary?: string;\n\treadFiles?: string[];\n\tmodifiedFiles?: string[];\n\taborted?: boolean;\n\terror?: string;\n}\n\n/** Details stored in BranchSummaryEntry.details for file tracking */\nexport interface BranchSummaryDetails {\n\treadFiles: string[];\n\tmodifiedFiles: string[];\n}\n\nexport type { FileOperations } from \"./utils.ts\";\n\nexport interface BranchPreparation {\n\t/** Messages extracted for summarization, in chronological order */\n\tmessages: AgentMessage[];\n\t/** File operations extracted from tool calls */\n\tfileOps: FileOperations;\n\t/** Total estimated tokens in messages */\n\ttotalTokens: number;\n}\n\nexport interface CollectEntriesResult {\n\t/** Entries to summarize, in chronological order */\n\tentries: SessionEntry[];\n\t/** Common ancestor between old and new position, if any */\n\tcommonAncestorId: string | null;\n}\n\nexport interface GenerateBranchSummaryOptions {\n\t/** Model to use for summarization */\n\tmodel: Model<any>;\n\t/** API key for the model */\n\tapiKey: string;\n\t/** Request headers for the model */\n\theaders?: Record<string, string>;\n\t/** Abort signal for cancellation */\n\tsignal: AbortSignal;\n\t/** Optional custom instructions for summarization */\n\tcustomInstructions?: string;\n\t/** If true, customInstructions replaces the default prompt instead of being appended */\n\treplaceInstructions?: boolean;\n\t/** Tokens reserved for prompt + LLM response (default 16384) */\n\treserveTokens?: number;\n}\n\n// ============================================================================\n// Entry Collection\n// ============================================================================\n\n/**\n * Collect entries that should be summarized when navigating from one position to another.\n *\n * Walks from oldLeafId back to the common ancestor with targetId, collecting entries\n * along the way. Does NOT stop at compaction boundaries - those are included and their\n * summaries become context.\n *\n * @param session - Session manager (read-only access)\n * @param oldLeafId - Current position (where we're navigating from)\n * @param targetId - Target position (where we're navigating to)\n * @returns Entries to summarize and the common ancestor\n */\nexport function collectEntriesForBranchSummary(\n\tsession: ReadonlySessionManager,\n\toldLeafId: string | null,\n\ttargetId: string,\n): CollectEntriesResult {\n\t// If no old position, nothing to summarize\n\tif (!oldLeafId) {\n\t\treturn { entries: [], commonAncestorId: null };\n\t}\n\n\t// Find common ancestor (deepest node that's on both paths)\n\tconst oldPath = new Set(session.getBranch(oldLeafId).map((e) => e.id));\n\tconst targetPath = session.getBranch(targetId);\n\n\t// targetPath is root-first, so iterate backwards to find deepest common ancestor\n\tlet commonAncestorId: string | null = null;\n\tfor (let i = targetPath.length - 1; i >= 0; i--) {\n\t\tif (oldPath.has(targetPath[i].id)) {\n\t\t\tcommonAncestorId = targetPath[i].id;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Collect entries from old leaf back to common ancestor\n\tconst entries: SessionEntry[] = [];\n\tlet current: string | null = oldLeafId;\n\n\twhile (current && current !== commonAncestorId) {\n\t\tconst entry = session.getEntry(current);\n\t\tif (!entry) break;\n\t\tentries.push(entry);\n\t\tcurrent = entry.parentId;\n\t}\n\n\t// Reverse to get chronological order\n\tentries.reverse();\n\n\treturn { entries, commonAncestorId };\n}\n\n// ============================================================================\n// Entry to Message Conversion\n// ============================================================================\n\n/**\n * Extract AgentMessage from a session entry.\n * Similar to getMessageFromEntry in compaction.ts but also handles compaction entries.\n */\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tswitch (entry.type) {\n\t\tcase \"message\":\n\t\t\t// Skip tool results - context is in assistant's tool call\n\t\t\tif (entry.message.role === \"toolResult\") return undefined;\n\t\t\treturn entry.message;\n\n\t\tcase \"custom_message\":\n\t\t\treturn createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);\n\n\t\tcase \"branch_summary\":\n\t\t\treturn createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);\n\n\t\tcase \"compaction\":\n\t\t\treturn createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);\n\n\t\t// These don't contribute to conversation content\n\t\tcase \"thinking_level_change\":\n\t\tcase \"model_change\":\n\t\tcase \"custom\":\n\t\tcase \"label\":\n\t\tcase \"session_info\":\n\t\t\treturn undefined;\n\t}\n}\n\n/**\n * Prepare entries for summarization with token budget.\n *\n * Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget.\n * This ensures we keep the most recent context when the branch is too long.\n *\n * Also collects file operations from:\n * - Tool calls in assistant messages\n * - Existing branch_summary entries' details (for cumulative tracking)\n *\n * @param entries - Entries in chronological order\n * @param tokenBudget - Maximum tokens to include (0 = no limit)\n */\nexport function prepareBranchEntries(entries: SessionEntry[], tokenBudget: number = 0): BranchPreparation {\n\tconst messages: AgentMessage[] = [];\n\tconst fileOps = createFileOps();\n\tlet totalTokens = 0;\n\n\t// First pass: collect file ops from ALL entries (even if they don't fit in token budget)\n\t// This ensures we capture cumulative file tracking from nested branch summaries\n\t// Only extract from pi-generated summaries (fromHook !== true), not extension-generated ones\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"branch_summary\" && !entry.fromHook && entry.details) {\n\t\t\tconst details = entry.details as BranchSummaryDetails;\n\t\t\tif (Array.isArray(details.readFiles)) {\n\t\t\t\tfor (const f of details.readFiles) fileOps.read.add(f);\n\t\t\t}\n\t\t\tif (Array.isArray(details.modifiedFiles)) {\n\t\t\t\t// Modified files go into both edited and written for proper deduplication\n\t\t\t\tfor (const f of details.modifiedFiles) {\n\t\t\t\t\tfileOps.edited.add(f);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Second pass: walk from newest to oldest, adding messages until token budget\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tconst message = getMessageFromEntry(entry);\n\t\tif (!message) continue;\n\n\t\t// Extract file ops from assistant messages (tool calls)\n\t\textractFileOpsFromMessage(message, fileOps);\n\n\t\tconst tokens = estimateTokens(message);\n\n\t\t// Check budget before adding\n\t\tif (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {\n\t\t\t// If this is a summary entry, try to fit it anyway as it's important context\n\t\t\tif (entry.type === \"compaction\" || entry.type === \"branch_summary\") {\n\t\t\t\tif (totalTokens < tokenBudget * 0.9) {\n\t\t\t\t\tmessages.unshift(message);\n\t\t\t\t\ttotalTokens += tokens;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Stop - we've hit the budget\n\t\t\tbreak;\n\t\t}\n\n\t\tmessages.unshift(message);\n\t\ttotalTokens += tokens;\n\t}\n\n\treturn { messages, fileOps, totalTokens };\n}\n\n// ============================================================================\n// Summary Generation\n// ============================================================================\n\nconst BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here.\nSummary of that exploration:\n\n`;\n\nconst BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later.\n\nUse this EXACT format:\n\n## Goal\n[What was the user trying to accomplish in this branch?]\n\n## Constraints & Preferences\n- [Any constraints, preferences, or requirements mentioned]\n- [Or \"(none)\" if none were mentioned]\n\n## Progress\n### Done\n- [x] [Completed tasks/changes]\n\n### In Progress\n- [ ] [Work that was started but not finished]\n\n### Blocked\n- [Issues preventing progress, if any]\n\n## Key Decisions\n- **[Decision]**: [Brief rationale]\n\n## Next Steps\n1. [What should happen next to continue this work]\n\nKeep each section concise. Preserve exact file paths, function names, and error messages.`;\n\n/**\n * Generate a summary of abandoned branch entries.\n *\n * @param entries - Session entries to summarize (chronological order)\n * @param options - Generation options\n */\nexport async function generateBranchSummary(\n\tentries: SessionEntry[],\n\toptions: GenerateBranchSummaryOptions,\n): Promise<BranchSummaryResult> {\n\tconst { model, apiKey, headers, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options;\n\n\t// Token budget = context window minus reserved space for prompt + response\n\tconst contextWindow = model.contextWindow || 128000;\n\tconst tokenBudget = contextWindow - reserveTokens;\n\n\tconst { messages, fileOps } = prepareBranchEntries(entries, tokenBudget);\n\n\tif (messages.length === 0) {\n\t\treturn { summary: \"No content to summarize\" };\n\t}\n\n\t// Transform to LLM-compatible messages, then serialize to text\n\t// Serialization prevents the model from treating it as a conversation to continue\n\tconst llmMessages = convertToLlm(messages);\n\tconst conversationText = serializeConversation(llmMessages);\n\n\t// Build prompt\n\tlet instructions: string;\n\tif (replaceInstructions && customInstructions) {\n\t\tinstructions = customInstructions;\n\t} else if (customInstructions) {\n\t\tinstructions = `${BRANCH_SUMMARY_PROMPT}\\n\\nAdditional focus: ${customInstructions}`;\n\t} else {\n\t\tinstructions = BRANCH_SUMMARY_PROMPT;\n\t}\n\tconst promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n${instructions}`;\n\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\t// Call LLM for summarization\n\tconst response = await completeSimple(\n\t\tmodel,\n\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\t{ apiKey, headers, signal, maxTokens: 2048 },\n\t);\n\n\t// Check if aborted or errored\n\tif (response.stopReason === \"aborted\") {\n\t\treturn { aborted: true };\n\t}\n\tif (response.stopReason === \"error\") {\n\t\treturn { error: response.errorMessage || \"Summarization failed\" };\n\t}\n\n\tlet summary = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\t// Prepend preamble to provide context about the branch summary\n\tsummary = BRANCH_SUMMARY_PREAMBLE + summary;\n\n\t// Compute file lists and append to summary\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tsummary += formatFileOperations(readFiles, modifiedFiles);\n\n\treturn {\n\t\tsummary: summary || \"No summary generated\",\n\t\treadFiles,\n\t\tmodifiedFiles,\n\t};\n}\n"]}
1
+ {"version":3,"file":"branch-summarization.d.ts","sourceRoot":"","sources":["../../src/compaction/branch-summarization.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAA6B,KAAK,EAAuB,MAAM,mBAAmB,CAAC;AAS/F,OAAO,KAAK,EAAE,sBAAsB,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC1F,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAIN,KAAK,cAAc,EAInB,MAAM,YAAY,CAAC;AAMpB,MAAM,WAAW,mBAAmB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACpC,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,aAAa,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,iBAAiB;IACjC,mEAAmE;IACnE,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,gDAAgD;IAChD,OAAO,EAAE,cAAc,CAAC;IACxB,yCAAyC;IACzC,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACpC,mDAAmD;IACnD,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED,MAAM,WAAW,4BAA4B;IAC5C,qCAAqC;IACrC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,oCAAoC;IACpC,MAAM,EAAE,WAAW,CAAC;IACpB,qDAAqD;IACrD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wFAAwF;IACxF,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACpB;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAC7C,OAAO,EAAE,sBAAsB,EAC/B,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,QAAQ,EAAE,MAAM,GACd,oBAAoB,CAkCtB;AAoCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,iBAAiB,CAoDxG;AAyDD,wBAAsB,qBAAqB,CAC1C,OAAO,EAAE,YAAY,EAAE,EACvB,OAAO,EAAE,4BAA4B,GACnC,OAAO,CAAC,mBAAmB,CAAC,CAuF9B","sourcesContent":["/**\n * Branch summarization for tree navigation.\n *\n * When navigating to a different point in the session tree, this generates\n * a summary of the branch being left so context isn't lost.\n */\n\nimport type { AssistantMessage, Context, Model, SimpleStreamOptions } from \"@caupulican/pi-ai\";\nimport { completeSimple } from \"@caupulican/pi-ai\";\nimport {\n\tconvertToLlm,\n\tcreateBranchSummaryMessage,\n\tcreateCompactionSummaryMessage,\n\tcreateCustomMessage,\n} from \"../messages.ts\";\nimport { classifyFailure } from \"../reliability/classifier.ts\";\nimport type { ReadonlySessionManager, SessionEntry } from \"../session/session-manager.ts\";\nimport type { AgentMessage, StreamFn } from \"../types.ts\";\nimport { estimateTokens } from \"./compaction.ts\";\nimport {\n\tcomputeFileLists,\n\tcreateFileOps,\n\textractFileOpsFromMessage,\n\ttype FileOperations,\n\tformatFileOperations,\n\tSUMMARIZATION_SYSTEM_PROMPT,\n\tserializeConversation,\n} from \"./utils.ts\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface BranchSummaryResult {\n\tsummary?: string;\n\treadFiles?: string[];\n\tmodifiedFiles?: string[];\n\taborted?: boolean;\n\terror?: string;\n}\n\n/** Details stored in BranchSummaryEntry.details for file tracking */\nexport interface BranchSummaryDetails {\n\treadFiles: string[];\n\tmodifiedFiles: string[];\n}\n\nexport type { FileOperations } from \"./utils.ts\";\n\nexport interface BranchPreparation {\n\t/** Messages extracted for summarization, in chronological order */\n\tmessages: AgentMessage[];\n\t/** File operations extracted from tool calls */\n\tfileOps: FileOperations;\n\t/** Total estimated tokens in messages */\n\ttotalTokens: number;\n}\n\nexport interface CollectEntriesResult {\n\t/** Entries to summarize, in chronological order */\n\tentries: SessionEntry[];\n\t/** Common ancestor between old and new position, if any */\n\tcommonAncestorId: string | null;\n}\n\nexport interface GenerateBranchSummaryOptions {\n\t/** Model to use for summarization */\n\tmodel: Model<any>;\n\t/** API key for the model */\n\tapiKey: string;\n\t/** Request headers for the model */\n\theaders?: Record<string, string>;\n\t/** Abort signal for cancellation */\n\tsignal: AbortSignal;\n\t/** Optional custom instructions for summarization */\n\tcustomInstructions?: string;\n\t/** If true, customInstructions replaces the default prompt instead of being appended */\n\treplaceInstructions?: boolean;\n\t/** Tokens reserved for prompt + LLM response (default 16384) */\n\treserveTokens?: number;\n\t/** Optional wrapped stream function (watchdog/retry-capable host path). */\n\tstreamFn?: StreamFn;\n}\n\n// ============================================================================\n// Entry Collection\n// ============================================================================\n\n/**\n * Collect entries that should be summarized when navigating from one position to another.\n *\n * Walks from oldLeafId back to the common ancestor with targetId, collecting entries\n * along the way. Does NOT stop at compaction boundaries - those are included and their\n * summaries become context.\n *\n * @param session - Session manager (read-only access)\n * @param oldLeafId - Current position (where we're navigating from)\n * @param targetId - Target position (where we're navigating to)\n * @returns Entries to summarize and the common ancestor\n */\nexport function collectEntriesForBranchSummary(\n\tsession: ReadonlySessionManager,\n\toldLeafId: string | null,\n\ttargetId: string,\n): CollectEntriesResult {\n\t// If no old position, nothing to summarize\n\tif (!oldLeafId) {\n\t\treturn { entries: [], commonAncestorId: null };\n\t}\n\n\t// Find common ancestor (deepest node that's on both paths)\n\tconst oldPath = new Set(session.getBranch(oldLeafId).map((e) => e.id));\n\tconst targetPath = session.getBranch(targetId);\n\n\t// targetPath is root-first, so iterate backwards to find deepest common ancestor\n\tlet commonAncestorId: string | null = null;\n\tfor (let i = targetPath.length - 1; i >= 0; i--) {\n\t\tif (oldPath.has(targetPath[i].id)) {\n\t\t\tcommonAncestorId = targetPath[i].id;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Collect entries from old leaf back to common ancestor\n\tconst entries: SessionEntry[] = [];\n\tlet current: string | null = oldLeafId;\n\n\twhile (current && current !== commonAncestorId) {\n\t\tconst entry = session.getEntry(current);\n\t\tif (!entry) break;\n\t\tentries.push(entry);\n\t\tcurrent = entry.parentId;\n\t}\n\n\t// Reverse to get chronological order\n\tentries.reverse();\n\n\treturn { entries, commonAncestorId };\n}\n\n// ============================================================================\n// Entry to Message Conversion\n// ============================================================================\n\n/**\n * Extract AgentMessage from a session entry.\n * Similar to getMessageFromEntry in compaction.ts but also handles compaction entries.\n */\nfunction getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {\n\tswitch (entry.type) {\n\t\tcase \"message\":\n\t\t\t// Skip tool results - context is in assistant's tool call\n\t\t\tif (entry.message.role === \"toolResult\") return undefined;\n\t\t\treturn entry.message;\n\n\t\tcase \"custom_message\":\n\t\t\treturn createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);\n\n\t\tcase \"branch_summary\":\n\t\t\treturn createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);\n\n\t\tcase \"compaction\":\n\t\t\treturn createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);\n\n\t\t// These don't contribute to conversation content\n\t\tcase \"thinking_level_change\":\n\t\tcase \"model_change\":\n\t\tcase \"custom\":\n\t\tcase \"label\":\n\t\tcase \"session_info\":\n\t\t\treturn undefined;\n\t}\n}\n\n/**\n * Prepare entries for summarization with token budget.\n *\n * Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget.\n * This ensures we keep the most recent context when the branch is too long.\n *\n * Also collects file operations from:\n * - Tool calls in assistant messages\n * - Existing branch_summary entries' details (for cumulative tracking)\n *\n * @param entries - Entries in chronological order\n * @param tokenBudget - Maximum tokens to include (0 = no limit)\n */\nexport function prepareBranchEntries(entries: SessionEntry[], tokenBudget: number = 0): BranchPreparation {\n\tconst messages: AgentMessage[] = [];\n\tconst fileOps = createFileOps();\n\tlet totalTokens = 0;\n\n\t// First pass: collect file ops from ALL entries (even if they don't fit in token budget)\n\t// This ensures we capture cumulative file tracking from nested branch summaries\n\t// Only extract from pi-generated summaries (fromHook !== true), not extension-generated ones\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"branch_summary\" && !entry.fromHook && entry.details) {\n\t\t\tconst details = entry.details as BranchSummaryDetails;\n\t\t\tif (Array.isArray(details.readFiles)) {\n\t\t\t\tfor (const f of details.readFiles) fileOps.read.add(f);\n\t\t\t}\n\t\t\tif (Array.isArray(details.modifiedFiles)) {\n\t\t\t\t// Modified files go into both edited and written for proper deduplication\n\t\t\t\tfor (const f of details.modifiedFiles) {\n\t\t\t\t\tfileOps.edited.add(f);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Second pass: walk from newest to oldest, adding messages until token budget\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tconst message = getMessageFromEntry(entry);\n\t\tif (!message) continue;\n\n\t\t// Extract file ops from assistant messages (tool calls)\n\t\textractFileOpsFromMessage(message, fileOps);\n\n\t\tconst tokens = estimateTokens(message);\n\n\t\t// Check budget before adding\n\t\tif (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {\n\t\t\t// If this is a summary entry, try to fit it anyway as it's important context\n\t\t\tif (entry.type === \"compaction\" || entry.type === \"branch_summary\") {\n\t\t\t\tif (totalTokens < tokenBudget * 0.9) {\n\t\t\t\t\tmessages.unshift(message);\n\t\t\t\t\ttotalTokens += tokens;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Stop - we've hit the budget\n\t\t\tbreak;\n\t\t}\n\n\t\tmessages.unshift(message);\n\t\ttotalTokens += tokens;\n\t}\n\n\treturn { messages, fileOps, totalTokens };\n}\n\n// ============================================================================\n// Summary Generation\n// ============================================================================\n\nconst BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here.\nSummary of that exploration:\n\n`;\n\nconst BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later.\n\nUse this EXACT format:\n\n## Goal\n[What was the user trying to accomplish in this branch?]\n\n## Constraints & Preferences\n- [Any constraints, preferences, or requirements mentioned]\n- [Or \"(none)\" if none were mentioned]\n\n## Progress\n### Done\n- [x] [Completed tasks/changes]\n\n### In Progress\n- [ ] [Work that was started but not finished]\n\n### Blocked\n- [Issues preventing progress, if any]\n\n## Key Decisions\n- **[Decision]**: [Brief rationale]\n\n## Next Steps\n1. [What should happen next to continue this work]\n\nKeep each section concise. Preserve exact file paths, function names, and error messages.`;\n\n/**\n * Generate a summary of abandoned branch entries.\n *\n * @param entries - Session entries to summarize (chronological order)\n * @param options - Generation options\n */\nasync function completeBranchSummary(\n\tmodel: Model<any>,\n\tcontext: Context,\n\toptions: SimpleStreamOptions,\n\tstreamFn?: StreamFn,\n): Promise<AssistantMessage> {\n\tif (!streamFn) return completeSimple(model, context, options);\n\tconst stream = await streamFn(model, context, options);\n\treturn stream.result();\n}\n\nexport async function generateBranchSummary(\n\tentries: SessionEntry[],\n\toptions: GenerateBranchSummaryOptions,\n): Promise<BranchSummaryResult> {\n\tconst {\n\t\tmodel,\n\t\tapiKey,\n\t\theaders,\n\t\tsignal,\n\t\tcustomInstructions,\n\t\treplaceInstructions,\n\t\treserveTokens = 16384,\n\t\tstreamFn,\n\t} = options;\n\n\t// Token budget = context window minus reserved space for prompt + response\n\tconst contextWindow = model.contextWindow || 128000;\n\tconst tokenBudget = contextWindow - reserveTokens;\n\n\tconst { messages, fileOps } = prepareBranchEntries(entries, tokenBudget);\n\n\tif (messages.length === 0) {\n\t\treturn { summary: \"No content to summarize\" };\n\t}\n\n\t// Transform to LLM-compatible messages, then serialize to text\n\t// Serialization prevents the model from treating it as a conversation to continue\n\tconst llmMessages = convertToLlm(messages);\n\tconst conversationText = serializeConversation(llmMessages);\n\n\t// Build prompt\n\tlet instructions: string;\n\tif (replaceInstructions && customInstructions) {\n\t\tinstructions = customInstructions;\n\t} else if (customInstructions) {\n\t\tinstructions = `${BRANCH_SUMMARY_PROMPT}\\n\\nAdditional focus: ${customInstructions}`;\n\t} else {\n\t\tinstructions = BRANCH_SUMMARY_PROMPT;\n\t}\n\tconst promptText = `<conversation>\\n${conversationText}\\n</conversation>\\n\\n${instructions}`;\n\n\tconst summarizationMessages = [\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: promptText }],\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\t// Call LLM for summarization. Retry transient provider/watchdog failures so branch summaries\n\t// use the same reliability doctrine as compaction instead of bypassing the host stream wrapper.\n\tlet response: AssistantMessage | undefined;\n\tfor (let attempt = 0; attempt < 3; attempt++) {\n\t\tresponse = await completeBranchSummary(\n\t\t\tmodel,\n\t\t\t{ systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },\n\t\t\t{ apiKey, headers, signal, maxTokens: 2048 },\n\t\t\tstreamFn,\n\t\t);\n\t\tif (response.stopReason !== \"error\") break;\n\t\tconst classified = classifyFailure({ message: response.errorMessage ?? \"\", provider: response.provider });\n\t\tif (!classified.retryable || signal.aborted) break;\n\t}\n\tif (!response) return { error: \"Summarization failed\" };\n\n\t// Check if aborted or errored\n\tif (response.stopReason === \"aborted\") {\n\t\treturn { aborted: true };\n\t}\n\tif (response.stopReason === \"error\") {\n\t\treturn { error: response.errorMessage || \"Summarization failed\" };\n\t}\n\n\tlet summary = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\t// Prepend preamble to provide context about the branch summary\n\tsummary = BRANCH_SUMMARY_PREAMBLE + summary;\n\n\t// Compute file lists and append to summary\n\tconst { readFiles, modifiedFiles } = computeFileLists(fileOps);\n\tsummary += formatFileOperations(readFiles, modifiedFiles);\n\n\treturn {\n\t\tsummary: summary || \"No summary generated\",\n\t\treadFiles,\n\t\tmodifiedFiles,\n\t};\n}\n"]}
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { completeSimple } from "@caupulican/pi-ai";
8
8
  import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
9
+ import { classifyFailure } from "../reliability/classifier.js";
9
10
  import { estimateTokens } from "./compaction.js";
10
11
  import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
11
12
  // ============================================================================
@@ -184,8 +185,14 @@ Keep each section concise. Preserve exact file paths, function names, and error
184
185
  * @param entries - Session entries to summarize (chronological order)
185
186
  * @param options - Generation options
186
187
  */
188
+ async function completeBranchSummary(model, context, options, streamFn) {
189
+ if (!streamFn)
190
+ return completeSimple(model, context, options);
191
+ const stream = await streamFn(model, context, options);
192
+ return stream.result();
193
+ }
187
194
  export async function generateBranchSummary(entries, options) {
188
- const { model, apiKey, headers, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options;
195
+ const { model, apiKey, headers, signal, customInstructions, replaceInstructions, reserveTokens = 16384, streamFn, } = options;
189
196
  // Token budget = context window minus reserved space for prompt + response
190
197
  const contextWindow = model.contextWindow || 128000;
191
198
  const tokenBudget = contextWindow - reserveTokens;
@@ -216,8 +223,19 @@ export async function generateBranchSummary(entries, options) {
216
223
  timestamp: Date.now(),
217
224
  },
218
225
  ];
219
- // Call LLM for summarization
220
- const response = await completeSimple(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, { apiKey, headers, signal, maxTokens: 2048 });
226
+ // Call LLM for summarization. Retry transient provider/watchdog failures so branch summaries
227
+ // use the same reliability doctrine as compaction instead of bypassing the host stream wrapper.
228
+ let response;
229
+ for (let attempt = 0; attempt < 3; attempt++) {
230
+ response = await completeBranchSummary(model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, { apiKey, headers, signal, maxTokens: 2048 }, streamFn);
231
+ if (response.stopReason !== "error")
232
+ break;
233
+ const classified = classifyFailure({ message: response.errorMessage ?? "", provider: response.provider });
234
+ if (!classified.retryable || signal.aborted)
235
+ break;
236
+ }
237
+ if (!response)
238
+ return { error: "Summarization failed" };
221
239
  // Check if aborted or errored
222
240
  if (response.stopReason === "aborted") {
223
241
  return { aborted: true };