feat: promote native tool delivery to a NativeTools capability - #836
Open
mpfaffenberger wants to merge 2 commits into
Open
feat: promote native tool delivery to a NativeTools capability#836mpfaffenberger wants to merge 2 commits into
mpfaffenberger wants to merge 2 commits into
Conversation
Code Puppy's own tool suite (file ops, shell, sub-agents, browser, plugin extras) was bolted onto the constructed pydantic-ai Agent after the fact via @agent.tool registration. pydantic-ai has a dedicated capability seam for exactly this contribution -- get_toolset() -- so the tools now arrive as a NativeTools capability wrapping a FunctionToolset. Registration mechanics are unchanged: every register_* function only uses the .tool decorator, and FunctionToolset.tool is signature-identical to Agent.tool, so build_native_toolset simply points the existing registry (plugin merging, edit_file expansion, kill-switches, per-model filtering, UC wrappers) at a toolset. Per-tool retry budgets are unchanged too: both paths leave max_retries=None, which resolves to the agent-level retries at get_tools time. Because the toolset knows its tool names up front, the builder's two-pass probe build is gone -- and with it a latent bug: the probe introspected probe_agent._tools, an attribute pydantic-ai v2 removed, so MCP collision filtering had silently become a no-op. Names now come from the native toolset, restoring the documented filter behaviour. _extract_pydantic_agent_tools (context-overhead estimators) now walks the agent's public toolsets property -- covering both the capability shape (CombinedToolset/CapabilityOwnedToolset chains) and legacy @agent.tool registration -- before falling back to the old direct reads. One observable divergence, pinned by test: the built agent's internal _function_toolset stays empty; tools live in the capability toolset. No code_puppy or core-plugin reader depends on the old location. 14 contract tests in tests/agents/test_native_tools_capability.py. Full suite: 7592 passed; the single red (test_render_version_check_current) is pre-existing on main (verified with changes stashed).
- Soften the 'signature-identical' claim: FunctionToolset.tool is argument-compatible with Agent.tool (default spellings differ, resolved tool definitions are equivalent) -- docstring reworded. - New wire-parity contract test comparing the model-visible ToolDefinition across both registration paths (provenance bookkeeping toolset_id/capability_id normalized out; nothing in code_puppy reads either field). - Document the collision-set scope in _builder.py: native tool names only; tools contributed by other capabilities (ToolOutputLimits' read_tool_result) are intentionally not in the set. - Document the extractor's deliberate breadth (theoretical double-count with the separate mcp_servers estimator input is acceptable slack for a best-effort estimate). 15 contract tests now. Full suite re-run: 7593 passed, same single pre-existing red.
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Ninth in the capability series (#828–#835). Promotes native tool delivery — Code Puppy's own tool suite (file ops, shell, sub-agent invocation, browser, plugin extras, UC wrappers) — from post-construction
@agent.toolregistration onto the constructedpydantic_ai.Agent, to a first-classNativeToolscapability on theget_toolset()seam.New module:
code_puppy/agents/_native_tools.pybuild_native_toolset(tool_names, model_name, agent_name)— points the existing registry at aFunctionToolset. Zero changes toregister_tools_for_agent: everyregister_*function only ever uses the.tooldecorator, andFunctionToolset.toolis argument-compatible withAgent.tool(default spellings differ; resolved model-visible tool definitions are equivalent — pinned by a wire-parity test). All registry semantics (plugin tool merging,edit_fileexpansion, kill-switches, per-model filtering) live where they always did.@dataclass NativeTools(AbstractCapability[Any])— delivers the toolset viaget_toolset(); returnsNonewhen empty (tool-less agents contribute nothing, same chain as before);get_serialization_name() -> None(built from the live registry — not spec-constructible, series precedent).Both construction sites converted:
_builder.build_pydantic_agent(+ the stripped tool probe) andtools/subagent_invocation.py.Feature-parity notes
max_retries=None, which pydantic-ai resolves to the agent-levelretries=3atget_toolstime. Pinned by test.probe_agent._toolsfor MCP collision filtering; pydantic-ai v2 removed that attribute, soexisting_tool_nameswas always∅andfilter_conflicting_mcp_toolshad silently become a no-op (a real collision would have raised a toolset-conflict error at run time). The native toolset knows its names up front, so filtering now works as documented again. Pinned by an end-to-end test (MCP shadow ofread_filehidden; unique MCP tool survives)._function_toolsetis now empty — tools ride the capability toolset (CombinedToolset/CapabilityOwnedToolsetchain on the publictoolsetsproperty, the exact traversal feat: deliver MCP toolsets via a pydantic-ai capability (McpToolsets) #834 verified DBOS-transparent). The only readers were the context-overhead estimators via_extract_pydantic_agent_tools, which now walks the publictoolsetsproperty (covers both shapes) before falling back to the legacy reads. No core-plugin reads the old location (audited).agent_namenot passed → plugin per-agent extras unchanged).Tests
14 contract tests in
tests/agents/test_native_tools_capability.py: registry semantics ride-through (expansion, unknown-tool skip), capability contract (populated/empty/serialization), tool-metadata parity betweenAgent.toolandFunctionToolset.tool, real dispatch throughAgent.run(), builder + sub-agent integration (divergence pins), repaired collision filter end-to-end, probe countability, extractor on both shapes.Full suite: 7592 passed; the single red (
test_render_version_check_current) is pre-existing on main — verified failing with these changes stashed.ruff check+ruff formatclean.Not merged on purpose
Per series protocol — awaiting human review. Note this PR grazes the same two
capabilities=[...]blocks as #828–#835; whoever lands last inherits the (trivial) rebases.Review round 1 (code-puppy clone): APPROVE with 2 non-blocking findings + 1 nit — all addressed in
13e4d24b(claim precision + wire-parity test; collision-set scope + extractor breadth documented).