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/README.md
CHANGED
|
@@ -121,7 +121,7 @@ An `Agent` holds:
|
|
|
121
121
|
| `tools` | `Record<string, ToolDefinition> \| ToolDefinition[]` | Deterministic functions the model can call. See [Tools](#tools). |
|
|
122
122
|
| `subagents` | `SubAgent[] \| Agent[]` | Pre-defined workers. Each worker becomes a callable tool. Useful for fixed roles such as researcher or critic. |
|
|
123
123
|
| `subagentModel` | `string \| ModelSpec \| ModelProviderInstance` | Developer-only model for dynamically spawned workers. Never choosable by the Main Agent. Deprecated alias of `dynamicSubagents.model` (which wins). Falls back to `SUB_AGENT_MODEL`. |
|
|
124
|
-
| `dynamicSubagents` | `DynamicSubagentsConfig` | Enables and constrains LLM-spawned
|
|
124
|
+
| `dynamicSubagents` | `DynamicSubagentsConfig` | Enables and constrains LLM-spawned workers: `{ enabled, model?, maxSpawn?, maxDepth?, workerBudget?, thinkingLevel?, tools?, timeout?, allowScheduledActions? }` (`timeout` is for scheduled actions only). See [Pre-defined vs Dynamic Delegation](#pre-defined-vs-dynamic-delegation) and [Scheduled actions](#scheduled-actions). |
|
|
125
125
|
| `thinking` | `ThinkingOption` | Canonical reasoning option: `none \| dynamic \| minimal \| low \| medium \| high \| xhigh \| max`, or `{ level, budgetTokens }`. Legacy `thinkingLevel` still works (canonical wins). Validated against the model catalog before a request. |
|
|
126
126
|
| `cache` | `CacheOption` | `"implicit" \| "5m" \| "30m" \| "1h" \| "12h"`. Prompt-cache lifetime; unsupported values warn and fall back (see [Cache](#cache)). |
|
|
127
127
|
| `serviceTier` | `"flex" \| "priority"` | Cost / priority routing where supported. Omit for standard routing. |
|
|
@@ -269,47 +269,60 @@ const agent = new Agent({
|
|
|
269
269
|
instructions: "You are the Main Agent",
|
|
270
270
|
dynamicSubagents: {
|
|
271
271
|
enabled: true,
|
|
272
|
-
maxSpawn: 4,
|
|
272
|
+
maxSpawn: 4, // open workers at once
|
|
273
273
|
thinkingLevel: "low",
|
|
274
|
-
tools: { get_weather, recent_news },
|
|
275
|
-
timeout: 60000,
|
|
274
|
+
tools: { get_weather, recent_news }, // pool: the model grants a subset per worker
|
|
276
275
|
},
|
|
277
276
|
});
|
|
278
277
|
```
|
|
279
278
|
|
|
280
|
-
|
|
279
|
+
One `subagents` tool is injected. It takes `{ command, id?, name?, role?, instructions?, task?, tools?, history? }` and its manual is the tool description (`src/agent/subagents.md`, with the worker pool and the open-worker limit filled in; the system prompt stays untouched):
|
|
280
|
+
|
|
281
|
+
- `spawn` starts a worker (`name`, `task`; optional `instructions`, `role`, `tools`, `history`) and waits for its first reply. `message` sends `task` to an open worker `id`, which remembers the whole conversation. `close` frees the slot. `list` shows the open workers and the numbered finished turns that `history: N` can copy into a new worker.
|
|
282
|
+
- Results are plain text with one header line: `[worker researcher-1a2b3c4d] replied:` + the worker's text, or `[worker researcher-1a2b3c4d] failed: <reason>`. Several `spawn` calls in one model turn run in parallel. A bad request comes back as an `Error: ...` line the model can fix.
|
|
283
|
+
- `maxSpawn` is the limit on **open** workers (the model must close one to open another). `budget.maxTotalSubagents` still counts spawns per run.
|
|
284
|
+
- `tools` is the developer-owned pool. Workers get zero tools unless the Main Agent grants a subset by name; unknown names are rejected with the pool listed.
|
|
285
|
+
- The Main Agent cannot choose worker models, reasoning levels or timeouts. The `subagents` tool has no timeouts (`workerTimeoutMs`/`timeout` apply only to scheduled actions).
|
|
286
|
+
- `maxDepth` (default 3, whole number >= 1) is how many levels of workers may exist below the agent: `1` keeps workers flat, `3` lets a worker start workers that start workers. A worker with levels left gets its own `subagents` tool, and its children can only be granted the tools **it** was granted. The levels are saved inside each other (`<session>/workers/<id>/workers/<id>/…`).
|
|
287
|
+
- `workerBudget: { maxCostUsd?, maxTotalTokens? }` is each worker's own spending cap, applied as that worker's per-run `budget` (a spawn, a message or a scheduled run is one run; the usage of the worker's own workers counts toward it) and passed down to its workers. Going over fails that call with a budget error (`[worker <id>] failed: Token budget exceeded…`), leaves the worker open and does not affect the parent. Default: unlimited; models with unknown pricing count $0 toward `maxCostUsd`. Values must be numbers above 0.
|
|
288
|
+
- Workers are real agents with their own session: see [Persistent workers](#persistent-workers-agentworkers) below (the same functions in code, saved on disk with `persist`, resumed after a restart). Each reply's usage is metered into the parent's usage and budget.
|
|
289
|
+
- Each run of a worker gets a system-generated 32-char TrackingID, displayed as `NAME-{TrackingID}`. Thinking/text deltas update the worker trace live (`agent.track(id)`, `subscribeToSubAgent`) and surface on the parent stream as `subagent_delta` events.
|
|
281
290
|
|
|
282
291
|
```ts
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
role?,
|
|
287
|
-
instructions,
|
|
288
|
-
task,
|
|
289
|
-
tools?: string[],
|
|
290
|
-
timeoutMs?: number
|
|
291
|
-
}[]
|
|
292
|
-
}
|
|
292
|
+
const res = await agent.run("Audit auth pipeline and write a threat model");
|
|
293
|
+
|
|
294
|
+
console.log(res.subagents.map((s) => s.name));
|
|
293
295
|
```
|
|
294
296
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
-
|
|
298
|
-
- Workers are stateless: one task in, one result out, then shut down. No history, no recursion. The Main Agent cannot choose worker models or reasoning levels.
|
|
299
|
-
- Worker names use UPPER-KEBAB (`HBM-PRICING-ANALYST`). Each worker gets a system-generated 32-char TrackingID, displayed as `NAME-{TrackingID}` (e.g. `HBM-PRICING-ANALYST-9f2c…`).
|
|
300
|
-
- Workers stream internally: thinking/text deltas update the worker trace live (`agent.track(id)`, `subscribeToSubAgent`) and surface on the parent stream as `subagent_delta` events.
|
|
297
|
+
### Persistent workers (`agent.workers`)
|
|
298
|
+
|
|
299
|
+
Besides one-shot delegation, an agent can run **workers that keep their conversation**: start one, message it again and again, close it. A worker is a plain `Agent` with its own instructions and tools, on the developer's worker model (`dynamicSubagents.model` or `SUB_AGENT_MODEL`). With `persist: { dir }` on the parent it is saved under `<parent session folder>/workers/<id>/` (own journal, tool logs and `worker.json`), so it is rebuilt with its history after a restart.
|
|
301
300
|
|
|
302
301
|
```ts
|
|
303
|
-
const
|
|
302
|
+
const reply = await lead.workers.spawn({
|
|
303
|
+
name: "Researcher",
|
|
304
|
+
instructions: "You research databases and cite sources.", // the worker's OWN prompt; the parent's is never copied
|
|
305
|
+
task: "Compare SQLite and Postgres for a small SaaS.",
|
|
306
|
+
tools: ["recent_news"], // subset of dynamicSubagents.tools; unknown names throw
|
|
307
|
+
history: 1, // optional: start from a copy of the parent's first 1 finished turns (see lead.workers.turns()); default "none"
|
|
308
|
+
});
|
|
309
|
+
console.log(reply.id, reply.text); // e.g. "researcher-1a2b3c4d"
|
|
304
310
|
|
|
305
|
-
|
|
311
|
+
const next = await lead.workers.message(reply.id, "Now focus on write-heavy loads."); // same worker, same conversation
|
|
312
|
+
lead.workers.list(); // open workers: { id, name, role?, tools, busy }
|
|
313
|
+
lead.workers.close(reply.id); // can no longer be messaged; its session stays on disk
|
|
306
314
|
```
|
|
307
315
|
|
|
316
|
+
- A turn is one **finished** run (`done` or ended by `ctx.stop`); the running turn and failed or aborted runs are never copied, so a worker never gets a prompt without its answer.
|
|
317
|
+
- `spawn` and `message` wait for the reply and never throw for a failing worker: the `WorkerReply` then carries `error` and a text that says so. They throw `ValidationError` for a bad request (unknown tool or id, closed or busy worker, too many open workers: `dynamicSubagents.maxSpawn` is the open-worker limit, `budget.maxTotalSubagents` still counts spawns per run).
|
|
318
|
+
- A worker handles one call at a time. Pass `{ signal }` to abort its run; the worker stays open.
|
|
319
|
+
- Exactly once across a crash: pass `{ callId }` (the calling tool call's id). A replayed `spawn` finds the worker it already started, a cut-off run is continued with `resume()` instead of sending the text twice, and a call that had already been answered returns that saved answer.
|
|
320
|
+
- Without `persist` the workers live in memory only. A damaged `worker.json` is skipped with a `worker.invalid` event.
|
|
321
|
+
- The reply's `metadata` (usage, steps, lineage) has the same shape as `AgentResponse.subagents`; `agent.track(metadata.trackingId)` shows the run live.
|
|
322
|
+
|
|
308
323
|
### Delegation Helpers
|
|
309
324
|
|
|
310
325
|
* `buildAgentTools(list)` — wraps multiple agents and deduplicates names. Used internally by `subagents`.
|
|
311
|
-
* `createSubagentSpawnTool(parentAgent)` — builds the dynamic subagent spawner. Rarely needed directly.
|
|
312
|
-
* `DynamicSubagentTask` — `{ name, role?, instructions, task, tools?, timeoutMs? }`, the shape produced for dynamic delegation. No `model` — workers run on the developer-configured model.
|
|
313
326
|
|
|
314
327
|
---
|
|
315
328
|
## Scheduled actions
|
|
@@ -426,11 +439,11 @@ const pay = tool({
|
|
|
426
439
|
|
|
427
440
|
`ctx.stop` throws, so the code after it never runs (even if you forget `return`). The reason is saved as that call's result, calls that were running in parallel in the same batch still run to completion (none is skipped or aborted), and then the run ends with `finishReason: "stopped"` and **no further model turn**. The agent waits for the next user message; the model then sees the reason as the tool's result. A steer typed while the batch ran becomes the next run. The stop is saved with the call, so a crash cannot turn a "no" into a "yes": after a restart the run is closed as stopped without calling the model. A tool's `timeoutMs` counts the time spent waiting for your check, so leave it off for tools that ask a person.
|
|
428
441
|
|
|
429
|
-
To guard a built-in tool such as `
|
|
442
|
+
To guard a built-in tool such as `subagents` or `scheduled_actions`, wrap it on the agent after construction:
|
|
430
443
|
|
|
431
444
|
```ts
|
|
432
|
-
const inner = agent.tools["
|
|
433
|
-
agent.tools["
|
|
445
|
+
const inner = agent.tools["subagents"]!;
|
|
446
|
+
agent.tools["subagents"] = {
|
|
434
447
|
...inner,
|
|
435
448
|
execute: async (input, ctx) => ((await getPermission("Spawn workers?")) ? inner.execute(input, ctx) : ctx.stop("spawning was not approved")),
|
|
436
449
|
};
|
|
@@ -1011,9 +1024,32 @@ TokenUsage {
|
|
|
1011
1024
|
}
|
|
1012
1025
|
```
|
|
1013
1026
|
|
|
1014
|
-
Cost calculation prefers provider-reported totals. When unavailable, costs are computed using catalog pricing.
|
|
1027
|
+
Cost calculation prefers provider-reported totals. When unavailable, costs are computed using catalog pricing, per model turn: fresh input at the input price, cache reads at the cache-read price, cache writes at the cache-write price (both are part of `inputTokens`, so they are not billed twice), output at the output price (thinking tokens are part of output), scaled by `serviceTier` (`flex` x0.5, `priority` x2, estimates). A catalog entry without a cache price bills those tokens at the input rate (cache is never free); unknown pricing stays unknown.
|
|
1015
1028
|
|
|
1016
1029
|
Subagent usage is rolled into the parent totals.
|
|
1030
|
+
|
|
1031
|
+
### Usage report (`agent.usageReport()`)
|
|
1032
|
+
|
|
1033
|
+
`res.usage` is the cost of one run, workers included. `agent.usageReport()` is the other view: what the agent and **each** worker spent over their lives, itemized, with nothing counted twice.
|
|
1034
|
+
|
|
1035
|
+
```ts
|
|
1036
|
+
const report = agent.usageReport();
|
|
1037
|
+
report.agent; // this agent's OWN model calls: id, name, model, turns, tokens, cacheHitRate, cost
|
|
1038
|
+
report.workers; // every worker below it (open, closed, nested in `workers`, and `status: "fixed"` for `subagents: [...]`)
|
|
1039
|
+
report.total; // agent + all workers
|
|
1040
|
+
// each line, e.g. report.workers[0]:
|
|
1041
|
+
// { id: "researcher-1a2b3c4d", status: "open", model: "google/gemini-3.5-flash-lite", turns: 2,
|
|
1042
|
+
// tokens: { input: 2000, freshInput: 600, cacheRead: 1200, cacheWrite: 200, output: 400, thinking: 100, total: 2400 },
|
|
1043
|
+
// cacheHitRate: 0.6,
|
|
1044
|
+
// cost: { source: "catalog", unpricedTurns: 0, totalUsd: 0.00891,
|
|
1045
|
+
// parts: { inputUsd: 0.0018, outputUsd: 0.006, cacheReadUsd: 0.00036, cacheWriteUsd: 0.00075 } } }
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
- **Own turns only.** Every agent records the model calls it made itself, priced **per turn** in integer micro-USD, and saves them in its session (`usage`), so the numbers survive a restart (load the session and the totals carry on; workers are rebuilt from their folders). A parent's `res.usage` already contains its workers' usage, which is why the report never adds responses together: `report.total.tokens.total` equals what the responses show.
|
|
1049
|
+
- **Cache-aware.** `input` includes the cache reads and writes (`freshInput = input - cacheRead - cacheWrite`); `output` includes `thinking`. Fresh input, cache reads and cache writes are each priced at their own catalog price, and `serviceTier` is applied. `cacheHitRate` is `cacheRead / input` (0 to 1, `null` when nothing was sent).
|
|
1050
|
+
- **Honest costs.** `cost.source` is `provider` (the provider reported the cost, which is exact), `catalog` (estimated from the model catalog), `mixed`, or `unknown`. Unknown pricing is `totalUsd: null`, never 0; `unpricedTurns` above 0 means the total is a lower bound. For turns whose cost the provider reported only the total is exact, so `parts` can add up to less than `totalUsd`.
|
|
1051
|
+
- **Not in the report:** the one-shot workers of scheduled actions (their cost is in each run file), and workers started inside a fixed `subagents` worker's per-call copy (their cost is in the response usage). `reset()` does not clear the totals: they are spending, not conversation.
|
|
1052
|
+
|
|
1017
1053
|
### ProviderRawData
|
|
1018
1054
|
|
|
1019
1055
|
```ts
|
|
@@ -1288,7 +1324,7 @@ bun run examples/04-sub-agents.ts
|
|
|
1288
1324
|
# fixed researcher + critic pipeline
|
|
1289
1325
|
|
|
1290
1326
|
bun run examples/03-multi_agent.ts
|
|
1291
|
-
# dynamic
|
|
1327
|
+
# dynamic subagents tool demo
|
|
1292
1328
|
|
|
1293
1329
|
bun run examples/01-metadata.ts
|
|
1294
1330
|
# usage + raw inspection
|
|
@@ -1,34 +1,28 @@
|
|
|
1
|
-
You are an Agent — you evaluate user objectives and decide whether to solve them directly or delegate to specialized parallel
|
|
1
|
+
You are an Agent — you evaluate user objectives and decide whether to solve them directly or delegate to specialized parallel workers with the `subagents` tool.
|
|
2
2
|
|
|
3
3
|
### Workflow & Decision Matrix
|
|
4
4
|
1. **Triage**:
|
|
5
5
|
- Single-focus, factual, code-generation, or conversational prompts → Answer directly, concisely, and completely.
|
|
6
|
-
- Do NOT
|
|
6
|
+
- Do NOT start workers for trivial, single-step tasks.
|
|
7
7
|
2. **Delegate**:
|
|
8
|
-
|
|
9
|
-
- For each
|
|
10
|
-
- `name`:
|
|
8
|
+
- Complex research, multi-angle analysis, deep technical trade-offs, or parallel explorations → call `subagents` with `command: "spawn"` once per worker, 2 to 4 focused workers, all in the same turn so they run in parallel.
|
|
9
|
+
- For each worker define:
|
|
10
|
+
- `name`: short label describing its domain focus (e.g. `market-analyst`, `systems-architect`).
|
|
11
11
|
- `role`: Distinct specialized persona.
|
|
12
12
|
- `instructions`: Self-contained, expert prompt detailing scope, methodologies, and constraints.
|
|
13
13
|
- `task`: Focused, non-overlapping research query or execution directive.
|
|
14
14
|
3. **Aggregate**:
|
|
15
|
-
-
|
|
16
|
-
```xml
|
|
17
|
-
<SUB-AGENTS-RESPONSE>
|
|
18
|
-
<DYNAMIC_ROLE_1>
|
|
19
|
-
Findings from sub-agent 1
|
|
20
|
-
</DYNAMIC_ROLE_1>
|
|
21
|
-
<DYNAMIC_ROLE_2>
|
|
22
|
-
Findings from sub-agent 2
|
|
23
|
-
</DYNAMIC_ROLE_2>
|
|
24
|
-
</SUB-AGENTS-RESPONSE>
|
|
15
|
+
- Each worker answers with a header line and its findings:
|
|
25
16
|
```
|
|
26
|
-
|
|
17
|
+
[worker market-analyst-1a2b3c4d] replied:
|
|
18
|
+
Findings from the worker
|
|
19
|
+
```
|
|
20
|
+
- Use `command: "message"` with the worker's `id` to ask a follow-up; the worker remembers the conversation. `command: "close"` when you no longer need it.
|
|
27
21
|
4. **Synthesize**:
|
|
28
|
-
- Deliver an editorial-grade, cohesive report that integrates all
|
|
22
|
+
- Deliver an editorial-grade, cohesive report that integrates all worker insights.
|
|
29
23
|
- Reconcile contradictions, de-duplicate redundant information, and highlight critical trade-offs.
|
|
30
|
-
- Structure with executive summaries, comparative Markdown tables, and concrete strategic conclusions. Never present an uncurated list of
|
|
24
|
+
- Structure with executive summaries, comparative Markdown tables, and concrete strategic conclusions. Never present an uncurated list of worker summaries.
|
|
31
25
|
|
|
32
26
|
### Execution Rules
|
|
33
|
-
- **Tool Protocol**:
|
|
34
|
-
- **Context Isolation**:
|
|
27
|
+
- **Tool Protocol**: Call `subagents` directly without conversational preambles. Start parallel workers with several `spawn` calls in one turn.
|
|
28
|
+
- **Context Isolation**: Workers start blank; provide complete background in their `instructions` and `task`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-accelerator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sashvat Bharat",
|
package/src/agent/agent.ts
CHANGED
|
@@ -10,8 +10,11 @@ import { AgentResponse, type StreamEvent } from "../types/response.ts";
|
|
|
10
10
|
import { AssistantMessageEventStream } from "../streaming/event-stream.ts";
|
|
11
11
|
import { AgentContext } from "./context.ts";
|
|
12
12
|
import { resolveModel } from "../providers/registry.ts";
|
|
13
|
-
import { buildAgentTools
|
|
13
|
+
import { buildAgentTools } from "./delegation.ts";
|
|
14
|
+
import { createSubagentsTool } from "./subagents-tool.ts";
|
|
15
|
+
import { WorkerRegistry } from "./workers.ts";
|
|
14
16
|
import {
|
|
17
|
+
DEFAULT_MAX_DEPTH,
|
|
15
18
|
normalizeToolMap,
|
|
16
19
|
resolveDynamicModelInput,
|
|
17
20
|
resolveMainModel,
|
|
@@ -34,7 +37,8 @@ import {
|
|
|
34
37
|
listSubAgentTraceIdsForSessions,
|
|
35
38
|
type SubAgentTrace,
|
|
36
39
|
} from "./delegation.ts";
|
|
37
|
-
import {
|
|
40
|
+
import { addTurn, buildUsageReport, emptyUsage, mergeUsage, modelName, readUsage, type UsageNode, type UsageReport, type UsageTotals } from "../session/usage.ts";
|
|
41
|
+
import { saveSessionFile, sessionDirFor, loadSessionDir, findInvalidMessage, priceUsage, telemetryBus, type SessionRunMark, type SessionRunEnd, type SessionPending, type SessionToolRecord } from "../session/store.ts";
|
|
38
42
|
import { SessionJournal } from "../session/journal.ts";
|
|
39
43
|
import { ToolLogs, type ToolLogWriter } from "../session/tool-log.ts";
|
|
40
44
|
import { isLocalPathInput } from "../utils/safe-io.ts";
|
|
@@ -167,6 +171,10 @@ export class Agent {
|
|
|
167
171
|
timeout: number;
|
|
168
172
|
/** Scheduled actions (`scheduled_actions` + `get_datetime` tools) are on. */
|
|
169
173
|
allowScheduledActions: boolean;
|
|
174
|
+
/** Levels of workers allowed below this agent. */
|
|
175
|
+
maxDepth: number;
|
|
176
|
+
/** Per-run spending cap of each worker (unset = unlimited). */
|
|
177
|
+
workerBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
|
|
170
178
|
};
|
|
171
179
|
/** Home folder for everything written to disk (`sessions/`, `scheduled_actions/`, `telemetry/`). */
|
|
172
180
|
readonly home: string;
|
|
@@ -263,6 +271,8 @@ export class Agent {
|
|
|
263
271
|
readonly runMarks: SessionRunMark[] = [];
|
|
264
272
|
/** Tool calls finished in the current run; saved the moment each finishes so a crash never repeats them. @internal */
|
|
265
273
|
readonly toolRecords: SessionToolRecord[] = [];
|
|
274
|
+
/** What this agent spent on its OWN model calls (workers keep their own); saved with the session. @internal */
|
|
275
|
+
readonly usageTotals: UsageTotals = emptyUsage();
|
|
266
276
|
/** The run in flight (its mark has no ending yet, but it is not "interrupted"). */
|
|
267
277
|
private activeMark?: SessionRunMark;
|
|
268
278
|
/** What to do with a cut-off run at construction (see `AgentConfig.resume`). */
|
|
@@ -271,6 +281,7 @@ export class Agent {
|
|
|
271
281
|
private readonly runRetries: number;
|
|
272
282
|
/** The resume in flight, so a second `resume()` returns the same promise. */
|
|
273
283
|
private resuming?: Promise<AgentResponse | undefined>;
|
|
284
|
+
private workerRegistry?: WorkerRegistry;
|
|
274
285
|
|
|
275
286
|
/**
|
|
276
287
|
* Creates an agent and registers its model, tools, cache, and delegation settings.
|
|
@@ -307,6 +318,17 @@ export class Agent {
|
|
|
307
318
|
const dynEnabled = dynRaw ? (dynRaw.enabled ?? true) : false;
|
|
308
319
|
if (dynRaw) {
|
|
309
320
|
const dynTools = normalizeToolMap((dynRaw as { tools?: Record<string, ToolDefinition> | ToolDefinition[] }).tools);
|
|
321
|
+
const rawDepth = (dynRaw as { maxDepth?: unknown }).maxDepth;
|
|
322
|
+
if (rawDepth !== undefined && !(Number.isInteger(rawDepth) && (rawDepth as number) >= 1)) {
|
|
323
|
+
throw new ConfigError(`[Agent Accelerator] Invalid dynamicSubagents.maxDepth ${JSON.stringify(rawDepth)}: expected a whole number >= 1 (default ${DEFAULT_MAX_DEPTH}); set dynamicSubagents.enabled: false to turn workers off.`);
|
|
324
|
+
}
|
|
325
|
+
const rawBudget = (dynRaw as { workerBudget?: { maxCostUsd?: unknown; maxTotalTokens?: unknown } }).workerBudget;
|
|
326
|
+
for (const key of ["maxCostUsd", "maxTotalTokens"] as const) {
|
|
327
|
+
const v = rawBudget?.[key];
|
|
328
|
+
if (v !== undefined && !(typeof v === "number" && Number.isFinite(v) && v > 0)) {
|
|
329
|
+
throw new ConfigError(`[Agent Accelerator] Invalid dynamicSubagents.workerBudget.${key} ${JSON.stringify(v)}: expected a number greater than 0, or omit it for no limit.`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
310
332
|
const rawMax = (dynRaw as any)?.maxSpawn;
|
|
311
333
|
const maxSpawn = Number.isFinite(rawMax) ? Math.max(1, Math.floor(rawMax as number)) : 4;
|
|
312
334
|
// `workerTimeoutMs ?? timeout`, or -1 when `modelMaySetTimeout` (see normalizeWorkerTimeout).
|
|
@@ -319,6 +341,8 @@ export class Agent {
|
|
|
319
341
|
tools: dynTools,
|
|
320
342
|
timeout,
|
|
321
343
|
allowScheduledActions: dynRaw.allowScheduledActions === true,
|
|
344
|
+
maxDepth: (rawDepth as number | undefined) ?? DEFAULT_MAX_DEPTH,
|
|
345
|
+
...(rawBudget ? { workerBudget: { ...(rawBudget as { maxCostUsd?: number; maxTotalTokens?: number }) } } : {}),
|
|
322
346
|
};
|
|
323
347
|
}
|
|
324
348
|
this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
|
|
@@ -438,8 +462,8 @@ export class Agent {
|
|
|
438
462
|
);
|
|
439
463
|
}
|
|
440
464
|
if (dyn?.enabled === true) {
|
|
441
|
-
const
|
|
442
|
-
this.tools[
|
|
465
|
+
const subagentsTool = createSubagentsTool(this);
|
|
466
|
+
this.tools[subagentsTool.name || "subagents"] = subagentsTool;
|
|
443
467
|
}
|
|
444
468
|
if (dyn?.allowScheduledActions) {
|
|
445
469
|
// Fixed by config, so the tool set (and the prompt cache) stays stable.
|
|
@@ -459,6 +483,11 @@ export class Agent {
|
|
|
459
483
|
}
|
|
460
484
|
}
|
|
461
485
|
|
|
486
|
+
/** The persistent sub-agent workers of this agent: `spawn`, `message`, `close`, `list`, `turns` (see `WorkerRegistry`). */
|
|
487
|
+
get workers(): WorkerRegistry {
|
|
488
|
+
return (this.workerRegistry ??= new WorkerRegistry(this));
|
|
489
|
+
}
|
|
490
|
+
|
|
462
491
|
// Loads a persisted session that a crash cut off (a run without an ending) or that still had waiting prompts, so it can be resumed; true when there is something to resume; in: the session folder of `persist.dir`, out: context, runMarks, toolRecords, pendingQueue.
|
|
463
492
|
private recoverSession(): boolean {
|
|
464
493
|
const root = this.persistConfig?.dir;
|
|
@@ -475,6 +504,7 @@ export class Agent {
|
|
|
475
504
|
if (!(session.runs ?? []).some((r) => !r.end) && waiting.length === 0) return false;
|
|
476
505
|
this.context.messages = session.messages;
|
|
477
506
|
this.runMarks.splice(0, this.runMarks.length, ...(session.runs ?? []));
|
|
507
|
+
this.restoreUsage(session.usage);
|
|
478
508
|
const open = new Set(unfinishedToolCalls(this.context.messages).map((c) => c.id));
|
|
479
509
|
this.toolRecords.splice(0, this.toolRecords.length, ...(session.tools ?? []).filter((t) => open.has(t.id)));
|
|
480
510
|
for (const w of waiting) {
|
|
@@ -489,6 +519,42 @@ export class Agent {
|
|
|
489
519
|
return true;
|
|
490
520
|
}
|
|
491
521
|
|
|
522
|
+
// Puts saved spending totals back after a restart or import (zeros when the file has none); in: a loaded session's `usage`, out: this.usageTotals.
|
|
523
|
+
private restoreUsage(saved: unknown): void {
|
|
524
|
+
Object.assign(this.usageTotals, readUsage(saved));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Records one model call of THIS agent (priced per turn, with the service tier); in: the loop's onTurnUsage, out: this.usageTotals (saved by the next persist).
|
|
528
|
+
private recordTurnUsage(usage: TokenUsage, spec: Parameters<typeof priceUsage>[1]): void {
|
|
529
|
+
addTurn(this.usageTotals, usage, priceUsage(usage, spec, this.serviceTier ? { serviceTier: this.serviceTier } : undefined));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Adds another agent's spending to this one (a fixed worker's per-call copy into its template). @internal */
|
|
533
|
+
absorbUsage(other: UsageTotals): void {
|
|
534
|
+
mergeUsage(this.usageTotals, other);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* What this agent and every worker below it spent: per agent and per worker (tokens, cache hits, thinking, exact
|
|
539
|
+
* cost per turn) plus a whole-session total, with nothing counted twice. `agent` is this agent's own model calls;
|
|
540
|
+
* workers (open, closed, nested, and rebuilt from disk after a restart) and fixed `subagents` workers are listed
|
|
541
|
+
* below it. Spending of one-shot scheduled-action workers is in their run files, not here.
|
|
542
|
+
*/
|
|
543
|
+
usageReport(): UsageReport {
|
|
544
|
+
return buildUsageReport({ id: this.sessionId, name: this.name, model: modelName(this.modelStringOrSpec), totals: this.usageTotals }, [...this.workers.usageNodes(), ...this.fixedWorkerNodes()]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// The `subagents: [...]` workers as usage nodes (their per-call copies' spending is merged into them); in: the constructor config, out: usageReport.
|
|
548
|
+
private fixedWorkerNodes(): UsageNode[] {
|
|
549
|
+
const nodes: UsageNode[] = [];
|
|
550
|
+
for (const entry of this.forkConfig.subagents ?? []) {
|
|
551
|
+
const worker = "agent" in entry ? entry.agent : entry;
|
|
552
|
+
const name = "agent" in entry ? entry.name : worker.name;
|
|
553
|
+
nodes.push({ id: name, name, model: modelName(worker.modelStringOrSpec), status: "fixed", totals: worker.usageTotals, workers: [] });
|
|
554
|
+
}
|
|
555
|
+
return nodes;
|
|
556
|
+
}
|
|
557
|
+
|
|
492
558
|
// Says on the telemetry bus that resuming (or a prompt that was waiting) failed; in: a rejected resume, out: telemetryBus.
|
|
493
559
|
private reportResumeFailure(err: unknown): void {
|
|
494
560
|
telemetryBus.emitWarning("agent.resume_failed", `[Agent Accelerator] Resuming session ${this.sessionId} failed: ${err instanceof Error ? err.message : String(err)}`, { sessionId: this.sessionId });
|
|
@@ -579,26 +645,6 @@ export class Agent {
|
|
|
579
645
|
"3. Once your reasoning is complete, output your final response or function calls directly in the standard response output.";
|
|
580
646
|
out = out ? `${out}\n\n${guidance}` : guidance;
|
|
581
647
|
}
|
|
582
|
-
if (this.dynamicSubagents?.enabled === true && !out.includes("[Dynamic Sub-Agents]")) {
|
|
583
|
-
const dyn = this.dynamicSubagents;
|
|
584
|
-
const toolNames = Object.keys(dyn.tools);
|
|
585
|
-
const timeoutNote =
|
|
586
|
-
dyn.timeout === -1
|
|
587
|
-
? "Timeout policy: set a per-sub-agent timeoutMs (ms) for each task; omit it for no limit."
|
|
588
|
-
: dyn.timeout === 0
|
|
589
|
-
? "Timeout policy: workers run with no time limit."
|
|
590
|
-
: `Timeout policy: every worker is limited to ${dyn.timeout}ms; per-task timeouts are ignored.`;
|
|
591
|
-
const policy =
|
|
592
|
-
"[Dynamic Sub-Agents]\n" +
|
|
593
|
-
`1. You may spawn at most ${dyn.maxSpawn} sub-agent(s) per spawn_subagents call. Extra tasks beyond ${dyn.maxSpawn} are ignored.\n` +
|
|
594
|
-
"2. Workers are stateless: each receives one task, returns its result, then shuts down. No conversation history is kept.\n" +
|
|
595
|
-
"3. You cannot choose worker models or reasoning levels — they are fixed by the developer.\n" +
|
|
596
|
-
(toolNames.length > 0
|
|
597
|
-
? `4. Worker-available tools: ${toolNames.join(", ")}. Grant each worker ONLY the tools its task needs via the per-task tools list; omit it for no tools.\n`
|
|
598
|
-
: "4. No worker tools are available; omit the per-task tools list.\n") +
|
|
599
|
-
`5. ${timeoutNote}`;
|
|
600
|
-
out = out ? `${out}\n\n${policy}` : policy;
|
|
601
|
-
}
|
|
602
648
|
return out || undefined;
|
|
603
649
|
}
|
|
604
650
|
|
|
@@ -1161,6 +1207,7 @@ export class Agent {
|
|
|
1161
1207
|
...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
|
|
1162
1208
|
...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
|
|
1163
1209
|
onProgress: () => this.persistNow(),
|
|
1210
|
+
onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
|
|
1164
1211
|
onToolResult: (r: ToolResultRecord) => this.recordTool(r),
|
|
1165
1212
|
openToolLog: (call: { name: string }, sid: string | undefined) => this.openToolLog(call, sid),
|
|
1166
1213
|
...(resumeMark ? { resume: { stored: this.storedToolResults() } } : {}),
|
|
@@ -1251,6 +1298,7 @@ export class Agent {
|
|
|
1251
1298
|
...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
|
|
1252
1299
|
...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
|
|
1253
1300
|
onProgress: () => this.persistNow(),
|
|
1301
|
+
onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
|
|
1254
1302
|
onToolResult: (r: ToolResultRecord) => this.recordTool(r),
|
|
1255
1303
|
openToolLog: (call: { name: string }, sid: string | undefined) => this.openToolLog(call, sid),
|
|
1256
1304
|
steerInbox,
|
|
@@ -1501,6 +1549,7 @@ export class Agent {
|
|
|
1501
1549
|
}
|
|
1502
1550
|
this.context.thoughtSignatures = [];
|
|
1503
1551
|
this.runMarks.splice(0, this.runMarks.length, ...(data.runs ?? []).filter((r) => r.i < this.context.messages.length));
|
|
1552
|
+
this.restoreUsage(data.usage);
|
|
1504
1553
|
if (data.systemPrompt) {
|
|
1505
1554
|
this.context.systemPrompt = data.systemPrompt;
|
|
1506
1555
|
} else {
|
package/src/agent/context.ts
CHANGED
|
@@ -11,7 +11,7 @@ function fail(message: string): never {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** Deep clone that preserves Uint8Array/ArrayBuffer media (structuredClone may be absent). */
|
|
14
|
-
function deepCloneValue<T>(value: T): T {
|
|
14
|
+
export function deepCloneValue<T>(value: T): T {
|
|
15
15
|
if (value instanceof Uint8Array) return new Uint8Array(value) as T;
|
|
16
16
|
if (value instanceof ArrayBuffer) return value.slice(0) as T;
|
|
17
17
|
if (Array.isArray(value)) return value.map((v) => deepCloneValue(v)) as T;
|