Skip to content

Conversation

@pokey
Copy link
Contributor

@pokey pokey commented Oct 20, 2025

Adds toolRetryMiddleware to automatically retry failed tool calls with configurable exponential backoff, exception filtering, and error handling.

Port of langchain-ai/langchain#33503

Example

import { createAgent, toolRetryMiddleware } from "langchain";

// Retry up to 3 times with exponential backoff
const retry = toolRetryMiddleware({
  maxRetries: 3,
  initialDelay: 1000,
  backoffFactor: 2.0,
});

const agent = createAgent({
  model: "openai:gpt-4o",
  tools: [searchTool, databaseTool],
  middleware: [retry],
});

// Tool failures are automatically retried
const result = await agent.invoke({
  messages: [{ role: "user", content: "Search for AI news" }],
});

For advanced usage with specific exception handling:

class TimeoutError extends Error {}

function shouldRetry(error: Error): boolean {
  // Only retry on 5xx errors or timeouts
  if (error.constructor.name === "HTTPError") {
    const statusCode = (error as any).statusCode;
    return 500 <= statusCode && statusCode < 600;
  }
  return error.constructor === TimeoutError;
}

const retry = toolRetryMiddleware({
  maxRetries: 4,
  retryOn: shouldRetry,
  tools: ["search_database"], // Only apply to specific tools
});

Adds toolRetryMiddleware to automatically retry failed tool calls with configurable exponential backoff, exception filtering, and error handling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@changeset-bot
Copy link

changeset-bot bot commented Oct 20, 2025

⚠️ No Changeset found

Latest commit: 8d975d1

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

/**
* Initial delay in milliseconds before first retry. Default is 1000 (1 second).
*/
initialDelay?: number;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just flagging that this is milliseconds, where python uses seconds. It is more common in js to use milliseconds, but I don't love the inconsistency with the Python version. Any opinions? We could name it initialDelayMs to make unit clearer if we decide to stick with milliseconds

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No super strong opinions, but would agree we should use ms.

Copy link
Contributor Author

@pokey pokey Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added Ms suffix to make it clearer

@pokey pokey marked this pull request as ready for review October 20, 2025 13:21
/**
* Initial delay in milliseconds before first retry. Default is 1000 (1 second).
*/
initialDelay?: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No super strong opinions, but would agree we should use ms.

Comment on lines 249 to 251
const toolFilter: string[] | undefined = tools?.map((tool) =>
typeof tool === "string" ? tool : (tool.name as string)
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might pay to be more strict here:

Suggested change
const toolFilter: string[] | undefined = tools?.map((tool) =>
typeof tool === "string" ? tool : (tool.name as string)
);
const toolFilter = tools?.reduce((acc, tool) => {
if (typeof tool === "string") return [...acc, tool];
else if ("name" in tool && typeof tool.name === "string") return [...acc, tool];
else return acc;
}, [] as string[]);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we'd just silent drop tools that don't look right? Would it not be better to fail fast?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk if it super matters. If that's something we do want to guard against, I'd say we need to throw a better error instead of just whatever comes out of accessing tool.name on a weird looking object

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing a better error sounds good to me. Any objection?

@pokey
Copy link
Contributor Author

pokey commented Oct 21, 2025

Ok feedback addressed and left some responses

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants