Repository navigation
Description
🔴 Required Information
Describe the Bug:
When an agent uses a tool with require_confirmation=True, the evaluation trajectory includes the framework-generated adk_request_confirmation function call alongside the model's real tool calls. get_all_tool_calls() (src/google/adk/evaluation/eval_case.py:239) collects every function_call part in invocation_events and does not exclude ADK's client function calls (CLIENT_FUNCTION_CALL_NAMES in src/google/adk/utils/_function_call_names.py). As a result:
tool_trajectory_avg_scorewithmatch_type: EXACTscores a correct trajectory 0.0, even withignore_args: true.tool_call_count_v1counts the confirmation request as a tool call.
Steps to Reproduce:
- Run the minimal reproduction below with
pytest(no API key needed; a scriptedBaseLlmplays the model). - The same agent, script and eval case are run with
require_confirmation=Falseandrequire_confirmation=True.
Expected Behavior:
Both runs pass. The model called get_order then cancel_order in both, which matches the expected trajectory. adk_request_confirmation is emitted by the framework, not chosen by the model.
Observed Behavior:
require_confirmation=False: PASSEDrequire_confirmation=True: FAILED.tool_trajectory_avg_score for orders_agent Failed. Expected 1.0, but got 0.0.The actual tool calls areget_order,cancel_order,adk_request_confirmation(originalFunctionCall=..., toolConfirmation={... 'confirmed': False}).
The same happens with adk eval and a live Gemini model (gemini-3.5-flash): actual trajectory get_order → cancel_order → adk_request_confirmation, tool_trajectory_avg_score: FAILED (0.0), tool_call_count_v1: 3.
Environment Details:
- ADK Library Version: 2.14.0 (
mainatdd7161f) - Desktop OS: macOS 14.7 (Darwin 23.6.0)
- Python Version: 3.11
Model Information:
- Are you using LiteLLM: No
- Which model is being used: scripted
BaseLlm(repro);gemini-3.5-flash(live check)
🟡 Optional Information
Regression: Unknown.
Additional Context:
- The other client function calls (
adk_request_credential,adk_request_input) are collected by the same code path, so auth- and input-gated tools are probably affected too. Not verified at runtime. - Precedent for treating these names as framework calls:
src/google/adk/cli/api_server.py:769(_ADK_RESERVED_FUNCTION_NAMES) andsrc/google/adk/a2a/agent/_remote_a2a_agent.py:129. - Possible fix: skip names in
CLIENT_FUNCTION_CALL_NAMESinget_all_tool_calls()(and the matching responses inget_all_tool_responses()), or in the trajectory/efficiency evaluators. Happy to send a PR with unit tests if the approach sounds right. - Related limitation (separate issue if useful): inference stops at the confirmation pause (
actual_responseis empty), and an eval case has no way to supply the confirmation decision, so post-approval behaviour can't be evaluated. - Related: tool_trajectory_avg_score returns 0.0 even when tool name and args match exactly #5410 / fix(eval): include function-call events in invocation_events when skip_summarization is set #5417 (function-call events in
invocation_events).
Minimal Reproduction Code:
"""Offline repro: a confirmation-gated tool breaks tool_trajectory_avg_score.
No API key needed: a scripted BaseLlm plays the model.
The same agent, the same scripted model and the same eval case are run twice:
- require_confirmation=False -> trajectory [get_order, cancel_order] -> PASS
- require_confirmation=True -> ADK adds its own `adk_request_confirmation`
function call to the actual trajectory -> FAIL (EXACT, ignore_args=True)
Run:
pytest -q test_confirmation_trajectory.py
"""
import os
from typing import AsyncGenerator
from google.adk.agents import LlmAgent
from google.adk.evaluation.agent_evaluator import AgentEvaluator
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.adk.tools.function_tool import FunctionTool
from google.genai import types
import pytest
HERE = os.path.dirname(os.path.abspath(__file__))
class ScriptedModel(BaseLlm):
"""Returns pre-written responses in order."""
model: str = "scripted"
script: list[types.Content]
calls: int = 0
async def generate_content_async(
self, llm_request: LlmRequest, stream: bool = False
) -> AsyncGenerator[LlmResponse, None]:
content = self.script[min(self.calls, len(self.script) - 1)]
self.calls += 1
yield LlmResponse(content=content)
def _call(name: str, args: dict) -> types.Content:
return types.Content(
role="model",
parts=[types.Part(function_call=types.FunctionCall(name=name, args=args))],
)
def get_order(order_id: str) -> dict:
"""Looks up an order."""
return {"id": order_id, "status": "processing"}
def cancel_order(order_id: str) -> dict:
"""Cancels an order."""
return {"id": order_id, "status": "cancelled"}
def _build_agent(require_confirmation: bool) -> LlmAgent:
model = ScriptedModel(
script=[
_call("get_order", {"order_id": "A-100"}),
_call("cancel_order", {"order_id": "A-100"}),
types.Content(role="model", parts=[types.Part(text="Order A-100 is cancelled.")]),
]
)
return LlmAgent(
name="orders_agent",
model=model,
instruction="Help with orders.",
tools=[
get_order,
FunctionTool(cancel_order, require_confirmation=require_confirmation),
],
)
@pytest.mark.asyncio
@pytest.mark.parametrize("require_confirmation", [False, True])
async def test_trajectory_with_and_without_confirmation(require_confirmation):
# Expected trajectory in data/cancel.test.json: get_order, cancel_order.
# test_config.json: tool_trajectory_avg_score EXACT, ignore_args=True.
await AgentEvaluator.evaluate(
agent_module=_build_agent(require_confirmation),
eval_dataset_file_path_or_dir=os.path.join(HERE, "data", "cancel.test.json"),
num_runs=1,
)
cancel.test.json
"eval_set_id": "confirmation_trajectory_repro",
"eval_cases": [
{
"eval_id": "cancel_order",
"conversation": [
{
"invocation_id": "inv-1",
"user_content": {"role": "user", "parts": [{"text": "Cancel order A-100."}]},
"final_response": {"role": "model", "parts": [{"text": "Order A-100 is cancelled."}]},
"intermediate_data": {
"tool_uses": [
{"name": "get_order", "args": {"order_id": "A-100"}},
{"name": "cancel_order", "args": {"order_id": "A-100"}}
],
"intermediate_responses": []
}
}
],
"session_input": {"app_name": "orders_agent", "user_id": "user", "state": {}}
}
]
}
test_config.json
{
"criteria": {
"tool_trajectory_avg_score": {"threshold": 1.0, "match_type": "EXACT", "ignore_args": true}
}
}
How often has this issue occurred?: Always (100%).
Activity
Anoshor commentedon Nov 7, 2026
Hi, I'd like to work on this. The fix I have in mind is to have
get_all_tool_calls()skip the framework's client function calls (CLIENT_FUNCTION_CALL_NAMES), so the trajectory and tool-count evaluators only see calls the model chose, plus a unit test covering a confirmation-gated tool. I'll open a PR shortly.fix(eval): exclude ADK client function calls from evaluated tool calls
fix: exclude ADK client function calls from evaluated tool calls