Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
26 kB · 971 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972/** * Agent loop that works with AgentMessage throughout. * Transforms to Message[] only at the LLM call boundary. */
import { type AssistantMessage, type AssistantMessageEvent, type Context, EventStream, streamSimple, type ToolResultMessage, validateToolArguments,} from "@earendil-works/pi-ai";import type { AgentContext, AgentEvent, AgentLoopConfig, AgentMessage, AgentTool, AgentToolCall, AgentToolResult, StreamFn,} from "./types.js";
export type AgentEventSink = (event: AgentEvent) => Promise<void> | void;
const ABORT_ERROR_MESSAGE = "Request was aborted";const EMPTY_USAGE: AssistantMessage["usage"] = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },};
function createAbortError(): Error { return new Error(ABORT_ERROR_MESSAGE);}
function throwIfAborted(signal: AbortSignal | undefined): void { if (signal?.aborted) { throw createAbortError(); }}
function raceWithAbort<T>(operation: Promise<T>, signal: AbortSignal | undefined, onAbort?: () => void): Promise<T> { if (!signal) { return operation; } if (signal.aborted) { onAbort?.(); void operation.catch(() => undefined); return Promise.reject(createAbortError()); }
return new Promise<T>((resolve, reject) => { let settled = false; const cleanup = () => { signal.removeEventListener("abort", abort); }; const abort = () => { if (settled) { return; } settled = true; cleanup(); onAbort?.(); reject(createAbortError()); }; signal.addEventListener("abort", abort, { once: true }); operation.then( (value) => { if (settled) { return; } settled = true; cleanup(); resolve(value); }, (error: unknown) => { if (settled) { return; } settled = true; cleanup(); reject(error); }, ); });}
function maybePromiseWithAbort<T>( operation: T | Promise<T>, signal: AbortSignal | undefined, onAbort?: () => void,): Promise<T> { return raceWithAbort(Promise.resolve(operation), signal, onAbort);}
function isAbortError(error: unknown): boolean { return error instanceof Error && (error.message === ABORT_ERROR_MESSAGE || error.name === "AbortError");}
type PostTurnResult<T> = { status: "completed"; value: T } | { status: "aborted" };
async function settlePostTurn<T>(operation: Promise<T>, signal: AbortSignal | undefined): Promise<PostTurnResult<T>> { try { return { status: "completed", value: await operation }; } catch (error) { if (signal?.aborted && isAbortError(error)) { return { status: "aborted" }; } throw error; }}
function cloneAssistantContent(content: AssistantMessage["content"]): AssistantMessage["content"] { return content.map((part) => { if (part.type === "toolCall") { return { ...part, arguments: { ...part.arguments } }; } return { ...part }; });}
function cloneUsage(usage: AssistantMessage["usage"]): AssistantMessage["usage"] { return { ...usage, cost: { ...usage.cost } };}
function createAbortedAssistantMessage( config: AgentLoopConfig, partialMessage: AssistantMessage | null,): AssistantMessage { return { role: "assistant", content: partialMessage ? cloneAssistantContent(partialMessage.content) : [{ type: "text", text: "" }], api: partialMessage?.api ?? config.model.api, provider: partialMessage?.provider ?? config.model.provider, model: partialMessage?.model ?? config.model.id, usage: cloneUsage(partialMessage?.usage ?? EMPTY_USAGE), stopReason: "aborted", errorMessage: ABORT_ERROR_MESSAGE, timestamp: Date.now(), };}
function getTerminalMessage(event: Extract<AssistantMessageEvent, { type: "done" | "error" }>): AssistantMessage { return event.type === "done" ? event.message : event.error;}
function endAgentStreamOnError( stream: EventStream<AgentEvent, AgentMessage[]>, promise: Promise<AgentMessage[]>,): void { void promise.then( (messages) => { stream.end(messages); }, () => { stream.end([]); }, );}
async function pollMessagesUnlessAborted( poll: (() => AgentMessage[] | Promise<AgentMessage[]>) | undefined, signal: AbortSignal | undefined,): Promise<AgentMessage[]> { if (!poll || signal?.aborted) { return []; } return (await maybePromiseWithAbort(poll(), signal)) || [];}
/** * Start an agent loop with a new prompt message. * The prompt is added to the context and events are emitted for it. */export function agentLoop( prompts: AgentMessage[], context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn,): EventStream<AgentEvent, AgentMessage[]> { const stream = createAgentStream();
endAgentStreamOnError( stream, runAgentLoop( prompts, context, config, async (event) => { stream.push(event); }, signal, streamFn, ), );
return stream;}
/** * Continue an agent loop from the current context without adding a new message. * Used for retries - context already has user message or tool results. * * **Important:** The last message in context must convert to a `user` or `toolResult` message * via `convertToLlm`. If it doesn't, the LLM provider will reject the request. * This cannot be validated here since `convertToLlm` is only called once per turn. */export function agentLoopContinue( context: AgentContext, config: AgentLoopConfig, signal?: AbortSignal, streamFn?: StreamFn,): EventStream<AgentEvent, AgentMessage[]> { if (context.messages.length === 0) { throw new Error("Cannot continue: no messages in context"); }
if (context.messages[context.messages.length - 1].role === "assistant") { throw new Error("Cannot continue from message role: assistant"); }
const stream = createAgentStream();
endAgentStreamOnError( stream, runAgentLoopContinue( context, config, async (event) => { stream.push(event); }, signal, streamFn, ), );
return stream;}
export async function runAgentLoop( prompts: AgentMessage[], context: AgentContext, config: AgentLoopConfig, emit: AgentEventSink, signal?: AbortSignal, streamFn?: StreamFn,): Promise<AgentMessage[]> { const newMessages: AgentMessage[] = [...prompts]; const currentContext: AgentContext = { ...context, messages: [...context.messages, ...prompts], };
await emit({ type: "agent_start" }); await emit({ type: "turn_start" }); for (const prompt of prompts) { await emit({ type: "message_start", message: prompt }); await emit({ type: "message_end", message: prompt }); }
await runLoop(currentContext, newMessages, config, signal, emit, streamFn); return newMessages;}
export async function runAgentLoopContinue( context: AgentContext, config: AgentLoopConfig, emit: AgentEventSink, signal?: AbortSignal, streamFn?: StreamFn,): Promise<AgentMessage[]> { if (context.messages.length === 0) { throw new Error("Cannot continue: no messages in context"); }
if (context.messages[context.messages.length - 1].role === "assistant") { throw new Error("Cannot continue from message role: assistant"); }
const newMessages: AgentMessage[] = []; const currentContext: AgentContext = { ...context };
await emit({ type: "agent_start" }); await emit({ type: "turn_start" });
await runLoop(currentContext, newMessages, config, signal, emit, streamFn); return newMessages;}
function createAgentStream(): EventStream<AgentEvent, AgentMessage[]> { return new EventStream<AgentEvent, AgentMessage[]>( (event: AgentEvent) => event.type === "agent_end", (event: AgentEvent) => (event.type === "agent_end" ? event.messages : []), );}
async function runLoop( currentContext: AgentContext, newMessages: AgentMessage[], config: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink, streamFn?: StreamFn,): Promise<void> { let firstTurn = true; let lastTurn: Parameters<NonNullable<AgentLoopConfig["getContinuationMessages"]>>[0] | undefined; let pendingMessages: AgentMessage[] = await pollMessagesUnlessAborted(config.getSteeringMessages, signal);
const shouldStopBeforeTurn = (): boolean => !firstTurn && (config.shouldStopBeforeTurn?.() ?? false);
while (true) { throwIfAborted(signal); let hasMoreToolCalls = true;
while (hasMoreToolCalls || pendingMessages.length > 0) { throwIfAborted(signal); if (!firstTurn) { await emit({ type: "turn_start" }); } else { firstTurn = false; }
if (pendingMessages.length > 0) { for (const message of pendingMessages) { await emit({ type: "message_start", message }); await emit({ type: "message_end", message }); currentContext.messages.push(message); newMessages.push(message); } pendingMessages = []; }
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn); newMessages.push(message);
if (message.stopReason === "error" || message.stopReason === "aborted") { await emit({ type: "turn_end", message, toolResults: [] }); await emit({ type: "agent_end", messages: newMessages }); return; }
const toolCalls = message.content.filter((c) => c.type === "toolCall");
const toolResults: ToolResultMessage[] = []; hasMoreToolCalls = false; if (toolCalls.length > 0) { const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit); toolResults.push(...executedToolBatch.messages); hasMoreToolCalls = !executedToolBatch.terminate;
for (const result of toolResults) { currentContext.messages.push(result); newMessages.push(result); } }
await emit({ type: "turn_end", message, toolResults }); if (signal?.aborted) { await emit({ type: "agent_end", messages: newMessages }); return; } lastTurn = { message, toolResults, context: currentContext, newMessages, };
const shouldStopResult = await settlePostTurn( maybePromiseWithAbort( config.shouldStopAfterTurn?.({ message, toolResults, context: currentContext, newMessages, }) ?? false, signal, ), signal, ); if (shouldStopResult.status === "aborted") { await emit({ type: "agent_end", messages: newMessages }); return; } if (shouldStopResult.value || shouldStopBeforeTurn()) { await emit({ type: "agent_end", messages: newMessages }); return; }
const steeringMessagesResult = await settlePostTurn( pollMessagesUnlessAborted(config.getSteeringMessages, signal), signal, ); if (steeringMessagesResult.status === "aborted") { await emit({ type: "agent_end", messages: newMessages }); return; } pendingMessages = steeringMessagesResult.value; // Steering drained by this poll owns the turn boundary; stop only when it was empty. if (pendingMessages.length === 0 && shouldStopBeforeTurn()) { await emit({ type: "agent_end", messages: newMessages }); return; } }
if (shouldStopBeforeTurn()) break; const followUpMessagesResult = await settlePostTurn( pollMessagesUnlessAborted(config.getFollowUpMessages, signal), signal, ); if (followUpMessagesResult.status === "aborted") { await emit({ type: "agent_end", messages: newMessages }); return; } const followUpMessages = followUpMessagesResult.value; if (followUpMessages.length > 0) { pendingMessages = followUpMessages; continue; }
if (shouldStopBeforeTurn()) break; const continuationMessagesResult = lastTurn ? await settlePostTurn( maybePromiseWithAbort(config.getContinuationMessages?.(lastTurn, signal) ?? [], signal), signal, ) : ({ status: "completed", value: [] } satisfies PostTurnResult<AgentMessage[]>); if (continuationMessagesResult.status === "aborted") { await emit({ type: "agent_end", messages: newMessages }); return; } const continuationMessages = continuationMessagesResult.value || []; if (continuationMessages.length > 0) { pendingMessages = continuationMessages; continue; }
break; }
await emit({ type: "agent_end", messages: newMessages });}
async function streamAssistantResponse( context: AgentContext, config: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink, streamFn?: StreamFn,): Promise<AssistantMessage> { let partialMessage: AssistantMessage | null = null; let addedPartial = false; const finishAbortedMessage = async () => { const finalMessage = createAbortedAssistantMessage(config, partialMessage); if (addedPartial) { context.messages[context.messages.length - 1] = finalMessage; } else { context.messages.push(finalMessage); await emit({ type: "message_start", message: { ...finalMessage } }); } await emit({ type: "message_end", message: finalMessage }); return finalMessage; };
try { throwIfAborted(signal); let messages = context.messages; if (config.transformContext) { messages = await maybePromiseWithAbort(config.transformContext(messages, signal), signal); }
const llmMessages = await maybePromiseWithAbort(config.convertToLlm(messages), signal);
const streamFunction = streamFn || streamSimple;
const resolvedApiKey = (config.getApiKey ? await maybePromiseWithAbort(config.getApiKey(config.model.provider), signal) : undefined) || config.apiKey;
const llmContext: Context = { systemPrompt: config.getSystemPrompt?.() ?? context.systemPrompt, messages: llmMessages, tools: context.tools, };
const response = await maybePromiseWithAbort( streamFunction(config.model, llmContext, { ...config, apiKey: resolvedApiKey, signal, }), signal, ); const iterator = response[Symbol.asyncIterator](); const closeIterator = () => { void Promise.resolve(iterator.return?.()).catch(() => undefined); }; while (true) { const next = await raceWithAbort<IteratorResult<AssistantMessageEvent>>( iterator.next(), signal, closeIterator, ); if (next.done) { break; } const event = next.value; switch (event.type) { case "start": partialMessage = event.partial; context.messages.push(partialMessage); addedPartial = true; await emit({ type: "message_start", message: { ...partialMessage } }); break;
case "text_start": case "text_delta": case "text_end": case "thinking_start": case "thinking_delta": case "thinking_end": case "toolcall_start": case "toolcall_delta": case "toolcall_end": if (partialMessage) { partialMessage = event.partial; context.messages[context.messages.length - 1] = partialMessage; await emit({ type: "message_update", assistantMessageEvent: event, message: { ...partialMessage }, }); } break;
case "done": case "error": { let finalMessage = getTerminalMessage(event); try { finalMessage = await maybePromiseWithAbort(response.result(), signal); } catch (error) { if (!signal?.aborted || !isAbortError(error)) { throw error; } } if (addedPartial) { context.messages[context.messages.length - 1] = finalMessage; } else { context.messages.push(finalMessage); } if (!addedPartial) { await emit({ type: "message_start", message: { ...finalMessage } }); } await emit({ type: "message_end", message: finalMessage }); return finalMessage; } } }
const finalMessage = await maybePromiseWithAbort(response.result(), signal); if (addedPartial) { context.messages[context.messages.length - 1] = finalMessage; } else { context.messages.push(finalMessage); await emit({ type: "message_start", message: { ...finalMessage } }); } await emit({ type: "message_end", message: finalMessage }); return finalMessage; } catch (error) { if (signal?.aborted && isAbortError(error)) { return finishAbortedMessage(); } throw error; }}
async function executeToolCalls( currentContext: AgentContext, assistantMessage: AssistantMessage, config: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink,): Promise<ExecutedToolCallBatch> { const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall"); const hasSequentialToolCall = toolCalls.some( (tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential", ); if (config.toolExecution === "sequential" || hasSequentialToolCall) { return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit); } return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit);}
type ExecutedToolCallBatch = { messages: ToolResultMessage[]; terminate: boolean;};
async function executeToolCallsSequential( currentContext: AgentContext, assistantMessage: AssistantMessage, toolCalls: AgentToolCall[], config: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink,): Promise<ExecutedToolCallBatch> { const finalizedCalls: FinalizedToolCallOutcome[] = []; const messages: ToolResultMessage[] = [];
for (const toolCall of toolCalls) { if (signal?.aborted) { break; }
const startedAtMs = performance.now(); await emit({ type: "tool_execution_start", toolCallId: toolCall.id, toolName: toolCall.name, args: toolCall.arguments, });
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal); let finalized: FinalizedToolCallOutcome; if (preparation.kind === "immediate") { finalized = { toolCall, result: preparation.result, isError: preparation.isError, }; } else { const executed = await executePreparedToolCall(preparation, signal, emit); finalized = await finalizeExecutedToolCall( currentContext, assistantMessage, preparation, executed, config, signal, ); }
finalized.durationMs = Math.round(performance.now() - startedAtMs); await emitToolExecutionEnd(finalized, emit); const toolResultMessage = createToolResultMessage(finalized); await emitToolResultMessage(toolResultMessage, emit); finalizedCalls.push(finalized); messages.push(toolResultMessage);
if (signal?.aborted) { break; } }
return { messages, terminate: shouldTerminateToolBatch(finalizedCalls), };}
async function executeToolCallsParallel( currentContext: AgentContext, assistantMessage: AssistantMessage, toolCalls: AgentToolCall[], config: AgentLoopConfig, signal: AbortSignal | undefined, emit: AgentEventSink,): Promise<ExecutedToolCallBatch> { const finalizedCalls: FinalizedToolCallEntry[] = [];
for (const toolCall of toolCalls) { const startedAtMs = performance.now(); await emit({ type: "tool_execution_start", toolCallId: toolCall.id, toolName: toolCall.name, args: toolCall.arguments, });
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal); if (preparation.kind === "immediate") { const finalized = { toolCall, result: preparation.result, isError: preparation.isError, durationMs: Math.round(performance.now() - startedAtMs), } satisfies FinalizedToolCallOutcome; await emitToolExecutionEnd(finalized, emit); finalizedCalls.push(finalized); continue; }
finalizedCalls.push(async () => { const executed = await executePreparedToolCall(preparation, signal, emit); const finalized = await finalizeExecutedToolCall( currentContext, assistantMessage, preparation, executed, config, signal, ); finalized.durationMs = Math.round(performance.now() - startedAtMs); await emitToolExecutionEnd(finalized, emit); return finalized; }); }
const orderedFinalizedCalls = await Promise.all( finalizedCalls.map((entry) => (typeof entry === "function" ? entry() : Promise.resolve(entry))), ); const messages: ToolResultMessage[] = []; for (const finalized of orderedFinalizedCalls) { const toolResultMessage = createToolResultMessage(finalized); await emitToolResultMessage(toolResultMessage, emit); messages.push(toolResultMessage); }
return { messages, terminate: shouldTerminateToolBatch(orderedFinalizedCalls), };}
type PreparedToolCall = { kind: "prepared"; toolCall: AgentToolCall; tool: AgentTool<any>; args: unknown;};
type ImmediateToolCallOutcome = { kind: "immediate"; result: AgentToolResult<any>; isError: boolean;};
type ExecutedToolCallOutcome = { result: AgentToolResult<any>; isError: boolean;};
type FinalizedToolCallOutcome = { toolCall: AgentToolCall; result: AgentToolResult<any>; isError: boolean; durationMs?: number;};
type FinalizedToolCallEntry = FinalizedToolCallOutcome | (() => Promise<FinalizedToolCallOutcome>);
function shouldTerminateToolBatch(finalizedCalls: FinalizedToolCallOutcome[]): boolean { return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);}
function prepareToolCallArguments(tool: AgentTool<any>, toolCall: AgentToolCall): AgentToolCall { if (!tool.prepareArguments) { return toolCall; } const preparedArguments = tool.prepareArguments(toolCall.arguments); if (preparedArguments === toolCall.arguments) { return toolCall; } return { ...toolCall, arguments: preparedArguments as Record<string, any>, };}
async function prepareToolCall( currentContext: AgentContext, assistantMessage: AssistantMessage, toolCall: AgentToolCall, config: AgentLoopConfig, signal: AbortSignal | undefined,): Promise<PreparedToolCall | ImmediateToolCallOutcome> { const tool = currentContext.tools?.find((t) => t.name === toolCall.name); if (!tool) { return { kind: "immediate", result: createErrorToolResult(`Tool ${toolCall.name} not found`), isError: true, }; }
try { const preparedToolCall = prepareToolCallArguments(tool, toolCall); const validatedArgs = validateToolArguments(tool, preparedToolCall); if (config.beforeToolCall) { const beforeResult = await maybePromiseWithAbort( config.beforeToolCall( { assistantMessage, toolCall, args: validatedArgs, context: currentContext, }, signal, ), signal, ); if (beforeResult?.block) { return { kind: "immediate", result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"), isError: true, }; } } return { kind: "prepared", toolCall, tool, args: validatedArgs, }; } catch (error) { return { kind: "immediate", result: createErrorToolResult(error instanceof Error ? error.message : String(error)), isError: true, }; }}
async function executePreparedToolCall( prepared: PreparedToolCall, signal: AbortSignal | undefined, emit: AgentEventSink,): Promise<ExecutedToolCallOutcome> { const updateEvents: Promise<void>[] = []; let acceptingUpdates = true;
try { throwIfAborted(signal); const result = await raceWithAbort( prepared.tool.execute(prepared.toolCall.id, prepared.args as never, signal, (partialResult) => { if (!acceptingUpdates || signal?.aborted) { return; } updateEvents.push( Promise.resolve( emit({ type: "tool_execution_update", toolCallId: prepared.toolCall.id, toolName: prepared.toolCall.name, args: prepared.toolCall.arguments, partialResult, }), ), ); }), signal, ); acceptingUpdates = false; try { await raceWithAbort( Promise.all(updateEvents).then(() => undefined), signal, ); } catch (error) { if (!signal?.aborted || !isAbortError(error)) { throw error; } } return { result, isError: false }; } catch (error) { acceptingUpdates = false; await raceWithAbort( Promise.all(updateEvents).then(() => undefined), signal, ).catch(() => undefined); return { result: createErrorToolResult( signal?.aborted ? "Tool execution aborted" : error instanceof Error ? error.message : String(error), ), isError: true, }; }}
async function finalizeExecutedToolCall( currentContext: AgentContext, assistantMessage: AssistantMessage, prepared: PreparedToolCall, executed: ExecutedToolCallOutcome, config: AgentLoopConfig, signal: AbortSignal | undefined,): Promise<FinalizedToolCallOutcome> { let result = executed.result; let isError = executed.isError;
if (config.afterToolCall) { try { const afterResult = await maybePromiseWithAbort( config.afterToolCall( { assistantMessage, toolCall: prepared.toolCall, args: prepared.args, result, isError, context: currentContext, }, signal, ), signal, ); if (afterResult) { result = { content: afterResult.content ?? result.content, details: afterResult.details ?? result.details, terminate: afterResult.terminate ?? result.terminate, }; isError = afterResult.isError ?? isError; } } catch (error) { result = createErrorToolResult(error instanceof Error ? error.message : String(error)); isError = true; } }
return { toolCall: prepared.toolCall, result, isError, };}
function createErrorToolResult(message: string): AgentToolResult<any> { return { content: [{ type: "text", text: message }], details: {}, };}
async function emitToolExecutionEnd(finalized: FinalizedToolCallOutcome, emit: AgentEventSink): Promise<void> { await emit({ type: "tool_execution_end", toolCallId: finalized.toolCall.id, toolName: finalized.toolCall.name, result: finalized.result, isError: finalized.isError, durationMs: finalized.durationMs, });}
function createToolResultMessage(finalized: FinalizedToolCallOutcome): ToolResultMessage { return { role: "toolResult", toolCallId: finalized.toolCall.id, toolName: finalized.toolCall.name, content: finalized.result.content, details: finalized.result.details, isError: finalized.isError, timestamp: Date.now(), durationMs: finalized.durationMs, };}
async function emitToolResultMessage(toolResultMessage: ToolResultMessage, emit: AgentEventSink): Promise<void> { await emit({ type: "message_start", message: toolResultMessage }); await emit({ type: "message_end", message: toolResultMessage });}