UNPKG

@tanstack/ai

Version:

Type-safe TypeScript AI SDK for streaming chat, tool calling, agents, structured outputs, and multimodal generation.

831 lines (830 loc) • 31 kB
import { EventType } from "../../../types.js"; import { addTokenUsage, isTanstackUsage, rebuildTokenUsage, toSpecTokenUsage } from "../../../utilities/ag-ui-usage.js"; import { tanstackMetadata, withTanstackMetadata } from "../../../utilities/merge-metadata.js"; import { convertSchemaToJsonSchema, isStandardSchema } from "../tools/schema-converter.js"; import { INTERRUPT_BINDING_METADATA_KEY } from "../../../interrupt-resume.js"; import { SUBAGENT_INPUT_KEY } from "../../../utilities/subagent-wire.js"; import { envProviderKeys } from "../../../byok/env-keys.js"; import { mergeStreams } from "../../../utilities/merge-streams.js"; import { EMIT_STREAM_CHUNK, SUBAGENT_TOOL } from "../tools/tool-calls.js"; import { getLoadChild } from "../middleware/load-child.js"; import { createBoundActivities } from "./bound.js"; import { SubagentBudget } from "./limits.js"; //#region src/activities/chat/agents/spawn.ts var SUBAGENT_STARTED = EventType.SUBAGENT_STARTED; var SUBAGENT_FINISHED = EventType.SUBAGENT_FINISHED; var SUBAGENT_ERROR = EventType.SUBAGENT_ERROR; /** * Fields an agent tool takes next to the agent's own fields, when the run * can use them. An `inputSchema` cannot use a name that the run uses. * - `sessionId`: the `subagentRunId` of a stored child. The call continues * it. Needs a middleware that provides `LoadChildCapability`. * - `background`: start the child and return at once. Needs `binding.start`. */ var RESERVED_FIELDS = ["sessionId", "background"]; /** The reserved fields that a run uses. */ function reservedFields(bag, canContinue) { const can = { sessionId: canContinue, background: bag.binding?.start !== void 0 }; return RESERVED_FIELDS.filter((name) => can[name]); } /** Throws when an agent's `inputSchema` uses a reserved field of the run. */ function assertNoReservedFields(bag, canContinue) { const names = reservedFields(bag, canContinue); for (const agent of names.length > 0 ? bag.agents : []) { const schema = convertSchemaToJsonSchema(agent.inputSchema, { io: "input" }); const taken = names.find((name) => schema?.properties?.[name]); if (taken !== void 0) throw new Error(`Agent "${agent.name}" uses the reserved input field "${taken}". Rename that field.`); } } /** Split the reserved fields off a tool call. */ function splitReserved(value, names) { if (typeof value !== "object" || value === null || Array.isArray(value)) return { fields: value, reserved: {} }; const fields = {}; const reserved = {}; for (const [key, entry] of Object.entries(value)) { const target = names.includes(key) ? reserved : fields; target[key] = entry; } return { fields, reserved }; } function createSubagentSink() { return { interrupts: [], usage: [] }; } function createSubagentId() { return `subagent-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; } function childRunId(parentRunId, subagentRunId) { return `${parentRunId}:${subagentRunId}`; } /** * Bind child interrupts to the parent run. The client resumes the parent run, * so each binding must name that run. The resumed child then validates with * the parent's interrupted run id. */ function rebindInterrupts(interrupts, runId) { return interrupts.map((interrupt) => { const binding = interrupt.metadata?.[INTERRUPT_BINDING_METADATA_KEY]; if (typeof binding !== "object" || binding === null) return interrupt; return { ...interrupt, metadata: { ...interrupt.metadata, [INTERRUPT_BINDING_METADATA_KEY]: { ...binding, interruptedRunId: runId, generation: 0 } } }; }); } function createAbortError() { const error = /* @__PURE__ */ new Error("Aborted"); error.name = "AbortError"; return error; } function isAbortError(error, signal) { if (signal?.aborted) return true; return error instanceof Error && (error.name === "AbortError" || error.message === "Aborted"); } function stoppedEvent(subagentRunId) { return { type: SUBAGENT_ERROR, subagentRunId, message: "Stopped", timestamp: Date.now() }; } function childThreadId(sandbox, parentThreadId, name) { return sandbox === "inherit" ? parentThreadId : `${parentThreadId}:${name}`; } function linkAbort(parent) { const controller = new AbortController(); if (!parent) return { controller, dispose: () => {} }; if (parent.aborted) { controller.abort(parent.reason); return { controller, dispose: () => {} }; } const onAbort = () => controller.abort(parent.reason); parent.addEventListener("abort", onAbort, { once: true }); return { controller, dispose: () => parent.removeEventListener("abort", onAbort) }; } function orAbort(promise, signal) { if (!signal) return promise; if (signal.aborted) return Promise.reject(createAbortError()); return new Promise((resolve, reject) => { const onAbort = () => reject(createAbortError()); signal.addEventListener("abort", onAbort, { once: true }); promise.then((value) => { signal.removeEventListener("abort", onAbort); resolve(value); }, (error) => { signal.removeEventListener("abort", onAbort); reject(error); }); }); } function agentByName(agents, name) { const agent = agents.find((entry) => entry.name === name); if (!agent) throw new Error(`Unknown subagent: ${name}`); return agent; } /** The root budget of each bag that has no budget from its parent. */ var rootBudgets = /* @__PURE__ */ new WeakMap(); /** * The subagent tree budget of `bag`: the one its parent passed down, else * one root budget per bag. The `limits` of a bag apply only at the root. */ function treeBudget(bag) { const inherited = bag.binding?.budget; if (inherited) return inherited; let budget = rootBudgets.get(bag); if (!budget) { budget = SubagentBudget.root(bag.limits); rootBudgets.set(bag, budget); } return budget; } function openAgentStream(entry, bag, ctx, sink, parentToolCallId, binding = { ...bag.binding, budget: treeBudget(bag).child() }) { const agent = agentByName(bag.agents, entry.name); const resume = entry.resume; const continued = entry.continued; const subagentRunId = resume?.subagentRunId ?? continued?.subagentRunId ?? createSubagentId(); if (resume !== void 0 && ctx.interruptedRunId === void 0) throw new Error(`Subagent "${entry.name}" has interrupt answers, but the run has no parentRunId. Pass the interrupted run id as parentRunId.`); const base = continued?.messages ?? ctx.messages; const messages = entry.prompt === void 0 ? base : [...base, { role: "user", content: entry.prompt }]; const storedIds = new Set(continued?.messages.map((message) => message.id)); const resumed = resume !== void 0 ? { messages: [...messages, ...resume.messages.filter((message) => message.id === void 0 || !storedIds.has(message.id))], parentRunId: ctx.interruptedRunId, resume: resume.entries } : void 0; return spawnAgentStream(agent, { input: entry.input, messages: resumed?.messages ?? messages, ...ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}, threadId: childThreadId(bag.sandbox, ctx.threadId, entry.name), runId: childRunId(ctx.parentRunId, subagentRunId), parentRunId: resumed?.parentRunId ?? ctx.parentRunId, subagentRunId, ...ctx.parentSubagentRunId !== void 0 ? { parentSubagentRunId: ctx.parentSubagentRunId } : {}, ...resumed ? { resume: resumed.resume } : {} }, sink, parentToolCallId, binding); } var ROUTER_PICK_ERROR = "subagents.router must return main, a name, { name, input }, a list of them, { names, order }, or { steps }."; function assertOrder(order) { if (order !== void 0 && order !== "parallel" && order !== "sequence") throw new Error("subagents.router order must be parallel or sequence."); } /** One step of a pick: plain names, the input of each name that has one, and the order. */ function normalizeStep(picks, agents, order) { assertOrder(order); if (!isPickList(picks) || picks.length === 0) throw new Error(ROUTER_PICK_ERROR); const names = picks.map((pick) => typeof pick === "string" ? pick : pick.name); if (names.includes("main") && names.length > 1) throw new Error("Do not mix main into a subagent list."); const inputs = {}; for (const pick of picks) { if (typeof pick === "string") { if (pick !== "main") agentByName(agents, pick); continue; } if (pick.input === void 0) { if (pick.name !== "main") agentByName(agents, pick.name); continue; } if (pick.name === "main") throw new Error("main takes no input."); if (agentByName(agents, pick.name).inputSchema === void 0) throw new Error(`Agent "${pick.name}" has no inputSchema, so it takes no input.`); if (names.filter((name) => name === pick.name).length > 1) throw new Error(`Agent "${pick.name}" has an input and is in one step more than once. Put each pick in its own step.`); inputs[pick.name] = pick.input; } return { names, ...Object.keys(inputs).length > 0 && { inputs }, ...order !== void 0 && { order } }; } function isPickList(pick) { return Array.isArray(pick); } function normalizeRouterPick(pick, agents) { if (isPickList(pick)) return { steps: [normalizeStep(pick, agents)] }; if (typeof pick === "string") return { steps: [normalizeStep([pick], agents)] }; if (typeof pick !== "object" || pick === null) throw new Error(ROUTER_PICK_ERROR); if ("name" in pick) return { steps: [normalizeStep([pick], agents)] }; if ("steps" in pick) { if (pick.steps.length === 0) throw new Error(ROUTER_PICK_ERROR); const steps = pick.steps.map((step) => normalizeStep(step.names, agents, step.order)); const flat = steps.flatMap((step) => step.names); if (flat.includes("main") && flat.length > 1) throw new Error("Do not mix main into a subagent list."); return { steps }; } return { steps: [normalizeStep(pick.names, agents, pick.order)] }; } /** The input of `name` in a step. Skips keys that `Object.prototype` has. */ function stepInput(step, name) { return step.inputs && Object.hasOwn(step.inputs, name) ? step.inputs[name] : void 0; } /** * Tag a child chunk with its subagent. A chunk that a nested child already * tagged keeps its own id, and a nested child's start names this child as * its parent. */ function attributeChunk(chunk, subagentRunId) { if (chunk.type === SUBAGENT_STARTED) return chunk.parentSubagentRunId !== void 0 ? chunk : { ...chunk, parentSubagentRunId: subagentRunId }; if (chunk.type === SUBAGENT_FINISHED || chunk.type === SUBAGENT_ERROR) return chunk; if ("subagentRunId" in chunk && typeof chunk.subagentRunId === "string") return chunk; return { ...chunk, subagentRunId }; } function runUsage(chunk) { if (chunk?.type !== EventType.RUN_FINISHED) return []; if (Array.isArray(chunk.usage)) return chunk.usage; return isTanstackUsage(chunk.usage) ? toSpecTokenUsage(chunk.usage).usage : []; } /** The full usage of a run: token counts plus cost and the other fields. */ function fullUsage(chunk) { if (chunk?.type !== EventType.RUN_FINISHED) return void 0; return rebuildTokenUsage(chunk.usage, tanstackMetadata(chunk)?.usage); } /** Add a finished child run's usage to the sink. */ function collectUsage(sink, finished) { sink.usage.push(...runUsage(finished)); const full = fullUsage(finished); if (full) sink.total = sink.total ? addTokenUsage(sink.total, full) : full; } /** * Put the children's usage on a parent terminal. `usage[]` keeps one entry per * model call. `metadata.tanstack.usage` holds the summed cost and the other * TanStack fields, so `fromSpecTokenUsage` reads the full total. Empties the * sink, so the next parent terminal does not count it again. * * `RUN_ERROR` is accepted too: a turn that failed still spent whatever its * children spent. Such a chunk carries no usage of its own, so `runUsage` and * `fullUsage` return empty for it and the children's total stands alone. */ function withChildUsage(chunk, sink) { if (sink.usage.length === 0 && !sink.total) return chunk; const own = fullUsage(chunk); const total = own && sink.total ? addTokenUsage(own, sink.total) : own ?? sink.total; const usage = [...runUsage(chunk), ...sink.usage.splice(0)]; sink.total = void 0; const leftover = total ? toSpecTokenUsage(total).leftover : void 0; const next = { ...chunk, usage }; if (!leftover) return next; return withTanstackMetadata(next, { usage: leftover }); } function isAsyncIterable(value) { return typeof value === "object" && value !== null && Symbol.asyncIterator in value; } /** * The chunks of a `chat({ stream: false })` result, else `undefined`. A `run` * that resolves to a chat result streams like a `run` that returns the chat * stream: its text, usage, and interrupts reach the parent. */ function chatResultChunks(value) { if (typeof value !== "object" || value === null || !("text" in value) || typeof value.text !== "string" || !("chunks" in value) || !Array.isArray(value.chunks)) return; const chunks = value.chunks; return (async function* () { yield* chunks; })(); } /** * Chunks for an agent whose `run` resolved to a plain value instead of a * stream. A string also streams as the child's text, so the parent model, a * `sequence` router, and the UI all read it like chat output. `messageId` is * new for each run and call. */ function* valueResultChunks(value, id, messageId) { if (typeof value === "string" && value !== "") { const timestamp = Date.now(); yield attributeChunk({ type: EventType.TEXT_MESSAGE_START, messageId, role: "assistant", timestamp }, id); yield attributeChunk({ type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta: value, timestamp }, id); yield attributeChunk({ type: EventType.TEXT_MESSAGE_END, messageId, timestamp }, id); } yield { type: SUBAGENT_FINISHED, subagentRunId: id, ...value !== void 0 ? { result: value } : {}, timestamp: Date.now() }; } /** * Build the context `run` receives: the spawn input plus `forward`, the * provider keys, and the bound activity functions. */ function runContext(agentName, input, abortController, binding) { return { ...input, forward: { threadId: input.threadId, runId: input.runId, parentRunId: input.parentRunId, subagentRunId: input.subagentRunId, subagentName: agentName, ...input.parentSubagentRunId !== void 0 ? { parentSubagentRunId: input.parentSubagentRunId } : {}, ...input.resume ? { resume: input.resume } : {}, abortController, ...binding?.promptCache ? { promptCache: binding.promptCache } : {} }, keys: binding?.keys ?? envProviderKeys, ...createBoundActivities(agentName, input, abortController, binding) }; } /** * Run one agent and stream its chunks, from `SUBAGENT_STARTED` to * `SUBAGENT_FINISHED` or `SUBAGENT_ERROR`. The package root exports this * as `runAgentStream`. The caller checks `ctx.input` against * `agent.inputSchema` first. Pass `createSubagentSink()` to keep the * child's interrupt objects and token usage. The finished event carries * `interruptIds` only. */ async function* spawnAgentStream(agent, input, sink, parentToolCallId, binding) { const id = input.subagentRunId; const started = { type: SUBAGENT_STARTED, subagentRunId: id, name: agent.name, description: agent.description, ...parentToolCallId !== void 0 ? { parentToolCallId } : {}, timestamp: Date.now() }; yield input.input === void 0 || parentToolCallId === void 0 ? started : withTanstackMetadata(started, { [SUBAGENT_INPUT_KEY]: input.input }); const link = linkAbort(input.abortSignal); const ctx = runContext(agent.name, input, link.controller, binding); let iterator; let finished; try { if (ctx.abortSignal?.aborted) { yield stoppedEvent(id); return; } const produced = await orAbort(Promise.resolve(agent.run(ctx)), ctx.abortSignal); const stream = isAsyncIterable(produced) ? produced : chatResultChunks(produced); if (!stream) { if (ctx.abortSignal?.aborted) { yield stoppedEvent(id); return; } yield* valueResultChunks(produced, id, parentToolCallId === void 0 ? `${ctx.runId}-text` : `${ctx.runId}-${parentToolCallId}-text`); return; } iterator = stream[Symbol.asyncIterator](); while (true) { if (ctx.abortSignal?.aborted) { yield stoppedEvent(id); return; } const result = await orAbort(iterator.next(), ctx.abortSignal); if (result.done) break; const chunk = result.value; if (chunk.type === EventType.RUN_STARTED || chunk.type === EventType.MESSAGES_SNAPSHOT) continue; if (chunk.type === EventType.RUN_FINISHED) { if (sink) collectUsage(sink, chunk); finished = chunk; continue; } if (chunk.type === EventType.RUN_ERROR) { yield { type: SUBAGENT_ERROR, subagentRunId: id, message: chunk.message || "Subagent failed", ...chunk.code ? { code: chunk.code } : {}, timestamp: Date.now() }; return; } yield attributeChunk(chunk, id); } if (ctx.abortSignal?.aborted) { yield stoppedEvent(id); return; } const outcome = finished?.type === EventType.RUN_FINISHED ? finished.outcome : void 0; if (outcome?.type === "cancelled") { yield stoppedEvent(id); return; } if (outcome?.type === "interrupt") { const interrupts = outcome.interrupts.map((interrupt) => interrupt.subagentRunId ? interrupt : { ...interrupt, subagentRunId: id }); sink?.interrupts.push(...interrupts); yield { type: SUBAGENT_FINISHED, subagentRunId: id, outcome: { type: "suspended", interruptIds: interrupts.filter((interrupt) => interrupt.subagentRunId === id).map((interrupt) => interrupt.id) }, timestamp: Date.now() }; return; } const result = finished?.type === EventType.RUN_FINISHED ? finished.result : void 0; yield { type: SUBAGENT_FINISHED, subagentRunId: id, ...result !== void 0 ? { result } : {}, timestamp: Date.now() }; } catch (error) { yield { type: SUBAGENT_ERROR, subagentRunId: id, message: isAbortError(error, ctx.abortSignal) ? "Stopped" : error instanceof Error ? error.message : String(error), timestamp: Date.now() }; } finally { try { await iterator?.return?.(); } catch {} link.dispose(); } } /** True when a child in these chunks failed or stopped for outside input. */ function stopsSequence(chunks, id) { return chunks.some((chunk) => chunk.type === SUBAGENT_ERROR && chunk.subagentRunId === id || chunk.type === SUBAGENT_FINISHED && chunk.subagentRunId === id && chunk.outcome?.type === "suspended"); } async function* spawnNamedAgents(entries, bag, ctx, sink) { if (bag.sandbox === "inherit" && entries.length > 1) throw new Error("subagents.sandbox 'inherit' cannot start two children in one turn"); const group = linkAbort(ctx.abortSignal); const groupCtx = { ...ctx, abortSignal: group.controller.signal }; try { if (bag.order === "sequence") { let messages = ctx.messages; for (const entry of entries) { const chunks = []; let id; for await (const chunk of openAgentStream(entry, bag, { ...groupCtx, messages }, sink)) { if (chunk.type === SUBAGENT_STARTED && id === void 0) id = chunk.subagentRunId; chunks.push(chunk); yield chunk; } if (id !== void 0 && stopsSequence(chunks, id)) return; const text = [entry.resume?.text, collectNamedText(chunks, [entry.name])].filter((part) => part !== void 0 && part !== "").join(""); if (text) messages = [...messages, { role: "assistant", content: text }]; } return; } const streams = entries.map((entry) => openAgentStream(entry, bag, groupCtx, sink)); const onlyStream = streams.length === 1 ? streams[0] : void 0; if (onlyStream) { yield* onlyStream; return; } yield* mergeStreams(streams); } finally { group.controller.abort(); group.dispose(); } } /** * Text of the named direct children, in `names` order. Text from nested * children stays out: their chunks carry their own id. */ function collectNamedText(chunks, names) { const nameByRunId = /* @__PURE__ */ new Map(); const textByName = /* @__PURE__ */ new Map(); for (const chunk of chunks) { if (chunk.type === SUBAGENT_STARTED) { if (chunk.parentSubagentRunId === void 0) nameByRunId.set(chunk.subagentRunId, chunk.name); continue; } if (chunk.type !== EventType.TEXT_MESSAGE_CONTENT) continue; if (!("subagentRunId" in chunk) || typeof chunk.subagentRunId !== "string") continue; const name = nameByRunId.get(chunk.subagentRunId); if (!name) continue; textByName.set(name, `${textByName.get(name) ?? ""}${chunk.delta}`); } return names.map((name) => textByName.get(name)?.trim() ?? "").filter((text) => text.length > 0).join("\n\n"); } /** * The parent conversation up to the message that carries this tool call, with * that message's tool calls removed. Its string text stays; array content is * dropped. */ function messagesBeforeCall(messages, toolCallId) { const index = messages.findIndex((message) => message.toolCalls?.some((call) => call.id === toolCallId)); if (index === -1) return [...messages]; const host = messages[index]; const kept = messages.slice(0, index); if (host && typeof host.content === "string" && host.content !== "") { const { toolCalls: _calls, ...text } = host; kept.push(text); } return kept; } /** * Record the parent messages when the model calls a subagent tool, so the * child reads the conversation as it is at that call. Also read the run's * `loadChild` service, so a call can continue a stored child. */ function subagentCallMessages(names) { const byCall = /* @__PURE__ */ new Map(); let loadChild; return { middleware: { name: "subagent-call-messages", onStart(ctx) { loadChild = getLoadChild(ctx, { optional: true }); }, onBeforeToolCall(ctx, hook) { if (!names.has(hook.toolName)) return void 0; byCall.set(hook.toolCallId, messagesBeforeCall(ctx.messages, hook.toolCallId)); } }, messagesFor: (toolCallId) => toolCallId === void 0 ? void 0 : byCall.get(toolCallId), childLoader: () => loadChild }; } /** * The wire schema of an agent tool: the agent's `inputSchema`, or a `prompt` * for an agent without one. The reserved fields of the run sit next to * those fields. */ function agentToolSchema(agent, names) { const schema = convertSchemaToJsonSchema(agent.inputSchema, { io: "input" }) ?? { type: "object", properties: { prompt: { type: "string" } } }; if (names.length === 0) return schema; const extra = { ...names.includes("sessionId") && { sessionId: { type: "string", description: "The subagentRunId of an earlier result. The call continues that child." } }, ...names.includes("background") && { background: { type: "boolean", description: "true starts the child and returns its subagentRunId at once." } } }; if (schema.properties === void 0) return { allOf: [schema, { type: "object", properties: extra }] }; return { ...schema, properties: { ...schema.properties, ...extra } }; } /** * The input schema of an agent tool. The tool loop checks it after * middleware, just before the tool runs, as for any tool. With reserved fields, a wrapper * checks the agent's fields and passes the reserved ones through. */ function agentToolInput(agent, names) { const inner = agent.inputSchema; if (isStandardSchema(inner) && names.length === 0) return inner; const schema = agentToolSchema(agent, names); if (!isStandardSchema(inner)) return schema; const toJson = () => schema; return { "~standard": { version: 1, vendor: "tanstack-ai", validate: (value) => { const { fields, reserved } = splitReserved(value, names); const add = (result) => { if (result.issues !== void 0 || Object.keys(reserved).length === 0) return result; if (typeof result.value !== "object" || result.value === null || Array.isArray(result.value)) return { issues: [{ message: "Agent input must be an object when the call uses sessionId or background." }] }; return { value: { ...result.value, ...reserved } }; }; const result = inner["~standard"].validate(fields); return result instanceof Promise ? result.then(add) : add(result); }, jsonSchema: { input: toJson, output: toJson } } }; } /** * Split a checked agent tool call into the agent's input and the reserved * fields. A reserved field of the wrong type throws a tool error. */ function readCall(agent, args, names) { const { fields, reserved } = splitReserved(args, names); const { sessionId, background } = reserved; const prompt = agent.inputSchema === void 0 && typeof fields === "object" && fields !== null ? Reflect.get(fields, "prompt") : void 0; if (prompt !== void 0 && typeof prompt !== "string" || sessionId !== void 0 && typeof sessionId !== "string" || background !== void 0 && typeof background !== "boolean") throw new Error("prompt and sessionId must be strings, and background a boolean."); return { ...agent.inputSchema !== void 0 && { input: fields }, ...typeof prompt === "string" && { prompt }, ...typeof sessionId === "string" && { sessionId }, background: background === true }; } function createSyntheticSubagentTools(bag, parent) { const budget = treeBudget(bag); let active = 0; /** Run one child for a tool call. The model gets its text or result. */ async function runChild(child, context) { const toolContext = context; const toolCallId = toolContext?.toolCallId; const suspended = parent.turn?.children.find((turnChild) => turnChild.status === "suspended" && turnChild.parentToolCallId !== void 0 && turnChild.parentToolCallId === toolCallId); const entry = { ...child, ...suspended && { resume: { subagentRunId: suspended.subagentRunId, messages: suspended.messages, entries: suspended.resume, text: suspended.text } } }; const timeout = budget.childTimeout(); const refusal = suspended ? void 0 : timeout === 0 ? budget.limits.timeoutMs === 0 ? "subagent limit reached (timeoutMs 0)" : "subagent limit reached (no time left)" : budget.reserve(active); if (refusal !== void 0) return { subagentRunId: "", text: "", error: refusal }; active += 1; const sink = createSubagentSink(); const link = linkAbort(parent.abortSignal); let timedOut = false; const timer = timeout === void 0 ? void 0 : setTimeout(() => { timedOut = true; link.controller.abort(/* @__PURE__ */ new Error(`subagent timed out after ${timeout} ms`)); }, timeout); const childBinding = { ...bag.binding, budget: budget.child(timeout === void 0 ? void 0 : Date.now() + timeout) }; let subagentRunId = suspended?.subagentRunId ?? ""; let text = suspended?.text ?? ""; let error; let result; try { for await (const chunk of openAgentStream(entry, bag, { messages: parent.messagesFor?.(toolCallId) ?? parent.messages, abortSignal: link.controller.signal, threadId: parent.threadId, parentRunId: parent.runId, ...parent.interruptedRunId !== void 0 ? { interruptedRunId: parent.interruptedRunId } : {}, ...parent.parentSubagentRunId !== void 0 ? { parentSubagentRunId: parent.parentSubagentRunId } : {} }, sink, toolCallId, childBinding)) { if (chunk.type === SUBAGENT_STARTED && subagentRunId === "") subagentRunId = chunk.subagentRunId; if (chunk.type === EventType.TEXT_MESSAGE_CONTENT && "subagentRunId" in chunk && chunk.subagentRunId === subagentRunId) text += chunk.delta; if (chunk.type === SUBAGENT_ERROR && chunk.subagentRunId === subagentRunId) error = chunk.message; if (chunk.type === SUBAGENT_FINISHED && chunk.subagentRunId === subagentRunId && chunk.result !== void 0) result = chunk.result; toolContext?.[EMIT_STREAM_CHUNK]?.(chunk); } } finally { active -= 1; if (timer !== void 0) clearTimeout(timer); link.dispose(); } if (timedOut) error = `subagent timed out after ${timeout} ms`; parent.sink.usage.push(...sink.usage); if (sink.total) parent.sink.total = parent.sink.total ? addTokenUsage(parent.sink.total, sink.total) : sink.total; return { subagentRunId, text, ...result !== void 0 ? { result } : {}, ...error !== void 0 ? { error } : {}, ...sink.interrupts.length > 0 ? { interrupts: sink.interrupts } : {}, ...subagentRunId !== "" && parent.canContinue === true ? { keepRunId: true } : {} }; } /** * The stored child that a `sessionId` names, for `agent`. Throws a tool * error, also when the child ran under another agent. */ async function storedChild(sessionId, agent) { const load = parent.childLoader?.(); if (!load) throw new Error("sessionId needs a persistence store."); const stored = await load(sessionId); if (!stored) throw new Error(`Unknown sessionId "${sessionId}".`); if (stored.agent !== void 0 && stored.agent !== agent) throw new Error(`Session "${sessionId}" belongs to agent "${stored.agent}", not "${agent}".`); return { subagentRunId: sessionId, messages: stored.messages }; } const names = reservedFields(bag, parent.canContinue === true); return bag.agents.map((agent) => ({ name: agent.name, description: agent.description, inputSchema: agentToolInput(agent, names), [SUBAGENT_TOOL]: true, execute: async (args, context) => { const call = readCall(agent, args, names); const fields = { ...call.input !== void 0 && { input: call.input }, ...call.prompt !== void 0 && { prompt: call.prompt } }; const child = { name: agent.name, ...fields }; if (call.background) { if (call.sessionId !== void 0) throw new Error("background cannot continue a sessionId."); const start = bag.binding?.start; if (!start) throw new Error("background needs a host that runs children in the background."); const refusal = budget.reserve(active); if (refusal !== void 0) throw new Error(refusal); active += 1; let started; try { started = await start({ agent: agent.name, ...fields, ...context?.toolCallId !== void 0 && { parentToolCallId: context.toolCallId } }, { wake: true }); } catch (error) { active -= 1; budget.release(); throw error; } Promise.resolve(started.done).finally(() => { active -= 1; }); const { subagentRunId } = started; return { subagentRunId, text: "", result: { status: "started" }, keepRunId: true }; } const { sessionId } = call; const work = sessionId === void 0 ? runChild(child, context) : storedChild(sessionId, agent.name).then((continued) => runChild({ ...child, continued }, context)); const detach = context?.detach; if (!detach) return work; const moved = detach(work).then((text) => ({ subagentRunId: "", text })); return Promise.race([work, moved]); } })); } //#endregion export { SUBAGENT_ERROR, SUBAGENT_FINISHED, SUBAGENT_STARTED, assertNoReservedFields, collectNamedText, collectUsage, createSubagentId, createSubagentSink, createSyntheticSubagentTools, normalizeRouterPick, rebindInterrupts, spawnAgentStream, spawnNamedAgents, stepInput, subagentCallMessages, withChildUsage }; //# sourceMappingURL=spawn.js.map