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,365 @@
1
+ import type {
2
+ ToolDefinition,
3
+ ToolCallRecord,
4
+ ToolResultRecord,
5
+ ToolExecutionContext,
6
+ } from "../types/tool.ts";
7
+ import { toJsonSafe } from "../utils/serialization.ts";
8
+
9
+ export interface ExecuteToolsOptions {
10
+ tools: Record<string, ToolDefinition>;
11
+ toolCalls: ToolCallRecord[];
12
+ agentName?: string;
13
+ parallel?: boolean;
14
+ signal?: AbortSignal;
15
+ sessionId?: string;
16
+ }
17
+
18
+ const DEFAULT_TOOL_CONCURRENCY = 8;
19
+
20
+ function nowMs(): number {
21
+ return Date.now();
22
+ }
23
+
24
+ function elapsedMs(start: number): number {
25
+ return Math.max(1, nowMs() - start);
26
+ }
27
+
28
+ function numericOption(value: number | string | undefined): number | undefined {
29
+ if (value === undefined || value === "") return undefined;
30
+ const parsed = typeof value === "number" ? value : Number(value);
31
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
32
+ }
33
+
34
+ function integerOption(value: number | string | undefined): number | undefined {
35
+ const parsed = numericOption(value);
36
+ return parsed === undefined ? undefined : Math.floor(parsed);
37
+ }
38
+
39
+ function normalizeToolName(name: string): string {
40
+ return name
41
+ .trim()
42
+ .replace(/^(?:functions?|tools?)\./i, "")
43
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
44
+ .replace(/[\s-]+/g, "_")
45
+ .toLowerCase();
46
+ }
47
+
48
+ function levenshtein(a: string, b: string): number {
49
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
50
+ for (let i = 1; i <= a.length; i++) {
51
+ let previous = row[0]!;
52
+ row[0] = i;
53
+ for (let j = 1; j <= b.length; j++) {
54
+ const current = row[j]!;
55
+ row[j] = a[i - 1] === b[j - 1]
56
+ ? previous
57
+ : Math.min(previous + 1, row[j - 1]! + 1, current + 1);
58
+ previous = current;
59
+ }
60
+ }
61
+ return row[b.length]!;
62
+ }
63
+
64
+ interface ResolvedTool {
65
+ definition?: ToolDefinition;
66
+ name: string;
67
+ suggestion?: string;
68
+ }
69
+
70
+ function resolveTool(tools: Record<string, ToolDefinition>, requestedName: string): ResolvedTool {
71
+ const direct = tools[requestedName];
72
+ if (direct) return { definition: direct, name: direct.name || requestedName };
73
+
74
+ const normalized = normalizeToolName(requestedName);
75
+ const entries = Object.entries(tools);
76
+ for (const [key, definition] of entries) {
77
+ const declared = definition.name || key;
78
+ if (normalizeToolName(key) === normalized || normalizeToolName(declared) === normalized) {
79
+ return { definition, name: declared };
80
+ }
81
+ }
82
+
83
+ let best: { name: string; distance: number } | undefined;
84
+ for (const [key, definition] of entries) {
85
+ const declared = definition.name || key;
86
+ const distance = levenshtein(normalized, normalizeToolName(declared));
87
+ if (!best || distance < best.distance) best = { name: declared, distance };
88
+ }
89
+ const threshold = Math.max(2, Math.floor(normalized.length * 0.35));
90
+ return {
91
+ name: requestedName,
92
+ suggestion: best && best.distance <= threshold ? best.name : undefined,
93
+ };
94
+ }
95
+
96
+ class Semaphore {
97
+ private active = 0;
98
+ private readonly waiters: Array<{
99
+ resolve: (release: () => void) => void;
100
+ reject: (error: Error) => void;
101
+ signal?: AbortSignal;
102
+ }> = [];
103
+
104
+ constructor(private readonly limit: number) {}
105
+
106
+ acquire(signal?: AbortSignal): Promise<() => void> {
107
+ if (signal?.aborted) return Promise.reject(new Error("Tool execution aborted"));
108
+ if (this.limit === Number.POSITIVE_INFINITY || this.active < this.limit) {
109
+ this.active++;
110
+ return Promise.resolve(() => this.release());
111
+ }
112
+ return new Promise((resolve, reject) => {
113
+ const waiter = { resolve, reject, signal };
114
+ this.waiters.push(waiter);
115
+ const onAbort = () => {
116
+ const index = this.waiters.indexOf(waiter);
117
+ if (index >= 0) this.waiters.splice(index, 1);
118
+ reject(new Error("Tool execution aborted"));
119
+ };
120
+ signal?.addEventListener("abort", onAbort, { once: true });
121
+ });
122
+ }
123
+
124
+ private release(): void {
125
+ this.active = Math.max(0, this.active - 1);
126
+ while (this.waiters.length > 0) {
127
+ const waiter = this.waiters.shift()!;
128
+ if (waiter.signal?.aborted) {
129
+ waiter.reject(new Error("Tool execution aborted"));
130
+ continue;
131
+ }
132
+ this.active++;
133
+ waiter.resolve(() => this.release());
134
+ return;
135
+ }
136
+ }
137
+ }
138
+
139
+ function isTransientError(error: any): boolean {
140
+ if (!error) return false;
141
+ if (error.transient === true || error.code === "ETIMEDOUT") return true;
142
+ const status = Number(error.status ?? error.statusCode ?? error.response?.status);
143
+ if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
144
+ const code = String(error.code || "").toUpperCase();
145
+ if (["ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "UND_ERR_CONNECT_TIMEOUT", "EPIPE"].includes(code)) return true;
146
+ return /(?:timed? ?out|temporar(?:y|ily)|connection reset|connection refused|service unavailable|rate limit|\b5\d\d\b)/i.test(
147
+ String(error.message || error)
148
+ );
149
+ }
150
+
151
+ function waitForRetry(ms: number, signal?: AbortSignal): Promise<void> {
152
+ if (ms <= 0) return Promise.resolve();
153
+ return new Promise((resolve, reject) => {
154
+ const timer = setTimeout(() => {
155
+ signal?.removeEventListener("abort", onAbort);
156
+ resolve();
157
+ }, ms);
158
+ const onAbort = () => {
159
+ clearTimeout(timer);
160
+ signal?.removeEventListener("abort", onAbort);
161
+ reject(new Error("Tool execution aborted"));
162
+ };
163
+ signal?.addEventListener("abort", onAbort, { once: true });
164
+ if (signal?.aborted) onAbort();
165
+ });
166
+ }
167
+
168
+ async function executeAttempt(
169
+ toolDef: ToolDefinition,
170
+ parsedInput: unknown,
171
+ context: ToolExecutionContext,
172
+ timeoutMs: number | undefined
173
+ ): Promise<unknown> {
174
+ if (!timeoutMs || timeoutMs <= 0) {
175
+ return toolDef.execute(parsedInput as any, context);
176
+ }
177
+
178
+ const controller = new AbortController();
179
+ const parentAbort = () => controller.abort();
180
+ context.signal?.addEventListener("abort", parentAbort, { once: true });
181
+ const executionContext = { ...context, signal: controller.signal };
182
+ let timer: ReturnType<typeof setTimeout> | undefined;
183
+ const startedAt = nowMs();
184
+ const makeTimeoutError = () => {
185
+ const error: any = new Error(`Tool execution timed out after ${timeoutMs}ms`);
186
+ error.code = "ETIMEDOUT";
187
+ error.transient = true;
188
+ return error;
189
+ };
190
+ try {
191
+ const timeout = new Promise<never>((_, reject) => {
192
+ timer = setTimeout(() => {
193
+ controller.abort();
194
+ reject(makeTimeoutError());
195
+ }, timeoutMs);
196
+ });
197
+ const result = await Promise.race([
198
+ Promise.resolve(toolDef.execute(parsedInput as any, executionContext)),
199
+ timeout,
200
+ ]);
201
+ // A resolved promise can beat a timer callback even when the event loop
202
+ // has already crossed the deadline. Enforce the wall-clock deadline too.
203
+ if (nowMs() - startedAt >= timeoutMs) {
204
+ controller.abort();
205
+ throw makeTimeoutError();
206
+ }
207
+ return result;
208
+ } finally {
209
+ if (timer) clearTimeout(timer);
210
+ context.signal?.removeEventListener("abort", parentAbort);
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Executes one or more tool calls in parallel or sequence, handling errors safely
216
+ */
217
+ /**
218
+ * Executes model tool calls with validation, timeout, retry, serialization, and bounded concurrency.
219
+ *
220
+ * @example `const results = await executeToolCalls({ tools, toolCalls });`
221
+ */
222
+ export async function executeToolCalls(
223
+ options: ExecuteToolsOptions
224
+ ): Promise<ToolResultRecord[]> {
225
+ const { tools, toolCalls, agentName, parallel = true, signal, sessionId } = options;
226
+ const globalSemaphore = new Semaphore(DEFAULT_TOOL_CONCURRENCY);
227
+ const perToolSemaphores = new Map<string, Semaphore>();
228
+
229
+ const getToolSemaphore = (name: string, definition: ToolDefinition): Semaphore => {
230
+ const existing = perToolSemaphores.get(name);
231
+ if (existing) return existing;
232
+ const configured = integerOption(
233
+ definition.maxConcurrency
234
+ );
235
+ const limit = configured && configured > 0 ? configured : DEFAULT_TOOL_CONCURRENCY;
236
+ const semaphore = new Semaphore(limit);
237
+ perToolSemaphores.set(name, semaphore);
238
+ return semaphore;
239
+ };
240
+
241
+ const runSingle = async (call: ToolCallRecord): Promise<ToolResultRecord> => {
242
+ const startTime = nowMs();
243
+ const resolved = resolveTool(tools, call.name);
244
+ const toolDef = resolved.definition;
245
+
246
+ if (!toolDef) {
247
+ const suffix = resolved.suggestion ? ` Did you mean '${resolved.suggestion}'?` : "";
248
+ return {
249
+ id: call.id,
250
+ name: call.name,
251
+ result: `Error: Tool '${call.name}' not found.${suffix}`,
252
+ isError: true,
253
+ durationMs: elapsedMs(startTime),
254
+ };
255
+ }
256
+
257
+ let releaseGlobal: (() => void) | undefined;
258
+ let releaseTool: (() => void) | undefined;
259
+ let executionStartTime = startTime;
260
+
261
+ try {
262
+ if (signal?.aborted) {
263
+ throw new Error("Tool execution aborted");
264
+ }
265
+
266
+ releaseGlobal = await globalSemaphore.acquire(signal);
267
+ releaseTool = await getToolSemaphore(resolved.name, toolDef).acquire(signal);
268
+
269
+ const context: ToolExecutionContext = {
270
+ toolCallId: call.id,
271
+ agentName,
272
+ signal,
273
+ sessionId,
274
+ };
275
+
276
+ // Validate input if Zod schema is provided
277
+ let parsedInput = call.arguments;
278
+ if (toolDef.input && typeof (toolDef.input as any).safeParse === "function") {
279
+ const parseRes = (toolDef.input as any).safeParse(call.arguments);
280
+ if (!parseRes.success) {
281
+ const receivedKeys =
282
+ call.arguments && typeof call.arguments === "object" && !Array.isArray(call.arguments)
283
+ ? Object.keys(call.arguments as Record<string, unknown>).join(", ") || "(none)"
284
+ : typeof call.arguments;
285
+ const expectedParams =
286
+ toolDef.parameters && typeof toolDef.parameters === "object"
287
+ ? JSON.stringify(toolDef.parameters).slice(0, 800)
288
+ : undefined;
289
+ const detail = parseRes.error.message;
290
+ const hint = expectedParams
291
+ ? ` Expected parameters: ${expectedParams}.`
292
+ : "";
293
+ return {
294
+ id: call.id,
295
+ name: call.name,
296
+ result:
297
+ `Schema validation error for tool '${call.name}': ${detail}` +
298
+ ` Received argument keys: [${receivedKeys}].` +
299
+ hint +
300
+ ` Fix the arguments and call the tool again with corrected JSON. Do not explain the error, just retry the call.`,
301
+ isError: true,
302
+ durationMs: elapsedMs(startTime),
303
+ };
304
+ }
305
+ parsedInput = parseRes.data;
306
+ }
307
+
308
+ const configuredTries = integerOption(toolDef.maxTries);
309
+ // A positive value is the total attempt count. 0/omitted deliberately
310
+ // means there is no configured retry limit for transient failures.
311
+ const maxAttempts = configuredTries && configuredTries > 0 ? configuredTries : Number.POSITIVE_INFINITY;
312
+ const timeoutMs = numericOption(toolDef.timeoutMs);
313
+ // Telemetry starts when the tool body is about to run, excluding queue
314
+ // wait and schema validation. Retries/backoff remain part of this call.
315
+ executionStartTime = nowMs();
316
+ let rawResult: unknown;
317
+ let attempt = 0;
318
+ while (true) {
319
+ attempt++;
320
+ try {
321
+ rawResult = await executeAttempt(toolDef, parsedInput, context, timeoutMs);
322
+ break;
323
+ } catch (err: any) {
324
+ if (signal?.aborted) throw new Error("Tool execution aborted");
325
+ if (!isTransientError(err) || attempt >= maxAttempts) throw err;
326
+ // A timeout is already a deliberate per-attempt deadline. Retrying
327
+ // it forever by default would make timeoutMs ineffective; callers
328
+ // can opt into timeout retries with an explicit maxTries value.
329
+ if (err?.code === "ETIMEDOUT" && (!configuredTries || configuredTries <= 0)) throw err;
330
+ await waitForRetry(Math.min(30_000, 250 * 2 ** Math.min(attempt - 1, 7)), signal);
331
+ }
332
+ }
333
+
334
+ return {
335
+ id: call.id,
336
+ name: resolved.name,
337
+ result: rawResult !== undefined ? toJsonSafe(rawResult) : "Success",
338
+ isError: false,
339
+ durationMs: elapsedMs(executionStartTime),
340
+ };
341
+ } catch (err: any) {
342
+ return {
343
+ id: call.id,
344
+ name: call.name,
345
+ result: `Error executing ${call.name}: ${err?.message || String(err)}`,
346
+ isError: true,
347
+ durationMs: elapsedMs(executionStartTime),
348
+ };
349
+ } finally {
350
+ releaseTool?.();
351
+ releaseGlobal?.();
352
+ }
353
+ };
354
+
355
+ if (parallel && toolCalls.length > 1) {
356
+ const results = await Promise.all(toolCalls.map((tc) => runSingle(tc)));
357
+ return results;
358
+ }
359
+
360
+ const results: ToolResultRecord[] = [];
361
+ for (const tc of toolCalls) {
362
+ results.push(await runSingle(tc));
363
+ }
364
+ return results;
365
+ }
@@ -0,0 +1,221 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Converts a Zod schema or plain JSON object schema into standard JSON Schema for LLMs
5
+ */
6
+ /**
7
+ * Converts a Zod schema or JSON Schema object into provider-compatible JSON Schema.
8
+ *
9
+ * @example `const parameters = zodToJsonSchema(z.object({ query: z.string() }));`
10
+ */
11
+ export function zodToJsonSchema(schema: unknown): Record<string, unknown> {
12
+ if (!schema) {
13
+ return {
14
+ type: "object",
15
+ properties: {},
16
+ };
17
+ }
18
+
19
+ // If already a plain JSON Schema object
20
+ if (
21
+ typeof schema === "object" &&
22
+ schema !== null &&
23
+ !("parse" in schema) &&
24
+ !("safeParse" in schema) &&
25
+ !("_def" in schema)
26
+ ) {
27
+ return schema as Record<string, unknown>;
28
+ }
29
+
30
+ // Check if Zod schema
31
+ if (
32
+ typeof schema === "object" &&
33
+ schema !== null &&
34
+ ("parse" in schema || "_def" in schema || "~standard" in schema)
35
+ ) {
36
+ try {
37
+ // Use zod's native toJSONSchema if available (Zod 4+)
38
+ if (typeof (z as any).toJSONSchema === "function") {
39
+ const jsonSchema = (z as any).toJSONSchema(schema);
40
+ return cleanJsonSchema(jsonSchema);
41
+ }
42
+ if (typeof (schema as any).toJSONSchema === "function") {
43
+ const jsonSchema = (schema as any).toJSONSchema();
44
+ return cleanJsonSchema(jsonSchema);
45
+ }
46
+ } catch {
47
+ // Fallback manual schema extractor below
48
+ }
49
+
50
+ return extractZodObjectSchema(schema);
51
+ }
52
+
53
+ return {
54
+ type: "object",
55
+ properties: {},
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Recursively removes $schema/$defs/definitions but preserves additionalProperties when explicitly set (S4 fix)
61
+ * S10: resolves $ref against $defs before stripping (agent-accel typebox-helpers inline)
62
+ */
63
+ /**
64
+ * Removes unsupported schema metadata and resolves local `$ref` definitions.
65
+ *
66
+ * @example `const clean = cleanJsonSchema(rawSchema);`
67
+ */
68
+ export function cleanJsonSchema(schema: any, rootDefs?: Record<string, any>): Record<string, unknown> {
69
+ if (typeof schema !== "object" || schema === null) {
70
+ return schema;
71
+ }
72
+ // Capture defs at root for $ref resolution
73
+ const defs = rootDefs ?? schema.$defs ?? schema.definitions;
74
+ // Handle $ref first
75
+ if (schema.$ref && typeof schema.$ref === "string" && defs) {
76
+ const refName = schema.$ref.replace(/^#\/\$defs\//, "").replace(/^#\/definitions\//, "");
77
+ const target = defs[refName];
78
+ if (target) {
79
+ // Merge sibling props (e.g. description) with target
80
+ const { $ref, ...siblings } = schema;
81
+ const resolved = cleanJsonSchema(target, defs);
82
+ return { ...resolved, ...cleanJsonSchema(siblings, defs) } as any;
83
+ }
84
+ }
85
+ const { $schema, $defs, definitions, ...rest } = schema;
86
+
87
+ // Preserve additionalProperties if it was explicitly false, otherwise omit if undefined to keep strictness opt-in
88
+ // (previous version stripped it unconditionally, breaking strict schemas)
89
+
90
+ if (rest.type === "object" && !rest.properties) {
91
+ rest.properties = {};
92
+ }
93
+
94
+ if (rest.properties && typeof rest.properties === "object") {
95
+ const cleanedProps: Record<string, unknown> = {};
96
+ for (const [key, value] of Object.entries(rest.properties)) {
97
+ cleanedProps[key] = cleanJsonSchema(value, defs);
98
+ }
99
+ rest.properties = cleanedProps;
100
+ }
101
+
102
+ if (rest.items) {
103
+ rest.items = cleanJsonSchema(rest.items, defs);
104
+ }
105
+ if (rest.anyOf) rest.anyOf = (rest.anyOf as any[]).map((v: any) => cleanJsonSchema(v, defs));
106
+ if (rest.oneOf) rest.oneOf = (rest.oneOf as any[]).map((v: any) => cleanJsonSchema(v, defs));
107
+ if (rest.allOf) rest.allOf = (rest.allOf as any[]).map((v: any) => cleanJsonSchema(v, defs));
108
+ if (rest.prefixItems) rest.prefixItems = (rest.prefixItems as any[]).map((v: any) => cleanJsonSchema(v, defs));
109
+ // Recursively clean nested $ref inside properties that were not top-level
110
+ for (const k of Object.keys(rest)) {
111
+ if (rest[k] && typeof rest[k] === "object" && !Array.isArray(rest[k]) && (rest[k] as any).$ref) {
112
+ rest[k] = cleanJsonSchema(rest[k], defs);
113
+ }
114
+ }
115
+ return rest;
116
+ }
117
+
118
+ /**
119
+ * Fallback schema extractor if toJSONSchema is not directly called
120
+ */
121
+ function extractZodObjectSchema(zodSchema: any): Record<string, unknown> {
122
+ const shape = zodSchema?.shape || zodSchema?._def?.shape?.() || zodSchema?._def?.shape;
123
+ if (!shape) {
124
+ return {
125
+ type: "object",
126
+ properties: {},
127
+ };
128
+ }
129
+
130
+ const properties: Record<string, unknown> = {};
131
+ const required: string[] = [];
132
+
133
+ for (const [key, propSchema] of Object.entries(shape as Record<string, any>)) {
134
+ const isOptional =
135
+ propSchema.isOptional?.() ||
136
+ propSchema._def?.typeName === "ZodOptional" ||
137
+ propSchema._def?.type === "optional";
138
+
139
+ if (!isOptional) {
140
+ required.push(key);
141
+ }
142
+
143
+ properties[key] = inferZodPropertyType(propSchema);
144
+ }
145
+
146
+ return {
147
+ type: "object",
148
+ properties,
149
+ ...(required.length > 0 ? { required } : {}),
150
+ };
151
+ }
152
+
153
+ function inferZodPropertyType(prop: any): Record<string, unknown> {
154
+ const unwrapped = prop.unwrap?.() || prop._def?.innerType || prop;
155
+ const description = prop.description || unwrapped.description || prop._def?.description;
156
+
157
+ const typeName = unwrapped._def?.typeName || unwrapped._def?.type || unwrapped.constructor?.name || "";
158
+ const tn = String(typeName).toLowerCase();
159
+
160
+ if (tn.includes("number") || tn === "zodnumber") return { type: "number", ...(description ? { description } : {}) };
161
+ if (tn.includes("boolean") || tn === "zodboolean") return { type: "boolean", ...(description ? { description } : {}) };
162
+ if (tn.includes("integer")) return { type: "integer", ...(description ? { description } : {}) };
163
+ if (tn.includes("enum") || tn === "zodenum") {
164
+ const vals = unwrapped._def?.values || unwrapped._def?.entries || [];
165
+ const arr = Array.isArray(vals) ? vals : Object.values(vals);
166
+ return { type: "string", enum: arr, ...(description ? { description } : {}) };
167
+ }
168
+ if (tn.includes("literal")) {
169
+ const val = unwrapped._def?.value;
170
+ return { type: typeof val, enum: [val], ...(description ? { description } : {}) };
171
+ }
172
+ if (tn.includes("array") || tn === "zodarray") {
173
+ const elem = unwrapped._def?.type || unwrapped._def?.element || unwrapped._def?.valueType || {};
174
+ return {
175
+ type: "array",
176
+ items: inferZodPropertyType(elem),
177
+ ...(description ? { description } : {}),
178
+ };
179
+ }
180
+ if (tn.includes("object") || tn === "zodobject") {
181
+ return {
182
+ ...extractZodObjectSchema(unwrapped),
183
+ ...(description ? { description } : {}),
184
+ };
185
+ }
186
+ if (tn.includes("union") || tn === "zodunion") {
187
+ const opts = unwrapped._def?.options || [];
188
+ return { anyOf: opts.map((o: any) => inferZodPropertyType(o)), ...(description ? { description } : {}) };
189
+ }
190
+
191
+ return {
192
+ type: "string",
193
+ ...(description ? { description } : {}),
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Google uses OpenAPI 3.0 Schema and strictly rejects JSON Schema keywords
199
+ * such as $schema, $defs, definitions, and additionalProperties.
200
+ */
201
+ /** Removes JSON Schema keywords rejected by Google OpenAPI 3 schemas. */
202
+ export function stripSchemaForGoogle(schema: any): any {
203
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
204
+ if (Array.isArray(schema)) return schema.map(stripSchemaForGoogle);
205
+ return schema;
206
+ }
207
+ const { $schema, $defs, definitions, additionalProperties, ...rest } = schema as any;
208
+ const out: any = { ...rest };
209
+ if (out.properties && typeof out.properties === "object") {
210
+ const cleaned: any = {};
211
+ for (const [k, v] of Object.entries(out.properties)) {
212
+ cleaned[k] = stripSchemaForGoogle(v);
213
+ }
214
+ out.properties = cleaned;
215
+ }
216
+ if (out.items) out.items = stripSchemaForGoogle(out.items);
217
+ if (out.anyOf) out.anyOf = (out.anyOf as any[]).map(stripSchemaForGoogle);
218
+ if (out.oneOf) out.oneOf = (out.oneOf as any[]).map(stripSchemaForGoogle);
219
+ if (out.allOf) out.allOf = (out.allOf as any[]).map(stripSchemaForGoogle);
220
+ return out;
221
+ }
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ import type {
3
+ ToolDefinition,
4
+ StandardToolDeclaration,
5
+ ToolExecuteFn,
6
+ } from "../types/tool.ts";
7
+ import { zodToJsonSchema } from "./schema.ts";
8
+
9
+ /** Complete configuration accepted by {@link tool}. */
10
+ export interface CreateToolOptions<TInput = any, TOutput = any> {
11
+ /** Optional public name; record keys are used when omitted. */
12
+ name?: string;
13
+ /** Clear description that helps the model decide when to call this tool. */
14
+ description: string;
15
+ /** Zod input schema or a plain JSON Schema object. */
16
+ input?: z.ZodType<TInput> | Record<string, unknown>;
17
+ /** Explicit JSON Schema passed to providers instead of deriving it from `input`. */
18
+ parameters?: Record<string, unknown>;
19
+ /** Provider-specific strict argument validation flag. */
20
+ strict?: boolean;
21
+ /** Maximum best-effort wall-clock time, in milliseconds, for one attempt. `0`/omitted means unlimited. `ctx.signal` enables cooperative cancellation. */
22
+ timeoutMs?: number;
23
+ /** Maximum total attempts for transient failures. `0`/omitted means no configured limit. */
24
+ maxTries?: number | string;
25
+ /** Maximum simultaneous executions of this tool. `0`/omitted uses the global pool. */
26
+ maxConcurrency?: number | string;
27
+ /** Implementation invoked with validated input and execution metadata. */
28
+ execute: ToolExecuteFn<TInput, TOutput>;
29
+ }
30
+
31
+ /**
32
+ * Creates a type-safe tool definition for an Agent.
33
+ *
34
+ * The returned definition can be supplied in `AgentConfig.tools`. The model
35
+ * receives the name, description, and generated JSON Schema; the runtime then
36
+ * validates arguments, applies timeout/retry/concurrency policies, executes
37
+ * the function, and makes its result safe for model context.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const getStatus = tool({
42
+ * name: "get_status",
43
+ * description: "Check user authentication status.",
44
+ * input: z.object({ username: z.string() }),
45
+ * timeoutMs: 5_000,
46
+ * maxTries: 2,
47
+ * maxConcurrency: 2,
48
+ * execute: async ({ username }, ctx) => {
49
+ * return username === "Akshat Dwivedi" ? "Valid" : "Invalid";
50
+ * },
51
+ * });
52
+ * ```
53
+ *
54
+ * @typeParam TInput Type inferred from the input schema.
55
+ * @typeParam TOutput Value returned by the implementation.
56
+ */
57
+ export function tool<TInput = any, TOutput = any>(
58
+ options: CreateToolOptions<TInput, TOutput>
59
+ ): ToolDefinition<TInput, TOutput> {
60
+ return {
61
+ name: options.name,
62
+ description: options.description,
63
+ input: options.input,
64
+ parameters: options.parameters,
65
+ strict: options.strict,
66
+ timeoutMs: options.timeoutMs,
67
+ maxTries: options.maxTries,
68
+ maxConcurrency: options.maxConcurrency,
69
+ execute: options.execute,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Converts internal tool definitions to standard declarations for AI providers.
75
+ *
76
+ * @example `const declarations = toStandardToolDeclarations({ getStatus });`
77
+ */
78
+ export function toStandardToolDeclarations(
79
+ tools: Record<string, ToolDefinition> | ToolDefinition[] | undefined
80
+ ): StandardToolDeclaration[] {
81
+ if (!tools) return [];
82
+
83
+ const list: ToolDefinition[] = Array.isArray(tools)
84
+ ? tools
85
+ : Object.entries(tools).map(([key, def]) => ({
86
+ ...def,
87
+ name: def.name || key,
88
+ }));
89
+
90
+ return list.map((t) => {
91
+ const name = t.name ?? "unnamed_tool";
92
+ const schema = t.parameters ?? (t.input ? zodToJsonSchema(t.input) : { type: "object", properties: {} });
93
+
94
+ return {
95
+ name,
96
+ description: t.description,
97
+ parameters: schema,
98
+ ...(t.strict !== undefined ? { strict: t.strict } : {}),
99
+ };
100
+ });
101
+ }