agent-accelerator 0.5.3 → 0.5.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.
- package/README.md +84 -37
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +14 -20
- package/package.json +1 -1
- package/src/agent/agent.ts +181 -56
- package/src/agent/config-blocks.ts +80 -0
- package/src/agent/context.ts +1 -1
- package/src/agent/delegation.ts +208 -524
- package/src/agent/loop.ts +18 -14
- 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 +43 -33
- package/src/agent/workers.ts +405 -0
- package/src/index.ts +6 -5
- package/src/providers/openrouter.ts +49 -225
- package/src/providers/shared.ts +2 -2
- package/src/scheduler/scheduled-actions.md +4 -2
- package/src/scheduler/scheduler.ts +87 -50
- package/src/scheduler/store.ts +23 -1
- package/src/scheduler/tool.ts +9 -8
- package/src/session/journal.ts +12 -1
- package/src/session/store.ts +24 -10
- package/src/session/usage.ts +201 -0
- package/src/streaming/event-stream.ts +7 -2
- package/src/tools/executor.ts +17 -2
- package/src/tools/subagent-result.ts +1 -1
- package/src/tools/tool.ts +0 -16
- package/src/types/agent.ts +44 -16
- package/src/types/scheduler.ts +9 -2
- 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. */
|
|
@@ -340,9 +342,11 @@ function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): L
|
|
|
340
342
|
const deadlineMs = num(raw.deadlineMs) ?? Infinity;
|
|
341
343
|
const maxToolCallsPerRun = num(raw.maxToolCallsPerRun) ?? Infinity;
|
|
342
344
|
const maxTotalTokens = num(raw.maxTotalTokens) ?? Infinity;
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
const
|
|
345
|
+
// A network timeout: `0` or `Infinity` turns it off, any other number is milliseconds, anything else is unset (the default applies).
|
|
346
|
+
const timeout = (v: unknown): number | undefined => (typeof v === "number" && !Number.isNaN(v) ? v : undefined);
|
|
347
|
+
const requestTimeoutMs = timeout(raw.requestTimeoutMs) ?? 300_000;
|
|
348
|
+
const firstByteTimeoutMs = timeout(raw.firstByteTimeoutMs) ?? 120_000;
|
|
349
|
+
const streamIdleTimeoutMs = timeout(raw.streamIdleTimeoutMs) ?? 300_000;
|
|
346
350
|
return { maxTurns, deadlineMs, maxToolCallsPerRun, maxTotalTokens, requestTimeoutMs, firstByteTimeoutMs, streamIdleTimeoutMs };
|
|
347
351
|
}
|
|
348
352
|
|
|
@@ -411,16 +415,14 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
|
|
|
411
415
|
}
|
|
412
416
|
}
|
|
413
417
|
|
|
414
|
-
/**
|
|
415
|
-
|
|
416
|
-
* micro-USD accumulation. Returns undefined when unknown (never a 0 object). */
|
|
417
|
-
export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec): TokenUsage["cost"] {
|
|
418
|
+
/** 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`. */
|
|
419
|
+
export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serviceTier?: string): TokenUsage["cost"] {
|
|
418
420
|
try {
|
|
419
421
|
if (usage.cost && usage.cost.totalCost !== undefined && usage.cost.totalCost > 0) {
|
|
420
422
|
return usage.cost;
|
|
421
423
|
}
|
|
422
424
|
if (!spec) return usage.cost;
|
|
423
|
-
const priced = priceUsage(usage, spec);
|
|
425
|
+
const priced = priceUsage(usage, spec, serviceTier ? { serviceTier } : undefined);
|
|
424
426
|
return priced
|
|
425
427
|
? {
|
|
426
428
|
inputCost: priced.inputCost,
|
|
@@ -448,7 +450,7 @@ export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec): Tok
|
|
|
448
450
|
// This function never mutates `source` (Q-35): the priced cost is cloned
|
|
449
451
|
// into a local before folding into the target.
|
|
450
452
|
// ---------------------------------------------------------------------------
|
|
451
|
-
export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec): void {
|
|
453
|
+
export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: ModelSpec, serviceTier?: string): void {
|
|
452
454
|
target.inputTokens += source.inputTokens || 0;
|
|
453
455
|
target.outputTokens += source.outputTokens || 0;
|
|
454
456
|
target.totalTokens += source.totalTokens || 0;
|
|
@@ -475,7 +477,7 @@ export function accumulateUsage(target: TokenUsage, source: TokenUsage, spec?: M
|
|
|
475
477
|
});
|
|
476
478
|
}
|
|
477
479
|
|
|
478
|
-
const priced = computeCostFromPricing(source, spec);
|
|
480
|
+
const priced = computeCostFromPricing(source, spec, serviceTier);
|
|
479
481
|
// Q-35: pure — read `source` only; never assign back to `source.cost`.
|
|
480
482
|
// Clone the priced cost so later target folds cannot alias source state.
|
|
481
483
|
const sourceCost = priced ? { ...priced } : undefined;
|
|
@@ -799,8 +801,9 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
|
|
|
799
801
|
}
|
|
800
802
|
}
|
|
801
803
|
|
|
802
|
-
accumulateUsage(accumulatedUsage, genResult.usage, spec);
|
|
803
|
-
config.
|
|
804
|
+
accumulateUsage(accumulatedUsage, genResult.usage, spec, options?.serviceTier);
|
|
805
|
+
try { config.onTurnUsage?.(genResult.usage, spec); } catch {}
|
|
806
|
+
config.enforceBudget?.({ ...genResult.usage, cost: computeCostFromPricing(genResult.usage, spec, options?.serviceTier) });
|
|
804
807
|
checkTokenLimit(accumulatedUsage, limits.maxTotalTokens);
|
|
805
808
|
|
|
806
809
|
// Record assistant turn in context
|
|
@@ -1252,8 +1255,9 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
|
|
|
1252
1255
|
}
|
|
1253
1256
|
}
|
|
1254
1257
|
|
|
1255
|
-
accumulateUsage(accumulatedUsage, turnResponse.usage, spec2);
|
|
1256
|
-
config.
|
|
1258
|
+
accumulateUsage(accumulatedUsage, turnResponse.usage, spec2, options?.serviceTier);
|
|
1259
|
+
try { config.onTurnUsage?.(turnResponse.usage, spec2); } catch {}
|
|
1260
|
+
config.enforceBudget?.({ ...turnResponse.usage, cost: computeCostFromPricing(turnResponse.usage, spec2, options?.serviceTier) });
|
|
1257
1261
|
checkTokenLimit(accumulatedUsage, streamLimits.maxTotalTokens);
|
|
1258
1262
|
|
|
1259
1263
|
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 ?? Infinity;
|
|
33
|
+
return tool({
|
|
34
|
+
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)"),
|
|
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. 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
|
+
- {{LIMIT}}
|
|
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
|
// ---------------------------------------------------------------------------
|
|
@@ -176,9 +171,11 @@ export function normalizeToolMap(
|
|
|
176
171
|
// Shared dynamic-worker helpers (pure, no store access)
|
|
177
172
|
// ---------------------------------------------------------------------------
|
|
178
173
|
|
|
179
|
-
|
|
180
|
-
export function
|
|
181
|
-
|
|
174
|
+
// The time limit of one scheduled run in ms, 0 = none: the action's own `timeoutMs`, never above the developer's `workerTimeoutMs` (Infinity = none), which is also the limit when the action sets none; in: Agent.dynamicSubagents.workerTimeoutMs + the saved action, out: Scheduler.attempt.
|
|
175
|
+
export function scheduledRunTimeoutMs(cap: number, taskTimeoutMs: number | undefined): number {
|
|
176
|
+
const own = typeof taskTimeoutMs === "number" && taskTimeoutMs > 0 ? Math.floor(taskTimeoutMs) : Infinity;
|
|
177
|
+
const ms = Math.min(own, cap);
|
|
178
|
+
return Number.isFinite(ms) ? ms : 0;
|
|
182
179
|
}
|
|
183
180
|
|
|
184
181
|
/**
|
|
@@ -203,11 +200,6 @@ export function grantWorkerTools(
|
|
|
203
200
|
return grantedTools;
|
|
204
201
|
}
|
|
205
202
|
|
|
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
203
|
function providerOf(modelStr: string | unknown | undefined): string | undefined {
|
|
212
204
|
if (!modelStr) return undefined;
|
|
213
205
|
try {
|
|
@@ -246,18 +238,34 @@ export function resolveWorkerCredentials(
|
|
|
246
238
|
return { apiKeyToUse: undefined, baseUrlToUse: undefined, attemptedProvider };
|
|
247
239
|
}
|
|
248
240
|
|
|
241
|
+
/** How many levels of workers may exist below an agent unless `dynamicSubagents.maxDepth` says otherwise: unlimited. */
|
|
242
|
+
export const DEFAULT_MAX_DEPTH = Infinity;
|
|
243
|
+
|
|
249
244
|
/**
|
|
250
|
-
* Config of a
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* `
|
|
245
|
+
* Config of a dynamic worker (scheduled actions, the worker registry): the developer's fixed worker
|
|
246
|
+
* model, level and limits, never the LLM's choice. Credentials carry over only when the providers match, and
|
|
247
|
+
* `inheritableConfig()` keeps the worker no less constrained than its parent. Stateless one-shot by default;
|
|
248
|
+
* `persistent` makes a multi-turn worker that keeps its history (saved under `persistent.dir` when given, and
|
|
249
|
+
* continued by `resume()` after a crash, never automatically). A persistent worker with levels left below it
|
|
250
|
+
* (`maxDepth` of the parent, minus one for itself) gets its own `subagents` tool; its children can only be granted
|
|
251
|
+
* the tools it was granted. One-shot workers never spawn. `dynamicSubagents.workerBudget` becomes each worker's own
|
|
252
|
+
* per-run `budget` (and is passed to its children).
|
|
254
253
|
*/
|
|
255
254
|
export function buildDynamicWorkerConfig(
|
|
256
255
|
parent: Agent,
|
|
257
|
-
worker: {
|
|
256
|
+
worker: {
|
|
257
|
+
name: string;
|
|
258
|
+
role?: string;
|
|
259
|
+
instructions: string;
|
|
260
|
+
sessionId: string;
|
|
261
|
+
tools: Record<string, ToolDefinition>;
|
|
262
|
+
persistent?: { dir?: string };
|
|
263
|
+
}
|
|
258
264
|
): AgentConfig {
|
|
259
265
|
const model = parent.subagentModel;
|
|
260
266
|
const { apiKeyToUse, baseUrlToUse } = resolveWorkerCredentials(parent, model);
|
|
267
|
+
const dyn = parent.dynamicSubagents;
|
|
268
|
+
const levelsBelow = (dyn?.maxDepth ?? DEFAULT_MAX_DEPTH) - 1;
|
|
261
269
|
return {
|
|
262
270
|
name: worker.name,
|
|
263
271
|
description: worker.role || `Sub-agent ${worker.name}`,
|
|
@@ -268,8 +276,22 @@ export function buildDynamicWorkerConfig(
|
|
|
268
276
|
thinkingLevel: parent.dynamicSubagents?.thinkingLevel ?? parent.thinkingConfig?.level,
|
|
269
277
|
sessionId: worker.sessionId,
|
|
270
278
|
tools: worker.tools,
|
|
271
|
-
|
|
272
|
-
stateless:
|
|
279
|
+
...(dyn?.workerBudget ? { budget: { ...dyn.workerBudget } } : {}),
|
|
280
|
+
stateless: !worker.persistent,
|
|
281
|
+
...(worker.persistent?.dir ? { persist: { dir: worker.persistent.dir }, resume: "explicit" as const } : {}),
|
|
282
|
+
...(worker.persistent && levelsBelow >= 1
|
|
283
|
+
? {
|
|
284
|
+
dynamicSubagents: {
|
|
285
|
+
enabled: true,
|
|
286
|
+
model,
|
|
287
|
+
...(dyn?.maxSpawn !== undefined ? { maxSpawn: dyn.maxSpawn } : {}),
|
|
288
|
+
...(dyn?.thinkingLevel ? { thinkingLevel: dyn.thinkingLevel } : {}),
|
|
289
|
+
tools: worker.tools,
|
|
290
|
+
...(dyn?.workerBudget ? { workerBudget: dyn.workerBudget } : {}),
|
|
291
|
+
maxDepth: levelsBelow,
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
: {}),
|
|
273
295
|
serviceTier: parent.serviceTier,
|
|
274
296
|
headers: parent.customHeaders,
|
|
275
297
|
maxTurns: parent.maxTurns,
|
|
@@ -277,18 +299,6 @@ export function buildDynamicWorkerConfig(
|
|
|
277
299
|
};
|
|
278
300
|
}
|
|
279
301
|
|
|
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
302
|
// ---------------------------------------------------------------------------
|
|
293
303
|
// Shared live worker state machine (single home for fixed + dynamic workers)
|
|
294
304
|
// ---------------------------------------------------------------------------
|
|
@@ -322,7 +332,7 @@ export interface LiveWorkerHooks {
|
|
|
322
332
|
|
|
323
333
|
/**
|
|
324
334
|
* Single live worker state machine shared by fixed (`agentToTool`) and
|
|
325
|
-
*
|
|
335
|
+
* registry workers. Owns `liveTurn` / `partialText` /
|
|
326
336
|
* `partialThinking`, finalizes partial streaming entries on `onTurn`, and
|
|
327
337
|
* forwards deltas to the parent stream. The trace object is mutated in place;
|
|
328
338
|
* trimming / touching / notifying / persisting are delegated to hooks so this
|