pydantic-ai-slim 1.0.8__tar.gz → 1.0.9__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.

Potentially problematic release.


This version of pydantic-ai-slim might be problematic. Click here for more details.

Files changed (127) hide show
  1. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/PKG-INFO +5 -5
  2. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_agent_graph.py +20 -14
  3. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_cli.py +1 -1
  4. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_otel_messages.py +2 -0
  5. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_parts_manager.py +82 -12
  6. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_run_context.py +8 -1
  7. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_tool_manager.py +1 -0
  8. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/ag_ui.py +86 -33
  9. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/builtin_tools.py +12 -0
  10. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/temporal/_model.py +14 -6
  11. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/temporal/_run_context.py +2 -1
  12. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/messages.py +69 -30
  13. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/anthropic.py +119 -45
  14. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/function.py +17 -8
  15. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/google.py +105 -16
  16. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/groq.py +68 -17
  17. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/openai.py +262 -41
  18. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/__init__.py +1 -1
  19. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/result.py +21 -3
  20. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/function.py +8 -2
  21. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pyproject.toml +2 -2
  22. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/.gitignore +0 -0
  23. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/LICENSE +0 -0
  24. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/README.md +0 -0
  25. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/__init__.py +0 -0
  26. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/__main__.py +0 -0
  27. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_a2a.py +0 -0
  28. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_function_schema.py +0 -0
  29. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_griffe.py +0 -0
  30. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_mcp.py +0 -0
  31. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_output.py +0 -0
  32. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_system_prompt.py +0 -0
  33. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_thinking_part.py +0 -0
  34. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/_utils.py +0 -0
  35. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/agent/__init__.py +0 -0
  36. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/agent/abstract.py +0 -0
  37. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/agent/wrapper.py +0 -0
  38. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/common_tools/__init__.py +0 -0
  39. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  40. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/common_tools/tavily.py +0 -0
  41. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/direct.py +0 -0
  42. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/__init__.py +0 -0
  43. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/dbos/__init__.py +0 -0
  44. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/dbos/_agent.py +0 -0
  45. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/dbos/_mcp_server.py +0 -0
  46. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/dbos/_model.py +0 -0
  47. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/dbos/_utils.py +0 -0
  48. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/temporal/__init__.py +0 -0
  49. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/temporal/_agent.py +0 -0
  50. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/temporal/_function_toolset.py +0 -0
  51. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/temporal/_logfire.py +0 -0
  52. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/temporal/_mcp_server.py +0 -0
  53. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/durable_exec/temporal/_toolset.py +0 -0
  54. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/exceptions.py +0 -0
  55. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/ext/__init__.py +0 -0
  56. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/ext/aci.py +0 -0
  57. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/ext/langchain.py +0 -0
  58. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/format_prompt.py +0 -0
  59. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/mcp.py +0 -0
  60. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/__init__.py +0 -0
  61. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/bedrock.py +0 -0
  62. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/cohere.py +0 -0
  63. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/fallback.py +0 -0
  64. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/gemini.py +0 -0
  65. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/huggingface.py +0 -0
  66. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/instrumented.py +0 -0
  67. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/mcp_sampling.py +0 -0
  68. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/mistral.py +0 -0
  69. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/test.py +0 -0
  70. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/models/wrapper.py +0 -0
  71. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/output.py +0 -0
  72. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/__init__.py +0 -0
  73. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/_json_schema.py +0 -0
  74. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/amazon.py +0 -0
  75. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/anthropic.py +0 -0
  76. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/cohere.py +0 -0
  77. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/deepseek.py +0 -0
  78. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/google.py +0 -0
  79. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/grok.py +0 -0
  80. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/groq.py +0 -0
  81. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/harmony.py +0 -0
  82. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/meta.py +0 -0
  83. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/mistral.py +0 -0
  84. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/moonshotai.py +0 -0
  85. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/openai.py +0 -0
  86. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/profiles/qwen.py +0 -0
  87. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/anthropic.py +0 -0
  88. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/azure.py +0 -0
  89. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/bedrock.py +0 -0
  90. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/cerebras.py +0 -0
  91. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/cohere.py +0 -0
  92. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/deepseek.py +0 -0
  93. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/fireworks.py +0 -0
  94. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/gateway.py +0 -0
  95. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/github.py +0 -0
  96. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/google.py +0 -0
  97. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/google_gla.py +0 -0
  98. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/google_vertex.py +0 -0
  99. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/grok.py +0 -0
  100. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/groq.py +0 -0
  101. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/heroku.py +0 -0
  102. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/huggingface.py +0 -0
  103. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/litellm.py +0 -0
  104. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/mistral.py +0 -0
  105. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/moonshotai.py +0 -0
  106. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/ollama.py +0 -0
  107. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/openai.py +0 -0
  108. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/openrouter.py +0 -0
  109. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/together.py +0 -0
  110. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/providers/vercel.py +0 -0
  111. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/py.typed +0 -0
  112. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/retries.py +0 -0
  113. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/run.py +0 -0
  114. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/settings.py +0 -0
  115. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/tools.py +0 -0
  116. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/__init__.py +0 -0
  117. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/_dynamic.py +0 -0
  118. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/abstract.py +0 -0
  119. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/approval_required.py +0 -0
  120. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/combined.py +0 -0
  121. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/external.py +0 -0
  122. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/filtered.py +0 -0
  123. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/prefixed.py +0 -0
  124. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/prepared.py +0 -0
  125. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/renamed.py +0 -0
  126. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/pydantic_ai/toolsets/wrapper.py +0 -0
  127. {pydantic_ai_slim-1.0.8 → pydantic_ai_slim-1.0.9}/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.9
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.9
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.9; 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,21 +545,22 @@ 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
550
  thinking_parts: list[_messages.ThinkingPart] = []
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
+ yield _messages.BuiltinToolCallEvent(part) # pyright: ignore[reportDeprecated]
561
562
  elif isinstance(part, _messages.BuiltinToolReturnPart):
562
- yield _messages.BuiltinToolResultEvent(part)
563
+ yield _messages.BuiltinToolResultEvent(part) # pyright: ignore[reportDeprecated]
563
564
  elif isinstance(part, _messages.ThinkingPart):
564
565
  thinking_parts.append(part)
565
566
  else:
@@ -572,9 +573,9 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
572
573
  if tool_calls:
573
574
  async for event in self._handle_tool_calls(ctx, tool_calls):
574
575
  yield event
575
- elif texts:
576
+ elif text:
576
577
  # 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
+ self._next_node = await self._handle_text_response(ctx, text)
578
579
  elif thinking_parts:
579
580
  # handle thinking-only responses (responses that contain only ThinkingPart instances)
580
581
  # this can happen with models that support thinking mode when they don't provide
@@ -593,9 +594,16 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
593
594
  if isinstance(ctx.deps.output_schema, _output.TextOutputSchema):
594
595
  for message in reversed(ctx.state.message_history):
595
596
  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)
597
+ text = ''
598
+ for part in message.parts:
599
+ if isinstance(part, _messages.TextPart):
600
+ text += part.content
601
+ elif isinstance(part, _messages.BuiltinToolCallPart):
602
+ # Text parts before a built-in tool call are essentially thoughts,
603
+ # not part of the final result output, so we reset the accumulated text
604
+ text = '' # pragma: no cover
605
+ if text:
606
+ self._next_node = await self._handle_text_response(ctx, text)
599
607
  return
600
608
 
601
609
  raise exceptions.UnexpectedModelBehavior('Received empty model response')
@@ -655,11 +663,9 @@ class CallToolsNode(AgentNode[DepsT, NodeRunEndT]):
655
663
  async def _handle_text_response(
656
664
  self,
657
665
  ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]],
658
- texts: list[str],
666
+ text: str,
659
667
  ) -> ModelRequestNode[DepsT, NodeRunEndT] | End[result.FinalResult[NodeRunEndT]]:
660
668
  output_schema = ctx.deps.output_schema
661
-
662
- text = '\n\n'.join(texts)
663
669
  try:
664
670
  run_context = build_run_context(ctx)
665
671
  if isinstance(output_schema, _output.TextOutputSchema):
@@ -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.
@@ -18,6 +18,9 @@ class AbstractBuiltinTool(ABC):
18
18
  The builtin tools are passed to the model as part of the `ModelRequestParameters`.
19
19
  """
20
20
 
21
+ kind: str = 'unknown_builtin_tool'
22
+ """Built-in tool identifier, this should be available on all built-in tools as a discriminator."""
23
+
21
24
 
22
25
  @dataclass(kw_only=True)
23
26
  class WebSearchTool(AbstractBuiltinTool):
@@ -80,6 +83,9 @@ class WebSearchTool(AbstractBuiltinTool):
80
83
  * Anthropic
81
84
  """
82
85
 
86
+ kind: str = 'web_search'
87
+ """The kind of tool."""
88
+
83
89
 
84
90
  class WebSearchUserLocation(TypedDict, total=False):
85
91
  """Allows you to localize search results based on a user's location.
@@ -113,6 +119,9 @@ class CodeExecutionTool(AbstractBuiltinTool):
113
119
  * Google
114
120
  """
115
121
 
122
+ kind: str = 'code_execution'
123
+ """The kind of tool."""
124
+
116
125
 
