agent-accelerator 0.5.7 → 0.5.9
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 +27 -8
- package/package.json +1 -1
- package/src/agent/agent.ts +84 -45
- package/src/agent/architecture.md +1 -0
- package/src/agent/context.ts +3 -3
- package/src/agent/delegation.ts +12 -12
- package/src/agent/loop.ts +50 -53
- package/src/agent/subagent.ts +4 -4
- package/src/agent/worker.ts +2 -1
- package/src/architecture.md +1 -1
- package/src/index.ts +7 -7
- package/src/models/architecture.md +1 -1
- package/src/models/catalog-cache.ts +7 -7
- package/src/models/catalog.ts +22 -34
- package/src/providers/google.ts +7 -7
- package/src/providers/openai-compat.ts +10 -16
- package/src/providers/openai.ts +8 -7
- package/src/providers/openrouter.ts +4 -4
- package/src/providers/registry.ts +6 -5
- package/src/providers/shared.ts +6 -10
- package/src/scheduler/architecture.md +6 -3
- package/src/scheduler/scheduler.ts +111 -57
- package/src/scheduler/store.ts +273 -59
- package/src/session/architecture.md +1 -1
- package/src/session/store.ts +22 -22
- package/src/streaming/event-stream.ts +11 -11
- package/src/telemetry-file.ts +74 -24
- package/src/telemetry.ts +2 -2
- package/src/tools/executor.ts +1 -1
- package/src/tools/tool.ts +8 -8
- package/src/types/agent.ts +22 -20
- package/src/types/response.ts +8 -8
- package/src/types/scheduler.ts +2 -2
- package/src/types/tool.ts +5 -5
- package/src/update-models.ts +2 -2
- package/src/utils/architecture.md +3 -3
- package/src/utils/cache.ts +4 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/errors.ts +45 -31
- package/src/utils/headers.ts +4 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/retry.ts +2 -28
- package/src/utils/session.ts +5 -5
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ An `Agent` holds:
|
|
|
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
|
|
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
130
|
| `budget` | `Budget` | `{ maxCostUsd?, maxTotalTokens?, maxDurationMs?, maxTotalSubagents? }`, per run, workers included. Unlimited by default; exceeding it throws `BudgetExceededError` with partial usage. |
|
|
131
131
|
| `sessionId` | `string` | Stable identifier used for cache affinity. Auto-generated when omitted. |
|
|
132
132
|
| `headers` | `Record<string,string>` | Additional headers merged into every request. |
|
|
@@ -136,7 +136,7 @@ An `Agent` holds:
|
|
|
136
136
|
| `runRetries` | `number` | A run that FAILED for a transient reason (provider down or overloaded, timeouts, a cut stream, after `retry` gave up) is tried again from where it stopped, waiting 5 s, 15 s, 45 s, then 60 s. Default `3`, `0` = off. Finished tool calls are not repeated; the response reports the failed attempts' tool calls and usage. Never for aborts, 400/401/403/404, bad config, budgets or limits. `run()` and queued runs only, not `stream()`. |
|
|
137
137
|
| `resume` | `"automatic" \| "explicit"` | Crash recovery (needs `persist: { dir }` + the same `sessionId`): `automatic` (default) continues a cut-off run when the Agent is constructed, `explicit` waits for `agent.resume()`. Finished tool calls are never run again. `agent.interrupted()` lists cut-off runs. |
|
|
138
138
|
| `persist` | `{ dir?: string; file?: string }` | Durable sessions, saved after every step (user/assistant/tool/sub-agent). `{ dir }` keeps `sessions/<sessionId>/session.json` (snapshot) plus an append-only `journal.ndjson` (one line per step, so a long chat costs the same per step as a short one), `{ file }` a single `.session.json`, `{}` uses `<home>/sessions`. Writes are durable (fsync, `.bak` of the last good file, atomic rename) and never fail a turn. |
|
|
139
|
-
| `home` | `string` | Folder for everything the SDK writes to disk (`sessions/`, `
|
|
139
|
+
| `home` | `string` | Folder for everything the SDK writes to disk (`sessions/`, `scheduled/`, `telemetry/`). Defaults to `setHome(dir)`, then the `AGENT_ACCEL_HOME` env var, then `.agent-accel/` in the working directory. An explicit `persist.dir` / `persist.file` still wins. |
|
|
140
140
|
|
|
141
141
|
When `dynamicSubagents.enabled` is set without a resolvable worker model, construction throws `SubAgentModelError`.
|
|
142
142
|
|
|
@@ -390,10 +390,10 @@ Every function except `createScheduledAction` finds the home folder through `set
|
|
|
390
390
|
|
|
391
391
|
**How it behaves**
|
|
392
392
|
|
|
393
|
-
* **One local clock.** A single timer per
|
|
394
|
-
* **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.
|
|
393
|
+
* **One local clock.** A single timer per process and home folder ticks once a second against the machine's own time and only asks what is overdue, so drift or sleep cannot skip a run. There is no timezone or daylight-saving logic. The timer is `unref`'d and runs only while something is pending, so actions run only while your process is alive.
|
|
394
|
+
* **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 once its delivery time has passed. Several processes may share one home: only the process that registered the Agent starts, delivers and recovers its actions, each action has its own folder and short lock, and a run whose owner died is recovered by another process that owns the Agent.
|
|
395
395
|
* **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`.
|
|
396
|
-
* **Files are the truth.** Everything lives in `<home>/
|
|
396
|
+
* **Files are the truth.** Everything lives in `<home>/scheduled/` (the format is a stable contract with a changelog: [docs/scheduled-action-files.md](docs/scheduled-action-files.md)): one folder per action holding its `task.json`, a `delivery.json` while a saved result waits for its delivery time, and 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`.
|
|
397
397
|
* **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.
|
|
398
398
|
* **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.
|
|
399
399
|
* **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`.
|
|
@@ -1089,16 +1089,35 @@ Credential headers are always redacted (`Authorization`, `x-goog-api-key`, anyth
|
|
|
1089
1089
|
---
|
|
1090
1090
|
## Errors
|
|
1091
1091
|
|
|
1092
|
-
Provider failures throw `
|
|
1092
|
+
Provider failures throw a `ProviderError` — one actionable line,
|
|
1093
1093
|
never a wire dump:
|
|
1094
1094
|
|
|
1095
1095
|
```text
|
|
1096
1096
|
[openrouter/nvidia/nemotron-3.5-lightning:free] request failed (404): No endpoints found that support input video
|
|
1097
1097
|
```
|
|
1098
1098
|
|
|
1099
|
-
|
|
1099
|
+
Branch on the class and `err.retryable` (true for HTTP 408/425/429/5xx,
|
|
1100
|
+
network errors and a cut stream), never on the message:
|
|
1101
|
+
|
|
1102
|
+
```ts
|
|
1103
|
+
import { ProviderError, TimeoutError } from "agent-accelerator";
|
|
1104
|
+
|
|
1105
|
+
try {
|
|
1106
|
+
await agent.run("...");
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
if (err instanceof ProviderError && err.retryable) retryLater(); // e.g. a 429 or 503
|
|
1109
|
+
else if (err instanceof TimeoutError) retryLater(); // a request / first-byte / idle timeout
|
|
1110
|
+
else throw err;
|
|
1111
|
+
}
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
`err.code` is the SDK kind (`"provider"`, `"incomplete_stream"` for a cut
|
|
1115
|
+
stream; `"timeout"`, `"config"`, ... on the other classes); the provider's own
|
|
1116
|
+
error code (e.g. `"not_found"`, `"rate_limit_exceeded"`) is `err.providerCode`.
|
|
1117
|
+
Raw details (`statusCode`, `url`, truncated body, `providerCode`) stay attached as
|
|
1100
1118
|
non-enumerable properties: available programmatically, invisible in runtime
|
|
1101
|
-
dumps.
|
|
1119
|
+
dumps. Input a model cannot take (a modality the catalog says it lacks, video on
|
|
1120
|
+
the OpenAI Responses wire) fails before the network with a `ValidationError`. URL query strings are stripped (keys sometimes live there); headers
|
|
1102
1121
|
and cookies are never attached. Helpers:
|
|
1103
1122
|
|
|
1104
1123
|
* `toConciseProviderError(err, providerId, modelId)` — collapse any provider error.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-accelerator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
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
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import * as path from "node:path";
|
|
23
23
|
import { registerAgent } from "../scheduler/scheduler.ts";
|
|
24
24
|
import { resolveHome } from "../scheduler/store.ts";
|
|
25
|
-
import { persistTelemetry } from "../telemetry-file.ts";
|
|
25
|
+
import { persistTelemetry, registerSessionFolder, unregisterSessionFolder } from "../telemetry-file.ts";
|
|
26
26
|
import { lockFolder } from "../utils/durable.ts";
|
|
27
27
|
import { createScheduledActionsTool, getDatetimeTool } from "../scheduler/tool.ts";
|
|
28
28
|
import { applyEditable, assertModelAllowed, modelKey, toEditableConfig } from "./config-blocks.ts";
|
|
@@ -63,10 +63,10 @@ export function resolveEffectiveThinking(
|
|
|
63
63
|
return { enabled: true, level: overrideLevel as ThinkingConfig["level"] };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
import { ConfigError, SessionLoadError, isAbortError } from "../types/errors.ts";
|
|
66
|
+
import { ConfigError, SessionLoadError, ValidationError, isAbortError } from "../types/errors.ts";
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Disposers for the manual user-signal forward listeners (
|
|
69
|
+
* Disposers for the manual user-signal forward listeners (fallback when
|
|
70
70
|
* `AbortSignal.any` is unavailable). Keyed by run controller so no expando
|
|
71
71
|
* properties are attached to AbortController instances.
|
|
72
72
|
*/
|
|
@@ -75,12 +75,12 @@ const mergeDisposers = new WeakMap<AbortController, () => void>();
|
|
|
75
75
|
/** Waits before run retry 1, 2, 3 ... (the last one repeats). Module-level so tests can shrink it; not public API. */
|
|
76
76
|
export const RUN_RETRY_DELAYS_MS: number[] = [5_000, 15_000, 45_000, 60_000];
|
|
77
77
|
|
|
78
|
-
/** Max named sessions kept per Agent (
|
|
78
|
+
/** Max named sessions kept per Agent (LRU). */
|
|
79
79
|
const SESSION_LRU_MAX = 100;
|
|
80
|
-
/** Idle TTL for named sessions
|
|
80
|
+
/** Idle TTL for named sessions. */
|
|
81
81
|
const SESSION_TTL_MS = 60 * 60 * 1000;
|
|
82
82
|
|
|
83
|
-
/** Normalized per-run limits
|
|
83
|
+
/** Normalized per-run limits. Sub-agents inherit these unless overridden. */
|
|
84
84
|
export interface AgentRunLimits {
|
|
85
85
|
maxTurns?: number;
|
|
86
86
|
deadlineMs?: number;
|
|
@@ -91,7 +91,7 @@ export interface AgentRunLimits {
|
|
|
91
91
|
streamIdleTimeoutMs: number;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
/** Resolves `(config as any).limits` with
|
|
94
|
+
/** Resolves `(config as any).limits` with defaults (timeouts tolerate reasoning). */
|
|
95
95
|
function resolveAgentLimits(config: { limits?: Record<string, unknown> }): AgentRunLimits {
|
|
96
96
|
const raw = (config as { limits?: Record<string, unknown> }).limits ?? {};
|
|
97
97
|
const num = (v: unknown): number | undefined =>
|
|
@@ -181,7 +181,7 @@ export class Agent {
|
|
|
181
181
|
/** Per-run spending cap of each worker (unset = unlimited). */
|
|
182
182
|
workerBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
|
|
183
183
|
};
|
|
184
|
-
/** Home folder for everything written to disk (`sessions/`, `
|
|
184
|
+
/** Home folder for everything written to disk (`sessions/`, `scheduled/`, `telemetry/`). */
|
|
185
185
|
readonly home: string;
|
|
186
186
|
/** Registered model-callable tools. */
|
|
187
187
|
readonly tools: Record<string, ToolDefinition> = {};
|
|
@@ -240,10 +240,10 @@ export class Agent {
|
|
|
240
240
|
readonly persistConfig?: { dir?: string; file?: string };
|
|
241
241
|
/** Mid-session queue policy for new queries arriving while a run is active. */
|
|
242
242
|
readonly midSessionConfig: Required<Pick<MidSessionConfig, "mode">> & MidSessionConfig & { maxSteers?: number };
|
|
243
|
-
/** Max buffered steer entries per run (
|
|
243
|
+
/** Max buffered steer entries per run (default 20). */
|
|
244
244
|
readonly maxSteers: number;
|
|
245
245
|
/**
|
|
246
|
-
* Normalized run limits
|
|
246
|
+
* Normalized run limits. Read from `(config as any).limits`;
|
|
247
247
|
* sub-agents inherit these unless overridden. `maxTurns`/`deadlineMs`/
|
|
248
248
|
* `maxToolCallsPerRun` default to unlimited; timeouts default to
|
|
249
249
|
* request 300s / first-byte 120s / idle 300s (tolerate reasoning).
|
|
@@ -265,9 +265,9 @@ export class Agent {
|
|
|
265
265
|
/** Put here by `interrupt(message)`: runs before every other waiting prompt. */
|
|
266
266
|
urgent?: boolean;
|
|
267
267
|
}> = [];
|
|
268
|
-
/** Named session contexts for `sessionId` runs (
|
|
268
|
+
/** Named session contexts for `sessionId` runs (LRU 100 + TTL). */
|
|
269
269
|
private sessions = new Map<string, { context: AgentContext; lastAccess: number }>();
|
|
270
|
-
/** Coalesced persistence single-flight flag
|
|
270
|
+
/** Coalesced persistence single-flight flag. */
|
|
271
271
|
private persistScheduled = false;
|
|
272
272
|
/** Step journal of the persisted session folder (dir mode only). */
|
|
273
273
|
private journal?: SessionJournal;
|
|
@@ -384,11 +384,6 @@ export class Agent {
|
|
|
384
384
|
config.persist && !config.persist.dir && !config.persist.file
|
|
385
385
|
? { ...config.persist, dir: path.join(this.home, "sessions") }
|
|
386
386
|
: config.persist;
|
|
387
|
-
// One process per folder: fail fast here, before anything is written (the home for `persist: {}`, else the folder named).
|
|
388
|
-
if (config.persist) {
|
|
389
|
-
const { dir, file } = config.persist;
|
|
390
|
-
lockFolder(dir ?? (file ? path.dirname(path.resolve(file)) : this.home));
|
|
391
|
-
}
|
|
392
387
|
// `whenBusy` (canonical) wins over legacy `mode`; anything else is "auto".
|
|
393
388
|
const normalizedMode = resolveWhenBusy(config.midSession);
|
|
394
389
|
const rawMaxQueued = (config.midSession as any)?.maxQueued;
|
|
@@ -402,9 +397,9 @@ export class Agent {
|
|
|
402
397
|
? { maxSteers: Math.floor(rawMaxSteers as number) }
|
|
403
398
|
: { maxSteers: 20 }),
|
|
404
399
|
};
|
|
405
|
-
//
|
|
400
|
+
// Bound on buffered steer entries (default 20).
|
|
406
401
|
this.maxSteers = this.midSessionConfig.maxSteers ?? 20;
|
|
407
|
-
//
|
|
402
|
+
// Run limits (sub-agents inherit unless overridden).
|
|
408
403
|
this.runLimits = resolveAgentLimits(config as unknown as { limits?: Record<string, unknown> });
|
|
409
404
|
|
|
410
405
|
// DX4: single thinkingLevel flag — also inherit from ModelProviderInstance when omitted
|
|
@@ -463,6 +458,8 @@ export class Agent {
|
|
|
463
458
|
? { ...(config.fileAccess.roots ? { roots: [...config.fileAccess.roots] } : {}) }
|
|
464
459
|
: undefined;
|
|
465
460
|
this.stateless = config.stateless ?? false;
|
|
461
|
+
// One process per session folder: fail fast here, before anything is written (another process may use other sessions of the same home).
|
|
462
|
+
this.lockSessionFolder();
|
|
466
463
|
|
|
467
464
|
// Tools registration (unified normalization shared with dynamic pool).
|
|
468
465
|
if (config.tools) {
|
|
@@ -498,6 +495,7 @@ export class Agent {
|
|
|
498
495
|
assertNoToolCollisions(this.tools);
|
|
499
496
|
|
|
500
497
|
this.context = new AgentContext(this.getFullInstructions());
|
|
498
|
+
this.registerTelemetryFolder();
|
|
501
499
|
if (dyn?.allowScheduledActions) registerAgent(this);
|
|
502
500
|
if (this.recoverSession() && this.resumeMode === "automatic") {
|
|
503
501
|
// Synchronously, so a run() issued right after construction queues BEHIND the resumed run.
|
|
@@ -510,6 +508,34 @@ export class Agent {
|
|
|
510
508
|
return (this.workerRegistry ??= new WorkerRegistry(this));
|
|
511
509
|
}
|
|
512
510
|
|
|
511
|
+
// 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.
|
|
512
|
+
private lockSessionFolder(): void {
|
|
513
|
+
const cfg = this.persistConfig;
|
|
514
|
+
if (!cfg || this.stateless) return;
|
|
515
|
+
if (cfg.dir) {
|
|
516
|
+
let dir: string;
|
|
517
|
+
try {
|
|
518
|
+
dir = sessionDirFor(cfg.dir, this.sessionId);
|
|
519
|
+
} catch {
|
|
520
|
+
return; // not a usable session id for a folder name, so nothing is saved under it either
|
|
521
|
+
}
|
|
522
|
+
lockFolder(dir);
|
|
523
|
+
} else if (cfg.file) {
|
|
524
|
+
lockFolder(path.dirname(path.resolve(cfg.file)));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Tells the telemetry sink where this Agent's session folder is (only for a session persisted to a folder), so its events are written next to its journal; in: the constructor and importSession, out: the telemetry routing map.
|
|
529
|
+
private registerTelemetryFolder(): void {
|
|
530
|
+
const root = this.persistConfig?.dir;
|
|
531
|
+
if (!root || this.stateless) return;
|
|
532
|
+
try {
|
|
533
|
+
registerSessionFolder(this.sessionId, sessionDirFor(root, this.sessionId));
|
|
534
|
+
} catch {
|
|
535
|
+
// why: not a usable session id for a folder name, so nothing is saved under it either.
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
513
539
|
// Loads a persisted session that a crash cut off (a run without an ending) or that still had waiting prompts, so it can be resumed; true when there is something to resume; in: the session folder of `persist.dir`, out: context, runMarks, toolRecords, pendingQueue.
|
|
514
540
|
private recoverSession(): boolean {
|
|
515
541
|
const root = this.persistConfig?.dir;
|
|
@@ -671,20 +697,22 @@ export class Agent {
|
|
|
671
697
|
return out || undefined;
|
|
672
698
|
}
|
|
673
699
|
|
|
674
|
-
/**
|
|
700
|
+
/** Starts a fresh conversation: clears messages, signatures, run marks and the run's saved tool results, keeps config and spending totals. */
|
|
675
701
|
reset(): void {
|
|
676
702
|
const sid = this.sessionId;
|
|
677
703
|
this.context.messages = [];
|
|
678
704
|
this.context.thoughtSignatures = [];
|
|
679
705
|
this.context.systemPrompt = this.getFullInstructions();
|
|
680
706
|
this.runMarks.length = 0;
|
|
681
|
-
//
|
|
707
|
+
this.toolRecords.length = 0; // a cleared conversation has no run to resume, so its finished tool results go too
|
|
708
|
+
this.activeMark = undefined;
|
|
709
|
+
// Reset also drops session-scoped provider chaining/routing.
|
|
682
710
|
try { clearInteractionChains(sid); } catch {}
|
|
683
711
|
try { clearSessionRouting(sid); } catch {}
|
|
684
712
|
}
|
|
685
713
|
|
|
686
714
|
/**
|
|
687
|
-
* Returns a named session handle
|
|
715
|
+
* Returns a named session handle. `session()` creates an isolated
|
|
688
716
|
* `AgentContext` bound to `id` (separate histories per id); `agent.run`
|
|
689
717
|
* with `{ sessionId }` selects/creates the same entry. Sessions are
|
|
690
718
|
* ephemeral in-memory (LRU 100 + 1h TTL). `agent.run`/`stream` without a
|
|
@@ -696,14 +724,15 @@ export class Agent {
|
|
|
696
724
|
return { id: sid, context: ctx, runController: null };
|
|
697
725
|
}
|
|
698
726
|
|
|
699
|
-
/** Drops one named session
|
|
727
|
+
/** Drops one named session. No-op for unknown ids. */
|
|
700
728
|
disposeSession(id: string): void {
|
|
729
|
+
unregisterSessionFolder(id);
|
|
701
730
|
try { this.sessions.delete(id); } catch {}
|
|
702
731
|
try { clearInteractionChains(id); } catch {}
|
|
703
732
|
try { clearSessionRouting(id); } catch {}
|
|
704
733
|
}
|
|
705
734
|
|
|
706
|
-
/** Drops all named sessions except the default context
|
|
735
|
+
/** Drops all named sessions except the default context. */
|
|
707
736
|
clearSessions(): void {
|
|
708
737
|
try {
|
|
709
738
|
for (const id of [...this.sessions.keys()]) {
|
|
@@ -753,7 +782,7 @@ export class Agent {
|
|
|
753
782
|
return fresh;
|
|
754
783
|
}
|
|
755
784
|
|
|
756
|
-
/** Rejects + drops all queued follow-ups
|
|
785
|
+
/** Rejects + drops all queued follow-ups. Returns the cleared count. */
|
|
757
786
|
clearQueue(): number {
|
|
758
787
|
const n = this.pendingQueue.length;
|
|
759
788
|
if (n === 0) return 0;
|
|
@@ -767,7 +796,7 @@ export class Agent {
|
|
|
767
796
|
return n;
|
|
768
797
|
}
|
|
769
798
|
|
|
770
|
-
/** Throws when the steer inbox is at its
|
|
799
|
+
/** Throws when the steer inbox is at its bound. */
|
|
771
800
|
private assertSteerRoom(inbox: SteerEntry[]): void {
|
|
772
801
|
const max = this.maxSteers ?? 20;
|
|
773
802
|
if (inbox.length >= max) {
|
|
@@ -810,7 +839,7 @@ export class Agent {
|
|
|
810
839
|
// Throws synchronously on an empty content-part array so run() and stream() fail at the same moment; in: any prompt entry point, out: nothing.
|
|
811
840
|
private assertPromptValid(prompt: string | ContentPart[]): void {
|
|
812
841
|
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
813
|
-
throw new
|
|
842
|
+
throw new ValidationError(
|
|
814
843
|
"[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
|
|
815
844
|
);
|
|
816
845
|
}
|
|
@@ -964,7 +993,7 @@ export class Agent {
|
|
|
964
993
|
const next = this.pendingQueue.shift();
|
|
965
994
|
if (!next) return;
|
|
966
995
|
this.persistNow();
|
|
967
|
-
//
|
|
996
|
+
// Reject queued items whose signal aborted before start.
|
|
968
997
|
try {
|
|
969
998
|
if ((next.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
|
|
970
999
|
const reason = ((next.options as { signal?: AbortSignal }).signal as unknown as { reason?: unknown })?.reason;
|
|
@@ -1016,7 +1045,7 @@ export class Agent {
|
|
|
1016
1045
|
|
|
1017
1046
|
// Ends the active run slot: leftover steers become follow-ups at the FRONT of the queue (behind urgent ones), then the queue is pumped; in: a settled run, out: pendingQueue + persistNow.
|
|
1018
1047
|
private finishCurrentRun(): void {
|
|
1019
|
-
//
|
|
1048
|
+
// Leftover steer entries become queued follow-ups instead of dropping.
|
|
1020
1049
|
let leftover: SteerEntry[] = [];
|
|
1021
1050
|
try { leftover = this.currentRun?.steerInbox?.splice(0) ?? []; } catch { leftover = []; }
|
|
1022
1051
|
this.currentRun = null;
|
|
@@ -1080,7 +1109,7 @@ export class Agent {
|
|
|
1080
1109
|
}
|
|
1081
1110
|
return runController.signal;
|
|
1082
1111
|
}
|
|
1083
|
-
//
|
|
1112
|
+
// Prefer AbortSignal.any (no expando, no listener leak).
|
|
1084
1113
|
try {
|
|
1085
1114
|
const anyFn = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;
|
|
1086
1115
|
if (typeof anyFn === "function") {
|
|
@@ -1233,7 +1262,7 @@ export class Agent {
|
|
|
1233
1262
|
const steerInbox: SteerEntry[] = [];
|
|
1234
1263
|
const mergedSignal = this.mergeUserSignal(options?.signal, runController);
|
|
1235
1264
|
const runOptions = this.withResolvedThinking({ ...options, signal: mergedSignal });
|
|
1236
|
-
//
|
|
1265
|
+
// Route to the named session context (default = this.context).
|
|
1237
1266
|
const targetContext = this.getSessionContext(runOptions?.sessionId || this.sessionId);
|
|
1238
1267
|
const task = (async (): Promise<AgentResponse> => {
|
|
1239
1268
|
await ensureModelCatalogFresh();
|
|
@@ -1274,7 +1303,7 @@ export class Agent {
|
|
|
1274
1303
|
...(resumeMark ? { resume: { stored: this.storedToolResults() } } : {}),
|
|
1275
1304
|
steerInbox,
|
|
1276
1305
|
onSteerInjected: () => this.persistNow(),
|
|
1277
|
-
//
|
|
1306
|
+
// Loop enforces these (plus local timeout races).
|
|
1278
1307
|
limits: { ...this.runLimits },
|
|
1279
1308
|
...(this.output !== undefined ? { output: this.output } : {}),
|
|
1280
1309
|
};
|
|
@@ -1333,7 +1362,7 @@ export class Agent {
|
|
|
1333
1362
|
if (effectiveLevel) {
|
|
1334
1363
|
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
1335
1364
|
}
|
|
1336
|
-
//
|
|
1365
|
+
// Named session routing (default session for compat).
|
|
1337
1366
|
const targetContext = this.getSessionContext(options?.sessionId || this.sessionId);
|
|
1338
1367
|
const mark = this.prepareTurn(prompt, options, targetContext);
|
|
1339
1368
|
this.persistNow();
|
|
@@ -1365,7 +1394,7 @@ export class Agent {
|
|
|
1365
1394
|
openToolLog: (call: { name: string }, sid: string | undefined) => this.openToolLog(call, sid),
|
|
1366
1395
|
steerInbox,
|
|
1367
1396
|
onSteerInjected: () => this.persistNow(),
|
|
1368
|
-
//
|
|
1397
|
+
// Loop enforces these (plus local timeout races).
|
|
1369
1398
|
limits: { ...this.runLimits },
|
|
1370
1399
|
...(this.output !== undefined ? { output: this.output } : {}),
|
|
1371
1400
|
};
|
|
@@ -1455,14 +1484,14 @@ export class Agent {
|
|
|
1455
1484
|
* Called automatically after every step during runs; safe to call
|
|
1456
1485
|
* manually. Never throws (failures route to `onPersistError`).
|
|
1457
1486
|
*
|
|
1458
|
-
*
|
|
1487
|
+
* Stateless agents skip persistence unless
|
|
1459
1488
|
* `(persist as any).allowStateless` is set. Calls in the same tick are
|
|
1460
1489
|
* coalesced via a `queueMicrotask` single flight.
|
|
1461
1490
|
*/
|
|
1462
1491
|
persistNow(): void {
|
|
1463
1492
|
const cfg = this.persistConfig;
|
|
1464
1493
|
if (!cfg || (!cfg.dir && !cfg.file)) return;
|
|
1465
|
-
//
|
|
1494
|
+
// No stateless persist by default.
|
|
1466
1495
|
try {
|
|
1467
1496
|
if (this.stateless && !(cfg as unknown as { allowStateless?: unknown }).allowStateless) return;
|
|
1468
1497
|
} catch {}
|
|
@@ -1504,7 +1533,7 @@ export class Agent {
|
|
|
1504
1533
|
if (cfg.dir) this.journalFor(cfg.dir).sync(this as any);
|
|
1505
1534
|
else if (cfg.file) saveSessionFile(cfg.file, this as any, null);
|
|
1506
1535
|
} catch (err) {
|
|
1507
|
-
//
|
|
1536
|
+
// Surface via hook instead of swallowing silently.
|
|
1508
1537
|
try { (cfg as unknown as { onPersistError?: (e: unknown) => void }).onPersistError?.(err); } catch {}
|
|
1509
1538
|
}
|
|
1510
1539
|
}
|
|
@@ -1574,6 +1603,16 @@ export class Agent {
|
|
|
1574
1603
|
this.warnOnLocalMediaPaths(data.messages ?? []);
|
|
1575
1604
|
const prevSid = this.sessionId;
|
|
1576
1605
|
if (data.sessionId) (this as any).sessionId = data.sessionId;
|
|
1606
|
+
if (this.sessionId !== prevSid) {
|
|
1607
|
+
try {
|
|
1608
|
+
this.lockSessionFolder();
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
(this as any).sessionId = prevSid; // the session belongs to another process: keep ours untouched
|
|
1611
|
+
throw err;
|
|
1612
|
+
}
|
|
1613
|
+
unregisterSessionFolder(prevSid);
|
|
1614
|
+
this.registerTelemetryFolder();
|
|
1615
|
+
}
|
|
1577
1616
|
if ((data as any).parentSessionId) (this as any).parentSessionId = (data as any).parentSessionId;
|
|
1578
1617
|
if ((data as any).subagents && typeof (data as any).subagents === "object") {
|
|
1579
1618
|
try {
|
|
@@ -1617,7 +1656,7 @@ export class Agent {
|
|
|
1617
1656
|
} else {
|
|
1618
1657
|
this.context.systemPrompt = this.getFullInstructions();
|
|
1619
1658
|
}
|
|
1620
|
-
//
|
|
1659
|
+
// Importing a snapshot invalidates prior provider chaining/routing
|
|
1621
1660
|
// for both the previous and the restored session.
|
|
1622
1661
|
try { clearInteractionChains(prevSid); } catch {}
|
|
1623
1662
|
try { clearSessionRouting(prevSid); } catch {}
|
|
@@ -1653,16 +1692,16 @@ export class Agent {
|
|
|
1653
1692
|
|
|
1654
1693
|
// Adds the user prompt to the target context (fresh context when stateless, stable system prompt, additionalContext as a user prefix) and marks the run start for the default session; in: a run start, out: the new SessionRunMark.
|
|
1655
1694
|
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext): SessionRunMark | undefined {
|
|
1656
|
-
//
|
|
1695
|
+
// Prompt validation throws synchronously in both run() and
|
|
1657
1696
|
// stream() (same timing; documented here).
|
|
1658
1697
|
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
1659
|
-
throw new
|
|
1698
|
+
throw new ValidationError(
|
|
1660
1699
|
"[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
|
|
1661
1700
|
);
|
|
1662
1701
|
}
|
|
1663
1702
|
const ctx = target ?? this.getSessionContext(options?.sessionId || this.sessionId);
|
|
1664
1703
|
if (this.stateless) {
|
|
1665
|
-
//
|
|
1704
|
+
// Stateless runs use a fresh ephemeral context (never shared).
|
|
1666
1705
|
ctx.messages = [];
|
|
1667
1706
|
ctx.thoughtSignatures = [];
|
|
1668
1707
|
}
|
|
@@ -1728,7 +1767,7 @@ export class Agent {
|
|
|
1728
1767
|
prompt: string | ContentPart[],
|
|
1729
1768
|
options?: AgentRunOptions
|
|
1730
1769
|
): Promise<AgentResponse> | AssistantMessageEventStream {
|
|
1731
|
-
//
|
|
1770
|
+
// Prompt validation throws synchronously in both run() and
|
|
1732
1771
|
// stream() (same timing; async work starts only after validation).
|
|
1733
1772
|
this.assertPromptValid(prompt);
|
|
1734
1773
|
const isStream = options?.stream === true;
|
|
@@ -1866,7 +1905,7 @@ export class Agent {
|
|
|
1866
1905
|
prompt: string | ContentPart[],
|
|
1867
1906
|
options?: AgentRunOptions
|
|
1868
1907
|
): AssistantMessageEventStream {
|
|
1869
|
-
//
|
|
1908
|
+
// Same sync validation timing as run().
|
|
1870
1909
|
this.assertPromptValid(prompt);
|
|
1871
1910
|
// Mid-session: busy agent serializes via steer (same turn) or queue (next turn).
|
|
1872
1911
|
if (this.currentRun) {
|
|
@@ -2217,7 +2256,7 @@ export class Agent {
|
|
|
2217
2256
|
/** What a failed run attempt got done before it failed (the `partial` the loop attaches to its error). */
|
|
2218
2257
|
type FailedAttempt = Partial<AgentResponse> & { usage: TokenUsage };
|
|
2219
2258
|
|
|
2220
|
-
// Adds what failed attempts already did (tool calls, results, turns, token usage) to the response of the attempt that finally worked, so a retry never hides side effects or cost; in: the final response + each failed attempt's `err.partial`, out: the run's AgentResponse.
|
|
2259
|
+
// Adds what failed attempts already did (tool calls, results, workers, turns, token usage) to the response of the attempt that finally worked, so a retry never hides side effects or cost; in: the final response + each failed attempt's `err.partial`, out: the run's AgentResponse.
|
|
2221
2260
|
function withFailedAttempts(res: AgentResponse, failed: FailedAttempt[]): AgentResponse {
|
|
2222
2261
|
const usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
2223
2262
|
for (const f of failed) accumulateUsage(usage, f.usage);
|
|
@@ -2229,7 +2268,7 @@ function withFailedAttempts(res: AgentResponse, failed: FailedAttempt[]): AgentR
|
|
|
2229
2268
|
thoughtSignature: res.thoughtSignature,
|
|
2230
2269
|
toolCalls: [...failed.flatMap((f) => f.toolCalls ?? []), ...res.toolCalls],
|
|
2231
2270
|
toolResults: [...failed.flatMap((f) => f.toolResults ?? []), ...res.toolResults],
|
|
2232
|
-
subagents: res.subagents,
|
|
2271
|
+
subagents: [...failed.flatMap((f) => f.subagents ?? []), ...res.subagents],
|
|
2233
2272
|
usage,
|
|
2234
2273
|
responseId: res.responseId,
|
|
2235
2274
|
model: res.model,
|
|
@@ -23,3 +23,4 @@ The agent and everything that runs under it.
|
|
|
23
23
|
- Exactly-once worker calls rest on the call key (parent run mark id + tool call id) and `meta.last`.
|
|
24
24
|
- Only results branded by `makeSubagentResult` have their usage metadata trusted.
|
|
25
25
|
- Each Agent records only its own model turns; never sum `AgentResponse.usage` for totals.
|
|
26
|
+
- `reset()` clears the conversation (messages, run marks, the run's saved tool results) but never the spending totals.
|
package/src/agent/context.ts
CHANGED
|
@@ -170,8 +170,8 @@ export class AgentContext {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
/**
|
|
173
|
-
* Captures a detached snapshot of conversation state for run-level rollback
|
|
174
|
-
*
|
|
173
|
+
* Captures a detached snapshot of conversation state for run-level rollback.
|
|
174
|
+
* The snapshot is a structural deep copy (NOT JSON): `Uint8Array` /
|
|
175
175
|
* `ArrayBuffer` media must survive a rollback byte-for-byte, and large
|
|
176
176
|
* base64 strings are shared by reference rather than re-serialized.
|
|
177
177
|
*/
|
|
@@ -185,7 +185,7 @@ export class AgentContext {
|
|
|
185
185
|
/**
|
|
186
186
|
* Restores a checkpoint captured by `checkpoint()`. Malformed checkpoints
|
|
187
187
|
* are ignored (current state kept). When `sessionId` is provided, also
|
|
188
|
-
* clears session-scoped provider state
|
|
188
|
+
* clears session-scoped provider state so a rollback never resumes
|
|
189
189
|
* a stale interaction chain.
|
|
190
190
|
*/
|
|
191
191
|
rollback(cp: { messages: Message[]; signatures: string[] }, sessionId?: string): void {
|
package/src/agent/delegation.ts
CHANGED
|
@@ -74,7 +74,7 @@ export function isSessionDescendant(child: string, parent: string): boolean {
|
|
|
74
74
|
if (!child || !parent) return false;
|
|
75
75
|
if (child === parent) return true;
|
|
76
76
|
if (child.startsWith(parent + "-")) return true;
|
|
77
|
-
//
|
|
77
|
+
// Hash includes() is collision-prone; require delimited match to reduce spoofing. Explicit {rootSessionId,parentSessionId} lineage is future work.
|
|
78
78
|
const h = hashSessionPart(parent);
|
|
79
79
|
return child.includes(`-${h}-`) || child.endsWith(`-${h}`);
|
|
80
80
|
}
|
|
@@ -101,13 +101,13 @@ export interface SubAgentTrace {
|
|
|
101
101
|
const subagentTraces = new Map<string, SubAgentTrace>();
|
|
102
102
|
const subagentListeners = new Map<string, Set<(trace: SubAgentTrace) => void>>();
|
|
103
103
|
|
|
104
|
-
/** Max trace keys held (LRU, evicts oldest whole trace on insert)
|
|
104
|
+
/** Max trace keys held (LRU, evicts oldest whole trace on insert). */
|
|
105
105
|
const MAX_SUBAGENT_TRACES = 500;
|
|
106
|
-
/** Trace TTL: 1h wall-clock, lazily expired on read
|
|
106
|
+
/** Trace TTL: 1h wall-clock, lazily expired on read. */
|
|
107
107
|
const SUBAGENT_TRACE_TTL_MS = 60 * 60 * 1000;
|
|
108
|
-
/** Max steps retained per trace; oldest steps are dropped beyond this
|
|
108
|
+
/** Max steps retained per trace; oldest steps are dropped beyond this. */
|
|
109
109
|
const MAX_SUBAGENT_STEPS_PER_TRACE = 100;
|
|
110
|
-
/** Live subscriptions auto-expire after 5min
|
|
110
|
+
/** Live subscriptions auto-expire after 5min. */
|
|
111
111
|
const SUBAGENT_SUBSCRIBE_TTL_MS = 5 * 60 * 1000;
|
|
112
112
|
|
|
113
113
|
/** Last-write wall-clock per canonical trackingId (drives TTL; reads do not extend it). */
|
|
@@ -135,7 +135,7 @@ function touchTrace(canonical: string, now = Date.now()): void {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
* Evicts expired traces, then oldest whole traces while over budget
|
|
138
|
+
* Evicts expired traces, then oldest whole traces while over budget.
|
|
139
139
|
* Called on every insert. Whole-trace eviction removes both the canonical
|
|
140
140
|
* key and its display-id alias.
|
|
141
141
|
*/
|
|
@@ -155,7 +155,7 @@ function pruneTraces(now = Date.now()): void {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
/** Drops oldest steps beyond {@link MAX_SUBAGENT_STEPS_PER_TRACE}
|
|
158
|
+
/** Drops oldest steps beyond {@link MAX_SUBAGENT_STEPS_PER_TRACE}. */
|
|
159
159
|
function trimTraceSteps(trace: SubAgentTrace): void {
|
|
160
160
|
if (trace.steps.length > MAX_SUBAGENT_STEPS_PER_TRACE) {
|
|
161
161
|
trace.steps.splice(0, trace.steps.length - MAX_SUBAGENT_STEPS_PER_TRACE);
|
|
@@ -268,7 +268,7 @@ export function subscribeToSubAgentForSessions(
|
|
|
268
268
|
|
|
269
269
|
/**
|
|
270
270
|
* Subscribes to live updates of one worker trace. Returns an unsubscribe fn.
|
|
271
|
-
* Subscriptions auto-expire after 5min to avoid listener leaks
|
|
271
|
+
* Subscriptions auto-expire after 5min to avoid listener leaks.
|
|
272
272
|
*/
|
|
273
273
|
export function subscribeToSubAgent(id: string, fn: (trace: SubAgentTrace) => void): () => void {
|
|
274
274
|
const t = lookupTrace(id);
|
|
@@ -469,7 +469,7 @@ export function workerMetadata(a: {
|
|
|
469
469
|
}
|
|
470
470
|
|
|
471
471
|
// ---------------------------------------------------------------------------
|
|
472
|
-
// Budgets
|
|
472
|
+
// Budgets: counters + guards. The loop charges usage per turn; the
|
|
473
473
|
// worker registry enforces `maxTotalSubagents` through `reserveSpawn`. Durations use the monotonic `nowMs()`
|
|
474
474
|
// clock; TTL timestamps above use wall-clock `Date.now()`.
|
|
475
475
|
// ---------------------------------------------------------------------------
|
|
@@ -552,7 +552,7 @@ export function checkBudget(
|
|
|
552
552
|
}
|
|
553
553
|
|
|
554
554
|
/**
|
|
555
|
-
* Adds worker usage to the parent's budget counters
|
|
555
|
+
* Adds worker usage to the parent's budget counters. Never throws;
|
|
556
556
|
* call {@link checkBudget} afterwards to enforce.
|
|
557
557
|
*/
|
|
558
558
|
export function chargeBudgetUsage(parentAgent: Agent, usage: unknown): BudgetState {
|
|
@@ -563,7 +563,7 @@ export function chargeBudgetUsage(parentAgent: Agent, usage: unknown): BudgetSta
|
|
|
563
563
|
}
|
|
564
564
|
|
|
565
565
|
/**
|
|
566
|
-
* Reserves `count` spawns against `maxTotalSubagents
|
|
566
|
+
* Reserves `count` spawns against `maxTotalSubagents`. Increments the
|
|
567
567
|
* parent's `budgetState.spawned` and throws {@link BudgetExceededError} when
|
|
568
568
|
* the reservation would exceed the budget. Call before spawning.
|
|
569
569
|
*/
|
|
@@ -623,7 +623,7 @@ export function agentToTool(
|
|
|
623
623
|
}),
|
|
624
624
|
execute: async ({ task }, ctx) => {
|
|
625
625
|
const startTime = nowMs();
|
|
626
|
-
//
|
|
626
|
+
// Lineage for fixed-delegation calls.
|
|
627
627
|
const callRunId = newRunId();
|
|
628
628
|
const callTurnId = newTurnId();
|
|
629
629
|
const parentSessionId = ctx?.sessionId || agentInstance.sessionId || "session";
|