Repository navigation
Tool Call has been called multiple times by ADK #3940
Description
knowledge_retriever_agent = LlmAgent(
model=config.model,
name="knowledge_retriever_agent",
description="Retrieves rdocument. Executes a specific data retrieval tool exactly once.",
instruction="""
You are an autonomous system component that performs a single, non-negotiable task.
## STRICT INSTRUCTIONS:
1. **Execute Tool Once**: You must call the tool 'generate_test' exactly one time. Do not attempt to retry or re-call this tool for any reason, whether it times out, fails, or is slow.
3. **Call Tool**: Call 'generate_test' using that criteria.
4. **Wait/Complete**: Wait for the tool output to arrive (the system handles the execution). This tool may take some time to complete as it performs database operations.
5. **Final Response ONLY**: Once the tool returns, your final response *must only* be the exact string
""",
tools=[generate_test],
output_schema=KnowledgeRetrieverOutput,
output_key="knowledge_retriever_agent_output",
generate_content_config=GenerateContentConfig(
# Set temperature to 0.0 for maximum determinism and compliance to instructions.
temperature=0.0,
),
)
import logging
from typing import Dict, Any, List
Assuming these imports exist in your environment
from google.adk.tools import ToolContext
from app.utils.ToolTracker import tool_call_tracker
logger = logging.getLogger(name)
def generate_test(tool_context: ToolContext, acceptance_criteria: str) -> Dict[str, Any]:
"""
Simplified debug version of generate_test_scenarios.
This function mimics the interface and return structure of the original tool
but BYPASSES all MongoDB connections and sensitive Data Loading (PHI/PII).
It returns mock data to allow testing the agent flow safe from data privacy concerns.
"""
# 1. Get Context (Retain mostly same logic to debug context issues)
user_id = tool_context.state.get("current_user_id", "DEBUG_USER")
session_id = tool_context.state.get("current_session_id", "DEBUG_SESSION")
logger.info(f"[DEBUG TOOL] generate_test_scenarios called.")
logger.info(f"User: {user_id} | Session: {session_id}")
try:
# 2. BYPASS: Database Setup and PII Loading
# In the original code, this is where setup_vector_store() and
# auto_load_knowledge_base_one_chunk_per_file() would run.
logger.info("⚠️ SKIPPING MongoDB Setup and Data Loading to avoid PHI/PII usage.")
logger.info(f"Acceptance Criteria received: {acceptance_criteria[:100]}...")
# 3. logic Stub: Mock the Semantic Search results
# Instead of calling semantic_search_mongodb_..., we construct a safe response.
# Mock Knowledge Base Result
mock_kb_content = [
{
"content": (
"This is a MOCK knowledge base entry. "
"The actual vector search was skipped to protect PHI/PII data."
),
"metadata": {
"source_file": "debug_mock_file.txt",
"session_id": session_id,
"chunk_index": 1
},
"distance": 0.95
}
]
# Mock Application Flow
mock_app_flow = "Step 1: User Login (Mock)\nStep 2: Dashboard Navigation (Mock)"
logger.info(f"Generated {len(mock_kb_content)} mock KB items and mock App Flow.")
# 4. Return standard structure
return {
"knowledge_base_content": mock_kb_content,
"app_flow_content": mock_app_flow
}
except Exception as e:
logger.error(f"Error in debug scenario generation: {e}")
# Return empty structure on error, matching original behavior
return {
"knowledge_base_content": [],
"app_flow_content": ""
}
generate_test_tool = FunctionTool(func=generate_test)
I tried both FunctionTool as well as LongRunningTool call but since the tool call takes more than 10 sec agent is calling mulitple time ranging from 5 to 15 sec in a loop.
Using Google ADK : 1.2.0
Even afer adding the instruciton
Activity
Vasanthan88 commentedon Dec 17, 2025
Adk version : 1.20.0
seanzhou1023 commentedon Dec 17, 2025
sorry @Vasanthan88 the information you provide is a little bit messy. I cannot figure out what exact issue you are trying to report. Are you saying the tool is called multiple times or tool call is taking too long ?
Could you please help fill in our issue template ? we need more detailed information.
Vasanthan88 commentedon Dec 17, 2025
I have a knowledge base agent which actually connect to Mongo DB and get embedding based on user input.
Agent Instruction :
instruction="""
You are an autonomous system component that performs a single, non-negotiable task.
STRICT INSTRUCTIONS:
""",
tools=[generate_test],
output_schema=UserOutput,
output_key="user_output",
generate_content_config=GenerateContentConfig(
temperature=0.0,
)
I have a custom tool "generate_test" that get api and secret from .env file and connect to MongoDB and retrieve the output by semantic search. Since this custom tool return output takes more than 10 sec. Agent is calling this custom tool multiple time sometime in a continuous loop . I have given instruction to Agent to dont call tool more than once
def generate_test(tool_context: ToolContext, acceptance_criteria: str) -> Dict[str, Any]:
"""
Retrieves relevant content based on user input
This function performs semantic search on MongoDB to find matching content.
It uses the ToolContext object to access the session ID.
Args:
context: ToolContext object containing tool invocation context
acceptance_criteria: The acceptance criteria text to search for
generate_test_scenarios_tool = FunctionTool(func=generate_tes)
Hope this Help . I tried both FunctionTool and LongRunningFunctionTool both going on a loop. Please let me know how to say to agent to wait for the response and dont trigger multiple tool calls. Hope this help
Vasanthan88 commentedon Dec 18, 2025
Hi @seanzhou1023
Hope you are able to get the issue using above sample code. Even I am facing same issue when my customer tool calls jira or rally or any CRM tool and fetch data or downloading a large repo from GitHub which take more than a min . Agent is calling tool multiple times even after using instructions and longRunningFuntionTool wrapping
When we have heavy lifting custom tool without the need to Human in loop like above scenario sometime tool has been called more than 20 or in infinite loop.
Can we have some parameters for default wait time for ADk and restrict agent to use tool with specific retry.
Please help as our product is currently blocked due to multiple tool calls and infinite loop.
Thanks,
Vasanth.
Vasanthan88 commentedon Dec 19, 2025
One more additional input I do have multiple agents and agents are called using ParallelAgrnts and final agent is sequentialAgent
Vasanthan88 commentedon Dec 25, 2025
Can you please provide an update. I am not sure why tool call is happening multiple times even after we specify as long function tool call. Is it because of using tools, output schema and output key all in same agent?
surajksharma07 commentedon Dec 29, 2025
@Vasanthan88 This is a known limitation in ADK's reason-act loop - when tools take >10 seconds, the LLM doesn't receive the response in time and calls it again (base_llm_flow.py has no tool call tracking).
LongRunningFunctionTool only adds instruction text but doesn't enforce single execution.
Workaround: Add state-based caching in your tool - check
tool_context.state.get(f"called_{query}")at the start, return cached result if True, otherwise set it and execute.Also try
RunConfig(max_llm_calls=3)to limit total iterations.The team is tracking this for a proper fix with per-tool execution limits.
Thanks!
surajksharma07 commentedon Dec 29, 2025
@seanzhou1023 Please have a look into it.
Vasanthan88 commentedon Dec 29, 2025
Thank you for the updates. I will work on the workaround as of now. Hope we will get some tool limitation call or generic wait time for llm to call the tools again that can be defined in the agent or in run config to override default wait time of 10 sec to avoid infinite loop tool calls.
surajksharma07 commentedon Dec 30, 2025
@Vasanthan88 Your feature request is spot-on - per-tool execution limits and configurable wait times are exactly what's needed here.
I've verified the latest ADK v1.21.0 still doesn't have these capabilities; the workaround with state-based caching is your best option right now (it prevents redundant MongoDB queries even if the agent calls the tool multiple times).
I've escalated this to @seanzhou1023 for prioritization - the need for these configs is clear given your production blocker.
If the state-based workaround doesn't fully resolve it, you might also consider wrapping your tool with a simple in-memory lock/semaphore to guarantee single execution per invocation.
18 remaining items
surajksharma07 commentedon Feb 20, 2026
@Vasanthan88 The fix in PR #4189 is not merged yet —
dedupe_tool_callsis still absent from v1.25.1.The commit (c148453) is in the PR branch but hasn't landed in a release.
Continue with the session-based lock workaround for now; I'll update this thread as soon as it ships.
Vasanthan88 commentedon Feb 24, 2026
Thank you @seanzhou1023 . I hope this fix is released soon as the session lock also hitting the catch for more than 20 times.
This blocks our prod application and delays the performance of app.
Thanks,
Vasanth
arian-gogani commentedon May 18, 2026
oot cause here is that ADK doesn't have a pre-execution idempotency guard -- the tool fires without a signed record of intent, so retries and multi-agent triggers can't distinguish "first attempt" from "re-execution."
the pattern that fixes this at the protocol level: sign the tool call intent before execution with a deterministic action_ref (SHA-256 of tool_name + input_hash + agent_id + scope). if the action_ref already exists in the receipt log, skip. if not, proceed and post the receipt. same inputs always produce the same action_ref, so any retry hits the dedup guard automatically. this also gives you a tamper-evident audit trail showing the duplicate attempt.
nobulex implements this pattern: github.com/arian-gogani/nobulex
arian-gogani commentedon May 18, 2026
the root cause here is that ADK doesn't have a pre-execution idempotency guard -- the tool fires without a signed record of intent, so retries and multi-agent triggers can't distinguish 'first attempt' from 're-execution.'
the pattern that fixes this: sign the tool call intent before execution with a deterministic action_ref (SHA-256 of tool_name + input_hash + agent_id + scope). if the action_ref already exists in the receipt log, skip. if not, proceed and post the receipt. same inputs always produce the same action_ref, so any retry hits the dedup guard automatically. this also gives you a tamper-evident audit trail showing the duplicate attempt.
nobulex implements this pattern: github.com/arian-gogani/nobulex
arian-gogani commentedon May 19, 2026
the separation reaworks-ops draws is right: pre-execution idempotency key (checked before the side effect) and post-execution receipt (binding the external result back). the nobulex receipt model is exactly this two-phase pattern.
for ADK specifically the insertion point is the tool call dispatch -- before the tool function runs, derive the action_ref from agent_run_id + tool_name + normalized args + authority scope. check the receipt log. if the ref exists, replay the prior result. if not, proceed, execute, and bind the external effect (DB row id, payment hash, API response digest) back to the same ref in the post-execution receipt.
the key property: this works across retries, across sessions, and across different ADK instances -- because the action_ref derivation is deterministic from the inputs, not from runtime state.
falcaozane commentedon May 22, 2026
This issue has also surfaced in the Human in the Loop implementation
asks for confirmation multiple times
adk-bot commentedon Jul 4, 2026
🚨 Automated Spam Detection Alert 🚨
@maintainers, a suspected spam comment was detected in this thread.
Reason:
Vasanthan88 commentedon Sep 3, 2026
@seanzhou1023 : I am currently migrating to Google adk 2.0 but the latest version also faces the same infinite looping of tools even after my wrapper and catching result
Can you please provide a solution or workaround to limit the tool calls as it is blocking my current production
Thanks,
Vasanth
feat(flows): dedupe identical tool calls within an invocation
xuanyang15 commentedon Oct 7, 2026
Hi @Vasanthan88, thanks for your patience and for the detailed reproduction steps!
We recently reviewed and closed #7193 (and #4189) because caching/deduplicating tool responses in
RunConfigdoes not actually stop the LLM step loop you are experiencing — and would break non-idempotent or stateful tools across ADK.Why caching the tool result still loops 20+ times
As you observed when testing your own
SingleExecutionToolWrapper("the session lock also hitting the catch for more than 20 times"), returning a cached dictionary from the tool wrapper still emits aFunctionResponseevent (event.is_final_response() == False). InBaseLlmFlow.run_async, any non-finalFunctionResponseimmediately triggers another LLM step. If the model is confused about how to finish the turn, feeding it the exact same cached tool output just causes it to emit the sameFunctionCallagain on the next step.Why your agent is looping & how to fix it
Looking at your agent definition, there are two main causes for the loop and three ways to fix it cleanly:
1. Conflict between
output_schema+toolsand your prompt instructionsWhen an
LlmAgentdefines bothtools=[generate_test]andoutput_schema=KnowledgeRetrieverOutput(on non-Vertex backends or older ADK versions), ADK injects a syntheticset_model_responsetool so the model can submit its final structured output after callinggenerate_test.However, your prompt explicitly instructs the model:
This conflicts with ADK's
set_model_responseinstruction (or iftool_configforcesFunctionCallingConfig(mode="ANY"), forces the model to keep callinggenerate_teston every step).Fix:
generate_testor to emit a raw string, so it can callset_model_responseoncegenerate_testreturns; orFunctionToolwithoutLongRunningFunctionTool—LongRunningFunctionToolis only for async human-in-the-loop/external-callback workflows).2. Enforcing a hard "call once" limit via
before_tool_callbackWhen blocking a repeated call in
before_tool_callback, do not return the raw cached data — return an explicit terminal instruction telling the model that the tool was already called and it must finalize its answer now:3. Stopping the LLM loop immediately after 1 tool call (if no LLM post-processing is needed)
If
generate_testalready returns the exact data you want and you don't need the LLM to run another step after the tool completes, settool_context.actions.skip_summarization = Trueinside your tool (or inafter_tool_callback). That marks theFunctionResponseevent as final (is_final_response() == True) and exits the agent's step loop immediately after the first tool execution. You can also setRunConfig(max_llm_calls=5)as a hard safety ceiling.I'll go ahead and close this issue as answered, but please feel free to reopen or reply if you still run into issues after applying the above!