Skip to content

Feature request: let a background AgentTool/dispatched agent's own intermediate events be observed live (not just wait=false + poll for the final result) #7094

Description

@ferponse

** Please make sure you read the contribution guide and file the issues in the right place. **
Contribution guide.

🔴 Required Information

Is your feature request related to a specific problem?

Related to #4759 and the in-progress PR #6592 (AgentDispatcherToolset) — this is a narrower, additive request on top of those, not a replacement.

We want a chat orchestrator to invoke another agent as a tool, without blocking the orchestrator's own turn (same goal as #4759/#6592), and we want an external caller (e.g. a chat UI) to observe the background agent's own intermediate activity — its text, its own function_call/function_response events — live, as they happen, while it runs in its own isolated Runner/session.

We looked for a native way to do this in 2.7.1 and confirmed none of the existing primitives fit:

  • AgentTool (tools/agent_tool.py): builds an isolated child Runner, but awaits it fully inside run_async before returning the tool's function_response. Blocking, and the child's intermediate events never leave the wrapper — only the final merged text does.
  • mode='task' sub-agents (_TaskAgentTool): not backgrounding — it's a human-in-the-loop hand-off. If the task agent doesn't finish in one internal pass, the whole invocation ends (NodeInterruptedError propagates to Runner._drive_root_node), and the user's next plain-text message gets silently rerouted to the paused task agent (Runner._find_active_task_scope, _append_user_event) instead of the orchestrator. The orchestrator doesn't keep working — it becomes a silent proxy until the task agent calls finish_task.
  • LongRunningFunctionTool (is_long_running): a real "return now, deliver the FunctionResponse later via a fresh run_async() call" mechanism, and that later call can legitimately be triggered by a server-side callback rather than the end user (matching is purely by function_call.id). But marking the dispatch tool itself long-running makes Event.is_final_response() true immediately, so the orchestrator's turn ends right at the dispatch call — it can't even emit an acknowledgement in the same turn, let alone keep working.
  • response_scheduling (FunctionResponseScheduling): the one truly automatic, no-new-client-message push mechanism we found — but it's implemented only in handle_function_calls_live (flows/llm_flows/functions.py), gated behind the Live/bidi (run_live/LiveRequestQueue) runtime. Not available to a standard request/response Runner.run_async() service, which is what most chat backends (including ours) use.

We looked at PR #6592 closely — it already covers "dispatch, don't block, poll/await for the final result, follow-up questions on the same child session, on_complete callback." That's great and we'd use it. But per its own "Remaining bounds" section, its completion signaling is deliberately poll/callback only — there's no way for something outside the tool call itself (e.g. our WS layer) to observe the child's intermediate text/tool_call/tool_response events as they're produced, only the final get_agent_result/await_agent payload.

Describe the Solution You'd Like

On top of #6592's dispatcher (or as a variant of AgentTool), expose the background child's own ADK event stream to something outside the tool call — e.g. an optional on_event callback (sync, called once per child event, analogous to on_complete but per-event) alongside on_complete, so a host application can render live progress (a "sub-agent card" with its own nested activity) for a dispatched/background agent-as-tool the same way it already can for a foreground one.

Two things we'd want kept separate, since they answer different consumers:

  1. The orchestrator's own conversational context should still only ever see one final message per dispatch (as it does today via AgentTool's function_response, or via feat(tools): Add AgentDispatcherToolset for runtime agents #6592's get_agent_result) — never the play-by-play.
  2. An external observer (UI, logging, tracing) should be able to see the child's own intermediate events live, independent of whether/when the orchestrator's context is updated.

Impact on your work

We're building this by hand today (isolated Runner + InMemorySessionService, a ContextVar-based side channel into our own event-processing loop, and a manual session_service.append_event for the single final message) specifically to drive a live "sub-agent activity" UI. It works, but every consumer of AgentTool/#6592 who wants observability into a background child's own steps will end up reinventing the same side channel unless it's part of the toolset/AgentTool contract itself.

Willingness to contribute

No — happy to share our working approach as a reference if useful, but not proposing to drive the PR.

🟡 Recommended Information

Describe Alternatives You've Considered

  • Subclassing AgentTool.run_async to drive the child Runner by hand and forward events ourselves (what we did) — works, but touches several private APIs (_get_input_schema, tool_context._invocation_context, PluginManager internals) that aren't a documented extension point.
  • mode='single_turn': event.branch ("<agent>@<function_call.id>") gives free, correct correlation for concurrent invocations and the events do reach the parent's own stream — but it's fully inline/blocking (tool_context.run_node is awaited), so it doesn't help with not blocking the orchestrator's turn.

Proposed API / Implementation

Sketch, additive to #6592's AgentDispatcherToolset:

async def dispatch_agent(
    self,
    name: str,
    instruction: str,
    user_message: str,
    wait: bool = False,
    on_event: Callable[[str, Event], None] | None = None,  # (dispatch_id, event)
    on_complete: Callable[[str, str], None] | None = None,  # (dispatch_id, result)
    ...
) -> dict:
  ...

on_event fires once per event the child's own isolated Runner.run_async() yields, same shape/timing as what a foreground AgentTool's caller already sees inside agent_tool.py's drain loop — just not thrown away.

Activity

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

Metadata

Metadata

Assignees

Labels

needs review[Status] The PR/issue is awaiting review from the maintainertools[Component] This issue is related to tools

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions