google / google/adk-python

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

Đang mở
#7,094 1 bình luận 0 reaction 1 người được giao Được @sanketpatil06 nhận Xem trên GitHub
needs review tools
Ngôn ngữ chính
Python
Star
21.5k
Fork
4k
Merge trung bình
1 ngày 14 giờ
Pull request đã merge (30 ngày)
37

Mô tả

** Please make sure you read the contribution guide and file the issues in the right place. **
[Contribution guide.](https://google.github.io/adk-docs/contributing-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 `await`s 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 #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` (`"@"`) 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 `await`ed), so it doesn't help with not blocking the orchestrator's turn.

### Proposed API / Implementation

Sketch, additive to #6592's `AgentDispatcherToolset`:

```python
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.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.