pydantic-ai-slim 1.0.8__tar.gz → 1.0.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/PKG-INFO +5 -5
  2. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_agent_graph.py +67 -55
  3. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_cli.py +1 -1
  4. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_otel_messages.py +2 -0
  5. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_parts_manager.py +82 -12
  6. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_run_context.py +8 -1
  7. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_tool_manager.py +1 -0
  8. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/ag_ui.py +86 -33
  9. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/agent/__init__.py +2 -1
  10. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/builtin_tools.py +12 -0
  11. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_model.py +14 -6
  12. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_run_context.py +2 -1
  13. pydantic_ai_slim-1.0.10/pydantic_ai/format_prompt.py +205 -0
  14. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/messages.py +65 -30
  15. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/anthropic.py +119 -45
  16. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/function.py +17 -8
  17. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/google.py +132 -33
  18. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/groq.py +68 -17
  19. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/openai.py +262 -41
  20. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/__init__.py +1 -1
  21. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/result.py +21 -3
  22. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/function.py +8 -2
  23. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pyproject.toml +2 -2
  24. pydantic_ai_slim-1.0.8/pydantic_ai/format_prompt.py +0 -113
  25. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/.gitignore +0 -0
  26. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/LICENSE +0 -0
  27. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/README.md +0 -0
  28. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/__init__.py +0 -0
  29. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/__main__.py +0 -0
  30. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_a2a.py +0 -0
  31. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_function_schema.py +0 -0
  32. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_griffe.py +0 -0
  33. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_mcp.py +0 -0
  34. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_output.py +0 -0
  35. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_system_prompt.py +0 -0
  36. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_thinking_part.py +0 -0
  37. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/_utils.py +0 -0
  38. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/agent/abstract.py +0 -0
  39. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/agent/wrapper.py +0 -0
  40. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/common_tools/__init__.py +0 -0
  41. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  42. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/common_tools/tavily.py +0 -0
  43. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/direct.py +0 -0
  44. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/__init__.py +0 -0
  45. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/__init__.py +0 -0
  46. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/_agent.py +0 -0
  47. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/_mcp_server.py +0 -0
  48. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/_model.py +0 -0
  49. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/dbos/_utils.py +0 -0
  50. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/__init__.py +0 -0
  51. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_agent.py +0 -0
  52. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_function_toolset.py +0 -0
  53. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_logfire.py +0 -0
  54. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_mcp_server.py +0 -0
  55. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/durable_exec/temporal/_toolset.py +0 -0
  56. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/exceptions.py +0 -0
  57. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/ext/__init__.py +0 -0
  58. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/ext/aci.py +0 -0
  59. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/ext/langchain.py +0 -0
  60. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/mcp.py +0 -0
  61. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/__init__.py +0 -0
  62. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/bedrock.py +0 -0
  63. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/cohere.py +0 -0
  64. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/fallback.py +0 -0
  65. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/gemini.py +0 -0
  66. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/huggingface.py +0 -0
  67. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/instrumented.py +0 -0
  68. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/mcp_sampling.py +0 -0
  69. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/mistral.py +0 -0
  70. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/test.py +0 -0
  71. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/models/wrapper.py +0 -0
  72. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/output.py +0 -0
  73. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/__init__.py +0 -0
  74. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/_json_schema.py +0 -0
  75. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/amazon.py +0 -0
  76. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/anthropic.py +0 -0
  77. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/cohere.py +0 -0
  78. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/deepseek.py +0 -0
  79. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/google.py +0 -0
  80. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/grok.py +0 -0
  81. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/groq.py +0 -0
  82. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/harmony.py +0 -0
  83. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/meta.py +0 -0
  84. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/mistral.py +0 -0
  85. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/moonshotai.py +0 -0
  86. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/openai.py +0 -0
  87. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/profiles/qwen.py +0 -0
  88. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/anthropic.py +0 -0
  89. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/azure.py +0 -0
  90. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/bedrock.py +0 -0
  91. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/cerebras.py +0 -0
  92. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/cohere.py +0 -0
  93. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/deepseek.py +0 -0
  94. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/fireworks.py +0 -0
  95. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/gateway.py +0 -0
  96. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/github.py +0 -0
  97. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/google.py +0 -0
  98. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/google_gla.py +0 -0
  99. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/google_vertex.py +0 -0
  100. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/grok.py +0 -0
  101. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/groq.py +0 -0
  102. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/heroku.py +0 -0
  103. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/huggingface.py +0 -0
  104. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/litellm.py +0 -0
  105. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/mistral.py +0 -0
  106. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/moonshotai.py +0 -0
  107. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/ollama.py +0 -0
  108. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/openai.py +0 -0
  109. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/openrouter.py +0 -0
  110. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/together.py +0 -0
  111. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/providers/vercel.py +0 -0
  112. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/py.typed +0 -0
  113. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/retries.py +0 -0
  114. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/run.py +0 -0
  115. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/settings.py +0 -0
  116. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/tools.py +0 -0
  117. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/__init__.py +0 -0
  118. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/_dynamic.py +0 -0
  119. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/abstract.py +0 -0
  120. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/approval_required.py +0 -0
  121. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/combined.py +0 -0
  122. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/external.py +0 -0
  123. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/filtered.py +0 -0
  124. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/prefixed.py +0 -0
  125. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/prepared.py +0 -0
  126. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/renamed.py +0 -0
  127. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/toolsets/wrapper.py +0 -0
  128. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.10}/pydantic_ai/usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 1.0.8
3
+ Version: 1.0.10
4
4
  Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
5
5
  Project-URL: Homepage, https://github.com/pydantic/pydantic-ai/tree/main/pydantic_ai_slim
6
6
  Project-URL: Source, https://github.com/pydantic/pydantic-ai/tree/main/pydantic_ai_slim
@@ -33,7 +33,7 @@ Requires-Dist: genai-prices>=0.0.23
33
33
  Requires-Dist: griffe>=1.3.2
34
34
  Requires-Dist: httpx>=0.27
35
35
  Requires-Dist: opentelemetry-api>=1.28.0
36
- Requires-Dist: pydantic-graph==1.0.8
36
+ Requires-Dist: pydantic-graph==1.0.10
37
37
  Requires-Dist: pydantic>=2.10
38
38
  Requires-Dist: typing-inspection>=0.4.0
39
39
  Provides-Extra: a2a
@@ -53,11 +53,11 @@ Requires-Dist: rich>=13; extra == 'cli'
53
53
  Provides-Extra: cohere
54
54
  Requires-Dist: cohere>=5.18.0; (platform_system != 'Emscripten') and extra == 'cohere'
55
55
  Provides-Extra: dbos
56
- Requires-Dist: dbos>=1.13.0; extra == 'dbos'
56
+ Requires-Dist: dbos>=1.14.0; extra == 'dbos'
57
57
  Provides-Extra: duckduckgo
58
58
  Requires-Dist: ddgs>=9.0.0; extra == 'duckduckgo'
59
59
  Provides-Extra: evals
60
- Requires-Dist: pydantic-evals==1.0.8; extra == 'evals'
60
+ Requires-Dist: pydantic-evals==1.0.10; extra == 'evals'
61
61
  Provides-Extra: google
62
62
  Requires-Dist: google-genai>=1.31.0; extra == 'google'
63
63
  Provides-Extra: groq
@@ -71,7 +71,7 @@ Requires-Dist: mcp>=1.12.3; extra == 'mcp'
71
71
  Provides-Extra: mistral
72
72
  Requires-Dist: mistralai>=1.9.10; extra == 'mistral'
73
73
  Provides-Extra: openai
74
- Requires-Dist: openai>=1.99.9; extra == 'openai'
74
+ Requires-Dist: openai>=1.107.2; extra == 'openai'
75
75
  Provides-Extra: retries
76
76
  Requires-Dist: tenacity>=8.2.3; extra == 'retries'
77
77
  Provides-Extra: tavily
@@ -545,23 +545,26 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
545
545
  # Ensure that the stream is only run once
546
546
 
547
547
  async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa: C901
548
- texts: list[str] = []
548
+ text = ''
549
549
  tool_calls: list[_messages.ToolCallPart] = []
550
- thinking_parts: list[_messages.ThinkingPart] = []
550
+ invisible_parts: bool = False
551
551
 
552
552
  for part in self.model_response.parts:
553
553
  if isinstance(part, _messages.TextPart):
554
- # ignore empty content for text parts, see #437
555
- if part.content:
556
- texts.append(part.content)
554
+ text += part.content
557
555
  elif isinstance(part, _messages.ToolCallPart):
558
556
  tool_calls.append(part)
559
557
  elif isinstance(part, _messages.BuiltinToolCallPart):
560
- yield _messages.BuiltinToolCallEvent(part)
558
+ # Text parts before a built-in tool call are essentially thoughts,
559
+ # not part of the final result output, so we reset the accumulated text
560
+ text = ''
561
+ invisible_parts = True
562
+ yield _messages.BuiltinToolCallEvent(part) # pyright: ignore[reportDeprecated]
561
563
  elif isinstance(part, _messages.BuiltinToolReturnPart):
562
- yield _messages.BuiltinToolResultEvent(part)
564
+ invisible_parts = True
565
+ yield _messages.BuiltinToolResultEvent(part) # pyright: ignore[reportDeprecated]
563
566
  elif isinstance(part, _messages.ThinkingPart):
564
- thinking_parts.append(part)
567
+ invisible_parts = True
565
568
  else:
566
569
  assert_never(part)
567
570
 
@@ -569,36 +572,51 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
569
572
  # In the future, we'd consider making this configurable at the agent or run level.
