@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.
- package/README.md +3 -1
- package/dist/adapter-kit/index.d.ts +2 -2
- package/dist/adapter-kit/index.js +1 -1
- package/dist/adapters/jest/lifecycle.js +4 -2
- package/dist/adapters/node-test/index.js +4 -2
- package/dist/agents/claude/sdk-message-projector.js +4 -5
- package/dist/agents/claude/tool-permission-bridge.js +2 -1
- package/dist/agents/codex-app-server/item-projection.js +2 -2
- package/dist/agents/codex-app-server/mcp-approval-correlator.js +3 -2
- package/dist/agents/opencode.js +4 -5
- package/dist/commands/report.js +5 -25
- package/dist/internal/direct-mcp-v2/acp-author-projector.js +20 -7
- package/dist/reporters/cli.js +20 -1
- package/dist/reporters/github-comment.js +18 -7
- package/dist/reporters/loader.d.ts +4 -0
- package/dist/reporters/loader.js +26 -9
- package/dist/reporting/comparison-contract.js +20 -6
- package/dist/reporting/core.js +18 -8
- package/dist/reporting/report-parser.js +57 -2
- package/dist/reporting/types.d.ts +2 -1
- package/dist/runners/adapter.d.ts +4 -2
- package/dist/runners/lifecycle-hooks.js +6 -1
- package/dist/runners/orchestrator.js +3 -4
- package/dist/runners/repeated-invocation.js +2 -5
- package/dist/runners/report-projection.js +1 -0
- package/dist/runners/vitest-lifecycle.d.ts +1 -0
- package/dist/runners/vitest-lifecycle.js +21 -4
- package/dist/sdk/agent-flow.d.ts +51 -0
- package/dist/sdk/agent-flow.js +23 -0
- package/dist/sdk/agent.js +10 -1
- package/dist/sdk/evaluate.d.ts +2 -1
- package/dist/sdk/evaluate.js +114 -4
- package/dist/sdk/index.d.ts +4 -2
- package/dist/sdk/index.js +2 -2
- package/dist/sdk/judge-prompt-builder.js +11 -7
- package/dist/sdk/lifecycle.d.ts +3 -1
- package/dist/sdk/lifecycle.js +68 -5
- package/dist/sdk/mcp-event-input.d.ts +1 -0
- package/dist/sdk/mcp-event-input.js +3 -0
- package/dist/sdk/mcp-evidence.js +16 -3
- package/dist/sdk/mcp-safety.js +2 -2
- package/dist/sdk/result-capture.d.ts +14 -1
- package/dist/sdk/result-capture.js +49 -0
- package/dist/sdk/scripted-mcp-events.js +3 -2
- package/dist/sdk/tool-event-log.js +18 -3
- package/dist/sdk/tool-event-secrets.d.ts +4 -0
- package/dist/sdk/tool-event-secrets.js +33 -2
- package/dist/sdk/types.d.ts +6 -0
- package/dist/tool-event-results.d.ts +3 -0
- package/dist/tool-event-results.js +153 -23
- package/dist/types.d.ts +13 -7
- package/docs/agent-flow-evaluation.md +31 -0
- package/package.json +3 -2
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`.
|
|
@@ -459,7 +461,7 @@ export default {
|
|
|
459
461
|
|
|
460
462
|
Pathgrade reads `pathgrade.config.*` for CLI and affected-selection behavior. `runner.adapter` and `--adapter=<name|path>` select the runner; `--adapter` wins over config. `attempts` defaults to `1` and must be a positive integer. Built-in adapters `vitest`, `jest`, and `node-test` support repeated attempts; third-party invocation adapters must advertise `supportsRepeatedAttempts: true` and produce the normalized child snapshot contract.
|
|
461
463
|
|
|
462
|
-
New reports use schema version 2. They preserve every attempt with `case_id`, `attempt_id`, and `attempt_index`; include the display-safe `runner_outcome` that determined binary success; publish `mean_reward` for partial scores; and publish finite-sample pass@k only for complete binary attempts. Runner assertions, runner diagnostics, and native runner references remain in the normalized run model and are not copied into public report artifacts; evaluation diagnostics retain their existing report behavior. A partial reward or incomplete attempt makes pass@k explicitly unavailable. Version-1 reports remain readable, but Pathgrade no longer computes pass@k or pass^k by pooling heterogeneous cases.
|
|
464
|
+
New reports use schema version 2. They preserve every attempt with `case_id`, `attempt_id`, and `attempt_index`; include the display-safe `runner_outcome` that determined binary success; publish `mean_reward` for partial scores; and publish finite-sample pass@k only for complete binary attempts. `runner_status` and `threshold_status` expose the two gates independently, and canonical `status` passes only when runner assertions pass and any configured threshold passes. Runner assertions, runner diagnostics, and native runner references remain in the normalized run model and are not copied into public report artifacts; evaluation diagnostics retain their existing report behavior. A partial reward or incomplete attempt makes pass@k explicitly unavailable. Version-1 and older version-2 reports remain readable with missing gate fields derived during loading, but Pathgrade no longer computes pass@k or pass^k by pooling heterogeneous cases.
|
|
463
465
|
|
|
464
466
|
Third-party runner adapters are supported through `@wix/pathgrade/adapter-kit`. Adapter names resolve as follows:
|
|
465
467
|
|
|
@@ -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,
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
});
|
|
@@ -22,10 +22,9 @@ const SDK_ERROR_SUBTYPES = [
|
|
|
22
22
|
'error_max_structured_output_retries',
|
|
23
23
|
];
|
|
24
24
|
import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../../tool-events.js';
|
|
25
|
-
import { sanitizePersistenceValue } from '../../tool-event-results.js';
|
|
25
|
+
import { sanitizePersistenceValue, sanitizeUntrustedPersistenceValue } from '../../tool-event-results.js';
|
|
26
26
|
import { attachTurnResultSensitiveValues } from '../../sdk/turn-result-secrets.js';
|
|
27
|
-
import { attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
|
|
28
|
-
import { attachOriginalMcpInput } from '../../sdk/mcp-event-input.js';
|
|
27
|
+
import { attachLiveMcpInput, attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
|
|
29
28
|
import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
30
29
|
import { applyObservedToolResult, extractObservedToolResults, } from './tool-results.js';
|
|
31
30
|
export function projectSdkMessages(input) {
|
|
@@ -145,7 +144,7 @@ export function projectSdkMessages(input) {
|
|
|
145
144
|
...(input.deniedMcpEvents?.all() ?? []),
|
|
146
145
|
]);
|
|
147
146
|
const finalToolEvents = prependSlashCommandSkillEvent(enriched, input.firstMessage, initSkills).map((event) => attachToolEventSensitiveValues(event, input.sensitiveValues ?? []));
|
|
148
|
-
const traceOutput =
|
|
147
|
+
const traceOutput = sanitizeUntrustedPersistenceValue(input.messages, input.sensitiveValues)
|
|
149
148
|
.map((message) => JSON.stringify(message))
|
|
150
149
|
.join('\n');
|
|
151
150
|
const result = {
|
|
@@ -216,7 +215,7 @@ function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMc
|
|
|
216
215
|
tool: mcpTool.tool,
|
|
217
216
|
status: 'incomplete',
|
|
218
217
|
};
|
|
219
|
-
return
|
|
218
|
+
return attachLiveMcpInput({
|
|
220
219
|
action: 'mcp_tool_call',
|
|
221
220
|
provider: 'claude',
|
|
222
221
|
providerToolName: normalizedProviderToolName,
|
|
@@ -3,6 +3,7 @@ import { createAskUserBridge, } from './ask-user-bridge.js';
|
|
|
3
3
|
import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
4
4
|
import { canonicalizeJson } from '../../core/canonical-json.js';
|
|
5
5
|
import { buildScriptedMcpApprovalEvent, buildScriptedMcpDeniedCallEvent, } from '../../sdk/scripted-mcp-events.js';
|
|
6
|
+
import { attachLiveMcpInput } from '../../sdk/tool-event-secrets.js';
|
|
6
7
|
export function createClaudeToolPermissionBridge(deps) {
|
|
7
8
|
const askUserBridge = createAskUserBridge({
|
|
8
9
|
askBus: deps.askBus,
|
|
@@ -149,5 +150,5 @@ function recordDeniedMcpEvent(opts) {
|
|
|
149
150
|
input: opts.input,
|
|
150
151
|
})).slice(0, 200),
|
|
151
152
|
};
|
|
152
|
-
opts.store.record(opts.toolUseId, event);
|
|
153
|
+
opts.store.record(opts.toolUseId, attachLiveMcpInput(event, opts.input));
|
|
153
154
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { buildSummary, extractSkillNameFromPath, inferCodexExecAction, } from '../../tool-events.js';
|
|
2
2
|
import { sanitizeToolEventResult } from '../../tool-event-results.js';
|
|
3
|
-
import {
|
|
3
|
+
import { attachLiveMcpInput } from '../../sdk/tool-event-secrets.js';
|
|
4
4
|
export function projectItemIntoTurn(item, turn, sensitiveValues, timing = {}) {
|
|
5
5
|
if (item.type === 'agentMessage') {
|
|
6
6
|
const message = item;
|
|
@@ -76,7 +76,7 @@ function projectMcpCall(call, turn, sensitiveValues, timing) {
|
|
|
76
76
|
const status = call.status === 'completed' && error === undefined
|
|
77
77
|
? 'completed' : call.status === 'failed' || error !== undefined ? 'error' : 'incomplete';
|
|
78
78
|
const resultContent = call.result === undefined ? undefined : JSON.stringify(call.result);
|
|
79
|
-
turn.nonAskToolEvents.push(
|
|
79
|
+
turn.nonAskToolEvents.push(attachLiveMcpInput({
|
|
80
80
|
action: 'mcp_tool_call', provider: 'codex', providerToolName, toolUseId: call.id,
|
|
81
81
|
turnNumber: turn.turnNumber, status, ...projectToolTiming(timing, finiteNumber(call.durationMs)),
|
|
82
82
|
arguments: { ...args, server: call.server, tool: call.tool, status: call.status ?? 'unknown' },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { decideMcpToolCall, redactMcpSecrets, } from '../../sdk/mcp-safety.js';
|
|
2
2
|
import { canonicalizeJson } from '../../core/canonical-json.js';
|
|
3
3
|
import { buildScriptedMcpApprovalEvent, buildScriptedMcpDeniedCallEvent, } from '../../sdk/scripted-mcp-events.js';
|
|
4
|
+
import { attachLiveMcpInput } from '../../sdk/tool-event-secrets.js';
|
|
4
5
|
function isRecord(value) {
|
|
5
6
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
6
7
|
}
|
|
@@ -64,7 +65,7 @@ export function queuePolicyDeniedMcpToolCall(turn, request, decision, rawParams)
|
|
|
64
65
|
export function buildPolicyDeniedMcpToolEvent(turnNumber, pending, terminal) {
|
|
65
66
|
const args = redactMcpSecrets(pending.request.arguments);
|
|
66
67
|
const providerToolName = `${pending.request.serverName}.${pending.request.toolName}`;
|
|
67
|
-
return {
|
|
68
|
+
return attachLiveMcpInput({
|
|
68
69
|
action: 'mcp_tool_call', provider: 'codex', providerToolName,
|
|
69
70
|
...(terminal ? { toolUseId: terminal.id } : {}),
|
|
70
71
|
turnNumber, status: 'error',
|
|
@@ -79,7 +80,7 @@ export function buildPolicyDeniedMcpToolEvent(turnNumber, pending, terminal) {
|
|
|
79
80
|
},
|
|
80
81
|
summary: `MCP tool ${providerToolName} policy_denied`, confidence: 'high',
|
|
81
82
|
rawSnippet: JSON.stringify(redactMcpSecrets(terminal ? { approval: pending.rawParams, terminal } : pending.rawParams)),
|
|
82
|
-
};
|
|
83
|
+
}, pending.request.arguments);
|
|
83
84
|
}
|
|
84
85
|
export function consumeMatchingMcpDenial(turn, serverName, toolName, args) {
|
|
85
86
|
const argsKey = canonicalizeJson(args);
|
package/dist/agents/opencode.js
CHANGED
|
@@ -3,12 +3,11 @@ import * as path from 'node:path';
|
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import { BaseAgent, getRuntimeEnv, getWorkspacePath, } from '../types.js';
|
|
5
5
|
import { buildSummary, enrichSkillEvents } from '../tool-events.js';
|
|
6
|
-
import { collectSensitiveEnvValues,
|
|
6
|
+
import { collectSensitiveEnvValues, sanitizeToolEventResult, sanitizeUntrustedPersistenceValue, } from '../tool-event-results.js';
|
|
7
7
|
import { readStagedMcpServers } from '../providers/mcp-config.js';
|
|
8
8
|
import { removeSandboxRoot } from '../providers/sandbox-lifecycle.js';
|
|
9
9
|
import { attachTurnResultSensitiveValues, cloneTurnResultWithSensitiveValues } from '../sdk/turn-result-secrets.js';
|
|
10
|
-
import {
|
|
11
|
-
import { attachToolEventSensitiveValues } from '../sdk/tool-event-secrets.js';
|
|
10
|
+
import { attachLiveMcpInput, attachToolEventSensitiveValues } from '../sdk/tool-event-secrets.js';
|
|
12
11
|
import { currentOpenCodePlatformKey, OPENCODE_RUNTIME_LOCK, } from './opencode/contract.js';
|
|
13
12
|
import { OpenCodeRuntimePolicy, OPENCODE_PERMISSION } from './opencode/runtime-policy.js';
|
|
14
13
|
import { killOpenCodeProcessGroup, registerOpenCodeProcessGroup, unregisterOpenCodeProcessGroup, } from './opencode/process-groups.js';
|
|
@@ -217,7 +216,7 @@ export function parseOpenCodeOutput(stdout, processResult, mcpToolNames, sensiti
|
|
|
217
216
|
rawSnippet: JSON.stringify({ tool, status: state.status, input }).slice(0, 2_000),
|
|
218
217
|
};
|
|
219
218
|
toolEvents.push(action === 'mcp_tool_call' && input
|
|
220
|
-
?
|
|
219
|
+
? attachLiveMcpInput(toolEvent, input)
|
|
221
220
|
: toolEvent);
|
|
222
221
|
sanitizedTrace.push({ type, tool, status, input });
|
|
223
222
|
continue;
|
|
@@ -256,7 +255,7 @@ export function parseOpenCodeOutput(stdout, processResult, mcpToolNames, sensiti
|
|
|
256
255
|
if (stepFinishCount === 0)
|
|
257
256
|
throw new Error('OpenCode protocol error: missing step_finish');
|
|
258
257
|
const assistantMessage = textParts.join('');
|
|
259
|
-
const traceOutput =
|
|
258
|
+
const traceOutput = sanitizeUntrustedPersistenceValue(sanitizedTrace, sensitiveValues)
|
|
260
259
|
.map((event) => JSON.stringify(event))
|
|
261
260
|
.join('\n');
|
|
262
261
|
return {
|
package/dist/commands/report.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import * as path from 'path';
|
|
21
21
|
import fs from 'fs-extra';
|
|
22
22
|
import { readSidecar } from '../affected/sidecar.js';
|
|
23
|
+
import { parsePathgradeReport } from '../reporting/report-parser.js';
|
|
23
24
|
import { formatNoAffectedEvalsMarkdown, formatReportMarkdown, MISSING_RESULTS_BODY, postOrUpdateComment, resolvePrContext, } from '../reporters/github-comment.js';
|
|
24
25
|
const DEFAULT_RESULTS_PATH = path.join('.pathgrade', 'results.json');
|
|
25
26
|
/**
|
|
@@ -39,38 +40,17 @@ function resolveCommentId(explicit) {
|
|
|
39
40
|
return workflow;
|
|
40
41
|
return 'default';
|
|
41
42
|
}
|
|
42
|
-
function isPathgradeReport(value) {
|
|
43
|
-
if (!value || typeof value !== 'object')
|
|
44
|
-
return false;
|
|
45
|
-
const v = value;
|
|
46
|
-
return ((v.version === 1 || v.version === 2) &&
|
|
47
|
-
typeof v.overall_pass_rate === 'number' &&
|
|
48
|
-
(v.status === 'pass' || v.status === 'fail') &&
|
|
49
|
-
Array.isArray(v.groups) &&
|
|
50
|
-
(v.version === 1 || (typeof v.overall_mean_reward === 'number'
|
|
51
|
-
&& hasValidAttemptCounts(v)
|
|
52
|
-
&& v.groups.every(group => typeof group.mean_reward === 'number'))));
|
|
53
|
-
}
|
|
54
|
-
function hasValidAttemptCounts(report) {
|
|
55
|
-
const requested = report.attempts_requested;
|
|
56
|
-
const completed = report.attempts_completed;
|
|
57
|
-
return typeof requested === 'number'
|
|
58
|
-
&& Number.isSafeInteger(requested)
|
|
59
|
-
&& requested >= 1
|
|
60
|
-
&& typeof completed === 'number'
|
|
61
|
-
&& Number.isSafeInteger(completed)
|
|
62
|
-
&& completed >= 0
|
|
63
|
-
&& completed <= requested;
|
|
64
|
-
}
|
|
65
43
|
async function loadReport(resolvedPath) {
|
|
66
44
|
if (!(await fs.pathExists(resolvedPath))) {
|
|
67
45
|
throw new Error(`results file not found at ${resolvedPath}`);
|
|
68
46
|
}
|
|
69
47
|
const raw = await fs.readJSON(resolvedPath);
|
|
70
|
-
|
|
48
|
+
try {
|
|
49
|
+
return parsePathgradeReport(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
71
52
|
throw new Error(`results file at ${resolvedPath} is not a valid pathgrade report`);
|
|
72
53
|
}
|
|
73
|
-
return raw;
|
|
74
54
|
}
|
|
75
55
|
function printMarkdownAndPassRate(markdown, passRate) {
|
|
76
56
|
// Strip trailing newlines so we control spacing precisely:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { buildSummary } from '../../tool-events.js';
|
|
2
|
-
import { sanitizePersistenceValue, sanitizeToolEventResult } from '../../tool-event-results.js';
|
|
2
|
+
import { sanitizePersistenceValue, sanitizeToolEventResult, sanitizeUntrustedPersistenceValue, } from '../../tool-event-results.js';
|
|
3
|
+
import { getOriginalMcpInput } from '../../sdk/mcp-event-input.js';
|
|
4
|
+
import { attachLiveMcpInput, attachToolEventSensitiveValues } from '../../sdk/tool-event-secrets.js';
|
|
3
5
|
export const ACP_AUTHOR_TRACE_MAX_CHARS = 64 * 1024;
|
|
4
6
|
function object(value) {
|
|
5
7
|
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
@@ -92,7 +94,7 @@ export function projectAcpAuthorTurn(input) {
|
|
|
92
94
|
sessionUpdate: current.sessionUpdate,
|
|
93
95
|
title: current.title,
|
|
94
96
|
...(current.status ? { status: current.status } : {}),
|
|
95
|
-
...(current.rawInput ? { rawInput: current.rawInput } : {}),
|
|
97
|
+
...(current.rawInput ? { rawInput: sanitizeUntrustedPersistenceValue(current.rawInput, redactionValues) } : {}),
|
|
96
98
|
});
|
|
97
99
|
if (current.status !== 'completed' && current.status !== 'failed') {
|
|
98
100
|
if (current.toolCallId)
|
|
@@ -114,7 +116,7 @@ export function projectAcpAuthorTurn(input) {
|
|
|
114
116
|
toolCalls.set(current.toolCallId, { ...correlated, terminalFingerprint });
|
|
115
117
|
const normalized = input.normalizeToolName(correlated.title);
|
|
116
118
|
const resultText = textValue(correlated.status === 'completed' ? correlated.output : correlated.error);
|
|
117
|
-
|
|
119
|
+
const event = {
|
|
118
120
|
action: normalized.action,
|
|
119
121
|
provider: input.provider,
|
|
120
122
|
providerToolName: normalized.providerToolName,
|
|
@@ -124,18 +126,21 @@ export function projectAcpAuthorTurn(input) {
|
|
|
124
126
|
...(resultText !== undefined ? { result: sanitizeToolEventResult({ content: resultText }, redactionValues) } : {}),
|
|
125
127
|
summary: buildSummary(normalized.action, normalized.providerToolName, correlated.rawInput),
|
|
126
128
|
confidence: normalized.action === 'unknown' ? 'low' : 'high',
|
|
127
|
-
rawSnippet: JSON.stringify(
|
|
128
|
-
}
|
|
129
|
+
rawSnippet: JSON.stringify(sanitizeUntrustedPersistenceValue({ title: normalized.providerToolName, status: correlated.status, rawInput: correlated.rawInput }, redactionValues)).slice(0, 2_000),
|
|
130
|
+
};
|
|
131
|
+
toolEvents.push(normalized.action === 'mcp_tool_call' && correlated.rawInput
|
|
132
|
+
? attachLiveMcpInput(event, correlated.rawInput)
|
|
133
|
+
: event);
|
|
129
134
|
}
|
|
130
135
|
const assistantMessage = sanitizePersistenceValue(assistantChunks.join(''), redactionValues);
|
|
131
|
-
const rawTrace = JSON.stringify(
|
|
136
|
+
const rawTrace = JSON.stringify(sanitizeUntrustedPersistenceValue({
|
|
132
137
|
terminal: { stopReason: input.terminal.stopReason },
|
|
133
138
|
updates: trace,
|
|
134
139
|
}, redactionValues));
|
|
135
140
|
const traceOutput = rawTrace.length > ACP_AUTHOR_TRACE_MAX_CHARS
|
|
136
141
|
? `${rawTrace.slice(0, ACP_AUTHOR_TRACE_MAX_CHARS - 1)}…`
|
|
137
142
|
: rawTrace;
|
|
138
|
-
|
|
143
|
+
const result = sanitizePersistenceValue({
|
|
139
144
|
rawOutput: traceOutput,
|
|
140
145
|
traceOutput,
|
|
141
146
|
assistantMessage,
|
|
@@ -144,4 +149,12 @@ export function projectAcpAuthorTurn(input) {
|
|
|
144
149
|
exitCode: input.terminal.stopReason === 'end_turn' && !providerError ? 0 : 1,
|
|
145
150
|
toolEvents,
|
|
146
151
|
}, redactionValues);
|
|
152
|
+
result.toolEvents.forEach((event, index) => {
|
|
153
|
+
const originalInput = toolEvents[index] ? getOriginalMcpInput(toolEvents[index]) : undefined;
|
|
154
|
+
if (event.action === 'mcp_tool_call' && originalInput) {
|
|
155
|
+
attachLiveMcpInput(event, originalInput);
|
|
156
|
+
attachToolEventSensitiveValues(event, redactionValues);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return result;
|
|
147
160
|
}
|
package/dist/reporters/cli.js
CHANGED
|
@@ -19,7 +19,8 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
19
19
|
console.log(`\n${fmt.bold('pathgrade preview')} ${fmt.dim(`${entries.length} reports from ${resolved}`)}\n`);
|
|
20
20
|
for (const { file, ...report } of entries) {
|
|
21
21
|
const meanReward = report.mean_reward ?? report.pass_rate ?? 0;
|
|
22
|
-
const isPass = report.
|
|
22
|
+
const isPass = report.run_runner_status === 'fail'
|
|
23
|
+
? false : report.status === undefined ? meanReward >= 0.5 : report.status === 'pass';
|
|
23
24
|
const trials = report.trials || [];
|
|
24
25
|
const avgDur = trials.reduce((s, t) => s + (t.duration_ms || 0), 0) / (trials.length || 1);
|
|
25
26
|
const totalTokens = trials.reduce((s, t) => s + (t.input_tokens || 0) + (t.output_tokens || 0) + (t.conversation_input_tokens || 0) + (t.conversation_output_tokens || 0), 0);
|
|
@@ -43,6 +44,17 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
43
44
|
for (const [label, value] of metrics) {
|
|
44
45
|
console.log(` ${fmt.dim(label.padEnd(14))} ${fmt.bold(value)}`);
|
|
45
46
|
}
|
|
47
|
+
if (report.runner_status !== undefined || report.status !== undefined) {
|
|
48
|
+
console.log(` ${fmt.dim('Runner assertions'.padEnd(20))} ${gateLabel(report.runner_status ?? report.status)}`);
|
|
49
|
+
}
|
|
50
|
+
if (report.run_runner_status !== undefined) {
|
|
51
|
+
console.log(` ${fmt.dim('Run assertions'.padEnd(20))} ${gateLabel(report.run_runner_status)}`);
|
|
52
|
+
}
|
|
53
|
+
if (report.threshold != null) {
|
|
54
|
+
const thresholdStatus = report.threshold_status === 'pass' || report.threshold_status === 'fail' ? report.threshold_status : meanReward >= report.threshold ? 'pass' : 'fail';
|
|
55
|
+
console.log(` ${fmt.dim('Aggregate threshold'.padEnd(20))} ${gateLabel(thresholdStatus)} — ${(meanReward * 100).toFixed(1)}% ${thresholdStatus === 'pass' ? '>=' : '<'} ${(report.threshold * 100).toFixed(1)}%`);
|
|
56
|
+
}
|
|
57
|
+
console.log(` ${fmt.dim('Overall'.padEnd(20))} ${gateLabel(isPass ? 'pass' : 'fail')}`);
|
|
46
58
|
console.log();
|
|
47
59
|
// ── Trials
|
|
48
60
|
for (const trial of trials) {
|
|
@@ -85,6 +97,10 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
85
97
|
}
|
|
86
98
|
console.log();
|
|
87
99
|
}
|
|
100
|
+
if (trials.some((trial) => trial.scorer_results?.some(scorer => scorer.status === 'skipped'))) {
|
|
101
|
+
console.log(` ${fmt.dim('Skipped scorers keep their declared weight and contribute zero.')}`);
|
|
102
|
+
console.log();
|
|
103
|
+
}
|
|
88
104
|
// ── LLM scorer details
|
|
89
105
|
const hasLlm = trials.some((t) => t.scorer_results?.some((g) => g.scorer_type === 'llm_rubric'));
|
|
90
106
|
if (hasLlm) {
|
|
@@ -120,6 +136,9 @@ export async function runCliPreview(resultsDir, opts) {
|
|
|
120
136
|
console.log();
|
|
121
137
|
}
|
|
122
138
|
}
|
|
139
|
+
function gateLabel(status) {
|
|
140
|
+
return status === 'pass' ? fmt.pass('PASS') : fmt.fail('FAIL');
|
|
141
|
+
}
|
|
123
142
|
function formatPassAtK(value, reason) {
|
|
124
143
|
if (typeof value === 'number')
|
|
125
144
|
return `${(value * 100).toFixed(1)}% (legacy v1)`;
|
|
@@ -83,15 +83,22 @@ export function formatReportMarkdown(report, opts) {
|
|
|
83
83
|
}
|
|
84
84
|
if (report.threshold != null) {
|
|
85
85
|
lines.push('');
|
|
86
|
-
lines.push(
|
|
86
|
+
lines.push(`**Runner assertions:** ${(report.runner_status ?? report.status).toUpperCase()}`);
|
|
87
|
+
lines.push(`**Aggregate threshold:** ${(report.threshold_status ?? (meanReward >= report.threshold ? 'pass' : 'fail')).toUpperCase()} — ${pct(meanReward)} ${meanReward >= report.threshold ? '>=' : '<'} ${pct(report.threshold)}`);
|
|
88
|
+
lines.push(`**Overall:** ${report.status.toUpperCase()}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push(`**Runner assertions:** ${(report.runner_status ?? report.status).toUpperCase()}`);
|
|
93
|
+
lines.push(`**Overall:** ${report.status.toUpperCase()}`);
|
|
87
94
|
}
|
|
88
95
|
lines.push('');
|
|
89
|
-
lines.push('| Group | Mean reward | Success rate | pass@k | Skills | Avg duration |');
|
|
90
|
-
lines.push('
|
|
96
|
+
lines.push('| Group | Status | Mean reward | Success rate | pass@k | Skills | Avg duration |');
|
|
97
|
+
lines.push('|---|---|---|---|---|---|---|');
|
|
91
98
|
for (const group of report.groups) {
|
|
92
99
|
const skills = group.skills_used.length > 0 ? group.skills_used.join(', ') : '—';
|
|
93
100
|
const avg = computeAvgDuration(group.trials);
|
|
94
|
-
lines.push(`| ${escapeTableCell(group.task)} | ${pct(group.mean_reward ?? group.pass_rate ?? 0)} | ${group.success_rate === undefined ? '—' : pct(group.success_rate)} | ${escapeTableCell(formatPassAtK(group.pass_at_k, group.pass_at_k_unavailable_reason))} | ${escapeTableCell(skills)} | ${durationSeconds(avg)} |`);
|
|
101
|
+
lines.push(`| ${escapeTableCell(group.task)} | ${(group.status ?? (group.pass_rate === 1 ? 'pass' : 'fail')).toUpperCase()} | ${pct(group.mean_reward ?? group.pass_rate ?? 0)} | ${group.success_rate === undefined ? '—' : pct(group.success_rate)} | ${escapeTableCell(formatPassAtK(group.pass_at_k, group.pass_at_k_unavailable_reason))} | ${escapeTableCell(skills)} | ${durationSeconds(avg)} |`);
|
|
95
102
|
}
|
|
96
103
|
if (report.selection) {
|
|
97
104
|
lines.push('');
|
|
@@ -320,10 +327,14 @@ function formatGroupDetails(group) {
|
|
|
320
327
|
out.push(`reward: **${trial.reward === undefined ? 'n/a' : trial.reward.toFixed(2)}** | duration: ${durationSeconds(trial.duration_ms)} | completion: \`${reason}\``);
|
|
321
328
|
if (trial.scorer_results.length > 0) {
|
|
322
329
|
out.push('');
|
|
323
|
-
out.push('| Scorer | Score | Weight | Details |');
|
|
324
|
-
out.push('
|
|
330
|
+
out.push('| Scorer | Status | Score | Weight | Details |');
|
|
331
|
+
out.push('|---|---|---|---|---|');
|
|
325
332
|
for (const s of trial.scorer_results) {
|
|
326
|
-
out.push(`| ${escapeTableCell(s.scorer_type)} | ${s.score.toFixed(2)} | ${s.weight.toFixed(2)} | ${escapeTableCell(s.details ?? '')} |`);
|
|
333
|
+
out.push(`| ${escapeTableCell(s.scorer_type)} | ${(s.status ?? 'ok').toUpperCase()} | ${s.score.toFixed(2)} | ${s.weight.toFixed(2)} | ${escapeTableCell(s.details ?? '')} |`);
|
|
334
|
+
}
|
|
335
|
+
if (trial.scorer_results.some(scorer => scorer.status === 'skipped')) {
|
|
336
|
+
out.push('');
|
|
337
|
+
out.push('Skipped scorers keep their declared weight and contribute zero.');
|
|
327
338
|
}
|
|
328
339
|
}
|
|
329
340
|
const warnings = trial.diagnostics?.warnings ?? [];
|
|
@@ -3,6 +3,10 @@ export interface LoadedReport extends EvalReport {
|
|
|
3
3
|
file: string;
|
|
4
4
|
timestamp?: string;
|
|
5
5
|
status?: 'pass' | 'fail';
|
|
6
|
+
runner_status?: 'pass' | 'fail';
|
|
7
|
+
run_runner_status?: 'pass' | 'fail';
|
|
8
|
+
threshold_status?: 'pass' | 'fail' | 'not_configured';
|
|
9
|
+
threshold?: number;
|
|
6
10
|
}
|
|
7
11
|
export declare function loadReports(resultsDir: string, opts?: {
|
|
8
12
|
skipTraces?: boolean;
|
package/dist/reporters/loader.js
CHANGED
|
@@ -34,11 +34,18 @@ export async function loadReports(resultsDir, opts) {
|
|
|
34
34
|
if (raw.version !== 1 && raw.version !== 2)
|
|
35
35
|
continue;
|
|
36
36
|
for (const group of raw.groups) {
|
|
37
|
+
const runnerStatus = groupRunnerStatus(group);
|
|
38
|
+
const thresholdStatus = groupThresholdStatus(raw, group);
|
|
37
39
|
const report = {
|
|
38
40
|
file,
|
|
39
41
|
timestamp: raw.timestamp,
|
|
42
|
+
threshold: raw.threshold,
|
|
40
43
|
...group,
|
|
41
|
-
|
|
44
|
+
runner_status: runnerStatus,
|
|
45
|
+
...(raw.runner_status === 'pass' || raw.runner_status === 'fail'
|
|
46
|
+
? { run_runner_status: raw.runner_status } : {}),
|
|
47
|
+
threshold_status: thresholdStatus,
|
|
48
|
+
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
42
49
|
};
|
|
43
50
|
if (!opts?.skipTraces && group.trace_file) {
|
|
44
51
|
await hydrateTraces(report, group.trace_file, resolved);
|
|
@@ -50,15 +57,25 @@ export async function loadReports(resultsDir, opts) {
|
|
|
50
57
|
}
|
|
51
58
|
return results;
|
|
52
59
|
}
|
|
53
|
-
function
|
|
60
|
+
function groupRunnerStatus(group) {
|
|
61
|
+
if (group.runner_status === 'pass' || group.runner_status === 'fail')
|
|
62
|
+
return group.runner_status;
|
|
63
|
+
const outcomes = Array.isArray(group.trials)
|
|
64
|
+
? group.trials.map((trial) => trial.runner_outcome).filter((outcome) => outcome !== undefined)
|
|
65
|
+
: [];
|
|
66
|
+
if (outcomes.length > 0) {
|
|
67
|
+
return outcomes.every((outcome) => outcome === 'passed') ? 'pass' : 'fail';
|
|
68
|
+
}
|
|
54
69
|
if (group.status === 'pass' || group.status === 'fail')
|
|
55
70
|
return group.status;
|
|
56
71
|
const meanReward = group.mean_reward ?? group.pass_rate ?? 0;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
return meanReward === 1 ? 'pass' : 'fail';
|
|
73
|
+
}
|
|
74
|
+
function groupThresholdStatus(raw, group) {
|
|
75
|
+
if (group.threshold_status === 'pass' || group.threshold_status === 'fail'
|
|
76
|
+
|| group.threshold_status === 'not_configured')
|
|
77
|
+
return group.threshold_status;
|
|
78
|
+
if (typeof raw.threshold !== 'number')
|
|
79
|
+
return 'not_configured';
|
|
80
|
+
return (group.mean_reward ?? group.pass_rate ?? 0) >= raw.threshold ? 'pass' : 'fail';
|
|
64
81
|
}
|
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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) {
|
package/dist/reporting/core.js
CHANGED
|
@@ -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;
|
|
@@ -52,9 +52,12 @@ export function buildPathgradeReport(input) {
|
|
|
52
52
|
.map(attempt => attempt.score)
|
|
53
53
|
.filter((score) => score !== undefined))));
|
|
54
54
|
const overallMeanReward = average(scores);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const runnerStatus = (input.runStatus === undefined || input.runStatus === 'completed')
|
|
56
|
+
&& reportableGroups.every(group => group.cases.every(testCase => (testCase.state !== 'failed'
|
|
57
|
+
&& testCase.attempts.every(attempt => attempt.outcome.kind === 'passed')))) ? 'pass' : 'fail';
|
|
58
|
+
const thresholdStatus = input.threshold == null
|
|
59
|
+
? 'not_configured'
|
|
60
|
+
: overallMeanReward >= input.threshold ? 'pass' : 'fail';
|
|
58
61
|
return {
|
|
59
62
|
report: {
|
|
60
63
|
version: 2,
|
|
@@ -64,7 +67,9 @@ export function buildPathgradeReport(input) {
|
|
|
64
67
|
attempts_completed: attemptsCompleted,
|
|
65
68
|
overall_mean_reward: overallMeanReward,
|
|
66
69
|
overall_pass_rate: overallMeanReward,
|
|
67
|
-
|
|
70
|
+
runner_status: runnerStatus,
|
|
71
|
+
threshold_status: thresholdStatus,
|
|
72
|
+
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
68
73
|
groups: consolidatedGroups,
|
|
69
74
|
...(input.selection ? { selection: input.selection } : {}),
|
|
70
75
|
},
|
|
@@ -185,15 +190,20 @@ function buildEvalReport(groupName, cases, run) {
|
|
|
185
190
|
.filter((score) => score !== undefined);
|
|
186
191
|
const meanReward = average(rewards);
|
|
187
192
|
const metrics = binaryMetrics(cases, run);
|
|
193
|
+
const runnerStatus = cases.every(testCase => (testCase.state !== 'failed'
|
|
194
|
+
&& testCase.attempts.every(attempt => attempt.outcome.kind === 'passed'))) ? 'pass' : 'fail';
|
|
195
|
+
const thresholdStatus = run.threshold == null
|
|
196
|
+
? 'not_configured'
|
|
197
|
+
: meanReward >= run.threshold ? 'pass' : 'fail';
|
|
188
198
|
const skills = new Set();
|
|
189
199
|
for (const trial of trials)
|
|
190
200
|
for (const skill of trial.skills_used ?? [])
|
|
191
201
|
skills.add(skill);
|
|
192
202
|
return {
|
|
193
203
|
task: groupName,
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
204
|
+
runner_status: runnerStatus,
|
|
205
|
+
threshold_status: thresholdStatus,
|
|
206
|
+
status: runnerStatus === 'fail' || thresholdStatus === 'fail' ? 'fail' : 'pass',
|
|
197
207
|
mean_reward: meanReward,
|
|
198
208
|
...(metrics.successRate !== undefined ? { success_rate: metrics.successRate } : {}),
|
|
199
209
|
...(metrics.passAtK ? {
|