ag-ui-protocol / ag-ui-protocol/ag-ui

[Bug]: ADK middleware has no NodeTool awareness — nested Workflow silently restarts on HITL resume, output_schema leaks, NodeTool tracked as client-answerable

Abierto
#2,674 0 comentarios 0 reacciones 0 asignados Ver en GitHub
Lenguaje dominante
Python
Estrellas
15.9k
Forks
1.4k
Merge medio
1 d 17 h
PR fusionados (30 d)
163

Descripción

### Summary

`ag_ui_adk` has no awareness of ADK's **node-as-a-tool** topology (`NodeTool`, [google/adk-python#5581](https://github.com/google/adk-python/issues/5581), ADK >= 2.8), where a `Workflow` is attached to an `LlmAgent` via `tools=[...]` rather than `sub_agents=[...]`. A GitHub code search for `NodeTool` across this repo returns 0 hits.

Three separate mechanisms assume a Workflow can only be reached as the **root** or through `sub_agents` / `graph.nodes`. Each breaks when the Workflow sits one hop away behind a tool. The most serious one is a silent failure: **the user answers a HITL picker and nothing happens** — no exception, no error, no log.

This is effectively a regression of #1669 under a topology that issue did not anticipate, plus a third instance of the traversal-gap family already accepted in #1860/#1889 and #2036.

Line numbers are against current `main`, `integrations/adk-middleware/python/src/ag_ui_adk/adk_agent.py`.

### Topology

```python
# ADK >= 2.8: LlmAgent._pre_validate_tools auto-wraps any BaseNode into a NodeTool
root_agent = Agent(
name="coordinator",
model="gemini-3.5-flash",
tools=[hs_classifier_workflow], # a Workflow, as a tool — not a sub_agent
)
app = App(name="coordinator", root_agent=root_agent,
resumability_config=ResumabilityConfig(is_resumable=True))
```

`hs_classifier_workflow` emits a `RequestInput` (a HITL picker). The root is an `LlmAgent`; the Workflow is reachable only via `tools`.

---

### Bug 1 — HITL resume: `_root_agent_is_workflow()` gates on the root, so a nested Workflow is stranded

`_root_agent_is_workflow()` (line 459) is used at line 2911 as `and not self._root_agent_is_workflow()`.

Its own docstring states the mechanism precisely:

> The #1534 pre-append workaround for LlmAgent roots — which replaces `new_message` with an empty placeholder — strands Workflow roots because there's no `function_response` in the placeholder for the Workflow to resume from. See ag-ui#1669.

That reasoning applies verbatim to a Workflow **one level down**. With an `LlmAgent` root, `_root_agent_is_workflow()` returns `False`, the gate passes, the #1534 placeholder substitution runs, and the nested Workflow gets no `function_response` — so `Workflow._run_impl` finds nothing in `_extract_resume_inputs(new_message)` and restarts from `START` instead of rehydrating its `WAITING` node.

**Symptom:** the picker is answered, the response is posted, and the run silently restarts. Nothing raises.

I should flag that I filed #1669 and proposed the `not self._root_agent_is_workflow()` expression that PR #1746 implemented verbatim — so this is my own suggested fix being too narrow, not a maintainer oversight. The correct predicate is "is a Workflow **active / reachable** for this resume", not "is the root a Workflow". #1444 is an earlier instance of the same root-type-gating anti-pattern (`SequentialAgent` / `LoopAgent`), which suggests the general form is worth fixing rather than patching per-topology.

A per-run predicate keyed on the resume payload itself holds for any nesting depth, which is what we ended up using downstream.

### Bug 2 — `output_schema` JSON leaks into chat: the traversal skips `tools`

`_collect_output_schema_agent_names` (line 2339) recurses into `sub_agents` (2351) and `graph.nodes` (2355), but **never `tools`**. An `LlmAgent` with `output_schema` inside a Workflow attached as a NodeTool is therefore never collected, `EventTranslator` does not suppress it, and its structured JSON renders as a user-visible chat message — exactly what #1390 added this for.

This is the same argument accepted in **#1860 → #1889** (which added `graph.nodes`) and again in **#2036** (which found the identical gap in `_update_agent_tools_recursive` and `_shallow_copy_agent_tree`). `tools` is the next hop in that same family. Note `_shallow_copy_agent_tree` (line 2380) does a shallow `getattr(..., 'tools')` list rebuild but likewise does not recurse into tools.

### Bug 3 — NodeTool calls are tracked as client-answerable pending tool calls

`_add_pending_tool_call_with_context` registers a NodeTool invocation as a pending tool call awaiting a client answer. A NodeTool is executed **server-side by ADK**; the client never answers it. This interacts with the balance gate from #1935, which assumes function-call/response parity for client-answerable calls.

---

### Environment

- `ag-ui-adk` 0.7.0
- `google-adk` 2.8.0 (from `main` @ `b0180620f`, since the nested-HITL fix [google/adk-python#7043](https://github.com/google/adk-python/issues/7043) / `6d1451806` is not in any tagged release yet)
- Python 3.14, Vertex AI backend, CopilotKit frontend

### Why this is worth fixing generally

Node-as-a-tool is a first-class ADK feature as of 2.8, and it is the natural way to give one conversational agent several independently-invocable workflows — each call its own replay scope, with the user able to talk to the coordinator between steps. Any app using that shape hits all three bugs, and Bug 1 fails silently. #1849 (ADK 2.x support) looks like the right umbrella.

Happy to open a PR if the maintainers agree on the shape — particularly on whether Bug 1 should become a reachability check or a per-run predicate keyed on the resume payload.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.