Skip to content

Human in the loop within custom agent and SequentialAgent workflow does not pause execution #3184

@yanbin-pan

Description

@yanbin-pan

Description

When a Custom agent overrides the _run_async_impl method, it does not correctly pause its execution when a sub-agent enters a PAUSED state due to tool_context.request_confirmation(). The custom agent is built as described in: https://google.github.io/adk-docs/agents/custom-agents/#design-pattern-example-storyflowagent

The parent agent's async for event in sub_agent.run_async(ctx): loop completes as soon as the first confirmation is requested. The parent agent's logic then immediately continues to the next step, instead of waiting for the user to provide the confirmation and for the sub-agent to resume and complete its task.

In the provided example, DeepAnalyticsAgent (parent custom agent) invokes task_brief_agent (sub-agent). This sub-agent correctly requests user confirmation and pauses. However, DeepAnalyticsAgent does not wait; it proceeds instantly to invoke task_supervisor_agent, breaking the intended sequential workflow.

My Custom Agent has 2 sub-agents

To Reproduce

  1. Define a tool (_confirm_research_brief) that uses tool_context.request_confirmation().

  2. Assign this tool to a sub-agent (task_brief_agent).

  3. Define a custom parent agent (DeepAnalyticsAgent) that overrides _run_async_impl.

  4. Inside _run_async_impl, sequentially invoke task_brief_agent and then another agent (task_supervisor_agent) using run_async.

  5. Execute the DeepAnalyticsAgent.

Observe that task_supervisor_agent begins execution as soon as the confirmation UI for task_brief_agent is displayed, without waiting for user input.

def _confirm_research_brief(research_brief: str, tool_context: ToolContext) -> str:
    """Function tool to request user confirmation of the research brief."""
    tool_confirmation = tool_context.tool_confirmation

    if not tool_confirmation:
        tool_context.request_confirmation(
            hint=(
                'Please review the research brief and confirm if it is satisfactory.'
            ),
            payload={
                'approved': 'no',
                'edit_brief': ''
            },
        )
        return {'status': 'User Approval Requested'}

    approved = tool_confirmation.payload.get('approved', 'no').lower()

    if approved == 'yes':
        # SUCCESS: Set the final state with the original brief.
        tool_context.state["final_task_brief"] = research_brief
        return {"status": "The user approved the brief."}

    elif approved == 'edit':
        edited_brief = tool_confirmation.payload.get('edit_brief')
        if edited_brief:
            # SUCCESS: Set the final state with the EDITED brief.
            tool_context.state["final_task_brief"] = edited_brief
            return {"status": "The user edited and approved the brief."}
        else:
            # Handle case where user chose 'edit' but provided no text.
            tool_context.state["final_task_brief"] = None
            return {"status": "User chose edit but provided no changes. Rejection assumed."}

    else: # 'no' or any other case
        # REJECTION: Explicitly set the state to None to signal failure.
        tool_context.state["final_task_brief"] = None
        return {"status": "The user rejected the brief."}

task_brief_agent = LlmAgent(
    model=LLM_MODEL,
    name="task_brief_agent",
    description="Takes user question and generates a research brief to guide the process of addressing it.",
    instruction=ENV_PROMPT.get_template("task_brief.j2").render(),
    tools=[AgentTool(agent=rag_agent), _confirm_research_brief],
    output_key="task_brief",
)


class DeepAnalyticsAgent(BaseAgent):
    task_brief_agent: LlmAgent
    task_supervisor_agent: TaskSupervisorAgent
    model_config = {"arbitrary_types_allowed": True}

    def __init__(
        self,
        name: str,
        task_brief_agent: LlmAgent,
        task_supervisor_agent: TaskSupervisorAgent,
    ):
        sub_agents_list = [
            task_brief_agent,
            task_supervisor_agent,
        ]

        super().__init__(
            name=name,
            task_brief_agent=task_brief_agent,
            task_supervisor_agent=task_supervisor_agent,
            sub_agents=sub_agents_list, 
        )
    
    @override
    async def _run_async_impl(
        self, ctx: InvocationContext
    ) -> AsyncGenerator[Event, None]:
        if not self.sub_agents:
            return

        async for event in self.task_brief_agent.run_async(ctx):
            logger.info(f"[{self.name}] Task Sequence Event: {event.model_dump_json(indent=2, exclude_none=True)}")
            yield event
        
        task_brief = ctx.session.state["task_brief"]
        print(task_brief)

        async for event in self.task_supervisor_agent.run_async(ctx):
            logger.info(f"[{self.name}] Task Sequence Event: {event.model_dump_json(indent=2, exclude_none=True)}")
            yield event

root_agent = DeepAnalyticsAgent(
    name="DeepAnalyticsAgent",
    task_brief_agent=task_brief_agent,
    task_supervisor_agent=task_supervisor_agent,
)

Expected behavior
The execution of the parent DeepAnalyticsAgent should pause when task_brief_agent requests user confirmation. The async for loop iterating over self.task_brief_agent.run_async(ctx) should not complete until the sub-agent's lifecycle for the current task is fully finished (i.e., after the user responds and the agent processes that response). The task_supervisor_agent should only begin its execution after this confirmation is received and handled.

Desktop (please complete the following information):

  • OS: macOS
  • Python version(python - v.12:
  • ADK version(pip show google-adk): 1.16.0

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: gemini-2.5-flash

Metadata

Metadata

Assignees

Labels

agent engine[Component] This issue is related to Agent Engine deploymentanswered[Status] This issue has been answered by the maintainer

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions