agent-accelerator 0.5.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +3 -1
  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 +366 -161
  7. package/src/agent/architecture.md +13 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +8 -3
  10. package/src/agent/delegation.ts +82 -29
  11. package/src/agent/loop.ts +444 -365
  12. package/src/agent/subagent.ts +1 -3
  13. package/src/agent/subagents-tool.ts +45 -7
  14. package/src/agent/subagents.md +3 -0
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/worker.ts +56 -27
  17. package/src/agent/workers.ts +113 -20
  18. package/src/architecture.md +16 -15
  19. package/src/index.ts +34 -10
  20. package/src/models/architecture.md +4 -3
  21. package/src/models/catalog-cache.ts +6 -5
  22. package/src/models/catalog.ts +39 -30
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +147 -59
  25. package/src/providers/openai-compat.ts +57 -49
  26. package/src/providers/openai.ts +54 -37
  27. package/src/providers/openrouter.ts +85 -30
  28. package/src/providers/registry.ts +6 -21
  29. package/src/providers/shared.ts +87 -33
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +7 -6
  32. package/src/scheduler/scheduled-actions.md +4 -0
  33. package/src/scheduler/scheduler.ts +186 -60
  34. package/src/scheduler/store.ts +23 -10
  35. package/src/scheduler/time.ts +17 -3
  36. package/src/scheduler/tool.ts +33 -7
  37. package/src/session/architecture.md +7 -6
  38. package/src/session/journal.ts +38 -12
  39. package/src/session/store.ts +122 -61
  40. package/src/session/usage.ts +58 -9
  41. package/src/streaming/architecture.md +4 -3
  42. package/src/streaming/event-stream.ts +26 -12
  43. package/src/telemetry.ts +6 -2
  44. package/src/tools/architecture.md +8 -7
  45. package/src/tools/executor.ts +50 -48
  46. package/src/tools/schema.ts +10 -6
  47. package/src/tools/subagent-result.ts +6 -1
  48. package/src/tools/tool.ts +9 -11
  49. package/src/types/agent.ts +2 -21
  50. package/src/types/architecture.md +11 -10
  51. package/src/types/core.ts +1 -9
  52. package/src/types/errors.ts +26 -4
  53. package/src/types/model.ts +0 -4
  54. package/src/types/tool.ts +12 -2
  55. package/src/update-models.ts +5 -5
  56. package/src/utils/architecture.md +16 -15
  57. package/src/utils/cache.ts +1 -3
  58. package/src/utils/durable.ts +17 -5
  59. package/src/utils/env.ts +9 -7
  60. package/src/utils/errors.ts +6 -14
  61. package/src/utils/headers.ts +3 -10
  62. package/src/utils/media.ts +23 -8
  63. package/src/utils/raw-capture.ts +9 -2
  64. package/src/utils/retry.ts +15 -3
  65. package/src/utils/safe-io.ts +29 -7
  66. package/src/utils/serialization.ts +1 -4
  67. 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(),
