Skip to content

fix(google-adk): represent a shared FunctionTool as one tool with N bindings - #330

Merged
pengfei-threemoonslab merged 1 commit into
mainfrom
claude/github-issue-321-286e8b
Aug 7, 2026
Merged

fix(google-adk): represent a shared FunctionTool as one tool with N bindings#330
pengfei-threemoonslab merged 1 commit into
mainfrom
claude/github-issue-321-286e8b

Conversation

@pengfei-threemoonslab

Copy link
Copy Markdown
Contributor

Summary

  • Fixes bug(google-adk): shared FunctionTool bindings collide as duplicate tool observations #321. Binding one FunctionTool to a coordinator and its sub-agents — the canonical ADK multi-agent shape — aborted the scan with Duplicate tool observation identity before any finding or release_decision existed. Reproduced at 7e0bfdf5 with the issue's three-agent shape; the same manifest now scans to a decision.
  • The Python extractor emitted one Tool per agent binding, and observation identity is (source_type, source_id, native_locator) where the locator is file + function name, so binding Bump actions/github-script from 7.1.0 to 9.0.0 #2 collided with binding Bump actions/checkout from 4.3.1 to 6.0.2 #1. Adding the agent name to that identity is rejected for the reason the issue gives: one function is one action, and a capability per binding inflates every catalog-derived count.
  • The function is now observed once, and the many-to-many relation travels as framework-owned AgentBindingObservation records on LoadedToolSource — the surface the OpenAI Agents SDK adapter already uses, whose stated contract is that catalog-controlled metadata never becomes authority-bearing binding evidence. adk_agent_name is dropped from ADK Python tools (a single-valued annotation can only ever name one of N agents); adk_agent_source_id stays, because it is what lets toolset-loaded tools resolve back to the ADK source.
  • Worth noting: name-based edge resolution requires exactly one match, so even without the parse abort the shared shape would have produced unresolved_bound_tool for every shared tool. Collapsing to one canonical observation fixes the binding graph too, not just the crash.
  • Two sibling shapes fixed while in here: a toolset assigned to a variable and shared between agents is loaded once instead of once per agent (it was minting a duplicate observation of the same spec under a second source id), and a function bound as both FunctionTool and LongRunningFunctionTool keeps the stricter long-running contract plus a warning instead of letting binding order decide the operation contract.

Acceptance criteria

Criterion Where
Shared FunctionTool across three agents scans without InputParseError test_google_adk_shared_function_tool_is_one_capability_with_three_bindings
One canonical observation per function definition same test — len(observation_ids) == 1 per catalog entry
All three bindings available to root-reachability and reviewer evidence same test — full agent x tool edge set, reachable_tool_ids, and binding_assessment.claims[] naming every binding agent
Unique tool counts and binding counts distinguishable frameworks.google_adk.function_tool_count (definitions) vs. new tool_binding_count (edges): 2 vs. 6
Offline regression fixture: two shared mapping tools, root + two sub-agents SHARED_TOOL_AGENT_SOURCE in tests/test_google_adk.py
No agent code imported or executed fixture raises at module level; tests/test_adapter_static_only.py + tests/test_fixture_no_import.py unchanged and green

Per the review comment on the issue, true duplicates still fail closed: test_google_adk_true_duplicate_source_still_fails_closed declares the same entrypoint twice under one source id and asserts Duplicate tool observation identity still raises. The guard itself is untouched — the fix is upstream of it.

Type

  • Input adapter change
  • Report, schema, or SARIF output

Verification

CI is authoritative for python -m ruff check ., python -m compileall -q src tests, and python -m pytest.

