@wix/pathgrade 1.0.37 → 1.0.39

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.
Files changed (53) hide show
  1. package/README.md +3 -1
  2. package/dist/adapter-kit/index.d.ts +2 -2
  3. package/dist/adapter-kit/index.js +1 -1
  4. package/dist/adapters/jest/lifecycle.js +4 -2
  5. package/dist/adapters/node-test/index.js +4 -2
  6. package/dist/agents/claude/sdk-message-projector.js +4 -5
  7. package/dist/agents/claude/tool-permission-bridge.js +2 -1
  8. package/dist/agents/codex-app-server/item-projection.js +2 -2
  9. package/dist/agents/codex-app-server/mcp-approval-correlator.js +3 -2
  10. package/dist/agents/opencode.js +4 -5
  11. package/dist/commands/report.js +5 -25
  12. package/dist/internal/direct-mcp-v2/acp-author-projector.js +20 -7
  13. package/dist/reporters/cli.js +20 -1
  14. package/dist/reporters/github-comment.js +18 -7
  15. package/dist/reporters/loader.d.ts +4 -0
  16. package/dist/reporters/loader.js +26 -9
  17. package/dist/reporting/comparison-contract.js +20 -6
  18. package/dist/reporting/core.js +18 -8
  19. package/dist/reporting/report-parser.js +57 -2
  20. package/dist/reporting/types.d.ts +2 -1
  21. package/dist/runners/adapter.d.ts +4 -2
  22. package/dist/runners/lifecycle-hooks.js +6 -1
  23. package/dist/runners/orchestrator.js +3 -4
  24. package/dist/runners/repeated-invocation.js +2 -5
  25. package/dist/runners/report-projection.js +1 -0
  26. package/dist/runners/vitest-lifecycle.d.ts +1 -0
  27. package/dist/runners/vitest-lifecycle.js +21 -4
  28. package/dist/sdk/agent-flow.d.ts +51 -0
  29. package/dist/sdk/agent-flow.js +23 -0
  30. package/dist/sdk/agent.js +10 -1
  31. package/dist/sdk/evaluate.d.ts +2 -1
  32. package/dist/sdk/evaluate.js +114 -4
  33. package/dist/sdk/index.d.ts +4 -2
  34. package/dist/sdk/index.js +2 -2
  35. package/dist/sdk/judge-prompt-builder.js +11 -7
  36. package/dist/sdk/lifecycle.d.ts +3 -1
  37. package/dist/sdk/lifecycle.js +68 -5
  38. package/dist/sdk/mcp-event-input.d.ts +1 -0
  39. package/dist/sdk/mcp-event-input.js +3 -0
  40. package/dist/sdk/mcp-evidence.js +16 -3
  41. package/dist/sdk/mcp-safety.js +2 -2
  42. package/dist/sdk/result-capture.d.ts +14 -1
  43. package/dist/sdk/result-capture.js +49 -0
  44. package/dist/sdk/scripted-mcp-events.js +3 -2
  45. package/dist/sdk/tool-event-log.js +18 -3
  46. package/dist/sdk/tool-event-secrets.d.ts +4 -0
  47. package/dist/sdk/tool-event-secrets.js +33 -2
  48. package/dist/sdk/types.d.ts +6 -0
  49. package/dist/tool-event-results.d.ts +3 -0
  50. package/dist/tool-event-results.js +153 -23
  51. package/dist/types.d.ts +13 -7
  52. package/docs/agent-flow-evaluation.md +31 -0
  53. package/package.json +3 -2
@@ -6,18 +6,44 @@ export function parsePathgradeReport(value) {
6
6
  || (value.version === 2 && (typeof value.overall_mean_reward !== 'number' || !validAttemptCounts(value)))
7
7
  || !optionalNumber(value.threshold)
8
8
  || (value.status !== 'pass' && value.status !== 'fail')
9
+ || !optionalGateStatus(value.runner_status)
10
+ || !optionalThresholdStatus(value.threshold_status)
9
11
  || (value.run_kind !== undefined && value.run_kind !== 'evaluation' && value.run_kind !== 'no-affected')
10
12
  || !Array.isArray(value.groups)
11
13
  || !value.groups.every(group => isReportGroup(group, value.version))
12
14
  || !isSelection(value.selection)) {
13
15
  throw new Error('PathGrade results.json is missing or has an unsupported schema');
14
16
  }
