UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

157 lines (156 loc) • 6.74 kB
import { ToolResponse } from "./ToolResponse.js"; import { ToolExecutionStream } from "./ToolExecutionStream.js"; // src/core/tool/toolResultStream.ts const TOOL_EXECUTION_ID = Symbol.for("assistant-stream.tool-execution-id"); const TOOL_ABORTED = Symbol("assistant-stream.tool-aborted"); const isStandardSchemaV1 = (schema) => { return typeof schema === "object" && schema !== null && "~standard" in schema && schema["~standard"].version === 1; }; const isThenable = (value) => typeof value?.then === "function"; const raceWithAbort = async (value, abortSignal, delayAbort = false) => { let onAbort; const abortPromise = new Promise((resolve) => { onAbort = () => { if (delayAbort) queueMicrotask(() => queueMicrotask(() => resolve(TOOL_ABORTED))); else resolve(TOOL_ABORTED); }; if (abortSignal.aborted) onAbort(); else abortSignal.addEventListener("abort", onAbort, { once: true }); }); try { return await Promise.race([value, abortPromise]); } finally { abortSignal.removeEventListener("abort", onAbort); } }; const cancelledToolResponse = () => new ToolResponse({ result: "Tool execution was cancelled.", isError: true }); function getToolResponse(tools, abortSignal, toolCall, human) { const tool = tools?.[toolCall.toolName]; if (!tool?.execute) return void 0; const getResult = async (toolExecute) => { if (abortSignal.aborted) return cancelledToolResponse(); let executeFn = toolExecute; let args = toolCall.args; if (isStandardSchemaV1(tool.parameters)) { const result = tool.parameters["~standard"].validate(toolCall.args); const validationResult = isThenable(result) ? await raceWithAbort(result, abortSignal) : result; if (validationResult === TOOL_ABORTED) return cancelledToolResponse(); if (validationResult.issues) executeFn = tool.experimental_onSchemaValidationError ?? (() => { throw new Error(`Function parameter validation failed. ${JSON.stringify(validationResult.issues)}`); }); else args = validationResult.value; } if (abortSignal.aborted) return cancelledToolResponse(); const executePromise = (async () => { const executionContext = { toolCallId: toolCall.toolCallId, abortSignal, human: (payload) => human(toolCall.toolCallId, payload, toolCall.executionId), [TOOL_EXECUTION_ID]: toolCall.executionId }; const result = await executeFn(args, executionContext); const response = ToolResponse.toResponse(result); if (tool.toModelOutput && !response.isError && response.modelContent === void 0) try { const modelContent = await tool.toModelOutput({ toolCallId: toolCall.toolCallId, input: args, output: response.result }); return new ToolResponse({ result: response.result, artifact: response.artifact, isError: response.isError, messages: response.messages, modelContent }); } catch (e) { console.warn(`[assistant-stream] tool "${toolCall.toolName}" toModelOutput threw; falling back to default projection.`, e); } return response; })(); const executionResult = await raceWithAbort(executePromise, abortSignal, true); return executionResult === TOOL_ABORTED ? cancelledToolResponse() : executionResult; }; return getResult(tool.execute); } function getToolStreamResponse(tools, abortSignal, reader, context, human) { const executionContext = { toolCallId: context.toolCallId, abortSignal, human: (payload) => human(context.toolCallId, payload, context.executionId), [TOOL_EXECUTION_ID]: context.executionId }; return tools?.[context.toolName]?.streamCall?.(reader, executionContext); } const isPendingToolCall = (part) => part.type === "tool-call" && part.state !== "result" && part.result === void 0 && part.approval === void 0; /** @deprecated Experimental since 2025-04-07. Not scheduled for removal; the API may change in any release. */ async function unstable_runPendingTools(message, tools, abortSignal, human) { const toolCallPromises = message.parts.filter(isPendingToolCall).map(async (part) => { const promiseOrUndefined = getToolResponse(tools, abortSignal, { ...part, executionId: Symbol() }, human ?? (async () => { throw new Error("Tool human input is not supported in this context"); })); if (promiseOrUndefined) return { part, result: await promiseOrUndefined }; return null; }); const toolCallResults = (await Promise.all(toolCallPromises)).filter((result) => result !== null); if (toolCallResults.length === 0) return message; const toolCallResultsByPart = new Map(toolCallResults.map(({ part, result }) => [part, result])); const updatedParts = message.parts.map((p) => { if (isPendingToolCall(p)) { const toolResponse = toolCallResultsByPart.get(p); if (toolResponse) return { ...p, state: "result", ...toolResponse.artifact !== void 0 ? { artifact: toolResponse.artifact } : {}, ...toolResponse.modelContent !== void 0 ? { modelContent: toolResponse.modelContent } : {}, ...toolResponse.messages !== void 0 ? { messages: toolResponse.messages } : {}, result: toolResponse.result, isError: toolResponse.isError }; } return p; }); return { ...message, parts: updatedParts, content: updatedParts }; } /** * Transform stream that executes frontend tools and appends tool results. * * The transform watches streamed tool-call arguments, runs the matching * frontend tool once its arguments are complete, and emits a result chunk for * the tool call. A call streamed inside a step runs when that step finishes, * and a call that carries an approval is left to the provider. Backend and * human tools pass through according to their tool definition. * * @param tools Tool registry or function returning the current registry. * @param abortSignal Signal, or signal getter, used for the current run. * @param human Callback used to resolve human-tool requests from UI input. * @param options Optional execution lifecycle callbacks. */ function toolResultStream(tools, abortSignal, human, options) { const toolsFn = typeof tools === "function" ? tools : () => tools; const abortSignalFn = typeof abortSignal === "function" ? abortSignal : () => abortSignal; const internalOptions = options; const internalHuman = human; const executionOptions = { execute: (toolCall) => getToolResponse(toolsFn(), abortSignalFn(), toolCall, internalHuman), streamCall: ({ reader, ...context }) => getToolStreamResponse(toolsFn(), abortSignalFn(), reader, context, internalHuman), onExecutionStart: internalOptions?.onExecutionStart, onExecutionEnd: internalOptions?.onExecutionEnd }; return new ToolExecutionStream(executionOptions); } export { toolResultStream, unstable_runPendingTools }; //# sourceMappingURL=toolResultStream.js.map