Repository: pydantic/pydantic-ai
File: pydantic_ai_slim/pydantic_ai/_tool_manager.py (Python)

Code before:
                tool_call_approved=approved,
                partial_output=allow_partial,
            )

            pyd_allow_partial = 'trailing-strings' if allow_partial else 'off'
            validator = tool.args_validator
            if isinstance(call.args, str):
                args_dict = validator.validate_json(
                    call.args or '{}', allow_partial=pyd_allow_partial, context=ctx.validation_context
                )
            else:
                args_dict = validator.validate_python(
                    call.args or {}, allow_partial=pyd_allow_partial, context=ctx.validation_context
                )

            # Determine effective timeout: per-tool timeout takes precedence over default
            effective_timeout = tool.timeout if tool.timeout is not None else self.default_timeout

            if effective_timeout is not None:
                try:
                    with anyio.fail_after(effective_timeout):
                        result = await self.toolset.call_tool(name, args_dict, ctx, tool)
                except TimeoutError:
                    m = _messages.RetryPromptPart(
                        tool_name=name,
                        content=f"Tool '{name}' timed out after {effective_timeout} seconds. Please try a different approach.",
                        tool_call_id=call.tool_call_id,
                    )
                    self.failed_tools.add(name)
                    raise ToolRetryError(m) from None
            else:
                result = await self.toolset.call_tool(name, args_dict, ctx, tool)

            return result
        except (ValidationError, ModelRetry) as e:
            max_retries = tool.max_retries if tool is not None else 1
            current_retry = self.ctx.retries.get(name, 0)

            if current_retry == max_retries:
                raise UnexpectedModelBehavior(f'Tool {name!r} exceeded max retries count of {max_retries}') from e
            else:
                if wrap_validation_errors:
                    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)
                    elif isinstance(e, ModelRetry):
                        m = _messages.RetryPromptPart(

Review comment (suggestion):
- 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:
- https://github.com/pydantic/pydantic-ai/issues/3566 
- https://github.com/pydantic/pydantic-ai/issues/2586#issuecomment-3308216750

It may be time to do some of that work to allow more control over what exactly is sent back to the model here.

Code after:
            pyd_allow_partial = 'trailing-strings' if allow_partial else 'off'
            validator = tool.args_validator
            if isinstance(call.args, str):
                args_dict = validator.validate_json(
                    call.args or '{}', allow_partial=pyd_allow_partial, context=ctx.validation_context
                )
            else:
                args_dict = validator.validate_python(
                    call.args or {}, allow_partial=pyd_allow_partial, context=ctx.validation_context
                )

            return await self.toolset.call_tool(name, args_dict, ctx, tool)
        except (ValidationError, ModelRetry) as e:
            max_retries = tool.max_retries if tool is not None else 1
            current_retry = self.ctx.retries.get(name, 0)

            if current_retry == max_retries:
                raise UnexpectedModelBehavior(f'Tool {name!r} exceeded max retries count of {max_retries}') from e
            else:
                if wrap_validation_errors:
                    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)
                    elif isinstance(e, ModelRetry):
                        m = _messages.RetryPromptPart(
                            tool_name=name,
                            content=e.message,
                            tool_call_id=call.tool_call_id,
                        )
                        e = ToolRetryError(m)
                    else:
                        assert_never(e)

                if not allow_partial:
                    # If we're validating partial arguments, we don't want to count this as a failed tool as it may still succeed once the full arguments are received.
                    self.failed_tools.add(name)

                raise e

    async def _call_function_tool(
        self,
        call: ToolCallPart,
        *,
        allow_partial: bool,
        wrap_validation_errors: bool,
        approved: bool,
        tracer: Tracer,