@@ -3511,7 +3511,7 @@ def empty(_: list[ModelMessage], _info: AgentInfo) -> ModelResponse:
35113511 agent = Agent (FunctionModel (empty ))
35123512
35133513 with capture_run_messages () as messages :
3514- with pytest .raises (UnexpectedModelBehavior , match = r'Exceeded maximum retries \(1\) for output validation' ):
3514+ with pytest .raises (UnexpectedModelBehavior , match = r"Tool 'foobar' exceeded max retries count of 1" ):
35153515 agent .run_sync ('Hello' )
35163516 assert messages == snapshot (
35173517 [
@@ -3607,7 +3607,7 @@ def empty(_: list[ModelMessage], _info: AgentInfo) -> ModelResponse:
36073607 agent = Agent (FunctionModel (empty ), retries = num_retries )
36083608
36093609 with capture_run_messages () as messages :
3610- with pytest .raises (UnexpectedModelBehavior , match = r'Exceeded maximum retries \(2\) for output validation' ):
3610+ with pytest .raises (UnexpectedModelBehavior , match = r"Tool 'foobar' exceeded max retries count of 2" ):
36113611 agent .run_sync ('Hello' )
36123612 assert messages == snapshot (
36133613 [
@@ -9149,6 +9149,44 @@ def test_override_resets_after_context(self):
91499149 assert result .output == 'max_tokens=100 temperature=None'
91509150
91519151
9152+ def test_unknown_tool_with_valid_tool_does_not_exhaust_retries ():
9153+ """Unknown tool calls should not increment the global retry counter.
9154+
9155+ When the model returns both an unknown tool and a valid tool in the same
9156+ response, the unknown tool is handled via per-tool retries (ModelRetry)
9157+ downstream. The global retry counter should only reflect output validation
9158+ retries, not unknown-tool retries, so valid tools keep working.
9159+
9160+ We set retries=2 (per-tool max for unknown tools) and output_retries=1
9161+ (global max for output validation) to isolate the bug: per-tool retries
9162+ allow 2 rounds of the unknown tool, but the old code's global increment
9163+ would exhaust output_retries after just 2 rounds.
9164+ """
9165+ call_count = 0
9166+
9167+ def model_function (messages : list [ModelMessage ], info : AgentInfo ) -> ModelResponse :
9168+ nonlocal call_count
9169+ call_count += 1
9170+ if call_count <= 2 :
9171+ return ModelResponse (
9172+ parts = [
9173+ ToolCallPart ('nonexistent_tool' , '{}' ),
9174+ ToolCallPart ('valid_tool' , '{"x": 1}' ),
9175+ ]
9176+ )
9177+ return ModelResponse (parts = [TextPart ('done' )])
9178+
9179+ agent = Agent (FunctionModel (model_function ), retries = 2 , output_retries = 1 )
9180+
9181+ @agent .tool_plain
9182+ def valid_tool (x : int ) -> str :
9183+ return f'result={ x } '
9184+
9185+ result = agent .run_sync ('Hello' )
9186+ assert result .output == 'done'
9187+ assert call_count == 3
9188+
9189+
91529190# endregion
91539191
91549192
0 commit comments