agent-accelerator 0.5.3 → 0.5.4

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/README.md CHANGED
@@ -121,7 +121,7 @@ An `Agent` holds:
121
121
  | `tools` | `Record<string, ToolDefinition> \| ToolDefinition[]` | Deterministic functions the model can call. See [Tools](#tools). |
122
122
  | `subagents` | `SubAgent[] \| Agent[]` | Pre-defined workers. Each worker becomes a callable tool. Useful for fixed roles such as researcher or critic. |
123
123
  | `subagentModel` | `string \| ModelSpec \| ModelProviderInstance` | Developer-only model for dynamically spawned workers. Never choosable by the Main Agent. Deprecated alias of `dynamicSubagents.model` (which wins). Falls back to `SUB_AGENT_MODEL`. |
124
- | `dynamicSubagents` | `DynamicSubagentsConfig` | Enables and constrains LLM-spawned stateless workers: `{ enabled, model?, maxSpawn?, thinkingLevel?, tools?, timeout?, allowScheduledActions? }`. See [Pre-defined vs Dynamic Delegation](#pre-defined-vs-dynamic-delegation) and [Scheduled actions](#scheduled-actions). |
124
+ | `dynamicSubagents` | `DynamicSubagentsConfig` | Enables and constrains LLM-spawned workers: `{ enabled, model?, maxSpawn?, maxDepth?, workerBudget?, thinkingLevel?, tools?, timeout?, allowScheduledActions? }` (`timeout` is for scheduled actions only). See [Pre-defined vs Dynamic Delegation](#pre-defined-vs-dynamic-delegation) and [Scheduled actions](#scheduled-actions). |
125
125
  | `thinking` | `ThinkingOption` | Canonical reasoning option: `none \| dynamic \| minimal \| low \| medium \| high \| xhigh \| max`, or `{ level, budgetTokens }`. Legacy `thinkingLevel` still works (canonical wins). Validated against the model catalog before a request. |
126
126
  | `cache` | `CacheOption` | `"implicit" \| "5m" \| "30m" \| "1h" \| "12h"`. Prompt-cache lifetime; unsupported values warn and fall back (see [Cache](#cache)). |
127
127
  | `serviceTier` | `"flex" \| "priority"` | Cost / priority routing where supported. Omit for standard routing. |
@@ -269,47 +269,60 @@ const agent = new Agent({
269
269
  instructions: "You are the Main Agent",
270
270
  dynamicSubagents: {
271
271
  enabled: true,
272
- maxSpawn: 4,
272
+ maxSpawn: 4, // open workers at once
273
273
  thinkingLevel: "low",
274
- tools: { get_weather, recent_news },
275
- timeout: 60000,
274
+ tools: { get_weather, recent_news }, // pool: the model grants a subset per worker
276
275
  },
277
276
  });
278
277
  ```
279
278
 
280
- A `spawn_subagents` tool is injected with the following task shape:
279
+ One `subagents` tool is injected. It takes `{ command, id?, name?, role?, instructions?, task?, tools?, history? }` and its manual is the tool description (`src/agent/subagents.md`, with the worker pool and the open-worker limit filled in; the system prompt stays untouched):
280
+
281
+ - `spawn` starts a worker (`name`, `task`; optional `instructions`, `role`, `tools`, `history`) and waits for its first reply. `message` sends `task` to an open worker `id`, which remembers the whole conversation. `close` frees the slot. `list` shows the open workers and the numbered finished turns that `history: N` can copy into a new worker.
282
+ - Results are plain text with one header line: `[worker researcher-1a2b3c4d] replied:` + the worker's text, or `[worker researcher-1a2b3c4d] failed: <reason>`. Several `spawn` calls in one model turn run in parallel. A bad request comes back as an `Error: ...` line the model can fix.
283
+ - `maxSpawn` is the limit on **open** workers (the model must close one to open another). `budget.maxTotalSubagents` still counts spawns per run.
284
+ - `tools` is the developer-owned pool. Workers get zero tools unless the Main Agent grants a subset by name; unknown names are rejected with the pool listed.
285
+ - The Main Agent cannot choose worker models, reasoning levels or timeouts. The `subagents` tool has no timeouts (`workerTimeoutMs`/`timeout` apply only to scheduled actions).
286
+ - `maxDepth` (default 3, whole number >= 1) is how many levels of workers may exist below the agent: `1` keeps workers flat, `3` lets a worker start workers that start workers. A worker with levels left gets its own `subagents` tool, and its children can only be granted the tools **it** was granted. The levels are saved inside each other (`<session>/workers/<id>/workers/<id>/…`).
287
+ - `workerBudget: { maxCostUsd?, maxTotalTokens? }` is each worker's own spending cap, applied as that worker's per-run `budget` (a spawn, a message or a scheduled run is one run; the usage of the worker's own workers counts toward it) and passed down to its workers. Going over fails that call with a budget error (`[worker <id>] failed: Token budget exceeded…`), leaves the worker open and does not affect the parent. Default: unlimited; models with unknown pricing count $0 toward `maxCostUsd`. Values must be numbers above 0.
288
+ - Workers are real agents with their own session: see [Persistent workers](#persistent-workers-agentworkers) below (the same functions in code, saved on disk with `persist`, resumed after a restart). Each reply's usage is metered into the parent's usage and budget.
289
+ - Each run of a worker gets a system-generated 32-char TrackingID, displayed as `NAME-{TrackingID}`. Thinking/text deltas update the worker trace live (`agent.track(id)`, `subscribeToSubAgent`) and surface on the parent stream as `subagent_delta` events.
281
290
 
282
291
  ```ts
283
- {
284
- tasks: {
285
- name,
286
- role?,
287
- instructions,
288
- task,
289
- tools?: string[],
290
- timeoutMs?: number
291
- }[]
292
- }
292
+ const res = await agent.run("Audit auth pipeline and write a threat model");
293
+
294
+ console.log(res.subagents.map((s) => s.name));
293
295
  ```
294
296
 
295
- - `maxSpawn` — max workers per call. Extras are trimmed safely, and the limit is stated in the tool description plus the Main Agent's system prompt so the model knows it.
296
- - `tools` — developer-owned pool. Workers get zero tools unless the Main Agent grants a per-task `tools` subset by name (unknown names are ignored), keeping worker context small.
297
- - `timeout` (ms) — `0` = no limit, `-1` = the Main Agent sets a per-task `timeoutMs`, `>0` = fixed limit for every worker. Timed-out workers report an error entry; the rest of the batch still completes.
298
- - Workers are stateless: one task in, one result out, then shut down. No history, no recursion. The Main Agent cannot choose worker models or reasoning levels.
299
- - Worker names use UPPER-KEBAB (`HBM-PRICING-ANALYST`). Each worker gets a system-generated 32-char TrackingID, displayed as `NAME-{TrackingID}` (e.g. `HBM-PRICING-ANALYST-9f2c…`).
300
- - Workers stream internally: thinking/text deltas update the worker trace live (`agent.track(id)`, `subscribeToSubAgent`) and surface on the parent stream as `subagent_delta` events.
297
+ ### Persistent workers (`agent.workers`)
298
+
299
+ Besides one-shot delegation, an agent can run **workers that keep their conversation**: start one, message it again and again, close it. A worker is a plain `Agent` with its own instructions and tools, on the developer's worker model (`dynamicSubagents.model` or `SUB_AGENT_MODEL`). With `persist: { dir }` on the parent it is saved under `<parent session folder>/workers/<id>/` (own journal, tool logs and `worker.json`), so it is rebuilt with its history after a restart.
301
300
 
302
301
  ```ts
303
- const res = await agent.run("Audit auth pipeline and write a threat model");
302
+ const reply = await lead.workers.spawn({
303
+ name: "Researcher",
304
+ instructions: "You research databases and cite sources.", // the worker's OWN prompt; the parent's is never copied
305
+ task: "Compare SQLite and Postgres for a small SaaS.",
306
+ tools: ["recent_news"], // subset of dynamicSubagents.tools; unknown names throw
307
+ history: 1, // optional: start from a copy of the parent's first 1 finished turns (see lead.workers.turns()); default "none"
308
+ });
309
+ console.log(reply.id, reply.text); // e.g. "researcher-1a2b3c4d"
304
310
 
305
- console.log(res.subagents.map((s) => s.name));
311
+ const next = await lead.workers.message(reply.id, "Now focus on write-heavy loads."); // same worker, same conversation
312
+ lead.workers.list(); // open workers: { id, name, role?, tools, busy }
313
+ lead.workers.close(reply.id); // can no longer be messaged; its session stays on disk
306
314
  ```
307
315
 
316
+ - A turn is one **finished** run (`done` or ended by `ctx.stop`); the running turn and failed or aborted runs are never copied, so a worker never gets a prompt without its answer.
317
+ - `spawn` and `message` wait for the reply and never throw for a failing worker: the `WorkerReply` then carries `error` and a text that says so. They throw `ValidationError` for a bad request (unknown tool or id, closed or busy worker, too many open workers: `dynamicSubagents.maxSpawn` is the open-worker limit, `budget.maxTotalSubagents` still counts spawns per run).
318
+ - A worker handles one call at a time. Pass `{ signal }` to abort its run; the worker stays open.
319
+ - Exactly once across a crash: pass `{ callId }` (the calling tool call's id). A replayed `spawn` finds the worker it already started, a cut-off run is continued with `resume()` instead of sending the text twice, and a call that had already been answered returns that saved answer.
320
+ - Without `persist` the workers live in memory only. A damaged `worker.json` is skipped with a `worker.invalid` event.
321
+ - The reply's `metadata` (usage, steps, lineage) has the same shape as `AgentResponse.subagents`; `agent.track(metadata.trackingId)` shows the run live.
322
+
308
323
  ### Delegation Helpers
309
324
 
310
325
  * `buildAgentTools(list)` — wraps multiple agents and deduplicates names. Used internally by `subagents`.
311
- * `createSubagentSpawnTool(parentAgent)` — builds the dynamic subagent spawner. Rarely needed directly.
312
- * `DynamicSubagentTask` — `{ name, role?, instructions, task, tools?, timeoutMs? }`, the shape produced for dynamic delegation. No `model` — workers run on the developer-configured model.
313
326
 
314
327
  ---
315
328
  ## Scheduled actions
@@ -426,11 +439,11 @@ const pay = tool({
426
439
 
427
440
  `ctx.stop` throws, so the code after it never runs (even if you forget `return`). The reason is saved as that call's result, calls that were running in parallel in the same batch still run to completion (none is skipped or aborted), and then the run ends with `finishReason: "stopped"` and **no further model turn**. The agent waits for the next user message; the model then sees the reason as the tool's result. A steer typed while the batch ran becomes the next run. The stop is saved with the call, so a crash cannot turn a "no" into a "yes": after a restart the run is closed as stopped without calling the model. A tool's `timeoutMs` counts the time spent waiting for your check, so leave it off for tools that ask a person.
428
441
 
429
- To guard a built-in tool such as `spawn_subagents` or `scheduled_actions`, wrap it on the agent after construction:
442
+ To guard a built-in tool such as `subagents` or `scheduled_actions`, wrap it on the agent after construction:
430
443
 
431
444
  ```ts
432
- const inner = agent.tools["spawn_subagents"]!;
433
- agent.tools["spawn_subagents"] = {
445
+ const inner = agent.tools["subagents"]!;
446
+ agent.tools["subagents"] = {
434
447
  ...inner,
435
448
  execute: async (input, ctx) => ((await getPermission("Spawn workers?")) ? inner.execute(input, ctx) : ctx.stop("spawning was not approved")),
436
449
  };
@@ -1011,9 +1024,32 @@ TokenUsage {
1011
1024
  }
1012
1025
  ```
1013
1026
 
1014
- Cost calculation prefers provider-reported totals. When unavailable, costs are computed using catalog pricing.
1027
+ Cost calculation prefers provider-reported totals. When unavailable, costs are computed using catalog pricing, per model turn: fresh input at the input price, cache reads at the cache-read price, cache writes at the cache-write price (both are part of `inputTokens`, so they are not billed twice), output at the output price (thinking tokens are part of output), scaled by `serviceTier` (`flex` x0.5, `priority` x2, estimates). A catalog entry without a cache price bills those tokens at the input rate (cache is never free); unknown pricing stays unknown.
1015
1028
 
1016
1029
  Subagent usage is rolled into the parent totals.
1030
+
1031
+ ### Usage report (`agent.usageReport()`)
1032
+
1033
+ `res.usage` is the cost of one run, workers included. `agent.usageReport()` is the other view: what the agent and **each** worker spent over their lives, itemized, with nothing counted twice.
1034
+
1035
+ ```ts
1036
+ const report = agent.usageReport();
1037
+ report.agent; // this agent's OWN model calls: id, name, model, turns, tokens, cacheHitRate, cost
1038
+ report.workers; // every worker below it (open, closed, nested in `workers`, and `status: "fixed"` for `subagents: [...]`)
1039
+ report.total; // agent + all workers
1040
+ // each line, e.g. report.workers[0]:
1041
+ // { id: "researcher-1a2b3c4d", status: "open", model: "google/gemini-3.5-flash-lite", turns: 2,
1042
+ // tokens: { input: 2000, freshInput: 600, cacheRead: 1200, cacheWrite: 200, output: 400, thinking: 100, total: 2400 },
1043
+ // cacheHitRate: 0.6,
1044
+ // cost: { source: "catalog", unpricedTurns: 0, totalUsd: 0.00891,
1045
+ // parts: { inputUsd: 0.0018, outputUsd: 0.006, cacheReadUsd: 0.00036, cacheWriteUsd: 0.00075 } } }
1046
+ ```
1047
+
1048
+ - **Own turns only.** Every agent records the model calls it made itself, priced **per turn** in integer micro-USD, and saves them in its session (`usage`), so the numbers survive a restart (load the session and the totals carry on; workers are rebuilt from their folders). A parent's `res.usage` already contains its workers' usage, which is why the report never adds responses together: `report.total.tokens.total` equals what the responses show.
1049
+ - **Cache-aware.** `input` includes the cache reads and writes (`freshInput = input - cacheRead - cacheWrite`); `output` includes `thinking`. Fresh input, cache reads and cache writes are each priced at their own catalog price, and `serviceTier` is applied. `cacheHitRate` is `cacheRead / input` (0 to 1, `null` when nothing was sent).
1050
+ - **Honest costs.** `cost.source` is `provider` (the provider reported the cost, which is exact), `catalog` (estimated from the model catalog), `mixed`, or `unknown`. Unknown pricing is `totalUsd: null`, never 0; `unpricedTurns` above 0 means the total is a lower bound. For turns whose cost the provider reported only the total is exact, so `parts` can add up to less than `totalUsd`.
1051
+ - **Not in the report:** the one-shot workers of scheduled actions (their cost is in each run file), and workers started inside a fixed `subagents` worker's per-call copy (their cost is in the response usage). `reset()` does not clear the totals: they are spending, not conversation.
1052
+
1017
1053
  ### ProviderRawData
1018
1054
 
1019
1055
  ```ts
@@ -1288,7 +1324,7 @@ bun run examples/04-sub-agents.ts
1288
1324
  # fixed researcher + critic pipeline
1289
1325
 
1290
1326
  bun run examples/03-multi_agent.ts
1291
- # dynamic spawn_subagents demo
1327
+ # dynamic subagents tool demo
1292
1328
 
1293
1329
  bun run examples/01-metadata.ts
1294
1330
  # usage + raw inspection
@@ -1,34 +1,28 @@
1
- You are an Agent — you evaluate user objectives and decide whether to solve them directly or delegate to specialized parallel sub-agents.
1
+ You are an Agent — you evaluate user objectives and decide whether to solve them directly or delegate to specialized parallel workers with the `subagents` tool.
2
2
 
3
3
  ### Workflow & Decision Matrix
4
4
  1. **Triage**:
5
5
  - Single-focus, factual, code-generation, or conversational prompts → Answer directly, concisely, and completely.
6
- - Do NOT spawn sub-agents for trivial, single-step tasks.
6
+ - Do NOT start workers for trivial, single-step tasks.
7
7
  2. **Delegate**:
8
- - Complex research, multi-angle analysis, deep technical trade-offs, or parallel explorations → Invoke `spawn_subagents` with 2 to 4 focused sub-agents in a single batched call.
9
- - For each sub-agent define:
10
- - `name`: `UPPER_SNAKE_CASE` identifier describing domain focus (e.g., `MARKET_ANALYST`, `SYSTEMS_ARCHITECT`).
8
+ - Complex research, multi-angle analysis, deep technical trade-offs, or parallel explorations → call `subagents` with `command: "spawn"` once per worker, 2 to 4 focused workers, all in the same turn so they run in parallel.
9
+ - For each worker define:
10
+ - `name`: short label describing its domain focus (e.g. `market-analyst`, `systems-architect`).
11
11
  - `role`: Distinct specialized persona.
12
12
  - `instructions`: Self-contained, expert prompt detailing scope, methodologies, and constraints.
13
13
  - `task`: Focused, non-overlapping research query or execution directive.
14
14
  3. **Aggregate**:
15
- - Sub-agent findings arrive concurrently wrapped in XML:
16
- ```xml
17
- <SUB-AGENTS-RESPONSE>
18
- <DYNAMIC_ROLE_1>
19
- Findings from sub-agent 1
20
- </DYNAMIC_ROLE_1>
21
- <DYNAMIC_ROLE_2>
22
- Findings from sub-agent 2
23
- </DYNAMIC_ROLE_2>
24
- </SUB-AGENTS-RESPONSE>
15
+ - Each worker answers with a header line and its findings:
25
16
  ```
26
- - Node tags match each sub-agent's `name` in `UPPER_SNAKE_CASE`.
17
+ [worker market-analyst-1a2b3c4d] replied:
18
+ Findings from the worker
19
+ ```
20
+ - Use `command: "message"` with the worker's `id` to ask a follow-up; the worker remembers the conversation. `command: "close"` when you no longer need it.
27
21
  4. **Synthesize**:
28
- - Deliver an editorial-grade, cohesive report that integrates all sub-agent insights.
22
+ - Deliver an editorial-grade, cohesive report that integrates all worker insights.
29
23
  - Reconcile contradictions, de-duplicate redundant information, and highlight critical trade-offs.
30
- - Structure with executive summaries, comparative Markdown tables, and concrete strategic conclusions. Never present an uncurated list of sub-agent summaries.
24
+ - Structure with executive summaries, comparative Markdown tables, and concrete strategic conclusions. Never present an uncurated list of worker summaries.
31
25
 
32
26
  ### Execution Rules
33
- - **Tool Protocol**: Invoke `spawn_subagents` directly without conversational preambles. Ensure all sub-agents are passed in a single tool call array.
34
- - **Context Isolation**: Sub-agents run in isolated contexts; provide complete background in their `instructions` and `task`.
27
+ - **Tool Protocol**: Call `subagents` directly without conversational preambles. Start parallel workers with several `spawn` calls in one turn.
28
+ - **Context Isolation**: Workers start blank; provide complete background in their `instructions` and `task`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-accelerator",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
5
5
  "license": "MIT",
6
6
  "author": "Sashvat Bharat",
@@ -10,8 +10,11 @@ import { AgentResponse, type StreamEvent } from "../types/response.ts";
10
10
  import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
11
11
  import { AgentContext } from "./context.ts";
12
12
  import { resolveModel } from "../providers/registry.ts";
13
- import { buildAgentTools, createSubagentSpawnTool } from "./delegation.ts";
13
+ import { buildAgentTools } from "./delegation.ts";
14
+ import { createSubagentsTool } from "./subagents-tool.ts";
15
+ import { WorkerRegistry } from "./workers.ts";
14
16
  import {
17
+ DEFAULT_MAX_DEPTH,
15
18
  normalizeToolMap,
16
19
  resolveDynamicModelInput,
17
20
  resolveMainModel,
@@ -34,7 +37,8 @@ import {
34
37
  listSubAgentTraceIdsForSessions,
35
38
  type SubAgentTrace,
36
39
  } from "./delegation.ts";
37
- import { saveSessionFile, sessionDirFor, loadSessionDir, findInvalidMessage, telemetryBus, type SessionRunMark, type SessionRunEnd, type SessionPending, type SessionToolRecord } from "../session/store.ts";
40
+ import { addTurn, buildUsageReport, emptyUsage, mergeUsage, modelName, readUsage, type UsageNode, type UsageReport, type UsageTotals } from "../session/usage.ts";
41
+ import { saveSessionFile, sessionDirFor, loadSessionDir, findInvalidMessage, priceUsage, telemetryBus, type SessionRunMark, type SessionRunEnd, type SessionPending, type SessionToolRecord } from "../session/store.ts";
38
42
  import { SessionJournal } from "../session/journal.ts";
39
43
  import { ToolLogs, type ToolLogWriter } from "../session/tool-log.ts";
40
44
  import { isLocalPathInput } from "../utils/safe-io.ts";
@@ -167,6 +171,10 @@ export class Agent {
167
171
  timeout: number;
168
172
  /** Scheduled actions (`scheduled_actions` + `get_datetime` tools) are on. */
169
173
  allowScheduledActions: boolean;
174
+ /** Levels of workers allowed below this agent. */
175
+ maxDepth: number;
176
+ /** Per-run spending cap of each worker (unset = unlimited). */
177
+ workerBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
170
178
  };
171
179
  /** Home folder for everything written to disk (`sessions/`, `scheduled_actions/`, `telemetry/`). */
172
180
  readonly home: string;
@@ -263,6 +271,8 @@ export class Agent {
263
271
  readonly runMarks: SessionRunMark[] = [];
264
272
  /** Tool calls finished in the current run; saved the moment each finishes so a crash never repeats them. @internal */
265
273
  readonly toolRecords: SessionToolRecord[] = [];
274
+ /** What this agent spent on its OWN model calls (workers keep their own); saved with the session. @internal */
275
+ readonly usageTotals: UsageTotals = emptyUsage();
266
276
  /** The run in flight (its mark has no ending yet, but it is not "interrupted"). */
267
277
  private activeMark?: SessionRunMark;
268
278
  /** What to do with a cut-off run at construction (see `AgentConfig.resume`). */
@@ -271,6 +281,7 @@ export class Agent {
271
281
  private readonly runRetries: number;
272
282
  /** The resume in flight, so a second `resume()` returns the same promise. */
273
283
  private resuming?: Promise<AgentResponse | undefined>;
284
+ private workerRegistry?: WorkerRegistry;
274
285
 
275
286
  /**
276
287
  * Creates an agent and registers its model, tools, cache, and delegation settings.
@@ -307,6 +318,17 @@ export class Agent {
307
318
  const dynEnabled = dynRaw ? (dynRaw.enabled ?? true) : false;
308
319
  if (dynRaw) {
309
320
  const dynTools = normalizeToolMap((dynRaw as { tools?: Record<string, ToolDefinition> | ToolDefinition[] }).tools);
321
+ const rawDepth = (dynRaw as { maxDepth?: unknown }).maxDepth;
322
+ if (rawDepth !== undefined && !(Number.isInteger(rawDepth) && (rawDepth as number) >= 1)) {
323
+ throw new ConfigError(`[Agent Accelerator] Invalid dynamicSubagents.maxDepth ${JSON.stringify(rawDepth)}: expected a whole number >= 1 (default ${DEFAULT_MAX_DEPTH}); set dynamicSubagents.enabled: false to turn workers off.`);
324
+ }
325
+ const rawBudget = (dynRaw as { workerBudget?: { maxCostUsd?: unknown; maxTotalTokens?: unknown } }).workerBudget;
326
+ for (const key of ["maxCostUsd", "maxTotalTokens"] as const) {
327
+ const v = rawBudget?.[key];
328
+ if (v !== undefined && !(typeof v === "number" && Number.isFinite(v) && v > 0)) {
329
+ throw new ConfigError(`[Agent Accelerator] Invalid dynamicSubagents.workerBudget.${key} ${JSON.stringify(v)}: expected a number greater than 0, or omit it for no limit.`);
330
+ }
331
+ }
310
332
  const rawMax = (dynRaw as any)?.maxSpawn;
311
333
  const maxSpawn = Number.isFinite(rawMax) ? Math.max(1, Math.floor(rawMax as number)) : 4;
312
334
  // `workerTimeoutMs ?? timeout`, or -1 when `modelMaySetTimeout` (see normalizeWorkerTimeout).
@@ -319,6 +341,8 @@ export class Agent {
319
341
  tools: dynTools,
320
342
  timeout,
321
343
  allowScheduledActions: dynRaw.allowScheduledActions === true,
344
+ maxDepth: (rawDepth as number | undefined) ?? DEFAULT_MAX_DEPTH,
345
+ ...(rawBudget ? { workerBudget: { ...(rawBudget as { maxCostUsd?: number; maxTotalTokens?: number }) } } : {}),
322
346
  };
323
347
  }
324
348
  this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
@@ -438,8 +462,8 @@ export class Agent {
438
462
  );
439
463
  }
440
464
  if (dyn?.enabled === true) {
441
- const spawnTool = createSubagentSpawnTool(this);
442
- this.tools[spawnTool.name || "spawn_subagents"] = spawnTool;
465
+ const subagentsTool = createSubagentsTool(this);
466
+ this.tools[subagentsTool.name || "subagents"] = subagentsTool;
443
467
  }
444
468
  if (dyn?.allowScheduledActions) {
445
469
  // Fixed by config, so the tool set (and the prompt cache) stays stable.
@@ -459,6 +483,11 @@ export class Agent {
459
483
  }
460
484
  }
461
485
 
486
+ /** The persistent sub-agent workers of this agent: `spawn`, `message`, `close`, `list`, `turns` (see `WorkerRegistry`). */
487
+ get workers(): WorkerRegistry {
488
+ return (this.workerRegistry ??= new WorkerRegistry(this));
489
+ }
490
+
462
491
  // Loads a persisted session that a crash cut off (a run without an ending) or that still had waiting prompts, so it can be resumed; true when there is something to resume; in: the session folder of `persist.dir`, out: context, runMarks, toolRecords, pendingQueue.
463
492
  private recoverSession(): boolean {
464
493
  const root = this.persistConfig?.dir;
@@ -475,6 +504,7 @@ export class Agent {
475
504
  if (!(session.runs ?? []).some((r) => !r.end) && waiting.length === 0) return false;
476
505
  this.context.messages = session.messages;
477
506
  this.runMarks.splice(0, this.runMarks.length, ...(session.runs ?? []));
507
+ this.restoreUsage(session.usage);
478
508
  const open = new Set(unfinishedToolCalls(this.context.messages).map((c) => c.id));
479
509
  this.toolRecords.splice(0, this.toolRecords.length, ...(session.tools ?? []).filter((t) => open.has(t.id)));
480
510
  for (const w of waiting) {
@@ -489,6 +519,42 @@ export class Agent {
489
519
  return true;
490
520
  }
491
521
 
522
+ // Puts saved spending totals back after a restart or import (zeros when the file has none); in: a loaded session's `usage`, out: this.usageTotals.
523
+ private restoreUsage(saved: unknown): void {
524
+ Object.assign(this.usageTotals, readUsage(saved));
525
+ }
526
+
527
+ // Records one model call of THIS agent (priced per turn, with the service tier); in: the loop's onTurnUsage, out: this.usageTotals (saved by the next persist).
528
+ private recordTurnUsage(usage: TokenUsage, spec: Parameters<typeof priceUsage>[1]): void {
529
+ addTurn(this.usageTotals, usage, priceUsage(usage, spec, this.serviceTier ? { serviceTier: this.serviceTier } : undefined));
530
+ }
531
+
532
+ /** Adds another agent's spending to this one (a fixed worker's per-call copy into its template). @internal */
533
+ absorbUsage(other: UsageTotals): void {
534
+ mergeUsage(this.usageTotals, other);
535
+ }
536
+
537
+ /**
538
+ * What this agent and every worker below it spent: per agent and per worker (tokens, cache hits, thinking, exact
539
+ * cost per turn) plus a whole-session total, with nothing counted twice. `agent` is this agent's own model calls;
540
+ * workers (open, closed, nested, and rebuilt from disk after a restart) and fixed `subagents` workers are listed
541
+ * below it. Spending of one-shot scheduled-action workers is in their run files, not here.
542
+ */
543
+ usageReport(): UsageReport {
544
+ return buildUsageReport({ id: this.sessionId, name: this.name, model: modelName(this.modelStringOrSpec), totals: this.usageTotals }, [...this.workers.usageNodes(), ...this.fixedWorkerNodes()]);
545
+ }
546
+
547
+ // The `subagents: [...]` workers as usage nodes (their per-call copies' spending is merged into them); in: the constructor config, out: usageReport.
548
+ private fixedWorkerNodes(): UsageNode[] {
549
+ const nodes: UsageNode[] = [];
550
+ for (const entry of this.forkConfig.subagents ?? []) {
551
+ const worker = "agent" in entry ? entry.agent : entry;
552
+ const name = "agent" in entry ? entry.name : worker.name;
553
+ nodes.push({ id: name, name, model: modelName(worker.modelStringOrSpec), status: "fixed", totals: worker.usageTotals, workers: [] });
554
+ }
555
+ return nodes;
556
+ }
557
+
492
558
  // Says on the telemetry bus that resuming (or a prompt that was waiting) failed; in: a rejected resume, out: telemetryBus.
493
559
  private reportResumeFailure(err: unknown): void {
494
560
  telemetryBus.emitWarning("agent.resume_failed", `[Agent Accelerator] Resuming session ${this.sessionId} failed: ${err instanceof Error ? err.message : String(err)}`, { sessionId: this.sessionId });
@@ -579,26 +645,6 @@ export class Agent {
579
645
  "3. Once your reasoning is complete, output your final response or function calls directly in the standard response output.";
580
646
  out = out ? `${out}\n\n${guidance}` : guidance;
581
647
  }
582
- if (this.dynamicSubagents?.enabled === true && !out.includes("[Dynamic Sub-Agents]")) {
583
- const dyn = this.dynamicSubagents;
584
- const toolNames = Object.keys(dyn.tools);
585
- const timeoutNote =
586
- dyn.timeout === -1
587
- ? "Timeout policy: set a per-sub-agent timeoutMs (ms) for each task; omit it for no limit."
588
- : dyn.timeout === 0
589
- ? "Timeout policy: workers run with no time limit."
590
- : `Timeout policy: every worker is limited to ${dyn.timeout}ms; per-task timeouts are ignored.`;
591
- const policy =
592
- "[Dynamic Sub-Agents]\n" +
593
- `1. You may spawn at most ${dyn.maxSpawn} sub-agent(s) per spawn_subagents call. Extra tasks beyond ${dyn.maxSpawn} are ignored.\n` +
594
- "2. Workers are stateless: each receives one task, returns its result, then shuts down. No conversation history is kept.\n" +
595
- "3. You cannot choose worker models or reasoning levels — they are fixed by the developer.\n" +
596
- (toolNames.length > 0
597
- ? `4. Worker-available tools: ${toolNames.join(", ")}. Grant each worker ONLY the tools its task needs via the per-task tools list; omit it for no tools.\n`
598
- : "4. No worker tools are available; omit the per-task tools list.\n") +
599
- `5. ${timeoutNote}`;
600
- out = out ? `${out}\n\n${policy}` : policy;
601
- }
602
648
  return out || undefined;
603
649
  }
604
650
 
@@ -1161,6 +1207,7 @@ export class Agent {
1161
1207
  ...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
1162
1208
  ...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1163
1209
  onProgress: () => this.persistNow(),
1210
+ onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
1164
1211
  onToolResult: (r: ToolResultRecord) => this.recordTool(r),
1165
1212
  openToolLog: (call: { name: string }, sid: string | undefined) => this.openToolLog(call, sid),
1166
1213
  ...(resumeMark ? { resume: { stored: this.storedToolResults() } } : {}),
@@ -1251,6 +1298,7 @@ export class Agent {
1251
1298
  ...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
1252
1299
  ...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1253
1300
  onProgress: () => this.persistNow(),
1301
+ onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
1254
1302
  onToolResult: (r: ToolResultRecord) => this.recordTool(r),
1255
1303
  openToolLog: (call: { name: string }, sid: string | undefined) => this.openToolLog(call, sid),
1256
1304
  steerInbox,
@@ -1501,6 +1549,7 @@ export class Agent {
1501
1549
  }
1502
1550
  this.context.thoughtSignatures = [];
1503
1551
  this.runMarks.splice(0, this.runMarks.length, ...(data.runs ?? []).filter((r) => r.i < this.context.messages.length));
1552
+ this.restoreUsage(data.usage);
1504
1553
  if (data.systemPrompt) {
1505
1554
  this.context.systemPrompt = data.systemPrompt;
1506
1555
  } else {
@@ -11,7 +11,7 @@ function fail(message: string): never {
11
11
  }
12
12
 
13
13
  /** Deep clone that preserves Uint8Array/ArrayBuffer media (structuredClone may be absent). */
14
- function deepCloneValue<T>(value: T): T {
14
+ export function deepCloneValue<T>(value: T): T {
15
15
  if (value instanceof Uint8Array) return new Uint8Array(value) as T;
16
16
  if (value instanceof ArrayBuffer) return value.slice(0) as T;
17
17
  if (Array.isArray(value)) return value.map((v) => deepCloneValue(v)) as T;