agent-accelerator 0.5.4 → 0.5.6
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 +34 -17
- package/package.json +1 -1
- package/src/agent/agent.ts +111 -35
- package/src/agent/config-blocks.ts +80 -0
- package/src/agent/loop.ts +6 -4
- package/src/agent/subagents-tool.ts +2 -2
- package/src/agent/subagents.md +2 -2
- package/src/agent/worker.ts +7 -5
- package/src/agent/workers.ts +2 -2
- package/src/index.ts +3 -3
- package/src/providers/google.ts +6 -39
- package/src/providers/openai-compat.ts +31 -104
- package/src/providers/openai.ts +30 -103
- package/src/providers/openrouter.ts +151 -400
- package/src/providers/shared.ts +136 -7
- package/src/providers.ts +21 -6
- package/src/scheduler/scheduled-actions.md +4 -2
- package/src/scheduler/scheduler.ts +87 -50
- package/src/scheduler/store.ts +23 -1
- package/src/scheduler/tool.ts +9 -8
- package/src/session/store.ts +3 -3
- package/src/streaming/event-stream.ts +7 -2
- package/src/tools/executor.ts +17 -2
- package/src/tools/tool.ts +0 -16
- package/src/types/agent.ts +32 -17
- package/src/types/message.ts +1 -1
- package/src/types/scheduler.ts +9 -2
- package/src/utils/errors.ts +4 -1
- package/src/utils/headers.ts +1 -1
- package/src/utils/media.ts +10 -1
package/README.md
CHANGED
|
@@ -121,11 +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?,
|
|
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). |
|
|
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
|
+
| `limits` | `AgentRunLimits` | `{ maxTurns?, deadlineMs?, maxToolCallsPerRun?, maxTotalTokens?, requestTimeoutMs?, firstByteTimeoutMs?, streamIdleTimeoutMs? }`. The first four are unlimited by default. 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
|
+
| `budget` | `Budget` | `{ maxCostUsd?, maxTotalTokens?, maxDurationMs?, maxTotalSubagents? }`, per run, workers included. Unlimited by default; exceeding it throws `BudgetExceededError` with partial usage. |
|
|
129
131
|
| `sessionId` | `string` | Stable identifier used for cache affinity. Auto-generated when omitted. |
|
|
130
132
|
| `headers` | `Record<string,string>` | Additional headers merged into every request. |
|
|
131
133
|
| `apiKey` | `string` | Overrides environment-based API-key lookup for this agent. |
|
|
@@ -144,6 +146,11 @@ When `dynamicSubagents.enabled` is set without a resolvable worker model, constr
|
|
|
144
146
|
await agent.run(prompt, opts?) // non-streaming, or streaming when opts.stream is true
|
|
145
147
|
agent.ask(prompt, optsOrBool?) // deprecated (use run): string deltas when streaming, otherwise same as run
|
|
146
148
|
agent.stream(prompt, opts?) // AssistantMessageEventStream
|
|
149
|
+
agent.steer(prompt) / agent.queue(prompt) // while busy: join the active run / start after it
|
|
150
|
+
agent.interrupt() // stop the active run now (provider request aborted, stream cancelled, tools get ctx.signal)
|
|
151
|
+
await agent.interrupt(message) // stop it AND run `message` next, ahead of queued follow-ups (stream: opts.enqueue = "interrupt")
|
|
152
|
+
agent.toConfig() // { model, instructions, tools }: the part an agent may edit, plain JSON (no keys)
|
|
153
|
+
Agent.fromConfig(editable, base) // new Agent: your full `base` config + an agent's edits of model/instructions/tools only
|
|
147
154
|
agent.reset() // clears messages + signatures, keeps config
|
|
148
155
|
agent.exportSession(telemetry?) // storable snapshot for saveSessionFile
|
|
149
156
|
agent.importSession(saved) // restores messages + config from loadSessionFile
|
|
@@ -170,7 +177,8 @@ Use `ContentPart[]` when sending images, audio, or video alongside text.
|
|
|
170
177
|
| `additionalContext` | Added only to the current user turn. Keeps the system prompt stable for better caching. |
|
|
171
178
|
| `sessionId` | Per-run session override. |
|
|
172
179
|
| `headers` | Per-run header merge. |
|
|
173
|
-
| `signal` | `AbortSignal` used to cancel generation, tools, and subagents.
|
|
180
|
+
| `signal` | `AbortSignal` used to cancel generation, tools, and subagents. The provider request is aborted and a stream's body is cancelled; see [what interrupting stops at the provider](docs/guides/safety-and-ops.md#what-interrupting-stops-at-the-provider). |
|
|
181
|
+
| `enqueue` | When the agent is busy: `"steer"` (join the active run), `"queue"` (run after it) or `"interrupt"` (stop it and run this prompt next, in every `midSession` mode). |
|
|
174
182
|
|
|
175
183
|
### Streaming
|
|
176
184
|
|
|
@@ -269,7 +277,7 @@ const agent = new Agent({
|
|
|
269
277
|
instructions: "You are the Main Agent",
|
|
270
278
|
dynamicSubagents: {
|
|
271
279
|
enabled: true,
|
|
272
|
-
maxSpawn: 4, // open workers at once
|
|
280
|
+
maxSpawn: 4, // optional cap on open workers at once (default: no limit)
|
|
273
281
|
thinkingLevel: "low",
|
|
274
282
|
tools: { get_weather, recent_news }, // pool: the model grants a subset per worker
|
|
275
283
|
},
|
|
@@ -280,10 +288,10 @@ One `subagents` tool is injected. It takes `{ command, id?, name?, role?, instru
|
|
|
280
288
|
|
|
281
289
|
- `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
290
|
- 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`
|
|
291
|
+
- `maxSpawn` caps **open** workers (the model must close one to open another); by default there is no cap. `budget.maxTotalSubagents` still counts spawns per run. Every budget and limit is off until you set it, except three network timeouts (see the `limits` row).
|
|
284
292
|
- `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
|
|
286
|
-
- `maxDepth`
|
|
293
|
+
- 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
|
+
- `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>/…`).
|
|
287
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.
|
|
288
296
|
- 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
297
|
- 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.
|
|
@@ -341,7 +349,7 @@ const agent = new Agent({
|
|
|
341
349
|
});
|
|
342
350
|
```
|
|
343
351
|
|
|
344
|
-
The Main Agent then gets two tools, fixed by config so the prompt cache stays stable: `scheduled_actions` (one tool, `command` = `create | list | get | update |
|
|
352
|
+
The Main Agent then gets two tools, fixed by config so the prompt cache stays stable: `scheduled_actions` (one tool, `command` = `create | list | get | update | delete | run_now | skip`) and `get_datetime` (the machine's local date and time, also exported as `getDatetimeTool`). The tool description is exactly [`src/scheduler/scheduled-actions.md`](src/scheduler/scheduled-actions.md) (the worker pool names are filled in), so you can read what the model is told.
|
|
345
353
|
|
|
346
354
|
The same code is available to your program:
|
|
347
355
|
|
|
@@ -360,6 +368,7 @@ createScheduledAction(agent, {
|
|
|
360
368
|
startCookingInAdvance: "00:00:30", // optional, a "DD:HH:MM" duration (default 10 minutes)
|
|
361
369
|
repeatEvery: "01:00:00", // optional; omit to run once
|
|
362
370
|
retries: 1, // optional; used only while time remains before delivery
|
|
371
|
+
onMissed: "run", // optional: "skip" | "run" if your program was down at delivery time (omit to decide later)
|
|
363
372
|
},
|
|
364
373
|
});
|
|
365
374
|
|
|
@@ -375,17 +384,19 @@ telemetryBus.onEvent((e) => {
|
|
|
375
384
|
| `updateScheduledAction(id, patch, { home? })` | Change instructions, task, tools, timeout, role or any schedule field. Schedule changes are refused while it is cooking. |
|
|
376
385
|
| `runScheduledAction(id, { home? })` | Run now, outside the schedule (also resolves a missed run). Needs the creating `Agent` alive. |
|
|
377
386
|
| `skipScheduledAction(id, { home? })` | Ignore a missed run and wait for the next scheduled time. |
|
|
378
|
-
| `
|
|
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. |
|
|
379
388
|
|
|
380
389
|
**How it behaves**
|
|
381
390
|
|
|
382
391
|
* **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.
|
|
383
392
|
* **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.
|
|
384
393
|
* **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`.
|
|
385
|
-
* **Files are the truth.** Everything lives in `<home>/scheduled_actions
|
|
386
|
-
* **
|
|
394
|
+
* **Files are the truth.** Everything lives in `<home>/scheduled_actions/` (the format is a stable contract with a changelog: [docs/scheduled-action-files.md](docs/scheduled-action-files.md)): `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`.
|
|
395
|
+
* **What a run costs.** Each run file's header records the run's exact spending, the same numbers `usageReport()` keeps for workers: `turns`, `tokens` (input + output), `inputTokens`, `freshInputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `outputTokens`, `thinkingTokens`, `cacheHitRate` (0 to 1), `costUsd`, `costSource` (`provider`, `catalog`, `mixed` or `unknown`) and `unpricedTurns` (only when above 0, then `costUsd` is a lower bound). Unknown pricing is `costUsd: unknown`, never 0. The numbers cover every attempt of the run (`retries` included) and are recorded for failed runs too. The report itself does not list scheduled runs.
|
|
396
|
+
* **Time limit of a run.** An action may carry `timeoutMs` (top level of the action, not inside `scheduled_action`): a run that takes longer is stopped and recorded as failed. By default there is no limit. `dynamicSubagents.workerTimeoutMs` is an optional ceiling: an action cannot ask for more, and it is the limit for actions that set none (`0` and `Infinity` mean no ceiling). The former `dynamicSubagents.timeout`, `modelMaySetTimeout` and the `-1` mode were removed; passing them throws a `ConfigError` that says what to use.
|
|
397
|
+
* **Ready, then delivered.** A finished run is saved immediately (`scheduled_action.ready`); at `timeToDeliver` the SDK emits `scheduled_action.delivered` on `telemetryBus` with `{ id, name, status, file, readyAt, deliverAt, late }`. There is no notification system: what happens next is your code. Other events: `.created`, `.updated`, `.started`, `.failed`, `.missed`, `.skipped`, `.deleted`, plus the warnings `.invalid` and `.store_corrupt`.
|
|
387
398
|
* **Overlap and failure.** A run still cooking when the next one is due is skipped, never stacked. A failed run still writes a file saying why and is delivered like any result (`status: "failed"`).
|
|
388
|
-
* **Missed runs.** If the process was down when a run was due, the SDK emits `scheduled_action.missed` once
|
|
399
|
+
* **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.
|
|
389
400
|
|
|
390
401
|
---
|
|
391
402
|
## Tools
|
|
@@ -702,9 +713,12 @@ Thinking levels map to `reasoning.effort` verbatim, the same as OpenAI (`dynamic
|
|
|
702
713
|
It also:
|
|
703
714
|
|
|
704
715
|
* passes `service_tier` (`flex` | `priority`);
|
|
705
|
-
*
|
|
716
|
+
* sends audio with its real format (`wav`, `mp3`, `aiff`, `aac`, `ogg`, `flac`, `m4a`) and QuickTime video as `video/mov`;
|
|
717
|
+
* 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;
|
|
706
718
|
* maps `tool_choice` (`auto` omitted; `none`, `required`, and function pins supported);
|
|
707
|
-
*
|
|
719
|
+
* 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;
|
|
720
|
+
* 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`);
|
|
721
|
+
* accepts `anthropic/...` and `~anthropic/...` model names for the `5m` / `1h` cache setting, and sends at most 4 `stop` sequences (the Chat Completions maximum; extra ones are dropped with one `provider_warning`).
|
|
708
722
|
|
|
709
723
|
`serviceTier` values other than `flex` / `priority` are omitted (standard routing).
|
|
710
724
|
|
|
@@ -1048,7 +1062,7 @@ report.total; // agent + all workers
|
|
|
1048
1062
|
- **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
1063
|
- **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
1064
|
- **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.
|
|
1065
|
+
- **Not in the report:** the one-shot workers of scheduled actions (their exact cost is in each run file's header), 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
1066
|
|
|
1053
1067
|
### ProviderRawData
|
|
1054
1068
|
|
|
@@ -1289,8 +1303,11 @@ documents. The supported lists are the `GOOGLE_*_MIME` constants at the top of
|
|
|
1289
1303
|
`resolution` (`MediaResolution`: `"low" | "medium" | "high" | "ultra_high"`)
|
|
1290
1304
|
sets how much detail Google spends on one image or video, and so how many
|
|
1291
1305
|
input tokens it costs (image: 280 / 560 / 1120 / 2240; video per frame:
|
|
1292
|
-
`low` and `medium` 70, `high` 280). Omit it for Google's default.
|
|
1293
|
-
|
|
1306
|
+
`low` and `medium` 70, `high` 280). Omit it for Google's default. On
|
|
1307
|
+
OpenRouter an image's `resolution` is sent as `image_url.detail` (`low` → `low`,
|
|
1308
|
+
`medium` → `auto`, `high` → `high`, `ultra_high` → `original`, which OpenRouter
|
|
1309
|
+
downgrades to `high` where unsupported); a video's `resolution` is ignored. Other
|
|
1310
|
+
providers (and OpenRouter videos) ignore it with one `provider_warning`.
|
|
1294
1311
|
|
|
1295
1312
|
Before any network call, the executor checks `image` / `audio` / `video` /
|
|
1296
1313
|
`file`-as-PDF parts against the catalog's `modalities.input` for that
|
|
@@ -1348,7 +1365,7 @@ bun run examples/11-session-identity.ts
|
|
|
1348
1365
|
# sub-agent TrackingIDs, live worker boxes, lineage, track(), durable sessions
|
|
1349
1366
|
|
|
1350
1367
|
bun run examples/12-mid-session.ts
|
|
1351
|
-
# steer, queue and stream while a turn is running
|
|
1368
|
+
# steer, queue and stream while a turn is running (interrupt() stops it; interrupt(message) stops it and runs the message)
|
|
1352
1369
|
|
|
1353
1370
|
bun run examples/13-structured-output.ts
|
|
1354
1371
|
# JSON Schema structured outputs on every provider
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-accelerator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { AgentConfig, AgentRunOptions, MidSessionConfig, ToolChoiceOption, RetryPolicyConfig, Budget, BudgetState, InterruptedRun } from "../types/agent.ts";
|
|
1
|
+
import type { AgentConfig, EditableAgentConfig, AgentRunOptions, MidSessionConfig, ToolChoiceOption, RetryPolicyConfig, Budget, BudgetState, InterruptedRun } from "../types/agent.ts";
|
|
2
2
|
import type { TokenUsage } from "../types/core.ts";
|
|
3
3
|
import type { ProviderRequestOptions } from "../types/model.ts";
|
|
4
|
-
import { normalizeThinkingV2, resolveWhenBusy
|
|
4
|
+
import { normalizeThinkingV2, resolveWhenBusy } from "../tools/tool.ts";
|
|
5
5
|
import type { ToolDefinition, ToolResultRecord } from "../types/tool.ts";
|
|
6
6
|
import { isSubagentResult } from "../tools/subagent-result.ts";
|
|
7
7
|
import { CACHE_OPTIONS, type ThinkingLevel, type ThinkingConfig, type CacheOption, type ServiceTier } from "../types/core.ts";
|
|
@@ -25,6 +25,7 @@ import { resolveHome } from "../scheduler/store.ts";
|
|
|
25
25
|
import { persistTelemetry } from "../telemetry-file.ts";
|
|
26
26
|
import { lockFolder } from "../utils/durable.ts";
|
|
27
27
|
import { createScheduledActionsTool, getDatetimeTool } from "../scheduler/tool.ts";
|
|
28
|
+
import { applyEditable, assertModelAllowed, modelKey, toEditableConfig } from "./config-blocks.ts";
|
|
28
29
|
import { assertNoToolCollisions } from "../tools/executor.ts";
|
|
29
30
|
import { runAgentLoop, streamAgentLoop, unfinishedToolCalls, endsWithStop, accumulateUsage, type SteerEntry } from "./loop.ts";
|
|
30
31
|
import { isRetryableRun, readRetryAfterMs, sleep } from "../utils/retry.ts";
|
|
@@ -95,6 +96,8 @@ function resolveAgentLimits(config: { limits?: Record<string, unknown> }): Agent
|
|
|
95
96
|
const raw = (config as { limits?: Record<string, unknown> }).limits ?? {};
|
|
96
97
|
const num = (v: unknown): number | undefined =>
|
|
97
98
|
typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
99
|
+
// A network timeout: `0` or `Infinity` turns it off, any other number is milliseconds, anything else is unset (the default applies).
|
|
100
|
+
const timeout = (v: unknown): number | undefined => (typeof v === "number" && !Number.isNaN(v) ? v : undefined);
|
|
98
101
|
return {
|
|
99
102
|
...(num(raw.maxTurns) !== undefined ? { maxTurns: Math.max(1, Math.floor(num(raw.maxTurns)!)) } : {}),
|
|
100
103
|
...(num(raw.deadlineMs) !== undefined ? { deadlineMs: num(raw.deadlineMs)! } : {}),
|
|
@@ -102,9 +105,9 @@ function resolveAgentLimits(config: { limits?: Record<string, unknown> }): Agent
|
|
|
102
105
|
? { maxToolCallsPerRun: Math.max(1, Math.floor(num(raw.maxToolCallsPerRun)!)) }
|
|
103
106
|
: {}),
|
|
104
107
|
...(num(raw.maxTotalTokens) !== undefined ? { maxTotalTokens: Math.max(1, Math.floor(num(raw.maxTotalTokens)!)) } : {}),
|
|
105
|
-
requestTimeoutMs:
|
|
106
|
-
firstByteTimeoutMs:
|
|
107
|
-
streamIdleTimeoutMs:
|
|
108
|
+
requestTimeoutMs: timeout(raw.requestTimeoutMs) ?? 300_000,
|
|
109
|
+
firstByteTimeoutMs: timeout(raw.firstByteTimeoutMs) ?? 120_000,
|
|
110
|
+
streamIdleTimeoutMs: timeout(raw.streamIdleTimeoutMs) ?? 300_000,
|
|
108
111
|
};
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -165,13 +168,15 @@ export class Agent {
|
|
|
165
168
|
readonly dynamicSubagents?: {
|
|
166
169
|
enabled: boolean;
|
|
167
170
|
model?: string | any;
|
|
171
|
+
/** Open workers at once; `Infinity` (the default) = no limit. */
|
|
168
172
|
maxSpawn: number;
|
|
169
173
|
thinkingLevel?: ThinkingLevel;
|
|
170
174
|
tools: Record<string, ToolDefinition>;
|
|
171
|
-
|
|
175
|
+
/** Longest a scheduled run may take, in ms; `Infinity` (the default) = no limit. */
|
|
176
|
+
workerTimeoutMs: number;
|
|
172
177
|
/** Scheduled actions (`scheduled_actions` + `get_datetime` tools) are on. */
|
|
173
178
|
allowScheduledActions: boolean;
|
|
174
|
-
/** Levels of workers allowed below this agent. */
|
|
179
|
+
/** Levels of workers allowed below this agent; `Infinity` (the default) = no limit. */
|
|
175
180
|
maxDepth: number;
|
|
176
181
|
/** Per-run spending cap of each worker (unset = unlimited). */
|
|
177
182
|
workerBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
|
|
@@ -257,6 +262,8 @@ export class Agent {
|
|
|
257
262
|
deferredStream?: AssistantMessageEventStream;
|
|
258
263
|
resolve: (res: AgentResponse) => void;
|
|
259
264
|
reject: (err: unknown) => void;
|
|
265
|
+
/** Put here by `interrupt(message)`: runs before every other waiting prompt. */
|
|
266
|
+
urgent?: boolean;
|
|
260
267
|
}> = [];
|
|
261
268
|
/** Named session contexts for `sessionId` runs (Q-19a, LRU 100 + TTL). */
|
|
262
269
|
private sessions = new Map<string, { context: AgentContext; lastAccess: number }>();
|
|
@@ -311,6 +318,7 @@ export class Agent {
|
|
|
311
318
|
// ModelProviderInstance {model,apiKey,...} and ModelSpec {id,provider}.
|
|
312
319
|
const { modelStringOrSpec, rawModel } = resolveMainModel(config.model);
|
|
313
320
|
this.modelStringOrSpec = modelStringOrSpec as string | any;
|
|
321
|
+
assertModelAllowed(config.allowedModels, modelKey(modelStringOrSpec));
|
|
314
322
|
const dynRaw = config.dynamicSubagents;
|
|
315
323
|
// Unified worker-model chain: dynamicSubagents.model > subagentModel > SUB_AGENT_MODEL.
|
|
316
324
|
const dynModel = resolveDynamicModelInput(dynRaw, config.subagentModel);
|
|
@@ -319,8 +327,9 @@ export class Agent {
|
|
|
319
327
|
if (dynRaw) {
|
|
320
328
|
const dynTools = normalizeToolMap((dynRaw as { tools?: Record<string, ToolDefinition> | ToolDefinition[] }).tools);
|
|
321
329
|
const rawDepth = (dynRaw as { maxDepth?: unknown }).maxDepth;
|
|
322
|
-
|
|
323
|
-
|
|
330
|
+
// Depth is unlimited unless the developer sets a cap (a whole number >= 1; Infinity also means unlimited).
|
|
331
|
+
if (rawDepth !== undefined && rawDepth !== Infinity && !(Number.isInteger(rawDepth) && (rawDepth as number) >= 1)) {
|
|
332
|
+
throw new ConfigError(`[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.`);
|
|
324
333
|
}
|
|
325
334
|
const rawBudget = (dynRaw as { workerBudget?: { maxCostUsd?: unknown; maxTotalTokens?: unknown } }).workerBudget;
|
|
326
335
|
for (const key of ["maxCostUsd", "maxTotalTokens"] as const) {
|
|
@@ -329,17 +338,30 @@ export class Agent {
|
|
|
329
338
|
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
339
|
}
|
|
331
340
|
}
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
341
|
+
// Open workers are unlimited unless the developer sets a cap (a number >= 1; Infinity also means unlimited).
|
|
342
|
+
const rawMax = (dynRaw as { maxSpawn?: unknown }).maxSpawn;
|
|
343
|
+
if (rawMax !== undefined && (typeof rawMax !== "number" || Number.isNaN(rawMax))) {
|
|
344
|
+
throw new ConfigError(`[Agent Accelerator] Invalid dynamicSubagents.maxSpawn ${JSON.stringify(rawMax)}: expected a whole number >= 1, or omit it for no limit.`);
|
|
345
|
+
}
|
|
346
|
+
const maxSpawn = rawMax === undefined || rawMax === Infinity ? Infinity : Math.max(1, Math.floor(rawMax));
|
|
347
|
+
// Only scheduled actions are timed: no limit unless the developer sets one (0 and Infinity mean none).
|
|
348
|
+
for (const removed of ["timeout", "modelMaySetTimeout"]) {
|
|
349
|
+
if ((dynRaw as Record<string, unknown>)[removed] !== undefined) {
|
|
350
|
+
throw new ConfigError(`[Agent Accelerator] dynamicSubagents.${removed} was removed: set dynamicSubagents.workerTimeoutMs (the longest a scheduled run may take, optional) and let each scheduled action carry its own timeoutMs.`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const rawTimeout = (dynRaw as { workerTimeoutMs?: unknown }).workerTimeoutMs;
|
|
354
|
+
if (rawTimeout !== undefined && (typeof rawTimeout !== "number" || Number.isNaN(rawTimeout) || rawTimeout < 0)) {
|
|
355
|
+
throw new ConfigError(`[Agent Accelerator] Invalid dynamicSubagents.workerTimeoutMs ${JSON.stringify(rawTimeout)}: expected a number of milliseconds > 0, or omit it (0 and Infinity mean no limit).`);
|
|
356
|
+
}
|
|
357
|
+
const workerTimeoutMs = !rawTimeout ? Infinity : Math.max(1, Math.floor(rawTimeout));
|
|
336
358
|
this.dynamicSubagents = {
|
|
337
359
|
enabled: dynEnabled,
|
|
338
360
|
model: dynModel as string | any | undefined,
|
|
339
361
|
maxSpawn,
|
|
340
362
|
thinkingLevel: (dynRaw as any)?.thinkingLevel,
|
|
341
363
|
tools: dynTools,
|
|
342
|
-
|
|
364
|
+
workerTimeoutMs,
|
|
343
365
|
allowScheduledActions: dynRaw.allowScheduledActions === true,
|
|
344
366
|
maxDepth: (rawDepth as number | undefined) ?? DEFAULT_MAX_DEPTH,
|
|
345
367
|
...(rawBudget ? { workerBudget: { ...(rawBudget as { maxCostUsd?: number; maxTotalTokens?: number }) } } : {}),
|
|
@@ -805,22 +827,46 @@ export class Agent {
|
|
|
805
827
|
}
|
|
806
828
|
|
|
807
829
|
/**
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
* the run
|
|
811
|
-
*
|
|
830
|
+
* Stops the active run now (STOP.) and, given a message, starts it next (STOP. Do this instead). The provider
|
|
831
|
+
* request is aborted (connection closed, so the provider stops generating where it can), the stream is cancelled, and
|
|
832
|
+
* running tools and workers get the abort signal; the interrupted run rejects with `AbortError` and keeps its partial
|
|
833
|
+
* history. `interrupt()` and `interrupt(error)` only stop (no-op when idle) and return nothing. `interrupt(message)`
|
|
834
|
+
* also runs `message` as the next run, ahead of queued follow-ups, in every `midSession` mode, and resolves with that
|
|
835
|
+
* run's response; when idle it simply runs the message. For a stream use `stream(message, { enqueue: "interrupt" })`.
|
|
812
836
|
*/
|
|
813
|
-
interrupt(reason?:
|
|
837
|
+
interrupt(reason?: Error): void;
|
|
838
|
+
interrupt(message: string | ContentPart[], options?: AgentRunOptions): Promise<AgentResponse>;
|
|
839
|
+
interrupt(arg?: Error | string | ContentPart[], options?: AgentRunOptions): void | Promise<AgentResponse> {
|
|
840
|
+
if (typeof arg === "string" || Array.isArray(arg)) {
|
|
841
|
+
if (options?.stream === true) {
|
|
842
|
+
throw new Error('[Agent Accelerator] interrupt(message) is non-streaming. Use stream(prompt, { enqueue: "interrupt" }) for a streaming interrupt.');
|
|
843
|
+
}
|
|
844
|
+
return this.run(arg, { ...options, enqueue: "interrupt" });
|
|
845
|
+
}
|
|
846
|
+
this.abortActive(arg instanceof Error ? arg : undefined);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Aborts the active run (provider request, tools, workers, stream) through its own controller; no-op when idle; in: interrupt and queueUrgent, out: the run settles as AbortError.
|
|
850
|
+
private abortActive(reason?: Error): void {
|
|
814
851
|
const active = this.currentRun;
|
|
815
852
|
if (!active) return;
|
|
816
853
|
try {
|
|
817
|
-
active.abortController.abort(reason
|
|
854
|
+
active.abortController.abort(reason ?? new Error("Agent interrupted"));
|
|
818
855
|
} catch {}
|
|
819
856
|
try {
|
|
820
857
|
active.outerStream?.cancel(reason);
|
|
821
858
|
} catch {}
|
|
822
859
|
}
|
|
823
860
|
|
|
861
|
+
// Puts a prompt ahead of everything waiting (behind earlier interrupts) and aborts the active run, so it starts as soon as that run has settled; ignores midSession mode and maxQueued; in: run()/stream() with enqueue "interrupt", out: pendingQueue + the abort.
|
|
862
|
+
private queueUrgent(entry: Agent["pendingQueue"][number]): void {
|
|
863
|
+
entry.urgent = true;
|
|
864
|
+
const firstOrdinary = this.pendingQueue.findIndex((e) => !e.urgent);
|
|
865
|
+
this.pendingQueue.splice(firstOrdinary < 0 ? this.pendingQueue.length : firstOrdinary, 0, entry);
|
|
866
|
+
this.persistNow();
|
|
867
|
+
this.abortActive();
|
|
868
|
+
}
|
|
869
|
+
|
|
824
870
|
/**
|
|
825
871
|
* Steers the active turn (While you're doing that, change direction).
|
|
826
872
|
* The current provider/tool work finishes first, then `prompt` is injected
|
|
@@ -1009,7 +1055,9 @@ export class Agent {
|
|
|
1009
1055
|
reject: entry.settle?.reject ?? (() => {}),
|
|
1010
1056
|
});
|
|
1011
1057
|
}
|
|
1012
|
-
|
|
1058
|
+
// ...but behind an interrupt(message), which is the newest and most urgent instruction.
|
|
1059
|
+
const firstOrdinary = this.pendingQueue.findIndex((e) => !e.urgent);
|
|
1060
|
+
this.pendingQueue.splice(firstOrdinary < 0 ? this.pendingQueue.length : firstOrdinary, 0, ...lateSteers);
|
|
1013
1061
|
} catch {}
|
|
1014
1062
|
}
|
|
1015
1063
|
this.persistNow();
|
|
@@ -1105,6 +1153,24 @@ export class Agent {
|
|
|
1105
1153
|
return copy;
|
|
1106
1154
|
}
|
|
1107
1155
|
|
|
1156
|
+
/**
|
|
1157
|
+
* The part of this agent that an agent may edit, as plain JSON: `{ model, instructions, tools }` (`model` is the
|
|
1158
|
+
* `provider/model` string, never a key; `tools` are the names of the tools you passed in `tools`). Store it, show it
|
|
1159
|
+
* to the model, and rebuild with {@link Agent.fromConfig}.
|
|
1160
|
+
*/
|
|
1161
|
+
toConfig(): Required<EditableAgentConfig> {
|
|
1162
|
+
return toEditableConfig(this.modelStringOrSpec, this.instructions, this.forkConfig.tools);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Builds an Agent from your full `base` config with an agent's edits applied. Only `model`, `instructions` and
|
|
1167
|
+
* `tools` of `editable` count (other keys are ignored with a `config.not_editable` warning); `tools` are names picked
|
|
1168
|
+
* from `base.tools`, an unknown name throws; `base.allowedModels` is enforced. Keys and limits always come from `base`.
|
|
1169
|
+
*/
|
|
1170
|
+
static fromConfig(editable: EditableAgentConfig, base: AgentConfig): Agent {
|
|
1171
|
+
return new Agent(applyEditable(editable, base));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1108
1174
|
/** Budgets are per run: counters restart whenever a new run begins. */
|
|
1109
1175
|
private resetBudgetState(): void {
|
|
1110
1176
|
if (this.budget) this.budgetState = { spawned: 0, totalTokens: 0, totalCostUsd: 0, startedAtMs: nowMs() };
|
|
@@ -1690,8 +1756,11 @@ export class Agent {
|
|
|
1690
1756
|
return s;
|
|
1691
1757
|
}
|
|
1692
1758
|
|
|
1693
|
-
// Mid-session: busy agent serializes via steer (same turn) or queue (next turn).
|
|
1759
|
+
// Mid-session: busy agent serializes via steer (same turn) or queue (next turn), or stops the active run and starts this prompt (interrupt).
|
|
1694
1760
|
if (this.currentRun) {
|
|
1761
|
+
if (options?.enqueue === "interrupt") {
|
|
1762
|
+
return new Promise<AgentResponse>((resolve, reject) => this.queueUrgent({ prompt, options, isStream: false, resolve, reject }));
|
|
1763
|
+
}
|
|
1695
1764
|
const enforced = this.midSessionConfig.mode;
|
|
1696
1765
|
const requested = options?.enqueue;
|
|
1697
1766
|
if (requested && (enforced === "steer" || enforced === "queue") && requested !== enforced) {
|
|
@@ -1805,13 +1874,14 @@ export class Agent {
|
|
|
1805
1874
|
// Mid-session: busy agent serializes via steer (same turn) or queue (next turn).
|
|
1806
1875
|
if (this.currentRun) {
|
|
1807
1876
|
const enforcedStream = this.midSessionConfig.mode;
|
|
1808
|
-
const
|
|
1877
|
+
const urgent = options?.enqueue === "interrupt"; // stops the active run and goes first, in every mode and whatever the queue holds
|
|
1878
|
+
const requestedStream = options?.enqueue === "interrupt" ? undefined : options?.enqueue;
|
|
1809
1879
|
if (requestedStream && (enforcedStream === "steer" || enforcedStream === "queue") && requestedStream !== enforcedStream) {
|
|
1810
1880
|
throw new Error(
|
|
1811
1881
|
`[Agent Accelerator] enqueue "${requestedStream}" is disabled by midSession.mode "${enforcedStream}". Use ${enforcedStream}() or set mode to "auto"/"${requestedStream}".`
|
|
1812
1882
|
);
|
|
1813
1883
|
}
|
|
1814
|
-
const mode = this.resolveEnqueueMode(requestedStream);
|
|
1884
|
+
const mode = urgent ? "queue" : this.resolveEnqueueMode(requestedStream);
|
|
1815
1885
|
if (mode === "steer") {
|
|
1816
1886
|
this.assertPromptValid(prompt);
|
|
1817
1887
|
this.assertSteerRoom(this.currentRun.steerInbox);
|
|
@@ -1844,14 +1914,14 @@ export class Agent {
|
|
|
1844
1914
|
}
|
|
1845
1915
|
return stub;
|
|
1846
1916
|
}
|
|
1847
|
-
if (this.midSessionConfig.mode === "steer") {
|
|
1917
|
+
if (!urgent && this.midSessionConfig.mode === "steer") {
|
|
1848
1918
|
throw new Error(
|
|
1849
1919
|
'[Agent Accelerator] enqueue "queue" is disabled by midSession.mode "steer". Use steer() or set mode to "auto"/"queue".'
|
|
1850
1920
|
);
|
|
1851
1921
|
}
|
|
1852
1922
|
this.assertPromptValid(prompt);
|
|
1853
1923
|
const max = this.midSessionConfig.maxQueued ?? 20;
|
|
1854
|
-
if (this.pendingQueue.length >= max) {
|
|
1924
|
+
if (!urgent && this.pendingQueue.length >= max) {
|
|
1855
1925
|
throw new Error(
|
|
1856
1926
|
`[Agent Accelerator] Queue is full (${this.pendingQueue.length}/${max}). Wait for the active run to finish or increase midSession.maxQueued.`
|
|
1857
1927
|
);
|
|
@@ -1859,13 +1929,15 @@ export class Agent {
|
|
|
1859
1929
|
const deferred = new AssistantMessageEventStream();
|
|
1860
1930
|
this.wireStreamCallbacks(deferred, options);
|
|
1861
1931
|
// Notify the active stream (if any) that a follow-up was queued.
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1932
|
+
if (!urgent) {
|
|
1933
|
+
try {
|
|
1934
|
+
this.currentRun.outerStream?.push({
|
|
1935
|
+
type: "queued",
|
|
1936
|
+
queuedPrompt: this.previewPrompt(prompt),
|
|
1937
|
+
queueLength: this.pendingQueue.length + 1,
|
|
1938
|
+
} as any);
|
|
1939
|
+
} catch {}
|
|
1940
|
+
}
|
|
1869
1941
|
const queuedEntry: {
|
|
1870
1942
|
prompt: string | ContentPart[];
|
|
1871
1943
|
options?: AgentRunOptions;
|
|
@@ -1901,8 +1973,12 @@ export class Agent {
|
|
|
1901
1973
|
}
|
|
1902
1974
|
} catch {}
|
|
1903
1975
|
});
|
|
1904
|
-
|
|
1905
|
-
|
|
1976
|
+
if (urgent) {
|
|
1977
|
+
this.queueUrgent(queuedEntry as Agent["pendingQueue"][number]);
|
|
1978
|
+
} else {
|
|
1979
|
+
this.pendingQueue.push(queuedEntry as any);
|
|
1980
|
+
this.persistNow();
|
|
1981
|
+
}
|
|
1906
1982
|
// Attach deferred completion to queued promise without exposing it.
|
|
1907
1983
|
// Real events are piped in pumpQueue() when the turn starts.
|
|
1908
1984
|
void done.then(
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AgentConfig, EditableAgentConfig } from "../types/agent.ts";
|
|
2
|
+
import type { ToolDefinition } from "../types/tool.ts";
|
|
3
|
+
import { ConfigError } from "../types/errors.ts";
|
|
4
|
+
import { telemetryBus } from "../session/store.ts";
|
|
5
|
+
import { normalizeToolMap } from "./worker.ts";
|
|
6
|
+
|
|
7
|
+
// The building blocks for self-improving agents (RSI): an agent may edit ONLY `model`, `instructions` and `tools`;
|
|
8
|
+
// everything else (limits, budgets, cache, thinking, keys, ...) stays with the developer who wrote `base`.
|
|
9
|
+
|
|
10
|
+
const EDITABLE_FIELDS = ["model", "instructions", "tools"] as const;
|
|
11
|
+
|
|
12
|
+
// The model as a plain `provider/id` string (a string stays as written, a ModelSpec becomes `provider/id`); in: Agent.modelStringOrSpec or a base `model`, out: toConfig, the allowedModels check, applyEditable.
|
|
13
|
+
export function modelKey(model: unknown): string | undefined {
|
|
14
|
+
if (typeof model === "string") return model.trim() || undefined;
|
|
15
|
+
const m = model as { model?: unknown; id?: unknown; provider?: unknown } | null | undefined;
|
|
16
|
+
if (m && typeof m.model === "string") return m.model.trim() || undefined;
|
|
17
|
+
if (m && typeof m.id === "string" && typeof m.provider === "string") return `${m.provider}/${m.id}`;
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Throws when `allowedModels` is set and the model is not in it (entries are compared as written, e.g. "google/gemini-3.5-flash"); in: AgentConfig.allowedModels + the model, out: the Agent constructor and applyEditable.
|
|
22
|
+
export function assertModelAllowed(allowed: unknown, model: string | undefined): void {
|
|
23
|
+
if (allowed === undefined) return;
|
|
24
|
+
if (!Array.isArray(allowed) || allowed.length === 0 || allowed.some((m) => typeof m !== "string" || !m.trim())) {
|
|
25
|
+
throw new ConfigError(`[Agent Accelerator] Invalid allowedModels ${JSON.stringify(allowed)}: expected a non-empty list of "provider/model" strings, or omit it to allow any model.`);
|
|
26
|
+
}
|
|
27
|
+
if (!model || !allowed.includes(model)) {
|
|
28
|
+
throw new ConfigError(`[Agent Accelerator] Model ${JSON.stringify(model ?? null)} is not in allowedModels. Allowed: ${allowed.map((m) => JSON.stringify(m)).join(", ")}.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The editable part of an agent as plain JSON; in: the Agent's model, instructions and the developer's tool list (not the built-in delegation/scheduler tools), out: Agent.toConfig.
|
|
33
|
+
export function toEditableConfig(model: unknown, instructions: string, tools: AgentConfig["tools"]): Required<EditableAgentConfig> {
|
|
34
|
+
return { model: modelKey(model) ?? "", instructions, tools: Object.keys(normalizeToolMap(tools)) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Merges what an agent edited into the developer's config: only model/instructions/tools can change; tools are names picked from base.tools; the model is checked against allowedModels when the Agent is built; in: Agent.fromConfig args, out: the AgentConfig for `new Agent`.
|
|
38
|
+
export function applyEditable(editable: EditableAgentConfig, base: AgentConfig): AgentConfig {
|
|
39
|
+
if (!editable || typeof editable !== "object" || Array.isArray(editable)) {
|
|
40
|
+
throw new ConfigError(`[Agent Accelerator] Invalid editable config ${JSON.stringify(editable)}: expected an object like { model, instructions, tools } (see Agent.toConfig()).`);
|
|
41
|
+
}
|
|
42
|
+
const e = editable as Record<string, unknown>;
|
|
43
|
+
const ignored = Object.keys(e).filter((k) => !(EDITABLE_FIELDS as readonly string[]).includes(k) && e[k] !== undefined);
|
|
44
|
+
if (ignored.length > 0) {
|
|
45
|
+
telemetryBus.emitWarning(
|
|
46
|
+
"config.not_editable",
|
|
47
|
+
`[Agent Accelerator] Ignored ${ignored.map((k) => `"${k}"`).join(", ")} in the editable config: an agent can change only ${EDITABLE_FIELDS.join(", ")}. The rest comes from the developer's config.`,
|
|
48
|
+
{ fields: ignored }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const out: AgentConfig = { ...base };
|
|
52
|
+
if (e.model !== undefined) {
|
|
53
|
+
const next = typeof e.model === "string" ? e.model.trim() : "";
|
|
54
|
+
if (!next) throw new ConfigError(`[Agent Accelerator] Invalid editable model ${JSON.stringify(e.model)}: expected a "provider/model" string.`);
|
|
55
|
+
// A model helper's key and base URL belong to its own provider: they are never sent to another one.
|
|
56
|
+
const prev = base.model as { model?: unknown; apiKey?: string; baseUrl?: string } | string | undefined;
|
|
57
|
+
const sameProvider = next.includes("/") && next.split("/")[0] === modelKey(prev)?.split("/")[0];
|
|
58
|
+
out.model = prev && typeof prev === "object" && typeof prev.model === "string"
|
|
59
|
+
? { ...prev, model: next, ...(sameProvider ? {} : { apiKey: undefined, baseUrl: undefined }) }
|
|
60
|
+
: next;
|
|
61
|
+
}
|
|
62
|
+
if (e.instructions !== undefined) {
|
|
63
|
+
if (typeof e.instructions !== "string") throw new ConfigError(`[Agent Accelerator] Invalid editable instructions: expected a string, got ${typeof e.instructions}.`);
|
|
64
|
+
out.instructions = e.instructions;
|
|
65
|
+
}
|
|
66
|
+
if (e.tools !== undefined) {
|
|
67
|
+
if (!Array.isArray(e.tools) || e.tools.some((n) => typeof n !== "string")) {
|
|
68
|
+
throw new ConfigError(`[Agent Accelerator] Invalid editable tools ${JSON.stringify(e.tools)}: expected a list of tool names from the developer's tool pool.`);
|
|
69
|
+
}
|
|
70
|
+
const pool = normalizeToolMap(base.tools);
|
|
71
|
+
const unknown = (e.tools as string[]).filter((n) => !Object.hasOwn(pool, n));
|
|
72
|
+
if (unknown.length > 0) {
|
|
73
|
+
throw new ConfigError(`[Agent Accelerator] Unknown tool ${unknown.map((n) => JSON.stringify(n)).join(", ")} in the editable config. Available: ${Object.keys(pool).map((n) => JSON.stringify(n)).join(", ") || "none"}.`);
|
|
74
|
+
}
|
|
75
|
+
const picked: Record<string, ToolDefinition> = {};
|
|
76
|
+
for (const n of e.tools as string[]) picked[n] = pool[n]!;
|
|
77
|
+
out.tools = picked;
|
|
78
|
+
}
|
|
79
|
+
return out; // allowedModels is enforced by the Agent constructor
|
|
80
|
+
}
|
package/src/agent/loop.ts
CHANGED
|
@@ -342,9 +342,11 @@ function resolveLoopLimits(config: AgentLoopConfig, fallbackMaxTurns: number): L
|
|
|
342
342
|
const deadlineMs = num(raw.deadlineMs) ?? Infinity;
|
|
343
343
|
const maxToolCallsPerRun = num(raw.maxToolCallsPerRun) ?? Infinity;
|
|
344
344
|
const maxTotalTokens = num(raw.maxTotalTokens) ?? Infinity;
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
const
|
|
345
|
+
// A network timeout: `0` or `Infinity` turns it off, any other number is milliseconds, anything else is unset (the default applies).
|
|
346
|
+
const timeout = (v: unknown): number | undefined => (typeof v === "number" && !Number.isNaN(v) ? v : undefined);
|
|
347
|
+
const requestTimeoutMs = timeout(raw.requestTimeoutMs) ?? 300_000;
|
|
348
|
+
const firstByteTimeoutMs = timeout(raw.firstByteTimeoutMs) ?? 120_000;
|
|
349
|
+
const streamIdleTimeoutMs = timeout(raw.streamIdleTimeoutMs) ?? 300_000;
|
|
348
350
|
return { maxTurns, deadlineMs, maxToolCallsPerRun, maxTotalTokens, requestTimeoutMs, firstByteTimeoutMs, streamIdleTimeoutMs };
|
|
349
351
|
}
|
|
350
352
|
|
|
@@ -416,7 +418,7 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
|
|
|
416
418
|
/** Prices one usage snapshot with the one cost model (`priceUsage`): provider-reported total first, else catalog pricing scaled by `serviceTier`; undefined when unknown. In: loop turns and `accumulateUsage`, out: `usage.cost`. */
|
|
417
419
|
export function computeCostFromPricing(usage: TokenUsage, spec?: ModelSpec, serviceTier?: string): TokenUsage["cost"] {
|
|
418
420
|
try {
|
|
419
|
-
if (usage.cost && usage.cost.totalCost
|
|
421
|
+
if (usage.cost && typeof usage.cost.totalCost === "number" && usage.cost.totalCost >= 0) {
|
|
420
422
|
return usage.cost;
|
|
421
423
|
}
|
|
422
424
|
if (!spec) return usage.cost;
|
|
@@ -29,11 +29,11 @@ function asResult(reply: WorkerReply): ReturnType<typeof makeSubagentResult> {
|
|
|
29
29
|
/** Builds the `subagents` tool for `agent`; its description is the manual with the worker pool and the open-worker limit filled in. */
|
|
30
30
|
export function createSubagentsTool(agent: Agent): ToolDefinition {
|
|
31
31
|
const poolNames = Object.keys(agent.dynamicSubagents?.tools ?? {});
|
|
32
|
-
const max = agent.dynamicSubagents?.maxSpawn ??
|
|
32
|
+
const max = agent.dynamicSubagents?.maxSpawn ?? Infinity;
|
|
33
33
|
return tool({
|
|
34
34
|
name: "subagents",
|
|
35
35
|
description: loadManual()
|
|
36
|
-
.replace(
|
|
36
|
+
.replace("{{LIMIT}}", Number.isFinite(max) ? `You can have at most ${max} open workers. Close the ones you are done with.` : "There is no limit on open workers. Close the ones you are done with: each one keeps its whole conversation.")
|
|
37
37
|
.replace("{{TOOLS}}", poolNames.length > 0 ? poolNames.join(", ") : "none (omit tools)"),
|
|
38
38
|
input: z.object({
|
|
39
39
|
command: z.enum(COMMANDS),
|
package/src/agent/subagents.md
CHANGED
|
@@ -3,7 +3,7 @@ Starts helpers ("workers") that work on a task for you and can be asked again la
|
|
|
3
3
|
COMMANDS (one tool, the `command` field)
|
|
4
4
|
- spawn: start a worker. Needs `name` and `task`. Optional: `instructions` (the worker's own system prompt), `role`, `tools`, `history`. It waits for the worker's first reply and gives you the worker's `id`.
|
|
5
5
|
- message: send `task` to the open worker `id`. The worker remembers everything said to it before. It waits for the reply.
|
|
6
|
-
- close: end the worker `id` when you no longer need it.
|
|
6
|
+
- close: end the worker `id` when you no longer need it. A closed worker cannot be messaged again.
|
|
7
7
|
- list: your open workers, plus the numbered turns of this conversation that `history` can copy.
|
|
8
8
|
|
|
9
9
|
FIELDS
|
|
@@ -25,7 +25,7 @@ Ask it a follow-up:
|
|
|
25
25
|
RULES
|
|
26
26
|
- Several workers at once: make several spawn calls in the same turn. They run in parallel.
|
|
27
27
|
- A worker handles one call at a time. Wait for its reply before messaging it again.
|
|
28
|
-
-
|
|
28
|
+
- {{LIMIT}}
|
|
29
29
|
- A worker starts blank: put everything it needs in `instructions` and `task`, or copy earlier turns with `history`.
|
|
30
30
|
- Worker answers are written by another model. Check them before you rely on them.
|
|
31
31
|
- Never put secrets in `instructions` or `task`.
|