Skip to content

Commit 6de3cdb

Browse files
committed
Merge remote-tracking branch 'origin/main' into support-api-type
2 parents dafa104 + faca9c4 commit 6de3cdb

File tree

15 files changed

+190
-72
lines changed

15 files changed

+190
-72
lines changed

docs/deferred-tools.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,18 @@ print(result.all_messages())
142142
),
143143
ModelRequest(
144144
parts=[
145-
ToolReturnPart(
146-
tool_name='delete_file',
147-
content='Deleting files is not allowed',
148-
tool_call_id='delete_file',
149-
timestamp=datetime.datetime(...),
150-
),
151145
ToolReturnPart(
152146
tool_name='update_file',
153147
content="File '.env' updated: ''",
154148
tool_call_id='update_file_dotenv',
155149
timestamp=datetime.datetime(...),
156150
),
151+
ToolReturnPart(
152+
tool_name='delete_file',
153+
content='Deleting files is not allowed',
154+
tool_call_id='delete_file',
155+
timestamp=datetime.datetime(...),
156+
),
157157
]
158158
),
159159
ModelResponse(

docs/logfire.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ The following providers have dedicated documentation on Pydantic AI:
263263
- [Agenta](https://docs.agenta.ai/observability/integrations/pydanticai)
264264
- [Confident AI](https://documentation.confident-ai.com/docs/llm-tracing/integrations/pydanticai)
265265
- [LangWatch](https://docs.langwatch.ai/integration/python/integrations/pydantic-ai)
266+
- [Braintrust](https://www.braintrust.dev/docs/integrations/sdk-integrations/pydantic-ai)
266267

267268
## Advanced usage
268269

docs/models/overview.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ You can use [`FallbackModel`][pydantic_ai.models.fallback.FallbackModel] to atte
8686
in sequence until one successfully returns a result. Under the hood, Pydantic AI automatically switches
8787
from one model to the next if the current model returns a 4xx or 5xx status code.
8888

89+
!!! note
90+
91+
The provider SDKs on which Models are based (like OpenAI, Anthropic, etc.) often have built-in retry logic that can delay the `FallbackModel` from activating.
92+
93+
When using `FallbackModel`, it's recommended to disable provider SDK retries to ensure immediate fallback, for example by setting `max_retries=0` on a [custom OpenAI client](openai.md#custom-openai-client).
94+
8995
In the following example, the agent first makes a request to the OpenAI model (which fails due to an invalid API key),
9096
and then falls back to the Anthropic model.
9197

pydantic_ai_slim/pydantic_ai/_agent_graph.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,6 @@ def build_run_context(ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT
775775
if ctx.deps.instrumentation_settings
776776
else DEFAULT_INSTRUMENTATION_VERSION,
777777
run_step=ctx.state.run_step,
778-
tool_call_approved=ctx.state.run_step == 0,
779778
)
780779

781780

@@ -1039,7 +1038,7 @@ async def _call_tool(
10391038
elif isinstance(tool_call_result, ToolApproved):
10401039
if tool_call_result.override_args is not None:
10411040
tool_call = dataclasses.replace(tool_call, args=tool_call_result.override_args)
1042-
tool_result = await tool_manager.handle_call(tool_call)
1041+
tool_result = await tool_manager.handle_call(tool_call, approved=True)
10431042
elif isinstance(tool_call_result, ToolDenied):
10441043
return _messages.ToolReturnPart(
10451044
tool_name=tool_call.tool_name,

pydantic_ai_slim/pydantic_ai/_tool_manager.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -93,37 +93,47 @@ async def handle_call(
9393
call: ToolCallPart,
9494
allow_partial: bool = False,
9595
wrap_validation_errors: bool = True,
96+
*,
97+
approved: bool = False,
9698
) -> Any:
9799
"""Handle a tool call by validating the arguments, calling the tool, and handling retries.
98100
99101
Args:
100102
call: The tool call part to handle.
101103
allow_partial: Whether to allow partial validation of the tool arguments.
102104
wrap_validation_errors: Whether to wrap validation errors in a retry prompt part.
103-
usage_limits: Optional usage limits to check before executing tools.
105+
approved: Whether the tool call has been approved.
104106
"""
105107
if self.tools is None or self.ctx is None:
106108
raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
107109

108110
if (tool := self.tools.get(call.tool_name)) and tool.tool_def.kind == 'output':
109111
# Output tool calls are not traced and not counted
110-
return await self._call_tool(call, allow_partial, wrap_validation_errors)
112+
return await self._call_tool(
113+
call,
114+
allow_partial=allow_partial,
115+
wrap_validation_errors=wrap_validation_errors,
116+
approved=approved,
117+
)
111118
else:
112119
return await self._call_function_tool(
113120
call,
114-
allow_partial,
115-
wrap_validation_errors,
116-
self.ctx.tracer,
117-
self.ctx.trace_include_content,
118-
self.ctx.instrumentation_version,
119-
self.ctx.usage,
121+
allow_partial=allow_partial,
122+
wrap_validation_errors=wrap_validation_errors,
123+
approved=approved,
124+
tracer=self.ctx.tracer,
125+
include_content=self.ctx.trace_include_content,
126+
instrumentation_version=self.ctx.instrumentation_version,
127+
usage=self.ctx.usage,
120128
)
121129

122130
async def _call_tool(
123131
self,
124132
call: ToolCallPart,
133+
*,
125134
allow_partial: bool,
126135
wrap_validation_errors: bool,
136+
approved: bool,
127137
) -> Any:
128138
if self.tools is None or self.ctx is None:
129139
raise ValueError('ToolManager has not been prepared for a run step yet') # pragma: no cover
@@ -138,15 +148,16 @@ async def _call_tool(
138148
msg = 'No tools available.'
139149
raise ModelRetry(f'Unknown tool name: {name!r}. {msg}')
140150

141-
if tool.tool_def.defer:
142-
raise RuntimeError('Deferred tools cannot be called')
151+
if tool.tool_def.kind == 'external':
152+
raise RuntimeError('External tools cannot be called')
143153

144154
ctx = replace(
145155
self.ctx,
146156
tool_name=name,
147157
tool_call_id=call.tool_call_id,
148158
retry=self.ctx.retries.get(name, 0),
149159
max_retries=tool.max_retries,
160+
tool_call_approved=approved,
150161
partial_output=allow_partial,
151162
)
152163

@@ -194,8 +205,10 @@ async def _call_tool(
194205
async def _call_function_tool(
195206
self,
196207
call: ToolCallPart,
208+
*,
197209
allow_partial: bool,
198210
wrap_validation_errors: bool,
211+
approved: bool,
199212
tracer: Tracer,
200213
include_content: bool,
201214
instrumentation_version: int,
@@ -234,7 +247,12 @@ async def _call_function_tool(
234247
attributes=span_attributes,
235248
) as span:
236249
try:
237-
tool_result = await self._call_tool(call, allow_partial, wrap_validation_errors)
250+
tool_result = await self._call_tool(
251+
call,
252+
allow_partial=allow_partial,
253+
wrap_validation_errors=wrap_validation_errors,
254+
approved=approved,
255+
)
238256
usage.tool_calls += 1
239257

240258
except ToolRetryError as e:

pydantic_ai_slim/pydantic_ai/models/__init__.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,16 @@
126126
'cerebras:gpt-oss-120b',
127127
'cerebras:llama3.1-8b',
128128
'cerebras:llama-3.3-70b',
129-
'cerebras:llama-4-scout-17b-16e-instruct',
130-
'cerebras:llama-4-maverick-17b-128e-instruct',
131129
'cerebras:qwen-3-235b-a22b-instruct-2507',
132130
'cerebras:qwen-3-32b',
133-
'cerebras:qwen-3-coder-480b',
134131
'cerebras:qwen-3-235b-a22b-thinking-2507',
135132
'cohere:c4ai-aya-expanse-32b',
136133
'cohere:c4ai-aya-expanse-8b',
137134
'cohere:command-nightly',
138135
'cohere:command-r-08-2024',
139136
'cohere:command-r-plus-08-2024',
140137
'cohere:command-r7b-12-2024',
138+
'cerebras:zai-glm-4.6',
141139
'deepseek:deepseek-chat',
142140
'deepseek:deepseek-reasoner',
143141
'google-gla:gemini-2.0-flash',
@@ -189,11 +187,15 @@
189187
'groq:llama-3.2-3b-preview',
190188
'groq:llama-3.2-11b-vision-preview',
191189
'groq:llama-3.2-90b-vision-preview',
190+
'heroku:amazon-rerank-1-0',
192191
'heroku:claude-3-5-haiku',
193192
'heroku:claude-3-5-sonnet-latest',
194193
'heroku:claude-3-7-sonnet',
195-
'heroku:claude-4-sonnet',
196194
'heroku:claude-3-haiku',
195+
'heroku:claude-4-5-haiku',
196+
'heroku:claude-4-5-sonnet',
197+
'heroku:claude-4-sonnet',
198+
'heroku:cohere-rerank-3-5',
197199
'heroku:gpt-oss-120b',
198200
'heroku:nova-lite',
199201
'heroku:nova-pro',

pydantic_ai_slim/pydantic_ai/models/google.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
677677
provider_name=self.provider_name,
678678
)
679679

680-
if part.text is not None:
680+
if part.text:
681681
if part.thought:
682682
yield self._parts_manager.handle_thinking_delta(vendor_part_id='thinking', content=part.text)
683683
else:
@@ -822,7 +822,7 @@ def _process_response_from_parts(
822822
elif part.code_execution_result is not None:
823823
assert code_execution_tool_call_id is not None
824824
item = _map_code_execution_result(part.code_execution_result, provider_name, code_execution_tool_call_id)
825-
elif part.text is not None:
825+
elif part.text:
826826
if part.thought:
827827
item = ThinkingPart(content=part.text)
828828
else:

pydantic_ai_slim/pydantic_ai/run.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,36 @@ def result(self) -> AgentRunResult[OutputDataT] | None:
135135
self._traceparent(required=False),
136136
)
137137

138+
def all_messages(self) -> list[_messages.ModelMessage]:
139+
"""Return all messages for the run so far.
140+
141+
Messages from older runs are included.
142+
"""
143+
return self.ctx.state.message_history
144+
145+
def all_messages_json(self, *, output_tool_return_content: str | None = None) -> bytes:
146+
"""Return all messages from [`all_messages`][pydantic_ai.agent.AgentRun.all_messages] as JSON bytes.
147+
148+
Returns:
149+
JSON bytes representing the messages.
150+
"""
151+
return _messages.ModelMessagesTypeAdapter.dump_json(self.all_messages())
152+
153+
def new_messages(self) -> list[_messages.ModelMessage]:
154+
"""Return new messages for the run so far.
155+
156+
Messages from older runs are excluded.
157+
"""
158+
return self.all_messages()[self.ctx.deps.new_message_index :]
159+
160+
def new_messages_json(self) -> bytes:
161+
"""Return new messages from [`new_messages`][pydantic_ai.agent.AgentRun.new_messages] as JSON bytes.
162+
163+
Returns:
164+
JSON bytes representing the new messages.
165+
"""
166+
return _messages.ModelMessagesTypeAdapter.dump_json(self.new_messages())
167+
138168
def __aiter__(
139169
self,
140170
) -> AsyncIterator[_agent_graph.AgentNode[AgentDepsT, OutputDataT] | End[FinalResult[OutputDataT]]]:

pydantic_ai_slim/pydantic_ai/tools.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations as _annotations
22

33
from collections.abc import Awaitable, Callable, Sequence
4-
from dataclasses import KW_ONLY, dataclass, field, replace
4+
from dataclasses import KW_ONLY, dataclass, field
55
from typing import Annotated, Any, Concatenate, Generic, Literal, TypeAlias, cast
66

77
from pydantic import Discriminator, Tag
@@ -415,6 +415,7 @@ def tool_def(self):
415415
strict=self.strict,
416416
sequential=self.sequential,
417417
metadata=self.metadata,
418+
kind='unapproved' if self.requires_approval else 'function',
418419
)
419420

420421
async def prepare_tool_def(self, ctx: RunContext[ToolAgentDepsT]) -> ToolDefinition | None:
@@ -428,9 +429,6 @@ async def prepare_tool_def(self, ctx: RunContext[ToolAgentDepsT]) -> ToolDefinit
428429
"""
429430
base_tool_def = self.tool_def
430431

431-
if self.requires_approval and not ctx.tool_call_approved:
432-
base_tool_def = replace(base_tool_def, kind='unapproved')
433-
434432
if self.prepare is not None:
435433
return await self.prepare(ctx, base_tool_def)
436434
else:

pydantic_ai_slim/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ ag-ui = ["ag-ui-protocol>=0.1.8", "starlette>=0.45.3"]
106106
# Retries
107107
retries = ["tenacity>=8.2.3"]
108108
# Temporal
109-
temporal = ["temporalio==1.18.0"]
109+
temporal = ["temporalio==1.18.2"]
110110
# DBOS
111111
dbos = ["dbos>=1.14.0"]
112112
# Prefect

0 commit comments

Comments
 (0)