Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions pydantic_ai_slim/pydantic_ai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
ImageGenerationTool,
MCPServerTool,
MemoryTool,
ProgrammaticCodeExecutionTool,
ToolSearchTool,
UrlContextTool,
WebSearchTool,
WebSearchUserLocation,
Expand Down Expand Up @@ -218,9 +220,11 @@
'WebSearchUserLocation',
'UrlContextTool',
'CodeExecutionTool',
'ProgrammaticCodeExecutionTool',
'ImageGenerationTool',
'MemoryTool',
'MCPServerTool',
'ToolSearchTool',
# output
'ToolOutput',
'NativeOutput',
Expand Down
30 changes: 30 additions & 0 deletions pydantic_ai_slim/pydantic_ai/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,9 @@ def tool(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
defer_loading: bool = False,
allowed_callers: list[str] | None = None,
input_examples: list[dict[str, Any]] | None = None,
) -> Callable[[ToolFuncContext[AgentDepsT, ToolParams]], ToolFuncContext[AgentDepsT, ToolParams]]: ...

def tool(
Expand All @@ -1041,6 +1044,9 @@ def tool(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
defer_loading: bool = False,
allowed_callers: list[str] | None = None,
input_examples: list[dict[str, Any]] | None = None,
) -> Any:
"""Decorator to register a tool function which takes [`RunContext`][pydantic_ai.tools.RunContext] as its first argument.

Expand Down Expand Up @@ -1090,6 +1096,12 @@ async def spam(ctx: RunContext[str], y: float) -> float:
requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
defer_loading: Whether to defer loading this tool until discovered via tool search. Defaults to False.
See [`ToolDefinition.defer_loading`][pydantic_ai.tools.ToolDefinition.defer_loading] for more info.
allowed_callers: List of tool types that can call this tool programmatically. Defaults to None.
See [`ToolDefinition.allowed_callers`][pydantic_ai.tools.ToolDefinition.allowed_callers] for more info.
input_examples: Example inputs demonstrating correct tool usage. Defaults to None.
See [`ToolDefinition.input_examples`][pydantic_ai.tools.ToolDefinition.input_examples] for more info.
"""

def tool_decorator(
Expand All @@ -1110,6 +1122,9 @@ def tool_decorator(
sequential=sequential,
requires_approval=requires_approval,
metadata=metadata,
defer_loading=defer_loading,
allowed_callers=allowed_callers,
input_examples=input_examples,
)
return func_

Expand All @@ -1134,6 +1149,9 @@ def tool_plain(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
defer_loading: bool = False,
allowed_callers: list[str] | None = None,
input_examples: list[dict[str, Any]] | None = None,
) -> Callable[[ToolFuncPlain[ToolParams]], ToolFuncPlain[ToolParams]]: ...

def tool_plain(
Expand All @@ -1152,6 +1170,9 @@ def tool_plain(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
defer_loading: bool = False,
allowed_callers: list[str] | None = None,
input_examples: list[dict[str, Any]] | None = None,
) -> Any:
"""Decorator to register a tool function which DOES NOT take `RunContext` as an argument.

Expand Down Expand Up @@ -1201,6 +1222,12 @@ async def spam(ctx: RunContext[str]) -> float:
requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
defer_loading: Whether to defer loading this tool until discovered via tool search. Defaults to False.
See [`ToolDefinition.defer_loading`][pydantic_ai.tools.ToolDefinition.defer_loading] for more info.
allowed_callers: List of tool types that can call this tool programmatically. Defaults to None.
See [`ToolDefinition.allowed_callers`][pydantic_ai.tools.ToolDefinition.allowed_callers] for more info.
input_examples: Example inputs demonstrating correct tool usage. Defaults to None.
See [`ToolDefinition.input_examples`][pydantic_ai.tools.ToolDefinition.input_examples] for more info.
"""

def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams]:
Expand All @@ -1219,6 +1246,9 @@ def tool_decorator(func_: ToolFuncPlain[ToolParams]) -> ToolFuncPlain[ToolParams
sequential=sequential,
requires_approval=requires_approval,
metadata=metadata,
defer_loading=defer_loading,
allowed_callers=allowed_callers,
input_examples=input_examples,
)
return func_

Expand Down
51 changes: 51 additions & 0 deletions pydantic_ai_slim/pydantic_ai/builtin_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
'WebSearchTool',
'WebSearchUserLocation',
'CodeExecutionTool',
'ProgrammaticCodeExecutionTool',
'UrlContextTool',
'ImageGenerationTool',
'MemoryTool',
'MCPServerTool',
'ToolSearchTool',
)

_BUILTIN_TOOL_TYPES: dict[str, type[AbstractBuiltinTool]] = {}
Expand Down Expand Up @@ -334,6 +336,55 @@ def unique_id(self) -> str:
return ':'.join([self.kind, self.id])


@dataclass(kw_only=True)
class ToolSearchTool(AbstractBuiltinTool):
"""A builtin tool that enables dynamic tool discovery without loading all definitions upfront.

Instead of consuming tokens on hundreds of tool definitions, Claude searches for relevant tools
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description should be generic, not Claude specific. If the details are Claude specific, the link to the docs below suffices.

on-demand. Only matching tools get expanded into full definitions in the model's context.

You should mark tools with `defer_loading=True` to make them discoverable on-demand while
keeping critical tools always loaded (with `defer_loading=False`).

Supported by:

* Anthropic (with `advanced-tool-use-2025-11-20` beta)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to mention the beta, as we'd enable it automatically if this is used


See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-search-tool for more info.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link should be on the same line as Anthropic, as we may add more providers in the future

"""

search_type: Literal['regex', 'bm25'] = 'regex'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to build this builtin tool class with the expectation that other providers will support this in the future as well. But we don't know what values they'll support here, or whether they'll support regex at all. So the default value here should always be a "neutral" value. I.e., let's add None to use the provider's default, and then document what that is for Anthropic.

"""The type of search to use for tool discovery.

- `'regex'`: Constructs Python `re.search()` patterns. Max 200 characters per query. Case-sensitive by default.
- `'bm25'`: Uses natural language queries with semantic matching across tool metadata.
"""

kind: str = 'tool_search'
"""The kind of tool."""


@dataclass(kw_only=True)
class ProgrammaticCodeExecutionTool(AbstractBuiltinTool):
"""A builtin tool that enables programmatic tool calling via code execution.

This is an enhanced version of CodeExecutionTool that allows Claude to write Python code
that calls your custom tools programmatically within the execution container.

Tools that should be callable from code must have `allowed_callers=['code_execution_20250825']`
set in their ToolDefinition.

Supported by:

* Anthropic (with `advanced-tool-use-2025-11-20` beta)

See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool for more info.
"""

kind: str = 'programmatic_code_execution'
"""The kind of tool."""


def _tool_discriminator(tool_data: dict[str, Any] | AbstractBuiltinTool) -> str:
if isinstance(tool_data, dict):
return tool_data.get('kind', AbstractBuiltinTool.kind)
Expand Down
88 changes: 84 additions & 4 deletions pydantic_ai_slim/pydantic_ai/models/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
from .._run_context import RunContext
from .._utils import guard_tool_call_id as _guard_tool_call_id
from ..builtin_tools import CodeExecutionTool, MCPServerTool, MemoryTool, WebSearchTool
from ..builtin_tools import (
CodeExecutionTool,
MCPServerTool,
MemoryTool,
ProgrammaticCodeExecutionTool,
ToolSearchTool,
WebSearchTool,
)
from ..exceptions import ModelAPIError, UserError
from ..messages import (
BinaryContent,
Expand Down Expand Up @@ -502,6 +509,15 @@ def _add_builtin_tools(
) -> tuple[list[BetaToolUnionParam], list[BetaRequestMCPServerURLDefinitionParam], list[str]]:
beta_features: list[str] = []
mcp_servers: list[BetaRequestMCPServerURLDefinitionParam] = []

# Check if any tools use advanced tool use features (defer_loading, allowed_callers, input_examples)
uses_advanced_tool_use = any(
tool_def.defer_loading or tool_def.allowed_callers or tool_def.input_examples
for tool_def in model_request_parameters.tool_defs.values()
)
if uses_advanced_tool_use:
beta_features.append('advanced-tool-use-2025-11-20')

for tool in model_request_parameters.builtin_tools:
if isinstance(tool, WebSearchTool):
user_location = UserLocation(type='approximate', **tool.user_location) if tool.user_location else None
Expand All @@ -515,6 +531,46 @@ def _add_builtin_tools(
user_location=user_location,
)
)
elif isinstance(tool, ToolSearchTool):
# Tool Search Tool for dynamic tool discovery
# Note: These tool types are new in Anthropic's advanced-tool-use beta
# and may not yet be in the SDK's type definitions
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the SDK

if tool.search_type == 'regex':
tools.append(
cast(
BetaToolUnionParam,
{
'type': 'tool_search_tool_regex_20251119',
'name': 'tool_search_tool_regex',
},
)
)
else: # bm25
tools.append(
cast(
BetaToolUnionParam,
{
'type': 'tool_search_tool_bm25_20251119',
'name': 'tool_search_tool_bm25',
},
)
)
if 'advanced-tool-use-2025-11-20' not in beta_features:
beta_features.append('advanced-tool-use-2025-11-20')
elif isinstance(tool, ProgrammaticCodeExecutionTool):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not introduce a new tool type just for this window of time where Anthropic has multiple versions of code-execution. Instead, we should make our logic smarter to automatically use the newer code_execution version when advanced-tool-use is enabled / when allowed_callers is used.

# Programmatic Code Execution Tool (newer version that supports allowed_callers)
# Note: This tool type is new in Anthropic's advanced-tool-use beta
tools.append(
cast(
BetaToolUnionParam,
{
'type': 'code_execution_20250825',
'name': 'code_execution',
},
)
)
if 'advanced-tool-use-2025-11-20' not in beta_features:
beta_features.append('advanced-tool-use-2025-11-20')
elif isinstance(tool, CodeExecutionTool): # pragma: no branch
tools.append(BetaCodeExecutionTool20250522Param(name='code_execution', type='code_execution_20250522'))
beta_features.append('code-execution-2025-05-22')
Expand Down Expand Up @@ -848,11 +904,19 @@ async def _map_user_prompt(

@staticmethod
def _map_tool_definition(f: ToolDefinition) -> BetaToolParam:
return {
tool_param: BetaToolParam = {
'name': f.name,
'description': f.description or '',
'input_schema': f.parameters_json_schema,
}
# Add advanced tool use fields (Anthropic beta: advanced-tool-use-2025-11-20)
if f.defer_loading:
tool_param['defer_loading'] = True # type: ignore[typeddict-unknown-key]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update the SDK and drop these type ignores

if f.allowed_callers:
tool_param['allowed_callers'] = f.allowed_callers # type: ignore[typeddict-unknown-key]
if f.input_examples:
tool_param['input_examples'] = f.input_examples # type: ignore[typeddict-unknown-key]
return tool_param


def _map_usage(
Expand Down Expand Up @@ -1061,10 +1125,26 @@ def _map_server_tool_use_block(item: BetaServerToolUseBlock, provider_name: str)
args=cast(dict[str, Any], item.input) or None,
tool_call_id=item.id,
)
elif item.name in ('web_fetch', 'bash_code_execution', 'text_editor_code_execution'): # pragma: no cover
# Note: Tool search tool names are new in Anthropic's advanced-tool-use beta
# and may not yet be in the SDK's type definitions for item.name
elif cast(str, item.name) in ('tool_search_tool_regex', 'tool_search_tool_bm25'):
# Tool Search Tool for dynamic tool discovery
return BuiltinToolCallPart(
provider_name=provider_name,
tool_name=ToolSearchTool.kind,
args=cast(dict[str, Any], item.input) or None,
tool_call_id=item.id,
)
elif cast(str, item.name) in ('web_fetch', 'bash_code_execution', 'text_editor_code_execution'): # pragma: no cover
raise NotImplementedError(f'Anthropic built-in tool {item.name!r} is not currently supported.')
else:
assert_never(item.name)
# For new server tools we don't recognize yet, return a generic BuiltinToolCallPart
return BuiltinToolCallPart(
provider_name=provider_name,
tool_name=item.name,
args=cast(dict[str, Any], item.input) or None,
tool_call_id=item.id,
)


web_search_tool_result_content_ta: TypeAdapter[BetaWebSearchToolResultBlockContent] = TypeAdapter(
Expand Down
51 changes: 51 additions & 0 deletions pydantic_ai_slim/pydantic_ai/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ class Tool(Generic[ToolAgentDepsT]):
sequential: bool
requires_approval: bool
metadata: dict[str, Any] | None
defer_loading: bool
allowed_callers: list[str] | None
input_examples: list[dict[str, Any]] | None
function_schema: _function_schema.FunctionSchema
"""
The base JSON schema for the tool's parameters.
Expand All @@ -285,6 +288,9 @@ def __init__(
sequential: bool = False,
requires_approval: bool = False,
metadata: dict[str, Any] | None = None,
defer_loading: bool = False,
allowed_callers: list[str] | None = None,
input_examples: list[dict[str, Any]] | None = None,
function_schema: _function_schema.FunctionSchema | None = None,
):
"""Create a new tool instance.
Expand Down Expand Up @@ -341,6 +347,12 @@ async def prep_my_tool(
requires_approval: Whether this tool requires human-in-the-loop approval. Defaults to False.
See the [tools documentation](../deferred-tools.md#human-in-the-loop-tool-approval) for more info.
metadata: Optional metadata for the tool. This is not sent to the model but can be used for filtering and tool behavior customization.
defer_loading: Whether to defer loading this tool until discovered via tool search. Defaults to False.
See [`ToolDefinition.defer_loading`][pydantic_ai.tools.ToolDefinition.defer_loading] for more info.
allowed_callers: List of tool types that can call this tool programmatically. Defaults to None.
See [`ToolDefinition.allowed_callers`][pydantic_ai.tools.ToolDefinition.allowed_callers] for more info.
input_examples: Example inputs demonstrating correct tool usage. Defaults to None.
See [`ToolDefinition.input_examples`][pydantic_ai.tools.ToolDefinition.input_examples] for more info.
function_schema: The function schema to use for the tool. If not provided, it will be generated.
"""
self.function = function
Expand All @@ -362,6 +374,9 @@ async def prep_my_tool(
self.sequential = sequential
self.requires_approval = requires_approval
self.metadata = metadata
self.defer_loading = defer_loading
self.allowed_callers = allowed_callers
self.input_examples = input_examples

@classmethod
def from_schema(
Expand Down Expand Up @@ -418,6 +433,9 @@ def tool_def(self):
sequential=self.sequential,
metadata=self.metadata,
kind='unapproved' if self.requires_approval else 'function',
defer_loading=self.defer_loading,
allowed_callers=self.allowed_callers,
input_examples=self.input_examples,
)

async def prepare_tool_def(self, ctx: RunContext[ToolAgentDepsT]) -> ToolDefinition | None:
Expand Down Expand Up @@ -503,6 +521,39 @@ class ToolDefinition:
For MCP tools, this contains the `meta`, `annotations`, and `output_schema` fields from the tool definition.
"""

defer_loading: bool = False
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the comment below (in the code, above in the chronological feed), I'm a little uncomfortable adding a field for an Anthropic-only feature that will likely come to other providers as well, but we don't know yet in what form, nor if Anthropic keeps this form since it's still in beta. Generally I'd say it's too early to add this field on ToolDefinition where we'll have to maintain it for backward compatibility, if it may turn out other providers take a different approach that would be incompatible, and we should wait until at least 2 providers have something to know how to do it.

But on the other hand, defer_loading seems like a reasonable not-Anthropic-specific name, and I could see how we could implement support for this in Pydantic AI using a new flag that will insert our own tool search tool.

So I'm OK with this field, but I'd want it to automatically add the ToolSearchTool when used. Similar to how specifying output_type=BinaryImage on OpenAIResponsesModel automatically adds the ImageGenerationTool, and how using strict=True on a ToolDefinition automatically adds the structured-outputs-2025-11-13 Anthropic beta header.

"""Whether to defer loading this tool until it's discovered via tool search.

When `True`, this tool will not be loaded into the model's context initially. Instead, Claude will
use the Tool Search tool to discover it on-demand when needed, reducing token usage.

Requires the `ToolSearchTool` builtin tool to be enabled.

Note: this is currently only supported by Anthropic models with the `advanced-tool-use-2025-11-20` beta.
See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-search-tool for more info.
"""

allowed_callers: list[str] | None = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is way too Anthropic-specific to be on the generic ToolDefinition, especially since it uses a magic value that will change and users shouldn't need to know or have in their code anyway.

The goal is for Pydantic AI's representations to be generic so that we can make the same agent definition work with different providers, no matter how differently they may implement a feature.

Ideally it'd also be something that we can make work for providers that don't natively support the feature, for example by implementing our own "programmatic tool calling" using a run_python tool or something (see exploration in #2037, #3093).

So ideally we'd have a generically named boolean field like programmatically_callable (bad name because it's not clear that "programmatically" refers to by the LLM rather than by the dev, but you get the idea), and then document that AnthropicModel automatically respects it, or that you can use some Pydantic AI-specific feature (like a ProgrammaticToolCallingToolset) to make it work with non-Anthropic models.

In Anthropic's case, just setting that field on one of the tool definitions should then result in the code execution tool and beta header to be added.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more things:

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also have to consider these limitations from https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#feature-incompatibilities:

  • Structured outputs: Tools with strict: true are not supported with programmatic calling
  • Tool choice: You cannot force programmatic calling of a specific tool via tool_choice
  • Parallel tool use: disable_parallel_tool_use: true is not supported with programmatic calling

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling#the-allowed-callers-field, allowed_callers can be ["direct", "code_execution_20250825"], but:

We recommend choosing either ["direct"] or ["code_execution_20250825"] for each tool rather than enabling both, as this provides clearer guidance to Claude for how best to use the tool.

Still, programmatically_callable: true does not clearly imply "not directly callable", so maybe we should send ["direct", "code_execution_20250825"] when it's set, and use programmatically_callable: 'only' to indicate just code_execution_20250825?

"""List of tool types that are allowed to call this tool programmatically.

When set to `['code_execution_20250825']`, Claude can call this tool from within code execution,
enabling programmatic tool calling where Claude writes Python code that invokes your tools.

Note: this is currently only supported by Anthropic models with the `advanced-tool-use-2025-11-20` beta.
See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool for more info.
"""

input_examples: list[dict[str, Any]] | None = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this just be examples to be more generic? Or do we expect output_examples as well?

"""Example inputs demonstrating correct tool usage patterns.

Provide 1-5 realistic examples showing parameter conventions, optional field patterns,
nested structures, and API-specific conventions. Each example must validate against
the tool's `parameters_json_schema`.

Note: this is currently only supported by Anthropic models with the `advanced-tool-use-2025-11-20` beta.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above; don't mention the beta header, just enable it automatically

See https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/tool-use-examples for more info.
"""

@property
def defer(self) -> bool:
"""Whether calls to this tool will be deferred.
Expand Down
Loading
Loading