117
126
  class UrlContextTool(AbstractBuiltinTool):
118
127
  """Allows your agent to access contents from URLs.
@@ -121,3 +130,6 @@ class UrlContextTool(AbstractBuiltinTool):
121
130
 
122
131
  * Google
123
132
  """
133
+
134
+ kind: str = 'url_context'
135
+ """The kind of tool."""
@@ -4,7 +4,7 @@ from collections.abc import AsyncIterator, Callable
4
4
  from contextlib import asynccontextmanager
5
5
  from dataclasses import dataclass
6
6
  from datetime import datetime
7
- from typing import Any
7
+ from typing import Any, cast
8
8
 
9
9
  from pydantic import ConfigDict, with_config
10
10
  from temporalio import activity, workflow
@@ -30,7 +30,8 @@ from ._run_context import TemporalRunContext
30
30
  @with_config(ConfigDict(arbitrary_types_allowed=True))
31
31
  class _RequestParams:
32
32
  messages: list[ModelMessage]
33
- model_settings: ModelSettings | None
33
+ # `model_settings` can't be a `ModelSettings` because Temporal would end up dropping fields only defined on its subclasses.
34
+ model_settings: dict[str, Any] | None
34
35
  model_request_parameters: ModelRequestParameters
35
36
  serialized_run_context: Any
36
37
 
@@ -82,7 +83,11 @@ class TemporalModel(WrapperModel):
82
83
 
83
84
  @activity.defn(name=f'{activity_name_prefix}__model_request')
84
85
  async def request_activity(params: _RequestParams) -> ModelResponse:
85
- return await self.wrapped.request(params.messages, params.model_settings, params.model_request_parameters)
86
+ return await self.wrapped.request(
87
+ params.messages,
88
+ cast(ModelSettings | None, params.model_settings),
89
+ params.model_request_parameters,
90
+ )
86
91
 
87
92
  self.request_activity = request_activity
88
93
 
@@ -92,7 +97,10 @@ class TemporalModel(WrapperModel):
92
97
 
93
98
  run_context = self.run_context_type.deserialize_run_context(params.serialized_run_context, deps=deps)
94
99
  async with self.wrapped.request_stream(
95
- params.messages, params.model_settings, params.model_request_parameters, run_context
100
+ params.messages,
101
+ cast(ModelSettings | None, params.model_settings),
102
+ params.model_request_parameters,
103
+ run_context,
96
104
  ) as streamed_response:
97
105
  await self.event_stream_handler(run_context, streamed_response)
98
106
 
@@ -124,7 +132,7 @@ class TemporalModel(WrapperModel):
124
132
  activity=self.request_activity,
125
133
  arg=_RequestParams(
126
134
  messages=messages,
127
- model_settings=model_settings,
135
+ model_settings=cast(dict[str, Any] | None, model_settings),
128
136
  model_request_parameters=model_request_parameters,
129
137
  serialized_run_context=None,
130
138
  ),
@@ -161,7 +169,7 @@ class TemporalModel(WrapperModel):
161
169
  args=[
162
170
  _RequestParams(
163
171
  messages=messages,
164
- model_settings=model_settings,
172
+ model_settings=cast(dict[str, Any] | None, model_settings),
165
173
  model_request_parameters=model_request_parameters,
166
174
  serialized_run_context=serialized_run_context,
167
175
  ),
@@ -9,7 +9,7 @@ from pydantic_ai.tools import AgentDepsT, RunContext
9
9
  class TemporalRunContext(RunContext[AgentDepsT]):
10
10
  """The [`RunContext`][pydantic_ai.tools.RunContext] subclass to use to serialize and deserialize the run context for use inside a Temporal activity.
11
11
 
12
- By default, only the `deps`, `retries`, `tool_call_id`, `tool_name`, `tool_call_approved`, `retry` and `run_step` attributes will be available.
12
+ By default, only the `deps`, `retries`, `tool_call_id`, `tool_name`, `tool_call_approved`, `retry`, `max_retries` and `run_step` attributes will be available.
13
13
  To make another attribute available, create a `TemporalRunContext` subclass with a custom `serialize_run_context` class method that returns a dictionary that includes the attribute and pass it to [`TemporalAgent`][pydantic_ai.durable_exec.temporal.TemporalAgent].
14
14
  """
15
15
 
@@ -42,6 +42,7 @@ class TemporalRunContext(RunContext[AgentDepsT]):
42
42
  'tool_name': ctx.tool_name,
43
43
  'tool_call_approved': ctx.tool_call_approved,
44
44
  'retry': ctx.retry,
45
+ 'max_retries': ctx.max_retries,
45
46
  'run_step': ctx.run_step,
46
47
  }
47
48