agent-accelerator 0.5.6 → 0.5.8
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 +10 -17
- package/package.json +1 -1
- package/src/agent/agent.ts +92 -57
- package/src/agent/architecture.md +25 -0
- package/src/agent/context.ts +7 -3
- package/src/agent/delegation.ts +30 -96
- package/src/agent/loop.ts +54 -56
- package/src/agent/subagent.ts +4 -4
- package/src/agent/worker.ts +1 -3
- package/src/architecture.md +28 -0
- package/src/index.ts +13 -80
- package/src/models/architecture.md +14 -0
- package/src/models/catalog-cache.ts +10 -7
- package/src/models/catalog.ts +20 -13
- package/src/providers/architecture.md +24 -0
- package/src/providers/google.ts +24 -4
- package/src/providers/openai-compat.ts +29 -7
- package/src/providers/openai.ts +22 -6
- package/src/providers/openrouter.ts +14 -2
- package/src/providers/registry.ts +1 -0
- package/src/providers/shared.ts +4 -4
- package/src/providers.ts +9 -53
- package/src/scheduler/architecture.md +22 -0
- package/src/scheduler/scheduler.ts +131 -56
- package/src/scheduler/store.ts +274 -54
- package/src/scheduler/time.ts +2 -0
- package/src/session/architecture.md +19 -0
- package/src/session/store.ts +37 -22
- package/src/session/usage.ts +1 -0
- package/src/streaming/architecture.md +13 -0
- package/src/streaming/event-stream.ts +16 -11
- package/src/telemetry-file.ts +74 -24
- package/src/telemetry.ts +3 -2
- package/src/tools/architecture.md +19 -0
- package/src/tools/executor.ts +14 -1
- package/src/tools/schema.ts +1 -0
- package/src/tools/subagent-result.ts +1 -0
- package/src/tools/tool.ts +8 -8
- package/src/types/agent.ts +22 -20
- package/src/types/architecture.md +20 -0
- 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 +25 -0
- package/src/utils/base64.ts +2 -0
- package/src/utils/cache.ts +6 -4
- package/src/utils/durable.ts +78 -3
- package/src/utils/env.ts +1 -1
- package/src/utils/errors.ts +4 -0
- package/src/utils/headers.ts +7 -4
- package/src/utils/home.ts +1 -1
- package/src/utils/media.ts +1 -0
- package/src/utils/raw-capture.ts +3 -0
- package/src/utils/retry.ts +2 -1
- package/src/utils/safe-io.ts +4 -0
- package/src/utils/session.ts +10 -166
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -386,12 +386,14 @@ telemetryBus.onEvent((e) => {
|
|
|
386
386
|
| `skipScheduledAction(id, { home? })` | Ignore a missed run and wait for the next scheduled time. |
|
|
387
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. |
|
|
388
388
|
|
|
389
|
+
Every function except `createScheduledAction` finds the home folder through `setHome()`, `AGENT_ACCEL_HOME`, or `.agent-accel/` in the working directory. If you built the Agent with its own `home`, pass the same `{ home }` to these calls (the model's `scheduled_actions` tool already does).
|
|
390
|
+
|
|
389
391
|
**How it behaves**
|
|
390
392
|
|
|
391
|
-
* **One local clock.** A single timer per
|
|
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.
|
|
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.
|
|
393
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`.
|
|
394
|
-
* **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`.
|
|
395
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.
|
|
396
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.
|
|
397
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`.
|
|
@@ -657,7 +659,7 @@ Implement the `Provider` interface when adding a fully custom transport.
|
|
|
657
659
|
|
|
658
660
|
### Google
|
|
659
661
|
|
|
660
|
-
`GoogleInteractionsProvider`
|
|
662
|
+
`GoogleInteractionsProvider` and `GOOGLE_MODELS` provide Google support over the Interactions API (`POST {baseUrl}/interactions`, streaming via `?alt=sse`).
|
|
661
663
|
|
|
662
664
|
Turns chain statefully through `previous_interaction_id` per session, falling back to stateless full-history sends when the session switches providers or models.
|
|
663
665
|
|
|
@@ -683,7 +685,7 @@ Helpers:
|
|
|
683
685
|
|
|
684
686
|
### OpenAI
|
|
685
687
|
|
|
686
|
-
`OpenAIResponsesProvider`
|
|
688
|
+
`OpenAIResponsesProvider` and `OPENAI_MODELS` provide OpenAI support over the Responses API (`POST {baseUrl}/responses`).
|
|
687
689
|
|
|
688
690
|
Every turn is stateless: the full history is sent explicitly with `store: false`, so no `previous_response_id`, `background`, or conversation chaining is ever used.
|
|
689
691
|
|
|
@@ -702,7 +704,7 @@ It also:
|
|
|
702
704
|
|
|
703
705
|
### OpenRouter
|
|
704
706
|
|
|
705
|
-
`OpenRouterChatCompletionsProvider`
|
|
707
|
+
`OpenRouterChatCompletionsProvider` and `OPENROUTER_MODELS` provide OpenRouter support over Chat Completions (`POST {baseUrl}/chat/completions`).
|
|
706
708
|
|
|
707
709
|
Every turn is stateless: the full `messages[]` array is sent explicitly — no chaining primitives exist on this endpoint.
|
|
708
710
|
|
|
@@ -731,7 +733,7 @@ setAttribution({ referer: "https://my.app", title: "My App" });
|
|
|
731
733
|
|
|
732
734
|
### Custom
|
|
733
735
|
|
|
734
|
-
`
|
|
736
|
+
`OpenAICompatibleChatProvider` is also exposed through the `CustomProvider`, `createCustomProvider`, and `createOpenAICompatibleProvider(prefix, opts?)` aliases.
|
|
735
737
|
|
|
736
738
|
`CustomProviderOptions`:
|
|
737
739
|
|
|
@@ -931,15 +933,6 @@ Use:
|
|
|
931
933
|
Breaking out of `for await` cancels the stream. Your `AbortSignal` and stream cancellation share one linked controller, so `controller.abort()` and `stream.cancel()` both stop the HTTP request. Abort rejects with `AbortError` (never retried) and never resolves partial results. `agent.run(prompt, { stream: true, signal })` exposes the same
|
|
932
934
|
`cancel` handle.
|
|
933
935
|
|
|
934
|
-
`SSEParser` provides:
|
|
935
|
-
|
|
936
|
-
```ts
|
|
937
|
-
feed(chunk)
|
|
938
|
-
flush()
|
|
939
|
-
```
|
|
940
|
-
|
|
941
|
-
It parses provider SSE streams and is only needed when implementing custom transports.
|
|
942
|
-
|
|
943
936
|
---
|
|
944
937
|
## Response, Usage, and Raw Data
|
|
945
938
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-accelerator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
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,14 +22,14 @@ 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";
|
|
29
29
|
import { assertNoToolCollisions } from "../tools/executor.ts";
|
|
30
30
|
import { runAgentLoop, streamAgentLoop, unfinishedToolCalls, endsWithStop, accumulateUsage, type SteerEntry } from "./loop.ts";
|
|
31
31
|
import { isRetryableRun, readRetryAfterMs, sleep } from "../utils/retry.ts";
|
|
32
|
-
import { createSessionId, nowMs, newRunId } from "../utils/session.ts";
|
|
32
|
+
import { createSessionId, nowMs, newRunId, previewPrompt } from "../utils/session.ts";
|
|
33
33
|
import {
|
|
34
34
|
chargeBudgetUsage,
|
|
35
35
|
checkBudget,
|
|
@@ -66,7 +66,7 @@ export function resolveEffectiveThinking(
|
|
|
66
66
|
import { ConfigError, SessionLoadError, 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;
|
|
@@ -590,7 +616,7 @@ export class Agent {
|
|
|
590
616
|
.map((m) => ({
|
|
591
617
|
runId: m.id,
|
|
592
618
|
startedAt: m.at,
|
|
593
|
-
prompt:
|
|
619
|
+
prompt: previewPrompt((this.context.messages[m.i]?.content ?? "") as string | ContentPart[]),
|
|
594
620
|
pendingToolCalls: calls,
|
|
595
621
|
}));
|
|
596
622
|
}
|
|
@@ -652,6 +678,7 @@ export class Agent {
|
|
|
652
678
|
this.doPersist();
|
|
653
679
|
}
|
|
654
680
|
|
|
681
|
+
// Builds the system prompt: the developer's instructions plus, when thinking is on, the fixed Reasoning Directive; in: this.instructions + thinkingConfig, out: every AgentContext.systemPrompt.
|
|
655
682
|
private getFullInstructions(): string | undefined {
|
|
656
683
|
const base = this.instructions || "";
|
|
657
684
|
let out = base;
|
|
@@ -677,13 +704,13 @@ export class Agent {
|
|
|
677
704
|
this.context.thoughtSignatures = [];
|
|
678
705
|
this.context.systemPrompt = this.getFullInstructions();
|
|
679
706
|
this.runMarks.length = 0;
|
|
680
|
-
//
|
|
707
|
+
// Reset also drops session-scoped provider chaining/routing.
|
|
681
708
|
try { clearInteractionChains(sid); } catch {}
|
|
682
709
|
try { clearSessionRouting(sid); } catch {}
|
|
683
710
|
}
|
|
684
711
|
|
|
685
712
|
/**
|
|
686
|
-
* Returns a named session handle
|
|
713
|
+
* Returns a named session handle. `session()` creates an isolated
|
|
687
714
|
* `AgentContext` bound to `id` (separate histories per id); `agent.run`
|
|
688
715
|
* with `{ sessionId }` selects/creates the same entry. Sessions are
|
|
689
716
|
* ephemeral in-memory (LRU 100 + 1h TTL). `agent.run`/`stream` without a
|
|
@@ -695,14 +722,15 @@ export class Agent {
|
|
|
695
722
|
return { id: sid, context: ctx, runController: null };
|
|
696
723
|
}
|
|
697
724
|
|
|
698
|
-
/** Drops one named session
|
|
725
|
+
/** Drops one named session. No-op for unknown ids. */
|
|
699
726
|
disposeSession(id: string): void {
|
|
727
|
+
unregisterSessionFolder(id);
|
|
700
728
|
try { this.sessions.delete(id); } catch {}
|
|
701
729
|
try { clearInteractionChains(id); } catch {}
|
|
702
730
|
try { clearSessionRouting(id); } catch {}
|
|
703
731
|
}
|
|
704
732
|
|
|
705
|
-
/** Drops all named sessions except the default context
|
|
733
|
+
/** Drops all named sessions except the default context. */
|
|
706
734
|
clearSessions(): void {
|
|
707
735
|
try {
|
|
708
736
|
for (const id of [...this.sessions.keys()]) {
|
|
@@ -752,7 +780,7 @@ export class Agent {
|
|
|
752
780
|
return fresh;
|
|
753
781
|
}
|
|
754
782
|
|
|
755
|
-
/** Rejects + drops all queued follow-ups
|
|
783
|
+
/** Rejects + drops all queued follow-ups. Returns the cleared count. */
|
|
756
784
|
clearQueue(): number {
|
|
757
785
|
const n = this.pendingQueue.length;
|
|
758
786
|
if (n === 0) return 0;
|
|
@@ -766,7 +794,7 @@ export class Agent {
|
|
|
766
794
|
return n;
|
|
767
795
|
}
|
|
768
796
|
|
|
769
|
-
/** Throws when the steer inbox is at its
|
|
797
|
+
/** Throws when the steer inbox is at its bound. */
|
|
770
798
|
private assertSteerRoom(inbox: SteerEntry[]): void {
|
|
771
799
|
const max = this.maxSteers ?? 20;
|
|
772
800
|
if (inbox.length >= max) {
|
|
@@ -799,12 +827,14 @@ export class Agent {
|
|
|
799
827
|
return this.midSessionConfig.mode;
|
|
800
828
|
}
|
|
801
829
|
|
|
830
|
+
// Picks steer or queue for a call that arrives while busy: the developer's enforced midSession mode wins over the per-call request; in: run()/stream() `enqueue`, out: their busy branch.
|
|
802
831
|
private resolveEnqueueMode(requested?: "steer" | "queue"): "steer" | "queue" {
|
|
803
832
|
const enforced = this.midSessionConfig.mode;
|
|
804
833
|
if (enforced === "steer" || enforced === "queue") return enforced;
|
|
805
834
|
return requested === "steer" ? "steer" : "queue";
|
|
806
835
|
}
|
|
807
836
|
|
|
837
|
+
// Throws synchronously on an empty content-part array so run() and stream() fail at the same moment; in: any prompt entry point, out: nothing.
|
|
808
838
|
private assertPromptValid(prompt: string | ContentPart[]): void {
|
|
809
839
|
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
810
840
|
throw new Error(
|
|
@@ -813,19 +843,6 @@ export class Agent {
|
|
|
813
843
|
}
|
|
814
844
|
}
|
|
815
845
|
|
|
816
|
-
private previewPrompt(prompt: string | ContentPart[]): string {
|
|
817
|
-
if (typeof prompt === "string") return prompt.slice(0, 500);
|
|
818
|
-
try {
|
|
819
|
-
const texts = prompt
|
|
820
|
-
.filter((p) => (p as { type?: unknown }).type === "text")
|
|
821
|
-
.map((p) => (p as { text?: string }).text ?? "")
|
|
822
|
-
.join("\n");
|
|
823
|
-
return (texts || "[multipart]").slice(0, 500);
|
|
824
|
-
} catch {
|
|
825
|
-
return "[multipart]";
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
846
|
/**
|
|
830
847
|
* Stops the active run now (STOP.) and, given a message, starts it next (STOP. Do this instead). The provider
|
|
831
848
|
* request is aborted (connection closed, so the provider stops generating where it can), the stream is cancelled, and
|
|
@@ -961,19 +978,20 @@ export class Agent {
|
|
|
961
978
|
try {
|
|
962
979
|
this.currentRun?.outerStream?.push({
|
|
963
980
|
type: "queued",
|
|
964
|
-
queuedPrompt:
|
|
981
|
+
queuedPrompt: previewPrompt(prompt),
|
|
965
982
|
queueLength: this.pendingQueue.length,
|
|
966
983
|
} as any);
|
|
967
984
|
} catch {}
|
|
968
985
|
});
|
|
969
986
|
}
|
|
970
987
|
|
|
988
|
+
// Starts the next waiting prompt (urgent ones first, aborted ones rejected) as a streaming or non-streaming run once the agent is idle; in: finishCurrentRun and queue changes, out: a new run whose result settles the queued caller.
|
|
971
989
|
private pumpQueue(): void {
|
|
972
990
|
if (this.currentRun !== null) return;
|
|
973
991
|
const next = this.pendingQueue.shift();
|
|
974
992
|
if (!next) return;
|
|
975
993
|
this.persistNow();
|
|
976
|
-
//
|
|
994
|
+
// Reject queued items whose signal aborted before start.
|
|
977
995
|
try {
|
|
978
996
|
if ((next.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
|
|
979
997
|
const reason = ((next.options as { signal?: AbortSignal }).signal as unknown as { reason?: unknown })?.reason;
|
|
@@ -1023,8 +1041,9 @@ export class Agent {
|
|
|
1023
1041
|
this.startNonStreamingRun(next.prompt, next.options).then(next.resolve, next.reject);
|
|
1024
1042
|
}
|
|
1025
1043
|
|
|
1044
|
+
// 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.
|
|
1026
1045
|
private finishCurrentRun(): void {
|
|
1027
|
-
//
|
|
1046
|
+
// Leftover steer entries become queued follow-ups instead of dropping.
|
|
1028
1047
|
let leftover: SteerEntry[] = [];
|
|
1029
1048
|
try { leftover = this.currentRun?.steerInbox?.splice(0) ?? []; } catch { leftover = []; }
|
|
1030
1049
|
this.currentRun = null;
|
|
@@ -1065,6 +1084,7 @@ export class Agent {
|
|
|
1065
1084
|
queueMicrotask(() => this.pumpQueue());
|
|
1066
1085
|
}
|
|
1067
1086
|
|
|
1087
|
+
// Removes the abort listener a run's merged signal fallback installed; in: a settled run's controller, out: nothing.
|
|
1068
1088
|
private cleanupMerge(runController: AbortController): void {
|
|
1069
1089
|
try {
|
|
1070
1090
|
const dispose = mergeDisposers.get(runController);
|
|
@@ -1075,6 +1095,7 @@ export class Agent {
|
|
|
1075
1095
|
} catch {}
|
|
1076
1096
|
}
|
|
1077
1097
|
|
|
1098
|
+
// Joins the caller's AbortSignal with the run's own controller (AbortSignal.any, else a manual forward) so both stop the run; in: AgentRunOptions.signal + the run controller, out: the signal handed to the loop.
|
|
1078
1099
|
private mergeUserSignal(userSignal?: AbortSignal, runController?: AbortController): AbortSignal | undefined {
|
|
1079
1100
|
if (!runController) return userSignal;
|
|
1080
1101
|
if (!userSignal) return runController.signal;
|
|
@@ -1086,7 +1107,7 @@ export class Agent {
|
|
|
1086
1107
|
}
|
|
1087
1108
|
return runController.signal;
|
|
1088
1109
|
}
|
|
1089
|
-
//
|
|
1110
|
+
// Prefer AbortSignal.any (no expando, no listener leak).
|
|
1090
1111
|
try {
|
|
1091
1112
|
const anyFn = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;
|
|
1092
1113
|
if (typeof anyFn === "function") {
|
|
@@ -1227,6 +1248,7 @@ export class Agent {
|
|
|
1227
1248
|
};
|
|
1228
1249
|
}
|
|
1229
1250
|
|
|
1251
|
+
// Runs one non-streaming run as a task: catalog refresh, model resolve, prompt/mark (or a resumed mark), loop with `runRetries` backoff, trace merge, run ending recorded; in: run()/queue/resume(), out: the AgentResponse promise.
|
|
1230
1252
|
private startNonStreamingRun(
|
|
1231
1253
|
prompt: string | ContentPart[],
|
|
1232
1254
|
options?: AgentRunOptions,
|
|
@@ -1238,7 +1260,7 @@ export class Agent {
|
|
|
1238
1260
|
const steerInbox: SteerEntry[] = [];
|
|
1239
1261
|
const mergedSignal = this.mergeUserSignal(options?.signal, runController);
|
|
1240
1262
|
const runOptions = this.withResolvedThinking({ ...options, signal: mergedSignal });
|
|
1241
|
-
//
|
|
1263
|
+
// Route to the named session context (default = this.context).
|
|
1242
1264
|
const targetContext = this.getSessionContext(runOptions?.sessionId || this.sessionId);
|
|
1243
1265
|
const task = (async (): Promise<AgentResponse> => {
|
|
1244
1266
|
await ensureModelCatalogFresh();
|
|
@@ -1279,7 +1301,7 @@ export class Agent {
|
|
|
1279
1301
|
...(resumeMark ? { resume: { stored: this.storedToolResults() } } : {}),
|
|
1280
1302
|
steerInbox,
|
|
1281
1303
|
onSteerInjected: () => this.persistNow(),
|
|
1282
|
-
//
|
|
1304
|
+
// Loop enforces these (plus local timeout races).
|
|
1283
1305
|
limits: { ...this.runLimits },
|
|
1284
1306
|
...(this.output !== undefined ? { output: this.output } : {}),
|
|
1285
1307
|
};
|
|
@@ -1328,6 +1350,7 @@ export class Agent {
|
|
|
1328
1350
|
return task;
|
|
1329
1351
|
}
|
|
1330
1352
|
|
|
1353
|
+
// Starts one streaming run: validates the model and thinking level synchronously, adds the prompt, builds the loop config and returns the loop's event stream, recording the run ending when it settles; in: stream()/pumpQueue, out: an AssistantMessageEventStream.
|
|
1331
1354
|
private startStreamingRun(
|
|
1332
1355
|
prompt: string | ContentPart[],
|
|
1333
1356
|
options?: AgentRunOptions
|
|
@@ -1337,7 +1360,7 @@ export class Agent {
|
|
|
1337
1360
|
if (effectiveLevel) {
|
|
1338
1361
|
validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
|
|
1339
1362
|
}
|
|
1340
|
-
//
|
|
1363
|
+
// Named session routing (default session for compat).
|
|
1341
1364
|
const targetContext = this.getSessionContext(options?.sessionId || this.sessionId);
|
|
1342
1365
|
const mark = this.prepareTurn(prompt, options, targetContext);
|
|
1343
1366
|
this.persistNow();
|
|
@@ -1369,7 +1392,7 @@ export class Agent {
|
|
|
1369
1392
|
openToolLog: (call: { name: string }, sid: string | undefined) => this.openToolLog(call, sid),
|
|
1370
1393
|
steerInbox,
|
|
1371
1394
|
onSteerInjected: () => this.persistNow(),
|
|
1372
|
-
//
|
|
1395
|
+
// Loop enforces these (plus local timeout races).
|
|
1373
1396
|
limits: { ...this.runLimits },
|
|
1374
1397
|
...(this.output !== undefined ? { output: this.output } : {}),
|
|
1375
1398
|
};
|
|
@@ -1459,14 +1482,14 @@ export class Agent {
|
|
|
1459
1482
|
* Called automatically after every step during runs; safe to call
|
|
1460
1483
|
* manually. Never throws (failures route to `onPersistError`).
|
|
1461
1484
|
*
|
|
1462
|
-
*
|
|
1485
|
+
* Stateless agents skip persistence unless
|
|
1463
1486
|
* `(persist as any).allowStateless` is set. Calls in the same tick are
|
|
1464
1487
|
* coalesced via a `queueMicrotask` single flight.
|
|
1465
1488
|
*/
|
|
1466
1489
|
persistNow(): void {
|
|
1467
1490
|
const cfg = this.persistConfig;
|
|
1468
1491
|
if (!cfg || (!cfg.dir && !cfg.file)) return;
|
|
1469
|
-
//
|
|
1492
|
+
// No stateless persist by default.
|
|
1470
1493
|
try {
|
|
1471
1494
|
if (this.stateless && !(cfg as unknown as { allowStateless?: unknown }).allowStateless) return;
|
|
1472
1495
|
} catch {}
|
|
@@ -1508,7 +1531,7 @@ export class Agent {
|
|
|
1508
1531
|
if (cfg.dir) this.journalFor(cfg.dir).sync(this as any);
|
|
1509
1532
|
else if (cfg.file) saveSessionFile(cfg.file, this as any, null);
|
|
1510
1533
|
} catch (err) {
|
|
1511
|
-
//
|
|
1534
|
+
// Surface via hook instead of swallowing silently.
|
|
1512
1535
|
try { (cfg as unknown as { onPersistError?: (e: unknown) => void }).onPersistError?.(err); } catch {}
|
|
1513
1536
|
}
|
|
1514
1537
|
}
|
|
@@ -1578,6 +1601,16 @@ export class Agent {
|
|
|
1578
1601
|
this.warnOnLocalMediaPaths(data.messages ?? []);
|
|
1579
1602
|
const prevSid = this.sessionId;
|
|
1580
1603
|
if (data.sessionId) (this as any).sessionId = data.sessionId;
|
|
1604
|
+
if (this.sessionId !== prevSid) {
|
|
1605
|
+
try {
|
|
1606
|
+
this.lockSessionFolder();
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
(this as any).sessionId = prevSid; // the session belongs to another process: keep ours untouched
|
|
1609
|
+
throw err;
|
|
1610
|
+
}
|
|
1611
|
+
unregisterSessionFolder(prevSid);
|
|
1612
|
+
this.registerTelemetryFolder();
|
|
1613
|
+
}
|
|
1581
1614
|
if ((data as any).parentSessionId) (this as any).parentSessionId = (data as any).parentSessionId;
|
|
1582
1615
|
if ((data as any).subagents && typeof (data as any).subagents === "object") {
|
|
1583
1616
|
try {
|
|
@@ -1621,7 +1654,7 @@ export class Agent {
|
|
|
1621
1654
|
} else {
|
|
1622
1655
|
this.context.systemPrompt = this.getFullInstructions();
|
|
1623
1656
|
}
|
|
1624
|
-
//
|
|
1657
|
+
// Importing a snapshot invalidates prior provider chaining/routing
|
|
1625
1658
|
// for both the previous and the restored session.
|
|
1626
1659
|
try { clearInteractionChains(prevSid); } catch {}
|
|
1627
1660
|
try { clearSessionRouting(prevSid); } catch {}
|
|
@@ -1655,8 +1688,9 @@ export class Agent {
|
|
|
1655
1688
|
} catch {}
|
|
1656
1689
|
}
|
|
1657
1690
|
|
|
1691
|
+
// 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.
|
|
1658
1692
|
private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext): SessionRunMark | undefined {
|
|
1659
|
-
//
|
|
1693
|
+
// Prompt validation throws synchronously in both run() and
|
|
1660
1694
|
// stream() (same timing; documented here).
|
|
1661
1695
|
if (Array.isArray(prompt) && prompt.length === 0) {
|
|
1662
1696
|
throw new Error(
|
|
@@ -1665,7 +1699,7 @@ export class Agent {
|
|
|
1665
1699
|
}
|
|
1666
1700
|
const ctx = target ?? this.getSessionContext(options?.sessionId || this.sessionId);
|
|
1667
1701
|
if (this.stateless) {
|
|
1668
|
-
//
|
|
1702
|
+
// Stateless runs use a fresh ephemeral context (never shared).
|
|
1669
1703
|
ctx.messages = [];
|
|
1670
1704
|
ctx.thoughtSignatures = [];
|
|
1671
1705
|
}
|
|
@@ -1731,7 +1765,7 @@ export class Agent {
|
|
|
1731
1765
|
prompt: string | ContentPart[],
|
|
1732
1766
|
options?: AgentRunOptions
|
|
1733
1767
|
): Promise<AgentResponse> | AssistantMessageEventStream {
|
|
1734
|
-
//
|
|
1768
|
+
// Prompt validation throws synchronously in both run() and
|
|
1735
1769
|
// stream() (same timing; async work starts only after validation).
|
|
1736
1770
|
this.assertPromptValid(prompt);
|
|
1737
1771
|
const isStream = options?.stream === true;
|
|
@@ -1791,7 +1825,7 @@ export class Agent {
|
|
|
1791
1825
|
try {
|
|
1792
1826
|
this.currentRun?.outerStream?.push({
|
|
1793
1827
|
type: "queued",
|
|
1794
|
-
queuedPrompt:
|
|
1828
|
+
queuedPrompt: previewPrompt(prompt),
|
|
1795
1829
|
queueLength: this.pendingQueue.length,
|
|
1796
1830
|
} as any);
|
|
1797
1831
|
} catch {}
|
|
@@ -1869,7 +1903,7 @@ export class Agent {
|
|
|
1869
1903
|
prompt: string | ContentPart[],
|
|
1870
1904
|
options?: AgentRunOptions
|
|
1871
1905
|
): AssistantMessageEventStream {
|
|
1872
|
-
//
|
|
1906
|
+
// Same sync validation timing as run().
|
|
1873
1907
|
this.assertPromptValid(prompt);
|
|
1874
1908
|
// Mid-session: busy agent serializes via steer (same turn) or queue (next turn).
|
|
1875
1909
|
if (this.currentRun) {
|
|
@@ -1933,7 +1967,7 @@ export class Agent {
|
|
|
1933
1967
|
try {
|
|
1934
1968
|
this.currentRun.outerStream?.push({
|
|
1935
1969
|
type: "queued",
|
|
1936
|
-
queuedPrompt:
|
|
1970
|
+
queuedPrompt: previewPrompt(prompt),
|
|
1937
1971
|
queueLength: this.pendingQueue.length + 1,
|
|
1938
1972
|
} as any);
|
|
1939
1973
|
} catch {}
|
|
@@ -2008,6 +2042,7 @@ export class Agent {
|
|
|
2008
2042
|
return s;
|
|
2009
2043
|
}
|
|
2010
2044
|
|
|
2045
|
+
// Attaches onEvent/onDelta/onThinkingDelta to a stream and, with wrapThinking, adds display-only <think> tags and whitespace hygiene on the outer events; in: stream()/pumpQueue, out: listeners on the stream.
|
|
2011
2046
|
private wireStreamCallbacks(
|
|
2012
2047
|
s: AssistantMessageEventStream,
|
|
2013
2048
|
options?: AgentRunOptions
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# src/agent/
|
|
2
|
+
|
|
3
|
+
The agent and everything that runs under it.
|
|
4
|
+
|
|
5
|
+
| File | What it is | Used by |
|
|
6
|
+
| :-- | :-- | :-- |
|
|
7
|
+
| `agent.ts` | `Agent`: `run`/`stream`/`ask`, `steer`/`queue`/`interrupt`, persistence hooks, `resume()`/`interrupted()`, `usageReport()`, `fork()`, `toConfig()`/`fromConfig()`, the `workers` getter. | callers, workers, scheduler |
|
|
8
|
+
| `loop.ts` | `runAgentLoop` / `streamAgentLoop`: per turn budget, pairing repair, file access, provider call, tool execution, repeat guard. Also `ensureToolPairing`, `accumulateUsage`. | `Agent` |
|
|
9
|
+
| `context.ts` | `AgentContext`: the validated message list with checkpoint/rollback. | `Agent`, loops |
|
|
10
|
+
| `delegation.ts` | Fixed workers (`agentToTool`, `buildAgentTools`), the live worker trace store, session-id helpers, budget helpers, `openWorkerTrace`/`workerMetadata`. | `Agent`, `workers.ts` |
|
|
11
|
+
| `worker.ts` | Worker core: model/tool normalization, `buildDynamicWorkerConfig` (shared preset), `createLiveWorker`. | `Agent`, registry, scheduler |
|
|
12
|
+
| `workers.ts` | `WorkerRegistry` (`agent.workers`): persistent multi-turn workers. | `Agent`, `subagents` tool |
|
|
13
|
+
| `subagents-tool.ts` + `subagents.md` | The one model-facing `subagents` tool; the `.md` is its description. | `Agent` constructor |
|
|
14
|
+
| `subagent.ts` | `SubAgent` class and `agentFromEnv` (thin, kept for compatibility). | callers |
|
|
15
|
+
| `turns.ts` | Pure grouping of messages into finished turns (history cut for workers). | `workers.ts` |
|
|
16
|
+
| `config-blocks.ts` | RSI blocks: only `model`, `instructions`, `tools` are editable; `allowedModels` check. | `Agent` |
|
|
17
|
+
|
|
18
|
+
## Invariants
|
|
19
|
+
- `runAgentLoop` and `streamAgentLoop` duplicate their turn logic: every loop change goes into both.
|
|
20
|
+
- An `Agent` runs one thing at a time; delegation forks a copy per call so parallel calls never queue.
|
|
21
|
+
- A worker is a plain `Agent` with a parent link and presets. Never duplicate logic between Agent and worker.
|
|
22
|
+
- A worker is never less constrained than its parent (`inheritableConfig()`); its children draw only from the tools it was granted.
|
|
23
|
+
- Exactly-once worker calls rest on the call key (parent run mark id + tool call id) and `meta.last`.
|
|
24
|
+
- Only results branded by `makeSubagentResult` have their usage metadata trusted.
|
|
25
|
+
- Each Agent records only its own model turns; never sum `AgentResponse.usage` for totals.
|
package/src/agent/context.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { clearInteractionChains } from "../providers/google.ts";
|
|
|
6
6
|
|
|
7
7
|
const USER_PART_TYPES = new Set(["text", "image", "audio", "video", "file"]);
|
|
8
8
|
|
|
9
|
+
// Throws the AgentContext validation error with the SDK prefix; in: the add* validators, out: a thrown Error.
|
|
9
10
|
function fail(message: string): never {
|
|
10
11
|
throw new Error(`[Agent Accelerator] AgentContext: ${message}`);
|
|
11
12
|
}
|
|
@@ -32,6 +33,7 @@ export class AgentContext {
|
|
|
32
33
|
this.systemPrompt = systemPrompt;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// Validates a user prompt (string or media/text parts) and appends it; in: Agent.prepareTurn and drained steers, out: messages.
|
|
35
37
|
addUserMessage(content: string | ContentPart[]): void {
|
|
36
38
|
if (Array.isArray(content)) {
|
|
37
39
|
if (content.length === 0) return;
|
|
@@ -120,6 +122,7 @@ export class AgentContext {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
// Appends one tool message per result, keeping logFile and stopped marks; in: the loops after a tool batch, out: messages.
|
|
123
126
|
addToolResults(results: ToolResultRecord[]): void {
|
|
124
127
|
if (!results || results.length === 0) return;
|
|
125
128
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -145,6 +148,7 @@ export class AgentContext {
|
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
|
|
151
|
+
// Copies the context with media bytes preserved (structuredClone, else a deep copy); in: callers needing an isolated history, out: a new AgentContext.
|
|
148
152
|
clone(): AgentContext {
|
|
149
153
|
const next = new AgentContext(this.systemPrompt);
|
|
150
154
|
// Preserve Uint8Array/ArrayBuffer media via structuredClone when available (M8 fix)
|
|
@@ -166,8 +170,8 @@ export class AgentContext {
|
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
/**
|
|
169
|
-
* Captures a detached snapshot of conversation state for run-level rollback
|
|
170
|
-
*
|
|
173
|
+
* Captures a detached snapshot of conversation state for run-level rollback.
|
|
174
|
+
* The snapshot is a structural deep copy (NOT JSON): `Uint8Array` /
|
|
171
175
|
* `ArrayBuffer` media must survive a rollback byte-for-byte, and large
|
|
172
176
|
* base64 strings are shared by reference rather than re-serialized.
|
|
173
177
|
*/
|
|
@@ -181,7 +185,7 @@ export class AgentContext {
|
|
|
181
185
|
/**
|
|
182
186
|
* Restores a checkpoint captured by `checkpoint()`. Malformed checkpoints
|
|
183
187
|
* are ignored (current state kept). When `sessionId` is provided, also
|
|
184
|
-
* clears session-scoped provider state
|
|
188
|
+
* clears session-scoped provider state so a rollback never resumes
|
|
185
189
|
* a stale interaction chain.
|
|
186
190
|
*/
|
|
187
191
|
rollback(cp: { messages: Message[]; signatures: string[] }, sessionId?: string): void {
|