@radaros/core 0.3.5 → 0.3.6

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 (60) hide show
  1. package/dist/index.d.ts +1407 -0
  2. package/dist/index.js +5269 -0
  3. package/package.json +6 -2
  4. package/src/a2a/a2a-remote-agent.ts +0 -270
  5. package/src/a2a/types.ts +0 -142
  6. package/src/agent/agent.ts +0 -417
  7. package/src/agent/llm-loop.ts +0 -290
  8. package/src/agent/run-context.ts +0 -35
  9. package/src/agent/types.ts +0 -89
  10. package/src/events/event-bus.ts +0 -45
  11. package/src/events/types.ts +0 -16
  12. package/src/guardrails/types.ts +0 -5
  13. package/src/hooks/types.ts +0 -6
  14. package/src/index.ts +0 -157
  15. package/src/knowledge/knowledge-base.ts +0 -146
  16. package/src/logger/logger.ts +0 -249
  17. package/src/mcp/mcp-client.ts +0 -264
  18. package/src/memory/memory.ts +0 -87
  19. package/src/memory/types.ts +0 -13
  20. package/src/memory/user-memory.ts +0 -211
  21. package/src/models/provider.ts +0 -22
  22. package/src/models/providers/anthropic.ts +0 -360
  23. package/src/models/providers/google.ts +0 -386
  24. package/src/models/providers/ollama.ts +0 -211
  25. package/src/models/providers/openai.ts +0 -345
  26. package/src/models/providers/vertex.ts +0 -427
  27. package/src/models/registry.ts +0 -107
  28. package/src/models/types.ts +0 -124
  29. package/src/session/session-manager.ts +0 -75
  30. package/src/session/types.ts +0 -10
  31. package/src/storage/driver.ts +0 -10
  32. package/src/storage/in-memory.ts +0 -44
  33. package/src/storage/mongodb.ts +0 -70
  34. package/src/storage/postgres.ts +0 -81
  35. package/src/storage/sqlite.ts +0 -81
  36. package/src/team/modes.ts +0 -1
  37. package/src/team/team.ts +0 -323
  38. package/src/team/types.ts +0 -26
  39. package/src/toolkits/base.ts +0 -15
  40. package/src/toolkits/duckduckgo.ts +0 -256
  41. package/src/toolkits/gmail.ts +0 -226
  42. package/src/toolkits/hackernews.ts +0 -121
  43. package/src/toolkits/websearch.ts +0 -158
  44. package/src/toolkits/whatsapp.ts +0 -209
  45. package/src/tools/define-tool.ts +0 -22
  46. package/src/tools/tool-executor.ts +0 -221
  47. package/src/tools/types.ts +0 -36
  48. package/src/utils/retry.ts +0 -56
  49. package/src/vector/base.ts +0 -44
  50. package/src/vector/embeddings/google.ts +0 -64
  51. package/src/vector/embeddings/openai.ts +0 -66
  52. package/src/vector/in-memory.ts +0 -115
  53. package/src/vector/mongodb.ts +0 -241
  54. package/src/vector/pgvector.ts +0 -169
  55. package/src/vector/qdrant.ts +0 -203
  56. package/src/vector/types.ts +0 -55
  57. package/src/workflow/step-runner.ts +0 -303
  58. package/src/workflow/types.ts +0 -55
  59. package/src/workflow/workflow.ts +0 -68
  60. package/tsconfig.json +0 -8
