@wix/pathgrade 1.0.36 → 1.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -164,6 +164,8 @@ npx pathgrade run
164
164
  - **Scorer**: a function or judge that evaluates output or behavior
165
165
  - **Evaluation**: the aggregated result of one or more scorers, returned as a score from `0.0` to `1.0`
166
166
 
167
+ For protocol-neutral multi-agent graphs, see [Evaluating agent flows](docs/agent-flow-evaluation.md).
168
+
167
169
  ## Scorers
168
170
 
169
171
  Scorers evaluate the agent's output and behavior. `evaluate()` runs all scorers and computes a weighted average between `0.0` and `1.0`.
@@ -13,5 +13,5 @@ export { getPathgradeDir } from '../reporters/results-path.js';
13
13
  export { printReportSummary } from '../reporters/report-summary.js';
14
14
  export { fmt } from '../utils/cli.js';
15
15
  export { getCurrentCaseContext, installCaseContextProvider, runWithCaseContext, type CaseContext, type CaseContextProvider, type CaseContextProviderHandle, type CaseContextScope, type CurrentCaseContext, } from '../sdk/case-context.js';
16
- export { subscribeToEvalResults, type EvalResultObserver, type ResultObserverHandle, type ResultObserverOptions, type ResultObserverOwner, } from '../sdk/result-capture.js';
17
- export type { Agent, PathgradeTestMeta, RecordedEvalResult, } from '../sdk/types.js';
16
+ export { subscribeToEvalResults, subscribeToEvaluationResults, type EvalResultObserver, type ResultObserverHandle, type ResultObserverOptions, type ResultObserverOwner, type EvaluationResultEvent, type EvaluationResultObserver, } from '../sdk/result-capture.js';
17
+ export type { Agent, AgentFlowTrace, PathgradeTestMeta, RecordedEvalResult, } from '../sdk/types.js';
@@ -11,4 +11,4 @@ export { getPathgradeDir } from '../reporters/results-path.js';
11
11
  export { printReportSummary } from '../reporters/report-summary.js';
12
12
  export { fmt } from '../utils/cli.js';
13
13
  export { getCurrentCaseContext, installCaseContextProvider, runWithCaseContext, } from '../sdk/case-context.js';
14
- export { subscribeToEvalResults, } from '../sdk/result-capture.js';
14
+ export { subscribeToEvalResults, subscribeToEvaluationResults, } from '../sdk/result-capture.js';
@@ -1,4 +1,4 @@
1
- import { installCaseContextProvider, subscribeToEvalResults, createRunnerLifecycleHooks, } from '@wix/pathgrade/adapter-kit';
1
+ import { installCaseContextProvider, subscribeToEvaluationResults, createRunnerLifecycleHooks, } from '@wix/pathgrade/adapter-kit';
2
2
  import { appendJestMetadata } from './metadata.js';
3
3
  import { jestCaseId } from './results.js';
4
4
  const metadataByCaseId = new Map();
@@ -58,5 +58,7 @@ function resetJestLifecycleMetadata() {
58
58
  metadataByCaseId.clear();
59
59
  }
60
60
  function defaultSubscribeToResults(callback) {
61
- return subscribeToEvalResults(({ result, agent }) => callback({ result, agent }), { owner: 'adapter', key: 'jest-lifecycle' });
61
+ return subscribeToEvaluationResults(event => callback(event.sourceKind === 'flow'
62
+ ? { result: event.result, flow: event.flow, case: event.case }
63
+ : { result: event.result, agent: event.agent, case: event.case }), { owner: 'adapter', key: 'jest-lifecycle' });
62
64
  }
@@ -2,7 +2,7 @@ import nodeTest from 'node:test';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { subscribeToEvalResults } from '../../sdk/result-capture.js';
5
+ import { subscribeToEvaluationResults } from '../../sdk/result-capture.js';
6
6
  import { createRunnerLifecycleHooks } from '../../runners/lifecycle-hooks.js';
7
7
  const lifecycle = createRunnerLifecycleHooks();
8
8
  let subscribed = false;
@@ -69,7 +69,9 @@ function installResultCapture() {
69
69
  if (subscribed)
70
70
  return;
71
71
  subscribed = true;
72
- subscribeToEvalResults(event => lifecycle.onResult(event), {
72
+ subscribeToEvaluationResults(event => lifecycle.onResult(event.sourceKind === 'flow'
73
+ ? { result: event.result, flow: event.flow, case: event.case }
74
+ : { result: event.result, agent: event.agent, case: event.case }), {
73
75
  owner: 'adapter',
74
76
  key: 'node-test-lifecycle',
75
77
  });
@@ -1,6 +1,5 @@
1
1
  import type { AgentTurnResult } from '../../types.js';
2
2
  import type { AppServerTransport } from './transport.js';
3
- export declare const WIX_CODEX_USD_PER_CREDIT = 0.034;
4
3
  export declare class CodexTurnUsageAccounting {
5
4
  private baseline?;
6
5
  private latest?;
@@ -2,7 +2,7 @@ const TOKEN_KEYS = [
2
2
  'totalTokens', 'inputTokens', 'cachedInputTokens',
3
3
  'cacheWriteInputTokens', 'outputTokens', 'reasoningOutputTokens',
4
4
  ];
5
- export const WIX_CODEX_USD_PER_CREDIT = 0.034;
5
+ const WIX_CODEX_USD_PER_CREDIT = 0.034;
6
6
  const USAGE_READ_FALLBACK_TIMEOUT_MS = 5_000;
7
7
  export class CodexTurnUsageAccounting {
8
8
  baseline;
@@ -12,7 +12,7 @@ export function buildComparisonContract(input) {
12
12
  case: caseIdentity,
13
13
  definition: evaluation.evaluationDefinitionKey ?? `position:${index}`,
14
14
  scorer: evaluation.scorerRevision,
15
- runtime: runtimeIdentity(evaluation.agent ?? evaluation.trial?.agent),
15
+ runtime: runtimeIdentity(evaluation),
16
16
  }));
17
17
  });
18
18
  const definitionRevision = input.group.sourceRevision
@@ -83,12 +83,26 @@ export function createScorerRevision(scorers) {
83
83
  return undefined;
84
84
  }
85
85
  }
