praxis-agent 0.64.1 → 0.65.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.
- package/README.md +6 -4
- package/dist/cli-runtime.js +23 -2
- package/dist/evals/eval-contract.d.ts +11 -0
- package/dist/evals/project-eval-comparison.d.ts +3 -1
- package/dist/evals/project-eval-comparison.js +31 -5
- package/dist/evals/project-eval-identity.d.ts +51 -0
- package/dist/evals/project-eval-identity.js +341 -0
- package/dist/evals/project-eval-runner.d.ts +7 -4
- package/dist/evals/project-eval-runner.js +39 -17
- package/dist/evals/project-eval.d.ts +7 -4
- package/dist/evals/project-eval.js +14 -3
- package/dist/providers/provider-registry.d.ts +2 -1
- package/dist/providers/provider-registry.js +23 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -134,10 +134,12 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
134
134
|
|
|
135
135
|
- **Outcome-driven evaluation** — `praxis eval <target>` runs contained cases
|
|
136
136
|
in isolated workspaces, requires explicit verifier authorization, and writes
|
|
137
|
-
versioned artifacts locally; usage and cost
|
|
138
|
-
unknown. Separate runs can be compared with
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
versioned artifacts with deterministic run identities locally; usage and cost
|
|
138
|
+
remain explicitly available or unknown. Separate runs can be compared with
|
|
139
|
+
`praxis eval compare` only when provider, model, configuration, tools, prompt,
|
|
140
|
+
corpus, and runtime-environment identities match. Unknown token/cost evidence
|
|
141
|
+
produces null deltas, while the gate requires no pass-rate or safety-rate
|
|
142
|
+
regression and rejects incomplete safety evidence.
|
|
141
143
|
- **Local agent runtime** — C+ Quiet Operator responsive TUI with a linear
|
|
142
144
|
`❯` user / `⏺` assistant conversation, `✻` thinking activity, and `!` shell
|
|
143
145
|
composer grammar, compact stable tool rows, responsive density,
|
package/dist/cli-runtime.js
CHANGED
|
@@ -51,7 +51,7 @@ import { FallbackModelProvider } from './providers/fallback-provider.js';
|
|
|
51
51
|
import { ProviderCredentialVault } from './persistence/provider-credential-vault.js';
|
|
52
52
|
import { parseContextEnvironment, parseProviderEnvironment, } from './providers/environment.js';
|
|
53
53
|
import { ProviderAuthenticationError, resolveProviderCredential, } from './providers/provider-auth.js';
|
|
54
|
-
import { resolveProviderContextWindowTokens, resolveProviderRegistry, } from './providers/provider-registry.js';
|
|
54
|
+
import { resolveProviderContextWindowTokens, resolveProviderRegistry, resolveProviderRuntimeTarget, } from './providers/provider-registry.js';
|
|
55
55
|
import { ProviderSettingsError, resolveProviderTarget, } from './providers/provider-settings.js';
|
|
56
56
|
import { ModelPricingRegistry, usageCostUsd } from './core/usage.js';
|
|
57
57
|
import { LocalToolRegistry } from './tools/local-tools.js';
|
|
@@ -2358,6 +2358,27 @@ const defaultPluginEvalRuntimeFactory = {
|
|
|
2358
2358
|
};
|
|
2359
2359
|
},
|
|
2360
2360
|
};
|
|
2361
|
+
const defaultProjectEvalRuntimeFactory = {
|
|
2362
|
+
create: (options) => defaultPluginEvalRuntimeFactory.create(options),
|
|
2363
|
+
identify: async (options) => {
|
|
2364
|
+
const environment = process.env;
|
|
2365
|
+
const target = await resolveProviderRuntimeTarget({
|
|
2366
|
+
configRoot: options.configRoot,
|
|
2367
|
+
cwd: options.cwd,
|
|
2368
|
+
environment,
|
|
2369
|
+
...(options.model === undefined ? {} : { model: options.model }),
|
|
2370
|
+
includeSettings: true,
|
|
2371
|
+
includeProjectSettings: false,
|
|
2372
|
+
});
|
|
2373
|
+
return {
|
|
2374
|
+
providerId: target.providerId,
|
|
2375
|
+
profileId: target.profileId,
|
|
2376
|
+
protocol: target.protocol,
|
|
2377
|
+
endpoint: target.baseUrl,
|
|
2378
|
+
modelId: target.modelId,
|
|
2379
|
+
};
|
|
2380
|
+
},
|
|
2381
|
+
};
|
|
2361
2382
|
const defaultPluginEvalJudge = {
|
|
2362
2383
|
vote: async ({ criteria, focus, baseline, model, signal }) => {
|
|
2363
2384
|
const environment = process.env;
|
|
@@ -2600,7 +2621,7 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
|
|
|
2600
2621
|
judge: defaultPluginEvalJudge,
|
|
2601
2622
|
},
|
|
2602
2623
|
projectEval: {
|
|
2603
|
-
runtimeFactory:
|
|
2624
|
+
runtimeFactory: defaultProjectEvalRuntimeFactory,
|
|
2604
2625
|
version: VERSION,
|
|
2605
2626
|
configRoot: resolveDataPlaneRoot(),
|
|
2606
2627
|
},
|
|
@@ -27,6 +27,17 @@ export interface EvalRuntimeFactoryOptions {
|
|
|
27
27
|
export interface EvalRuntimeFactory {
|
|
28
28
|
create(options: EvalRuntimeFactoryOptions): Promise<EvalRuntime>;
|
|
29
29
|
}
|
|
30
|
+
export type EvalRuntimeFactoryIdentityOptions = Omit<EvalRuntimeFactoryOptions, 'eventSink'>;
|
|
31
|
+
export interface EvalRuntimeIdentityDescriptor {
|
|
32
|
+
providerId: string;
|
|
33
|
+
profileId: string;
|
|
34
|
+
protocol: string;
|
|
35
|
+
endpoint: string;
|
|
36
|
+
modelId: string;
|
|
37
|
+
}
|
|
38
|
+
export interface IdentifiedEvalRuntimeFactory extends EvalRuntimeFactory {
|
|
39
|
+
identify(options: EvalRuntimeFactoryIdentityOptions): Promise<EvalRuntimeIdentityDescriptor>;
|
|
40
|
+
}
|
|
30
41
|
export interface EvalTraceEvent {
|
|
31
42
|
type: string;
|
|
32
43
|
tool?: string;
|
|
@@ -20,18 +20,20 @@ export interface ProjectEvalComparisonMetric<T = number | null> {
|
|
|
20
20
|
delta: T;
|
|
21
21
|
}
|
|
22
22
|
export interface ProjectEvalComparisonResult {
|
|
23
|
-
schema_version: '1.
|
|
23
|
+
schema_version: '1.1';
|
|
24
24
|
baseline: {
|
|
25
25
|
name: string;
|
|
26
26
|
source_path: string;
|
|
27
27
|
version: string;
|
|
28
28
|
model: string | null;
|
|
29
|
+
identity_sha256: `sha256:${string}`;
|
|
29
30
|
};
|
|
30
31
|
candidate: {
|
|
31
32
|
name: string;
|
|
32
33
|
source_path: string;
|
|
33
34
|
version: string;
|
|
34
35
|
model: string | null;
|
|
36
|
+
identity_sha256: `sha256:${string}`;
|
|
35
37
|
};
|
|
36
38
|
comparable_run_count: number;
|
|
37
39
|
passed: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, lstat, readFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
4
|
+
import { assertProjectEvalIdentitiesComparable, validateProjectEvalAggregateIdentity, validateProjectEvalIdentity, } from './project-eval-identity.js';
|
|
4
5
|
const MAX_AGGREGATE_BYTES = 8 * 1024 * 1024;
|
|
5
6
|
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
6
7
|
export const PROJECT_EVAL_COMPARE_HELP = `Usage: praxis eval compare [options]
|
|
@@ -139,7 +140,10 @@ function validateRun(value, index) {
|
|
|
139
140
|
const runNumber = numberField(run.run, `${path}.run`, true);
|
|
140
141
|
if (runNumber < 1)
|
|
141
142
|
fail(`${path}.run`, 'must be positive');
|
|
142
|
-
stringField(run.model, `${path}.model
|
|
143
|
+
const model = stringField(run.model, `${path}.model`);
|
|
144
|
+
const identity = validateProjectEvalIdentity(run.identity);
|
|
145
|
+
if (model !== identity.model_id)
|
|
146
|
+
fail(`${path}.model`, 'does not match identity.model_id');
|
|
143
147
|
boolField(run.passed, `${path}.passed`);
|
|
144
148
|
if (run.score !== 0 && run.score !== 1)
|
|
145
149
|
fail(`${path}.score`, 'must be 0 or 1');
|
|
@@ -167,7 +171,7 @@ function validateRun(value, index) {
|
|
|
167
171
|
numberField(run.tool_errors, `${path}.tool_errors`, true);
|
|
168
172
|
numberField(run.retries, `${path}.retries`, true);
|
|
169
173
|
}
|
|
170
|
-
return run;
|
|
174
|
+
return { ...run, model, identity };
|
|
171
175
|
}
|
|
172
176
|
export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cwd()) {
|
|
173
177
|
const sourcePath = resolve(callerCwd, inputPath);
|
|
@@ -190,8 +194,8 @@ export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cw
|
|
|
190
194
|
}
|
|
191
195
|
const aggregate = objectField(value, 'root');
|
|
192
196
|
const data = aggregate;
|
|
193
|
-
if (aggregate.schema_version !== '1.
|
|
194
|
-
fail('schema_version', 'must be "1.0"');
|
|
197
|
+
if (aggregate.schema_version !== '1.1')
|
|
198
|
+
fail('schema_version', 'must be "1.1"; legacy "1.0" aggregates are unsupported');
|
|
195
199
|
stringField(aggregate.version, 'version');
|
|
196
200
|
stringField(aggregate.start, 'start');
|
|
197
201
|
numberField(aggregate.duration_ms, 'duration_ms');
|
|
@@ -230,6 +234,14 @@ export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cw
|
|
|
230
234
|
if (!Array.isArray(aggregate.runs) || aggregate.runs.length > 100000)
|
|
231
235
|
fail('runs', 'expected a bounded array');
|
|
232
236
|
const runs = aggregate.runs.map(validateRun);
|
|
237
|
+
for (const [index, run] of runs.entries())
|
|
238
|
+
if (run.identity.runtime.praxis_version !== aggregate.version)
|
|
239
|
+
fail(`runs[${index}].identity.runtime.praxis_version`, 'must match aggregate version');
|
|
240
|
+
const expectedAggregateModel = runs.length > 0 && runs.every((run) => run.model === runs[0]?.model)
|
|
241
|
+
? (runs[0]?.model ?? null)
|
|
242
|
+
: null;
|
|
243
|
+
if (aggregate.model !== expectedAggregateModel)
|
|
244
|
+
fail('model', 'does not match completed run identity models');
|
|
233
245
|
if (data.run_count !== runs.length)
|
|
234
246
|
fail('run_count', 'does not match runs length');
|
|
235
247
|
if (data.completed_run_count > data.planned_run_count ||
|
|
@@ -285,6 +297,11 @@ export async function loadProjectEvalAggregate(inputPath, callerCwd = process.cw
|
|
|
285
297
|
keys.add(key);
|
|
286
298
|
caseNames.add(run.case);
|
|
287
299
|
}
|
|
300
|
+
validateProjectEvalAggregateIdentity(aggregate.identity_sha256, runs.map((run) => ({
|
|
301
|
+
case: run.case,
|
|
302
|
+
run: run.run,
|
|
303
|
+
identity_sha256: run.identity.identity_sha256,
|
|
304
|
+
})));
|
|
288
305
|
if (!data.partial && caseNames.size !== data.case_count)
|
|
289
306
|
fail('case_count', 'does not match completed run cases');
|
|
290
307
|
if (data.interrupted && !data.partial)
|
|
@@ -393,6 +410,13 @@ export function compareProjectEvalAggregates(baseline, candidate, baselineName,
|
|
|
393
410
|
throw new Error('Aggregates have different comparable run sets');
|
|
394
411
|
if (leftRuns.length === 0)
|
|
395
412
|
throw new Error('Comparison requires at least one completed run');
|
|
413
|
+
for (let index = 0; index < leftRuns.length; index += 1) {
|
|
414
|
+
const leftRun = leftRuns[index];
|
|
415
|
+
const rightRun = rightRuns[index];
|
|
416
|
+
if (!leftRun || !rightRun)
|
|
417
|
+
continue;
|
|
418
|
+
assertProjectEvalIdentitiesComparable(leftRun.identity, rightRun.identity, `Identity mismatch for (${leftRun.case}, ${leftRun.run})`);
|
|
419
|
+
}
|
|
396
420
|
const regressions = rightRuns.flatMap((run, index) => leftRuns[index]?.passed && !run.passed
|
|
397
421
|
? [
|
|
398
422
|
{
|
|
@@ -418,18 +442,20 @@ export function compareProjectEvalAggregates(baseline, candidate, baselineName,
|
|
|
418
442
|
? metric(leftRuns.reduce((n, r) => n + r.permission_decisions[name], 0), rightRuns.reduce((n, r) => n + r.permission_decisions[name], 0))
|
|
419
443
|
: nullableMetric(null, null);
|
|
420
444
|
const result = {
|
|
421
|
-
schema_version: '1.
|
|
445
|
+
schema_version: '1.1',
|
|
422
446
|
baseline: {
|
|
423
447
|
name: baselineName,
|
|
424
448
|
source_path: baseline.sourcePath,
|
|
425
449
|
version: left.version,
|
|
426
450
|
model: left.model,
|
|
451
|
+
identity_sha256: left.identity_sha256,
|
|
427
452
|
},
|
|
428
453
|
candidate: {
|
|
429
454
|
name: candidateName,
|
|
430
455
|
source_path: candidate.sourcePath,
|
|
431
456
|
version: right.version,
|
|
432
457
|
model: right.model,
|
|
458
|
+
identity_sha256: right.identity_sha256,
|
|
433
459
|
},
|
|
434
460
|
comparable_run_count: leftRuns.length,
|
|
435
461
|
passed: regressions.length === 0 &&
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { EvalRuntimeIdentityDescriptor } from './eval-contract.js';
|
|
2
|
+
import type { ProjectEvalCase } from './project-eval-schema.js';
|
|
3
|
+
import type { FileManifest } from './project-eval-workspace.js';
|
|
4
|
+
export declare const PROJECT_EVAL_IDENTITY_SCHEMA_VERSION: "1.0";
|
|
5
|
+
export type ProjectEvalIdentitySchemaVersion = typeof PROJECT_EVAL_IDENTITY_SCHEMA_VERSION;
|
|
6
|
+
export type IdentityDigest = `sha256:${string}`;
|
|
7
|
+
export interface ProjectEvalRuntimeIdentity {
|
|
8
|
+
engine: 'praxis';
|
|
9
|
+
praxis_version: string;
|
|
10
|
+
node_version: string;
|
|
11
|
+
platform: string;
|
|
12
|
+
architecture: string;
|
|
13
|
+
runtime_sha256: IdentityDigest;
|
|
14
|
+
}
|
|
15
|
+
export interface ProjectEvalIdentity {
|
|
16
|
+
schema_version: ProjectEvalIdentitySchemaVersion;
|
|
17
|
+
provider_id: string;
|
|
18
|
+
profile_id: string;
|
|
19
|
+
protocol: string;
|
|
20
|
+
endpoint_sha256: IdentityDigest;
|
|
21
|
+
model_id: string;
|
|
22
|
+
configuration_sha256: IdentityDigest;
|
|
23
|
+
tools_sha256: IdentityDigest;
|
|
24
|
+
prompt_sha256: IdentityDigest;
|
|
25
|
+
corpus_sha256: IdentityDigest;
|
|
26
|
+
runtime: ProjectEvalRuntimeIdentity;
|
|
27
|
+
identity_sha256: IdentityDigest;
|
|
28
|
+
}
|
|
29
|
+
export interface CreateProjectEvalIdentityInput {
|
|
30
|
+
provider: EvalRuntimeIdentityDescriptor;
|
|
31
|
+
case: ProjectEvalCase;
|
|
32
|
+
sourceBefore: FileManifest;
|
|
33
|
+
effectiveTools: readonly string[];
|
|
34
|
+
runVerification: boolean;
|
|
35
|
+
praxisVersion: string;
|
|
36
|
+
nodeVersion?: string;
|
|
37
|
+
platform?: string;
|
|
38
|
+
architecture?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface AggregateIdentityRun {
|
|
41
|
+
case: string;
|
|
42
|
+
run: number;
|
|
43
|
+
identity_sha256: IdentityDigest;
|
|
44
|
+
}
|
|
45
|
+
export declare function createProjectEvalIdentity(input: CreateProjectEvalIdentityInput): ProjectEvalIdentity;
|
|
46
|
+
export declare function validateProjectEvalIdentity(value: unknown): ProjectEvalIdentity;
|
|
47
|
+
export declare function computeProjectEvalAggregateIdentity(runs: readonly AggregateIdentityRun[]): IdentityDigest;
|
|
48
|
+
export declare function validateProjectEvalAggregateIdentity(value: unknown, runs: readonly AggregateIdentityRun[]): IdentityDigest;
|
|
49
|
+
export declare function firstProjectEvalIdentityMismatch(left: ProjectEvalIdentity, right: ProjectEvalIdentity): string | null;
|
|
50
|
+
export declare function assertProjectEvalIdentitiesComparable(left: ProjectEvalIdentity, right: ProjectEvalIdentity, context?: string): void;
|
|
51
|
+
//# sourceMappingURL=project-eval-identity.d.ts.map
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { posix, win32 } from 'node:path';
|
|
3
|
+
import { redactSensitiveValue, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
4
|
+
export const PROJECT_EVAL_IDENTITY_SCHEMA_VERSION = '1.0';
|
|
5
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
6
|
+
const MAX_DEPTH = 32;
|
|
7
|
+
const MAX_NODES = 500_000;
|
|
8
|
+
const MAX_STRING = 64 * 1024;
|
|
9
|
+
function fail(message) {
|
|
10
|
+
throw new Error(`Invalid Project Eval identity: ${message}`);
|
|
11
|
+
}
|
|
12
|
+
function codeUnitCompare(left, right) {
|
|
13
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
14
|
+
}
|
|
15
|
+
function assertSerializable(value, path = '$', depth = 0, state = { nodes: 0 }, ancestry = new WeakSet()) {
|
|
16
|
+
state.nodes += 1;
|
|
17
|
+
if (state.nodes > MAX_NODES)
|
|
18
|
+
fail(`${path} exceeds object node limit`);
|
|
19
|
+
if (depth > MAX_DEPTH)
|
|
20
|
+
fail(`${path} exceeds object depth limit`);
|
|
21
|
+
if (value === null ||
|
|
22
|
+
typeof value === 'boolean' ||
|
|
23
|
+
typeof value === 'number') {
|
|
24
|
+
if (typeof value === 'number' && !Number.isFinite(value))
|
|
25
|
+
fail(`${path} contains a non-finite number`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
if (value.length > MAX_STRING)
|
|
30
|
+
fail(`${path} contains an oversized string`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === 'bigint' ||
|
|
34
|
+
typeof value === 'function' ||
|
|
35
|
+
typeof value === 'symbol' ||
|
|
36
|
+
value === undefined)
|
|
37
|
+
fail(`${path} contains an unsupported value`);
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
if (ancestry.has(value))
|
|
40
|
+
fail(`${path} contains a circular reference`);
|
|
41
|
+
ancestry.add(value);
|
|
42
|
+
try {
|
|
43
|
+
for (let index = 0; index < value.length; index += 1)
|
|
44
|
+
assertSerializable(value[index], `${path}[${index}]`, depth + 1, state, ancestry);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
ancestry.delete(value);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'object') {
|
|
52
|
+
const prototype = Object.getPrototypeOf(value);
|
|
53
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
54
|
+
fail(`${path} contains an unsupported object`);
|
|
55
|
+
if (ancestry.has(value))
|
|
56
|
+
fail(`${path} contains a circular reference`);
|
|
57
|
+
ancestry.add(value);
|
|
58
|
+
try {
|
|
59
|
+
for (const [key, child] of Object.entries(value)) {
|
|
60
|
+
if (key.length > 512)
|
|
61
|
+
fail(`${path} contains an oversized key`);
|
|
62
|
+
assertSerializable(child, `${path}.${key}`, depth + 1, state, ancestry);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
ancestry.delete(value);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
fail(`${path} contains an unsupported value`);
|
|
71
|
+
}
|
|
72
|
+
function canonical(value) {
|
|
73
|
+
assertSerializable(value);
|
|
74
|
+
const ancestry = new WeakSet();
|
|
75
|
+
const serialize = (item) => {
|
|
76
|
+
if (item === null || typeof item !== 'object')
|
|
77
|
+
return JSON.stringify(item);
|
|
78
|
+
if (ancestry.has(item))
|
|
79
|
+
fail('contains a circular reference');
|
|
80
|
+
ancestry.add(item);
|
|
81
|
+
try {
|
|
82
|
+
if (Array.isArray(item))
|
|
83
|
+
return `[${item.map((child) => serialize(child)).join(',')}]`;
|
|
84
|
+
return `{${Object.entries(item)
|
|
85
|
+
.sort(([left], [right]) => codeUnitCompare(left, right))
|
|
86
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${serialize(child)}`)
|
|
87
|
+
.join(',')}}`;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
ancestry.delete(item);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return serialize(value);
|
|
94
|
+
}
|
|
95
|
+
function digest(value) {
|
|
96
|
+
return `sha256:${createHash('sha256').update(canonical(value), 'utf8').digest('hex')}`;
|
|
97
|
+
}
|
|
98
|
+
function string(value, path) {
|
|
99
|
+
if (typeof value !== 'string' ||
|
|
100
|
+
value.length === 0 ||
|
|
101
|
+
value.length > MAX_STRING)
|
|
102
|
+
fail(`${path} must be a bounded non-empty string`);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
function digestField(value, path) {
|
|
106
|
+
if (typeof value !== 'string' || !DIGEST.test(value))
|
|
107
|
+
fail(`${path} must be a sha256 digest`);
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function positiveInteger(value, path) {
|
|
111
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
112
|
+
fail(`${path} must be a positive integer`);
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function configurationSource(input) {
|
|
116
|
+
const c = input.case;
|
|
117
|
+
const sensitiveValues = sensitiveEnvironmentValues(c.execution.env);
|
|
118
|
+
const graders = c.graders.map((grader) => grader.type === 'tool_used' && !Number.isFinite(grader.max)
|
|
119
|
+
? { ...grader, max: 'unbounded' }
|
|
120
|
+
: grader);
|
|
121
|
+
const configuration = {
|
|
122
|
+
case: {
|
|
123
|
+
name: c.name,
|
|
124
|
+
schema_version: c.schemaVersion,
|
|
125
|
+
},
|
|
126
|
+
execution: {
|
|
127
|
+
max_turns: c.execution.maxTurns,
|
|
128
|
+
timeout_seconds: c.execution.timeoutSeconds,
|
|
129
|
+
env: c.execution.env,
|
|
130
|
+
},
|
|
131
|
+
verification: {
|
|
132
|
+
enabled: input.runVerification,
|
|
133
|
+
definitions: c.verification,
|
|
134
|
+
},
|
|
135
|
+
graders,
|
|
136
|
+
expect: {
|
|
137
|
+
allowed_changed_paths: c.expect.allowedChangedPaths,
|
|
138
|
+
expected_changed_paths: c.expect.expectedChangedPaths,
|
|
139
|
+
forbidden_changed_paths: c.expect.forbiddenChangedPaths,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
return normalizeConfiguration(redactSensitiveValue(configuration, sensitiveValues));
|
|
143
|
+
}
|
|
144
|
+
function normalizeConfiguration(value) {
|
|
145
|
+
if (typeof value === 'string')
|
|
146
|
+
return posix.isAbsolute(value) || win32.isAbsolute(value)
|
|
147
|
+
? '[ABSOLUTE_PATH]'
|
|
148
|
+
: value;
|
|
149
|
+
if (Array.isArray(value))
|
|
150
|
+
return value.map(normalizeConfiguration);
|
|
151
|
+
if (value && typeof value === 'object')
|
|
152
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [
|
|
153
|
+
key,
|
|
154
|
+
normalizeConfiguration(child),
|
|
155
|
+
]));
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
function corpusSource(sourceBefore) {
|
|
159
|
+
return {
|
|
160
|
+
files: Object.fromEntries(Object.entries(sourceBefore.files).map(([path, file]) => [
|
|
161
|
+
path,
|
|
162
|
+
{
|
|
163
|
+
hash: file.hash,
|
|
164
|
+
size: file.size,
|
|
165
|
+
mode: file.mode,
|
|
166
|
+
},
|
|
167
|
+
])),
|
|
168
|
+
total_bytes: sourceBefore.totalBytes,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function freeze(value) {
|
|
172
|
+
if (value && typeof value === 'object') {
|
|
173
|
+
Object.freeze(value);
|
|
174
|
+
for (const child of Object.values(value))
|
|
175
|
+
freeze(child);
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
export function createProjectEvalIdentity(input) {
|
|
180
|
+
const provider = input.provider;
|
|
181
|
+
const runtimeBase = {
|
|
182
|
+
engine: 'praxis',
|
|
183
|
+
praxis_version: string(input.praxisVersion, 'praxis_version'),
|
|
184
|
+
node_version: string(input.nodeVersion ?? process.version, 'node_version'),
|
|
185
|
+
platform: string(input.platform ?? process.platform, 'platform'),
|
|
186
|
+
architecture: string(input.architecture ?? process.arch, 'architecture'),
|
|
187
|
+
};
|
|
188
|
+
const identityWithoutDigests = {
|
|
189
|
+
schema_version: PROJECT_EVAL_IDENTITY_SCHEMA_VERSION,
|
|
190
|
+
provider_id: string(provider.providerId, 'provider_id'),
|
|
191
|
+
profile_id: string(provider.profileId, 'profile_id'),
|
|
192
|
+
protocol: string(provider.protocol, 'protocol'),
|
|
193
|
+
endpoint_sha256: digest(string(provider.endpoint, 'endpoint')),
|
|
194
|
+
model_id: string(provider.modelId, 'model_id'),
|
|
195
|
+
configuration_sha256: digest(configurationSource(input)),
|
|
196
|
+
tools_sha256: digest(input.effectiveTools),
|
|
197
|
+
prompt_sha256: digest({
|
|
198
|
+
prompt: input.case.execution.prompt,
|
|
199
|
+
append_system_prompt: input.case.execution.appendSystemPrompt ?? null,
|
|
200
|
+
}),
|
|
201
|
+
corpus_sha256: digest(corpusSource(input.sourceBefore)),
|
|
202
|
+
runtime: {
|
|
203
|
+
...runtimeBase,
|
|
204
|
+
runtime_sha256: digest(runtimeBase),
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
return freeze({
|
|
208
|
+
...identityWithoutDigests,
|
|
209
|
+
identity_sha256: digest(identityWithoutDigests),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function validateRuntime(value, path) {
|
|
213
|
+
const runtime = value;
|
|
214
|
+
if (!runtime || typeof runtime !== 'object' || Array.isArray(runtime))
|
|
215
|
+
fail(`${path} must be an object`);
|
|
216
|
+
const runtimeKeys = [
|
|
217
|
+
'engine',
|
|
218
|
+
'praxis_version',
|
|
219
|
+
'node_version',
|
|
220
|
+
'platform',
|
|
221
|
+
'architecture',
|
|
222
|
+
'runtime_sha256',
|
|
223
|
+
];
|
|
224
|
+
const runtimeUnknown = Object.keys(runtime).find((key) => !runtimeKeys.includes(key));
|
|
225
|
+
if (runtimeUnknown)
|
|
226
|
+
fail(`${path}.${runtimeUnknown} is not supported`);
|
|
227
|
+
if (runtime.engine !== 'praxis')
|
|
228
|
+
fail(`${path}.engine must be "praxis"`);
|
|
229
|
+
const validated = {
|
|
230
|
+
engine: 'praxis',
|
|
231
|
+
praxis_version: string(runtime.praxis_version, `${path}.praxis_version`),
|
|
232
|
+
node_version: string(runtime.node_version, `${path}.node_version`),
|
|
233
|
+
platform: string(runtime.platform, `${path}.platform`),
|
|
234
|
+
architecture: string(runtime.architecture, `${path}.architecture`),
|
|
235
|
+
runtime_sha256: digestField(runtime.runtime_sha256, `${path}.runtime_sha256`),
|
|
236
|
+
};
|
|
237
|
+
if (validated.runtime_sha256 !==
|
|
238
|
+
digest({
|
|
239
|
+
engine: validated.engine,
|
|
240
|
+
praxis_version: validated.praxis_version,
|
|
241
|
+
node_version: validated.node_version,
|
|
242
|
+
platform: validated.platform,
|
|
243
|
+
architecture: validated.architecture,
|
|
244
|
+
}))
|
|
245
|
+
fail(`${path}.runtime_sha256 does not match runtime fields`);
|
|
246
|
+
return validated;
|
|
247
|
+
}
|
|
248
|
+
export function validateProjectEvalIdentity(value) {
|
|
249
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
250
|
+
fail('must be an object');
|
|
251
|
+
const source = value;
|
|
252
|
+
const keys = [
|
|
253
|
+
'schema_version',
|
|
254
|
+
'provider_id',
|
|
255
|
+
'profile_id',
|
|
256
|
+
'protocol',
|
|
257
|
+
'endpoint_sha256',
|
|
258
|
+
'model_id',
|
|
259
|
+
'configuration_sha256',
|
|
260
|
+
'tools_sha256',
|
|
261
|
+
'prompt_sha256',
|
|
262
|
+
'corpus_sha256',
|
|
263
|
+
'runtime',
|
|
264
|
+
'identity_sha256',
|
|
265
|
+
];
|
|
266
|
+
const unknown = Object.keys(source).find((key) => !keys.includes(key));
|
|
267
|
+
if (unknown)
|
|
268
|
+
fail(`${unknown} is not supported`);
|
|
269
|
+
if (source.schema_version !== PROJECT_EVAL_IDENTITY_SCHEMA_VERSION)
|
|
270
|
+
fail('schema_version must be "1.0"');
|
|
271
|
+
const validated = {
|
|
272
|
+
schema_version: PROJECT_EVAL_IDENTITY_SCHEMA_VERSION,
|
|
273
|
+
provider_id: string(source.provider_id, 'provider_id'),
|
|
274
|
+
profile_id: string(source.profile_id, 'profile_id'),
|
|
275
|
+
protocol: string(source.protocol, 'protocol'),
|
|
276
|
+
endpoint_sha256: digestField(source.endpoint_sha256, 'endpoint_sha256'),
|
|
277
|
+
model_id: string(source.model_id, 'model_id'),
|
|
278
|
+
configuration_sha256: digestField(source.configuration_sha256, 'configuration_sha256'),
|
|
279
|
+
tools_sha256: digestField(source.tools_sha256, 'tools_sha256'),
|
|
280
|
+
prompt_sha256: digestField(source.prompt_sha256, 'prompt_sha256'),
|
|
281
|
+
corpus_sha256: digestField(source.corpus_sha256, 'corpus_sha256'),
|
|
282
|
+
runtime: validateRuntime(source.runtime, 'runtime'),
|
|
283
|
+
identity_sha256: digestField(source.identity_sha256, 'identity_sha256'),
|
|
284
|
+
};
|
|
285
|
+
const withoutIdentity = { ...validated };
|
|
286
|
+
delete withoutIdentity.identity_sha256;
|
|
287
|
+
if (validated.identity_sha256 !== digest(withoutIdentity))
|
|
288
|
+
fail('identity_sha256 does not match identity fields');
|
|
289
|
+
return freeze(validated);
|
|
290
|
+
}
|
|
291
|
+
export function computeProjectEvalAggregateIdentity(runs) {
|
|
292
|
+
const entries = runs
|
|
293
|
+
.map((run) => ({
|
|
294
|
+
case: string(run.case, 'runs.case'),
|
|
295
|
+
run: positiveInteger(run.run, 'runs.run'),
|
|
296
|
+
identity_sha256: digestField(run.identity_sha256, 'runs.identity_sha256'),
|
|
297
|
+
}))
|
|
298
|
+
.sort((left, right) => {
|
|
299
|
+
const caseOrder = codeUnitCompare(left.case, right.case);
|
|
300
|
+
return caseOrder === 0 ? left.run - right.run : caseOrder;
|
|
301
|
+
});
|
|
302
|
+
return digest(entries);
|
|
303
|
+
}
|
|
304
|
+
export function validateProjectEvalAggregateIdentity(value, runs) {
|
|
305
|
+
const declared = digestField(value, 'identity_sha256');
|
|
306
|
+
const expected = computeProjectEvalAggregateIdentity(runs);
|
|
307
|
+
if (declared !== expected)
|
|
308
|
+
fail('aggregate identity_sha256 does not match runs');
|
|
309
|
+
return declared;
|
|
310
|
+
}
|
|
311
|
+
const comparableIdentityDimensions = [
|
|
312
|
+
['provider_id', 'provider_id'],
|
|
313
|
+
['profile_id', 'profile_id'],
|
|
314
|
+
['protocol', 'protocol'],
|
|
315
|
+
['endpoint_sha256', 'endpoint_sha256'],
|
|
316
|
+
['model_id', 'model_id'],
|
|
317
|
+
['configuration_sha256', 'configuration_sha256'],
|
|
318
|
+
['tools_sha256', 'tools_sha256'],
|
|
319
|
+
['prompt_sha256', 'prompt_sha256'],
|
|
320
|
+
['corpus_sha256', 'corpus_sha256'],
|
|
321
|
+
['runtime.engine', 'runtime.engine'],
|
|
322
|
+
['runtime.node_version', 'runtime.node_version'],
|
|
323
|
+
['runtime.platform', 'platform'],
|
|
324
|
+
['runtime.architecture', 'architecture'],
|
|
325
|
+
];
|
|
326
|
+
export function firstProjectEvalIdentityMismatch(left, right) {
|
|
327
|
+
for (const [field, label] of comparableIdentityDimensions) {
|
|
328
|
+
const read = (identity) => field.startsWith('runtime.')
|
|
329
|
+
? identity.runtime[field.slice('runtime.'.length)]
|
|
330
|
+
: identity[field];
|
|
331
|
+
if (read(left) !== read(right))
|
|
332
|
+
return label;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
export function assertProjectEvalIdentitiesComparable(left, right, context) {
|
|
337
|
+
const mismatch = firstProjectEvalIdentityMismatch(left, right);
|
|
338
|
+
if (mismatch)
|
|
339
|
+
throw new Error(`${context ? `${context}: ` : ''}non-comparable identity: ${mismatch} differs`);
|
|
340
|
+
}
|
|
341
|
+
//# sourceMappingURL=project-eval-identity.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ModelUsage } from '../core/runtime.js';
|
|
2
|
-
import { type EvalGraderResult, type
|
|
2
|
+
import { type EvalGraderResult, type IdentifiedEvalRuntimeFactory } from './eval-contract.js';
|
|
3
|
+
import { type ProjectEvalIdentity } from './project-eval-identity.js';
|
|
3
4
|
import type { ProjectEvalCase } from './project-eval-schema.js';
|
|
4
5
|
export type ProjectEvalTermination = 'timeout' | 'interrupted' | null;
|
|
5
6
|
export interface ProjectEvalVerificationResult {
|
|
@@ -17,11 +18,12 @@ export interface ProjectEvalVerificationResult {
|
|
|
17
18
|
error: string | null;
|
|
18
19
|
}
|
|
19
20
|
export interface ProjectEvalRunResult {
|
|
20
|
-
schema_version: '1.
|
|
21
|
+
schema_version: '1.1';
|
|
21
22
|
case: string;
|
|
22
23
|
run: number;
|
|
23
24
|
version: string;
|
|
24
|
-
model: string
|
|
25
|
+
model: string;
|
|
26
|
+
identity: ProjectEvalIdentity;
|
|
25
27
|
passed: boolean;
|
|
26
28
|
score: 0 | 1;
|
|
27
29
|
turns: number;
|
|
@@ -44,6 +46,7 @@ export interface ProjectEvalRunResult {
|
|
|
44
46
|
trace: 'trace.jsonl';
|
|
45
47
|
workspace_diff: 'workspace-diff.json';
|
|
46
48
|
verification: 'verification.json';
|
|
49
|
+
identity: 'identity.json';
|
|
47
50
|
result: 'result.json';
|
|
48
51
|
};
|
|
49
52
|
error: string | null;
|
|
@@ -52,7 +55,7 @@ export interface ProjectEvalRunResult {
|
|
|
52
55
|
}
|
|
53
56
|
interface ProjectEvalRunOptions {
|
|
54
57
|
case: ProjectEvalCase;
|
|
55
|
-
factory:
|
|
58
|
+
factory: IdentifiedEvalRuntimeFactory;
|
|
56
59
|
run: number;
|
|
57
60
|
allowTools?: readonly string[];
|
|
58
61
|
model?: string;
|
|
@@ -3,6 +3,7 @@ import { BoundedProcessRunner } from '../platform/bounded-process-runner.js';
|
|
|
3
3
|
import { redactSensitiveValue, redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
4
4
|
import { gradeDeterministicEvalRun } from './eval-graders.js';
|
|
5
5
|
import { normalizeEvalTraceEvent, resolveEvalAllowedTools, } from './eval-contract.js';
|
|
6
|
+
import { createProjectEvalIdentity, } from './project-eval-identity.js';
|
|
6
7
|
import { cleanupProjectEvalWorkspace, createProjectEvalWorkspace, diffProjectEvalWorkspace, } from './project-eval-workspace.js';
|
|
7
8
|
import { minimatch } from 'minimatch';
|
|
8
9
|
import { join } from 'node:path';
|
|
@@ -117,6 +118,38 @@ export async function runProjectEvalCase(options) {
|
|
|
117
118
|
let lastMessage = '';
|
|
118
119
|
let graderError = null;
|
|
119
120
|
let graders = [];
|
|
121
|
+
const effectiveModel = options.model ?? options.case.execution.model;
|
|
122
|
+
const factoryOptions = {
|
|
123
|
+
dataPlane: 'native',
|
|
124
|
+
cwd: workspace.cwd,
|
|
125
|
+
configRoot: workspace.config,
|
|
126
|
+
home: workspace.home,
|
|
127
|
+
maxTurns: options.case.execution.maxTurns,
|
|
128
|
+
pluginDirectories: [],
|
|
129
|
+
allowedTools,
|
|
130
|
+
...(effectiveModel ? { model: effectiveModel } : {}),
|
|
131
|
+
...(options.case.execution.appendSystemPrompt
|
|
132
|
+
? { appendSystemPrompt: options.case.execution.appendSystemPrompt }
|
|
133
|
+
: {}),
|
|
134
|
+
addDirs: [],
|
|
135
|
+
env: options.case.execution.env,
|
|
136
|
+
};
|
|
137
|
+
let identity;
|
|
138
|
+
try {
|
|
139
|
+
const descriptor = await options.factory.identify(factoryOptions);
|
|
140
|
+
identity = createProjectEvalIdentity({
|
|
141
|
+
provider: descriptor,
|
|
142
|
+
case: options.case,
|
|
143
|
+
sourceBefore: workspace.sourceBefore,
|
|
144
|
+
effectiveTools: allowedTools,
|
|
145
|
+
runVerification: options.runVerification ?? false,
|
|
146
|
+
praxisVersion: options.version,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
await cleanupProjectEvalWorkspace(workspace.root).catch(() => undefined);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
120
153
|
const runtimeController = new AbortController();
|
|
121
154
|
let deadlineReached = false;
|
|
122
155
|
const forwardAbort = () => runtimeController.abort(options.signal?.reason);
|
|
@@ -129,21 +162,7 @@ export async function runProjectEvalCase(options) {
|
|
|
129
162
|
}, options.case.execution.timeoutSeconds * 1000);
|
|
130
163
|
try {
|
|
131
164
|
runtime = await options.factory.create({
|
|
132
|
-
|
|
133
|
-
cwd: workspace.cwd,
|
|
134
|
-
configRoot: workspace.config,
|
|
135
|
-
home: workspace.home,
|
|
136
|
-
maxTurns: options.case.execution.maxTurns,
|
|
137
|
-
pluginDirectories: [],
|
|
138
|
-
allowedTools,
|
|
139
|
-
...((options.model ?? options.case.execution.model)
|
|
140
|
-
? { model: options.model ?? options.case.execution.model }
|
|
141
|
-
: {}),
|
|
142
|
-
...(options.case.execution.appendSystemPrompt
|
|
143
|
-
? { appendSystemPrompt: options.case.execution.appendSystemPrompt }
|
|
144
|
-
: {}),
|
|
145
|
-
addDirs: [],
|
|
146
|
-
env: options.case.execution.env,
|
|
165
|
+
...factoryOptions,
|
|
147
166
|
eventSink: (event) => {
|
|
148
167
|
if (traceOverflow)
|
|
149
168
|
return;
|
|
@@ -332,6 +351,7 @@ export async function runProjectEvalCase(options) {
|
|
|
332
351
|
await writeFileAtomically(join(runDirectory, 'trace.jsonl'), traceRecords.join('\n'));
|
|
333
352
|
await writeFileAtomically(join(runDirectory, 'workspace-diff.json'), JSON.stringify({ schema_version: '1.0', ...workspaceDiff }, null, 2));
|
|
334
353
|
await writeFileAtomically(join(runDirectory, 'verification.json'), JSON.stringify(verifications, null, 2));
|
|
354
|
+
await writeFileAtomically(join(runDirectory, 'identity.json'), JSON.stringify(identity, null, 2));
|
|
335
355
|
}
|
|
336
356
|
catch (error) {
|
|
337
357
|
artifactError = errorText(error);
|
|
@@ -361,11 +381,12 @@ export async function runProjectEvalCase(options) {
|
|
|
361
381
|
checks.find((item) => !item.passed)?.explanation ??
|
|
362
382
|
null;
|
|
363
383
|
const result = {
|
|
364
|
-
schema_version: '1.
|
|
384
|
+
schema_version: '1.1',
|
|
365
385
|
case: options.case.name,
|
|
366
386
|
run: options.run,
|
|
367
387
|
version: options.version,
|
|
368
|
-
model:
|
|
388
|
+
model: identity.model_id,
|
|
389
|
+
identity,
|
|
369
390
|
passed,
|
|
370
391
|
score: passed ? 1 : 0,
|
|
371
392
|
turns,
|
|
@@ -381,6 +402,7 @@ export async function runProjectEvalCase(options) {
|
|
|
381
402
|
trace: 'trace.jsonl',
|
|
382
403
|
workspace_diff: 'workspace-diff.json',
|
|
383
404
|
verification: 'verification.json',
|
|
405
|
+
identity: 'identity.json',
|
|
384
406
|
result: 'result.json',
|
|
385
407
|
},
|
|
386
408
|
error: primaryError,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ModelUsage } from '../core/runtime.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { IdentifiedEvalRuntimeFactory } from './eval-contract.js';
|
|
3
3
|
import { type ProjectEvalRunResult } from './project-eval-runner.js';
|
|
4
|
+
import { type ProjectEvalIdentity } from './project-eval-identity.js';
|
|
4
5
|
export declare const PROJECT_EVAL_HELP = "Usage: praxis eval [options] <target>\n\nRun deterministic project outcome evaluations in isolated workspaces.\n\nOptions:\n --case <glob> Filter case names\n --tag <tag[,tag]> Filter tags; repeatable\n --runs <1..50> Override run count\n --model <model> Override model\n --allow-tools <rules> Grant gated tools; comma-separated and repeatable\n --run-verification Enable verifier subprocesses\n --output-dir <dir> Write artifacts to this directory\n --keep-temp Preserve temporary workspaces\n --json Print exactly one aggregate JSON value\n --verbose Print run progress to stderr\n -h, --help Display help\n\nUse praxis eval compare --help to compare two completed aggregate artifacts.";
|
|
5
6
|
export interface ProjectEvalDependencies {
|
|
6
|
-
runtimeFactory:
|
|
7
|
+
runtimeFactory: IdentifiedEvalRuntimeFactory;
|
|
7
8
|
version?: string;
|
|
8
9
|
configRoot: string;
|
|
9
10
|
}
|
|
@@ -31,7 +32,7 @@ export interface ProjectEvalUsageTotals {
|
|
|
31
32
|
export interface ProjectEvalRunSummary {
|
|
32
33
|
case: string;
|
|
33
34
|
run: number;
|
|
34
|
-
model: string
|
|
35
|
+
model: string;
|
|
35
36
|
passed: boolean;
|
|
36
37
|
score: 0 | 1;
|
|
37
38
|
turns: number;
|
|
@@ -50,9 +51,10 @@ export interface ProjectEvalRunSummary {
|
|
|
50
51
|
retries: number;
|
|
51
52
|
error: string | null;
|
|
52
53
|
artifact_dir: string;
|
|
54
|
+
identity: ProjectEvalIdentity;
|
|
53
55
|
}
|
|
54
56
|
export interface ProjectEvalAggregate {
|
|
55
|
-
schema_version: '1.
|
|
57
|
+
schema_version: '1.1';
|
|
56
58
|
version: string;
|
|
57
59
|
start: string;
|
|
58
60
|
duration_ms: number;
|
|
@@ -89,6 +91,7 @@ export interface ProjectEvalAggregate {
|
|
|
89
91
|
};
|
|
90
92
|
partial: boolean;
|
|
91
93
|
interrupted: boolean;
|
|
94
|
+
identity_sha256: `sha256:${string}`;
|
|
92
95
|
runs: readonly ProjectEvalRunSummary[];
|
|
93
96
|
}
|
|
94
97
|
export declare function parseProjectEvalOptions(argv: readonly string[]): ProjectEvalOptions;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, relative, resolve } from 'node:path';
|
|
2
2
|
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
3
3
|
import { runProjectEvalCase, } from './project-eval-runner.js';
|
|
4
|
+
import { computeProjectEvalAggregateIdentity, } from './project-eval-identity.js';
|
|
4
5
|
import { executeProjectEvalCompareCommand } from './project-eval-comparison.js';
|
|
5
6
|
import { discoverProjectEvalCases } from './project-eval-schema.js';
|
|
6
7
|
export const PROJECT_EVAL_HELP = `Usage: praxis eval [options] <target>
|
|
@@ -134,6 +135,7 @@ function runSummary(result, outputDirectory) {
|
|
|
134
135
|
retries: result.retries,
|
|
135
136
|
error: result.error,
|
|
136
137
|
artifact_dir: relative(outputDirectory, join(outputDirectory, result.case, `run-${result.run}`)).replaceAll('\\', '/'),
|
|
138
|
+
identity: result.identity,
|
|
137
139
|
};
|
|
138
140
|
}
|
|
139
141
|
export async function executeProjectEvalCommand(argv, io, dependencies, callerCwd = process.cwd(), signal) {
|
|
@@ -184,14 +186,18 @@ export async function executeProjectEvalCommand(argv, io, dependencies, callerCw
|
|
|
184
186
|
}
|
|
185
187
|
const passed = results.filter((result) => result.passed).length;
|
|
186
188
|
const knownCostResults = results.filter((result) => result.cost_known);
|
|
189
|
+
const summaries = results.map((result) => runSummary(result, outputDirectory));
|
|
187
190
|
const aggregate = {
|
|
188
|
-
schema_version: '1.
|
|
191
|
+
schema_version: '1.1',
|
|
189
192
|
version: dependencies.version ?? 'unknown',
|
|
190
193
|
start: new Date(started).toISOString(),
|
|
191
194
|
duration_ms: Date.now() - started,
|
|
192
195
|
target,
|
|
193
196
|
output_dir: outputDirectory,
|
|
194
|
-
model:
|
|
197
|
+
model: summaries.length > 0 &&
|
|
198
|
+
summaries.every((summary) => summary.model === summaries[0]?.model)
|
|
199
|
+
? (summaries[0]?.model ?? null)
|
|
200
|
+
: null,
|
|
195
201
|
case_count: cases.length,
|
|
196
202
|
planned_run_count: plannedRunCount,
|
|
197
203
|
completed_run_count: results.length,
|
|
@@ -226,7 +232,12 @@ export async function executeProjectEvalCommand(argv, io, dependencies, callerCw
|
|
|
226
232
|
},
|
|
227
233
|
partial: interrupted || results.length < plannedRunCount,
|
|
228
234
|
interrupted,
|
|
229
|
-
|
|
235
|
+
identity_sha256: computeProjectEvalAggregateIdentity(summaries.map((summary) => ({
|
|
236
|
+
case: summary.case,
|
|
237
|
+
run: summary.run,
|
|
238
|
+
identity_sha256: summary.identity.identity_sha256,
|
|
239
|
+
}))),
|
|
240
|
+
runs: summaries,
|
|
230
241
|
};
|
|
231
242
|
await writeFileAtomically(join(outputDirectory, 'aggregate-result.json'), JSON.stringify(aggregate, null, 2));
|
|
232
243
|
if (options.json)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ModelProvider, ModelThinkingConfig } from '../core/runtime.js';
|
|
2
2
|
import { type CodexOAuthVault } from './codex-oauth.js';
|
|
3
|
-
import { type ProviderProtocol, type ProviderTarget } from './provider-settings.js';
|
|
3
|
+
import { type ResolveProviderTargetOptions, type ProviderProtocol, type ProviderTarget } from './provider-settings.js';
|
|
4
4
|
import { type AnthropicModelAliasOverrides } from './anthropic-model-alias.js';
|
|
5
5
|
import type { ProviderCredentialSourceMetadata, ProviderCredentialReader, ResolvedProviderCredential } from './provider-auth.js';
|
|
6
6
|
import { parseProviderEnvironment, type ContextEnvironment } from './environment.js';
|
|
@@ -44,6 +44,7 @@ export interface ResolveProviderRegistryOptions {
|
|
|
44
44
|
fetchImplementation?: typeof fetch;
|
|
45
45
|
}
|
|
46
46
|
export type ProviderRegistrySourceMetadata = ProviderCredentialSourceMetadata;
|
|
47
|
+
export declare function resolveProviderRuntimeTarget(options: ResolveProviderTargetOptions): Promise<ProviderTarget>;
|
|
47
48
|
export interface ProviderRegistry {
|
|
48
49
|
readonly target: ProviderTarget;
|
|
49
50
|
readonly credentialSource: ProviderRegistrySourceMetadata;
|
|
@@ -19,6 +19,24 @@ export class ProviderRegistryError extends Error {
|
|
|
19
19
|
this.code = code;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
function effectiveProviderTarget(target, overrides) {
|
|
23
|
+
if (target.providerId !== 'anthropic' ||
|
|
24
|
+
target.protocol !== 'anthropic-messages')
|
|
25
|
+
return target;
|
|
26
|
+
return {
|
|
27
|
+
...target,
|
|
28
|
+
modelId: resolveAnthropicModelAlias(target.modelId, overrides),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function resolveProviderRuntimeTarget(options) {
|
|
32
|
+
const environment = options.environment ?? process.env;
|
|
33
|
+
const target = await resolveProviderTarget({ ...options, environment });
|
|
34
|
+
const aliases = target.providerId === 'anthropic' &&
|
|
35
|
+
target.protocol === 'anthropic-messages'
|
|
36
|
+
? anthropicModelAliasOverridesFromEnvironment(environment)
|
|
37
|
+
: undefined;
|
|
38
|
+
return effectiveProviderTarget(target, aliases);
|
|
39
|
+
}
|
|
22
40
|
export function resolveProviderContextWindowTokens(options) {
|
|
23
41
|
if (options.protocol === 'anthropic-messages')
|
|
24
42
|
return resolveAnthropicModelSpec(options.modelId, options.explicitContextWindowTokens).contextWindowTokens;
|
|
@@ -113,8 +131,10 @@ class NativeProviderRegistry {
|
|
|
113
131
|
: { fetchImplementation: options.fetchImplementation });
|
|
114
132
|
}
|
|
115
133
|
}
|
|
116
|
-
create(modelId
|
|
117
|
-
const target =
|
|
134
|
+
create(modelId) {
|
|
135
|
+
const target = modelId === undefined
|
|
136
|
+
? this.target
|
|
137
|
+
: this.resolveTarget({ ...this.target, modelId });
|
|
118
138
|
if (target.protocol === 'codex-subscription') {
|
|
119
139
|
if (!this.codexManager)
|
|
120
140
|
throw new ProviderAuthenticationError('invalid_credential', 'Provider authentication failed: Codex subscription credentials are unavailable');
|
|
@@ -239,13 +259,7 @@ class NativeProviderRegistry {
|
|
|
239
259
|
return override !== undefined && override.trim().length > 0;
|
|
240
260
|
}
|
|
241
261
|
resolveTarget(target) {
|
|
242
|
-
|
|
243
|
-
target.protocol !== 'anthropic-messages')
|
|
244
|
-
return target;
|
|
245
|
-
return {
|
|
246
|
-
...target,
|
|
247
|
-
modelId: resolveAnthropicModelAlias(target.modelId, this.options.anthropicModelAliasOverrides),
|
|
248
|
-
};
|
|
262
|
+
return effectiveProviderTarget(target, this.options.anthropicModelAliasOverrides);
|
|
249
263
|
}
|
|
250
264
|
withDeadline(provider) {
|
|
251
265
|
const environment = this.options.providerEnvironment;
|