fix(google-adk): represent a shared FunctionTool as one tool with N bindings - #330
Merged
Merged
Conversation
…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>
9 tasks
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.
Summary
FunctionToolto a coordinator and its sub-agents — the canonical ADK multi-agent shape — aborted the scan withDuplicate tool observation identitybefore any finding orrelease_decisionexisted. Reproduced at7e0bfdf5with the issue's three-agent shape; the same manifest now scans to a decision.Toolper 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.AgentBindingObservationrecords onLoadedToolSource— 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_nameis dropped from ADK Python tools (a single-valued annotation can only ever name one of N agents);adk_agent_source_idstays, because it is what lets toolset-loaded tools resolve back to the ADK source.unresolved_bound_toolfor every shared tool. Collapsing to one canonical observation fixes the binding graph too, not just the crash.FunctionToolandLongRunningFunctionToolkeeps the stricter long-running contract plus a warning instead of letting binding order decide the operation contract.Acceptance criteria
FunctionToolacross three agents scans withoutInputParseErrortest_google_adk_shared_function_tool_is_one_capability_with_three_bindingslen(observation_ids) == 1per catalog entryreachable_tool_ids, andbinding_assessment.claims[]naming every binding agentframeworks.google_adk.function_tool_count(definitions) vs. newtool_binding_count(edges): 2 vs. 6SHARED_TOOL_AGENT_SOURCEintests/test_google_adk.pytests/test_adapter_static_only.py+tests/test_fixture_no_import.pyunchanged and greenPer the review comment on the issue, true duplicates still fail closed:
test_google_adk_true_duplicate_source_still_fails_closeddeclares the same entrypoint twice under one source id and assertsDuplicate tool observation identitystill raises. The guard itself is untouched — the fix is upstream of it.Type
Verification
CI is authoritative for
python -m ruff check .,python -m compileall -q src tests, andpython -m pytest.Additional local checks run:
pytest -n auto -m "not perf" --ignore=tests/test_adapter_static_only.py(exit 0), plustests/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.status: structural,pass_eligible: true, and zero binding issues.samples/google_adk_agentbefore/after: decisions, findings, tool ids, and the agent x tool edge set are identical. Two intended deltas — binding edges now cite theLlmAgent(...)call site (agent.py:34) instead of the tool's own inventory/spec file with a null pointer, and theannotationshash moves for ADK tools becauseadk_agent_nameis 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.binding_surface_facts.tool_edgesand movestool_binding_count4 -> 5.Release-readiness notes
docs/checks.md— none added or changedSTABILITY.md—frameworks.google_adk.tool_binding_countis 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_versionstays0.34; the publishedrequiredset 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_diffcarries only reachable-tool and handoff deltas, so "a second agent gained the refund tool" is visible inbinding_surface_factsand intool_binding_countbut 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