@@ -1,158 +0,0 @@
1
- import { z } from "zod";
2
- import type { ToolDef } from "../tools/types.js";
3
- import type { RunContext } from "../agent/run-context.js";
4
- import { Toolkit } from "./base.js";
5
-
6
- export interface WebSearchConfig {
7
- /** Search provider: "tavily" or "serpapi". */
8
- provider: "tavily" | "serpapi";
9
- /** API key for the search provider. Falls back to TAVILY_API_KEY or SERPAPI_API_KEY env vars. */
10
- apiKey?: string;
11
- /** Max results to return per search (default 5). */
12
- maxResults?: number;
13
- }
14
-
15
- /**
16
- * Web Search Toolkit — search the web from your agent.
17
- *
18
- * Supports Tavily and SerpAPI backends.
19
- *
20
- * @example
21
- * ```ts
22
- * const search = new WebSearchToolkit({ provider: "tavily" });
23
- * const agent = new Agent({ tools: [...search.getTools()] });
24
- * ```
25
- */
26
- export class WebSearchToolkit extends Toolkit {
27
- readonly name = "websearch";
28
- private config: WebSearchConfig;
29
-
30
- constructor(config: WebSearchConfig) {
31
- super();
32
- this.config = config;
33
- }
34
-
35
- private getApiKey(): string {
36
- if (this.config.apiKey) return this.config.apiKey;
37
-
38
- const envKey =
39
- this.config.provider === "tavily"
40
- ? process.env.TAVILY_API_KEY
41
- : process.env.SERPAPI_API_KEY;
42
-
43
- if (!envKey) {
44
- const envName =
45
- this.config.provider === "tavily"
46
- ? "TAVILY_API_KEY"
47
- : "SERPAPI_API_KEY";
48
- throw new Error(
49
- `WebSearchToolkit: No API key provided. Set ${envName} env var or pass apiKey in config.`
50
- );
51
- }
52
- return envKey;
53
- }
54
-
55
- getTools(): ToolDef[] {
56
- const self = this;
57
-
58
- return [
59
- {
60
- name: "web_search",
61
- description:
62
- "Search the web for current information. Returns titles, URLs, and snippets from search results.",
63
- parameters: z.object({
64
- query: z.string().describe("The search query"),
65
- maxResults: z
66
- .number()
67
- .optional()
68
- .describe("Maximum number of results (default 5)"),
69
- }),
70
- execute: async (
71
- args: Record<string, unknown>,
72
- _ctx: RunContext
73
- ): Promise<string> => {
74
- const query = args.query as string;
75
- const max =
76
- (args.maxResults as number) ?? self.config.maxResults ?? 5;
77
-
78
- if (self.config.provider === "tavily") {
79
- return self.searchTavily(query, max);
80
- }
81
- return self.searchSerpApi(query, max);
82
- },
83
- },
84
- ];
85
- }
86
-
87
- private async searchTavily(
88
- query: string,
89
- maxResults: number
90
- ): Promise<string> {
91
- const apiKey = this.getApiKey();
92
- const res = await fetch("https://api.tavily.com/search", {
93
- method: "POST",
94
- headers: { "Content-Type": "application/json" },
95
- body: JSON.stringify({
96
- api_key: apiKey,
97
- query,
98
- max_results: maxResults,
99
- include_answer: true,
100
- }),
101
- });
102
-
103
- if (!res.ok) {
104
- throw new Error(`Tavily search failed: ${res.status} ${res.statusText}`);
105
- }
106
-
107
- const data = (await res.json()) as any;
108
- const results: string[] = [];
109
-
110
- if (data.answer) {
111
- results.push(`Answer: ${data.answer}\n`);
112
- }
113
-
114
- for (const r of data.results ?? []) {
115
- results.push(`Title: ${r.title}\nURL: ${r.url}\nSnippet: ${r.content}\n`);
116
- }
117
-
118
- return results.join("\n---\n") || "No results found.";
119
- }
120
-
121
- private async searchSerpApi(
122
- query: string,
123
- maxResults: number
124
- ): Promise<string> {
125
- const apiKey = this.getApiKey();
126
- const params = new URLSearchParams({
127
- q: query,
128
- api_key: apiKey,
129
- engine: "google",
130
- num: String(maxResults),
131
- });
132
-
133
- const res = await fetch(
134
- `https://serpapi.com/search.json?${params.toString()}`
135
- );
136
-
137
- if (!res.ok) {
138
- throw new Error(
139
- `SerpAPI search failed: ${res.status} ${res.statusText}`
140
- );
141
- }
142
-
143
- const data = (await res.json()) as any;
144
- const results: string[] = [];
145
-
146
- if (data.answer_box?.answer) {
147
- results.push(`Answer: ${data.answer_box.answer}\n`);
148
- }
149
-
150
- for (const r of (data.organic_results ?? []).slice(0, maxResults)) {
151
- results.push(
152
- `Title: ${r.title}\nURL: ${r.link}\nSnippet: ${r.snippet ?? ""}\n`
153
- );
154
- }
155
-
156
- return results.join("\n---\n") || "No results found.";
157
- }
158
- }
@@ -1,209 +0,0 @@
1
- import { z } from "zod";
2
- import type { ToolDef } from "../tools/types.js";
3
- import type { RunContext } from "../agent/run-context.js";
4
- import { Toolkit } from "./base.js";
5
-
6
- export interface WhatsAppConfig {
7
- /** WhatsApp Business API access token. Falls back to WHATSAPP_ACCESS_TOKEN env var. */
8
- accessToken?: string;
9
- /** WhatsApp Business phone number ID. Falls back to WHATSAPP_PHONE_NUMBER_ID env var. */
10
- phoneNumberId?: string;
11
- /** API version (default "v22.0"). Falls back to WHATSAPP_VERSION env var. */
12
- version?: string;
13
- /** Default recipient WhatsApp ID. Falls back to WHATSAPP_RECIPIENT_WAID env var. */
14
- recipientWaid?: string;
15
- }
16
-
17
- /**
18
- * WhatsApp Toolkit — send messages via WhatsApp Business Cloud API (Meta).
19
- *
20
- * Uses the WhatsApp Cloud API directly (no Twilio).
21
- * Setup: https://developers.facebook.com/docs/whatsapp/cloud-api/get-started
22
- *
23
- * @example
24
- * ```ts
25
- * const whatsapp = new WhatsAppToolkit({
26
- * accessToken: "...",
27
- * phoneNumberId: "...",
28
- * });
29
- * const agent = new Agent({ tools: [...whatsapp.getTools()] });
30
- * ```
31
- */
32
- export class WhatsAppToolkit extends Toolkit {
33
- readonly name = "whatsapp";
34
- private accessToken: string;
35
- private phoneNumberId: string;
36
- private version: string;
37
- private recipientWaid: string | undefined;
38
-
39
- constructor(config: WhatsAppConfig = {}) {
40
- super();
41
- this.accessToken =
42
- config.accessToken ?? process.env.WHATSAPP_ACCESS_TOKEN ?? "";
43
- this.phoneNumberId =
44
- config.phoneNumberId ?? process.env.WHATSAPP_PHONE_NUMBER_ID ?? "";
45
- this.version =
46
- config.version ?? process.env.WHATSAPP_VERSION ?? "v22.0";
47
- this.recipientWaid =
48
- config.recipientWaid ?? process.env.WHATSAPP_RECIPIENT_WAID;
49
- }
50
-
51
- private getBaseUrl(): string {
52
- return `https://graph.facebook.com/${this.version}/${this.phoneNumberId}/messages`;
53
- }
54
-
55
- private validate(): void {
56
- if (!this.accessToken) {
57
- throw new Error(
58
- "WhatsAppToolkit: accessToken is required. Set WHATSAPP_ACCESS_TOKEN env var or pass in config."
59
- );
60
- }
61
- if (!this.phoneNumberId) {
62
- throw new Error(
63
- "WhatsAppToolkit: phoneNumberId is required. Set WHATSAPP_PHONE_NUMBER_ID env var or pass in config."
64
- );
65
- }
66
- }
67
-
68
- private resolveRecipient(recipient?: string): string {
69
- const r = recipient ?? this.recipientWaid;
70
- if (!r) {
71
- throw new Error(
72
- "WhatsAppToolkit: recipient is required. Provide it in the tool call or set WHATSAPP_RECIPIENT_WAID env var."
73
- );
74
- }
75
- return r.replace(/[^0-9]/g, "");
76
- }
77
-
78
- getTools(): ToolDef[] {
79
- const self = this;
80
-
81
- return [
82
- {
83
- name: "whatsapp_send_text",
84
- description:
85
- "Send a text message to a WhatsApp user via WhatsApp Business Cloud API.",
86
- parameters: z.object({
87
- text: z.string().describe("The text message to send"),
88
- recipient: z
89
- .string()
90
- .optional()
91
- .describe(
92
- "Recipient WhatsApp number with country code (e.g. 919876543210). Uses default if omitted."
93
- ),
94
- previewUrl: z
95
- .boolean()
96
- .optional()
97
- .describe("Enable URL previews in the message (default false)"),
98
- }),
99
- execute: async (
100
- args: Record<string, unknown>,
101
- _ctx: RunContext
102
- ): Promise<string> => {
103
- self.validate();
104
- const recipient = self.resolveRecipient(args.recipient as string);
105
-
106
- const res = await fetch(self.getBaseUrl(), {
107
- method: "POST",
108
- headers: {
109
- Authorization: `Bearer ${self.accessToken}`,
110
- "Content-Type": "application/json",
111
- },
112
- body: JSON.stringify({
113
- messaging_product: "whatsapp",
114
- recipient_type: "individual",
115
- to: recipient,
116
- type: "text",
117
- text: {
118
- preview_url: (args.previewUrl as boolean) ?? false,
119
- body: args.text as string,
120
- },
121
- }),
122
- });
123
-
124
- if (!res.ok) {
125
- const err = await res.text();
126
- throw new Error(
127
- `WhatsApp send failed: ${res.status} ${err}`
128
- );
129
- }
130
-
131
- const data = (await res.json()) as any;
132
- const msgId = data.messages?.[0]?.id ?? "unknown";
133
- return `Message sent successfully to ${recipient}. Message ID: ${msgId}`;
134
- },
135
- },
136
- {
137
- name: "whatsapp_send_template",
138
- description:
139
- "Send a template message to a WhatsApp user. Required for first-time outreach (24-hour messaging window).",
140
- parameters: z.object({
141
- templateName: z
142
- .string()
143
- .describe(
144
- 'The pre-approved template name (e.g. "hello_world")'
145
- ),
146
- recipient: z
147
- .string()
148
- .optional()
149
- .describe(
150
- "Recipient WhatsApp number with country code. Uses default if omitted."
151
- ),
152
- languageCode: z
153
- .string()
154
- .optional()
155
- .describe('Template language code (default "en_US")'),
156
- components: z
157
- .array(z.record(z.any()))
158
- .optional()
159
- .describe(
160
- "Template components for dynamic content (header, body, button parameters)"
161
- ),
162
- }),
163
- execute: async (
164
- args: Record<string, unknown>,
165
- _ctx: RunContext
166
- ): Promise<string> => {
167
- self.validate();
168
- const recipient = self.resolveRecipient(args.recipient as string);
169
- const langCode = (args.languageCode as string) ?? "en_US";
170
-
171
- const template: Record<string, unknown> = {
172
- name: args.templateName as string,
173
- language: { code: langCode },
174
- };
175
-
176
- if (args.components) {
177
- template.components = args.components;
178
- }
179
-
180
- const res = await fetch(self.getBaseUrl(), {
181
- method: "POST",
182
- headers: {
183
- Authorization: `Bearer ${self.accessToken}`,
184
- "Content-Type": "application/json",
185
- },
186
- body: JSON.stringify({
187
- messaging_product: "whatsapp",
188
- recipient_type: "individual",
189
- to: recipient,
190
- type: "template",
191
- template,
192
- }),
193
- });
194
-
195
- if (!res.ok) {
196
- const err = await res.text();
197
- throw new Error(
198
- `WhatsApp template send failed: ${res.status} ${err}`
199
- );
200
- }
201
-
202
- const data = (await res.json()) as any;
203
- const msgId = data.messages?.[0]?.id ?? "unknown";
204
- return `Template message "${args.templateName}" sent to ${recipient}. Message ID: ${msgId}`;
205
- },
206
- },
207
- ];
208
- }
209
- }
@@ -1,22 +0,0 @@
1
- import type { z } from "zod";
2
- import type { RunContext } from "../agent/run-context.js";
3
- import type { ToolDef, ToolResult, ToolCacheConfig } from "./types.js";
4
-
5
- export function defineTool<T extends z.ZodObject<any>>(config: {
6
- name: string;
7
- description: string;
8
- parameters: T;
9
- execute: (
10
- args: z.infer<T>,
11
- ctx: RunContext
12
- ) => Promise<string | ToolResult>;
13
- cache?: ToolCacheConfig;
14
- }): ToolDef {
15
- return {
16
- name: config.name,
17
- description: config.description,
18
- parameters: config.parameters,
19
- execute: config.execute as ToolDef["execute"],
20
- cache: config.cache,
21
- };
22
- }
@@ -1,221 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import type { ToolCall } from "../models/types.js";
3
- import type { RunContext } from "../agent/run-context.js";
4
- import type { ToolDef, ToolCallResult, ToolResult } from "./types.js";
5
-
6
- const _require = createRequire(import.meta.url);
7
-
8
- interface CacheEntry {
9
- result: string | ToolResult;
10
- expiresAt: number;
11
- }
12
-
13
- export class ToolExecutor {
14
- private tools: Map<string, ToolDef>;
15
- private concurrency: number;
16
- private cache = new Map<string, CacheEntry>();
17
- private cachedDefs: Array<{
18
- name: string;
19
- description: string;
20
- parameters: Record<string, unknown>;
21
- }> | null = null;
22
-
23
- constructor(tools: ToolDef[], concurrency: number = 5) {
24
- this.tools = new Map(tools.map((t) => [t.name, t]));
25
- this.concurrency = concurrency;
26
- this.cachedDefs = this.buildToolDefinitions();
27
- }
28
-
29
- clearCache(): void {
30
- this.cache.clear();
31
- }
32
-
33
- private getCacheKey(toolName: string, args: Record<string, unknown>): string {
34
- const sortedArgs = JSON.stringify(args, Object.keys(args).sort());
35
- return `${toolName}:${sortedArgs}`;
36
- }
37
-
38
- private getCached(toolName: string, args: Record<string, unknown>): (string | ToolResult) | undefined {
39
- const tool = this.tools.get(toolName);
40
- if (!tool?.cache) return undefined;
41
-
42
- const key = this.getCacheKey(toolName, args);
43
- const entry = this.cache.get(key);
44
- if (!entry) return undefined;
45
-
46
- if (Date.now() > entry.expiresAt) {
47
- this.cache.delete(key);
48
- return undefined;
49
- }
50
-
51
- return entry.result;
52
- }
53
-
54
- private setCache(toolName: string, args: Record<string, unknown>, result: string | ToolResult): void {
55
- const tool = this.tools.get(toolName);
56
- if (!tool?.cache) return;
57
-
58
- const key = this.getCacheKey(toolName, args);
59
- this.cache.set(key, {
60
- result,
61
- expiresAt: Date.now() + tool.cache.ttl,
62
- });
63
- }
64
-
65
- async executeAll(
66
- toolCalls: ToolCall[],
67
- ctx: RunContext
68
- ): Promise<ToolCallResult[]> {
69
- const results: ToolCallResult[] = [];
70
-
71
- for (let i = 0; i < toolCalls.length; i += this.concurrency) {
72
- const batch = toolCalls.slice(i, i + this.concurrency);
73
- const batchResults = await Promise.allSettled(
74
- batch.map((tc) => this.executeSingle(tc, ctx))
75
- );
76
-
77
- for (let j = 0; j < batchResults.length; j++) {
78
- const settled = batchResults[j];
79
- const tc = batch[j];
80
-
81
- if (settled.status === "fulfilled") {
82
- results.push(settled.value);
83
- } else {
84
- results.push({
85
- toolCallId: tc.id,
86
- toolName: tc.name,
87
- result: `Error: ${settled.reason?.message ?? "Unknown error"}`,
88
- error: settled.reason?.message ?? "Unknown error",
89
- });
90
- }
91
- }
92
- }
93
-
94
- return results;
95
- }
96
-
97
- private async executeSingle(
98
- toolCall: ToolCall,
99
- ctx: RunContext
100
- ): Promise<ToolCallResult> {
101
- const tool = this.tools.get(toolCall.name);
102
- if (!tool) {
103
- return {
104
- toolCallId: toolCall.id,
105
- toolName: toolCall.name,
106
- result: `Error: Tool "${toolCall.name}" not found`,
107
- error: `Tool "${toolCall.name}" not found`,
108
- };
109
- }
110
-
111
- ctx.eventBus.emit("tool.call", {
112
- runId: ctx.runId,
113
- toolName: toolCall.name,
114
- args: toolCall.arguments,
115
- });
116
-
117
- const cachedResult = this.getCached(toolCall.name, toolCall.arguments);
118
- if (cachedResult !== undefined) {
119
- const resultContent =
120
- typeof cachedResult === "string" ? cachedResult : cachedResult.content;
121
-
122
- ctx.eventBus.emit("tool.result", {
123
- runId: ctx.runId,
124
- toolName: toolCall.name,
125
- result: `[cached] ${resultContent}`,
126
- });
127
-
128
- return {
129
- toolCallId: toolCall.id,
130
- toolName: toolCall.name,
131
- result: cachedResult,
132
- };
133
- }
134
-
135
- const parsed = tool.parameters.safeParse(toolCall.arguments);
136
- if (!parsed.success) {
137
- const errMsg = `Invalid arguments: ${parsed.error.message}`;
138
- const result: ToolCallResult = {
139
- toolCallId: toolCall.id,
140
- toolName: toolCall.name,
141
- result: errMsg,
142
- error: errMsg,
143
- };
144
-
145
- ctx.eventBus.emit("tool.result", {
146
- runId: ctx.runId,
147
- toolName: toolCall.name,
148
- result: errMsg,
149
- });
150
-
151
- return result;
152
- }
153
-
154
- const rawResult = await tool.execute(parsed.data, ctx);
155
- const resultContent =
156
- typeof rawResult === "string" ? rawResult : rawResult.content;
157
-
158
- this.setCache(toolCall.name, toolCall.arguments, rawResult);
159
-
160
- ctx.eventBus.emit("tool.result", {
161
- runId: ctx.runId,
162
- toolName: toolCall.name,
163
- result: resultContent,
164
- });
165
-
166
- return {
167
- toolCallId: toolCall.id,
168
- toolName: toolCall.name,
169
- result: rawResult,
170
- };
171
- }
172
-
173
- getToolDefinitions(): Array<{
174
- name: string;
175
- description: string;
176
- parameters: Record<string, unknown>;
177
- }> {
178
- if (this.cachedDefs) return this.cachedDefs;
179
- this.cachedDefs = this.buildToolDefinitions();
180
- return this.cachedDefs;
181
- }
182
-
183
- private buildToolDefinitions(): Array<{
184
- name: string;
185
- description: string;
186
- parameters: Record<string, unknown>;
187
- }> {
188
- const { zodToJsonSchema } = _require("zod-to-json-schema");
189
- const defs: Array<{
190
- name: string;
191
- description: string;
192
- parameters: Record<string, unknown>;
193
- }> = [];
194
-
195
- for (const tool of this.tools.values()) {
196
- if (tool.rawJsonSchema) {
197
- defs.push({
198
- name: tool.name,
199
- description: tool.description,
200
- parameters: tool.rawJsonSchema,
201
- });
202
- } else {
203
- const jsonSchema = zodToJsonSchema(tool.parameters, {
204
- target: "jsonSchema7",
205
- $refStrategy: "none",
206
- }) as Record<string, unknown>;
207
-
208
- delete jsonSchema["$schema"];
209
- delete jsonSchema["additionalProperties"];
210
-
211
- defs.push({
212
- name: tool.name,
213
- description: tool.description,
214
- parameters: jsonSchema,
215
- });
216
- }
217
- }
218
-
219
- return defs;
220
- }
221
- }
@@ -1,36 +0,0 @@
1
- import type { z } from "zod";
2
- import type { RunContext } from "../agent/run-context.js";
3
-
4
- export interface Artifact {
5
- type: string;
6
- data: unknown;
7
- mimeType?: string;
8
- }
9
-
10
- export interface ToolResult {
11
- content: string;
12
- artifacts?: Artifact[];
13
- }
14
-
15
- export interface ToolCacheConfig {
16
- /** Time-to-live in milliseconds. Cached results expire after this duration. */
17
- ttl: number;
18
- }
19
-
20
- export interface ToolDef {
21
- name: string;
22
- description: string;
23
- parameters: z.ZodObject<any>;
24
- execute: (args: Record<string, unknown>, ctx: RunContext) => Promise<string | ToolResult>;
25
- /** Raw JSON Schema to send to the LLM, bypassing Zod-to-JSON conversion (used by MCP tools). */
26
- rawJsonSchema?: Record<string, unknown>;
27
- /** Enable result caching for this tool. */
28
- cache?: ToolCacheConfig;
29
- }
30
-
31
- export interface ToolCallResult {
32
- toolCallId: string;
33
- toolName: string;
34
- result: string | ToolResult;
35
- error?: string;
36
- }
@@ -1,56 +0,0 @@
1
- export interface RetryConfig {
2
- maxRetries: number;
3
- initialDelayMs: number;
4
- maxDelayMs: number;
5
- retryableErrors?: (error: unknown) => boolean;
6
- }
7
-
8
- const DEFAULT_CONFIG: RetryConfig = {
9
- maxRetries: 3,
10
- initialDelayMs: 500,
11
- maxDelayMs: 10_000,
12
- retryableErrors: isRetryableError,
13
- };
14
-
15
- function isRetryableError(error: unknown): boolean {
16
- if (error && typeof error === "object") {
17
- const status = (error as any).status ?? (error as any).statusCode;
18
- if (status === 429 || (status >= 500 && status < 600)) return true;
19
-
20
- const code = (error as any).code;
21
- if (code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ENOTFOUND") return true;
22
-
23
- const msg = (error as any).message;
24
- if (typeof msg === "string" && /rate.limit|too.many.requests|overloaded/i.test(msg)) return true;
25
- }
26
- return false;
27
- }
28
-
29
- function sleep(ms: number): Promise<void> {
30
- return new Promise((resolve) => setTimeout(resolve, ms));
31
- }
32
-
33
- export async function withRetry<T>(
34
- fn: () => Promise<T>,
35
- config?: Partial<RetryConfig>
36
- ): Promise<T> {
37
- const cfg = { ...DEFAULT_CONFIG, ...config };
38
- let lastError: unknown;
39
-
40
- for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
41
- try {
42
- return await fn();
43
- } catch (error) {
44
- lastError = error;
45
- if (attempt >= cfg.maxRetries || !cfg.retryableErrors!(error)) throw error;
46
-
47
- const delay = Math.min(
48
- cfg.initialDelayMs * Math.pow(2, attempt) + Math.random() * 200,
49
- cfg.maxDelayMs
50
- );
51
- await sleep(delay);
52
- }
53
- }
54
-
55
- throw lastError;
56
- }