Skip to content

Dev UI graph: edges to tools/nodes with ":" in the name attach to a phantom node聽#7543

Description

@kavishkartha05

馃敶 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:

  1. Install ADK from source at current upstream/main (or pip install google-adk).
  2. Run the snippet below (no dot binary is needed because it uses format="dot").
  3. Inspect the emitted DOT source.
  4. [FILL IN, optional: run adk web with 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:tool

How often has this issue occurred?:

  • Always (100%) for any node/tool name containing :

Activity

  1. kavishkartha05 commented on Oct 15, 2026

    @kavishkartha05
    Author

    I'd like to take this. I've reproduced it end to end with adk web (a function tool named srv:tool): the node is declared as "srv:tool" but the edge is emitted as colon_agent -> srv:tool, which DOT reads as node srv with port tool.

    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.

  2. added a commit that references this issue on Oct 15, 2026
    cb82fb7
  3. surajksharma07 commented on Oct 15, 2026

    @surajksharma07
    Collaborator

    @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.

  4. kavishkartha05 commented on Oct 15, 2026

    @kavishkartha05
    Author

    @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 the format="dot" source that:

    • every edge endpoint is a declared node,
    • no phantom "b" endpoint appears, and
    • the node is declared under its real name.

    All three fail on upstream/main and 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 the graphviz>=0.20.2,<1 pin, 0.20.2 and 0.21, and they all pass.

    cli/agent_graph.py has the same issue. draw_edge calls Digraph.edge() directly, so a tool named "srv:tool" yields root -> 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 in get_agent_graph and 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_edge hook: I looked at appending formatted edge lines to dot.body instead, but that means using quoting.attr_list and 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 the dot.body approach if you prefer to avoid the private hook.

  5. surajksharma07 commented on Oct 15, 2026

    @surajksharma07
    Collaborator

    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.

  6. kavishkartha05 commented on Oct 15, 2026

    @kavishkartha05
    Author

    @surajksharma07 Thanks, folded in. I pushed a follow-up commit (b10c02451) that applies the same fix to agent_graph.py. get_agent_graph and the cluster subgraphs for Sequential/Loop/Parallel agents now use the same subclass, so a tool named srv:tool draws root -> "srv:tool" instead of attaching to a phantom srv node. I added tests for a colon-named tool on a plain agent and inside each cluster type, and they fail on upstream/main. Output is byte-identical to upstream/main for graphs without a colon (22 agent_graph scenarios 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.

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

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions