{"version":3,"file":"spawn-agent-host.d.ts","sourceRoot":"","sources":["../../../src/components/capabilities/spawn-agent-host.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAClG,OAAO,EACN,KAAK,GAAG,EAIR,KAAK,KAAK,EAGV,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAG/D,OAAO,KAAK,EAAE,oBAAoB,EAAiD,MAAM,kBAAkB,CAAC;AAY5G,MAAM,MAAM,sBAAsB,GAAG,WAAW,GAAG,QAAQ,GAAG,cAAc,GAAG,YAAY,CAAC;AAE5F,MAAM,WAAW,sBAAsB;IACtC,aAAa,EAAE,CAAC,CAAC;IACjB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,qCAAqC;IACrD,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,qBAAqB,CAAC,CAAC;IACnE,oFAAoF;IACpF,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IAC5B,sFAAsF;IACtF,YAAY,EAAE,MAAM,CAAC;IACrB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACpB;AAkND,wBAAgB,iCAAiC,CAChD,OAAO,EAAE,qCAAqC,GAC5C,oBAAoB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAqFlC","sourcesContent":["import { Buffer } from \"node:buffer\";\nimport { Agent, type AgentMessage, type AgentTool, type StreamFn } from \"@ch1nyzzz/pi-agent-core\";\nimport {\n\ttype Api,\n\ttype AssistantMessage,\n\ttype Context,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\ttype SimpleStreamOptions,\n\tstreamSimple,\n} from \"@ch1nyzzz/pi-ai/compat\";\nimport type { ModelRegistry } from \"@ch1nyzzz/pi-coding-agent\";\nimport { canonicalJson } from \"../../storage.ts\";\nimport type { EvoCapabilityResourceUsage } from \"./service.ts\";\nimport type { EvoRawSpawnAgentHost, EvoSpawnAgentHostResult, EvoSpawnAgentRequest } from \"./spawn-agent.ts\";\n\nconst EMPTY_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};\nconst DEFAULT_MAX_TRANSCRIPT_BYTES = 4 * 1024 * 1024;\n\nexport type EvoSpawnAgentRunStatus = \"completed\" | \"length\" | \"output-limit\" | \"turn-limit\";\n\nexport interface EvoSpawnAgentRunResult {\n\tschemaVersion: 1;\n\tstatus: EvoSpawnAgentRunStatus;\n\tmodel: { provider: string; id: string };\n\tturns: number;\n\tstopReason: string;\n\tmessages: unknown[];\n}\n\nexport interface EvoModelRegistrySpawnAgentHostOptions {\n\tmodelRegistry: Pick<ModelRegistry, \"find\" | \"getApiKeyAndHeaders\">;\n\t/** Trusted, pre-constructed tools. No extension or resource loader is consulted. */\n\ttools: readonly AgentTool[];\n\t/** Fixed host prompt. It is sent to the model but never returned to the component. */\n\tsystemPrompt: string;\n\t/** Hard maximum number of provider turns in one spawned run. */\n\tmaxTurns: number;\n\t/** Hard byte limit for the JSON transcript returned to the component. */\n\tmaxTranscriptBytes?: number;\n\t/** Injectable provider stream for focused tests. Defaults to pi-ai streamSimple. */\n\tstreamFn?: StreamFn;\n}\n\ninterface RunBudget {\n\tremainingOutputTokens: number;\n\tstartedTurns: number;\n\tstopStatus?: Extract<EvoSpawnAgentRunStatus, \"output-limit\" | \"turn-limit\">;\n\tusage: EvoCapabilityResourceUsage;\n}\n\nfunction positiveInteger(value: unknown, label: string): number {\n\tif (!Number.isSafeInteger(value) || (value as number) <= 0) {\n\t\tthrow new Error(`${label} must be a positive safe integer`);\n\t}\n\treturn value as number;\n}\n\nfunction nonNegativeNumber(value: unknown, label: string): number {\n\tif (typeof value !== \"number\" || !Number.isFinite(value) || value < 0) {\n\t\tthrow new Error(`${label} must be a non-negative finite number`);\n\t}\n\treturn value;\n}\n\nfunction checkedProduct(left: number, right: number, label: string): number {\n\tconst product = left * right;\n\tif (!Number.isSafeInteger(product)) throw new Error(`${label} exceeds the safe integer range`);\n\treturn product;\n}\n\nfunction checkedSum(left: number, right: number, label: string): number {\n\tconst sum = left + right;\n\tif (!Number.isSafeInteger(sum)) throw new Error(`${label} exceeds the safe integer range`);\n\treturn sum;\n}\n\nfunction modelRoute(route: string): { provider: string; id: string } | undefined {\n\tconst slash = route.indexOf(\"/\");\n\tif (slash <= 0 || slash === route.length - 1) return undefined;\n\treturn { provider: route.slice(0, slash), id: route.slice(slash + 1) };\n}\n\nfunction maximumRates(model: Model<Api>): { input: number; output: number } {\n\tconst rates = [model.cost, ...(model.cost.tiers ?? [])];\n\treturn {\n\t\tinput: Math.max(...rates.map((rate) => Math.max(rate.input * 2, rate.cacheWrite, rate.cacheRead))),\n\t\toutput: Math.max(...rates.map((rate) => rate.output)),\n\t};\n}\n\nfunction reservationFor(model: Model<Api>, request: EvoSpawnAgentRequest, maxTurns: number) {\n\tconst contextWindow = positiveInteger(model.contextWindow, \"spawn-agent model.contextWindow\");\n\tconst modelMaxTokens = positiveInteger(model.maxTokens, \"spawn-agent model.maxTokens\");\n\tconst maxOutputTokens = positiveInteger(request.maxOutputTokens, \"spawn-agent request.maxOutputTokens\");\n\tconst maximumRunOutput = checkedProduct(modelMaxTokens, maxTurns, \"spawn-agent maximum run output\");\n\tif (maxOutputTokens > maximumRunOutput) {\n\t\tthrow new Error(\"spawn-agent request.maxOutputTokens exceeds the host turn/model output bound\");\n\t}\n\tconst inputTokens = checkedProduct(contextWindow, maxTurns, \"spawn-agent input reservation\");\n\tconst totalTokens = checkedSum(inputTokens, maxOutputTokens, \"spawn-agent total reservation\");\n\tconst rates = maximumRates(model);\n\treturn {\n\t\tinputTokens,\n\t\toutputTokens: maxOutputTokens,\n\t\ttotalTokens,\n\t\tcostUsd: (inputTokens * rates.input + maxOutputTokens * rates.output) / 1_000_000,\n\t};\n}\n\nfunction validateMessageUsage(message: AssistantMessage): EvoCapabilityResourceUsage {\n\tconst usage = message.usage;\n\tconst inputTokens = checkedSum(\n\t\tcheckedSum(\n\t\t\tpositiveOrZeroInteger(usage.input, \"spawn-agent usage.input\"),\n\t\t\tpositiveOrZeroInteger(usage.cacheRead, \"spawn-agent usage.cacheRead\"),\n\t\t\t\"spawn-agent usage input\",\n\t\t),\n\t\tpositiveOrZeroInteger(usage.cacheWrite, \"spawn-agent usage.cacheWrite\"),\n\t\t\"spawn-agent usage input\",\n\t);\n\treturn {\n\t\tinputTokens,\n\t\toutputTokens: positiveOrZeroInteger(usage.output, \"spawn-agent usage.output\"),\n\t\ttotalTokens: positiveOrZeroInteger(usage.totalTokens, \"spawn-agent usage.totalTokens\"),\n\t\tcostUsd: nonNegativeNumber(usage.cost.total, \"spawn-agent usage.cost.total\"),\n\t};\n}\n\nfunction positiveOrZeroInteger(value: unknown, label: string): number {\n\tif (!Number.isSafeInteger(value) || (value as number) < 0) {\n\t\tthrow new Error(`${label} must be a non-negative safe integer`);\n\t}\n\treturn value as number;\n}\n\nfunction addUsage(target: EvoCapabilityResourceUsage, addition: EvoCapabilityResourceUsage): void {\n\ttarget.inputTokens = checkedSum(target.inputTokens, addition.inputTokens, \"spawn-agent aggregate input usage\");\n\ttarget.outputTokens = checkedSum(target.outputTokens, addition.outputTokens, \"spawn-agent aggregate output usage\");\n\ttarget.totalTokens = checkedSum(target.totalTokens, addition.totalTokens, \"spawn-agent aggregate total usage\");\n\ttarget.costUsd += addition.costUsd;\n\tif (!Number.isFinite(target.costUsd)) throw new Error(\"spawn-agent aggregate cost exceeds the finite range\");\n}\n\nfunction failureMessage(model: Model<Api>, error: unknown, aborted: boolean): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [{ type: \"text\", text: \"\" }],\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tusage: EMPTY_USAGE,\n\t\tstopReason: aborted ? \"aborted\" : \"error\",\n\t\terrorMessage: error instanceof Error ? error.message : String(error),\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nfunction awaitWithAbort<T>(pending: Promise<T>, signal: AbortSignal | undefined): Promise<T> {\n\tif (!signal) return pending;\n\tlet removeAbortListener = (): void => {};\n\tconst aborted = new Promise<never>((_resolve, reject) => {\n\t\tconst onAbort = (): void => reject(abortError(signal));\n\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\tremoveAbortListener = (): void => signal.removeEventListener(\"abort\", onAbort);\n\t\tif (signal.aborted) onAbort();\n\t});\n\t// Promise.race installs rejection handlers on both inputs. If abort wins,\n\t// a later auth-refresh rejection is therefore still observed.\n\treturn Promise.race([pending, aborted]).finally(removeAbortListener);\n}\n\nfunction createBoundedStream(\n\tmodelRegistry: Pick<ModelRegistry, \"getApiKeyAndHeaders\">,\n\tbaseStreamFn: StreamFn,\n\tbudget: RunBudget,\n\tmaxTurns: number,\n): StreamFn {\n\treturn (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => {\n\t\tconst outer = createAssistantMessageEventStream();\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tif (options?.signal?.aborted) throw options.signal.reason ?? new Error(\"spawn-agent request aborted\");\n\t\t\t\tif (budget.startedTurns >= maxTurns) throw new Error(\"spawn-agent turn budget is exhausted\");\n\t\t\t\tif (budget.remainingOutputTokens <= 0) throw new Error(\"spawn-agent output budget is exhausted\");\n\t\t\t\tbudget.startedTurns += 1;\n\t\t\t\tconst turnOutputLimit = Math.min(model.maxTokens, budget.remainingOutputTokens);\n\t\t\t\tconst auth = await awaitWithAbort(modelRegistry.getApiKeyAndHeaders(model), options?.signal);\n\t\t\t\tif (options?.signal?.aborted) throw abortError(options.signal);\n\t\t\t\tif (!auth.ok) throw new Error(auth.error);\n\t\t\t\tconst source = await baseStreamFn(model, context, {\n\t\t\t\t\t...options,\n\t\t\t\t\tmaxTokens: turnOutputLimit,\n\t\t\t\t\tmaxRetries: 0,\n\t\t\t\t\tmaxRetryDelayMs: 0,\n\t\t\t\t\tapiKey: auth.apiKey,\n\t\t\t\t\theaders: auth.headers,\n\t\t\t\t\tenv: auth.env,\n\t\t\t\t});\n\t\t\t\tfor await (const event of source) {\n\t\t\t\t\tif (event.type === \"done\" || event.type === \"error\") {\n\t\t\t\t\t\tconst message = event.type === \"done\" ? event.message : event.error;\n\t\t\t\t\t\tconst usage = validateMessageUsage(message);\n\t\t\t\t\t\taddUsage(budget.usage, usage);\n\t\t\t\t\t\tbudget.remainingOutputTokens -= usage.outputTokens;\n\t\t\t\t\t\tif (budget.remainingOutputTokens < 0 || usage.outputTokens > turnOutputLimit) {\n\t\t\t\t\t\t\tthrow new Error(\"spawn-agent provider exceeded the hard output token limit\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\touter.push(event);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst aborted = options?.signal?.aborted === true;\n\t\t\t\tconst message = failureMessage(model, aborted ? options?.signal?.reason : error, aborted);\n\t\t\t\touter.push({ type: \"error\", reason: aborted ? \"aborted\" : \"error\", error: message });\n\t\t\t}\n\t\t})();\n\t\treturn outer;\n\t};\n}\n\nfunction requestedTools(request: EvoSpawnAgentRequest, available: ReadonlyMap<string, AgentTool>): AgentTool[] {\n\tif (request.tools.includes(\"agent\")) throw new Error(\"spawn-agent child tools must not include agent\");\n\treturn request.tools.map((name) => {\n\t\tconst tool = available.get(name);\n\t\tif (!tool) throw new Error(`spawn-agent tool is unavailable: ${name}`);\n\t\treturn tool;\n\t});\n}\n\nfunction lastAssistantMessage(messages: readonly AgentMessage[]): AssistantMessage | undefined {\n\tfor (let index = messages.length - 1; index >= 0; index -= 1) {\n\t\tconst message = messages[index];\n\t\tif (message?.role === \"assistant\") return message as AssistantMessage;\n\t}\n\treturn undefined;\n}\n\nfunction jsonMessages(messages: readonly AgentMessage[], maxBytes: number): unknown[] {\n\tconst json = canonicalJson(messages);\n\tif (Buffer.byteLength(json, \"utf8\") > maxBytes) {\n\t\tthrow new Error(\"spawn-agent transcript exceeds the configured byte limit\");\n\t}\n\tconst value = JSON.parse(json) as unknown;\n\tif (!Array.isArray(value)) throw new Error(\"spawn-agent transcript did not serialize to an array\");\n\treturn value;\n}\n\nfunction abortError(signal: AbortSignal): Error {\n\treturn signal.reason instanceof Error ? signal.reason : new Error(\"spawn-agent request aborted\");\n}\n\nexport function createModelRegistrySpawnAgentHost(\n\toptions: EvoModelRegistrySpawnAgentHostOptions,\n): EvoRawSpawnAgentHost<Model<Api>> {\n\tconst maxTurns = positiveInteger(options.maxTurns, \"spawn-agent host maxTurns\");\n\tconst maxTranscriptBytes =\n\t\toptions.maxTranscriptBytes === undefined\n\t\t\t? DEFAULT_MAX_TRANSCRIPT_BYTES\n\t\t\t: positiveInteger(options.maxTranscriptBytes, \"spawn-agent host maxTranscriptBytes\");\n\tif (!options.systemPrompt) throw new Error(\"spawn-agent host systemPrompt must be non-empty\");\n\tconst toolMap = new Map<string, AgentTool>();\n\tfor (const tool of options.tools) {\n\t\tif (!tool.name) throw new Error(\"spawn-agent host tool name must be non-empty\");\n\t\tif (toolMap.has(tool.name)) throw new Error(`spawn-agent host has duplicate tool: ${tool.name}`);\n\t\ttoolMap.set(tool.name, tool);\n\t}\n\tconst baseStreamFn = options.streamFn ?? streamSimple;\n\n\treturn {\n\t\tresolveModel(route) {\n\t\t\tconst parsed = modelRoute(route);\n\t\t\treturn parsed ? options.modelRegistry.find(parsed.provider, parsed.id) : undefined;\n\t\t},\n\t\testimateReservation(model, request) {\n\t\t\treturn reservationFor(model, request, maxTurns);\n\t\t},\n\t\tasync runAgent(model, request, runOptions): Promise<EvoSpawnAgentHostResult> {\n\t\t\tif (runOptions.signal.aborted) throw abortError(runOptions.signal);\n\t\t\tconst tools = requestedTools(request, toolMap);\n\t\t\treservationFor(model, request, maxTurns);\n\t\t\tconst budget: RunBudget = {\n\t\t\t\tremainingOutputTokens: request.maxOutputTokens,\n\t\t\t\tstartedTurns: 0,\n\t\t\t\tusage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, costUsd: 0 },\n\t\t\t};\n\t\t\tconst agent = new Agent({\n\t\t\t\tinitialState: {\n\t\t\t\t\tsystemPrompt: options.systemPrompt,\n\t\t\t\t\tmodel,\n\t\t\t\t\tthinkingLevel: request.reasoning ?? \"off\",\n\t\t\t\t\ttools,\n\t\t\t\t},\n\t\t\t\tstreamFn: createBoundedStream(options.modelRegistry, baseStreamFn, budget, maxTurns),\n\t\t\t\ttoolExecution: \"sequential\",\n\t\t\t\tprepareNextTurnWithContext(context) {\n\t\t\t\t\tconst hasToolCalls =\n\t\t\t\t\t\tcontext.message.role === \"assistant\" &&\n\t\t\t\t\t\tcontext.message.content.some((content) => content.type === \"toolCall\");\n\t\t\t\t\tif (!hasToolCalls) return undefined;\n\t\t\t\t\tif (budget.remainingOutputTokens <= 0) {\n\t\t\t\t\t\tbudget.stopStatus = \"output-limit\";\n\t\t\t\t\t\treturn { stop: true };\n\t\t\t\t\t}\n\t\t\t\t\tif (budget.startedTurns >= maxTurns) {\n\t\t\t\t\t\tbudget.stopStatus = \"turn-limit\";\n\t\t\t\t\t\treturn { stop: true };\n\t\t\t\t\t}\n\t\t\t\t\treturn undefined;\n\t\t\t\t},\n\t\t\t});\n\t\t\tconst abort = (): void => agent.abort();\n\t\t\trunOptions.signal.addEventListener(\"abort\", abort, { once: true });\n\t\t\ttry {\n\t\t\t\tawait agent.prompt(request.prompt);\n\t\t\t\tif (runOptions.signal.aborted) throw abortError(runOptions.signal);\n\t\t\t\tconst finalMessage = lastAssistantMessage(agent.state.messages);\n\t\t\t\tif (!finalMessage) throw new Error(\"spawn-agent run completed without an assistant message\");\n\t\t\t\tif (finalMessage.stopReason === \"error\" || finalMessage.stopReason === \"aborted\") {\n\t\t\t\t\tthrow new Error(finalMessage.errorMessage ?? `spawn-agent run ended with ${finalMessage.stopReason}`);\n\t\t\t\t}\n\t\t\t\tconst status: EvoSpawnAgentRunStatus =\n\t\t\t\t\tbudget.stopStatus ?? (finalMessage.stopReason === \"length\" ? \"length\" : \"completed\");\n\t\t\t\treturn {\n\t\t\t\t\tresult: {\n\t\t\t\t\t\tschemaVersion: 1,\n\t\t\t\t\t\tstatus,\n\t\t\t\t\t\tmodel: { provider: model.provider, id: model.id },\n\t\t\t\t\t\tturns: budget.startedTurns,\n\t\t\t\t\t\tstopReason: finalMessage.stopReason,\n\t\t\t\t\t\tmessages: jsonMessages(agent.state.messages, maxTranscriptBytes),\n\t\t\t\t\t} satisfies EvoSpawnAgentRunResult,\n\t\t\t\t\tusage: budget.usage,\n\t\t\t\t};\n\t\t\t} finally {\n\t\t\t\trunOptions.signal.removeEventListener(\"abort\", abort);\n\t\t\t}\n\t\t},\n\t};\n}\n"]}