Skip to content

[AAASM-2923] 🐛 (adapters): Forward-compat tool interception for ADK 1.x & Pydantic AI >=0.3.0#119

Merged
Chisanan232 merged 4 commits into
masterfrom
v0.0.1/AAASM-2923/forward_compat_interception
Jun 14, 2026
Merged

[AAASM-2923] 🐛 (adapters): Forward-compat tool interception for ADK 1.x & Pydantic AI >=0.3.0#119
Chisanan232 merged 4 commits into
masterfrom
v0.0.1/AAASM-2923/forward_compat_interception

Conversation

@Chisanan232

Copy link
Copy Markdown
Contributor

Description

Two framework adapters intercepted tool execution at a hook point that newer library versions no longer route through, so interception silently broke (or raised AttributeError) on current releases. This makes both adapters version-tolerant and fail-soft.

Pydantic AI (agent_assembly/adapters/pydantic_ai/patch.py)Tool._run was removed/restructured in Pydantic AI >=0.3.0, so the old patch raised AttributeError (the example had to pin <0.3.0). The adapter now detects the available hook across versions: Tool._run on <0.3.0, else AbstractToolset.call_tool(self, name, tool_args, ctx, tool) on >=0.3.0 (the actual >=0.3.0 tool-execution path, confirmed against current Pydantic AI docs). When neither hook point exists, apply() is a no-op that returns False instead of raising. revert() stays symmetric and idempotent for whichever class was patched.

Google ADK (agent_assembly/adapters/google_adk/patch.py) — concrete ADK 1.x tools (e.g. FunctionTool, confirmed via ADK docs to inherit BaseTool and override run_async) define their own run_async on the subclass, so a patch on BaseTool.run_async never ran for them. The adapter now discovers concrete tool classes in google.adk.tools that define their own run_async and patches each in addition to BaseTool. Patch state is tracked in each class's own __dict__ so an already-patched base does not mask a subclass patch, keeping per-class apply/revert symmetric and idempotent.

All existing governance behavior is preserved on both new paths: check_tool_start → pending/approval → deny raises PolicyViolationError → spawn context → record result.

Type of Change

  • ✨ New feature
  • 🔧 Bug fix
  • ♻️ Refactoring
  • 🍀 Performance improvement
  • 📚 Documentation update
  • 🚀 Release

Breaking Changes

Does this PR introduce any breaking changes?

  • No
  • Yes (please describe below)

No public API change. New behavior is additive: older library versions keep working via the existing hooks; newer versions are now intercepted instead of failing.

Related Issues

  • Related JIRA ticket: AAASM-2923
  • Related GitHub issues: N/A

Testing

Describe the testing performed for this PR:

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • No tests required (explain why)

New offline fake-class unit tests (no real libraries required) model the new API shapes:

  • Pydantic AI: a Tool with no _run exposing the >=0.3.0 AbstractToolset.call_tool hook — allow/deny flows, idempotency, revert symmetry, and the no-known-hook fail-soft path.
  • Google ADK: a FunctionTool subclass that overrides run_async — concrete-class discovery, that the subclass override is patched and intercepted on deny, and symmetric revert of base + subclass.

Existing pytest.importorskip integration tests are unchanged.

Validation gate (run before push, all green for changed code):

  • ruff check . — pre-existing unrelated failures only; the four changed files are clean.
  • ruff format --check . — pre-existing unrelated reformat candidates only; the four changed files are formatted.
  • mypy agent_assembly — pre-existing unrelated errors only (incl. native _core stub); the two changed impl files are clean.
  • pytest test/427 passed, 10 skipped, 0 failed (incl. 9 new tests).

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Comments added for complex logic
  • Documentation updated if needed
  • All tests passing

🤖 Generated with Claude Code

Chisanan232 and others added 4 commits June 14, 2026 20:05
Tool._run was removed/restructured in Pydantic AI >=0.3.0, so the old
patch raised AttributeError. Detect the available hook: Tool._run on
<0.3.0, else AbstractToolset.call_tool on >=0.3.0. When neither exists,
apply() no-ops and returns False instead of raising. revert() stays
symmetric and idempotent for whichever class was patched.

Refs AAASM-2923

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add offline fake-class tests modelling Pydantic AI >=0.3.0: a Tool with
no _run plus an AbstractToolset.call_tool hook. Cover allow/deny flows,
idempotency, revert symmetry, and the no-known-hook fail-soft path.

Refs AAASM-2923

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Concrete ADK 1.x tools (e.g. FunctionTool) override run_async on the
subclass, so a patch on BaseTool.run_async never runs for them. Discover
concrete tool classes in google.adk.tools that define their own
run_async and patch each in addition to BaseTool. Track patch state in
the class's own __dict__ so an already-patched base does not mask a
subclass patch, keeping per-class apply/revert symmetric and idempotent.

Refs AAASM-2923

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add offline fake-class tests modelling an ADK 1.x FunctionTool subclass
that overrides run_async. Cover concrete-class discovery, that the
subclass override is patched and intercepted on deny, and symmetric
revert of both base and subclass.

Refs AAASM-2923

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
agent_assembly/adapters/pydantic_ai/patch.py 90.54% 7 Missing ⚠️
agent_assembly/adapters/google_adk/patch.py 84.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@sonarqubecloud

Copy link
Copy Markdown

@Chisanan232

Copy link
Copy Markdown
Contributor Author

✅ Claude Code review — ready to merge

CI: All checks green — Analyze (python), CI Success, unit / integration / contract test jobs, Build documentation, SonarCloud, and codecov/patch all pass. The e2e and Deploy documentation jobs are skipping by design (not failures). No red, nothing to fix.

Scope vs AAASM-2923 acceptance criteria:

AC Status
Confirm real current APIs before coding (context7) ✅ Pydantic AI ≥0.3.0 → AbstractToolset.call_tool (Tool._run gone); Google ADK 1.x → FunctionTool overrides run_async
Pydantic AI intercepts on ≥0.3.0 and <0.3.0 apply() tries Tool._run, falls back to AbstractToolset.call_tool
Fail-soft — no AttributeError when hook absent _apply_tool_run_patch guards a missing/uncallable _run and returns False; apply() resets process_agent_id and returns False when neither hook exists
Google ADK intercepts concrete tools overriding run_async _load_google_adk_concrete_tool_classes discovers BaseTool subclasses with their own run_async; both base and concrete are patched
revert() symmetric & idempotent ✅ Reverts concrete classes then base; uses own-__dict__ checks
Offline fake-class + monkeypatch tests; integration keeps importorskip ✅ 9 new unit tests model both API shapes
ruff / mypy / pytest green ✅ Suite 427 passed, 0 failed; ruff/mypy clean on the changed files (the ~189 ruff / ~58 mypy findings elsewhere are pre-existing on master and untouched here)

Good catch beyond the AC: fixed a latent masking bug — the patched-flag check now reads the class's own __dict__ (vars(cls)) instead of inherited getattr, so an already-patched base no longer suppresses patching of a FunctionTool subclass. Governance flow (check → pending/approval → deny→PolicyViolationError → spawn context → record) is preserved on every new path.

Residual risk (acceptable, documented):

  • New hook paths are validated by fake-class unit tests onlypydantic-ai / google-adk aren't installed in CI, so the importorskip integration tests skip. Fakes mirror the context7-documented signatures faithfully.
  • The ≥0.3.0 patch hooks AbstractToolset.call_tool at the base level; a custom toolset that overrides call_tool without calling super() would bypass interception — same shape as the ADK concern and extensible the same way if it ever surfaces.

Follow-up (not this PR): the adapter is now forward-compatible, so the pydantic-ai example's <0.3.0 pin can be relaxed in the agent-assembly-examples repo — correctly left out here to avoid cross-repo coupling.

Verdict: Scope fully covered, CI green, correct and well-tested. Approving for merge.

— Claude Code

@Chisanan232 Chisanan232 merged commit b3f8ffd into master Jun 14, 2026
21 checks passed
@Chisanan232 Chisanan232 deleted the v0.0.1/AAASM-2923/forward_compat_interception branch June 14, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant