44from typing import Any
55
66import pytest
7+ from pydantic import ValidationError
8+ from pydantic_core import ErrorDetails
79
810from pydantic_ai import ModelRetry
911from pydantic_ai .exceptions import (
1315 IncompleteToolCall ,
1416 ModelAPIError ,
1517 ModelHTTPError ,
18+ ToolRetryError ,
1619 UnexpectedModelBehavior ,
1720 UsageLimitExceeded ,
1821 UserError ,
1922)
23+ from pydantic_ai .messages import RetryPromptPart
2024
2125
2226@pytest .mark .parametrize (
3236 lambda : ModelAPIError ('model' , 'test message' ),
3337 lambda : ModelHTTPError (500 , 'model' ),
3438 lambda : IncompleteToolCall ('test' ),
39+ lambda : ToolRetryError (RetryPromptPart (content = 'test' , tool_name = 'test' )),
3540 ],
3641 ids = [
3742 'ModelRetry' ,
4449 'ModelAPIError' ,
4550 'ModelHTTPError' ,
4651 'IncompleteToolCall' ,
52+ 'ToolRetryError' ,
4753 ],
4854)
4955def test_exceptions_hashable (exc_factory : Callable [[], Any ]):
@@ -59,3 +65,46 @@ def test_exceptions_hashable(exc_factory: Callable[[], Any]):
5965
6066 assert exc in s
6167 assert d [exc ] == 'value'
68+
69+
70+ def test_tool_retry_error_str_with_string_content ():
71+ """Test that ToolRetryError uses string content as message automatically."""
72+ part = RetryPromptPart (content = 'error from tool' , tool_name = 'my_tool' )
73+ error = ToolRetryError (part )
74+ assert str (error ) == 'error from tool'
75+
76+
77+ def test_tool_retry_error_str_with_error_details ():
78+ """Test that ToolRetryError formats ErrorDetails automatically."""
79+ validation_error = ValidationError .from_exception_data (
80+ 'Test' , [{'type' : 'string_type' , 'loc' : ('name' ,), 'input' : 123 }]
81+ )
82+ part = RetryPromptPart (content = validation_error .errors (include_url = False ), tool_name = 'my_tool' )
83+ error = ToolRetryError (part )
84+
85+ assert str (error ) == (
86+ "1 validation error for 'my_tool'\n name\n Input should be a valid string [type=string_type, input_value=123]"
87+ )
88+
89+
90+ def test_tool_retry_error_str_with_value_error_type ():
91+ """Test that ToolRetryError handles value_error type without ctx.error.
92+
93+ When ErrorDetails are serialized, the exception object in ctx is stripped.
94+ This test ensures we handle error types that normally require ctx.error.
95+ """
96+ # Simulate serialized ErrorDetails where ctx.error has been stripped
97+ error_details : list [ErrorDetails ] = [
98+ {
99+ 'type' : 'value_error' ,
100+ 'loc' : ('field' ,),
101+ 'msg' : 'Value error, must not be foo' ,
102+ 'input' : 'foo' ,
103+ }
104+ ]
105+ part = RetryPromptPart (content = error_details , tool_name = 'my_tool' )
106+ error = ToolRetryError (part )
107+
108+ assert str (error ) == (
109+ "1 validation error for 'my_tool'\n field\n Value error, must not be foo [type=value_error, input_value='foo']"
110+ )
0 commit comments