15
- return value;
17
+ const threshold = value.threshold;
18
+ const groups = value.groups.map(group => {
19
+ const runnerStatus = deriveRunnerStatus(group);
20
+ const thresholdStatus = deriveThresholdStatus(group.mean_reward ?? group.pass_rate, threshold);
21
+ return {
22
+ ...group,
23
+ runner_status: runnerStatus,
24
+ threshold_status: thresholdStatus,
25
+ status: composeStatus(runnerStatus, thresholdStatus),
26
+ };
27
+ });
28
+ const legacyFailure = value.runner_status === undefined && value.status === 'fail' && groups.length === 0;
29
+ const runnerStatus = value.runner_status !== 'fail' && !legacyFailure
30
+ && (groups.length > 0 || value.runner_status === 'pass' || value.status === 'pass')
31
+ && groups.every(group => group.runner_status === 'pass') ? 'pass' : 'fail';
32
+ const thresholdStatus = deriveThresholdStatus(value.overall_mean_reward ?? value.overall_pass_rate, threshold);
33
+ return {
34
+ ...value,
35
+ groups,
36
+ runner_status: runnerStatus,
37
+ threshold_status: thresholdStatus,
38
+ status: composeStatus(runnerStatus, thresholdStatus),
39
+ };
16
40
  }