Additional local checks run:

  • Full CI-equivalent suite green: pytest -n auto -m "not perf" --ignore=tests/test_adapter_static_only.py (exit 0), plus tests/test_adapter_static_only.py, test_p0_safety_canaries.py, test_p0_binding_canaries.py, test_agent_boundary.py, ruff, compileall, generate_schemas.py --check.
  • Reproduced the issue verbatim first (three agents, two shared mapping tools) and confirmed the exact error, then confirmed the same manifest now exits 0 with 2 canonical tools, 6 bindings, 3 agent nodes, both sub-agent handoffs, status: structural, pass_eligible: true, and zero binding issues.
  • A/B'd samples/google_adk_agent before/after: decisions, findings, tool ids, and the agent x tool edge set are identical. Two intended deltas — binding edges now cite the LlmAgent(...) call site (agent.py:34) instead of the tool's own inventory/spec file with a null pointer, and the annotations hash moves for ADK tools because adk_agent_name is gone. Identity, schema, policy, and risk hashes are unchanged, so fingerprints and baselines are unaffected; the first scan after upgrade may show a metadata-only tool-surface change.
  • Verified a per-agent widening (a sub-agent gains an already-reachable tool) shows up in binding_surface_facts.tool_edges and moves tool_binding_count 4 -> 5.

Release-readiness notes

  • No user-code import added to default scan paths
  • No network access added to default scan paths
  • New or changed check IDs are documented in docs/checks.md — none added or changed
  • Report/schema changes are additive or documented in STABILITY.mdframeworks.google_adk.tool_binding_count is additive under the existing "framework blocks may grow" rule, and that rule now states that tool counts count definitions while bindings move with the wiring. report_schema_version stays 0.34; the published required set is untouched (additionalProperties: true), so the new key can be promoted to required at the next bump.

Follow-up (not in scope here)

binding_surface_diff carries only reachable-tool and handoff deltas, so "a second agent gained the refund tool" is visible in binding_surface_facts and in tool_binding_count but produces no diff row when the tool was already root-reachable. That per-edge delta is now representable for the first time; happy to file it as a separate issue rather than widen this PR.

🤖 Generated with Claude Code

…indings

Binding the same `FunctionTool` to a coordinator and its sub-agents is the
canonical Google ADK multi-agent shape, and it aborted the scan with
`Duplicate tool observation identity` before any finding or `release_decision`
existed. That is worse than an abstention: `insufficient_evidence` at least
routes a human, while a hard input failure produces nothing to act on — so on
real multi-agent ADK repositories the supported adapter returned no gate at all.

The Python extractor emitted one `Tool` per *agent binding*, and catalog
observation identity is `(source_type, source_id, native_locator)` where the
locator is the file plus the function name, so the second binding of one
function collided with the first. Widening that identity with the agent name
is not the fix: one function is one action, and minting a capability per
binding would inflate every count derived from the catalog and quietly change
what "unique tools" means.

The function is now observed once, and the many-to-many binding relation
travels as framework-owned `AgentBindingObservation` records — the surface the
OpenAI Agents SDK adapter already uses, and the one whose contract is that
catalog-controlled metadata never becomes authority-bearing binding evidence.
All bindings survive as first-class edges in the binding graph, in
root-reachability, and in each tool's `binding_assessment.claims[]`, with every
claim pointing at its own `LlmAgent(...)` call site — which the old annotation
could not do, since a single-valued `adk_agent_name` can only name one of N
agents. Name-based edge resolution also required exactly one match, so the
shared shape could not have resolved even if it had parsed.

Sharing a tool stays distinct from declaring one twice: the observation
identity guard is untouched and a source that genuinely repeats a declaration
still fails closed. A toolset assigned to a variable and shared between agents
is likewise loaded once instead of once per agent, and a function bound as both
`FunctionTool` and `LongRunningFunctionTool` keeps the stricter long-running
contract plus a warning rather than letting binding order decide.

`frameworks.google_adk` gains `tool_binding_count` (additive) alongside
`function_tool_count`, which counts tool definitions, so a function shared by
three agents reads as one tool and three bindings. ADK Python tools no longer
carry `adk_agent_name`, so the first scan after upgrade may report a
metadata-only annotation-hash change; tool identities, fingerprints, baselines,
and decisions are unaffected, and `report_schema_version` stays at 0.34.

Closes #321

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pengfei-threemoonslab
pengfei-threemoonslab merged commit dc68a83 into main Aug 7, 2026
4 checks passed
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.

bug(google-adk): shared FunctionTool bindings collide as duplicate tool observations

1 participant