570
573
  # This accounts for cases like anthropic returns that might contain a text response
571
574
  # and a tool call response, where the text response just indicates the tool call will happen.
572
- if tool_calls:
573
- async for event in self._handle_tool_calls(ctx, tool_calls):
574
- yield event
575
- elif texts:
576
- # No events are emitted during the handling of text responses, so we don't need to yield anything
577
- self._next_node = await self._handle_text_response(ctx, texts)
578
- elif thinking_parts:
579
- # handle thinking-only responses (responses that contain only ThinkingPart instances)
580
- # this can happen with models that support thinking mode when they don't provide
581
- # actionable output alongside their thinking content.
582
- self._next_node = ModelRequestNode[DepsT, NodeRunEndT](
583
- _messages.ModelRequest(
584
- parts=[_messages.RetryPromptPart('Responses without text or tool calls are not permitted.')]
575
+ try:
576
+ if tool_calls:
577
+ async for event in self._handle_tool_calls(ctx, tool_calls):
578
+ yield event
579
+ elif text:
580
+ # No events are emitted during the handling of text responses, so we don't need to yield anything
581
+ self._next_node = await self._handle_text_response(ctx, text)
582
+ elif invisible_parts:
583
+ # handle responses with only thinking or built-in tool parts.
584
+ # this can happen with models that support thinking mode when they don't provide
585
+ # actionable output alongside their thinking content. so we tell the model to try again.
586
+ m = _messages.RetryPromptPart(
587
+ content='Responses without text or tool calls are not permitted.',
585
588
  )
586
- )
587
- else:
588
- # we got an empty response with no tool calls, text, or thinking
589
- # this sometimes happens with anthropic (and perhaps other models)
590
- # when the model has already returned text along side tool calls
591
- # in this scenario, if text responses are allowed, we return text from the most recent model
592
- # response, if any
593
- if isinstance(ctx.deps.output_schema, _output.TextOutputSchema):
594
- for message in reversed(ctx.state.message_history):
595
- if isinstance(message, _messages.ModelResponse):
596
- last_texts = [p.content for p in message.parts if isinstance(p, _messages.TextPart)]
597
- if last_texts:
598
- self._next_node = await self._handle_text_response(ctx, last_texts)
599
- return
600
-
601
- raise exceptions.UnexpectedModelBehavior('Received empty model response')
589
+ raise ToolRetryError(m)
590
+ else:
591
+ # we got an empty response with no tool calls, text, thinking, or built-in tool calls.
592
+ # this sometimes happens with anthropic (and perhaps other models)
593
+ # when the model has already returned text along side tool calls
594
+ # in this scenario, if text responses are allowed, we return text from the most recent model
595
+ # response, if any
596
+ if isinstance(ctx.deps.output_schema, _output.TextOutputSchema):
597
+ for message in reversed(ctx.state.message_history):
598
+ if isinstance(message, _messages.ModelResponse):
599
+ text = ''
600
+ for part in message.parts:
601
+ if isinstance(part, _messages.TextPart):
602
+ text += part.content
603
+ elif isinstance(part, _messages.BuiltinToolCallPart):
604
+ # Text parts before a built-in tool call are essentially thoughts,
605
+ # not part of the final result output, so we reset the accumulated text
606
+ text = '' # pragma: no cover
607
+ if text:
608
+ self._next_node = await self._handle_text_response(ctx, text)
609
+ return
610
+
611
+ # Go back to the model request node with an empty request, which means we'll essentially
612
+ # resubmit the most recent request that resulted in an empty response,
613
+ # as the empty response and request will not create any items in the API payload,
614
+ # in the hope the model will return a non-empty response this time.
615
+ ctx.state.increment_retries(ctx.deps.max_result_retries)
616
+ self._next_node = ModelRequestNode[DepsT, NodeRunEndT](_messages.ModelRequest(parts=[]))
617
+ except ToolRetryError as e:
618
+ ctx.state.increment_retries(ctx.deps.max_result_retries, e)
619
+ self._next_node = ModelRequestNode[DepsT, NodeRunEndT](_messages.ModelRequest(parts=[e.tool_retry]))
602
620
 
603
621
  self._events_iterator = _run_stream()
604
622
 
@@ -655,28 +673,22 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
655
673
  async def _handle_text_response(
656
674
  self,
657
675
  ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]],
658
- texts: list[str],
676
+ text: str,
659
677
  ) -> ModelRequestNode[DepsT, NodeRunEndT] | End[result.FinalResult[NodeRunEndT]]:
660
678
  output_schema = ctx.deps.output_schema
679
+ run_context = build_run_context(ctx)
661
680
 
