Skip to content

fix(cli): keep graph edges attached to node names with a colon - #7546

Open
kavishkartha05 wants to merge 6 commits into
google:mainfrom
kavishkartha05:fix/graph-viz-colon-node-names
Open

kavishkartha05 wants to merge 6 commits into
google:mainfrom
kavishkartha05:fix/graph-viz-colon-node-names

Conversation

@kavishkartha05

@kavishkartha05 kavishkartha05 commented Oct 15, 2026 •

Copy link
Copy Markdown

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

An issue exists (#7543), so the description below summarizes it.

Problem:
plot_workflow_graph (used by /dev/apps/{app}/build_graph_image) and
get_agent_graph (cli/agent_graph.py) declare a node named srv:tool as
"srv:tool", but graphviz.Digraph.edge() splits an endpoint on : before
quoting it, so the edge is written as a -> srv:tool. DOT reads that as node
srv with port tool: the edge attaches to a phantom node srv and the real
node is drawn disconnected. Function tool names can legitimately contain :.
Agent and workflow-node names must be Python identifiers, so tool names are the
realistic way to trigger this.

Solution:
Quote edge endpoints through a small Digraph subclass
(_quote_edge = staticmethod(quoting.quote)), so the real name stays the DOT
id and is quoted in edge statements. It is used in both places that draw edges
from user-controlled names: plot_workflow_graph
(cli/utils/graph_visualization.py) and get_agent_graph
(cli/agent_graph.py), including the cluster subgraphs for Sequential, Loop
and Parallel agents, since tool edges inside a cluster are drawn on the
cluster's own Digraph. Output is byte-identical for graphs with no : in any
name, including __START__ and __END__ (checked on 22 agent_graph outputs
and 12 plot_workflow_graph outputs against upstream/main).

Alternatives considered:

  • Sanitized internal ids with the real name as the label (e.g.
    __node_0__): rejected. adk-web reads each node's SVG <title> (the DOT id)
    as the node name for click and expand handling
    (src/app/utils/svg-interaction.utils.ts,
    agent-structure-graph-dialog.ts in google/adk-web), so renaming ids would
    break selecting those nodes.
  • Pre-quoting the endpoint passed to edge(): rejected. The library splits
    on : before quoting, so "srv:tool" becomes "\"srv":"tool\"".
  • Writing quoted edge lines to dot.body with the public quoting helpers:
    not simpler. It needs quoting.attr_list, a manual trailing newline, and
    replaces three edge() call sites.

Caveat: the subclass overrides the private Digraph._quote_edge hook. I
verified it on graphviz 0.20.2 (the lower bound of graphviz>=0.20.2,<1) and
0.21. A unit test fails with an explicit message if the hook is renamed or stops
working. No port syntax is used anywhere in these graphs.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

New tests in tests/unittests/cli/utils/test_graph_visualization.py (colon
names as edge target, source and terminal node, the __START__/__END__ test,
and a guard test that fails if graphviz's _quote_edge hook goes away) and
tests/unittests/cli/test_agent_graph.py (4 cases: a colon-named tool on a
plain agent, and inside Sequential, Loop and Parallel clusters). All fail on
upstream/main and pass here. A partial fix that skips the cluster
constructors fails the cluster cases.

pytest on the three graph test files: 41 passed on graphviz 0.20.2 and on
0.21 (both bounds of graphviz>=0.20.2,<1)
pytest tests/unittests/cli -n auto: 1489 passed, 5 skipped, 1 xfailed
mypy . as in CI (baseline vs PR): 535 errors on both, 0 new
pre-commit run --files <changed files>: all hooks pass (real addlicense
run, not skipped)

The new tests use format="dot", so no graphviz binary is needed.

Manual End-to-End (E2E) Tests:

I ran adk web on an agent with a function tool named srv:tool and captured
the response of GET /dev/apps/colon_agent/build_graph_image:

BEFORE (upstream/main)
  "srv:tool" [label=<... srv:tool> ... style="rounded,filled,dashed" tooltip=Tool]
  colon_agent -> srv:tool [color="#64748B" style=dashed]

AFTER (this PR)
  "srv:tool" [label=<... srv:tool> ... style="rounded,filled,dashed" tooltip=Tool]
  colon_agent -> "srv:tool" [color="#64748B" style=dashed]

I also opened the agent structure graph in the adk web UI on both builds:

  • Before: the edge from colon_agent points to a phantom srv node, and
    the real srv:tool node is drawn disconnected.
  • After: the edge connects colon_agent to srv:tool.
  • The node's SVG <title> is still srv:tool (checked in DevTools), which is
    what adk-web reads as the node name. Tool nodes aren't clickable in this
    view, so I couldn't exercise click/drill-down directly; it is unaffected by
    design, since only edge endpoints are quoted differently.
  • The agent_graph.py change is covered by the unit tests above and a
    standalone repro of its DOT output; I did not screenshot that view
    separately.

Before (upstream/main):

Screenshot 2026-10-14 at 6 52 25 PM

After (this PR):

Screenshot 2026-10-14 at 6 52 39 PM

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Screenshots are in the manual E2E section above.

🤖 Developed with Claude Code

@google-cla

google-cla Bot commented Oct 15, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a custom _Digraph subclass that overrides the private _quote_edge method of graphviz.Digraph to prevent colon-named nodes from being incorrectly split into ports, along with comprehensive unit tests to verify this behavior. Feedback suggests avoiding the subclassing of private methods by instead appending formatted edge strings directly to the public dot.body attribute, which provides a more robust and maintainable solution.

Comment on lines +29 to +42
class _Digraph(graphviz.Digraph): # type: ignore[misc] # graphviz is untyped
"""A Digraph whose edge endpoints are quoted as plain node ids.

Digraph.edge() splits an endpoint on ":" before quoting it, treating the
rest as a port, so an edge to a node named "srv:tool" is written as
`a -> srv:tool` and attaches to a phantom node "srv". Quoting the whole
endpoint keeps the real name as the DOT id, which adk-web reads back from
the SVG title as the node name. No port syntax is used in these graphs.

This overrides the private `_quote_edge` hook. Verified on graphviz 0.20.2
and 0.21; a unit test fails if the hook is renamed or stops working.
"""

_quote_edge = staticmethod(quoting.quote)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Overriding the private _quote_edge method of graphviz.Digraph introduces a dependency on the internal implementation details of the graphviz library. While you have mitigated this risk with a unit test (test_digraph_edge_hook_quotes_colon_endpoints), private methods (those prefixed with an underscore) are not part of the public API and can be renamed, removed, or have their signatures changed in future minor or patch releases of graphviz without warning.\n\nAn alternative, more robust approach that avoids subclassing and overriding private methods is to bypass dot.edge() and append the formatted edge strings directly to dot.body (which is a public, documented list of lines for the graph body).\n\nFor example, you could define a helper function:\n\npython\ndef _add_edge(\n dot: graphviz.Digraph,\n from_node: str,\n to_node: str,\n label: str | None = None,\n style: str | None = None,\n color: str | None = None,\n) -> None:\n from_quoted = quoting.quote(from_node)\n to_quoted = quoting.quote(to_node)\n attrs = []\n if label:\n attrs.append(f\"label={quoting.quote(label)}\")\n if style:\n attrs.append(f\"style={quoting.quote(style)}\")\n if color:\n attrs.append(f\"color={quoting.quote(color)}\")\n \n attr_str = f\" [{', '.join(attrs)}]\" if attrs else \"\"\n dot.body.append(f'\\t{from_quoted} -> {to_quoted}{attr_str}')\n\n\nAnd then use standard graphviz.Digraph and replace dot.edge(...) calls with _add_edge(dot, ...). This completely eliminates the need for subclassing, the type: ignore[misc] comment, and the fragile private method override.

@kavishkartha05 kavishkartha05 Oct 15, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I considered the dot.body route. It's public API, but it needs quoting.attr_list and an additional manual trailing newline and replaces all three edge() call sites, so it changes output paths for graphs without colon names. The subclass keeps those byte-identical (checked on 12 scenarios), and test_digraph_edge_hook_quotes_colon_endpoints fails with an explicit message if graphviz renames or changes the hook (verified on 0.20.2 and 0.21). Happy to switch to the dot.body approach if maintainers prefer avoiding the private hook.

graphviz.Digraph.edge() splits an endpoint on ":" before quoting it, so
an edge to a node or tool named "srv:tool" was written as
`a -> srv:tool`. DOT reads that as node "srv" with port "tool", so the
edge attached to a phantom node while the real node was drawn
disconnected.

Quote edge endpoints through a small Digraph subclass so the real name
stays the DOT id. adk-web reads the SVG title as the node name, so
renaming ids would break click handling. Output is unchanged for graphs
without ":" in any name, including __START__ and __END__.

Add the first tests for plot_workflow_graph, using format="dot" so no
graphviz binary is needed.

Fixes google#7543
kavishkartha05 and others added 5 commits October 15, 2026 00:23
Add tests for a workflow node named "b:x" in each edge position handled
by plot_workflow_graph: as an edge target (a -> b:x), as an edge source
(b:x -> c) and as a terminal node linked to END (b:x -> __END__).

Each test asserts on the DOT source that every edge endpoint is a
declared node, that no phantom "b" endpoint appears, and that the node
is declared under its real name.
get_agent_graph draws every edge through Digraph.edge(), which splits an
endpoint on ":" before quoting it. An edge to a tool named "srv:tool"
was written as `root -> srv:tool`, which DOT reads as node "srv" with
port "tool", so the edge attached to a phantom node and the real tool
node was drawn disconnected. This is the same bug fixed for
plot_workflow_graph in this PR.

Build the top-level graph and the cluster subgraphs for Sequential,
Loop and Parallel agents from the _Digraph subclass added earlier, so
edge endpoints are quoted whole and the real name stays the DOT id.
Tool edges inside a cluster are drawn on the cluster's own Digraph, so
it needs the subclass too. Output is unchanged for graphs without a
colon in any name.

Add tests for a colon-named tool on a plain agent and inside each
cluster type.
@kavishkartha05

Copy link
Copy Markdown
Author

@wyf7107 would greatly appreciate a PR review. You may want to reference #7543 discussion. Thanks :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants