Skip to content

Conversation

@jamesaud
Copy link

@jamesaud jamesaud commented Dec 12, 2025

Summary

ToolRetryError was not passing a message to Exception.__init__(), causing str(error) to return an empty string.

This came up when the agent loop crashes due to max tool retries, which breaks error monitoring tools like Sentry or Langsmith that rely on str(exception) to display and group errors.

Problem

from pydantic_ai.exceptions import ToolRetryError
from pydantic_ai.messages import RetryPromptPart

part = RetryPromptPart(content='Test error', tool_name='test')
error = ToolRetryError(part)

str(error)  # Returns '' (empty string)
error.args  # Returns ()

Solution

Pass tool_retry.model_response() to the parent Exception.__init__():

str(error)  # Now returns 'Test error\n\nFix the errors and try again.'

Changes

  • pydantic_ai_slim/pydantic_ai/exceptions.py: Pass message to parent class
  • tests/test_exceptions.py: Add ToolRetryError to hashable test + add dedicated string representation test

ToolRetryError was not passing a message to Exception.__init__(),
causing str(error) to return an empty string. This breaks error
monitoring tools like Sentry that rely on str(exception) to display
error messages.

The fix passes tool_retry.model_response() to the parent class,
ensuring the error message is properly accessible via str(error)
and error.args.
@jamesaud jamesaud closed this Dec 12, 2025
@jamesaud jamesaud reopened this Dec 12, 2025
def __init__(self, tool_retry: RetryPromptPart):
self.tool_retry = tool_retry
super().__init__()
super().__init__(tool_retry.model_response())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we use tool_retry.content that contains just the error message, without the surrounding text instructing the model to retry?

Besides a str, content can also be a list of Pydantic error details, so we would need to stringify/dump those as to JSON.

Copy link
Author

Choose a reason for hiding this comment

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

Great, thanks for the info! Using tool_retry.content now. Added a bit of formatting code to make list[ErrorDetail] more human readable.

Copy link
Author

Choose a reason for hiding this comment

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

Plz wait for re-review - need to fix CI.

@DouweM DouweM self-assigned this Dec 12, 2025
@DouweM DouweM changed the title fix: pass message to ToolRetryError parent class Set ToolRetryError message Dec 12, 2025
@jamesaud jamesaud force-pushed the fix-tool-retry-error-message branch 9 times, most recently from 3b384a2 to 09a0ca4 Compare December 14, 2025 16:52
@jamesaud jamesaud requested a review from DouweM December 14, 2025 16:53
@jamesaud jamesaud marked this pull request as draft December 14, 2025 17:07
@jamesaud jamesaud marked this pull request as ready for review December 15, 2025 14:26
@jamesaud jamesaud force-pushed the fix-tool-retry-error-message branch from 4f7147a to 916a8e1 Compare December 15, 2025 14:29
expected = """\
Tool 'my_tool' failed: 2 validation errors
field1: Field required
field2: Input should be a valid string"""
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a bit lossy because we don't get to see the original input or error type. Can you see if we can use the same rendering that is used when Pydantic ValidationError is raised and printed in terminal? We get the ErrorDetails from a ValidationError here, so maybe we can pass in the string message here where we still have that error:

if isinstance(e, ValidationError):
m = _messages.RetryPromptPart(
tool_name=name,
content=e.errors(include_url=False, include_context=False),
tool_call_id=call.tool_call_id,
)
e = ToolRetryError(m)

Copy link
Author

@jamesaud jamesaud Dec 15, 2025

Choose a reason for hiding this comment

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

Thank you for the suggestion! I simplified the scope of the PR to explicitly pass str(e) from the call sites.
If callers do not provide message, then it will be blank.

Another option would be to fallback to lossy behavior as a backup if message is not passed.

@jamesaud jamesaud requested a review from DouweM December 15, 2025 15:42
tool_call_id=call.tool_call_id,
)
e = ToolRetryError(m)
e = ToolRetryError(m, message=e.message)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wouldn't mind setting message automatically if isinstance(tool_retry.content, str)

assert d[exc] == 'value'


def test_tool_retry_error_str():
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should test that it is set correctly in the ValidationError case

content=e.errors(include_url=False),
)
raise ToolRetryError(m) from e
raise ToolRetryError(m, message=str(e)) from e
Copy link
Collaborator

Choose a reason for hiding this comment

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

If ToolRetryError.__init__ is able to access the __cause__ that's set by from e (I'm not sure if it can), we could do this automatically

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