agent-accelerator 0.5.3 → 0.5.5

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.
package/src/agent/loop.ts CHANGED
@@ -71,6 +71,8 @@ export interface AgentLoopConfig {
71
71
  enforceBudget?: (usage?: TokenUsage) => void;
72
72
  /** Called after every turn for durable persistence checkpoints (never fails a turn). */
73
73
  onProgress?: () => void;
74
+ /** Called once per model call this agent made itself (never for its workers), with the provider's usage and the catalog spec: the source of `usageReport()`. */
75
+ onTurnUsage?: (usage: TokenUsage, spec: ModelSpec | undefined) => void;
74
76
  /** Called the moment each tool call finishes, before the rest of its batch (the crash-proof record of finished work). */
75
77
  onToolResult?: (result: ToolResultRecord) => void;
76
78
  /** Opens the log file of one tool call (see `ToolLogs`); unset when the Agent does not persist to a folder. */
@@ -340,9 +342,11 @@ function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): L
340
342
  const deadlineMs = num(raw.deadlineMs) ?? Infinity;
341
343
  const maxToolCallsPerRun = num(raw.maxToolCallsPerRun) ?? Infinity;
342
344
  const maxTotalTokens = num(raw.maxTotalTokens) ?? Infinity;
343
- const requestTimeoutMs = num(raw.requestTimeoutMs) ?? 300_000;
344
- const firstByteTimeoutMs = num(raw.firstByteTimeoutMs) ?? 120_000;
345
- const streamIdleTimeoutMs = num(raw.streamIdleTimeoutMs) ?? 300_000;
345
+ // A network timeout: `0` or `Infinity` turns it off, any other number is milliseconds, anything else is unset (the default applies).
346
+ const timeout = (v: unknown): number | undefined => (typeof v === "number" && !Number.isNaN(v) ? v : undefined);
347
+ const requestTimeoutMs = timeout(raw.requestTimeoutMs) ?? 300_000;
348
+ const firstByteTimeoutMs = timeout(raw.firstByteTimeoutMs) ?? 120_000;
349
+ const streamIdleTimeoutMs = timeout(raw.streamIdleTimeoutMs) ?? 300_000;
346
350
  return { maxTurns, deadlineMs, maxToolCallsPerRun, maxTotalTokens, requestTimeoutMs, firstByteTimeoutMs, streamIdleTimeoutMs };
347
351
  }
348
352
 
@@ -411,16 +415,14 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
411
415
  }
412
416
  }
413
417
 
414
- /** Compat wrapper around the one cost model (`priceUsage` in session/store.ts, Q-36).
415
- * Prefers provider-reported totals, else catalog pricing with integer
416
- * micro-USD accumulation. Returns undefined when unknown (never a 0 object). */
417
- export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec): TokenUsage["cost"] {
418
+ /** Prices one usage snapshot with the one cost model (`priceUsage`): provider-reported total first, else catalog pricing scaled by `serviceTier`; undefined when unknown. In: loop turns and `accumulateUsage`, out: `usage.cost`. */
419
+ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serviceTier?: string): TokenUsage["cost"] {
418
420
  try {
419
421
  if (usage.cost && usage.cost.totalCost !== undefined && usage.cost.totalCost > 0) {
420
422
  return usage.cost;
421
423
  }
422
424
  if (!spec) return usage.cost;
423
- const priced = priceUsage(usage, spec);
425
+ const priced = priceUsage(usage, spec, serviceTier ? { serviceTier } : undefined);
424
426
  return priced
425
427
  ? {
426
428
  inputCost: priced.inputCost,
@@ -448,7 +450,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec): Tok
448
450
  // This function never mutates `source` (Q-35): the priced cost is cloned
449
451
  // into a local before folding into the target.
450
452
  // ---------------------------------------------------------------------------
451
- export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec): void {
453
+ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec, serviceTier?: string): void {
452
454
  target.inputTokens += source.inputTokens || 0;
453
455
  target.outputTokens += source.outputTokens || 0;
454
456
  target.totalTokens += source.totalTokens || 0;
@@ -475,7 +477,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
475
477
  });
476
478
  }
477
479
 
478
- const priced = computeCostFromPricing(source, spec);
480
+ const priced = computeCostFromPricing(source, spec, serviceTier);
479
481
  // Q-35: pure — read `source` only; never assign back to `source.cost`.
480
482
  // Clone the priced cost so later target folds cannot alias source state.
481
483
  const sourceCost = priced ? { ...priced } : undefined;
@@ -799,8 +801,9 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
799
801
  }
800
802
  }
801
803
 
802
- accumulateUsage(accumulatedUsage, genResult.usage, spec);
803
- config.enforceBudget?.({ ...genResult.usage, cost: computeCostFromPricing(genResult.usage, spec) });
804
+ accumulateUsage(accumulatedUsage, genResult.usage, spec, options?.serviceTier);
805
+ try { config.onTurnUsage?.(genResult.usage, spec); } catch {}
806
+ config.enforceBudget?.({ ...genResult.usage, cost: computeCostFromPricing(genResult.usage, spec, options?.serviceTier) });
804
807
  checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
805
808
 
806
809
  // Record assistant turn in context
@@ -1252,8 +1255,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1252
1255
  }
1253
1256
  }
1254
1257
 
1255
- accumulateUsage(accumulatedUsage, turnResponse.usage, spec2);
1256
- config.enforceBudget?.({ ...turnResponse.usage, cost: computeCostFromPricing(turnResponse.usage, spec2) });
1258
+ accumulateUsage(accumulatedUsage, turnResponse.usage, spec2, options?.serviceTier);
1259
+ try { config.onTurnUsage?.(turnResponse.usage, spec2); } catch {}
1260
+ config.enforceBudget?.({ ...turnResponse.usage, cost: computeCostFromPricing(turnResponse.usage, spec2, options?.serviceTier) });
1257
1261
  checkTokenLimit(accumulatedUsage, streamLimits.maxTotalTokens);
1258
1262
 
