UNPKG

@measey/mycoder-agent

Version:

Agent module for mycoder - an AI-powered software development assistant

212 lines 10.3 kB
import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; const parameterSchema = z.object({ method: z .string() .describe('HTTP method to use (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)'), url: z.string().describe('URL to make the request to'), params: z .record(z.any()) .optional() .describe('Optional query parameters to append to the URL'), body: z .record(z.any()) .optional() .describe('Optional request body (for POST, PUT, PATCH requests)'), headers: z.record(z.string()).optional().describe('Optional request headers'), // New parameters for error handling maxRetries: z .number() .min(0) .max(5) .optional() .describe('Maximum number of retries for 4xx errors (default: 3)'), retryDelay: z .number() .min(100) .max(30000) .optional() .describe('Initial delay in ms before retrying (default: 1000)'), slowMode: z .boolean() .optional() .describe('Enable slow mode to avoid rate limits (default: false)'), }); const returnSchema = z .object({ status: z.number(), statusText: z.string(), headers: z.record(z.string()), body: z.union([z.string(), z.record(z.any())]), retries: z.number().optional(), slowModeEnabled: z.boolean().optional(), }) .describe('HTTP response including status, headers, and body'); /** * Sleep for a specified number of milliseconds * @param ms Milliseconds to sleep * @internal */ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); /** * Calculate exponential backoff delay with jitter * @param attempt Current attempt number (0-based) * @param baseDelay Base delay in milliseconds * @returns Delay in milliseconds with jitter */ const calculateBackoff = (attempt, baseDelay) => { // Calculate exponential backoff: baseDelay * 2^attempt const expBackoff = baseDelay * Math.pow(2, attempt); // Add jitter (±20%) to avoid thundering herd problem const jitter = expBackoff * 0.2 * (Math.random() * 2 - 1); // Return backoff with jitter, capped at 30 seconds return Math.min(expBackoff + jitter, 30000); }; export const fetchTool = { name: 'fetch', description: 'Executes HTTP requests using native Node.js fetch API, for using APIs, not for browsing the web.', logPrefix: '🌐', parameters: parameterSchema, returns: returnSchema, parametersJsonSchema: zodToJsonSchema(parameterSchema), returnsJsonSchema: zodToJsonSchema(returnSchema), execute: async ({ method, url, params, body, headers, maxRetries = 3, retryDelay = 1000, slowMode = false, }, { logger }) => { let retries = 0; let slowModeEnabled = slowMode; let lastError = null; while (retries <= maxRetries) { try { // If in slow mode, add a delay before making the request if (slowModeEnabled && retries > 0) { const slowModeDelay = 2000; // 2 seconds delay in slow mode logger.debug(`Slow mode enabled, waiting ${slowModeDelay}ms before request`); await sleep(slowModeDelay); } logger.debug(`Starting ${method} request to ${url}${retries > 0 ? ` (retry ${retries}/${maxRetries})` : ''}`); const urlObj = new URL(url); // Add query parameters if (params) { logger.debug('Adding query parameters:', params); Object.entries(params).forEach(([key, value]) => urlObj.searchParams.append(key, value)); } // Prepare request options const options = { method, headers: { ...(body && !['GET', 'HEAD'].includes(method) && { 'content-type': 'application/json', }), ...headers, }, ...(body && !['GET', 'HEAD'].includes(method) && { body: JSON.stringify(body), }), }; logger.debug('Request options:', options); const response = await fetch(urlObj.toString(), options); logger.debug(`Request completed with status ${response.status} ${response.statusText}`); // Handle different 4xx errors if (response.status >= 400 && response.status < 500) { if (response.status === 400) { // Bad Request - might be a temporary issue or problem with the request if (retries < maxRetries) { retries++; const delay = calculateBackoff(retries, retryDelay); logger.warn(`400 Bad Request Error. Retrying in ${Math.round(delay)}ms (${retries}/${maxRetries})`); await sleep(delay); continue; } else { // Throw an error after max retries for bad request throw new Error(`Failed after ${maxRetries} retries: Bad Request (400)`); } } else if (response.status === 429) { // Rate Limit Exceeded - implement exponential backoff if (retries < maxRetries) { retries++; // Enable slow mode after the first rate limit error slowModeEnabled = true; // Get retry-after header if available, or use exponential backoff const retryAfter = response.headers.get('retry-after'); let delay; if (retryAfter) { // If retry-after contains a timestamp if (isNaN(Number(retryAfter))) { const retryDate = new Date(retryAfter).getTime(); delay = retryDate - Date.now(); } else { // If retry-after contains seconds delay = parseInt(retryAfter, 10) * 1000; } } else { // Use exponential backoff if no retry-after header delay = calculateBackoff(retries, retryDelay); } logger.warn(`429 Rate Limit Exceeded. Enabling slow mode and retrying in ${Math.round(delay)}ms (${retries}/${maxRetries})`); await sleep(delay); continue; } else { // Throw an error after max retries for rate limit throw new Error(`Failed after ${maxRetries} retries: Rate Limit Exceeded (429)`); } } else if (retries < maxRetries) { // Other 4xx errors might be temporary, retry with backoff retries++; const delay = calculateBackoff(retries, retryDelay); logger.warn(`${response.status} Error. Retrying in ${Math.round(delay)}ms (${retries}/${maxRetries})`); await sleep(delay); continue; } else { // Throw an error after max retries for other 4xx errors throw new Error(`Failed after ${maxRetries} retries: HTTP ${response.status} (${response.statusText})`); } } const contentType = response.headers.get('content-type'); const responseBody = contentType?.includes('application/json') ? await response.json() : await response.text(); logger.debug('Response content-type:', contentType); return { status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers), body: responseBody, retries, slowModeEnabled, }; } catch (error) { lastError = error; logger.error(`Request failed: ${error}`); if (retries < maxRetries) { retries++; const delay = calculateBackoff(retries, retryDelay); logger.warn(`Network error. Retrying in ${Math.round(delay)}ms (${retries}/${maxRetries})`); await sleep(delay); } else { throw new Error(`Failed after ${maxRetries} retries: ${lastError.message}`); } } } // This should never be reached due to the throw above, but TypeScript needs it throw new Error(`Failed after ${maxRetries} retries: ${lastError?.message || 'Unknown error'}`); }, logParameters(params, { logger }) { const { method, url, params: queryParams, maxRetries, slowMode } = params; logger.log(`${method} ${url}${queryParams ? `?${new URLSearchParams(queryParams).toString()}` : ''}${maxRetries !== undefined ? ` (max retries: ${maxRetries})` : ''}${slowMode ? ' (slow mode)' : ''}`); }, logReturns: (result, { logger }) => { const { status, statusText, retries, slowModeEnabled } = result; logger.log(`${status} ${statusText}${retries ? ` after ${retries} retries` : ''}${slowModeEnabled ? ' (slow mode enabled)' : ''}`); }, }; //# sourceMappingURL=fetch.js.map