@aiwg/cli 2026.10.6 → 2026.10.7
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/bin/aiwg.mjs +4 -2
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/decision.js +3 -2
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +1 -1
- package/dist/src/cli/handlers/init.js +1 -1
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/setup.js +24 -20
- package/dist/src/cli/handlers/use.js +57 -15
- package/dist/src/cli/prompt-utils.js +73 -0
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +19 -6
- package/dist/src/decision/qualification/gate-evidence.js +6 -4
- package/dist/src/decision/qualification/load-staged.js +47 -10
- package/dist/src/decision/qualification/runner.js +7 -3
- package/dist/src/discovery/interview.js +10 -5
- package/dist/src/discovery/plan-apply.js +26 -30
- package/dist/src/discovery/plan-validation.js +2 -2
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +2 -2
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +17 -90
- package/dist/src/mcp/grok-build-config.mjs +2 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/worker-installation.d.mts +1 -1
- package/dist/src/models/worker-installation.mjs +65 -4
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/serve/worker-invocation.js +10 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/package.json +1 -1
- package/tools/agents/providers/base.mjs +5 -1
- package/tools/agents/providers/claude.mjs +5 -2
- package/tools/agents/providers/codex.mjs +4 -1
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +5 -2
- package/tools/agents/providers/hermes.mjs +5 -2
- package/tools/agents/providers/omp.mjs +3 -1
- package/tools/agents/providers/rule-delivery.mjs +29 -12
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +6 -3
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/process-monitor.mjs +42 -4
- package/tools/ralph-external/session-launcher.mjs +125 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ContextWriteJournal } from '../../smiths/context-pipeline/context-file-safety.js';
|
|
1
2
|
import { trackTemporaryDirectory } from '../../utils/temporary-directory.js';
|
|
2
3
|
/**
|
|
3
4
|
* Use Command Handler
|
|
@@ -1675,6 +1676,11 @@ export async function countPiAgentFiles(target, agentsPath) {
|
|
|
1675
1676
|
// and additive passes can describe the same destination more than once.
|
|
1676
1677
|
let usePreviewActions;
|
|
1677
1678
|
// Recursive framework/addon/provider passes share the boundary's resolved policy.
|
|
1679
|
+
async function generateDeploymentContextFiles(options) {
|
|
1680
|
+
const result = await generateContextFiles(options);
|
|
1681
|
+
useDeployment?.contextFiles.push(result);
|
|
1682
|
+
return result;
|
|
1683
|
+
}
|
|
1678
1684
|
let useDeployment;
|
|
1679
1685
|
function useDeploymentArgs(args) {
|
|
1680
1686
|
const forwarded = [...args];
|
|
@@ -2118,7 +2124,7 @@ async function generateGlobalProjectContext(opts) {
|
|
|
2118
2124
|
skills: userPaths.skills,
|
|
2119
2125
|
behaviors: userPaths.behaviors,
|
|
2120
2126
|
});
|
|
2121
|
-
await
|
|
2127
|
+
await generateDeploymentContextFiles({
|
|
2122
2128
|
provider: opts.provider,
|
|
2123
2129
|
projectPath: opts.projectPath,
|
|
2124
2130
|
sections,
|
|
@@ -2181,7 +2187,7 @@ async function ensurePostDeployPhases(opts) {
|
|
|
2181
2187
|
behaviors: paths.behaviors,
|
|
2182
2188
|
});
|
|
2183
2189
|
try {
|
|
2184
|
-
await
|
|
2190
|
+
await generateDeploymentContextFiles({
|
|
2185
2191
|
provider: opts.provider,
|
|
2186
2192
|
projectPath: opts.projectPath,
|
|
2187
2193
|
sections,
|
|
@@ -2194,8 +2200,8 @@ async function ensurePostDeployPhases(opts) {
|
|
|
2194
2200
|
},
|
|
2195
2201
|
});
|
|
2196
2202
|
}
|
|
2197
|
-
catch {
|
|
2198
|
-
|
|
2203
|
+
catch (error) {
|
|
2204
|
+
throw new Error(`Context generation failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
2199
2205
|
}
|
|
2200
2206
|
}
|
|
2201
2207
|
async function deploySourceDirectory(opts) {
|
|
@@ -2544,11 +2550,40 @@ export class UseHandler {
|
|
|
2544
2550
|
useDeployment = {
|
|
2545
2551
|
version: previousDeployment?.version ?? resolveDeployVersion(ctx.frameworkRoot || await getFrameworkRoot()),
|
|
2546
2552
|
skipContextFiles: previousDeployment?.skipContextFiles || ctx.args.includes('--no-context-files'),
|
|
2553
|
+
contextFiles: previousDeployment?.contextFiles ?? [],
|
|
2547
2554
|
};
|
|
2555
|
+
const journal = previousDeployment ? undefined : new ContextWriteJournal();
|
|
2548
2556
|
try {
|
|
2549
|
-
|
|
2557
|
+
let result;
|
|
2558
|
+
try {
|
|
2559
|
+
result = await this.executeDeployment(ctx);
|
|
2560
|
+
}
|
|
2561
|
+
catch (error) {
|
|
2562
|
+
result = { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
|
|
2563
|
+
}
|
|
2564
|
+
if (journal) {
|
|
2565
|
+
const rollback = result.exitCode !== 0 ? journal.rollback() : undefined;
|
|
2566
|
+
if (ctx.args.includes('--json')) {
|
|
2567
|
+
let structured;
|
|
2568
|
+
try {
|
|
2569
|
+
structured = JSON.parse(result.message || '{}');
|
|
2570
|
+
}
|
|
2571
|
+
catch {
|
|
2572
|
+
structured = { exitCode: result.exitCode, message: result.message };
|
|
2573
|
+
}
|
|
2574
|
+
structured.contextFiles = useDeployment.contextFiles;
|
|
2575
|
+
if (rollback)
|
|
2576
|
+
structured.contextRollback = rollback;
|
|
2577
|
+
result = { ...result, message: JSON.stringify(structured, null, 2), rawOutput: true };
|
|
2578
|
+
}
|
|
2579
|
+
else if (rollback?.conflicts.length) {
|
|
2580
|
+
result.message = [result.message, ...rollback.conflicts.map(conflict => `Context rollback conflict: ${conflict.path}: ${conflict.reason}`)].filter(Boolean).join('\n');
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return result;
|
|
2550
2584
|
}
|
|
2551
2585
|
finally {
|
|
2586
|
+
journal?.dispose();
|
|
2552
2587
|
useDeployment = previousDeployment;
|
|
2553
2588
|
}
|
|
2554
2589
|
}
|
|
@@ -2639,6 +2674,9 @@ export class UseHandler {
|
|
|
2639
2674
|
try {
|
|
2640
2675
|
coreResult = await this.executeCore({ ...ctx, args: coreArgs });
|
|
2641
2676
|
}
|
|
2677
|
+
catch (error) {
|
|
2678
|
+
coreResult = { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
|
|
2679
|
+
}
|
|
2642
2680
|
finally {
|
|
2643
2681
|
usePreviewActions = previousPreview;
|
|
2644
2682
|
if (json || dryRun) {
|
|
@@ -2664,12 +2702,18 @@ export class UseHandler {
|
|
|
2664
2702
|
else {
|
|
2665
2703
|
if (coreResult.exitCode === 0 && !dryRun && !VALID_FRAMEWORKS.includes(requestedBundle)) {
|
|
2666
2704
|
for (const provider of providers) {
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2705
|
+
try {
|
|
2706
|
+
await ensurePostDeployPhases({
|
|
2707
|
+
frameworkRoot,
|
|
2708
|
+
projectPath: projectDir,
|
|
2709
|
+
provider,
|
|
2710
|
+
args: remainingArgs,
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
catch (error) {
|
|
2714
|
+
coreResult = { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
|
|
2715
|
+
break;
|
|
2716
|
+
}
|
|
2673
2717
|
}
|
|
2674
2718
|
}
|
|
2675
2719
|
const providerResults = [];
|
|
@@ -4377,7 +4421,7 @@ export class UseHandler {
|
|
|
4377
4421
|
skills: paths.skills,
|
|
4378
4422
|
behaviors: paths.behaviors,
|
|
4379
4423
|
});
|
|
4380
|
-
const ctxResult = await
|
|
4424
|
+
const ctxResult = await generateDeploymentContextFiles({
|
|
4381
4425
|
provider: provider,
|
|
4382
4426
|
projectPath: target,
|
|
4383
4427
|
sections,
|
|
@@ -4431,10 +4475,8 @@ export class UseHandler {
|
|
|
4431
4475
|
}
|
|
4432
4476
|
}
|
|
4433
4477
|
catch (err) {
|
|
4434
|
-
// Non-fatal: context-pipeline emission must not block deployment.
|
|
4435
|
-
// Operator can re-run aiwg use to retry.
|
|
4436
4478
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4437
|
-
|
|
4479
|
+
return { exitCode: 1, message: `Context generation failed: ${msg}` };
|
|
4438
4480
|
}
|
|
4439
4481
|
}
|
|
4440
4482
|
span.end('use:complete', { framework });
|
|
@@ -88,7 +88,16 @@ export function askWithTimeout(rl, prompt, parse, fallback, fallbackLabel, timeo
|
|
|
88
88
|
const cleanup = () => {
|
|
89
89
|
clearTimeout(timer);
|
|
90
90
|
signal?.removeEventListener('abort', onAbort);
|
|
91
|
+
rl.removeListener('close', onClose);
|
|
91
92
|
};
|
|
93
|
+
const onClose = () => {
|
|
94
|
+
if (settled)
|
|
95
|
+
return;
|
|
96
|
+
settled = true;
|
|
97
|
+
cleanup();
|
|
98
|
+
resolve(fallback);
|
|
99
|
+
};
|
|
100
|
+
rl.on('close', onClose);
|
|
92
101
|
if (signal?.aborted) {
|
|
93
102
|
// Fire synchronously so the caller's try/catch sees the reject without
|
|
94
103
|
// awaiting a tick.
|
|
@@ -112,6 +121,70 @@ export function askWithTimeout(rl, prompt, parse, fallback, fallbackLabel, timeo
|
|
|
112
121
|
export async function askString(rl, prompt, fallback = '', signal) {
|
|
113
122
|
return askWithTimeout(rl, prompt, (a) => a.trim(), fallback, fallback || '(empty)', promptTimeoutMs(), signal);
|
|
114
123
|
}
|
|
124
|
+
/** Collect a hidden paste through an empty line, with one overall prompt deadline. */
|
|
125
|
+
export function askMultilineSecret(rl, signal) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const lines = [];
|
|
128
|
+
let length = 0;
|
|
129
|
+
let settled = false;
|
|
130
|
+
const cleanup = () => {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
signal?.removeEventListener('abort', onAbort);
|
|
133
|
+
rl.removeListener('line', onLine);
|
|
134
|
+
rl.removeListener('close', onClose);
|
|
135
|
+
lines.length = 0;
|
|
136
|
+
};
|
|
137
|
+
const finish = (value) => {
|
|
138
|
+
if (settled)
|
|
139
|
+
return;
|
|
140
|
+
settled = true;
|
|
141
|
+
cleanup();
|
|
142
|
+
resolve(value);
|
|
143
|
+
};
|
|
144
|
+
const onLine = (line) => {
|
|
145
|
+
if (!line) {
|
|
146
|
+
finish(lines.join('\n'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
length += line.length + (lines.length ? 1 : 0);
|
|
150
|
+
if (length > 16384) {
|
|
151
|
+
settled = true;
|
|
152
|
+
cleanup();
|
|
153
|
+
reject(new Error('Sensitive input exceeds 16384 characters.'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
lines.push(line);
|
|
157
|
+
};
|
|
158
|
+
// A closed terminal or timeout discards incomplete input, never echoes it.
|
|
159
|
+
const onClose = () => finish('');
|
|
160
|
+
const onAbort = () => {
|
|
161
|
+
if (settled)
|
|
162
|
+
return;
|
|
163
|
+
settled = true;
|
|
164
|
+
cleanup();
|
|
165
|
+
try {
|
|
166
|
+
rl.close();
|
|
167
|
+
}
|
|
168
|
+
catch { /* already closed */ }
|
|
169
|
+
reject(signal.reason ?? new Error('Aborted'));
|
|
170
|
+
};
|
|
171
|
+
const timeoutMs = promptTimeoutMs();
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
if (settled)
|
|
174
|
+
return;
|
|
175
|
+
ui.warn(` No input completed within ${Math.round(timeoutMs / 1000)}s — discarding incomplete sensitive input`);
|
|
176
|
+
finish('');
|
|
177
|
+
}, timeoutMs);
|
|
178
|
+
timer.unref?.();
|
|
179
|
+
rl.on('line', onLine);
|
|
180
|
+
rl.on('close', onClose);
|
|
181
|
+
if (signal?.aborted) {
|
|
182
|
+
onAbort();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
115
188
|
/**
|
|
116
189
|
* Prompt for a yes/no answer with timeout and default. Answers starting with
|
|
117
190
|
* 'y' (case-insensitive) are considered yes; everything else is no. On
|
|
@@ -13,6 +13,7 @@ import * as path from 'path';
|
|
|
13
13
|
import { execFile } from 'child_process';
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
import { boundedSpawnSync } from '../process/bounded-child-process.mjs';
|
|
16
|
+
import { helperEnvironment, HELPER_ENV_ALLOWLIST } from '../process/cli-command.mjs';
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
17
18
|
const GITIGNORE_PROBE_BASENAME = '.aiwg-ignore-probe';
|
|
18
19
|
// ===========================
|
|
@@ -94,6 +95,7 @@ async function isGitWorktree(projectRoot) {
|
|
|
94
95
|
try {
|
|
95
96
|
const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
96
97
|
cwd: projectRoot,
|
|
98
|
+
env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']),
|
|
97
99
|
windowsHide: true,
|
|
98
100
|
});
|
|
99
101
|
return stdout.trim() === 'true';
|
|
@@ -117,6 +119,7 @@ async function resolveCoveredPatterns(projectRoot, patterns, lines) {
|
|
|
117
119
|
try {
|
|
118
120
|
await execFileAsync('git', ['check-ignore', '--quiet', '--', probePath(pattern)], {
|
|
119
121
|
cwd: projectRoot,
|
|
122
|
+
env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']),
|
|
120
123
|
windowsHide: true,
|
|
121
124
|
});
|
|
122
125
|
return true;
|
|
@@ -239,6 +242,7 @@ export function dropGitignored(targetDir, files) {
|
|
|
239
242
|
}
|
|
240
243
|
const result = boundedSpawnSync('git', ['check-ignore', '-z', '--stdin'], 'quick', {
|
|
241
244
|
cwd: targetDir,
|
|
245
|
+
env: helperEnvironment(process.env, [...HELPER_ENV_ALLOWLIST, 'GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR']),
|
|
242
246
|
input: `${files.join('\0')}\0`,
|
|
243
247
|
encoding: 'utf8',
|
|
244
248
|
});
|
|
@@ -13,6 +13,7 @@ import { existsSync, statSync } from 'fs';
|
|
|
13
13
|
import { homedir } from 'os';
|
|
14
14
|
import { dirname, isAbsolute, relative, resolve } from 'path';
|
|
15
15
|
import { promisify } from 'util';
|
|
16
|
+
import { helperEnvironment } from '../process/cli-command.mjs';
|
|
16
17
|
import { getConfigPath, readAiwgConfig, resolveDelivery, resolveRemoteProvider, resolveRemotes, } from './aiwg-config.js';
|
|
17
18
|
const execFileAsync = promisify(execFile);
|
|
18
19
|
function expandHome(value) {
|
|
@@ -96,7 +97,7 @@ async function readRemoteUrl(repoPath, remote) {
|
|
|
96
97
|
if (remote.includes('://') || /^[^@/\s]+@[^:/\s]+:/.test(remote))
|
|
97
98
|
return remote;
|
|
98
99
|
try {
|
|
99
|
-
const { stdout } = await execFileAsync('git', ['-C', repoPath, 'remote', 'get-url', remote], { encoding: 'utf8' });
|
|
100
|
+
const { stdout } = await execFileAsync('git', ['-C', repoPath, 'remote', 'get-url', remote], { encoding: 'utf8', env: helperEnvironment() });
|
|
100
101
|
return stdout.trim() || undefined;
|
|
101
102
|
}
|
|
102
103
|
catch {
|
|
@@ -44,22 +44,15 @@ export class JevDecisionAdapter {
|
|
|
44
44
|
this.region = options.region?.trim() ? options.region : null;
|
|
45
45
|
this.observeResponseBody = options.observeResponseBody;
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return;
|
|
51
|
-
let parsed = {};
|
|
52
|
-
try {
|
|
53
|
-
parsed = asRecord(parseBoundedJson(body));
|
|
54
|
-
}
|
|
55
|
-
catch { /* Invalid answers still have bounded body evidence. */ }
|
|
56
|
-
let usage = emptyUsage();
|
|
47
|
+
/** An error response may still report billed usage and correlation metadata. */
|
|
48
|
+
parseErrorResponse(body) {
|
|
49
|
+
let parsed;
|
|
57
50
|
try {
|
|
58
|
-
|
|
51
|
+
parsed = parseBoundedJson(body);
|
|
59
52
|
}
|
|
60
|
-
catch { /*
|
|
61
|
-
this.
|
|
62
|
-
|
|
53
|
+
catch { /* Error classification does not require valid JSON. */ }
|
|
54
|
+
this.observeParsed(body, parsed);
|
|
55
|
+
return parsed;
|
|
63
56
|
}
|
|
64
57
|
/**
|
|
65
58
|
* Derives the observer's model/usage from a value `parseBoundedJson` has already produced (or `undefined`
|
|
@@ -81,6 +74,15 @@ export class JevDecisionAdapter {
|
|
|
81
74
|
catch { /* Missing usage is unknown, including on auth errors. */ }
|
|
82
75
|
return { actualModel: typeof record.model === 'string' && record.model ? record.model : null, usage };
|
|
83
76
|
}
|
|
77
|
+
responseMetadata(parsed, metadata) {
|
|
78
|
+
let body = {};
|
|
79
|
+
try {
|
|
80
|
+
body = asRecord(parsed);
|
|
81
|
+
}
|
|
82
|
+
catch { /* No body correlation metadata. */ }
|
|
83
|
+
const id = metadata.requestId ? null : safeRequestId(body.request_id ?? body.requestId);
|
|
84
|
+
return { ...metadata, ...this.observationMetadataOf(parsed), ...(id ? { requestId: id, requestIdSource: 'body' } : {}) };
|
|
85
|
+
}
|
|
84
86
|
/** Observes exactly once, from the single parse attempt `parsed` already represents (successful or not). */
|
|
85
87
|
observeParsed(body, parsed) {
|
|
86
88
|
this.observeResponseBody?.(body, this.observationMetadataOf(parsed));
|
|
@@ -209,22 +211,7 @@ export class JevDecisionAdapter {
|
|
|
209
211
|
if (!response.ok) {
|
|
210
212
|
try {
|
|
211
213
|
const errorBody = await readBoundedBody(response, signal);
|
|
212
|
-
this.
|
|
213
|
-
if (!metadata.requestId && response.headers.get('content-type')?.includes('json')) {
|
|
214
|
-
let parsedError;
|
|
215
|
-
try {
|
|
216
|
-
parsedError = parseBoundedJson(errorBody);
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
parsedError = null;
|
|
220
|
-
}
|
|
221
|
-
if (parsedError && typeof parsedError === 'object' && !Array.isArray(parsedError)) {
|
|
222
|
-
const field = parsedError.request_id ?? parsedError.requestId;
|
|
223
|
-
const safe = safeRequestId(field);
|
|
224
|
-
if (safe)
|
|
225
|
-
metadata = { ...metadata, requestId: safe, requestIdSource: 'body' };
|
|
226
|
-
}
|
|
227
|
-
}
|
|
214
|
+
metadata = this.responseMetadata(this.parseErrorResponse(errorBody), metadata);
|
|
228
215
|
}
|
|
229
216
|
catch {
|
|
230
217
|
if (request.signal.aborted)
|
|
@@ -248,6 +235,7 @@ export class JevDecisionAdapter {
|
|
|
248
235
|
finally {
|
|
249
236
|
this.observeParsed(encoded, parsed);
|
|
250
237
|
}
|
|
238
|
+
metadata = this.responseMetadata(parsed, metadata);
|
|
251
239
|
if (request.signal.aborted)
|
|
252
240
|
return externalInterruption(request, 'terminal-response', metadata);
|
|
253
241
|
if (deadline.aborted)
|
|
@@ -353,7 +341,7 @@ export class JevDecisionAdapter {
|
|
|
353
341
|
}
|
|
354
342
|
if (!response.ok) {
|
|
355
343
|
try {
|
|
356
|
-
this.
|
|
344
|
+
metadata = this.responseMetadata(this.parseErrorResponse(await readBoundedBody(response, signal)), metadata);
|
|
357
345
|
}
|
|
358
346
|
catch {
|
|
359
347
|
return batchFailure(requests, failure('invalid-output', metadata));
|
|
@@ -371,6 +359,7 @@ export class JevDecisionAdapter {
|
|
|
371
359
|
finally {
|
|
372
360
|
this.observeParsed(encoded, parsed);
|
|
373
361
|
}
|
|
362
|
+
metadata = this.responseMetadata(parsed, metadata);
|
|
374
363
|
const normalized = normalizeBatchResponse(requests, parsed).map(answer => ({
|
|
375
364
|
questionId: answer.questionId,
|
|
376
365
|
observation: { ...answer.observation, ...metadata },
|
package/dist/src/decision/ask.js
CHANGED
|
@@ -495,6 +495,11 @@ export async function setupJev(input = {}, options = {}) {
|
|
|
495
495
|
next: ['Run aiwg decision ask --question "<q>" --yes-no --json.'],
|
|
496
496
|
};
|
|
497
497
|
if (input.verify) {
|
|
498
|
+
// Verify the credential just saved, even when the shell still names an
|
|
499
|
+
// older token. Ordinary ask calls retain their environment precedence.
|
|
500
|
+
const verificationEnv = { ...environment };
|
|
501
|
+
delete verificationEnv.JEV_API_KEY;
|
|
502
|
+
delete verificationEnv.AIWG_DECISION_JEV_API_KEY;
|
|
498
503
|
const verified = await askDecision({
|
|
499
504
|
question: 'Is this Jev decision setup verification request well formed?',
|
|
500
505
|
mode: { kind: 'yes-no' },
|
|
@@ -502,7 +507,7 @@ export async function setupJev(input = {}, options = {}) {
|
|
|
502
507
|
timeoutMs: 15_000,
|
|
503
508
|
passes: 1,
|
|
504
509
|
escalationPasses: 1,
|
|
505
|
-
}, options);
|
|
510
|
+
}, { ...options, env: verificationEnv });
|
|
506
511
|
result.verified = verified.status === 'answered';
|
|
507
512
|
result.model = verified.model;
|
|
508
513
|
result.usage = verified.usage;
|
|
@@ -57,7 +57,7 @@ export function benchmarkComparison(capture, pricing) {
|
|
|
57
57
|
return `| Jev, ${passes} pass${passes === 1 ? '' : 'es'} | ${display(t.input)} | n/a | ${display(t.output)} | ${display(t.input === null || t.output === null ? null : t.input + t.output)} | ${t.latency.toFixed(1)} | ${money(t.cost)} |`;
|
|
58
58
|
}),
|
|
59
59
|
`| ${capture.baselineLabel.replace(/[|\r\n]/g, ' ')} | ${display(baseline.input)} | ${display(baseline.cached)} | ${display(baseline.output)} | ${display(baseline.input === null || baseline.output === null ? null : baseline.input + baseline.output)} | ${baseline.latency.toFixed(1)} | ${money(baseline.cost)} |`, '',
|
|
60
|
-
capture.rows.some(r => r.baseline.cost || r.jev.some(s => s.cost)) ? 'Captured costs use sourced rates recorded per observation; unknown costs remain unknown.' : pricing ? `Rates: decision.ask.cost.* (${pricing.source ?? 'manual'}, checked ${pricing.checked_at ?? 'unknown'}).` : 'Costs unknown: no explicit decision.ask.cost.* rates.',
|
|
60
|
+
capture.rows.some(r => r.baseline.cost || r.jev.some(s => s.cost)) ? 'Captured costs use provider-reported USD or sourced rates recorded per observation; unknown costs remain unknown.' : pricing ? `Rates: decision.ask.cost.* (${pricing.source ?? 'manual'}, checked ${pricing.checked_at ?? 'unknown'}).` : 'Costs unknown: no explicit decision.ask.cost.* rates.',
|
|
61
61
|
'Costs use reported usage and explicit pricing assumptions, not subscription invoices.',
|
|
62
62
|
'Standalone baseline requests do not measure the marginal cost of an inline LLM decision.',
|
|
63
63
|
];
|
|
@@ -116,6 +116,9 @@ function validCost(sample) {
|
|
|
116
116
|
if (sample.cost === undefined)
|
|
117
117
|
return true;
|
|
118
118
|
const cost = sample.cost;
|
|
119
|
+
if (cost?.source === 'provider-reported')
|
|
120
|
+
return fields(cost, ['usd', 'price', 'source'])
|
|
121
|
+
&& cost.price === null && typeof cost.usd === 'number' && Number.isFinite(cost.usd) && cost.usd >= 0;
|
|
119
122
|
return fields(cost, ['usd', 'price']) && (cost.price === null ? cost.usd === null
|
|
120
123
|
: validPrice(cost.price) && cost.price.model === sample.model && cost.usd === tokenCost(sample.usage, cost.price));
|
|
121
124
|
}
|
|
@@ -128,10 +131,18 @@ export async function collectBenchmark(dataset, options) {
|
|
|
128
131
|
throw new Error('Positive maxUsd required for live collection.');
|
|
129
132
|
if (!Number.isInteger(options.passes) || options.passes < 1 || options.passes > MAX_BENCHMARK_PASSES)
|
|
130
133
|
throw new Error('passes must be 1..20.');
|
|
134
|
+
if (options.jevCapture && options.extendCapture)
|
|
135
|
+
throw new Error('Cannot combine Jev reuse and capture extension.');
|
|
131
136
|
const cases = splitBenchmarkHoldout(dataset, options.holdout ?? 0.3, options.seed ?? '3291');
|
|
132
137
|
const prior = options.extendCapture;
|
|
133
138
|
if (prior)
|
|
134
139
|
validateBenchmarkCapture(cases, prior);
|
|
140
|
+
const reused = options.jevCapture;
|
|
141
|
+
if (reused) {
|
|
142
|
+
validateBenchmarkCapture(cases, reused);
|
|
143
|
+
if (reused.rows.length !== cases.length || reused.rows.some(row => row.jev.length !== options.passes))
|
|
144
|
+
throw new Error('Reused Jev capture must contain every case with the same pass count.');
|
|
145
|
+
}
|
|
135
146
|
if (typeof options.baselineLabel !== 'string' || !options.baselineLabel.trim() || options.baselineLabel.length > 200
|
|
136
147
|
|| prior && prior.baselineLabel !== options.baselineLabel)
|
|
137
148
|
throw new Error('Invalid or changed baseline label.');
|
|
@@ -147,7 +158,7 @@ export async function collectBenchmark(dataset, options) {
|
|
|
147
158
|
throw new Error('Extension cannot remove existing passes.');
|
|
148
159
|
const planned = cases.reduce((sum, c) => {
|
|
149
160
|
const row = rows.find(r => r.id === c.id);
|
|
150
|
-
return sum + (row ? 0 : 1) + Math.max(0, options.passes - (row?.jev.length ?? 0));
|
|
161
|
+
return sum + (row ? 0 : 1) + (reused ? 0 : Math.max(0, options.passes - (row?.jev.length ?? 0)));
|
|
151
162
|
}, 0);
|
|
152
163
|
if (planned > options.maxCalls)
|
|
153
164
|
throw new Error(`Insufficient maxCalls: need ${planned}; no providers invoked.`);
|
|
@@ -177,7 +188,7 @@ export async function collectBenchmark(dataset, options) {
|
|
|
177
188
|
return capture;
|
|
178
189
|
}
|
|
179
190
|
const environment = options.env ?? process.env;
|
|
180
|
-
if (!options.jev && planned > 0 && cases.some(c => (rows.find(r => r.id === c.id)?.jev.length ?? 0) < options.passes)
|
|
191
|
+
if (!reused && !options.jev && planned > 0 && cases.some(c => (rows.find(r => r.id === c.id)?.jev.length ?? 0) < options.passes)
|
|
181
192
|
&& !(environment.JEV_API_KEY?.trim() || environment.AIWG_DECISION_JEV_API_KEY?.trim()))
|
|
182
193
|
throw new Error('Live benchmark requires JEV_API_KEY or AIWG_DECISION_JEV_API_KEY in the caller environment.');
|
|
183
194
|
const jev = options.jev ?? (async (input) => {
|
|
@@ -201,8 +212,9 @@ export async function collectBenchmark(dataset, options) {
|
|
|
201
212
|
if (!validObservation(sample) || !modeAnswer(c, sample))
|
|
202
213
|
throw new Error(`Invalid provider observation: ${c.id}`);
|
|
203
214
|
const price = (baseline ? options.baselinePrices ?? [] : jevPrices).find(p => p.model === sample.model) ?? null;
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
sample.cost = result.cost?.source === 'provider-reported' && validCost(result)
|
|
216
|
+
? structuredClone(result.cost) : { usd: price ? tokenCost(sample.usage, price) : null, price: structuredClone(price) };
|
|
217
|
+
const usd = sample.cost.usd;
|
|
206
218
|
if (usd === null)
|
|
207
219
|
stopReason = 'unknown-cost';
|
|
208
220
|
else {
|
|
@@ -215,7 +227,8 @@ export async function collectBenchmark(dataset, options) {
|
|
|
215
227
|
for (const c of cases) {
|
|
216
228
|
let row = rows.find(r => r.id === c.id);
|
|
217
229
|
if (!row) {
|
|
218
|
-
row = { id: c.id, baseline: await invoke(c, true),
|
|
230
|
+
row = { id: c.id, baseline: await invoke(c, true),
|
|
231
|
+
jev: structuredClone(reused?.rows.find(r => r.id === c.id).jev ?? []) };
|
|
219
232
|
rows.push(row);
|
|
220
233
|
}
|
|
221
234
|
if (options.onCheckpoint)
|
|
@@ -4,7 +4,7 @@ import { matchEvidenceDigest } from '../../gates/stats/index.js';
|
|
|
4
4
|
import { CalibrationRegistry, calibrationArtifactDigest } from '../calibration/registry.js';
|
|
5
5
|
import { validateCaseInventory } from './manifest.js';
|
|
6
6
|
import { verifyQualificationSplits } from './quality.js';
|
|
7
|
-
import { verifyDecisionStagedLoadRecord } from './load.js';
|
|
7
|
+
import { DECISION_STAGED_LOAD_RESULT_VERSIONS, verifyDecisionStagedLoadRecord } from './load.js';
|
|
8
8
|
import { validateReviewEvaluation } from '../review/validate.js';
|
|
9
9
|
const range = (prefix, from, to) => Array.from({ length: to - from + 1 }, (_, index) => `${prefix}${String(from + index).padStart(2, '0')}`);
|
|
10
10
|
export const DECISION_GATE_SUITES = {
|
|
@@ -34,7 +34,7 @@ export const DECISION_GATE_SUITES = {
|
|
|
34
34
|
export const GATE_ARTIFACT_SCHEMAS = {
|
|
35
35
|
'immutable-splits': 'decision-binary-benchmark-plan/v2',
|
|
36
36
|
'calibration-qualified': 'decision-calibration-artifact/v1',
|
|
37
|
-
'load-manifest-qualified': 'decision-load-result/
|
|
37
|
+
'load-manifest-qualified': 'decision-load-result/v2',
|
|
38
38
|
'review-decision-recorded': 'decision-qualification-review/v1',
|
|
39
39
|
};
|
|
40
40
|
export const GATE_ARTIFACT_FLAGS = Object.keys(GATE_ARTIFACT_SCHEMAS);
|
|
@@ -127,7 +127,7 @@ function validCalibration(value, context) {
|
|
|
127
127
|
&& artifact.metrics.selectiveRisk <= artifact.profile.maximumSelectiveRisk;
|
|
128
128
|
}
|
|
129
129
|
function validLoadResult(value, context) {
|
|
130
|
-
if (!record(value) || value.schemaVersion
|
|
130
|
+
if (!record(value) || !DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(String(value.schemaVersion)))
|
|
131
131
|
return false;
|
|
132
132
|
// Legacy records remain readable, but only completed live v3 evidence bound to
|
|
133
133
|
// this exact clean source revision can qualify the performance gate.
|
|
@@ -177,7 +177,9 @@ export function deriveGateEvidence(manifest, proof = {}) {
|
|
|
177
177
|
const ref = manifest.gateArtifacts?.[flag];
|
|
178
178
|
const versionOk = flag === 'immutable-splits'
|
|
179
179
|
? ref !== undefined && IMMUTABLE_SPLIT_VERSIONS.includes(ref.schemaVersion)
|
|
180
|
-
:
|
|
180
|
+
: flag === 'load-manifest-qualified'
|
|
181
|
+
? ref !== undefined && DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(ref.schemaVersion)
|
|
182
|
+
: ref !== undefined && ref.schemaVersion === GATE_ARTIFACT_SCHEMAS[flag];
|
|
181
183
|
flags[flag] = versionOk && ref !== undefined
|
|
182
184
|
&& typeof ref.artifact === 'string' && ref.artifact.length > 0 && sha256Pattern.test(ref.digest);
|
|
183
185
|
if (flag === 'load-manifest-qualified') {
|
|
@@ -4,7 +4,9 @@ import { JevDecisionAdapter, compileJevQuestion } from '../adapters/jev.js';
|
|
|
4
4
|
import { DecisionAdmissionRegistry } from '../admission.js';
|
|
5
5
|
import { CanonicalJsonByteEstimator } from '../context-plan.js';
|
|
6
6
|
import { assertContextLiveSource } from '../context-live-qualification.js';
|
|
7
|
+
import { tokenCost } from '../pricing.js';
|
|
7
8
|
import { admissionControllerLoadDriver, decisionLoadArrivals, compareDecisionLoadBounds, decisionLoadManifestDigest, decisionLoadPolicy, runDecisionLoadHarness, } from './load.js';
|
|
9
|
+
export const DECISION_STAGED_LOAD_RESULT_VERSIONS = ['decision-load-result/v1', 'decision-load-result/v2'];
|
|
8
10
|
const finite = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
9
11
|
const positive = (value) => finite(value) && value > 0;
|
|
10
12
|
const exactCommit = (value) => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value);
|
|
@@ -289,7 +291,9 @@ export async function runDecisionStagedLoad(options) {
|
|
|
289
291
|
onObservation: (arrival, attempt, admission, observation) => {
|
|
290
292
|
const receipt = inFlight.get(`${arrival.ordinal}:${attempt}`);
|
|
291
293
|
Object.assign(receipt, { settledAtMs: clock.now(), admission: structuredClone(admission), status: observation.status, reason: observation.reason,
|
|
292
|
-
usage: observation.usage, ...(observation.httpStatus === undefined ? {} : { httpStatus: observation.httpStatus })
|
|
294
|
+
usage: observation.usage, ...(observation.httpStatus === undefined ? {} : { httpStatus: observation.httpStatus }),
|
|
295
|
+
...(observation.actualModel ? { servedModel: observation.actualModel } : {}),
|
|
296
|
+
...(observation.requestId ? { requestId: observation.requestId } : {}) });
|
|
293
297
|
inFlight.delete(`${arrival.ordinal}:${attempt}`);
|
|
294
298
|
guard.observe(arrival.providerId, observation, receipt.tokens, receipt.costUsd);
|
|
295
299
|
},
|
|
@@ -335,7 +339,7 @@ export async function runDecisionStagedLoad(options) {
|
|
|
335
339
|
host: { hostname: hostname(), cpuClass: `${cpus().length} cores; ${cpus()[0]?.model ?? 'unknown'}`, memoryGiB: totalmem() / 2 ** 30,
|
|
336
340
|
networkPath: options.networkPath, node: process.version, platform: process.platform, arch: process.arch },
|
|
337
341
|
pricing: options.pricing, stages, receipts, samples, profileHistory: registry.profileHistory('load-harness'), breakerTransitions: transitions,
|
|
338
|
-
|
|
342
|
+
reservedUsage: guard.usage, stop: guard.stop, startedAtMs: start, elapsedMs: samples.at(-1)?.atMs ?? clock.now() - start });
|
|
339
343
|
}
|
|
340
344
|
/** Token-bucket debit (burst plus refill), matching the admission controller's rate semantics. */
|
|
341
345
|
function bucketDebit(receipts, capacity, periodMs, amount) {
|
|
@@ -400,9 +404,9 @@ export function compareDecisionStagedLoad(record) {
|
|
|
400
404
|
}
|
|
401
405
|
}
|
|
402
406
|
}
|
|
403
|
-
atMost('budget.maxUsd.stopAtFraction', m.budget.maxUsd * m.budget.stopAtFraction, record.
|
|
404
|
-
atMost('budget.maxCalls', m.budget.maxCalls, record.
|
|
405
|
-
atMost('budget.maxTokens', m.budget.maxTokens, record.
|
|
407
|
+
atMost('budget.maxUsd.stopAtFraction', m.budget.maxUsd * m.budget.stopAtFraction, record.reservedUsage.usd);
|
|
408
|
+
atMost('budget.maxCalls', m.budget.maxCalls, record.reservedUsage.calls);
|
|
409
|
+
atMost('budget.maxTokens', m.budget.maxTokens, record.reservedUsage.tokens);
|
|
406
410
|
atMost('budget.maxWallClockSeconds', m.budget.maxWallClockSeconds, record.elapsedMs / 1000);
|
|
407
411
|
atMost('maximumSampleGapMs', m.bounds.maximumSampleGapMs, maximum(record.samples.slice(1).map((s, i) => s.atMs - record.samples[i].atMs)));
|
|
408
412
|
const endingAt = record.startedAtMs + record.elapsedMs;
|
|
@@ -440,10 +444,29 @@ export function compareDecisionStagedLoad(record) {
|
|
|
440
444
|
atMost('stop.maximumCrossPrincipalQuotaViolations', 0, violations);
|
|
441
445
|
return out;
|
|
442
446
|
}
|
|
447
|
+
function reportedUsage(receipts, pricing) {
|
|
448
|
+
const sum = (values) => values.reduce((total, value) => total === null || value === null ? null : total + value, 0);
|
|
449
|
+
const inputs = receipts.map(r => r.usage.inputTokens);
|
|
450
|
+
const outputs = receipts.map(r => r.usage.outputTokens);
|
|
451
|
+
const totals = receipts.map(r => r.usage.inputTokens === null || r.usage.outputTokens === null ? null : r.usage.inputTokens + r.usage.outputTokens);
|
|
452
|
+
const inputTokens = sum(inputs);
|
|
453
|
+
const outputTokens = sum(outputs);
|
|
454
|
+
// The request is pinned to the priced model; an explicitly different served model has no applicable rate.
|
|
455
|
+
const costs = receipts.map(r => r.usage.inputTokens === null || r.usage.outputTokens === null
|
|
456
|
+
|| (r.servedModel !== undefined && r.servedModel !== pricing.jev_model) ? null
|
|
457
|
+
: tokenCost(r.usage, { inputPerMillionUsd: pricing.jev_input_per_mtok, outputPerMillionUsd: pricing.jev_output_per_mtok }));
|
|
458
|
+
const knownSum = (values) => values.length > 0 && values.every(v => v === null)
|
|
459
|
+
? null : sum(values.filter(v => v !== null));
|
|
460
|
+
return { calls: receipts.length, inputTokens, outputTokens, tokens: inputTokens === null || outputTokens === null ? null : inputTokens + outputTokens,
|
|
461
|
+
usd: sum(costs), incompleteCalls: costs.filter(cost => cost === null).length,
|
|
462
|
+
knownSubtotal: { inputTokens: knownSum(inputs), outputTokens: knownSum(outputs), tokens: knownSum(totals), usd: knownSum(costs) } };
|
|
463
|
+
}
|
|
443
464
|
export function decisionStagedLoadResultRecord(input) {
|
|
444
465
|
const manifest = readDecisionLoadManifest(input.manifestBytes);
|
|
445
466
|
validateDecisionLoadManifestV3(manifest);
|
|
446
|
-
|
|
467
|
+
validateDecisionLoadPricing(input.pricing, manifest.model);
|
|
468
|
+
const payload = { ...input, schemaVersion: 'decision-load-result/v2', kind: 'load-manifest-qualified',
|
|
469
|
+
reportedUsage: reportedUsage(input.receipts, input.pricing),
|
|
447
470
|
manifest, manifestDigest: decisionLoadManifestDigest(input.manifestBytes) };
|
|
448
471
|
for (const key of ['digest', 'comparisons', 'passed', 'queueWaitDistribution', 'retries'])
|
|
449
472
|
delete payload[key];
|
|
@@ -461,7 +484,7 @@ export function decisionStagedLoadResultRecord(input) {
|
|
|
461
484
|
export function verifyDecisionStagedLoadRecord(value, sourceCommit) {
|
|
462
485
|
try {
|
|
463
486
|
const r = value;
|
|
464
|
-
if (!r || r.schemaVersion
|
|
487
|
+
if (!r || !DECISION_STAGED_LOAD_RESULT_VERSIONS.includes(r.schemaVersion) || r.kind !== 'load-manifest-qualified' || r.mode !== 'staged-provider'
|
|
465
488
|
|| r.execution !== 'live' || r.dirty !== false || !exactCommit(r.sourceCommit) || (sourceCommit !== undefined && sourceCommit !== r.sourceCommit)
|
|
466
489
|
|| r.stop !== null || !r.passed || !finite(r.startedAtMs) || !r.host.networkPath || !r.host.cpuClass || !positive(r.host.memoryGiB))
|
|
467
490
|
return false;
|
|
@@ -512,8 +535,17 @@ export function verifyDecisionStagedLoadRecord(value, sourceCommit) {
|
|
|
512
535
|
|| !positive(receipt.tokens) || !finite(receipt.costUsd) || !finite(receipt.admittedAtMs) || receipt.settledAtMs < receipt.admittedAtMs
|
|
513
536
|
|| receipt.admission.decision !== 'admit' || receipt.costUsd !== price(receipt.tokens, r.pricing)))
|
|
514
537
|
return false;
|
|
515
|
-
|
|
516
|
-
|
|
538
|
+
const reservedUsage = r.schemaVersion === 'decision-load-result/v1' ? r.usage : r.reservedUsage;
|
|
539
|
+
if (!reservedUsage || ![reservedUsage.calls, reservedUsage.tokens, reservedUsage.usd].every(finite)
|
|
540
|
+
|| reservedUsage.calls !== r.receipts.length || reservedUsage.tokens !== r.receipts.reduce((sum, receipt) => sum + receipt.tokens, 0)
|
|
541
|
+
|| Math.abs(reservedUsage.usd - r.receipts.reduce((sum, receipt) => sum + receipt.costUsd, 0)) > 1e-9)
|
|
542
|
+
return false;
|
|
543
|
+
if (r.schemaVersion === 'decision-load-result/v2' && r.receipts.some(receipt => !receipt.usage
|
|
544
|
+
|| ![receipt.usage.inputTokens, receipt.usage.outputTokens].every(v => v === null || (Number.isSafeInteger(v) && v >= 0))
|
|
545
|
+
|| (receipt.usage.inputTokens ?? 0) + (receipt.usage.outputTokens ?? 0) > receipt.tokens
|
|
546
|
+
|| (receipt.usage.costUsd !== null && (!finite(receipt.usage.costUsd) || receipt.usage.costUsd > receipt.costUsd + 1e-12))
|
|
547
|
+
|| (receipt.servedModel !== undefined && (typeof receipt.servedModel !== 'string' || !receipt.servedModel))
|
|
548
|
+
|| (receipt.requestId !== undefined && (typeof receipt.requestId !== 'string' || !receipt.requestId))))
|
|
517
549
|
return false;
|
|
518
550
|
if (!r.samples.every((sample, index) => finite(sample.atMs) && finite(sample.residentMemoryMiB) && finite(sample.heapUsedMiB)
|
|
519
551
|
&& finite(sample.cpuPercentOneCore) && finite(sample.active) && finite(sample.queued) && (index === 0 ? sample.atMs === 0 : sample.atMs >= r.samples[index - 1].atMs
|
|
@@ -529,7 +561,12 @@ export function verifyDecisionStagedLoadRecord(value, sourceCommit) {
|
|
|
529
561
|
|| change?.kind !== 'change' || change.profileVersion !== manifest.profileVersion || change.digest !== profileDigest(manifest.workspace, manifest.providers)
|
|
530
562
|
|| rollback?.kind !== 'rollback' || rollback.profileVersion !== initial.profileVersion || rollback.digest !== initial.digest)
|
|
531
563
|
return false;
|
|
532
|
-
const rebuilt = decisionStagedLoadResultRecord(r);
|
|
564
|
+
const rebuilt = decisionStagedLoadResultRecord({ ...r, reservedUsage });
|
|
565
|
+
if (r.schemaVersion === 'decision-load-result/v1') {
|
|
566
|
+
const { reservedUsage: _, reportedUsage: __, digest: ___, ...legacy } = rebuilt;
|
|
567
|
+
const legacyBody = { ...legacy, schemaVersion: r.schemaVersion };
|
|
568
|
+
return rebuilt.passed && canonicalJson({ ...legacyBody, digest: decisionLoadManifestDigest(canonicalJson(legacyBody)) }) === canonicalJson(r);
|
|
569
|
+
}
|
|
533
570
|
return rebuilt.passed && canonicalJson(rebuilt) === canonicalJson(r);
|
|
534
571
|
}
|
|
535
572
|
catch {
|