Skip to content

Conversation

@DEENUU1
Copy link

@DEENUU1 DEENUU1 commented Nov 28, 2025

Add tool timeout support

Summary

This PR adds timeout support for tool execution, allowing users to set both global and per-tool timeouts. When a tool exceeds its timeout, a RetryPromptPart is returned to the model instead of raising an exception, enabling the model to try a different approach.

Features

  • Global timeout: Set a default timeout for all tools via Agent(tool_timeout=30)
  • Per-tool timeout: Override the global timeout for specific tools via @agent.tool(timeout=60)
  • Precedence: Per-tool timeout takes precedence over global timeout
  • Backward compatible: Default is None (no timeout)

Usage

from pydantic_ai import Agent

# Global timeout for all tools (30 seconds)
agent = Agent('openai:gpt-4o', tool_timeout=30)

@agent.tool_plain
async def slow_tool() -> str:
    # Will timeout after 30 seconds (global)
    ...

@agent.tool_plain(timeout=60)
async def very_slow_tool() -> str:
    # Will timeout after 60 seconds (per-tool override)
    ...

Implementation Details

  • Uses anyio.fail_after() for async timeout handling
  • On timeout, returns RetryPromptPart with message: "Tool '{name}' timed out after {timeout} seconds. Please try a different approach."
  • Timeout counts as a tool failure (increments retry counter)
  • Added timeout field to Tool, ToolDefinition, and ToolsetTool classes

Tests Added

  • test_tool_timeout_triggers_retry - Verifies slow tools return RetryPromptPart
  • test_per_tool_timeout_overrides_global - Verifies per-tool > global precedence
  • test_no_timeout_by_default - Verifies backward compatibility
  • test_tool_timeout_retry_counts_as_failed - Verifies retry mechanism integration
  • test_tool_timeout_message_format - Verifies error message format
  • test_tool_timeout_definition - Verifies ToolDefinition has timeout field
  • test_tool_timeout_default_none - Verifies default timeout is None

@DouweM DouweM self-assigned this Dec 1, 2025
tool_call_id=call.tool_call_id,
)
self.failed_tools.add(name)
raise ToolRetryError(m) from None
Copy link
Collaborator

Choose a reason for hiding this comment

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

What do you think of implementing this on FunctionToolset.call_tool instead? Then I believe ToolManager should be able to stay untouched, as the default timeout can be passed by the agent into the FunctionToolset/Tool instances as is done with max_retries.

The downside is that this then wouldn't apply to non-function tools like MCP servers, but I wonder if that's OK as they have their own timeout mechanisms anyway.

except TimeoutError:
m = _messages.RetryPromptPart(
tool_name=name,
content=f"Tool '{name}' timed out after {effective_timeout} seconds. Please try a different approach.",
Copy link
Collaborator

Choose a reason for hiding this comment

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

  • I don't think we need to repeat the tool name as the model will have it in context anyway (since this part becomes to the tool call result part)
  • RetryPromptPart.model_response() will already add a Fix the errors and try again. suffix, so the message would end in Please try a different approach. Fix the errors and try again. which is redundant, somewhat contradictory, and possibly confusing to the model...

This is related to:

It may be time to do some of that work to allow more control over what exactly is sent back to the model here.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants