pydantic-ai-slim 1.0.0b1__tar.gz → 1.0.1__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 (121) hide show
  1. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/PKG-INFO +6 -7
  2. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_a2a.py +1 -1
  3. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_agent_graph.py +16 -19
  4. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_parts_manager.py +3 -1
  5. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_tool_manager.py +29 -6
  6. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/ag_ui.py +75 -43
  7. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/agent/__init__.py +7 -7
  8. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/durable_exec/temporal/_agent.py +71 -10
  9. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/exceptions.py +2 -2
  10. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/mcp.py +13 -25
  11. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/messages.py +78 -19
  12. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/__init__.py +1 -0
  13. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/anthropic.py +4 -11
  14. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/bedrock.py +6 -14
  15. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/gemini.py +3 -1
  16. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/google.py +15 -1
  17. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/groq.py +122 -34
  18. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/instrumented.py +5 -0
  19. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/openai.py +17 -13
  20. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/__init__.py +4 -0
  21. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/google_vertex.py +2 -1
  22. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/groq.py +21 -2
  23. pydantic_ai_slim-1.0.1/pydantic_ai/providers/litellm.py +134 -0
  24. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/retries.py +42 -2
  25. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/tools.py +7 -7
  26. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/combined.py +2 -2
  27. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/function.py +47 -19
  28. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/usage.py +37 -3
  29. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pyproject.toml +9 -5
  30. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/.gitignore +0 -0
  31. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/LICENSE +0 -0
  32. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/README.md +0 -0
  33. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/__init__.py +0 -0
  34. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/__main__.py +0 -0
  35. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_cli.py +0 -0
  36. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_function_schema.py +0 -0
  37. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_griffe.py +0 -0
  38. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_mcp.py +0 -0
  39. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_otel_messages.py +0 -0
  40. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_output.py +0 -0
  41. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_run_context.py +0 -0
  42. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_system_prompt.py +0 -0
  43. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_thinking_part.py +0 -0
  44. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/_utils.py +0 -0
  45. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/agent/abstract.py +0 -0
  46. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/agent/wrapper.py +0 -0
  47. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/builtin_tools.py +0 -0
  48. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/common_tools/__init__.py +0 -0
  49. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/common_tools/duckduckgo.py +0 -0
  50. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/common_tools/tavily.py +0 -0
  51. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/direct.py +0 -0
  52. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/durable_exec/__init__.py +0 -0
  53. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/durable_exec/temporal/__init__.py +0 -0
  54. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/durable_exec/temporal/_function_toolset.py +0 -0
  55. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/durable_exec/temporal/_logfire.py +0 -0
  56. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/durable_exec/temporal/_mcp_server.py +0 -0
  57. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/durable_exec/temporal/_model.py +0 -0
  58. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/durable_exec/temporal/_run_context.py +0 -0
  59. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/durable_exec/temporal/_toolset.py +0 -0
  60. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/ext/__init__.py +0 -0
  61. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/ext/aci.py +0 -0
  62. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/ext/langchain.py +0 -0
  63. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/format_prompt.py +0 -0
  64. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/cohere.py +0 -0
  65. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/fallback.py +0 -0
  66. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/function.py +0 -0
  67. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/huggingface.py +0 -0
  68. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/mcp_sampling.py +0 -0
  69. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/mistral.py +0 -0
  70. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/test.py +0 -0
  71. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/models/wrapper.py +0 -0
  72. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/output.py +0 -0
  73. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/__init__.py +0 -0
  74. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/_json_schema.py +0 -0
  75. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/amazon.py +0 -0
  76. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/anthropic.py +0 -0
  77. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/cohere.py +0 -0
  78. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/deepseek.py +0 -0
  79. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/google.py +0 -0
  80. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/grok.py +0 -0
  81. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/groq.py +0 -0
  82. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/harmony.py +0 -0
  83. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/meta.py +0 -0
  84. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/mistral.py +0 -0
  85. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/moonshotai.py +0 -0
  86. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/openai.py +0 -0
  87. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/profiles/qwen.py +0 -0
  88. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/anthropic.py +0 -0
  89. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/azure.py +0 -0
  90. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/bedrock.py +0 -0
  91. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/cerebras.py +0 -0
  92. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/cohere.py +0 -0
  93. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/deepseek.py +0 -0
  94. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/fireworks.py +0 -0
  95. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/github.py +0 -0
  96. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/google.py +0 -0
  97. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/google_gla.py +0 -0
  98. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/grok.py +0 -0
  99. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/heroku.py +0 -0
  100. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/huggingface.py +0 -0
  101. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/mistral.py +0 -0
  102. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/moonshotai.py +0 -0
  103. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/ollama.py +0 -0
  104. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/openai.py +0 -0
  105. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/openrouter.py +0 -0
  106. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/together.py +0 -0
  107. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/providers/vercel.py +0 -0
  108. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/py.typed +0 -0
  109. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/result.py +0 -0
  110. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/run.py +0 -0
  111. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/settings.py +0 -0
  112. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/__init__.py +0 -0
  113. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/_dynamic.py +0 -0
  114. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/abstract.py +0 -0
  115. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/approval_required.py +0 -0
  116. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/external.py +0 -0
  117. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/filtered.py +0 -0
  118. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/prefixed.py +0 -0
  119. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/prepared.py +0 -0
  120. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/renamed.py +0 -0
  121. {pydantic_ai_slim-1.0.0b1 → pydantic_ai_slim-1.0.1}/pydantic_ai/toolsets/wrapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic-ai-slim
3
- Version: 1.0.0b1
3
+ Version: 1.0.1
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
@@ -9,7 +9,7 @@ Project-URL: Changelog, https://github.com/pydantic/pydantic-ai/releases
9
9
  Author-email: Samuel Colvin <samuel@pydantic.dev>, Marcelo Trylesinski <marcelotryle@gmail.com>, David Montague <david@pydantic.dev>, Alex Hall <alex@pydantic.dev>, Douwe Maan <douwe@pydantic.dev>
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
12
- Classifier: Development Status :: 4 - Beta
12
+ Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Environment :: MacOS X
15
15
  Classifier: Intended Audience :: Developers
@@ -28,13 +28,12 @@ Classifier: Programming Language :: Python :: 3.13
28
28
  Classifier: Topic :: Internet
29
29
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
30
30
  Requires-Python: >=3.10
31
- Requires-Dist: eval-type-backport>=0.2.0
32
31
  Requires-Dist: exceptiongroup; python_version < '3.11'
33
32
  Requires-Dist: genai-prices>=0.0.22
34
33
  Requires-Dist: griffe>=1.3.2
35
34
  Requires-Dist: httpx>=0.27
36
35
  Requires-Dist: opentelemetry-api>=1.28.0
37
- Requires-Dist: pydantic-graph==1.0.0b1
36
+ Requires-Dist: pydantic-graph==1.0.1
38
37
  Requires-Dist: pydantic>=2.10
39
38
  Requires-Dist: typing-inspection>=0.4.0
40
39
  Provides-Extra: a2a
@@ -56,7 +55,7 @@ Requires-Dist: cohere>=5.16.0; (platform_system != 'Emscripten') and extra == 'c
56
55
  Provides-Extra: duckduckgo
57
56
  Requires-Dist: ddgs>=9.0.0; extra == 'duckduckgo'
58
57
  Provides-Extra: evals
59
- Requires-Dist: pydantic-evals==1.0.0b1; extra == 'evals'
58
+ Requires-Dist: pydantic-evals==1.0.1; extra == 'evals'
60
59
  Provides-Extra: google
61
60
  Requires-Dist: google-genai>=1.31.0; extra == 'google'
62
61
  Provides-Extra: groq
@@ -66,7 +65,7 @@ Requires-Dist: huggingface-hub[inference]>=0.33.5; extra == 'huggingface'
66
65
  Provides-Extra: logfire
67
66
  Requires-Dist: logfire[httpx]>=3.14.1; extra == 'logfire'
68
67
  Provides-Extra: mcp
69
- Requires-Dist: mcp>=1.12.3; (python_version >= '3.10') and extra == 'mcp'
68
+ Requires-Dist: mcp>=1.12.3; extra == 'mcp'
70
69
  Provides-Extra: mistral
71
70
  Requires-Dist: mistralai>=1.9.2; extra == 'mistral'
72
71
  Provides-Extra: openai
@@ -76,7 +75,7 @@ Requires-Dist: tenacity>=8.2.3; extra == 'retries'
76
75
  Provides-Extra: tavily
77
76
  Requires-Dist: tavily-python>=0.5.0; extra == 'tavily'
78
77
  Provides-Extra: temporal
79
- Requires-Dist: temporalio==1.16.0; extra == 'temporal'
78
+ Requires-Dist: temporalio==1.17.0; extra == 'temporal'
80
79
  Provides-Extra: vertexai
81
80
  Requires-Dist: google-auth>=2.36.0; extra == 'vertexai'
82
81
  Requires-Dist: requests>=2.32.2; extra == 'vertexai'
@@ -272,7 +272,7 @@ class AgentWorker(Worker[list[ModelMessage]], Generic[WorkerOutputT, AgentDepsT]
272
272
  assert_never(part)
273
273
  return model_parts
274
274
 
275
- def _response_parts_to_a2a(self, parts: list[ModelResponsePart]) -> list[Part]:
275
+ def _response_parts_to_a2a(self, parts: Sequence[ModelResponsePart]) -> list[Part]:
276
276
  """Convert pydantic-ai ModelResponsePart objects to A2A Part objects.
277
277
 
278
278
  This handles the conversion from pydantic-ai's internal response parts to
@@ -2,7 +2,6 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  import asyncio
4
4
  import dataclasses
5
- import hashlib
6
5
  from collections import defaultdict, deque
7
6
  from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Sequence
8
7
  from contextlib import asynccontextmanager, contextmanager
@@ -302,16 +301,21 @@ class UserPromptNode(AgentNode[DepsT, NodeRunEndT]):
302
301
  if self.system_prompt_dynamic_functions:
303
302
  for msg in messages:
304
303
  if isinstance(msg, _messages.ModelRequest):
305
- for i, part in enumerate(msg.parts):
304
+ reevaluated_message_parts: list[_messages.ModelRequestPart] = []
305
+ for part in msg.parts:
306
306
  if isinstance(part, _messages.SystemPromptPart) and part.dynamic_ref:
307
307
  # Look up the runner by its ref
308
308
  if runner := self.system_prompt_dynamic_functions.get( # pragma: lax no cover
309
309
  part.dynamic_ref
310
310
  ):
311
311
  updated_part_content = await runner.run(run_context)
312
- msg.parts[i] = _messages.SystemPromptPart(
313
- updated_part_content, dynamic_ref=part.dynamic_ref
314
- )
312
+ part = _messages.SystemPromptPart(updated_part_content, dynamic_ref=part.dynamic_ref)
313
+
314
+ reevaluated_message_parts.append(part)
315
+
316
+ # Replace message parts with reevaluated ones to prevent mutating parts list
317
+ if reevaluated_message_parts != msg.parts:
318
+ msg.parts = reevaluated_message_parts
315
319
 
316
320
  async def _sys_parts(self, run_context: RunContext[DepsT]) -> list[_messages.ModelRequestPart]:
317
321
  """Build the initial messages for the conversation."""
@@ -650,13 +654,6 @@ def build_run_context(ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT
650
654
  )
651
655
 
652
656
 
653
- def multi_modal_content_identifier(identifier: str | bytes) -> str:
654
- """Generate stable identifier for multi-modal content to help LLM in finding a specific file in tool call responses."""
655
- if isinstance(identifier, str):
656
- identifier = identifier.encode('utf-8')
657
- return hashlib.sha1(identifier).hexdigest()[:6]
658
-
659
-
660
657
  async def process_function_tools( # noqa: C901
661
658
  tool_manager: ToolManager[DepsT],
662
659
  tool_calls: list[_messages.ToolCallPart],
@@ -764,6 +761,7 @@ async def process_function_tools( # noqa: C901
764
761
  calls_to_run,
765
762
  deferred_tool_results,
766
763
  ctx.deps.tracer,
764
+ ctx.deps.usage_limits,
767
765
  output_parts,
768
766
  deferred_calls,
769
767
  ):
@@ -810,6 +808,7 @@ async def _call_tools(
810
808
  tool_calls: list[_messages.ToolCallPart],
811
809
  deferred_tool_results: dict[str, DeferredToolResult],
812
810
  tracer: Tracer,
811
+ usage_limits: _usage.UsageLimits | None,
813
812
  output_parts: list[_messages.ModelRequestPart],
814
813
  output_deferred_calls: dict[Literal['external', 'unapproved'], list[_messages.ToolCallPart]],
815
814
  ) -> AsyncIterator[_messages.HandleResponseEvent]:
@@ -830,7 +829,7 @@ async def _call_tools(
830
829
  ):
831
830
  tasks = [
832
831
  asyncio.create_task(
833
- _call_tool(tool_manager, call, deferred_tool_results.get(call.tool_call_id)),
832
+ _call_tool(tool_manager, call, deferred_tool_results.get(call.tool_call_id), usage_limits),
834
833
  name=call.tool_name,
835
834
  )
836
835
  for call in tool_calls
@@ -870,14 +869,15 @@ async def _call_tool(
870
869
  tool_manager: ToolManager[DepsT],
871
870
  tool_call: _messages.ToolCallPart,
872
871
  tool_call_result: DeferredToolResult | None,
872
+ usage_limits: _usage.UsageLimits | None,
873
873
  ) -> tuple[_messages.ToolReturnPart | _messages.RetryPromptPart, _messages.UserPromptPart | None]:
874
874
  try:
875
875
  if tool_call_result is None:
876
- tool_result = await tool_manager.handle_call(tool_call)
876
+ tool_result = await tool_manager.handle_call(tool_call, usage_limits=usage_limits)
877
877
  elif isinstance(tool_call_result, ToolApproved):
878
878
  if tool_call_result.override_args is not None:
879
879
  tool_call = dataclasses.replace(tool_call, args=tool_call_result.override_args)
880
- tool_result = await tool_manager.handle_call(tool_call)
880
+ tool_result = await tool_manager.handle_call(tool_call, usage_limits=usage_limits)
881
881
  elif isinstance(tool_call_result, ToolDenied):
882
882
  return _messages.ToolReturnPart(
883
883
  tool_name=tool_call.tool_name,
@@ -915,10 +915,7 @@ async def _call_tool(
915
915
  f'`ToolReturn` should be used directly.'
916
916
  )
917
917
  elif isinstance(content, _messages.MultiModalContent):
918
- if isinstance(content, _messages.BinaryContent):
919
- identifier = content.identifier or multi_modal_content_identifier(content.data)
920
- else:
921
- identifier = multi_modal_content_identifier(content.url)
918
+ identifier = content.identifier
922
919
 
923
920
  return_values.append(f'See file {identifier}')
924
921
  user_contents.extend([f'This is file {identifier}:', content])
@@ -154,6 +154,7 @@ class ModelResponsePartsManager:
154
154
  *,
155
155
  vendor_part_id: Hashable | None,
156
156
  content: str | None = None,
157
+ id: str | None = None,
157
158
  signature: str | None = None,
158
159
  ) -> ModelResponseStreamEvent:
159
160
  """Handle incoming thinking content, creating or updating a ThinkingPart in the manager as appropriate.
@@ -167,6 +168,7 @@ class ModelResponsePartsManager:
167
168
  of thinking. If None, a new part will be created unless the latest part is already
168
169
  a ThinkingPart.
169
170
  content: The thinking content to append to the appropriate ThinkingPart.
171
+ id: An optional id for the thinking part.
170
172
  signature: An optional signature for the thinking content.
171
173
 
172
174
  Returns:
@@ -197,7 +199,7 @@ class ModelResponsePartsManager:
197
199
  if content is not None:
198
200
  # There is no existing thinking part that should be updated, so create a new one
199
201
  new_part_index = len(self._parts)
200
- part = ThinkingPart(content=content, signature=signature)
202
+ part = ThinkingPart(content=content, id=id, signature=signature)
201
203
  if vendor_part_id is not None: # pragma: no branch
202
204
  self._vendor_id_to_part_index[vendor_part_id] = new_part_index
203
205
  self._parts.append(part)
@@ -14,6 +14,7 @@ from .exceptions import ModelRetry, ToolRetryError, UnexpectedModelBehavior
14
14
  from .messages import ToolCallPart
15
15
  from .tools import ToolDefinition
16
16
  from .toolsets.abstract import AbstractToolset, ToolsetTool
17
+ from .usage import UsageLimits
17
18
 
18
19
 
19
20
  @dataclass
@@ -66,7 +67,11 @@ class ToolManager(Generic[AgentDepsT]):
66
67
  return None
67
68
 
68
69
  async def handle_call(
69
- self, call: ToolCallPart, allow_partial: bool = False, wrap_validation_errors: bool = True
70
+ self,
71
+ call: ToolCallPart,
72
+ allow_partial: bool = False,
73
+ wrap_validation_errors: bool = True,
74
+ usage_limits: UsageLimits | None = None,
70
75
  ) -> Any:
71
76
  """Handle a tool call by validating the arguments, calling the tool, and handling retries.
72
77
 
@@ -74,13 +79,14 @@ class ToolManager(Generic[AgentDepsT]):
74
79
  call: The tool call part to handle.
75
80
  allow_partial: Whether to allow partial validation of the tool arguments.
76
81
  wrap_validation_errors: Whether to wrap validation errors in a retry prompt part.
82
+ usage_limits: Optional usage limits to check before executing tools.
77
83
  """
78
84
  if self.tools is None or self.ctx is None:
79
85
  raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
80
86
 
81
87
  if (tool := self.tools.get(call.tool_name)) and tool.tool_def.kind == 'output':
82
- # Output tool calls are not traced
83
- return await self._call_tool(call, allow_partial, wrap_validation_errors)
88
+ # Output tool calls are not traced and not counted
89
+ return await self._call_tool(call, allow_partial, wrap_validation_errors, count_tool_usage=False)
84
90
  else:
85
91
  return await self._call_tool_traced(
86
92
  call,
@@ -88,9 +94,17 @@ class ToolManager(Generic[AgentDepsT]):
88
94
  wrap_validation_errors,
89
95
  self.ctx.tracer,
90
96
  self.ctx.trace_include_content,
97
+ usage_limits,
91
98
  )
92
99
 
93
- async def _call_tool(self, call: ToolCallPart, allow_partial: bool, wrap_validation_errors: bool) -> Any:
100
+ async def _call_tool(
101
+ self,
102
+ call: ToolCallPart,
103
+ allow_partial: bool,
104
+ wrap_validation_errors: bool,
105
+ usage_limits: UsageLimits | None = None,
106
+ count_tool_usage: bool = True,
107
+ ) -> Any:
94
108
  if self.tools is None or self.ctx is None:
95
109
  raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
96
110
 
@@ -121,7 +135,15 @@ class ToolManager(Generic[AgentDepsT]):
121
135
  else:
122
136
  args_dict = validator.validate_python(call.args or {}, allow_partial=pyd_allow_partial)
123
137
 
124
- return await self.toolset.call_tool(name, args_dict, ctx, tool)
138
+ if usage_limits is not None and count_tool_usage:
139
+ usage_limits.check_before_tool_call(self.ctx.usage)
140
+
141
+ result = await self.toolset.call_tool(name, args_dict, ctx, tool)
142
+
143
+ if count_tool_usage:
144
+ self.ctx.usage.tool_calls += 1
145
+
146
+ return result
125
147
  except (ValidationError, ModelRetry) as e:
126
148
  max_retries = tool.max_retries if tool is not None else 1
127
149
  current_retry = self.ctx.retries.get(name, 0)
@@ -160,6 +182,7 @@ class ToolManager(Generic[AgentDepsT]):
160
182
  wrap_validation_errors: bool,
161
183
  tracer: Tracer,
162
184
  include_content: bool = False,
185
+ usage_limits: UsageLimits | None = None,
163
186
  ) -> Any:
164
187
  """See <https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-spans/#execute-tool-span>."""
165
188
  span_attributes = {
@@ -189,7 +212,7 @@ class ToolManager(Generic[AgentDepsT]):
189
212
  }
190
213
  with tracer.start_as_current_span('running tool', attributes=span_attributes) as span:
191
214
  try:
192
- tool_result = await self._call_tool(call, allow_partial, wrap_validation_errors)
215
+ tool_result = await self._call_tool(call, allow_partial, wrap_validation_errors, usage_limits)
193
216
  except ToolRetryError as e:
194
217
  part = e.tool_retry
195
218
  if include_content and span.is_recording():
@@ -68,6 +68,9 @@ try:
68
68
  TextMessageContentEvent,
69
69
  TextMessageEndEvent,
70
70
  TextMessageStartEvent,
71
+ # TODO: Enable once https://github.com/ag-ui-protocol/ag-ui/issues/289 is resolved.
72
+ # ThinkingEndEvent,
73
+ # ThinkingStartEvent,
71
74
  ThinkingTextMessageContentEvent,
72
75
  ThinkingTextMessageEndEvent,
73
76
  ThinkingTextMessageStartEvent,
@@ -392,6 +395,12 @@ async def _agent_stream(run: AgentRun[AgentDepsT, Any]) -> AsyncIterator[BaseEve
392
395
  if stream_ctx.part_end: # pragma: no branch
393
396
  yield stream_ctx.part_end
394
397
  stream_ctx.part_end = None
398
+ if stream_ctx.thinking:
399
+ # TODO: Enable once https://github.com/ag-ui-protocol/ag-ui/issues/289 is resolved.
400
+ # yield ThinkingEndEvent(
401
+ # type=EventType.THINKING_END,
402
+ # )
403
+ stream_ctx.thinking = False
395
404
  elif isinstance(node, CallToolsNode):
396
405
  async with node.stream(run.ctx) as handle_stream:
397
406
  async for event in handle_stream:
@@ -400,7 +409,7 @@ async def _agent_stream(run: AgentRun[AgentDepsT, Any]) -> AsyncIterator[BaseEve
400
409
  yield msg
401
410
 
402
411
 
403
- async def _handle_model_request_event(
412
+ async def _handle_model_request_event( # noqa: C901
404
413
  stream_ctx: _RequestStreamContext,
405
414
  agent_event: ModelResponseStreamEvent,
406
415
  ) -> AsyncIterator[BaseEvent]:
@@ -420,56 +429,70 @@ async def _handle_model_request_event(
420
429
  stream_ctx.part_end = None
421
430
 
422
431
  part = agent_event.part
423
- if isinstance(part, TextPart):
424
- message_id = stream_ctx.new_message_id()
425
- yield TextMessageStartEvent(
426
- message_id=message_id,
427
- )
428
- if part.content: # pragma: no branch
429
- yield TextMessageContentEvent(
430
- message_id=message_id,
432
+ if isinstance(part, ThinkingPart): # pragma: no branch
433
+ if not stream_ctx.thinking:
434
+ # TODO: Enable once https://github.com/ag-ui-protocol/ag-ui/issues/289 is resolved.
435
+ # yield ThinkingStartEvent(
436
+ # type=EventType.THINKING_START,
437
+ # )
438
+ stream_ctx.thinking = True
439
+
440
+ if part.content:
441
+ yield ThinkingTextMessageStartEvent(
442
+ type=EventType.THINKING_TEXT_MESSAGE_START,
443
+ )
444
+ yield ThinkingTextMessageContentEvent(
445
+ type=EventType.THINKING_TEXT_MESSAGE_CONTENT,
431
446
  delta=part.content,
432
447
  )
433
- stream_ctx.part_end = TextMessageEndEvent(
434
- message_id=message_id,
435
- )
436
- elif isinstance(part, ToolCallPart): # pragma: no branch
437
- message_id = stream_ctx.message_id or stream_ctx.new_message_id()
438
- yield ToolCallStartEvent(
439
- tool_call_id=part.tool_call_id,
440
- tool_call_name=part.tool_name,
441
- parent_message_id=message_id,
442
- )
443
- if part.args:
444
- yield ToolCallArgsEvent(
448
+ stream_ctx.part_end = ThinkingTextMessageEndEvent(
449
+ type=EventType.THINKING_TEXT_MESSAGE_END,
450
+ )
451
+ else:
452
+ if stream_ctx.thinking:
453
+ # TODO: Enable once https://github.com/ag-ui-protocol/ag-ui/issues/289 is resolved.
454
+ # yield ThinkingEndEvent(
455
+ # type=EventType.THINKING_END,
456
+ # )
457
+ stream_ctx.thinking = False
458
+
459
+ if isinstance(part, TextPart):
460
+ message_id = stream_ctx.new_message_id()
461
+ yield TextMessageStartEvent(
462
+ message_id=message_id,
463
+ )
464
+ if part.content: # pragma: no branch
465
+ yield TextMessageContentEvent(
466
+ message_id=message_id,
467
+ delta=part.content,
468
+ )
469
+ stream_ctx.part_end = TextMessageEndEvent(
470
+ message_id=message_id,
471
+ )
472
+ elif isinstance(part, ToolCallPart): # pragma: no branch
473
+ message_id = stream_ctx.message_id or stream_ctx.new_message_id()
474
+ yield ToolCallStartEvent(
475
+ tool_call_id=part.tool_call_id,
476
+ tool_call_name=part.tool_name,
477
+ parent_message_id=message_id,
478
+ )
479
+ if part.args:
480
+ yield ToolCallArgsEvent(
481
+ tool_call_id=part.tool_call_id,
482
+ delta=part.args if isinstance(part.args, str) else json.dumps(part.args),
483
+ )
484
+ stream_ctx.part_end = ToolCallEndEvent(
445
485
  tool_call_id=part.tool_call_id,
446
- delta=part.args if isinstance(part.args, str) else json.dumps(part.args),
447
486
  )
448
- stream_ctx.part_end = ToolCallEndEvent(
449
- tool_call_id=part.tool_call_id,
450
- )
451
-
452
- elif isinstance(part, ThinkingPart): # pragma: no branch
453
- yield ThinkingTextMessageStartEvent(
454
- type=EventType.THINKING_TEXT_MESSAGE_START,
455
- )
456
- # Always send the content even if it's empty, as it may be
457
- # used to indicate the start of thinking.
458
- yield ThinkingTextMessageContentEvent(
459
- type=EventType.THINKING_TEXT_MESSAGE_CONTENT,
460
- delta=part.content,
461
- )
462
- stream_ctx.part_end = ThinkingTextMessageEndEvent(
463
- type=EventType.THINKING_TEXT_MESSAGE_END,
464
- )
465
487
 
466
488
  elif isinstance(agent_event, PartDeltaEvent):
467
489
  delta = agent_event.delta
468
490
  if isinstance(delta, TextPartDelta):
469
- yield TextMessageContentEvent(
470
- message_id=stream_ctx.message_id,
471
- delta=delta.content_delta,
472
- )
491
+ if delta.content_delta: # pragma: no branch
492
+ yield TextMessageContentEvent(
493
+ message_id=stream_ctx.message_id,
494
+ delta=delta.content_delta,
495
+ )
473
496
  elif isinstance(delta, ToolCallPartDelta): # pragma: no branch
474
497
  assert delta.tool_call_id, '`ToolCallPartDelta.tool_call_id` must be set'
475
498
  yield ToolCallArgsEvent(
@@ -478,6 +501,14 @@ async def _handle_model_request_event(
478
501
  )
479
502
  elif isinstance(delta, ThinkingPartDelta): # pragma: no branch
480
503
  if delta.content_delta: # pragma: no branch
504
+ if not isinstance(stream_ctx.part_end, ThinkingTextMessageEndEvent):
505
+ yield ThinkingTextMessageStartEvent(
506
+ type=EventType.THINKING_TEXT_MESSAGE_START,
507
+ )
508
+ stream_ctx.part_end = ThinkingTextMessageEndEvent(
509
+ type=EventType.THINKING_TEXT_MESSAGE_END,
510
+ )
511
+
481
512
  yield ThinkingTextMessageContentEvent(
482
513
  type=EventType.THINKING_TEXT_MESSAGE_CONTENT,
483
514
  delta=delta.content_delta,
@@ -629,6 +660,7 @@ class _RequestStreamContext:
629
660
 
630
661
  message_id: str = ''
631
662
  part_end: BaseEvent | None = None
663
+ thinking: bool = False
632
664
 
633
665
  def new_message_id(self) -> str:
634
666
  """Generate a new message ID for the request stream.
@@ -4,15 +4,15 @@ import dataclasses
4
4
  import inspect
5
5
  import json
6
6
  import warnings
7
+ from asyncio import Lock
7
8
  from collections.abc import AsyncIterator, Awaitable, Callable, Iterator, Sequence
8
9
  from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager, contextmanager
9
10
  from contextvars import ContextVar
10
11
  from typing import TYPE_CHECKING, Any, ClassVar, cast, overload
11
12
 
12
- import anyio
13
13
  from opentelemetry.trace import NoOpTracer, use_span
14
14
  from pydantic.json_schema import GenerateJsonSchema
15
- from typing_extensions import TypeVar, deprecated
15
+ from typing_extensions import Self, TypeVar, deprecated
16
16
 
17
17
  from pydantic_graph import Graph
18
18
 
@@ -157,7 +157,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
157
157
 
158
158
  _event_stream_handler: EventStreamHandler[AgentDepsT] | None = dataclasses.field(repr=False)
159
159
 
160
- _enter_lock: anyio.Lock = dataclasses.field(repr=False)
160
+ _enter_lock: Lock = dataclasses.field(repr=False)
161
161
  _entered_count: int = dataclasses.field(repr=False)
162
162
  _exit_stack: AsyncExitStack | None = dataclasses.field(repr=False)
163
163
 
@@ -374,7 +374,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
374
374
  _utils.Option[Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]]]
375
375
  ] = ContextVar('_override_tools', default=None)
376
376
 
377
- self._enter_lock = anyio.Lock()
377
+ self._enter_lock = Lock()
378
378
  self._entered_count = 0
379
379
  self._exit_stack = None
380
380
 
@@ -1066,7 +1066,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
1066
1066
  strict: Whether to enforce JSON schema compliance (only affects OpenAI).
1067
1067
  See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
1068
1068
  requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
1069
- See the [tools documentation](../tools.md#human-in-the-loop-tool-approval) for more info.
1069
+ See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
1070
1070
  """
1071
1071
 
1072
1072
  def tool_decorator(
@@ -1165,7 +1165,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
1165
1165
  strict: Whether to enforce JSON schema compliance (only affects OpenAI).
1166
1166
  See [`ToolDefinition`][pydantic_ai.tools.ToolDefinition] for more info.
1167
1167
  requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
1168
- See the [tools documentation](../tools.md#human-in-the-loop-tool-approval) for more info.
1168
+ See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
1169
1169
  """
1170
1170
 
1171
1171
  def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams]:
@@ -1355,7 +1355,7 @@ class Agent(AbstractAgent[AgentDepsT, OutputDataT]):
1355
1355
 
1356
1356
  return schema # pyright: ignore[reportReturnType]
1357
1357
 
1358
- async def __aenter__(self) -> AbstractAgent[AgentDepsT, OutputDataT]:
1358
+ async def __aenter__(self) -> Self:
1359
1359
  """Enter the agent context.
1360
1360
 
1361
1361
  This will start all [`MCPServerStdio`s][pydantic_ai.mcp.MCPServerStdio] registered as `toolsets` so they are ready to be used.