@wix/pathgrade 0.37.0 → 1.0.0

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 (116) hide show
  1. package/README.md +116 -32
  2. package/dist/adapter-kit/index.d.ts +17 -0
  3. package/dist/adapter-kit/index.js +14 -0
  4. package/dist/adapters/jest/index.d.ts +3 -0
  5. package/dist/adapters/jest/index.js +3 -0
  6. package/dist/adapters/jest/invocation-adapter.d.ts +15 -0
  7. package/dist/adapters/jest/invocation-adapter.js +115 -0
  8. package/dist/adapters/jest/lifecycle.d.ts +21 -0
  9. package/dist/adapters/jest/lifecycle.js +62 -0
  10. package/dist/adapters/jest/metadata.d.ts +6 -0
  11. package/dist/adapters/jest/metadata.js +35 -0
  12. package/dist/adapters/jest/reporter.cjs +10 -0
  13. package/dist/adapters/jest/reporter.d.ts +5 -0
  14. package/dist/adapters/jest/reporter.js +67 -0
  15. package/dist/adapters/jest/results.d.ts +31 -0
  16. package/dist/adapters/jest/results.js +144 -0
  17. package/dist/adapters/jest/runner-adapter.d.ts +9 -0
  18. package/dist/adapters/jest/runner-adapter.js +49 -0
  19. package/dist/adapters/jest/setup.d.ts +1 -0
  20. package/dist/adapters/jest/setup.js +10 -0
  21. package/dist/adapters/node-test/index.d.ts +8 -0
  22. package/dist/adapters/node-test/index.js +106 -0
  23. package/dist/adapters/node-test/invocation-adapter.d.ts +5 -0
  24. package/dist/adapters/node-test/invocation-adapter.js +47 -0
  25. package/dist/adapters/node-test/runner-adapter.d.ts +2 -0
  26. package/dist/adapters/node-test/runner-adapter.js +116 -0
  27. package/dist/adapters/vitest/index.d.ts +9 -0
  28. package/dist/adapters/vitest/index.js +82 -0
  29. package/dist/adapters/vitest/lifecycle.d.ts +24 -0
  30. package/dist/adapters/vitest/lifecycle.js +41 -0
  31. package/dist/adapters/vitest/reporter.d.ts +12 -0
  32. package/dist/adapters/vitest/reporter.js +75 -0
  33. package/dist/adapters/vitest/setup.d.ts +1 -0
  34. package/dist/adapters/vitest/setup.js +3 -0
  35. package/dist/agents/codex-app-server/agent.js +5 -1
  36. package/dist/agents/codex-app-server/protocol/index.js +3 -3
  37. package/dist/agents/codex-app-server/turn-completion.d.ts +1 -0
  38. package/dist/agents/codex-app-server/turn-completion.js +9 -0
  39. package/dist/commands/affected.js +22 -15
  40. package/dist/commands/run-args.d.ts +4 -3
  41. package/dist/commands/run-args.js +12 -6
  42. package/dist/commands/run-changed.d.ts +4 -4
  43. package/dist/commands/run-changed.js +45 -58
  44. package/dist/config/pathgrade.d.ts +48 -0
  45. package/dist/config/pathgrade.js +204 -0
  46. package/dist/pathgrade.d.ts +2 -2
  47. package/dist/pathgrade.js +35 -34
  48. package/dist/plugin/index.d.ts +2 -9
  49. package/dist/plugin/index.js +1 -81
  50. package/dist/plugin/lifecycle.d.ts +1 -45
  51. package/dist/plugin/lifecycle.js +1 -146
  52. package/dist/plugin/reporter.d.ts +1 -37
  53. package/dist/plugin/reporter.js +1 -280
  54. package/dist/plugin/setup.d.ts +1 -1
  55. package/dist/plugin/setup.js +1 -3
  56. package/dist/providers/copy-filter.d.ts +5 -0
  57. package/dist/providers/copy-filter.js +9 -0
  58. package/dist/providers/sandbox.js +2 -2
  59. package/dist/providers/workspace.d.ts +6 -0
  60. package/dist/providers/workspace.js +34 -14
  61. package/dist/reporters/diagnostics.d.ts +2 -40
  62. package/dist/reporters/diagnostics.js +1 -80
  63. package/dist/reporters/report-summary.d.ts +6 -0
  64. package/dist/reporters/report-summary.js +29 -0
  65. package/dist/reporting/artifacts.d.ts +2 -0
  66. package/dist/reporting/artifacts.js +20 -0
  67. package/dist/reporting/core.d.ts +2 -0
  68. package/dist/reporting/core.js +173 -0
  69. package/dist/reporting/types.d.ts +57 -0
  70. package/dist/reporting/types.js +1 -0
  71. package/dist/reporting/vitest-edge.d.ts +1 -0
  72. package/dist/reporting/vitest-edge.js +1 -0
  73. package/dist/runners/adapter-loader.d.ts +17 -0
  74. package/dist/runners/adapter-loader.js +67 -0
  75. package/dist/runners/adapter.d.ts +65 -0
  76. package/dist/runners/adapter.js +1 -0
  77. package/dist/runners/invocation.d.ts +10 -0
  78. package/dist/runners/invocation.js +1 -0
  79. package/dist/runners/lifecycle-hooks.d.ts +2 -0
  80. package/dist/runners/lifecycle-hooks.js +18 -0
  81. package/dist/runners/model-builders.d.ts +4 -0
  82. package/dist/runners/model-builders.js +67 -0
  83. package/dist/runners/model-validation.d.ts +12 -0
  84. package/dist/runners/model-validation.js +213 -0
  85. package/dist/runners/model.d.ts +117 -0
  86. package/dist/runners/model.js +1 -0
  87. package/dist/runners/orchestrator.d.ts +30 -0
  88. package/dist/runners/orchestrator.js +74 -0
  89. package/dist/runners/report-projection.d.ts +6 -0
  90. package/dist/runners/report-projection.js +62 -0
  91. package/dist/runners/selection.d.ts +6 -0
  92. package/dist/runners/selection.js +10 -0
  93. package/dist/runners/vitest-adapter.d.ts +8 -0
  94. package/dist/runners/vitest-adapter.js +108 -0
  95. package/dist/runners/vitest-invocation.d.ts +10 -0
  96. package/dist/runners/vitest-invocation.js +44 -0
  97. package/dist/runners/vitest-lifecycle.d.ts +43 -0
  98. package/dist/runners/vitest-lifecycle.js +150 -0
  99. package/dist/sdk/agent.js +15 -16
  100. package/dist/sdk/case-context.d.ts +20 -0
  101. package/dist/sdk/case-context.js +31 -0
  102. package/dist/sdk/diagnostics.d.ts +40 -0
  103. package/dist/sdk/diagnostics.js +80 -0
  104. package/dist/sdk/eval-runtime.d.ts +4 -0
  105. package/dist/sdk/eval-runtime.js +11 -1
  106. package/dist/sdk/evaluate.js +2 -1
  107. package/dist/sdk/index.d.ts +2 -0
  108. package/dist/sdk/index.js +1 -0
  109. package/dist/sdk/lifecycle.d.ts +40 -0
  110. package/dist/sdk/lifecycle.js +170 -0
  111. package/dist/sdk/result-capture.d.ts +18 -0
  112. package/dist/sdk/result-capture.js +49 -0
  113. package/dist/sdk/types.d.ts +1 -1
  114. package/dist/types.d.ts +2 -2
  115. package/dist/utils/llm-providers/anthropic.js +9 -2
  116. package/package.json +56 -4
