agent-accelerator 0.1.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1220 -0
  3. package/SYSTEM_PROMPT.md +14 -0
  4. package/SYSTEM_PROMPT_AGENT.md +34 -0
  5. package/SYSTEM_PROMPT_TOOLS.md +9 -0
  6. package/bunfig.toml +2 -0
  7. package/package.json +59 -0
  8. package/src/agent/agent.ts +615 -0
  9. package/src/agent/context.ts +161 -0
  10. package/src/agent/delegation.ts +481 -0
  11. package/src/agent/loop.ts +569 -0
  12. package/src/agent/subagent.ts +83 -0
  13. package/src/ai-sdk/converters.ts +342 -0
  14. package/src/ai-sdk/errors.ts +122 -0
  15. package/src/ai-sdk/executor.ts +454 -0
  16. package/src/ai-sdk/index.ts +55 -0
  17. package/src/ai-sdk/model-provider.ts +303 -0
  18. package/src/ai-sdk/options.ts +306 -0
  19. package/src/ai-sdk/provider.ts +415 -0
  20. package/src/ai-sdk/registry.ts +416 -0
  21. package/src/data/README.md +84 -0
  22. package/src/index.ts +190 -0
  23. package/src/models/catalog-cache.ts +273 -0
  24. package/src/models/catalog.ts +503 -0
  25. package/src/streaming/event-stream.ts +211 -0
  26. package/src/streaming/sse-parser.ts +97 -0
  27. package/src/tokens/counter.ts +136 -0
  28. package/src/tools/executor.ts +365 -0
  29. package/src/tools/schema.ts +221 -0
  30. package/src/tools/tool.ts +101 -0
  31. package/src/types/agent.ts +87 -0
  32. package/src/types/core.ts +86 -0
  33. package/src/types/message.ts +115 -0
  34. package/src/types/model.ts +212 -0
  35. package/src/types/provider-payloads.ts +434 -0
  36. package/src/types/response.ts +158 -0
  37. package/src/types/tool.ts +61 -0
  38. package/src/utils/base64.ts +27 -0
  39. package/src/utils/cache.ts +146 -0
  40. package/src/utils/env.ts +78 -0
  41. package/src/utils/headers.ts +110 -0
  42. package/src/utils/media.ts +137 -0
  43. package/src/utils/serialization.ts +91 -0
  44. package/src/utils/session.ts +26 -0
  45. package/src/utils/thought-signature.ts +27 -0
  46. package/tsconfig.json +31 -0
