agent-accelerator 0.5.3 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -30
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +14 -20
- package/package.json +1 -1
- package/src/agent/agent.ts +73 -24
- package/src/agent/context.ts +1 -1
- package/src/agent/delegation.ts +208 -524
- package/src/agent/loop.ts +13 -11
- package/src/agent/subagents-tool.ts +87 -0
- package/src/agent/subagents.md +31 -0
- package/src/agent/turns.ts +60 -0
- package/src/agent/worker.ts +38 -30
- package/src/agent/workers.ts +405 -0
- package/src/index.ts +3 -2
- package/src/session/journal.ts +12 -1
- package/src/session/store.ts +24 -10
- package/src/session/usage.ts +201 -0
- package/src/tools/subagent-result.ts +1 -1
- package/src/types/agent.ts +19 -6
- package/src/types/tool.ts +1 -1
- package/src/utils/headers.ts +1 -1
package/src/agent/loop.ts
CHANGED
|
@@ -71,6 +71,8 @@ export interface AgentLoopConfig {
|
|
|
71
71
|
enforceBudget?: (usage?: TokenUsage) => void;
|
|
72
72
|
/** Called after every turn for durable persistence checkpoints (never fails a turn). */
|
|
73
73
|
onProgress?: () => void;
|
|
74
|
+
/** Called once per model call this agent made itself (never for its workers), with the provider's usage and the catalog spec: the source of `usageReport()`. */
|
|
75
|
+
onTurnUsage?: (usage: TokenUsage, spec: ModelSpec | undefined) => void;
|
|
74
76
|
/** Called the moment each tool call finishes, before the rest of its batch (the crash-proof record of finished work). */
|
|
75
77
|
onToolResult?: (result: ToolResultRecord) => void;
|
|
76
78
|
/** Opens the log file of one tool call (see `ToolLogs`); unset when the Agent does not persist to a folder. */
|
|
@@ -411,16 +413,14 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
|
|
|
411
413
|
}
|
|
412
414
|
}
|
|
413
415
|
|
|
414
|
-
/**
|
|
415
|
-
|
|
416
|
-
* micro-USD accumulation. Returns undefined when unknown (never a 0 object). */
|
|
417
|
-
export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec): TokenUsage["cost"] {
|
|
416
|
+
/** Prices one usage snapshot with the one cost model (`priceUsage`): provider-reported total first, else catalog pricing scaled by `serviceTier`; undefined when unknown. In: loop turns and `accumulateUsage`, out: `usage.cost`. */
|
|
417
|
+
export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serviceTier?: string): TokenUsage["cost"] {
|
|
418
418
|
try {
|
|
419
419
|
if (usage.cost && usage.cost.totalCost !== undefined && usage.cost.totalCost > 0) {
|
|
420
420
|
return usage.cost;
|
|
421
421
|
}
|
|
422
422
|
if (!spec) return usage.cost;
|
|
423
|
-
const priced = priceUsage(usage, spec);
|
|
423
|
+
const priced = priceUsage(usage, spec, serviceTier ? { serviceTier } : undefined);
|
|
424
424
|
return priced
|
|
425
425
|
? {
|
|
426
426
|
inputCost: priced.inputCost,
|
|
@@ -448,7 +448,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec): Tok
|
|
|
448
448
|
// This function never mutates `source` (Q-35): the priced cost is cloned
|
|
449
449
|
// into a local before folding into the target.
|
|
450
450
|
// ---------------------------------------------------------------------------
|
|
451
|
-
export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec): void {
|
|
451
|
+
export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec, serviceTier?: string): void {
|
|
452
452
|
target.inputTokens += source.inputTokens || 0;
|
|
453
453
|
target.outputTokens += source.outputTokens || 0;
|
|
454
454
|
target.totalTokens += source.totalTokens || 0;
|
|
@@ -475,7 +475,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
475
475
|
});
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
const priced = computeCostFromPricing(source, spec);
|
|
478
|
+
const priced = computeCostFromPricing(source, spec, serviceTier);
|
|
479
479
|
// Q-35: pure — read `source` only; never assign back to `source.cost`.
|
|
480
480
|
// Clone the priced cost so later target folds cannot alias source state.
|
|
481
481
|
const sourceCost = priced ? { ...priced } : undefined;
|
|
@@ -799,8 +799,9 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
799
799
|
}
|
|
800
800
|
}
|
|
801
801
|
|
|
802
|
-
accumulateUsage(accumulatedUsage, genResult.usage, spec);
|
|
803
|
-
config.
|
|
802
|
+
accumulateUsage(accumulatedUsage, genResult.usage, spec, options?.serviceTier);
|
|
803
|
+
try { config.onTurnUsage?.(genResult.usage, spec); } catch {}
|
|
804
|
+
config.enforceBudget?.({ ...genResult.usage, cost: computeCostFromPricing(genResult.usage, spec, options?.serviceTier) });
|
|
804
805
|
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
805
806
|
|
|
806
807
|
// Record assistant turn in context
|
|
@@ -1252,8 +1253,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1252
1253
|
}
|
|
1253
1254
|
}
|
|
1254
1255
|
|
|
1255
|
-
accumulateUsage(accumulatedUsage, turnResponse.usage, spec2);
|
|
1256
|
-
config.
|
|
1256
|
+
accumulateUsage(accumulatedUsage, turnResponse.usage, spec2, options?.serviceTier);
|
|
1257
|
+
try { config.onTurnUsage?.(turnResponse.usage, spec2); } catch {}
|
|
1258
|
+
config.enforceBudget?.({ ...turnResponse.usage, cost: computeCostFromPricing(turnResponse.usage, spec2, options?.serviceTier) });
|
|
1257
1259
|
checkTokenLimit(accumulatedUsage, streamLimits.maxTotalTokens);
|
|
1258
1260
|
|
|
1259
1261
|
context.addAssistantMessage(
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
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
|
|
4
|
+
* header line naming the worker; the branded envelope under them carries usage and lineage to the loop.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { Agent } from "./agent.ts";
|
|
9
|
+
import { tool } from "../tools/tool.ts";
|
|
10
|
+
import { makeSubagentResult } from "../tools/subagent-result.ts";
|
|
11
|
+
import { ValidationError } from "../types/errors.ts";
|
|
12
|
+
import type { ToolDefinition } from "../types/tool.ts";
|
|
13
|
+
import type { WorkerReply } from "./workers.ts";
|
|
14
|
+
|
|
15
|
+
const COMMANDS = ["spawn", "message", "close", "list"] as const;
|
|
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
|
+
// 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}`;
|
|
26
|
+
return makeSubagentResult(text, [reply.metadata]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Builds the `subagents` tool for `agent`; its description is the manual with the worker pool and the open-worker limit filled in. */
|
|
30
|
+
export function createSubagentsTool(agent: Agent): ToolDefinition {
|
|
31
|
+
const poolNames = Object.keys(agent.dynamicSubagents?.tools ?? {});
|
|
32
|
+
const max = agent.dynamicSubagents?.maxSpawn ?? 4;
|
|
33
|
+
return tool({
|
|
34
|
+
name: "subagents",
|
|
35
|
+
description: loadManual()
|
|
36
|
+
.replace(/\{\{MAX\}\}/g, String(max))
|
|
37
|
+
.replace("{{TOOLS}}", poolNames.length > 0 ? poolNames.join(", ") : "none (omit tools)"),
|
|
38
|
+
input: z.object({
|
|
39
|
+
command: z.enum(COMMANDS),
|
|
40
|
+
id: z.string().optional().describe("The worker's full id (message, close)."),
|
|
41
|
+
name: z.string().optional().describe('Short label for a new worker, e.g. "researcher" (spawn).'),
|
|
42
|
+
role: z.string().optional().describe("Short persona for the worker (spawn)."),
|
|
43
|
+
instructions: z.string().optional().describe("The worker's own system prompt (spawn)."),
|
|
44
|
+
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)."),
|
|
47
|
+
}),
|
|
48
|
+
execute: async (a, ctx) => {
|
|
49
|
+
const opts = { signal: ctx.signal, callId: ctx.toolCallId, onSubagentEvent: ctx.onSubagentEvent };
|
|
50
|
+
const need = (field: "id" | "name" | "task"): string => {
|
|
51
|
+
const value = a[field];
|
|
52
|
+
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." };
|
|
54
|
+
throw new ValidationError(`${a.command} needs "${field}". Example: ${JSON.stringify(example)}`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
switch (a.command) {
|
|
60
|
+
case "spawn":
|
|
61
|
+
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)
|
|
63
|
+
);
|
|
64
|
+
case "message":
|
|
65
|
+
return asResult(await agent.workers.message(need("id"), need("task"), opts));
|
|
66
|
+
case "close":
|
|
67
|
+
return `Worker ${agent.workers.close(need("id")).id} closed.`;
|
|
68
|
+
case "list":
|
|
69
|
+
return describeWorkers(agent);
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// A bad request is feedback for the model; anything else (budget, configuration) is a real failure.
|
|
73
|
+
if (err instanceof ValidationError) return `Error: ${err.message}`;
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The `list` answer: the open workers and the conversation turns `history` can copy; in: the agent's registry, out: plain text for the model.
|
|
81
|
+
function describeWorkers(agent: Agent): string {
|
|
82
|
+
const workers = agent.workers.list();
|
|
83
|
+
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");
|
|
85
|
+
const copyable = turns.length === 0 ? "none yet" : turns.map((t) => `${t.n}. ${t.prompt}`).join("\n");
|
|
86
|
+
return `Open workers:\n${open}\n\nFinished turns of this conversation (history: N copies turns 1 to N):\n${copyable}`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
|
|
3
|
+
COMMANDS (one tool, the `command` field)
|
|
4
|
+
- 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
|
+
- message: send `task` to the open worker `id`. The worker remembers everything said to it before. It waits for the reply.
|
|
6
|
+
- close: end the worker `id` when you no longer need it. This frees one of your {{MAX}} open slots. A closed worker cannot be messaged again.
|
|
7
|
+
- list: your open workers, plus the numbered turns of this conversation that `history` can copy.
|
|
8
|
+
|
|
9
|
+
FIELDS
|
|
10
|
+
- 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
|
+
- instructions: the worker's own system prompt: its role, rules and the format you want back.
|
|
12
|
+
- task: for spawn, the first assignment; for message, the next message to that worker.
|
|
13
|
+
- tools: names of worker tools to grant. Available: {{TOOLS}}. Grant only what the task needs.
|
|
14
|
+
- history: a number N (default 0 = none). Starts the worker with a copy of the first N finished turns of this conversation, in order. Call list to see the turns and their numbers. The turn you are in right now is never copied: restate what you need in `task`.
|
|
15
|
+
|
|
16
|
+
RESULTS
|
|
17
|
+
Every spawn and message answers with one header line and the worker's text: `[worker <id>] replied:` or `[worker <id>] failed: <reason>`. A failed worker stays open: fix the problem and message it again, or close it.
|
|
18
|
+
|
|
19
|
+
EXAMPLES
|
|
20
|
+
Start a researcher:
|
|
21
|
+
{"command":"spawn","name":"researcher","instructions":"You research databases. Answer in 5 bullets with sources.","task":"Compare SQLite and Postgres for a small SaaS."}
|
|
22
|
+
Ask it a follow-up:
|
|
23
|
+
{"command":"message","id":"researcher-1a2b3c4d","task":"Now focus on write-heavy workloads."}
|
|
24
|
+
|
|
25
|
+
RULES
|
|
26
|
+
- Several workers at once: make several spawn calls in the same turn. They run in parallel.
|
|
27
|
+
- A worker handles one call at a time. Wait for its reply before messaging it again.
|
|
28
|
+
- You can have at most {{MAX}} open workers. Close the ones you are done with.
|
|
29
|
+
- A worker starts blank: put everything it needs in `instructions` and `task`, or copy earlier turns with `history`.
|
|
30
|
+
- Worker answers are written by another model. Check them before you rely on them.
|
|
31
|
+
- Never put secrets in `instructions` or `task`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn grouping: a "turn" is one finished run (the user prompt plus the whole answer to it: text, thinking, tool calls
|
|
3
|
+
* and results), found through the run marks the Agent keeps. A worker that starts from a copy of its parent's history
|
|
4
|
+
* gets whole finished turns only, so a prompt never reaches it without its answer. Pure functions, no Agent import.
|
|
5
|
+
*/
|
|
6
|
+
import type { Message, ContentPart } from "../types/message.ts";
|
|
7
|
+
import type { SessionRunMark } from "../session/store.ts";
|
|
8
|
+
import { ValidationError } from "../types/errors.ts";
|
|
9
|
+
import { previewPrompt } from "../utils/session.ts";
|
|
10
|
+
import { deepCloneValue } from "./context.ts";
|
|
11
|
+
|
|
12
|
+
/** One finished turn: its 1-based number (counting finished turns only), a prompt preview, and its messages `[from, to)`. */
|
|
13
|
+
export interface Turn {
|
|
14
|
+
n: number;
|
|
15
|
+
prompt: string;
|
|
16
|
+
from: number;
|
|
17
|
+
to: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Longest prompt preview shown to a model in a turn list. */
|
|
21
|
+
const PREVIEW_CHARS = 200;
|
|
22
|
+
|
|
23
|
+
// A run counts as finished when it ended with its answer (`done`) or with a tool's `ctx.stop` (`stopped`); in: a run mark, out: finishedTurns.
|
|
24
|
+
function isFinished(mark: SessionRunMark): boolean {
|
|
25
|
+
return mark.end === "done" || mark.end === "stopped";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The finished turns of a conversation, oldest first. The running turn and failed, aborted or abandoned runs are not turns. */
|
|
29
|
+
export function finishedTurns(messages: Message[], marks: SessionRunMark[]): Turn[] {
|
|
30
|
+
const sorted = marks.filter((m) => m.i >= 0 && m.i < messages.length).sort((a, b) => a.i - b.i);
|
|
31
|
+
const turns: Turn[] = [];
|
|
32
|
+
sorted.forEach((mark, k) => {
|
|
33
|
+
if (!isFinished(mark)) return;
|
|
34
|
+
turns.push({
|
|
35
|
+
n: turns.length + 1,
|
|
36
|
+
prompt: previewPrompt(messages[mark.i]!.content as string | ContentPart[]).slice(0, PREVIEW_CHARS),
|
|
37
|
+
from: mark.i,
|
|
38
|
+
to: sorted[k + 1]?.i ?? messages.length,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
return turns;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A deep copy of the first `n` finished turns (plus anything before the first run mark), in order. `0` gives an empty
|
|
46
|
+
* history. Throws a `ValidationError` naming how many finished turns exist when `n` is not a whole number in range.
|
|
47
|
+
*/
|
|
48
|
+
export function historyUpTo(messages: Message[], marks: SessionRunMark[], n: number): Message[] {
|
|
49
|
+
const turns = finishedTurns(messages, marks);
|
|
50
|
+
if (!Number.isInteger(n) || n < 0 || n > turns.length) {
|
|
51
|
+
throw new ValidationError(
|
|
52
|
+
`history: ${JSON.stringify(n)} is not valid; use "none" or a whole number from 1 to ${turns.length} (the number of finished turns).`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
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);
|
|
57
|
+
const kept = [...messages.slice(0, first)];
|
|
58
|
+
for (const turn of turns.slice(0, n)) kept.push(...messages.slice(turn.from, turn.to));
|
|
59
|
+
return deepCloneValue(kept);
|
|
60
|
+
}
|
package/src/agent/worker.ts
CHANGED
|
@@ -42,11 +42,6 @@ export function sanitizeToolName(raw: string): string {
|
|
|
42
42
|
return tag.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").slice(0, 64) || "sub-agent";
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/** Stable worker ids: `{sanitizedName}-{32hex}` display id + raw tracking id. */
|
|
46
|
-
export function makeWorkerIds(sanitizedName: string, trackingId: string): { trackingId: string; displayId: string } {
|
|
47
|
-
return { trackingId, displayId: `${sanitizedName}-${trackingId}` };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
45
|
// ---------------------------------------------------------------------------
|
|
51
46
|
// Shared Agent / SubAgent config normalization (behavior-preserving)
|
|
52
47
|
// ---------------------------------------------------------------------------
|
|
@@ -203,11 +198,6 @@ export function grantWorkerTools(
|
|
|
203
198
|
return grantedTools;
|
|
204
199
|
}
|
|
205
200
|
|
|
206
|
-
/** Resolves a provider id for credential reuse / error metadata (shared). */
|
|
207
|
-
export function providerOfWorkerModel(modelStr: string | unknown | undefined): string | undefined {
|
|
208
|
-
return providerOf(modelStr);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
201
|
function providerOf(modelStr: string | unknown | undefined): string | undefined {
|
|
212
202
|
if (!modelStr) return undefined;
|
|
213
203
|
try {
|
|
@@ -246,18 +236,34 @@ export function resolveWorkerCredentials(
|
|
|
246
236
|
return { apiKeyToUse: undefined, baseUrlToUse: undefined, attemptedProvider };
|
|
247
237
|
}
|
|
248
238
|
|
|
239
|
+
/** How many levels of workers may exist below an agent unless `dynamicSubagents.maxDepth` says otherwise. */
|
|
240
|
+
export const DEFAULT_MAX_DEPTH = 3;
|
|
241
|
+
|
|
249
242
|
/**
|
|
250
|
-
* Config of a
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* `
|
|
243
|
+
* Config of a dynamic worker (scheduled actions, the worker registry): the developer's fixed worker
|
|
244
|
+
* model, level and limits, never the LLM's choice. Credentials carry over only when the providers match, and
|
|
245
|
+
* `inheritableConfig()` keeps the worker no less constrained than its parent. Stateless one-shot by default;
|
|
246
|
+
* `persistent` makes a multi-turn worker that keeps its history (saved under `persistent.dir` when given, and
|
|
247
|
+
* continued by `resume()` after a crash, never automatically). A persistent worker with levels left below it
|
|
248
|
+
* (`maxDepth` of the parent, minus one for itself) gets its own `subagents` tool; its children can only be granted
|
|
249
|
+
* the tools it was granted. One-shot workers never spawn. `dynamicSubagents.workerBudget` becomes each worker's own
|
|
250
|
+
* per-run `budget` (and is passed to its children).
|
|
254
251
|
*/
|
|
255
252
|
export function buildDynamicWorkerConfig(
|
|
256
253
|
parent: Agent,
|
|
257
|
-
worker: {
|
|
254
|
+
worker: {
|
|
255
|
+
name: string;
|
|
256
|
+
role?: string;
|
|
257
|
+
instructions: string;
|
|
258
|
+
sessionId: string;
|
|
259
|
+
tools: Record<string, ToolDefinition>;
|
|
260
|
+
persistent?: { dir?: string };
|
|
261
|
+
}
|
|
258
262
|
): AgentConfig {
|
|
259
263
|
const model = parent.subagentModel;
|
|
260
264
|
const { apiKeyToUse, baseUrlToUse } = resolveWorkerCredentials(parent, model);
|
|
265
|
+
const dyn = parent.dynamicSubagents;
|
|
266
|
+
const levelsBelow = (dyn?.maxDepth ?? DEFAULT_MAX_DEPTH) - 1;
|
|
261
267
|
return {
|
|
262
268
|
name: worker.name,
|
|
263
269
|
description: worker.role || `Sub-agent ${worker.name}`,
|
|
@@ -268,8 +274,22 @@ export function buildDynamicWorkerConfig(
|
|
|
268
274
|
thinkingLevel: parent.dynamicSubagents?.thinkingLevel ?? parent.thinkingConfig?.level,
|
|
269
275
|
sessionId: worker.sessionId,
|
|
270
276
|
tools: worker.tools,
|
|
271
|
-
|
|
272
|
-
stateless:
|
|
277
|
+
...(dyn?.workerBudget ? { budget: { ...dyn.workerBudget } } : {}),
|
|
278
|
+
stateless: !worker.persistent,
|
|
279
|
+
...(worker.persistent?.dir ? { persist: { dir: worker.persistent.dir }, resume: "explicit" as const } : {}),
|
|
280
|
+
...(worker.persistent && levelsBelow >= 1
|
|
281
|
+
? {
|
|
282
|
+
dynamicSubagents: {
|
|
283
|
+
enabled: true,
|
|
284
|
+
model,
|
|
285
|
+
...(dyn?.maxSpawn !== undefined ? { maxSpawn: dyn.maxSpawn } : {}),
|
|
286
|
+
...(dyn?.thinkingLevel ? { thinkingLevel: dyn.thinkingLevel } : {}),
|
|
287
|
+
tools: worker.tools,
|
|
288
|
+
...(dyn?.workerBudget ? { workerBudget: dyn.workerBudget } : {}),
|
|
289
|
+
maxDepth: levelsBelow,
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
: {}),
|
|
273
293
|
serviceTier: parent.serviceTier,
|
|
274
294
|
headers: parent.customHeaders,
|
|
275
295
|
maxTurns: parent.maxTurns,
|
|
@@ -277,18 +297,6 @@ export function buildDynamicWorkerConfig(
|
|
|
277
297
|
};
|
|
278
298
|
}
|
|
279
299
|
|
|
280
|
-
/** Builds one `<NAME>...text...</NAME>` block inside `<SUB-AGENTS-RESPONSE>`. */
|
|
281
|
-
export function buildWorkerXmlBlock(name: string, text: string): string {
|
|
282
|
-
const escaped = escapeXml(text);
|
|
283
|
-
const indentedText = escaped.split("\n").map((line) => ` ${line}`).join("\n");
|
|
284
|
-
return ` <${name}>\n${indentedText}\n </${name}>`;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** Builds the `<SUB-AGENTS-RESPONSE>` envelope for dynamic workers. */
|
|
288
|
-
export function buildSubagentsXml(results: Array<{ name: string; text: string }>): string {
|
|
289
|
-
return `<SUB-AGENTS-RESPONSE>\n${results.map((r) => buildWorkerXmlBlock(r.name, r.text)).join("\n")}\n</SUB-AGENTS-RESPONSE>`;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
300
|
// ---------------------------------------------------------------------------
|
|
293
301
|
// Shared live worker state machine (single home for fixed + dynamic workers)
|
|
294
302
|
// ---------------------------------------------------------------------------
|
|
@@ -322,7 +330,7 @@ export interface LiveWorkerHooks {
|
|
|
322
330
|
|
|
323
331
|
/**
|
|
324
332
|
* Single live worker state machine shared by fixed (`agentToTool`) and
|
|
325
|
-
*
|
|
333
|
+
* registry workers. Owns `liveTurn` / `partialText` /
|
|
326
334
|
* `partialThinking`, finalizes partial streaming entries on `onTurn`, and
|
|
327
335
|
* forwards deltas to the parent stream. The trace object is mutated in place;
|
|
328
336
|
* trimming / touching / notifying / persisting are delegated to hooks so this
|