@@ -0,0 +1,44 @@
1
+ export function createVitestInvocationAdapter(input = {}) {
2
+ const spawnVitest = input.spawnVitest ?? defaultSpawnVitest;
3
+ return {
4
+ name: 'vitest',
5
+ async run(runInput) {
6
+ if (runInput.selectedFiles && hasPassWithNoTests(runInput.runnerArgs)) {
7
+ process.stderr.write('pathgrade run: --passWithNoTests cannot be used with pathgrade run --changed. ' +
8
+ 'The command already exits 0 when no evals are selected; if selected evals resolve to no Vitest files, CI must fail.\n');
9
+ return 1;
10
+ }
11
+ const argv = [
12
+ 'run',
13
+ ...(runInput.selectedFiles ?? []),
14
+ ...runInput.runnerArgs,
15
+ ];
16
+ return await spawnVitest({
17
+ argv,
18
+ env: runInput.env,
19
+ cwd: runInput.cwd,
20
+ });
21
+ },
22
+ };
23
+ }
24
+ function hasPassWithNoTests(args) {
25
+ return args.some(arg => {
26
+ if (arg === '--passWithNoTests')
27
+ return true;
28
+ if (!arg.startsWith('--passWithNoTests='))
29
+ return false;
30
+ return arg.slice('--passWithNoTests='.length).toLowerCase() !== 'false';
31
+ });
32
+ }
33
+ async function defaultSpawnVitest(req) {
34
+ const { spawn } = await import('child_process');
35
+ return await new Promise(resolve => {
36
+ const child = spawn('npx', ['vitest', ...req.argv], {
37
+ stdio: 'inherit',
38
+ env: req.env,
39
+ cwd: req.cwd,
40
+ shell: true,
41
+ });
42
+ child.on('close', code => resolve(code ?? 0));
43
+ });
44
+ }
@@ -0,0 +1,43 @@
1
+ import type { Agent, PathgradeTestMeta } from '../sdk/types.js';
2
+ import { type ResultObserverHandle } from '../sdk/result-capture.js';
3
+ import { type CaseContextProviderHandle } from '../sdk/case-context.js';
4
+ import { type LifecycleAgentOwner } from '../sdk/lifecycle.js';
5
+ import type { AdapterLifecycleHooks, EvalResultEvent } from './adapter.js';
6
+ export type VitestAfterEachFn = (fn: (ctx: {
7
+ task: {
8
+ id: string;
9
+ meta: Record<string, unknown>;
10
+ };
11
+ }) => Promise<void>) => void;
12
+ export type VitestAfterAllFn = (fn: () => Promise<void>) => void;
13
+ export type VitestAroundEachFn = (fn: (runTest: () => Promise<void>, ctx: {
14
+ task: {
15
+ id: string;
16
+ name: string;
17
+ meta: Record<string, unknown>;
18
+ suite?: unknown;
19
+ };
20
+ }) => Promise<void>) => void;
21
+ export interface VitestLifecycleInstallInput {
22
+ afterEach: VitestAfterEachFn;
23
+ afterAll?: VitestAfterAllFn;
24
+ aroundEach?: VitestAroundEachFn;
25
+ lifecycle?: AdapterLifecycleHooks;
26
+ subscribeToResults?: (callback: (event: EvalResultEvent) => void) => ResultObserverHandle;
27
+ installFileContextProvider?: () => CaseContextProviderHandle;
28
+ }
29
+ export interface VitestLifecycleInstallHandle {
30
+ restore(): void;
31
+ }
32
+ export declare function createVitestLifecycleHooks(): AdapterLifecycleHooks;
33
+ export declare function installVitestLifecycle(input: VitestLifecycleInstallInput): VitestLifecycleInstallHandle;
34
+ export declare function currentVitestAgentOwner(resultCaptureActive: boolean): LifecycleAgentOwner | null;
35
+ export declare function trackVitestAgent(agent: Agent, resultCaptureActive: boolean): void;
36
+ export declare function untrackVitestAgent(agent: Agent): void;
37
+ export declare function releaseVitestAgent(agent: Agent): void;
38
+ export declare function resetVitestLifecycle(handle: VitestLifecycleInstallHandle | null): void;
39
+ declare module 'vitest' {
40
+ interface TaskMeta {
41
+ pathgrade?: PathgradeTestMeta[];
42
+ }
43
+ }
@@ -0,0 +1,150 @@
1
+ import path from 'node:path';
2
+ import { subscribeToEvalResults } from '../sdk/result-capture.js';
3
+ import { getCurrentCaseContext, installCaseContextProvider, runWithCaseContext, } from '../sdk/case-context.js';
4
+ import { lifecycleCore } from '../sdk/lifecycle.js';
5
+ export function createVitestLifecycleHooks() {
6
+ return {
7
+ onResult: event => recordVitestResult(event.result, event.agent),
8
+ withCaseContext: (context, run) => runWithCaseContext(toCaseContext(context), run),
9
+ flushCase: caseId => lifecycleCore.flushCase({ caseId }),
10
+ cleanupRun: () => lifecycleCore.cleanupAll(),
11
+ };
12
+ }
13
+ export function installVitestLifecycle(input) {
14
+ const lifecycle = input.lifecycle ?? createVitestLifecycleHooks();
15
+ const resultCaptureHandle = (input.subscribeToResults ?? defaultSubscribeToResults)(event => lifecycle.onResult(event));
16
+ const fileContextHandle = (input.installFileContextProvider ?? defaultInstallFileContextProvider)();
17
+ let fileContextRestored = false;
18
+ let resultsUnsubscribed = false;
19
+ input.aroundEach?.(async (runTest, { task }) => lifecycle.withCaseContext(caseContextForTask(task), runTest));
20
+ input.afterEach(async ({ task }) => {
21
+ const results = await lifecycle.flushCase(task.id);
22
+ if (results.length > 0) {
23
+ task.meta.pathgrade = results;
24
+ }
25
+ });
26
+ input.afterAll?.(async () => {
27
+ await lifecycle.cleanupRun();
28
+ restoreFileContext();
29
+ });
30
+ return {
31
+ restore() {
32
+ restoreFileContext();
33
+ if (!resultsUnsubscribed) {
34
+ resultsUnsubscribed = true;
35
+ resultCaptureHandle.unsubscribe();
36
+ }
37
+ },
38
+ };
39
+ function restoreFileContext() {
40
+ if (fileContextRestored)
41
+ return;
42
+ fileContextRestored = true;
43
+ fileContextHandle.restore();
44
+ }
45
+ }
46
+ export function currentVitestAgentOwner(resultCaptureActive) {
47
+ const current = getCurrentCaseContext();
48
+ if (current.status === 'active') {
49
+ return current.context.scope === 'runner-case'
50
+ ? { type: 'runner-case', caseId: current.context.caseId }
51
+ : { type: 'runner-suite-shared', caseId: current.context.caseId };
52
+ }
53
+ const taskId = currentTaskId();
54
+ if (resultCaptureActive)
55
+ return null;
56
+ return taskId ? { type: 'runner-case', caseId: taskId } : { type: 'manual' };
57
+ }
58
+ export function trackVitestAgent(agent, resultCaptureActive) {
59
+ lifecycleCore.registerAgent(agent, currentVitestAgentOwner(resultCaptureActive));
60
+ }
61
+ export function untrackVitestAgent(agent) {
62
+ lifecycleCore.untrackAgent(agent);
63
+ }
64
+ export function releaseVitestAgent(agent) {
65
+ lifecycleCore.releaseAgent(agent);
66
+ }
67
+ export function resetVitestLifecycle(handle) {
68
+ lifecycleCore.reset();
69
+ handle?.restore();
70
+ }
71
+ function defaultSubscribeToResults(callback) {
72
+ return subscribeToEvalResults(({ result, agent }) => callback({ result, agent }), { owner: 'adapter', key: 'vitest-lifecycle' });
73
+ }
74
+ function defaultInstallFileContextProvider() {
75
+ return installCaseContextProvider(currentFileContext);
76
+ }
77
+ function toCaseContext(context) {
78
+ return {
79
+ caseId: context.caseId,
80
+ caseName: context.caseName,
81
+ filePath: context.filePath ?? context.sourceRef ?? '',
82
+ scope: context.scope,
83
+ };
84
+ }
85
+ function currentTaskId() {
86
+ try {
87
+ return globalThis.__vitest_worker__?.current?.id ?? '';
88
+ }
89
+ catch {
90
+ return '';
91
+ }
92
+ }
93
+ function filePathForTask(task) {
94
+ let current = task.suite;
95
+ while (current) {
96
+ if (typeof current.filepath === 'string')
97
+ return current.filepath;
98
+ current = current.suite;
99
+ }
100
+ try {
101
+ return globalThis.__vitest_worker__?.filepath ?? '';
102
+ }
103
+ catch {
104
+ return '';
105
+ }
106
+ }
107
+ function caseContextForTask(task) {
108
+ const filePath = filePathForTask(task);
109
+ return {
110
+ caseId: task.id,
111
+ caseName: task.name,
112
+ filePath,
113
+ sourceRef: filePath,
114
+ runnerNativeId: task.id,
115
+ scope: 'runner-case',
116
+ };
117
+ }
118
+ function currentFilePath() {
119
+ try {
120
+ return globalThis.__vitest_worker__?.filepath ?? '';
121
+ }
122
+ catch {
123
+ return '';
124
+ }
125
+ }
126
+ function currentFileContext() {
127
+ const filePath = currentFilePath();
128
+ if (!filePath)
129
+ return null;
130
+ return {
131
+ caseId: `file:${filePath}`,
132
+ caseName: path.basename(filePath),
133
+ filePath,
134
+ scope: 'runner-suite-shared',
135
+ };
136
+ }
137
+ function recordVitestResult(result, agent) {
138
+ const current = getCurrentCaseContext();
139
+ const taskId = currentTaskId();
140
+ const owner = lifecycleCore.getAgentOwner(agent);
141
+ if (owner?.type !== 'runner-case' && (current.status !== 'active' || current.context.scope !== 'runner-case')) {
142
+ if (!taskId) {
143
+ lifecycleCore.recordResult(result, agent);
144
+ return;
145
+ }
146
+ lifecycleCore.recordResult(result, agent, { type: 'runner-case', caseId: taskId });
147
+ return;
148
+ }
149
+ lifecycleCore.recordResult(result, agent);
150
+ }
package/dist/sdk/agent.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { prepareWorkspace } from '../providers/workspace.js';
2
2
  import { resolveAgentName, resolveCodexTransport } from './agent-resolution.js';
