Skip to content

Commit 6583620

Browse files
jsonbaileyclaude
andcommitted
feat: add ManagedGraphResult, GraphMetricSummary, and ManagedAgentGraph (AIC-2388)
Introduces GraphMetrics (runner-level: success, path, durationMs?, usage?, nodeMetrics — no handoffs), AgentGraphRunnerResult (content, metrics, raw?), GraphMetricSummary (extends GraphMetrics with resumptionToken?), and ManagedGraphResult (content, metrics, raw?, evaluations). Creates ManagedAgentGraph class with run(runner) that builds GraphMetricSummary from runner result plus graphTracker.resumptionToken. Exports new types and class from public API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3031d7d commit 6583620

4 files changed

Lines changed: 304 additions & 1 deletion

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { ManagedAgentGraph } from '../src/api/graph/ManagedAgentGraph';
2+
import { AgentGraphDefinition } from '../src/api/graph/AgentGraphDefinition';
3+
import { AgentGraphRunnerResult } from '../src/api/graph/types';
4+
import { LDGraphTracker } from '../src/api/graph/LDGraphTracker';
5+
6+
describe('ManagedAgentGraph', () => {
7+
const mockTracker: jest.Mocked<LDGraphTracker> = {
8+
getTrackData: jest.fn().mockReturnValue({ runId: 'r1', graphKey: 'g1', version: 1 }),
9+
getSummary: jest.fn().mockReturnValue({}),
10+
resumptionToken: 'graph-resumption-token',
11+
trackInvocationSuccess: jest.fn(),
12+
trackInvocationFailure: jest.fn(),
13+
trackDuration: jest.fn(),
14+
trackTotalTokens: jest.fn(),
15+
trackPath: jest.fn(),
16+
trackRedirect: jest.fn(),
17+
trackHandoffSuccess: jest.fn(),
18+
trackHandoffFailure: jest.fn(),
19+
};
20+
21+
let mockGraphDefinition: jest.Mocked<AgentGraphDefinition>;
22+
23+
beforeEach(() => {
24+
jest.clearAllMocks();
25+
mockGraphDefinition = {
26+
enabled: true,
27+
createTracker: jest.fn().mockReturnValue(mockTracker),
28+
getConfig: jest.fn(),
29+
getNode: jest.fn(),
30+
getChildNodes: jest.fn(),
31+
getParentNodes: jest.fn(),
32+
terminalNodes: jest.fn(),
33+
rootNode: jest.fn(),
34+
traverse: jest.fn(),
35+
reverseTraverse: jest.fn(),
36+
} as any;
37+
});
38+
39+
it('run() builds ManagedGraphResult from runner result', async () => {
40+
const runnerResult: AgentGraphRunnerResult = {
41+
content: 'Graph output',
42+
metrics: {
43+
success: true,
44+
path: ['node-a', 'node-b'],
45+
durationMs: 1500,
46+
usage: { total: 100, input: 50, output: 50 },
47+
nodeMetrics: {
48+
'node-a': { success: true },
49+
'node-b': { success: true },
50+
},
51+
},
52+
};
53+
54+
const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
55+
const result = await managedGraph.run(async (_def, _tracker) => runnerResult);
56+
57+
expect(result.content).toBe('Graph output');
58+
expect(result.metrics.success).toBe(true);
59+
expect(result.metrics.path).toEqual(['node-a', 'node-b']);
60+
expect(result.metrics.durationMs).toBe(1500);
61+
expect(result.metrics.resumptionToken).toBe('graph-resumption-token');
62+
expect(result.metrics.nodeMetrics).toEqual({
63+
'node-a': { success: true },
64+
'node-b': { success: true },
65+
});
66+
});
67+
68+
it('run() passes graphDefinition and graphTracker to runner', async () => {
69+
const runnerFn = jest.fn().mockResolvedValue({
70+
content: 'output',
71+
metrics: {
72+
success: true,
73+
path: [],
74+
nodeMetrics: {},
75+
},
76+
});
77+
78+
const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
79+
await managedGraph.run(runnerFn);
80+
81+
expect(runnerFn).toHaveBeenCalledWith(mockGraphDefinition, mockTracker);
82+
});
83+
84+
it('run() creates a tracker via graphDefinition.createTracker()', async () => {
85+
const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
86+
await managedGraph.run(async () => ({
87+
content: '',
88+
metrics: { success: true, path: [], nodeMetrics: {} },
89+
}));
90+
91+
expect(mockGraphDefinition.createTracker).toHaveBeenCalled();
92+
});
93+
94+
it('resolves to empty evaluations by default', async () => {
95+
const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
96+
const result = await managedGraph.run(async () => ({
97+
content: '',
98+
metrics: { success: true, path: [], nodeMetrics: {} },
99+
}));
100+
101+
const evaluations = await result.evaluations;
102+
expect(evaluations).toEqual([]);
103+
});
104+
105+
it('getGraphDefinition() returns the graph definition', () => {
106+
const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
107+
expect(managedGraph.getGraphDefinition()).toBe(mockGraphDefinition);
108+
});
109+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { LDLogger } from '@launchdarkly/js-server-sdk-common';
2+
3+
import { LDJudgeResult } from '../judge/types';
4+
import { LDGraphTracker } from './LDGraphTracker';
5+
import { AgentGraphDefinition } from './AgentGraphDefinition';
6+
import { AgentGraphRunnerResult, GraphMetricSummary, ManagedGraphResult } from './types';
7+
8+
/**
9+
* ManagedAgentGraph wraps an AgentGraphDefinition and provides a managed run()
10+
* method that returns ManagedGraphResult with async judge evaluations.
11+
*
12+
* The runner function is responsible for executing the graph and returning
13+
* an AgentGraphRunnerResult. ManagedAgentGraph builds the managed result from
14+
* the runner result, including GraphMetricSummary with the graphTracker's
15+
* resumptionToken.
16+
*/
17+
export class ManagedAgentGraph {
18+
constructor(
19+
private readonly _graphDefinition: AgentGraphDefinition,
20+
private readonly _logger?: LDLogger,
21+
) {}
22+
23+
/**
24+
* Runs the agent graph using the provided runner function and returns a ManagedGraphResult.
25+
*
26+
* The runner function receives the graph tracker and AgentGraphDefinition,
27+
* executes the graph, and returns an AgentGraphRunnerResult.
28+
*
29+
* run() returns before ManagedGraphResult.evaluations resolves.
30+
*
31+
* @param runner Async function that executes the graph and returns AgentGraphRunnerResult.
32+
* @returns ManagedGraphResult with GraphMetricSummary and evaluations promise.
33+
*/
34+
async run(
35+
runner: (
36+
graphDefinition: AgentGraphDefinition,
37+
graphTracker: LDGraphTracker,
38+
) => Promise<AgentGraphRunnerResult>,
39+
): Promise<ManagedGraphResult> {
40+
const graphTracker = this._graphDefinition.createTracker();
41+
42+
const runnerResult = await runner(this._graphDefinition, graphTracker);
43+
44+
const metrics: GraphMetricSummary = {
45+
success: runnerResult.metrics.success,
46+
path: runnerResult.metrics.path,
47+
durationMs: runnerResult.metrics.durationMs,
48+
usage: runnerResult.metrics.usage,
49+
nodeMetrics: runnerResult.metrics.nodeMetrics,
50+
resumptionToken: graphTracker.resumptionToken,
51+
};
52+
53+
// No graph-level evaluator by default
54+
const evaluations: Promise<LDJudgeResult[]> = Promise.resolve([]);
55+
56+
return {
57+
content: runnerResult.content,
58+
metrics,
59+
raw: runnerResult.raw,
60+
evaluations,
61+
};
62+
}
63+
64+
/**
65+
* Returns the underlying AgentGraphDefinition.
66+
*/
67+
getGraphDefinition(): AgentGraphDefinition {
68+
return this._graphDefinition;
69+
}
70+
}

packages/sdk/server-ai/src/api/graph/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './types';
22
export * from './LDGraphTracker';
33
export * from './AgentGraphNode';
44
export * from './AgentGraphDefinition';
5+
export * from './ManagedAgentGraph';

packages/sdk/server-ai/src/api/graph/types.ts

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { LDTokenUsage } from '../metrics';
1+
import { LDJudgeResult } from '../judge/types';
2+
import { LDAIMetrics, LDTokenUsage } from '../metrics';
23

34
/**
45
* Represents a directed edge in an agent graph, connecting a source node to a target node.
@@ -62,6 +63,128 @@ export interface LDGraphMetricSummary {
6263
path?: string[];
6364
}
6465

66+
// ============================================================================
67+
// Runner-Level Graph Types
68+
// ============================================================================
69+
70+
/**
71+
* Graph-level metrics for a completed graph run, as returned by a graph runner.
72+
* Does NOT include handoffs or evaluations — those are managed-layer concerns.
73+
*/
74+
export interface GraphMetrics {
75+
/**
76+
* Whether the graph invocation succeeded.
77+
*/
78+
success: boolean;
79+
80+
/**
81+
* Execution path through the graph as an ordered array of config keys.
82+
*/
83+
path: string[];
84+
85+
/**
86+
* Total graph execution duration in milliseconds, if tracked.
87+
*/
88+
durationMs?: number;
89+
90+
/**
91+
* Aggregate token usage across the entire graph invocation, if available.
92+
*/
93+
usage?: LDTokenUsage;
94+
95+
/**
96+
* Per-node metrics keyed by agent config key.
97+
*/
98+
nodeMetrics: Record<string, LDAIMetrics>;
99+
}
100+
101+
/**
102+
* The result returned by a graph runner invocation (provider-level).
103+
* Does NOT include evaluations or handoffs.
104+
*/
105+
export interface AgentGraphRunnerResult {
106+
/**
107+
* The text content of the graph's final response.
108+
*/
109+
content: string;
110+
111+
/**
112+
* Graph-level metrics for this invocation.
113+
*/
114+
metrics: GraphMetrics;
115+
116+
/**
117+
* The raw response object from the provider, if available.
118+
*/
119+
raw?: unknown;
120+
}
121+
122+
// ============================================================================
123+
// Managed-Layer Graph Types
124+
// ============================================================================
125+
126+
/**
127+
* Graph metric summary returned in ManagedGraphResult.
128+
* Includes per-node metrics and a resumption token.
129+
*/
130+
export interface GraphMetricSummary {
131+
/**
132+
* Whether the graph invocation succeeded.
133+
*/
134+
success: boolean;
135+
136+
/**
137+
* Execution path through the graph as an ordered array of config keys.
138+
*/
139+
path: string[];
140+
141+
/**
142+
* Total graph execution duration in milliseconds, if tracked.
143+
*/
144+
durationMs?: number;
145+
146+
/**
147+
* Aggregate token usage across the entire graph invocation, if available.
148+
*/
149+
usage?: LDTokenUsage;
150+
151+
/**
152+
* Per-node metrics keyed by agent config key.
153+
*/
154+
nodeMetrics: Record<string, LDAIMetrics>;
155+
156+
/**
157+
* Resumption token for deferred feedback association.
158+
*/
159+
resumptionToken?: string;
160+
}
161+
162+
/**
163+
* The result returned by a managed graph invocation (ManagedAgentGraph.run()).
164+
*/
165+
export interface ManagedGraphResult {
166+
/**
167+
* The text content of the graph's final response.
168+
*/
169+
content: string;
170+
171+
/**
172+
* Summarized metrics for this graph invocation.
173+
*/
174+
metrics: GraphMetricSummary;
175+
176+
/**
177+
* The raw response object from the provider, if available.
178+
*/
179+
raw?: unknown;
180+
181+
/**
182+
* Promise that resolves to the judge evaluation results.
183+
* Awaiting this promise guarantees both evaluation and tracking are complete.
184+
*/
185+
evaluations: Promise<LDJudgeResult[]>;
186+
}
187+
65188
/**
66189
* Tracking metadata returned by {@link LDGraphTracker.getTrackData}.
67190
*/

0 commit comments

Comments
 (0)