agent-accelerator 0.6.0 → 0.6.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.
Files changed (51) hide show
  1. package/README.md +62 -16
  2. package/package.json +1 -1
  3. package/src/agent/agent.ts +96 -26
  4. package/src/agent/architecture.md +15 -12
  5. package/src/agent/context.ts +1 -0
  6. package/src/agent/defaults.ts +88 -0
  7. package/src/agent/delegation.ts +8 -3
  8. package/src/agent/loop.ts +34 -13
  9. package/src/agent/subagents-tool.ts +14 -21
  10. package/src/agent/usage-limit.ts +92 -0
  11. package/src/agent/worker.ts +17 -3
  12. package/src/agent/workers.ts +13 -1
  13. package/src/architecture.md +1 -0
  14. package/src/index.ts +9 -9
  15. package/src/prompts/additional-context.md +2 -0
  16. package/src/prompts/architecture.md +19 -0
  17. package/src/prompts/prompts.ts +81 -0
  18. package/src/prompts/reasoning-directive.md +5 -0
  19. package/src/prompts/structured-output.md +2 -0
  20. package/src/prompts/tool-bridge.md +1 -0
  21. package/src/prompts/tool-interrupted.md +1 -0
  22. package/src/prompts/tool-not-found.md +1 -0
  23. package/src/prompts/tool-repeat-blocked.md +1 -0
  24. package/src/prompts/tool-schema-error.md +1 -0
  25. package/src/prompts/tool-truncated-arguments.md +1 -0
  26. package/src/prompts/worker-failed.md +1 -0
  27. package/src/prompts/worker-replied.md +2 -0
  28. package/src/providers/architecture.md +8 -8
  29. package/src/providers/google.ts +82 -11
  30. package/src/providers/openai-compat.ts +17 -7
  31. package/src/providers/openai.ts +10 -4
  32. package/src/providers/openrouter.ts +185 -18
  33. package/src/providers.ts +59 -9
  34. package/src/scheduler/architecture.md +7 -7
  35. package/src/scheduler/scheduler.ts +40 -0
  36. package/src/scheduler/tool.ts +5 -9
  37. package/src/session/journal.ts +1 -0
  38. package/src/session/store.ts +4 -0
  39. package/src/tools/architecture.md +7 -6
  40. package/src/tools/executor.ts +60 -15
  41. package/src/tools/tool-content.ts +32 -0
  42. package/src/types/agent.ts +40 -6
  43. package/src/types/core.ts +4 -0
  44. package/src/types/message.ts +7 -0
  45. package/src/types/model.ts +4 -1
  46. package/src/types/scheduler.ts +4 -4
  47. package/src/types/tool.ts +2 -0
  48. package/src/utils/errors.ts +3 -3
  49. package/src/utils/headers.ts +1 -1
  50. /package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +0 -0
  51. /package/src/{agent/subagents.md → prompts/subagents-tool.md} +0 -0
