agent-accelerator 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1220 -0
  3. package/SYSTEM_PROMPT.md +14 -0
  4. package/SYSTEM_PROMPT_AGENT.md +34 -0
  5. package/SYSTEM_PROMPT_TOOLS.md +9 -0
  6. package/bunfig.toml +2 -0
  7. package/package.json +59 -0
  8. package/src/agent/agent.ts +615 -0
  9. package/src/agent/context.ts +161 -0
  10. package/src/agent/delegation.ts +481 -0
  11. package/src/agent/loop.ts +569 -0
  12. package/src/agent/subagent.ts +83 -0
  13. package/src/ai-sdk/converters.ts +342 -0
  14. package/src/ai-sdk/errors.ts +122 -0
  15. package/src/ai-sdk/executor.ts +454 -0
  16. package/src/ai-sdk/index.ts +55 -0
  17. package/src/ai-sdk/model-provider.ts +303 -0
  18. package/src/ai-sdk/options.ts +306 -0
  19. package/src/ai-sdk/provider.ts +415 -0
  20. package/src/ai-sdk/registry.ts +416 -0
  21. package/src/data/README.md +84 -0
  22. package/src/index.ts +190 -0
  23. package/src/models/catalog-cache.ts +273 -0
  24. package/src/models/catalog.ts +503 -0
  25. package/src/streaming/event-stream.ts +211 -0
  26. package/src/streaming/sse-parser.ts +97 -0
  27. package/src/tokens/counter.ts +136 -0
  28. package/src/tools/executor.ts +365 -0
  29. package/src/tools/schema.ts +221 -0
  30. package/src/tools/tool.ts +101 -0
  31. package/src/types/agent.ts +87 -0
  32. package/src/types/core.ts +86 -0
  33. package/src/types/message.ts +115 -0
  34. package/src/types/model.ts +212 -0
  35. package/src/types/provider-payloads.ts +434 -0
  36. package/src/types/response.ts +158 -0
  37. package/src/types/tool.ts +61 -0
  38. package/src/utils/base64.ts +27 -0
  39. package/src/utils/cache.ts +146 -0
  40. package/src/utils/env.ts +78 -0
  41. package/src/utils/headers.ts +110 -0
  42. package/src/utils/media.ts +137 -0
  43. package/src/utils/serialization.ts +91 -0
  44. package/src/utils/session.ts +26 -0
  45. package/src/utils/thought-signature.ts +27 -0
  46. package/tsconfig.json +31 -0
@@ -0,0 +1,161 @@
1
+ import type { Message, ContentPart } from "../types/message.ts";
2
+ import type { ToolCallRecord, ToolResultRecord } from "../types/tool.ts";
3
+ import { safeStringify } from "../utils/serialization.ts";
4
+
5
+ const USER_PART_TYPES = new Set(["text", "image", "audio", "video", "file"]);
6
+
7
+ function fail(message: string): never {
8
+ throw new Error(`[Agent Accelerator] AgentContext: ${message}`);
9
+ }
10
+
11
+ /** Deep clone that preserves Uint8Array/ArrayBuffer media (structuredClone may be absent). */
12
+ function deepCloneValue<T>(value: T): T {
13
+ if (value instanceof Uint8Array) return new Uint8Array(value) as T;
14
+ if (value instanceof ArrayBuffer) return value.slice(0) as T;
15
+ if (Array.isArray(value)) return value.map((v) => deepCloneValue(v)) as T;
16
+ if (value !== null && typeof value === "object") {
17
+ const out: Record<string, unknown> = {};
18
+ for (const [k, v] of Object.entries(value)) out[k] = deepCloneValue(v);
19
+ return out as T;
20
+ }
21
+ return value;
22
+ }
23
+
24
+ export class AgentContext {
25
+ systemPrompt?: string;
26
+ messages: Message[] = [];
27
+ thoughtSignatures: string[] = [];
28
+ cachedContentId?: string;
29
+
30
+ constructor(systemPrompt?: string) {
31
+ this.systemPrompt = systemPrompt;
32
+ }
33
+
34
+ addUserMessage(content: string | ContentPart[]): void {
35
+ if (Array.isArray(content)) {
36
+ if (content.length === 0) return;
37
+ for (let i = 0; i < content.length; i++) {
38
+ const part = content[i] as ContentPart;
39
+ if (!part || typeof (part as { type?: unknown }).type !== "string") {
40
+ fail(`user content[${i}] is not a valid content part (missing string "type").`);
41
+ }
42
+ if (!USER_PART_TYPES.has(part.type)) {
43
+ fail(
44
+ `user content[${i}] has type "${(part as { type?: unknown }).type}" which is not valid in a user message. ` +
45
+ `Allowed: text, image, audio, video, file.`
46
+ );
47
+ }
48
+ }
49
+ } else if (typeof content !== "string") {
50
+ fail(`user content must be a string or an array of content parts, received ${typeof content}.`);
51
+ }
52
+ this.messages.push({
53
+ role: "user",
54
+ content,
55
+ });
56
+ }
57
+
58
+ addAssistantMessage(
59
+ text: string,
60
+ toolCalls?: ToolCallRecord[],
61
+ thinking?: string,
62
+ thoughtSignature?: string
63
+ ): void {
64
+ const parts: ContentPart[] = [];
65
+ const hasToolCalls = Boolean(toolCalls && toolCalls.length > 0);
66
+
67
+ const thinkingSig = !hasToolCalls && !text ? thoughtSignature : undefined;
68
+ const textSig = !hasToolCalls ? thoughtSignature : undefined;
69
+
70
+ if (thinking) {
71
+ parts.push({
72
+ type: "thinking",
73
+ thinking,
74
+ thoughtSignature: thinkingSig,
75
+ });
76
+ }
77
+
78
+ if (text) {
79
+ parts.push({
80
+ type: "text",
81
+ text,
82
+ thoughtSignature: textSig,
83
+ });
84
+ }
85
+
86
+ if (hasToolCalls && toolCalls) {
87
+ for (let i = 0; i < toolCalls.length; i++) {
88
+ const tc = toolCalls[i]!;
89
+ if (!tc.id || !tc.name) {
90
+ fail(`assistant toolCalls[${i}] is missing ${!tc.id ? '"id"' : '"name"'}; every tool call needs both.`);
91
+ }
92
+ parts.push({
93
+ type: "tool_call",
94
+ id: tc.id,
95
+ name: tc.name,
96
+ arguments: tc.arguments,
97
+ rawArguments: tc.rawArguments,
98
+ thoughtSignature: tc.thoughtSignature || (!thinkingSig && !textSig ? thoughtSignature : undefined),
99
+ });
100
+ }
101
+ }
102
+
103
+ // Never store a fully empty assistant turn: providers reject empty
104
+ // assistant content and it carries no signal for the next turn.
105
+ if (parts.length === 0 && !text) return;
106
+
107
+ this.messages.push({
108
+ role: "assistant",
109
+ content: parts.length > 0 ? parts : text,
110
+ thoughtSignature,
111
+ });
112
+
113
+ if (thoughtSignature) {
114
+ this.thoughtSignatures.push(thoughtSignature);
115
+ }
116
+ }
117
+
118
+ addToolResults(results: ToolResultRecord[]): void {
119
+ if (!results || results.length === 0) return;
120
+ for (let i = 0; i < results.length; i++) {
121
+ const res = results[i]!;
122
+ if (!res.id || !res.name) {
123
+ fail(`toolResults[${i}] is missing ${!res.id ? '"id"' : '"name"'}; results must match a tool call.`);
124
+ }
125
+ this.messages.push({
126
+ role: "tool",
127
+ name: res.name,
128
+ content: [
129
+ {
130
+ type: "tool_result",
131
+ id: res.id,
132
+ name: res.name,
133
+ result: res.result,
134
+ isError: res.isError,
135
+ },
136
+ ],
137
+ });
138
+ }
139
+ }
140
+
141
+ clone(): AgentContext {
142
+ const next = new AgentContext(this.systemPrompt);
143
+ // Preserve Uint8Array/ArrayBuffer media via structuredClone when available (M8 fix)
144
+ try {
145
+ if (typeof (globalThis as any).structuredClone === "function") {
146
+ next.messages = (globalThis as any).structuredClone(this.messages);
147
+ } else {
148
+ next.messages = deepCloneValue(this.messages);
149
+ }
150
+ } catch {
151
+ try {
152
+ next.messages = deepCloneValue(this.messages);
153
+ } catch {
154
+ next.messages = JSON.parse(safeStringify(this.messages));
155
+ }
156
+ }
157
+ next.thoughtSignatures = [...this.thoughtSignatures];
158
+ next.cachedContentId = this.cachedContentId;
159
+ return next;
160
+ }
161
+ }
@@ -0,0 +1,481 @@
1
+ import { z } from "zod";
2
+ import { tool } from "../tools/tool.ts";
3
+ import type { ToolDefinition } from "../types/tool.ts";
4
+ import type { SubAgentExecutionMetadata } from "../types/response.ts";
5
+ import type { Agent } from "./agent.ts";
6
+ import { resolveModel } from "../ai-sdk/registry.ts";
7
+
8
+ /** Task descriptor accepted by the automatic `spawn_subagents` tool.
9
+ *
10
+ * The Main Agent controls prompts (`name`/`role`/`instructions`/`task`), which
11
+ * worker tools to grant (`tools`), and — only when the developer sets
12
+ * `dynamicSubagents.timeout: -1` — each worker's `timeoutMs`. Model, reasoning
13
+ * level, and history are never LLM-choosable: workers are stateless and run on
14
+ * the developer-configured model.
15
+ */
16
+ export interface DynamicSubagentTask {
17
+ name: string;
18
+ role?: string;
19
+ instructions: string;
20
+ task: string;
21
+ /** Names of worker tools to grant this sub-agent. Must be a subset of the developer-configured `dynamicSubagents.tools` pool; unknown names are ignored. Omit for no tools. */
22
+ tools?: string[];
23
+ /** Per-worker timeout in ms. Honored ONLY when the developer sets `dynamicSubagents.timeout: -1`. Must be > 0, otherwise the worker runs with no limit. */
24
+ timeoutMs?: number;
25
+ }
26
+
27
+ function sanitizeXmlTag(raw: string): string {
28
+ let s = raw.toUpperCase().replace(/[^A-Z0-9_.-]/g, "_");
29
+ if (!/^[A-Z_]/.test(s)) s = `AGENT_${s}`;
30
+ s = s.replace(/[.-]/g, "_");
31
+ return s.slice(0, 64) || "SUBAGENT";
32
+ }
33
+ function sanitizeToolName(raw: string): string {
34
+ const tag = sanitizeXmlTag(raw);
35
+ return tag.toLowerCase().replace(/[^a-z0-9_-]/g, "_").slice(0, 64) || "sub_agent";
36
+ }
37
+
38
+ function escapeXml(text: string): string {
39
+ return text
40
+ .replace(/&/g, "&amp;")
41
+ .replace(/</g, "&lt;")
42
+ .replace(/>/g, "&gt;")
43
+ .replace(/"/g, "&quot;")
44
+ .replace(/'/g, "&apos;");
45
+ }
46
+
47
+ function createChildSessionId(parentId: string, tag: string): string {
48
+ let rand: string;
49
+ try {
50
+ if (typeof globalThis !== "undefined" && (globalThis as any).crypto?.randomUUID) {
51
+ rand = (globalThis as any).crypto.randomUUID().replace(/-/g, "").slice(0, 8);
52
+ } else {
53
+ // Node fallback - dynamic require avoids bundling issues
54
+ const nodeCrypto: any = (globalThis as any).process?.getBuiltinModule?.("node:crypto") ?? null;
55
+ if (nodeCrypto?.randomUUID) rand = nodeCrypto.randomUUID().replace(/-/g, "").slice(0, 8);
56
+ else rand = Math.random().toString(36).slice(2, 10);
57
+ }
58
+ } catch {
59
+ rand = Math.random().toString(36).slice(2, 10);
60
+ }
61
+ return `${parentId}-sub-${tag.toLowerCase().slice(0, 16)}-${rand}`;
62
+ }
63
+
64
+ function providerOf(modelStr: string | any | undefined): string | undefined {
65
+ if (!modelStr) return undefined;
66
+ try {
67
+ const m = typeof modelStr === "string" ? modelStr : (modelStr as any)?.model ?? (modelStr as any)?.id ?? String(modelStr);
68
+ return resolveModel(m as any).provider.id;
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Creates the built-in tool that spawns stateless dynamic sub-agents concurrently.
76
+ *
77
+ * Workers run on the developer-configured model with the developer-configured
78
+ * reasoning level. The Main Agent controls prompts, per-worker tool grants, and
79
+ * (when `dynamicSubagents.timeout: -1`) per-worker timeouts — nothing else.
80
+ *
81
+ * @example `new Agent({ model, dynamicSubagents: { enabled: true, maxSpawn: 4 } })`
82
+ */
83
+ export function createSubagentSpawnTool(parentAgent: Agent): ToolDefinition {
84
+ const dyn = (parentAgent as any).dynamicSubagents as
85
+ | { maxSpawn: number; tools: Record<string, unknown>; timeout: number }
86
+ | undefined;
87
+ const maxSpawn = dyn && Number.isFinite(dyn.maxSpawn) ? Math.max(1, Math.floor(dyn.maxSpawn)) : 4;
88
+ const poolNames = dyn ? Object.keys(dyn.tools ?? {}) : [];
89
+ const parentTimeout = dyn && Number.isFinite(dyn.timeout) ? Math.floor(dyn.timeout) : 0;
90
+ const timeoutNote =
91
+ parentTimeout === -1
92
+ ? "Set a per-task timeoutMs (ms, > 0) to time-limit a worker; omit it for no limit."
93
+ : parentTimeout === 0
94
+ ? "Workers run with no time limit; any per-task timeoutMs is ignored."
95
+ : `Every worker is limited to ${parentTimeout}ms; any per-task timeoutMs is ignored.`;
96
+ return tool({
97
+ name: "spawn_subagents",
98
+ description:
99
+ "Dynamically creates and runs specialized stateless sub-agents concurrently to handle sub-tasks. " +
100
+ "Use this whenever a query or task benefits from modular delegation, parallel research, multi-perspective analysis, or division of labor. " +
101
+ "All sub-agent outputs are aggregated and returned inside structured XML tags. " +
102
+ `You may spawn at most ${maxSpawn} sub-agent(s) per call — extra tasks beyond ${maxSpawn} are ignored. ` +
103
+ "Workers are stateless: each receives one task, returns its result, then shuts down; no conversation history is kept. " +
104
+ "You cannot choose worker models or reasoning levels. " +
105
+ (poolNames.length > 0
106
+ ? `Worker-available tools: ${poolNames.join(", ")}. Grant each worker ONLY the tools its task needs via the per-task tools list; omit it for no tools. `
107
+ : "No worker tools are available; omit the per-task tools list. ") +
108
+ timeoutNote + " " +
109
+ "Every entry in tasks MUST include all of: name (UPPER_SNAKE tag), instructions (system prompt for the worker), task (concrete assignment for the worker). " +
110
+ "Example: {\"tasks\": [{\"name\": \"HBM_PRICING_ANALYST\", \"role\": \"memory market analyst\", \"instructions\": \"You are a memory market analyst. Return sourced findings only.\", \"task\": \"Research HBM3E pricing, LTA structures, and supply constraints.\", \"tools\": [\"recent_news\"]}]}",
111
+ input: z.object({
112
+ tasks: z
113
+ .array(
114
+ z.object({
115
+ name: z
116
+ .string()
117
+ .optional()
118
+ .describe("Unique UPPER_SNAKE role tag dynamically derived from task (e.g. RESEARCH_ANALYST, MARKET_ANALYST, CODE_REVIEWER). Auto-generated when omitted."),
119
+ role: z
120
+ .string()
121
+ .optional()
122
+ .describe("Short persona / domain expertise for this sub-agent"),
123
+ instructions: z
124
+ .string()
125
+ .optional()
126
+ .describe("Personalized system prompt crafted by Main Agent to increase instruction following. Falls back to task when omitted."),
127
+ task: z
128
+ .string()
129
+ .optional()
130
+ .describe("Specific research/task prompt for this sub-agent. REQUIRED — falls back to instructions when omitted."),
131
+ tools: z
132
+ .array(z.string())
133
+ .optional()
134
+ .describe(
135
+ poolNames.length > 0
136
+ ? `Tool names to grant this worker (subset of: ${poolNames.join(", ")}). Unknown names are ignored. Omit for no tools.`
137
+ : "No worker tools are available; omit this field."
138
+ ),
139
+ timeoutMs: z
140
+ .number()
141
+ .optional()
142
+ .describe(
143
+ parentTimeout === -1
144
+ ? "Per-worker timeout in ms (> 0). Honored because the developer set timeout: -1. Omit for no limit."
145
+ : "Per-worker timeout is developer-controlled; this field is ignored."
146
+ ),
147
+ })
148
+ )
149
+ .min(1)
150
+ .max(maxSpawn)
151
+ .describe(`Array of sub-agents to spawn (max ${maxSpawn} per call) — each gets a personalized prompt and runs statelessly on the developer-configured model`),
152
+ }),
153
+ execute: async ({ tasks }, context) => {
154
+ if (!tasks || tasks.length === 0) {
155
+ return "Error: tasks array is empty. Provide at least one entry shaped like {\"name\": \"HBM_PRICING_ANALYST\", \"role\": \"memory market analyst\", \"instructions\": \"<system prompt>\", \"task\": \"<concrete assignment>\"}. Fix the arguments and call spawn_subagents again.";
156
+ }
157
+ const repaired = tasks.map((entry: Record<string, unknown>, index: number) => {
158
+ const rec = (entry ?? {}) as Record<string, unknown>;
159
+ const taskText =
160
+ (typeof rec["task"] === "string" && rec["task"].trim()) ||
161
+ (typeof rec["instructions"] === "string" && (rec["instructions"] as string).trim()) ||
162
+ "";
163
+ const instructionsText =
164
+ (typeof rec["instructions"] === "string" && (rec["instructions"] as string).trim()) ||
165
+ (typeof rec["task"] === "string" && (rec["task"] as string).trim()) ||
166
+ (typeof rec["role"] === "string" && (rec["role"] as string).trim()) ||
167
+ "";
168
+ const nameText =
169
+ (typeof rec["name"] === "string" && (rec["name"] as string).trim()) ||
170
+ `SUBAGENT_${index + 1}`;
171
+ const roleText =
172
+ typeof rec["role"] === "string" ? ((rec["role"] as string).trim() || undefined) : undefined;
173
+ const toolsList = Array.isArray(rec["tools"])
174
+ ? (rec["tools"] as unknown[]).filter((x): x is string => typeof x === "string" && x.trim().length > 0).map((x) => x.trim())
175
+ : undefined;
176
+ const timeoutRaw = rec["timeoutMs"];
177
+ const timeoutMs = typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw) ? Math.floor(timeoutRaw) : undefined;
178
+ return { ...rec, name: nameText, role: roleText, instructions: instructionsText, task: taskText, tools: toolsList, timeoutMs };
179
+ });
180
+ const invalid = repaired.findIndex((r) => !r.task);
181
+ if (invalid >= 0) {
182
+ const keys = Object.keys((tasks[invalid] ?? {}) as object).join(", ") || "(none)";
183
+ return (
184
+ `Error: tasks[${invalid}].task is missing and could not be inferred. ` +
185
+ `Received keys: [${keys}]. ` +
186
+ `Each tasks[] entry MUST include task (concrete assignment) plus instructions (system prompt) and name (UPPER_SNAKE tag). ` +
187
+ `Example: {\"name\": \"HBM_PRICING_ANALYST\", \"role\": \"memory market analyst\", \"instructions\": \"You are a memory market analyst.\", \"task\": \"Research HBM3E pricing.\"}. ` +
188
+ `Fix the entry and call spawn_subagents again.`
189
+ );
190
+ }
191
+ if (context?.signal?.aborted) {
192
+ throw new Error("Sub-agent spawning aborted");
193
+ }
194
+
195
+ const dynCfg = (parentAgent as any).dynamicSubagents as
196
+ | { model?: unknown; maxSpawn: number; thinkingLevel?: string; tools: Record<string, ToolDefinition>; timeout: number }
197
+ | undefined;
198
+ const effectiveMax = dynCfg && Number.isFinite(dynCfg.maxSpawn) ? Math.max(1, Math.floor(dynCfg.maxSpawn)) : 4;
199
+ const toolPool: Record<string, ToolDefinition> = (dynCfg?.tools as any) ?? {};
200
+ const cfgTimeout = dynCfg && Number.isFinite(dynCfg.timeout) ? Math.floor(dynCfg.timeout) : 0;
201
+
202
+ if (!parentAgent.subagentModel) {
203
+ throw new Error(
204
+ "[Agent Accelerator] Cannot spawn sub-agents: no sub-agent model is configured. " +
205
+ "Set dynamicSubagents.model in Agent config or set the SUB_AGENT_MODEL environment variable."
206
+ );
207
+ }
208
+
209
+ const { Agent: AgentClass } = await import("./agent.ts");
210
+
211
+ const subagentMetadataList: SubAgentExecutionMetadata[] = [];
212
+ const seenNames = new Set<string>();
213
+
214
+ // maxSpawn: trim extras safely — only the first N tasks run.
215
+ const limitedTasks = repaired.slice(0, effectiveMax);
216
+ const executedResults = await Promise.all(
217
+ limitedTasks.map(async (t) => {
218
+ const startTime = Date.now();
219
+ let sanitized = sanitizeXmlTag(t.name);
220
+ let deduped = sanitized;
221
+ let suffix = 1;
222
+ while (seenNames.has(deduped)) {
223
+ deduped = `${sanitized}_${suffix++}`;
224
+ }
225
+ seenNames.add(deduped);
226
+ const subagentName = deduped;
227
+
228
+ // Fixed developer-configured worker model — never LLM-choosable.
229
+ const chosenModel: any = parentAgent.subagentModel;
230
+ // Normalize chosenModel to string|ModelSpec handling
231
+ let chosenModelStrForProvider = typeof chosenModel === "string" ? chosenModel : (chosenModel as any)?.model ?? (chosenModel as any)?.id ?? String(chosenModel);
232
+ let attemptedProvider = providerOf(chosenModelStrForProvider);
233
+ // Determine creds: only reuse parent creds if provider matches parent provider, else let env resolve for cross-provider
234
+ const parentProvider = providerOf(parentAgent.modelStringOrSpec as any);
235
+ const targetProviderFinal = providerOf(chosenModelStrForProvider);
236
+ let apiKeyToUse: string | undefined;
237
+ let baseUrlToUse: string | undefined;
238
+ if (targetProviderFinal === parentProvider) {
239
+ apiKeyToUse = parentAgent.apiKey;
240
+ baseUrlToUse = parentAgent.baseUrl;
241
+ } else {
242
+ apiKeyToUse = undefined;
243
+ baseUrlToUse = undefined;
244
+ }
245
+
246
+ const runWithModel = async (modelToUse: any, apiKey: string | undefined, baseUrl: string | undefined) => {
247
+ if (context?.signal?.aborted) throw new Error("Aborted before spawn");
248
+ const childSessionId = createChildSessionId(parentAgent.sessionId, subagentName);
249
+ // Grant ONLY the Main Agent-selected subset from the developer pool. Unknown names are dropped.
250
+ const grantedTools: Record<string, ToolDefinition> = {};
251
+ for (const toolName of (t as any).tools ?? []) {
252
+ const pooled = toolPool[toolName];
253
+ if (pooled) grantedTools[toolName] = pooled;
254
+ }
255
+ // Timeout: >0 fixed for every worker; 0 = no limit; -1 = per-task timeoutMs from the Main Agent.
256
+ const workerTimeout = cfgTimeout > 0 ? cfgTimeout : cfgTimeout === -1 && (t as any).timeoutMs > 0 ? (t as any).timeoutMs : 0;
257
+ const subAgent = new AgentClass({
258
+ name: t.name,
259
+ description: t.role || `Sub-agent ${t.name}`,
260
+ instructions: t.instructions,
261
+ model: modelToUse,
262
+ apiKey,
263
+ baseUrl,
264
+ thinkingLevel: (dynCfg?.thinkingLevel as any) ?? parentAgent.thinkingConfig?.level,
265
+ sessionId: childSessionId,
266
+ tools: grantedTools,
267
+ // Stateless by design: one task in, one result out, then shut down. No history, no recursion.
268
+ stateless: true,
269
+ serviceTier: parentAgent.serviceTier,
270
+ headers: parentAgent.customHeaders,
271
+ maxTurns: parentAgent.maxTurns,
272
+ });
273
+ let abortListener: (() => void) | null = null;
274
+ const abortPromise = context?.signal
275
+ ? new Promise<never>((_, reject) => {
276
+ const onAbort = () => reject(new Error("Sub-agent aborted via parent signal"));
277
+ abortListener = onAbort;
278
+ if (context.signal!.aborted) reject(new Error("Sub-agent aborted via parent signal"));
279
+ else context.signal!.addEventListener("abort", onAbort, { once: true });
280
+ })
281
+ : null;
282
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
283
+ const timeoutPromise = workerTimeout > 0
284
+ ? new Promise<never>((_, reject) => {
285
+ timeoutId = setTimeout(() => reject(new Error(`Sub-agent ${subagentName} timed out after ${workerTimeout}ms`)), workerTimeout);
286
+ })
287
+ : null;
288
+ const taskPromise = subAgent.run(t.task, { signal: context?.signal } as any);
289
+ try {
290
+ const racers: Promise<unknown>[] = [taskPromise as unknown as Promise<unknown>];
291
+ if (abortPromise) racers.push(abortPromise);
292
+ if (timeoutPromise) racers.push(timeoutPromise);
293
+ const res = await Promise.race(racers);
294
+ return res;
295
+ } finally {
296
+ if (timeoutId) clearTimeout(timeoutId);
297
+ if (abortListener && context?.signal) {
298
+ try { context.signal.removeEventListener("abort", abortListener as any); } catch {}
299
+ }
300
+ }
301
+ };
302
+
303
+ try {
304
+ const res: any = await runWithModel(chosenModel, apiKeyToUse, baseUrlToUse);
305
+
306
+ const durationMs = Date.now() - startTime;
307
+ const rawText = (res as any).text;
308
+ const subagentText = typeof rawText === "string" && rawText.trim().length > 0
309
+ ? rawText
310
+ : `[Sub-agent ${subagentName} produced no output (empty response).]`;
311
+
312
+ const metadata: SubAgentExecutionMetadata = {
313
+ name: subagentName,
314
+ role: t.role,
315
+ task: t.task,
316
+ model: (res as any).model,
317
+ provider: (res as any).provider,
318
+ durationMs,
319
+ usage: (res as any).usage,
320
+ turns: (res as any).turns,
321
+ finishReason: (res as any).finishReason,
322
+ responseId: (res as any).responseId,
323
+ text: subagentText,
324
+ thinking: (res as any).thinking,
325
+ toolCalls: (res as any).toolCalls,
326
+ raw: (res as any).raw,
327
+ isError: false,
328
+ };
329
+ return { name: subagentName, text: subagentText, metadata };
330
+ } catch (err: any) {
331
+ const durationMs = Date.now() - startTime;
332
+ const errorMessage = err?.message || String(err);
333
+ const prov = attemptedProvider || providerOf(chosenModelStrForProvider) || providerOf(chosenModel as any) || "unknown";
334
+ const metadata: SubAgentExecutionMetadata = {
335
+ name: subagentName,
336
+ role: t.role,
337
+ task: t.task,
338
+ model: typeof chosenModel === "string" ? chosenModel : ((chosenModel as any)?.model ?? (chosenModel as any)?.id ?? "unknown"),
339
+ provider: prov,
340
+ durationMs,
341
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
342
+ turns: 0,
343
+ text: `Error executing sub-agent ${t.name}: ${errorMessage}`,
344
+ isError: true,
345
+ error: errorMessage,
346
+ };
347
+ return { name: subagentName, text: `Error executing sub-agent ${t.name}: ${errorMessage}`, metadata };
348
+ }
349
+ })
350
+ );
351
+
352
+ const xmlBlocks = executedResults.map((r) => {
353
+ subagentMetadataList.push(r.metadata);
354
+ const escaped = escapeXml(r.text);
355
+ const indentedText = escaped.split("\n").map((line) => ` ${line}`).join("\n");
356
+ return ` <${r.name}>\n${indentedText}\n </${r.name}>`;
357
+ });
358
+
359
+ const xmlOutput = `<SUB-AGENTS-RESPONSE>\n${xmlBlocks.join("\n")}\n</SUB-AGENTS-RESPONSE>`;
360
+
361
+ const resultPayload = {
362
+ xml: xmlOutput,
363
+ _subagentMetadata: subagentMetadataList,
364
+ toString() { return xmlOutput; },
365
+ };
366
+ return resultPayload;
367
+ },
368
+ });
369
+ }
370
+
371
+ /** Optional name/description wrapper for converting an Agent into a tool. */
372
+ export interface AgentAsToolTarget {
373
+ name?: string;
374
+ description?: string;
375
+ agent: Agent;
376
+ }
377
+
378
+ /**
379
+ * Converts one agent into a `{ task: string }` ToolDefinition.
380
+ *
381
+ * @example `const researchTool = researcher.asTool("research");`
382
+ */
383
+ export function agentToTool(
384
+ input: Agent | AgentAsToolTarget
385
+ ): ToolDefinition {
386
+ const agentInstance = "agent" in input ? input.agent : input;
387
+ const rawName =
388
+ ("name" in input && (input as any).name) ||
389
+ agentInstance.name ||
390
+ `sub_agent_${Math.random().toString(36).slice(2, 7)}`;
391
+ const name = sanitizeToolName(rawName);
392
+ const description =
393
+ ("description" in input && (input as any).description) ||
394
+ agentInstance.description ||
395
+ `Calls the sub-agent '${name}' to perform tasks.`;
396
+
397
+ return tool({
398
+ name,
399
+ description,
400
+ input: z.object({
401
+ task: z.string().describe("The detailed instruction or prompt to pass to this sub-agent."),
402
+ }),
403
+ execute: async ({ task }, ctx) => {
404
+ const startTime = Date.now();
405
+ try {
406
+ const parentSessionId = ctx?.sessionId || agentInstance.sessionId || "session";
407
+ const subSessionId = `${parentSessionId}-sub-${sanitizeToolName(name)}`;
408
+ const response: any = await agentInstance.run(task, { signal: ctx?.signal, sessionId: subSessionId } as any);
409
+ const durationMs = Date.now() - startTime;
410
+ const metadata: SubAgentExecutionMetadata = {
411
+ name,
412
+ task,
413
+ model: response.model,
414
+ provider: response.provider,
415
+ durationMs,
416
+ usage: response.usage,
417
+ turns: response.turns,
418
+ finishReason: response.finishReason,
419
+ responseId: response.responseId,
420
+ text: response.text,
421
+ thinking: response.thinking,
422
+ toolCalls: response.toolCalls,
423
+ raw: response.raw,
424
+ isError: false,
425
+ };
426
+ const wrapper: any = {
427
+ xml: response.text,
428
+ _subagentMetadata: [metadata],
429
+ toString() { return response.text; },
430
+ };
431
+ return wrapper;
432
+ } catch (err: any) {
433
+ const durationMs = Date.now() - startTime;
434
+ const msg = err?.message || String(err);
435
+ const metadata: SubAgentExecutionMetadata = {
436
+ name,
437
+ task,
438
+ model: "unknown",
439
+ provider: "unknown",
440
+ durationMs,
441
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
442
+ turns: 0,
443
+ text: `Error in ${name}: ${msg}`,
444
+ isError: true,
445
+ error: msg,
446
+ };
447
+ const wrapper: any = {
448
+ xml: `Error in ${name}: ${msg}`,
449
+ _subagentMetadata: [metadata],
450
+ toString() { return `Error in ${name}: ${msg}`; },
451
+ };
452
+ return wrapper;
453
+ }
454
+ },
455
+ });
456
+ }
457
+
458
+ /**
459
+ * Converts a list of agents into uniquely named delegation tools.
460
+ *
461
+ * @example `const tools = buildAgentTools([researcher, reviewer]);`
462
+ */
463
+ export function buildAgentTools(
464
+ agents?: (Agent | AgentAsToolTarget)[]
465
+ ): Record<string, ToolDefinition> {
466
+ if (!agents || agents.length === 0) return {};
467
+
468
+ const tools: Record<string, ToolDefinition> = {};
469
+ for (const item of agents) {
470
+ const t = agentToTool(item);
471
+ if (t.name) {
472
+ let finalName = t.name;
473
+ let n = 1;
474
+ while (tools[finalName]) {
475
+ finalName = `${t.name}_${n++}`;
476
+ }
477
+ tools[finalName] = { ...t, name: finalName };
478
+ }
479
+ }
480
+ return tools;
481
+ }