Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions packages/sdk/server-ai/__tests__/ManagedAgentGraph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { AgentGraphDefinition } from '../src/api/graph/AgentGraphDefinition';
import { LDGraphTracker } from '../src/api/graph/LDGraphTracker';
import { ManagedAgentGraph } from '../src/api/graph/ManagedAgentGraph';
import { AgentGraphRunnerResult } from '../src/api/graph/types';

describe('ManagedAgentGraph', () => {
const mockTracker: jest.Mocked<LDGraphTracker> = {
getTrackData: jest.fn().mockReturnValue({ runId: 'r1', graphKey: 'g1', version: 1 }),
getSummary: jest.fn().mockReturnValue({}),
resumptionToken: 'graph-resumption-token',
trackInvocationSuccess: jest.fn(),
trackInvocationFailure: jest.fn(),
trackDuration: jest.fn(),
trackTotalTokens: jest.fn(),
trackPath: jest.fn(),
trackRedirect: jest.fn(),
trackHandoffSuccess: jest.fn(),
trackHandoffFailure: jest.fn(),
};

let mockGraphDefinition: jest.Mocked<AgentGraphDefinition>;

beforeEach(() => {
jest.clearAllMocks();
mockGraphDefinition = {
enabled: true,
createTracker: jest.fn().mockReturnValue(mockTracker),
getConfig: jest.fn(),
getNode: jest.fn(),
getChildNodes: jest.fn(),
getParentNodes: jest.fn(),
terminalNodes: jest.fn(),
rootNode: jest.fn(),
traverse: jest.fn(),
reverseTraverse: jest.fn(),
} as any;
});

it('builds ManagedGraphResult from runner result', async () => {
const runnerResult: AgentGraphRunnerResult = {
content: 'Graph output',
metrics: {
success: true,
path: ['node-a', 'node-b'],
durationMs: 1500,
usage: { total: 100, input: 50, output: 50 },
nodeMetrics: {
'node-a': { success: true },
'node-b': { success: true },
},
},
};

const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
const result = await managedGraph.run(async (_def, _tracker) => runnerResult);

expect(result.content).toBe('Graph output');
expect(result.metrics.success).toBe(true);
expect(result.metrics.path).toEqual(['node-a', 'node-b']);
expect(result.metrics.durationMs).toBe(1500);
expect(result.metrics.resumptionToken).toBe('graph-resumption-token');
expect(result.metrics.nodeMetrics).toEqual({
'node-a': { success: true },
'node-b': { success: true },
});
});

it('passes graphDefinition and graphTracker to runner', async () => {
const runnerFn = jest.fn().mockResolvedValue({
content: 'output',
metrics: {
success: true,
path: [],
nodeMetrics: {},
},
});

const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
await managedGraph.run(runnerFn);

expect(runnerFn).toHaveBeenCalledWith(mockGraphDefinition, mockTracker);
});

it('creates a tracker via graphDefinition.createTracker()', async () => {
const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
await managedGraph.run(async () => ({
content: '',
metrics: { success: true, path: [], nodeMetrics: {} },
}));

expect(mockGraphDefinition.createTracker).toHaveBeenCalled();
});

it('resolves to empty evaluations by default', async () => {
const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
const result = await managedGraph.run(async () => ({
content: '',
metrics: { success: true, path: [], nodeMetrics: {} },
}));

const evaluations = await result.evaluations;
expect(evaluations).toEqual([]);
});

it('returns the graph definition via getGraphDefinition', () => {
const managedGraph = new ManagedAgentGraph(mockGraphDefinition);
expect(managedGraph.getGraphDefinition()).toBe(mockGraphDefinition);
});
});
69 changes: 69 additions & 0 deletions packages/sdk/server-ai/src/api/graph/ManagedAgentGraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { LDLogger } from '@launchdarkly/js-server-sdk-common';

import { LDJudgeResult } from '../judge/types';
import { AgentGraphDefinition } from './AgentGraphDefinition';
import { LDGraphTracker } from './LDGraphTracker';
import { AgentGraphRunnerResult, GraphMetricSummary, ManagedGraphResult } from './types';

