Skip to content

HITL tool confirmation fails with Tool not found when sub-agent is wrapped in SequentialAgent #1189

@DevTomek-pl

Description

@DevTomek-pl

🔴 Required Information

Describe the Bug:

HITL (Human-in-the-Loop) tool confirmation fails with VerifyException: Tool not found when a sub-agent that uses beforeToolCallbackSync with requestConfirmation() is wrapped inside a SequentialAgent (or any non-LlmAgent workflow agent).

After the user approves the tool execution, RequestConfirmationLlmRequestProcessor attempts to re-execute the confirmed tool. It resolves tools from invocationContext.agent(), but at that point the agent in context is the root LlmAgent, not the sub-agent that originally requested the confirmation. The root agent does not have the sub-agent's tools registered, causing the VerifyException.

The root cause is in Runner.findAgentToRun(): after HITL approval, it walks session events backwards to find the agent to resume. It calls isTransferableAcrossAgentTree(), which requires every agent in the parent chain to be an instance of LlmAgent. A SequentialAgent in the chain breaks this requirement, so the runner falls back to the root agent — which doesn't know the sub-agent's tools.

HITL Confirmation Flow Context:

This bug affects the HITL (Human-in-the-Loop) tool confirmation flow where an agent's beforeToolCallbackSync calls toolContext.requestConfirmation() to pause tool execution and ask the user for approval before proceeding.

Steps to Reproduce:

  1. Create a root LlmAgent (e.g. rootAgent) with a SequentialAgent as a sub-agent.
  2. The SequentialAgent wraps an LlmAgent sub-agent (e.g. childAgent) that has a tool (e.g. myTool) registered via FunctionTool.create(...).
  3. Set beforeToolCallbackSync on childAgent that calls toolContext.requestConfirmation(...) to request user approval before executing myTool.
  4. Send a user message that triggers rootAgent to delegate to the SequentialAgent, which delegates to childAgent, which calls myTool.
  5. childAgent's beforeToolCallbackSync fires and requests confirmation — the agent pauses.
  6. Resume the session with a user message containing a FunctionResponse for adk_request_confirmation with confirmed: true.
  7. Observe the crash:
com.google.common.base.VerifyException: Tool not found: myTool
    at com.google.adk.flows.llmflows.Functions.handleFunctionCalls(Functions.java:146)
    at com.google.adk.flows.llmflows.RequestConfirmationLlmRequestProcessor.lambda$assembleEvent$11(RequestConfirmationLlmRequestProcessor.java:225)

Expected Behavior:

After the user confirms the tool execution, RequestConfirmationLlmRequestProcessor should resolve the tool from the correct sub-agent (the one that originally requested the confirmation), not from the root agent. The tool should execute successfully.

Observed Behavior:

Runner.findAgentToRun() cannot transfer back to the LlmAgent sub-agent because isTransferableAcrossAgentTree() fails when a SequentialAgent (which is not an LlmAgent) is in the parent chain. The runner falls back to the root LlmAgent, which does not have the sub-agent's tools. Functions.handleFunctionCalls() throws VerifyException: Tool not found.

Environment Details:

  • ADK Library Version: 1.1.0
  • OS: macOS / Linux
  • Java Version: 21

Model Information:

  • Which model is being used: gemini-2.5-flash (not model-specific — this is a framework-level issue)

🟡 Optional Information

Additional Context:

The issue is in Runner.isTransferableAcrossAgentTree() (Runner.java:752-767):

private boolean isTransferableAcrossAgentTree(BaseAgent agentToRun) {
    BaseAgent current = agentToRun;
    while (current != null) {
        if (!(current instanceof LlmAgent)) {  // ← SequentialAgent fails here
            return false;
        }
        LlmAgent agent = (LlmAgent) current;
        if (agent.disallowTransferToParent()) {
            return false;
        }
        current = current.parentAgent();
    }
    return true;
}

This method rejects any agent whose parent chain includes a non-LlmAgent (like SequentialAgent or ParallelAgent). This means HITL confirmation cannot work for any tool inside an agent that is wrapped in a workflow agent.

A possible fix would be to either:

  1. Skip non-LlmAgent agents in the isTransferableAcrossAgentTree check (treating them as transparent wrappers), or
  2. Have RequestConfirmationLlmRequestProcessor.assembleEvent() resolve tools by searching the full agent tree (including sub-agents) rather than only looking at invocationContext.agent().

Note: This is a separate issue from #688, which describes an ID mismatch bug in the same RequestConfirmationLlmRequestProcessor class.

Workaround:

Remove the SequentialAgent wrapper and register the sub-agents directly as sub-agents of the root LlmAgent. This allows findAgentToRun() to correctly identify and transfer to the sub-agent after HITL approval.

Minimal Reproduction Code:

// Sub-agent with a tool that requires HITL confirmation
LlmAgent childAgent = LlmAgent.builder()
    .name("child_agent")
    .model(model)
    .instruction("Use myTool when asked.")
    .tools(FunctionTool.create(myToolInstance, "myTool"))
    .beforeToolCallbackSync((ctx, tool, input, toolCtx) -> {
        if (toolCtx.toolConfirmation().isPresent()) {
            return toolCtx.toolConfirmation().get().confirmed()
                ? Optional.empty()
                : Optional.of(Map.of("status", "cancelled"));
        }
        toolCtx.requestConfirmation("Please confirm execution", Map.of());
        return Optional.of(Map.of("status", "awaiting_approval"));
    })
    .build();

// Wrapping in a SequentialAgent causes HITL to break
SequentialAgent workflow = SequentialAgent.builder()
    .name("workflow")
    .subAgents(childAgent)
    .build();

LlmAgent rootAgent = LlmAgent.builder()
    .name("root")
    .model(model)
    .instruction("Delegate to workflow when needed.")
    .subAgents(workflow)
    .build();

Runner runner = Runner.builder()
    .agent(rootAgent)
    .appName("test-app")
    .sessionService(new InMemorySessionService())
    .build();

// Step 1: Send message that triggers the tool → gets confirmation request
// Step 2: Resume with adk_request_confirmation approved → VerifyException: Tool not found: myTool

How often has this issue occurred?:

  • Always (100%)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions