feat: Agent graph support#181
Conversation
…b.com:launchdarkly/java-core into mmccarthy/AIC-2837/java-ai-sdk-agent-graph
…l-arg logs to debug
…b.com:launchdarkly/java-core into mmccarthy/AIC-2837/java-ai-sdk-agent-graph
…hy/AIC-2837/java-ai-sdk-agent-graph
| if (visited.add(root.getKey())) { | ||
| Object result = fn.apply(root, ctx); | ||
| ctx.put(root.getKey(), result); | ||
| } |
There was a problem hiding this comment.
Reverse traverse skips cycle nodes
Medium Severity
When a validated graph has no terminal nodes (for example a directed cycle), reverseTraverse seeds an empty queue and only runs the final root block. Non-root nodes on the cycle never receive the visitor, despite the API stating each node is visited exactly once.
Reviewed by Cursor Bugbot for commit 96a810e. Configure here.
There was a problem hiding this comment.
See spec AIGRAPH 1.4
The spec says reverse_traverse starts from terminal nodes — no terminals means no starting point, so a no-op is correct.
tanderson-ld
left a comment
There was a problem hiding this comment.
I am reviewing from the java perspective. I can't really review from the AI product perspective.
…hy/AIC-2837/java-ai-sdk-agent-graph
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ 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 23f1c50. Configure here.
tanderson-ld
left a comment
There was a problem hiding this comment.
Approving for the java aspects, I am not in the loop on the AI SDK product / project requirements. This seems reasonable to me, but perhaps I am missing something obvious to someone more involved.


Summary
Adds agent graph support — flag evaluation, graph validation, BFS traversal, graph-level tracking, and resumption tokens. Callers fetch a graph definition via
agentGraph(graphKey, context, variables), inspect or traverse the node topology, and track graph-level metrics (invocation success/failure, duration, tokens, path) plus edge-level events (redirect, handoff) throughAIGraphTracker.New types
GraphEdge— immutable edge holding targetkeyand optionalhandoffmetadata map (unmodifiable defensive copy).AgentGraphNode— wraps a node key, its resolvedAIAgentConfig, and an unmodifiable outgoingGraphEdgelist (defensive copy).isTerminal()returns true when edges are empty.AgentGraphFlagValue(package-private) — parses the graph flag JSON protocol:root,edgesadjacency map, and_ldMeta(enabled, variationKey, version). Defensively handles malformed input without throwing.AgentGraphDefinition— the resolved graph:traverseis BFS root-to-leaves;reverseTraverseis BFS terminals-to-root (root always processed last). Both are cycle-safe — each node visited at most once. Visitor results stored in the context map under the node's key.AIGraphTracker— graph-level tracking:Uses
AtomicReference.compareAndSet(null, value)for at-most-once. Non-finite durations (NaN, infinity) are rejected without consuming the at-most-once slot. Reconstruction from a resumption token is handled viaLDAIClient.createGraphTracker(token, context), which pipes the configured logger from the client (the staticfromResumptionTokenmethod is package-private).AIGraphMetricSummary— immutable snapshot of graph tracker state (success, durationMs, tokens, path, resumptionToken). All nullable except resumptionToken.Client methods
agentGraphvalidates thatgraphKeyis non-null and non-blank (throwsNullPointerException/IllegalArgumentException), then evaluates the graph flag, validates (enabled -> root present -> all nodes reachable from root -> all child configs enabled), fetches each node'sAIAgentConfigpassinggraphKeyfor tracker correlation. Returns disabled definition on any validation failure. Emits$ld:ai:usage:agent-graphusage event.createGraphTrackerreconstructs anAIGraphTrackerfrom a resumption token, preserving the original run identity while using the client's configured logger.Logging
Default logger resolution (
Loggers.defaultLogger()) is centralized ininternal/Loggers.javaand used only from theLDAIClientImplsingle-argument constructor as an entry-point fallback. All other code paths receive the logger from the top via constructor injection, consistent with the convention in other LaunchDarkly SDKs (.NET, JS).Other changes
ResumptionTokensextended withencodeGraph/decodeGraphfor graph-specific tokens (fields:runId,graphKey,variationKey,version). Madepublicfor access fromAIGraphTracker. Decode enforces non-blankrunIdandgraphKey.agentConfigs()reordered to emit usage count before fetching configs.graphKeyparameter so child node trackers include graph identity in their track data.Test plan
./gradlew :lib:sdk:server-ai:testpassesAIGraphTrackerTest— invocation success/failure + shared guard, duration (including non-finite rejection), total tokens, path, redirect/handoff multi-fire, base data correctness, variationKey omission, getSummary, resumption token round-trip, concurrency (20-thread contention for invocation and duration)AgentGraphDefinitionTest— buildNodes, collectAllKeys, traverse/reverseTraverse (including cycles, single-node, diamond graphs), rootNode/getNode/getChildNodes/getParentNodes/terminalNodes, disabled graph behavior, createTrackerLDAIClientImplTest— agentGraph usage event, enabled/disabled graph, unreachable node validation, non-enabled child config validation, graphKey threading to child trackers, createGraphTracker delegation, null/blank graphKey rejectionAgentGraphFlagValueTest— parse root/edges/meta, missing fields, disabled flag, malformed input, handoff metadata, edge with missing key skippedResumptionTokensTest— graph token encode/decode round-trips, blank field rejection