agent-accelerator 0.5.2 → 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 +117 -32
- package/examples/prompts/SYSTEM_PROMPT_AGENT.md +14 -20
- package/package.json +1 -1
- package/src/agent/agent.ts +355 -38
- package/src/agent/context.ts +3 -1
- package/src/agent/delegation.ts +208 -524
- package/src/agent/loop.ts +139 -18
- 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 +5 -3
- package/src/models/catalog-cache.ts +3 -10
- package/src/scheduler/scheduler.ts +2 -0
- package/src/scheduler/store.ts +34 -21
- package/src/session/journal.ts +212 -0
- package/src/session/store.ts +203 -90
- package/src/session/tool-log.ts +62 -0
- package/src/session/usage.ts +201 -0
- package/src/telemetry-file.ts +4 -1
- package/src/tools/executor.ts +72 -15
- package/src/tools/subagent-result.ts +1 -1
- package/src/tools/tool.ts +0 -6
- package/src/types/agent.ts +45 -6
- package/src/types/message.ts +4 -0
- package/src/types/response.ts +2 -0
- package/src/types/tool.ts +19 -7
- package/src/utils/durable.ts +218 -0
- package/src/utils/headers.ts +1 -1
- package/src/utils/retry.ts +15 -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. |
|
|
@@ -131,7 +131,9 @@ An `Agent` holds:
|
|
|
131
131
|
| `apiKey` | `string` | Overrides environment-based API-key lookup for this agent. |
|
|
132
132
|
| `baseUrl` | `string` | Overrides the default endpoint for this agent. |
|
|
133
133
|
| `stateless` | `boolean` | When `true`, history is cleared before and after each `run`. Useful for one-shot evaluators. Defaults to `false`. |
|
|
134
|
-
| `
|
|
134
|
+
| `runRetries` | `number` | A run that FAILED for a transient reason (provider down or overloaded, timeouts, a cut stream, after `retry` gave up) is tried again from where it stopped, waiting 5 s, 15 s, 45 s, then 60 s. Default `3`, `0` = off. Finished tool calls are not repeated; the response reports the failed attempts' tool calls and usage. Never for aborts, 400/401/403/404, bad config, budgets or limits. `run()` and queued runs only, not `stream()`. |
|
|
135
|
+
| `resume` | `"automatic" \| "explicit"` | Crash recovery (needs `persist: { dir }` + the same `sessionId`): `automatic` (default) continues a cut-off run when the Agent is constructed, `explicit` waits for `agent.resume()`. Finished tool calls are never run again. `agent.interrupted()` lists cut-off runs. |
|
|
136
|
+
| `persist` | `{ dir?: string; file?: string }` | Durable sessions, saved after every step (user/assistant/tool/sub-agent). `{ dir }` keeps `sessions/<sessionId>/session.json` (snapshot) plus an append-only `journal.ndjson` (one line per step, so a long chat costs the same per step as a short one), `{ file }` a single `.session.json`, `{}` uses `<home>/sessions`. Writes are durable (fsync, `.bak` of the last good file, atomic rename) and never fail a turn. |
|
|
135
137
|
| `home` | `string` | Folder for everything the SDK writes to disk (`sessions/`, `scheduled_actions/`, `telemetry/`). Defaults to `setHome(dir)`, then the `AGENT_ACCEL_HOME` env var, then `.agent-accel/` in the working directory. An explicit `persist.dir` / `persist.file` still wins. |
|
|
136
138
|
|
|
137
139
|
When `dynamicSubagents.enabled` is set without a resolvable worker model, construction throws `SubAgentModelError`.
|
|
@@ -267,47 +269,60 @@ const agent = new Agent({
|
|
|
267
269
|
instructions: "You are the Main Agent",
|
|
268
270
|
dynamicSubagents: {
|
|
269
271
|
enabled: true,
|
|
270
|
-
maxSpawn: 4,
|
|
272
|
+
maxSpawn: 4, // open workers at once
|
|
271
273
|
thinkingLevel: "low",
|
|
272
|
-
tools: { get_weather, recent_news },
|
|
273
|
-
timeout: 60000,
|
|
274
|
+
tools: { get_weather, recent_news }, // pool: the model grants a subset per worker
|
|
274
275
|
},
|
|
275
276
|
});
|
|
276
277
|
```
|
|
277
278
|
|
|
278
|
-
|
|
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.
|
|
279
290
|
|
|
280
291
|
```ts
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
role?,
|
|
285
|
-
instructions,
|
|
286
|
-
task,
|
|
287
|
-
tools?: string[],
|
|
288
|
-
timeoutMs?: number
|
|
289
|
-
}[]
|
|
290
|
-
}
|
|
292
|
+
const res = await agent.run("Audit auth pipeline and write a threat model");
|
|
293
|
+
|
|
294
|
+
console.log(res.subagents.map((s) => s.name));
|
|
291
295
|
```
|
|
292
296
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
-
|
|
296
|
-
- 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.
|
|
297
|
-
- 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…`).
|
|
298
|
-
- 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.
|
|
299
300
|
|
|
300
301
|
```ts
|
|
301
|
-
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"
|
|
302
310
|
|
|
303
|
-
|
|
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
|
|
304
314
|
```
|
|
305
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
|
+
|
|
306
323
|
### Delegation Helpers
|
|
307
324
|
|
|
308
325
|
* `buildAgentTools(list)` — wraps multiple agents and deduplicates names. Used internally by `subagents`.
|
|
309
|
-
* `createSubagentSpawnTool(parentAgent)` — builds the dynamic subagent spawner. Rarely needed directly.
|
|
310
|
-
* `DynamicSubagentTask` — `{ name, role?, instructions, task, tools?, timeoutMs? }`, the shape produced for dynamic delegation. No `model` — workers run on the developer-configured model.
|
|
311
326
|
|
|
312
327
|
---
|
|
313
328
|
## Scheduled actions
|
|
@@ -364,7 +379,7 @@ telemetryBus.onEvent((e) => {
|
|
|
364
379
|
|
|
365
380
|
**How it behaves**
|
|
366
381
|
|
|
367
|
-
* **One local clock.** A single timer per home folder ticks once a second against the machine's own time and only asks what is overdue, so drift or sleep cannot skip a run. There is no timezone or daylight-saving logic. The timer is `unref`'d and runs only while something is pending, so actions run only while your process is alive.
|
|
382
|
+
* **One local clock.** A single timer per home folder (guarded by a one-process lock, `<home>/.lock`) ticks once a second against the machine's own time and only asks what is overdue, so drift or sleep cannot skip a run. There is no timezone or daylight-saving logic. The timer is `unref`'d and runs only while something is pending, so actions run only while your process is alive.
|
|
368
383
|
* **The creating `Agent` runs it.** The task stores the Agent's `name`; each run goes to the live Agent with that name (latest wins), using its worker model and tool pool. The scheduler holds only a weak reference, so keep your `Agent` referenced; if none is alive, the run counts as missed.
|
|
369
384
|
* **Each run is fresh.** A new stateless dynamic sub-agent built from the saved fields, on your fixed worker model and tool pool, never touching the Main Agent's session. It inherits the parent's limits, retry policy, `fileAccess` and `captureRaw`.
|
|
370
385
|
* **Files are the truth.** Everything lives in `<home>/scheduled_actions/`: `tasks.json` (every action), `deliveries.json` (saved results waiting for their delivery time) and one folder per action with one `.md` file per run (a small header, then the result). The file is re-read and validated before each run, so editing `instructions` or `task` there takes effect on the next run. API keys, the model and tool code are never stored. Don't put secrets in `instructions`.
|
|
@@ -406,6 +421,34 @@ const agent = new Agent({
|
|
|
406
421
|
|
|
407
422
|
The agent loop answers the 3rd consecutive identical call (same name and arguments) with a `repeat_blocked` error, so the model can reuse the prior result or change its arguments. Tools marked `repeatable: true`, or `idempotent: true` unless `repeatable` is set, are exempt.
|
|
408
423
|
|
|
424
|
+
|
|
425
|
+
### Asking permission: `ctx.stop`
|
|
426
|
+
|
|
427
|
+
The SDK has no approval modes; you write the check inside `execute` (your own function that asks your user) and call `ctx.stop(reason)` when the answer is no:
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
const pay = tool({
|
|
431
|
+
description: "Pay an invoice.",
|
|
432
|
+
input: z.object({ invoiceId: z.string(), amount: z.number() }),
|
|
433
|
+
execute: async ({ invoiceId, amount }, ctx) => {
|
|
434
|
+
if (!(await getPermission(`Pay $${amount} for ${invoiceId}?`))) ctx.stop("user declined the payment");
|
|
435
|
+
return await payInvoice(invoiceId, amount); // never reached after a stop
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
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.
|
|
441
|
+
|
|
442
|
+
To guard a built-in tool such as `subagents` or `scheduled_actions`, wrap it on the agent after construction:
|
|
443
|
+
|
|
444
|
+
```ts
|
|
445
|
+
const inner = agent.tools["subagents"]!;
|
|
446
|
+
agent.tools["subagents"] = {
|
|
447
|
+
...inner,
|
|
448
|
+
execute: async (input, ctx) => ((await getPermission("Spawn workers?")) ? inner.execute(input, ctx) : ctx.stop("spawning was not approved")),
|
|
449
|
+
};
|
|
450
|
+
```
|
|
451
|
+
|
|
409
452
|
### Tool Failures and Tracing
|
|
410
453
|
|
|
411
454
|
When a tool throws, the model gets the error line plus a JSON block: error `name`/`code`/`status`, `attempts`, `timedOut`, the last progress the tool reported, and a hint not to repeat finished work. Report progress from inside a tool with `ctx.emit({ type: "progress", ... })`, or throw `new ToolError(msg, { context: { progress } })`.
|
|
@@ -429,6 +472,20 @@ Every call also emits `tool.*` events on `telemetryBus` (nothing is written unle
|
|
|
429
472
|
telemetryBus.onEvent((e) => e.code.startsWith("tool.") && appendFileSync("tools.ndjson", JSON.stringify(e) + "\n"));
|
|
430
473
|
```
|
|
431
474
|
|
|
475
|
+
**Tool logs.** With `persist: { dir }` every tool call also gets its own log file in the session folder, `tool-logs/<n>-<tool>.ndjson` (numbered in the order calls start, continuing after restarts). The SDK writes the input, each attempt, retry, the full result or error (stack included) and the duration; your tool adds its own lines with `ctx.log(message, data?)`. Every line is flushed to disk as it is written, so after a crash the file shows how far the call got:
|
|
476
|
+
|
|
477
|
+
```ts
|
|
478
|
+
execute: async ({ files }, ctx) => {
|
|
479
|
+
for (const [i, f] of files.entries()) {
|
|
480
|
+
await upload(f);
|
|
481
|
+
ctx.log(`uploaded ${i + 1} of ${files.length}`, { file: f });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The call's result carries `logFile` (relative to the session folder), and the saved session keeps only that reference. The log is for you and your harness: it is never sent to the model, nothing in the SDK reads it back (a tool that lets the agent read its own logs is yours to build), and it is not size-capped. Without `persist: { dir }` (or for `stateless` agents) nothing is written and `ctx.log` is a no-op.
|
|
487
|
+
|
|
488
|
+
|
|
432
489
|
### Tool Timing and Deadlines
|
|
433
490
|
|
|
434
491
|
Every `ToolResultRecord` includes `durationMs`, measured in whole milliseconds with a minimum displayed value of `1ms`. The value covers the tool execution attempt (including retry/backoff time), but excludes queue wait and input-schema validation.
|
|
@@ -927,7 +984,7 @@ SubAgentExecutionMetadata {
|
|
|
927
984
|
}
|
|
928
985
|
```
|
|
929
986
|
|
|
930
|
-
`finishReason` is `"max_turns"` when a finite `maxTurns` limit stopped the run with tool calls still pending (with an `agent.max_turns_truncated` warning on `telemetryBus` naming the dropped tools).
|
|
987
|
+
`finishReason` is `"stopped"` when a tool ended the run with `ctx.stop(reason)` (see *Asking permission*). It is `"max_turns"` when a finite `maxTurns` limit stopped the run with tool calls still pending (with an `agent.max_turns_truncated` warning on `telemetryBus` naming the dropped tools).
|
|
931
988
|
|
|
932
989
|
### SubAgentStep
|
|
933
990
|
|
|
@@ -967,9 +1024,32 @@ TokenUsage {
|
|
|
967
1024
|
}
|
|
968
1025
|
```
|
|
969
1026
|
|
|
970
|
-
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.
|
|
971
1028
|
|
|
972
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
|
+
|
|
973
1053
|
### ProviderRawData
|
|
974
1054
|
|
|
975
1055
|
```ts
|
|
@@ -1230,6 +1310,7 @@ never in the gitignored snapshot.
|
|
|
1230
1310
|
* `getEnv(key, fallback?)` — resolves an environment variable.
|
|
1231
1311
|
* `getApiKey(provider, explicit?, env?)` — resolves API keys using the configured environment lookup order.
|
|
1232
1312
|
* `buildSessionHeaders(provider, cache?, custom?, sessionId?)` — builds provider-specific affinity headers. Normally handled automatically.
|
|
1313
|
+
* Two retry layers: `retry` repeats one failed provider CALL (seconds); `runRetries` repeats a whole failed RUN from where it stopped (minutes, `agent.run_retry` on the bus) while the agent stays busy, so queued prompts wait.
|
|
1233
1314
|
* `withRetries(fn, { maxRetries?, maxRetryDelayMs?, signal?, label? })` / `isTransientError(err)` — bounded retries for transient provider failures (defaults: 2 retries, 5s cap, aborts never retried).
|
|
1234
1315
|
|
|
1235
1316
|
---
|
|
@@ -1243,7 +1324,7 @@ bun run examples/04-sub-agents.ts
|
|
|
1243
1324
|
# fixed researcher + critic pipeline
|
|
1244
1325
|
|
|
1245
1326
|
bun run examples/03-multi_agent.ts
|
|
1246
|
-
# dynamic
|
|
1327
|
+
# dynamic subagents tool demo
|
|
1247
1328
|
|
|
1248
1329
|
bun run examples/01-metadata.ts
|
|
1249
1330
|
# usage + raw inspection
|
|
@@ -1275,6 +1356,10 @@ bun run examples/13-structured-output.ts
|
|
|
1275
1356
|
bun run examples/14-scheduled-actions.ts [--quick]
|
|
1276
1357
|
# scheduled actions walkthrough on your computer's clock: starts working ~1 minute from now,
|
|
1277
1358
|
# delivers ~2 minutes from now (code-scheduled and model-scheduled); --quick skips the waiting
|
|
1359
|
+
|
|
1360
|
+
bun run examples/15-durable-agent.ts
|
|
1361
|
+
# a durable agent: killed mid-tool (SIGKILL), picked up by a new process (interrupted()/resume()),
|
|
1362
|
+
# finished tools not repeated, run retry, ctx.stop approval, tool logs (about 5 s of waiting)
|
|
1278
1363
|
```
|
|
1279
1364
|
|
|
1280
1365
|
Every example ends its `run()` with `.catch(fail)` (`examples/_shared.ts`),
|
|
@@ -1326,10 +1411,10 @@ saveSessionFile(".session.json", agent, telemetry);
|
|
|
1326
1411
|
|
|
1327
1412
|
* `agent.exportSession(telemetry)` / `agent.importSession(saved)` round-trip messages, system prompt, model, thinking level, instructions, cache, worker model, and sub-agent traces without touching `AgentContext` internals.
|
|
1328
1413
|
* `SessionTelemetry` accumulates `input / output / cacheRead / cacheWrite / reasoning / cost`, with clamped `turnHitRate()` / `totalHitRate()`, a dual `formatBar(res)`, and `formatSessionBanner(saved, telemetry, path)` for startup. Cost follows the [TokenUsage](#response-usage-and-raw-data) rules.
|
|
1329
|
-
* Files are JSON with one line per top-level field and one line per turn (2-space indent), written
|
|
1414
|
+
* Files are JSON with one line per top-level field and one line per turn (2-space indent), written durably (temp file, fsync, `.bak` of the last good version, atomic rename; a damaged file falls back to its `.bak` on load). Google turns also store their raw `providerSteps`, so a resumed session replays them exactly.
|
|
1330
1415
|
* Binary media (bytes, data URLs, base64) is extracted to `media/` on save via `saveSessionDir("sessions", agent, telemetry)` → `sessions/<sessionId>/{session.json, media/*}`; remote URLs and local paths stay references. `loadSessionDir(dir)` resolves `media/…` refs back to absolute paths, and `findLatestSessionDir("sessions")` resumes the most recent session.
|
|
1331
1416
|
* Sub-agent traces persist in a dedicated `subagents` section keyed by TrackingID (`{ trackingId, name, sessionId, parentSessionId, status, task, turns, usage, steps, text }`). Files stay lean: redundant `rawArguments` and repeated step text are dropped on save (rebuilt/kept live in memory).
|
|
1332
|
-
* With `persist: { dir }` or `{ file }
|
|
1417
|
+
* With `persist: { dir }` each step appends one line to `journal.ndjson` (fsynced); the journal is folded into a fresh `session.json` after 500 lines or 4 MB, or when history changes another way (rollback, `reset()`, `importSession`). `loadSessionDir` replays the journal over the snapshot; a torn last line is ignored, damage earlier stops the replay with a `session.journal_corrupt` warning. A run that a crash cut off is picked up again by a new Agent with the same `sessionId` (see `resume`): the conversation is reloaded, tool calls with a saved result are not repeated, the rest run, then waiting prompts follow. The journal also records where each run began (`runs`: id, index of its user message, time) and the prompts still waiting in the steer inbox and follow-up queue (`pending`, read back by `loadSessionDir` as `session.pending`; `importSession` restores `runs`). Embedded media is written once as `media/<kind>-h<hash>.<ext>`. With `persist: { file }` the single file is rewritten after every step. Either way a crash loses at most the in-flight step. Do not call `saveSessionDir` on a folder an Agent persists to.
|
|
1333
1418
|
|
|
1334
1419
|
---
|
|
1335
1420
|
## Scripts and Structure
|
|
@@ -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",
|