662
- text = '\n\n'.join(texts)
663
- try:
664
- run_context = build_run_context(ctx)
665
- if isinstance(output_schema, _output.TextOutputSchema):
666
- result_data = await output_schema.process(text, run_context)
667
- else:
668
- m = _messages.RetryPromptPart(
669
- content='Plain text responses are not permitted, please include your response in a tool call',
670
- )
671
- raise ToolRetryError(m)
672
-
673
- for validator in ctx.deps.output_validators:
674
- result_data = await validator.validate(result_data, run_context)
675
- except ToolRetryError as e:
676
- ctx.state.increment_retries(ctx.deps.max_result_retries, e)
677
- return ModelRequestNode[DepsT, NodeRunEndT](_messages.ModelRequest(parts=[e.tool_retry]))
681
+ if isinstance(output_schema, _output.TextOutputSchema):
682
+ result_data = await output_schema.process(text, run_context)
678
683
  else:
679
- return self._handle_final_result(ctx, result.FinalResult(result_data), [])
684
+ m = _messages.RetryPromptPart(
685
+ content='Plain text responses are not permitted, please include your response in a tool call',
686
+ )
687
+ raise ToolRetryError(m)
688
+
689
+ for validator in ctx.deps.output_validators:
690
+ result_data = await validator.validate(result_data, run_context)
691
+ return self._handle_final_result(ctx, result.FinalResult(result_data), [])
680
692
 
681
693
  __repr__ = dataclasses_no_defaults_repr
682
694
 
