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
@@ -16,9 +16,7 @@ export interface SubAgentConfig extends AgentConfig {}
16
16
  * `Agent.fromEnv()` (attached below without touching `agent.ts`, owned by
17
17
  * another audit lane) delegates here.
18
18
  */
19
- export function agentFromEnv(
20
- config?: Omit<AgentConfig, "model"> & { model?: AgentConfig["model"] }
21
- ): Agent {
19
+ export function agentFromEnv(config?: Omit<AgentConfig, "model"> & { model?: AgentConfig["model"] }): Agent {
22
20
  return new Agent({
23
21
  ...config,
24
22
  model: config?.model ?? getModel() ?? getSubModel(),
@@ -1,9 +1,8 @@
1
1
  /**
2
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
3
+ * wrapper over `agent.workers`. The tool manual lives in `src/prompts/subagents-tool.md`, not in code. Results are plain text with one
4
4
  * header line naming the worker; the branded envelope under them carries usage and lineage to the loop.
5
5
  */
6
- import { readFileSync } from "node:fs";
7
6
  import { z } from "zod";
8
7
  import type { Agent } from "./agent.ts";
9
8
  import { tool } from "../tools/tool.ts";
@@ -11,18 +10,15 @@ import { makeSubagentResult } from "../tools/subagent-result.ts";
11
10
  import { ValidationError } from "../types/errors.ts";
12
11
  import type { ToolDefinition } from "../types/tool.ts";
13
12
  import type { WorkerReply } from "./workers.ts";
13
+ import { fillPrompt, type Prompts } from "../prompts/prompts.ts";
14
14
 
15
15
  const COMMANDS = ["spawn", "message", "close", "list"] as const;
16
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
17
  // 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}`;
18
+ function asResult(reply: WorkerReply, prompts: Prompts): ReturnType<typeof makeSubagentResult> {
19
+ const text = reply.error
20
+ ? fillPrompt(prompts.workerFailed, { id: reply.id, reason: reply.error })
21
+ : fillPrompt(prompts.workerReplied, { id: reply.id, text: reply.text });
26
22
  return makeSubagentResult(text, [reply.metadata]);
27
23
  }
28
24
 
@@ -32,9 +28,12 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
32
28
  const max = agent.dynamicSubagents?.maxSpawn ?? Infinity;
33
29
  return tool({
34
30
  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)"),
31
+ description: fillPrompt(agent.prompts.subagentsTool, {
32
+ LIMIT: Number.isFinite(max)
33
+ ? `You can have at most ${max} open workers. Close the ones you are done with.`
34
+ : "There is no limit on open workers. Close the ones you are done with: each one keeps its whole conversation.",
35
+ TOOLS: poolNames.length > 0 ? poolNames.join(", ") : "none (omit tools)",
36
+ }),
38
37
  input: z.object({
39
38
  command: z.enum(COMMANDS),
40
39
  id: z.string().optional().describe("The worker's full id (message, close)."),
@@ -42,15 +41,28 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
42
41
  role: z.string().optional().describe("Short persona for the worker (spawn)."),
43
42
  instructions: z.string().optional().describe("The worker's own system prompt (spawn)."),
44
43
  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)."),
44
+ tools: z
45
+ .array(z.string())
46
+ .optional()
47
+ .describe(`Worker tools to grant, from: ${poolNames.join(", ") || "none"} (spawn).`),
48
+ history: z
49
+ .number()
50
+ .int()
51
+ .min(0)
52
+ .optional()
53
+ .describe(
54
+ "How many of the first finished turns of this conversation to copy into the worker; 0 or omitted = none (spawn)."
55
+ ),
47
56
  }),
48
57
  execute: async (a, ctx) => {
49
58
  const opts = { signal: ctx.signal, callId: ctx.toolCallId, onSubagentEvent: ctx.onSubagentEvent };
50
59
  const need = (field: "id" | "name" | "task"): string => {
51
60
  const value = a[field];
52
61
  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." };
62
+ const example =
63
+ a.command === "spawn"
64
+ ? { command: "spawn", name: "researcher", task: "Do the work." }
65
+ : { command: a.command, id: "researcher-1a2b3c4d", task: "Next question." };
54
66
  throw new ValidationError(`${a.command} needs "${field}". Example: ${JSON.stringify(example)}`);
55
67
  }
56
68
  return value;
@@ -59,10 +71,21 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
59
71
  switch (a.command) {
60
72
  case "spawn":
61
73
  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)
74
+ await agent.workers.spawn(
75
+ {
76
+ name: need("name"),
77
+ role: a.role,
78
+ instructions: a.instructions,
79
+ task: need("task"),
80
+ tools: a.tools,
81
+ history: a.history,
82
+ },
83
+ opts
84
+ ),
85
+ agent.prompts
63
86
  );
64
87
  case "message":
65
- return asResult(await agent.workers.message(need("id"), need("task"), opts));
88
+ return asResult(await agent.workers.message(need("id"), need("task"), opts), agent.prompts);
66
89
  case "close":
67
90
  return `Worker ${agent.workers.close(need("id")).id} closed.`;
68
91
  case "list":
@@ -81,7 +104,15 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
81
104
  function describeWorkers(agent: Agent): string {
82
105
  const workers = agent.workers.list();
83
106
  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");
107
+ const open =
108
+ workers.length === 0
109
+ ? "none"
110
+ : workers
111
+ .map(
112
+ (w) =>
113
+ `- ${w.id} (${w.name}${w.role ? `, ${w.role}` : ""}) tools: ${w.tools.join(", ") || "none"}${w.busy ? " [busy]" : ""}`
114
+ )
115
+ .join("\n");
85
116
  const copyable = turns.length === 0 ? "none yet" : turns.map((t) => `${t.n}. ${t.prompt}`).join("\n");
86
117
  return `Open workers:\n${open}\n\nFinished turns of this conversation (history: N copies turns 1 to N):\n${copyable}`;
87
118
  }
@@ -53,7 +53,9 @@ export function historyUpTo(messages: Message[], marks: SessionRunMark[], n: num
53
53
  );
54
54
  }
55
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);
56
+ const first = marks
57
+ .filter((m) => m.i >= 0 && m.i < messages.length)
58
+ .reduce((lo, m) => Math.min(lo, m.i), messages.length);
57
59
  const kept = [...messages.slice(0, first)];
58
60
  for (const turn of turns.slice(0, n)) kept.push(...messages.slice(turn.from, turn.to));
59
61
  return deepCloneValue(kept);
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The chat-wide spending cap (`AgentConfig.usageLimit`). One `UsageLimit` belongs to the root agent of a chat and is
3
+ * handed to every agent below it (registry workers at any depth, fixed `subagents` workers), so each model turn of the
4
+ * whole tree adds to one running total and every turn is checked before it starts. The total survives a restart: the
5
+ * first run rebuilds the starting point from the saved sessions of the agent and its workers (`usageReport()`), after
6
+ * which only new turns are added. Scheduled actions are separate and never count.
7
+ */
8
+ import { BudgetExceededError } from "../types/errors.ts";
9
+
10
+ /** What the cap is made of; either part may be set, the first one reached stops the chat. */
11
+ export interface UsageLimitConfig {
12
+ maxCostUsd?: number;
13
+ maxTotalTokens?: number;
14
+ }
15
+
16
+ /** A one-line problem with a `usageLimit` value, or undefined when it is valid; in: the Agent constructor, out: its ConfigError. */
17
+ export function checkUsageLimitConfig(raw: unknown): string | undefined {
18
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
19
+ return "expected an object like { maxCostUsd, maxTotalTokens }";
20
+ const r = raw as Record<string, unknown>;
21
+ const keys = Object.keys(r).filter((k) => r[k] !== undefined);
22
+ if (keys.length === 0) return "set maxCostUsd, maxTotalTokens or both";
23
+ for (const k of keys) {
24
+ if (k !== "maxCostUsd" && k !== "maxTotalTokens") return `unknown key "${k}" (use maxCostUsd or maxTotalTokens)`;
25
+ const v = r[k];
26
+ if (!(typeof v === "number" && Number.isFinite(v) && v > 0)) return `${k} must be a number greater than 0`;
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ /** The running total of one chat and its limit. */
32
+ export class UsageLimit {
33
+ private baseTokens = 0;
34
+ private baseMicroUSD = 0;
35
+ private addedTokens = 0;
36
+ private addedMicroUSD = 0;
37
+ private started = false;
38
+
39
+ constructor(readonly limit: UsageLimitConfig) {}
40
+
41
+ /** Takes the starting point (what the chat spent before this process) once; later calls do nothing until `restart()`. */
42
+ begin(startingPoint: () => { tokens: number; microUSD: number }): void {
43
+ if (this.started) return;
44
+ this.started = true;
45
+ const s = startingPoint();
46
+ this.baseTokens = s.tokens;
47
+ this.baseMicroUSD = s.microUSD;
48
+ this.addedTokens = 0;
49
+ this.addedMicroUSD = 0;
50
+ }
51
+
52
+ /** Forgets the total (a session was imported over this agent): the next `begin` takes a new starting point. */
53
+ restart(): void {
54
+ this.started = false;
55
+ }
56
+
57
+ /** Adds one model turn of any agent in the chat. Unpriced turns add tokens only (unknown pricing is never a made-up $0 of cost). */
58
+ add(tokens: number, microUSD: number): void {
59
+ this.addedTokens += tokens;
60
+ this.addedMicroUSD += microUSD;
61
+ }
62
+
63
+ /** What the chat has spent so far. */
64
+ spent(): { tokens: number; costUsd: number } {
65
+ return {
66
+ tokens: this.baseTokens + this.addedTokens,
67
+ costUsd: (this.baseMicroUSD + this.addedMicroUSD) / 1_000_000,
68
+ };
69
+ }
70
+
71
+ /** Throws a `BudgetExceededError` ("Usage limit reached") once the chat has spent its whole limit. */
72
+ check(): void {
73
+ const { tokens, costUsd } = this.spent();
74
+ const { maxCostUsd, maxTotalTokens } = this.limit;
75
+ const byCost = maxCostUsd !== undefined && costUsd >= maxCostUsd;
76
+ const byTokens = maxTotalTokens !== undefined && tokens >= maxTotalTokens;
77
+ if (!byCost && !byTokens) return;
78
+ const parts = [
79
+ ...(maxCostUsd !== undefined ? [`$${costUsd.toFixed(6)} of $${maxCostUsd}`] : []),
80
+ ...(maxTotalTokens !== undefined ? [`${tokens} of ${maxTotalTokens} tokens`] : []),
81
+ ];
82
+ throw new BudgetExceededError(
83
+ `[Agent Accelerator] Usage limit reached: ${parts.join(", ")} spent by this chat (the agent and all its workers).`,
84
+ {
85
+ context: {
86
+ limit: "usageLimit",
87
+ partial: { usage: { inputTokens: 0, outputTokens: 0, totalTokens: tokens }, turns: 0 },
88
+ },
89
+ }
90
+ );
91
+ }
92
+ }
@@ -30,14 +30,23 @@ export interface LiveWorkerTrace {
30
30
  export function sanitizeXmlTag(raw: string): string {
31
31
  let s = raw.toUpperCase().replace(/[^A-Z0-9.-]/g, "-");
32
32
  if (!/^[A-Z]/.test(s)) s = `AGENT-${s}`;
33
- s = s.replace(/[.]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
33
+ s = s
34
+ .replace(/[.]/g, "-")
35
+ .replace(/-{2,}/g, "-")
36
+ .replace(/^-+|-+$/g, "");
34
37
  return s.slice(0, 64) || "SUBAGENT";
35
38
  }
36
39
 
37
40
  /** Sanitizes a worker name into a tool name (`lower-kebab`, 64 chars). */
38
41
  export function sanitizeToolName(raw: string): string {
39
42
  const tag = sanitizeXmlTag(raw);
40
- return tag.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").slice(0, 64) || "sub-agent";
43
+ return (
44
+ tag
45
+ .toLowerCase()
46
+ .replace(/[^a-z0-9-]/g, "-")
47
+ .replace(/-{2,}/g, "-")
48
+ .slice(0, 64) || "sub-agent"
49
+ );
41
50
  }
42
51
 
43
52
  // ---------------------------------------------------------------------------
@@ -91,13 +100,8 @@ export function resolveMainModel(configModel: AgentConfig["model"]): {
91
100
  * `dynamicSubagents.model ?? subagentModel ?? SUB_AGENT_MODEL`.
92
101
  * Unwraps `ModelProviderInstance` / `ModelSpec` the same way.
93
102
  */
94
- export function resolveDynamicWorkerModel(
95
- dynModelRawInput: unknown
96
- ): string | Record<string, unknown> | undefined {
97
- const dynModelRaw = dynModelRawInput as
98
- | { model?: unknown; id?: unknown; provider?: unknown }
99
- | string
100
- | undefined;
103
+ export function resolveDynamicWorkerModel(dynModelRawInput: unknown): string | Record<string, unknown> | undefined {
104
+ const dynModelRaw = dynModelRawInput as { model?: unknown; id?: unknown; provider?: unknown } | string | undefined;
101
105
  let dynModel: string | Record<string, unknown> | undefined;
102
106
  if (dynModelRaw && typeof dynModelRaw === "object" && "model" in (dynModelRaw as Record<string, unknown>)) {
103
107
  dynModel = (dynModelRaw as { model: string }).model as unknown as string | Record<string, unknown>;
@@ -109,7 +113,7 @@ export function resolveDynamicWorkerModel(
109
113
  ) {
110
114
  dynModel = dynModelRaw as Record<string, unknown>;
111
115
  } else {
112
- dynModel = (dynModelRaw as string | undefined) || getSubModel() as string | undefined;
116
+ dynModel = (dynModelRaw as string | undefined) || (getSubModel() as string | undefined);
113
117
  }
114
118
  return dynModel;
115
119
  }
@@ -132,7 +136,10 @@ export function resolveDynamicModelInput(
132
136
  * `config.model ?? SUB_AGENT_MODEL ?? MODEL`, throwing with the same message
133
137
  * when nothing resolves. Kept here so `Agent` / `SubAgent` share one chain.
134
138
  */
135
- export function resolveSubAgentModel(config: { name?: string; model?: AgentConfig["model"] }): NonNullable<AgentConfig["model"]> {
139
+ export function resolveSubAgentModel(config: {
140
+ name?: string;
141
+ model?: AgentConfig["model"];
142
+ }): NonNullable<AgentConfig["model"]> {
136
143
  const resolvedModel = config.model ?? getSubModel() ?? getModel();
137
144
  if (!resolvedModel) {
138
145
  throw new ConfigError(
@@ -191,8 +198,10 @@ export function grantWorkerTools(
191
198
  if (declared) poolByNormalized.set(normalizeToolName(declared), def);
192
199
  }
193
200
  const grantedTools: Record<string, ToolDefinition> = {};
194
- for (const toolName of ((requested as string[] | undefined) ?? [])) {
195
- const pooled = (toolPool as Record<string, ToolDefinition>)[toolName] ?? poolByNormalized.get(normalizeToolName(String(toolName)));
201
+ for (const toolName of (requested as string[] | undefined) ?? []) {
202
+ const pooled =
203
+ (toolPool as Record<string, ToolDefinition>)[toolName] ??
204
+ poolByNormalized.get(normalizeToolName(String(toolName)));
196
205
  if (pooled) grantedTools[(pooled as ToolDefinition).name || toolName] = pooled;
197
206
  }
198
207
  return grantedTools;
@@ -205,9 +214,9 @@ function providerOf(modelStr: string | unknown | undefined): string | undefined
205
214
  const m =
206
215
  typeof modelStr === "string"
207
216
  ? modelStr
208
- : (modelStr as { model?: unknown; id?: unknown })?.model ??
217
+ : ((modelStr as { model?: unknown; id?: unknown })?.model ??
209
218
  (modelStr as { id?: unknown })?.id ??
210
- String(modelStr);
219
+ String(modelStr));
211
220
  return resolveModel(m as string).provider.id;
212
221
  } catch {
213
222
  return undefined;
@@ -225,9 +234,9 @@ export function resolveWorkerCredentials(
225
234
  const chosenModelStrForProvider =
226
235
  typeof chosenModel === "string"
227
236
  ? chosenModel
228
- : (chosenModel as { model?: unknown; id?: unknown })?.model ??
237
+ : ((chosenModel as { model?: unknown; id?: unknown })?.model ??
229
238
  (chosenModel as { id?: unknown })?.id ??
230
- String(chosenModel);
239
+ String(chosenModel));
231
240
  const attemptedProvider = providerOf(chosenModelStrForProvider);
232
241
  const parentProvider = providerOf(parentAgent.modelStringOrSpec as string);
233
242
  const targetProviderFinal = providerOf(chosenModelStrForProvider);
@@ -237,6 +246,14 @@ export function resolveWorkerCredentials(
237
246
  return { apiKeyToUse: undefined, baseUrlToUse: undefined, attemptedProvider };
238
247
  }
239
248
 
249
+ // Configs built by `buildDynamicWorkerConfig`: the Agent constructor checks it so SDK-built workers never take `configure()` or env defaults.
250
+ const workerConfigs = new WeakSet<object>();
251
+
252
+ /** True for a config made by `buildDynamicWorkerConfig` (a chat worker or a scheduled run), not by the developer. */
253
+ export function isWorkerConfig(config: object): boolean {
254
+ return workerConfigs.has(config);
255
+ }
256
+
240
257
  /** How many levels of workers may exist below an agent unless `dynamicSubagents.maxDepth` says otherwise: unlimited. */
241
258
  export const DEFAULT_MAX_DEPTH = Infinity;
242
259
 
@@ -247,7 +264,7 @@ export const DEFAULT_MAX_DEPTH = Infinity;
247
264
  * `persistent` makes a multi-turn worker that keeps its history (saved under `persistent.dir` when given, and
248
265
  * continued by `resume()` after a crash, never automatically). A persistent worker with levels left below it
249
266
  * (`maxDepth` of the parent, minus one for itself) gets its own `subagents` tool; its children can only be granted
250
- * the tools it was granted. One-shot workers never spawn. `dynamicSubagents.workerBudget` becomes each worker's own
267
+ * the tools it was granted. One-shot workers never spawn. `dynamicSubagents.workerBudget` becomes each chat worker's own
251
268
  * per-run `budget` (and is passed to its children).
252
269
  */
253
270
  export function buildDynamicWorkerConfig(
@@ -265,7 +282,7 @@ export function buildDynamicWorkerConfig(
265
282
  const { apiKeyToUse, baseUrlToUse } = resolveWorkerCredentials(parent, model);
266
283
  const dyn = parent.dynamicSubagents;
267
284
  const levelsBelow = (dyn?.maxDepth ?? DEFAULT_MAX_DEPTH) - 1;
268
- return {
285
+ const config: AgentConfig = {
269
286
  name: worker.name,
270
287
  description: worker.role || `Sub-agent ${worker.name}`,
271
288
  instructions: worker.instructions,
@@ -275,7 +292,11 @@ export function buildDynamicWorkerConfig(
275
292
  thinkingLevel: parent.dynamicSubagents?.thinkingLevel ?? parent.thinkingConfig?.level,
276
293
  sessionId: worker.sessionId,
277
294
  tools: worker.tools,
278
- ...(dyn?.workerBudget ? { budget: { ...dyn.workerBudget } } : {}),
295
+ // A chat worker (persistent) is capped by `workerBudget`; a one-shot scheduled run only by `scheduledActionBudget`.
296
+ ...(() => {
297
+ const cap = worker.persistent ? dyn?.workerBudget : dyn?.scheduledActionBudget;
298
+ return cap ? { budget: { ...cap } } : {};
299
+ })(),
279
300
  stateless: !worker.persistent,
280
301
  ...(worker.persistent?.dir ? { persist: { dir: worker.persistent.dir }, resume: "explicit" as const } : {}),
281
302
  ...(worker.persistent && levelsBelow >= 1
@@ -296,6 +317,8 @@ export function buildDynamicWorkerConfig(
296
317
  maxTurns: parent.maxTurns,
297
318
  ...parent.inheritableConfig(),
298
319
  };
320
+ workerConfigs.add(config);
321
+ return config;
299
322
  }
300
323
 
301
324
  // ---------------------------------------------------------------------------
@@ -380,10 +403,18 @@ export function createLiveWorker(
380
403
  trace.steps.push({ turn: turn.turns, type: "tool_result", name: tr.name, isError: tr.isError, timestamp });
381
404
  }
382
405
  trace.turns = Math.max(trace.turns, turn.turns);
383
- try { hooks.trim?.(trace); } catch {}
384
- try { hooks.touch?.(trace.trackingId); } catch {}
385
- try { hooks.notify?.(trackingId); } catch {}
386
- try { hooks.persist?.(); } catch {}
406
+ try {
407
+ hooks.trim?.(trace);
408
+ } catch {}
409
+ try {
410
+ hooks.touch?.(trace.trackingId);
411
+ } catch {}
412
+ try {
413
+ hooks.notify?.(trackingId);
414
+ } catch {}
415
+ try {
416
+ hooks.persist?.();
417
+ } catch {}
387
418
  } catch {}
388
419
  };
389
420
 
@@ -409,8 +440,12 @@ export function createLiveWorker(
409
440
  entry.thinking = partialThinking;
410
441
  entry.timestamp = Date.now();
411
442
  }
412
- try { hooks.trim?.(trace); } catch {}
413
- try { hooks.notify?.(trackingId); } catch {}
443
+ try {
444
+ hooks.trim?.(trace);
445
+ } catch {}
446
+ try {
447
+ hooks.notify?.(trackingId);
448
+ } catch {}
414
449
  try {
415
450
  hooks.emit?.({
416
451
  trackingId: displayId,
@@ -429,11 +464,19 @@ export function createLiveWorker(
429
464
  }): (() => void) => {
430
465
  const textListener = (e: { delta?: string }): void => onWorkerDelta(e.delta, undefined);
431
466
  const thinkingListener = (e: { thinkingDelta?: string }): void => onWorkerDelta(undefined, e.thinkingDelta);
432
- try { workerStream.on("text_delta", textListener as (e: { delta?: string; thinkingDelta?: string }) => void); } catch {}
433
- try { workerStream.on("thinking_delta", thinkingListener as (e: { delta?: string; thinkingDelta?: string }) => void); } catch {}
467
+ try {
468
+ workerStream.on("text_delta", textListener as (e: { delta?: string; thinkingDelta?: string }) => void);
469
+ } catch {}
470
+ try {
471
+ workerStream.on("thinking_delta", thinkingListener as (e: { delta?: string; thinkingDelta?: string }) => void);
472
+ } catch {}
434
473
  return () => {
435
- try { workerStream.off("text_delta", textListener as (e: { delta?: string; thinkingDelta?: string }) => void); } catch {}
436
- try { workerStream.off("thinking_delta", thinkingListener as (e: { delta?: string; thinkingDelta?: string }) => void); } catch {}
474
+ try {
475
+ workerStream.off("text_delta", textListener as (e: { delta?: string; thinkingDelta?: string }) => void);
476
+ } catch {}
477
+ try {
478
+ workerStream.off("thinking_delta", thinkingListener as (e: { delta?: string; thinkingDelta?: string }) => void);
479
+ } catch {}
437
480
  };
438
481
  };
439
482