agentnet 0.0.1
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.
- package/LICENSE.txt +202 -0
- package/README.md +488 -0
- package/package.json +23 -0
- package/src/agent/agent-loader.js +274 -0
- package/src/agent/agent.js +416 -0
- package/src/agent/client.js +14 -0
- package/src/agent/executor.js +320 -0
- package/src/agent/runtime.js +142 -0
- package/src/agent/runtimes/nats.js +379 -0
- package/src/errors/index.js +195 -0
- package/src/examples/agents-smartness.yaml +308 -0
- package/src/examples/agents.yaml +394 -0
- package/src/examples/def.js +74 -0
- package/src/examples/def2.js +65 -0
- package/src/examples/def3.js +67 -0
- package/src/examples/simple.js +103 -0
- package/src/index.js +115 -0
- package/src/llm/gemini.js +155 -0
- package/src/llm/gpt.js +155 -0
- package/src/store/store.js +167 -0
- package/src/utils/logger.js +209 -0
- package/src/utils/store.js +212 -0
- package/src/utils/validation.js +287 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NATS Runtime implementation for agent communication
|
|
3
|
+
*/
|
|
4
|
+
import { Message } from '../../index.js';
|
|
5
|
+
import { logger } from '../../utils/logger.js';
|
|
6
|
+
import {
|
|
7
|
+
TransportError,
|
|
8
|
+
DiscoveryError,
|
|
9
|
+
HandoffError,
|
|
10
|
+
TimeoutError,
|
|
11
|
+
withTimeout,
|
|
12
|
+
withRetry
|
|
13
|
+
} from '../../errors/index.js';
|
|
14
|
+
|
|
15
|
+
const HEARTBEAT_INTERVAL = 1000;
|
|
16
|
+
const TIMEOUT_TASK_REQUEST = 60000;
|
|
17
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sets up discovery subscription to find other agents
|
|
21
|
+
*/
|
|
22
|
+
async function setupDiscoverySubscription(nc, discoveryTopic, agentName, discoveredAgents) {
|
|
23
|
+
let discoverySub;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
discoverySub = nc.subscribe(discoveryTopic);
|
|
27
|
+
logger.info(`Agent ${agentName} subscribed to discovery topic ${discoveryTopic}`);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new DiscoveryError(
|
|
30
|
+
`Failed to subscribe to discovery topic ${discoveryTopic}`,
|
|
31
|
+
{ agentName, topic: discoveryTopic },
|
|
32
|
+
error
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const handleDiscovery = async () => {
|
|
37
|
+
try {
|
|
38
|
+
for await (const m of discoverySub) {
|
|
39
|
+
try {
|
|
40
|
+
const payloadSetup = JSON.parse(m.string());
|
|
41
|
+
|
|
42
|
+
// Validate the payload structure
|
|
43
|
+
if (!payloadSetup.agentName || !Array.isArray(payloadSetup.schemas)) {
|
|
44
|
+
logger.warn('Invalid discovery payload received', { payload: payloadSetup });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const schema of payloadSetup.schemas) {
|
|
49
|
+
// Skip invalid schemas
|
|
50
|
+
if (!schema || !schema.name) {
|
|
51
|
+
logger.warn('Invalid schema in discovery payload', { schema });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const agentKey = `${payloadSetup.agentName}-${schema.name}`;
|
|
56
|
+
|
|
57
|
+
if (payloadSetup.agentName !== agentName && !discoveredAgents[agentKey]) {
|
|
58
|
+
logger.info(`${agentName} discovered agent capability: ${payloadSetup.agentName} with capability ${schema.name}`);
|
|
59
|
+
|
|
60
|
+
const handoffFunction = async (conversation, state, input) => {
|
|
61
|
+
try {
|
|
62
|
+
// Use withTimeout to ensure handoffs don't hang
|
|
63
|
+
return await withTimeout(
|
|
64
|
+
async () => {
|
|
65
|
+
try {
|
|
66
|
+
const response = await withRetry(
|
|
67
|
+
async () => {
|
|
68
|
+
const message = new Message({
|
|
69
|
+
session: state,
|
|
70
|
+
content: input
|
|
71
|
+
})
|
|
72
|
+
const req = await nc.request(
|
|
73
|
+
payloadSetup.agentName,
|
|
74
|
+
message.serialize(),
|
|
75
|
+
{ timeout: TIMEOUT_TASK_REQUEST }
|
|
76
|
+
);
|
|
77
|
+
return req.string();
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
maxRetries: 2,
|
|
81
|
+
onRetry: ({ attempt }) => {
|
|
82
|
+
logger.warn(`Retrying handoff attempt ${attempt} to ${payloadSetup.agentName}`, {
|
|
83
|
+
schema: schema.name
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
return response;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw new HandoffError(
|
|
91
|
+
`Handoff to agent ${payloadSetup.agentName} failed: ${error.message}`,
|
|
92
|
+
agentName,
|
|
93
|
+
payloadSetup.agentName,
|
|
94
|
+
{ schemaName: schema.name }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
TIMEOUT_TASK_REQUEST,
|
|
99
|
+
`handoff to ${payloadSetup.agentName}`
|
|
100
|
+
);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logger.error(`Handoff error to ${payloadSetup.agentName}`, {
|
|
103
|
+
error,
|
|
104
|
+
schema: schema.name
|
|
105
|
+
});
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
discoveredAgents[agentKey] = {
|
|
111
|
+
name: schema.name,
|
|
112
|
+
schema: schema,
|
|
113
|
+
function: handoffFunction
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.error('Error processing discovery message', { error });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error("Discovery subscription error", { error });
|
|
123
|
+
|
|
124
|
+
// Attempt to resubscribe if the connection is still active
|
|
125
|
+
if (nc.isConnected()) {
|
|
126
|
+
logger.info('Attempting to resubscribe to discovery topic');
|
|
127
|
+
try {
|
|
128
|
+
discoverySub = nc.subscribe(discoveryTopic);
|
|
129
|
+
handleDiscovery(); // Restart the handling process
|
|
130
|
+
} catch (resubError) {
|
|
131
|
+
logger.error('Failed to resubscribe to discovery topic', { error: resubError });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Start processing discovery messages
|
|
138
|
+
handleDiscovery();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Sets up a heartbeat to announce this agent's capabilities
|
|
143
|
+
*/
|
|
144
|
+
function setupDiscoveryHeartbeat(nc, discoveryTopic, agentName, discoverySchemas) {
|
|
145
|
+
let consecutiveErrors = 0;
|
|
146
|
+
|
|
147
|
+
return setInterval(async () => {
|
|
148
|
+
try {
|
|
149
|
+
const heartbeatPayload = {
|
|
150
|
+
type: 'discovery',
|
|
151
|
+
agentName: agentName,
|
|
152
|
+
schemas: discoverySchemas
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
await nc.publish(discoveryTopic, JSON.stringify(heartbeatPayload));
|
|
156
|
+
|
|
157
|
+
// Reset error counter on success
|
|
158
|
+
if (consecutiveErrors > 0) {
|
|
159
|
+
logger.info(`Discovery heartbeat resumed for ${agentName}`);
|
|
160
|
+
consecutiveErrors = 0;
|
|
161
|
+
}
|
|
162
|
+
} catch (error) {
|
|
163
|
+
consecutiveErrors++;
|
|
164
|
+
|
|
165
|
+
// Log with increasing severity based on consecutive failures
|
|
166
|
+
if (consecutiveErrors > 5) {
|
|
167
|
+
logger.error(`Failed to publish discovery heartbeat (${consecutiveErrors} consecutive failures)`, {
|
|
168
|
+
error,
|
|
169
|
+
agentName,
|
|
170
|
+
topic: discoveryTopic
|
|
171
|
+
});
|
|
172
|
+
} else {
|
|
173
|
+
logger.warn(`Error publishing discovery heartbeat (attempt ${consecutiveErrors})`, {
|
|
174
|
+
error: error.message,
|
|
175
|
+
agentName
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}, HEARTBEAT_INTERVAL);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Creates a task handler for incoming requests
|
|
184
|
+
*/
|
|
185
|
+
async function createTaskHandler(nc, agentName, processingFunction) {
|
|
186
|
+
let taskSub;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
taskSub = nc.subscribe(agentName, { queue: agentName });
|
|
190
|
+
logger.info(`Agent ${agentName} subscribed for task handling`);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new TransportError(
|
|
193
|
+
`Failed to subscribe for task handling: ${error.message}`,
|
|
194
|
+
'NATS',
|
|
195
|
+
{ agentName }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
for await (const m of taskSub) {
|
|
201
|
+
let payload;
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Parse and validate the payload
|
|
205
|
+
payload = m.json();
|
|
206
|
+
if (!payload || typeof payload !== 'object') {
|
|
207
|
+
throw new Error('Invalid payload: not a JSON object');
|
|
208
|
+
}
|
|
209
|
+
const message = new Message(payload)
|
|
210
|
+
const input = message.getContent()
|
|
211
|
+
const session = message.getSession()
|
|
212
|
+
|
|
213
|
+
logger.debug(`Received task request for ${agentName}`, {
|
|
214
|
+
inputPreview: typeof input === 'string'
|
|
215
|
+
? input.substring(0, 100)
|
|
216
|
+
: 'Non-string input'
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Process the task with timeout
|
|
220
|
+
const response = await withTimeout(
|
|
221
|
+
async () => processingFunction(message),
|
|
222
|
+
TIMEOUT_TASK_REQUEST * 2, // Double the timeout for processing
|
|
223
|
+
`task processing for ${agentName}`
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Respond with the result
|
|
227
|
+
await m.respond(response.serialize());
|
|
228
|
+
|
|
229
|
+
logger.debug(`Completed task request for ${agentName}`);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.error("Error processing task", {
|
|
232
|
+
error,
|
|
233
|
+
agentName,
|
|
234
|
+
inputPreview: payload && input
|
|
235
|
+
? (typeof input === 'string'
|
|
236
|
+
? input.substring(0, 100)
|
|
237
|
+
: 'Non-string input')
|
|
238
|
+
: 'No input'
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Send error response back
|
|
242
|
+
try {
|
|
243
|
+
await m.respond(JSON.stringify({
|
|
244
|
+
error: true,
|
|
245
|
+
message: error.message,
|
|
246
|
+
type: error.name || 'Error'
|
|
247
|
+
}));
|
|
248
|
+
} catch (respondError) {
|
|
249
|
+
logger.error("Failed to send error response", { error: respondError });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
logger.error("Task subscription error", { error, agentName });
|
|
255
|
+
|
|
256
|
+
// Attempt to resubscribe if the connection is still active
|
|
257
|
+
if (nc.isConnected()) {
|
|
258
|
+
logger.info('Attempting to resubscribe for task handling');
|
|
259
|
+
try {
|
|
260
|
+
const newTaskSub = nc.subscribe(agentName, { queue: agentName });
|
|
261
|
+
// Start a new processing loop
|
|
262
|
+
createTaskHandler(nc, agentName, processingFunction);
|
|
263
|
+
} catch (resubError) {
|
|
264
|
+
logger.error('Failed to resubscribe for task handling', { error: resubError });
|
|
265
|
+
throw new TransportError(
|
|
266
|
+
"Failed to resubscribe for task handling",
|
|
267
|
+
'NATS',
|
|
268
|
+
{ agentName, originalError: error.message, resubError: resubError.message }
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
throw new TransportError(
|
|
273
|
+
"NATS connection lost during task handling",
|
|
274
|
+
'NATS',
|
|
275
|
+
{ agentName }
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Safely connects to NATS with retry logic
|
|
283
|
+
*/
|
|
284
|
+
async function safeConnect(instance, options = {}) {
|
|
285
|
+
const { maxRetries = MAX_RECONNECT_ATTEMPTS } = options;
|
|
286
|
+
|
|
287
|
+
return withRetry(
|
|
288
|
+
async () => {
|
|
289
|
+
try {
|
|
290
|
+
return await instance.connect();
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw new TransportError(
|
|
293
|
+
`Failed to connect to NATS: ${error.message}`,
|
|
294
|
+
'NATS',
|
|
295
|
+
{ details: error.message }
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
maxRetries,
|
|
301
|
+
baseDelayMs: 500,
|
|
302
|
+
onRetry: ({ attempt }) => {
|
|
303
|
+
logger.warn(`NATS connect attempt ${attempt}/${maxRetries} failed, retrying...`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Creates a NATS-based runtime for agent communication
|
|
311
|
+
*/
|
|
312
|
+
export async function NatsIOAgentRuntime(agentName, ioInterfaces, discoverySchemas) {
|
|
313
|
+
if (ioInterfaces.length > 1) {
|
|
314
|
+
throw new TransportError(
|
|
315
|
+
'Only one IO Nats interface is supported',
|
|
316
|
+
'NATS',
|
|
317
|
+
{ agentName, interfacesCount: ioInterfaces.length }
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (ioInterfaces.length === 0) {
|
|
322
|
+
logger.warn(`No NATS interfaces provided for agent ${agentName}, creating passive runtime`);
|
|
323
|
+
return { handleTask: async () => {}, discoveredAgents: [] };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const io = ioInterfaces[0];
|
|
327
|
+
const intervals = [];
|
|
328
|
+
const discoveredAgents = {};
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
// Connect to NATS with retry logic
|
|
332
|
+
logger.info(`Connecting to NATS for agent ${agentName}`);
|
|
333
|
+
const nc = await safeConnect(io.instance);
|
|
334
|
+
|
|
335
|
+
// Verify configuration
|
|
336
|
+
if (!io.config || !io.config.bindings || !io.config.bindings.discoveryTopic) {
|
|
337
|
+
throw new TransportError(
|
|
338
|
+
'Missing required NATS configuration: discoveryTopic',
|
|
339
|
+
'NATS',
|
|
340
|
+
{ agentName }
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const discoveryTopic = io.config.bindings.discoveryTopic;
|
|
345
|
+
logger.info(`Agent ${agentName} initialized with discovery topic ${discoveryTopic}`);
|
|
346
|
+
|
|
347
|
+
// Step 1. Subscribe to discovery topic
|
|
348
|
+
await setupDiscoverySubscription(nc, discoveryTopic, agentName, discoveredAgents);
|
|
349
|
+
|
|
350
|
+
// Step 2. Publish discovery heartbeat
|
|
351
|
+
const interval = setupDiscoveryHeartbeat(nc, discoveryTopic, agentName, discoverySchemas);
|
|
352
|
+
intervals.push(interval);
|
|
353
|
+
|
|
354
|
+
// Step 3. Create task handler
|
|
355
|
+
const handleTask = async (fn) => {
|
|
356
|
+
if (typeof fn !== 'function') {
|
|
357
|
+
throw new Error('Task handler must be a function');
|
|
358
|
+
}
|
|
359
|
+
await createTaskHandler(nc, agentName, fn);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return { handleTask, discoveredAgents };
|
|
363
|
+
} catch (error) {
|
|
364
|
+
// Clean up intervals if connection fails
|
|
365
|
+
intervals.forEach(clearInterval);
|
|
366
|
+
|
|
367
|
+
// Enhance the error with context if it's not already a TransportError
|
|
368
|
+
if (!(error instanceof TransportError)) {
|
|
369
|
+
error = new TransportError(
|
|
370
|
+
`Failed to initialize NATS runtime: ${error.message}`,
|
|
371
|
+
'NATS',
|
|
372
|
+
{ agentName }
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
logger.error("NATS runtime initialization failed", { error, agentName });
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base error class for all SmartAgent errors
|
|
3
|
+
*/
|
|
4
|
+
export class SmartAgentError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'SmartAgentError';
|
|
8
|
+
Error.captureStackTrace(this, this.constructor);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration related errors
|
|
14
|
+
*/
|
|
15
|
+
export class ConfigurationError extends SmartAgentError {
|
|
16
|
+
constructor(message, configContext = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'ConfigurationError';
|
|
19
|
+
this.configContext = configContext;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tool execution related errors
|
|
25
|
+
*/
|
|
26
|
+
export class ToolExecutionError extends SmartAgentError {
|
|
27
|
+
constructor(message, toolName, input, cause = null) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'ToolExecutionError';
|
|
30
|
+
this.toolName = toolName;
|
|
31
|
+
this.input = input;
|
|
32
|
+
this.cause = cause;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Transport/IO related errors
|
|
38
|
+
*/
|
|
39
|
+
export class TransportError extends SmartAgentError {
|
|
40
|
+
constructor(message, transportType, details = {}) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'TransportError';
|
|
43
|
+
this.transportType = transportType;
|
|
44
|
+
this.details = details;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Errors related to LLM API calls
|
|
50
|
+
*/
|
|
51
|
+
export class LLMError extends SmartAgentError {
|
|
52
|
+
constructor(message, provider, details = {}) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = 'LLMError';
|
|
55
|
+
this.provider = provider;
|
|
56
|
+
this.details = details;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Errors related to agent discovery
|
|
62
|
+
*/
|
|
63
|
+
export class DiscoveryError extends SmartAgentError {
|
|
64
|
+
constructor(message, discoveryContext = {}) {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = 'DiscoveryError';
|
|
67
|
+
this.discoveryContext = discoveryContext;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Errors related to agent handoffs
|
|
73
|
+
*/
|
|
74
|
+
export class HandoffError extends SmartAgentError {
|
|
75
|
+
constructor(message, sourceAgent, targetAgent, payload = {}) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.name = 'HandoffError';
|
|
78
|
+
this.sourceAgent = sourceAgent;
|
|
79
|
+
this.targetAgent = targetAgent;
|
|
80
|
+
this.payload = payload;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Errors during agent compilation
|
|
86
|
+
*/
|
|
87
|
+
export class CompilationError extends SmartAgentError {
|
|
88
|
+
constructor(message, agentName, cause = null) {
|
|
89
|
+
super(message);
|
|
90
|
+
this.name = 'CompilationError';
|
|
91
|
+
this.agentName = agentName;
|
|
92
|
+
this.cause = cause;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validation errors
|
|
98
|
+
*/
|
|
99
|
+
export class ValidationError extends SmartAgentError {
|
|
100
|
+
constructor(message, errors = []) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.name = 'ValidationError';
|
|
103
|
+
this.errors = errors;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Timeouts in agent operations
|
|
109
|
+
*/
|
|
110
|
+
export class TimeoutError extends SmartAgentError {
|
|
111
|
+
constructor(message, operation, timeoutMs) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = 'TimeoutError';
|
|
114
|
+
this.operation = operation;
|
|
115
|
+
this.timeoutMs = timeoutMs;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Helper to wrap async functions with timeout
|
|
121
|
+
* @param {Function} fn - Async function to execute
|
|
122
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
123
|
+
* @param {string} operationName - Name of the operation for error context
|
|
124
|
+
*/
|
|
125
|
+
export async function withTimeout(fn, timeoutMs, operationName) {
|
|
126
|
+
let timeoutId;
|
|
127
|
+
|
|
128
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
129
|
+
timeoutId = setTimeout(() => {
|
|
130
|
+
reject(new TimeoutError(
|
|
131
|
+
`Operation '${operationName}' timed out after ${timeoutMs}ms`,
|
|
132
|
+
operationName,
|
|
133
|
+
timeoutMs
|
|
134
|
+
));
|
|
135
|
+
}, timeoutMs);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
return await Promise.race([fn(), timeoutPromise]);
|
|
140
|
+
} finally {
|
|
141
|
+
clearTimeout(timeoutId);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Helper for retrying operations with exponential backoff
|
|
147
|
+
* @param {Function} fn - Async function to retry
|
|
148
|
+
* @param {Object} options - Retry options
|
|
149
|
+
*/
|
|
150
|
+
export async function withRetry(fn, options = {}) {
|
|
151
|
+
const {
|
|
152
|
+
maxRetries = 3,
|
|
153
|
+
baseDelayMs = 300,
|
|
154
|
+
maxDelayMs = 5000,
|
|
155
|
+
retryableErrors = [TransportError, TimeoutError],
|
|
156
|
+
onRetry = () => {}
|
|
157
|
+
} = options;
|
|
158
|
+
|
|
159
|
+
let lastError;
|
|
160
|
+
|
|
161
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
162
|
+
try {
|
|
163
|
+
return await fn();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
lastError = error;
|
|
166
|
+
|
|
167
|
+
const isRetryable = retryableErrors.some(ErrorClass =>
|
|
168
|
+
error instanceof ErrorClass
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (!isRetryable || attempt >= maxRetries) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Calculate delay with exponential backoff and jitter
|
|
176
|
+
const delay = Math.min(
|
|
177
|
+
baseDelayMs * 2 ** attempt * (0.75 + Math.random() * 0.5),
|
|
178
|
+
maxDelayMs
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Call onRetry callback
|
|
182
|
+
onRetry({
|
|
183
|
+
error,
|
|
184
|
+
attempt: attempt + 1,
|
|
185
|
+
maxRetries,
|
|
186
|
+
delayMs: delay
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Wait before retrying
|
|
190
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw lastError;
|
|
195
|
+
}
|