Repository navigation
Dev UI graph: edges to tools/nodes with ":" in the name attach to a phantom node聽#7543
Description
馃敶 Required Information
Describe the Bug:
plot_workflow_graph (src/google/adk/cli/utils/graph_visualization.py) declares nodes with quoted names but emits edge endpoints unquoted. When a node or tool name contains a colon (e.g. srv:tool), Graphviz parses a -> srv:tool as node srv with port tool, not as the node named "srv:tool". The edge then points to a phantom node and the real "srv:tool" node is left disconnected. This function backs the dev UI graph endpoint (build_graph_image in cli/dev_server.py).
Steps to Reproduce:
- Install ADK from source at current
upstream/main(orpip install google-adk). - Run the snippet below (no
dotbinary is needed because it usesformat="dot"). - Inspect the emitted DOT source.
- [FILL IN, optional: run
adk webwith an agent whose tool name contains:and open the graph view.]
Expected Behavior:
The edge a -> "srv:tool" terminates at the node named srv:tool, and that node appears connected in the rendered graph.
Observed Behavior:
The emitted DOT contains an unquoted edge endpoint:
a -> srv:tool
DOT interprets this as node srv + port tool, so the edge attaches to a phantom srv node while the declared "srv:tool" node floats unconnected. No error or stack trace is raised.
Environment Details:
- ADK Library Version (pip show google-adk): [FILL IN, or commit SHA of upstream/main]
- Desktop OS: [FILL IN, e.g. macOS]
- Python Version (python -V): [FILL IN]
Model Information:
- Are you using LiteLLM: N/A (no model involved; the bug is in graph rendering)
- Which model is being used: N/A
馃煛 Optional Information
Regression:
Unknown. [FILL IN if you checked an older version; otherwise leave as "Not checked".]
Logs:
N/A (silent failure; the incorrect DOT output is shown above)
Screenshots / Video:
[FILL IN: attach an adk web screenshot if reproduced there; otherwise remove this section or write "Not yet verified in the web UI; reproduced at the DOT-output level."]
Additional Context:
I verified this by running plot_workflow_graph directly and reading the DOT source. I have not verified how the adk-web frontend renders it. I searched open and closed issues and PRs and found no existing report for this. I'd like to send a fix plus the first unit tests for this module (tests can assert on the DOT source, so no Graphviz binary is needed). Happy to follow whichever approach maintainers prefer (quoting edge endpoints vs. sanitized internal IDs with the real name as the label).
Minimal Reproduction Code:
from google.adk.cli.utils.graph_visualization import plot_workflow_graph
dot = plot_workflow_graph(
{
"root_agent": {
"name": "wf",
"graph": {
"nodes": [
{
"name": "a",
"type": "agent",
"tools": [{"name": "srv:tool", "type": "tool"}],
}
],
"edges": [],
},
}
},
format="dot",
)
print(dot)
# emits an edge like: a -> srv:toolHow often has this issue occurred?:
- Always (100%) for any node/tool name containing
:
Activity
kavishkartha05 commentedon Oct 15, 2026
I'd like to take this. I've reproduced it end to end with
adk web(a function tool namedsrv:tool): the node is declared as "srv:tool" but the edge is emitted ascolon_agent -> srv:tool, which DOT reads as nodesrvwith porttool.Fix: graphviz.Digraph.edge() splits endpoints on ":" before quoting, so I quote the whole endpoint via a small Digraph subclass. That keeps the real name as the DOT id, which adk-web relies on (it reads the SVG <title> as the node name for click handling), so renaming nodes to internal ids wasn't an option. Output is byte-identical for graphs without colon names. PR to follow shortly with tests.
fix(cli): keep graph edges attached to node names with a colon
surajksharma07 commentedon Oct 15, 2026
@kavishkartha05 Reproduced this on main (2.12.0) and it's not only tool edges: a workflow node like "b:x" also breaks on both ends (a -> b:x, b:x -> END).
Your _quote_edge override fixes all three cases on my side and leaves colon-free graphs unchanged. Since it relies on a private graphviz hook though the dot.body route Gemini suggested might be the safer option long term.
Could you add a test for the colon-as-source/workflow-node case and check whether cli/agent_graph.py's draw_edge needs the same treatment?
Once you've tested that thoroughly on your end (graphviz 0.20.2 and 0.21) let us know and we'll get #7546 moving for review.
kavishkartha05 commentedon Oct 15, 2026
@surajksharma07 Thanks for the review. I added the requested tests in a follow-up commit (
f2e36d088, tests only). For a workflow node named"b:x"they cover the node as an edge target, as an edge source, and as a terminal node linked to__END__. Each asserts on theformat="dot"source that:"b"endpoint appears, andAll three fail on
upstream/mainand pass on the branch.I ran the graph tests (
test_graph_visualization.py,test_agent_graph.py,test_graph_serialization.py) against both ends of thegraphviz>=0.20.2,<1pin, 0.20.2 and 0.21, and they all pass.cli/agent_graph.pyhas the same issue.draw_edgecallsDigraph.edge()directly, so a tool named"srv:tool"yieldsroot -> srv:tool. It is only reachable through tool names, since agent and workflow-node names must be Python identifiers. The fix is the same subclass used inget_agent_graphand the two cluster constructors (about 4 lines plus a test). I can fold it into this PR, since it's the same root cause, or send it as a separate PR.On the private
_quote_edgehook: I looked at appending formatted edge lines todot.bodyinstead, but that means usingquoting.attr_listand managing newlines by hand at all three call sites, which is more code than the small subclass. The hook exists in both pinned versions, and a unit test fails with a clear message if it is renamed or stops working. I'm glad to switch to thedot.bodyapproach if you prefer to avoid the private hook.surajksharma07 commentedon Oct 15, 2026
Pulled the branch with f2e36d0 and all 8 tests in test_graph_visualization.py pass for me on graphviz 0.20.2 and 0.21 @kavishkartha05. You鈥檙e right that workflow node names have to be identifiers (_base_node.py checks this) so tool names are the only real way to hit this. My "b:x" case was mostly synthetic.
Also confirmed agent_graph.py on main: a FunctionTool renamed to "srv:tool" gives root -> srv:tool. Please fold that fix and its test into this PR since it鈥檚 the same root cause and it鈥檚 better to land them together.
The subclass is fine with the guard test in place. If the reviewers would rather not rely on the private hook the switch to dot.body can happen in review.
One small thing: the PR description still has the [BEFORE SCREENSHOT]/[AFTER SCREENSHOT] placeholders. Could you attach the screenshots or remove those lines? Once the agent_graph change is pushed will flag #7546 for review.
kavishkartha05 commentedon Oct 15, 2026
@surajksharma07 Thanks, folded in. I pushed a follow-up commit (
b10c02451) that applies the same fix toagent_graph.py.get_agent_graphand the cluster subgraphs for Sequential/Loop/Parallel agents now use the same subclass, so a tool namedsrv:tooldrawsroot -> "srv:tool"instead of attaching to a phantomsrvnode. I added tests for a colon-named tool on a plain agent and inside each cluster type, and they fail onupstream/main. Output is byte-identical toupstream/mainfor graphs without a colon (22agent_graphscenarios compared), mypy shows no new errors, and the graph tests pass on both graphviz 0.20.2 and 0.21. Also the screenshot placeholders in the PR description are replaced with before/after screenshots, and the description now covers both files.