@aiwg/cli 2026.10.4 → 2026.10.6
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/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +3 -1
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +72 -9
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +146 -16
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-preparation.js +91 -26
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +130 -56
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/index-worker.mjs +9 -7
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/cli/handlers/decision.js +19 -1
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +40 -6
- package/dist/src/cli/handlers/setup.js +428 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +279 -93
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +8 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/benchmark.js +469 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +18 -15
- package/dist/src/decision/qualification/load-staged.js +539 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +233 -0
- package/dist/src/discovery/plan-apply.js +571 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/mcp/cli.mjs +5 -6
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +7 -0
- package/dist/src/models/worker-installation.mjs +169 -37
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +114 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +33 -19
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/batch-contracts.js +1 -1
- package/dist/src/sessions/batch-import.js +202 -2
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +73 -54
- package/dist/src/sessions/coverage-preflight.js +26 -1
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/policy.js +18 -7
- package/dist/src/sessions/refresh.js +2 -3
- package/dist/src/sessions/repository.js +4 -2
- package/dist/src/sessions/workspace-discovery.js +92 -26
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +19 -13
- package/tools/agents/providers/claude.mjs +1 -1
- package/tools/agents/providers/codex.mjs +50 -56
- package/tools/agents/providers/copilot.mjs +8 -2
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +7 -13
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +28 -4
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +263 -71
- package/tools/agents/providers/windsurf.mjs +7 -0
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +7 -4
- package/tools/ralph-external/session-launcher.mjs +280 -32
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { askDecision } from './ask.js';
|
|
3
|
+
import { BUNDLED_JEV_PRICING, tokenCost, validTokenPrice } from './pricing.js';
|
|
4
|
+
import { decisionSavingsWeights, estimateDecisionSavings } from './savings.js';
|
|
5
|
+
/** Require explicit rates and complete usage; defaults are not benchmark evidence. */
|
|
6
|
+
function observationCost(sample, rates, baseline = false) {
|
|
7
|
+
if (sample.cost !== undefined)
|
|
8
|
+
return sample.cost.usd;
|
|
9
|
+
const usage = sample.usage;
|
|
10
|
+
if (!rates || !usage || usage.inputTokens === null || usage.cachedInputTokens)
|
|
11
|
+
return null;
|
|
12
|
+
if (usage.outputTokens === null && (baseline || rates.jev_output_per_mtok !== 0 || rates.jev_input_per_mtok === undefined))
|
|
13
|
+
return null;
|
|
14
|
+
if (baseline) {
|
|
15
|
+
if (rates.llm_per_mtok === undefined)
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
if (rates.jev_model && rates.jev_model !== sample.model)
|
|
20
|
+
return null;
|
|
21
|
+
if (!(rates.jev_input_per_mtok !== undefined && rates.jev_output_per_mtok !== undefined) && rates.jev_per_mtok === undefined)
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const weights = decisionSavingsWeights({ cost: baseline ? { jev_per_mtok: rates.llm_per_mtok } : rates });
|
|
25
|
+
const cost = estimateDecisionSavings(0, false, usage.inputTokens + (usage.outputTokens ?? 0), weights, baseline ? null : usage);
|
|
26
|
+
return -cost.cost_weighted_net_usd;
|
|
27
|
+
}
|
|
28
|
+
function validateRates(rates) {
|
|
29
|
+
if (rates && Object.entries(rates).some(([key, value]) => /per_mtok$/.test(key) && (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1_000_000)))
|
|
30
|
+
throw new Error('Invalid benchmark rates.');
|
|
31
|
+
}
|
|
32
|
+
export const BENCHMARK_PASS_COUNTS = [1, 3, 5, 10, 15, 20];
|
|
33
|
+
export const MAX_BENCHMARK_PASSES = 20;
|
|
34
|
+
/** Descriptive collection totals, not policy-selection evidence. Missing usage stays unknown. */
|
|
35
|
+
export function benchmarkComparison(capture, pricing) {
|
|
36
|
+
validateRates(pricing);
|
|
37
|
+
const money = (n) => n === null ? 'unknown' : `$${n.toFixed(8)}`;
|
|
38
|
+
const totals = (samples, baseline = false) => {
|
|
39
|
+
const input = samples.every(s => s.usage?.inputTokens != null) ? samples.reduce((sum, s) => sum + s.usage.inputTokens, 0) : null;
|
|
40
|
+
const output = samples.every(s => s.usage?.outputTokens != null) ? samples.reduce((sum, s) => sum + s.usage.outputTokens, 0) : null;
|
|
41
|
+
const cached = samples.every(s => s.usage?.cachedInputTokens != null) ? samples.reduce((sum, s) => sum + s.usage.cachedInputTokens, 0) : null;
|
|
42
|
+
const costs = samples.map(s => observationCost(s, pricing, baseline));
|
|
43
|
+
return { input, output, cached, cost: costs.some(c => c === null) ? null : costs.reduce((sum, c) => sum + c, 0),
|
|
44
|
+
latency: samples.length ? samples.reduce((sum, s) => sum + s.latencyMs, 0) / capture.rows.length : 0 };
|
|
45
|
+
};
|
|
46
|
+
const display = (n) => n === null ? 'unknown' : n.toLocaleString('en-US');
|
|
47
|
+
const baseline = totals(capture.rows.map(r => r.baseline), true);
|
|
48
|
+
const available = Math.min(...capture.rows.map(r => r.jev.length));
|
|
49
|
+
const passCounts = [...new Set([...BENCHMARK_PASS_COUNTS.filter(n => n <= available), available])];
|
|
50
|
+
const lines = [
|
|
51
|
+
`Comparison across ${capture.rows.length} labeled decisions (tuning + holdout):`, '',
|
|
52
|
+
'| Configuration | Input tokens | Cached input (subset) | Output tokens | Total tokens | Mean ms/decision | Estimated USD |',
|
|
53
|
+
'|---|---:|---:|---:|---:|---:|---:|',
|
|
54
|
+
...passCounts.map(passes => {
|
|
55
|
+
const samples = capture.rows.flatMap(r => r.jev.slice(0, passes));
|
|
56
|
+
const t = totals(samples, false);
|
|
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
|
+
}),
|
|
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.',
|
|
61
|
+
'Costs use reported usage and explicit pricing assumptions, not subscription invoices.',
|
|
62
|
+
'Standalone baseline requests do not measure the marginal cost of an inline LLM decision.',
|
|
63
|
+
];
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
export function benchmarkDigest(cases) {
|
|
67
|
+
return createHash('sha256').update(JSON.stringify(cases)).digest('hex');
|
|
68
|
+
}
|
|
69
|
+
export function validateBenchmarkCases(value) {
|
|
70
|
+
if (!Array.isArray(value) || value.length < 2 || value.length > 100)
|
|
71
|
+
throw new Error('Benchmark needs 2..100 labeled cases.');
|
|
72
|
+
const ids = new Set();
|
|
73
|
+
for (const c of value) {
|
|
74
|
+
if (!fields(c, ['id', 'split', 'input', 'expected']) || typeof c.id !== 'string' || !/^[A-Za-z0-9_-]{1,100}$/.test(c.id) || ids.has(c.id) || !['tuning', 'holdout'].includes(c.split)
|
|
75
|
+
|| !fields(c.input, ['question', 'mode', 'context']) || (c.input.context !== undefined && (typeof c.input.context !== 'string' || c.input.context.length > 100_000)) || typeof c.input.question !== 'string' || !c.input.question.trim() || c.input.question.length > 10_000
|
|
76
|
+
|| !c.input.mode || !['yes-no', 'choices', 'scale'].includes(c.input.mode.kind))
|
|
77
|
+
throw new Error('Invalid or duplicate benchmark case.');
|
|
78
|
+
const mode = c.input.mode;
|
|
79
|
+
if (!fields(mode, mode.kind === 'choices' ? ['kind', 'choices'] : mode.kind === 'scale' ? ['kind', 'low', 'high'] : ['kind']))
|
|
80
|
+
throw new Error('Invalid decision mode fields.');
|
|
81
|
+
if (mode.kind === 'yes-no' ? typeof c.expected !== 'boolean'
|
|
82
|
+
: mode.kind === 'choices' ? !Array.isArray(mode.choices) || mode.choices.length < 2 || mode.choices.length > 100 || mode.choices.some(v => typeof v !== 'string' || !v.trim() || v.length > 1000) || new Set(mode.choices).size !== mode.choices.length || !mode.choices.includes(c.expected)
|
|
83
|
+
: !Number.isSafeInteger(mode.low) || !Number.isSafeInteger(mode.high) || mode.low >= mode.high || typeof c.expected !== 'number' || !Number.isInteger(c.expected) || c.expected < mode.low || c.expected > mode.high)
|
|
84
|
+
throw new Error('Gold answer does not match decision mode.');
|
|
85
|
+
ids.add(c.id);
|
|
86
|
+
}
|
|
87
|
+
if (!value.some(c => c.split === 'tuning') || !value.some(c => c.split === 'holdout'))
|
|
88
|
+
throw new Error('Both tuning and holdout cases are required.');
|
|
89
|
+
}
|
|
90
|
+
function validObservation(r) {
|
|
91
|
+
return !!r && ['answered', 'fallback'].includes(r.status) && (r.answer === null || ['string', 'number', 'boolean'].includes(typeof r.answer))
|
|
92
|
+
&& (typeof r.answer !== 'number' || Number.isFinite(r.answer))
|
|
93
|
+
&& (r.confidence === null || Number.isFinite(r.confidence) && r.confidence >= 0 && r.confidence <= 1)
|
|
94
|
+
&& (r.probability === undefined || Number.isFinite(r.probability) && r.probability >= 0 && r.probability <= 1)
|
|
95
|
+
&& Number.isFinite(r.latencyMs) && r.latencyMs >= 0
|
|
96
|
+
&& (r.model === null || typeof r.model === 'string')
|
|
97
|
+
&& (r.usage === null || [r.usage.inputTokens, r.usage.outputTokens].every(n => n === null || Number.isSafeInteger(n) && n >= 0))
|
|
98
|
+
&& (r.usage?.cachedInputTokens == null || Number.isSafeInteger(r.usage.cachedInputTokens) && r.usage.cachedInputTokens >= 0
|
|
99
|
+
&& r.usage.inputTokens !== null && r.usage.cachedInputTokens <= r.usage.inputTokens)
|
|
100
|
+
&& (r.status !== 'answered' || r.answer !== null);
|
|
101
|
+
}
|
|
102
|
+
function modeAnswer(c, r) {
|
|
103
|
+
if (r.status !== 'answered')
|
|
104
|
+
return true;
|
|
105
|
+
const m = c.input.mode;
|
|
106
|
+
return m.kind === 'yes-no' ? typeof r.answer === 'boolean' : m.kind === 'choices' ? m.choices.includes(r.answer)
|
|
107
|
+
: typeof r.answer === 'number' && Number.isInteger(r.answer) && r.answer >= m.low && r.answer <= m.high;
|
|
108
|
+
}
|
|
109
|
+
function validPrice(price) {
|
|
110
|
+
return fields(price, ['model', 'source', 'checkedAt', 'inputPerMillionUsd', 'outputPerMillionUsd', 'cachedInputPerMillionUsd'])
|
|
111
|
+
&& validTokenPrice(price) && typeof price.model === 'string' && !!price.model.trim()
|
|
112
|
+
&& typeof price.source === 'string' && /^https:\/\//.test(price.source)
|
|
113
|
+
&& typeof price.checkedAt === 'string' && Number.isFinite(Date.parse(price.checkedAt));
|
|
114
|
+
}
|
|
115
|
+
function validCost(sample) {
|
|
116
|
+
if (sample.cost === undefined)
|
|
117
|
+
return true;
|
|
118
|
+
const cost = sample.cost;
|
|
119
|
+
return fields(cost, ['usd', 'price']) && (cost.price === null ? cost.usd === null
|
|
120
|
+
: validPrice(cost.price) && cost.price.model === sample.model && cost.usd === tokenCost(sample.usage, cost.price));
|
|
121
|
+
}
|
|
122
|
+
/** Opt-in only. One invocation is one provider call; production ask never calls this collector. */
|
|
123
|
+
export async function collectBenchmark(dataset, options) {
|
|
124
|
+
validateBenchmarkCases(dataset);
|
|
125
|
+
if (options.live !== true || !Number.isSafeInteger(options.maxCalls) || options.maxCalls < 0)
|
|
126
|
+
throw new Error('Explicit live opt-in and nonnegative maxCalls required.');
|
|
127
|
+
if (!Number.isFinite(options.maxUsd) || options.maxUsd <= 0)
|
|
128
|
+
throw new Error('Positive maxUsd required for live collection.');
|
|
129
|
+
if (!Number.isInteger(options.passes) || options.passes < 1 || options.passes > MAX_BENCHMARK_PASSES)
|
|
130
|
+
throw new Error('passes must be 1..20.');
|
|
131
|
+
const cases = splitBenchmarkHoldout(dataset, options.holdout ?? 0.3, options.seed ?? '3291');
|
|
132
|
+
const prior = options.extendCapture;
|
|
133
|
+
if (prior)
|
|
134
|
+
validateBenchmarkCapture(cases, prior);
|
|
135
|
+
if (typeof options.baselineLabel !== 'string' || !options.baselineLabel.trim() || options.baselineLabel.length > 200
|
|
136
|
+
|| prior && prior.baselineLabel !== options.baselineLabel)
|
|
137
|
+
throw new Error('Invalid or changed baseline label.');
|
|
138
|
+
const jevPrices = options.jevPrices ?? Object.entries(BUNDLED_JEV_PRICING.models).map(([model, price]) => ({
|
|
139
|
+
...price, model, source: BUNDLED_JEV_PRICING.source, checkedAt: BUNDLED_JEV_PRICING.checkedAt,
|
|
140
|
+
}));
|
|
141
|
+
for (const prices of [jevPrices, options.baselinePrices ?? []]) {
|
|
142
|
+
if (prices.some(p => !validPrice(p)) || new Set(prices.map(p => p.model)).size !== prices.length)
|
|
143
|
+
throw new Error('Invalid or duplicate sourced model rates.');
|
|
144
|
+
}
|
|
145
|
+
const rows = structuredClone(prior?.rows ?? []);
|
|
146
|
+
if (rows.some(row => row.jev.length > options.passes))
|
|
147
|
+
throw new Error('Extension cannot remove existing passes.');
|
|
148
|
+
const planned = cases.reduce((sum, c) => {
|
|
149
|
+
const row = rows.find(r => r.id === c.id);
|
|
150
|
+
return sum + (row ? 0 : 1) + Math.max(0, options.passes - (row?.jev.length ?? 0));
|
|
151
|
+
}, 0);
|
|
152
|
+
if (planned > options.maxCalls)
|
|
153
|
+
throw new Error(`Insufficient maxCalls: need ${planned}; no providers invoked.`);
|
|
154
|
+
const now = options.now ?? Date.now;
|
|
155
|
+
let calls = 0, knownUsd = 0;
|
|
156
|
+
for (const row of rows)
|
|
157
|
+
for (const [index, sample] of [row.baseline, ...row.jev].entries()) {
|
|
158
|
+
// Legacy captures can acquire a sourced cost without changing their recorded response.
|
|
159
|
+
if (!sample.cost) {
|
|
160
|
+
const price = (index === 0 ? options.baselinePrices ?? [] : jevPrices).find(p => p.model === sample.model) ?? null;
|
|
161
|
+
sample.cost = { usd: price ? tokenCost(sample.usage, price) : null, price: structuredClone(price) };
|
|
162
|
+
}
|
|
163
|
+
if (sample.cost.usd === null)
|
|
164
|
+
throw new Error('Cannot safely extend an unpriced capture.');
|
|
165
|
+
knownUsd += sample.cost.usd;
|
|
166
|
+
}
|
|
167
|
+
let stopReason = 'incomplete';
|
|
168
|
+
const snapshot = () => ({ schema: 'aiwg-decision-benchmark-capture/v1',
|
|
169
|
+
datasetDigest: benchmarkDigest(cases), capturedAt: new Date(now()).toISOString(),
|
|
170
|
+
baselineLabel: options.baselineLabel, rows: structuredClone(rows), collection: { stopReason, calls, knownUsd } });
|
|
171
|
+
if (planned === 0 || knownUsd >= options.maxUsd * 0.8) {
|
|
172
|
+
stopReason = planned === 0 ? 'complete' : 'usd-limit';
|
|
173
|
+
const capture = snapshot();
|
|
174
|
+
validateBenchmarkCapture(cases, capture);
|
|
175
|
+
if (options.onCheckpoint)
|
|
176
|
+
await options.onCheckpoint(capture);
|
|
177
|
+
return capture;
|
|
178
|
+
}
|
|
179
|
+
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)
|
|
181
|
+
&& !(environment.JEV_API_KEY?.trim() || environment.AIWG_DECISION_JEV_API_KEY?.trim()))
|
|
182
|
+
throw new Error('Live benchmark requires JEV_API_KEY or AIWG_DECISION_JEV_API_KEY in the caller environment.');
|
|
183
|
+
const jev = options.jev ?? (async (input) => {
|
|
184
|
+
// Force environment-only lookup; lookupJevCredential skips credential files with this explicit flag.
|
|
185
|
+
return askDecision(input, { env: { ...environment, AIWG_DECISION_ENABLED: '1' }, now });
|
|
186
|
+
});
|
|
187
|
+
const invoke = async (c, baseline) => {
|
|
188
|
+
const started = now();
|
|
189
|
+
calls++;
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
result = await (baseline ? options.baseline : jev)({ ...structuredClone(c.input), threshold: 0, passes: 1, escalationPasses: 1 });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// No retry: a failed request can still be billable. Stop when its usage is unknown.
|
|
196
|
+
result = { status: 'fallback', answer: null, confidence: null, model: null, usage: null, latencyMs: 0 };
|
|
197
|
+
}
|
|
198
|
+
const sample = { answer: result.answer, confidence: result.confidence, status: result.status,
|
|
199
|
+
model: result.model, usage: structuredClone(result.usage), latencyMs: Math.max(0, now() - started),
|
|
200
|
+
...(result.probability === undefined ? {} : { probability: result.probability }) };
|
|
201
|
+
if (!validObservation(sample) || !modeAnswer(c, sample))
|
|
202
|
+
throw new Error(`Invalid provider observation: ${c.id}`);
|
|
203
|
+
const price = (baseline ? options.baselinePrices ?? [] : jevPrices).find(p => p.model === sample.model) ?? null;
|
|
204
|
+
const usd = price ? tokenCost(sample.usage, price) : null;
|
|
205
|
+
sample.cost = { usd, price: structuredClone(price) };
|
|
206
|
+
if (usd === null)
|
|
207
|
+
stopReason = 'unknown-cost';
|
|
208
|
+
else {
|
|
209
|
+
knownUsd += usd;
|
|
210
|
+
if (knownUsd >= options.maxUsd * 0.8)
|
|
211
|
+
stopReason = 'usd-limit';
|
|
212
|
+
}
|
|
213
|
+
return sample;
|
|
214
|
+
};
|
|
215
|
+
for (const c of cases) {
|
|
216
|
+
let row = rows.find(r => r.id === c.id);
|
|
217
|
+
if (!row) {
|
|
218
|
+
row = { id: c.id, baseline: await invoke(c, true), jev: [] };
|
|
219
|
+
rows.push(row);
|
|
220
|
+
}
|
|
221
|
+
if (options.onCheckpoint)
|
|
222
|
+
await options.onCheckpoint(snapshot());
|
|
223
|
+
if (stopReason !== 'incomplete')
|
|
224
|
+
break;
|
|
225
|
+
while (row.jev.length < options.passes) {
|
|
226
|
+
row.jev.push(await invoke(c, false));
|
|
227
|
+
if (options.onCheckpoint)
|
|
228
|
+
await options.onCheckpoint(snapshot());
|
|
229
|
+
if (stopReason !== 'incomplete')
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (stopReason !== 'incomplete')
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (cases.every(c => rows.some(r => r.id === c.id && r.jev.length === options.passes)))
|
|
236
|
+
stopReason = 'complete';
|
|
237
|
+
const capture = snapshot();
|
|
238
|
+
validateBenchmarkCapture(cases, capture);
|
|
239
|
+
if (options.onCheckpoint)
|
|
240
|
+
await options.onCheckpoint(capture);
|
|
241
|
+
return capture;
|
|
242
|
+
}
|
|
243
|
+
function mean(values) {
|
|
244
|
+
return values.length ? values.reduce((sum, n) => sum + n, 0) / values.length : null;
|
|
245
|
+
}
|
|
246
|
+
/** Sample SD: one observation cannot estimate variation and is reported as null. */
|
|
247
|
+
function sampleStdDev(values) {
|
|
248
|
+
if (values.length < 2)
|
|
249
|
+
return null;
|
|
250
|
+
const average = mean(values);
|
|
251
|
+
return Math.sqrt(values.reduce((sum, n) => sum + (n - average) ** 2, 0) / (values.length - 1));
|
|
252
|
+
}
|
|
253
|
+
export function benchmarkScoringDeviation(cases, capture, passes) {
|
|
254
|
+
validateBenchmarkCapture(cases, capture);
|
|
255
|
+
if (!Number.isInteger(passes) || passes < 1 || passes > MAX_BENCHMARK_PASSES)
|
|
256
|
+
throw new Error('Invalid deviation pass count.');
|
|
257
|
+
const rows = cases.map(c => {
|
|
258
|
+
const row = capture.rows.find(r => r.id === c.id);
|
|
259
|
+
if (!row || row.jev.length < passes)
|
|
260
|
+
throw new Error('Insufficient observations for deviation.');
|
|
261
|
+
const samples = row.jev.slice(0, passes);
|
|
262
|
+
const answered = samples.filter(r => r.status === 'answered' && r.answer !== null);
|
|
263
|
+
const counts = new Map();
|
|
264
|
+
for (const r of answered) {
|
|
265
|
+
const key = JSON.stringify(r.answer);
|
|
266
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
267
|
+
}
|
|
268
|
+
const confidence = samples.flatMap(r => r.confidence === null ? [] : [r.confidence]);
|
|
269
|
+
const scores = c.input.mode.kind === 'scale' ? answered.flatMap(r => typeof r.answer === 'number' ? [r.answer] : []) : [];
|
|
270
|
+
const probabilities = c.input.mode.kind === 'yes-no' ? samples.flatMap(r => r.probability === undefined ? [] : [r.probability]) : [];
|
|
271
|
+
return { id: c.id, split: c.split, kind: c.input.mode.kind, observations: samples.length, answered: answered.length,
|
|
272
|
+
rawCorrect: answered.filter(r => r.answer === c.expected).length,
|
|
273
|
+
nonModalAnswers: answered.length - Math.max(0, ...counts.values()),
|
|
274
|
+
confidenceMean: mean(confidence), confidenceStdDev: sampleStdDev(confidence),
|
|
275
|
+
scaleScoreMean: mean(scores), scaleScoreStdDev: sampleStdDev(scores),
|
|
276
|
+
scaleScoreMin: scores.length ? Math.min(...scores) : null, scaleScoreMax: scores.length ? Math.max(...scores) : null,
|
|
277
|
+
scaleMaeGold: mean(scores.map(n => Math.abs(n - Number(c.expected)))),
|
|
278
|
+
scaleMaeBaseline: row.baseline.status === 'answered' && typeof row.baseline.answer === 'number'
|
|
279
|
+
? mean(scores.map(n => Math.abs(n - Number(row.baseline.answer)))) : null,
|
|
280
|
+
yesNoBrier: mean(probabilities.map(p => (p - Number(c.expected)) ** 2)),
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
const present = (values) => values.filter((n) => n !== null);
|
|
284
|
+
const answered = rows.reduce((sum, r) => sum + r.answered, 0);
|
|
285
|
+
return { passes, observations: cases.length * passes, answered,
|
|
286
|
+
rawAccuracy: rows.reduce((sum, r) => sum + r.rawCorrect, 0) / (cases.length * passes),
|
|
287
|
+
answerDisagreementRate: answered ? rows.reduce((sum, r) => sum + r.nonModalAnswers, 0) / answered : null,
|
|
288
|
+
meanConfidence: mean(present(rows.map(r => r.confidenceMean))),
|
|
289
|
+
meanWithinCaseConfidenceStdDev: mean(present(rows.map(r => r.confidenceStdDev))),
|
|
290
|
+
meanWithinCaseScaleScoreStdDev: mean(present(rows.map(r => r.scaleScoreStdDev))),
|
|
291
|
+
meanScaleMaeGold: mean(present(rows.map(r => r.scaleMaeGold))),
|
|
292
|
+
meanScaleMaeBaseline: mean(present(rows.map(r => r.scaleMaeBaseline))),
|
|
293
|
+
meanYesNoBrier: mean(present(rows.map(r => r.yesNoBrier))), cases: rows };
|
|
294
|
+
}
|
|
295
|
+
function select(observations, policy) {
|
|
296
|
+
const samples = observations.slice(0, policy.passes);
|
|
297
|
+
const votes = new Map();
|
|
298
|
+
for (const r of samples) {
|
|
299
|
+
if (r.status !== 'answered' || r.answer === null || r.confidence === null)
|
|
300
|
+
continue;
|
|
301
|
+
const key = JSON.stringify(r.answer);
|
|
302
|
+
const vote = votes.get(key) ?? { answer: r.answer, count: 0, confidence: 0 };
|
|
303
|
+
vote.count++;
|
|
304
|
+
vote.confidence += r.confidence;
|
|
305
|
+
votes.set(key, vote);
|
|
306
|
+
}
|
|
307
|
+
const ranked = [...votes.values()].sort((a, b) => b.count - a.count);
|
|
308
|
+
const best = ranked[0];
|
|
309
|
+
if (!best || ranked[1]?.count === best.count || best.count / policy.passes < policy.agreement
|
|
310
|
+
|| best.confidence / best.count < policy.threshold)
|
|
311
|
+
return null;
|
|
312
|
+
return best.answer;
|
|
313
|
+
}
|
|
314
|
+
/** Threshold/pass/consensus sweeps reuse captures. Holdout NEVER selects the policy. */
|
|
315
|
+
export function analyzeBenchmark(cases, capture, policies, pricing, options = {}) {
|
|
316
|
+
validateBenchmarkCases(cases);
|
|
317
|
+
validateBenchmarkCapture(cases, capture);
|
|
318
|
+
validateRates(pricing);
|
|
319
|
+
if (capture.rows.length !== cases.length)
|
|
320
|
+
throw new Error('Incomplete capture: extend before analysis.');
|
|
321
|
+
if (!policies.length || policies.length > 100 || policies.some(p => !Number.isInteger(p.passes) || p.passes < 1 || p.passes > MAX_BENCHMARK_PASSES
|
|
322
|
+
|| !Number.isFinite(p.threshold) || p.threshold < 0 || p.threshold > 1 || !Number.isFinite(p.agreement) || p.agreement <= 0 || p.agreement > 1))
|
|
323
|
+
throw new Error('Invalid benchmark policy grid.');
|
|
324
|
+
const minimumAccuracy = options.minimumAccuracy ?? 1;
|
|
325
|
+
const maximumFallbackRate = options.maximumFallbackRate ?? 1;
|
|
326
|
+
if (![minimumAccuracy, maximumFallbackRate].every(n => Number.isFinite(n) && n >= 0 && n <= 1))
|
|
327
|
+
throw new Error('Invalid benchmark quality limits.');
|
|
328
|
+
const byId = new Map(capture.rows.map(r => [r.id, r]));
|
|
329
|
+
for (const c of cases) {
|
|
330
|
+
const row = byId.get(c.id);
|
|
331
|
+
if (!row || !validObservation(row.baseline) || !modeAnswer(c, row.baseline) || !Array.isArray(row.jev)
|
|
332
|
+
|| row.jev.length < Math.max(...policies.map(p => p.passes)) || row.jev.some(r => !validObservation(r) || !modeAnswer(c, r)))
|
|
333
|
+
throw new Error('Invalid or insufficient captured observations.');
|
|
334
|
+
}
|
|
335
|
+
const measurements = policies.map(policy => {
|
|
336
|
+
const splits = ['tuning', 'holdout'].map(split => {
|
|
337
|
+
const selected = cases.filter(c => c.split === split);
|
|
338
|
+
let accepted = 0, jevCorrect = 0, baselineCorrect = 0, routedCorrect = 0, agreements = 0;
|
|
339
|
+
let jevUsd = 0, baselineUsd = 0, routedUsd = 0, unpriced = 0, unpricedJev = 0, unpricedBaseline = 0, jevMs = 0, baselineMs = 0, routedMs = 0;
|
|
340
|
+
for (const c of selected) {
|
|
341
|
+
const row = byId.get(c.id);
|
|
342
|
+
const answer = select(row.jev, policy);
|
|
343
|
+
const baselineCost = observationCost(row.baseline, pricing, true);
|
|
344
|
+
let caseJevUsd = 0, caseJevMs = 0;
|
|
345
|
+
let unknownJevCost = false;
|
|
346
|
+
for (const r of row.jev.slice(0, policy.passes)) {
|
|
347
|
+
const cost = observationCost(r, pricing);
|
|
348
|
+
if (cost === null)
|
|
349
|
+
unknownJevCost = true;
|
|
350
|
+
else
|
|
351
|
+
caseJevUsd += cost;
|
|
352
|
+
caseJevMs += r.latencyMs;
|
|
353
|
+
}
|
|
354
|
+
if (unknownJevCost)
|
|
355
|
+
unpricedJev++;
|
|
356
|
+
if (baselineCost === null)
|
|
357
|
+
unpricedBaseline++;
|
|
358
|
+
if (unknownJevCost || baselineCost === null)
|
|
359
|
+
unpriced++;
|
|
360
|
+
jevUsd += caseJevUsd;
|
|
361
|
+
baselineUsd += baselineCost ?? 0;
|
|
362
|
+
routedUsd += caseJevUsd + (answer === null ? baselineCost ?? 0 : 0);
|
|
363
|
+
jevMs += caseJevMs;
|
|
364
|
+
baselineMs += row.baseline.latencyMs;
|
|
365
|
+
routedMs += caseJevMs + (answer === null ? row.baseline.latencyMs : 0);
|
|
366
|
+
if (row.baseline.status === 'answered' && row.baseline.answer === c.expected)
|
|
367
|
+
baselineCorrect++;
|
|
368
|
+
if (answer !== null) {
|
|
369
|
+
accepted++;
|
|
370
|
+
if (answer === c.expected)
|
|
371
|
+
jevCorrect++;
|
|
372
|
+
if (row.baseline.status === 'answered' && answer === row.baseline.answer)
|
|
373
|
+
agreements++;
|
|
374
|
+
}
|
|
375
|
+
if ((answer ?? (row.baseline.status === 'answered' ? row.baseline.answer : null)) === c.expected)
|
|
376
|
+
routedCorrect++;
|
|
377
|
+
}
|
|
378
|
+
return { split, cases: selected.length, accepted, fallbackRate: 1 - accepted / selected.length,
|
|
379
|
+
selectiveAccuracy: accepted ? jevCorrect / accepted : null,
|
|
380
|
+
baselineAccuracy: baselineCorrect / selected.length, routedAccuracy: routedCorrect / selected.length,
|
|
381
|
+
agreementWithBaseline: accepted ? agreements / accepted : null,
|
|
382
|
+
estimatedJevUsd: unpricedJev ? null : jevUsd, baselineUsd: unpricedBaseline ? null : baselineUsd,
|
|
383
|
+
routedUsd: unpriced ? null : routedUsd, savingsUsd: unpriced ? null : baselineUsd - routedUsd,
|
|
384
|
+
unpricedCases: unpriced, meanJevMs: jevMs / selected.length, meanBaselineMs: baselineMs / selected.length,
|
|
385
|
+
meanRoutedMs: routedMs / selected.length };
|
|
386
|
+
});
|
|
387
|
+
return { policy, tuning: splits[0], holdout: splits[1] };
|
|
388
|
+
});
|
|
389
|
+
const eligible = measurements.filter(m => m.tuning.routedAccuracy >= minimumAccuracy && m.tuning.fallbackRate <= maximumFallbackRate);
|
|
390
|
+
// With no comparable USD rate, use measured routed latency rather than inventing a price.
|
|
391
|
+
const useCost = eligible.every(m => m.tuning.routedUsd !== null);
|
|
392
|
+
eligible.sort((a, b) => (useCost ? a.tuning.routedUsd - b.tuning.routedUsd : a.tuning.meanRoutedMs - b.tuning.meanRoutedMs)
|
|
393
|
+
|| b.policy.threshold - a.policy.threshold);
|
|
394
|
+
return { schema: 'aiwg-decision-benchmark-report/v1', datasetDigest: capture.datasetDigest,
|
|
395
|
+
baselineLabel: capture.baselineLabel, pricing: pricing ?? null,
|
|
396
|
+
minimumAccuracy, maximumFallbackRate, recommended: eligible[0]?.policy ?? null, measurements,
|
|
397
|
+
scoringDeviation: [...new Set(policies.map(p => p.passes))].sort((a, b) => a - b).map(passes => benchmarkScoringDeviation(cases, capture, passes)),
|
|
398
|
+
deviationDefinitions: {
|
|
399
|
+
scaleScoreStdDev: 'Within-case sample SD of raw 1-N scale scores, averaged across scale cases; null for one pass.',
|
|
400
|
+
confidenceStdDev: 'Within-case sample SD of reported confidence (0..1), averaged across cases; not a calibration guarantee.',
|
|
401
|
+
answerDisagreementRate: 'Fraction of answered passes differing from their case modal answer; categorical labels are never assigned numeric scores.',
|
|
402
|
+
scaleMae: 'Mean absolute difference of raw scale scores against gold labels or the saved LLM score; expressed in scale points.',
|
|
403
|
+
yesNoBrier: 'Mean squared error of reported truth probability against gold boolean labels; null when probability is unavailable.',
|
|
404
|
+
},
|
|
405
|
+
limitations: ['Small labeled benchmark; LLM agreement is not ground truth.',
|
|
406
|
+
'Recommendations use tuning cases only; evaluate holdout before adoption. No configuration is changed.',
|
|
407
|
+
'Serial pass latency; modeled routing cost includes LLM cost on fallbacks. Benchmark collection itself pays both arms.',
|
|
408
|
+
'Repeated Jev passes may be correlated; identical answers are not independent confidence evidence.',
|
|
409
|
+
'Standalone baseline requests do not estimate marginal reasoning inside an existing conversation.',
|
|
410
|
+
'Legacy captures may lack cached-input counts; cache-aware prices then leave costs unknown. No subscription invoice is inferred.',
|
|
411
|
+
'Prices are API-equivalent estimates, not subscription invoices. Missing usage or rates makes savings unknown.'] };
|
|
412
|
+
}
|
|
413
|
+
export function benchmarkDeviationComparison(report) {
|
|
414
|
+
const format = (n, multiplier = 1) => n === null ? 'n/a' : (n * multiplier).toFixed(4);
|
|
415
|
+
return [
|
|
416
|
+
'Scoring deviation across repeated passes:', '',
|
|
417
|
+
'| Passes | Raw accuracy % | Mean confidence % | Confidence SD (pp) | Scale-score SD (points) | Scale MAE vs gold | Answer disagreement % | Yes/no Brier |',
|
|
418
|
+
'|---|---:|---:|---:|---:|---:|---:|---:|',
|
|
419
|
+
...report.scoringDeviation.map(d => `| ${d.passes} | ${format(d.rawAccuracy, 100)} | ${format(d.meanConfidence, 100)} | ${format(d.meanWithinCaseConfidenceStdDev, 100)} | ${format(d.meanWithinCaseScaleScoreStdDev)} | ${format(d.meanScaleMaeGold)} | ${format(d.answerDisagreementRate, 100)} | ${format(d.meanYesNoBrier)} |`), '',
|
|
420
|
+
'SD is the within-case sample standard deviation, averaged across applicable cases. One pass cannot estimate SD (n/a).',
|
|
421
|
+
'Scale scores use their original rubric points; categorical answers have no numeric score. Raw accuracy counts every pass, before confidence-based routing.',
|
|
422
|
+
'Pass-count rows are nested prefixes of one capture, not independent experiments; repeated calls may be correlated.',
|
|
423
|
+
].join('\n');
|
|
424
|
+
}
|
|
425
|
+
export const MAX_BENCHMARK_BYTES = 4 * 1024 * 1024;
|
|
426
|
+
function fields(value, allowed) {
|
|
427
|
+
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
428
|
+
&& Object.keys(value).every(key => allowed.includes(key));
|
|
429
|
+
}
|
|
430
|
+
/** Captures contain values only: no paths, executable hooks, or provider settings. */
|
|
431
|
+
export function validateBenchmarkCapture(cases, value) {
|
|
432
|
+
validateBenchmarkCases(cases);
|
|
433
|
+
const c = value;
|
|
434
|
+
if (!fields(c, ['schema', 'datasetDigest', 'capturedAt', 'baselineLabel', 'rows', 'collection'])
|
|
435
|
+
|| c.schema !== 'aiwg-decision-benchmark-capture/v1' || c.datasetDigest !== benchmarkDigest(cases)
|
|
436
|
+
|| typeof c.capturedAt !== 'string' || !/^\d{4}-\d\d-\d\dT/.test(c.capturedAt) || !Number.isFinite(Date.parse(c.capturedAt))
|
|
437
|
+
|| typeof c.baselineLabel !== 'string' || !c.baselineLabel.trim() || c.baselineLabel.length > 200
|
|
438
|
+
|| !Array.isArray(c.rows) || c.rows.length > cases.length
|
|
439
|
+
|| ((!c.collection || c.collection.stopReason === 'complete') && c.rows.length !== cases.length)
|
|
440
|
+
|| new Set(c.rows.map(r => r?.id)).size !== c.rows.length)
|
|
441
|
+
throw new Error('Capture does not match this dataset or schema.');
|
|
442
|
+
if (c.collection !== undefined && (!fields(c.collection, ['stopReason', 'calls', 'knownUsd'])
|
|
443
|
+
|| !['complete', 'incomplete', 'usd-limit', 'unknown-cost'].includes(c.collection.stopReason)
|
|
444
|
+
|| !Number.isSafeInteger(c.collection.calls) || c.collection.calls < 0
|
|
445
|
+
|| !Number.isFinite(c.collection.knownUsd) || c.collection.knownUsd < 0))
|
|
446
|
+
throw new Error('Invalid collection totals.');
|
|
447
|
+
const observation = (r) => fields(r, ['answer', 'confidence', 'probability', 'status', 'model', 'usage', 'latencyMs', 'cost'])
|
|
448
|
+
&& (r.usage === null || fields(r.usage, ['inputTokens', 'outputTokens', 'cachedInputTokens'])) && validObservation(r) && validCost(r);
|
|
449
|
+
for (const row of c.rows) {
|
|
450
|
+
const item = cases.find(item => item.id === row?.id);
|
|
451
|
+
if (!fields(row, ['id', 'baseline', 'jev']) || !item || !observation(row.baseline) || !modeAnswer(item, row.baseline)
|
|
452
|
+
|| !Array.isArray(row.jev) || row.jev.length < (c.collection && c.collection.stopReason !== 'complete' ? 0 : 1) || row.jev.length > MAX_BENCHMARK_PASSES
|
|
453
|
+
|| row.jev.some(r => !observation(r) || !modeAnswer(item, r)))
|
|
454
|
+
throw new Error('Invalid captured observations.');
|
|
455
|
+
}
|
|
456
|
+
if (Buffer.byteLength(JSON.stringify(c)) > MAX_BENCHMARK_BYTES)
|
|
457
|
+
throw new Error('Capture exceeds size limit.');
|
|
458
|
+
}
|
|
459
|
+
/** Stable by ID and seed, independent of file order and observed answers. */
|
|
460
|
+
export function splitBenchmarkHoldout(cases, fraction, seed = '3291') {
|
|
461
|
+
validateBenchmarkCases(cases);
|
|
462
|
+
if (!Number.isFinite(fraction) || fraction <= 0 || fraction >= 1)
|
|
463
|
+
throw new Error('Holdout fraction must be between 0 and 1.');
|
|
464
|
+
const count = Math.max(1, Math.min(cases.length - 1, Math.round(cases.length * fraction)));
|
|
465
|
+
const hash = (id) => createHash('sha256').update(JSON.stringify([seed, id])).digest('hex');
|
|
466
|
+
const ids = new Set([...cases].sort((a, b) => hash(a.id).localeCompare(hash(b.id))).slice(0, count).map(c => c.id));
|
|
467
|
+
return cases.map(c => ({ ...c, split: ids.has(c.id) ? 'holdout' : 'tuning' }));
|
|
468
|
+
}
|
|
469
|
+
//# sourceMappingURL=benchmark.js.map
|
|
@@ -62,6 +62,7 @@ import { projectAiwgPath, projectAiwgWritePath } from '../config/project-artifac
|
|
|
62
62
|
import { redactText } from '../governance/redaction.js';
|
|
63
63
|
import { CanonicalJsonByteEstimator } from './context-plan.js';
|
|
64
64
|
import { resolveActiveProvider } from '../cli/provider-resolution.js';
|
|
65
|
+
import { validateDecisionConfig } from '../config/decision-config.js';
|
|
65
66
|
import { readAiwgConfig } from '../config/aiwg-config.js';
|
|
66
67
|
import { decisionSavingsWeights, estimateDecisionSavings } from './savings.js';
|
|
67
68
|
export const DECISION_LEDGER_SCHEMA = 'aiwg-decision-ledger-record/v2';
|
|
@@ -231,7 +232,7 @@ export function buildDecisionLedgerRecord(input, result, context) {
|
|
|
231
232
|
const timestamp = new Date((context.now ?? Date.now)()).toISOString();
|
|
232
233
|
const legacy = estimateSavingsV1(input, result);
|
|
233
234
|
const inputTokens = new CanonicalJsonByteEstimator().estimate(contextMessageForEstimate(input)).tokens;
|
|
234
|
-
const savings = estimateDecisionSavings(inputTokens, result.status === 'answered', (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0), context.savingsWeights ?? decisionSavingsWeights());
|
|
235
|
+
const savings = estimateDecisionSavings(inputTokens, result.status === 'answered', (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0), context.savingsWeights ?? decisionSavingsWeights(), result.usage);
|
|
235
236
|
const caller = context.caller && ATTRIBUTION_FIELD_PATTERN.test(context.caller) ? context.caller : null;
|
|
236
237
|
const rawAnswer = result.answer;
|
|
237
238
|
const answer = typeof rawAnswer === 'string' ? sanitizeField(rawAnswer) : rawAnswer;
|
|
@@ -669,6 +670,11 @@ function isDecisionLedgerRecord(value) {
|
|
|
669
670
|
if (!isFiniteNumber(method[key]) || method[key] < 0 || method[key] > 1_000_000)
|
|
670
671
|
return false;
|
|
671
672
|
}
|
|
673
|
+
if (validateDecisionConfig({ ask: { cost: Object.fromEntries(['llm_per_mtok', 'jev_per_mtok', 'jev_input_per_mtok', 'jev_output_per_mtok', 'source', 'checked_at', 'jev_model']
|
|
674
|
+
.filter(key => method[key] !== undefined).map(key => [key, method[key]])) } }).length)
|
|
675
|
+
return false;
|
|
676
|
+
if (method.jev_rate_basis !== undefined && !['split', 'blended'].includes(method.jev_rate_basis))
|
|
677
|
+
return false;
|
|
672
678
|
if (typeof record.sessionSource !== 'string')
|
|
673
679
|
return false;
|
|
674
680
|
for (const key of ['llm_tokens_avoided', 'jev_tokens']) {
|
|
@@ -787,7 +793,7 @@ function groupKeyFor(record, groupBy) {
|
|
|
787
793
|
}
|
|
788
794
|
function newAccumulator() {
|
|
789
795
|
return {
|
|
790
|
-
llmTokensAvoided: 0, costWeightedNetUsd: 0, legacyReestimatedCalls: 0,
|
|
796
|
+
llmTokensAvoided: 0, costWeightedNetUsd: 0, legacyReestimatedCalls: 0, ratesUsed: [],
|
|
791
797
|
totalCalls: 0, answered: 0, fallback: 0, fallbackCountByReason: {},
|
|
792
798
|
agreementCount: 0, agreementSum: 0, escalatedCalls: 0, passesRun: 0,
|
|
793
799
|
confidenceCount: 0, confidenceSum: 0, confidenceMin: null, confidenceMax: null,
|
|
@@ -806,6 +812,18 @@ function accumulate(acc, record, weights) {
|
|
|
806
812
|
acc.passesRun += record.passesRun ?? (record.fallbackReason === 'not-configured' ? 0 : 1);
|
|
807
813
|
const inputTokens = record.usage?.inputTokens ?? 0;
|
|
808
814
|
const outputTokens = record.usage?.outputTokens ?? 0;
|
|
815
|
+
const method = record.schema === DECISION_LEDGER_SCHEMA
|
|
816
|
+
? record.estimationMethod
|
|
817
|
+
: estimateDecisionSavings(0, false, inputTokens + outputTokens, weights, record.usage).estimationMethod;
|
|
818
|
+
// Input estimates vary per call; group only the recorded rates and provenance.
|
|
819
|
+
const { input_tokens: _input, ...rateMethod } = method;
|
|
820
|
+
const canonicalRates = (rates) => JSON.stringify(Object.entries(rates).sort(([a], [b]) => a.localeCompare(b)));
|
|
821
|
+
const rateKey = canonicalRates(rateMethod);
|
|
822
|
+
const prior = acc.ratesUsed.find(item => canonicalRates(item.method) === rateKey);
|
|
823
|
+
if (prior)
|
|
824
|
+
prior.calls++;
|
|
825
|
+
else
|
|
826
|
+
acc.ratesUsed.push({ calls: 1, method: rateMethod });
|
|
809
827
|
if (record.schema === DECISION_LEDGER_SCHEMA) {
|
|
810
828
|
acc.llmTokensAvoided += record.llm_tokens_avoided;
|
|
811
829
|
acc.costWeightedNetUsd += record.cost_weighted_net_usd;
|
|
@@ -815,7 +833,7 @@ function accumulate(acc, record, weights) {
|
|
|
815
833
|
// per-kind allowance. Digests alone cannot reconstruct caller text.
|
|
816
834
|
const recoveredInput = record.status === 'answered'
|
|
817
835
|
? Math.max(0, record.estimatedLlmTokensAvoided - DECISION_SAVINGS_REASONING_ALLOWANCE_TOKENS[record.decisionKind]) : 0;
|
|
818
|
-
const estimate = estimateDecisionSavings(recoveredInput, record.status === 'answered', inputTokens + outputTokens, weights);
|
|
836
|
+
const estimate = estimateDecisionSavings(recoveredInput, record.status === 'answered', inputTokens + outputTokens, weights, record.usage);
|
|
819
837
|
acc.llmTokensAvoided += estimate.llm_tokens_avoided;
|
|
820
838
|
acc.costWeightedNetUsd += estimate.cost_weighted_net_usd;
|
|
821
839
|
acc.legacyReestimatedCalls += 1;
|
|
@@ -851,6 +869,7 @@ function finalizeGroup(key, acc) {
|
|
|
851
869
|
jev_tokens: acc.jevInputTokens + acc.jevOutputTokens,
|
|
852
870
|
cost_weighted_net_usd: acc.costWeightedNetUsd,
|
|
853
871
|
legacyReestimatedCalls: acc.legacyReestimatedCalls,
|
|
872
|
+
ratesUsed: acc.ratesUsed,
|
|
854
873
|
totalCalls: acc.totalCalls,
|
|
855
874
|
answered: acc.answered,
|
|
856
875
|
fallback: acc.fallback,
|