@ponythewhite/base-context-agent 1.0.0
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.
- package/LICENSE +23 -0
- package/NOTICE +22 -0
- package/README.md +551 -0
- package/dist/LICENSE +23 -0
- package/dist/NOTICE +22 -0
- package/dist/agent-loop.d.ts +24 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +897 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent.d.ts +144 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +569 -0
- package/dist/agent.js.map +1 -0
- package/dist/build-info.json +14 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/invocation-output.d.ts +30 -0
- package/dist/invocation-output.d.ts.map +1 -0
- package/dist/invocation-output.js +125 -0
- package/dist/invocation-output.js.map +1 -0
- package/dist/proxy.d.ts +59 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +268 -0
- package/dist/proxy.js.map +1 -0
- package/dist/types.d.ts +512 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-loop.d.ts","sourceRoot":"","sources":["../src/agent-loop.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAIN,WAAW,EAMX,MAAM,+BAA+B,CAAC;AAEvC,OAAO,KAAK,EACX,YAAY,EAEZ,UAAU,EACV,eAAe,EACf,YAAY,EAMZ,QAAQ,EAGR,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAwJzE;;;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,CAkBzC;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,CAyBzC;AA8ED,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,CAkBzB;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 AssistantMessageEvent,\n\ttype Context,\n\tEventStream,\n\tisLocalRequestPreparationError,\n\tmarkLocalRequestPreparationError,\n\tstreamSimple,\n\ttype ToolResultMessage,\n\tvalidateToolArguments,\n} from \"@ponythewhite/base-context-ai\";\nimport { AgentOutputLimitError, InvocationOutput } from \"./invocation-output.js\";\nimport type {\n\tAgentContext,\n\tAgentContextBuildResult,\n\tAgentEvent,\n\tAgentLoopConfig,\n\tAgentMessage,\n\tAgentTool,\n\tAgentToolCall,\n\tAgentToolResult,\n\tBoundToolExecution,\n\tFinalizedToolExchange,\n\tStreamFn,\n\tToolExecutionMode,\n\tToolExecutionOutcome,\n} from \"./types.js\";\n\nexport type AgentEventSink = (event: AgentEvent) => Promise<void> | void;\n\nconst ABORT_ERROR_MESSAGE = \"Request was aborted\";\nconst EMPTY_USAGE: AssistantMessage[\"usage\"] = {\n\tinput: 0,\n\toutput: 0,\n\tcacheRead: 0,\n\tcacheWrite: 0,\n\ttotalTokens: 0,\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n};\n\nfunction createAbortError(): Error {\n\treturn new Error(ABORT_ERROR_MESSAGE);\n}\n\nfunction throwIfAborted(signal: AbortSignal | undefined): void {\n\tif (signal?.aborted) {\n\t\tthrow createAbortError();\n\t}\n}\n\nfunction raceWithAbort<T>(operation: Promise<T>, signal: AbortSignal | undefined, onAbort?: () => void): Promise<T> {\n\tif (!signal) {\n\t\treturn operation;\n\t}\n\tif (signal.aborted) {\n\t\tonAbort?.();\n\t\tvoid operation.catch(() => undefined);\n\t\treturn Promise.reject(createAbortError());\n\t}\n\n\treturn new Promise<T>((resolve, reject) => {\n\t\tlet settled = false;\n\t\tconst cleanup = () => {\n\t\t\tsignal.removeEventListener(\"abort\", abort);\n\t\t};\n\t\tconst abort = () => {\n\t\t\tif (settled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tcleanup();\n\t\t\tonAbort?.();\n\t\t\treject(createAbortError());\n\t\t};\n\t\tsignal.addEventListener(\"abort\", abort, { once: true });\n\t\toperation.then(\n\t\t\t(value) => {\n\t\t\t\tif (settled) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsettled = true;\n\t\t\t\tcleanup();\n\t\t\t\tresolve(value);\n\t\t\t},\n\t\t\t(error: unknown) => {\n\t\t\t\tif (settled) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsettled = true;\n\t\t\t\tcleanup();\n\t\t\t\treject(error);\n\t\t\t},\n\t\t);\n\t});\n}\n\nfunction maybePromiseWithAbort<T>(\n\toperation: T | Promise<T>,\n\tsignal: AbortSignal | undefined,\n\tonAbort?: () => void,\n): Promise<T> {\n\treturn raceWithAbort(Promise.resolve(operation), signal, onAbort);\n}\n\nfunction isAbortError(error: unknown): boolean {\n\treturn error instanceof Error && (error.message === ABORT_ERROR_MESSAGE || error.name === \"AbortError\");\n}\n\ntype PostTurnResult<T> = { status: \"completed\"; value: T } | { status: \"aborted\" };\n\nasync function settlePostTurn<T>(operation: Promise<T>, signal: AbortSignal | undefined): Promise<PostTurnResult<T>> {\n\ttry {\n\t\treturn { status: \"completed\", value: await operation };\n\t} catch (error) {\n\t\tif (signal?.aborted && isAbortError(error)) {\n\t\t\treturn { status: \"aborted\" };\n\t\t}\n\t\tthrow error;\n\t}\n}\n\nfunction cloneAssistantContent(content: AssistantMessage[\"content\"]): AssistantMessage[\"content\"] {\n\treturn content.map((part) => {\n\t\tif (part.type === \"toolCall\") {\n\t\t\treturn { ...part, arguments: { ...part.arguments } };\n\t\t}\n\t\treturn { ...part };\n\t});\n}\n\nfunction cloneUsage(usage: AssistantMessage[\"usage\"]): AssistantMessage[\"usage\"] {\n\treturn { ...usage, cost: { ...usage.cost } };\n}\n\nfunction createAbortedAssistantMessage(\n\tconfig: AgentLoopConfig,\n\tpartialMessage: AssistantMessage | null,\n): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: partialMessage ? cloneAssistantContent(partialMessage.content) : [{ type: \"text\", text: \"\" }],\n\t\tapi: partialMessage?.api ?? config.model.api,\n\t\tprovider: partialMessage?.provider ?? config.model.provider,\n\t\tmodel: partialMessage?.model ?? config.model.id,\n\t\tusage: cloneUsage(partialMessage?.usage ?? EMPTY_USAGE),\n\t\tstopReason: \"aborted\",\n\t\terrorMessage: ABORT_ERROR_MESSAGE,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nfunction getTerminalMessage(event: Extract<AssistantMessageEvent, { type: \"done\" | \"error\" }>): AssistantMessage {\n\treturn event.type === \"done\" ? event.message : event.error;\n}\n\nfunction endAgentStreamOnError(\n\tstream: EventStream<AgentEvent, AgentMessage[]>,\n\tpromise: Promise<AgentMessage[]>,\n): void {\n\tvoid promise.then(\n\t\t(messages) => {\n\t\t\tstream.end(messages);\n\t\t},\n\t\t(error) => {\n\t\t\tif (error instanceof AgentOutputLimitError) stream.fail(error);\n\t\t\telse stream.end([]);\n\t\t},\n\t);\n}\n\nasync function pollMessagesUnlessAborted(\n\tpoll: (() => AgentMessage[] | Promise<AgentMessage[]>) | undefined,\n\tsignal: AbortSignal | undefined,\n): Promise<AgentMessage[]> {\n\tif (!poll || signal?.aborted) {\n\t\treturn [];\n\t}\n\treturn (await maybePromiseWithAbort(poll(), signal)) || [];\n}\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\tendAgentStreamOnError(\n\t\tstream,\n\t\trunAgentLoop(\n\t\t\tprompts,\n\t\t\tcontext,\n\t\t\tconfig,\n\t\t\tasync (event) => {\n\t\t\t\tstream.push(event);\n\t\t\t},\n\t\t\tsignal,\n\t\t\tstreamFn,\n\t\t),\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\tendAgentStreamOnError(\n\t\tstream,\n\t\trunAgentLoopContinue(\n\t\t\tcontext,\n\t\t\tconfig,\n\t\t\tasync (event) => {\n\t\t\t\tstream.push(event);\n\t\t\t},\n\t\t\tsignal,\n\t\t\tstreamFn,\n\t\t),\n\t);\n\n\treturn stream;\n}\n\nasync function withInvocationOutput(\n\tmessages: AgentMessage[],\n\tconfig: AgentLoopConfig,\n\temit: AgentEventSink,\n\trun: (emit: AgentEventSink, output: InvocationOutput | undefined) => Promise<void>,\n): Promise<AgentMessage[]> {\n\tconst output = config.outputPolicy ? new InvocationOutput(config.outputPolicy, messages) : undefined;\n\tlet releaseUpdates = output ? output.policy.bindUpdates?.(output.refresh.bind(output)) : undefined;\n\tconst stopUpdates = () => {\n\t\tconst release = releaseUpdates;\n\t\treleaseUpdates = undefined;\n\t\trelease?.();\n\t};\n\tlet updateSettlement: Promise<void> | undefined;\n\tconst settleUpdates = () => {\n\t\tif (!updateSettlement) {\n\t\t\ttry {\n\t\t\t\tupdateSettlement = output?.policy.settleUpdates?.() ?? Promise.resolve();\n\t\t\t} catch (error) {\n\t\t\t\tupdateSettlement = Promise.reject(error);\n\t\t\t}\n\t\t}\n\t\treturn updateSettlement;\n\t};\n\tconst ownedEmit: AgentEventSink = output\n\t\t? async (event) => {\n\t\t\t\tif (event.type === \"agent_end\") {\n\t\t\t\t\tawait settleUpdates();\n\t\t\t\t\toutput.throwIfRefused();\n\t\t\t\t\tconst finalizedMessages = output.copy();\n\t\t\t\t\tstopUpdates();\n\t\t\t\t\tawait emit({ type: \"agent_end\", messages: finalizedMessages });\n\t\t\t\t} else {\n\t\t\t\t\tawait emit(event);\n\t\t\t\t\t// The native sink joins this message's replacement/persistence job. Keep its original subject.\n\t\t\t\t\tif (event.type === \"message_end\") output.capture(event.message);\n\t\t\t\t}\n\t\t\t}\n\t\t: emit;\n\ttry {\n\t\tawait run(ownedEmit, output);\n\t\treturn messages;\n\t} catch (error) {\n\t\tif (!output) throw error;\n\t\tlet failure = error;\n\t\ttry {\n\t\t\tawait settleUpdates();\n\t\t} catch (settlementError) {\n\t\t\tif (settlementError !== failure)\n\t\t\t\tfailure = new AggregateError(\n\t\t\t\t\t[failure, settlementError],\n\t\t\t\t\t\"Invocation and accepted update settlement failed\",\n\t\t\t\t\t{ cause: failure },\n\t\t\t\t);\n\t\t}\n\t\tif (!output.refusal) throw failure;\n\t\tlet cause: unknown = failure instanceof AgentOutputLimitError ? failure.cause : failure;\n\t\tstopUpdates();\n\t\ttry {\n\t\t\tawait emit({ type: \"agent_end\", refusal: { ...output.refusal } });\n\t\t} catch (notificationError) {\n\t\t\tcause =\n\t\t\t\tcause === undefined\n\t\t\t\t\t? notificationError\n\t\t\t\t\t: new AggregateError([cause, notificationError], \"Output refusal and terminal notification failed\");\n\t\t}\n\t\tthrow new AgentOutputLimitError({ ...output.refusal }, cause === undefined ? undefined : { cause });\n\t} finally {\n\t\ttry {\n\t\t\tstopUpdates();\n\t\t} finally {\n\t\t\toutput?.dispose();\n\t\t}\n\t}\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[] = config.outputPolicy ? [] : [...prompts];\n\treturn withInvocationOutput(newMessages, config, emit, async (ownedEmit, output) => {\n\t\tif (output) {\n\t\t\toutput.checkRoom(prompts.length);\n\t\t\tprompts = [...prompts];\n\t\t}\n\t\t// Keep only the working wrapper across awaits, not the initial snapshot.\n\t\tcontext = { ...context, messages: [...context.messages, ...prompts] };\n\t\tawait ownedEmit({ type: \"agent_start\" });\n\t\tawait ownedEmit({ type: \"turn_start\" });\n\t\tfor (const prompt of prompts) {\n\t\t\tawait ownedEmit({ type: \"message_start\", message: prompt });\n\t\t\tawait ownedEmit({ type: \"message_end\", message: prompt });\n\t\t\toutput?.throwIfRefused();\n\t\t}\n\t\tawait runLoop(context, newMessages, config, signal, ownedEmit, streamFn, output);\n\t});\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\treturn withInvocationOutput(newMessages, config, emit, async (ownedEmit, output) => {\n\t\tcontext = { ...context };\n\t\tawait ownedEmit({ type: \"agent_start\" });\n\t\toutput?.throwIfRefused();\n\t\tawait ownedEmit({ type: \"turn_start\" });\n\t\tawait runLoop(context, newMessages, config, signal, ownedEmit, streamFn, output);\n\t});\n}\n\nfunction createAgentStream(): EventStream<AgentEvent, AgentMessage[]> {\n\treturn new EventStream<AgentEvent, AgentMessage[]>(\n\t\t(event: AgentEvent) => event.type === \"agent_end\" && !event.refusal,\n\t\t(event: AgentEvent) => (event.type === \"agent_end\" && !event.refusal ? event.messages : []),\n\t);\n}\n\nasync function runLoop(\n\tcurrentContext: AgentContext,\n\tnewMessages: AgentMessage[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n\toutput?: InvocationOutput,\n): Promise<void> {\n\tlet firstTurn = true;\n\tlet lastTurn: Parameters<NonNullable<AgentLoopConfig[\"getContinuationMessages\"]>>[0] | undefined;\n\tlet pendingMessages: AgentMessage[] = await pollMessagesUnlessAborted(config.getSteeringMessages, signal);\n\n\tconst shouldStopBeforeTurn = (): boolean => !firstTurn && (config.shouldStopBeforeTurn?.() ?? false);\n\n\twhile (true) {\n\t\tthrowIfAborted(signal);\n\t\tlet hasMoreToolCalls = true;\n\n\t\twhile (hasMoreToolCalls || pendingMessages.length > 0) {\n\t\t\tthrowIfAborted(signal);\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\tif (pendingMessages.length > 0) {\n\t\t\t\toutput?.checkRoom(pendingMessages.length);\n\t\t\t\tif (output) pendingMessages = [...pendingMessages];\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\tif (!output) newMessages.push(message);\n\t\t\t\t\toutput?.throwIfRefused();\n\t\t\t\t}\n\t\t\t\tpendingMessages = [];\n\t\t\t}\n\n\t\t\toutput?.checkRoom(1);\n\t\t\tconst message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);\n\t\t\tif (!output) newMessages.push(message);\n\t\t\toutput?.throwIfRefused();\n\n\t\t\tif (message.stopReason === \"error\" || message.stopReason === \"aborted\") {\n\t\t\t\tawait emit({\n\t\t\t\t\ttype: \"turn_end\",\n\t\t\t\t\tmessage,\n\t\t\t\t\ttoolResults: [],\n\t\t\t\t\ttoolExecution: config.toolExecution ?? \"parallel\",\n\t\t\t\t\texchanges: [],\n\t\t\t\t});\n\t\t\t\toutput?.refresh(message);\n\t\t\t\tif (\n\t\t\t\t\tmessage.stopReason === \"error\" &&\n\t\t\t\t\tconfig.recoverProviderFailure &&\n\t\t\t\t\t(await maybePromiseWithAbort(config.recoverProviderFailure(message, signal), signal))\n\t\t\t\t) {\n\t\t\t\t\t// Retain and charge the failed output; only omit it from the next request.\n\t\t\t\t\tcurrentContext.messages = currentContext.messages.filter((candidate) => candidate !== message);\n\t\t\t\t\thasMoreToolCalls = true;\n\t\t\t\t\tcontinue;\n\t\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\tconst toolCalls = message.content.filter((c) => c.type === \"toolCall\");\n\n\t\t\tconst toolResults: ToolResultMessage[] = [];\n\t\t\tconst exchanges: FinalizedToolExchange[] = [];\n\t\t\tlet toolExecution = config.toolExecution ?? \"parallel\";\n\t\t\thasMoreToolCalls = false;\n\t\t\tif (toolCalls.length > 0) {\n\t\t\t\toutput?.checkRoom(toolCalls.length);\n\t\t\t\tconst executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit, output);\n\t\t\t\ttoolResults.push(...executedToolBatch.messages);\n\t\t\t\texchanges.push(...executedToolBatch.exchanges);\n\t\t\t\ttoolExecution = executedToolBatch.toolExecution;\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\tif (!output) newMessages.push(result);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\toutput?.throwIfRefused();\n\t\t\tawait emit({ type: \"turn_end\", message, toolResults, toolExecution, exchanges });\n\t\t\toutput?.refresh(message);\n\t\t\tfor (const result of toolResults) output?.refresh(result);\n\t\t\toutput?.throwIfRefused();\n\t\t\tif (signal?.aborted) {\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tlastTurn = {\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\n\t\t\tconst turnContext = { ...lastTurn, newMessages: output?.copy() ?? newMessages };\n\t\t\tconst shouldStopResult = await settlePostTurn(\n\t\t\t\tmaybePromiseWithAbort(\n\t\t\t\t\tconfig.getTurnOutcome\n\t\t\t\t\t\t? config.getTurnOutcome({ ...turnContext, hasMoreToolCalls }, signal)\n\t\t\t\t\t\t: Promise.resolve(config.shouldStopAfterTurn?.(turnContext) ?? false).then((stop) => ({\n\t\t\t\t\t\t\t\tkind: stop ? (\"finish\" as const) : (\"proceed\" as const),\n\t\t\t\t\t\t\t})),\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\toutput?.refresh(message);\n\t\t\tfor (const result of toolResults) output?.refresh(result);\n\t\t\toutput?.throwIfRefused();\n\t\t\tif (shouldStopResult.status === \"aborted\") {\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (shouldStopResult.value.kind !== \"proceed\" || shouldStopBeforeTurn()) {\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst steeringMessagesResult = await settlePostTurn(\n\t\t\t\tpollMessagesUnlessAborted(config.getSteeringMessages, signal),\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (steeringMessagesResult.status === \"aborted\") {\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tpendingMessages = steeringMessagesResult.value;\n\t\t\t// Steering drained by this poll owns the turn boundary; stop only when it was empty.\n\t\t\tif (pendingMessages.length === 0 && shouldStopBeforeTurn()) {\n\t\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tif (shouldStopBeforeTurn()) break;\n\t\tconst followUpMessagesResult = await settlePostTurn(\n\t\t\tpollMessagesUnlessAborted(config.getFollowUpMessages, signal),\n\t\t\tsignal,\n\t\t);\n\t\tif (followUpMessagesResult.status === \"aborted\") {\n\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\treturn;\n\t\t}\n\t\tconst followUpMessages = followUpMessagesResult.value;\n\t\tif (followUpMessages.length > 0) {\n\t\t\tpendingMessages = followUpMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (shouldStopBeforeTurn()) break;\n\t\tif (lastTurn && config.getContinuationOutcome) {\n\t\t\tconst outcome = await settlePostTurn(\n\t\t\t\tmaybePromiseWithAbort(\n\t\t\t\t\tconfig.getContinuationOutcome(output ? { ...lastTurn, newMessages: output.copy() } : lastTurn, signal),\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tif (outcome.status === \"completed\" && outcome.value.kind === \"continue\") {\n\t\t\t\tpendingMessages = outcome.value.messages;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tconst continuationMessagesResult = lastTurn\n\t\t\t? await settlePostTurn(\n\t\t\t\t\tmaybePromiseWithAbort(\n\t\t\t\t\t\tconfig.getContinuationMessages?.(\n\t\t\t\t\t\t\toutput ? { ...lastTurn, newMessages: output.copy() } : lastTurn,\n\t\t\t\t\t\t\tsignal,\n\t\t\t\t\t\t) ?? [],\n\t\t\t\t\t\tsignal,\n\t\t\t\t\t),\n\t\t\t\t\tsignal,\n\t\t\t\t)\n\t\t\t: ({ status: \"completed\", value: [] } satisfies PostTurnResult<AgentMessage[]>);\n\t\tif (continuationMessagesResult.status === \"aborted\") {\n\t\t\tawait emit({ type: \"agent_end\", messages: newMessages });\n\t\t\treturn;\n\t\t}\n\t\tconst continuationMessages = continuationMessagesResult.value || [];\n\t\tif (continuationMessages.length > 0) {\n\t\t\tpendingMessages = continuationMessages;\n\t\t\tcontinue;\n\t\t}\n\n\t\tbreak;\n\t}\n\n\tawait emit({ type: \"agent_end\", messages: newMessages });\n}\n\nasync function streamAssistantResponse(\n\tcontext: AgentContext,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\tstreamFn?: StreamFn,\n\tallowRecovery = true,\n): Promise<AssistantMessage> {\n\tconst build: { projection: AgentContextBuildResult } = { projection: undefined };\n\tlet addedPartial = false;\n\tconst runResponse = async (): Promise<AssistantMessage> => {\n\t\tlet partialMessage: AssistantMessage | null = null;\n\t\tconst finishAbortedMessage = async () => {\n\t\t\tconst finalMessage = createAbortedAssistantMessage(config, partialMessage);\n\t\t\tif (addedPartial) {\n\t\t\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t\t\t} else {\n\t\t\t\tcontext.messages.push(finalMessage);\n\t\t\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t\t\t}\n\t\t\tawait emit({ type: \"message_end\", message: finalMessage });\n\t\t\treturn finalMessage;\n\t\t};\n\n\t\ttry {\n\t\t\tthrowIfAborted(signal);\n\t\t\t// Do not race source persistence against cancellation; drain it before leaving this build.\n\t\t\tconst projection = await config.beforeContextBuild?.();\n\t\t\tbuild.projection = projection;\n\t\t\tif (projection && projection.adoptMessages === true) {\n\t\t\t\tcontext.messages = projection.messages.slice();\n\t\t\t\tawait config.onContextAdopted?.(context.messages);\n\t\t\t}\n\t\t\tthrowIfAborted(signal);\n\t\t\tlet messages = projection ? projection.messages.slice() : context.messages;\n\t\t\tif (config.transformContext) {\n\t\t\t\tmessages = await maybePromiseWithAbort(config.transformContext(messages, signal), signal);\n\t\t\t}\n\n\t\t\tconst llmMessages = await maybePromiseWithAbort(config.convertToLlm(messages), signal);\n\n\t\t\tconst streamFunction = streamFn || streamSimple;\n\n\t\t\tconst resolvedApiKey =\n\t\t\t\t(config.getApiKey\n\t\t\t\t\t? await maybePromiseWithAbort(config.getApiKey(config.model.provider), signal)\n\t\t\t\t\t: undefined) || config.apiKey;\n\n\t\t\tconst llmContext: Context = {\n\t\t\t\tsystemPrompt: config.getSystemPrompt?.() ?? context.systemPrompt,\n\t\t\t\tmessages: llmMessages,\n\t\t\t\ttools: context.tools,\n\t\t\t};\n\n\t\t\tconst {\n\t\t\t\tbeforeContextBuild: _beforeContextBuild,\n\t\t\t\tonContextAdopted: _onContextAdopted,\n\t\t\t\trecoverRequestPreparation: _recoverRequestPreparation,\n\t\t\t\trecoverProviderFailure: _recoverProviderFailure,\n\t\t\t\toutputPolicy: _outputPolicy,\n\t\t\t\townedStreamFn,\n\t\t\t\t...streamOptions\n\t\t\t} = config;\n\t\t\tconst options = { ...streamOptions, apiKey: resolvedApiKey, signal };\n\t\t\tconst response = await maybePromiseWithAbort(\n\t\t\t\townedStreamFn\n\t\t\t\t\t? ownedStreamFn(config.model, llmContext, options, projection ? projection.streamContext : undefined)\n\t\t\t\t\t: streamFunction(config.model, llmContext, options),\n\t\t\t\tsignal,\n\t\t\t);\n\t\t\tconst iterator = response[Symbol.asyncIterator]();\n\t\t\tconst closeIterator = () => {\n\t\t\t\tvoid Promise.resolve(iterator.return?.()).catch(() => undefined);\n\t\t\t};\n\t\t\twhile (true) {\n\t\t\t\tconst next = await raceWithAbort<IteratorResult<AssistantMessageEvent>>(\n\t\t\t\t\titerator.next(),\n\t\t\t\t\tsignal,\n\t\t\t\t\tcloseIterator,\n\t\t\t\t);\n\t\t\t\tif (next.done) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tconst event = next.value;\n\t\t\t\tswitch (event.type) {\n\t\t\t\t\tcase \"start\":\n\t\t\t\t\t\tpartialMessage = event.partial;\n\t\t\t\t\t\tcontext.messages.push(partialMessage);\n\t\t\t\t\t\taddedPartial = true;\n\t\t\t\t\t\tawait emit({ type: \"message_start\", message: { ...partialMessage } });\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"text_start\":\n\t\t\t\t\tcase \"text_delta\":\n\t\t\t\t\tcase \"text_end\":\n\t\t\t\t\tcase \"thinking_start\":\n\t\t\t\t\tcase \"thinking_delta\":\n\t\t\t\t\tcase \"thinking_end\":\n\t\t\t\t\tcase \"toolcall_start\":\n\t\t\t\t\tcase \"toolcall_delta\":\n\t\t\t\t\tcase \"toolcall_end\":\n\t\t\t\t\t\tif (partialMessage) {\n\t\t\t\t\t\t\tpartialMessage = event.partial;\n\t\t\t\t\t\t\tcontext.messages[context.messages.length - 1] = partialMessage;\n\t\t\t\t\t\t\tawait emit({\n\t\t\t\t\t\t\t\ttype: \"message_update\",\n\t\t\t\t\t\t\t\tassistantMessageEvent: event,\n\t\t\t\t\t\t\t\tmessage: { ...partialMessage },\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tcase \"done\":\n\t\t\t\t\tcase \"error\": {\n\t\t\t\t\t\tlet finalMessage = getTerminalMessage(event);\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tfinalMessage = await maybePromiseWithAbort(response.result(), signal);\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tif (!signal?.aborted || !isAbortError(error)) {\n\t\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (addedPartial) {\n\t\t\t\t\t\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tcontext.messages.push(finalMessage);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (!addedPartial) {\n\t\t\t\t\t\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait emit({ type: \"message_end\", message: finalMessage });\n\t\t\t\t\t\treturn finalMessage;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst finalMessage = await maybePromiseWithAbort(response.result(), signal);\n\t\t\tif (addedPartial) {\n\t\t\t\tcontext.messages[context.messages.length - 1] = finalMessage;\n\t\t\t} else {\n\t\t\t\tcontext.messages.push(finalMessage);\n\t\t\t\tawait emit({ type: \"message_start\", message: { ...finalMessage } });\n\t\t\t}\n\t\t\tawait emit({ type: \"message_end\", message: finalMessage });\n\t\t\treturn finalMessage;\n\t\t} catch (error) {\n\t\t\tif (signal?.aborted && isAbortError(error)) {\n\t\t\t\treturn await finishAbortedMessage();\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t};\n\n\tlet outcome: { message: AssistantMessage } | { error: unknown };\n\ttry {\n\t\toutcome = { message: await runResponse() };\n\t} catch (error) {\n\t\toutcome = { error };\n\t}\n\ttry {\n\t\tif (build.projection) await build.projection.release?.();\n\t} catch (error) {\n\t\tif (\"error\" in outcome) {\n\t\t\tthrow new AggregateError(\n\t\t\t\t[outcome.error, error],\n\t\t\t\t`Agent inference and context cleanup failed: ${String(outcome.error)}`,\n\t\t\t);\n\t\t}\n\t\tthrow error;\n\t}\n\tif (\"error\" in outcome) {\n\t\tif (\n\t\t\tallowRecovery &&\n\t\t\t!addedPartial &&\n\t\t\tisLocalRequestPreparationError(outcome.error) &&\n\t\t\tconfig.recoverRequestPreparation\n\t\t) {\n\t\t\tlet recovered: boolean;\n\t\t\ttry {\n\t\t\t\trecovered = await config.recoverRequestPreparation(outcome.error, signal);\n\t\t\t} catch (error) {\n\t\t\t\t// Preserve the real owner error, including a post-ACK compaction failure.\n\t\t\t\tmarkLocalRequestPreparationError(error);\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tif (recovered) {\n\t\t\t\t// Rebuild/adopt the committed owner view. Keep this invocation's output collector,\n\t\t\t\t// completed tools and emitted messages; only this unsent request gets one reprepare.\n\t\t\t\treturn streamAssistantResponse(context, config, signal, emit, streamFn, false);\n\t\t\t}\n\t\t}\n\t\tthrow outcome.error;\n\t}\n\treturn outcome.message;\n}\n\nasync function executeToolCalls(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\toutput?: InvocationOutput,\n): Promise<ExecutedToolCallBatch> {\n\tconst toolCalls = assistantMessage.content\n\t\t.filter((c) => c.type === \"toolCall\")\n\t\t.map((toolCall, sourceOrder) => ({\n\t\t\ttoolCall,\n\t\t\tsourceOrder,\n\t\t\texecutionId: crypto.randomUUID(),\n\t\t\tassistantMessage,\n\t\t\toriginalInput: structuredClone(toolCall.arguments),\n\t\t}));\n\tconst hasSequentialToolCall = toolCalls.some(\n\t\t({ toolCall }) => currentContext.tools?.find((t) => t.name === toolCall.name)?.executionMode === \"sequential\",\n\t);\n\tif (config.toolExecution === \"sequential\" || hasSequentialToolCall) {\n\t\treturn executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit, output);\n\t}\n\treturn executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);\n}\n\ntype ToolCallSource = {\n\towner?: BoundToolExecution;\n\texecutionId: string;\n\tassistantMessage: AssistantMessage;\n\ttoolCall: AgentToolCall;\n\tsourceOrder: number;\n\toriginalInput: unknown;\n};\n\ntype ExecutedToolCallBatch = {\n\tmessages: ToolResultMessage[];\n\texchanges: FinalizedToolExchange[];\n\ttoolExecution: ToolExecutionMode;\n\tterminate: boolean;\n};\n\nasync function executeToolCallsSequential(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: ToolCallSource[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n\toutput?: InvocationOutput,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallOutcome[] = [];\n\tconst messages: ToolResultMessage[] = [];\n\tconst exchanges: FinalizedToolExchange[] = [];\n\n\tfor (const source of toolCalls) {\n\t\tconst { toolCall } = source;\n\t\tif (signal?.aborted) {\n\t\t\tbreak;\n\t\t}\n\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\texecutionOutcome: \"not_started\",\n\t\t\t};\n\t\t} else {\n\t\t\tconst executed = await executePreparedToolCall(preparation, source, \"sequential\", config, 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\tconst published = await publishToolExchange(finalized, source, \"sequential\", config, signal, emit);\n\t\tawait emitToolResultMessage(published.exchange.result, emit);\n\t\tfinalizedCalls.push(finalized);\n\t\tmessages.push(published.exchange.result);\n\t\texchanges.push(published.exchange);\n\n\t\tif (signal?.aborted || output?.refusal) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn {\n\t\tmessages,\n\t\texchanges,\n\t\ttoolExecution: \"sequential\",\n\t\tterminate: shouldTerminateToolBatch(finalizedCalls),\n\t};\n}\n\nasync function executeToolCallsParallel(\n\tcurrentContext: AgentContext,\n\tassistantMessage: AssistantMessage,\n\ttoolCalls: ToolCallSource[],\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallBatch> {\n\tconst finalizedCalls: FinalizedToolCallEntry[] = [];\n\n\tfor (const source of toolCalls) {\n\t\tconst { toolCall } = source;\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\texecutionOutcome: \"not_started\",\n\t\t\t} satisfies FinalizedToolCallOutcome;\n\t\t\tfinalizedCalls.push(await publishToolExchange(finalized, source, \"parallel\", config, signal, emit));\n\t\t\tcontinue;\n\t\t}\n\n\t\tfinalizedCalls.push(async () => {\n\t\t\tconst executed = await executePreparedToolCall(preparation, source, \"parallel\", config, 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\treturn publishToolExchange(finalized, source, \"parallel\", config, signal, emit);\n\t\t});\n\t}\n\n\t// An owner/publication failure must not abandon other invocations already started.\n\tconst settledCalls = await Promise.allSettled(\n\t\tfinalizedCalls.map((entry) => (typeof entry === \"function\" ? entry() : Promise.resolve(entry))),\n\t);\n\tconst orderedFinalizedCalls = settledCalls.flatMap((settled) =>\n\t\tsettled.status === \"fulfilled\" ? [settled.value] : [],\n\t);\n\tconst failure = settledCalls.find((settled) => settled.status === \"rejected\");\n\tconst messages: ToolResultMessage[] = [];\n\tfor (const finalized of orderedFinalizedCalls) {\n\t\tconst toolResultMessage = finalized.exchange.result;\n\t\tawait emitToolResultMessage(toolResultMessage, emit);\n\t\tmessages.push(toolResultMessage);\n\t}\n\n\tif (failure?.status === \"rejected\") throw failure.reason;\n\treturn {\n\t\tmessages,\n\t\texchanges: orderedFinalizedCalls.map((finalized) => finalized.exchange),\n\t\ttoolExecution: \"parallel\",\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\texecutedInput?: unknown;\n\texecutionOutcome: ToolExecutionOutcome;\n};\n\ntype FinalizedToolCallOutcome = ExecutedToolCallOutcome & {\n\ttoolCall: AgentToolCall;\n};\n\ntype PublishedToolCallOutcome = FinalizedToolCallOutcome & { exchange: FinalizedToolExchange };\n\ntype FinalizedToolCallEntry = PublishedToolCallOutcome | (() => Promise<PublishedToolCallOutcome>);\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 maybePromiseWithAbort(\n\t\t\t\tconfig.beforeToolCall(\n\t\t\t\t\t{\n\t\t\t\t\t\tassistantMessage,\n\t\t\t\t\t\ttoolCall,\n\t\t\t\t\t\targs: validatedArgs,\n\t\t\t\t\t\tcontext: currentContext,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\n\t\t\t\t),\n\t\t\t\tsignal,\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\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\tsource: ToolCallSource,\n\ttoolExecution: ToolExecutionMode,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<ExecutedToolCallOutcome> {\n\tconst invocationArgs = structuredClone(prepared.args);\n\tconst executedInput = structuredClone(invocationArgs);\n\tconst execute = prepared.tool.execute;\n\tif (!signal?.aborted) {\n\t\t// Admission failures must not become synthetic tool results or fall through to effects.\n\t\tconst owner = await config.onToolInvocationStarting?.(\n\t\t\t{\n\t\t\t\texecutionId: source.executionId,\n\t\t\t\tsourceOrder: source.sourceOrder,\n\t\t\t\ttoolCallId: prepared.toolCall.id,\n\t\t\t\ttoolName: prepared.toolCall.name,\n\t\t\t\toriginalInput: source.originalInput,\n\t\t\t\texecutedInput,\n\t\t\t\ttoolExecution,\n\t\t\t},\n\t\t\tsignal,\n\t\t\tprepared.tool,\n\t\t\texecute,\n\t\t\tsource.assistantMessage,\n\t\t);\n\t\tif (owner) source.owner = owner;\n\t}\n\tconst updateEvents: Promise<void>[] = [];\n\tlet acceptingUpdates = true;\n\tlet executionStarted = false;\n\n\ttry {\n\t\tthrowIfAborted(signal);\n\t\texecutionStarted = true;\n\t\tconst run = () =>\n\t\t\texecute.call(prepared.tool, prepared.toolCall.id, invocationArgs as never, signal, (partialResult) => {\n\t\t\t\tif (!acceptingUpdates || signal?.aborted) {\n\t\t\t\t\treturn;\n\t\t\t\t}\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\tconst result = await raceWithAbort(source.owner ? source.owner.run(run) : run(), signal);\n\t\tacceptingUpdates = false;\n\t\ttry {\n\t\t\tawait raceWithAbort(\n\t\t\t\tPromise.all(updateEvents).then(() => undefined),\n\t\t\t\tsignal,\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tif (!signal?.aborted || !isAbortError(error)) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t\treturn { result, isError: false, executedInput, executionOutcome: \"completed\" };\n\t} catch (error) {\n\t\tacceptingUpdates = false;\n\t\tawait raceWithAbort(\n\t\t\tPromise.all(updateEvents).then(() => undefined),\n\t\t\tsignal,\n\t\t).catch(() => undefined);\n\t\treturn {\n\t\t\tresult: createErrorToolResult(\n\t\t\t\tsignal?.aborted ? \"Tool execution aborted\" : error instanceof Error ? error.message : String(error),\n\t\t\t),\n\t\t\tisError: true,\n\t\t\t...(executionStarted ? { executedInput } : {}),\n\t\t\texecutionOutcome: !executionStarted ? \"not_started\" : signal?.aborted ? \"outcome_unknown\" : \"failed\",\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 maybePromiseWithAbort(\n\t\t\t\tconfig.afterToolCall(\n\t\t\t\t\t{\n\t\t\t\t\t\tassistantMessage,\n\t\t\t\t\t\ttoolCall: prepared.toolCall,\n\t\t\t\t\t\targs: prepared.args,\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tisError,\n\t\t\t\t\t\tcontext: currentContext,\n\t\t\t\t\t},\n\t\t\t\t\tsignal,\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\t...executed,\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 publishToolExchange(\n\tfinalized: FinalizedToolCallOutcome,\n\tsource: ToolCallSource,\n\ttoolExecution: ToolExecutionMode,\n\tconfig: AgentLoopConfig,\n\tsignal: AbortSignal | undefined,\n\temit: AgentEventSink,\n): Promise<PublishedToolCallOutcome> {\n\tconst exchange: FinalizedToolExchange = {\n\t\texecutionId: source.executionId,\n\t\tsourceOrder: source.sourceOrder,\n\t\ttoolCallId: finalized.toolCall.id,\n\t\ttoolName: finalized.toolCall.name,\n\t\toriginalInput: source.originalInput,\n\t\t...(finalized.executionOutcome === \"not_started\" ? {} : { executedInput: finalized.executedInput }),\n\t\ttoolExecution,\n\t\texecutionOutcome: finalized.executionOutcome,\n\t\tcancellationRequested: signal?.aborted ?? false,\n\t\tresult: createToolResultMessage(finalized),\n\t};\n\tawait config.onToolExchangeFinalized?.(exchange, signal, source.owner);\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\texchange,\n\t});\n\treturn { ...finalized, exchange };\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"]}
|