agent-accelerator 0.5.9 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +3 -1
  2. package/examples/prompts/SYSTEM_PROMPT.md +2 -0
  3. package/examples/prompts/SYSTEM_PROMPT_AGENT.md +4 -0
  4. package/examples/prompts/SYSTEM_PROMPT_TOOLS.md +1 -0
  5. package/package.json +4 -1
  6. package/src/agent/agent.ts +366 -161
  7. package/src/agent/architecture.md +13 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +8 -3
  10. package/src/agent/delegation.ts +82 -29
  11. package/src/agent/loop.ts +444 -365
  12. package/src/agent/subagent.ts +1 -3
  13. package/src/agent/subagents-tool.ts +45 -7
  14. package/src/agent/subagents.md +3 -0
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/worker.ts +56 -27
  17. package/src/agent/workers.ts +113 -20
  18. package/src/architecture.md +16 -15
  19. package/src/index.ts +34 -10
  20. package/src/models/architecture.md +4 -3
  21. package/src/models/catalog-cache.ts +6 -5
  22. package/src/models/catalog.ts +39 -30
  23. package/src/providers/architecture.md +10 -8
  24. package/src/providers/google.ts +147 -59
  25. package/src/providers/openai-compat.ts +57 -49
  26. package/src/providers/openai.ts +54 -37
  27. package/src/providers/openrouter.ts +85 -30
  28. package/src/providers/registry.ts +6 -21
  29. package/src/providers/shared.ts +87 -33
  30. package/src/providers.ts +56 -115
  31. package/src/scheduler/architecture.md +7 -6
  32. package/src/scheduler/scheduled-actions.md +4 -0
  33. package/src/scheduler/scheduler.ts +186 -60
  34. package/src/scheduler/store.ts +23 -10
  35. package/src/scheduler/time.ts +17 -3
  36. package/src/scheduler/tool.ts +33 -7
  37. package/src/session/architecture.md +7 -6
  38. package/src/session/journal.ts +38 -12
  39. package/src/session/store.ts +122 -61
  40. package/src/session/usage.ts +58 -9
  41. package/src/streaming/architecture.md +4 -3
  42. package/src/streaming/event-stream.ts +26 -12
  43. package/src/telemetry.ts +6 -2
  44. package/src/tools/architecture.md +8 -7
  45. package/src/tools/executor.ts +50 -48
  46. package/src/tools/schema.ts +10 -6
  47. package/src/tools/subagent-result.ts +6 -1
  48. package/src/tools/tool.ts +9 -11
  49. package/src/types/agent.ts +2 -21
  50. package/src/types/architecture.md +11 -10
  51. package/src/types/core.ts +1 -9
  52. package/src/types/errors.ts +26 -4
  53. package/src/types/model.ts +0 -4
  54. package/src/types/tool.ts +12 -2
  55. package/src/update-models.ts +5 -5
  56. package/src/utils/architecture.md +16 -15
  57. package/src/utils/cache.ts +1 -3
  58. package/src/utils/durable.ts +17 -5
  59. package/src/utils/env.ts +9 -7
  60. package/src/utils/errors.ts +6 -14
  61. package/src/utils/headers.ts +3 -10
  62. package/src/utils/media.ts +23 -8
  63. package/src/utils/raw-capture.ts +9 -2
  64. package/src/utils/retry.ts +15 -3
  65. package/src/utils/safe-io.ts +29 -7
  66. package/src/utils/serialization.ts +1 -4
  67. package/src/utils/session.ts +1 -2
package/src/types/core.ts CHANGED
@@ -2,15 +2,7 @@
2
2
  * Level-based reasoning — single flag for all levels (bloatfree)
3
3
  */
4
4
  /** Public reasoning control accepted by AgentConfig. */
5
- export type ThinkingLevel =
6
- | "none"
7
- | "dynamic"
8
- | "minimal"
9
- | "low"
10
- | "medium"
11
- | "high"
12
- | "xhigh"
13
- | "max";
5
+ export type ThinkingLevel = "none" | "dynamic" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
14
6
 
15
7
  // Internal normalized config (SDK-internal, not exposed as Agent flag)
16
8
  /** Internal normalized reasoning configuration passed to providers. */
