agent-accelerator 0.5.8 → 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 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. 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). |
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. |
@@ -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 `AgentAccelProviderError` — one actionable line,
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
- Raw details (`statusCode`, `url`, truncated body) stay attached as
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. URL query strings are stripped (keys sometimes live there); headers
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.8",
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",
@@ -63,7 +63,7 @@ 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
69
  * Disposers for the manual user-signal forward listeners (fallback when
@@ -697,13 +697,15 @@ export class Agent {
697
697
  return out || undefined;
698
698
  }
699
699
 
700
- /** Clears conversation messages and provider thought signatures while keeping configuration. */
700
+ /** Starts a fresh conversation: clears messages, signatures, run marks and the run's saved tool results, keeps config and spending totals. */
701
701
  reset(): void {
702
702
  const sid = this.sessionId;
703
703
  this.context.messages = [];
704
704
  this.context.thoughtSignatures = [];
705
705
  this.context.systemPrompt = this.getFullInstructions();
706
706
  this.runMarks.length = 0;
707
+ this.toolRecords.length = 0; // a cleared conversation has no run to resume, so its finished tool results go too
708
+ this.activeMark = undefined;
707
709
  // Reset also drops session-scoped provider chaining/routing.
708
710
  try { clearInteractionChains(sid); } catch {}
709
711
  try { clearSessionRouting(sid); } catch {}
@@ -837,7 +839,7 @@ export class Agent {
837
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.
838
840
  private assertPromptValid(prompt: string | ContentPart[]): void {
839
841
  if (Array.isArray(prompt) && prompt.length === 0) {
840
- throw new Error(
842
+ throw new ValidationError(
841
843
  "[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
842
844
  );
843
845
  }
@@ -1693,7 +1695,7 @@ export class Agent {
1693
1695
  // Prompt validation throws synchronously in both run() and
1694
1696
  // stream() (same timing; documented here).
1695
1697
  if (Array.isArray(prompt) && prompt.length === 0) {
1696
- throw new Error(
1698
+ throw new ValidationError(
1697
1699
  "[Agent Accelerator] Prompt cannot be empty: pass a non-empty string or at least one content part."
1698
1700
  );
1699
1701
  }
@@ -2254,7 +2256,7 @@ export class Agent {
2254
2256
  /** What a failed run attempt got done before it failed (the `partial` the loop attaches to its error). */
2255
2257
  type FailedAttempt = Partial<AgentResponse> & { usage: TokenUsage };
2256
2258
 
2257
- // 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.
2258
2260
  function withFailedAttempts(res: AgentResponse, failed: FailedAttempt[]): AgentResponse {
2259
2261
  const usage: TokenUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
2260
2262
  for (const f of failed) accumulateUsage(usage, f.usage);
@@ -2266,7 +2268,7 @@ function withFailedAttempts(res: AgentResponse, failed: FailedAttempt[]): AgentR
2266
2268
  thoughtSignature: res.thoughtSignature,
2267
2269
  toolCalls: [...failed.flatMap((f) => f.toolCalls ?? []), ...res.toolCalls],
2268
2270
  toolResults: [...failed.flatMap((f) => f.toolResults ?? []), ...res.toolResults],
2269
- subagents: res.subagents,
2271
+ subagents: [...failed.flatMap((f) => f.subagents ?? []), ...res.subagents],
2270
2272
  usage,
2271
2273
  responseId: res.responseId,
2272
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/loop.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  import { getModelFromCatalog, ensureModelCatalogFresh, validateModelThinking } from "../models/catalog.ts";
24
24
  import { enforceFileAccess } from "../utils/safe-io.ts";
25
25
  import { isSubagentResult, normalizeSubagentMetadata } from "../tools/subagent-result.ts";
26
- import { BudgetExceededError } from "../types/errors.ts";
26
+ import { BudgetExceededError, TimeoutError } from "../types/errors.ts";
27
27
  import { applyCaptureRaw, type CaptureRawMode } from "../utils/raw-capture.ts";
28
28
  import { previewPrompt } from "../utils/session.ts";
29
29
  import { noteProviderTurn, normalizeStructuredOutput, parseStructuredOutput } from "../providers.ts";
@@ -389,7 +389,7 @@ function checkTokenLimit(usage: TokenUsage, maxTotalTokens: number): void {
389
389
  // Throws when the run has been going longer than limits.deadlineMs; in: both loops each turn, out: nothing or a thrown error.
390
390
  function checkDeadline(startTime: number, deadlineMs: number): void {
391
391
  if (Number.isFinite(deadlineMs) && deadlineMs !== Infinity && Date.now() - startTime > deadlineMs) {
392
- throw new Error(`[Agent Accelerator] Run deadline exceeded after ${deadlineMs}ms.`);
392
+ throw new BudgetExceededError(`[Agent Accelerator] Run deadline exceeded after ${deadlineMs}ms.`);
393
393
  }
394
394
  }
395
395
 
@@ -400,9 +400,7 @@ async function generateWithTimeout<T>(task: () => Promise<T>, timeoutMs: number)
400
400
  try {
401
401
  const timeoutP = new Promise<never>((_, reject) => {
402
402
  timer = setTimeout(() => {
403
- const e = new Error(`[Agent Accelerator] Request timed out after ${timeoutMs}ms.`);
404
- e.name = "TimeoutError";
405
- reject(e);
403
+ reject(new TimeoutError(`[Agent Accelerator] Request timed out after ${timeoutMs}ms.`));
406
404
  }, timeoutMs);
407
405
  try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
408
406
  });
@@ -859,7 +857,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
859
857
  allToolCalls.push(...genResult.toolCalls);
860
858
  // Per-run tool-call budget.
861
859
  if (allToolCalls.length > limits.maxToolCallsPerRun) {
862
- throw new Error(
860
+ throw new BudgetExceededError(
863
861
  `[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${limits.maxToolCallsPerRun}).`
864
862
  );
865
863
  }
@@ -998,6 +996,7 @@ export async function runAgentLoop(config: AgentLoopConfig): Promise<AgentRespon
998
996
  turns,
999
997
  toolCalls: allToolCalls,
1000
998
  toolResults: allToolResults,
999
+ subagents: allSubagents,
1001
1000
  text: finalResult?.text,
1002
1001
  };
1003
1002
  } catch {
@@ -1191,11 +1190,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1191
1190
  Number.isFinite(waitMs) && waitMs !== Infinity && waitMs > 0
1192
1191
  ? new Promise<never>((_, reject) => {
1193
1192
  timer = setTimeout(() => {
1194
- const e = new Error(
1195
- `[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`
1196
- );
1197
- e.name = "TimeoutError";
1198
- reject(e);
1193
+ reject(new TimeoutError(`[Agent Accelerator] Stream ${first ? "first-byte" : "idle"} timed out after ${waitMs}ms.`));
1199
1194
  }, waitMs);
1200
1195
  try { (timer as unknown as { unref?: () => void }).unref?.(); } catch {}
1201
1196
  })
@@ -1311,7 +1306,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1311
1306
  emitProgress(config); // the calls are on record BEFORE they run, so a crash can finish them instead of forgetting them
1312
1307
  allToolCalls.push(...turnResponse.toolCalls);
1313
1308
  if (allToolCalls.length > streamLimits.maxToolCallsPerRun) {
1314
- throw new Error(
1309
+ throw new BudgetExceededError(
1315
1310
  `[Agent Accelerator] maxToolCallsPerRun exceeded (${allToolCalls.length} > ${streamLimits.maxToolCallsPerRun}).`
1316
1311
  );
1317
1312
  }
@@ -1444,6 +1439,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1444
1439
  turns,
1445
1440
  toolCalls: allToolCalls,
1446
1441
  toolResults: allToolResults,
1442
+ subagents: allSubagents,
1447
1443
  text: lastResponse?.text,
1448
1444
  }; } catch {}
1449
1445
  outerStream.fail(raw);
@@ -1504,6 +1500,7 @@ export function streamAgentLoop(config: AgentLoopConfig): AssistantMessageEventS
1504
1500
  turns,
1505
1501
  toolCalls: allToolCalls,
1506
1502
  toolResults: allToolResults,
1503
+ subagents: allSubagents,
1507
1504
  text: lastResponse?.text,
1508
1505
  };
1509
1506
  // Aborts always surface as AbortError with partial attached.
@@ -5,6 +5,7 @@ import type { Agent } from "./agent.ts";
5
5
  import { normalizeToolName } from "../tools/executor.ts";
6
6
  import { getModel, getSubModel } from "../utils/env.ts";
7
7
  import { resolveModel } from "../providers/registry.ts";
8
+ import { ConfigError } from "../types/errors.ts";
8
9
 
9
10
  /** Minimal live-trace shape shared by fixed + dynamic workers (structural subset of `SubAgentTrace`). */
10
11
  export interface LiveWorkerTrace {
@@ -134,7 +135,7 @@ export function resolveDynamicModelInput(
134
135
  export function resolveSubAgentModel(config: { name?: string; model?: AgentConfig["model"] }): NonNullable<AgentConfig["model"]> {
135
136
  const resolvedModel = config.model ?? getSubModel() ?? getModel();
136
137
  if (!resolvedModel) {
137
- throw new Error(
138
+ throw new ConfigError(
138
139
  `[Agent Accelerator] SubAgent '${config.name || "subagent"}' requires a model. ` +
139
140
  `Specify 'model' in SubAgent config, or set SUB_AGENT_MODEL or MODEL in environment variables.`
140
141
  );
@@ -5,7 +5,7 @@ The model catalog: context limits, prices, modalities and reasoning options come
5
5
  | File | What it is |
6
6
  | :-- | :-- |
7
7
  | `catalog-cache.ts` | Downloads and caches `models.dev/api.json` in `<home>/catalog/` (or `AGENT_CACHE_DIR`). Stale-while-revalidate: an expired catalog is served at once and refreshed in the background (8 s timeout, 10 min backoff after a failure); only a cold start waits. `AGENT_CATALOG_REFRESH=off` stops automatic refreshes. |
8
- | `catalog.ts` | Lookups (`getModelFromCatalog`), `registerModel`, thinking validation (`validateModelThinking`), and the exported `GOOGLE_/OPENAI_/OPENROUTER_MODELS` arrays (rebuilt in place when the catalog changes). |
8
+ | `catalog.ts` | Lookups (`getModelFromCatalog`), `registerModel`, thinking validation (`validateModelThinking`, which throws `ThinkingLevelError`, a one-line `ConfigError`), and the exported `GOOGLE_/OPENAI_/OPENROUTER_MODELS` arrays (rebuilt in place when the catalog changes). |
9
9
 
10
10
  ## Invariants
11
11
  - Never hand-edit the cache file; register custom models with `registerModel()`. Verified modality fixes go in `MODALITY_OVERRIDES`.
@@ -19,6 +19,7 @@ import {
19
19
  } from "./catalog-cache.ts";
20
20
  import type { ModelSpec, ProviderId, ModelLimit, ModelCost, ModelModalities } from "../types/model.ts";
21
21
  import type { ThinkingLevel } from "../types/core.ts";
22
+ import { ConfigError } from "../types/errors.ts";
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Provider aliases — maps our internal ids to models.dev keys
@@ -421,8 +422,8 @@ export function getModelThinkingInfo(provider: string, modelId: string): ModelTh
421
422
  };
422
423
  }
423
424
 
424
- /** Thrown when a requested ThinkingLevel is unsupported by the selected model. */
425
- export class ThinkingLevelError extends Error {
425
+ /** Thrown when a requested ThinkingLevel is unsupported by the selected model: a `ConfigError` (one line, never retried) with `allowedLevels`. */
426
+ export class ThinkingLevelError extends ConfigError {
426
427
  readonly provider: string;
427
428
  readonly modelId: string;
428
429
  readonly requestedLevel: string;
@@ -443,31 +444,18 @@ export class ThinkingLevelError extends Error {
443
444
  reason: string;
444
445
  remedy?: string;
445
446
  }) {
446
- const remedyLines = opts.remedy
447
- ? `\n\n How to fix:\n ${opts.remedy}`
448
- : "";
449
- const allowedLine = opts.allowedLevels.length > 0
450
- ? `\n Allowed Levels: [${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}]`
451
- : "";
452
-
453
- const formattedMessage =
454
- `[Agent Accelerator] ThinkingLevel Mismatch for "${opts.provider}/${opts.modelId}":\n` +
455
- ` Requested Level: "${opts.requestedLevel}"\n` +
456
- ` Issue: ${opts.reason}` +
457
- allowedLine +
458
- remedyLines;
459
-
460
- super(formattedMessage);
447
+ const allowed = opts.allowedLevels.length > 0 ? ` Allowed levels: ${opts.allowedLevels.map((l) => `"${l}"`).join(", ")}.` : "";
448
+ const remedy = opts.remedy ? ` How to fix: ${opts.remedy}` : "";
449
+ super(
450
+ `[Agent Accelerator] Thinking level "${opts.requestedLevel}" does not fit "${opts.provider}/${opts.modelId}": ${opts.reason}${allowed}${remedy}`,
451
+ { provider: opts.provider, model: opts.modelId }
452
+ );
461
453
  this.name = "ThinkingLevelError";
462
454
  this.provider = opts.provider;
463
455
  this.modelId = opts.modelId;
464
456
  this.requestedLevel = opts.requestedLevel;
465
457
  this.allowedLevels = opts.allowedLevels;
466
458
  this.supportsThinking = opts.supportsThinking;
467
-
468
- if (Error.captureStackTrace) {
469
- Error.captureStackTrace(this, ThinkingLevelError);
470
- }
471
459
  }
472
460
  }
473
461
 
@@ -383,7 +383,7 @@ function cleanModelId(model: string | ModelSpec): string {
383
383
  function assertSupportedGeneration(clean: string): void {
384
384
  const match = clean.match(/^gemini-(\d+)(?:[.\-]|$)/i);
385
385
  if (match && Number(match[1]) < 3) {
386
- throw new Error(
386
+ throw new ConfigError(
387
387
  `[Agent Accelerator] [google/${clean}] Unsupported generation: Gemini ${match[1]}.x and older are strictly rejected by this SDK. ` +
388
388
  `Use a Gemini 3 or later model for full support and feature coverage.`
389
389
  );
@@ -1022,7 +1022,7 @@ export class GoogleInteractionsProvider implements Provider {
1022
1022
  assertSupportedGeneration(clean);
1023
1023
  const apiKey = resolveApiKey(options);
1024
1024
  if (!apiKey) {
1025
- throw new Error(
1025
+ throw new ConfigError(
1026
1026
  "[Agent Accelerator] Missing Google API key. Set GEMINI_API_KEY (or GOOGLE_API_KEY) or pass apiKey."
1027
1027
  );
1028
1028
  }
@@ -1133,7 +1133,7 @@ export class GoogleInteractionsProvider implements Provider {
1133
1133
  assertSupportedGeneration(clean);
1134
1134
  const apiKey = resolveApiKey(options);
1135
1135
  if (!apiKey) {
1136
- throw new Error(
1136
+ throw new ConfigError(
1137
1137
  "[Agent Accelerator] Missing Google API key. Set GEMINI_API_KEY (or GOOGLE_API_KEY) or pass apiKey."
1138
1138
  );
1139
1139
  }
@@ -59,7 +59,7 @@ import { normalizeMediaInput } from "../utils/media.ts";
59
59
  import { safeStringify } from "../utils/serialization.ts";
60
60
  import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
61
61
  import { withRetries } from "../utils/retry.ts";
62
- import { ConfigError } from "../types/errors.ts";
62
+ import { ConfigError, ValidationError } from "../types/errors.ts";
63
63
  import {
64
64
  parseArguments,
65
65
  normalizeThinkingParts,
@@ -350,13 +350,7 @@ function assertNoVideoOrFileParts(
350
350
  kind === "video"
351
351
  ? `[${prefix}/${modelId}] unsupported video input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a video-capable model (e.g. google/gemini-*) or drop the video part.`
352
352
  : `[${prefix}/${modelId}] unsupported file input (generic OpenAI-compatible endpoints carry text/image/audio only). Use a file-capable model (e.g. google/gemini-*) or convert the document to text before sending it.`;
353
- const err = new Error(message);
354
- err.name = "AgentAccelProviderError";
355
- Object.defineProperties(err, {
356
- provider: { value: prefix, enumerable: false },
357
- model: { value: modelId, enumerable: false },
358
- });
359
- throw err;
353
+ throw new ValidationError(message, { provider: prefix, model: modelId });
360
354
  }
361
355
 
362
356
  // Text, image (link or data URL) and audio parts as Chat content blocks; in: a user message's parts, out: fullHistoryMessages.
@@ -701,7 +695,7 @@ export class OpenAICompatibleChatProvider implements Provider {
701
695
  private requireApiKey(baseUrl: string, options?: ProviderRequestOptions): string | undefined {
702
696
  const key = this.apiKey(options);
703
697
  if (!key && !isLocalEndpoint(baseUrl)) {
704
- throw new Error(
698
+ throw new ConfigError(
705
699
  `[Agent Accelerator] Missing API key for "${this.prefix}". Set ${envPrefixOf(this.prefix)}_API_KEY or pass apiKey. (Local endpoints may omit the key.)`
706
700
  );
707
701
  }
@@ -51,6 +51,7 @@ import { clampCacheKey } from "../utils/cache.ts";
51
51
  import { normalizeMediaInput } from "../utils/media.ts";
52
52
  import { safeStringify } from "../utils/serialization.ts";
53
53
  import { toConciseProviderError, assertModalitiesSupported, assertNoVideoPartsOnResponses } from "../utils/errors.ts";
54
+ import { ConfigError } from "../types/errors.ts";
54
55
  import { withRetries } from "../utils/retry.ts";
55
56
  import {
56
57
  parseArguments,
@@ -716,7 +717,7 @@ export class OpenAIResponsesProvider implements Provider {
716
717
  const clean = cleanModelId(model);
717
718
  const apiKey = resolveApiKey(options);
718
719
  if (!apiKey) {
719
- throw new Error(
720
+ throw new ConfigError(
720
721
  "[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
721
722
  );
722
723
  }
@@ -804,7 +805,7 @@ export class OpenAIResponsesProvider implements Provider {
804
805
  try {
805
806
  const apiKey = resolveApiKey(options);
806
807
  if (!apiKey) {
807
- throw new Error(
808
+ throw new ConfigError(
808
809
  "[Agent Accelerator] Missing OpenAI API key. Set OPENAI_API_KEY (or OPENAI_BASE_API_KEY) or pass apiKey."
809
810
  );
810
811
  }
@@ -46,7 +46,7 @@ import { buildSessionHeaders, setHeaderCaseInsensitive } from "../utils/headers.
46
46
  import { normalizeMediaInput } from "../utils/media.ts";
47
47
  import { safeStringify } from "../utils/serialization.ts";
48
48
  import { toConciseProviderError, assertModalitiesSupported } from "../utils/errors.ts";
49
- import { isAbortError } from "../types/errors.ts";
49
+ import { ConfigError, isAbortError } from "../types/errors.ts";
50
50
  import {
51
51
  newToolCallId,
52
52
  redactedHeaders,
@@ -648,7 +648,7 @@ export class OpenRouterChatCompletionsProvider implements Provider {
648
648
  try {
649
649
  const apiKey = resolveApiKey(options);
650
650
  if (!apiKey) {
651
- throw new Error(
651
+ throw new ConfigError(
652
652
  "[Agent Accelerator] Missing OpenRouter API key. Set OPENROUTER_API_KEY or pass apiKey."
653
653
  );
654
654
  }
@@ -1,3 +1,4 @@
1
+ import { ConfigError } from "../types/errors.ts";
1
2
  import type { Provider, ProviderId, ModelSpec } from "../types/model.ts";
2
3
  import type { ThinkingLevel } from "../types/core.ts";
3
4
  import { OpenAICompatibleChatProvider } from "./openai-compat.ts";
@@ -54,7 +55,7 @@ function removedProviderMessage(id: string): string {
54
55
  /** Throws for removed provider prefixes. */
55
56
  function assertProviderNotRemoved(id: string): void {
56
57
  if (REMOVED_PROVIDERS.has(id.trim().toLowerCase())) {
57
- throw new Error(removedProviderMessage(id.trim()));
58
+ throw new ConfigError(removedProviderMessage(id.trim()));
58
59
  }
59
60
  }
60
61
 
@@ -83,7 +84,7 @@ export function ensureCustomProvider(
83
84
  const id = normalizeProviderPrefix(prefix) as ProviderId;
84
85
  if (FIRST_CLASS.has(id.toLowerCase())) {
85
86
  if (opts?.baseUrl !== undefined || opts?.apiKey !== undefined) {
86
- throw new Error(
87
+ throw new ConfigError(
87
88
  `[Agent Accelerator] Provider "${id}" is first-class and cannot be replaced with custom baseUrl/apiKey. Pass baseUrl/apiKey per-Agent instead.`
88
89
  );
89
90
  }
@@ -93,7 +94,7 @@ export function ensureCustomProvider(
93
94
  if (existing) {
94
95
  const prevBase = (existing as { configuredBaseUrl?: string }).configuredBaseUrl;
95
96
  if (opts?.baseUrl !== undefined && prevBase !== undefined && prevBase !== opts.baseUrl) {
96
- throw new Error(
97
+ throw new ConfigError(
97
98
  `[Agent Accelerator] Conflicting re-registration for provider "${id}". Registry is immutable after creation.`
98
99
  );
99
100
  }
@@ -120,7 +121,7 @@ export function getProvider(id: ProviderId | string): Provider {
120
121
  const provider = providerRegistry.get(key);
121
122
  if (provider) return provider;
122
123
  if (!key) {
123
- throw new Error(
124
+ throw new ConfigError(
124
125
  `Provider '' is not supported or not registered. Available first-support providers: google, openrouter, openai — or any custom prefix via {PREFIX}_API_KEY + {PREFIX}_BASE_URL.`
125
126
  );
126
127
  }
@@ -146,7 +147,7 @@ export interface ResolvedModel {
146
147
  */
147
148
  export function resolveModel(model: string | ModelSpec): ResolvedModel {
148
149
  if (!model || (typeof model === "string" && !model.trim())) {
149
- throw new Error(
150
+ throw new ConfigError(
150
151
  "[Agent Accelerator] No model specified. Please specify 'model' in Agent configuration or set the MODEL environment variable."
151
152
  );
152
153
  }
@@ -78,7 +78,7 @@ export function readErrorPayload(
78
78
  }
79
79
  }
80
80
 
81
- /** Retryable incomplete-stream error for truncated SSE. */
81
+ /** Retryable incomplete-stream error for truncated SSE: a `ProviderError` whose `code` is `incomplete_stream`. */
82
82
  export function incompleteStreamError(provider: string, modelId: string): Error {
83
83
  const err = toConciseProviderError(
84
84
  Object.assign(new Error("Stream ended without terminal event"), {
@@ -110,12 +110,8 @@ export function isNotFoundChainError(error: unknown): boolean {
110
110
  NaN
111
111
  );
112
112
  if (status === 404) return true;
113
- const code = String(
114
- (err["code"] as string | undefined) ??
115
- (err["errorType"] as string | undefined) ??
116
- ""
117
- ).toLowerCase();
118
- return code === "not_found";
113
+ // `providerCode` on a wrapped ProviderError (its `code` is the SDK kind), `code` on a raw error.
114
+ return [err["providerCode"], err["code"], err["errorType"]].some((c) => String(c ?? "").toLowerCase() === "not_found");
119
115
  }
120
116
 
121
117
  /** Per-request timeout ms from provider options. */
@@ -4,18 +4,18 @@ Scheduled actions: a worker that runs later (once or repeating) and saves its an
4
4
 
5
5
  | File | What it is |
6
6
  | :-- | :-- |
7
- | `scheduler.ts` | One `Scheduler` per home folder: a 1 s unref'd clock that runs only while something is pending, startup sweep, cook/deliver state machine, and the public functions (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`). |
7
+ | `scheduler.ts` | One `Scheduler` per home folder: a 1 s unref'd clock that runs only while something is pending, startup sweep, dead-owner recovery, heartbeats, cook/deliver state machine, and the public functions (`createScheduledAction`, `list/update/run/skip/deleteScheduledAction`). |
8
8
  | `store.ts` | Disk layer under `<home>/scheduled/`: `<id>/task.json`, `<id>/delivery.json`, run files, one short mutex per action; validation of records. |
9
9
  | `time.ts` | Pure `DD:HH:MM` and local-clock maths. No timezone, DST or `Intl`; `now` is always passed in. |
10
10
  | `tool.ts` + `scheduled-actions.md` | The model-facing `scheduled_actions` tool and `get_datetime`; the `.md` is the tool description. |
11
11
 
12
12
  ## Invariants
13
13
  - The files are the truth: every write re-reads first and goes through the durable writer. The file format is a stable public contract: see `docs/scheduled-action-files.md` and add a changelog row for any change.
14
- - Write order when a run ends: run file, then `<id>/delivery.json`, then `<id>/task.json`. The startup sweep repairs a crash in between.
14
+ - Write order when a run ends: run file, then `<id>/delivery.json`, then `<id>/task.json`. A crash in between is repaired by the dead-owner recovery (`recoverCooking`, at start and every 15 s): the delivery is rebuilt from the run file, or the run is marked `missed` when no run file exists.
15
15
  - `startCook` advances the next times BEFORE the run starts; a run still cooking when the next is due is skipped, never stacked.
16
16
  - Runs are fresh stateless workers built by `buildDynamicWorkerConfig`; their spending guardrail is `workerBudget` only.
17
17
  - The Agent registry holds `WeakRef`s by name: keep every Agent that owns actions referenced, or its runs become `missed`.
18
- - Several processes may share a home (the file store is safe for it; the home lock is lifted in a later change of this release). Ownership: a process only starts, delivers or recovers actions whose Agent it registered. An action nobody here owns is left alone until its delivery time passed with no run started, then marked `missed` (`no_agent`).
18
+ - Several processes may share a home: scheduled actions take no home lock, only the short per-action mutex (`<id>/.mutex`); the one-process lock is on session folders only. Ownership: a process only starts, delivers or recovers actions whose Agent it registered. An action nobody here owns is left alone until its delivery time passed with no run started, then marked `missed` (`no_agent`).
19
19
  - Exactly once: a scheduled start re-checks under the action's mutex that it is still `waiting` and due; `markMissed` only changes the state the caller saw; a delivery is emitted only by the process that removed its entry from `delivery.json`.
20
20
  - A run in flight keeps `<id>/.heartbeat` (pid, host, token; mtime refreshed every 5 s, stale after 60 s), written before the record shows `cooking`. A cooking action whose owner is dead (pid gone, stale, or none) is recovered by a live process that owns its Agent, at start and every 15 s. `delete` of a run another live process is cooking is refused, even with `force`.
21
21
  - Times come from `Date`, local clock only. Tests inject `scheduler.now` / `tickMs` and call `tick()`.
@@ -12,8 +12,8 @@ Small shared helpers. Each file has one job.
12
12
  | `safe-io.ts` | `fileAccess.roots` containment and the guarded remote fetch (private hosts refused, redirects re-checked, size cap). |
13
13
  | `raw-capture.ts` | The `captureRaw` policy for `AgentResponse.raw`. |
14
14
  | `cache.ts` | Cache-key clamp (64 chars) and the central secret redaction. |
15
- | `errors.ts` | `toConciseProviderError` and pre-network modality guards. |
16
- | `retry.ts` | `withRetries`, `isTransientError`, `isRetryableRun`, `sleep`. Structured data only (status, code, flags), never message text. |
15
+ | `errors.ts` | `toConciseProviderError` (any raw provider failure becomes one-line `ProviderError`: `code` `"provider"`, `retryable` from status/code, the provider's code as `providerCode`; SDK errors pass through), `isTransientError`, and the pre-network modality guards (`ValidationError`). |
16
+ | `retry.ts` | `withRetries`, `isTransientError` (re-exported from `errors.ts`), `isRetryableRun`, `sleep`. Structured data only (status, code, flags), never message text. |
17
17
  | `session.ts` | Id helpers (`createSessionId`, `hashSessionPart`, `createTrackingId`, run/turn ids), `parseModelRef`, `previewPrompt`. |
18
18
  | `serialization.ts` | `toJsonSafe`, `safeStringify`, `escapeXml`. |
19
19
  | `base64.ts`, `thought-signature.ts` | Runtime-agnostic base64; Gemini thought-signature helpers. |
@@ -1,5 +1,32 @@
1
1
  import type { ProviderContext } from "../types/message.ts";
2
2
  import { getModelFromCatalog } from "../models/catalog.ts";
3
+ import { AgentAccelError, ProviderError, ValidationError, isAbortError } from "../types/errors.ts";
4
+
5
+ /**
6
+ * Checks whether a provider error is transient and safe to retry.
7
+ * Structured-data only: status / code / transient flag. Never message text,
8
+ * so a 400 mentioning "512 tokens" is not retried.
9
+ */
10
+ export function isTransientError(error: unknown): boolean {
11
+ if (!error) return false;
12
+ if (isAbortError(error)) return false;
13
+ const err = error as Record<string, unknown>;
14
+ if (err["transient"] === true) return true;
15
+ if ((err as { retryable?: unknown }).retryable === true) return true;
16
+ const status = Number(
17
+ (err["status"] as number | undefined) ??
18
+ (err["statusCode"] as number | undefined) ??
19
+ ((err["response"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
20
+ ((err["lastError"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
21
+ NaN
22
+ );
23
+ if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
24
+ const code = String((err["providerCode"] as string | undefined) ?? (err["code"] as string | undefined) ?? "").toUpperCase();
25
+ if (["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "EPIPE", "ETIMEDOUT"].includes(code)) {
26
+ return true;
27
+ }
28
+ return false;
29
+ }
3
30
 
4
31
  const CONCISE = "__agentAccelConcise";
5
32
 
@@ -49,12 +76,12 @@ function displayModel(providerId: string, modelId: string): string {
49
76
  }
50
77
 
51
78
  /**
52
- * Collapses a raw provider error (which carries the full request/response
53
- * dump as enumerable props) into a one-line actionable error. Raw details stay
54
- * available but non-enumerable, so runtime dumps stay small.
79
+ * Collapses a raw provider error (which carries the full request/response dump as enumerable props) into a one-line
80
+ * `ProviderError` with `retryable` set from its status/code; the provider's own error code is `providerCode`. Raw
81
+ * details stay available but non-enumerable, so runtime dumps stay small. An SDK error (already typed) passes through.
55
82
  */
56
83
  export function toConciseProviderError(error: unknown, providerId: string, modelId: string): Error {
57
- if (error instanceof Error && (error as any)[CONCISE]) return error;
84
+ if (error instanceof AgentAccelError || (error instanceof Error && (error as any)[CONCISE])) return error;
58
85
  const err = error as any;
59
86
  const status = readStatus(err);
60
87
  const url = readUrl(err);
@@ -70,10 +97,10 @@ export function toConciseProviderError(error: unknown, providerId: string, model
70
97
  if (rawMeta && !detail.includes(rawMeta.slice(0, 80))) {
71
98
  detail = `${detail}\n${rawMeta.slice(0, 500)}`.slice(0, 800);
72
99
  }
73
- const concise = new Error(
74
- `[${displayModel(providerId, modelId)}] request failed${status ? ` (${status})` : ""}: ${detail}`
100
+ const concise = new ProviderError(
101
+ `[${displayModel(providerId, modelId)}] request failed${status ? ` (${status})` : ""}: ${detail}`,
102
+ { status, provider: providerId, model: modelId, retryable: isTransientError(err) }
75
103
  );
76
- concise.name = "AgentAccelProviderError";
77
104
  Object.defineProperties(concise, {
78
105
  [CONCISE]: { value: true, enumerable: false },
79
106
  statusCode: { value: status, enumerable: false },
@@ -90,11 +117,10 @@ export function toConciseProviderError(error: unknown, providerId: string, model
90
117
  ...(typeof err?.errorType === "string"
91
118
  ? { errorType: { value: err.errorType, enumerable: false } }
92
119
  : {}),
93
- // Forwarded when present (e.g. Google's `not_found` chain-eviction code,
94
- // which carries no HTTP status on SSE error frames). Survives re-wraps
95
- // so chain-expiry detectors never lose it.
120
+ // The provider's own error code (e.g. Google's `not_found` chain-eviction code, which carries no HTTP status on
121
+ // SSE error frames); `code` itself is the SDK kind (`"provider"`). Survives re-wraps so chain detectors never lose it.
96
122
  ...((typeof err?.code === "string" || typeof err?.code === "number") && err.code !== ""
97
- ? { code: { value: err.code, enumerable: false } }
123
+ ? { providerCode: { value: err.code, enumerable: false } }
98
124
  : {}),
99
125
  // Server rate-limit hint in ms (from Retry-After); survives re-wraps so
100
126
  // `withRetries` can honor it instead of hammering a 429.
@@ -120,7 +146,7 @@ const PART_TO_MODALITY: Record<string, "image" | "audio" | "video" | "pdf"> = {
120
146
  * lacks a requested input modality (image/audio/video/document; files are
121
147
  * not checked on OpenRouter, which parses them for any model). Unknown
122
148
  * models (custom providers, dynamic routers) are skipped: the provider
123
- * verdict stands and surfaces via toConciseProviderError instead.
149
+ * verdict stands and surfaces via toConciseProviderError instead. Throws a `ValidationError`.
124
150
  */
125
151
  export function assertModalitiesSupported(
126
152
  context: ProviderContext,
@@ -147,16 +173,10 @@ export function assertModalitiesSupported(
147
173
  if (!supported) return;
148
174
  const missing = [...needed].filter((k) => !supported.includes(k));
149
175
  if (missing.length === 0) return;
150
- const err = new Error(
151
- `[${displayModel(providerId, modelId)}] unsupported ${missing.join("+")} input (supports: ${supported.join(", ") || "text"}). Use a capable model or drop the ${missing.join("+")} part.`
176
+ throw new ValidationError(
177
+ `[${displayModel(providerId, modelId)}] unsupported ${missing.join("+")} input (supports: ${supported.join(", ") || "text"}). Use a capable model or drop the ${missing.join("+")} part.`,
178
+ { provider: providerId, model: modelId }
152
179
  );
153
- err.name = "AgentAccelProviderError";
154
- Object.defineProperties(err, {
155
- [CONCISE]: { value: true, enumerable: false },
156
- provider: { value: providerId, enumerable: false },
157
- model: { value: modelId, enumerable: false },
158
- });
159
- throw err;
160
180
  }
161
181
 
162
182
  /**
@@ -169,7 +189,7 @@ export function assertModalitiesSupported(
169
189
  * `openrouter/stealth/space-bunny-alpha` under the old Responses skin despite
170
190
  * its catalog `video` flag, which is chat-transport oriented). Fail fast with
171
191
  * a clear one-liner instead; video stays Gemini-only, and OpenRouter Chat
172
- * Completions carries `video_url` natively (no guard there).
192
+ * Completions carries `video_url` natively (no guard there). Throws a `ValidationError`.
173
193
  */
174
194
  export function assertNoVideoPartsOnResponses(
175
195
  context: ProviderContext,
@@ -197,14 +217,8 @@ export function assertNoVideoPartsOnResponses(
197
217
  // contradict the rejection).
198
218
  if (known && known.length > 0) supported = known.filter((m) => m !== "video").join(", ") || "text";
199
219
  } catch {}
200
- const err = new Error(
201
- `[${displayModel(providerId, modelId)}] unsupported video input (the Responses API accepts text/image/file only; supports: ${supported}). Use a video-capable model (e.g. google/gemini-*) or drop the video part.`
220
+ throw new ValidationError(
221
+ `[${displayModel(providerId, modelId)}] unsupported video input (the Responses API accepts text/image/file only; supports: ${supported}). Use a video-capable model (e.g. google/gemini-*) or drop the video part.`,
222
+ { provider: providerId, model: modelId }
202
223
  );
203
- err.name = "AgentAccelProviderError";
204
- Object.defineProperties(err, {
205
- [CONCISE]: { value: true, enumerable: false },
206
- provider: { value: providerId, enumerable: false },
207
- model: { value: modelId, enumerable: false },
208
- });
209
- throw err;
210
224
  }
@@ -24,7 +24,7 @@ export function getPackageVersion(): string {
24
24
  const v = (globalThis as any).process?.env?.npm_package_version;
25
25
  if (typeof v === "string" && v) return v;
26
26
  } catch {}
27
- return "0.5.8";
27
+ return "0.5.9";
28
28
  }
29
29
 
30
30
  // User-Agent string with version and platform; in: AGENT_ACCEL_USER_AGENT, out: a string.
@@ -1,33 +1,7 @@
1
- import { toConciseProviderError } from "./errors.ts";
1
+ import { toConciseProviderError, isTransientError } from "./errors.ts";
2
2
  import { isAbortError } from "../types/errors.ts";
3
3
 
4
- export { isAbortError };
5
-
6
- /**
7
- * Checks whether a provider error is transient and safe to retry.
8
- * Structured-data only: status / code / transient flag. Never message text,
9
- * so a 400 mentioning "512 tokens" is not retried.
10
- */
11
- export function isTransientError(error: unknown): boolean {
12
- if (!error) return false;
13
- if (isAbortError(error)) return false;
14
- const err = error as Record<string, unknown>;
15
- if (err["transient"] === true) return true;
16
- if ((err as { retryable?: unknown }).retryable === true) return true;
17
- const status = Number(
18
- (err["status"] as number | undefined) ??
19
- (err["statusCode"] as number | undefined) ??
20
- ((err["response"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
21
- ((err["lastError"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
22
- NaN
23
- );
24
- if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
25
- const code = String((err["code"] as string | undefined) ?? "").toUpperCase();
26
- if (["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "EPIPE", "ETIMEDOUT"].includes(code)) {
27
- return true;
28
- }
29
- return false;
30
- }
4
+ export { isAbortError, isTransientError };
31
5
 
32
6
  /**
33
7
  * Whether a FAILED RUN (the provider layer already retried the call) is worth trying again: the failure is transient