From 262ea0d5cd108673f0c94617b01da34da98673ab Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Fri, 1 May 2026 09:07:01 -0500 Subject: [PATCH] feat: add ManagedGraphResult, GraphMetricSummary, and ManagedAgentGraph (AIC-2388) Adds GraphMetricSummary (extends GraphMetrics with resumptionToken?) and ManagedGraphResult (content, metrics, raw?, evaluations) to api/graph/types.ts. 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 --- .../__tests__/ManagedAgentGraph.test.ts | 109 ++++++++++++++++++ .../src/api/graph/ManagedAgentGraph.ts | 69 +++++++++++ packages/sdk/server-ai/src/api/graph/index.ts | 1 + packages/sdk/server-ai/src/api/graph/types.ts | 67 +++++++++++ 4 files changed, 246 insertions(+) create mode 100644 packages/sdk/server-ai/__tests__/ManagedAgentGraph.test.ts create mode 100644 packages/sdk/server-ai/src/api/graph/ManagedAgentGraph.ts diff --git a/packages/sdk/server-ai/__tests__/ManagedAgentGraph.test.ts b/packages/sdk/server-ai/__tests__/ManagedAgentGraph.test.ts new file mode 100644 index 0000000000..eb2bb9fc0a --- /dev/null +++ b/packages/sdk/server-ai/__tests__/ManagedAgentGraph.test.ts @@ -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 = { + 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; + + 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); + }); +}); diff --git a/packages/sdk/server-ai/src/api/graph/ManagedAgentGraph.ts b/packages/sdk/server-ai/src/api/graph/ManagedAgentGraph.ts new file mode 100644 index 0000000000..90fe6d168f --- /dev/null +++ b/packages/sdk/server-ai/src/api/graph/ManagedAgentGraph.ts @@ -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, + ): Promise { + 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 = Promise.resolve([]); + + return { + content: runnerResult.content, + metrics, + raw: runnerResult.raw, + evaluations, + }; + } + + /** + * Returns the underlying AgentGraphDefinition. + */ + getGraphDefinition(): AgentGraphDefinition { + return this._graphDefinition; + } +} diff --git a/packages/sdk/server-ai/src/api/graph/index.ts b/packages/sdk/server-ai/src/api/graph/index.ts index 9d899029d5..6b26c398b0 100644 --- a/packages/sdk/server-ai/src/api/graph/index.ts +++ b/packages/sdk/server-ai/src/api/graph/index.ts @@ -2,3 +2,4 @@ export * from './types'; export * from './LDGraphTracker'; export * from './AgentGraphNode'; export * from './AgentGraphDefinition'; +export * from './ManagedAgentGraph'; diff --git a/packages/sdk/server-ai/src/api/graph/types.ts b/packages/sdk/server-ai/src/api/graph/types.ts index 6cca861c25..290ef313f6 100644 --- a/packages/sdk/server-ai/src/api/graph/types.ts +++ b/packages/sdk/server-ai/src/api/graph/types.ts @@ -1,3 +1,4 @@ +import { LDJudgeResult } from '../judge/types'; import { LDAIMetrics, LDTokenUsage } from '../metrics'; /** @@ -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; + + /** + * 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; +} + /** * Tracking metadata returned by {@link LDGraphTracker.getTrackData}. */