@@ -356,7 +356,7 @@ def handle_slash_command(
356
356
  except IndexError:
357
357
  console.print('[dim]No output available to copy.[/dim]')
358
358
  else:
359
- text_to_copy = '\n\n'.join(part.content for part in parts if isinstance(part, TextPart))
359
+ text_to_copy = ''.join(part.content for part in parts if isinstance(part, TextPart))
360
360
  text_to_copy = text_to_copy.strip()
361
361
  if text_to_copy:
362
362
  pyperclip.copy(text_to_copy)
@@ -21,6 +21,7 @@ class ToolCallPart(TypedDict):
21
21
  id: str
22
22
  name: str
23
23
  arguments: NotRequired[JsonValue]
24
+ builtin: NotRequired[bool] # Not (currently?) part of the spec, used by Logfire
24
25
 
25
26
 
26
27
  class ToolCallResponsePart(TypedDict):
@@ -28,6 +29,7 @@ class ToolCallResponsePart(TypedDict):
28
29
  id: str
29
30
  name: str
30
31
  result: NotRequired[JsonValue]
32
+ builtin: NotRequired[bool] # Not (currently?) part of the spec, used by Logfire
31
33
 
32
34
 
33
35
  class MediaUrlPart(TypedDict):
@@ -19,6 +19,8 @@ from typing import Any
19
19
 
20
20
  from pydantic_ai.exceptions import UnexpectedModelBehavior
21
21
  from pydantic_ai.messages import (
22
+ BuiltinToolCallPart,
23
+ BuiltinToolReturnPart,
22
24
  ModelResponsePart,
23
25
  ModelResponseStreamEvent,
24
26
  PartDeltaEvent,
@@ -226,11 +228,11 @@ class ModelResponsePartsManager:
226
228
  self,
227
229
  *,
228
230
  vendor_part_id: Hashable | None,
229
- tool_name: str | None,
230
- args: str | dict[str, Any] | None,
231
- tool_call_id: str | None,
231
+ tool_name: str | None = None,
232
+ args: str | dict[str, Any] | None = None,
233
+ tool_call_id: str | None = None,
232
234
  ) -> ModelResponseStreamEvent | None:
233
- """Handle or update a tool call, creating or updating a `ToolCallPart` or `ToolCallPartDelta`.
235
+ """Handle or update a tool call, creating or updating a `ToolCallPart`, `BuiltinToolCallPart`, or `ToolCallPartDelta`.
234
236
 
235
237
  Managed items remain as `ToolCallPartDelta`s until they have at least a tool_name, at which
236
238
  point they are upgraded to `ToolCallPart`s.
@@ -247,15 +249,17 @@ class ModelResponsePartsManager:
247
249
  tool_call_id: An optional string representing an identifier for this tool call.
248
250
 
249
251
  Returns:
250
- - A `PartStartEvent` if a new ToolCallPart is created.
252
+ - A `PartStartEvent` if a new ToolCallPart or BuiltinToolCallPart is created.
251
253
  - A `PartDeltaEvent` if an existing part is updated.
252
254
  - `None` if no new event is emitted (e.g., the part is still incomplete).
253
255
 
254
256
  Raises:
255
257
  UnexpectedModelBehavior: If attempting to apply a tool call delta to a part that is not
256
- a ToolCallPart or ToolCallPartDelta.
258
+ a ToolCallPart, BuiltinToolCallPart, or ToolCallPartDelta.
257
259
  """
258
- existing_matching_part_and_index: tuple[ToolCallPartDelta | ToolCallPart, int] | None = None
260
+ existing_matching_part_and_index: tuple[ToolCallPartDelta | ToolCallPart | BuiltinToolCallPart, int] | None = (
261
+ None
262
+ )
259
263
 
260
264
  if vendor_part_id is None:
261
265
  # vendor_part_id is None, so check if the latest part is a matching tool call or delta to update
@@ -264,14 +268,14 @@ class ModelResponsePartsManager:
264
268
  if tool_name is None and self._parts:
265
269
  part_index = len(self._parts) - 1
266
270
  latest_part = self._parts[part_index]
267
- if isinstance(latest_part, ToolCallPart | ToolCallPartDelta): # pragma: no branch
271
+ if isinstance(latest_part, ToolCallPart | BuiltinToolCallPart | ToolCallPartDelta): # pragma: no branch
268
272
  existing_matching_part_and_index = latest_part, part_index
269
273
  else:
270
274
  # vendor_part_id is provided, so look up the corresponding part or delta
271
275
  part_index = self._vendor_id_to_part_index.get(vendor_part_id)
272
276
  if part_index is not None:
273
277
  existing_part = self._parts[part_index]
274
- if not isinstance(existing_part, ToolCallPartDelta | ToolCallPart):
278
+ if not isinstance(existing_part, ToolCallPartDelta | ToolCallPart | BuiltinToolCallPart):
275
279
  raise UnexpectedModelBehavior(f'Cannot apply a tool call delta to {existing_part=}')
276
280
  existing_matching_part_and_index = existing_part, part_index
277
281
 
@@ -284,7 +288,7 @@ class ModelResponsePartsManager:
284
288
  new_part_index = len(self._parts)
285
289
  self._parts.append(part)
286
290
  # Only emit a PartStartEvent if we have enough information to produce a full ToolCallPart
287
- if isinstance(part, ToolCallPart):
291
+ if isinstance(part, ToolCallPart | BuiltinToolCallPart):
288
292
  return PartStartEvent(index=new_part_index, part=part)
289
293
  else:
290
294
  # Update the existing part or delta with the new information
@@ -292,7 +296,7 @@ class ModelResponsePartsManager:
292
296
  delta = ToolCallPartDelta(tool_name_delta=tool_name, args_delta=args, tool_call_id=tool_call_id)
293
297
  updated_part = delta.apply(existing_part)
294
298
  self._parts[part_index] = updated_part
295
- if isinstance(updated_part, ToolCallPart):
299
+ if isinstance(updated_part, ToolCallPart | BuiltinToolCallPart):
296
300
  if isinstance(existing_part, ToolCallPartDelta):
297
301
  # We just upgraded a delta to a full part, so emit a PartStartEvent
298
302
  return PartStartEvent(index=part_index, part=updated_part)
@@ -337,7 +341,7 @@ class ModelResponsePartsManager:
337
341
  else:
338
342
  # vendor_part_id is provided, so find and overwrite or create a new ToolCallPart.
339
343
  maybe_part_index = self._vendor_id_to_part_index.get(vendor_part_id)
340
- if maybe_part_index is not None:
344
+ if maybe_part_index is not None and isinstance(self._parts[maybe_part_index], ToolCallPart):
341
345
  new_part_index = maybe_part_index
342
346
  self._parts[new_part_index] = new_part
343
347
  else:
@@ -345,3 +349,69 @@ class ModelResponsePartsManager:
345
349
  self._parts.append(new_part)
346
350
  self._vendor_id_to_part_index[vendor_part_id] = new_part_index
347
351
  return PartStartEvent(index=new_part_index, part=new_part)
352
+
353
+ def handle_builtin_tool_call_part(
354
+ self,
355
+ *,
356
+ vendor_part_id: Hashable | None,
357
+ part: BuiltinToolCallPart,
358
+ ) -> ModelResponseStreamEvent:
359
+ """Create or overwrite a BuiltinToolCallPart.
360
+
361
+ Args:
362
+ vendor_part_id: The vendor's ID for this tool call part. If not
363
+ None and an existing part is found, that part is overwritten.
364
+ part: The BuiltinToolCallPart.
365
+
366
+ Returns:
367
+ ModelResponseStreamEvent: A `PartStartEvent` indicating that a new tool call part
368
+ has been added to the manager, or replaced an existing part.
369
+ """
370
+ if vendor_part_id is None:
371
+ # vendor_part_id is None, so we unconditionally append a new BuiltinToolCallPart to the end of the list
372
+ new_part_index = len(self._parts)
373
+ self._parts.append(part)
374
+ else:
375
+ # vendor_part_id is provided, so find and overwrite or create a new BuiltinToolCallPart.
376
+ maybe_part_index = self._vendor_id_to_part_index.get(vendor_part_id)
377
+ if maybe_part_index is not None and isinstance(self._parts[maybe_part_index], BuiltinToolCallPart):
378
+ new_part_index = maybe_part_index
379
+ self._parts[new_part_index] = part
380
+ else:
381
+ new_part_index = len(self._parts)
382
+ self._parts.append(part)
383
+ self._vendor_id_to_part_index[vendor_part_id] = new_part_index
384
+ return PartStartEvent(index=new_part_index, part=part)
385
+
386
+ def handle_builtin_tool_return_part(
387
+ self,
388
+ *,
389
+ vendor_part_id: Hashable | None,
390
+ part: BuiltinToolReturnPart,
391
+ ) -> ModelResponseStreamEvent:
392
+ """Create or overwrite a BuiltinToolReturnPart.
393
+
394
+ Args:
395
+ vendor_part_id: The vendor's ID for this tool call part. If not
396
+ None and an existing part is found, that part is overwritten.
397
+ part: The BuiltinToolReturnPart.
398
+
399
+ Returns:
400
+ ModelResponseStreamEvent: A `PartStartEvent` indicating that a new tool call part
401
+ has been added to the manager, or replaced an existing part.
402
+ """
403
+ if vendor_part_id is None:
404
+ # vendor_part_id is None, so we unconditionally append a new BuiltinToolReturnPart to the end of the list
405
+ new_part_index = len(self._parts)
406
+ self._parts.append(part)
407
+ else:
408
+ # vendor_part_id is provided, so find and overwrite or create a new BuiltinToolReturnPart.
409
+ maybe_part_index = self._vendor_id_to_part_index.get(vendor_part_id)
410
+ if maybe_part_index is not None and isinstance(self._parts[maybe_part_index], BuiltinToolReturnPart):
411
+ new_part_index = maybe_part_index
412
+ self._parts[new_part_index] = part
413
+ else:
414
+ new_part_index = len(self._parts)
415
+ self._parts.append(part)
416
+ self._vendor_id_to_part_index[vendor_part_id] = new_part_index
417
+ return PartStartEvent(index=new_part_index, part=part)
@@ -43,10 +43,17 @@ class RunContext(Generic[AgentDepsT]):
43
43
  tool_name: str | None = None
44
44
  """Name of the tool being called."""
45
45
  retry: int = 0
46
- """Number of retries so far."""
46
+ """Number of retries of this tool so far."""
47
+ max_retries: int = 0
48
+ """The maximum number of retries of this tool."""
47
49
  run_step: int = 0
48
50
  """The current step in the run."""
49
51
  tool_call_approved: bool = False
50
52
  """Whether a tool call that required approval has now been approved."""
51
53
 
54
+ @property
55
+ def last_attempt(self) -> bool:
56
+ """Whether this is the last attempt at running this tool before an error is raised."""
57
+ return self.retry == self.max_retries
58
+
52
59
  __repr__ = _utils.dataclasses_no_defaults_repr
@@ -147,6 +147,7 @@ class ToolManager(Generic[AgentDepsT]):
147
147
  tool_name=name,
148
148
  tool_call_id=call.tool_call_id,
149
149
  retry=self.ctx.retries.get(name, 0),
150
+ max_retries=tool.max_retries,
150
151
  )
151
152
 
152
153
  pyd_allow_partial = 'trailing-strings' if allow_partial else 'off'
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  import json
10
10
  import uuid
11
11
  from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Mapping, Sequence
12
- from dataclasses import Field, dataclass, replace
12
+ from dataclasses import Field, dataclass, field, replace
13
13
  from http import HTTPStatus
14
14
  from typing import (
15
15
  Any,
@@ -23,13 +23,15 @@ from typing import (
23
23
  )
24
24
 
25
25
  from pydantic import BaseModel, ValidationError
26
- from typing_extensions import assert_never
27
26
 
28
27
  from . import _utils
29
28
  from ._agent_graph import CallToolsNode, ModelRequestNode
30
29
  from .agent import AbstractAgent, AgentRun, AgentRunResult
31
30
  from .exceptions import UserError
32
31
  from .messages import (
32
+ BaseToolCallPart,
33
+ BuiltinToolCallPart,
34
+ BuiltinToolReturnPart,
33
35
  FunctionToolResultEvent,
34
36
  ModelMessage,
35
37
  ModelRequest,
@@ -123,6 +125,8 @@ SSE_CONTENT_TYPE: Final[str] = 'text/event-stream'
123
125
  OnCompleteFunc: TypeAlias = Callable[[AgentRunResult[Any]], None] | Callable[[AgentRunResult[Any]], Awaitable[None]]
124
126
  """Callback function type that receives the `AgentRunResult` of the completed run. Can be sync or async."""
125
127
 
128
+ _BUILTIN_TOOL_CALL_ID_PREFIX: Final[str] = 'pyd_ai_builtin'
129
+
126
130
 
127
131
  class AGUIApp(Generic[AgentDepsT, OutputDataT], Starlette):
128
132
  """ASGI application for running Pydantic AI agents with AG-UI protocol support."""
@@ -487,20 +491,37 @@ async def _handle_model_request_event( # noqa: C901
487
491
  stream_ctx.part_end = TextMessageEndEvent(
488
492
  message_id=message_id,
489
493
  )
490
- elif isinstance(part, ToolCallPart): # pragma: no branch
494
+ elif isinstance(part, BaseToolCallPart):
495
+ tool_call_id = part.tool_call_id
496
+ if isinstance(part, BuiltinToolCallPart):
497
+ builtin_tool_call_id = '|'.join(
498
+ [_BUILTIN_TOOL_CALL_ID_PREFIX, part.provider_name or '', tool_call_id]
499
+ )
500
+ stream_ctx.builtin_tool_call_ids[tool_call_id] = builtin_tool_call_id
501
+ tool_call_id = builtin_tool_call_id
502
+
491
503
  message_id = stream_ctx.message_id or stream_ctx.new_message_id()
492
504
  yield ToolCallStartEvent(
493
- tool_call_id=part.tool_call_id,
505
+ tool_call_id=tool_call_id,
494
506
  tool_call_name=part.tool_name,
495
507
  parent_message_id=message_id,
496
508
  )
497
509
  if part.args:
498
510
  yield ToolCallArgsEvent(
499
- tool_call_id=part.tool_call_id,
500
- delta=part.args if isinstance(part.args, str) else json.dumps(part.args),
511
+ tool_call_id=tool_call_id,
512
+ delta=part.args_as_json_str(),
501
513
  )
502
514
  stream_ctx.part_end = ToolCallEndEvent(
503
- tool_call_id=part.tool_call_id,
515
+ tool_call_id=tool_call_id,
516
+ )
517
+ elif isinstance(part, BuiltinToolReturnPart): # pragma: no branch
518
+ tool_call_id = stream_ctx.builtin_tool_call_ids[part.tool_call_id]
519
+ yield ToolCallResultEvent(
520
+ message_id=stream_ctx.new_message_id(),
521
+ type=EventType.TOOL_CALL_RESULT,
522
+ role='tool',
523
+ tool_call_id=tool_call_id,
524
+ content=part.model_response_str(),
504
525
  )
505
526
 
506
527
  elif isinstance(agent_event, PartDeltaEvent):
@@ -512,9 +533,12 @@ async def _handle_model_request_event( # noqa: C901
512
533
  delta=delta.content_delta,
513
534
  )
514
535
  elif isinstance(delta, ToolCallPartDelta): # pragma: no branch
515
- assert delta.tool_call_id, '`ToolCallPartDelta.tool_call_id` must be set'
536
+ tool_call_id = delta.tool_call_id
537
+ assert tool_call_id, '`ToolCallPartDelta.tool_call_id` must be set'
538
+ if tool_call_id in stream_ctx.builtin_tool_call_ids:
539
+ tool_call_id = stream_ctx.builtin_tool_call_ids[tool_call_id]
516
540
  yield ToolCallArgsEvent(
517
- tool_call_id=delta.tool_call_id,
541
+ tool_call_id=tool_call_id,
518
542
  delta=delta.args_delta if isinstance(delta.args_delta, str) else json.dumps(delta.args_delta),
519
543
  )
520
544
  elif isinstance(delta, ThinkingPartDelta): # pragma: no branch
@@ -550,9 +574,8 @@ async def _handle_tool_result_event(
550
574
  if not isinstance(result, ToolReturnPart):
551
575
  return
552
576
 
553
- message_id = stream_ctx.new_message_id()
554
577
  yield ToolCallResultEvent(
555
- message_id=message_id,
578
+ message_id=stream_ctx.new_message_id(),
556
579
  type=EventType.TOOL_CALL_RESULT,
557
580
  role='tool',
558
581
  tool_call_id=result.tool_call_id,
@@ -579,7 +602,9 @@ def _messages_from_ag_ui(messages: list[Message]) -> list[ModelMessage]:
579
602
  request_parts: list[ModelRequestPart] | None = None
580
603
  response_parts: list[ModelResponsePart] | None = None
581
604
  for msg in messages:
582
- if isinstance(msg, UserMessage | SystemMessage | DeveloperMessage | ToolMessage):
605
+ if isinstance(msg, UserMessage | SystemMessage | DeveloperMessage) or (
606
+ isinstance(msg, ToolMessage) and not msg.tool_call_id.startswith(_BUILTIN_TOOL_CALL_ID_PREFIX)
607
+ ):
583
608
  if request_parts is None:
584
609
  request_parts = []
585
610
  result.append(ModelRequest(parts=request_parts))
@@ -589,44 +614,71 @@ def _messages_from_ag_ui(messages: list[Message]) -> list[ModelMessage]:
589
614
  request_parts.append(UserPromptPart(content=msg.content))
590
615
  elif isinstance(msg, SystemMessage | DeveloperMessage):
591
616
  request_parts.append(SystemPromptPart(content=msg.content))
592
- elif isinstance(msg, ToolMessage):
593
- tool_name = tool_calls.get(msg.tool_call_id)
617
+ else:
618
+ tool_call_id = msg.tool_call_id
619
+ tool_name = tool_calls.get(tool_call_id)
594
620
  if tool_name is None: # pragma: no cover
595
- raise _ToolCallNotFoundError(tool_call_id=msg.tool_call_id)
621
+ raise _ToolCallNotFoundError(tool_call_id=tool_call_id)
596
622
 
597
623
  request_parts.append(
598
624
  ToolReturnPart(
599
625
  tool_name=tool_name,
600
626
  content=msg.content,
601
- tool_call_id=msg.tool_call_id,
627
+ tool_call_id=tool_call_id,
602
628
  )
603
629
  )
604
- else:
605
- assert_never(msg)
606
630
 
607
- elif isinstance(msg, AssistantMessage):
631
+ elif isinstance(msg, AssistantMessage) or ( # pragma: no branch
632
+ isinstance(msg, ToolMessage) and msg.tool_call_id.startswith(_BUILTIN_TOOL_CALL_ID_PREFIX)
633
+ ):
608
634
  if response_parts is None:
609
635
  response_parts = []
610
636
  result.append(ModelResponse(parts=response_parts))
611
637
  request_parts = None
612
638
 
613
- if msg.content:
614
- response_parts.append(TextPart(content=msg.content))
615
-
616
- if msg.tool_calls:
617
- for tool_call in msg.tool_calls:
618
- tool_calls[tool_call.id] = tool_call.function.name
639
+ if isinstance(msg, AssistantMessage):
640
+ if msg.content:
641
+ response_parts.append(TextPart(content=msg.content))
642
+
643
+ if msg.tool_calls:
644
+ for tool_call in msg.tool_calls:
645
+ tool_call_id = tool_call.id
646
+ tool_name = tool_call.function.name
647
+ tool_calls[tool_call_id] = tool_name
648
+
649
+ if tool_call_id.startswith(_BUILTIN_TOOL_CALL_ID_PREFIX):
650
+ _, provider_name, tool_call_id = tool_call_id.split('|', 2)
651
+ response_parts.append(
652
+ BuiltinToolCallPart(
653
+ tool_name=tool_name,
654
+ args=tool_call.function.arguments,
655
+ tool_call_id=tool_call_id,
656
+ provider_name=provider_name,
657
+ )
658
+ )
659
+ else:
660
+ response_parts.append(
661
+ ToolCallPart(
662
+ tool_name=tool_name,
663
+ tool_call_id=tool_call_id,
664
+ args=tool_call.function.arguments,
665
+ )
666
+ )
667
+ else:
668
+ tool_call_id = msg.tool_call_id
669
+ tool_name = tool_calls.get(tool_call_id)
670
+ if tool_name is None: # pragma: no cover
671
+ raise _ToolCallNotFoundError(tool_call_id=tool_call_id)
672
+ _, provider_name, tool_call_id = tool_call_id.split('|', 2)
619
673
 
620
- response_parts.extend(
621
- ToolCallPart(
622
- tool_name=tool_call.function.name,
623
- tool_call_id=tool_call.id,
624
- args=tool_call.function.arguments,
674
+ response_parts.append(
675
+ BuiltinToolReturnPart(
676
+ tool_name=tool_name,
677
+ content=msg.content,
678
+ tool_call_id=tool_call_id,
679
+ provider_name=provider_name,
625
680
  )
626
- for tool_call in msg.tool_calls
627
681
  )
628
- else:
629
- assert_never(msg)
630
682
 
631
683
  return result
632
684
 
@@ -687,6 +739,7 @@ class _RequestStreamContext:
687
739
  message_id: str = ''
688
740
  part_end: BaseEvent | None = None
689
741
  thinking: bool = False
742
+ builtin_tool_call_ids: dict[str, str] = field(default_factory=dict)
690
743
 
691
744
  def new_message_id(self) -> str:
692
745
  """Generate a new message ID for the request stream.
@@ -259,7 +259,8 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
259
259
  name: The name of the agent, used for logging. If `None`, we try to infer the agent name from the call frame
260
260
  when the agent is first run.
261
261
  model_settings: Optional model request settings to use for this agent's runs, by default.
262
- retries: The default number of retries to allow before raising an error.
262
+ retries: The default number of retries to allow for tool calls and output validation, before raising an error.
263
+ For model request retries, see the [HTTP Request Retries](../retries.md) documentation.
263
264
  output_retries: The maximum number of retries to allow for output validation, defaults to `retries`.
264
265
  tools: Tools to register with the agent, you can also register tools via the decorators
265
266
  [`@agent.tool`][pydantic_ai.Agent.tool] and [`@agent.tool_plain`][pydantic_ai.Agent.tool_plain].