Skip to content

Commit d173f88

Browse files
authored
Merge branch 'pydantic:main' into feature/1783-minimal-mcp-resources-support
2 parents 9804f18 + 9c31521 commit d173f88

File tree

14 files changed

+207
-137
lines changed

14 files changed

+207
-137
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 }}

.github/workflows/ci.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,12 @@ jobs:
473473
import os
474474
import tweepy
475475
476-
client = tweepy.Client(os.getenv("TWITTER_ACCESS_TOKEN"))
476+
client = tweepy.Client(
477+
access_token=os.getenv("TWITTER_ACCESS_TOKEN"),
478+
access_token_secret=os.getenv("TWITTER_ACCESS_TOKEN_SECRET"),
479+
consumer_key=os.getenv("TWITTER_CONSUMER_KEY"),
480+
consumer_secret=os.getenv("TWITTER_CONSUMER_SECRET"),
481+
)
477482
version = os.getenv("VERSION").strip('"')
478483
tweet = os.getenv("TWEET").format(version=version)
479484
client.create_tweet(text=tweet)
@@ -483,4 +488,7 @@ jobs:
483488
Pydantic AI version {version} is out! 🎉
484489
485490
https://github.com/pydantic/pydantic-ai/releases/tag/v{version}
491+
TWITTER_CONSUMER_KEY: ${{ secrets.TWITTER_CONSUMER_KEY }}
492+
TWITTER_CONSUMER_SECRET: ${{ secrets.TWITTER_CONSUMER_SECRET }}
486493
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
494+
TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }}

docs-site/wrangler.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ compatibility_date = "2025-06-17"
44
routes = ["ai.pydantic.dev/*"]
55
main = "src/index.ts"
66
workers_dev = false
7+
preview_urls = true
78
compatibility_flags = [ "nodejs_compat_v2" ]
89

910
[vars]

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/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

Lines changed: 23 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,22 @@
11
from __future__ import annotations
22

33
from collections.abc import Callable
4-
from dataclasses import dataclass
5-
from typing import Annotated, Any, Literal
4+
from typing import Any, Literal
65

7-
from pydantic import ConfigDict, Discriminator, with_config
86
from temporalio import activity, workflow
97
from temporalio.workflow import ActivityConfig
10-
from typing_extensions import assert_never
118

129
from pydantic_ai import FunctionToolset, ToolsetTool
13-
from pydantic_ai.exceptions import ApprovalRequired, CallDeferred, ModelRetry, UserError
10+
from pydantic_ai.exceptions import UserError
1411
from pydantic_ai.tools import AgentDepsT, RunContext
1512
from pydantic_ai.toolsets.function import FunctionToolsetTool
1613

1714
from ._run_context import TemporalRunContext
18-
from ._toolset import TemporalWrapperToolset
19-
20-
21-
@dataclass
22-
@with_config(ConfigDict(arbitrary_types_allowed=True))
23-
class _CallToolParams:
24-
name: str
25-
tool_args: dict[str, Any]
26-
serialized_run_context: Any
27-
28-
29-
@dataclass
30-
class _ApprovalRequired:
31-
kind: Literal['approval_required'] = 'approval_required'
32-
33-
34-
@dataclass
35-
class _CallDeferred:
36-
kind: Literal['call_deferred'] = 'call_deferred'
37-
38-
39-
@dataclass
40-
class _ModelRetry:
41-
message: str
42-
kind: Literal['model_retry'] = 'model_retry'
43-
44-
45-
@dataclass
46-
class _ToolReturn:
47-
result: Any
48-
kind: Literal['tool_return'] = 'tool_return'
49-
50-
51-
_CallToolResult = Annotated[
52-
_ApprovalRequired | _CallDeferred | _ModelRetry | _ToolReturn,
53-
Discriminator('kind'),
54-
]
15+
from ._toolset import (
16+
CallToolParams,
17+
CallToolResult,
18+
TemporalWrapperToolset,
19+
)
5520

5621