@@ -40,7 +40,11 @@ export class AgentAccelError extends Error {
40
40
  readonly code: AgentAccelErrorCode;
41
41
  readonly retryable: boolean;
42
42
  readonly context: AgentAccelErrorContext;
43
- constructor(code: AgentAccelErrorCode, message: string, opts?: { retryable?: boolean; context?: AgentAccelErrorContext; cause?: unknown }) {
43
+ constructor(
44
+ code: AgentAccelErrorCode,
45
+ message: string,
46
+ opts?: { retryable?: boolean; context?: AgentAccelErrorContext; cause?: unknown }
47
+ ) {
44
48
  super(message);
45
49
  this.name = "AgentAccelError";
46
50
  this.code = code;
@@ -59,10 +63,26 @@ export class ConfigError extends AgentAccelError {
59
63
 
60
64
  export class ProviderError extends AgentAccelError {
61
65
  readonly status?: number;
62
- constructor(message: string, opts?: { status?: number; provider?: string; model?: string; requestId?: string; retryable?: boolean; context?: AgentAccelErrorContext }) {
66
+ constructor(
67
+ message: string,
68
+ opts?: {
69
+ status?: number;
70
+ provider?: string;
71
+ model?: string;
72
+ requestId?: string;
73
+ retryable?: boolean;
74
+ context?: AgentAccelErrorContext;
75
+ }
76
+ ) {
63
77
  super("provider", message, {
64
78
  retryable: opts?.retryable ?? false,
65
- context: { ...opts?.context, provider: opts?.provider, model: opts?.model, status: opts?.status, requestId: opts?.requestId },
79
+ context: {
80
+ ...opts?.context,
81
+ provider: opts?.provider,
82
+ model: opts?.model,
83
+ status: opts?.status,
84
+ requestId: opts?.requestId,
85
+ },
66
86
  });
67
87
  this.name = "ProviderError";
68
88
  this.status = opts?.status;
@@ -124,7 +144,9 @@ export function isAbortError(error: unknown): boolean {
124
144
  /** Format without ANSI for CLI pretty-printing. */
125
145
  export function formatErrorPlain(err: unknown): string {
126
146
  if (err instanceof AgentAccelError) {
127
- const ctx = err.context?.provider ? ` [${err.context.provider}${err.context.model ? `/${err.context.model}` : ""}]` : "";
147
+ const ctx = err.context?.provider
148
+ ? ` [${err.context.provider}${err.context.model ? `/${err.context.model}` : ""}]`
149
+ : "";
128
150
  const status = err.context?.status ? ` (${err.context.status})` : "";
129
151
  return `[${err.code}]${ctx}${status}: ${err.message}`;
130
152
  }
@@ -185,10 +185,6 @@ export interface ProviderRequestOptions {
185
185
  googleTier?: "free" | "paid";
186
186
  /** Upper bound on generated tokens for one model call. */
187
187
  maxOutputTokens?: number;
188
- /** Stop sequence(s). Chat Completions only; other wires warn+drop. */
189
- stop?: string[];
190
- /** Deterministic-sampling seed. Chat Completions only; other wires warn+drop. */
191
- seed?: number;
192
188
  /** `fetch` implementation used for the provider request (defaults to global `fetch`). */
193
189
  fetch?: typeof fetch;
194
190
  /** Base backoff delay in ms for provider retries (default 250). */
package/src/types/tool.ts CHANGED
@@ -21,7 +21,13 @@ export interface ToolExecutionContext {
21
21
  /** Session identifier associated with this invocation. */
22
22
  sessionId?: string;
23
23
  /** Forwards live sub-agent deltas (used by the `subagents` tool for realtime tracking). */
24
- onSubagentEvent?: (event: { trackingId: string; delta?: string; thinkingDelta?: string; partialText?: string; partialThinking?: string }) => void;
24
+ onSubagentEvent?: (event: {
25
+ trackingId: string;
26
+ delta?: string;
27
+ thinkingDelta?: string;
28
+ partialText?: string;
29
+ partialThinking?: string;
30
+ }) => void;
25
31
  // Tool API v2 context additions (all optional, additive).
26
32
  /** Stable id for this run (`newRunId`). Mirrors SubAgentRunIds.runId. */
27
33
  runId?: string;
@@ -161,5 +167,9 @@ export const TRUNCATED_ARGS_KEY = "__truncated_raw";
161
167
  export function isTruncatedArguments(args: unknown): boolean {
162
168
  if (!args || typeof args !== "object" || Array.isArray(args)) return false;
163
169
  const keys = Object.keys(args as Record<string, unknown>);
164
- return keys.length === 1 && keys[0] === TRUNCATED_ARGS_KEY && typeof (args as Record<string, unknown>)[TRUNCATED_ARGS_KEY] === "string";
170
+ return (
171
+ keys.length === 1 &&
172
+ keys[0] === TRUNCATED_ARGS_KEY &&
173
+ typeof (args as Record<string, unknown>)[TRUNCATED_ARGS_KEY] === "string"
174
+ );
165
175
  }
@@ -28,7 +28,9 @@ for (const arg of args) {
28
28
  }
29
29
  // Reject non-numeric TTLs (e.g. --ttl=abc) instead of silently caching NaN.
30
30
  if (!Number.isFinite(customTtlMs)) {
31
- throw new ConfigError(`Invalid --ttl value "${val}": expected a number with optional h/m suffix (e.g. --ttl=24h).`);
31
+ throw new ConfigError(
32
+ `Invalid --ttl value "${val}": expected a number with optional h/m suffix (e.g. --ttl=24h).`
33
+ );
32
34
  }
33
35
  }
34
36
  }
@@ -42,13 +44,11 @@ try {
42
44
  const ttlHours = (result.ttlMs / (1000 * 60 * 60)).toFixed(1);
43
45
 
44
46
  if (result.fromCache) {
45
- console.log(
46
- `\x1b[32m✓\x1b[0m Models catalog is already fresh (within ${ttlHours}h TTL).`
47
- );
47
+ console.log(`\x1b[32m✓\x1b[0m Models catalog is already fresh (within ${ttlHours}h TTL).`);
48
48
  } else {
49
49
  console.log(
50
50
  `\x1b[32m✓\x1b[0m Successfully downloaded and cached \x1b[1m${result.modelCount.toLocaleString()}\x1b[0m models ` +
51
- `across \x1b[1m${result.providerCount}\x1b[0m providers in ${duration}s.`
51
+ `across \x1b[1m${result.providerCount}\x1b[0m providers in ${duration}s.`
52
52
  );
53
53
  }
54
54
  console.log(` \x1b[90mCache location: ${result.cachePath}\x1b[0m`);
@@ -2,23 +2,24 @@
2
2
 
3
3
  Small shared helpers. Each file has one job.
4
4
 
5
- | File | What it does |
6
- | :-- | :-- |
7
- | `durable.ts` | The ONE disk writer (`writeFileDurable`: temp, fsync, `.bak`, rename, folder fsync), `readWithBackup`, `lockFolder` (one process per folder, used for session folders; stale-lock takeover, temp sweep) and `withFileMutex` (short per-file lock for state several processes change, e.g. one scheduled action). |
8
- | `home.ts` | `resolveHome` / `setHome`: the single folder everything is stored in. |
9
- | `env.ts` | Process-env lookups and API key resolution. No `globalThis` fallback. |
10
- | `headers.ts` | Session-affinity headers, opt-in attribution, browser stripping, package version fallback. |
11
- | `media.ts` | Normalizes a path, URL, data URL, base64 or bytes to base64 and a MIME type; magic-byte sniffing. |
12
- | `safe-io.ts` | `fileAccess.roots` containment and the guarded remote fetch (private hosts refused, redirects re-checked, size cap). |
13
- | `raw-capture.ts` | The `captureRaw` policy for `AgentResponse.raw`. |
14
- | `cache.ts` | Cache-key clamp (64 chars) and the central secret redaction. |
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
- | `session.ts` | Id helpers (`createSessionId`, `hashSessionPart`, `createTrackingId`, run/turn ids), `parseModelRef`, `previewPrompt`. |
18
- | `serialization.ts` | `toJsonSafe`, `safeStringify`, `escapeXml`. |
19
- | `base64.ts`, `thought-signature.ts` | Runtime-agnostic base64; Gemini thought-signature helpers. |
5
+ | File | What it does |
6
+ | :---------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7
+ | `durable.ts` | The ONE disk writer (`writeFileDurable`: temp, fsync, `.bak`, rename, folder fsync), `readWithBackup`, `lockFolder` (one process per folder, used for session folders; stale-lock takeover, temp sweep) and `withFileMutex` (short per-file lock for state several processes change, e.g. one scheduled action). |
8
+ | `home.ts` | `resolveHome` / `setHome`: the single folder everything is stored in. |
9
+ | `env.ts` | Process-env lookups and API key resolution. No `globalThis` fallback. |
10
+ | `headers.ts` | Session-affinity headers, opt-in attribution, browser stripping, package version fallback. |
11
+ | `media.ts` | Normalizes a path, URL, data URL, base64 or bytes to base64 and a MIME type; magic-byte sniffing. |
12
+ | `safe-io.ts` | `fileAccess.roots` containment and the guarded remote fetch (private hosts refused, redirects re-checked, size cap). |
13
+ | `raw-capture.ts` | The `captureRaw` policy for `AgentResponse.raw`. |
14
+ | `cache.ts` | Cache-key clamp (64 chars) and the central secret redaction. |
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
+ | `session.ts` | Id helpers (`createSessionId`, `hashSessionPart`, `createTrackingId`, run/turn ids), `parseModelRef`, `previewPrompt`. |
18
+ | `serialization.ts` | `toJsonSafe`, `safeStringify`, `escapeXml`. |
19
+ | `base64.ts`, `thought-signature.ts` | Runtime-agnostic base64; Gemini thought-signature helpers. |
20
20
 
21
21
  ## Invariants
22
+
22
23
  - Never write state with a bare `writeFileSync`; use `writeFileDurable`.
23
24
  - `media.ts` and `safe-io.ts` import `node:*` lazily so browser bundles keep working.
24
25
  - Redaction is central (`cache.ts`); adapters must not keep their own deny-lists.
@@ -58,9 +58,7 @@ export function redactValue(value: unknown): unknown {
58
58
  * Deny-list (name contains): *key*, *token*, *secret*, cookie, authorization.
59
59
  * Value patterns (redacted regardless of name): sk-, AIza, gsk-, xox-.
60
60
  */
61
- export function redactHeadersCentral(
62
- headers: Record<string, unknown> | undefined | null
63
- ): Record<string, unknown> {
61
+ export function redactHeadersCentral(headers: Record<string, unknown> | undefined | null): Record<string, unknown> {
64
62
  if (!headers || typeof headers !== "object") return {};
65
63
  const out: Record<string, unknown> = {};
66
64
  for (const [k, v] of Object.entries(headers)) {
@@ -32,7 +32,11 @@ export function fsyncFolder(dir: string): void {
32
32
  * Durable atomic write: temp file (0600) -> fsync -> previous version kept as `<file>.bak` (hard link, no copy)
33
33
  * -> rename -> fsync of the folder. Throws on failure; the temp file is removed. `backup: false` skips the `.bak`.
34
34
  */
35
- export function writeFileDurable(filePath: string, content: string | Uint8Array, opts: { backup?: boolean } = {}): void {
35
+ export function writeFileDurable(
36
+ filePath: string,
37
+ content: string | Uint8Array,
38
+ opts: { backup?: boolean } = {}
39
+ ): void {
36
40
  const dir = path.dirname(filePath);
37
41
  try {
38
42
  fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
@@ -73,7 +77,10 @@ export function writeFileDurable(filePath: string, content: string | Uint8Array,
73
77
  * Reads `file`; when `isValid` rejects it, falls back to `<file>.bak`. `undefined` when the file is missing (a deleted
74
78
  * file is never resurrected from its backup). When both are invalid the primary text comes back, `fromBackup: false`.
75
79
  */
76
- export function readWithBackup(file: string, isValid: (text: string) => boolean): { text: string; fromBackup: boolean } | undefined {
80
+ export function readWithBackup(
81
+ file: string,
82
+ isValid: (text: string) => boolean
83
+ ): { text: string; fromBackup: boolean } | undefined {
77
84
  let text: string;
78
85
  try {
79
86
  text = fs.readFileSync(file, "utf8");
@@ -234,7 +241,8 @@ const heldMutexes = new Set<string>();
234
241
  */
235
242
  export function withFileMutex<T>(lockFile: string, fn: () => T): T {
236
243
  const file = path.resolve(lockFile);
237
- if (heldMutexes.has(file)) throw new ConfigError(`[Agent Accelerator] Mutex "${file}" is already held by this process (nested use).`);
244
+ if (heldMutexes.has(file))
245
+ throw new ConfigError(`[Agent Accelerator] Mutex "${file}" is already held by this process (nested use).`);
238
246
  const mine = JSON.stringify({ pid: process.pid, host: os.hostname(), at: Date.now() });
239
247
  const deadline = Date.now() + MUTEX_WAIT_MS;
240
248
  let unreadable = 0;
@@ -265,7 +273,8 @@ export function withFileMutex<T>(lockFile: string, fn: () => T): T {
265
273
  owner = undefined;
266
274
  }
267
275
  let stale = false;
268
- if (typeof owner?.pid !== "number") stale = ++unreadable > 5; // empty or garbage: another process may be mid-write
276
+ if (typeof owner?.pid !== "number")
277
+ stale = ++unreadable > 5; // empty or garbage: another process may be mid-write
269
278
  else if (owner.host === os.hostname() && !processAlive(owner.pid)) stale = true;
270
279
  else if (Date.now() - mtime > MUTEX_MAX_AGE_MS) stale = true;
271
280
  if (stale) {
@@ -276,7 +285,10 @@ export function withFileMutex<T>(lockFile: string, fn: () => T): T {
276
285
  }
277
286
  continue;
278
287
  }
279
- if (Date.now() > deadline) throw new ConfigError(`[Agent Accelerator] Timed out after ${MUTEX_WAIT_MS} ms waiting for the lock "${file}" (held by process ${String(owner?.pid)}).`);
288
+ if (Date.now() > deadline)
289
+ throw new ConfigError(
290
+ `[Agent Accelerator] Timed out after ${MUTEX_WAIT_MS} ms waiting for the lock "${file}" (held by process ${String(owner?.pid)}).`
291
+ );
280
292
  pause(typeof owner?.pid === "number" ? 2 : 40);
281
293
  }
282
294
  heldMutexes.add(file);
package/src/utils/env.ts CHANGED
@@ -11,7 +11,10 @@ export function getEnv(key: string, fallback?: string): string | undefined {
11
11
  if (typeof process !== "undefined" && process.env && process.env[key]) {
12
12
  // Trim quotes/whitespace from .env files (`sk-..."\n` -> 401 otherwise).
13
13
  const raw = String(process.env[key]);
14
- const trimmed = raw.trim().replace(/^["']|["']$/g, "").trim();
14
+ const trimmed = raw
15
+ .trim()
16
+ .replace(/^["']|["']$/g, "")
17
+ .trim();
15
18
  if (trimmed) return trimmed;
16
19
  }
17
20
  return fallback;
@@ -27,7 +30,10 @@ export type ProviderEnv = Record<string, string>;
27
30
  export function getApiKey(provider: string, explicitKey?: string, env?: ProviderEnv): string | undefined {
28
31
  const clean = (v: unknown): string | undefined => {
29
32
  if (typeof v !== "string") return undefined;
30
- const t = v.trim().replace(/^["']|["']$/g, "").trim();
33
+ const t = v
34
+ .trim()
35
+ .replace(/^["']|["']$/g, "")
36
+ .trim();
31
37
  return t || undefined;
32
38
  };
33
39
  const cleanedExplicit = clean(explicitKey);
@@ -56,11 +62,7 @@ export function getApiKey(provider: string, explicitKey?: string, env?: Provider
56
62
  switch (provider.toLowerCase()) {
57
63
  case "google":
58
64
  case "gemini":
59
- return (
60
- getEnv("GEMINI_API_KEY") ||
61
- getEnv("GOOGLE_API_KEY") ||
62
- getEnv("GOOGLE_GENAI_API_KEY")
63
- );
65
+ return getEnv("GEMINI_API_KEY") || getEnv("GOOGLE_API_KEY") || getEnv("GOOGLE_GENAI_API_KEY");
64
66
  case "openrouter":
65
67
  return getEnv("OPENROUTER_API_KEY");
66
68
  case "openai":
@@ -21,7 +21,9 @@ export function isTransientError(error: unknown): boolean {
21
21
  NaN
22
22
  );
23
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();
24
+ const code = String(
25
+ (err["providerCode"] as string | undefined) ?? (err["code"] as string | undefined) ?? ""
26
+ ).toUpperCase();
25
27
  if (["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "EPIPE", "ETIMEDOUT"].includes(code)) {
26
28
  return true;
27
29
  }
@@ -114,9 +116,7 @@ export function toConciseProviderError(error: unknown, providerId: string, model
114
116
  // Forwarded when present (e.g. OpenRouter's canonical `error_type`,
115
117
  // which disambiguates lossy native codes). Survives re-wraps so retry
116
118
  // helpers never strip it.
117
- ...(typeof err?.errorType === "string"
118
- ? { errorType: { value: err.errorType, enumerable: false } }
119
- : {}),
119
+ ...(typeof err?.errorType === "string" ? { errorType: { value: err.errorType, enumerable: false } } : {}),
120
120
  // The provider's own error code (e.g. Google's `not_found` chain-eviction code, which carries no HTTP status on
121
121
  // SSE error frames); `code` itself is the SDK kind (`"provider"`). Survives re-wraps so chain detectors never lose it.
122
122
  ...((typeof err?.code === "string" || typeof err?.code === "number") && err.code !== ""
@@ -148,11 +148,7 @@ const PART_TO_MODALITY: Record<string, "image" | "audio" | "video" | "pdf"> = {
148
148
  * models (custom providers, dynamic routers) are skipped: the provider
149
149
  * verdict stands and surfaces via toConciseProviderError instead. Throws a `ValidationError`.
150
150
  */
151
- export function assertModalitiesSupported(
152
- context: ProviderContext,
153
- providerId: string,
154
- modelId: string
155
- ): void {
151
+ export function assertModalitiesSupported(context: ProviderContext, providerId: string, modelId: string): void {
156
152
  const needed = new Set<string>();
157
153
  for (const msg of context.messages) {
158
154
  if (!Array.isArray((msg as any)?.content)) continue;
@@ -191,11 +187,7 @@ export function assertModalitiesSupported(
191
187
  * a clear one-liner instead; video stays Gemini-only, and OpenRouter Chat
192
188
  * Completions carries `video_url` natively (no guard there). Throws a `ValidationError`.
193
189
  */
194
- export function assertNoVideoPartsOnResponses(
195
- context: ProviderContext,
196
- providerId: string,
197
- modelId: string
198
- ): void {
190
+ export function assertNoVideoPartsOnResponses(context: ProviderContext, providerId: string, modelId: string): void {
199
191
  let hasVideo = false;
200
192
  for (const msg of context.messages) {
201
193
  if (!Array.isArray((msg as any)?.content)) continue;
@@ -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.9";
27
+ return "0.6.0";
28
28
  }
29
29
 
30
30
  // User-Agent string with version and platform; in: AGENT_ACCEL_USER_AGENT, out: a string.
@@ -93,11 +93,7 @@ function findHeaderKey(headers: Record<string, string>, name: string): string |
93
93
  * coexist on the wire.
94
94
  * @example `setHeaderCaseInsensitive(headers, "Authorization", "Bearer k");`
95
95
  */
96
- export function setHeaderCaseInsensitive(
97
- headers: Record<string, string>,
98
- name: string,
99
- value: string
100
- ): void {
96
+ export function setHeaderCaseInsensitive(headers: Record<string, string>, name: string, value: string): void {
101
97
  const existing = findHeaderKey(headers, name);
102
98
  if (existing !== undefined && existing !== name) delete headers[existing];
103
99
  headers[name] = value;
@@ -111,10 +107,7 @@ export function setHeaderCaseInsensitive(
111
107
  * Returns true when a stored signature may be echoed on `currentProvider`:
112
108
  * untagged (legacy) echoes everywhere, tagged echoes only on a match.
113
109
  */
114
- export function shouldEchoSignature(
115
- storedProvider: unknown,
116
- currentProvider: string
117
- ): boolean {
110
+ export function shouldEchoSignature(storedProvider: unknown, currentProvider: string): boolean {
118
111
  if (!storedProvider) return true;
119
112
  return storedProvider === currentProvider;
120
113
  }
@@ -2,6 +2,7 @@
2
2
  // paths are loaded via a lazy dynamic import so browsers never resolve it.
3
3
  import { bytesToBase64, base64ToBytes } from "./base64.ts";
4
4
  import { fetchRemoteBytes } from "./safe-io.ts";
5
+ import { ValidationError } from "../types/errors.ts";
5
6
 
6
7
  export interface NormalizedMedia {
7
8
  mimeType: string;
@@ -63,7 +64,12 @@ export function sniffMimeType(bytes: Uint8Array): string | undefined {
63
64
  if (ascii(bytes, 0, "FORM") && ascii(bytes, 8, "AIF")) return "audio/aiff";
64
65
  if (bytes[0] === 0xff && ((bytes[1] ?? 0) & 0xf6) === 0xf0) return "audio/aac"; // ADTS: sync bits plus layer 00 (MP3 frames have a non-zero layer)
65
66
  if (ascii(bytes, 0, "ID3") || (bytes[0] === 0xff && ((bytes[1] ?? 0) & 0xe0) === 0xe0)) return "audio/mp3";
66
- if (ascii(bytes, 4, "ftyp")) return ascii(bytes, 8, "M4A") || ascii(bytes, 8, "M4B") ? "audio/m4a" : ascii(bytes, 8, "qt") ? "video/quicktime" : "video/mp4";
67
+ if (ascii(bytes, 4, "ftyp"))
68
+ return ascii(bytes, 8, "M4A") || ascii(bytes, 8, "M4B")
69
+ ? "audio/m4a"
70
+ : ascii(bytes, 8, "qt")
71
+ ? "video/quicktime"
72
+ : "video/mp4";
67
73
  if (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 1 && bytes[3] === 0xba) return "video/mpeg"; // MPEG program stream
68
74
  if (bytes[0] === 0x1a && bytes[1] === 0x45 && bytes[2] === 0xdf && bytes[3] === 0xa3) return "video/webm";
69
75
  return undefined;
@@ -72,9 +78,15 @@ export function sniffMimeType(bytes: Uint8Array): string | undefined {
72
78
  const GENERIC_MIME = new Set(["", "application/octet-stream", "binary/octet-stream"]);
73
79
 
74
80
  /** Picks the best MIME type: explicit hint, then a specific candidate, then magic bytes, then the fallback. */
75
- function resolveMime(explicit: string | undefined, candidate: string | null | undefined, bytes: Uint8Array, fallback: string): string {
81
+ function resolveMime(
82
+ explicit: string | undefined,
83
+ candidate: string | null | undefined,
84
+ bytes: Uint8Array,
85
+ fallback: string
86
+ ): string {
76
87
  if (explicit) return explicit;
77
- const specific = candidate && !GENERIC_MIME.has(candidate.split(";")[0]!.trim().toLowerCase()) ? candidate : undefined;
88
+ const specific =
89
+ candidate && !GENERIC_MIME.has(candidate.split(";")[0]!.trim().toLowerCase()) ? candidate : undefined;
78
90
  return specific || sniffMimeType(bytes) || candidate || fallback;
79
91
  }
80
92
 
@@ -130,7 +142,7 @@ export async function normalizeMediaInput(
130
142
  // but redirects are re-checked and the download is size-capped.
131
143
  const response = await fetchRemoteBytes(input, { allowPrivateNetwork: true });
132
144
  if (!response.ok) {
133
- throw new Error(`Failed to fetch media from URL: ${input} (${response.statusText})`);
145
+ throw new ValidationError(`Failed to fetch media from URL: ${input} (${response.statusText})`);
134
146
  }
135
147
  const buffer = response.bytes;
136
148
  const mimeType = resolveMime(explicitMimeType, response.contentType, buffer, inferMimeType(input));
@@ -175,10 +187,11 @@ export async function normalizeMediaInput(
175
187
  base64Data,
176
188
  dataUrl: `data:${mimeType};base64,${base64Data}`,
177
189
  };
178
- } catch (err) {
190
+ } catch {
179
191
  // S11: don't silently return path string as base64 (would 400). Re-check if it's actually base64 with "/"
180
192
  const trimmed = input.replace(/\s/g, "");
181
- const isMaybeBase64 = trimmed.length > 100 && /^[A-Za-z0-9+/=\n\r]+$/.test(trimmed.slice(0, 500)) && trimmed.length % 4 === 0;
193
+ const isMaybeBase64 =
194
+ trimmed.length > 100 && /^[A-Za-z0-9+/=\n\r]+$/.test(trimmed.slice(0, 500)) && trimmed.length % 4 === 0;
182
195
  if (isMaybeBase64) {
183
196
  const mimeType = explicitMimeType || sniffBase64(trimmed) || "image/jpeg";
184
197
  return {
@@ -187,9 +200,11 @@ export async function normalizeMediaInput(
187
200
  dataUrl: `data:${mimeType};base64,${trimmed}`,
188
201
  };
189
202
  }
190
- throw new Error(`Failed to normalize media input: not a valid file path, data URL, http URL, or base64 string: ${input.slice(0, 80)}`);
203
+ throw new ValidationError(
204
+ `Failed to normalize media input: not a valid file path, data URL, http URL, or base64 string: ${input.slice(0, 80)}`
205
+ );
191
206
  }
192
207
  }
193
208
 
194
- throw new Error(`Unsupported media input type: ${typeof input}`);
209
+ throw new ValidationError(`Unsupported media input type: ${typeof input}`);
195
210
  }
@@ -16,7 +16,11 @@ function byteLength(value: unknown): number {
16
16
  function toolNames(tools: unknown): string[] | undefined {
17
17
  if (!Array.isArray(tools)) return undefined;
18
18
  const names = tools
19
- .map((t) => (t as { name?: unknown; function?: { name?: unknown } })?.name ?? (t as { function?: { name?: unknown } })?.function?.name)
19
+ .map(
20
+ (t) =>
21
+ (t as { name?: unknown; function?: { name?: unknown } })?.name ??
22
+ (t as { function?: { name?: unknown } })?.function?.name
23
+ )
20
24
  .filter((n): n is string => typeof n === "string");
21
25
  return names.length > 0 ? names : undefined;
22
26
  }
@@ -56,7 +60,10 @@ function summarizeResponseBody(body: unknown): unknown {
56
60
  * model output never leak through `response.raw` / `toJSON()` into logs.
57
61
  * - `false`: keep only URL, method and HTTP status.
58
62
  */
59
- export function applyCaptureRaw(raw: ProviderRawData | undefined, mode: CaptureRawMode | undefined): ProviderRawData | undefined {
63
+ export function applyCaptureRaw(
64
+ raw: ProviderRawData | undefined,
65
+ mode: CaptureRawMode | undefined
66
+ ): ProviderRawData | undefined {
60
67
  if (!raw || mode === true) return raw;
61
68
  const redactedMode = mode !== false; // undefined -> default "redacted"
62
69
  const request = raw.request
@@ -13,7 +13,18 @@ export function isRetryableRun(error: unknown): boolean {
13
13
  if (!error || isAbortError(error)) return false;
14
14
  const name = (error as { name?: unknown }).name;
15
15
  if (name === "TimeoutError") return true; // the loop's own request / first-byte / idle timeouts
16
- if (["BudgetExceededError", "ConfigError", "ValidationError", "SecurityPolicyError", "SessionLoadError", "ThinkingLevelError", "ToolError"].includes(String(name))) return false;
16
+ if (
17
+ [
18
+ "BudgetExceededError",
19
+ "ConfigError",
20
+ "ValidationError",
21
+ "SecurityPolicyError",
22
+ "SessionLoadError",
23
+ "ThinkingLevelError",
24
+ "ToolError",
25
+ ].includes(String(name))
26
+ )
27
+ return false;
17
28
  return isTransientError(error);
18
29
  }
19
30
 
@@ -24,8 +35,9 @@ export function readRetryAfterMs(error: unknown): number | undefined {
24
35
  const direct = err["retryAfterMs"];
25
36
  if (typeof direct === "number" && Number.isFinite(direct) && direct >= 0) return Math.min(60_000, direct);
26
37
  const raw =
27
- ((err["response"] as Record<string, unknown> | undefined)?.["headers"] as Record<string, unknown> | undefined)?.["retry-after"] ??
28
- ((err["headers"] as Record<string, unknown> | undefined)?.["retry-after"]);
38
+ ((err["response"] as Record<string, unknown> | undefined)?.["headers"] as Record<string, unknown> | undefined)?.[
39
+ "retry-after"
40
+ ] ?? (err["headers"] as Record<string, unknown> | undefined)?.["retry-after"];
29
41
  if (raw === undefined) return undefined;
30
42
  const secs = Number(Array.isArray(raw) ? raw[0] : raw);
31
43
  if (!Number.isFinite(secs) || secs < 0) return undefined;
@@ -139,19 +139,31 @@ function isPrivateHostname(host: string): boolean {
139
139
  // Throws RemoteFetchError for a non-http(s) URL or, unless private hosts are allowed, a private host (also by DNS result); in: fetchRemoteBytes on every hop, out: nothing or a thrown error.
140
140
  async function assertRemoteUrlAllowed(url: URL, allowPrivateNetwork: boolean): Promise<void> {
141
141
  if (url.protocol !== "http:" && url.protocol !== "https:") {
142
- throw new RemoteFetchError("blocked", `[Agent Accelerator] Only http(s) URLs are allowed (got ${url.protocol}).`, url.href);
142
+ throw new RemoteFetchError(
143
+ "blocked",
144
+ `[Agent Accelerator] Only http(s) URLs are allowed (got ${url.protocol}).`,
145
+ url.href
146
+ );
143
147
  }
144
148
  if (allowPrivateNetwork) return;
145
149
  const host = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
146
150
  if (isPrivateHostname(host)) {
147
- throw new RemoteFetchError("blocked", `[Agent Accelerator] Refusing to fetch private/internal host "${host}".`, url.href);
151
+ throw new RemoteFetchError(
152
+ "blocked",
153
+ `[Agent Accelerator] Refusing to fetch private/internal host "${host}".`,
154
+ url.href
155
+ );
148
156
  }
149
157
  if (/^[\d.]+$/.test(host) || host.includes(":")) return; // IP literal already checked
150
158
  try {
151
159
  const dns = await import("node:dns");
152
160
  const addrs = await dns.promises.lookup(host, { all: true });
153
161
  if (addrs.some((a) => isPrivateAddress(a.address))) {
154
- throw new RemoteFetchError("blocked", `[Agent Accelerator] Host "${host}" resolves to a private/internal address.`, url.href);
162
+ throw new RemoteFetchError(
163
+ "blocked",
164
+ `[Agent Accelerator] Host "${host}" resolves to a private/internal address.`,
165
+ url.href
166
+ );
155
167
  }
156
168
  } catch (err) {
157
169
  if (err instanceof RemoteFetchError) throw err;
@@ -169,7 +181,9 @@ async function readCapped(res: Response, maxBytes: number, url: string): Promise
169
181
  );
170
182
  const declared = Number(res.headers.get("content-length"));
171
183
  if (Number.isFinite(declared) && declared > maxBytes) {
172
- try { await res.body?.cancel(); } catch {}
184
+ try {
185
+ await res.body?.cancel();
186
+ } catch {}
173
187
  throw tooLarge();
174
188
  }
175
189
  if (!res.body) {
@@ -185,7 +199,9 @@ async function readCapped(res: Response, maxBytes: number, url: string): Promise
185
199
  if (done) break;
186
200
  total += value.byteLength;
187
201
  if (total > maxBytes) {
188
- try { await reader.cancel(); } catch {}
202
+ try {
203
+ await reader.cancel();
204
+ } catch {}
189
205
  throw tooLarge();
190
206
  }
191
207
  chunks.push(value);
@@ -227,7 +243,11 @@ export async function fetchRemoteBytes(
227
243
  if ((res as { type?: string }).type === "opaqueredirect") {
228
244
  // Browser runtimes hide the Location header: only trusted callers may follow blindly.
229
245
  if (!allowPrivate) {
230
- throw new RemoteFetchError("blocked", "[Agent Accelerator] Redirects cannot be validated in this runtime.", current.href);
246
+ throw new RemoteFetchError(
247
+ "blocked",
248
+ "[Agent Accelerator] Redirects cannot be validated in this runtime.",
249
+ current.href
250
+ );
231
251
  }
232
252
  const followed = await fetch(current.href, { signal: opts.signal });
233
253
  return {
@@ -240,7 +260,9 @@ export async function fetchRemoteBytes(
240
260
  }
241
261
  const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
242
262
  if (location) {
243
- try { await res.body?.cancel(); } catch {}
263
+ try {
264
+ await res.body?.cancel();
265
+ } catch {}
244
266
  current = new URL(location, current);
245
267
  continue;
246
268
  }
@@ -53,10 +53,7 @@ export function toJsonSafe(value: unknown, seen = new WeakSet<object>()): unknow
53
53
  }
54
54
 
55
55
  if (value instanceof Map) {
56
- return Array.from(value.entries()).map(([key, entry]) => [
57
- toJsonSafe(key, seen),
58
- toJsonSafe(entry, seen),
59
- ]);
56
+ return Array.from(value.entries()).map(([key, entry]) => [toJsonSafe(key, seen), toJsonSafe(entry, seen)]);
60
57
  }
61
58
 
62
59
  if (value instanceof Set) {
@@ -15,8 +15,7 @@ export function createSessionId(prefix?: string): string {
15
15
  const head = prefix ? `${prefix}-` : "";
16
16
  let id: string;
17
17
  try {
18
- const webCrypto =
19
- (globalThis as any)?.crypto ?? (typeof crypto !== "undefined" ? crypto : undefined);
18
+ const webCrypto = (globalThis as any)?.crypto ?? (typeof crypto !== "undefined" ? crypto : undefined);
20
19
  if (webCrypto && typeof webCrypto.randomUUID === "function") {
21
20
  id = `${head}${webCrypto.randomUUID()}`;
22
21
  } else {