Skip to content

feat: Agent graph support#181

Open
mattrmc1 wants to merge 37 commits into
mainfrom
mmccarthy/AIC-2837/java-ai-sdk-agent-graph
Open

feat: Agent graph support#181
mattrmc1 wants to merge 37 commits into
mainfrom
mmccarthy/AIC-2837/java-ai-sdk-agent-graph

Conversation

@mattrmc1

@mattrmc1 mattrmc1 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

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) through AIGraphTracker.

New types

GraphEdge — immutable edge holding target key and optional handoff metadata map (unmodifiable defensive copy).

AgentGraphNode — wraps a node key, its resolved AIAgentConfig, and an unmodifiable outgoing GraphEdge list (defensive copy). isTerminal() returns true when edges are empty.

AgentGraphFlagValue (package-private) — parses the graph flag JSON protocol: root, edges adjacency map, and _ldMeta (enabled, variationKey, version). Defensively handles malformed input without throwing.

AgentGraphDefinition — the resolved graph:

boolean isEnabled();
AgentGraphNode rootNode();
AgentGraphNode getNode(String nodeKey);
List<AgentGraphNode> getChildNodes(String nodeKey);
List<AgentGraphNode> getParentNodes(String nodeKey);
List<AgentGraphNode> terminalNodes();
AIGraphTracker createTracker();

void traverse(BiFunction<AgentGraphNode, Map<String, Object>, Object> fn, Map<String, Object> ctx);
void reverseTraverse(BiFunction<AgentGraphNode, Map<String, Object>, Object> fn, Map<String, Object> ctx);

traverse is BFS root-to-leaves; reverseTraverse is 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:

// At-most-once (invocation success/failure share one guard):
void trackInvocationSuccess();
void trackInvocationFailure();
void trackDuration(double durationMs);
void trackTotalTokens(TokenUsage tokens);
void trackPath(List<String> path);

// Multi-fire:
void trackRedirect(String sourceKey, String redirectedTarget);
void trackHandoffSuccess(String sourceKey, String targetKey);
void trackHandoffFailure(String sourceKey, String targetKey);

AIGraphMetricSummary getSummary();
String getResumptionToken();

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 via LDAIClient.createGraphTracker(token, context), which pipes the configured logger from the client (the static fromResumptionToken method is package-private).

AIGraphMetricSummary — immutable snapshot of graph tracker state (success, durationMs, tokens, path, resumptionToken). All nullable except resumptionToken.

Client methods

AgentGraphDefinition agentGraph(String graphKey, LDContext context, Map<String, Object> variables);
AgentGraphDefinition agentGraph(String graphKey, LDContext context);
AIGraphTracker createGraphTracker(String resumptionToken, LDContext context);

agentGraph validates that graphKey is non-null and non-blank (throws NullPointerException / IllegalArgumentException), then evaluates the graph flag, validates (enabled -> root present -> all nodes reachable from root -> all child configs enabled), fetches each node's AIAgentConfig passing graphKey for tracker correlation. Returns disabled definition on any validation failure. Emits $ld:ai:usage:agent-graph usage event.

createGraphTracker reconstructs an AIGraphTracker from a resumption token, preserving the original run identity while using the client's configured logger.

Logging

Default logger resolution (Loggers.defaultLogger()) is centralized in internal/Loggers.java and used only from the LDAIClientImpl single-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

  • ResumptionTokens extended with encodeGraph/decodeGraph for graph-specific tokens (fields: runId, graphKey, variationKey, version). Made public for access from AIGraphTracker. Decode enforces non-blank runId and graphKey.
  • agentConfigs() reordered to emit usage count before fetching configs.
  • Config evaluation methods gain graphKey parameter so child node trackers include graph identity in their track data.

Test plan

  • ./gradlew :lib:sdk:server-ai:test passes
  • AIGraphTrackerTest — 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, createTracker
  • LDAIClientImplTest — agentGraph usage event, enabled/disabled graph, unreachable node validation, non-enabled child config validation, graphKey threading to child trackers, createGraphTracker delegation, null/blank graphKey rejection
  • AgentGraphFlagValueTest — parse root/edges/meta, missing fields, disabled flag, malformed input, handoff metadata, edge with missing key skipped
  • ResumptionTokensTest — graph token encode/decode round-trips, blank field rejection

@mattrmc1 mattrmc1 marked this pull request as ready for review June 24, 2026 21:26
@mattrmc1 mattrmc1 requested a review from a team as a code owner June 24, 2026 21:26
Base automatically changed from mmccarthy/AIC-2664/ai-config-tracker-overhaul to main June 25, 2026 16:05
if (visited.add(root.getKey())) {
Object result = fn.apply(root, ctx);
ctx.put(root.getKey(), result);
}

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

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 96a810e. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@mattrmc1 mattrmc1 requested a review from jsonbailey June 29, 2026 16:45
@mattrmc1 mattrmc1 requested a review from tanderson-ld June 29, 2026 19:13

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

I am reviewing from the java perspective. I can't really review from the AI product perspective.

@mattrmc1 mattrmc1 requested a review from tanderson-ld July 1, 2026 17:55

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

There are 2 total unresolved issues (including 1 from previous review).

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 23f1c50. Configure here.

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

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.

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