agent-accelerator 0.5.9 → 0.6.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.
Files changed (85) hide show
  1. package/README.md +64 -16
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +447 -172
  7. package/src/agent/architecture.md +16 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +9 -3
  10. package/src/agent/defaults.ts +88 -0
  11. package/src/agent/delegation.ts +86 -28
  12. package/src/agent/loop.ts +474 -374
  13. package/src/agent/subagent.ts +1 -3
  14. package/src/agent/subagents-tool.ts +50 -19
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/usage-limit.ts +92 -0
  17. package/src/agent/worker.ts +73 -30
  18. package/src/agent/workers.ts +126 -21
  19. package/src/architecture.md +17 -15
  20. package/src/index.ts +43 -19
  21. package/src/models/architecture.md +4 -3
  22. package/src/models/catalog-cache.ts +6 -5
  23. package/src/models/catalog.ts +39 -30
  24. package/src/prompts/additional-context.md +2 -0
  25. package/src/prompts/architecture.md +19 -0
  26. package/src/prompts/prompts.ts +81 -0
  27. package/src/prompts/reasoning-directive.md +5 -0
  28. package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
  29. package/src/prompts/structured-output.md +2 -0
  30. package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
  31. package/src/prompts/tool-bridge.md +1 -0
  32. package/src/prompts/tool-interrupted.md +1 -0
  33. package/src/prompts/tool-not-found.md +1 -0
  34. package/src/prompts/tool-repeat-blocked.md +1 -0
  35. package/src/prompts/tool-schema-error.md +1 -0
  36. package/src/prompts/tool-truncated-arguments.md +1 -0
  37. package/src/prompts/worker-failed.md +1 -0
  38. package/src/prompts/worker-replied.md +2 -0
  39. package/src/providers/architecture.md +10 -8
  40. package/src/providers/google.ts +228 -69
  41. package/src/providers/openai-compat.ts +74 -56
  42. package/src/providers/openai.ts +64 -41
  43. package/src/providers/openrouter.ts +270 -48
  44. package/src/providers/registry.ts +6 -21
  45. package/src/providers/shared.ts +87 -33
  46. package/src/providers.ts +115 -124
  47. package/src/scheduler/architecture.md +8 -7
  48. package/src/scheduler/scheduler.ts +226 -60
  49. package/src/scheduler/store.ts +23 -10
  50. package/src/scheduler/time.ts +17 -3
  51. package/src/scheduler/tool.ts +38 -16
  52. package/src/session/architecture.md +7 -6
  53. package/src/session/journal.ts +39 -12
  54. package/src/session/store.ts +126 -61
  55. package/src/session/usage.ts +58 -9
  56. package/src/streaming/architecture.md +4 -3
  57. package/src/streaming/event-stream.ts +26 -12
  58. package/src/telemetry.ts +6 -2
  59. package/src/tools/architecture.md +9 -7
  60. package/src/tools/executor.ts +109 -62
  61. package/src/tools/schema.ts +10 -6
  62. package/src/tools/subagent-result.ts +6 -1
  63. package/src/tools/tool-content.ts +32 -0
  64. package/src/tools/tool.ts +9 -11
  65. package/src/types/agent.ts +37 -22
  66. package/src/types/architecture.md +11 -10
  67. package/src/types/core.ts +5 -9
  68. package/src/types/errors.ts +26 -4
  69. package/src/types/message.ts +7 -0
  70. package/src/types/model.ts +4 -5
  71. package/src/types/scheduler.ts +4 -4
  72. package/src/types/tool.ts +14 -2
  73. package/src/update-models.ts +5 -5
  74. package/src/utils/architecture.md +16 -15
  75. package/src/utils/cache.ts +1 -3
  76. package/src/utils/durable.ts +17 -5
  77. package/src/utils/env.ts +9 -7
  78. package/src/utils/errors.ts +9 -17
  79. package/src/utils/headers.ts +3 -10
  80. package/src/utils/media.ts +23 -8
  81. package/src/utils/raw-capture.ts +9 -2
  82. package/src/utils/retry.ts +15 -3
  83. package/src/utils/safe-io.ts +29 -7
  84. package/src/utils/serialization.ts +1 -4
  85. package/src/utils/session.ts +1 -2
@@ -2,25 +2,29 @@
2
2
 
3
3
  The agent and everything that runs under it.
4
4
 
5
- | File | What it is | Used by |
6
- | :-- | :-- | :-- |
7
- | `agent.ts` | `Agent`: `run`/`stream`/`ask`, `steer`/`queue`/`interrupt`, persistence hooks, `resume()`/`interrupted()`, `usageReport()`, `fork()`, `toConfig()`/`fromConfig()`, the `workers` getter. | callers, workers, scheduler |
8
- | `loop.ts` | `runAgentLoop` / `streamAgentLoop`: per turn budget, pairing repair, file access, provider call, tool execution, repeat guard. Also `ensureToolPairing`, `accumulateUsage`. | `Agent` |
9
- | `context.ts` | `AgentContext`: the validated message list with checkpoint/rollback. | `Agent`, loops |
10
- | `delegation.ts` | Fixed workers (`agentToTool`, `buildAgentTools`), the live worker trace store, session-id helpers, budget helpers, `openWorkerTrace`/`workerMetadata`. | `Agent`, `workers.ts` |
11
- | `worker.ts` | Worker core: model/tool normalization, `buildDynamicWorkerConfig` (shared preset), `createLiveWorker`. | `Agent`, registry, scheduler |
12
- | `workers.ts` | `WorkerRegistry` (`agent.workers`): persistent multi-turn workers. | `Agent`, `subagents` tool |
13
- | `subagents-tool.ts` + `subagents.md` | The one model-facing `subagents` tool; the `.md` is its description. | `Agent` constructor |
14
- | `subagent.ts` | `SubAgent` class and `agentFromEnv` (thin, kept for compatibility). | callers |
15
- | `turns.ts` | Pure grouping of messages into finished turns (history cut for workers). | `workers.ts` |
16
- | `config-blocks.ts` | RSI blocks: only `model`, `instructions`, `tools` are editable; `allowedModels` check. | `Agent` |
5
+ | File | What it is | Used by |
6
+ | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- |
7
+ | `agent.ts` | `Agent`: `run`/`stream`/`ask`, `steer`/`queue`/`interrupt`, persistence hooks, `resume()`/`interrupted()`, `usageReport()`, `fork()`, `toConfig()`/`fromConfig()`, the `workers` getter. | callers, workers, scheduler |
8
+ | `loop.ts` | `runAgentLoop` / `streamAgentLoop`: per turn budget, pairing repair, file access, provider call, tool execution, repeat guard. Also `ensureToolPairing`, `accumulateUsage`. | `Agent` |
9
+ | `context.ts` | `AgentContext`: the validated message list with checkpoint/rollback. | `Agent`, loops |
10
+ | `delegation.ts` | Fixed workers (`agentToTool`, `buildAgentTools`), the live worker trace store, session-id helpers, budget helpers, `openWorkerTrace`/`workerMetadata`. | `Agent`, `workers.ts` |
11
+ | `worker.ts` | Worker core: model/tool normalization, `buildDynamicWorkerConfig` (shared preset), `createLiveWorker`. | `Agent`, registry, scheduler |
12
+ | `workers.ts` | `WorkerRegistry` (`agent.workers`): persistent multi-turn workers. | `Agent`, `subagents` tool |
13
+ | `subagents-tool.ts` | The one model-facing `subagents` tool; its description is the `subagentsTool` prompt (`../prompts/subagents-tool.md`). | `Agent` constructor |
14
+ | `subagent.ts` | `SubAgent` class and `agentFromEnv` (thin, kept for compatibility). | callers |
15
+ | `turns.ts` | Pure grouping of messages into finished turns (history cut for workers). | `workers.ts` |
16
+ | `defaults.ts` | `configure()` and the `AGENT_ACCEL_*` env vars: developer defaults for `usageLimit` and `dynamicSubagents.scheduledActionBudget`, per field (Agent config > `configure()` > env). SDK-built workers never take them (`isWorkerConfig`). | `Agent`, barrel |
17
+ | `usage-limit.ts` | `UsageLimit`: the chat-wide cap (`AgentConfig.usageLimit`). The root agent owns it, registry and fixed workers share it (`agent.chat`); every model turn adds to it and is checked against it. | `Agent`, `workers.ts`, `delegation.ts` |
18
+ | `config-blocks.ts` | RSI blocks: only `model`, `instructions`, `tools` are editable; `allowedModels` check. | `Agent` |
17
19
 
18
20
  ## Invariants
21
+
19
22
  - `runAgentLoop` and `streamAgentLoop` duplicate their turn logic: every loop change goes into both.
20
23
  - An `Agent` runs one thing at a time; delegation forks a copy per call so parallel calls never queue.
21
24
  - A worker is a plain `Agent` with a parent link and presets. Never duplicate logic between Agent and worker.
22
25
  - A worker is never less constrained than its parent (`inheritableConfig()`); its children draw only from the tools it was granted.
23
26
  - Exactly-once worker calls rest on the call key (parent run mark id + tool call id) and `meta.last`.
24
27
  - Only results branded by `makeSubagentResult` have their usage metadata trusted.
28
+ - A chat-wide cap (`usageLimit`) counts every model turn of the root and of all workers through one shared `UsageLimit`; its starting point is read once per process from the saved sessions. Scheduled workers never get it.
25
29
  - Each Agent records only its own model turns; never sum `AgentResponse.usage` for totals.
26
30
  - `reset()` clears the conversation (messages, run marks, the run's saved tool results) but never the spending totals.
@@ -22,25 +22,37 @@ export function modelKey(model: unknown): string | undefined {
22
22
  export function assertModelAllowed(allowed: unknown, model: string | undefined): void {
23
23
  if (allowed === undefined) return;
24
24
  if (!Array.isArray(allowed) || allowed.length === 0 || allowed.some((m) => typeof m !== "string" || !m.trim())) {
25
- throw new ConfigError(`[Agent Accelerator] Invalid allowedModels ${JSON.stringify(allowed)}: expected a non-empty list of "provider/model" strings, or omit it to allow any model.`);
25
+ throw new ConfigError(
26
+ `[Agent Accelerator] Invalid allowedModels ${JSON.stringify(allowed)}: expected a non-empty list of "provider/model" strings, or omit it to allow any model.`
27
+ );
26
28
  }
27
29
  if (!model || !allowed.includes(model)) {
28
- throw new ConfigError(`[Agent Accelerator] Model ${JSON.stringify(model ?? null)} is not in allowedModels. Allowed: ${allowed.map((m) => JSON.stringify(m)).join(", ")}.`);
30
+ throw new ConfigError(
31
+ `[Agent Accelerator] Model ${JSON.stringify(model ?? null)} is not in allowedModels. Allowed: ${allowed.map((m) => JSON.stringify(m)).join(", ")}.`
32
+ );
29
33
  }
30
34
  }
31
35
 
32
36
  // The editable part of an agent as plain JSON; in: the Agent's model, instructions and the developer's tool list (not the built-in delegation/scheduler tools), out: Agent.toConfig.
33
- export function toEditableConfig(model: unknown, instructions: string, tools: AgentConfig["tools"]): Required<EditableAgentConfig> {
37
+ export function toEditableConfig(
38
+ model: unknown,
39
+ instructions: string,
40
+ tools: AgentConfig["tools"]
41
+ ): Required<EditableAgentConfig> {
34
42
  return { model: modelKey(model) ?? "", instructions, tools: Object.keys(normalizeToolMap(tools)) };
35
43
  }
36
44
 
37
45
  // Merges what an agent edited into the developer's config: only model/instructions/tools can change; tools are names picked from base.tools; the model is checked against allowedModels when the Agent is built; in: Agent.fromConfig args, out: the AgentConfig for `new Agent`.
38
46
  export function applyEditable(editable: EditableAgentConfig, base: AgentConfig): AgentConfig {
39
47
  if (!editable || typeof editable !== "object" || Array.isArray(editable)) {
40
- throw new ConfigError(`[Agent Accelerator] Invalid editable config ${JSON.stringify(editable)}: expected an object like { model, instructions, tools } (see Agent.toConfig()).`);
48
+ throw new ConfigError(
49
+ `[Agent Accelerator] Invalid editable config ${JSON.stringify(editable)}: expected an object like { model, instructions, tools } (see Agent.toConfig()).`
50
+ );
41
51
  }
42
52
  const e = editable as Record<string, unknown>;
43
- const ignored = Object.keys(e).filter((k) => !(EDITABLE_FIELDS as readonly string[]).includes(k) && e[k] !== undefined);
53
+ const ignored = Object.keys(e).filter(
54
+ (k) => !(EDITABLE_FIELDS as readonly string[]).includes(k) && e[k] !== undefined
55
+ );
44
56
  if (ignored.length > 0) {
45
57
  telemetryBus.emitWarning(
46
58
  "config.not_editable",
@@ -51,26 +63,41 @@ export function applyEditable(editable: EditableAgentConfig, base: AgentConfig):
51
63
  const out: AgentConfig = { ...base };
52
64
  if (e.model !== undefined) {
53
65
  const next = typeof e.model === "string" ? e.model.trim() : "";
54
- if (!next) throw new ConfigError(`[Agent Accelerator] Invalid editable model ${JSON.stringify(e.model)}: expected a "provider/model" string.`);
66
+ if (!next)
67
+ throw new ConfigError(
68
+ `[Agent Accelerator] Invalid editable model ${JSON.stringify(e.model)}: expected a "provider/model" string.`
69
+ );
55
70
  // A model helper's key and base URL belong to its own provider: they are never sent to another one.
56
71
  const prev = base.model as { model?: unknown; apiKey?: string; baseUrl?: string } | string | undefined;
57
72
  const sameProvider = next.includes("/") && next.split("/")[0] === modelKey(prev)?.split("/")[0];
58
- out.model = prev && typeof prev === "object" && typeof prev.model === "string"
59
- ? { ...prev, model: next, ...(sameProvider ? {} : { apiKey: undefined, baseUrl: undefined }) }
60
- : next;
73
+ out.model =
74
+ prev && typeof prev === "object" && typeof prev.model === "string"
75
+ ? { ...prev, model: next, ...(sameProvider ? {} : { apiKey: undefined, baseUrl: undefined }) }
76
+ : next;
61
77
  }
62
78
  if (e.instructions !== undefined) {
63
- if (typeof e.instructions !== "string") throw new ConfigError(`[Agent Accelerator] Invalid editable instructions: expected a string, got ${typeof e.instructions}.`);
79
+ if (typeof e.instructions !== "string")
80
+ throw new ConfigError(
81
+ `[Agent Accelerator] Invalid editable instructions: expected a string, got ${typeof e.instructions}.`
82
+ );
64
83
  out.instructions = e.instructions;
65
84
  }
66
85
  if (e.tools !== undefined) {
67
86
  if (!Array.isArray(e.tools) || e.tools.some((n) => typeof n !== "string")) {
68
- throw new ConfigError(`[Agent Accelerator] Invalid editable tools ${JSON.stringify(e.tools)}: expected a list of tool names from the developer's tool pool.`);
87
+ throw new ConfigError(
88
+ `[Agent Accelerator] Invalid editable tools ${JSON.stringify(e.tools)}: expected a list of tool names from the developer's tool pool.`
89
+ );
69
90
  }
70
91
  const pool = normalizeToolMap(base.tools);
71
92
  const unknown = (e.tools as string[]).filter((n) => !Object.hasOwn(pool, n));
72
93
  if (unknown.length > 0) {
73
- throw new ConfigError(`[Agent Accelerator] Unknown tool ${unknown.map((n) => JSON.stringify(n)).join(", ")} in the editable config. Available: ${Object.keys(pool).map((n) => JSON.stringify(n)).join(", ") || "none"}.`);
94
+ throw new ConfigError(
95
+ `[Agent Accelerator] Unknown tool ${unknown.map((n) => JSON.stringify(n)).join(", ")} in the editable config. Available: ${
96
+ Object.keys(pool)
97
+ .map((n) => JSON.stringify(n))
98
+ .join(", ") || "none"
99
+ }.`
100
+ );
74
101
  }
75
102
  const picked: Record<string, ToolDefinition> = {};
76
103
  for (const n of e.tools as string[]) picked[n] = pool[n]!;
@@ -3,12 +3,13 @@ import type { ToolCallRecord, ToolResultRecord } from "../types/tool.ts";
3
3
  import { safeStringify } from "../utils/serialization.ts";
4
4
  import { clearSessionRouting } from "../providers.ts";
5
5
  import { clearInteractionChains } from "../providers/google.ts";
6
+ import { ValidationError } from "../types/errors.ts";
6
7
 
7
8
  const USER_PART_TYPES = new Set(["text", "image", "audio", "video", "file"]);
8
9
 
9
10
  // Throws the AgentContext validation error with the SDK prefix; in: the add* validators, out: a thrown Error.
10
11
  function fail(message: string): never {
11
- throw new Error(`[Agent Accelerator] AgentContext: ${message}`);
12
+ throw new ValidationError(`[Agent Accelerator] AgentContext: ${message}`);
12
13
  }
13
14
 
14
15
  /** Deep clone that preserves Uint8Array/ArrayBuffer media (structuredClone may be absent). */
@@ -142,6 +143,7 @@ export class AgentContext {
142
143
  isError: res.isError,
143
144
  ...(res.logFile ? { logFile: res.logFile } : {}),
144
145
  ...(res.stopped ? { stopped: true } : {}),
146
+ ...(res.parts && res.parts.length > 0 ? { parts: res.parts } : {}),
145
147
  },
146
148
  ],
147
149
  });
@@ -192,8 +194,12 @@ export class AgentContext {
192
194
  if (Array.isArray(cp?.messages)) this.messages = deepCloneValue(cp.messages);
193
195
  if (Array.isArray(cp?.signatures)) this.thoughtSignatures = [...cp.signatures];
194
196
  if (sessionId) {
195
- try { clearInteractionChains(sessionId); } catch {}
196
- try { clearSessionRouting(sessionId); } catch {}
197
+ try {
198
+ clearInteractionChains(sessionId);
199
+ } catch {}
200
+ try {
201
+ clearSessionRouting(sessionId);
202
+ } catch {}
197
203
  }
198
204
  }
199
205
  }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Developer defaults for the spending caps, set in code (`configure()`) or through env vars, so one place can cap every
3
+ * agent of a program. Precedence, per field (`maxCostUsd`, `maxTotalTokens`): the Agent's own config, then `configure()`,
4
+ * then the env var, then no cap. Only agents YOU construct take defaults: the workers and scheduled runs the SDK builds
5
+ * itself never do (a chat worker joins its parent's cap; a scheduled run has its own).
6
+ */
7
+ import { ConfigError } from "../types/errors.ts";
8
+ import { getEnv } from "../utils/env.ts";
9
+ import { checkUsageLimitConfig, type UsageLimitConfig } from "./usage-limit.ts";
10
+
11
+ /** What `configure()` can set; every key is optional and has the same meaning as the Agent option of the same name. */
12
+ export interface AgentDefaults {
13
+ /** Default for `AgentConfig.usageLimit` (the chat-wide cap). Env: `AGENT_ACCEL_USAGE_LIMIT_USD`, `AGENT_ACCEL_USAGE_LIMIT_TOKENS`. */
14
+ usageLimit?: UsageLimitConfig;
15
+ /** Default for `dynamicSubagents.scheduledActionBudget`. Env: `AGENT_ACCEL_SCHEDULED_BUDGET_USD`, `AGENT_ACCEL_SCHEDULED_BUDGET_TOKENS`. */
16
+ scheduledActionBudget?: UsageLimitConfig;
17
+ }
18
+
19
+ type CapName = keyof AgentDefaults;
20
+
21
+ const ENV: Record<CapName, { maxCostUsd: string; maxTotalTokens: string }> = {
22
+ usageLimit: { maxCostUsd: "AGENT_ACCEL_USAGE_LIMIT_USD", maxTotalTokens: "AGENT_ACCEL_USAGE_LIMIT_TOKENS" },
23
+ scheduledActionBudget: {
24
+ maxCostUsd: "AGENT_ACCEL_SCHEDULED_BUDGET_USD",
25
+ maxTotalTokens: "AGENT_ACCEL_SCHEDULED_BUDGET_TOKENS",
26
+ },
27
+ };
28
+
29
+ let configured: AgentDefaults = {};
30
+
31
+ /**
32
+ * Sets developer defaults for the spending caps; call it once at startup, like `setHome()`. It REPLACES what an earlier
33
+ * call set (`configure({})` clears it). A key an Agent sets itself wins, field by field; env vars are the fallback.
34
+ * Throws a `ConfigError` for a value that is not a number above 0.
35
+ *
36
+ * @example `configure({ usageLimit: { maxCostUsd: 5 }, scheduledActionBudget: { maxCostUsd: 0.5 } });`
37
+ */
38
+ export function configure(defaults: AgentDefaults = {}): void {
39
+ if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) {
40
+ throw new ConfigError("[Agent Accelerator] configure() expects an object like { usageLimit: { maxCostUsd: 5 } }.");
41
+ }
42
+ for (const key of Object.keys(defaults)) {
43
+ if (!(key in ENV)) {
44
+ throw new ConfigError(
45
+ `[Agent Accelerator] configure(): unknown setting "${key}" (known: ${Object.keys(ENV).join(", ")}).`
46
+ );
47
+ }
48
+ const value = (defaults as Record<string, unknown>)[key];
49
+ if (value === undefined) continue;
50
+ const problem = checkUsageLimitConfig(value);
51
+ if (problem) throw new ConfigError(`[Agent Accelerator] configure(): invalid ${key}: ${problem}.`);
52
+ }
53
+ configured = Object.fromEntries(
54
+ Object.entries(defaults)
55
+ .filter(([, v]) => v !== undefined)
56
+ .map(([k, v]) => [k, { ...(v as UsageLimitConfig) }])
57
+ ) as AgentDefaults;
58
+ }
59
+
60
+ // Reads one cap field from its env var: undefined when unset, a ConfigError naming the variable when it is not a number above 0; in: mergeCap, out: the value.
61
+ function fromEnv(variable: string): number | undefined {
62
+ const raw = getEnv(variable);
63
+ if (raw === undefined) return undefined;
64
+ const n = Number(raw);
65
+ if (!(Number.isFinite(n) && n > 0)) {
66
+ throw new ConfigError(
67
+ `[Agent Accelerator] Invalid ${variable} ${JSON.stringify(raw)}: expected a number greater than 0.`
68
+ );
69
+ }
70
+ return n;
71
+ }
72
+
73
+ /** The cap an Agent uses: its own value, field by field over `configure()` over the env vars; undefined when nothing sets one. `withDefaults: false` ignores `configure()` and the env (SDK-built workers). */
74
+ export function mergeCap(
75
+ name: CapName,
76
+ own: UsageLimitConfig | undefined,
77
+ withDefaults: boolean
78
+ ): UsageLimitConfig | undefined {
79
+ const base = withDefaults ? configured[name] : undefined;
80
+ const maxCostUsd = own?.maxCostUsd ?? base?.maxCostUsd ?? (withDefaults ? fromEnv(ENV[name].maxCostUsd) : undefined);
81
+ const maxTotalTokens =
82
+ own?.maxTotalTokens ?? base?.maxTotalTokens ?? (withDefaults ? fromEnv(ENV[name].maxTotalTokens) : undefined);
83
+ if (maxCostUsd === undefined && maxTotalTokens === undefined) return undefined;
84
+ return {
85
+ ...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
86
+ ...(maxTotalTokens !== undefined ? { maxTotalTokens } : {}),
87
+ };
88
+ }
@@ -15,6 +15,7 @@ import { nowMs } from "../utils/session.ts";
15
15
  import { newRunId } from "../utils/session.ts";
16
16
  import { newTurnId } from "../utils/session.ts";
17
17
  import { sanitizeToolName, createLiveWorker } from "./worker.ts";
18
+ import type { UsageLimit } from "./usage-limit.ts";
18
19
 
19
20
  /** Builds a random child session id that fits the 64-char provider limit, embedding a hash of the parent when it must be cut; in: a parent id + tag from scheduled runs, out: the worker's session id. */
20
21
  export function createChildSessionId(parentId: string, tag: string): string {
@@ -61,7 +62,7 @@ export function createFixedChildSessionId(parentId: string, name: string): strin
61
62
  return `${parentId.slice(0, maxParent - 9)}-${parentHash}${suffix}`;
62
63
  }
63
64
  if (maxParent > 0) return parentId.slice(0, maxParent) + suffix;
64
- return (`${parentId}-sub-${name}`).slice(0, 64);
65
+ return `${parentId}-sub-${name}`.slice(0, 64);
65
66
  }
66
67
 
67
68
  /**
@@ -225,7 +226,9 @@ function notifyTrace(trackingId: string): void {
225
226
  if (!listeners || listeners.size === 0) return;
226
227
  const snap = snapshotTrace(t);
227
228
  for (const fn of [...listeners]) {
228
- try { fn(snap); } catch {}
229
+ try {
230
+ fn(snap);
231
+ } catch {}
229
232
  }
230
233
  }
231
234
 
@@ -280,15 +283,23 @@ export function subscribeToSubAgent(id: string, fn: (trace: SubAgentTrace) => vo
280
283
  }
281
284
  set.add(fn);
282
285
  const timer = setTimeout(() => {
283
- try { subagentListeners.get(key)?.delete(fn); } catch {}
286
+ try {
287
+ subagentListeners.get(key)?.delete(fn);
288
+ } catch {}
284
289
  }, SUBAGENT_SUBSCRIBE_TTL_MS);
285
- try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
290
+ try {
291
+ (timer as unknown as { unref?: () => void }).unref?.();
292
+ } catch {}
286
293
  let done = false;
287
294
  return () => {
288
295
  if (done) return;
289
296
  done = true;
290
- try { clearTimeout(timer); } catch {}
291
- try { subagentListeners.get(key)?.delete(fn); } catch {}
297
+ try {
298
+ clearTimeout(timer);
299
+ } catch {}
300
+ try {
301
+ subagentListeners.get(key)?.delete(fn);
302
+ } catch {}
292
303
  };
293
304
  }
294
305
 
@@ -334,7 +345,13 @@ export function openWorkerTrace(a: {
334
345
  role?: string;
335
346
  /** When given, the trace is also kept in this agent's own registry and saved with its session. */
336
347
  parent?: Agent;
337
- emit?: (event: { trackingId: string; delta?: string; thinkingDelta?: string; partialText?: string; partialThinking?: string }) => void;
348
+ emit?: (event: {
349
+ trackingId: string;
350
+ delta?: string;
351
+ thinkingDelta?: string;
352
+ partialText?: string;
353
+ partialThinking?: string;
354
+ }) => void;
338
355
  }): WorkerTraceHandle {
339
356
  const trackingId = createTrackingId();
340
357
  const displayId = `${a.name}-${trackingId}`;
@@ -360,9 +377,19 @@ export function openWorkerTrace(a: {
360
377
  trim: (t) => trimTraceSteps(t),
361
378
  touch: (id) => touchTrace(id),
362
379
  notify: (id) => notifyTrace(id),
363
- ...(a.parent ? { persist: () => { try { a.parent!.persistNow(); } catch {} } } : {}),
380
+ ...(a.parent
381
+ ? {
382
+ persist: () => {
383
+ try {
384
+ a.parent!.persistNow();
385
+ } catch {}
386
+ },
387
+ }
388
+ : {}),
364
389
  emit: (ev) => {
365
- try { a.emit?.(ev); } catch {}
390
+ try {
391
+ a.emit?.(ev);
392
+ } catch {}
366
393
  },
367
394
  });
368
395
  const settle = (): void => {
@@ -386,7 +413,11 @@ export function openWorkerTrace(a: {
386
413
  t.turns = result.turns ?? t.turns;
387
414
  t.text = result.text;
388
415
  if (result.usage) {
389
- t.usage = { inputTokens: result.usage.inputTokens ?? 0, outputTokens: result.usage.outputTokens ?? 0, totalTokens: result.usage.totalTokens ?? 0 };
416
+ t.usage = {
417
+ inputTokens: result.usage.inputTokens ?? 0,
418
+ outputTokens: result.usage.outputTokens ?? 0,
419
+ totalTokens: result.usage.totalTokens ?? 0,
420
+ };
390
421
  }
391
422
  }
392
423
  settle();
@@ -492,7 +523,8 @@ function ensureBudgetState(parentAgent: Agent): BudgetState {
492
523
  function usageTokens(usage: unknown): number {
493
524
  const u = usage as { totalTokens?: unknown; inputTokens?: unknown; outputTokens?: unknown } | undefined;
494
525
  if (!u || typeof u !== "object") return 0;
495
- if (typeof u.totalTokens === "number" && Number.isFinite(u.totalTokens)) return Math.max(0, Math.floor(u.totalTokens));
526
+ if (typeof u.totalTokens === "number" && Number.isFinite(u.totalTokens))
527
+ return Math.max(0, Math.floor(u.totalTokens));
496
528
  const inp = typeof u.inputTokens === "number" && Number.isFinite(u.inputTokens) ? u.inputTokens : 0;
497
529
  const out = typeof u.outputTokens === "number" && Number.isFinite(u.outputTokens) ? u.outputTokens : 0;
498
530
  return Math.max(0, Math.floor(inp + out));
@@ -532,10 +564,9 @@ export function checkBudget(
532
564
  );
533
565
  }
534
566
  if (budget.maxTotalTokens !== undefined && totalTokens > budget.maxTotalTokens) {
535
- throw new BudgetExceededError(
536
- `Token budget exceeded: ${totalTokens} > maxTotalTokens ${budget.maxTotalTokens}.`,
537
- { context: { partial } }
538
- );
567
+ throw new BudgetExceededError(`Token budget exceeded: ${totalTokens} > maxTotalTokens ${budget.maxTotalTokens}.`, {
568
+ context: { partial },
569
+ });
539
570
  }
540
571
  if (budget.maxCostUsd !== undefined && totalCostUsd > budget.maxCostUsd) {
541
572
  throw new BudgetExceededError(
@@ -601,9 +632,7 @@ export interface AgentAsToolTarget {
601
632
  *
602
633
  * @example `const researchTool = researcher.asTool("research");`
603
634
  */
604
- export function agentToTool(
605
- input: Agent | AgentAsToolTarget
606
- ): ToolDefinition {
635
+ export function agentToTool(input: Agent | AgentAsToolTarget, opts?: { chat?: UsageLimit }): ToolDefinition {
607
636
  const agentInstance = "agent" in input ? input.agent : input;
608
637
  const rawName =
609
638
  ("name" in input && (input as any).name) ||
@@ -635,28 +664,56 @@ export function agentToTool(
635
664
  task,
636
665
  emit: (ev) => ctx?.onSubagentEvent?.(ev),
637
666
  });
638
- const lineage = { name, displayId: live.displayId, sessionId: subSessionId, parentSessionId, task, runId: callRunId, turnId: callTurnId };
667
+ const lineage = {
668
+ name,
669
+ displayId: live.displayId,
670
+ sessionId: subSessionId,
671
+ parentSessionId,
672
+ task,
673
+ runId: callRunId,
674
+ turnId: callTurnId,
675
+ };
639
676
  try {
640
677
  // One isolated copy per call: the shared instance is single-flight, so
641
678
  // calling it directly would serialize (or steer) parallel tool calls.
642
679
  const worker: Agent =
643
- typeof (agentInstance as Agent).fork === "function" ? agentInstance.fork(subSessionId, parentSessionId) : agentInstance;
644
- const workerStream = worker.stream(task, { signal: ctx?.signal, sessionId: subSessionId, onTurn: live.onWorkerTurn } as any);
645
- const detachWorkerListeners = live.attachToStream(workerStream as unknown as Parameters<typeof live.attachToStream>[0]);
680
+ typeof (agentInstance as Agent).fork === "function"
681
+ ? agentInstance.fork(subSessionId, parentSessionId)
682
+ : agentInstance;
683
+ if (worker !== agentInstance && opts?.chat) worker.chat = opts.chat; // this call counts into the parent chat's cap
684
+ const workerStream = worker.stream(task, {
685
+ signal: ctx?.signal,
686
+ sessionId: subSessionId,
687
+ onTurn: live.onWorkerTurn,
688
+ } as any);
689
+ const detachWorkerListeners = live.attachToStream(
690
+ workerStream as unknown as Parameters<typeof live.attachToStream>[0]
691
+ );
646
692
  let response: AgentResponse;
647
693
  try {
648
694
  response = await workerStream.result();
649
695
  } finally {
650
- try { detachWorkerListeners(); } catch {}
696
+ try {
697
+ detachWorkerListeners();
698
+ } catch {}
651
699
  // The per-call session id is never reused: drop its provider chain/routing state.
652
700
  if (worker !== agentInstance) {
653
- try { agentInstance.absorbUsage(worker.usageTotals); } catch {} // the template keeps what its copies spent
654
- try { worker.reset(); } catch {}
701
+ try {
702
+ agentInstance.absorbUsage(worker.usageTotals);
703
+ } catch {} // the template keeps what its copies spent
704
+ try {
705
+ worker.reset();
706
+ } catch {}
655
707
  }
656
708
  }
657
709
  live.finish(response);
658
710
  return makeSubagentResult(response.text, [
659
- workerMetadata({ ...lineage, durationMs: Math.max(0, nowMs() - startTime), result: response, steps: live.steps() }),
711
+ workerMetadata({
712
+ ...lineage,
713
+ durationMs: Math.max(0, nowMs() - startTime),
714
+ result: response,
715
+ steps: live.steps(),
716
+ }),
660
717
  ]);
661
718
  } catch (err: any) {
662
719
  const msg = err?.message || String(err);
@@ -675,13 +732,14 @@ export function agentToTool(
675
732
  * @example `const tools = buildAgentTools([researcher, reviewer]);`
676
733
  */
677
734
  export function buildAgentTools(
678
- agents?: (Agent | AgentAsToolTarget)[]
735
+ agents?: (Agent | AgentAsToolTarget)[],
736
+ opts?: { chat?: UsageLimit }
679
737
  ): Record<string, ToolDefinition> {
680
738
  if (!agents || agents.length === 0) return {};
681
739
 
682
740
  const tools: Record<string, ToolDefinition> = {};
683
741
  for (const item of agents) {
684
- const t = agentToTool(item);
742
+ const t = agentToTool(item, opts);
685
743
  if (t.name) {
686
744
  let finalName = t.name;
687
745
  let n = 1;