agent-accelerator 0.1.0 → 0.2.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 (45) hide show
  1. package/README.md +83 -112
  2. package/{SYSTEM_PROMPT_AGENT.md → examples/prompts/SYSTEM_PROMPT_AGENT.md} +1 -1
  3. package/package.json +14 -9
  4. package/src/agent/agent.ts +191 -71
  5. package/src/agent/context.ts +1 -0
  6. package/src/agent/delegation.ts +61 -12
  7. package/src/agent/loop.ts +71 -48
  8. package/src/data/README.md +6 -6
  9. package/src/index.ts +130 -45
  10. package/src/models/catalog-cache.ts +60 -9
  11. package/src/models/catalog.ts +53 -7
  12. package/src/providers/google.ts +926 -0
  13. package/src/providers/openai-compat.ts +1147 -0
  14. package/src/providers/openai.ts +959 -0
  15. package/src/providers/openrouter-responses.ts +949 -0
  16. package/src/providers/openrouter.ts +1037 -0
  17. package/src/{ai-sdk → providers}/registry.ts +43 -55
  18. package/src/providers.ts +490 -0
  19. package/src/streaming/sse-parser.ts +6 -4
  20. package/src/tools/executor.ts +19 -6
  21. package/src/tools/schema.ts +21 -11
  22. package/src/types/agent.ts +8 -1
  23. package/src/types/core.ts +1 -1
  24. package/src/types/message.ts +5 -0
  25. package/src/types/model.ts +3 -7
  26. package/src/types/provider-payloads.ts +2 -84
  27. package/src/types/tool.ts +6 -0
  28. package/src/update-models.ts +56 -0
  29. package/src/utils/cache.ts +1 -1
  30. package/src/utils/documents.ts +517 -0
  31. package/src/utils/env.ts +0 -7
  32. package/src/{ai-sdk → utils}/errors.ts +61 -2
  33. package/src/utils/headers.ts +10 -20
  34. package/src/utils/media.ts +5 -2
  35. package/src/utils/retry.ts +89 -0
  36. package/src/utils/serialization.ts +15 -0
  37. package/src/ai-sdk/converters.ts +0 -342
  38. package/src/ai-sdk/executor.ts +0 -454
  39. package/src/ai-sdk/index.ts +0 -55
  40. package/src/ai-sdk/model-provider.ts +0 -303
  41. package/src/ai-sdk/options.ts +0 -306
  42. package/src/ai-sdk/provider.ts +0 -415
  43. package/src/tokens/counter.ts +0 -136
  44. /package/{SYSTEM_PROMPT.md → examples/prompts/SYSTEM_PROMPT.md} +0 -0
  45. /package/{SYSTEM_PROMPT_TOOLS.md → examples/prompts/SYSTEM_PROMPT_TOOLS.md} +0 -0
