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.
- package/README.md +3 -1
- package/examples/prompts/SYSTEM_PROMPT.md +2 -0
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
- package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
- package/package.json +4 -1
- package/src/agent/agent.ts +366 -161
- package/src/agent/architecture.md +13 -12
- package/src/agent/config-blocks.ts +39 -12
- package/src/agent/context.ts +8 -3
- package/src/agent/delegation.ts +82 -29
- package/src/agent/loop.ts +444 -365
- package/src/agent/subagent.ts +1 -3
- package/src/agent/subagents-tool.ts +45 -7
- package/src/agent/subagents.md +3 -0
- package/src/agent/turns.ts +3 -1
- package/src/agent/worker.ts +56 -27
- package/src/agent/workers.ts +113 -20
- package/src/architecture.md +16 -15
- package/src/index.ts +34 -10
- package/src/models/architecture.md +4 -3
- package/src/models/catalog-cache.ts +6 -5
- package/src/models/catalog.ts +39 -30
- package/src/providers/architecture.md +10 -8
- package/src/providers/google.ts +147 -59
- package/src/providers/openai-compat.ts +57 -49
- package/src/providers/openai.ts +54 -37
- package/src/providers/openrouter.ts +85 -30
- package/src/providers/registry.ts +6 -21
- package/src/providers/shared.ts +87 -33
- package/src/providers.ts +56 -115
- package/src/scheduler/architecture.md +7 -6
- package/src/scheduler/scheduled-actions.md +4 -0
- package/src/scheduler/scheduler.ts +186 -60
- package/src/scheduler/store.ts +23 -10
- package/src/scheduler/time.ts +17 -3
- package/src/scheduler/tool.ts +33 -7
- package/src/session/architecture.md +7 -6
- package/src/session/journal.ts +38 -12
- package/src/session/store.ts +122 -61
- package/src/session/usage.ts +58 -9
- package/src/streaming/architecture.md +4 -3
- package/src/streaming/event-stream.ts +26 -12
- package/src/telemetry.ts +6 -2
- package/src/tools/architecture.md +8 -7
- package/src/tools/executor.ts +50 -48
- package/src/tools/schema.ts +10 -6
- package/src/tools/subagent-result.ts +6 -1
- package/src/tools/tool.ts +9 -11
- package/src/types/agent.ts +2 -21
- package/src/types/architecture.md +11 -10
- package/src/types/core.ts +1 -9
- package/src/types/errors.ts +26 -4
- package/src/types/model.ts +0 -4
- package/src/types/tool.ts +12 -2
- package/src/update-models.ts +5 -5
- package/src/utils/architecture.md +16 -15
- package/src/utils/cache.ts +1 -3
- package/src/utils/durable.ts +17 -5
- package/src/utils/env.ts +9 -7
- package/src/utils/errors.ts +6 -14
- package/src/utils/headers.ts +3 -10
- package/src/utils/media.ts +23 -8
- package/src/utils/raw-capture.ts +9 -2
- package/src/utils/retry.ts +15 -3
- package/src/utils/safe-io.ts +29 -7
- package/src/utils/serialization.ts +1 -4
- package/src/utils/session.ts +1 -2
package/src/agent/subagent.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
46
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
}
|
package/src/agent/subagents.md
CHANGED
|
@@ -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}}
|
package/src/agent/turns.ts
CHANGED
|
@@ -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
|
|
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);
|
package/src/agent/worker.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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: {
|
|
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 (
|
|
195
|
-
const pooled =
|
|
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 {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
try {
|
|
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 {
|
|
413
|
-
|
|
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 {
|
|
433
|
-
|
|
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 {
|
|
436
|
-
|
|
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
|
|
package/src/agent/workers.ts
CHANGED
|
@@ -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 {
|
|
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: {
|
|
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): {
|
|
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({
|
|
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))
|
|
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(
|
|
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)
|
|
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)
|
|
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)
|
|
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")
|
|
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(
|
|
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(
|
|
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(", ")}; ` +
|
|
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(
|
|
342
|
-
|
|
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 = {
|
|
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 =>
|
|
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()
|
|
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 {
|
|
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 {
|
|
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
|
}
|
package/src/architecture.md
CHANGED
|
@@ -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
|
|
6
|
-
|
|
|
7
|
-
| `index.ts`
|
|
8
|
-
| `telemetry.ts`
|
|
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`
|
|
11
|
-
| `providers.ts`
|
|
12
|
-
| `agent/`
|
|
13
|
-
| `providers/`
|
|
14
|
-
| `models/`
|
|
15
|
-
| `tools/`
|
|
16
|
-
| `streaming/`
|
|
17
|
-
| `session/`
|
|
18
|
-
| `scheduler/`
|
|
19
|
-
| `types/`
|
|
20
|
-
| `utils/`
|
|
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).
|