Skip to content

Tool Call has been called multiple times by ADK #3940

Closed

Description

@Vasanthan88

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

  1. added
    core[Component] This issue is related to the core interface and implementation
    on Dec 17, 2025
  2. added theissue type on Dec 17, 2025
  3. Vasanthan88 commented on Dec 17, 2025

    @Vasanthan88
    Author

    Adk version : 1.20.0

  4. seanzhou1023 commented on Dec 17, 2025

    @seanzhou1023
    Collaborator

    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.

  5. Vasanthan88 commented on Dec 17, 2025

    @Vasanthan88
    Author

    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:

    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.
    2. Call Tool: Call 'generate_test' using that criteria.
    3. 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.
    4. Final Response ONLY: Once the tool returns, your final response must only be the exact string
      """,
      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

    Returns:
        Dict[str, Any]: A dictionary containing:
            - "user_content": List of relevant knowledge base content items (each with content, metadata, distance)
    """
    
    try:
        # Setup MongoDB connections and embeddings
        load_dotenv()
       client, db, collection = setup_vector_store()
        text_splitter = setup_text_splitter()
        google_embeddings = setup_google_embeddings()
        success = auto_load_knowledge_base_one_chunk_per_file(client, db, collection, google_embeddings, application_name)
        
        semantic_search_knowedge_base_content_outcome = []
        if kb_count > 0:
            semantic_search_knowedge_base_content_outcome = semantic_search_mongodb_to_get_similar_knowledge_base_content(
                application_name, acceptance_criteria, collection, google_embeddings, top_k=1, session_id=search_session_id,
            )
        else:
            logger.warning("Knowledge base collection is empty. No search performed.")
    
        return {
            "user_content": semantic_search_knowedge_base_content_outcome,
        }
        
    except Exception as e:
        return {
            "user_content": [],
        }
    

    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

  6. Vasanthan88 commented on Dec 18, 2025

    @Vasanthan88
    Author

    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.

  7. Vasanthan88 commented on Dec 19, 2025

    @Vasanthan88
    Author

    One more additional input I do have multiple agents and agents are called using ParallelAgrnts and final agent is sequentialAgent

  8. Vasanthan88 commented on Dec 25, 2025

    @Vasanthan88
    Author

    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?

  9. surajksharma07 commented on Dec 29, 2025

    @surajksharma07
    Collaborator

    @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!

  10. surajksharma07 commented on Dec 29, 2025

    @surajksharma07
    Collaborator

    @seanzhou1023 Please have a look into it.

  11. Vasanthan88 commented on Dec 29, 2025

    @Vasanthan88
    Author

    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.

  12. surajksharma07 commented on Dec 30, 2025

    @surajksharma07
    Collaborator

    @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.

  13. 18 remaining items

  14. surajksharma07 commented on Feb 20, 2026

    @surajksharma07
    Collaborator

    @Vasanthan88 The fix in PR #4189 is not merged yet — dedupe_tool_calls is 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.

  15. Vasanthan88 commented on Feb 24, 2026

    @Vasanthan88
    Author

    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

  16. arian-gogani commented on May 18, 2026

    @arian-gogani

    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

  17. arian-gogani commented on May 18, 2026

    @arian-gogani

    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

  18. arian-gogani commented on May 19, 2026

    @arian-gogani

    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.

  19. falcaozane commented on May 22, 2026

    @falcaozane

    This issue has also surfaced in the Human in the Loop implementation
    asks for confirmation multiple times

  20. adk-bot commented on Jul 4, 2026

    @adk-bot
    Collaborator

    🚨 Automated Spam Detection Alert 🚨
    @maintainers, a suspected spam comment was detected in this thread.

    Reason:

    @arian-gogani posted multiple repetitive comments promoting a 3rd-party repository (github.com/arian-gogani/nobulex) as self-promotion/spam.
    
  21. added
    spam[Status] Issues suspected of having comments which are spam
    on Jul 4, 2026
  22. Vasanthan88 commented on Sep 3, 2026

    @Vasanthan88
    Author

    @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

  23. self-assigned this
    on Oct 7, 2026
  24. xuanyang15 commented on Oct 7, 2026

    @xuanyang15
    Collaborator

    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 RunConfig does 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 a FunctionResponse event (event.is_final_response() == False). In BaseLlmFlow.run_async, any non-final FunctionResponse immediately 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 same FunctionCall again 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 + tools and your prompt instructions

    When an LlmAgent defines both tools=[generate_test] and output_schema=KnowledgeRetrieverOutput (on non-Vertex backends or older ADK versions), ADK injects a synthetic set_model_response tool so the model can submit its final structured output after calling generate_test.
    However, your prompt explicitly instructs the model:

    "1. Execute Tool Once: You must call the tool 'generate_test' ... 5. Final Response ONLY: Once the tool returns, your final response *must only* be the exact string..."

    This conflicts with ADK's set_model_response instruction (or if tool_config forces FunctionCallingConfig(mode="ANY"), forces the model to keep calling generate_test on every step).

    Fix:

    • Remove rules (1) and (5) that tell the model only to call generate_test or to emit a raw string, so it can call set_model_response once generate_test returns; or
    • Split retrieval and schema formatting into two steps (or use a plain FunctionTool without LongRunningFunctionTool — LongRunningFunctionTool is only for async human-in-the-loop/external-callback workflows).

    2. Enforcing a hard "call once" limit via before_tool_callback

    When 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:

    def limit_generate_test_once(tool, args, tool_context):
      if tool.name != "generate_test":
        return None
      seen_key = f"temp:called:{tool.name}"
      if tool_context.state.get(seen_key):
        return {
            "error": (
                f"Tool '{tool.name}' has already been executed in this turn. "
                "Do NOT call it again. Use the result already in the conversation "
                "history and produce your final response immediately."
            )
        }
      tool_context.state[seen_key] = True
      return None
    
    
    knowledge_retriever_agent = LlmAgent(
        name="knowledge_retriever_agent",
        model="gemini-2.5-flash",
        tools=[generate_test],
        output_schema=KnowledgeRetrieverOutput,
        output_key="knowledge_retriever_agent_output",
        before_tool_callback=limit_generate_test_once,
        instruction="...",
    )

    3. Stopping the LLM loop immediately after 1 tool call (if no LLM post-processing is needed)

    If generate_test already returns the exact data you want and you don't need the LLM to run another step after the tool completes, set tool_context.actions.skip_summarization = True inside your tool (or in after_tool_callback). That marks the FunctionResponse event as final (is_final_response() == True) and exits the agent's step loop immediately after the first tool execution. You can also set RunConfig(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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

core[Component] This issue is related to the core interface and implementationneeds review[Status] The PR/issue is awaiting review from the maintainerspam[Status] Issues suspected of having comments which are spam

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions