pydantic-ai-slim 1.4.0__tar.gz → 1.6.0__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 (139) hide show
  1. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/PKG-INFO +5 -3
  2. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_agent_graph.py +23 -12
  3. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_output.py +5 -1
  4. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_utils.py +8 -8
  5. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/agent/__init__.py +3 -7
  6. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/temporal/__init__.py +11 -0
  7. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/temporal/_function_toolset.py +2 -1
  8. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/exceptions.py +1 -1
  9. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/messages.py +1 -1
  10. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/openai.py +2 -0
  11. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/test.py +6 -3
  12. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/openai.py +7 -0
  13. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/google.py +31 -2
  14. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/run.py +40 -24
  15. pydantic_ai_slim-1.6.0/pydantic_ai/toolsets/fastmcp.py +215 -0
  16. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pyproject.toml +2 -0
  17. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/.gitignore +0 -0
  18. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/LICENSE +0 -0
  19. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/README.md +0 -0
  20. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/__init__.py +0 -0
  21. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/__main__.py +0 -0
  22. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_a2a.py +0 -0
  23. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_cli.py +0 -0
  24. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_function_schema.py +0 -0
  25. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_griffe.py +0 -0
  26. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_instrumentation.py +0 -0
  27. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_json_schema.py +0 -0
  28. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_mcp.py +0 -0
  29. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_otel_messages.py +0 -0
  30. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_parts_manager.py +0 -0
  31. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_run_context.py +0 -0
  32. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_system_prompt.py +0 -0
  33. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_thinking_part.py +0 -0
  34. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/_tool_manager.py +0 -0
  35. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/ag_ui.py +0 -0
  36. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/agent/abstract.py +0 -0
  37. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/agent/wrapper.py +0 -0
  38. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/builtin_tools.py +0 -0
  39. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/common_tools/__init__.py +0 -0
  40. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  41. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/common_tools/tavily.py +0 -0
  42. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/direct.py +0 -0
  43. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/__init__.py +0 -0
  44. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/dbos/__init__.py +0 -0
  45. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/dbos/_agent.py +0 -0
  46. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/dbos/_mcp_server.py +0 -0
  47. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/dbos/_model.py +0 -0
  48. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/dbos/_utils.py +0 -0
  49. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/prefect/__init__.py +0 -0
  50. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/prefect/_agent.py +0 -0
  51. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/prefect/_cache_policies.py +0 -0
  52. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/prefect/_function_toolset.py +0 -0
  53. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/prefect/_mcp_server.py +0 -0
  54. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/prefect/_model.py +0 -0
  55. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/prefect/_toolset.py +0 -0
  56. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/prefect/_types.py +0 -0
  57. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/temporal/_agent.py +0 -0
  58. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/temporal/_logfire.py +0 -0
  59. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/temporal/_mcp_server.py +0 -0
  60. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/temporal/_model.py +0 -0
  61. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/temporal/_run_context.py +0 -0
  62. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/durable_exec/temporal/_toolset.py +0 -0
  63. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/ext/__init__.py +0 -0
  64. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/ext/aci.py +0 -0
  65. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/ext/langchain.py +0 -0
  66. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/format_prompt.py +0 -0
  67. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/mcp.py +0 -0
  68. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/__init__.py +0 -0
  69. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/anthropic.py +0 -0
  70. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/bedrock.py +0 -0
  71. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/cohere.py +0 -0
  72. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/fallback.py +0 -0
  73. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/function.py +0 -0
  74. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/gemini.py +0 -0
  75. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/google.py +0 -0
  76. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/groq.py +0 -0
  77. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/huggingface.py +0 -0
  78. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/instrumented.py +0 -0
  79. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/mcp_sampling.py +0 -0
  80. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/mistral.py +0 -0
  81. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/models/wrapper.py +0 -0
  82. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/output.py +0 -0
  83. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/__init__.py +0 -0
  84. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/amazon.py +0 -0
  85. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/anthropic.py +0 -0
  86. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/cohere.py +0 -0
  87. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/deepseek.py +0 -0
  88. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/google.py +0 -0
  89. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/grok.py +0 -0
  90. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/groq.py +0 -0
  91. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/harmony.py +0 -0
  92. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/meta.py +0 -0
  93. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/mistral.py +0 -0
  94. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/moonshotai.py +0 -0
  95. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/profiles/qwen.py +0 -0
  96. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/__init__.py +0 -0
  97. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/anthropic.py +0 -0
  98. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/azure.py +0 -0
  99. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/bedrock.py +0 -0
  100. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/cerebras.py +0 -0
  101. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/cohere.py +0 -0
  102. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/deepseek.py +0 -0
  103. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/fireworks.py +0 -0
  104. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/gateway.py +0 -0
  105. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/github.py +0 -0
  106. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/google_gla.py +0 -0
  107. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/google_vertex.py +0 -0
  108. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/grok.py +0 -0
  109. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/groq.py +0 -0
  110. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/heroku.py +0 -0
  111. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/huggingface.py +0 -0
  112. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/litellm.py +0 -0
  113. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/mistral.py +0 -0
  114. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/moonshotai.py +0 -0
  115. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/nebius.py +0 -0
  116. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/ollama.py +0 -0
  117. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/openai.py +0 -0
  118. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/openrouter.py +0 -0
  119. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/ovhcloud.py +0 -0
  120. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/together.py +0 -0
  121. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/providers/vercel.py +0 -0
  122. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/py.typed +0 -0
  123. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/result.py +0 -0
  124. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/retries.py +0 -0
  125. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/settings.py +0 -0
  126. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/tools.py +0 -0
  127. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/__init__.py +0 -0
  128. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/_dynamic.py +0 -0
  129. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/abstract.py +0 -0
  130. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/approval_required.py +0 -0
  131. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/combined.py +0 -0
  132. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/external.py +0 -0
  133. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/filtered.py +0 -0
  134. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/function.py +0 -0
  135. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/prefixed.py +0 -0
  136. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/prepared.py +0 -0
  137. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/renamed.py +0 -0
  138. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/pydantic_ai/toolsets/wrapper.py +0 -0
  139. {pydantic_ai_slim-1.4.0 → pydantic_ai_slim-1.6.0}/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.4.0
3
+ Version: 1.6.0
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.35
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.4.0
36
+ Requires-Dist: pydantic-graph==1.6.0
37
37
  Requires-Dist: pydantic>=2.10
38
38
  Requires-Dist: typing-inspection>=0.4.0
39
39
  Provides-Extra: a2a
@@ -57,7 +57,9 @@ 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.4.0; extra == 'evals'
60
+ Requires-Dist: pydantic-evals==1.6.0; extra == 'evals'
61
+ Provides-Extra: fastmcp
62
+ Requires-Dist: fastmcp>=2.12.0; extra == 'fastmcp'
61
63
  Provides-Extra: google
62
64
  Requires-Dist: google-genai>=1.46.0; extra == 'google'
63
65
  Provides-Extra: groq
@@ -20,7 +20,8 @@ from pydantic_ai._instrumentation import DEFAULT_INSTRUMENTATION_VERSION
20
20
  from pydantic_ai._tool_manager import ToolManager
21
21
  from pydantic_ai._utils import dataclasses_no_defaults_repr, get_union_args, is_async_callable, run_in_executor
22
22
  from pydantic_ai.builtin_tools import AbstractBuiltinTool
23
- from pydantic_graph import BaseNode, Graph, GraphRunContext
23
+ from pydantic_graph import BaseNode, GraphRunContext
24
+ from pydantic_graph.beta import Graph, GraphBuilder
24
25
  from pydantic_graph.nodes import End, NodeRunEndT
25
26
 
26
27
  from . import _output, _system_prompt, exceptions, messages as _messages, models, result, usage as _usage
@@ -1162,22 +1163,32 @@ def build_agent_graph(
1162
1163
  name: str | None,
1163
1164
  deps_type: type[DepsT],
1164
1165
  output_type: OutputSpec[OutputT],
1165
- ) -> Graph[GraphAgentState, GraphAgentDeps[DepsT, result.FinalResult[OutputT]], result.FinalResult[OutputT]]:
1166
+ ) -> Graph[
1167
+ GraphAgentState,
1168
+ GraphAgentDeps[DepsT, OutputT],
1169
+ UserPromptNode[DepsT, OutputT],
1170
+ result.FinalResult[OutputT],
1171
+ ]:
1166
1172
  """Build the execution [Graph][pydantic_graph.Graph] for a given agent."""
1167
- nodes = (
1168
- UserPromptNode[DepsT],
1169
- ModelRequestNode[DepsT],
1170
- CallToolsNode[DepsT],
1171
- SetFinalResult[DepsT],
1172
- )
1173
- graph = Graph[GraphAgentState, GraphAgentDeps[DepsT, Any], result.FinalResult[OutputT]](
1174
- nodes=nodes,
1173
+ g = GraphBuilder(
1175
1174
  name=name or 'Agent',
1176
1175
  state_type=GraphAgentState,
1177
- run_end_type=result.FinalResult[OutputT],
1176
+ deps_type=GraphAgentDeps[DepsT, OutputT],
1177
+ input_type=UserPromptNode[DepsT, OutputT],
1178
+ output_type=result.FinalResult[OutputT],
1178
1179
  auto_instrument=False,
1179
1180
  )
1180
- return graph
1181
+
1182
+ g.add(
1183
+ g.edge_from(g.start_node).to(UserPromptNode[DepsT, OutputT]),
1184
+ g.node(UserPromptNode[DepsT, OutputT]),
1185
+ g.node(ModelRequestNode[DepsT, OutputT]),
1186
+ g.node(CallToolsNode[DepsT, OutputT]),
1187
+ g.node(
1188
+ SetFinalResult[DepsT, OutputT],
1189
+ ),
1190
+ )
1191
+ return g.build(validate_graph_structure=False)
1181
1192
 
1182
1193
 
1183
1194
  async def _process_message_history(
@@ -2,6 +2,7 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  import inspect
4
4
  import json
5
+ import re
5
6
  from abc import ABC, abstractmethod
6
7
  from collections.abc import Awaitable, Callable, Sequence
7
8
  from dataclasses import dataclass, field
@@ -70,6 +71,7 @@ Usage `OutputValidatorFunc[AgentDepsT, T]`.
70
71
 
71
72
  DEFAULT_OUTPUT_TOOL_NAME = 'final_result'
72
73
  DEFAULT_OUTPUT_TOOL_DESCRIPTION = 'The final response which ends this conversation'
74
+ OUTPUT_TOOL_NAME_SANITIZER = re.compile(r'[^a-zA-Z0-9-_]')
73
75
 
74
76
 
75
77
  async def execute_traced_output_function(
@@ -997,7 +999,9 @@ class OutputToolset(AbstractToolset[AgentDepsT]):
997
999
  if name is None:
998
1000
  name = default_name
999
1001
  if multiple:
1000
- name += f'_{object_def.name}'
1002
+ # strip unsupported characters like "[" and "]" from generic class names
1003
+ safe_name = OUTPUT_TOOL_NAME_SANITIZER.sub('', object_def.name or '')
1004
+ name += f'_{safe_name}'
1001
1005
 
1002
1006
  i = 1
1003
1007
  original_name = name
@@ -147,7 +147,7 @@ async def group_by_temporal(
147
147
  aiterable: The async iterable to group.
148
148
  soft_max_interval: Maximum interval over which to group items, this should avoid a trickle of items causing
149
149
  a group to never be yielded. It's a soft max in the sense that once we're over this time, we yield items
150
- as soon as `aiter.__anext__()` returns. If `None`, no grouping/debouncing is performed
150
+ as soon as `anext(aiter)` returns. If `None`, no grouping/debouncing is performed
151
151
 
152
152
  Returns:
153
153
  A context manager usable as an async iterable of lists of items produced by the input async iterable.
@@ -171,7 +171,7 @@ async def group_by_temporal(
171
171
  buffer: list[T] = []
172
172
  group_start_time = time.monotonic()
173
173
 
174
- aiterator = aiterable.__aiter__()
174
+ aiterator = aiter(aiterable)
175
175
  while True:
176
176
  if group_start_time is None:
177
177
  # group hasn't started, we just wait for the maximum interval
@@ -182,9 +182,9 @@ async def group_by_temporal(
182
182
 
183
183
  # if there's no current task, we get the next one
184
184
  if task is None:
185
- # aiter.__anext__() returns an Awaitable[T], not a Coroutine which asyncio.create_task expects
185
+ # anext(aiter) returns an Awaitable[T], not a Coroutine which asyncio.create_task expects
186
186
  # so far, this doesn't seem to be a problem
187
- task = asyncio.create_task(aiterator.__anext__()) # pyright: ignore[reportArgumentType]
187
+ task = asyncio.create_task(anext(aiterator)) # pyright: ignore[reportArgumentType]
188
188
 
189
189
  # we use asyncio.wait to avoid cancelling the coroutine if it's not done
190
190
  done, _ = await asyncio.wait((task,), timeout=wait_time)
@@ -284,10 +284,10 @@ class PeekableAsyncStream(Generic[T]):
284
284
 
285
285
  # Otherwise, we need to fetch the next item from the underlying iterator.
286
286
  if self._source_iter is None:
287
- self._source_iter = self._source.__aiter__()
287
+ self._source_iter = aiter(self._source)
288
288
 
289
289
  try:
290
- self._buffer = await self._source_iter.__anext__()
290
+ self._buffer = await anext(self._source_iter)
291
291
  except StopAsyncIteration:
292
292
  self._exhausted = True
293
293
  return UNSET
@@ -318,10 +318,10 @@ class PeekableAsyncStream(Generic[T]):
318
318
 
319
319
  # Otherwise, fetch the next item from the source.
320
320
  if self._source_iter is None:
321
- self._source_iter = self._source.__aiter__()
321
+ self._source_iter = aiter(self._source)
322
322
 
323
323
  try:
324
- return await self._source_iter.__anext__()
324
+ return await anext(self._source_iter)
325
325
  except StopAsyncIteration:
326
326
  self._exhausted = True
327
327
  raise
@@ -15,7 +15,6 @@ from pydantic.json_schema import GenerateJsonSchema
15
15
  from typing_extensions import Self, TypeVar, deprecated
16
16
 
17
17
  from pydantic_ai._instrumentation import DEFAULT_INSTRUMENTATION_VERSION, InstrumentationNames
18
- from pydantic_graph import Graph
19
18
 
20
19
  from .. import (
21
20
  _agent_graph,
@@ -41,7 +40,6 @@ from ..builtin_tools import AbstractBuiltinTool
41
40
  from ..models.instrumented import InstrumentationSettings, InstrumentedModel, instrument_model
42
41
  from ..output import OutputDataT, OutputSpec
43
42
  from ..profiles import ModelProfile
44
- from ..result import FinalResult
45
43
  from ..run import AgentRun, AgentRunResult
46
44
  from ..settings import ModelSettings, merge_model_settings
47
45
  from ..tools import (
@@ -566,9 +564,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
566
564
  tool_manager = ToolManager[AgentDepsT](toolset)
567
565
 
568
566
  # Build the graph
569
- graph: Graph[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, Any], FinalResult[Any]] = (
570
- _agent_graph.build_agent_graph(self.name, self._deps_type, output_type_)
571
- )
567
+ graph = _agent_graph.build_agent_graph(self.name, self._deps_type, output_type_)
572
568
 
573
569
  # Build the initial state
574
570
  usage = usage or _usage.RunUsage()
@@ -628,7 +624,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
628
624
  instrumentation_settings=instrumentation_settings,
629
625
  )
630
626
 
631
- start_node = _agent_graph.UserPromptNode[AgentDepsT](
627
+ user_prompt_node = _agent_graph.UserPromptNode[AgentDepsT](
632
628
  user_prompt=user_prompt,
633
629
  deferred_tool_results=deferred_tool_results,
634
630
  instructions=instructions_literal,
@@ -655,7 +651,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
655
651
 
656
652
  try:
657
653
  async with graph.iter(
658
- start_node,
654
+ inputs=user_prompt_node,
659
655
  state=state,
660
656
  deps=graph_deps,
661
657
  span=use_span(run_span) if run_span.is_recording() else None,
@@ -36,6 +36,17 @@ __all__ = [
36
36
  'TemporalWrapperToolset',
37
37
  ]
38
38
 
39
+ # We need eagerly import the anyio backends or it will happens inside workflow code and temporal has issues
40
+ # Note: It's difficult to add a test that covers this because pytest presumably does these imports itself
41
+ # when you have a @pytest.mark.anyio somewhere.
42
+ # I suppose we could add a test that runs a python script in a separate process, but I have not done that...
43
+ import anyio._backends._asyncio # pyright: ignore[reportUnusedImport]
44
+
45
+ try:
46
+ import anyio._backends._trio # noqa F401 # pyright: ignore[reportUnusedImport]
47
+ except ImportError:
48
+ pass
49
+
39
50
 
40
51
  class PydanticAIPlugin(ClientPlugin, WorkerPlugin):
41
52
  """Temporal client and worker plugin for Pydantic AI."""
@@ -2,11 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable
4
4
  from dataclasses import dataclass
5
- from typing import Annotated, Any, Literal, assert_never
5
+ from typing import Annotated, Any, Literal
6
6
 
7
7
  from pydantic import ConfigDict, Discriminator, with_config
8
8
  from temporalio import activity, workflow
9
9
  from temporalio.workflow import ActivityConfig
10
+ from typing_extensions import assert_never
10
11
 
11
12
  from pydantic_ai import FunctionToolset, ToolsetTool
12
13
  from pydantic_ai.exceptions import ApprovalRequired, CallDeferred, ModelRetry, UserError
@@ -159,7 +159,7 @@ class ModelHTTPError(AgentRunError):
159
159
  super().__init__(message)
160
160
 
161
161
 
162
- class FallbackExceptionGroup(ExceptionGroup):
162
+ class FallbackExceptionGroup(ExceptionGroup[Any]):
163
163
  """A group of exceptions that can be raised when all fallback models fail."""
164
164
 
165
165
 
@@ -480,7 +480,7 @@ class BinaryContent:
480
480
  """
481
481
 
482
482
  _identifier: Annotated[str | None, pydantic.Field(alias='identifier', default=None, exclude=True)] = field(
483
- compare=False, default=None
483
+ compare=False, default=None, repr=False
484
484
  )
485
485
 
486
486
  kind: Literal['binary'] = 'binary'
@@ -1431,6 +1431,8 @@ class OpenAIResponsesModel(Model):
1431
1431
  call_id=call_id,
1432
1432
  type='function_call',
1433
1433
  )
1434
+ if profile.openai_responses_requires_function_call_status_none:
1435
+ param['status'] = None # type: ignore[reportGeneralTypeIssues]
1434
1436
  if id and send_item_ids: # pragma: no branch
1435
1437
  param['id'] = id
1436
1438
  openai_messages.append(param)
@@ -44,11 +44,14 @@ class _WrappedTextOutput:
44
44
  value: str | None
45
45
 
46
46
 
47
- @dataclass
47
+ @dataclass(init=False)
48
48
  class _WrappedToolOutput:
49
49
  """A wrapper class to tag an output that came from the custom_output_args field."""
50
50
 
51
- value: Any | None
51
+ value: dict[str, Any] | None
52
+
53
+ def __init__(self, value: Any | None):
54
+ self.value = pydantic_core.to_jsonable_python(value)
52
55
 
53
56
 
54
57
  @dataclass(init=False)
@@ -364,7 +367,7 @@ class _JsonSchemaTestData:
364
367
  self.defs = schema.get('$defs', {})
365
368
  self.seed = seed
366
369
 
367
- def generate(self) -> Any:
370
+ def generate(self) -> dict[str, Any]:
368
371
  """Generate data for the JSON schema."""
369
372
  return self._gen_any(self.schema)
370
373
 
@@ -44,6 +44,13 @@ class OpenAIModelProfile(ModelProfile):
44
44
  openai_supports_encrypted_reasoning_content: bool = False
45
45
  """Whether the model supports including encrypted reasoning content in the response."""
46
46
 
47
+ openai_responses_requires_function_call_status_none: bool = False
48
+ """Whether the Responses API requires the `status` field on function tool calls to be `None`.
49
+
50
+ This is required by vLLM Responses API versions before https://github.com/vllm-project/vllm/pull/26706.
51
+ See https://github.com/pydantic/pydantic-ai/issues/3245 for more details.
52
+ """
53
+
47
54
  def __post_init__(self): # pragma: no cover
48
55
  if not self.openai_supports_sampling_settings:
49
56
  warnings.warn(
@@ -13,7 +13,8 @@ from pydantic_ai.providers import Provider
13
13
 
14
14
  try:
15
15
  from google.auth.credentials import Credentials
16
- from google.genai import Client
16
+ from google.genai._api_client import BaseApiClient
17
+ from google.genai.client import Client, DebugConfig
17
18
  from google.genai.types import HttpOptions
18
19
  except ImportError as _import_error:
19
20
  raise ImportError(
@@ -114,7 +115,7 @@ class GoogleProvider(Provider[Client]):
114
115
  base_url=base_url,
115
116
  headers={'User-Agent': get_user_agent()},
116
117
  httpx_async_client=http_client,
117
- # TODO: Remove once https://github.com/googleapis/python-genai/pull/1509#issuecomment-3430028790 is solved.
118
+ # TODO: Remove once https://github.com/googleapis/python-genai/issues/1565 is solved.
118
119
  async_client_args={'transport': httpx.AsyncHTTPTransport()},
119
120
  )
120
121
  if not vertexai:
@@ -186,9 +187,37 @@ More details [here](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/
186
187
 
187
188
 
188
189
  class _SafelyClosingClient(Client):
190
+ @staticmethod
191
+ def _get_api_client(
192
+ vertexai: bool | None = None,
193
+ api_key: str | None = None,
194
+ credentials: Credentials | None = None,
195
+ project: str | None = None,
196
+ location: str | None = None,
197
+ debug_config: DebugConfig | None = None,
198
+ http_options: HttpOptions | None = None,
199
+ ) -> BaseApiClient:
200
+ return _NonClosingApiClient(
201
+ vertexai=vertexai,
202
+ api_key=api_key,
203
+ credentials=credentials,
204
+ project=project,
205
+ location=location,
206
+ http_options=http_options,
207
+ )
208
+
189
209
  def close(self) -> None:
190
210
  # This is called from `Client.__del__`, even if `Client.__init__` raised an error before `self._api_client` is set, which would raise an `AttributeError` here.
211
+ # TODO: Remove once https://github.com/googleapis/python-genai/issues/1567 is solved.
191
212
  try:
192
213
  super().close()
193
214
  except AttributeError:
194
215
  pass
216
+
217
+
218
+ class _NonClosingApiClient(BaseApiClient):
219
+ async def aclose(self) -> None:
220
+ # The original implementation also calls `await self._async_httpx_client.aclose()`, but we don't want to close our `cached_async_http_client` or the one the user passed in.
221
+ # TODO: Remove once https://github.com/googleapis/python-genai/issues/1566 is solved.
222
+ if self._aiohttp_session:
223
+ await self._aiohttp_session.close() # pragma: no cover
@@ -1,12 +1,14 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
3
  import dataclasses
4
- from collections.abc import AsyncIterator
4
+ from collections.abc import AsyncIterator, Sequence
5
5
  from copy import deepcopy
6
6
  from datetime import datetime
7
7
  from typing import TYPE_CHECKING, Any, Generic, Literal, overload
8
8
 
9
- from pydantic_graph import End, GraphRun, GraphRunContext
9
+ from pydantic_graph import BaseNode, End, GraphRunContext
10
+ from pydantic_graph.beta.graph import EndMarker, GraphRun, GraphTask, JoinItem
11
+ from pydantic_graph.beta.step import NodeStep
10
12
 
11
13
  from . import (
12
14
  _agent_graph,
@@ -112,12 +114,8 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
112
114
 
113
115
  This is the next node that will be used during async iteration, or if a node is not passed to `self.next(...)`.
114
116
  """
115
- next_node = self._graph_run.next_node
116
- if isinstance(next_node, End):
117
- return next_node
118
- if _agent_graph.is_agent_node(next_node):
119
- return next_node
120
- raise exceptions.AgentRunError(f'Unexpected node type: {type(next_node)}') # pragma: no cover
117
+ task = self._graph_run.next_task
118
+ return self._task_to_node(task)
121
119
 
122
120
  @property
123
121
  def result(self) -> AgentRunResult[OutputDataT] | None:
@@ -126,13 +124,13 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
126
124
  Once the run returns an [`End`][pydantic_graph.nodes.End] node, `result` is populated
127
125
  with an [`AgentRunResult`][pydantic_ai.agent.AgentRunResult].
128
126
  """
129
- graph_run_result = self._graph_run.result
130
- if graph_run_result is None:
127
+ graph_run_output = self._graph_run.output
128
+ if graph_run_output is None:
131
129
  return None
132
130
  return AgentRunResult(
133
- graph_run_result.output.output,
134
- graph_run_result.output.tool_name,
135
- graph_run_result.state,
131
+ graph_run_output.output,
132
+ graph_run_output.tool_name,
133
+ self._graph_run.state,
136
134
  self._graph_run.deps.new_message_index,
137
135
  self._traceparent(required=False),
138
136
  )
@@ -147,11 +145,28 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
147
145
  self,
148
146
  ) -> _agent_graph.AgentNode[AgentDepsT, OutputDataT] | End[FinalResult[OutputDataT]]:
149
147
  """Advance to the next node automatically based on the last returned node."""
150
- next_node = await self._graph_run.__anext__()
151
- if _agent_graph.is_agent_node(node=next_node):
152
- return next_node
153
- assert isinstance(next_node, End), f'Unexpected node type: {type(next_node)}'
154
- return next_node
148
+ task = await anext(self._graph_run)
149
+ return self._task_to_node(task)
150
+
151
+ def _task_to_node(
152
+ self, task: EndMarker[FinalResult[OutputDataT]] | JoinItem | Sequence[GraphTask]
153
+ ) -> _agent_graph.AgentNode[AgentDepsT, OutputDataT] | End[FinalResult[OutputDataT]]:
154
+ if isinstance(task, Sequence) and len(task) == 1:
155
+ first_task = task[0]
156
+ if isinstance(first_task.inputs, BaseNode): # pragma: no branch
157
+ base_node: BaseNode[
158
+ _agent_graph.GraphAgentState,
159
+ _agent_graph.GraphAgentDeps[AgentDepsT, OutputDataT],
160
+ FinalResult[OutputDataT],
161
+ ] = first_task.inputs # type: ignore[reportUnknownMemberType]
162
+ if _agent_graph.is_agent_node(node=base_node): # pragma: no branch
163
+ return base_node
164
+ if isinstance(task, EndMarker):
165
+ return End(task.value)
166
+ raise exceptions.AgentRunError(f'Unexpected node: {task}') # pragma: no cover
167
+
168
+ def _node_to_task(self, node: _agent_graph.AgentNode[AgentDepsT, OutputDataT]) -> GraphTask:
169
+ return GraphTask(NodeStep(type(node)).id, inputs=node, fork_stack=())
155
170
 
156
171
  async def next(
157
172
  self,
@@ -222,11 +237,12 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
222
237
  """
223
238
  # Note: It might be nice to expose a synchronous interface for iteration, but we shouldn't do it
224
239
  # on this class, or else IDEs won't warn you if you accidentally use `for` instead of `async for` to iterate.
225
- next_node = await self._graph_run.next(node)
226
- if _agent_graph.is_agent_node(next_node):
227
- return next_node
228
- assert isinstance(next_node, End), f'Unexpected node type: {type(next_node)}'
229
- return next_node
240
+ task = [self._node_to_task(node)]
241
+ try:
242
+ task = await self._graph_run.next(task)
243
+ except StopAsyncIteration:
244
+ pass
245
+ return self._task_to_node(task)
230
246
 
231
247
  # TODO (v2): Make this a property
232
248
  def usage(self) -> _usage.RunUsage:
@@ -234,7 +250,7 @@ class AgentRun(Generic[AgentDepsT, OutputDataT]):
234
250
  return self._graph_run.state.usage
235
251
 
236
252
  def __repr__(self) -> str: # pragma: no cover
237
- result = self._graph_run.result
253
+ result = self._graph_run.output
238
254
  result_repr = '<run not finished>' if result is None else repr(result.output)
239
255
  return f'<{type(self).__name__} result={result_repr} usage={self.usage()}>'
240
256
 
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from asyncio import Lock
5
+ from contextlib import AsyncExitStack
6
+ from dataclasses import KW_ONLY, dataclass
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING, Any, Literal
9
+
10
+ from pydantic import AnyUrl
11
+ from typing_extensions import Self, assert_never
12
+
13
+ from pydantic_ai import messages
14
+ from pydantic_ai.exceptions import ModelRetry
15
+ from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition
16
+ from pydantic_ai.toolsets import AbstractToolset
17
+ from pydantic_ai.toolsets.abstract import ToolsetTool
18
+
19
+ try:
20
+ from fastmcp.client import Client
21
+ from fastmcp.client.transports import ClientTransport
22
+ from fastmcp.exceptions import ToolError
23
+ from fastmcp.mcp_config import MCPConfig
24
+ from fastmcp.server import FastMCP
25
+ from mcp.server.fastmcp import FastMCP as FastMCP1Server
26
+ from mcp.types import (
27
+ AudioContent,
28
+ BlobResourceContents,
29
+ ContentBlock,
30
+ EmbeddedResource,
31
+ ImageContent,
32
+ ResourceLink,
33
+ TextContent,
34
+ TextResourceContents,
35
+ Tool as MCPTool,
36
+ )
37
+
38
+ from pydantic_ai.mcp import TOOL_SCHEMA_VALIDATOR
39
+
40
+ except ImportError as _import_error:
41
+ raise ImportError(
42
+ 'Please install the `fastmcp` package to use the FastMCP server, '
43
+ 'you can use the `fastmcp` optional group — `pip install "pydantic-ai-slim[fastmcp]"`'
44
+ ) from _import_error
45
+
46
+
47
+ if TYPE_CHECKING:
48
+ from fastmcp.client.client import CallToolResult
49
+
50
+
51
+ FastMCPToolResult = messages.BinaryContent | dict[str, Any] | str | None
52
+
53
+ ToolErrorBehavior = Literal['model_retry', 'error']
54
+
55
+ UNKNOWN_BINARY_MEDIA_TYPE = 'application/octet-stream'
56
+
57
+
58
+ @dataclass(init=False)
59
+ class FastMCPToolset(AbstractToolset[AgentDepsT]):
60
+ """A FastMCP Toolset that uses the FastMCP Client to call tools from a local or remote MCP Server.
61
+
62
+ The Toolset can accept a FastMCP Client, a FastMCP Transport, or any other object which a FastMCP Transport can be created from.
63
+
64
+ See https://gofastmcp.com/clients/transports for a full list of transports available.
65
+ """
66
+
67
+ client: Client[Any]
68
+ """The FastMCP client to use."""
69
+
70
+ _: KW_ONLY
71
+
72
+ tool_error_behavior: Literal['model_retry', 'error']
73
+ """The behavior to take when a tool error occurs."""
74
+
75
+ max_retries: int
76
+ """The maximum number of retries to attempt if a tool call fails."""
77
+
78
+ _id: str | None
79
+
80
+ def __init__(
81
+ self,
82
+ client: Client[Any]
83
+ | ClientTransport
84
+ | FastMCP
85
+ | FastMCP1Server
86
+ | AnyUrl
87
+ | Path
88
+ | MCPConfig
89
+ | dict[str, Any]
90
+ | str,
91
+ *,
92
+ max_retries: int = 1,
93
+ tool_error_behavior: Literal['model_retry', 'error'] = 'model_retry',
94
+ id: str | None = None,
95
+ ) -> None:
96
+ if isinstance(client, Client):
97
+ self.client = client
98
+ else:
99
+ self.client = Client[Any](transport=client)
100
+
101
+ self._id = id
102
+ self.max_retries = max_retries
103
+ self.tool_error_behavior = tool_error_behavior
104
+
105
+ self._enter_lock: Lock = Lock()
106
+ self._running_count: int = 0
107
+ self._exit_stack: AsyncExitStack | None = None
108
+
109
+ @property
110
+ def id(self) -> str | None:
111
+ return self._id
112
+
113
+ async def __aenter__(self) -> Self:
114
+ async with self._enter_lock:
115
+ if self._running_count == 0:
116
+ self._exit_stack = AsyncExitStack()
117
+ await self._exit_stack.enter_async_context(self.client)
118
+
119
+ self._running_count += 1
120
+
121
+ return self
122
+
123
+ async def __aexit__(self, *args: Any) -> bool | None:
124
+ async with self._enter_lock:
125
+ self._running_count -= 1
126
+ if self._running_count == 0 and self._exit_stack:
127
+ await self._exit_stack.aclose()
128
+ self._exit_stack = None
129
+
130
+ return None
131
+
132
+ async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]:
133
+ async with self:
134
+ mcp_tools: list[MCPTool] = await self.client.list_tools()
135
+
136
+ return {
137
+ tool.name: _convert_mcp_tool_to_toolset_tool(toolset=self, mcp_tool=tool, retries=self.max_retries)
138
+ for tool in mcp_tools
139
+ }
140
+
141
+ async def call_tool(
142
+ self, name: str, tool_args: dict[str, Any], ctx: RunContext[AgentDepsT], tool: ToolsetTool[AgentDepsT]
143
+ ) -> Any:
144
+ async with self:
145
+ try:
146
+ call_tool_result: CallToolResult = await self.client.call_tool(name=name, arguments=tool_args)
147
+ except ToolError as e:
148
+ if self.tool_error_behavior == 'model_retry':
149
+ raise ModelRetry(message=str(e)) from e
150
+ else:
151
+ raise e
152
+
153
+ # If we have structured content, return that
154
+ if call_tool_result.structured_content:
155
+ return call_tool_result.structured_content
156
+
157
+ # Otherwise, return the content
158
+ return _map_fastmcp_tool_results(parts=call_tool_result.content)
159
+
160
+
161
+ def _convert_mcp_tool_to_toolset_tool(
162
+ toolset: FastMCPToolset[AgentDepsT],
163
+ mcp_tool: MCPTool,
164
+ retries: int,
165
+ ) -> ToolsetTool[AgentDepsT]:
166
+ """Convert an MCP tool to a toolset tool."""
167
+ return ToolsetTool[AgentDepsT](
168
+ tool_def=ToolDefinition(
169
+ name=mcp_tool.name,
170
+ description=mcp_tool.description,
171
+ parameters_json_schema=mcp_tool.inputSchema,
172
+ metadata={
173
+ 'meta': mcp_tool.meta,
174
+ 'annotations': mcp_tool.annotations.model_dump() if mcp_tool.annotations else None,
175
+ 'output_schema': mcp_tool.outputSchema or None,
176
+ },
177
+ ),
178
+ toolset=toolset,
179
+ max_retries=retries,
180
+ args_validator=TOOL_SCHEMA_VALIDATOR,
181
+ )
182
+
183
+
184
+ def _map_fastmcp_tool_results(parts: list[ContentBlock]) -> list[FastMCPToolResult] | FastMCPToolResult:
185
+ """Map FastMCP tool results to toolset tool results."""
186
+ mapped_results = [_map_fastmcp_tool_result(part) for part in parts]
187
+
188
+ if len(mapped_results) == 1:
189
+ return mapped_results[0]
190
+
191
+ return mapped_results
192
+
193
+
194
+ def _map_fastmcp_tool_result(part: ContentBlock) -> FastMCPToolResult:
195
+ if isinstance(part, TextContent):
196
+ return part.text
197
+ elif isinstance(part, ImageContent | AudioContent):
198
+ return messages.BinaryContent(data=base64.b64decode(part.data), media_type=part.mimeType)
199
+ elif isinstance(part, EmbeddedResource):
200
+ if isinstance(part.resource, BlobResourceContents):
201
+ return messages.BinaryContent(
202
+ data=base64.b64decode(part.resource.blob),
203
+ media_type=part.resource.mimeType or UNKNOWN_BINARY_MEDIA_TYPE,
204
+ )
205
+ elif isinstance(part.resource, TextResourceContents):
206
+ return part.resource.text
207
+ else:
208
+ assert_never(part.resource)
209
+ elif isinstance(part, ResourceLink):
210
+ # ResourceLink is not yet supported by the FastMCP toolset as reading resources is not yet supported.
211
+ raise NotImplementedError(
212
+ 'ResourceLink is not supported by the FastMCP toolset as reading resources is not yet supported.'
213
+ )
214
+ else:
215
+ assert_never(part)
@@ -88,6 +88,8 @@ cli = [
88
88
  ]
89
89
  # MCP
90
90
  mcp = ["mcp>=1.12.3"]
91
+ # FastMCP
92
+ fastmcp = ["fastmcp>=2.12.0"]
91
93
  # Evals
92
94
  evals = ["pydantic-evals=={{ version }}"]
93
95
  # A2A