Separate effective tool definitions from tool execution state
Problem
ToolsetTool currently mixes the effective ToolDefinition with execution state. CombinedToolset therefore retains a complete source_tool, including an older definition, while wrappers update the outer definition. This caused #7380 and requires #8082 to synchronize definitions, timeout, and toolset identity manually.
combined tool
├── effective tool_def
└── source_tool
├── stale tool_def
└── callable / validator / routing stateSuggested direction
Keep exactly one effective definition and separate it from the execution handle:
@dataclass
class ExecutableTool:
tool_def: ToolDefinition
execution: ToolExecutionawait tool.execution.call(
args=tool_args,
ctx=ctx,
tool_def=tool.tool_def,
)Wrappers should replace only the effective definition. Execution-specific state should contain the callable, validators, retry state, source toolset, and original registration identity—but not another authoritative ToolDefinition. Fields such as timeout should not be cached separately from tool_def.timeout.
Plan
- Document the invariant that preparation and execution use the same effective definition.
- Introduce an internal execution handle without changing public behavior.
- Move duplicated execution fields behind that handle incrementally.
- Simplify
CombinedToolsetso it routes execution without retaining a competing definition. - Cover nested combined/wrapper compositions, metadata, timeout, approval/defer state, and toolset identity.
This should follow the narrow fix in #8082 rather than delay it.
Source: pydantic/pydantic-ai