@@ -0,0 +1,89 @@
1
+ import { toConciseProviderError } from "./errors.ts";
2
+
3
+ /**
4
+ * Checks whether a provider error is a cancellation (never retried).
5
+ */
6
+ export function isAbortError(error: unknown): boolean {
7
+ const err = error as { name?: unknown; message?: unknown } | null;
8
+ if (!err || typeof err !== "object") return false;
9
+ if ((err as { name?: unknown }).name === "AbortError") return true;
10
+ return /abort|cancell?ed/i.test(String((err as { message?: unknown }).message ?? error));
11
+ }
12
+
13
+ /**
14
+ * Checks whether a provider error is transient and safe to retry.
15
+ */
16
+ export function isTransientError(error: unknown): boolean {
17
+ if (!error) return false;
18
+ if (isAbortError(error)) return false;
19
+ const err = error as Record<string, unknown>;
20
+ if (err["transient"] === true) return true;
21
+ const status = Number(
22
+ (err["status"] as number | undefined) ??
23
+ (err["statusCode"] as number | undefined) ??
24
+ ((err["response"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
25
+ ((err["lastError"] as Record<string, unknown> | undefined)?.["status"] as number | undefined) ??
26
+ NaN
27
+ );
28
+ if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
29
+ const code = String((err["code"] as string | undefined) ?? "").toUpperCase();
30
+ if (["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "EPIPE", "ETIMEDOUT"].includes(code)) {
31
+ return true;
32
+ }
33
+ return /(?:timed?\s?out|temporar(?:y|ily)|connection reset|connection refused|service unavailable|rate limit|\b5\d\d\b|overloaded)/i.test(
34
+ String((err["message"] as string | undefined) ?? error)
35
+ );
36
+ }
37
+
38
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
39
+ if (ms <= 0) return Promise.resolve();
40
+ return new Promise((resolve, reject) => {
41
+ const timer = setTimeout(() => {
42
+ signal?.removeEventListener("abort", onAbort);
43
+ resolve();
44
+ }, ms);
45
+ const onAbort = () => {
46
+ clearTimeout(timer);
47
+ signal?.removeEventListener("abort", onAbort);
48
+ reject(Object.assign(new Error("Operation aborted"), { name: "AbortError" }));
49
+ };
50
+ signal?.addEventListener("abort", onAbort, { once: true });
51
+ if (signal?.aborted) onAbort();
52
+ // Tag as AbortError so callers never retry it
53
+ // (name assigned at reject site)
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Runs a provider call with bounded retries for transient failures.
59
+ */
60
+ export async function withRetries<T>(
61
+ fn: () => Promise<T> | PromiseLike<T>,
62
+ options?: {
63
+ maxRetries?: number;
64
+ maxRetryDelayMs?: number;
65
+ signal?: AbortSignal;
66
+ label?: { providerId?: string; modelId?: string };
67
+ }
68
+ ): Promise<T> {
69
+ const maxRetries = options?.maxRetries ?? 2;
70
+ const maxDelay = options?.maxRetryDelayMs ?? 5000;
71
+ const fail = (error: unknown): never => {
72
+ if (options?.label?.providerId && options?.label?.modelId) {
73
+ throw toConciseProviderError(error, options.label.providerId, options.label.modelId);
74
+ }
75
+ throw error;
76
+ };
77
+ let attempt = 0;
78
+ for (;;) {
79
+ try {
80
+ return await fn();
81
+ } catch (error) {
82
+ if (options?.signal?.aborted || isAbortError(error)) throw error;
83
+ if (attempt >= maxRetries || !isTransientError(error)) fail(error);
84
+ const delay = Math.min(maxDelay, 250 * 2 ** attempt + Math.random() * 100);
85
+ attempt += 1;
86
+ await sleep(delay, options?.signal);
87
+ }
88
+ }
89
+ }
@@ -77,6 +77,21 @@ export function toJsonSafe(value: unknown, seen = new WeakSet<object>()): unknow
77
77
  }
78
78
  }
79
79
 
80
+ /**
81
+ * Escapes arbitrary text for embedding inside XML element content or
82
+ * double-quoted attributes.
83
+ *
84
+ * @example `const xml = "<Document>" + escapeXml(markdown) + "</Document>";`
85
+ */
86
+ export function escapeXml(text: string): string {
87
+ return text
88
+ .replace(/&/g, "&amp;")
89
+ .replace(/</g, "&lt;")
90
+ .replace(/>/g, "&gt;")
91
+ .replace(/"/g, "&quot;")
92
+ .replace(/'/g, "&apos;");
93
+ }
94
+
80
95
  /**
81
96
  * Safely serializes arbitrary values without throwing on circular or unsupported data.
82
97
  *
@@ -1,342 +0,0 @@
1
- import type {
2
- LanguageModelV4Prompt,
3
- LanguageModelV4FunctionTool,
4
- LanguageModelV4FinishReason,
5
- } from "@ai-sdk/provider";
6
- import type { ProviderContext, ContentPart, Message } from "../types/message.ts";
7
- import type { StandardToolDeclaration, ToolCallRecord } from "../types/tool.ts";
8
- import type { ProviderGenerateResult, ProviderRequestOptions } from "../types/model.ts";
9
- import type { TokenUsage } from "../types/core.ts";
10
- import { normalizeMediaInput } from "../utils/media.ts";
11
- import { base64ToBytes } from "../utils/base64.ts";
12
- import { safeStringify } from "../utils/serialization.ts";
13
- import { stripSchemaForGoogle } from "../tools/schema.ts";
14
-
15
- /**
16
- * Converts Agent Accelerator tools to Vercel AI SDK tools format.
17
- */
18
- export function toAiSdkTools(
19
- tools?: StandardToolDeclaration[],
20
- providerId?: string
21
- ): LanguageModelV4FunctionTool[] | undefined {
22
- if (!tools || tools.length === 0) return undefined;
23
- const norm = providerId?.toLowerCase().trim();
24
-
25
- return tools.map((t) => {
26
- const raw = (t.parameters || { type: "object", properties: {} }) as Record<string, unknown>;
27
- const inputSchema = norm === "google" || norm === "gemini" ? (stripSchemaForGoogle(raw) as any) : (raw as any);
28
- return {
29
- type: "function",
30
- name: t.name,
31
- description: t.description,
32
- inputSchema,
33
- ...(t.strict !== undefined ? { strict: t.strict } : {}),
34
- };
35
- });
36
- }
37
-
38
- function guessFilename(input: unknown, mimeType: string): string | undefined {
39
- if (typeof input !== "string" || input.startsWith("data:") || input.startsWith("http")) return undefined;
40
- const base = input.split(/[\\/]/).pop()?.split("?")[0]?.trim();
41
- if (base && base.includes(".")) return base;
42
- const ext = mimeType.split("/")[1]?.split(";")[0]?.trim();
43
- return ext ? `file.${ext}` : undefined;
44
- }
45
-
46
- /**
47
- * Providers known to accept echoed reasoning in history. Vercel serializes
48
- * prompt `reasoning` parts to `reasoning_content`, which strict
49
- * OpenAI-compatible endpoints (groq, ollama, …) reject with 400 — so off-list
50
- * providers get history without the thinking trace (tool calls preserved).
51
- */
52
- const REASONING_HISTORY_PROVIDERS = new Set([
53
- "google",
54
- "gemini",
55
- "openai",
56
- "openrouter",
57
- "opencode",
58
- "opencode-go",
59
- ]);
60
-
61
- /**
62
- * Converts Agent Accelerator ProviderContext to Vercel AI SDK LanguageModelV4Prompt.
63
- */
64
- export async function toAiSdkPrompt(
65
- context: ProviderContext,
66
- providerId?: string
67
- ): Promise<LanguageModelV4Prompt> {
68
- const prompt: LanguageModelV4Prompt = [];
69
-
70
- if (context.systemPrompt) {
71
- prompt.push({
72
- role: "system",
73
- content: context.systemPrompt,
74
- });
75
- }
76
-
77
- for (const msg of context.messages) {
78
- if (typeof msg.content === "string") {
79
- if (msg.role === "system") {
80
- prompt.push({ role: "system", content: msg.content });
81
- } else if (msg.role === "assistant") {
82
- prompt.push({
83
- role: "assistant",
84
- content: [{ type: "text", text: msg.content }],
85
- });
86
- } else if (msg.role === "tool") {
87
- prompt.push({
88
- role: "tool",
89
- content: [
90
- {
91
- type: "tool-result",
92
- toolCallId: (msg as any).tool_call_id || (msg as any).id || "call_0",
93
- toolName: (msg as any).name || "unknown",
94
- output: { type: "text", value: msg.content },
95
- },
96
- ],
97
- });
98
- } else {
99
- prompt.push({
100
- role: "user",
101
- content: [{ type: "text", text: msg.content }],
102
- });
103
- }
104
- } else if (Array.isArray(msg.content)) {
105
- if (msg.role === "assistant") {
106
- const assistantParts: Array<any> = [];
107
- for (const part of msg.content) {
108
- if (part.type === "text" && part.text) {
109
- assistantParts.push({ type: "text", text: part.text });
110
- } else if (part.type === "thinking" && part.thinking) {
111
- const keepReasoning =
112
- !providerId || REASONING_HISTORY_PROVIDERS.has(providerId.toLowerCase().trim());
113
- if (keepReasoning) {
114
- assistantParts.push({ type: "reasoning", text: part.thinking });
115
- }
116
- } else if (part.type === "tool_call") {
117
- assistantParts.push({
118
- type: "tool-call",
119
- toolCallId: part.id,
120
- toolName: part.name,
121
- input: part.arguments || {},
122
- providerMetadata: part.thoughtSignature
123
- ? { google: { thoughtSignature: part.thoughtSignature } }
124
- : undefined,
125
- });
126
- }
127
- }
128
- if (assistantParts.length > 0) {
129
- prompt.push({ role: "assistant", content: assistantParts });
130
- }
131
- } else if (msg.role === "tool") {
132
- const toolParts: Array<any> = [];
133
- for (const part of msg.content) {
134
- if (part.type === "tool_result") {
135
- toolParts.push({
136
- type: "tool-result",
137
- toolCallId: part.id,
138
- toolName: part.name,
139
- output: {
140
- type: "text",
141
- value: typeof part.result === "string" ? part.result : safeStringify(part.result),
142
- },
143
- isError: part.isError,
144
- });
145
- }
146
- }
147
- if (toolParts.length > 0) {
148
- prompt.push({ role: "tool", content: toolParts });
149
- }
150
- } else {
151
- const userParts: Array<any> = [];
152
- for (const part of msg.content) {
153
- if (part.type === "text" && part.text) {
154
- userParts.push({ type: "text", text: part.text });
155
- } else if (
156
- part.type === "image" ||
157
- part.type === "audio" ||
158
- part.type === "video" ||
159
- part.type === "file"
160
- ) {
161
- // Vercel V4 has no `image` part: every media part is a `file` part
162
- // with a tagged data payload. Images use image/* media types so all
163
- // providers route them to their native image input.
164
- const raw = (part as any).image ?? (part as any).audio ?? (part as any).video ?? (part as any).file;
165
- const norm = await normalizeMediaInput(raw, (part as any).mimeType);
166
- const bytes = base64ToBytes(norm.base64Data);
167
- const filename =
168
- (part as any).filename ?? guessFilename(raw, norm.mimeType);
169
- userParts.push({
170
- type: "file",
171
- mediaType: norm.mimeType,
172
- data: { type: "data", data: bytes },
173
- ...(filename ? { filename } : {}),
174
- });
175
- }
176
- }
177
- if (userParts.length > 0) {
178
- prompt.push({ role: "user", content: userParts });
179
- }
180
- }
181
- }
182
- }
183
-
184
- return prompt;
185
- }
186
-
187
- /**
188
- * Extracts normalized TokenUsage from Vercel AI SDK usage structure.
189
- */
190
- export function extractAiSdkUsage(rawUsage?: any): TokenUsage {
191
- if (!rawUsage) {
192
- return { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
193
- }
194
-
195
- const input =
196
- rawUsage.inputTokens?.total ??
197
- rawUsage.prompt_tokens ??
198
- rawUsage.input_tokens ??
199
- 0;
200
-
201
- const output =
202
- rawUsage.outputTokens?.total ??
203
- rawUsage.completion_tokens ??
204
- rawUsage.output_tokens ??
205
- 0;
206
-
207
- const total =
208
- rawUsage.total_tokens ??
209
- rawUsage.totalTokens ??
210
- input + output;
211
-
212
- const cached =
213
- rawUsage.inputTokens?.cacheRead ??
214
- rawUsage.prompt_tokens_details?.cached_tokens ??
215
- rawUsage.cached_tokens ??
216
- 0;
217
-
218
- const cacheWrite =
219
- rawUsage.inputTokens?.cacheWrite ??
220
- rawUsage.prompt_tokens_details?.cache_write_tokens ??
221
- rawUsage.cache_write_tokens ??
222
- 0;
223
-
224
- const thinking =
225
- rawUsage.outputTokens?.reasoning ??
226
- rawUsage.completion_tokens_details?.reasoning_tokens ??
227
- rawUsage.reasoning_tokens ??
228
- 0;
229
-
230
- return {
231
- inputTokens: input,
232
- outputTokens: output,
233
- totalTokens: total,
234
- cachedTokens: cached,
235
- cacheReadTokens: cached,
236
- cacheWriteTokens: cacheWrite,
237
- thinkingTokens: thinking,
238
- };
239
- }
240
-
241
- /**
242
- * Converts Vercel AI SDK doGenerate result into Agent Accelerator ProviderGenerateResult.
243
- */
244
- export function fromAiSdkGenerateResult(
245
- aiResult: any,
246
- providerId: string,
247
- modelId: string,
248
- durationMs: number
249
- ): ProviderGenerateResult {
250
- let text = "";
251
- let thinking: string | undefined;
252
- let thoughtSignature: string | undefined;
253
- const toolCalls: ToolCallRecord[] = [];
254
-
255
- if (aiResult.content && Array.isArray(aiResult.content)) {
256
- for (const part of aiResult.content) {
257
- if (part.type === "text" && part.text) {
258
- text += part.text;
259
- } else if (part.type === "reasoning" && part.text) {
260
- thinking = (thinking ? thinking + "\n" : "") + part.text;
261
- const sig =
262
- part.providerMetadata?.google?.thoughtSignature ||
263
- part.providerMetadata?.custom?.thoughtSignature;
264
- if (sig && typeof sig === "string") {
265
- thoughtSignature = sig;
266
- }
267
- } else if (part.type === "tool-call") {
268
- let args: Record<string, unknown> = {};
269
- if (typeof part.input === "string") {
270
- try {
271
- args = JSON.parse(part.input);
272
- } catch {
273
- args = { raw: part.input };
274
- }
275
- } else if (typeof part.input === "object" && part.input !== null) {
276
- args = part.input as Record<string, unknown>;
277
- }
278
-
279
- const sig =
280
- part.providerMetadata?.google?.thoughtSignature ||
281
- part.providerMetadata?.custom?.thoughtSignature;
282
-
283
- toolCalls.push({
284
- id: part.toolCallId || `call_${Math.random().toString(36).slice(2, 9)}`,
285
- name: part.toolName,
286
- arguments: args,
287
- rawArguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input || {}),
288
- thoughtSignature: typeof sig === "string" ? sig : undefined,
289
- });
290
-
291
- if (sig && !thoughtSignature) {
292
- thoughtSignature = sig;
293
- }
294
- }
295
- }
296
- }
297
-
298
- // Fallback / extraction from raw response body if Vercel AI SDK parts omitted custom metadata
299
- const rawBody = aiResult.response?.body;
300
- if (rawBody && typeof rawBody === "object") {
301
- const rawChoice = rawBody.choices?.[0];
302
- const rawMsg = rawChoice?.message;
303
- if (!thinking && rawMsg?.reasoning_content) {
304
- thinking = rawMsg.reasoning_content;
305
- }
306
- if (rawMsg?.tool_calls && Array.isArray(rawMsg.tool_calls)) {
307
- for (let i = 0; i < rawMsg.tool_calls.length; i++) {
308
- const rawTc = rawMsg.tool_calls[i];
309
- const extraSig = rawTc?.extra_content?.google?.thought_signature;
310
- if (extraSig && toolCalls[i]) {
311
- toolCalls[i]!.thoughtSignature = extraSig;
312
- if (!thoughtSignature) thoughtSignature = extraSig;
313
- }
314
- }
315
- }
316
- }
317
-
318
- const usage = extractAiSdkUsage(aiResult.usage ?? rawBody?.usage);
319
- const finishReason =
320
- typeof aiResult.finishReason === "object" && aiResult.finishReason !== null
321
- ? aiResult.finishReason.raw || aiResult.finishReason.unified || "stop"
322
- : String(aiResult.finishReason || "stop");
323
-
324
- const responseId = aiResult.response?.id || rawBody?.id;
325
-
326
- return {
327
- text,
328
- thinking,
329
- thoughtSignature,
330
- toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
331
- usage,
332
- finishReason,
333
- responseId,
334
- model: modelId,
335
- provider: providerId as any,
336
- raw: {
337
- request: aiResult.request,
338
- response: aiResult.response,
339
- },
340
- durationMs,
341
- };
342
- }