package/README.md CHANGED
@@ -121,12 +121,13 @@ 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 workers: `{ enabled, model?, maxSpawn?, maxDepth?, workerBudget?, thinkingLevel?, tools?, workerTimeoutMs?, allowScheduledActions? }` (`workerTimeoutMs` is for scheduled actions only). See [Pre-defined vs Dynamic Delegation](#pre-defined-vs-dynamic-delegation) and [Scheduled actions](#scheduled-actions). |
124
+ | `dynamicSubagents` | `DynamicSubagentsConfig` | Enables and constrains LLM-spawned workers: `{ enabled, model?, maxSpawn?, maxDepth?, workerBudget?, scheduledActionBudget?, thinkingLevel?, tools?, workerTimeoutMs?, allowScheduledActions? }` (`workerTimeoutMs` 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. |
128
128
  | `maxTurns` | `number` | Maximum model → tool → model loops per `run`. Infinite by default (`0` or omitted = no limit). Hitting a finite limit with pending tool calls warns and sets `finishReason: "max_turns"`. |
129
129
  | `limits` | `AgentRunLimits` | `{ maxTurns?, deadlineMs?, maxToolCallsPerRun?, maxTotalTokens?, requestTimeoutMs?, firstByteTimeoutMs?, streamIdleTimeoutMs? }`. The first four are unlimited by default; hitting `maxTurns` ends the run with `finishReason: "max_turns"`, the other three throw a `BudgetExceededError`. The three network timeouts default to 300 s / 120 s / 300 s so a silent connection fails instead of hanging; set one to `0` or `Infinity` to turn it off. A timeout raises a retryable `TimeoutError`. See [Safety and operations](docs/guides/safety-and-ops.md#network-timeouts). |
130
+ | `usageLimit` | `{ maxCostUsd?, maxTotalTokens? }` | A spending cap for the WHOLE chat, for its whole life: the agent and all its workers added up, kept across restarts. Reaching it throws `BudgetExceededError` ("Usage limit reached"). Scheduled actions never count. Unset = no cap. |
130
131
  | `budget` | `Budget` | `{ maxCostUsd?, maxTotalTokens?, maxDurationMs?, maxTotalSubagents? }`, per run, workers included. Unlimited by default; exceeding it throws `BudgetExceededError` with partial usage. |
131
132
  | `sessionId` | `string` | Stable identifier used for cache affinity. Auto-generated when omitted. |
132
133
  | `headers` | `Record<string,string>` | Additional headers merged into every request. |
@@ -292,7 +293,8 @@ One `subagents` tool is injected. It takes `{ command, id?, name?, role?, instru
292
293
  - `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.
293
294
  - The Main Agent cannot choose worker models, reasoning levels or timeouts. The `subagents` tool has no timeouts (`workerTimeoutMs` and an action's `timeoutMs` apply only to scheduled actions).
294
295
  - `maxDepth` caps how many levels of workers may exist below the agent. By default there is no cap (a worker can start workers that start workers, as deep as the model goes); set a whole number >= 1 to limit it: `1` keeps workers flat, `3` allows three levels. 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>/…`).
295
- - `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.
296
+ - `workerBudget: { maxCostUsd?, maxTotalTokens? }` is each chat worker's own spending cap, applied as that worker's per-run `budget` (a spawn or a message 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.
297
+ - `scheduledActionBudget: { maxCostUsd?, maxTotalTokens? }` is the same kind of cap for each scheduled-action run (retries included). Scheduled actions are detached from the chat, so only this cap applies to them: not `workerBudget`, the agent's `budget` or `usageLimit`. Default: unlimited.
296
298
  - 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.
297
299
  - 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.
298
300
 
@@ -354,9 +356,9 @@ The Main Agent then gets two tools, fixed by config so the prompt cache stays st
354
356
  The same code is available to your program:
355
357
 
356
358
  ```ts
357
- import { createScheduledAction, telemetryBus } from "agent-accelerator";
359
+ import { telemetryBus } from "agent-accelerator";
358
360
 
359
- createScheduledAction(agent, {
361
+ agent.scheduled.create({
360
362
  instructions: "You write a short, sourced AI research brief.",
361
363
  task: "Summarize the most important AI news of the last day in 5 bullets.",
362
364
  tools: ["recent_news"], // names from the worker pool
@@ -377,16 +379,16 @@ telemetryBus.onEvent((e) => {
377
379
  });
378
380
  ```
379
381
 
380
- | Function | What it does |
382
+ | `agent.scheduled` | What it does |
381
383
  | --- | --- |
382
- | `createScheduledAction(agent, input)` | Schedule a new action. Throws one `ValidationError` listing every problem. |
383
- | `listScheduledActions({ home? })` | Every action with its metadata, state (`waiting`, `cooking`, `missed`, `done`), `lastRun` and a `running` flag. |
384
- | `updateScheduledAction(id, patch, { home? })` | Change instructions, task, tools, timeout, role or any schedule field. Schedule changes are refused while it is cooking. |
385
- | `runScheduledAction(id, { home? })` | Run now, outside the schedule (also resolves a missed run). Needs the creating `Agent` alive. |
386
- | `skipScheduledAction(id, { home? })` | Ignore a missed run and wait for the next scheduled time. |
387
- | `deleteScheduledAction(id, { force?, purge?, home? })` | Delete for good. While it is cooking nothing changes unless `force: true`; `purge: true` also deletes past run files. |
384
+ | `create(input)` | Schedule a new action under this agent. Throws one `ValidationError` listing every problem, and a `ConfigError` when `allowScheduledActions` is off. |
385
+ | `list()` | Every action in the agent's home with its metadata, state (`waiting`, `cooking`, `missed`, `done`), `lastRun` and a `running` flag. |
386
+ | `update(id, patch)` | Change instructions, task, tools, timeout, role or any schedule field. Schedule changes are refused while it is cooking. |
387
+ | `run(id)` | Run now, outside the schedule (also resolves a missed run). Needs the creating `Agent` alive. |
388
+ | `skip(id)` | Ignore a missed run and wait for the next scheduled time. |
389
+ | `delete(id, { force?, purge? })` | Delete for good. While it is cooking nothing changes unless `force: true`; `purge: true` also deletes past run files. |
388
390
 
389
- Every function except `createScheduledAction` finds the home folder through `setHome()`, `AGENT_ACCEL_HOME`, or `.agent-accel/` in the working directory. If you built the Agent with its own `home`, pass the same `{ home }` to these calls (the model's `scheduled_actions` tool already does).
391
+ Every method works on **the agent's own home** (`home` option, else `setHome()`, `AGENT_ACCEL_HOME`, or `.agent-accel/`), so nothing needs a `{ home }` argument. `list`, `update`, `run`, `skip` and `delete` see every action in that home, like the model's `scheduled_actions` tool does.
390
392
 
391
393
  **How it behaves**
392
394
 
@@ -401,6 +403,21 @@ Every function except `createScheduledAction` finds the home folder through `set
401
403
  * **Missed runs.** If the process was down when a run was due, the SDK emits `scheduled_action.missed` once. By default it runs nothing by itself and the action waits as `missed`: pick `run_now` or `skip`. Set `scheduled_action.onMissed` per action to decide in advance: `"skip"` moves on to the next time (a one-shot is `done`; a `skipped` event follows), `"run"` runs it as soon as the program is back (delivered when it finishes, marked `late`). The same applies to a run a crash cut off. An action whose creating Agent is gone stays `missed` whatever `onMissed` says (nothing can run it). A result saved before the process exited is simply delivered (marked `late`) on the first tick after start. `late` means the result was delivered more than two ticks after its time, finished after its delivery second, or replaced a missed run. A `run_now` result is delivered when it finishes: it is `late` only when it replaces a missed run (a `run_now` on a `missed` action, or `onMissed: "run"`), never for an ordinary early run.
402
404
 
403
405
  ---
406
+ ## Prompts
407
+
408
+ The SDK's own prompts (the reasoning directive, tool refusals, worker reply headers, the manuals of the `subagents` and `scheduled_actions` tools) are plain `.md` files in `src/prompts/`, exported as `DEFAULT_PROMPTS`. `AgentConfig.prompts` replaces single ones for an agent and its workers; the rest keep their defaults. `{{name}}` marks values the SDK fills in.
409
+
410
+ ```ts
411
+ import { Agent, DEFAULT_PROMPTS } from "agent-accelerator";
412
+
413
+ const agent = new Agent({
414
+ model: "google/gemini-3.5-flash-lite",
415
+ prompts: { toolNotFound: "Unknown tool '{{tool}}'.{{suggestion}} Use only the tools you were given." },
416
+ });
417
+ ```
418
+
419
+ **Keeping the original prompt is recommended**: it is what the SDK is tested with, and `DEFAULT_PROMPTS` is the reference for writing your own. Names, placeholders and where each prompt appears: [the prompts guide](docs/guides/prompts.md).
420
+
404
421
  ## Tools
405
422
 
406
423
  ```ts
@@ -427,6 +444,7 @@ const agent = new Agent({
427
444
  ### Tool Helpers
428
445
 
429
446
  * `tool({ name?, description, input?, parameters?, strict?, timeoutMs?, maxTries?, maxConcurrency?, execute })` — creates a `ToolDefinition`. Provide either a Zod `input` schema or raw JSON `parameters`. `execute(input, ctx)` may return arbitrary values; results are converted safely for model context. `timeoutMs` defaults to `0` (no time limit) and is a best-effort event-loop deadline; `ctx.signal` enables cooperative cancellation. `maxAttempts` is the canonical name for `maxTries` (number or numeric string). A positive value is the total attempt limit; `0`/omitted means 3 attempts for `idempotent` tools and 1 for all others. `maxConcurrency` limits simultaneous calls for that tool (default pool: 8).
447
+ * `toolContent([...])` — a tool's return value of text and media (`return toolContent(["chart.png", { type: "image", image: bytes, mimeType: "image/png" }])`). The text is the result every provider sees; Google (Gemini 3 and later) also gets the media inside the function result, other providers warn once and get the text only. Only a value made by `toolContent` counts, so JSON fetched by a tool can never become media. See the tools guide.
430
448
  * `toStandardToolDeclarations(record | array)` — converts tools to `{ name, description, parameters }` for providers.
431
449
  * `zodToJsonSchema(schema)` — converts Zod schemas using native conversion with a fallback extractor.
432
450
  * `cleanJsonSchema(schema)` — removes `$schema`, `$defs`, and `definitions`, resolves `$ref`, and preserves explicit `additionalProperties`.
@@ -717,10 +735,24 @@ It also:
717
735
  * passes `service_tier` (`flex` | `priority`);
718
736
  * sends audio with its real format (`wav`, `mp3`, `aiff`, `aac`, `ogg`, `flac`, `m4a`) and QuickTime video as `video/mov`;
719
737
  * sends file parts as `file.file_data` (a public URL or a base64 data URL) and never adds a plugin: OpenRouter reads the file natively or with its own default parser;
720
- * maps `tool_choice` (`auto` omitted; `none`, `required`, and function pins supported);
738
+ * maps `tool_choice` (`auto` omitted; `none`, `required`, and function pins supported; `validated` is Google-only and is dropped with one warning);
721
739
  * treats an error chunk inside a stream (HTTP 200) like an HTTP error: its status comes from the chunk's `code`, or from its `error_type` (`rate_limit_exceeded` 429, `provider_overloaded` 503, `provider_unavailable` 502, `timeout` 504, `server` 500), so `runRetries` retries the retryable ones;
722
740
  * reports `cached_tokens` / `cache_write_tokens` / reasoning tokens, preferring provider-reported `cost` when present (a reported `0`, a free model or a cache hit, stays a known `$0`);
723
- * accepts `anthropic/...` and `~anthropic/...` model names for the `5m` / `1h` cache setting.
741
+ * accepts `anthropic/...` and `~anthropic/...` model names for the `5m` / `1h` cache setting;
742
+ * sends a turn's `reasoning_details` blocks and parsed-PDF `file` annotations back on the next turns, exactly as OpenRouter returned them;
743
+ * reads provider routing and a few request fields from env vars (see below), never from `AgentConfig`.
744
+
745
+ **Env vars** (an Agent's own `env: { ... }` wins over `process.env`; nothing is sent for a variable that is not set; a bad value throws a `ConfigError` before any request):
746
+
747
+ | Variable | Sent as |
748
+ |---|---|
749
+ | `OPENROUTER_ORDER`, `OPENROUTER_ONLY`, `OPENROUTER_IGNORE` | `provider.order` / `only` / `ignore` (comma-separated provider slugs) |
750
+ | `OPENROUTER_ALLOW_FALLBACKS`, `OPENROUTER_REQUIRE_PARAMETERS`, `OPENROUTER_ZDR` | `provider.allow_fallbacks` / `require_parameters` / `zdr` (`true`/`false`/`1`/`0`) |
751
+ | `OPENROUTER_DATA_COLLECTION` | `provider.data_collection` (`allow` or `deny`) |
752
+ | `OPENROUTER_SORT` | `provider.sort` (`price`, `throughput` or `latency`) |
753
+ | `OPENROUTER_PARALLEL_TOOL_CALLS` | `parallel_tool_calls` (only when the request has tools) |
754
+ | `OPENROUTER_REASONING_EXCLUDE` | `reasoning.exclude` (the model reasons but the text is not returned) |
755
+ | `OPENROUTER_USER` | `user` (a stable end-user id) |
724
756
 
725
757
  `serviceTier` values other than `flex` / `priority` are omitted (standard routing).
726
758
 
@@ -1021,6 +1053,8 @@ TokenUsage {
1021
1053
  cacheReadTokens?,
1022
1054
  cacheWriteTokens?,
1023
1055
  thinkingTokens?,
1056
+ inputDetails?: { audioTokens?, videoTokens? }, // media part of inputTokens (OpenRouter), report only
1057
+ outputDetails?: { audioTokens?, imageTokens? }, // media part of outputTokens (OpenRouter), report only
1024
1058
  cost?: {
1025
1059
  inputCost?,
1026
1060
  outputCost?,
@@ -1138,6 +1172,7 @@ The model catalog is synced from [models.dev](https://models.dev) into a local c
1138
1172
 
1139
1173
  - **Automated 12-hour validation**: `.run()`, `.ask()` and `.stream()` check `<home>/catalog/models.dev.json`. Within 12 hours it reads from disk with no network delay. After that the expired catalog is served immediately and refreshed from `https://models.dev/api.json` in the background (8 s timeout). Only a cold start, with nothing loaded yet, waits for the download. A failed refresh pauses automatic refreshes for 10 minutes.
1140
1174
  - **Git & Package Safety**: The cache lives in the single Agent Accelerator home (`<home>/catalog/models.dev.json`, `.agent-accel/` by default and gitignored), never inside the package or scattered in the project.
1175
+ - **Spending-cap defaults**: `configure({ usageLimit, scheduledActionBudget })` sets the two caps for every agent you build (one call at startup, like `setHome()`; each call replaces the last, `configure({})` clears it). The env vars `AGENT_ACCEL_USAGE_LIMIT_USD` / `_TOKENS` and `AGENT_ACCEL_SCHEDULED_BUDGET_USD` / `_TOKENS` do the same. Precedence, field by field: the Agent's own config, then `configure()`, then env, then no cap. Workers and scheduled runs built by the SDK never take them.
1141
1176
  - **Environment**: `setHome(dir)` / `AGENT_ACCEL_HOME` move the whole home; `AGENT_CACHE_DIR` moves only the cache (for read-only or serverless filesystems). `AGENT_CATALOG_REFRESH=off` disables automatic refreshes; `bun run update-models` and `refreshModelCatalog()` still work.
1142
1177
 
1143
1178
  ### Developer Catalog Controls
@@ -1275,7 +1310,7 @@ Supported content parts include:
1275
1310
  * `ToolResultPart { type:"tool_result", id, name, result, isError? }`
1276
1311
  * `ImagePart { type:"image", image, mimeType?, resolution? }`
1277
1312
  * `AudioPart { type:"audio", audio, mimeType? }`
1278
- * `VideoPart { type:"video", video, mimeType?, resolution? }`
1313
+ * `VideoPart { type:"video", video, mimeType?, resolution?, processing? }`
1279
1314
  * `FilePart { type:"file", file, mimeType?, filename? }` — PDFs/documents.
1280
1315
 
1281
1316
  `image`, `audio`, `video`, and `file` inputs accept:
@@ -1321,6 +1356,16 @@ OpenRouter an image's `resolution` is sent as `image_url.detail` (`low` → `low
1321
1356
  downgrades to `high` where unsupported); a video's `resolution` is ignored. Other
1322
1357
  providers (and OpenRouter videos) ignore it with one `provider_warning`.
1323
1358
 
1359
+ `processing` (`VideoProcessing`: `"static" | "agentic"`) sets how Google reads one
1360
+ video. `"static"` (the default) puts frames taken at a fixed rate into the
1361
+ context. `"agentic"` lets the model explore the timeline and load only the
1362
+ transcript and frames the prompt needs, which suits long videos and
1363
+ needle-in-a-haystack questions (Google lists Gemini 4 Argon, 3.8 Flash, 3.6 Flash
1364
+ and 3.5 Flash Lite as supporting it; for any other model Google returns its own
1365
+ error). The model's `processing_call` / `processing_result` steps are kept and
1366
+ replayed with the rest of the turn's raw steps. Other providers ignore it with
1367
+ one `provider_warning`; an unknown value throws a `ValidationError`.
1368
+
1324
1369
  Before any network call, the executor checks `image` / `audio` / `video` /
1325
1370
  `file`-as-PDF parts against the catalog's `modalities.input` for that
1326
1371
  provider/model and fails fast with a one-line error naming the gap
@@ -1462,7 +1507,8 @@ bun run update-models # refresh model catalog cache (supports --force, --ttl=24h
1462
1507
  ```text
1463
1508
  src/
1464
1509
  ├── agent/ # Agent, context, loop, delegation, subagent
1465
- ├── scheduler/ # Scheduled actions: clock, store, tool + its manual (scheduled-actions.md)
1510
+ ├── scheduler/ # Scheduled actions: clock, store, tool
1511
+ ├── prompts/ # The SDK's own prompts as .md files, DEFAULT_PROMPTS, AgentConfig.prompts
1466
1512
  ├── session/ # Session persistence + telemetry (snapshots, hit rates, .session.json store)
1467
1513
  ├── providers/ # native REST adapters (google/openai/openrouter/openai-compat) + registry + canonical contract
1468
1514
  ├── models/ # Dynamic catalog cache, parser, verified overrides
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-accelerator",
3
- "version": "0.6.0",
3
+ "version": "0.6.5",
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",
@@ -28,14 +28,23 @@ import { AgentContext } from "./context.ts";
28
28
  import { resolveModel } from "../providers/registry.ts";
29
29
  import { buildAgentTools } from "./delegation.ts";
30
30
  import { createSubagentsTool } from "./subagents-tool.ts";
31
- import { WorkerRegistry } from "./workers.ts";
32
- import { DEFAULT_MAX_DEPTH, normalizeToolMap, resolveDynamicModelInput, resolveMainModel } from "./worker.ts";
31
+ import { WorkerRegistry, savedSpend } from "./workers.ts";
32
+ import {
33
+ DEFAULT_MAX_DEPTH,
34
+ isWorkerConfig,
35
+ normalizeToolMap,
36
+ resolveDynamicModelInput,
37
+ resolveMainModel,
38
+ } from "./worker.ts";
33
39
  import * as path from "node:path";
34
- import { registerAgent } from "../scheduler/scheduler.ts";
40
+ import { registerAgent, ScheduledActions } from "../scheduler/scheduler.ts";
35
41
  import { resolveHome } from "../utils/home.ts";
36
42
  import { persistTelemetry, registerSessionFolder, unregisterSessionFolder } from "../telemetry-file.ts";
37
43
  import { lockFolder } from "../utils/durable.ts";
38
44
  import { createScheduledActionsTool, getDatetimeTool } from "../scheduler/tool.ts";
45
+ import { fillPrompt, resolvePrompts, type Prompts } from "../prompts/prompts.ts";
46
+ import { UsageLimit, checkUsageLimitConfig } from "./usage-limit.ts";
47
+ import { mergeCap } from "./defaults.ts";
39
48
  import { applyEditable, assertModelAllowed, modelKey, toEditableConfig } from "./config-blocks.ts";
40
49
  import { assertNoToolCollisions } from "../tools/executor.ts";
41
50
  import {
@@ -197,6 +206,8 @@ export class Agent {
197
206
  readonly description: string;
198
207
  /** Stable system instructions. */
199
208
  instructions: string;
209
+ /** The SDK prompts in use: the shipped defaults with `AgentConfig.prompts` applied. */
210
+ readonly prompts: Prompts;
200
211
  /** Original model string or catalog spec used for resolution. */
201
212
  readonly modelStringOrSpec: string | any;
202
213
  /** Configured dynamic sub-agent model, if enabled. */
@@ -217,6 +228,8 @@ export class Agent {
217
228
  maxDepth: number;
218
229
  /** Per-run spending cap of each worker (unset = unlimited). */
219
230
  workerBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
231
+ /** Per-run spending cap of each scheduled-action run (unset = unlimited). */
232
+ scheduledActionBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
220
233
  };
221
234
  /** Home folder for everything written to disk (`sessions/`, `scheduled/`, `telemetry/`). */
222
235
  readonly home: string;
@@ -324,6 +337,11 @@ export class Agent {
324
337
  /** The resume in flight, so a second `resume()` returns the same promise. */
325
338
  private resuming?: Promise<AgentResponse | undefined>;
326
339
  private workerRegistry?: WorkerRegistry;
340
+ /** The chat-wide spending cap this agent counts into and checks against: its own (`usageLimit`) or the root's, handed down to workers. @internal */
341
+ chat?: UsageLimit;
342
+ /** True for the agent that created `chat` from its own `usageLimit` (the root of the chat). */
343
+ private ownsChat = false;
344
+ private scheduledActions?: ScheduledActions;
327
345
 
328
346
  /**
329
347
  * Creates an agent and registers its model, tools, cache, and delegation settings.
@@ -349,6 +367,18 @@ export class Agent {
349
367
  this.description = config.description || "AI Agent powered by Agent Accelerator";
350
368
  // DX1: only instructions (no systemPrompt alias)
351
369
  this.instructions = config.instructions || "";
370
+ this.prompts = resolvePrompts(config.prompts);
371
+ // Workers and scheduled runs the SDK builds never take developer defaults (`configure()`, env vars).
372
+ const takesDefaults = !isWorkerConfig(config);
373
+ if (config.usageLimit !== undefined) {
374
+ const problem = checkUsageLimitConfig(config.usageLimit);
375
+ if (problem) throw new ConfigError(`[Agent Accelerator] Invalid usageLimit: ${problem}.`);
376
+ }
377
+ const chatCap = mergeCap("usageLimit", config.usageLimit, takesDefaults);
378
+ if (chatCap) {
379
+ this.chat = new UsageLimit(chatCap);
380
+ this.ownsChat = true;
381
+ }
352
382
  // Unified model resolution (shared with SubAgent/workers): supports
353
383
  // ModelProviderInstance {model,apiKey,...} and ModelSpec {id,provider}.
354
384
  const { modelStringOrSpec, rawModel } = resolveMainModel(config.model);
@@ -374,15 +404,25 @@ export class Agent {
374
404
  `[Agent Accelerator] Invalid dynamicSubagents.maxDepth ${JSON.stringify(rawDepth)}: expected a whole number >= 1, or omit it for no limit; set dynamicSubagents.enabled: false to turn workers off.`
375
405
  );
376
406
  }
377
- const rawBudget = (dynRaw as { workerBudget?: { maxCostUsd?: unknown; maxTotalTokens?: unknown } }).workerBudget;
378
- for (const key of ["maxCostUsd", "maxTotalTokens"] as const) {
379
- const v = rawBudget?.[key];
380
- if (v !== undefined && !(typeof v === "number" && Number.isFinite(v) && v > 0)) {
381
- throw new ConfigError(
382
- `[Agent Accelerator] Invalid dynamicSubagents.workerBudget.${key} ${JSON.stringify(v)}: expected a number greater than 0, or omit it for no limit.`
383
- );
407
+ type RawBudget = { maxCostUsd?: unknown; maxTotalTokens?: unknown } | undefined;
408
+ const checkBudgetKeys = (name: "workerBudget" | "scheduledActionBudget"): RawBudget => {
409
+ const raw = (dynRaw as Record<string, RawBudget>)[name];
410
+ for (const key of ["maxCostUsd", "maxTotalTokens"] as const) {
411
+ const v = raw?.[key];
412
+ if (v !== undefined && !(typeof v === "number" && Number.isFinite(v) && v > 0)) {
413
+ throw new ConfigError(
414
+ `[Agent Accelerator] Invalid dynamicSubagents.${name}.${key} ${JSON.stringify(v)}: expected a number greater than 0, or omit it for no limit.`
415
+ );
416
+ }
384
417
  }
385
- }
418
+ return raw;
419
+ };
420
+ const rawBudget = checkBudgetKeys("workerBudget");
421
+ const rawScheduledBudget = mergeCap(
422
+ "scheduledActionBudget",
423
+ checkBudgetKeys("scheduledActionBudget") as { maxCostUsd?: number; maxTotalTokens?: number } | undefined,
424
+ takesDefaults
425
+ );
386
426
  // Open workers are unlimited unless the developer sets a cap (a number >= 1; Infinity also means unlimited).
387
427
  const rawMax = (dynRaw as { maxSpawn?: unknown }).maxSpawn;
388
428
  if (rawMax !== undefined && (typeof rawMax !== "number" || Number.isNaN(rawMax))) {
@@ -416,6 +456,9 @@ export class Agent {
416
456
  allowScheduledActions: dynRaw.allowScheduledActions === true,
417
457
  maxDepth: (rawDepth as number | undefined) ?? DEFAULT_MAX_DEPTH,
418
458
  ...(rawBudget ? { workerBudget: { ...(rawBudget as { maxCostUsd?: number; maxTotalTokens?: number }) } } : {}),
459
+ ...(rawScheduledBudget
460
+ ? { scheduledActionBudget: { ...(rawScheduledBudget as { maxCostUsd?: number; maxTotalTokens?: number }) } }
461
+ : {}),
419
462
  };
420
463
  }
421
464
  this.apiKey = (rawModel as any)?.apiKey || config.apiKey;
@@ -519,7 +562,7 @@ export class Agent {
519
562
  }
520
563
 
521
564
  if (Array.isArray(config.subagents)) {
522
- const subagentTools = buildAgentTools(config.subagents);
565
+ const subagentTools = buildAgentTools(config.subagents, { chat: this.chat });
523
566
  for (const [toolName, toolDef] of Object.entries(subagentTools)) {
524
567
  this.tools[toolName] = toolDef;
525
568
  }
@@ -559,6 +602,11 @@ export class Agent {
559
602
  return (this.workerRegistry ??= new WorkerRegistry(this));
560
603
  }
561
604
 
605
+ /** The scheduled actions of this agent's home: `create`, `list`, `update`, `run`, `skip`, `delete` (see `ScheduledActions`). */
606
+ get scheduled(): ScheduledActions {
607
+ return (this.scheduledActions ??= new ScheduledActions(this));
608
+ }
609
+
562
610
  // Takes the one-process lock of the folder this Agent persists to: its own session folder, or the folder of a single `persist.file`; throws ConfigError when another live process holds it; in: the constructor and importSession (a new session id is a new folder), out: a `.lock` file there.
563
611
  private lockSessionFolder(): void {
564
612
  const cfg = this.persistConfig;
@@ -621,15 +669,14 @@ export class Agent {
621
669
  // 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.
622
670
  private restoreUsage(saved: unknown): void {
623
671
  Object.assign(this.usageTotals, readUsage(saved));
672
+ if (this.ownsChat) this.chat!.restart(); // a restored session changes the starting point of the chat total
624
673
  }
625
674
 
626
675
  // 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).
627
676
  private recordTurnUsage(usage: TokenUsage, spec: Parameters<typeof priceUsage>[1]): void {
628
- addTurn(
629
- this.usageTotals,
630
- usage,
631
- priceUsage(usage, spec, this.serviceTier ? { serviceTier: this.serviceTier } : undefined)
632
- );
677
+ const priced = priceUsage(usage, spec, this.serviceTier ? { serviceTier: this.serviceTier } : undefined);
678
+ addTurn(this.usageTotals, usage, priced);
679
+ this.chat?.add((usage.inputTokens || 0) + (usage.outputTokens || 0), priced?.breakdown.totalMicroUSD ?? 0);
633
680
  }
634
681
 
635
682
  /** Adds another agent's spending to this one (a fixed worker's per-call copy into its template). @internal */
@@ -738,6 +785,7 @@ export class Agent {
738
785
  result: t.result,
739
786
  ...(t.isError ? { isError: true } : {}),
740
787
  ...(t.stopped ? { stopped: true } : {}),
788
+ ...(t.parts && t.parts.length > 0 ? { parts: t.parts } : {}),
741
789
  } as ToolResultRecord,
742
790
  ])
743
791
  );
@@ -759,6 +807,7 @@ export class Agent {
759
807
  result: isSubagentResult(result.result) ? result.result.xml : result.result,
760
808
  ...(result.isError ? { isError: true } : {}),
761
809
  ...(result.stopped ? { stopped: true } : {}),
810
+ ...(result.parts && result.parts.length > 0 ? { parts: result.parts } : {}),
762
811
  });
763
812
  this.doPersist();
764
813
  }
@@ -769,12 +818,8 @@ export class Agent {
769
818
  let out = base;
770
819
  const isThinkingEnabled =
771
820
  this.thinkingConfig?.enabled !== false && this.thinkingConfig?.level && this.thinkingConfig?.level !== "none";
772
- if (isThinkingEnabled && !out.includes("[Reasoning Directive]")) {
773
- const guidance =
774
- "[Reasoning Directive]\n" +
775
- "1. Use internal reasoning strictly for private planning and step-by-step thinking.\n" +
776
- "2. Never attempt to execute tools or output final user deliverables inside reasoning.\n" +
777
- "3. Once your reasoning is complete, output your final response or function calls directly in the standard response output.";
821
+ const guidance = this.prompts.reasoningDirective;
822
+ if (isThinkingEnabled && !out.includes(guidance)) {
778
823
  out = out ? `${out}\n\n${guidance}` : guidance;
779
824
  }
780
825
  return out || undefined;
@@ -1299,6 +1344,7 @@ export class Agent {
1299
1344
  const d = this.callDefaults;
1300
1345
  return {
1301
1346
  limits: { ...this.runLimits },
1347
+ ...(this.forkConfig.prompts ? { prompts: { ...this.forkConfig.prompts } } : {}),
1302
1348
  ...(d.retry ? { retry: d.retry } : {}),
1303
1349
  ...(d.env ? { env: d.env } : {}),
1304
1350
  ...(d.fetch ? { fetch: d.fetch } : {}),
@@ -1350,13 +1396,34 @@ export class Agent {
1350
1396
  return new Agent(applyEditable(editable, base));
1351
1397
  }
1352
1398
 
1399
+ // Takes the chat total's starting point (what the saved sessions of this agent and its workers already spent) once, before the first turn of the chat's root agent; in: the run starters, out: this.chat.
1400
+ private beginChat(): void {
1401
+ if (!this.ownsChat) return;
1402
+ this.chat!.begin(() => {
1403
+ // A persisted chat starts from its saved sessions (this agent's and every worker's); a memory-only one from what it holds now.
1404
+ const root = this.persistConfig?.dir;
1405
+ if (root && !this.stateless) {
1406
+ try {
1407
+ const saved = savedSpend(sessionDirFor(root, this.sessionId));
1408
+ return { tokens: saved.inputTokens + saved.outputTokens, microUSD: saved.costMicroUSD.total };
1409
+ } catch {
1410
+ // not a usable session folder name: nothing was saved there
1411
+ }
1412
+ }
1413
+ const total = this.usageReport().total;
1414
+ return { tokens: total.tokens.total, microUSD: Math.round((total.cost.totalUsd ?? 0) * 1_000_000) };
1415
+ });
1416
+ }
1417
+
1353
1418
  /** Budgets are per run: counters restart whenever a new run begins. */
1354
1419
  private resetBudgetState(): void {
1420
+ this.beginChat();
1355
1421
  if (this.budget) this.budgetState = { spawned: 0, totalTokens: 0, totalCostUsd: 0, startedAtMs: nowMs() };
1356
1422
  }
1357
1423
 
1358
1424
  /** Loop hook: charge `usage` (when given) and throw `BudgetExceededError` once any cap is passed. */
1359
1425
  private enforceBudget(usage?: TokenUsage): void {
1426
+ this.chat?.check(); // the chat-wide cap: before every turn and right after each one
1360
1427
  if (!this.budget) return;
1361
1428
  if (usage) chargeBudgetUsage(this as unknown as Parameters<typeof chargeBudgetUsage>[0], usage);
1362
1429
  checkBudget(this.budget, this.budgetState);
@@ -1390,6 +1457,7 @@ export class Agent {
1390
1457
  serviceTier: this.serviceTier,
1391
1458
  sessionId,
1392
1459
  ...(this.outputSpec ? { output: this.outputSpec } : {}),
1460
+ prompts: { structuredOutput: this.prompts.structuredOutput, toolBridge: this.prompts.toolBridge },
1393
1461
  ...(this.googleTier ? { googleTier: this.googleTier } : {}),
1394
1462
  ...(toolChoice !== undefined ? { toolChoice } : {}),
1395
1463
  ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
@@ -1447,7 +1515,7 @@ export class Agent {
1447
1515
  ...(this.fileAccess ? { fileAccess: this.fileAccess } : {}),
1448
1516
  onFailure: this.onFailure,
1449
1517
  ...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
1450
- ...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1518
+ ...(this.budget || this.chat ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1451
1519
  onProgress: () => this.persistNow(),
1452
1520
  onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
1453
1521
  onToolResult: (r: ToolResultRecord) => this.recordTool(r),
@@ -1457,6 +1525,7 @@ export class Agent {
1457
1525
  onSteerInjected: () => this.persistNow(),
1458
1526
  // Loop enforces these (plus local timeout races).
1459
1527
  limits: { ...this.runLimits },
1528
+ prompts: this.prompts,
1460
1529
  ...(this.output !== undefined ? { output: this.output } : {}),
1461
1530
  };
1462
1531
 
@@ -1543,7 +1612,7 @@ export class Agent {
1543
1612
  ...(this.fileAccess ? { fileAccess: this.fileAccess } : {}),
1544
1613
  onFailure: this.onFailure,
1545
1614
  ...(this.captureRaw !== undefined ? { captureRaw: this.captureRaw } : {}),
1546
- ...(this.budget ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1615
+ ...(this.budget || this.chat ? { enforceBudget: (u?: TokenUsage) => this.enforceBudget(u) } : {}),
1547
1616
  onProgress: () => this.persistNow(),
1548
1617
  onTurnUsage: (u: TokenUsage, spec: Parameters<typeof priceUsage>[1]) => this.recordTurnUsage(u, spec),
1549
1618
  onToolResult: (r: ToolResultRecord) => this.recordTool(r),
@@ -1552,6 +1621,7 @@ export class Agent {
1552
1621
  onSteerInjected: () => this.persistNow(),
1553
1622
  // Loop enforces these (plus local timeout races).
1554
1623
  limits: { ...this.runLimits },
1624
+ prompts: this.prompts,
1555
1625
  ...(this.output !== undefined ? { output: this.output } : {}),
1556
1626
  };
1557
1627
 
@@ -1898,7 +1968,7 @@ export class Agent {
1898
1968
  if (options?.additionalContext) {
1899
1969
  // Preserve stable instructions as systemPrompt
1900
1970
  ctx.systemPrompt = fullInstructions;
1901
- const prefix = `[Additional Context]\n${options.additionalContext}\n\n`;
1971
+ const prefix = `${fillPrompt(this.prompts.additionalContext, { context: options.additionalContext })}\n\n`;
1902
1972
  if (typeof prompt === "string") {
1903
1973
  prompt = prefix + prompt;
1904
1974
  } else if (Array.isArray(prompt)) {
@@ -2,18 +2,20 @@
2
2
 
3
3
  The agent and everything that runs under it.
4
4
 
5
- | File | What it is | Used by |
6
- | :----------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- |
7
- | `agent.ts` | `Agent`: `run`/`stream`/`ask`, `steer`/`queue`/`interrupt`, persistence hooks, `resume()`/`interrupted()`, `usageReport()`, `fork()`, `toConfig()`/`fromConfig()`, the `workers` getter. | callers, workers, scheduler |
8
- | `loop.ts` | `runAgentLoop` / `streamAgentLoop`: per turn budget, pairing repair, file access, provider call, tool execution, repeat guard. Also `ensureToolPairing`, `accumulateUsage`. | `Agent` |
9
- | `context.ts` | `AgentContext`: the validated message list with checkpoint/rollback. | `Agent`, loops |
10
- | `delegation.ts` | Fixed workers (`agentToTool`, `buildAgentTools`), the live worker trace store, session-id helpers, budget helpers, `openWorkerTrace`/`workerMetadata`. | `Agent`, `workers.ts` |
11
- | `worker.ts` | Worker core: model/tool normalization, `buildDynamicWorkerConfig` (shared preset), `createLiveWorker`. | `Agent`, registry, scheduler |
12
- | `workers.ts` | `WorkerRegistry` (`agent.workers`): persistent multi-turn workers. | `Agent`, `subagents` tool |
13
- | `subagents-tool.ts` + `subagents.md` | The one model-facing `subagents` tool; the `.md` is its description. | `Agent` constructor |
14
- | `subagent.ts` | `SubAgent` class and `agentFromEnv` (thin, kept for compatibility). | callers |
15
- | `turns.ts` | Pure grouping of messages into finished turns (history cut for workers). | `workers.ts` |
16
- | `config-blocks.ts` | RSI blocks: only `model`, `instructions`, `tools` are editable; `allowedModels` check. | `Agent` |
5
+ | File | What it is | Used by |
6
+ | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- |
7
+ | `agent.ts` | `Agent`: `run`/`stream`/`ask`, `steer`/`queue`/`interrupt`, persistence hooks, `resume()`/`interrupted()`, `usageReport()`, `fork()`, `toConfig()`/`fromConfig()`, the `workers` getter. | callers, workers, scheduler |
8
+ | `loop.ts` | `runAgentLoop` / `streamAgentLoop`: per turn budget, pairing repair, file access, provider call, tool execution, repeat guard. Also `ensureToolPairing`, `accumulateUsage`. | `Agent` |
9
+ | `context.ts` | `AgentContext`: the validated message list with checkpoint/rollback. | `Agent`, loops |
10
+ | `delegation.ts` | Fixed workers (`agentToTool`, `buildAgentTools`), the live worker trace store, session-id helpers, budget helpers, `openWorkerTrace`/`workerMetadata`. | `Agent`, `workers.ts` |
11
+ | `worker.ts` | Worker core: model/tool normalization, `buildDynamicWorkerConfig` (shared preset), `createLiveWorker`. | `Agent`, registry, scheduler |
12
+ | `workers.ts` | `WorkerRegistry` (`agent.workers`): persistent multi-turn workers. | `Agent`, `subagents` tool |
13
+ | `subagents-tool.ts` | The one model-facing `subagents` tool; its description is the `subagentsTool` prompt (`../prompts/subagents-tool.md`). | `Agent` constructor |
14
+ | `subagent.ts` | `SubAgent` class and `agentFromEnv` (thin, kept for compatibility). | callers |
15
+ | `turns.ts` | Pure grouping of messages into finished turns (history cut for workers). | `workers.ts` |
16
+ | `defaults.ts` | `configure()` and the `AGENT_ACCEL_*` env vars: developer defaults for `usageLimit` and `dynamicSubagents.scheduledActionBudget`, per field (Agent config > `configure()` > env). SDK-built workers never take them (`isWorkerConfig`). | `Agent`, barrel |
17
+ | `usage-limit.ts` | `UsageLimit`: the chat-wide cap (`AgentConfig.usageLimit`). The root agent owns it, registry and fixed workers share it (`agent.chat`); every model turn adds to it and is checked against it. | `Agent`, `workers.ts`, `delegation.ts` |
18
+ | `config-blocks.ts` | RSI blocks: only `model`, `instructions`, `tools` are editable; `allowedModels` check. | `Agent` |
17
19
 
18
20
  ## Invariants
19
21
 
@@ -23,5 +25,6 @@ The agent and everything that runs under it.
23
25
  - A worker is never less constrained than its parent (`inheritableConfig()`); its children draw only from the tools it was granted.
24
26
  - Exactly-once worker calls rest on the call key (parent run mark id + tool call id) and `meta.last`.
25
27
  - Only results branded by `makeSubagentResult` have their usage metadata trusted.
28
+ - A chat-wide cap (`usageLimit`) counts every model turn of the root and of all workers through one shared `UsageLimit`; its starting point is read once per process from the saved sessions. Scheduled workers never get it.
26
29
  - Each Agent records only its own model turns; never sum `AgentResponse.usage` for totals.
27
30
  - `reset()` clears the conversation (messages, run marks, the run's saved tool results) but never the spending totals.
@@ -143,6 +143,7 @@ export class AgentContext {
143
143
  isError: res.isError,
144
144
  ...(res.logFile ? { logFile: res.logFile } : {}),
145
145
  ...(res.stopped ? { stopped: true } : {}),
146
+ ...(res.parts && res.parts.length > 0 ? { parts: res.parts } : {}),
146
147
  },
147
148
  ],
148
149
  });
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Developer defaults for the spending caps, set in code (`configure()`) or through env vars, so one place can cap every
3
+ * agent of a program. Precedence, per field (`maxCostUsd`, `maxTotalTokens`): the Agent's own config, then `configure()`,
4
+ * then the env var, then no cap. Only agents YOU construct take defaults: the workers and scheduled runs the SDK builds
5
+ * itself never do (a chat worker joins its parent's cap; a scheduled run has its own).
6
+ */
7
+ import { ConfigError } from "../types/errors.ts";
8
+ import { getEnv } from "../utils/env.ts";
9
+ import { checkUsageLimitConfig, type UsageLimitConfig } from "./usage-limit.ts";
10
+
11
+ /** What `configure()` can set; every key is optional and has the same meaning as the Agent option of the same name. */
12
+ export interface AgentDefaults {
13
+ /** Default for `AgentConfig.usageLimit` (the chat-wide cap). Env: `AGENT_ACCEL_USAGE_LIMIT_USD`, `AGENT_ACCEL_USAGE_LIMIT_TOKENS`. */
14
+ usageLimit?: UsageLimitConfig;
15
+ /** Default for `dynamicSubagents.scheduledActionBudget`. Env: `AGENT_ACCEL_SCHEDULED_BUDGET_USD`, `AGENT_ACCEL_SCHEDULED_BUDGET_TOKENS`. */
16
+ scheduledActionBudget?: UsageLimitConfig;
17
+ }
18
+
19
+ type CapName = keyof AgentDefaults;
20
+
21
+ const ENV: Record<CapName, { maxCostUsd: string; maxTotalTokens: string }> = {
22
+ usageLimit: { maxCostUsd: "AGENT_ACCEL_USAGE_LIMIT_USD", maxTotalTokens: "AGENT_ACCEL_USAGE_LIMIT_TOKENS" },
23
+ scheduledActionBudget: {
24
+ maxCostUsd: "AGENT_ACCEL_SCHEDULED_BUDGET_USD",
25
+ maxTotalTokens: "AGENT_ACCEL_SCHEDULED_BUDGET_TOKENS",
26
+ },
27
+ };
28
+
29
+ let configured: AgentDefaults = {};
30
+
31
+ /**
32
+ * Sets developer defaults for the spending caps; call it once at startup, like `setHome()`. It REPLACES what an earlier
33
+ * call set (`configure({})` clears it). A key an Agent sets itself wins, field by field; env vars are the fallback.
34
+ * Throws a `ConfigError` for a value that is not a number above 0.
35
+ *
36
+ * @example `configure({ usageLimit: { maxCostUsd: 5 }, scheduledActionBudget: { maxCostUsd: 0.5 } });`
37
+ */
38
+ export function configure(defaults: AgentDefaults = {}): void {
39
+ if (!defaults || typeof defaults !== "object" || Array.isArray(defaults)) {
40
+ throw new ConfigError("[Agent Accelerator] configure() expects an object like { usageLimit: { maxCostUsd: 5 } }.");
41
+ }
42
+ for (const key of Object.keys(defaults)) {
43
+ if (!(key in ENV)) {
44
+ throw new ConfigError(
45
+ `[Agent Accelerator] configure(): unknown setting "${key}" (known: ${Object.keys(ENV).join(", ")}).`
46
+ );
47
+ }
48
+ const value = (defaults as Record<string, unknown>)[key];
49
+ if (value === undefined) continue;
50
+ const problem = checkUsageLimitConfig(value);
51
+ if (problem) throw new ConfigError(`[Agent Accelerator] configure(): invalid ${key}: ${problem}.`);
52
+ }
53
+ configured = Object.fromEntries(
54
+ Object.entries(defaults)
55
+ .filter(([, v]) => v !== undefined)
56
+ .map(([k, v]) => [k, { ...(v as UsageLimitConfig) }])
57
+ ) as AgentDefaults;
58
+ }
59
+
60
+ // Reads one cap field from its env var: undefined when unset, a ConfigError naming the variable when it is not a number above 0; in: mergeCap, out: the value.
61
+ function fromEnv(variable: string): number | undefined {
62
+ const raw = getEnv(variable);
63
+ if (raw === undefined) return undefined;
64
+ const n = Number(raw);
65
+ if (!(Number.isFinite(n) && n > 0)) {
66
+ throw new ConfigError(
67
+ `[Agent Accelerator] Invalid ${variable} ${JSON.stringify(raw)}: expected a number greater than 0.`
68
+ );
69
+ }
70
+ return n;
71
+ }
72
+
73
+ /** The cap an Agent uses: its own value, field by field over `configure()` over the env vars; undefined when nothing sets one. `withDefaults: false` ignores `configure()` and the env (SDK-built workers). */
74
+ export function mergeCap(
75
+ name: CapName,
76
+ own: UsageLimitConfig | undefined,
77
+ withDefaults: boolean
78
+ ): UsageLimitConfig | undefined {
79
+ const base = withDefaults ? configured[name] : undefined;
80
+ const maxCostUsd = own?.maxCostUsd ?? base?.maxCostUsd ?? (withDefaults ? fromEnv(ENV[name].maxCostUsd) : undefined);
81
+ const maxTotalTokens =
82
+ own?.maxTotalTokens ?? base?.maxTotalTokens ?? (withDefaults ? fromEnv(ENV[name].maxTotalTokens) : undefined);
83
+ if (maxCostUsd === undefined && maxTotalTokens === undefined) return undefined;
84
+ return {
85
+ ...(maxCostUsd !== undefined ? { maxCostUsd } : {}),
86
+ ...(maxTotalTokens !== undefined ? { maxTotalTokens } : {}),
87
+ };
88
+ }