3
- import { lifecycle } from '../plugin/lifecycle.js';
3
+ import { lifecycleCore } from './lifecycle.js';
4
4
  import { ChatSessionImpl } from './chat.js';
5
5
  import { runConversation } from './converse.js';
6
6
  import { createPersona } from './persona.js';
@@ -10,6 +10,7 @@ import { createAgentLLM } from '../utils/llm.js';
10
10
  import { buildRunSnapshot } from './snapshots.js';
11
11
  import { buildModelAgentResultLogEntry } from './agent-result-log.js';
12
12
  import { getVisibleAssistantMessage } from './visible-turn.js';
13
+ import { getCurrentCaseContext } from './case-context.js';
13
14
  import { createVerboseEmitter } from '../reporters/verbose-emitter.js';
14
15
  import fs from 'fs-extra';
15
16
  import * as path from 'path';
@@ -293,9 +294,9 @@ class AgentImpl {
293
294
  if (this.disposed)
294
295
  return;
295
296
  this.disposed = true;
296
- // Do NOT call lifecycle.untrackAgent here flush() in afterEach needs
297
- // the agent to still be in pendingAgents so it can collect results
298
- // (token usage, scorers, diagnostics) into task.meta.pathgrade.
297
+ // Runner-owned agents stay tracked until flush consumes metadata.
298
+ // Manual agents have no runner flush, so dispose releases them.
299
+ lifecycleCore.releaseAgent(this);
299
300
  if (this.debugOpt) {
300
301
  const dest = typeof this.debugOpt === 'string'
301
302
  ? this.debugOpt
@@ -329,16 +330,14 @@ function slugify(s) {
329
330
  .replace(/^-|-$/g, '')
330
331
  .toLowerCase();
331
332
  }
332
- function resolveTestContext() {
333
- try {
334
- const worker = globalThis.__vitest_worker__;
335
- const name = worker?.current?.name ? slugify(worker.current.name) : '';
336
- const dir = worker?.filepath ? path.dirname(worker.filepath) : '';
337
- return { name, dir };
338
- }
339
- catch {
333
+ function resolveCaseDebugContext() {
334
+ const current = getCurrentCaseContext();
335
+ if (current.status !== 'active')
340
336
  return { name: '', dir: '' };
341
- }
337
+ return {
338
+ name: current.context.caseName ? slugify(current.context.caseName) : '',
339
+ dir: current.context.filePath ? path.dirname(current.context.filePath) : '',
340
+ };
342
341
  }
343
342
  export async function createAgent(opts) {
344
343
  const agentName = resolveAgentName(opts, process.env);
@@ -346,8 +345,8 @@ export async function createAgent(opts) {
346
345
  ? resolveCodexTransport(opts, process.env)
347
346
  : undefined;
348
347
  const timeoutSetting = opts.timeout ?? 300;
349
- // Capture test context now, while vitest state is available
350
- const testCtx = opts.debug ? resolveTestContext() : { name: '', dir: '' };
348
+ // Capture runner context now; adapters own installation and restoration.
349
+ const testCtx = opts.debug ? resolveCaseDebugContext() : { name: '', dir: '' };
351
350
  const { timeout: _, mcpMock, mcpConfigFile, agent: __, debug: ___, model: ____, transport: _____, mcpSafety: ______, ...rest } = opts;
352
351
  const workspace = await prepareWorkspace({
353
352
  ...rest,
@@ -367,6 +366,6 @@ export async function createAgent(opts) {
367
366
  testName: testCtx.name || undefined,
368
367
  });
369
368
  const agent = new AgentImpl(workspace, agentName, llm, timeoutSetting, opts.conversationWindow, opts.model, opts.debug, debugName, debugBaseDir, verbose, transport, opts.mcpSafety);
370
- lifecycle.trackAgent(agent);
369
+ lifecycleCore.registerAgent(agent);
371
370
  return agent;
372
371
  }
@@ -0,0 +1,20 @@
1
+ export type CaseContextScope = 'runner-case' | 'runner-suite-shared';
2
+ export interface CaseContext {
3
+ caseId: string;
4
+ caseName: string;
5
+ filePath: string;
6
+ scope: CaseContextScope;
7
+ }
8
+ export type CurrentCaseContext = {
9
+ status: 'empty';
10
+ } | {
11
+ status: 'active';
12
+ context: CaseContext;
13
+ };
14
+ export interface CaseContextProviderHandle {
15
+ restore(): void;
16
+ }
17
+ export type CaseContextProvider = () => CaseContext | null | undefined;
18
+ export declare function getCurrentCaseContext(): CurrentCaseContext;
19
+ export declare function runWithCaseContext<T>(context: CaseContext, run: () => T): T;
20
+ export declare function installCaseContextProvider(provider: CaseContextProvider): CaseContextProviderHandle;
@@ -0,0 +1,31 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const caseContextStorage = new AsyncLocalStorage();
3
+ const providers = [];
4
+ export function getCurrentCaseContext() {
5
+ const context = caseContextStorage.getStore();
6
+ if (context)
7
+ return { status: 'active', context };
8
+ for (const provider of providers.toReversed()) {
9
+ const provided = provider();
10
+ if (provided)
11
+ return { status: 'active', context: provided };
12
+ }
13
+ return { status: 'empty' };
14
+ }
15
+ export function runWithCaseContext(context, run) {
16
+ return caseContextStorage.run(context, run);
17
+ }
18
+ export function installCaseContextProvider(provider) {
19
+ providers.push(provider);
20
+ let restored = false;
21
+ return {
22
+ restore() {
23
+ if (restored)
24
+ return;
25
+ restored = true;
26
+ const index = providers.lastIndexOf(provider);
27
+ if (index >= 0)
28
+ providers.splice(index, 1);
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,40 @@
1
+ import type { LogEntry } from '../types.js';
2
+ import type { ReactionFiredEntry, TurnDetail } from './types.js';
3
+ import { type RuntimePolicyAuditEntry } from './runtime-policy.js';
4
+ export interface DiagnosticsScorer {
5
+ name: string;
6
+ type: string;
7
+ score: number;
8
+ weight: number;
9
+ details?: string;
10
+ status?: 'ok' | 'error' | 'skipped';
11
+ }
12
+ export interface DiagnosticsTurnDetail extends TurnDetail {
13
+ apiRetries: number;
14
+ }
15
+ export type DiagnosticsRuntimePolicyEntry = RuntimePolicyAuditEntry;
16
+ export interface DiagnosticsReport {
17
+ turns: number;
18
+ totalDurationMs: number;
19
+ completionReason?: string;
20
+ completionDetail?: string;
21
+ score?: number;
22
+ turnDetails: DiagnosticsTurnDetail[];
23
+ reactionsFired: ReactionFiredEntry[];
24
+ runtimePoliciesApplied: DiagnosticsRuntimePolicyEntry[];
25
+ recommendedTimeoutMs: number;
26
+ warnings: string[];
27
+ scorers: Array<DiagnosticsScorer & {
28
+ status: 'ok' | 'error' | 'skipped';
29
+ }>;
30
+ }
31
+ export interface BuildDiagnosticsReportInput {
32
+ completionReason?: string;
33
+ completionDetail?: string;
34
+ score?: number;
35
+ turnDetails?: TurnDetail[];
36
+ reactionsFired?: ReactionFiredEntry[];
37
+ scorers?: DiagnosticsScorer[];
38
+ log: LogEntry[];
39
+ }
40
+ export declare function buildDiagnosticsReport(input: BuildDiagnosticsReportInput): DiagnosticsReport;
@@ -0,0 +1,80 @@
1
+ import { extractRuntimePolicyAuditEntries } from './runtime-policy.js';
2
+ export function buildDiagnosticsReport(input) {
3
+ const baseTurnDetails = (input.turnDetails && input.turnDetails.length > 0)
4
+ ? [...input.turnDetails]
5
+ : deriveTurnDetailsFromLog(input.log);
6
+ const retriesByTurn = countRetriesByTurn(input.log);
7
+ const turnDetails = baseTurnDetails
8
+ .sort((a, b) => a.turn - b.turn)
9
+ .map((detail) => ({
10
+ ...detail,
11
+ apiRetries: retriesByTurn.get(detail.turn) ?? 0,
12
+ }));
13
+ const warnings = turnDetails
14
+ .filter((detail) => detail.outputLines > 500)
15
+ .map((detail) => `Turn ${detail.turn}: output exceeded 500 lines (${detail.outputLines} lines)`);
16
+ return {
17
+ turns: turnDetails.length,
18
+ totalDurationMs: turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0),
19
+ completionReason: input.completionReason,
20
+ completionDetail: input.completionDetail,
21
+ score: input.score,
22
+ turnDetails,
23
+ reactionsFired: input.reactionsFired ?? [],
24
+ runtimePoliciesApplied: extractRuntimePolicyAuditEntries(input.log),
25
+ recommendedTimeoutMs: recommendTimeoutMs(turnDetails),
26
+ warnings,
27
+ scorers: (input.scorers ?? []).map((scorer) => ({
28
+ ...scorer,
29
+ status: scorer.status ?? 'ok',
30
+ })),
31
+ };
32
+ }
33
+ function deriveTurnDetailsFromLog(log) {
34
+ return log
35
+ .filter((entry) => entry.type === 'agent_result'
36
+ && entry.synthetic_blocked_prompt !== true
37
+ && typeof entry.turn_number === 'number'
38
+ && typeof entry.duration_ms === 'number'
39
+ && typeof entry.output_lines === 'number'
40
+ && typeof entry.output_chars === 'number')
41
+ .map((entry) => ({
42
+ turn: entry.turn_number,
43
+ durationMs: entry.duration_ms,
44
+ outputLines: entry.output_lines,
45
+ outputChars: entry.output_chars,
46
+ }));
47
+ }
48
+ function countRetriesByTurn(log) {
49
+ const retriesByTurn = new Map();
50
+ let pendingRetries = 0;
51
+ for (const entry of log) {
52
+ if (isRetryEntry(entry)) {
53
+ pendingRetries++;
54
+ continue;
55
+ }
56
+ if (entry.type === 'agent_result' && typeof entry.turn_number === 'number') {
57
+ if (pendingRetries > 0) {
58
+ retriesByTurn.set(entry.turn_number, (retriesByTurn.get(entry.turn_number) ?? 0) + pendingRetries);
59
+ pendingRetries = 0;
60
+ }
61
+ if (!retriesByTurn.has(entry.turn_number)) {
62
+ retriesByTurn.set(entry.turn_number, 0);
63
+ }
64
+ }
65
+ }
66
+ return retriesByTurn;
67
+ }
68
+ function isRetryEntry(entry) {
69
+ if (entry.type !== 'agent_result')
70
+ return false;
71
+ const text = `${entry.assistant_message ?? ''} ${entry.output ?? ''}`.toLowerCase();
72
+ return text.includes('retry') || text.includes('api_retry');
73
+ }
74
+ function recommendTimeoutMs(turnDetails) {
75
+ if (turnDetails.length <= 1) {
76
+ return (turnDetails[0]?.durationMs ?? 0) + 30_000;
77
+ }
78
+ const totalDurationMs = turnDetails.reduce((sum, detail) => sum + detail.durationMs, 0);
79
+ return Math.max(totalDurationMs + 200_000, 30_000);
80
+ }
@@ -3,6 +3,10 @@ import type { Agent, RecordedEvalResult } from './types.js';
3
3
  export type { LLMPort } from '../utils/llm-types.js';
4
4
  export interface EvalRuntime {
5
5
  llm: LLMPort;
6
+ /**
7
+ * Legacy compatibility seam. `setRuntime({ onResult })` is bridged into
8
+ * user-owned result observers; live capture is owned by result-capture.
9
+ */
6
10
  onResult: (result: RecordedEvalResult, agent: Agent) => void;
7
11
  }
8
12
  export declare function getRuntime(): EvalRuntime;
@@ -1,4 +1,5 @@
1
1
  import { callLLM } from '../utils/llm.js';
2
+ import { resetUserResultObservers, subscribeToEvalResults, } from './result-capture.js';
2
3
  function makeDefaultRuntime() {
3
4
  return {
4
5
  llm: { call: (prompt, opts) => callLLM(prompt, opts) },
@@ -6,12 +7,21 @@ function makeDefaultRuntime() {
6
7
  };
7
8
  }
8
9
  let current = makeDefaultRuntime();
10
+ let legacyRuntimeResultHandle = null;
9
11
  export function getRuntime() {
10
12
  return current;
11
13
  }
12
14
  export function setRuntime(partial) {
13
- current = { ...current, ...partial };
15
+ const { onResult, ...runtimeConfig } = partial;
16
+ current = { ...current, ...runtimeConfig };
17
+ if (onResult) {
18
+ legacyRuntimeResultHandle?.unsubscribe();
19
+ legacyRuntimeResultHandle = subscribeToEvalResults(({ result, agent }) => onResult(result, agent), { owner: 'user', key: 'legacy-runtime-onResult' });
20
+ }
14
21
  }
15
22
  export function resetRuntime() {
16
23
  current = makeDefaultRuntime();
24
+ legacyRuntimeResultHandle?.unsubscribe();
25
+ legacyRuntimeResultHandle = null;
26
+ resetUserResultObservers();
17
27
  }
@@ -1,5 +1,6 @@
1
1
  import { extractSkillsFromLog } from '../tool-events.js';
2
2
  import { getRuntime } from './eval-runtime.js';
3
+ import { emitEvalResult } from './result-capture.js';
3
4
  import { runJudgePipeline } from './judge-pipeline.js';
4
5
  import { runScorer } from './run-scorer.js';
5
6
  import { createLLMClient } from '../utils/llm.js';
@@ -76,7 +77,7 @@ function makeEvaluateAgent() {
76
77
  tokenUsage: deltaTokenUsage,
77
78
  trial: buildTrialResult(agent.log, { ...evalResult, tokenUsage: deltaTokenUsage }, conversationTokens, conversationCost),
78
79
  };
79
- getRuntime().onResult(recordedResult, agent);
80
+ emitEvalResult({ result: recordedResult, agent });
80
81
  maybeThrowOnScorerErrors(recordedResult, opts?.onScorerError ?? 'skip');
81
82
  return recordedResult;
82
83
  };
@@ -17,6 +17,7 @@ export { createAskBus, requireAskBusForLiveBatches, AskBusTimeoutError } from '.
17
17
  export { toAskUserToolEvent } from './ask-bus/projection.js';
18
18
  export type { AskUserToolEvent, AskUserToolEventArguments, AskUserToolEventQuestionArgument, } from './ask-bus/projection.js';
19
19
  export { buildAskBatchLogEntries } from './agent-result-log.js';
20
+ export { emitEvalResult, resetAllResultObserversForTests, resetUserResultObservers, subscribeToEvalResults, } from './result-capture.js';
20
21
  export { getAgentCapabilities } from './types.js';
21
22
  export type { AgentTransport, AgentCapabilities, AgentName, McpRunMode, McpSafetyOptions, McpToolPolicy, McpToolPolicyRule, } from './types.js';
22
23
  export type { AskBus, AskBatch, AskQuestion, AskOption, AskAnswer, AskResolution, AskBatchSnapshot, AskAnswerSnapshot, AskResolutionSnapshot, AskHandle, AskHandler, AskSource, AskLifecycle, AskAnswerSource, Unsubscribe as AskBusUnsubscribe, } from './ask-bus/types.js';
@@ -25,6 +26,7 @@ export type { ConversationWindow, ConversationWindowOptions } from './conversati
25
26
  export type { JudgePipelineOptions } from './judge-pipeline.js';
26
27
  export type { RunScorerOptions } from './run-scorer.js';
27
28
  export type { OnScorerErrorMode } from './evaluate.js';
29
+ export type { EvalResultEvent, EvalResultObserver, ResultObserverHandle, ResultObserverOptions, ResultObserverOwner, } from './result-capture.js';
28
30
  export type { RunSnapshot } from './snapshots.js';
29
31
  export type { ExpectedMcpStartupStatus, ExpectedMcpToolCall, McpStartupStatusEvidence, McpToolCallEvidence, } from './mcp-evidence.js';
30
32
  export type { McpPolicyDenialReason, McpToolCallRequest, McpToolPolicyDecision, } from './mcp-safety.js';
package/dist/sdk/index.js CHANGED
@@ -17,6 +17,7 @@ export { DEFAULT_COPY_IGNORE } from '../providers/copy-filter.js';
17
17
  export { createAskBus, requireAskBusForLiveBatches, AskBusTimeoutError } from './ask-bus/bus.js';
18
18
  export { toAskUserToolEvent } from './ask-bus/projection.js';
19
19
  export { buildAskBatchLogEntries } from './agent-result-log.js';
20
+ export { emitEvalResult, resetAllResultObserversForTests, resetUserResultObservers, subscribeToEvalResults, } from './result-capture.js';
20
21
  export { getAgentCapabilities } from './types.js';
21
22
  export { createLLMClient, ProviderNotSupportedError } from '../utils/llm.js';
22
23
  export { createMockLLM } from '../utils/llm-mocks.js';
@@ -0,0 +1,40 @@
1
+ import type { Agent, PathgradeTestMeta, RecordedEvalResult } from './types.js';
2
+ export type LifecycleAgentOwner = {
3
+ type: 'runner-case';
4
+ caseId: string;
5
+ } | {
6
+ type: 'runner-suite-shared';
7
+ caseId: string;
8
+ } | {
9
+ type: 'manual';
10
+ };
11
+ type ResultAttribution = {
12
+ type: 'runner-case';
13
+ caseId: string;
14
+ } | {
15
+ type: 'unattributed-runner';
16
+ } | {
17
+ type: 'legacy-unowned';
18
+ };
19
+ interface FlushCaseInput {
20
+ caseId: string;
21
+ }
22
+ declare function getAgentOwner(agent: Agent): LifecycleAgentOwner | undefined;
23
+ declare function registerAgent(agent: Agent, owner?: LifecycleAgentOwner | null): void;
24
+ declare function untrackAgent(agent: Agent): void;
25
+ declare function releaseAgent(agent: Agent): void;
26
+ declare function recordResult(result: RecordedEvalResult, agent: Agent, attribution?: ResultAttribution): void;
27
+ declare function flushCase(input: FlushCaseInput): Promise<PathgradeTestMeta[]>;
28
+ declare function cleanupAll(): Promise<void>;
29
+ declare function reset(): void;
30
+ export declare const lifecycleCore: {
31
+ registerAgent: typeof registerAgent;
32
+ untrackAgent: typeof untrackAgent;
33
+ releaseAgent: typeof releaseAgent;
34
+ getAgentOwner: typeof getAgentOwner;
35
+ recordResult: typeof recordResult;
36
+ flushCase: typeof flushCase;
37
+ cleanupAll: typeof cleanupAll;
38
+ reset: typeof reset;
39
+ };
40
+ export {};