@aiwg/cli 2026.9.21 → 2026.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agentic/code/providers/capability-matrix.yaml +2 -1
- package/dist/src/artifacts/cli.js +23 -18
- package/dist/src/artifacts/discovery-shadow-evaluator.js +177 -0
- package/dist/src/artifacts/discovery-shadow.js +195 -0
- package/dist/src/artifacts/fortemi-core-query-adapter.js +6 -2
- package/dist/src/artifacts/index-builder.js +47 -9
- package/dist/src/artifacts/query-engine.js +108 -5
- package/dist/src/artifacts/types.js +1 -0
- package/dist/src/cli/handlers/decision.js +88 -0
- package/dist/src/cli/handlers/gates.js +154 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/sessions.js +10 -7
- package/dist/src/cli/handlers/utilities.js +20 -0
- package/dist/src/config/aiwg-config.js +15 -2
- package/dist/src/decision/acceptance.js +25 -0
- package/dist/src/decision/adapters/jev.js +24 -2
- package/dist/src/decision/calibration/registry.js +9 -0
- package/dist/src/decision/compose.js +74 -15
- package/dist/src/decision/context-live-promotion.js +344 -0
- package/dist/src/decision/context-live-qualification.js +591 -0
- package/dist/src/decision/context-pruning.js +847 -0
- package/dist/src/decision/driver.js +430 -0
- package/dist/src/decision/egress-live-qualification.js +874 -0
- package/dist/src/decision/ensemble/contract.js +64 -1
- package/dist/src/decision/ensemble/index.js +1 -0
- package/dist/src/decision/ensemble/report.js +8 -42
- package/dist/src/decision/ensemble/runtime.js +571 -0
- package/dist/src/decision/ensemble-study/artifacts.js +85 -0
- package/dist/src/decision/ensemble-study/calibration.js +443 -0
- package/dist/src/decision/ensemble-study/corpus.js +229 -0
- package/dist/src/decision/ensemble-study/multifact-artifacts.js +23 -0
- package/dist/src/decision/ensemble-study/multifact-study.js +596 -0
- package/dist/src/decision/ensemble-study/multifact.js +241 -0
- package/dist/src/decision/ensemble-study/protocol.js +77 -0
- package/dist/src/decision/ensemble-study/score.js +462 -0
- package/dist/src/decision/ensemble-study/staged.js +87 -0
- package/dist/src/decision/ensemble-study/statistics.js +84 -0
- package/dist/src/decision/evaluate.js +227 -14
- package/dist/src/decision/export.js +4 -0
- package/dist/src/decision/feature-export.js +1074 -0
- package/dist/src/decision/graph-index.js +16 -0
- package/dist/src/decision/graph-live-qualification.js +1041 -0
- package/dist/src/decision/graph-live-workload.js +245 -0
- package/dist/src/decision/graph-skill-bridge.js +21 -3
- package/dist/src/decision/heldout/calibration.js +140 -0
- package/dist/src/decision/heldout/collector.js +430 -0
- package/dist/src/decision/heldout/contract.js +309 -0
- package/dist/src/decision/heldout/d29-generator-ids.js +12 -0
- package/dist/src/decision/heldout/d29-passage-baseline-v2.js +93 -0
- package/dist/src/decision/heldout/d29-passage-baseline-v3.js +100 -0
- package/dist/src/decision/heldout/d29-passage-baseline.js +66 -0
- package/dist/src/decision/heldout/d29-pools-v8.js +575 -0
- package/dist/src/decision/heldout/d29-pools.js +288 -0
- package/dist/src/decision/heldout/d29-v6.js +169 -0
- package/dist/src/decision/heldout/d29-v7.js +202 -0
- package/dist/src/decision/heldout/d29-v8.js +586 -0
- package/dist/src/decision/heldout/generator-registry.js +67 -0
- package/dist/src/decision/heldout/generators.js +696 -0
- package/dist/src/decision/heldout/index.js +8 -0
- package/dist/src/decision/heldout/journal.js +346 -0
- package/dist/src/decision/heldout/study-generators.js +64 -0
- package/dist/src/decision/heldout/types.js +2 -0
- package/dist/src/decision/host-policy.js +61 -0
- package/dist/src/decision/index.js +11 -0
- package/dist/src/decision/interop-profile.v1.json +112 -0
- package/dist/src/decision/interop.js +653 -0
- package/dist/src/decision/issue-triage/index.js +3 -0
- package/dist/src/decision/issue-triage/runtime.js +1069 -0
- package/dist/src/decision/issue-triage/types.js +2 -0
- package/dist/src/decision/patterns/catalog.js +28 -8
- package/dist/src/decision/patterns/playground.js +28 -11
- package/dist/src/decision/patterns/specs.js +19 -10
- package/dist/src/decision/preprocessed-evidence.js +724 -0
- package/dist/src/decision/qualification/capture.js +8 -2
- package/dist/src/decision/qualification/gate-evidence.js +19 -4
- package/dist/src/decision/qualification/load.js +8 -1
- package/dist/src/decision/qualification/quality.js +57 -49
- package/dist/src/decision/qualification/release.js +76 -3
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/routing/contract.js +175 -0
- package/dist/src/decision/routing/index.js +5 -0
- package/dist/src/decision/routing/report.js +216 -0
- package/dist/src/decision/routing/runtime.js +685 -0
- package/dist/src/decision/routing/types.js +2 -0
- package/dist/src/decision/sdlc-screening.js +1157 -0
- package/dist/src/decision/sensitivity/comparative.js +154 -0
- package/dist/src/decision/sensitivity/contract.js +218 -0
- package/dist/src/decision/sensitivity/index.js +5 -0
- package/dist/src/decision/sensitivity/runtime.js +912 -0
- package/dist/src/decision/sensitivity/types.js +2 -0
- package/dist/src/decision/validate.js +26 -0
- package/dist/src/extensions/commands/definitions.js +38 -0
- package/dist/src/extensions/manifest.js +44 -0
- package/dist/src/gates/discovery.js +315 -0
- package/dist/src/gates/driver.js +388 -0
- package/dist/src/gates/evaluate.js +719 -0
- package/dist/src/gates/floors.js +255 -0
- package/dist/src/gates/index.js +17 -0
- package/dist/src/gates/providers/evidence.js +38 -0
- package/dist/src/gates/providers/index.js +21 -0
- package/dist/src/gates/providers/isolated-child-source.js +446 -0
- package/dist/src/gates/providers/loader.js +1069 -0
- package/dist/src/gates/providers/paired.js +52 -0
- package/dist/src/gates/providers/proportion.js +60 -0
- package/dist/src/gates/providers/registry.js +40 -0
- package/dist/src/gates/providers/scalar.js +41 -0
- package/dist/src/gates/providers/screening.js +53 -0
- package/dist/src/gates/providers/types.js +25 -0
- package/dist/src/gates/registry.js +1003 -0
- package/dist/src/gates/report.js +102 -0
- package/dist/src/gates/schema.js +56 -0
- package/dist/src/gates/stats/binomial.js +26 -0
- package/dist/src/gates/stats/bootstrap.js +62 -0
- package/dist/src/gates/stats/clopper-pearson.js +95 -0
- package/dist/src/gates/stats/digest.js +26 -0
- package/dist/src/gates/stats/error.js +5 -0
- package/dist/src/gates/stats/index.js +9 -0
- package/dist/src/gates/stats/normal.js +35 -0
- package/dist/src/gates/stats/paired.js +120 -0
- package/dist/src/gates/stats/splits.js +30 -0
- package/dist/src/gates/types.js +8 -0
- package/dist/src/governance/redaction.js +14 -4
- package/dist/src/mcp/muse-native-config.mjs +123 -0
- package/dist/src/mcp/tools/decision.mjs +188 -0
- package/dist/src/mcp/tools/subsystems.mjs +2 -0
- package/dist/src/providers/capability-matrix.yaml +2 -1
- package/dist/src/providers/provider-definitions.js +2 -1
- package/dist/src/sessions/adapters/muse.js +336 -39
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/workspace-discovery.js +13 -2
- package/package.json +1 -1
- package/schemas/decision/ConformalFrozenOpenData.v2.schema.json +371 -0
- package/schemas/decision/ConformalOpenDataReport.v2.schema.json +421 -0
- package/schemas/decision/ConformalPreregistration.v2.schema.json +529 -0
- package/schemas/decision/ContextPruningCandidate.v1.schema.json +47 -0
- package/schemas/decision/ContextPruningEvaluationReport.v1.schema.json +758 -0
- package/schemas/decision/ContextPruningPreregistration.v1.schema.json +175 -0
- package/schemas/decision/ContextPruningReceipt.v1.schema.json +72 -0
- package/schemas/decision/D17Multifact.v1.schema.json +1851 -0
- package/schemas/decision/D17StudyAnalysis.v1.schema.json +42 -0
- package/schemas/decision/D17StudyAnalysis.v2.schema.json +51 -0
- package/schemas/decision/D17StudyCalibration.v1.schema.json +819 -0
- package/schemas/decision/D17StudyDryRun.v1.schema.json +169 -0
- package/schemas/decision/D17StudyDryRun.v2.schema.json +239 -0
- package/schemas/decision/D17StudyGold.v1.schema.json +108 -0
- package/schemas/decision/D17StudyGuide.v1.schema.json +63 -0
- package/schemas/decision/D17StudyNativeTemplates.v1.schema.json +619 -0
- package/schemas/decision/D17StudyProtocol.v1.schema.json +241 -0
- package/schemas/decision/D17StudyReport.v1.schema.json +902 -0
- package/schemas/decision/D17StudyReport.v2.schema.json +1319 -0
- package/schemas/decision/D17StudyReview.v1.schema.json +175 -0
- package/schemas/decision/D17StudySplits.v1.schema.json +217 -0
- package/schemas/decision/D29Study.v1.schema.json +1903 -0
- package/schemas/decision/D29Study.v2.schema.json +1344 -0
- package/schemas/decision/D29Study.v3.schema.json +609 -0
- package/schemas/decision/D29Study.v4.schema.json +1133 -0
- package/schemas/decision/D29Study.v6.schema.json +1205 -0
- package/schemas/decision/D29Study.v7.schema.json +1471 -0
- package/schemas/decision/D29Study.v8.schema.json +671 -0
- package/schemas/decision/D29Study.v9.schema.json +778 -0
- package/schemas/decision/DecisionComparativeReplayAuditSample.v1.schema.json +137 -0
- package/schemas/decision/DecisionComparativeReplayCorpus.v1.schema.json +258 -0
- package/schemas/decision/DecisionComparativeReplayFreeze.v1.schema.json +74 -0
- package/schemas/decision/DecisionComparativeReplayGold.v1.schema.json +50 -0
- package/schemas/decision/DecisionComparativeReplayGoldAsset.v1.schema.json +37 -0
- package/schemas/decision/DecisionComparativeReplayIntegrity.v1.schema.json +81 -0
- package/schemas/decision/DecisionComparativeReplayMembers.v1.schema.json +52 -0
- package/schemas/decision/DecisionComparativeReplayPreregistration.v1.schema.json +113 -0
- package/schemas/decision/DecisionComparativeReplayReport.v1.schema.json +407 -0
- package/schemas/decision/DecisionComparativeReplayReviewPackets.v1.schema.json +260 -0
- package/schemas/decision/DecisionComparativeReplaySplit.v1.schema.json +84 -0
- package/schemas/decision/DecisionFeatureObservation.v1.schema.json +564 -0
- package/schemas/decision/DecisionFeatureSet.v1.schema.json +155 -0
- package/schemas/decision/DecisionInteropMapping.v1.schema.json +85 -0
- package/schemas/decision/DecisionIssueTriageEvaluationManifest.v1.schema.json +174 -0
- package/schemas/decision/DecisionIssueTriageEvaluationReport.v1.schema.json +280 -0
- package/schemas/decision/DecisionIssueTriagePilotPack.v1.schema.json +114 -0
- package/schemas/decision/DecisionRouteReceipt.v1.schema.json +155 -0
- package/schemas/decision/DecisionRoutingPolicy.v1.schema.json +132 -0
- package/schemas/decision/DecisionRoutingShadowReport.v1.schema.json +170 -0
- package/schemas/decision/DecisionRoutingTask.v1.schema.json +49 -0
- package/schemas/decision/DecisionRuleset.schema.json +8 -2
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +8 -2
- package/schemas/decision/DecisionSensitivityPlan.v1.schema.json +191 -0
- package/schemas/decision/DecisionSensitivityReport.v1.schema.json +163 -0
- package/schemas/decision/HeldoutApproval.v1.schema.json +312 -0
- package/schemas/decision/HeldoutAttempt.v1.schema.json +211 -0
- package/schemas/decision/HeldoutBaseline.v1.schema.json +90 -0
- package/schemas/decision/HeldoutCalibrationPhase.v1.schema.json +87 -0
- package/schemas/decision/HeldoutCorpus.v1.schema.json +167 -0
- package/schemas/decision/HeldoutEvent.v1.schema.json +41 -0
- package/schemas/decision/HeldoutFrozen.v1.schema.json +142 -0
- package/schemas/decision/HeldoutPreregistration.v1.schema.json +222 -0
- package/schemas/decision/HeldoutSpendEvent.v1.schema.json +122 -0
- package/schemas/decision/HeldoutSpendHead.v1.schema.json +25 -0
- package/schemas/decision/HeldoutSpendHead.v2.schema.json +46 -0
- package/schemas/decision/HeldoutSummary.v1.schema.json +101 -0
- package/schemas/decision/OpaDecisionLogExport.v1.schema.json +65 -0
- package/schemas/decision/PreprocessedEvidence.v1.schema.json +306 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +175 -0
- package/schemas/decision/SdlcEvidenceScreening.v1.schema.json +499 -0
- package/schemas/decision/SdlcScreeningPreregistration.v1.schema.json +132 -0
- package/schemas/decision/SdlcScreeningPreregistration.v2.schema.json +138 -0
- package/schemas/decision/SdlcScreeningRelease.v1.schema.json +654 -0
- package/schemas/decision/SdlcScreeningRelease.v2.schema.json +654 -0
- package/tools/agents/deploy-agents.mjs +1 -1
- package/tools/agents/providers/base.mjs +26 -7
- package/tools/agents/providers/factory.mjs +10 -2
- package/tools/agents/providers/muse-hooks.mjs +1 -29
- package/tools/agents/providers/muse.mjs +6 -3
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
import { canonicalJson } from '../../security/artifact-trust.js';
|
|
2
|
+
import { redactStructured } from '../../governance/redaction.js';
|
|
3
|
+
import { admitEntry } from '../entry.js';
|
|
4
|
+
import { projectDecisionState } from '../projection.js';
|
|
5
|
+
import { executeDriftResponse, rollbackChampionForNewRuns } from '../ensemble/runtime.js';
|
|
6
|
+
import { compareRoutingKeys, frozenRoutingClone, RoutingContractError, routingDigest, validateJevRoutingEvidence, validateRoutingPolicy, validateRoutingTask, } from './contract.js';
|
|
7
|
+
/** Provider-level failures open that provider's circuit for the rest of the run. */
|
|
8
|
+
const CIRCUIT_REASONS = new Set(['rate-limit', 'outage', 'circuit-open']);
|
|
9
|
+
const DISPATCH_FAILURES = new Set(['rate-limit', 'outage', 'circuit-open', 'timeout', 'rejected', 'failed', 'cancelled']);
|
|
10
|
+
const RESULT_KEYS = ['actualModel', 'actualProvider', 'costMicros', 'inputTokens', 'latencyMs', 'outcomeLabel', 'outputTokens',
|
|
11
|
+
'reason', 'status', 'verified', 'workerId'];
|
|
12
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
|
|
13
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
14
|
+
/** Pure hard-constraint filter: no hooks, no reservations, no model input. Invalid policy or task throws. */
|
|
15
|
+
export function evaluateRoutingEligibility(policyInput, taskInput) {
|
|
16
|
+
const { policy, digest } = validateRoutingPolicy(policyInput);
|
|
17
|
+
const task = validateRoutingTask(taskInput);
|
|
18
|
+
const eligible = [];
|
|
19
|
+
const excluded = [];
|
|
20
|
+
for (const candidate of policy.candidates) {
|
|
21
|
+
const reasons = exclusionReasons(candidate, task, policy);
|
|
22
|
+
if (reasons.length)
|
|
23
|
+
excluded.push({ routeId: candidate.id, reasons });
|
|
24
|
+
else
|
|
25
|
+
eligible.push({ candidate, summary: summarize(candidate) });
|
|
26
|
+
}
|
|
27
|
+
return { policy, digest, task, eligible, excluded };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* D28 shadow routing. Shadow executes only the existing deterministic route (`defaultRouteId`, then
|
|
31
|
+
* `deterministicFallbackRouteId`) and records the Jev-assisted choice as a counterfactual that never
|
|
32
|
+
* executes. Disabled mode calls no hook. Every path after policy validation returns a receipt.
|
|
33
|
+
*/
|
|
34
|
+
export async function runRoutingPilot(policyInput, taskInput, options = {}) {
|
|
35
|
+
const { policy, digest } = validateRoutingPolicy(policyInput);
|
|
36
|
+
const clock = () => {
|
|
37
|
+
const value = (options.now ?? Date.now)();
|
|
38
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
39
|
+
throw new RoutingContractError('routing clock returned an invalid time');
|
|
40
|
+
return value;
|
|
41
|
+
};
|
|
42
|
+
let createdAtEpochMs = 0;
|
|
43
|
+
const body = {
|
|
44
|
+
task: { id: safeTaskId(taskInput), projection: null },
|
|
45
|
+
status: 'review', reason: 'runtime-error', eligibleRouteIds: [], excluded: [], jev: null,
|
|
46
|
+
counterfactual: notEvaluated('not-evaluated'), selectedRouteId: null, selectionReason: 'review',
|
|
47
|
+
actual: { workerId: null, provider: null, model: null }, attempts: [], skipped: [], fallbacks: [],
|
|
48
|
+
budget: { limitMicros: null, spentMicros: null }, deadlineEpochMs: null, usage: nullUsage(), outcome: { verified: null, label: null },
|
|
49
|
+
};
|
|
50
|
+
const receipt = () => {
|
|
51
|
+
const payload = {
|
|
52
|
+
schemaVersion: 'decision-route-receipt/v1',
|
|
53
|
+
routing: { id: policy.id, version: policy.version, policyVersion: policy.policyVersion, mode: policy.mode, digest },
|
|
54
|
+
candidatePins: candidatePins(policy),
|
|
55
|
+
createdAtEpochMs,
|
|
56
|
+
...body,
|
|
57
|
+
};
|
|
58
|
+
return { ...payload, digest: routingDigest(payload) };
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
createdAtEpochMs = clock();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
body.reason = 'clock-invalid';
|
|
65
|
+
return receipt();
|
|
66
|
+
}
|
|
67
|
+
if (options.enabled !== true || policy.mode === 'disabled') {
|
|
68
|
+
Object.assign(body, { status: 'disabled', reason: policy.mode === 'disabled' ? 'policy-disabled' : 'runtime-disabled',
|
|
69
|
+
selectionReason: 'existing-behavior-preserved', counterfactual: notEvaluated('disabled') });
|
|
70
|
+
return receipt();
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await runShadow(policy, taskInput, options, clock, createdAtEpochMs, body);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// A failure inside a hook or the runtime keeps every attempt already recorded.
|
|
77
|
+
body.status = 'review';
|
|
78
|
+
body.reason = 'runtime-error';
|
|
79
|
+
body.selectedRouteId = null;
|
|
80
|
+
body.selectionReason = 'review';
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return receipt();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Only a value that escaped validation can fail canonical encoding; keep the decision, drop the value.
|
|
87
|
+
body.jev = null;
|
|
88
|
+
body.actual = { workerId: null, provider: null, model: null };
|
|
89
|
+
body.status = 'review';
|
|
90
|
+
body.reason = 'runtime-error';
|
|
91
|
+
return receipt();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function runShadow(policy, taskInput, options, clock, startedAt, body) {
|
|
95
|
+
let task;
|
|
96
|
+
try {
|
|
97
|
+
task = validateRoutingTask(taskInput);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
Object.assign(body, { status: 'review', reason: 'invalid-task', counterfactual: notEvaluated('invalid-task') });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { eligible, excluded } = evaluateRoutingEligibility(policy, task);
|
|
104
|
+
body.eligibleRouteIds = eligible.map(item => item.candidate.id);
|
|
105
|
+
body.excluded = excluded;
|
|
106
|
+
const limitMicros = Math.min(task.requirements.maxCostMicros, policy.ceilings.maxCostMicros);
|
|
107
|
+
const deadlineEpochMs = startedAt + Math.min(task.requirements.deadlineMs, policy.ceilings.deadlineMs);
|
|
108
|
+
body.budget = { limitMicros, spentMicros: 0 };
|
|
109
|
+
body.deadlineEpochMs = deadlineEpochMs;
|
|
110
|
+
const eligibleById = new Map(eligible.map(item => [item.candidate.id, item.candidate]));
|
|
111
|
+
const chainIds = [...new Set([policy.defaultRouteId, policy.deterministicFallbackRouteId].filter((id) => id !== null))];
|
|
112
|
+
const chain = chainIds.map(id => eligibleById.get(id)).filter((item) => item !== undefined);
|
|
113
|
+
if (!eligible.length) {
|
|
114
|
+
Object.assign(body, { status: 'no-route', reason: 'empty-eligible-set' });
|
|
115
|
+
}
|
|
116
|
+
else if (!chainIds.length) {
|
|
117
|
+
Object.assign(body, { status: 'no-route', reason: 'no-deterministic-route' });
|
|
118
|
+
}
|
|
119
|
+
else if (!chain.length) {
|
|
120
|
+
Object.assign(body, { status: 'no-route', reason: 'deterministic-route-ineligible' });
|
|
121
|
+
}
|
|
122
|
+
else if (!options.dispatch) {
|
|
123
|
+
Object.assign(body, { status: 'selected', reason: 'deterministic-route', selectedRouteId: chain[0].id,
|
|
124
|
+
selectionReason: 'deterministic-authoritative' });
|
|
125
|
+
}
|
|
126
|
+
else if (!options.reserve || !options.release) {
|
|
127
|
+
Object.assign(body, { status: 'review', reason: 'reservation-unavailable' });
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
await executeDeterministicChain(policy, chain, options, clock, { limitMicros, deadlineEpochMs }, body);
|
|
131
|
+
}
|
|
132
|
+
// The authoritative outcome is already recorded; the shadow evaluation is bounded by the remaining
|
|
133
|
+
// task deadline and the caller's signal, and its failure can only mark the counterfactual.
|
|
134
|
+
body.task = { id: task.id, projection: null };
|
|
135
|
+
try {
|
|
136
|
+
const shadow = await counterfactual(policy, task, eligible, chain[0]?.id ?? null, options, deadlineEpochMs, clock);
|
|
137
|
+
body.counterfactual = shadow.counterfactual;
|
|
138
|
+
body.jev = shadow.jev;
|
|
139
|
+
body.task = { id: task.id, projection: shadow.projection };
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
body.counterfactual = { status: 'review', reason: 'runtime-error', routeId: null, ranking: [] };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function executeDeterministicChain(policy, chain, options, clock, limits, body) {
|
|
146
|
+
const dispatch = options.dispatch;
|
|
147
|
+
const reserve = options.reserve;
|
|
148
|
+
const release = options.release;
|
|
149
|
+
const delay = options.delay ?? realTimer;
|
|
150
|
+
const timer = options.timer ?? realTimer;
|
|
151
|
+
const openProviders = new Set();
|
|
152
|
+
const observedCost = new Map();
|
|
153
|
+
let spent = 0;
|
|
154
|
+
let ordinal = 0;
|
|
155
|
+
let fallbacksUsed = 0;
|
|
156
|
+
let exhausted = 'fallbacks-exhausted';
|
|
157
|
+
let concluded = false;
|
|
158
|
+
const stop = (reason) => {
|
|
159
|
+
concluded = true;
|
|
160
|
+
Object.assign(body, { status: 'review', reason, selectedRouteId: null, selectionReason: 'review' });
|
|
161
|
+
};
|
|
162
|
+
const skip = (routeId, reason) => {
|
|
163
|
+
body.skipped.push({ routeId, reason });
|
|
164
|
+
if (reason === 'budget-exhausted' || reason === 'deadline-exhausted')
|
|
165
|
+
exhausted = reason;
|
|
166
|
+
};
|
|
167
|
+
routes: for (const [index, route] of chain.entries()) {
|
|
168
|
+
let triesOnRoute = 0;
|
|
169
|
+
while (triesOnRoute < route.operations.maxAttempts) {
|
|
170
|
+
if (options.signal?.aborted) {
|
|
171
|
+
stop('cancelled');
|
|
172
|
+
break routes;
|
|
173
|
+
}
|
|
174
|
+
if (ordinal >= policy.ceilings.maxAttempts) {
|
|
175
|
+
stop('attempts-exhausted');
|
|
176
|
+
break routes;
|
|
177
|
+
}
|
|
178
|
+
if (index > 0 && triesOnRoute === 0 && fallbacksUsed >= policy.ceilings.maxFallbacks)
|
|
179
|
+
break routes;
|
|
180
|
+
// Every attempt, including a fallback, re-checks the constraints that change during the run.
|
|
181
|
+
if (openProviders.has(route.model.provider)) {
|
|
182
|
+
skip(route.id, 'circuit-open');
|
|
183
|
+
continue routes;
|
|
184
|
+
}
|
|
185
|
+
// Null means unknown, never zero: without known charged spend there is no enforceable
|
|
186
|
+
// remaining cap, so the run stops before any further dispatch.
|
|
187
|
+
if (spent === null) {
|
|
188
|
+
stop('cost-unknown');
|
|
189
|
+
break routes;
|
|
190
|
+
}
|
|
191
|
+
// Price the attempt at the worst cost already seen for this route or provider, never below the pinned estimate.
|
|
192
|
+
const cost = Math.max(route.operations.costMicrosPerAttempt, observedCost.get(`route:${route.id}`) ?? 0, observedCost.get(`provider:${route.model.provider}`) ?? 0);
|
|
193
|
+
const remaining = limits.limitMicros - spent;
|
|
194
|
+
if (!Number.isSafeInteger(remaining) || remaining < 0 || cost > remaining) {
|
|
195
|
+
skip(route.id, 'budget-exhausted');
|
|
196
|
+
continue routes;
|
|
197
|
+
}
|
|
198
|
+
const backoff = ordinal === 0 ? 0 : Math.min(policy.ceilings.retryDelayMs * 2 ** (ordinal - 1), MAX_BACKOFF_MS);
|
|
199
|
+
if (clock() + backoff + route.operations.deadlineMs > limits.deadlineEpochMs) {
|
|
200
|
+
skip(route.id, 'deadline-exhausted');
|
|
201
|
+
continue routes;
|
|
202
|
+
}
|
|
203
|
+
if (backoff > 0) {
|
|
204
|
+
try {
|
|
205
|
+
await delay(backoff, options.signal ?? new AbortController().signal);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
stop('cancelled');
|
|
209
|
+
break routes;
|
|
210
|
+
}
|
|
211
|
+
if (options.signal?.aborted) {
|
|
212
|
+
stop('cancelled');
|
|
213
|
+
break routes;
|
|
214
|
+
}
|
|
215
|
+
if (clock() + route.operations.deadlineMs > limits.deadlineEpochMs) {
|
|
216
|
+
skip(route.id, 'deadline-exhausted');
|
|
217
|
+
continue routes;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Read the clock before reserving: nothing between a granted reservation and its release may throw.
|
|
221
|
+
const attemptDeadline = clock() + route.operations.deadlineMs;
|
|
222
|
+
// The reservation and the dispatch both carry the remaining-cost cap, so a fallback admitted
|
|
223
|
+
// under its estimate cannot authorize spend above the remaining budget. A denial refuses it.
|
|
224
|
+
const budget = { remainingBudgetMicros: remaining, costCeilingMicros: remaining, estimatedCostMicros: cost };
|
|
225
|
+
let granted = false;
|
|
226
|
+
try {
|
|
227
|
+
granted = await reserve(route, ordinal + 1, budget) === true;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
granted = false;
|
|
231
|
+
}
|
|
232
|
+
if (!granted) {
|
|
233
|
+
skip(route.id, 'reservation-denied');
|
|
234
|
+
continue routes;
|
|
235
|
+
}
|
|
236
|
+
ordinal += 1;
|
|
237
|
+
triesOnRoute += 1;
|
|
238
|
+
if (index > 0 && triesOnRoute === 1)
|
|
239
|
+
fallbacksUsed += 1;
|
|
240
|
+
const outcome = await raceDispatch(dispatch, route, ordinal, attemptDeadline, route.operations.deadlineMs, timer, options.signal, budget.costCeilingMicros);
|
|
241
|
+
const result = outcome.forced ? failedDispatch(outcome.forced) : outcome.result;
|
|
242
|
+
const charged = result ? result.costMicros : null;
|
|
243
|
+
// A failing release hook must not discard the attempt record; the receipt keeps the charge.
|
|
244
|
+
try {
|
|
245
|
+
await release(route, ordinal, charged);
|
|
246
|
+
}
|
|
247
|
+
catch { /* recorded below */ }
|
|
248
|
+
spent = spent === null || charged === null ? null : spent + charged;
|
|
249
|
+
if (charged !== null) {
|
|
250
|
+
for (const key of [`route:${route.id}`, `provider:${route.model.provider}`])
|
|
251
|
+
observedCost.set(key, Math.max(observedCost.get(key) ?? 0, charged));
|
|
252
|
+
}
|
|
253
|
+
body.budget = { limitMicros: limits.limitMicros, spentMicros: spent };
|
|
254
|
+
if (!result) {
|
|
255
|
+
body.attempts.push(attemptReceipt(ordinal, route, failedDispatch('failed'), 'invalid-result'));
|
|
256
|
+
body.usage = aggregateUsage(body.attempts);
|
|
257
|
+
stop('invalid-dispatch-result');
|
|
258
|
+
break routes;
|
|
259
|
+
}
|
|
260
|
+
const substituted = result.status === 'success'
|
|
261
|
+
&& (result.actualProvider !== route.model.provider || result.actualModel !== route.model.pinnedVersion);
|
|
262
|
+
body.attempts.push(attemptReceipt(ordinal, route, result, substituted ? 'substituted' : result.reason));
|
|
263
|
+
body.usage = aggregateUsage(body.attempts);
|
|
264
|
+
body.actual = { workerId: result.workerId, provider: result.actualProvider, model: result.actualModel };
|
|
265
|
+
if (result.status === 'success') {
|
|
266
|
+
if (substituted)
|
|
267
|
+
stop(result.actualProvider === null || result.actualModel === null ? 'actual-model-unknown' : 'model-substitution');
|
|
268
|
+
else if (spent === null)
|
|
269
|
+
stop('cost-unknown');
|
|
270
|
+
else if (spent > limits.limitMicros)
|
|
271
|
+
stop('budget-exceeded');
|
|
272
|
+
else {
|
|
273
|
+
concluded = true;
|
|
274
|
+
Object.assign(body, { status: 'selected', reason: 'deterministic-route', selectedRouteId: route.id,
|
|
275
|
+
selectionReason: 'deterministic-authoritative', outcome: { verified: result.verified, label: result.outcomeLabel } });
|
|
276
|
+
}
|
|
277
|
+
break routes;
|
|
278
|
+
}
|
|
279
|
+
// A known overrun outranks everything; cancellation outranks an unknown cost it caused.
|
|
280
|
+
if (spent !== null && spent > limits.limitMicros) {
|
|
281
|
+
stop('budget-exceeded');
|
|
282
|
+
break routes;
|
|
283
|
+
}
|
|
284
|
+
if (result.reason === 'cancelled') {
|
|
285
|
+
stop('cancelled');
|
|
286
|
+
break routes;
|
|
287
|
+
}
|
|
288
|
+
if (spent === null) {
|
|
289
|
+
stop('cost-unknown');
|
|
290
|
+
break routes;
|
|
291
|
+
}
|
|
292
|
+
if (CIRCUIT_REASONS.has(result.reason)) {
|
|
293
|
+
openProviders.add(route.model.provider);
|
|
294
|
+
body.fallbacks.push(route.id);
|
|
295
|
+
continue routes;
|
|
296
|
+
}
|
|
297
|
+
if (result.reason !== 'timeout') {
|
|
298
|
+
stop('dispatch-failed');
|
|
299
|
+
break routes;
|
|
300
|
+
}
|
|
301
|
+
if (triesOnRoute >= route.operations.maxAttempts)
|
|
302
|
+
body.fallbacks.push(route.id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!concluded)
|
|
306
|
+
stop(exhausted);
|
|
307
|
+
}
|
|
308
|
+
/** Races one dispatch against its attempt deadline and the caller's signal; a hung dispatch is aborted. */
|
|
309
|
+
async function raceDispatch(dispatch, candidate, attemptOrdinal, deadlineEpochMs, budgetMs, timer, parent, costCeilingMicros) {
|
|
310
|
+
const controller = new AbortController();
|
|
311
|
+
const timerControl = new AbortController();
|
|
312
|
+
const onParent = () => controller.abort();
|
|
313
|
+
if (parent?.aborted)
|
|
314
|
+
controller.abort();
|
|
315
|
+
else
|
|
316
|
+
parent?.addEventListener('abort', onParent, { once: true });
|
|
317
|
+
let pending;
|
|
318
|
+
try {
|
|
319
|
+
pending = Promise.resolve(dispatch({ candidate, attemptOrdinal, deadlineEpochMs, costCeilingMicros, signal: controller.signal }));
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
pending = Promise.reject(error);
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
const outcome = await Promise.race([
|
|
326
|
+
pending.then(value => ({ kind: 'result', value }), () => ({ kind: 'rejected' })),
|
|
327
|
+
// A timer that fails before the race settles cannot prove the deadline holds, so it counts as a timeout.
|
|
328
|
+
Promise.resolve().then(() => timer(budgetMs, timerControl.signal)).then(() => ({ kind: 'timeout' }), () => ({ kind: 'timeout' })),
|
|
329
|
+
new Promise(resolve => {
|
|
330
|
+
if (controller.signal.aborted)
|
|
331
|
+
resolve({ kind: 'cancelled' });
|
|
332
|
+
controller.signal.addEventListener('abort', () => resolve({ kind: 'cancelled' }), { once: true });
|
|
333
|
+
}),
|
|
334
|
+
]);
|
|
335
|
+
if (outcome.kind === 'timeout')
|
|
336
|
+
controller.abort();
|
|
337
|
+
if (outcome.kind === 'result')
|
|
338
|
+
return { result: validDispatchResult(outcome.value) ? outcome.value : null, forced: null };
|
|
339
|
+
return { result: null, forced: outcome.kind };
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
timerControl.abort();
|
|
343
|
+
parent?.removeEventListener('abort', onParent);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function counterfactual(policy, task, eligible, deterministicRouteId, options, deadlineEpochMs, clock) {
|
|
347
|
+
const review = (reason, projection = null, jev = null) => ({ counterfactual: { status: 'review', reason, routeId: null, ranking: [] }, jev, projection });
|
|
348
|
+
if (!policy.jevEvidence.enabled)
|
|
349
|
+
return { counterfactual: notEvaluated('jev-evidence-disabled'), jev: null, projection: null };
|
|
350
|
+
if (!options.evidence)
|
|
351
|
+
return { counterfactual: notEvaluated('jev-evidence-unavailable'), jev: null, projection: null };
|
|
352
|
+
if (!eligible.length)
|
|
353
|
+
return { counterfactual: notEvaluated('empty-eligible-set'), jev: null, projection: null };
|
|
354
|
+
if (options.signal?.aborted)
|
|
355
|
+
return { counterfactual: notEvaluated('cancelled'), jev: null, projection: null };
|
|
356
|
+
if (task.projection?.provider !== 'jev')
|
|
357
|
+
return review('projection-denied');
|
|
358
|
+
// D10: only declared state fields cross, then credential-shaped values are redacted. The task
|
|
359
|
+
// description and every undeclared field stay behind.
|
|
360
|
+
let projectedState;
|
|
361
|
+
let projection;
|
|
362
|
+
try {
|
|
363
|
+
const projected = await projectDecisionState(task.state, task.projection);
|
|
364
|
+
projectedState = frozenRoutingClone(redactStructured(projected.state).value);
|
|
365
|
+
projection = projected.evidence;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return review('projection-denied');
|
|
369
|
+
}
|
|
370
|
+
// Never start or wait past the task deadline, and stop at once on the caller's abort.
|
|
371
|
+
const remainingMs = Math.min(deadlineEpochMs - clock(), policy.ceilings.deadlineMs);
|
|
372
|
+
if (remainingMs <= 0)
|
|
373
|
+
return { counterfactual: notEvaluated('deadline-exhausted'), jev: null, projection };
|
|
374
|
+
if (options.signal?.aborted)
|
|
375
|
+
return { counterfactual: notEvaluated('cancelled'), jev: null, projection };
|
|
376
|
+
const request = new AbortController();
|
|
377
|
+
const timerControl = new AbortController();
|
|
378
|
+
const onParent = () => request.abort();
|
|
379
|
+
options.signal?.addEventListener('abort', onParent, { once: true });
|
|
380
|
+
let outcome;
|
|
381
|
+
try {
|
|
382
|
+
outcome = await Promise.race([
|
|
383
|
+
Promise.resolve().then(() => options.evidence({
|
|
384
|
+
task: { id: task.id },
|
|
385
|
+
projectedState,
|
|
386
|
+
projection: frozenRoutingClone(projection),
|
|
387
|
+
candidates: frozenRoutingClone(eligible.map(item => item.summary)),
|
|
388
|
+
signal: request.signal,
|
|
389
|
+
})).then(value => ({ kind: 'evidence', value }), () => ({ kind: 'failed' })),
|
|
390
|
+
Promise.resolve().then(() => (options.timer ?? realTimer)(remainingMs, timerControl.signal))
|
|
391
|
+
.then(() => ({ kind: 'timeout' }), () => ({ kind: 'timeout' })),
|
|
392
|
+
new Promise(resolve => request.signal.addEventListener('abort', () => resolve({ kind: 'cancelled' }), { once: true })),
|
|
393
|
+
]);
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
timerControl.abort();
|
|
397
|
+
options.signal?.removeEventListener('abort', onParent);
|
|
398
|
+
}
|
|
399
|
+
if (outcome.kind !== 'evidence')
|
|
400
|
+
request.abort();
|
|
401
|
+
if (outcome.kind === 'cancelled')
|
|
402
|
+
return { counterfactual: notEvaluated('cancelled'), jev: null, projection };
|
|
403
|
+
if (outcome.kind === 'timeout')
|
|
404
|
+
return review('jev-evidence-timeout', projection);
|
|
405
|
+
if (outcome.kind === 'failed')
|
|
406
|
+
return review('jev-evidence-unavailable', projection);
|
|
407
|
+
const raw = outcome.value;
|
|
408
|
+
let evidence;
|
|
409
|
+
try {
|
|
410
|
+
evidence = frozenRoutingClone(validateJevRoutingEvidence(raw));
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return review('malformed-jev-evidence', projection);
|
|
414
|
+
}
|
|
415
|
+
if (evidence.model !== task.projection.model)
|
|
416
|
+
return review('malformed-jev-evidence', projection);
|
|
417
|
+
const normalized = {
|
|
418
|
+
...evidence,
|
|
419
|
+
distributions: [...evidence.distributions].sort((a, b) => compareRoutingKeys(a.routeId, b.routeId)),
|
|
420
|
+
};
|
|
421
|
+
if (!policy.jevEvidence.compatibleProfiles.includes(evidence.profile))
|
|
422
|
+
return review('incompatible-profile', projection, normalized);
|
|
423
|
+
if (evidence.calibration !== 'calibrated')
|
|
424
|
+
return review('uncalibrated-evidence', projection, normalized);
|
|
425
|
+
const ids = normalized.distributions.map(item => item.routeId);
|
|
426
|
+
const eligibleIds = new Set(eligible.map(item => item.candidate.id));
|
|
427
|
+
if (new Set(ids).size !== ids.length)
|
|
428
|
+
return review('duplicate-jev-route', projection);
|
|
429
|
+
if (ids.some(id => !eligibleIds.has(id)))
|
|
430
|
+
return review('malformed-jev-evidence', projection);
|
|
431
|
+
if (ids.length !== eligibleIds.size)
|
|
432
|
+
return review('incomplete-jev-evidence', projection, normalized);
|
|
433
|
+
if (Math.round(evidence.ambiguity * 10_000) > policy.jevEvidence.uncertaintyThresholdBps) {
|
|
434
|
+
return deterministicRouteId
|
|
435
|
+
? { counterfactual: { status: 'deterministic-fallback', reason: 'ambiguous-evidence', routeId: deterministicRouteId, ranking: [] }, jev: normalized, projection }
|
|
436
|
+
: review('ambiguous-evidence', projection, normalized);
|
|
437
|
+
}
|
|
438
|
+
const ranking = rankRoutes(policy, eligible, normalized).map(item => item.candidate.id);
|
|
439
|
+
return { counterfactual: { status: 'selected', reason: 'deterministic-utility', routeId: ranking[0], ranking }, jev: normalized, projection };
|
|
440
|
+
}
|
|
441
|
+
export class RoutingControlDrillError extends RoutingContractError {
|
|
442
|
+
state;
|
|
443
|
+
constructor(message, state) {
|
|
444
|
+
super(message);
|
|
445
|
+
this.state = state;
|
|
446
|
+
this.name = 'RoutingControlDrillError';
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* AC9 drill. Every drift response opens the Jev route circuit except a plain alert. For
|
|
451
|
+
* `restore-champion` the reversible step runs first: the prior pinned routing policy is restored and
|
|
452
|
+
* verified, then D17 rolls the alias back. If the alias rollback is refused the policy restore is
|
|
453
|
+
* compensated; any failure throws `RoutingControlDrillError` carrying the resulting state. Active-run
|
|
454
|
+
* pins are re-read after the response and must equal the pins read before it.
|
|
455
|
+
*/
|
|
456
|
+
export async function runRoutingControlDrill(input) {
|
|
457
|
+
const { control } = input;
|
|
458
|
+
const before = frozenRoutingClone(control.activeRunPins());
|
|
459
|
+
const history = control.policyHistory();
|
|
460
|
+
const previousPolicy = history[history.length - 1];
|
|
461
|
+
if (!previousPolicy)
|
|
462
|
+
throw new RoutingContractError('routing control has no current pinned policy');
|
|
463
|
+
let restoredPolicy = null;
|
|
464
|
+
let rollbackEvent = null;
|
|
465
|
+
let jevCircuitOpen = false;
|
|
466
|
+
const openCircuit = (reason) => { control.openJevCircuit(reason, input.at); jevCircuitOpen = true; };
|
|
467
|
+
const contain = (action) => async () => { openCircuit(action); };
|
|
468
|
+
const installed = (target, returned) => canonicalJson(returned) === canonicalJson(target) && canonicalJson(control.policyHistory().at(-1)) === canonicalJson(target);
|
|
469
|
+
const fail = (message, state) => {
|
|
470
|
+
throw new RoutingControlDrillError(message, { jevCircuitOpen, ...state });
|
|
471
|
+
};
|
|
472
|
+
const drift = await executeDriftResponse(input.driftPolicy, input.driftSignal, {
|
|
473
|
+
'restore-champion': async () => {
|
|
474
|
+
const prior = [...history].reverse().find(pin => pin.version !== previousPolicy.version || pin.digest !== previousPolicy.digest);
|
|
475
|
+
if (!prior)
|
|
476
|
+
throw new RoutingContractError('rollback requires a prior pinned routing policy');
|
|
477
|
+
openCircuit('restore-champion');
|
|
478
|
+
const unchanged = { policyRestored: false, aliasRolledBack: false, compensated: false, consistent: true, currentPolicy: previousPolicy };
|
|
479
|
+
let restored;
|
|
480
|
+
try {
|
|
481
|
+
restored = control.restorePolicy(prior, input.approvalReference, input.at);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
// The restore may have installed `prior` before throwing, so re-read the
|
|
485
|
+
// history and derive the reported state from what is actually current.
|
|
486
|
+
const current = control.policyHistory().at(-1) ?? previousPolicy;
|
|
487
|
+
const installedPrior = canonicalJson(current) === canonicalJson(prior);
|
|
488
|
+
return fail(`routing policy restore failed: ${messageOf(error)}`, {
|
|
489
|
+
policyRestored: installedPrior,
|
|
490
|
+
aliasRolledBack: false,
|
|
491
|
+
compensated: false,
|
|
492
|
+
// Consistent only when nothing changed: any other installed policy (prior or
|
|
493
|
+
// something else entirely) leaves policy and alias out of step.
|
|
494
|
+
consistent: canonicalJson(current) === canonicalJson(previousPolicy),
|
|
495
|
+
currentPolicy: current,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (!installed(prior, restored)) {
|
|
499
|
+
return fail('routing policy restore did not install the prior pinned policy', { ...unchanged, consistent: false, currentPolicy: control.policyHistory().at(-1) ?? previousPolicy });
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
rollbackEvent = rollbackChampionForNewRuns({
|
|
503
|
+
record: input.championChallenger, gateway: input.gateway, approvalReference: input.approvalReference, at: input.at,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
// The alias did not move, so put the routing policy back where it was.
|
|
508
|
+
let compensated = false;
|
|
509
|
+
try {
|
|
510
|
+
compensated = installed(previousPolicy, control.restorePolicy(previousPolicy, input.approvalReference, input.at));
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
compensated = false;
|
|
514
|
+
}
|
|
515
|
+
return fail(`D17 alias rollback refused: ${messageOf(error)}`, compensated
|
|
516
|
+
? { ...unchanged, compensated: true }
|
|
517
|
+
: { policyRestored: true, aliasRolledBack: false, compensated: false, consistent: false, currentPolicy: prior });
|
|
518
|
+
}
|
|
519
|
+
restoredPolicy = restored;
|
|
520
|
+
},
|
|
521
|
+
alert: async () => undefined,
|
|
522
|
+
'route-to-review': contain('route-to-review'),
|
|
523
|
+
'reduce-coverage': contain('reduce-coverage'),
|
|
524
|
+
'disable-challenger': contain('disable-challenger'),
|
|
525
|
+
'require-recertification': contain('require-recertification'),
|
|
526
|
+
});
|
|
527
|
+
const after = control.activeRunPins();
|
|
528
|
+
if (canonicalJson(after) !== canonicalJson(before)) {
|
|
529
|
+
return fail('active run pins changed during the drift response', {
|
|
530
|
+
policyRestored: restoredPolicy !== null,
|
|
531
|
+
aliasRolledBack: rollbackEvent !== null,
|
|
532
|
+
compensated: false,
|
|
533
|
+
consistent: false,
|
|
534
|
+
currentPolicy: control.policyHistory().at(-1) ?? previousPolicy,
|
|
535
|
+
activeRunPinsBefore: [...before],
|
|
536
|
+
activeRunPinsAfter: frozenRoutingClone([...after]),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
return { driftResponse: drift.executed, previousPolicy, restoredPolicy, rollbackEvent, jevCircuitOpen, activeRunPins: structuredClone([...after]) };
|
|
540
|
+
}
|
|
541
|
+
function messageOf(error) { return error instanceof Error ? error.message : 'unknown error'; }
|
|
542
|
+
function notEvaluated(reason) {
|
|
543
|
+
return { status: 'not-evaluated', reason, routeId: null, ranking: [] };
|
|
544
|
+
}
|
|
545
|
+
function safeTaskId(value) {
|
|
546
|
+
const id = value && typeof value === 'object' ? value.id : undefined;
|
|
547
|
+
return typeof id === 'string' && NAME.test(id) ? id : null;
|
|
548
|
+
}
|
|
549
|
+
function candidatePins(policy) {
|
|
550
|
+
return [...policy.candidates].sort((a, b) => compareRoutingKeys(a.id, b.id)).map(candidate => ({
|
|
551
|
+
routeId: candidate.id,
|
|
552
|
+
binding: structuredClone(candidate.binding),
|
|
553
|
+
model: structuredClone(candidate.model),
|
|
554
|
+
subagent: candidate.subagent ? structuredClone(candidate.subagent) : null,
|
|
555
|
+
health: candidate.operations.health,
|
|
556
|
+
priceCatalogVersion: candidate.operations.priceCatalogVersion,
|
|
557
|
+
latencyMsP95: candidate.operations.latencyMsP95,
|
|
558
|
+
costMicrosPerAttempt: candidate.operations.costMicrosPerAttempt,
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
const FILESYSTEM_RANK = { none: 0, read: 1, write: 2 };
|
|
562
|
+
/** Hard constraints. The authorization ceiling is two-sided: a route must hold what the task needs
|
|
563
|
+
* and nothing its ordinary authorization does not already allow, so routing can never widen it. */
|
|
564
|
+
function exclusionReasons(candidate, task, policy) {
|
|
565
|
+
const reasons = [];
|
|
566
|
+
const need = task.requirements;
|
|
567
|
+
const held = candidate.permissions;
|
|
568
|
+
const allowed = need.authorized;
|
|
569
|
+
if (!candidate.policy.privacy.includes(need.privacy))
|
|
570
|
+
reasons.push('privacy-denied');
|
|
571
|
+
if (need.authorizationScopes.some(scope => !candidate.policy.authorizationScopes.includes(scope)))
|
|
572
|
+
reasons.push('authorization-denied');
|
|
573
|
+
if (!candidate.policy.regions.includes(need.region))
|
|
574
|
+
reasons.push('region-denied');
|
|
575
|
+
if (need.tools.some(tool => !held.tools.includes(tool)))
|
|
576
|
+
reasons.push('tool-denied');
|
|
577
|
+
if (candidate.policy.contextBytes < need.contextBytes)
|
|
578
|
+
reasons.push('context-denied');
|
|
579
|
+
if (!candidate.policy.allowlisted || !need.allowlist.includes(candidate.id))
|
|
580
|
+
reasons.push('allowlist-denied');
|
|
581
|
+
if (candidate.operations.costMicrosPerAttempt === null || candidate.operations.costMicrosPerAttempt > Math.min(need.maxCostMicros, policy.ceilings.maxCostMicros))
|
|
582
|
+
reasons.push('budget-denied');
|
|
583
|
+
if (candidate.operations.deadlineMs > Math.min(need.deadlineMs, policy.ceilings.deadlineMs))
|
|
584
|
+
reasons.push('deadline-denied');
|
|
585
|
+
if (['outage', 'rate-limited', 'circuit-open'].includes(candidate.operations.health))
|
|
586
|
+
reasons.push('health-denied');
|
|
587
|
+
if (!candidate.policy.executable)
|
|
588
|
+
reasons.push('not-executable');
|
|
589
|
+
if (need.capabilities.some(capability => !candidate.capabilities.includes(capability)))
|
|
590
|
+
reasons.push('capability-denied');
|
|
591
|
+
if (held.tools.some(tool => !allowed.tools.includes(tool)) || (held.network && !allowed.network)
|
|
592
|
+
|| FILESYSTEM_RANK[held.filesystem] > FILESYSTEM_RANK[allowed.filesystem]
|
|
593
|
+
|| held.secrets.some(secret => !allowed.secrets.includes(secret))
|
|
594
|
+
|| held.actions.some(action => !allowed.actions.includes(action)))
|
|
595
|
+
reasons.push('permission-denied');
|
|
596
|
+
if (!need.providers.includes(candidate.model.provider))
|
|
597
|
+
reasons.push('provider-denied');
|
|
598
|
+
return reasons;
|
|
599
|
+
}
|
|
600
|
+
function summarize(candidate) {
|
|
601
|
+
return {
|
|
602
|
+
id: candidate.id,
|
|
603
|
+
capabilities: [...candidate.capabilities].sort(),
|
|
604
|
+
provider: candidate.model.provider,
|
|
605
|
+
backend: candidate.model.backend,
|
|
606
|
+
requestedModel: candidate.model.requested,
|
|
607
|
+
pinnedModelVersion: candidate.model.pinnedVersion,
|
|
608
|
+
subagentId: candidate.subagent?.id ?? null,
|
|
609
|
+
health: candidate.operations.health,
|
|
610
|
+
latencyMsP95: candidate.operations.latencyMsP95,
|
|
611
|
+
costMicrosPerAttempt: candidate.operations.costMicrosPerAttempt,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
/** Only called with complete, unique, finite, bounded evidence for every eligible route. */
|
|
615
|
+
function rankRoutes(policy, eligible, evidence) {
|
|
616
|
+
const byRoute = new Map(evidence.distributions.map(item => [item.routeId, item]));
|
|
617
|
+
const scored = eligible.map(item => ({ item, score: utility(policy, item.candidate, byRoute.get(item.candidate.id), evidence) }));
|
|
618
|
+
return scored.sort((a, b) => b.score - a.score || compareRoutingKeys(a.item.candidate.id, b.item.candidate.id)).map(entry => entry.item);
|
|
619
|
+
}
|
|
620
|
+
function utility(policy, candidate, evidence, all) {
|
|
621
|
+
const healthPenalty = candidate.operations.health === 'degraded' ? 1 : 0;
|
|
622
|
+
return evidence.taskFit * policy.utility.taskFit
|
|
623
|
+
+ (1 - Math.abs(all.taskComplexity - evidence.reasoningNeed)) * policy.utility.complexityFit
|
|
624
|
+
- all.ambiguity * policy.utility.ambiguityPenalty
|
|
625
|
+
- candidate.operations.latencyMsP95 * policy.utility.latencyPenalty
|
|
626
|
+
- candidate.operations.costMicrosPerAttempt * policy.utility.costPenalty
|
|
627
|
+
- healthPenalty * policy.utility.healthPenalty;
|
|
628
|
+
}
|
|
629
|
+
function validDispatchResult(value) {
|
|
630
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
631
|
+
return false;
|
|
632
|
+
try {
|
|
633
|
+
admitEntry(value);
|
|
634
|
+
}
|
|
635
|
+
catch {
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
const record = value;
|
|
639
|
+
if (canonicalJson(Object.keys(record).sort()) !== canonicalJson(RESULT_KEYS))
|
|
640
|
+
return false;
|
|
641
|
+
const text = (item) => item === null || (typeof item === 'string' && NAME.test(item));
|
|
642
|
+
const count = (item) => item === null || (typeof item === 'number' && Number.isSafeInteger(item) && item >= 0);
|
|
643
|
+
const statusOk = (record.status === 'success' && record.reason === 'none')
|
|
644
|
+
|| (record.status === 'failure' && typeof record.reason === 'string' && DISPATCH_FAILURES.has(record.reason));
|
|
645
|
+
return statusOk && [record.actualProvider, record.actualModel, record.workerId, record.outcomeLabel].every(text)
|
|
646
|
+
&& [record.inputTokens, record.outputTokens, record.costMicros, record.latencyMs].every(count)
|
|
647
|
+
&& (record.verified === null || typeof record.verified === 'boolean');
|
|
648
|
+
}
|
|
649
|
+
function failedDispatch(reason) {
|
|
650
|
+
return { status: 'failure', reason, actualProvider: null, actualModel: null, workerId: null,
|
|
651
|
+
inputTokens: null, outputTokens: null, costMicros: null, latencyMs: null, verified: null, outcomeLabel: null };
|
|
652
|
+
}
|
|
653
|
+
function attemptReceipt(ordinal, candidate, result, reason) {
|
|
654
|
+
return {
|
|
655
|
+
ordinal,
|
|
656
|
+
routeId: candidate.id,
|
|
657
|
+
status: result.status === 'success' ? 'succeeded' : 'failed',
|
|
658
|
+
reason,
|
|
659
|
+
actualProvider: result.actualProvider,
|
|
660
|
+
actualModel: result.actualModel,
|
|
661
|
+
workerId: result.workerId,
|
|
662
|
+
usage: { inputTokens: result.inputTokens, outputTokens: result.outputTokens, costMicros: result.costMicros },
|
|
663
|
+
latencyMs: result.latencyMs,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function nullUsage() { return { inputTokens: null, outputTokens: null, costMicros: null }; }
|
|
667
|
+
function aggregateUsage(attempts) {
|
|
668
|
+
const fields = ['inputTokens', 'outputTokens', 'costMicros'];
|
|
669
|
+
return Object.fromEntries(fields.map(field => [field, attempts.every(attempt => attempt.usage[field] !== null)
|
|
670
|
+
? attempts.reduce((sum, attempt) => sum + attempt.usage[field], 0) : null]));
|
|
671
|
+
}
|
|
672
|
+
function realTimer(ms, signal) {
|
|
673
|
+
return new Promise((resolve, reject) => {
|
|
674
|
+
if (signal.aborted) {
|
|
675
|
+
reject(new Error('cancelled'));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const timeout = setTimeout(resolve, ms);
|
|
679
|
+
signal.addEventListener('abort', () => {
|
|
680
|
+
clearTimeout(timeout);
|
|
681
|
+
reject(new Error('cancelled'));
|
|
682
|
+
}, { once: true });
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
//# sourceMappingURL=runtime.js.map
|