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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.
❌ 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 96a810e. Configure here.
| public static AIGraphTracker fromResumptionToken( | ||
| String token, LDClientInterface client, LDContext context, LDLogger logger) { | ||
| ResumptionTokens.DecodedGraph d = ResumptionTokens.decodeGraph(token); | ||
| int version = Math.max(1, d.getVersion()); |
There was a problem hiding this comment.
Graph token version clamped
Medium Severity
fromResumptionToken applies Math.max(1, d.getVersion()) after decodeGraph, so a token that legitimately carries version 0 or a negative value is rewritten before the tracker is built. Resumption decode is supposed to pass version through unchanged; only fresh tracker creation may default missing flag metadata to 1.
Triggered by learned rule: Do not suggest version clamping in AI SDK resumption tokens
Reviewed by Cursor Bugbot for commit 96a810e. Configure here.
| logger.warn("Skipping trackDuration: duration already recorded on this graph tracker."); | ||
| return; | ||
| } | ||
| client.trackMetric(GRAPH_DURATION_TOTAL, context, baseData().build(), durationMs); |
There was a problem hiding this comment.
Non-finite duration not rejected
Medium Severity
trackDuration(double durationMs) records whatever double is passed and sends it to trackMetric without checking Double.isNaN or Double.isInfinite. Non-finite values can consume the at-most-once slot and emit invalid metric values.
Triggered by learned rule: AI SDK tracker: validate strings for blank and numbers for non-finite, not just null
Reviewed by Cursor Bugbot for commit 96a810e. Configure here.
| 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.


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).AgentGraphNode— wraps a node key, its resolvedAIAgentConfig, and outgoingGraphEdgelist.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. Empty token usage doesn't burn the slot. Version clamped to minimum 1 on resumption decode.AIGraphMetricSummary— immutable snapshot of graph tracker state (success, durationMs, tokens, path, resumptionToken). All nullable except resumptionToken.Client methods
agentGraphevaluates 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.Other changes
ResumptionTokensextended withencodeGraph/decodeGraphfor graph-specific tokens (fields:runId,graphKey,variationKey,version). Madepublicfor access fromAIGraphTracker.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, total tokens (including zero-usage skip), 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 delegationAgentGraphFlagValueTest— parse root/edges/meta, missing fields, disabled flag, malformed input, handoff metadata, edge with missing key skippedResumptionTokensTest— graph token encode/decode round-tripsNote
Medium Risk
New public API surface and metric/event contracts; graph validation can disable entire graphs on structural or child-config failures, and resumption tokens carry variation metadata that must stay server-side.
Overview
Adds agent graph support to the server-side AI SDK: callers resolve multi-agent workflows from a graph flag via
LDAIClient#agentGraph, walk the topology, and emit graph- and edge-level metrics.LDAIClientgainsagentGraph(...)andcreateGraphTracker(...).LDAIClientImplevaluates the graph flag, validates structure (enabled, non-empty root, all nodes reachable from root, every node’s agent config enabled), loads node configs without per-node$ld:ai:usage:agent-configevents, emits$ld:ai:usage:agent-graph, and threadsgraphKeyinto per-nodeLDAIConfigTrackerfactories.agentConfigsnow records the batch usage metric before fetching configs.New public types:
GraphEdge,AgentGraphNode,AgentGraphDefinition(BFStraverse/ reverse BFSreverseTraverse, cycle-safe),AIGraphTracker(at-most-once invocation/duration/tokens/path; multi-fire redirect/handoff;AIGraphMetricSummary), plus internalAgentGraphFlagValueparsing.ResumptionTokensis extended withencodeGraph/decodeGraph(and stricter trim checks on config tokens);AIGraphTrackeruses graph tokens for cross-request continuity.Broad unit test coverage is added for trackers, graph definitions, flag parsing, and client behavior.
Reviewed by Cursor Bugbot for commit 96a810e. Bugbot is set up for automated code reviews on this repo. Configure here.