agent-accelerator 0.5.9 → 0.6.5

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 (85) hide show
  1. package/README.md +64 -16
  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 +447 -172
  7. package/src/agent/architecture.md +16 -12
  8. package/src/agent/config-blocks.ts +39 -12
  9. package/src/agent/context.ts +9 -3
  10. package/src/agent/defaults.ts +88 -0
  11. package/src/agent/delegation.ts +86 -28
  12. package/src/agent/loop.ts +474 -374
  13. package/src/agent/subagent.ts +1 -3
  14. package/src/agent/subagents-tool.ts +50 -19
  15. package/src/agent/turns.ts +3 -1
  16. package/src/agent/usage-limit.ts +92 -0
  17. package/src/agent/worker.ts +73 -30
  18. package/src/agent/workers.ts +126 -21
  19. package/src/architecture.md +17 -15
  20. package/src/index.ts +43 -19
  21. package/src/models/architecture.md +4 -3
  22. package/src/models/catalog-cache.ts +6 -5
  23. package/src/models/catalog.ts +39 -30
  24. package/src/prompts/additional-context.md +2 -0
  25. package/src/prompts/architecture.md +19 -0
  26. package/src/prompts/prompts.ts +81 -0
  27. package/src/prompts/reasoning-directive.md +5 -0
  28. package/src/{scheduler/scheduled-actions.md → prompts/scheduled-actions-tool.md} +4 -0
  29. package/src/prompts/structured-output.md +2 -0
  30. package/src/{agent/subagents.md → prompts/subagents-tool.md} +3 -0
  31. package/src/prompts/tool-bridge.md +1 -0
  32. package/src/prompts/tool-interrupted.md +1 -0
  33. package/src/prompts/tool-not-found.md +1 -0
  34. package/src/prompts/tool-repeat-blocked.md +1 -0
  35. package/src/prompts/tool-schema-error.md +1 -0
  36. package/src/prompts/tool-truncated-arguments.md +1 -0
  37. package/src/prompts/worker-failed.md +1 -0
  38. package/src/prompts/worker-replied.md +2 -0
  39. package/src/providers/architecture.md +10 -8
  40. package/src/providers/google.ts +228 -69
  41. package/src/providers/openai-compat.ts +74 -56
  42. package/src/providers/openai.ts +64 -41
  43. package/src/providers/openrouter.ts +270 -48
  44. package/src/providers/registry.ts +6 -21
  45. package/src/providers/shared.ts +87 -33
  46. package/src/providers.ts +115 -124
  47. package/src/scheduler/architecture.md +8 -7
  48. package/src/scheduler/scheduler.ts +226 -60
  49. package/src/scheduler/store.ts +23 -10
  50. package/src/scheduler/time.ts +17 -3
  51. package/src/scheduler/tool.ts +38 -16
  52. package/src/session/architecture.md +7 -6
  53. package/src/session/journal.ts +39 -12
  54. package/src/session/store.ts +126 -61
  55. package/src/session/usage.ts +58 -9
  56. package/src/streaming/architecture.md +4 -3
  57. package/src/streaming/event-stream.ts +26 -12
  58. package/src/telemetry.ts +6 -2
  59. package/src/tools/architecture.md +9 -7
  60. package/src/tools/executor.ts +109 -62
  61. package/src/tools/schema.ts +10 -6
  62. package/src/tools/subagent-result.ts +6 -1
  63. package/src/tools/tool-content.ts +32 -0
  64. package/src/tools/tool.ts +9 -11
  65. package/src/types/agent.ts +37 -22
  66. package/src/types/architecture.md +11 -10
  67. package/src/types/core.ts +5 -9
  68. package/src/types/errors.ts +26 -4
  69. package/src/types/message.ts +7 -0
  70. package/src/types/model.ts +4 -5
  71. package/src/types/scheduler.ts +4 -4
  72. package/src/types/tool.ts +14 -2
  73. package/src/update-models.ts +5 -5
  74. package/src/utils/architecture.md +16 -15
  75. package/src/utils/cache.ts +1 -3
  76. package/src/utils/durable.ts +17 -5
  77. package/src/utils/env.ts +9 -7
  78. package/src/utils/errors.ts +9 -17
  79. package/src/utils/headers.ts +3 -10
  80. package/src/utils/media.ts +23 -8
  81. package/src/utils/raw-capture.ts +9 -2
  82. package/src/utils/retry.ts +15 -3
  83. package/src/utils/safe-io.ts +29 -7
  84. package/src/utils/serialization.ts +1 -4
  85. package/src/utils/session.ts +1 -2
@@ -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;
@@ -185,17 +181,13 @@ export function assertModalitiesSupported(
185
181
  * That wire has no video shape — only `input_text`/`input_image`/`input_file`
186
182
  * exist in either vendor's Responses docs — so a video part sent as
187
183
  * `input_file` lands in the router's document parser and 400s confusingly
188
- * (`Failed to parse the file ... Provide a PDF document`, observed live on
189
- * `openrouter/stealth/space-bunny-alpha` under the old Responses skin despite
190
- * its catalog `video` flag, which is chat-transport oriented). Fail fast with
184
+ * (`Failed to parse the file ... Provide a PDF document`, observed live under
185
+ * the old OpenRouter Responses skin despite the catalog `video` flag, which is
186
+ * chat-transport oriented). Fail fast with
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.5";
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 {