86
- function runtimeIdentity(agent) {
86
+ function runtimeIdentity(evaluation) {
87
+ const agent = evaluation.agent ?? evaluation.trial?.agent;
88
+ if (agent) {
89
+ return {
90
+ name: agent.name,
91
+ model: agent.resolvedModel,
92
+ transport: agent.transport,
93
+ interaction: agent.interactionMode,
94
+ };
95
+ }
96
+ const flow = evaluation.trial?.flow_trace;
87
97
  return {
88
- name: agent?.name,
89
- model: agent?.resolvedModel,
90
- transport: agent?.transport,
91
- interaction: agent?.interactionMode,
98
+ flow: flow?.completeness.runtimeIdentity === 'complete'
99
+ && flow.participants.every(participant => participant.runtime) ? {
100
+ ...(flow.protocol ? { protocol: flow.protocol } : {}),
101
+ rootParticipantId: flow.rootParticipantId,
102
+ participants: flow.participants
103
+ .map(participant => ({ id: participant.id, runtime: participant.runtime }))
104
+ .toSorted((left, right) => left.id.localeCompare(right.id)),
105
+ } : undefined,
92
106
  };
93
107
  }
94
108
  function scorerDeclaration(scorer) {
@@ -30,7 +30,7 @@ export function buildPathgradeReport(input) {
30
30
  const traceFile = `traces/${slug(group.groupName)}.json`;
31
31
  traces.push({ traceFile, trials: report.trials });
32
32
  const strippedTrials = report.trials.map(trial => {
33
- const { session_log, conversation, ...rest } = trial;
33
+ const { session_log, conversation, flow_trace, ...rest } = trial;
34
34
  return rest;
35
35
  });
36
36
  const { trials: _trials, ...rest } = report;
@@ -1,5 +1,5 @@
1
1
  import type { PathgradeSelectionReport } from '../types.js';
2
- import type { Agent, PathgradeTestMeta, RecordedEvalResult } from '../sdk/types.js';
2
+ import type { Agent, AgentFlowTrace, PathgradeTestMeta, RecordedEvalResult } from '../sdk/types.js';
3
3
  import type { CaseContextScope } from '../sdk/case-context.js';
4
4
  import type { NormalizedRunSnapshot } from './model.js';
5
5
  export declare const runnerAdapterContractVersion = 1;
@@ -61,5 +61,7 @@ export interface AdapterCaseContext {
61
61
  }
62
62
  export interface EvalResultEvent {
63
63
  result: RecordedEvalResult;
64
- agent: Agent;
64
+ agent?: Agent;
65
+ flow?: AgentFlowTrace;
66
+ case?: import('../sdk/case-context.js').CaseContext;
65
67
  }
@@ -2,7 +2,12 @@ import { lifecycleCore } from '../sdk/lifecycle.js';
2
2
  import { runWithCaseContext } from '../sdk/case-context.js';
3
3
  export function createRunnerLifecycleHooks() {
4
4
  return {
5
- onResult: event => lifecycleCore.recordResult(event.result, event.agent),
5
+ onResult: event => {
6
+ if (event.flow)
7
+ lifecycleCore.recordFlowResult(event.result, event.flow, event.case?.caseId);
8
+ else if (event.agent)
9
+ lifecycleCore.recordResult(event.result, event.agent);
10
+ },
6
11
  withCaseContext: (context, run) => runWithCaseContext(toCaseContext(context), run),
7
12
  flushCase: caseId => lifecycleCore.flushCase({ caseId }),
8
13
  cleanupRun: () => lifecycleCore.cleanupAll(),
@@ -6,6 +6,7 @@ export type VitestAfterEachFn = (fn: (ctx: {
6
6
  task: {
7
7
  id: string;
8
8
  meta: Record<string, unknown>;
9
+ suite?: unknown;
9
10
  };
10
11
  }) => Promise<void>) => void;
11
12
  export type VitestAfterAllFn = (fn: () => Promise<void>) => void;
@@ -1,10 +1,15 @@
1
1
  import path from 'node:path';
2
- import { subscribeToEvalResults } from '../sdk/result-capture.js';
2
+ import { subscribeToEvaluationResults } from '../sdk/result-capture.js';
3
3
  import { getCurrentCaseContext, installCaseContextProvider, runWithCaseContext, } from '../sdk/case-context.js';
4
4
  import { lifecycleCore } from '../sdk/lifecycle.js';
5
5
  export function createVitestLifecycleHooks() {
6
6
  return {
7
- onResult: event => recordVitestResult(event.result, event.agent),
7
+ onResult: event => {
8
+ if (event.flow)
9
+ recordVitestFlowResult(event.result, event.flow, event.case);
10
+ else if (event.agent)
11
+ recordVitestResult(event.result, event.agent);
12
+ },
8
13
  withCaseContext: (context, run) => runWithCaseContext(toCaseContext(context), run),
9
14
  flushCase: caseId => lifecycleCore.flushCase({ caseId }),
10
15
  cleanupRun: () => lifecycleCore.cleanupAll(),
@@ -18,7 +23,11 @@ export function installVitestLifecycle(input) {
18
23
  let resultsUnsubscribed = false;
19
24
  input.aroundEach?.(async (runTest, { task }) => lifecycle.withCaseContext(caseContextForTask(task), runTest));
20
25
  input.afterEach(async ({ task }) => {
21
- const results = await lifecycle.flushCase(task.id);
26
+ const filePath = filePathForTask(task);
27
+ const results = [
28
+ ...(filePath ? await lifecycle.flushCase(`file:${filePath}`) : []),
29
+ ...await lifecycle.flushCase(task.id),
30
+ ];
22
31
  if (results.length > 0) {
23
32
  task.meta.pathgrade = results;
24
33
  }
@@ -69,7 +78,9 @@ export function resetVitestLifecycle(handle) {
69
78
  handle?.restore();
70
79
  }
71
80
  function defaultSubscribeToResults(callback) {
72
- return subscribeToEvalResults(({ result, agent }) => callback({ result, agent }), { owner: 'adapter', key: 'vitest-lifecycle' });
81
+ return subscribeToEvaluationResults(event => callback(event.sourceKind === 'flow'
82
+ ? { result: event.result, flow: event.flow, case: event.case }
83
+ : { result: event.result, agent: event.agent, case: event.case }), { owner: 'adapter', key: 'vitest-lifecycle' });
73
84
  }
74
85
  function defaultInstallFileContextProvider() {
75
86
  return installCaseContextProvider(currentFileContext);
@@ -148,3 +159,9 @@ function recordVitestResult(result, agent) {
148
159
  }
149
160
  lifecycleCore.recordResult(result, agent);
150
161
  }
162
+ function recordVitestFlowResult(result, flow, capturedCase) {
163
+ const caseId = capturedCase?.scope === 'runner-case'
164
+ ? capturedCase.caseId
165
+ : currentTaskId() || capturedCase?.caseId;
166
+ lifecycleCore.recordFlowResult(result, flow, caseId || undefined);
167
+ }
@@ -0,0 +1,51 @@
1
+ export type AgentFlowEvidenceAvailability = 'complete' | 'partial' | 'unavailable';
2
+ export interface AgentFlowEvidenceCompleteness {
3
+ topology: AgentFlowEvidenceAvailability;
4
+ outcomes: AgentFlowEvidenceAvailability;
5
+ timing: AgentFlowEvidenceAvailability;
6
+ usage: AgentFlowEvidenceAvailability;
7
+ runtimeIdentity: AgentFlowEvidenceAvailability;
8
+ }
9
+ export interface AgentFlowParticipant {
10
+ id: string;
11
+ name?: string;
12
+ role?: string;
13
+ runtime?: string;
14
+ }
15
+ export interface AgentFlowInteraction {
16
+ id: string;
17
+ sequence: number;
18
+ sourceParticipantId: string;
19
+ targetParticipantId: string;
20
+ operation: string;
21
+ parentInteractionId?: string;
22
+ taskId?: string;
23
+ contextId?: string;
24
+ state?: string;
25
+ inputText?: string;
26
+ outputText?: string;
27
+ error?: string;
28
+ startedAt?: string;
29
+ endedAt?: string;
30
+ }
31
+ export interface AgentFlowTrace {
32
+ version: 1;
33
+ protocol?: string;
34
+ rootParticipantId: string;
35
+ participants: readonly AgentFlowParticipant[];
36
+ interactions: readonly AgentFlowInteraction[];
37
+ outcome: {
38
+ state: string;
39
+ outputText?: string;
40
+ error?: string;
41
+ };
42
+ completeness: AgentFlowEvidenceCompleteness;
43
+ }
44
+ export interface AgentFlowSummary {
45
+ participant_count: number;
46
+ interaction_count: number;
47
+ max_observed_depth: number;
48
+ root_state: string;
49
+ completeness: AgentFlowEvidenceCompleteness;
50
+ }
51
+ export declare function summarizeFlow(flow: AgentFlowTrace): AgentFlowSummary;
@@ -0,0 +1,23 @@
1
+ export function summarizeFlow(flow) {
2
+ const parents = new Map(flow.interactions.map(interaction => [interaction.id, interaction.parentInteractionId]));
3
+ const depth = (id) => {
4
+ let current = parents.get(id);
5
+ let value = 1;
6
+ const seen = new Set([id]);
7
+ while (current && !seen.has(current)) {
8
+ seen.add(current);
9
+ value += 1;
10
+ current = parents.get(current);
11
+ }
12
+ return value;
13
+ };
14
+ return {
15
+ participant_count: flow.participants.length,
16
+ interaction_count: flow.interactions.length,
17
+ max_observed_depth: flow.interactions.length > 0
18
+ ? Math.max(...flow.interactions.map(interaction => depth(interaction.id)))
19
+ : 0,
20
+ root_state: flow.outcome.state,
21
+ completeness: structuredClone(flow.completeness),
22
+ };
23
+ }
@@ -1,4 +1,4 @@
1
- import type { Agent, Scorer, RecordedEvalResult, ScorerResultEntry, EvaluateOptions } from './types.js';
1
+ import type { Agent, Scorer, RecordedEvalResult, ScorerResultEntry, EvaluateOptions, AgentFlowTrace } from './types.js';
2
2
  export type OnScorerErrorMode = NonNullable<EvaluateOptions['onScorerError']>;
3
3
  export declare class EvalScorerError extends Error {
4
4
  readonly scorerErrors: ScorerResultEntry[];
@@ -9,6 +9,7 @@ type EvaluateFromSnapshot = (snapshotPath: string, scorers: Scorer[], opts?: Eva
9
9
  type EvaluateFn = ((agent: Agent, scorers: Scorer[], opts?: EvaluateOptions) => Promise<RecordedEvalResult>) & {
10
10
  fromSnapshot: EvaluateFromSnapshot;
11
11
  };
12
+ export declare function evaluateFlow(flow: AgentFlowTrace, scorers: Scorer[], opts?: EvaluateOptions): Promise<RecordedEvalResult>;
12
13
  /** Internal runConversation hook: step scorers must not consume final-run attribution. */
13
14
  export declare function evaluateStepScorers(agent: Agent, scorers: Scorer[], opts?: EvaluateOptions): Promise<RecordedEvalResult>;
14
15
  export declare const evaluate: EvaluateFn;
@@ -1,7 +1,7 @@
1
1
  import { countShellCommandsFromLog, extractSkillsFromLog, extractToolEventsFromLog } from '../tool-events.js';
2
2
  import { createScorerRevision } from '../reporting/comparison-contract.js';
3
3
  import { getRuntime } from './eval-runtime.js';
4
- import { emitEvalResult } from './result-capture.js';
4
+ import { emitEvalResult, emitFlowEvalResult } from './result-capture.js';
5
5
  import { runJudgePipeline } from './judge-pipeline.js';
6
6
  import { runScorer } from './run-scorer.js';
7
7
  import { createLLMClient } from '../utils/llm.js';
@@ -9,6 +9,7 @@ import { sandboxExec } from '../providers/sandbox-exec.js';
9
9
  import { buildTranscript, loadRunSnapshot, WorkspaceMissingError } from './snapshots.js';
10
10
  import fs from 'fs-extra';
11
11
  import path from 'path';
12
+ import { summarizeFlow } from './agent-flow.js';
12
13
  export class EvalScorerError extends Error {
13
14
  scorerErrors;
14
15
  result;
@@ -19,6 +20,60 @@ export class EvalScorerError extends Error {
19
20
  this.scorerErrors = scorerErrors;
20
21
  }
21
22
  }
23
+ export async function evaluateFlow(flow, scorers, opts) {
24
+ if (scorers.some(scorer => scorer.type === 'tool_usage')) {
25
+ throw new TypeError('tool_usage scorers require an Agent and cannot evaluate an AgentFlowTrace');
26
+ }
27
+ if (scorers.some(scorer => scorer.type === 'judge' && scorer.tools && scorer.tools.length > 0)) {
28
+ throw new TypeError('tool-enabled judge scorers require an Agent workspace and cannot evaluate an AgentFlowTrace');
29
+ }
30
+ const trackedLLM = opts?.llm ?? createLLMClient({ adapters: [{
31
+ name: 'runtime', isAvailable: async () => true,
32
+ call: (prompt, callOpts) => getRuntime().llm.call(prompt, callOpts),
33
+ }] });
34
+ const unavailable = async () => {
35
+ throw new WorkspaceMissingError('AgentFlowTrace does not include a workspace');
36
+ };
37
+ const ctx = {
38
+ workspace: '',
39
+ log: [],
40
+ transcript: JSON.stringify(flow),
41
+ toolEvents: [],
42
+ runCommand: unavailable,
43
+ artifacts: { list: () => [], read: unavailable, latest: async () => null },
44
+ flow,
45
+ };
46
+ const scoringStartedAt = performance.now();
47
+ const beforeTokenUsage = trackedLLM.tokenUsage ?? { inputTokens: 0, outputTokens: 0 };
48
+ const { result: evalResult, tokens: deltaTokenUsage } = trackedLLM.measure
49
+ ? await trackedLLM.measure(() => evaluateWithContext(ctx, scorers, { ...opts, llm: trackedLLM }))
50
+ : await (async () => {
51
+ const result = await evaluateWithContext(ctx, scorers, { ...opts, llm: trackedLLM });
52
+ const afterTokenUsage = trackedLLM.tokenUsage ?? beforeTokenUsage;
53
+ return { result, tokens: {
54
+ inputTokens: Math.max(0, afterTokenUsage.inputTokens - beforeTokenUsage.inputTokens),
55
+ outputTokens: Math.max(0, afterTokenUsage.outputTokens - beforeTokenUsage.outputTokens),
56
+ } };
57
+ })();
58
+ const scorerRevision = createScorerRevision(scorers);
59
+ const recordedResult = {
60
+ ...evalResult,
61
+ tokenUsage: deltaTokenUsage,
62
+ resultKind: 'evaluated',
63
+ scoringDurationMs: Math.max(0, performance.now() - scoringStartedAt),
64
+ recordedAt: new Date().toISOString(),
65
+ ...(opts?.evaluationDefinitionKey ? { evaluationDefinitionKey: opts.evaluationDefinitionKey } : {}),
66
+ ...(scorerRevision ? { scorerRevision } : {}),
67
+ trial: {
68
+ ...buildTrialResult([], { ...evalResult, tokenUsage: deltaTokenUsage }),
69
+ flow_summary: summarizeFlow(flow),
70
+ flow_trace: structuredClone(flow),
71
+ },
72
+ };
73
+ emitFlowEvalResult({ result: recordedResult, flow });
74
+ maybeThrowOnScorerErrors(recordedResult, opts?.onScorerError ?? 'skip');
75
+ return recordedResult;
76
+ }
22
77
  /**
23
78
  * Run scorers against a trial and compute a weighted average score.
24
79
  *
@@ -9,7 +9,7 @@ export type { McpMockApprovalRule, McpMockJsonValue } from './mcp-mock-approvals
9
9
  export type { CompileScenarioResult, EvidenceEnvelope, JsonObject, JsonPrimitive, JsonValue, MatchExpr, MatchPredicate, McpContent, ScenarioArtifact, ScenarioCase, ScenarioDiagnostic, ScenarioEvidence, ScenarioMachineV2, ScenarioRejection, ScenarioSuccess, ScenarioToolError, } from './scenario-machine-v2.js';
10
10
  export type { ExpectedScenarioCaseMatch, ScenarioCaseMatchEvidence, ScenarioStateTransitionEvidence, } from './scenario-evidence.js';
11
11
  export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
12
- export { evaluate, EvalScorerError } from './evaluate.js';
12
+ export { evaluate, evaluateFlow, EvalScorerError } from './evaluate.js';
13
13
  export { RUN_SNAPSHOT_VERSION, buildRunSnapshot, loadRunSnapshot, SnapshotParseError, SnapshotVersionError, WorkspaceMissingError, } from './snapshots.js';
14
14
  export { createPersona } from './persona.js';
15
15
  export { createConversationWindow } from './conversation-window.js';
@@ -26,11 +26,13 @@ export { createAskBus, requireAskBusForLiveBatches, AskBusTimeoutError } from '.
26
26
  export { toAskUserToolEvent } from './ask-bus/projection.js';
27
27
  export type { AskUserToolEvent, AskUserToolEventArguments, AskUserToolEventQuestionArgument, } from './ask-bus/projection.js';
28
28
  export { buildAskBatchLogEntries } from './agent-result-log.js';
29
- export { emitEvalResult, resetAllResultObserversForTests, resetUserResultObservers, subscribeToEvalResults, } from './result-capture.js';
29
+ export { emitEvalResult, emitFlowEvalResult, resetAllResultObserversForTests, resetUserResultObservers, subscribeToEvalResults, subscribeToEvaluationResults, } from './result-capture.js';
30
30
  export { getAgentCapabilities } from './types.js';
31
31
  export type { AgentTransport, AgentCapabilities, AgentName, McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './types.js';
32
32
  export type { AskBus, AskBatch, AskQuestion, AskOption, AskAnswer, AskResolution, AskBatchSnapshot, AskAnswerSnapshot, AskResolutionSnapshot, AskHandle, AskHandler, AskSource, AskLifecycle, AskAnswerSource, Unsubscribe as AskBusUnsubscribe, } from './ask-bus/types.js';
33
33
  export type { Agent, AgentOptions, DebugOptions, Message, Scorer, CheckScorer, ScoreScorer, JudgeScorer, ToolUsageScorer, ScorerContext, EvalResult, ScorerResultEntry, ScorerStatus, ChatSession, ConversationResult, ConverseOptions, UntilPredicate, UntilContext, Reaction, TextReaction, AskUserReaction, AskUserQuestion, AskUserOption, ReactionPreviewEntry, TextReactionPreviewEntry, AskUserReactionPreviewEntry, ReactionPreviewResult, ReactionPreviewTurn, StepScorer, Persona, PersonaConfig, ConversationWindowConfig, TurnDetail, ReactionFiredEntry, PathgradePluginOptions, PathgradeMeta, TurnTiming, TokenUsage, EvaluateOptions, ReactionPreviewStatus, ScoreResult, JudgeInput, CodeJudgeToolName, ToolExpectation, SessionArtifactMatchOptions, SessionArtifactContent, SessionArtifacts, RecordedEvalResult, PathgradeTestMeta, EvaluationResultKind, AgentExecutionMetadata, AgentExecutionTransport, AgentInteractionMode, } from './types.js';
34
+ export type { AgentFlowTrace, AgentFlowParticipant, AgentFlowInteraction, AgentFlowEvidenceAvailability, AgentFlowEvidenceCompleteness, AgentFlowSummary, } from './agent-flow.js';
35
+ export type { EvaluationResultEvent, EvaluationResultObserver } from './result-capture.js';
34
36
  export type { ConversationWindow, ConversationWindowOptions } from './conversation-window.js';
35
37
  export type { JudgePipelineOptions } from './judge-pipeline.js';
36
38
  export type { RunScorerOptions } from './run-scorer.js';
package/dist/sdk/index.js CHANGED
@@ -7,7 +7,7 @@ export { compileScenario, compileScenarioText } from './scenario-machine-v2.js';
7
7
  export { findScenarioCaseMatches, getFinalScenarioState, getScenarioStateTimeline, ScenarioEvidenceError, wasScenarioCaseMatched, } from './scenario-evidence.js';
8
8
  export { getMcpToolCall, isMcpToolCall, findMcpToolCalls, getMcpStartupStatus, isMcpStartupStatus, getMcpApproval, isMcpApproval, findMcpApprovals, getMcpInvocation, wasMcpToolInvoked, } from './mcp-evidence.js';
9
9
  export { decideMcpToolCall, redactMcpSecrets, } from './mcp-safety.js';
10
- export { evaluate, EvalScorerError } from './evaluate.js';
10
+ export { evaluate, evaluateFlow, EvalScorerError } from './evaluate.js';
11
11
  export { RUN_SNAPSHOT_VERSION, buildRunSnapshot, loadRunSnapshot, SnapshotParseError, SnapshotVersionError, WorkspaceMissingError, } from './snapshots.js';
12
12
  export { createPersona } from './persona.js';
13
13
  export { createConversationWindow } from './conversation-window.js';
@@ -23,7 +23,7 @@ export { parsePathgradeReport } from '../reporting/report-parser.js';
23
23
  export { createAskBus, requireAskBusForLiveBatches, AskBusTimeoutError } from './ask-bus/bus.js';
24
24
  export { toAskUserToolEvent } from './ask-bus/projection.js';
25
25
  export { buildAskBatchLogEntries } from './agent-result-log.js';
26
- export { emitEvalResult, resetAllResultObserversForTests, resetUserResultObservers, subscribeToEvalResults, } from './result-capture.js';
26
+ export { emitEvalResult, emitFlowEvalResult, resetAllResultObserversForTests, resetUserResultObservers, subscribeToEvalResults, subscribeToEvaluationResults, } from './result-capture.js';
27
27
  export { getAgentCapabilities } from './types.js';
28
28
  export { COMPARISON_CONTRACT_VERSION, NORMALIZED_RUN_MODEL_VERSION, PATHGRADE_REPORT_VERSION, TASK_INVENTORY_VERSION, } from '../reporting/reliability-contract.js';
29
29
  export { createAgentLLM, createLLMClient, ProviderNotSupportedError } from '../utils/llm.js';
@@ -1,4 +1,4 @@
1
- import type { Agent, PathgradeTestMeta, RecordedEvalResult } from './types.js';
1
+ import type { Agent, AgentFlowTrace, PathgradeTestMeta, RecordedEvalResult } from './types.js';
2
2
  export type LifecycleAgentOwner = {
3
3
  type: 'runner-case';
4
4
  caseId: string;
@@ -24,6 +24,7 @@ declare function registerAgent(agent: Agent, owner?: LifecycleAgentOwner | null)
24
24
  declare function untrackAgent(agent: Agent): void;
25
25
  declare function releaseAgent(agent: Agent): void;
26
26
  declare function recordResult(result: RecordedEvalResult, agent: Agent, attribution?: ResultAttribution): void;
27
+ declare function recordFlowResult(result: RecordedEvalResult, flow: AgentFlowTrace, caseId?: string): void;
27
28
  declare function flushCase(input: FlushCaseInput): Promise<PathgradeTestMeta[]>;
28
29
  declare function cleanupAll(): Promise<void>;
29
30
  declare function reset(): void;
@@ -33,6 +34,7 @@ export declare const lifecycleCore: {
33
34
  releaseAgent: typeof releaseAgent;
34
35
  getAgentOwner: typeof getAgentOwner;
35
36
  recordResult: typeof recordResult;
37
+ recordFlowResult: typeof recordFlowResult;
36
38
  flushCase: typeof flushCase;
37
39
  cleanupAll: typeof cleanupAll;
38
40
  reset: typeof reset;
@@ -1,14 +1,19 @@
1
1
  import { getCurrentCaseContext } from './case-context.js';
2
2
  import { buildDiagnosticsReport } from './diagnostics.js';
3
3
  import { countShellCommandsFromLog } from '../tool-events.js';
4
+ import { summarizeFlow } from './agent-flow.js';
4
5
  const lifecycleStateKey = Symbol.for('@wix/pathgrade/lifecycle-state');
5
6
  const sharedLifecycle = globalThis;
6
7
  const lifecycleState = sharedLifecycle[lifecycleStateKey] ?? (sharedLifecycle[lifecycleStateKey] = {
7
8
  pendingAgents: new Set(),
8
9
  agentOwners: new WeakMap(),
9
10
  agentResults: new WeakMap(),
11
+ flowResults: new Map(),
12
+ nextResultSequence: 0,
10
13
  });
11
- const { pendingAgents, agentOwners, agentResults } = lifecycleState;
14
+ lifecycleState.nextResultSequence ??= 0;
15
+ lifecycleState.flowResults ??= new Map();
16
+ const { pendingAgents, agentOwners, agentResults, flowResults } = lifecycleState;
12
17
  function currentAgentOwner() {
13
18
  const current = getCurrentCaseContext();
14
19
  if (current.status !== 'active')
@@ -74,6 +79,7 @@ function recordResult(result, agent, attribution) {
74
79
  const conversationEnd = [...agent.log].reverse().find((entry) => entry.type === 'conversation_end');
75
80
  const completionReason = conversationEnd?.completion_reason ?? (agent.log.some((entry) => entry.type === 'agent_result') ? 'completed' : undefined);
76
81
  agentResults.get(agent).push({
82
+ sequence: lifecycleState.nextResultSequence++,
77
83
  attribution: attribution ?? currentResultAttribution(owner),
78
84
  meta: {
79
85
  score: result.score,
@@ -101,8 +107,60 @@ function recordResult(result, agent, attribution) {
101
107
  },
102
108
  });
103
109
  }
110
+ function recordFlowResult(result, flow, caseId) {
111
+ const current = getCurrentCaseContext();
112
+ const resolvedCaseId = caseId ?? (current.status === 'active' && current.context.scope === 'runner-case'
113
+ ? current.context.caseId
114
+ : undefined);
115
+ if (!resolvedCaseId)
116
+ return;
117
+ const entries = flowResults.get(resolvedCaseId) ?? [];
118
+ entries.push({
119
+ sequence: lifecycleState.nextResultSequence++,
120
+ meta: {
121
+ score: result.score,
122
+ scorers: result.scorers,
123
+ ...(result.evaluationDefinitionKey ? { evaluationDefinitionKey: result.evaluationDefinitionKey } : {}),
124
+ ...(result.scorerRevision ? { scorerRevision: result.scorerRevision } : {}),
125
+ trial: {
126
+ ...(result.trial ?? {
127
+ trial_id: 0,
128
+ ...(result.score !== undefined ? { reward: result.score } : {}),
129
+ scorer_results: result.scorers.map(scorer => ({
130
+ scorer_type: scorer.type === 'check' || scorer.type === 'score' ? 'deterministic' : scorer.type === 'judge' ? 'llm_rubric' : 'tool_usage',
131
+ score: scorer.score,
132
+ weight: scorer.weight,
133
+ details: scorer.details ?? '',
134
+ status: scorer.status,
135
+ })),
136
+ duration_ms: 0,
137
+ n_commands: 0,
138
+ input_tokens: result.tokenUsage?.inputTokens ?? 0,
139
+ output_tokens: result.tokenUsage?.outputTokens ?? 0,
140
+ session_log: [],
141
+ }),
142
+ flow_summary: summarizeFlow(flow),
143
+ flow_trace: structuredClone(flow),
144
+ },
145
+ resultKind: result.resultKind ?? 'evaluated',
146
+ ...(result.scoringDurationMs !== undefined ? { scoringDurationMs: result.scoringDurationMs } : {}),
147
+ ...(result.recordedAt ? { recordedAt: result.recordedAt } : {}),
148
+ diagnostics: buildDiagnosticsReport({
149
+ completionReason: ['completed', 'failed', 'rejected', 'canceled', 'auth-required'].includes(flow.outcome.state)
150
+ ? flow.outcome.state
151
+ : undefined,
152
+ completionDetail: flow.outcome.error,
153
+ score: result.score,
154
+ scorers: result.scorers,
155
+ log: [],
156
+ }),
157
+ },
158
+ });
159
+ flowResults.set(resolvedCaseId, entries);
160
+ }
104
161
  async function flushCase(input) {
105
- const results = [];
162
+ const results = flowResults.get(input.caseId) ?? [];
163
+ flowResults.delete(input.caseId);
106
164
  const toDispose = [];
107
165
  for (const agent of pendingAgents) {
108
166
  const owner = agentOwners.get(agent);
@@ -110,7 +168,7 @@ async function flushCase(input) {
110
168
  const meta = agentResults.get(agent);
111
169
  const matchingMeta = meta?.filter((entry) => resultMatchesCase(entry, input.caseId));
112
170
  if (matchingMeta && matchingMeta.length > 0 && canFlushResultsToCase(owner, input.caseId)) {
113
- results.push(...matchingMeta.map((entry) => entry.meta));
171
+ results.push(...matchingMeta);
114
172
  const remainingMeta = meta?.filter((entry) => !resultMatchesCase(entry, input.caseId)) ?? [];
115
173
  if (remainingMeta.length > 0) {
116
174
  agentResults.set(agent, remainingMeta);
@@ -124,7 +182,7 @@ async function flushCase(input) {
124
182
  if (!hasResultsForCase) {
125
183
  const synthTrial = synthesizeTrialFromAgent(agent);
126
184
  if (synthTrial) {
127
- results.push(synthTrial);
185
+ results.push({ sequence: lifecycleState.nextResultSequence++, meta: synthTrial });
128
186
  }
129
187
  }
130
188
  pendingAgents.delete(agent);
@@ -133,7 +191,7 @@ async function flushCase(input) {
133
191
  }
134
192
  }
135
193
  await Promise.all(toDispose.map((agent) => agent.dispose().catch(() => { })));
136
- return results;
194
+ return results.toSorted((left, right) => left.sequence - right.sequence).map(entry => entry.meta);
137
195
  }
138
196
  function synthesizeTrialFromAgent(agent) {
139
197
  if (agent.log.length === 0)
@@ -181,10 +239,14 @@ function synthesizeTrialFromAgent(agent) {
181
239
  async function cleanupAll() {
182
240
  const toDispose = [...pendingAgents];
183
241
  pendingAgents.clear();
242
+ flowResults.clear();
243
+ lifecycleState.nextResultSequence = 0;
184
244
  await Promise.all(toDispose.map((agent) => agent.dispose().catch(() => { })));
185
245
  }
186
246
  function reset() {
187
247
  pendingAgents.clear();
248
+ flowResults.clear();
249
+ lifecycleState.nextResultSequence = 0;
188
250
  }
189
251
  export const lifecycleCore = {
190
252
  registerAgent,
@@ -192,6 +254,7 @@ export const lifecycleCore = {
192
254
  releaseAgent,
193
255
  getAgentOwner,
194
256
  recordResult,
257
+ recordFlowResult,
195
258
  flushCase,
196
259
  cleanupAll,
197
260
  reset,
@@ -1,4 +1,4 @@
1
- import type { Agent, RecordedEvalResult } from './types.js';
1
+ import type { Agent, AgentFlowTrace, RecordedEvalResult } from './types.js';
2
2
  import { type CaseContext } from './case-context.js';
3
3
  export interface EvalResultEvent {
4
4
  readonly result: RecordedEvalResult;
@@ -6,6 +6,15 @@ export interface EvalResultEvent {
6
6
  readonly case?: CaseContext;
7
7
  }
8
8
  export type EvalResultObserver = (event: EvalResultEvent) => void;
9
+ export type EvaluationResultEvent = (EvalResultEvent & {
10
+ readonly sourceKind: 'agent';
11
+ }) | {
12
+ readonly sourceKind: 'flow';
13
+ readonly result: RecordedEvalResult;
14
+ readonly flow: AgentFlowTrace;
15
+ readonly case?: CaseContext;
16
+ };
17
+ export type EvaluationResultObserver = (event: EvaluationResultEvent) => void;
9
18
  export type ResultObserverOwner = 'user' | 'adapter' | 'test';
10
19
  export interface ResultObserverOptions {
11
20
  readonly owner?: ResultObserverOwner;
@@ -15,6 +24,10 @@ export interface ResultObserverHandle {
15
24
  unsubscribe(): void;
16
25
  }
17
26
  export declare function subscribeToEvalResults(observer: EvalResultObserver, options?: ResultObserverOptions): ResultObserverHandle;
27
+ export declare function subscribeToEvaluationResults(observer: EvaluationResultObserver, options?: ResultObserverOptions): ResultObserverHandle;
18
28
  export declare function emitEvalResult(event: EvalResultEvent): void;
29
+ export declare function emitFlowEvalResult(event: Omit<Extract<EvaluationResultEvent, {
30
+ sourceKind: 'flow';
31
+ }>, 'sourceKind'>): void;
19
32
  export declare function resetUserResultObservers(): void;
20
33
  export declare function resetAllResultObserversForTests(): void;
@@ -3,6 +3,10 @@ const observerRegistryKey = Symbol.for('@wix/pathgrade/eval-result-observers');
3
3
  const globalRegistry = globalThis;
4
4
  const observers = globalRegistry[observerRegistryKey]
5
5
  ?? (globalRegistry[observerRegistryKey] = new Set());
6
+ const evaluationObserverRegistryKey = Symbol.for('@wix/pathgrade/evaluation-result-observers');
7
+ const evaluationGlobalRegistry = globalThis;
8
+ const evaluationObservers = evaluationGlobalRegistry[evaluationObserverRegistryKey]
9
+ ?? (evaluationGlobalRegistry[evaluationObserverRegistryKey] = new Set());
6
10
  export function subscribeToEvalResults(observer, options = {}) {
7
11
  if (options.owner === 'adapter' && options.key) {
8
12
  removeObserverByOwnerAndKey(options.owner, options.key);
@@ -23,6 +27,22 @@ export function subscribeToEvalResults(observer, options = {}) {
23
27
  },
24
28
  };
25
29
  }
30
+ export function subscribeToEvaluationResults(observer, options = {}) {
31
+ if (options.owner === 'adapter' && options.key) {
32
+ removeEvaluationObserverByOwnerAndKey(options.owner, options.key);
33
+ }
34
+ const subscription = { observer, owner: options.owner ?? 'user', key: options.key };
35
+ evaluationObservers.add(subscription);
36
+ let active = true;
37
+ return {
38
+ unsubscribe() {
39
+ if (!active)
40
+ return;
41
+ active = false;
42
+ evaluationObservers.delete(subscription);
43
+ },
44
+ };
45
+ }
26
46
  export function emitEvalResult(event) {
27
47
  const currentCase = getCurrentCaseContext();
28
48
  const deliveredEvent = event.case || currentCase.status !== 'active'
@@ -40,6 +60,14 @@ export function emitEvalResult(event) {
40
60
  // broken hook must not block adapter-owned reporting.
41
61
  }
42
62
  }
63
+ deliverEvaluationEvent({ ...deliveredEvent, sourceKind: 'agent' });
64
+ }
65
+ export function emitFlowEvalResult(event) {
66
+ const currentCase = getCurrentCaseContext();
67
+ const deliveredEvent = event.case || currentCase.status !== 'active'
68
+ ? event
69
+ : { ...event, case: currentCase.context };
70
+ deliverEvaluationEvent({ ...deliveredEvent, sourceKind: 'flow' });
43
71
  }
44
72
  export function resetUserResultObservers() {
45
73
  for (const subscription of observers) {
@@ -47,9 +75,14 @@ export function resetUserResultObservers() {
47
75
  observers.delete(subscription);
48
76
  }
49
77
  }
78
+ for (const subscription of evaluationObservers) {
79
+ if (subscription.owner === 'user')
80
+ evaluationObservers.delete(subscription);
81
+ }
50
82
  }
51
83
  export function resetAllResultObserversForTests() {
52
84
  observers.clear();
85
+ evaluationObservers.clear();
53
86
  }
54
87
  function removeObserverByOwnerAndKey(owner, key) {
55
88
  for (const subscription of observers) {
@@ -58,3 +91,19 @@ function removeObserverByOwnerAndKey(owner, key) {
58
91
  }
59
92
  }
60
93
  }
94
+ function removeEvaluationObserverByOwnerAndKey(owner, key) {
95
+ for (const subscription of evaluationObservers) {
96
+ if (subscription.owner === owner && subscription.key === key)
97
+ evaluationObservers.delete(subscription);
98
+ }
99
+ }
100
+ function deliverEvaluationEvent(event) {
101
+ for (const subscription of [...evaluationObservers]) {
102
+ try {
103
+ subscription.observer(event);
104
+ }
105
+ catch {
106
+ // Generic result capture has the same best-effort isolation as the legacy stream.
107
+ }
108
+ }
109
+ }
@@ -8,6 +8,8 @@ import type { McpSafetyOptions } from './mcp-safety.js';
8
8
  import type { McpMockApprovalRule } from './mcp-mock-approvals.js';
9
9
  import type { EvidenceEnvelope, JsonValue } from '../internal/direct-mcp-v2/types.js';
10
10
  import type { ScenarioMachineV2 } from './scenario-machine-v2.js';
11
+ import type { AgentFlowTrace } from './agent-flow.js';
12
+ export type { AgentFlowTrace, AgentFlowParticipant, AgentFlowInteraction, AgentFlowEvidenceAvailability, AgentFlowEvidenceCompleteness, AgentFlowSummary } from './agent-flow.js';
11
13
  export type AgentName = 'claude' | 'codex' | 'cursor' | 'opencode';
12
14
  export type AgentInteractionMode = 'prompt' | 'start_chat' | 'conversation';
13
15
  /** Runtime channel that actually executed the agent. */
@@ -351,6 +353,8 @@ export interface ScorerContext {
351
353
  artifacts: SessionArtifacts;
352
354
  /** Canonical host evidence for ScenarioMachineV2 scorers. */
353
355
  scenarioEvidence?: readonly EvidenceEnvelope[];
356
+ /** Protocol-neutral multi-agent execution evidence, when evaluating a flow. */
357
+ flow?: AgentFlowTrace;
354
358
  }
355
359
  export interface EvaluateOptions {
356
360
  failFast?: boolean;
package/dist/types.d.ts CHANGED
@@ -172,6 +172,8 @@ export interface TrialResult {
172
172
  scoring_duration_ms?: number;
173
173
  recorded_at?: string;
174
174
  agent?: import('./sdk/types.js').AgentExecutionMetadata;
175
+ flow_summary?: import('./sdk/types.js').AgentFlowSummary;
176
+ flow_trace?: import('./sdk/types.js').AgentFlowTrace;
175
177
  conversation?: {
176
178
  turns: ConversationTurn[];
177
179
  total_turns: number;
@@ -211,12 +213,12 @@ export interface ComparisonContract {
211
213
  unavailable_reasons?: ComparisonUnavailableReason[];
212
214
  }
213
215
  /**
214
- * TrialResult with `session_log` and `conversation` stripped. These fields
216
+ * TrialResult with large trace fields stripped. These fields
215
217
  * live only in the per-group trace files; the consolidated results.json keeps
216
218
  * the rest so consumers (preview, `pathgrade report`) can compute summaries
217
219
  * without loading trace data.
218
220
  */
219
- export type StrippedTrialResult = Omit<TrialResult, 'session_log' | 'conversation'>;
221
+ export type StrippedTrialResult = Omit<TrialResult, 'session_log' | 'conversation' | 'flow_trace'>;
220
222
  /**
221
223
  * Per-group entry in the consolidated `.pathgrade/results.json` report.
222
224
  */
@@ -0,0 +1,31 @@
1
+ # Evaluating agent flows
2
+
3
+ Use `evaluateFlow()` when the subject is an already-observed interaction graph rather than a Pathgrade-managed coding agent. The input is protocol-neutral, so an adapter can project A2A or another orchestration protocol into the same evidence contract.
4
+
5
+ ```typescript
6
+ import { check, evaluateFlow, type AgentFlowTrace } from '@wix/pathgrade';
7
+
8
+ const flow: AgentFlowTrace = {
9
+ version: 1,
10
+ protocol: 'a2a',
11
+ rootParticipantId: 'planner',
12
+ participants: [{ id: 'planner' }, { id: 'flights' }],
13
+ interactions: [{
14
+ id: 'delegate-1', sequence: 1,
15
+ sourceParticipantId: 'planner', targetParticipantId: 'flights',
16
+ operation: 'message/send', state: 'completed',
17
+ }],
18
+ outcome: { state: 'completed' },
19
+ completeness: {
20
+ topology: 'complete', outcomes: 'partial', timing: 'unavailable',
21
+ usage: 'unavailable', runtimeIdentity: 'partial',
22
+ },
23
+ };
24
+
25
+ await evaluateFlow(flow, [
26
+ check('delegated flight search', ({ flow }) =>
27
+ flow?.interactions.some(item => item.targetParticipantId === 'flights') === true),
28
+ ]);
29
+ ```
30
+
31
+ `check()`, `score()`, and `judge()` work with flow evidence through `ScorerContext.flow`. `toolUsage()` is agent-only and is rejected explicitly for flows. Consolidated reports retain a compact `flow_summary`; the full `flow_trace` stays in the trace artifact. Completeness fields describe evidence availability and must not be read as inferred downstream success or timing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/pathgrade",
3
- "version": "1.0.36",
3
+ "version": "1.0.38",
4
4
  "packageManager": "yarn@4.12.0",
5
5
  "description": "Evaluate whether AI agents discover and use your skills correctly",
6
6
  "exports": {
@@ -64,6 +64,7 @@
64
64
  "!dist/**/*.d.ts.map",
65
65
  "bin/",
66
66
  "docs/OPENAI_OAUTH_JUDGE.md",
67
+ "docs/agent-flow-evaluation.md",
67
68
  "templates/",
68
69
  "README.md"
69
70
  ],
@@ -141,5 +142,5 @@
141
142
  "typescript": "^5.9.3",
142
143
  "zod": "4.3.6"
143
144
  },
144
- "falconPackageHash": "598b0b5b60df2e9caafd91fa570ae1a473f6f61f29d14bdc8204ea46"
145
+ "falconPackageHash": "199b36204420a97fad83bac6e53d8b614d2b6592cb701e7db41d3bad"
145
146
  }