/**
* ManagedAgentGraph wraps an AgentGraphDefinition and provides a managed run()
* method that returns ManagedGraphResult with async judge evaluations.
*
* The runner function is responsible for executing the graph and returning
* an AgentGraphRunnerResult. ManagedAgentGraph builds the managed result from
* the runner result, including GraphMetricSummary with the graphTracker's
* resumptionToken.
*/
export class ManagedAgentGraph {
constructor(
private readonly _graphDefinition: AgentGraphDefinition,
private readonly _logger?: LDLogger,
) {}

/**
* Runs the agent graph using the provided runner function and returns a ManagedGraphResult.
*
* The runner function receives the graph tracker and AgentGraphDefinition,
* executes the graph, and returns an AgentGraphRunnerResult.
*
* run() returns before ManagedGraphResult.evaluations resolves.
*
* @param runner Async function that executes the graph and returns AgentGraphRunnerResult.
* @returns ManagedGraphResult with GraphMetricSummary and evaluations promise.
*/
async run(
runner: (
graphDefinition: AgentGraphDefinition,
graphTracker: LDGraphTracker,
) => Promise<AgentGraphRunnerResult>,
): Promise<ManagedGraphResult> {
const graphTracker = this._graphDefinition.createTracker();

const runnerResult = await runner(this._graphDefinition, graphTracker);

const metrics: GraphMetricSummary = {
success: runnerResult.metrics.success,
path: runnerResult.metrics.path,
durationMs: runnerResult.metrics.durationMs,
usage: runnerResult.metrics.usage,
nodeMetrics: runnerResult.metrics.nodeMetrics,
resumptionToken: graphTracker.resumptionToken,
};

const evaluations: Promise<LDJudgeResult[]> = Promise.resolve([]);

return {
content: runnerResult.content,
metrics,
raw: runnerResult.raw,
evaluations,
};
}

/**
* Returns the underlying AgentGraphDefinition.
*/
getGraphDefinition(): AgentGraphDefinition {
return this._graphDefinition;
}
}
1 change: 1 addition & 0 deletions packages/sdk/server-ai/src/api/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './types';
export * from './LDGraphTracker';
export * from './AgentGraphNode';
export * from './AgentGraphDefinition';
export * from './ManagedAgentGraph';
67 changes: 67 additions & 0 deletions packages/sdk/server-ai/src/api/graph/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LDJudgeResult } from '../judge/types';
import { LDAIMetrics, LDTokenUsage } from '../metrics';

/**
Expand Down Expand Up @@ -114,6 +115,72 @@ export interface AgentGraphRunnerResult {
raw?: unknown;
}

// ============================================================================
// Managed-Layer Graph Types
// ============================================================================

/**
* Graph metric summary returned in ManagedGraphResult.
* Includes per-node metrics and a resumption token.
*/
export interface GraphMetricSummary {
/**
* Whether the graph invocation succeeded.
*/
success: boolean;

/**
* Execution path through the graph as an ordered array of config keys.
*/
path: string[];

/**
* Total graph execution duration in milliseconds, if tracked.
*/
durationMs?: number;

/**
* Aggregate token usage across the entire graph invocation, if available.
*/
usage?: LDTokenUsage;

/**
* Per-node metrics keyed by agent config key.
*/
nodeMetrics: Record<string, LDAIMetrics>;

/**
* Resumption token for deferred feedback association.
*/
resumptionToken?: string;
}

/**
* The result returned by a managed graph invocation (ManagedAgentGraph.run()).
*/
export interface ManagedGraphResult {
/**
* The text content of the graph's final response.
*/
content: string;

/**
* Summarized metrics for this graph invocation.
*/
metrics: GraphMetricSummary;

/**
* The raw response object from the provider, if available.
*/
raw?: unknown;

/**
* Promise that resolves to the judge evaluation results.
* Awaiting this promise guarantees both evaluation and tracking are complete.
*/
evaluations: Promise<LDJudgeResult[]>;
}

/**
* Tracking metadata returned by {@link LDGraphTracker.getTrackData}.
*/
Expand Down
Loading