Skip to content

Commit 4b7f0c1

Browse files
authored
Merge branch 'main' into handle-streamed-thinking-over-multiple-chunks
2 parents b9bdd78 + 59faf42 commit 4b7f0c1

File tree

28 files changed

+850
-151
lines changed

28 files changed

+850
-151
lines changed

.github/workflows/after-ci.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ jobs:
4545
name: deploy-docs-preview
4646

4747
steps:
48+
- run: echo "$GITHUB_EVENT_JSON"
49+
env:
50+
GITHUB_EVENT_JSON: ${{ toJSON(github.event) }}
51+
4852
- uses: actions/checkout@v4
4953

5054
- uses: actions/setup-node@v4
@@ -57,7 +61,8 @@ jobs:
5761
enable-cache: true
5862
cache-suffix: deploy-docs-preview
5963

60-
- uses: dawidd6/action-download-artifact@v6
64+
- id: download-artifact
65+
uses: dawidd6/action-download-artifact@v6
6166
with:
6267
workflow: ci.yml
6368
name: site
@@ -69,6 +74,7 @@ jobs:
6974

7075
- uses: cloudflare/wrangler-action@v3
7176
id: deploy
77+
if: steps.download-artifact.outputs.found_artifact == 'true'
7278
with:
7379
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
7480
environment: previews
@@ -78,12 +84,9 @@ jobs:
7884
--var GIT_COMMIT_SHA:${{ github.event.workflow_run.head_sha }}
7985
--var GIT_BRANCH:${{ github.event.workflow_run.head_branch }}
8086
81-
- run: echo "$GITHUB_EVENT_JSON"
82-
env:
83-
GITHUB_EVENT_JSON: ${{ toJSON(github.event) }}
84-
8587
- name: Set preview URL
8688
run: uv run --no-project --with httpx .github/set_docs_pr_preview_url.py
89+
if: steps.deploy.outcome == 'success'
8790
env:
8891
DEPLOY_OUTPUT: ${{ steps.deploy.outputs.command-output }}
8992
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

docs/agents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ There are five ways to run an agent:
6565

6666
1. [`agent.run()`][pydantic_ai.agent.AbstractAgent.run] — an async function which returns a [`RunResult`][pydantic_ai.agent.AgentRunResult] containing a completed response.
6767
2. [`agent.run_sync()`][pydantic_ai.agent.AbstractAgent.run_sync] — a plain, synchronous function which returns a [`RunResult`][pydantic_ai.agent.AgentRunResult] containing a completed response (internally, this just calls `loop.run_until_complete(self.run())`).
68-
3. [`agent.run_stream()`][pydantic_ai.agent.AbstractAgent.run_stream] — an async context manager which returns a [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult], which contains methods to stream text and structured output as an async iterable.
68+
3. [`agent.run_stream()`][pydantic_ai.agent.AbstractAgent.run_stream] — an async context manager which returns a [`StreamedRunResult`][pydantic_ai.result.StreamedRunResult], which contains methods to stream text and structured output as an async iterable. [`agent.run_stream_sync()`][pydantic_ai.agent.AbstractAgent.run_stream_sync] is a synchronous variation that returns a [`StreamedRunResultSync`][pydantic_ai.result.StreamedRunResultSync] with synchronous versions of the same methods.
6969
4. [`agent.run_stream_events()`][pydantic_ai.agent.AbstractAgent.run_stream_events] — a function which returns an async iterable of [`AgentStreamEvent`s][pydantic_ai.messages.AgentStreamEvent] and a [`AgentRunResultEvent`][pydantic_ai.run.AgentRunResultEvent] containing the final run result.
7070
5. [`agent.iter()`][pydantic_ai.Agent.iter] — a context manager which returns an [`AgentRun`][pydantic_ai.agent.AgentRun], an async iterable over the nodes of the agent's underlying [`Graph`][pydantic_graph.graph.Graph].
7171

docs/api/result.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
inherited_members: true
66
members:
77
- StreamedRunResult
8+
- StreamedRunResultSync

docs/changelog.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Upgrade Guide
22

3-
In September 2025, Pydantic AI reached V1, which means we're committed to API stability: we will not introduce changes that break your code until V2 (if we do, you can shout at us as it's definitely a mistake).
4-
Once we release V2, in April 2026 at the earliest, we'll continue to provide security fixes for V1 for another 6 months minimum, so you have time to upgrade your applications.
3+
In September 2025, Pydantic AI reached V1, which means we're committed to API stability: we will not introduce changes that break your code until V2. For more information, review our [Version Policy](version-policy.md).
54

65
## Breaking Changes
76

docs/examples/ag-ui.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Next run the AG-UI Dojo example frontend.
4545
2. Change into to the `ag-ui/typescript-sdk` directory
4646

4747
```shell
48-
cd ag-ui/typescript-sdk
48+
cd ag-ui/sdks/typescript
4949
```
5050

5151
3. Run the Dojo app following the [official instructions](https://github.com/ag-ui-protocol/ag-ui/tree/main/typescript-sdk/apps/dojo#development-setup)

docs/version-policy.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## Version Policy
2+
3+
We will not intentionally make breaking changes in minor releases of V1. V2 will be released in April 2026 at the earliest, 6 months after the release of V1 in September 2025.
4+
5+
Once we release V2, we'll continue to provide security fixes for V1 for another 6 months minimum, so you have time to upgrade your applications.
6+
7+
Functionality marked as deprecated will not be removed until V2.
8+
9+
Of course, some apparently safe changes and bug fixes will inevitably break some users' code — obligatory link to [xkcd](https://xkcd.com/1172/).
10+
11+
The following changes will **NOT** be considered breaking changes, and may occur in minor releases:
12+
13+
* Bug fixes that may result in existing code breaking, provided that such code was relying on undocumented features/constructs/assumptions.
14+
* Adding new [message parts][pydantic_ai.messages], [stream events][pydantic_ai.messages.AgentStreamEvent], or optional fields on existing message (part) and event types. Always code defensively when consuming message parts or event streams, and use the [`ModelMessagesTypeAdapter`][pydantic_ai.messages.ModelMessagesTypeAdapter] to (de)serialize message histories.
15+
* Changing OpenTelemetry span attributes. Because different [observability platforms](logfire.md#using-opentelemetry) support different versions of the [OpenTelemetry Semantic Conventions for Generative AI systems](https://opentelemetry.io/docs/specs/semconv/gen-ai/), Pydantic AI lets you configure the [instrumentation version](logfire.md#configuring-data-format), but the default version may change in a minor release. Span attributes for [Pydantic Evals](evals.md) may also change as we iterate on Evals support in [Pydantic Logfire](https://logfire.pydantic.dev/docs/guides/web-ui/evals/).
16+
* Changing how `__repr__` behaves, even of public classes.
17+
18+
In all cases we will aim to minimize churn and do so only when justified by the increase of quality of Pydantic AI for users.
19+
20+
## Beta Features
21+
22+
At Pydantic, we like to move quickly and innovate! To that end, minor releases may introduce beta features (indicated by a `beta` module) that are active works in progress. While in its beta phase, a feature's API and behaviors may not be stable, and it's very possible that changes made to the feature will not be backward-compatible. We aim to move beta features out of beta within a few months after initial release, once users have had a chance to provide feedback and test the feature in production.
23+
24+
## Support for Python versions
25+
26+
Pydantic will drop support for a Python version when the following conditions are met:
27+
28+
* The Python version has reached its [expected end of life](https://devguide.python.org/versions/).
29+
* less than 5% of downloads of the most recent minor release are using that version.

mkdocs.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ nav:
1414
- install.md
1515
- help.md
1616
- troubleshooting.md
17-
- changelog.md
1817

1918
- Documentation:
2019
- Core Concepts:
@@ -191,6 +190,8 @@ nav:
191190

192191
- Project:
193192
- contributing.md
193+
- changelog.md
194+
- version-policy.md
194195

195196
extra:
196197
# hide the "Made with Material for MkDocs" message
@@ -333,7 +334,6 @@ plugins:
333334
- install.md
334335
- help.md
335336
- troubleshooting.md
336-
- changelog.md
337337
Concepts documentation:
338338
- a2a.md
339339
- ag-ui.md
@@ -365,11 +365,15 @@ plugins:
365365
- durable_execution/*.md
366366
MCP:
367367
- mcp/*.md
368+
UI Event Streams:
369+
- ui/*.md
368370
Optional:
369371
- testing.md
370372
- cli.md
371373
- logfire.md
372374
- contributing.md
375+
- changelog.md
376+
- version-policy.md
373377
Examples:
374378
- examples/*.md
375379

pydantic_ai_slim/pydantic_ai/_run_context.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,19 @@
1616
from .models import Model
1717
from .result import RunUsage
1818

19+
# TODO (v2): Change the default for all typevars like this from `None` to `object`
1920
AgentDepsT = TypeVar('AgentDepsT', default=None, contravariant=True)
2021
"""Type variable for agent dependencies."""
2122

23+
RunContextAgentDepsT = TypeVar('RunContextAgentDepsT', default=None, covariant=True)
24+
"""Type variable for the agent dependencies in `RunContext`."""
25+
2226

2327
@dataclasses.dataclass(repr=False, kw_only=True)
24-
class RunContext(Generic[AgentDepsT]):
28+
class RunContext(Generic[RunContextAgentDepsT]):
2529
"""Information about the current call."""
2630

27-
deps: AgentDepsT
31+
deps: RunContextAgentDepsT
2832
"""Dependencies for the agent."""
2933
model: Model
3034
"""The model used in this run."""

pydantic_ai_slim/pydantic_ai/_utils.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,15 @@ def sync_anext(iterator: Iterator[T]) -> T:
234234
raise StopAsyncIteration() from e
235235

236236

237+
def sync_async_iterator(async_iter: AsyncIterator[T]) -> Iterator[T]:
238+
loop = get_event_loop()
239+
while True:
240+
try:
241+
yield loop.run_until_complete(anext(async_iter))
242+
except StopAsyncIteration:
243+
break
244+
245+
237246
def now_utc() -> datetime:
238247
return datetime.now(tz=timezone.utc)
239248

@@ -489,3 +498,12 @@ def get_union_args(tp: Any) -> tuple[Any, ...]:
489498
return tuple(_unwrap_annotated(arg) for arg in get_args(tp))
490499
else:
491500
return ()
501+
502+
503+
def get_event_loop():
504+
try:
505+
event_loop = asyncio.get_event_loop()
506+
except RuntimeError: # pragma: lax no cover
507+
event_loop = asyncio.new_event_loop()
508+
asyncio.set_event_loop(event_loop)
509+
return event_loop

pydantic_ai_slim/pydantic_ai/agent/abstract.py

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from typing_extensions import Self, TypeIs, TypeVar
1313

1414
from pydantic_graph import End
15-
from pydantic_graph._utils import get_event_loop
1615

1716
from .. import (
1817
_agent_graph,
@@ -335,7 +334,7 @@ def run_sync(
335334
if infer_name and self.name is None:
336335
self._infer_name(inspect.currentframe())
337336

338-
return get_event_loop().run_until_complete(
337+
return _utils.get_event_loop().run_until_complete(
339338
self.run(
340339
user_prompt,
341340
output_type=output_type,
@@ -581,6 +580,133 @@ async def on_complete() -> None:
581580
if not yielded:
582581
raise exceptions.AgentRunError('Agent run finished without producing a final result') # pragma: no cover
583582

583+
@overload
584+
def run_stream_sync(
585+
self,
586+
user_prompt: str | Sequence[_messages.UserContent] | None = None,
587+
*,
588+
output_type: None = None,
589+
message_history: Sequence[_messages.ModelMessage] | None = None,
590+
deferred_tool_results: DeferredToolResults | None = None,
591+
model: models.Model | models.KnownModelName | str | None = None,
592+
deps: AgentDepsT = None,
593+
model_settings: ModelSettings | None = None,
594+
usage_limits: _usage.UsageLimits | None = None,
595+
usage: _usage.RunUsage | None = None,
596+
infer_name: bool = True,
597+
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
598+
builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
599+
event_stream_handler: EventStreamHandler[AgentDepsT] | None = None,
600+
) -> result.StreamedRunResultSync[AgentDepsT, OutputDataT]: ...
601+
602+
@overload
603+
def run_stream_sync(
604+
self,
605+
user_prompt: str | Sequence[_messages.UserContent] | None = None,
606+
*,
607+
output_type: OutputSpec[RunOutputDataT],
608+
message_history: Sequence[_messages.ModelMessage] | None = None,
609+
deferred_tool_results: DeferredToolResults | None = None,
610+
model: models.Model | models.KnownModelName | str | None = None,
611+
deps: AgentDepsT = None,
612+
model_settings: ModelSettings | None = None,
613+
usage_limits: _usage.UsageLimits | None = None,
614+
usage: _usage.RunUsage | None = None,
615+
infer_name: bool = True,
616+
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
617+
builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
618+
event_stream_handler: EventStreamHandler[AgentDepsT] | None = None,
619+
) -> result.StreamedRunResultSync[AgentDepsT, RunOutputDataT]: ...
620+
621+
def run_stream_sync(
622+
self,
623+
user_prompt: str | Sequence[_messages.UserContent] | None = None,
624+
*,
625+
output_type: OutputSpec[RunOutputDataT] | None = None,
626+
message_history: Sequence[_messages.ModelMessage] | None = None,
627+
deferred_tool_results: DeferredToolResults | None = None,
628+
model: models.Model | models.KnownModelName | str | None = None,
629+
deps: AgentDepsT = None,
630+
model_settings: ModelSettings | None = None,
631+
usage_limits: _usage.UsageLimits | None = None,
632+
usage: _usage.RunUsage | None = None,
633+
infer_name: bool = True,
634+
toolsets: Sequence[AbstractToolset[AgentDepsT]] | None = None,
635+
builtin_tools: Sequence[AbstractBuiltinTool] | None = None,
636+
event_stream_handler: EventStreamHandler[AgentDepsT] | None = None,
637+
) -> result.StreamedRunResultSync[AgentDepsT, Any]:
638+
"""Run the agent with a user prompt in sync streaming mode.
639+
640+
This is a convenience method that wraps [`run_stream()`][pydantic_ai.agent.AbstractAgent.run_stream] with `loop.run_until_complete(...)`.
641+
You therefore can't use this method inside async code or if there's an active event loop.
642+
643+
This method builds an internal agent graph (using system prompts, tools and output schemas) and then
644+
runs the graph until the model produces output matching the `output_type`, for example text or structured data.
645+
At this point, a streaming run result object is yielded from which you can stream the output as it comes in,
646+
and -- once this output has completed streaming -- get the complete output, message history, and usage.
647+
648+
As this method will consider the first output matching the `output_type` to be the final output,
649+
it will stop running the agent graph and will not execute any tool calls made by the model after this "final" output.
650+
If you want to always run the agent graph to completion and stream events and output at the same time,
651+
use [`agent.run()`][pydantic_ai.agent.AbstractAgent.run] with an `event_stream_handler` or [`agent.iter()`][pydantic_ai.agent.AbstractAgent.iter] instead.
652+
653+
Example:
654+
```python
655+
from pydantic_ai import Agent
656+
657+
agent = Agent('openai:gpt-4o')
658+
659+
def main():
660+
response = agent.run_stream_sync('What is the capital of the UK?')
661+
print(response.get_output())
662+
#> The capital of the UK is London.
663+
```
664+
665+
Args:
666+
user_prompt: User input to start/continue the conversation.
667+
output_type: Custom output type to use for this run, `output_type` may only be used if the agent has no
668+
output validators since output validators would expect an argument that matches the agent's output type.
669+
message_history: History of the conversation so far.
670+
deferred_tool_results: Optional results for deferred tool calls in the message history.
671+
model: Optional model to use for this run, required if `model` was not set when creating the agent.
672+
deps: Optional dependencies to use for this run.
673+
model_settings: Optional settings to use for this model's request.
674+
usage_limits: Optional limits on model request count or token usage.
675+
usage: Optional usage to start with, useful for resuming a conversation or agents used in tools.
676+
infer_name: Whether to try to infer the agent name from the call frame if it's not set.
677+
toolsets: Optional additional toolsets for this run.
678+
builtin_tools: Optional additional builtin tools for this run.
679+
event_stream_handler: Optional handler for events from the model's streaming response and the agent's execution of tools to use for this run.
680+
It will receive all the events up until the final result is found, which you can then read or stream from inside the context manager.
681+
Note that it does _not_ receive any events after the final result is found.
682+
683+
Returns:
684+
The result of the run.
685+
"""
686+
if infer_name and self.name is None:
687+
self._infer_name(inspect.currentframe())
688+
689+
async def _consume_stream():
690+
async with self.run_stream(
691+
user_prompt,
692+
output_type=output_type,
693+
message_history=message_history,
694+
deferred_tool_results=deferred_tool_results,
695+
model=model,
696+
deps=deps,
697+
model_settings=model_settings,
698+
usage_limits=usage_limits,
699+
usage=usage,
700+
infer_name=infer_name,
701+
toolsets=toolsets,
702+
builtin_tools=builtin_tools,
703+
event_stream_handler=event_stream_handler,
704+
) as stream_result:
705+
yield stream_result
706+
707+
async_result = _utils.get_event_loop().run_until_complete(anext(_consume_stream()))
708+
return result.StreamedRunResultSync(async_result)
709+
584710
@overload
585711
def run_stream_events(
586712
self,
@@ -1217,6 +1343,6 @@ def to_cli_sync(
12171343
agent.to_cli_sync(prog_name='assistant')
12181344
```
12191345
"""
1220-
return get_event_loop().run_until_complete(
1346+
return _utils.get_event_loop().run_until_complete(
12211347
self.to_cli(deps=deps, prog_name=prog_name, message_history=message_history)
12221348
)

0 commit comments

Comments
 (0)