UNPKG

langchain

Version:
1 lines • 12.9 kB
{"version":3,"file":"toolRetry.cjs","names":["z","RetrySchema","InvalidRetryConfigError","ToolMessage","createMiddleware","isGraphBubbleUp","calculateRetryDelay","getRetryAfterMs","sleep"],"sources":["../../../src/agents/middleware/toolRetry.ts"],"sourcesContent":["/**\n * Tool retry middleware for agents.\n */\nimport { z } from \"zod/v3\";\nimport { ToolMessage } from \"@langchain/core/messages\";\nimport type { ClientTool, ServerTool } from \"@langchain/core/tools\";\nimport { isGraphBubbleUp } from \"@langchain/langgraph\";\nimport { createMiddleware } from \"../middleware.js\";\nimport { sleep, calculateRetryDelay, getRetryAfterMs } from \"./utils.js\";\nimport { RetrySchema } from \"./constants.js\";\nimport { InvalidRetryConfigError } from \"./error.js\";\n\n/**\n * Configuration options for the Tool Retry Middleware.\n */\nexport const ToolRetryMiddlewareOptionsSchema = z\n .object({\n /**\n * Optional list of tools or tool names to apply retry logic to.\n * Can be a list of `BaseTool` instances or tool name strings.\n * If `undefined`, applies to all tools. Default is `undefined`.\n */\n tools: z\n .array(\n z.union([z.custom<ClientTool>(), z.custom<ServerTool>(), z.string()])\n )\n .optional(),\n\n /**\n * Behavior when all retries are exhausted. Options:\n * - `\"continue\"` (default): Return an AIMessage with error details, allowing\n * the agent to potentially handle the failure gracefully.\n * - `\"error\"`: Re-raise the exception, stopping agent execution.\n * - Custom function: Function that takes the exception and returns a string\n * for the AIMessage content, allowing custom error formatting.\n *\n * Deprecated values:\n * - `\"raise\"`: use `\"error\"` instead.\n * - `\"return_message\"`: use `\"continue\"` instead.\n */\n onFailure: z\n .union([\n z.literal(\"error\"),\n z.literal(\"continue\"),\n /**\n * @deprecated Use `\"error\"` instead.\n */\n z.literal(\"raise\"),\n /**\n * @deprecated Use `\"continue\"` instead.\n */\n z.literal(\"return_message\"),\n z.function().args(z.instanceof(Error)).returns(z.string()),\n ])\n .default(\"continue\"),\n })\n .merge(RetrySchema);\n\nexport type ToolRetryMiddlewareConfig = z.input<\n typeof ToolRetryMiddlewareOptionsSchema\n>;\n\n/**\n * Middleware that automatically retries failed tool calls with configurable backoff.\n *\n * Supports retrying on specific exceptions and exponential backoff.\n *\n * @example Basic usage with default settings (2 retries, exponential backoff)\n * ```ts\n * import { createAgent, toolRetryMiddleware } from \"langchain\";\n *\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * tools: [searchTool],\n * middleware: [toolRetryMiddleware()],\n * });\n * ```\n *\n * @example Retry specific exceptions only\n * ```ts\n * import { toolRetryMiddleware } from \"langchain\";\n *\n * const retry = toolRetryMiddleware({\n * maxRetries: 4,\n * retryOn: [TimeoutError, NetworkError],\n * backoffFactor: 1.5,\n * });\n * ```\n *\n * @example Custom exception filtering\n * ```ts\n * function shouldRetry(error: Error): boolean {\n * // Only retry on 5xx errors\n * if (error.name === \"HTTPError\" && \"statusCode\" in error) {\n * const statusCode = (error as any).statusCode;\n * return 500 <= statusCode && statusCode < 600;\n * }\n * return false;\n * }\n *\n * const retry = toolRetryMiddleware({\n * maxRetries: 3,\n * retryOn: shouldRetry,\n * });\n * ```\n *\n * @example Apply to specific tools with custom error handling\n * ```ts\n * const formatError = (error: Error) =>\n * \"Database temporarily unavailable. Please try again later.\";\n *\n * const retry = toolRetryMiddleware({\n * maxRetries: 4,\n * tools: [\"search_database\"],\n * onFailure: formatError,\n * });\n * ```\n *\n * @example Apply to specific tools using BaseTool instances\n * ```ts\n * import { tool } from \"@langchain/core/tools\";\n * import { z } from \"zod\";\n *\n * const searchDatabase = tool(\n * async ({ query }) => {\n * // Search implementation\n * return results;\n * },\n * {\n * name: \"search_database\",\n * description: \"Search the database\",\n * schema: z.object({ query: z.string() }),\n * }\n * );\n *\n * const retry = toolRetryMiddleware({\n * maxRetries: 4,\n * tools: [searchDatabase], // Pass BaseTool instance\n * });\n * ```\n *\n * @example Constant backoff (no exponential growth)\n * ```ts\n * const retry = toolRetryMiddleware({\n * maxRetries: 5,\n * backoffFactor: 0.0, // No exponential growth\n * initialDelayMs: 2000, // Always wait 2 seconds\n * });\n * ```\n *\n * @example Raise exception on failure\n * ```ts\n * const retry = toolRetryMiddleware({\n * maxRetries: 2,\n * onFailure: \"raise\", // Re-raise exception instead of returning message\n * });\n * ```\n *\n * @param config - Configuration options for the retry middleware\n * @returns A middleware instance that handles tool failures with retries\n */\nexport function toolRetryMiddleware(config: ToolRetryMiddlewareConfig = {}) {\n const { success, error, data } =\n ToolRetryMiddlewareOptionsSchema.safeParse(config);\n if (!success) {\n throw new InvalidRetryConfigError(error);\n }\n const {\n maxRetries,\n tools,\n retryOn,\n onFailure: onFailureConfig,\n backoffFactor,\n initialDelayMs,\n maxDelayMs,\n jitter,\n } = data;\n\n let onFailure = onFailureConfig;\n if (onFailureConfig === \"raise\") {\n console.warn(\n \"⚠️ `onFailure: 'raise'` is deprecated. Use `onFailure: 'error'` instead.\"\n );\n onFailure = \"error\";\n } else if (onFailureConfig === \"return_message\") {\n console.warn(\n \"⚠️ `onFailure: 'return_message'` is deprecated. Use `onFailure: 'continue'` instead.\"\n );\n onFailure = \"continue\";\n }\n\n // Extract tool names from BaseTool instances or strings\n const toolFilter: string[] = [];\n for (const tool of tools ?? []) {\n if (typeof tool === \"string\") {\n toolFilter.push(tool);\n } else if (\"name\" in tool && typeof tool.name === \"string\") {\n toolFilter.push(tool.name);\n } else {\n throw new TypeError(\n \"Expected a tool name string or tool instance to be passed to toolRetryMiddleware\"\n );\n }\n }\n\n /**\n * Check if retry logic should apply to this tool.\n */\n const shouldRetryTool = (toolName: string): boolean => {\n if (toolFilter.length === 0) {\n return true;\n }\n return toolFilter.includes(toolName);\n };\n\n /**\n * Check if the exception should trigger a retry.\n */\n const shouldRetryException = (error: Error): boolean => {\n if (typeof retryOn === \"function\") {\n return retryOn(error);\n }\n // retryOn is an array of error constructors\n return retryOn.some((ErrorConstructor) => {\n // oxlint-disable-next-line no-instanceof/no-instanceof\n return error instanceof ErrorConstructor;\n });\n };\n\n // Use the exported calculateRetryDelay function with our config\n const delayConfig = { backoffFactor, initialDelayMs, maxDelayMs, jitter };\n\n /**\n * Format the failure message when retries are exhausted.\n */\n const formatFailureMessage = (\n toolName: string,\n error: Error,\n attemptsMade: number\n ): string => {\n const errorType = error.constructor.name;\n const attemptWord = attemptsMade === 1 ? \"attempt\" : \"attempts\";\n return `Tool '${toolName}' failed after ${attemptsMade} ${attemptWord} with ${errorType}`;\n };\n\n /**\n * Handle failure when all retries are exhausted.\n */\n const handleFailure = (\n toolName: string,\n toolCallId: string,\n error: Error,\n attemptsMade: number\n ): ToolMessage => {\n if (onFailure === \"error\") {\n throw error;\n }\n\n let content: string;\n if (typeof onFailure === \"function\") {\n content = onFailure(error);\n } else {\n content = formatFailureMessage(toolName, error, attemptsMade);\n }\n\n return new ToolMessage({\n content,\n tool_call_id: toolCallId,\n name: toolName,\n status: \"error\",\n });\n };\n\n return createMiddleware({\n name: \"toolRetryMiddleware\",\n contextSchema: ToolRetryMiddlewareOptionsSchema,\n wrapToolCall: async (request, handler) => {\n const toolName = (request.tool?.name ?? request.toolCall.name) as string;\n\n // Check if retry should apply to this tool\n if (!shouldRetryTool(toolName)) {\n return handler(request);\n }\n\n const toolCallId = request.toolCall.id ?? \"\";\n\n // Initial attempt + retries\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n return await handler(request);\n } catch (error) {\n if (isGraphBubbleUp(error)) {\n throw error;\n }\n\n const attemptsMade = attempt + 1; // attempt is 0-indexed\n\n // Ensure error is an Error instance\n const err =\n error && typeof error === \"object\" && \"message\" in error\n ? (error as Error)\n : new Error(String(error));\n\n // Check if we should retry this exception\n if (!shouldRetryException(err)) {\n // Exception is not retryable, handle failure immediately\n return handleFailure(toolName, toolCallId, err, attemptsMade);\n }\n\n // Check if we have more retries left\n if (attempt < maxRetries) {\n // Calculate and apply backoff delay\n const delay = calculateRetryDelay(\n delayConfig,\n attempt,\n getRetryAfterMs(error)\n );\n if (delay > 0) {\n await sleep(delay);\n }\n // Continue to next retry\n } else {\n // No more retries, handle failure\n return handleFailure(toolName, toolCallId, err, attemptsMade);\n }\n }\n }\n\n // Unreachable: loop always returns via handler success or handleFailure\n throw new Error(\"Unexpected: retry loop completed without returning\");\n },\n });\n}\n"],"mappings":"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"}