@@ -0,0 +1,342 @@
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
+ }
@@ -0,0 +1,122 @@
1
+ import type { ProviderContext } from "../types/message.ts";
2
+ import { getModelFromCatalog } from "../models/catalog.ts";
3
+
4
+ const CONCISE = "__agentAccelConcise";
5
+
6
+ function readStatus(error: any): number | undefined {
7
+ const s =
8
+ error?.statusCode ??
9
+ error?.status ??
10
+ error?.response?.status ??
11
+ error?.lastError?.statusCode ??
12
+ error?.lastError?.status;
13
+ const n = Number(s);
14
+ return Number.isFinite(n) && n > 0 ? n : undefined;
15
+ }
16
+
17
+ function readUrl(error: any): string {
18
+ const raw =
19
+ (typeof error?.url === "string" && error.url) ||
20
+ (typeof error?.request?.url === "string" && error.request.url) ||
21
+ "";
22
+ return raw.split("?")[0];
23
+ }
24
+
25
+ function readProviderMessage(error: any): string {
26
+ const data = error?.data;
27
+ if (typeof data?.error?.message === "string" && data.error.message.trim()) return data.error.message.trim();
28
+ if (typeof data?.error === "string" && data.error.trim()) return data.error.trim();
29
+ if (typeof data?.message === "string" && data.message.trim()) return data.message.trim();
30
+ const body = error?.responseBody;
31
+ if (typeof body === "string" && body.trim()) {
32
+ try {
33
+ const parsed = JSON.parse(body);
34
+ if (typeof parsed?.error?.message === "string" && parsed.error.message.trim()) return parsed.error.message.trim();
35
+ if (typeof parsed?.message === "string" && parsed.message.trim()) return parsed.message.trim();
36
+ } catch {}
37
+ if (body.length <= 300) return body.trim();
38
+ }
39
+ return String(error?.message ?? error).slice(0, 300);
40
+ }
41
+
42
+ function displayModel(providerId: string, modelId: string): string {
43
+ if (modelId.toLowerCase().startsWith(`${providerId.toLowerCase()}/`)) return modelId;
44
+ return `${providerId}/${modelId}`;
45
+ }
46
+
47
+ /**
48
+ * Collapses a Vercel `APICallError` (which carries the full request/response
49
+ * dump as enumerable props) into a one-line actionable error. Raw details stay
50
+ * available but non-enumerable, so runtime dumps stay small.
51
+ */
52
+ export function toConciseProviderError(error: unknown, providerId: string, modelId: string): Error {
53
+ if (error instanceof Error && (error as any)[CONCISE]) return error;
54
+ const err = error as any;
55
+ const status = readStatus(err);
56
+ const url = readUrl(err);
57
+ const detail = readProviderMessage(err);
58
+ const concise = new Error(
59
+ `[${displayModel(providerId, modelId)}] request failed${status ? ` (${status})` : ""}: ${detail}`
60
+ );
61
+ concise.name = "AgentAccelProviderError";
62
+ Object.defineProperties(concise, {
63
+ [CONCISE]: { value: true, enumerable: false },
64
+ statusCode: { value: status, enumerable: false },
65
+ provider: { value: providerId, enumerable: false },
66
+ model: { value: modelId, enumerable: false },
67
+ url: { value: url, enumerable: false },
68
+ responseBody: {
69
+ value: typeof err?.responseBody === "string" ? err.responseBody.slice(0, 500) : undefined,
70
+ enumerable: false,
71
+ },
72
+ });
73
+ return concise;
74
+ }
75
+
76
+ const PART_TO_MODALITY: Record<string, "image" | "audio" | "video" | "pdf"> = {
77
+ image: "image",
78
+ audio: "audio",
79
+ video: "video",
80
+ file: "pdf",
81
+ };
82
+
83
+ /**
84
+ * Fails fast — before any network call — when the catalog knows the model
85
+ * lacks a requested input modality (image/audio/video/document). Unknown
86
+ * models (custom providers, dynamic routers) are skipped: the provider
87
+ * verdict stands and surfaces via toConciseProviderError instead.
88
+ */
89
+ export function assertModalitiesSupported(
90
+ context: ProviderContext,
91
+ providerId: string,
92
+ modelId: string
93
+ ): void {
94
+ const needed = new Set<string>();
95
+ for (const msg of context.messages) {
96
+ if (!Array.isArray((msg as any)?.content)) continue;
97
+ for (const part of (msg as any).content) {
98
+ const mapped = PART_TO_MODALITY[(part as any)?.type];
99
+ if (mapped) needed.add(mapped);
100
+ }
101
+ }
102
+ if (needed.size === 0) return;
103
+ let supported: readonly string[] | undefined;
104
+ try {
105
+ supported = getModelFromCatalog(providerId, modelId)?.modalities?.input;
106
+ } catch {
107
+ return;
108
+ }
109
+ if (!supported) return;
110
+ const missing = [...needed].filter((k) => !supported.includes(k));
111
+ if (missing.length === 0) return;
112
+ const err = new Error(
113
+ `[${displayModel(providerId, modelId)}] unsupported ${missing.join("+")} input (supports: ${supported.join(", ") || "text"}). Use a capable model or drop the ${missing.join("+")} part.`
114
+ );
115
+ err.name = "AgentAccelProviderError";
116
+ Object.defineProperties(err, {
117
+ [CONCISE]: { value: true, enumerable: false },
118
+ provider: { value: providerId, enumerable: false },
119
+ model: { value: modelId, enumerable: false },
120
+ });
121
+ throw err;
122
+ }