5722
class TemporalFunctionToolset(TemporalWrapperToolset[AgentDepsT]):
@@ -70,7 +35,7 @@ def __init__(
7035
self.tool_activity_config = tool_activity_config
7136
self.run_context_type = run_context_type
7237

73-
async def call_tool_activity(params: _CallToolParams, deps: AgentDepsT) -> _CallToolResult:
38+
async def call_tool_activity(params: CallToolParams, deps: AgentDepsT) -> CallToolResult:
7439
name = params.name
7540
ctx = self.run_context_type.deserialize_run_context(params.serialized_run_context, deps=deps)
7641
try:
@@ -84,15 +49,7 @@ async def call_tool_activity(params: _CallToolParams, deps: AgentDepsT) -> _Call
8449
# The tool args will already have been validated into their proper types in the `ToolManager`,
8550
# but `execute_activity` would have turned them into simple Python types again, so we need to re-validate them.
8651
args_dict = tool.args_validator.validate_python(params.tool_args)
87-
try:
88-
result = await self.wrapped.call_tool(name, args_dict, ctx, tool)
89-
return _ToolReturn(result=result)
90-
except ApprovalRequired:
91-
return _ApprovalRequired()
92-
except CallDeferred:
93-
return _CallDeferred()
94-
except ModelRetry as e:
95-
return _ModelRetry(message=e.message)
52+
return await self._wrap_call_tool_result(self.wrapped.call_tool(name, args_dict, ctx, tool))
9653

9754
# Set type hint explicitly so that Temporal can take care of serialization and deserialization
9855
call_tool_activity.__annotations__['deps'] = deps_type
@@ -123,25 +80,18 @@ async def call_tool(
12380

12481
tool_activity_config = self.activity_config | tool_activity_config
12582
serialized_run_context = self.run_context_type.serialize_run_context(ctx)
126-
result = await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
127-
activity=self.call_tool_activity,
128-
args=[
129-
_CallToolParams(
130-
name=name,
131-
tool_args=tool_args,
132-
serialized_run_context=serialized_run_context,
133-
),
134-
ctx.deps,
135-
],
136-
**tool_activity_config,
83+
return self._unwrap_call_tool_result(
84+
await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
85+
activity=self.call_tool_activity,
86+
args=[
87+
CallToolParams(
88+
name=name,
89+
tool_args=tool_args,
90+
serialized_run_context=serialized_run_context,
91+
tool_def=None,
92+
),
93+
ctx.deps,
94+
],
95+
**tool_activity_config,
96+
)
13797
)
138-
if isinstance(result, _ApprovalRequired):
139-
raise ApprovalRequired()
140-
elif isinstance(result, _CallDeferred):
141-
raise CallDeferred()
142-
elif isinstance(result, _ModelRetry):
143-
raise ModelRetry(result.message)
144-
elif isinstance(result, _ToolReturn):
145-
return result.result
146-
else:
147-
assert_never(result)

pydantic_ai_slim/pydantic_ai/durable_exec/temporal/_mcp_server.py

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111

1212
from pydantic_ai import ToolsetTool
1313
from pydantic_ai.exceptions import UserError
14-
from pydantic_ai.mcp import MCPServer, ToolResult
14+
from pydantic_ai.mcp import MCPServer
1515
from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition
1616

1717
from ._run_context import TemporalRunContext
18-
from ._toolset import TemporalWrapperToolset
18+
from ._toolset import (
19+
CallToolParams,
20+
CallToolResult,
21+
TemporalWrapperToolset,
22+
)
1923

2024

2125
@dataclass
@@ -24,15 +28,6 @@ class _GetToolsParams:
2428
serialized_run_context: Any
2529

2630

27-
@dataclass
28-
@with_config(ConfigDict(arbitrary_types_allowed=True))
29-
class _CallToolParams:
30-
name: str
31-
tool_args: dict[str, Any]
32-
serialized_run_context: Any
33-
tool_def: ToolDefinition
34-
35-
3631
class TemporalMCPServer(TemporalWrapperToolset[AgentDepsT]):
3732
def __init__(
3833
self,
@@ -72,13 +67,16 @@ async def get_tools_activity(params: _GetToolsParams, deps: AgentDepsT) -> dict[
7267
get_tools_activity
7368
)
7469

75-
async def call_tool_activity(params: _CallToolParams, deps: AgentDepsT) -> ToolResult:
70+
async def call_tool_activity(params: CallToolParams, deps: AgentDepsT) -> CallToolResult:
7671
run_context = self.run_context_type.deserialize_run_context(params.serialized_run_context, deps=deps)
77-
return await self.wrapped.call_tool(
78-
params.name,
79-
params.tool_args,
80-
run_context,
81-
self.tool_for_tool_def(params.tool_def),
72+
assert isinstance(params.tool_def, ToolDefinition)
73+
return await self._wrap_call_tool_result(
74+
self.wrapped.call_tool(
75+
params.name,
76+
params.tool_args,
77+
run_context,
78+
self.tool_for_tool_def(params.tool_def),
79+
)
8280
)
8381

8482
# Set type hint explicitly so that Temporal can take care of serialization and deserialization
@@ -125,22 +123,24 @@ async def call_tool(
125123
tool_args: dict[str, Any],
126124
ctx: RunContext[AgentDepsT],
127125
tool: ToolsetTool[AgentDepsT],
128-
) -> ToolResult:
126+
) -> CallToolResult:
129127
if not workflow.in_workflow():
130128
return await super().call_tool(name, tool_args, ctx, tool)
131129

132130
tool_activity_config = self.activity_config | self.tool_activity_config.get(name, {})
133131
serialized_run_context = self.run_context_type.serialize_run_context(ctx)
134-
return await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
135-
activity=self.call_tool_activity,
136-
args=[
137-
_CallToolParams(
138-
name=name,
139-
tool_args=tool_args,
140-
serialized_run_context=serialized_run_context,
141-
tool_def=tool.tool_def,
142-
),
143-
ctx.deps,
144-
],
145-
**tool_activity_config,
132+
return self._unwrap_call_tool_result(
133+
await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
134+
activity=self.call_tool_activity,
135+
args=[
136+
CallToolParams(
137+
name=name,
138+
tool_args=tool_args,
139+
serialized_run_context=serialized_run_context,
140+
tool_def=tool.tool_def,
141+
),
142+
ctx.deps,
143+
],
144+
**tool_activity_config,
145+
)
146146
)

0 commit comments

Comments
 (0)