1259
1263
  context.addAssistantMessage(
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The model-facing `subagents` tool: one flat tool with a `command` field (`spawn | message | close | list`), a thin
3
+ * wrapper over `agent.workers`. The tool manual lives in `subagents.md`, not in code. Results are plain text with one
4
+ * header line naming the worker; the branded envelope under them carries usage and lineage to the loop.
5
+ */
6
+ import { readFileSync } from "node:fs";
7
+ import { z } from "zod";
8
+ import type { Agent } from "./agent.ts";
9
+ import { tool } from "../tools/tool.ts";
10
+ import { makeSubagentResult } from "../tools/subagent-result.ts";
11
+ import { ValidationError } from "../types/errors.ts";
12
+ import type { ToolDefinition } from "../types/tool.ts";
13
+ import type { WorkerReply } from "./workers.ts";
14
+
15
+ const COMMANDS = ["spawn", "message", "close", "list"] as const;
16
+
17
+ let manual: string | undefined;
18
+ /** Read on first use (not at import), so a program that never enables workers never touches the file. */
19
+ function loadManual(): string {
20
+ return (manual ??= readFileSync(new URL("./subagents.md", import.meta.url), "utf8"));
21
+ }
22
+
23
+ // One worker reply as the model sees it: a header line naming the worker, then its text; in: a WorkerReply, out: the branded result.
24
+ function asResult(reply: WorkerReply): ReturnType<typeof makeSubagentResult> {
25
+ const text = reply.error ? `[worker ${reply.id}] failed: ${reply.error}` : `[worker ${reply.id}] replied:\n${reply.text}`;
26
+ return makeSubagentResult(text, [reply.metadata]);
27
+ }
28
+
29
+ /** Builds the `subagents` tool for `agent`; its description is the manual with the worker pool and the open-worker limit filled in. */
30
+ export function createSubagentsTool(agent: Agent): ToolDefinition {
31
+ const poolNames = Object.keys(agent.dynamicSubagents?.tools ?? {});
32
+ const max = agent.dynamicSubagents?.maxSpawn ?? Infinity;
33
+ return tool({
34
+ name: "subagents",
35
+ description: loadManual()
36
+ .replace("{{LIMIT}}", Number.isFinite(max) ? `You can have at most ${max} open workers. Close the ones you are done with.` : "There is no limit on open workers. Close the ones you are done with: each one keeps its whole conversation.")
37
+ .replace("{{TOOLS}}", poolNames.length > 0 ? poolNames.join(", ") : "none (omit tools)"),
38
+ input: z.object({
39
+ command: z.enum(COMMANDS),
40
+ id: z.string().optional().describe("The worker's full id (message, close)."),
41
+ name: z.string().optional().describe('Short label for a new worker, e.g. "researcher" (spawn).'),
42
+ role: z.string().optional().describe("Short persona for the worker (spawn)."),
43
+ instructions: z.string().optional().describe("The worker's own system prompt (spawn)."),
44
+ task: z.string().optional().describe("The assignment (spawn) or the next message to the worker (message)."),
45
+ tools: z.array(z.string()).optional().describe(`Worker tools to grant, from: ${poolNames.join(", ") || "none"} (spawn).`),
46
+ history: z.number().int().min(0).optional().describe("How many of the first finished turns of this conversation to copy into the worker; 0 or omitted = none (spawn)."),
47
+ }),
48
+ execute: async (a, ctx) => {
49
+ const opts = { signal: ctx.signal, callId: ctx.toolCallId, onSubagentEvent: ctx.onSubagentEvent };
50
+ const need = (field: "id" | "name" | "task"): string => {
51
+ const value = a[field];
52
+ if (!value?.trim()) {
53
+ const example = a.command === "spawn" ? { command: "spawn", name: "researcher", task: "Do the work." } : { command: a.command, id: "researcher-1a2b3c4d", task: "Next question." };
54
+ throw new ValidationError(`${a.command} needs "${field}". Example: ${JSON.stringify(example)}`);
55
+ }
56
+ return value;
57
+ };
58
+ try {
59
+ switch (a.command) {
60
+ case "spawn":
61
+ return asResult(
62
+ await agent.workers.spawn({ name: need("name"), role: a.role, instructions: a.instructions, task: need("task"), tools: a.tools, history: a.history }, opts)
63
+ );
64
+ case "message":
65
+ return asResult(await agent.workers.message(need("id"), need("task"), opts));
66
+ case "close":
67
+ return `Worker ${agent.workers.close(need("id")).id} closed.`;
68
+ case "list":
69
+ return describeWorkers(agent);
70
+ }
71
+ } catch (err) {
72
+ // A bad request is feedback for the model; anything else (budget, configuration) is a real failure.
73
+ if (err instanceof ValidationError) return `Error: ${err.message}`;
74
+ throw err;
75
+ }
76
+ },
77
+ });
78
+ }
79
+
80
+ // The `list` answer: the open workers and the conversation turns `history` can copy; in: the agent's registry, out: plain text for the model.
81
+ function describeWorkers(agent: Agent): string {
82
+ const workers = agent.workers.list();
83
+ const turns = agent.workers.turns();
84
+ const open = workers.length === 0 ? "none" : workers.map((w) => `- ${w.id} (${w.name}${w.role ? `, ${w.role}` : ""}) tools: ${w.tools.join(", ") || "none"}${w.busy ? " [busy]" : ""}`).join("\n");
85
+ const copyable = turns.length === 0 ? "none yet" : turns.map((t) => `${t.n}. ${t.prompt}`).join("\n");
86
+ return `Open workers:\n${open}\n\nFinished turns of this conversation (history: N copies turns 1 to N):\n${copyable}`;
87
+ }
@@ -0,0 +1,31 @@
1
+ Starts helpers ("workers") that work on a task for you and can be asked again later. A worker is a separate agent with its own instructions, its own tools and its own memory: it knows only what you give it (plus the earlier conversation you choose to copy). Use workers for parallel research, a second opinion, or a long side task you do not want in your own context.
2
+
3
+ COMMANDS (one tool, the `command` field)
4
+ - spawn: start a worker. Needs `name` and `task`. Optional: `instructions` (the worker's own system prompt), `role`, `tools`, `history`. It waits for the worker's first reply and gives you the worker's `id`.
5
+ - message: send `task` to the open worker `id`. The worker remembers everything said to it before. It waits for the reply.
6
+ - close: end the worker `id` when you no longer need it. A closed worker cannot be messaged again.
7
+ - list: your open workers, plus the numbered turns of this conversation that `history` can copy.
8
+
9
+ FIELDS
10
+ - name: a short label such as "researcher". The SDK adds a suffix; use the full `id` it returns ("researcher-1a2b3c4d") in message and close.
11
+ - instructions: the worker's own system prompt: its role, rules and the format you want back.
12
+ - task: for spawn, the first assignment; for message, the next message to that worker.
13
+ - tools: names of worker tools to grant. Available: {{TOOLS}}. Grant only what the task needs.
14
+ - history: a number N (default 0 = none). Starts the worker with a copy of the first N finished turns of this conversation, in order. Call list to see the turns and their numbers. The turn you are in right now is never copied: restate what you need in `task`.
15
+
16
+ RESULTS
17
+ Every spawn and message answers with one header line and the worker's text: `[worker <id>] replied:` or `[worker <id>] failed: <reason>`. A failed worker stays open: fix the problem and message it again, or close it.
18
+
19
+ EXAMPLES
20
+ Start a researcher:
21
+ {"command":"spawn","name":"researcher","instructions":"You research databases. Answer in 5 bullets with sources.","task":"Compare SQLite and Postgres for a small SaaS."}
22
+ Ask it a follow-up:
23
+ {"command":"message","id":"researcher-1a2b3c4d","task":"Now focus on write-heavy workloads."}
24
+
25
+ RULES
26
+ - Several workers at once: make several spawn calls in the same turn. They run in parallel.
27
+ - A worker handles one call at a time. Wait for its reply before messaging it again.
28
+ - {{LIMIT}}
29
+ - A worker starts blank: put everything it needs in `instructions` and `task`, or copy earlier turns with `history`.
30
+ - Worker answers are written by another model. Check them before you rely on them.
31
+ - Never put secrets in `instructions` or `task`.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Turn grouping: a "turn" is one finished run (the user prompt plus the whole answer to it: text, thinking, tool calls
3
+ * and results), found through the run marks the Agent keeps. A worker that starts from a copy of its parent's history
4
+ * gets whole finished turns only, so a prompt never reaches it without its answer. Pure functions, no Agent import.
5
+ */
6
+ import type { Message, ContentPart } from "../types/message.ts";
7
+ import type { SessionRunMark } from "../session/store.ts";
8
+ import { ValidationError } from "../types/errors.ts";
9
+ import { previewPrompt } from "../utils/session.ts";
10
+ import { deepCloneValue } from "./context.ts";
11
+
12
+ /** One finished turn: its 1-based number (counting finished turns only), a prompt preview, and its messages `[from, to)`. */
13
+ export interface Turn {
14
+ n: number;
15
+ prompt: string;
16
+ from: number;
17
+ to: number;
18
+ }
19
+
20
+ /** Longest prompt preview shown to a model in a turn list. */
21
+ const PREVIEW_CHARS = 200;
22
+
23
+ // A run counts as finished when it ended with its answer (`done`) or with a tool's `ctx.stop` (`stopped`); in: a run mark, out: finishedTurns.
24
+ function isFinished(mark: SessionRunMark): boolean {
25
+ return mark.end === "done" || mark.end === "stopped";
26
+ }
27
+
28
+ /** The finished turns of a conversation, oldest first. The running turn and failed, aborted or abandoned runs are not turns. */
29
+ export function finishedTurns(messages: Message[], marks: SessionRunMark[]): Turn[] {
30
+ const sorted = marks.filter((m) => m.i >= 0 && m.i < messages.length).sort((a, b) => a.i - b.i);
31
+ const turns: Turn[] = [];
32
+ sorted.forEach((mark, k) => {
33
+ if (!isFinished(mark)) return;
34
+ turns.push({
35
+ n: turns.length + 1,
36
+ prompt: previewPrompt(messages[mark.i]!.content as string | ContentPart[]).slice(0, PREVIEW_CHARS),
37
+ from: mark.i,
38
+ to: sorted[k + 1]?.i ?? messages.length,
39
+ });
40
+ });
41
+ return turns;
42
+ }
43
+
44
+ /**
45
+ * A deep copy of the first `n` finished turns (plus anything before the first run mark), in order. `0` gives an empty
46
+ * history. Throws a `ValidationError` naming how many finished turns exist when `n` is not a whole number in range.
47
+ */
48
+ export function historyUpTo(messages: Message[], marks: SessionRunMark[], n: number): Message[] {
49
+ const turns = finishedTurns(messages, marks);
50
+ if (!Number.isInteger(n) || n < 0 || n > turns.length) {
51
+ throw new ValidationError(
52
+ `history: ${JSON.stringify(n)} is not valid; use "none" or a whole number from 1 to ${turns.length} (the number of finished turns).`
53
+ );
54
+ }
55
+ if (n === 0) return [];
56
+ const first = marks.filter((m) => m.i >= 0 && m.i < messages.length).reduce((lo, m) => Math.min(lo, m.i), messages.length);
57
+ const kept = [...messages.slice(0, first)];
58
+ for (const turn of turns.slice(0, n)) kept.push(...messages.slice(turn.from, turn.to));
59
+ return deepCloneValue(kept);
60
+ }
@@ -42,11 +42,6 @@ export function sanitizeToolName(raw: string): string {
42
42
  return tag.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").slice(0, 64) || "sub-agent";
43
43
  }
44
44
 
45
- /** Stable worker ids: `{sanitizedName}-{32hex}` display id + raw tracking id. */
46
- export function makeWorkerIds(sanitizedName: string, trackingId: string): { trackingId: string; displayId: string } {
47
- return { trackingId, displayId: `${sanitizedName}-${trackingId}` };
48
- }
49
-
50
45
  // ---------------------------------------------------------------------------
51
46
  // Shared Agent / SubAgent config normalization (behavior-preserving)
52
47
  // ---------------------------------------------------------------------------
@@ -176,9 +171,11 @@ export function normalizeToolMap(
176
171
  // Shared dynamic-worker helpers (pure, no store access)
177
172
  // ---------------------------------------------------------------------------
178
173
 
179
- /** Resolves per-worker timeout: `>0` fixed, `0` unlimited, `-1` per-task. */
180
- export function normalizeWorkerTimeoutValue(cfgTimeout: number, taskTimeoutMs: unknown): number {
181
- return cfgTimeout > 0 ? cfgTimeout : cfgTimeout === -1 && (taskTimeoutMs as number) > 0 ? Math.floor(taskTimeoutMs as number) : 0;
174
+ // The time limit of one scheduled run in ms, 0 = none: the action's own `timeoutMs`, never above the developer's `workerTimeoutMs` (Infinity = none), which is also the limit when the action sets none; in: Agent.dynamicSubagents.workerTimeoutMs + the saved action, out: Scheduler.attempt.
175
+ export function scheduledRunTimeoutMs(cap: number, taskTimeoutMs: number | undefined): number {
176
+ const own = typeof taskTimeoutMs === "number" && taskTimeoutMs > 0 ? Math.floor(taskTimeoutMs) : Infinity;
177
+ const ms = Math.min(own, cap);
178
+ return Number.isFinite(ms) ? ms : 0;
182
179
  }
183
180
 
184
181
  /**
@@ -203,11 +200,6 @@ export function grantWorkerTools(
203
200
  return grantedTools;
204
201
  }
205
202
 
206
- /** Resolves a provider id for credential reuse / error metadata (shared). */
207
- export function providerOfWorkerModel(modelStr: string | unknown | undefined): string | undefined {
208
- return providerOf(modelStr);
209
- }
210
-
211
203
  function providerOf(modelStr: string | unknown | undefined): string | undefined {
212
204
  if (!modelStr) return undefined;
213
205
  try {
@@ -246,18 +238,34 @@ export function resolveWorkerCredentials(
246
238
  return { apiKeyToUse: undefined, baseUrlToUse: undefined, attemptedProvider };
247
239
  }
248
240
 
241
+ /** How many levels of workers may exist below an agent unless `dynamicSubagents.maxDepth` says otherwise: unlimited. */
242
+ export const DEFAULT_MAX_DEPTH = Infinity;
243
+
249
244
  /**
250
- * Config of a stateless dynamic worker (`spawn_subagents` and scheduled
251
- * actions): the developer's fixed worker model, level and limits, never the
252
- * LLM's choice. Credentials carry over only when the providers match, and
253
- * `inheritableConfig()` keeps the worker no less constrained than its parent.
245
+ * Config of a dynamic worker (scheduled actions, the worker registry): the developer's fixed worker
246
+ * model, level and limits, never the LLM's choice. Credentials carry over only when the providers match, and
247
+ * `inheritableConfig()` keeps the worker no less constrained than its parent. Stateless one-shot by default;
248
+ * `persistent` makes a multi-turn worker that keeps its history (saved under `persistent.dir` when given, and
249
+ * continued by `resume()` after a crash, never automatically). A persistent worker with levels left below it
250
+ * (`maxDepth` of the parent, minus one for itself) gets its own `subagents` tool; its children can only be granted
251
+ * the tools it was granted. One-shot workers never spawn. `dynamicSubagents.workerBudget` becomes each worker's own
252
+ * per-run `budget` (and is passed to its children).
254
253
  */
255
254
  export function buildDynamicWorkerConfig(
256
255
  parent: Agent,
257
- worker: { name: string; role?: string; instructions: string; sessionId: string; tools: Record<string, ToolDefinition> }
256
+ worker: {
257
+ name: string;
258
+ role?: string;
259
+ instructions: string;
260
+ sessionId: string;
261
+ tools: Record<string, ToolDefinition>;
262
+ persistent?: { dir?: string };
263
+ }
258
264
  ): AgentConfig {
259
265
  const model = parent.subagentModel;
260
266
  const { apiKeyToUse, baseUrlToUse } = resolveWorkerCredentials(parent, model);
267
+ const dyn = parent.dynamicSubagents;
268
+ const levelsBelow = (dyn?.maxDepth ?? DEFAULT_MAX_DEPTH) - 1;
261
269
  return {
262
270
  name: worker.name,
263
271
  description: worker.role || `Sub-agent ${worker.name}`,
@@ -268,8 +276,22 @@ export function buildDynamicWorkerConfig(
268
276
  thinkingLevel: parent.dynamicSubagents?.thinkingLevel ?? parent.thinkingConfig?.level,
269
277
  sessionId: worker.sessionId,
270
278
  tools: worker.tools,
271
- // Stateless by design: one task in, one result out, then shut down. No history, no recursion.
272
- stateless: true,
279
+ ...(dyn?.workerBudget ? { budget: { ...dyn.workerBudget } } : {}),
280
+ stateless: !worker.persistent,
281
+ ...(worker.persistent?.dir ? { persist: { dir: worker.persistent.dir }, resume: "explicit" as const } : {}),
282
+ ...(worker.persistent && levelsBelow >= 1
283
+ ? {
284
+ dynamicSubagents: {
285
+ enabled: true,
286
+ model,
287
+ ...(dyn?.maxSpawn !== undefined ? { maxSpawn: dyn.maxSpawn } : {}),
288
+ ...(dyn?.thinkingLevel ? { thinkingLevel: dyn.thinkingLevel } : {}),
289
+ tools: worker.tools,
290
+ ...(dyn?.workerBudget ? { workerBudget: dyn.workerBudget } : {}),
291
+ maxDepth: levelsBelow,
292
+ },
293
+ }
294
+ : {}),
273
295
  serviceTier: parent.serviceTier,
274
296
  headers: parent.customHeaders,
275
297
  maxTurns: parent.maxTurns,
@@ -277,18 +299,6 @@ export function buildDynamicWorkerConfig(
277
299
  };
278
300
  }
279
301
 
280
- /** Builds one `<NAME>...text...</NAME>` block inside `<SUB-AGENTS-RESPONSE>`. */
281
- export function buildWorkerXmlBlock(name: string, text: string): string {
282
- const escaped = escapeXml(text);
283
- const indentedText = escaped.split("\n").map((line) => ` ${line}`).join("\n");
284
- return ` <${name}>\n${indentedText}\n </${name}>`;
285
- }
286
-
287
- /** Builds the `<SUB-AGENTS-RESPONSE>` envelope for dynamic workers. */
288
- export function buildSubagentsXml(results: Array<{ name: string; text: string }>): string {
289
- return `<SUB-AGENTS-RESPONSE>\n${results.map((r) => buildWorkerXmlBlock(r.name, r.text)).join("\n")}\n</SUB-AGENTS-RESPONSE>`;
290
- }
291
-
292
302
  // ---------------------------------------------------------------------------
293
303
  // Shared live worker state machine (single home for fixed + dynamic workers)
294
304
  // ---------------------------------------------------------------------------
@@ -322,7 +332,7 @@ export interface LiveWorkerHooks {
322
332
 
323
333
  /**
324
334
  * Single live worker state machine shared by fixed (`agentToTool`) and
325
- * dynamic (`spawn_subagents`) workers. Owns `liveTurn` / `partialText` /
335
+ * registry workers. Owns `liveTurn` / `partialText` /
326
336
  * `partialThinking`, finalizes partial streaming entries on `onTurn`, and
327
337
  * forwards deltas to the parent stream. The trace object is mutated in place;
328
338
  * trimming / touching / notifying / persisting are delegated to hooks so this