Skip to content

fix!: Make AgentGraph traversal topological - #199

Open
mattrmc1 wants to merge 4 commits into
mainfrom
mmccarthy/AIC-3044/agent-graph-traversal
Open

fix!: Make AgentGraph traversal topological#199
mattrmc1 wants to merge 4 commits into
mainfrom
mmccarthy/AIC-3044/agent-graph-traversal

Conversation

@mattrmc1

@mattrmc1 mattrmc1 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Makes AgentGraphDefinition traversal topological and gives each visitor a dependency-scoped
execution context
, aligning the traversal behavior across the LaunchDarkly AI SDKs.

Previously traverse / reverseTraverse were plain BFS over a single shared, accumulating context
map. That had two problems:

  1. Ordering: where branches of unequal length converge, the convergence node ran on first
    discovery — before all of its predecessors had run.
  2. Context leakage & mutation: every callback saw the results of all previously-visited nodes
    (including unrelated parallel branches), and results were written back into the caller's context
    map.

What changed

lib/sdk/server-aiAgentGraphDefinition.java only (no public API/signature changes):

  • traverse now visits a node only after all of its reachable predecessors have been visited
    (Kahn over in-degree; the root is always released first). On cycles, the unvisited node with the
    lowest remaining in-degree is chosen next, ties broken by discovery order.
  • reverseTraverse now visits a node only after all of its reachable descendants have been
    visited, so the root is visited last (Kahn over out-degree, root excluded from cycle-break
    selection). Pure cycles now visit every node instead of being a no-op.
  • Scoped context: each callback receives a fresh map = the initial context plus only that node's
    true dependency results (transitive ancestors forward / descendants reverse). Dependencies are
    accumulated before the node is marked visited in both directions, so a self-loop node never
    includes itself; unrelated branches are excluded and the caller's map is never mutated. Cross-node
    data flows only through callback return values.
  • Determinism: discovery order is the graph's BFS encounter order (root first, then declared edge
    order), used only for tie-breaks.
void traverse(BiFunction<AgentGraphNode, Map<String, Object>, Object> fn, Map<String, Object> ctx);

Behavior change (breaking)

Titled fix! because visit order and callback-context contents change for graphs with convergent
paths, cycles, or parallel branches. Simple linear graphs are unaffected. Cross-SDK parity with
JS/Python/.NET/Java.

Tests

  • A data-driven test over the canonical cross-SDK vectors G1G6 (+G2b) asserts exact visit
    order and exact context keys in both directions.
  • Convergence runs the shared node last; pure cycle visits all nodes (root last); caller context not
    mutated; unrelated branches excluded; a dedicated self-loop test confirms a node is not in its own
    context; deterministic across runs. Redundant per-vector order-only tests were dropped in favor of
    the data-driven vector test.

Notes

  • No manual CHANGELOG.md edit — release-please generates it from the conventional-commit title.
  • Graph tracking / wire parsing unchanged; this PR is scoped to traversal + context.
  • Minor follow-up (not required): the class-level Javadoc still says traversal is "BFS-based" — worth
    updating to "topological / dependency-order" so the docs match the new behavior.

Note

Medium Risk
Same public method signatures but visit order and callback context contents change for convergent graphs, cycles, and parallel branches—callers that relied on BFS order or writing results into the shared map will break.

Overview
Breaking behavior change for AgentGraphDefinition.traverse and reverseTraverse: they no longer use shared BFS over one accumulating context map. Forward traversal now runs each node only after all reachable predecessors (Kahn-style, cycle-safe with discovery-order tie-breaks); reverse traversal runs descendants-first with the root last, including visiting all nodes on pure cycles.

Each visitor callback gets a fresh map (initial ctx plus only that node’s true dependency results—ancestors forward, descendants reverse). The caller’s ctx is not mutated with node outputs; unrelated parallel branches are excluded, and self-loops do not put a node in its own context.

Javadoc is updated to describe topological, deterministic traversal. Tests add canonical cross-SDK vectors (G1–G6), exact visit order and context-key assertions, determinism, and coverage for convergence, cycles, and caller-context immutability.

Reviewed by Cursor Bugbot for commit da3503b. Bugbot is set up for automated code reviews on this repo. Configure here.

@mattrmc1
mattrmc1 marked this pull request as ready for review July 29, 2026 21:54
@mattrmc1
mattrmc1 requested a review from a team as a code owner July 29, 2026 21:54

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit da3503b. Configure here.

for (GraphEdge e : node.getEdges()) {
if (reachable.contains(e.getKey())) {
d++;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reverse outdeg counts root edges

High Severity

reverseTraverse counts outgoing edges to the root in outdeg, but the Kahn loop never visits the root, so those edges are never satisfied. Nodes that only point at the root (or whose remaining degree is inflated by a back-edge to root) stay non-ready and get picked via cycle-break too early, before real descendants, so visit order and scoped dependency context are wrong for cycles that include the root.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit da3503b. Configure here.

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.

Do you have any performance requirements wrt throughput or memory allocation delays? May be worth a quick benchmark to see if your expected cases are well behaved.

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.

2 participants