feat(agents): promote tool-call JSON repair to a ToolCallJsonRepair capability - #841
Open
mpfaffenberger wants to merge 2 commits into
Open
feat(agents): promote tool-call JSON repair to a ToolCallJsonRepair capability#841mpfaffenberger wants to merge 2 commits into
mpfaffenberger wants to merge 2 commits into
Conversation
…apability Convert the patch_tool_call_json_repair monkeypatch (eager ToolManager.validate_tool_call surgery) into a first-class pydantic-ai capability on the before_tool_validate seam. - New code_puppy/agents/_json_repair.py: stateless ToolCallJsonRepair repairs malformed JSON string args via json_repair, mirrors the repair onto the live ToolCallPart (same history custody as the patch's in-place mutation), and returns the repaired args for validation. build_tool_call_json_repair() returns [] when json_repair is absent, mirroring the patch's quiet optional-dependency skip. - Both construction sites (_builder.py + subagent_invocation.py) splice the capability; position inert (tool-validate seam never interacts with the history hooks). - The monkeypatch is demoted to a guest-agent fallback: it steps aside when the run's root_capability tree contains ToolCallJsonRepair (public AbstractCapability.apply leaf-walk), and keeps repairing for raw pydantic-ai agents built by plugins (wiggum's tool-wielding judge). Explicit-when-ours, fallback-for-guests, same split as Logfire instrumentation. - Bounded divergence, documented + pinned: unknown/unavailable tool calls no longer get their recorded args repaired (resolution raises before the seam fires); the call fails with the identical ModelRetry and history keeps the model's true emitted bytes. - Archaeology: on pydantic-ai 2.31.0 output tools validate through validate_output_tool_call, which the patch never wrapped - output tools were never covered, so the seam's kind=='output' carve-out changes nothing. 16 contract tests: seam semantics, end-to-end FunctionModel runs with history-custody pins, wire parity vs the eager patch, patch gating both ways, unknown-tool divergence pin, wiring + seam-signature pins.
- Pin retry parity for the unknown-tool divergence: identical RetryPromptPart feedback and recovery under the eager patch and the capability; only the recorded args differ (raw vs repaired), which is the entire documented divergence. Note that unavailable tools share the same raise-before-seam code path. - Strengthen the seam-signature pin to compare parameter names, kinds, and defaults (annotations excluded deliberately: the seam spells RawToolArgs, we spell the underlying union).
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.
ToolCallJsonRepaircapabilityThirteenth in the capability series (#828–#836, #838–#840). Converts the tool-call JSON repair feature — until now an eager monkeypatch of
ToolManager.validate_tool_callinpydantic_patches.py— into a first-class pydantic-ai capability on thebefore_tool_validateseam. This is the first conversion in the series that retires monkeypatch surgery rather than a constructor kwarg or a run-site closure.What the feature does
LLMs sometimes emit slightly broken JSON in tool-call arguments (trailing commas, missing quotes, unclosed braces). Repairing the raw args before validation via
json_repairavoids burning a retry round-trip.How it moved
code_puppy/agents/_json_repair.py— statelessToolCallJsonRepair(AbstractCapability):before_tool_validaterepairs string args and mirrors the repair onto the liveToolCallPart(call.args = repaired) — the same in-place custody the patch performed, so message history records the JSON the tool actually ran with. Verified empirically: the part passed to the seam is the object recorded in run state.build_tool_call_json_repair()returns[]when the optionaljson_repairdependency is absent (mirrors the patch's quiet skip and thebuild_tool_output_limitsconditional-splice pattern)._builder.py,subagent_invocation.py) splice the capability. Position is inert — the tool-validate seam never interacts with the history hooks.validate_tool_callwalks the run'sToolManager.root_capabilitytree with the publicAbstractCapability.applyvisitor and steps aside whenToolCallJsonRepairis present. Raw pydantic-ai agents built by plugins keep eager repair — the audit found one real dependent: wiggum's judge, which registers genuine read-only tools.btw's side-query agent has no tools (inert either way);shell_safety's agent builds through our builder (gets the capability).Parity audit
before_tool_validateis the only hook receiving raw pre-validation args; it fires exactly once pervalidate_tool_call(allow_partial=Falseonly — no streaming-partial hazard)._resolve_toolraisesModelRetrybefore the seam fires, where the patch repaired first and failed after. The call fails identically either way; history now keeps the model's true emitted bytes (arguably more honest). Pinned bytest_unknown_tool_args_stay_raw_in_history.validate_output_tool_call— a method the patch never wrapped. Output tools were never covered by this feature, so pydantic-ai'skind == 'output'hook carve-out changes nothing. (An output-tool repair viabefore_output_validatewould be new behavior, deliberately out of scope — noted for a future PR if wanted.)Tests
17 contract tests in
tests/agents/test_tool_call_json_repair_capability.py:FunctionModelruns: tool executes with repaired args, zero retries burned, history carries repaired bytes;_run_owns_json_repairleaf-walk incl.CombinedCapabilitynesting;capabilities=[...]blocks + a seam-signature pin so a pydantic-ai hook-contract change fails loudly.Full suite: 7615 passed, 0 failed (28 skipped, 1 xpassed). Reviewed by code-puppy clone: pass 1 APPROVE with 1 non-blocking + 1 nit (both applied in 103571e), pass 2 APPROVE with zero findings.
Merge-order note
Touches the same two
capabilities=[...]blocks as the other twelve open capability PRs — whichever lands last eats a trivial rebase.