17
41
  function isReportGroup(value, version) {
18
42
  return isRecord(value)
19
43
  && typeof value.task === 'string'
20
44
  && (version === 1 ? legacyMetrics(value) : schemaV2Metrics(value))
45
+ && optionalGateStatus(value.runner_status)
46
+ && optionalThresholdStatus(value.threshold_status)
21
47
  && optionalString(value.source_file)
22
48
  && typeof value.trace_file === 'string'
23
49
  && Array.isArray(value.skills_used)
@@ -26,6 +52,34 @@ function isReportGroup(value, version) {
26
52
  && value.trials.every(isTrial)
27
53
  && (value.comparison_contract === undefined || isComparisonContract(value.comparison_contract));
28
54
  }
55
+ function deriveRunnerStatus(value) {
56
+ if (value.runner_status === 'fail')
57
+ return 'fail';
58
+ const outcomes = value.trials
59
+ .map(trial => trial.runner_outcome)
60
+ .filter(outcome => outcome !== undefined);
61
+ if (outcomes.length > 0)
62
+ return outcomes.every(outcome => outcome === 'passed') ? 'pass' : 'fail';
63
+ if (value.runner_status === 'pass')
64
+ return value.runner_status;
65
+ if (value.status === 'pass' || value.status === 'fail')
66
+ return value.status;
67
+ return value.pass_rate === 1 ? 'pass' : 'fail';
68
+ }
69
+ function deriveThresholdStatus(reward, threshold) {
70
+ if (threshold === undefined)
71
+ return 'not_configured';
72
+ return typeof reward === 'number' && reward >= threshold ? 'pass' : 'fail';
73
+ }
74
+ function composeStatus(runnerStatus, thresholdStatus) {
75
+ return runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass';
76
+ }
77
+ function optionalGateStatus(value) {
78
+ return value === undefined || value === 'pass' || value === 'fail';
79
+ }
80
+ function optionalThresholdStatus(value) {
81
+ return optionalGateStatus(value) || value === 'not_configured';
82
+ }
29
83
  function isTrial(value) {
30
84
  return isRecord(value)
31
85
  && typeof value.trial_id === 'number'
@@ -40,7 +94,8 @@ function isTrial(value) {
40
94
  && value.scorer_results.every(scorer => isRecord(scorer)
41
95
  && typeof scorer.scorer_type === 'string'
42
96
  && typeof scorer.score === 'number'
43
- && typeof scorer.weight === 'number');
97
+ && typeof scorer.weight === 'number'
98
+ && (scorer.status === undefined || scorer.status === 'ok' || scorer.status === 'error' || scorer.status === 'skipped'));
44
99
  }
45
100
  function legacyMetrics(value) {
46
101
  return typeof value.pass_rate === 'number'
@@ -1,9 +1,10 @@
1
1
  import type { DiagnosticsReport } from '../sdk/diagnostics.js';
2
2
  import type { AgentExecutionMetadata, EvaluationResultKind } from '../sdk/types.js';
3
- import type { AttemptOutcome } from '../runners/model.js';
3
+ import type { AttemptOutcome, RunStatus } from '../runners/model.js';
4
4
  import type { PathgradeReport, PathgradeSelectionReport, TrialResult } from '../types.js';
5
5
  export type ReportCaseState = 'passed' | 'failed' | 'skipped' | 'pending';
6
6
  export interface ReportRunInput {
7
+ runStatus?: RunStatus;
7
8
  threshold?: number;
8
9
  selection?: PathgradeSelectionReport;
9
10
  attemptsRequested?: number;
@@ -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(),
@@ -43,8 +43,8 @@ export async function runWithAdapter(input) {
43
43
  if (loadedSelection) {
44
44
  built = buildPathgradeReport({
45
45
  threshold: options.threshold,
46
+ ...reportInput,
46
47
  selection: loadedSelection,
47
- groups: reportInput.groups,
48
48
  });
49
49
  }
50
50
  const mode = options.reporterMode ?? 'cli';
@@ -56,14 +56,13 @@ export async function runWithAdapter(input) {
56
56
  if (mode === 'browser') {
57
57
  await options.openBrowser?.();
58
58
  }
59
- if (options.threshold != null && built.report.status === 'fail') {
59
+ if (options.threshold != null && built.report.threshold_status === 'fail') {
60
60
  options.onThresholdFailure?.({
61
61
  overallPassRate: built.report.overall_pass_rate,
62
62
  threshold: options.threshold,
63
63
  });
64
- return runExitCode === 0 ? 1 : runExitCode;
65
64
  }
66
- return runExitCode;
65
+ return built.report.status === 'fail' && runExitCode === 0 ? 1 : runExitCode;
67
66
  }
68
67
  finally {
69
68
  await lifecycle.cleanupRun();
@@ -80,13 +80,10 @@ export function withRepeatedAttempts(input) {
80
80
  if (input.config.reporter === 'browser') {
81
81
  await (input.openBrowser ?? openBrowserViewer)();
82
82
  }
83
- if (input.config.ci.threshold != null && built.report.status === 'fail') {
83
+ if (input.config.ci.threshold != null && built.report.threshold_status === 'fail') {
84
84
  process.stdout.write(`\n CI THRESHOLD FAILED avg score ${built.report.overall_pass_rate.toFixed(3)} < threshold ${input.config.ci.threshold}\n\n`);
85
- resultCode = firstNonzeroExit || 1;
86
- }
87
- else {
88
- resultCode = firstNonzeroExit;
89
85
  }
86
+ resultCode = firstNonzeroExit || (built.report.status === 'fail' ? 1 : 0);
90
87
  }
91
88
  }
92
89
  catch (error) {
@@ -41,6 +41,7 @@ export function projectNormalizedRunSnapshotToReportInput(snapshot, options = {}
41
41
  }
42
42
  return {
43
43
  ...(options.selection ? { selection: options.selection } : {}),
44
+ runStatus: snapshot.model.run.status,
44
45
  attemptsRequested: snapshot.model.run.attemptsRequested ?? 1,
45
46
  attemptsCompleted: snapshot.model.run.attemptsCompleted ?? 1,
46
47
  groups: [...groups.values()],
@@ -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
+ }
package/dist/sdk/agent.js CHANGED
@@ -20,6 +20,8 @@ import { collectOpenCodeMcpToolNames } from '../agents/opencode/contract.js';
20
20
  import { collectSensitiveEnvValues } from '../tool-event-results.js';
21
21
  import { createAskUserHandler } from './ask-bus/handler.js';
22
22
  import { resolveAgentRuntimeOptions } from './agent-runtime-options.js';
23
+ import { extractToolEventsFromLog } from '../tool-events.js';
24
+ import { clearToolEventRuntimeMetadata } from './tool-event-secrets.js';
23
25
  /**
24
26
  * Test-only injection point: override the sink used by the next emitter
25
27
  * built inside `createAgent`. Pass `null` to restore the default (stderr).
@@ -392,7 +394,14 @@ class AgentImpl {
392
394
  }
393
395
  }
394
396
  finally {
395
- await this.ws.dispose();
397
+ try {
398
+ await this.ws.dispose();
399
+ }
400
+ finally {
401
+ for (const event of extractToolEventsFromLog(this._log)) {
402
+ clearToolEventRuntimeMetadata(event);
403
+ }
404
+ }
396
405
  }
397
406
  }
398
407
  }
@@ -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,14 +1,18 @@
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';
8
8
  import { sandboxExec } from '../providers/sandbox-exec.js';
9
9
  import { buildTranscript, loadRunSnapshot, WorkspaceMissingError } from './snapshots.js';
10
+ import { sanitizePersistenceValue } from '../tool-event-results.js';
11
+ import { getOriginalMcpInput } from './mcp-event-input.js';
12
+ import { cloneToolEventWithRuntimeMetadata, collectToolEventSensitiveValues, } from './tool-event-secrets.js';
10
13
  import fs from 'fs-extra';
11
14
  import path from 'path';
15
+ import { summarizeFlow } from './agent-flow.js';
12
16
  export class EvalScorerError extends Error {
13
17
  scorerErrors;
14
18
  result;
@@ -19,6 +23,60 @@ export class EvalScorerError extends Error {
19
23
  this.scorerErrors = scorerErrors;
20
24
  }
21
25
  }
26
+ export async function evaluateFlow(flow, scorers, opts) {
27
+ if (scorers.some(scorer => scorer.type === 'tool_usage')) {
28
+ throw new TypeError('tool_usage scorers require an Agent and cannot evaluate an AgentFlowTrace');
29
+ }
30
+ if (scorers.some(scorer => scorer.type === 'judge' && scorer.tools && scorer.tools.length > 0)) {
31
+ throw new TypeError('tool-enabled judge scorers require an Agent workspace and cannot evaluate an AgentFlowTrace');
32
+ }
33
+ const trackedLLM = opts?.llm ?? createLLMClient({ adapters: [{
34
+ name: 'runtime', isAvailable: async () => true,
35
+ call: (prompt, callOpts) => getRuntime().llm.call(prompt, callOpts),
36
+ }] });
37
+ const unavailable = async () => {
38
+ throw new WorkspaceMissingError('AgentFlowTrace does not include a workspace');
39
+ };
40
+ const ctx = {
41
+ workspace: '',
42
+ log: [],
43
+ transcript: JSON.stringify(flow),
44
+ toolEvents: [],
45
+ runCommand: unavailable,
46
+ artifacts: { list: () => [], read: unavailable, latest: async () => null },
47
+ flow,
48
+ };
49
+ const scoringStartedAt = performance.now();
50
+ const beforeTokenUsage = trackedLLM.tokenUsage ?? { inputTokens: 0, outputTokens: 0 };
51
+ const { result: evalResult, tokens: deltaTokenUsage } = trackedLLM.measure
52
+ ? await trackedLLM.measure(() => evaluateWithContext(ctx, scorers, { ...opts, llm: trackedLLM }))
53
+ : await (async () => {
54
+ const result = await evaluateWithContext(ctx, scorers, { ...opts, llm: trackedLLM });
55
+ const afterTokenUsage = trackedLLM.tokenUsage ?? beforeTokenUsage;
56
+ return { result, tokens: {
57
+ inputTokens: Math.max(0, afterTokenUsage.inputTokens - beforeTokenUsage.inputTokens),
58
+ outputTokens: Math.max(0, afterTokenUsage.outputTokens - beforeTokenUsage.outputTokens),
59
+ } };
60
+ })();
61
+ const scorerRevision = createScorerRevision(scorers);
62
+ const recordedResult = {
63
+ ...evalResult,
64
+ tokenUsage: deltaTokenUsage,
65
+ resultKind: 'evaluated',
66
+ scoringDurationMs: Math.max(0, performance.now() - scoringStartedAt),
67
+ recordedAt: new Date().toISOString(),
68
+ ...(opts?.evaluationDefinitionKey ? { evaluationDefinitionKey: opts.evaluationDefinitionKey } : {}),
69
+ ...(scorerRevision ? { scorerRevision } : {}),
70
+ trial: {
71
+ ...buildTrialResult([], { ...evalResult, tokenUsage: deltaTokenUsage }),
72
+ flow_summary: summarizeFlow(flow),
73
+ flow_trace: structuredClone(flow),
74
+ },
75
+ };
76
+ emitFlowEvalResult({ result: recordedResult, flow });
77
+ maybeThrowOnScorerErrors(recordedResult, opts?.onScorerError ?? 'skip');
78
+ return recordedResult;
79
+ }
22
80
  /**
23
81
  * Run scorers against a trial and compute a weighted average score.
24
82
  *
@@ -99,6 +157,9 @@ export function evaluateStepScorers(agent, scorers, opts) {
99
157
  return evaluateAgent(agent, scorers, opts, false);
100
158
  }
101
159
  async function fromSnapshot(snapshotPath, scorers, opts) {
160
+ if (opts?.deterministicToolEvidence === 'live') {
161
+ throw new TypeError("evaluate.fromSnapshot() does not support deterministicToolEvidence: 'live'");
162
+ }
102
163
  const snapshot = await loadRunSnapshot(snapshotPath);
103
164
  const trackedLLM = opts?.llm ?? createLLMClient({ adapters: [{
104
165
  name: 'runtime', isAvailable: async () => true,
@@ -146,6 +207,9 @@ async function evaluateWithContext(ctx, scorers, opts) {
146
207
  call: (prompt, callOpts) => getRuntime().llm.call(prompt, callOpts),
147
208
  }] });
148
209
  const onScorerError = opts?.onScorerError ?? 'skip';
210
+ const deterministicCtx = opts?.deterministicToolEvidence === 'live'
211
+ ? createLiveDeterministicContext(ctx)
212
+ : ctx;
149
213
  const phase1 = [];
150
214
  const phase2 = [];
151
215
  const phase3 = [];
@@ -164,7 +228,7 @@ async function evaluateWithContext(ctx, scorers, opts) {
164
228
  }
165
229
  }
166
230
  const results = [];
167
- const phase1Results = await Promise.all(phase1.map((g) => runScorer(g, ctx)));
231
+ const phase1Results = await Promise.all(phase1.map((g) => runScorer(g, deterministicCtx)));
168
232
  results.push(...phase1Results);
169
233
  const anyCheckFailed = failFast && phase1Results.some((r) => r.type === 'check' && r.status !== 'error' && r.score === 0);
170
234
  if (anyCheckFailed) {
@@ -175,7 +239,7 @@ async function evaluateWithContext(ctx, scorers, opts) {
175
239
  else {
176
240
  const judgeResults = await runJudgePipeline(phase2, ctx, { llm: trackedLLM });
177
241
  results.push(...judgeResults);
178
- const phase3Results = await Promise.all(phase3.map((g) => runScorer(g, ctx)));
242
+ const phase3Results = await Promise.all(phase3.map((g) => runScorer(g, deterministicCtx)));
179
243
  results.push(...phase3Results);
180
244
  }
181
245
  const tokenUsage = trackedLLM.tokenUsage ?? { inputTokens: 0, outputTokens: 0 };
@@ -185,12 +249,58 @@ async function evaluateWithContext(ctx, scorers, opts) {
185
249
  const totalWeight = scoringResults.reduce((sum, r) => sum + r.weight, 0);
186
250
  const weightedSum = scoringResults.reduce((sum, r) => sum + r.score * r.weight, 0);
187
251
  const score = totalWeight > 0 ? weightedSum / totalWeight : 0;
188
- return {
252
+ return sanitizePersistenceValue({
189
253
  score,
190
254
  scorers: results,
191
255
  tokenUsage,
256
+ }, collectToolEventSensitiveValues(ctx.toolEvents));
257
+ }
258
+ function createLiveDeterministicContext(ctx) {
259
+ return {
260
+ ...ctx,
261
+ toolEvents: ctx.toolEvents.map((event) => {
262
+ const originalInput = event.action === 'mcp_tool_call'
263
+ ? getOriginalMcpInput(event)
264
+ : undefined;
265
+ const mcp = normalizedMcpClassification(event);
266
+ return originalInput
267
+ ? cloneToolEventWithRuntimeMetadata(event, { arguments: {
268
+ ...event.arguments,
269
+ server: event.arguments?.server,
270
+ tool: event.arguments?.tool,
271
+ status: event.arguments?.status,
272
+ ...structuredClone(originalInput),
273
+ }, ...(mcp ? { mcp } : {}) })
274
+ : event;
275
+ }),
192
276
  };
193
277
  }
278
+ function normalizedMcpClassification(event) {
279
+ if (event.mcp)
280
+ return event.mcp;
281
+ const args = event.arguments ?? {};
282
+ const separator = event.providerToolName.indexOf('.');
283
+ const serverName = typeof args.server === 'string' ? args.server
284
+ : separator > 0 ? event.providerToolName.slice(0, separator) : undefined;
285
+ const toolName = typeof args.tool === 'string' ? args.tool
286
+ : separator > 0 && separator < event.providerToolName.length - 1
287
+ ? event.providerToolName.slice(separator + 1) : undefined;
288
+ const status = event.status ?? args.status;
289
+ if (!serverName || !toolName)
290
+ return undefined;
291
+ if (status === 'completed')
292
+ return { serverName, toolName, invocation: 'confirmed', outcome: 'completed' };
293
+ if (status === 'error' || status === 'failed') {
294
+ return { serverName, toolName, invocation: 'confirmed', outcome: 'tool_error' };
295
+ }
296
+ if (status === 'user_denied' || status === 'policy_denied') {
297
+ return { serverName, toolName, invocation: 'not_invoked', outcome: status };
298
+ }
299
+ if (status === 'protocol_error') {
300
+ return { serverName, toolName, invocation: 'unknown', outcome: 'protocol_error' };
301
+ }
302
+ return undefined;
303
+ }
194
304
  function maybeThrowOnScorerErrors(result, mode) {
195
305
  if (mode !== 'fail')
196
306
  return;
@@ -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,13 +1,15 @@
1
1
  import { sanitizePersistenceValue } from '../tool-event-results.js';
2
- import { getToolEventSensitiveValues } from './tool-event-secrets.js';
2
+ import { collectToolEventSensitiveValues, } from './tool-event-secrets.js';
3
3
  export function buildJudgePrompt(scorer, ctx, input) {
4
4
  const sections = [];
5
- sections.push(`## Session Transcript\n${ctx.transcript}`);
5
+ const sensitiveValues = collectToolEventSensitiveValues(ctx.toolEvents);
6
+ sections.push(`## Session Transcript\n${sanitizePersistenceValue(ctx.transcript, sensitiveValues)}`);
6
7
  if (scorer.includeToolEvents && ctx.toolEvents.length > 0) {
7
8
  sections.push(`## Tool Events\n${formatToolEvents(ctx)}`);
8
9
  }
9
10
  if (input) {
10
- for (const [key, value] of Object.entries(input)) {
11
+ const sanitizedInput = sanitizePersistenceValue(input, sensitiveValues);
12
+ for (const [key, value] of Object.entries(sanitizedInput)) {
11
13
  const body = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
12
14
  sections.push(`## ${key}\n${body}`);
13
15
  }
@@ -22,10 +24,10 @@ ${scorer.rubric}
22
24
  Respond with ONLY a JSON object: {"score": <number>, "details": "<brief explanation>"}`;
23
25
  }
24
26
  function formatToolEvents(ctx) {
27
+ const sensitiveValues = collectToolEventSensitiveValues(ctx.toolEvents);
25
28
  return ctx.toolEvents
26
29
  .map((event) => {
27
30
  const turn = event.turnNumber ? `turn ${event.turnNumber}` : 'instruction';
28
- const sensitiveValues = getToolEventSensitiveValues(event);
29
31
  const details = [
30
32
  event.status ? ` status: ${event.status}` : undefined,
31
33
  event.mcp ? ` mcp: ${JSON.stringify(event.mcp)}` : undefined,
@@ -41,7 +43,8 @@ function formatToolEvents(ctx) {
41
43
  }
42
44
  export function buildBatchedJudgePrompt(judges, ctx, inputs) {
43
45
  const sections = [];
44
- sections.push(`## Session Transcript\n${ctx.transcript}`);
46
+ const sensitiveValues = collectToolEventSensitiveValues(ctx.toolEvents);
47
+ sections.push(`## Session Transcript\n${sanitizePersistenceValue(ctx.transcript, sensitiveValues)}`);
45
48
  if (judges.some((j) => j.includeToolEvents) && ctx.toolEvents.length > 0) {
46
49
  sections.push(`## Tool Events\n${formatToolEvents(ctx)}`);
47
50
  }
@@ -49,7 +52,8 @@ export function buildBatchedJudgePrompt(judges, ctx, inputs) {
49
52
  const parts = [`### Rubric ${i + 1}: "${j.name}"\n${j.rubric}`];
50
53
  const input = inputs[i];
51
54
  if (input) {
52
- for (const [key, value] of Object.entries(input)) {
55
+ const sanitizedInput = sanitizePersistenceValue(input, sensitiveValues);
56
+ for (const [key, value] of Object.entries(sanitizedInput)) {
53
57
  const body = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
54
58
  parts.push(`#### ${key}\n${body}`);
55
59
  }
@@ -77,7 +81,7 @@ export function buildToolUseJudgePrompt(scorer, ctx) {
77
81
  ].join('\n');
78
82
  const parts = [];
79
83
  parts.push('## Session Transcript');
80
- parts.push(ctx.transcript);
84
+ parts.push(sanitizePersistenceValue(ctx.transcript, collectToolEventSensitiveValues(ctx.toolEvents)));
81
85
  if (scorer.includeToolEvents && ctx.toolEvents.length > 0) {
82
86
  parts.push('## Tool Events');
83
87
  parts.push(formatToolEvents(ctx));