@@ -22,7 +22,9 @@ function loadManual(): string {
22
22
 
23
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
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}`;
25
+ const text = reply.error
26
+ ? `[worker ${reply.id}] failed: ${reply.error}`
27
+ : `[worker ${reply.id}] replied:\n${reply.text}`;
26
28
  return makeSubagentResult(text, [reply.metadata]);
27
29
  }
28
30
 
@@ -33,7 +35,12 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
33
35
  return tool({
34
36
  name: "subagents",
35
37
  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.")
38
+ .replace(
39
+ "{{LIMIT}}",
40
+ Number.isFinite(max)
41
+ ? `You can have at most ${max} open workers. Close the ones you are done with.`
42
+ : "There is no limit on open workers. Close the ones you are done with: each one keeps its whole conversation."
43
+ )
37
44
  .replace("{{TOOLS}}", poolNames.length > 0 ? poolNames.join(", ") : "none (omit tools)"),
38
45
  input: z.object({
39
46
  command: z.enum(COMMANDS),
@@ -42,15 +49,28 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
42
49
  role: z.string().optional().describe("Short persona for the worker (spawn)."),
43
50
  instructions: z.string().optional().describe("The worker's own system prompt (spawn)."),
44
51
  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)."),
52
+ tools: z
53
+ .array(z.string())
54
+ .optional()
55
+ .describe(`Worker tools to grant, from: ${poolNames.join(", ") || "none"} (spawn).`),
56
+ history: z
57
+ .number()
58
+ .int()
59
+ .min(0)
60
+ .optional()
61
+ .describe(
62
+ "How many of the first finished turns of this conversation to copy into the worker; 0 or omitted = none (spawn)."
63
+ ),
47
64
  }),
48
65
  execute: async (a, ctx) => {
49
66
  const opts = { signal: ctx.signal, callId: ctx.toolCallId, onSubagentEvent: ctx.onSubagentEvent };
50
67
  const need = (field: "id" | "name" | "task"): string => {
51
68
  const value = a[field];
52
69
  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." };
70
+ const example =
71
+ a.command === "spawn"
72
+ ? { command: "spawn", name: "researcher", task: "Do the work." }
73
+ : { command: a.command, id: "researcher-1a2b3c4d", task: "Next question." };
54
74
  throw new ValidationError(`${a.command} needs "${field}". Example: ${JSON.stringify(example)}`);
55
75
  }
56
76
  return value;
@@ -59,7 +79,17 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
59
79
  switch (a.command) {
60
80
  case "spawn":
61
81
  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)
82
+ await agent.workers.spawn(
83
+ {
84
+ name: need("name"),
85
+ role: a.role,
86
+ instructions: a.instructions,
87
+ task: need("task"),
88
+ tools: a.tools,
89
+ history: a.history,
90
+ },
91
+ opts
92
+ )
63
93
  );
64
94
  case "message":
65
95
  return asResult(await agent.workers.message(need("id"), need("task"), opts));
@@ -81,7 +111,15 @@ export function createSubagentsTool(agent: Agent): ToolDefinition {
81
111
  function describeWorkers(agent: Agent): string {
82
112
  const workers = agent.workers.list();
83
113
  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");
114
+ const open =
115
+ workers.length === 0
116
+ ? "none"
117
+ : workers
118
+ .map(
119
+ (w) =>
120
+ `- ${w.id} (${w.name}${w.role ? `, ${w.role}` : ""}) tools: ${w.tools.join(", ") || "none"}${w.busy ? " [busy]" : ""}`
121
+ )
122
+ .join("\n");
85
123
  const copyable = turns.length === 0 ? "none yet" : turns.map((t) => `${t.n}. ${t.prompt}`).join("\n");
86
124
  return `Open workers:\n${open}\n\nFinished turns of this conversation (history: N copies turns 1 to N):\n${copyable}`;
87
125
  }
@@ -1,12 +1,14 @@
1
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
2
 
3
3
  COMMANDS (one tool, the `command` field)
4
+
4
5
  - 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
6
  - message: send `task` to the open worker `id`. The worker remembers everything said to it before. It waits for the reply.
6
7
  - close: end the worker `id` when you no longer need it. A closed worker cannot be messaged again.
7
8
  - list: your open workers, plus the numbered turns of this conversation that `history` can copy.
8
9
 
9
10
  FIELDS
11
+
10
12
  - 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
13
  - instructions: the worker's own system prompt: its role, rules and the format you want back.
12
14
  - task: for spawn, the first assignment; for message, the next message to that worker.
@@ -23,6 +25,7 @@ Ask it a follow-up:
23
25
  {"command":"message","id":"researcher-1a2b3c4d","task":"Now focus on write-heavy workloads."}
24
26
 
25
27
  RULES
28
+
26
29
  - Several workers at once: make several spawn calls in the same turn. They run in parallel.
27
30
  - A worker handles one call at a time. Wait for its reply before messaging it again.
28
31
  - {{LIMIT}}
@@ -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);
@@ -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);
@@ -380,10 +389,18 @@ export function createLiveWorker(
380
389
  trace.steps.push({ turn: turn.turns, type: "tool_result", name: tr.name, isError: tr.isError, timestamp });
381
390
  }
382
391
  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 {}
392
+ try {
393
+ hooks.trim?.(trace);
394
+ } catch {}
395
+ try {
396
+ hooks.touch?.(trace.trackingId);
397
+ } catch {}
398
+ try {
399
+ hooks.notify?.(trackingId);
400
+ } catch {}
401
+ try {
402
+ hooks.persist?.();
403
+ } catch {}
387
404
  } catch {}
388
405
  };
389
406
 
@@ -409,8 +426,12 @@ export function createLiveWorker(
409
426
  entry.thinking = partialThinking;
410
427
  entry.timestamp = Date.now();
411
428
  }
412
- try { hooks.trim?.(trace); } catch {}
413
- try { hooks.notify?.(trackingId); } catch {}
429
+ try {
430
+ hooks.trim?.(trace);
431
+ } catch {}
432
+ try {
433
+ hooks.notify?.(trackingId);
434
+ } catch {}
414
435
  try {
415
436
  hooks.emit?.({
416
437
  trackingId: displayId,
@@ -429,11 +450,19 @@ export function createLiveWorker(
429
450
  }): (() => void) => {
430
451
  const textListener = (e: { delta?: string }): void => onWorkerDelta(e.delta, undefined);
431
452
  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 {}
453
+ try {
454
+ workerStream.on("text_delta", textListener as (e: { delta?: string; thinkingDelta?: string }) => void);
455
+ } catch {}
456
+ try {
457
+ workerStream.on("thinking_delta", thinkingListener as (e: { delta?: string; thinkingDelta?: string }) => void);
458
+ } catch {}
434
459
  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 {}
460
+ try {
461
+ workerStream.off("text_delta", textListener as (e: { delta?: string; thinkingDelta?: string }) => void);
462
+ } catch {}
463
+ try {
464
+ workerStream.off("thinking_delta", thinkingListener as (e: { delta?: string; thinkingDelta?: string }) => void);
465
+ } catch {}
437
466
  };
438
467
  };
439
468
 
@@ -15,7 +15,13 @@ import { sessionDirFor, loadSessionDir } from "../session/store.ts";
15
15
  import { telemetryBus } from "../telemetry.ts";
16
16
  import { hashSessionPart, newRunId, newTurnId, nowMs } from "../utils/session.ts";
17
17
  import { buildDynamicWorkerConfig, grantWorkerTools, sanitizeToolName } from "./worker.ts";
18
- import { createFixedChildSessionId, openWorkerTrace, reserveSpawn, workerMetadata, type WorkerRunResult } from "./delegation.ts";
18
+ import {
19
+ createFixedChildSessionId,
20
+ openWorkerTrace,
21
+ reserveSpawn,
22
+ workerMetadata,
23
+ type WorkerRunResult,
24
+ } from "./delegation.ts";
19
25
  import { finishedTurns, historyUpTo, type Turn } from "./turns.ts";
20
26
  import { modelName, readUsage, type UsageNode } from "../session/usage.ts";
21
27
 
@@ -59,7 +65,13 @@ export interface WorkerCallOptions {
59
65
  /** The calling tool call's id: makes the call exactly-once across a crash and replay (the `subagents` tool passes it). */
60
66
  callId?: string;
61
67
  /** Receives the worker's live text and thinking deltas. */
62
- onSubagentEvent?: (event: { trackingId: string; delta?: string; thinkingDelta?: string; partialText?: string; partialThinking?: string }) => void;
68
+ onSubagentEvent?: (event: {
69
+ trackingId: string;
70
+ delta?: string;
71
+ thinkingDelta?: string;
72
+ partialText?: string;
73
+ partialThinking?: string;
74
+ }) => void;
63
75
  }
64
76
 
65
77
  // Saved in `worker.json`: everything needed to rebuild a worker after a restart.
@@ -104,7 +116,11 @@ function lastAssistantText(messages: Message[]): string | undefined {
104
116
  }
105
117
 
106
118
  // Reads a worker that is not in memory from its folder: its saved spending, model, and the workers saved inside it; in: a worker's session folder, out: usageNodes.
107
- function readSavedWorker(dir: string | undefined): { model: string; totals: ReturnType<typeof readUsage>; workers: UsageNode[] } {
119
+ function readSavedWorker(dir: string | undefined): {
120
+ model: string;
121
+ totals: ReturnType<typeof readUsage>;
122
+ workers: UsageNode[];
123
+ } {
108
124
  if (!dir) return { model: "unknown", totals: readUsage(undefined), workers: [] };
109
125
  let session: ReturnType<typeof loadSessionDir>;
110
126
  try {
@@ -125,7 +141,15 @@ function readSavedWorker(dir: string | undefined): { model: string; totals: Retu
125
141
  try {
126
142
  const meta = JSON.parse(fs.readFileSync(path.join(root, d.name, META_FILE), "utf8")) as WorkerMeta;
127
143
  const inner = readSavedWorker(path.join(root, d.name));
128
- workers.push({ id: meta.id, name: meta.name, ...(meta.role ? { role: meta.role } : {}), model: inner.model, status: meta.status, totals: inner.totals, workers: inner.workers });
144
+ workers.push({
145
+ id: meta.id,
146
+ name: meta.name,
147
+ ...(meta.role ? { role: meta.role } : {}),
148
+ model: inner.model,
149
+ status: meta.status,
150
+ totals: inner.totals,
151
+ workers: inner.workers,
152
+ });
129
153
  } catch {
130
154
  // a damaged worker.json is skipped, as in load()
131
155
  }
@@ -167,10 +191,15 @@ export class WorkerRegistry {
167
191
  const dir = path.join(root, d.name);
168
192
  try {
169
193
  const meta = JSON.parse(fs.readFileSync(path.join(dir, META_FILE), "utf8")) as WorkerMeta;
170
- if (typeof meta.id !== "string" || typeof meta.sessionId !== "string" || !Array.isArray(meta.tools)) throw new Error("missing fields");
194
+ if (typeof meta.id !== "string" || typeof meta.sessionId !== "string" || !Array.isArray(meta.tools))
195
+ throw new Error("missing fields");
171
196
  this.entries.set(meta.id, { meta, dir, busy: false });
172
197
  } catch (err) {
173
- telemetryBus.emitWarning("worker.invalid", `[Agent Accelerator] ${path.join(dir, META_FILE)} is not a valid worker and is skipped: ${err instanceof Error ? err.message : String(err)}`, { dir });
198
+ telemetryBus.emitWarning(
199
+ "worker.invalid",
200
+ `[Agent Accelerator] ${path.join(dir, META_FILE)} is not a valid worker and is skipped: ${err instanceof Error ? err.message : String(err)}`,
201
+ { dir }
202
+ );
174
203
  }
175
204
  }
176
205
  }
@@ -219,11 +248,17 @@ export class WorkerRegistry {
219
248
  /** Closes a worker: it can no longer be messaged, its session stays on disk (and in `usageReport`). */
220
249
  close(id: string): WorkerInfo {
221
250
  const entry = this.open(id);
222
- if (entry.busy) throw new ValidationError(`Worker "${id}" is busy with a call; wait for it to finish before closing it.`);
251
+ if (entry.busy)
252
+ throw new ValidationError(`Worker "${id}" is busy with a call; wait for it to finish before closing it.`);
223
253
  entry.meta.status = "closed";
224
254
  entry.meta.closedAt = Date.now();
225
255
  this.save(entry);
226
- if (entry.agent) entry.kept = { model: modelName(entry.agent.modelStringOrSpec), totals: readUsage(entry.agent.usageTotals), workers: entry.agent.workers.usageNodes() };
256
+ if (entry.agent)
257
+ entry.kept = {
258
+ model: modelName(entry.agent.modelStringOrSpec),
259
+ totals: readUsage(entry.agent.usageTotals),
260
+ workers: entry.agent.workers.usageNodes(),
261
+ };
227
262
  entry.agent?.disposeSession(entry.meta.sessionId); // frees its provider chain; never reset(): that could rewrite the saved history
228
263
  entry.agent = undefined;
229
264
  return this.info(entry);
@@ -244,7 +279,13 @@ export class WorkerRegistry {
244
279
  return [...this.entries.values()].map((entry) => {
245
280
  const { id, name, role, status } = entry.meta;
246
281
  const base = { id, name, ...(role ? { role } : {}), status };
247
- if (entry.agent) return { ...base, model: modelName(entry.agent.modelStringOrSpec), totals: entry.agent.usageTotals, workers: entry.agent.workers.usageNodes() };
282
+ if (entry.agent)
283
+ return {
284
+ ...base,
285
+ model: modelName(entry.agent.modelStringOrSpec),
286
+ totals: entry.agent.usageTotals,
287
+ workers: entry.agent.workers.usageNodes(),
288
+ };
248
289
  return { ...base, ...(entry.kept ?? readSavedWorker(entry.dir)) };
249
290
  });
250
291
  }
@@ -253,7 +294,8 @@ export class WorkerRegistry {
253
294
  private open(id: string): Entry {
254
295
  const entry = this.entries.get(id);
255
296
  if (!entry) throw new ValidationError(`No worker with id "${id}". Use list to see the open workers.`);
256
- if (entry.meta.status === "closed") throw new ValidationError(`Worker "${id}" is closed. Spawn a new worker instead.`);
297
+ if (entry.meta.status === "closed")
298
+ throw new ValidationError(`Worker "${id}" is closed. Spawn a new worker instead.`);
257
299
  return entry;
258
300
  }
259
301
 
@@ -264,7 +306,13 @@ export class WorkerRegistry {
264
306
  }
265
307
 
266
308
  // Checks a new worker's request, builds its Agent (seeded with the copied history) and saves `worker.json`; in: spawn, out: a new open entry.
267
- private async create(id: string, name: string, task: string, key: string | undefined, spec: WorkerSpec): Promise<Entry> {
309
+ private async create(
310
+ id: string,
311
+ name: string,
312
+ task: string,
313
+ key: string | undefined,
314
+ spec: WorkerSpec
315
+ ): Promise<Entry> {
268
316
  const parent = this.parent;
269
317
  if (!parent.subagentModel) {
270
318
  throw new ConfigError(
@@ -273,7 +321,11 @@ export class WorkerRegistry {
273
321
  }
274
322
  const maxOpen = parent.dynamicSubagents?.maxSpawn ?? Infinity;
275
323
  if (this.list().length >= maxOpen) {
276
- throw new ValidationError(`Too many open workers (${maxOpen} is the limit). Close one first; open workers: ${this.list().map((w) => w.id).join(", ")}.`);
324
+ throw new ValidationError(
325
+ `Too many open workers (${maxOpen} is the limit). Close one first; open workers: ${this.list()
326
+ .map((w) => w.id)
327
+ .join(", ")}.`
328
+ );
277
329
  }
278
330
  const pool = parent.dynamicSubagents?.tools ?? {};
279
331
  const requested = spec.tools ?? [];
@@ -281,7 +333,8 @@ export class WorkerRegistry {
281
333
  if (unknown.length > 0) {
282
334
  const names = Object.keys(pool);
283
335
  throw new ValidationError(
284
- `tools: unknown worker tool(s) ${unknown.map((n) => `"${n}"`).join(", ")}; ` + (names.length > 0 ? `the pool is [${names.join(", ")}].` : "no worker tools are available, omit tools.")
336
+ `tools: unknown worker tool(s) ${unknown.map((n) => `"${n}"`).join(", ")}; ` +
337
+ (names.length > 0 ? `the pool is [${names.join(", ")}].` : "no worker tools are available, omit tools.")
285
338
  );
286
339
  }
287
340
  const history = spec.history ?? "none";
@@ -338,8 +391,16 @@ export class WorkerRegistry {
338
391
  }
339
392
 
340
393
  // Sends one text to a worker and turns what happens into a reply: runs it, or after a crash continues the cut-off run or returns the answer it had already given; in: spawn and message, out: a WorkerReply.
341
- private async deliver(entry: Entry, text: string, key: string | undefined, opts: WorkerCallOptions): Promise<WorkerReply> {
342
- if (entry.busy) throw new ValidationError(`Worker "${entry.meta.id}" is busy with another call; wait for it, or message another worker.`);
394
+ private async deliver(
395
+ entry: Entry,
396
+ text: string,
397
+ key: string | undefined,
398
+ opts: WorkerCallOptions
399
+ ): Promise<WorkerReply> {
400
+ if (entry.busy)
401
+ throw new ValidationError(
402
+ `Worker "${entry.meta.id}" is busy with another call; wait for it, or message another worker.`
403
+ );
343
404
  entry.busy = true;
344
405
  const { meta } = entry;
345
406
  const startTime = nowMs();
@@ -354,14 +415,24 @@ export class WorkerRegistry {
354
415
  parent: this.parent,
355
416
  emit: opts.onSubagentEvent,
356
417
  });
357
- const lineage = { name: meta.name, displayId: live.displayId, sessionId: meta.sessionId, parentSessionId: this.parent.sessionId, role: meta.role, task: text, runId: newRunId(), turnId: newTurnId() };
418
+ const lineage = {
419
+ name: meta.name,
420
+ displayId: live.displayId,
421
+ sessionId: meta.sessionId,
422
+ parentSessionId: this.parent.sessionId,
423
+ role: meta.role,
424
+ task: text,
425
+ runId: newRunId(),
426
+ turnId: newTurnId(),
427
+ };
358
428
  const replayed = this.isReplay(entry, key, text) && agent.runMarks.length > meta.last!.marks;
359
429
  if (!replayed) {
360
430
  if (key) meta.last = { key, text: hashSessionPart(text), marks: agent.runMarks.length };
361
431
  else delete meta.last;
362
432
  this.save(entry);
363
433
  }
364
- const onAbort = (): void => agent.interrupt(opts.signal?.reason instanceof Error ? opts.signal.reason : undefined);
434
+ const onAbort = (): void =>
435
+ agent.interrupt(opts.signal?.reason instanceof Error ? opts.signal.reason : undefined);
365
436
  try {
366
437
  if (opts.signal?.aborted) throw new Error("aborted before the worker started");
367
438
  opts.signal?.addEventListener("abort", onAbort, { once: true });
@@ -387,14 +458,36 @@ export class WorkerRegistry {
387
458
  if (!response) throw new Error("the worker had nothing to resume");
388
459
  result = response;
389
460
  }
390
- const reply = result.text.trim() ? result.text : `(the worker produced no text${result.finishReason ? `; finish reason: ${result.finishReason}` : ""})`;
461
+ const reply = result.text.trim()
462
+ ? result.text
463
+ : `(the worker produced no text${result.finishReason ? `; finish reason: ${result.finishReason}` : ""})`;
391
464
  live.finish(result);
392
- return { id: meta.id, text: reply, metadata: workerMetadata({ ...lineage, durationMs: Math.max(0, nowMs() - startTime), result, steps: live.steps() }) };
465
+ return {
466
+ id: meta.id,
467
+ text: reply,
468
+ metadata: workerMetadata({
469
+ ...lineage,
470
+ durationMs: Math.max(0, nowMs() - startTime),
471
+ result,
472
+ steps: live.steps(),
473
+ }),
474
+ };
393
475
  } catch (err) {
394
476
  const message = err instanceof Error ? err.message : String(err);
395
477
  live.fail(message);
396
478
  const errorText = `Error in worker ${meta.id}: ${message}`;
397
- return { id: meta.id, text: errorText, error: message, metadata: workerMetadata({ ...lineage, durationMs: Math.max(0, nowMs() - startTime), error: message, errorText, steps: live.steps() }) };
479
+ return {
480
+ id: meta.id,
481
+ text: errorText,
482
+ error: message,
483
+ metadata: workerMetadata({
484
+ ...lineage,
485
+ durationMs: Math.max(0, nowMs() - startTime),
486
+ error: message,
487
+ errorText,
488
+ steps: live.steps(),
489
+ }),
490
+ };
398
491
  } finally {
399
492
  opts.signal?.removeEventListener("abort", onAbort);
400
493
  }
@@ -2,24 +2,25 @@
2
2
 
3
3
  The whole SDK. `index.ts` is the only public entry (`main`/`module`/`types`); everything else is reached through it.
4
4
 
5
- | File / folder | What it is |
6
- | :-- | :-- |
7
- | `index.ts` | The public barrel. Adding or removing an export is a public API change. |
8
- | `telemetry.ts` | The in-process event bus (`telemetryBus`). Imports nothing from the SDK, so any module may use it. |
5
+ | File / folder | What it is |
6
+ | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `index.ts` | The public barrel. Adding or removing an export is a public API change. |
8
+ | `telemetry.ts` | The in-process event bus (`telemetryBus`). Imports nothing from the SDK, so any module may use it. |
9
9
  | `telemetry-file.ts` | Opt-in NDJSON sink for the bus: a persisted session's events go to `<session folder>/telemetry.ndjson`, the rest to `<home>/telemetry/<date>.ndjson`; never outside a home. |
10
- | `update-models.ts` | CLI behind `bun run update-models` (refreshes the model catalog cache). |
11
- | `providers.ts` | Provider-agnostic mappers (thinking, cache, tool choice, structured output, generation params). No HTTP. |
12
- | `agent/` | `Agent`, the model-to-tools loop, workers, delegation. |
13
- | `providers/` | One adapter per wire (Google, OpenAI, OpenRouter, OpenAI-compatible), the registry, shared helpers. |
14
- | `models/` | The model catalog (models.dev) and its cache. |
15
- | `tools/` | `tool()`, schemas, the executor. |
16
- | `streaming/` | The event stream and the SSE parser. |
17
- | `session/` | Persistence (snapshot + journal), usage accounting, tool logs. |
18
- | `scheduler/` | Scheduled actions. |
19
- | `types/` | Shared contracts (no logic except small helpers). |
20
- | `utils/` | Small shared helpers (durable writes, home, env, media, retry, safe I/O). |
10
+ | `update-models.ts` | CLI behind `bun run update-models` (refreshes the model catalog cache). |
11
+ | `providers.ts` | Provider-agnostic mappers (thinking, cache, tool choice, structured output, generation params). No HTTP. |
12
+ | `agent/` | `Agent`, the model-to-tools loop, workers, delegation. |
13
+ | `providers/` | One adapter per wire (Google, OpenAI, OpenRouter, OpenAI-compatible), the registry, shared helpers. |
14
+ | `models/` | The model catalog (models.dev) and its cache. |
15
+ | `tools/` | `tool()`, schemas, the executor. |
16
+ | `streaming/` | The event stream and the SSE parser. |
17
+ | `session/` | Persistence (snapshot + journal), usage accounting, tool logs. |
18
+ | `scheduler/` | Scheduled actions. |
19
+ | `types/` | Shared contracts (no logic except small helpers). |
20
+ | `utils/` | Small shared helpers (durable writes, home, env, media, retry, safe I/O). |
21
21
 
22
22
  ## Rules for every file here
23
+
23
24
  - Imports use `.ts` extensions; type-only imports use `import type`.
24
25
  - No `console.*`: warnings and traces go to `telemetryBus` (exceptions: the `update-models.ts` CLI and one compat `console.warn` in `providers.ts`).
25
26
  - All state is written under the one home folder (`utils/home.ts`). Files are replaced through `utils/durable.ts`; the append-only ones are the session journal and tool logs (fsynced per line) and the opt-in telemetry file (not fsynced).