@evoclock/pi-agentic-driver 0.9.0 → 0.9.2
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-driver/router.defaults.json +64 -0
- package/LICENSE +11 -8
- package/README.md +94 -29
- package/extensions/attended-authority-guard.ts +1 -1
- package/extensions/code-phage.js +1 -1
- package/extensions/herdr-communication.ts +9 -2
- package/extensions/herdr-dispatch.ts +1 -1
- package/extensions/herdr-lifecycle.ts +5 -4
- package/extensions/linux-microvm.ts +1 -1
- package/extensions/pulse.ts +105 -0
- package/extensions/task-board.ts +1 -1
- package/lib/adapters/diff-scope.mjs +1 -1
- package/lib/adapters/evidence.mjs +1 -1
- package/lib/adapters/narrative.mjs +1 -1
- package/lib/adapters/review-feedback.mjs +1 -1
- package/lib/adapters/visualization.mjs +1 -1
- package/lib/code-phage-core.mjs +1 -1
- package/lib/python_ast_metrics.py +1 -1
- package/lib/typescript_ast_metrics.mjs +1 -1
- package/package.json +17 -3
- package/scripts/aidr_writing_review.js +1 -1
- package/scripts/enforcement/attended_authority_guard.js +1 -1
- package/scripts/enforcement/guest_containment_taxonomy.v1.json +1 -1
- package/scripts/enforcement/herdr_async_dispatch_pi.js +66 -4
- package/scripts/enforcement/herdr_communication_pi.js +55 -19
- package/scripts/enforcement/herdr_lifecycle_pi.js +3 -3
- package/scripts/enforcement/linux_microvm_cutover_pi.js +1 -1
- package/scripts/enforcement/native_tui_context.js +1 -1
- package/scripts/enforcement/pulse_core_pi.js +292 -0
- package/scripts/enforcement/pulse_scheduler_pi.js +1083 -0
- package/scripts/enforcement/router_engine_pi.js +483 -0
- package/scripts/enforcement/router_ranking_pi.js +146 -0
- package/scripts/enforcement/router_replacement_pi.js +98 -0
- package/scripts/enforcement/router_runtime_pi.js +299 -0
- package/scripts/enforcement/router_schemas_pi.js +604 -0
- package/scripts/enforcement/router_store_pi.js +438 -0
- package/scripts/enforcement/task_board_core_pi.js +597 -48
- package/skills/pulse/SKILL.md +39 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
// Route-decision engine (ROUTER_DESIGN_TASK68 §6): deterministic, ordered
|
|
5
|
+
// eligibility evaluation over validated seats, the 40% reserve rule in final
|
|
6
|
+
// form, the consumption estimator, and the immutable route-decision record.
|
|
7
|
+
//
|
|
8
|
+
// Pure and side-effect-free, like pulse_core_pi.js: it consumes validated
|
|
9
|
+
// config, observations, and samples; it never reads files, never touches the
|
|
10
|
+
// board, and never grants authority. The digest of the emitted record is the
|
|
11
|
+
// routeDecisionDigest carried into envelopes and the Jev evalInputDigest.
|
|
12
|
+
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
14
|
+
import { canonicalJsonString, sha256Hex } from "./router_schemas_pi.js";
|
|
15
|
+
import {
|
|
16
|
+
SEAT_KINDS, CAPABILITIES, CONTAINMENT_TIERS, PHASES, QUOTA_UNITS,
|
|
17
|
+
ELIGIBILITY_RULES, computeConfigDigest, validateRouterConfig, wellFormedQuotaObservation,
|
|
18
|
+
} from "./router_schemas_pi.js";
|
|
19
|
+
import { rankOrFallback } from "./router_ranking_pi.js";
|
|
20
|
+
export { canonicalJsonString, sha256Hex };
|
|
21
|
+
|
|
22
|
+
export const ROUTE_DECISION_SCHEMA = "agentic-driver.route-decision.v1";
|
|
23
|
+
export const AUTOMATION_POLICY_VERSION = "agentic-driver.automation-policy.v1";
|
|
24
|
+
|
|
25
|
+
const HEX64_RE = /^[0-9a-f]{64}$/;
|
|
26
|
+
|
|
27
|
+
function isPlainObject(value) {
|
|
28
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Consumption estimator (§6.2): per seat and unit, the mean of the last
|
|
33
|
+
// N completed dispatches on that seat, outliers beyond sigma excluded,
|
|
34
|
+
// minimum minSamples samples; below minSamples the cold-start conservative
|
|
35
|
+
// constant = coldStartFraction × the smallest window total on that account.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
export function estimateConsumption({ seatId, unit, samples = [], reserve, accountWindows = [] }) {
|
|
39
|
+
if (!QUOTA_UNITS.includes(unit)) {
|
|
40
|
+
return { estimate: null, source: "unknown-unit", sampleCount: 0 };
|
|
41
|
+
}
|
|
42
|
+
const unitWindows = (Array.isArray(accountWindows) ? accountWindows : [])
|
|
43
|
+
.filter((window) => window?.unit === unit && Number.isFinite(window?.total));
|
|
44
|
+
const N = reserve?.estimateSamples ?? 20;
|
|
45
|
+
const minSamples = reserve?.estimateMinSamples ?? 5;
|
|
46
|
+
const sigma = reserve?.estimateOutlierSigma ?? 3;
|
|
47
|
+
const coldStartFraction = reserve?.coldStartFraction ?? 0.25;
|
|
48
|
+
|
|
49
|
+
const relevant = (Array.isArray(samples) ? samples : [])
|
|
50
|
+
.map((s, index) => ({ ...s, index }))
|
|
51
|
+
.filter((s) => s?.seatId === seatId && s?.unit === unit && Number.isFinite(s?.quantity) && s.quantity >= 0)
|
|
52
|
+
.sort((a, b) => {
|
|
53
|
+
const at = Date.parse(a.capturedAt), bt = Date.parse(b.capturedAt);
|
|
54
|
+
return Number.isFinite(at) && Number.isFinite(bt) ? at - bt || a.index - b.index : a.index - b.index;
|
|
55
|
+
})
|
|
56
|
+
.slice(-N).map((s) => s.quantity);
|
|
57
|
+
|
|
58
|
+
if (relevant.length >= minSamples) {
|
|
59
|
+
const mean = relevant.reduce((a, b) => a + b, 0) / relevant.length;
|
|
60
|
+
const variance = relevant.reduce((a, b) => a + (b - mean) ** 2, 0) / relevant.length;
|
|
61
|
+
const sd = Math.sqrt(variance);
|
|
62
|
+
const kept = relevant.filter((q) => sd === 0 || Math.abs(q - mean) <= sigma * sd);
|
|
63
|
+
// After exclusion there are always >= minSamples kept when sd === 0
|
|
64
|
+
// (all values identical); otherwise the kept set can shrink below the
|
|
65
|
+
// minimum — fall through to cold-start only if it does.
|
|
66
|
+
if (kept.length >= minSamples) {
|
|
67
|
+
// Conservative rounding: usage rounds UP (§6.2).
|
|
68
|
+
return { estimate: Math.ceil((kept.reduce((a, b) => a + b, 0) / kept.length) * 1e6) / 1e6, source: "estimated", sampleCount: kept.length };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Cold start: 25% (configurable) of the smallest window total on the
|
|
72
|
+
// account, for this unit. No window on this unit → the seat has no quota
|
|
73
|
+
// accounting for this unit and consumes nothing (quantity 0).
|
|
74
|
+
const totals = unitWindows.map((w) => w.total);
|
|
75
|
+
if (totals.length === 0) return { estimate: 0, source: "cold-start", sampleCount: 0 };
|
|
76
|
+
const smallest = Math.min(...totals);
|
|
77
|
+
// Conservative rounding: usage rounds UP (§6.2).
|
|
78
|
+
return { estimate: Math.ceil(smallest * coldStartFraction * 1e6) / 1e6, source: "cold-start", sampleCount: 0 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// The 40% reserve rule (§6.2, final form). A subscription seat is ineligible
|
|
83
|
+
// when the projected post-dispatch remaining capacity of ANY active quota
|
|
84
|
+
// window on its account would fall below floorPercent of that window's total.
|
|
85
|
+
// All active windows must pass. Per-unit consumption vectors; quantity-
|
|
86
|
+
// bearing reservations; remaining rounds down, usage rounds up; unknown
|
|
87
|
+
// capacity = ineligible (fail closed).
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
// Active window = one whose resetAt is in the future (or null/unbounded) at
|
|
91
|
+
// evaluation time. A window about to reset does not borrow headroom from the
|
|
92
|
+
// next window: it is evaluated on its own current totals.
|
|
93
|
+
export function activeWindows(windows, now) {
|
|
94
|
+
const at = typeof now === "string" ? Date.parse(now) : now;
|
|
95
|
+
return (Array.isArray(windows) ? windows : []).filter((w) => {
|
|
96
|
+
if (w?.resetAt === null || w?.resetAt === undefined) return true;
|
|
97
|
+
const reset = Date.parse(w.resetAt);
|
|
98
|
+
return Number.isFinite(reset) && reset > at;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Aggregate account-level views: sum windows of the same unit and overlapping
|
|
103
|
+
// reset windows across the account's seats' observations (§4). Returns a map
|
|
104
|
+
// keyed by `${unit}\u0000${resetAt ?? ""}` → {unit, total, used, remaining, resetAt}.
|
|
105
|
+
export function aggregateAccountWindows(observations, now) {
|
|
106
|
+
const at = typeof now === "string" ? Date.parse(now) : now;
|
|
107
|
+
const buckets = new Map();
|
|
108
|
+
for (const obs of Array.isArray(observations) ? observations : []) {
|
|
109
|
+
if (!isPlainObject(obs) || obs.sourceStatus !== "ok") continue;
|
|
110
|
+
if (Number.isFinite(at) && Date.parse(obs?.expiresAt) <= at) continue; // stale at read time
|
|
111
|
+
for (const window of Array.isArray(obs.quotaWindows) ? obs.quotaWindows : []) {
|
|
112
|
+
if (window?.resetAt !== null && window?.resetAt !== undefined) {
|
|
113
|
+
const reset = Date.parse(window.resetAt);
|
|
114
|
+
if (!Number.isFinite(reset) || reset <= at) continue; // not active
|
|
115
|
+
}
|
|
116
|
+
const key = `${window.unit}\u0000${window.resetAt ?? ""}`;
|
|
117
|
+
const current = buckets.get(key) ?? {
|
|
118
|
+
windowId: `${obs.accountId}:${window.unit}:${window.resetAt ?? "unbounded"}`,
|
|
119
|
+
unit: window.unit, resetAt: window.resetAt ?? null,
|
|
120
|
+
total: 0, used: 0, remaining: 0,
|
|
121
|
+
};
|
|
122
|
+
// Conservative rounding: remaining rounds DOWN, usage rounds UP.
|
|
123
|
+
current.total += window.total;
|
|
124
|
+
current.used += Math.ceil(window.used);
|
|
125
|
+
current.remaining += Math.floor(window.remaining);
|
|
126
|
+
buckets.set(key, current);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...buckets.values()];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Quantity-bearing reservation vectors for one candidate dispatch: one entry
|
|
133
|
+
// per active window unit on the account, with the estimator's per-unit
|
|
134
|
+
// quantity. Returns {vectors, sources} or {error} when capacity is unknown.
|
|
135
|
+
export function reservationVectors({ accountId, observations, seatId, reserve, samples = [], now }) {
|
|
136
|
+
const accountObs = (Array.isArray(observations) ? observations : [])
|
|
137
|
+
.filter((obs) => obs?.accountId === accountId);
|
|
138
|
+
if (accountObs.length === 0) {
|
|
139
|
+
return { error: "unknown capacity: no fresh ok observation for this account (fail closed)" };
|
|
140
|
+
}
|
|
141
|
+
const fresh = accountObs.filter((obs) => obs.sourceStatus === "ok");
|
|
142
|
+
if (fresh.length === 0) {
|
|
143
|
+
return { error: "unknown capacity: account observations report collector failure (fail closed)" };
|
|
144
|
+
}
|
|
145
|
+
const account = aggregateAccountWindows(accountObs, now);
|
|
146
|
+
if (account.length === 0) {
|
|
147
|
+
return { error: "unknown capacity: no active quota windows on this account (fail closed)" };
|
|
148
|
+
}
|
|
149
|
+
const vectors = [];
|
|
150
|
+
const sources = [];
|
|
151
|
+
for (const window of account) {
|
|
152
|
+
const estimate = estimateConsumption({ seatId, unit: window.unit, samples, reserve, accountWindows: account });
|
|
153
|
+
if (!Number.isFinite(estimate.estimate)) return { error: `unknown touched quota unit "${window.unit}" (fail closed)` };
|
|
154
|
+
vectors.push({
|
|
155
|
+
accountId, windowId: window.windowId, unit: window.unit, resetAt: window.resetAt,
|
|
156
|
+
quantity: estimate.estimate, windowTotal: window.total, windowRemaining: window.remaining,
|
|
157
|
+
});
|
|
158
|
+
sources.push({ unit: window.unit, source: estimate.source, sampleCount: estimate.sampleCount });
|
|
159
|
+
}
|
|
160
|
+
return { vectors, sources };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Evaluate the reserve rule for one seat. Returns {ok, reason, vectors} —
|
|
164
|
+
// vectors are the quantity-bearing reservation contents when the rule passes.
|
|
165
|
+
export function evaluateReserveRule({ seat, observations, samples = [], reserve, now, existingReservations = [], ownerInteractive = false }) {
|
|
166
|
+
if (seat.kind !== "subscription") return { ok: true, reason: null, vectors: [] };
|
|
167
|
+
if (!isPlainObject(reserve)) return { ok: false, reason: "reserve configuration is missing (fail closed)", vectors: [] };
|
|
168
|
+
const floorPercent = reserve.floorPercent ?? 40;
|
|
169
|
+
const override = ownerInteractive === true && reserve.ownerInteractiveOverride === true;
|
|
170
|
+
|
|
171
|
+
const accountId = seat.accountId;
|
|
172
|
+
if (typeof accountId !== "string" || accountId === "") {
|
|
173
|
+
return { ok: false, reason: "subscription seat has no accountId (fail closed)", vectors: [] };
|
|
174
|
+
}
|
|
175
|
+
const projection = reservationVectors({ accountId, observations, seatId: seat.seatId, reserve, samples, now });
|
|
176
|
+
if (projection.error) {
|
|
177
|
+
return { ok: false, reason: projection.error, vectors: [] };
|
|
178
|
+
}
|
|
179
|
+
// Prior unclaimed reservations on the same account consume headroom too.
|
|
180
|
+
const outstanding = new Map();
|
|
181
|
+
for (const res of Array.isArray(existingReservations) ? existingReservations : []) {
|
|
182
|
+
if (res?.accountId !== accountId) continue;
|
|
183
|
+
if (res?.state !== "reserved" && res?.state !== "claimed") continue;
|
|
184
|
+
const key = `${res.unit}\u0000${res.resetAt ?? ""}`;
|
|
185
|
+
outstanding.set(key, (outstanding.get(key) ?? 0) + (Number.isFinite(res?.quantity) ? res.quantity : 0));
|
|
186
|
+
}
|
|
187
|
+
for (const vector of projection.vectors) {
|
|
188
|
+
const key = `${vector.unit}\u0000${vector.resetAt ?? ""}`;
|
|
189
|
+
const prior = outstanding.get(key) ?? 0;
|
|
190
|
+
// Conservative rounding: remaining rounds DOWN, usage rounds UP.
|
|
191
|
+
const projectedRemaining = Math.floor(vector.windowRemaining) - Math.ceil(vector.quantity) - Math.ceil(prior);
|
|
192
|
+
const floor = (vector.windowTotal * floorPercent) / 100;
|
|
193
|
+
if (projectedRemaining < floor) {
|
|
194
|
+
if (!override) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
reason: `reserve floor: window ${vector.unit} (reset ${vector.resetAt ?? "none"}) would fall to ${projectedRemaining} < ${floorPercent}% of ${vector.windowTotal}`,
|
|
198
|
+
vectors: [],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { ok: true, reason: null, vectors: projection.vectors, sources: projection.sources };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Deterministic eligibility (§6.1): eight ordered rules; first failing rule
|
|
208
|
+
// excludes the seat. Inputs are validated/normalized by the caller.
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
// Evaluate all eight rules for one seat against one card. `inputs` carries:
|
|
212
|
+
// seat, card, policyRouteSeatIds, healthObservations, quotaObservations,
|
|
213
|
+
// consumptionSamples, reserve, now, existingReservations, activeSessions,
|
|
214
|
+
// providerLimit, modelInstalled (Set of installed model ids), ownerInteractive
|
|
215
|
+
export function evaluateSeatEligibility(inputs) {
|
|
216
|
+
const {
|
|
217
|
+
seat, card, policyRouteSeatIds = [], healthObservations = {},
|
|
218
|
+
quotaObservations = [], consumptionSamples = [], reserve = null,
|
|
219
|
+
now, existingReservations = [], activeSessions = [], providerLimit = null,
|
|
220
|
+
modelInstalled = null, ownerInteractive = false,
|
|
221
|
+
} = inputs;
|
|
222
|
+
const exclusions = [];
|
|
223
|
+
|
|
224
|
+
// Rule 1: seat.enabled === true (config).
|
|
225
|
+
if (seat.enabled !== true) {
|
|
226
|
+
exclusions.push({ rule: ELIGIBILITY_RULES[0], reason: "seat is disabled in config" });
|
|
227
|
+
return { eligible: false, exclusions };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Rule 2: seat referenced by the role's board-policy route list.
|
|
231
|
+
const role = typeof card?.role === "string" && card.role !== "" ? card.role : "implementer";
|
|
232
|
+
if (!policyRouteSeatIds.includes(seat.seatId)) {
|
|
233
|
+
exclusions.push({ rule: ELIGIBILITY_RULES[1], reason: `seat "${seat.seatId}" is not referenced by the board-policy route list for role "${role}"` });
|
|
234
|
+
return { eligible: false, exclusions };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Rule 3: card capabilities ⊆ seat capabilities (exact tag match at launch).
|
|
238
|
+
const cardCaps = Array.isArray(card?.capabilities) ? card.capabilities : [];
|
|
239
|
+
const seatCaps = new Set(Array.isArray(seat.capabilities) ? seat.capabilities : []);
|
|
240
|
+
const missing = cardCaps.filter((c) => !seatCaps.has(c));
|
|
241
|
+
if (missing.length > 0) {
|
|
242
|
+
exclusions.push({ rule: ELIGIBILITY_RULES[2], reason: `seat lacks required capabilities: ${missing.join(", ")}` });
|
|
243
|
+
return { eligible: false, exclusions };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Rule 4: containment tier compatibility. Tier 1 work requires testudo;
|
|
247
|
+
// docker-policy and none seats are ineligible for containment-required cards.
|
|
248
|
+
const requiresContainment = card?.placement === "container" || card?.containmentRequired === true;
|
|
249
|
+
if (requiresContainment && seat.containmentTier !== "testudo") {
|
|
250
|
+
exclusions.push({ rule: ELIGIBILITY_RULES[3], reason: `card requires containment; seat containmentTier is "${seat.containmentTier}"` });
|
|
251
|
+
return { eligible: false, exclusions };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Rule 5: endpoint health. Local seats require a fresh ok health
|
|
255
|
+
// observation; hosted/subscription seats require registry resolution
|
|
256
|
+
// (which the validated config already provides — the endpointRef resolved
|
|
257
|
+
// at config-validation time).
|
|
258
|
+
if (seat.kind === "local") {
|
|
259
|
+
const health = isPlainObject(healthObservations) ? healthObservations[seat.endpointRef] : undefined;
|
|
260
|
+
const fresh = isPlainObject(health) && health.sourceStatus === "ok"
|
|
261
|
+
&& Number.isFinite(Date.parse(health?.expiresAt))
|
|
262
|
+
&& (typeof now === "string" ? Date.parse(now) : now) < Date.parse(health.expiresAt);
|
|
263
|
+
if (!fresh) {
|
|
264
|
+
exclusions.push({ rule: ELIGIBILITY_RULES[4], reason: `endpoint "${seat.endpointRef}" has no fresh ok health observation` });
|
|
265
|
+
return { eligible: false, exclusions };
|
|
266
|
+
}
|
|
267
|
+
// Concurrency headroom on the local seat.
|
|
268
|
+
const active = (Array.isArray(activeSessions) ? activeSessions : [])
|
|
269
|
+
.filter((s) => s?.seatId === seat.seatId).length;
|
|
270
|
+
if (active >= seat.maxConcurrency) {
|
|
271
|
+
exclusions.push({ rule: ELIGIBILITY_RULES[4], reason: `local seat concurrency full (${active}/${seat.maxConcurrency})` });
|
|
272
|
+
return { eligible: false, exclusions };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Rule 6: model installed — the lifecycle's installed-model check. The
|
|
277
|
+
// caller supplies the trusted installed-model set; a null set fails closed.
|
|
278
|
+
if (modelInstalled === null || !(modelInstalled instanceof Set) || !modelInstalled.has(seat.model)) {
|
|
279
|
+
exclusions.push({ rule: ELIGIBILITY_RULES[5], reason: `model "${seat.model}" is not in the installed-model set` });
|
|
280
|
+
return { eligible: false, exclusions };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Rule 7: quota eligibility. Subscription seats: every active account
|
|
284
|
+
// window passes the 40% reserve projection. Local seats: concurrency
|
|
285
|
+
// headroom only (checked under rule 5 for the endpoint).
|
|
286
|
+
if (seat.kind === "subscription") {
|
|
287
|
+
const reserveResult = evaluateReserveRule({
|
|
288
|
+
seat, observations: quotaObservations, samples: consumptionSamples,
|
|
289
|
+
reserve, now, existingReservations, ownerInteractive,
|
|
290
|
+
});
|
|
291
|
+
if (!reserveResult.ok) {
|
|
292
|
+
exclusions.push({ rule: ELIGIBILITY_RULES[6], reason: reserveResult.reason });
|
|
293
|
+
return { eligible: false, exclusions };
|
|
294
|
+
}
|
|
295
|
+
var reservationVectors = reserveResult.vectors;
|
|
296
|
+
} else {
|
|
297
|
+
var reservationVectors = [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Rule 8: global capacity — composed free capacity > 0 under the corrected
|
|
301
|
+
// freeCapacity(). The caller supplies the composed number (it owns the
|
|
302
|
+
// pulse policy); the engine only applies the > 0 predicate.
|
|
303
|
+
if (!Number.isInteger(providerLimit) || providerLimit <= 0) {
|
|
304
|
+
// providerLimit here is the composed free-capacity count passed by the
|
|
305
|
+
// caller (renamed at the call boundary); 0 or absent fails closed.
|
|
306
|
+
exclusions.push({ rule: ELIGIBILITY_RULES[7], reason: "no free global capacity on the seat's route" });
|
|
307
|
+
return { eligible: false, exclusions };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { eligible: true, exclusions, reservationVectors };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Route decision (§6.3): one immutable record per (card, snapshot).
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
export function routeDecisionRecord({
|
|
318
|
+
decisionId = null, createdAt, cardHash, specHash, snapshotDigest, configDigest,
|
|
319
|
+
eligibleSeats, rankedOrder, selectedSeatId, ranking = null, configRevision,
|
|
320
|
+
reservationId = null, phase = "implement", parentDecisionId = null,
|
|
321
|
+
}) {
|
|
322
|
+
const record = {
|
|
323
|
+
schema: ROUTE_DECISION_SCHEMA,
|
|
324
|
+
decisionId: decisionId ?? randomUUID(),
|
|
325
|
+
createdAt,
|
|
326
|
+
cardHash,
|
|
327
|
+
specHash,
|
|
328
|
+
snapshotDigest,
|
|
329
|
+
configDigest,
|
|
330
|
+
eligibleSeats,
|
|
331
|
+
rankedOrder,
|
|
332
|
+
selectedSeatId,
|
|
333
|
+
ranking: ranking ?? { used: false, requestId: null, digest: null },
|
|
334
|
+
policyVersion: AUTOMATION_POLICY_VERSION,
|
|
335
|
+
configRevision,
|
|
336
|
+
reservationId,
|
|
337
|
+
phase: PHASES.includes(phase) ? phase : "implement",
|
|
338
|
+
parentDecisionId,
|
|
339
|
+
};
|
|
340
|
+
return Object.freeze(record);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function routeDecisionDigest(record) {
|
|
344
|
+
return sha256Hex(canonicalJsonString(record));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Well-formedness check for a route-decision record (closed shape).
|
|
348
|
+
export function wellFormedRouteDecision(record) {
|
|
349
|
+
if (!isPlainObject(record) || record.schema !== ROUTE_DECISION_SCHEMA) return false;
|
|
350
|
+
const keys = Object.keys(record).sort();
|
|
351
|
+
const expected = [
|
|
352
|
+
"schema", "decisionId", "createdAt", "cardHash", "specHash", "snapshotDigest",
|
|
353
|
+
"configDigest", "eligibleSeats", "rankedOrder", "selectedSeatId", "ranking",
|
|
354
|
+
"policyVersion", "configRevision", "reservationId", "phase", "parentDecisionId",
|
|
355
|
+
].sort();
|
|
356
|
+
if (keys.length !== expected.length || keys.some((k, i) => k !== expected[i])) return false;
|
|
357
|
+
if (typeof record.decisionId !== "string" || record.decisionId === "") return false;
|
|
358
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/.test(record.createdAt)) return false;
|
|
359
|
+
for (const field of ["cardHash", "specHash", "snapshotDigest", "configDigest"]) {
|
|
360
|
+
if (!HEX64_RE.test(record[field] ?? "")) return false;
|
|
361
|
+
}
|
|
362
|
+
if (!Array.isArray(record.eligibleSeats)) return false;
|
|
363
|
+
for (const entry of record.eligibleSeats) {
|
|
364
|
+
if (!isPlainObject(entry) || !Object.hasOwn(entry, "seatId") || !Object.hasOwn(entry, "excluded") || !Object.hasOwn(entry, "reason")) return false;
|
|
365
|
+
if (typeof entry.seatId !== "string" || typeof entry.excluded !== "boolean") return false;
|
|
366
|
+
if (entry.excluded && typeof entry.reason !== "string") return false;
|
|
367
|
+
}
|
|
368
|
+
if (!Array.isArray(record.rankedOrder) || !record.rankedOrder.every((id) => typeof id === "string")) return false;
|
|
369
|
+
if (typeof record.selectedSeatId !== "string" || record.selectedSeatId === "") return false;
|
|
370
|
+
const ranking = record.ranking;
|
|
371
|
+
if (!isPlainObject(ranking) || typeof ranking.used !== "boolean"
|
|
372
|
+
|| (ranking.requestId !== null && typeof ranking.requestId !== "string")
|
|
373
|
+
|| (ranking.digest !== null && !HEX64_RE.test(ranking.digest))) return false;
|
|
374
|
+
if (record.policyVersion !== AUTOMATION_POLICY_VERSION) return false;
|
|
375
|
+
if (!Number.isInteger(record.configRevision)) return false;
|
|
376
|
+
if (record.reservationId !== null && typeof record.reservationId !== "string") return false;
|
|
377
|
+
if (!PHASES.includes(record.phase)) return false;
|
|
378
|
+
if (record.parentDecisionId !== null && typeof record.parentDecisionId !== "string") return false;
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// The full route decision for one card: evaluate every seat in preference
|
|
384
|
+
// order, rank the eligible ones, select, and emit the immutable record.
|
|
385
|
+
// `janusRank` is the injected ranking adapter (async (candidates) =>
|
|
386
|
+
// {order, requestId} | null); null or a failed call falls back to
|
|
387
|
+
// preferences.order deterministically (ranking never blocks dispatch).
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
export async function routeDecision({
|
|
391
|
+
card, config, snapshotDigest, policyRouteSeatIds, healthObservations = {},
|
|
392
|
+
quotaObservations = [], consumptionSamples = [], now,
|
|
393
|
+
existingReservations = [], activeSessions = [], freeCapacityBySeat = {},
|
|
394
|
+
modelInstalled = null, ownerInteractive = false, janusRank = null,
|
|
395
|
+
decisionId = null, parentDecisionId = null, phase = "implement",
|
|
396
|
+
}) {
|
|
397
|
+
const configValidation = validateRouterConfig(config);
|
|
398
|
+
if (!configValidation.ok) {
|
|
399
|
+
return { ok: false, code: "config-invalid", reason: `router config is invalid and refuses to produce decisions: ${configValidation.errors.join("; ")} (fail closed)` };
|
|
400
|
+
}
|
|
401
|
+
const seatsById = new Map(config.seats.map((s) => [s.seatId, s]));
|
|
402
|
+
const validObservations = quotaObservations.every((obs) => {
|
|
403
|
+
if (!wellFormedQuotaObservation(obs).ok) return false;
|
|
404
|
+
const seat = seatsById.get(obs?.seatId);
|
|
405
|
+
const collector = config.collectors?.[obs?.collector];
|
|
406
|
+
return seat && seat.kind === "subscription" && obs.accountId === seat.accountId
|
|
407
|
+
&& obs.collector === seat.quotaCollector && collector?.accounts?.includes(obs.accountId);
|
|
408
|
+
});
|
|
409
|
+
if (!validObservations) return { ok: false, code: "observation-invalid", reason: "quota observation does not match configured seat/account/collector (fail closed)" };
|
|
410
|
+
const reserve = config.eligibility?.reserve ?? null;
|
|
411
|
+
const preferenceOrder = (config.preferences?.order ?? []).map((e) => e.seatId);
|
|
412
|
+
|
|
413
|
+
// Evaluate every seat in the config's declared order (deterministic);
|
|
414
|
+
// collect eligibility per seat with exclusion reasons for the audit list.
|
|
415
|
+
const eligible = [];
|
|
416
|
+
const eligibleSeats = [];
|
|
417
|
+
for (const seat of config.seats) {
|
|
418
|
+
const result = evaluateSeatEligibility({
|
|
419
|
+
seat, card, policyRouteSeatIds, healthObservations, quotaObservations,
|
|
420
|
+
consumptionSamples, reserve, now, existingReservations, activeSessions,
|
|
421
|
+
providerLimit: freeCapacityBySeat[seat.seatId] ?? 0,
|
|
422
|
+
modelInstalled, ownerInteractive,
|
|
423
|
+
});
|
|
424
|
+
eligibleSeats.push({
|
|
425
|
+
seatId: seat.seatId,
|
|
426
|
+
excluded: !result.eligible,
|
|
427
|
+
reason: result.eligible ? null : result.exclusions.map((e) => e.reason).join("; "),
|
|
428
|
+
});
|
|
429
|
+
if (result.eligible) {
|
|
430
|
+
eligible.push({ seat, vectors: result.reservationVectors });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Deterministic candidate ordering: preference order first (only seats in
|
|
435
|
+
// preferences.order, in that order), then remaining eligible seats in
|
|
436
|
+
// config declaration order — never arbitrary.
|
|
437
|
+
const preferred = preferenceOrder.filter((id) => eligible.some((e) => e.seat.seatId === id));
|
|
438
|
+
const rest = eligible.map((e) => e.seat.seatId).filter((id) => !preferred.includes(id));
|
|
439
|
+
const fallbackOrder = [...preferred, ...rest];
|
|
440
|
+
|
|
441
|
+
const quotaBuckets = Object.fromEntries(eligible.map(({ seat, vectors }) => {
|
|
442
|
+
if (seat.kind !== "subscription") return [seat.seatId, "high"];
|
|
443
|
+
const ratio = Math.min(...vectors.map((v) => (v.windowRemaining - v.quantity) / v.windowTotal));
|
|
444
|
+
return [seat.seatId, ratio < 0.5 ? "reserve-zone" : ratio < 0.65 ? "low" : ratio < 0.8 ? "medium" : "high"];
|
|
445
|
+
}));
|
|
446
|
+
const ranked = await rankOrFallback({ config, eligibleSeatIds: fallbackOrder, seatsById, quotaBuckets, janusRank, now });
|
|
447
|
+
const rankedOrder = ranked.order;
|
|
448
|
+
const ranking = { used: ranked.used, requestId: ranked.requestId, digest: ranked.digest };
|
|
449
|
+
|
|
450
|
+
const selectedSeatId = rankedOrder[0] ?? null;
|
|
451
|
+
if (selectedSeatId === null) {
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
code: "no-eligible-seat",
|
|
455
|
+
reason: "no eligible seat for this card under the current snapshot (fail closed)",
|
|
456
|
+
eligibleSeats,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Reservation: subscription seats with quota accounting get a
|
|
461
|
+
// quantity-bearing reservation id minted by the caller's store; the record
|
|
462
|
+
// carries it when present.
|
|
463
|
+
const selected = eligible.find((e) => e.seat.seatId === selectedSeatId);
|
|
464
|
+
const needsReservation = selected.seat.kind === "subscription" && selected.vectors.length > 0;
|
|
465
|
+
|
|
466
|
+
const finalDecisionId = decisionId ?? randomUUID();
|
|
467
|
+
const reservationId = needsReservation
|
|
468
|
+
? `res.${Buffer.from(JSON.stringify({ decisionId: finalDecisionId, seatId: selected.seat.seatId, accountId: selected.seat.accountId, vectors: selected.vectors }), "utf8").toString("base64url")}`
|
|
469
|
+
: null;
|
|
470
|
+
const record = routeDecisionRecord({
|
|
471
|
+
decisionId: finalDecisionId, createdAt: now, cardHash: card.hash ?? null, specHash: card.specHash ?? null,
|
|
472
|
+
snapshotDigest, configDigest: computeConfigDigest(config), eligibleSeats, rankedOrder,
|
|
473
|
+
selectedSeatId, ranking, configRevision: config.revision, reservationId, phase, parentDecisionId,
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
ok: true, record, digest: routeDecisionDigest(record), selectedSeatId,
|
|
477
|
+
reservationId, reservationVectors: selected.vectors, needsReservation,
|
|
478
|
+
accountId: selected.seat.accountId, provider: selected.seat.provider,
|
|
479
|
+
endpointRef: selected.seat.endpointRef, model: selected.seat.model,
|
|
480
|
+
containmentTier: selected.seat.containmentTier,
|
|
481
|
+
effort: ["low", "default", "high"].includes(card?.effort) ? card.effort : "default",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
// Ranking adapter (ROUTER_DESIGN_TASK68 §6.4): the janus HTTP client for
|
|
5
|
+
// route_rank + rank_confidence, with the deterministic preference-order
|
|
6
|
+
// fallback as the default path. Ranking is bounded (max 8 candidates),
|
|
7
|
+
// audit-only in its confidence output, and NEVER blocks dispatch: any janus
|
|
8
|
+
// failure — unavailable, timeout, malformed — falls back to the config's
|
|
9
|
+
// preference order. The fallback path works with janus absent entirely.
|
|
10
|
+
//
|
|
11
|
+
// No network calls happen inside any lock; the adapter is injected into the
|
|
12
|
+
// route-decision engine as `janusRank`.
|
|
13
|
+
|
|
14
|
+
import { canonicalJsonString, sha256Hex } from "./router_schemas_pi.js";
|
|
15
|
+
|
|
16
|
+
export const RANKING_MAX_CANDIDATES = 8;
|
|
17
|
+
export const RANKING_TIMEOUT_MS = 3000;
|
|
18
|
+
|
|
19
|
+
// Build the candidate state per seat (§6.4): no credentials, no endpoint
|
|
20
|
+
// URLs. locality is local | hosted; quota bucket is high | medium | low |
|
|
21
|
+
// reserve-zone (derived from the reserve evaluation, never from model
|
|
22
|
+
// output); phase-suitability tags come from the seat's capability set.
|
|
23
|
+
export function rankingCandidates({ eligibleSeatIds, seatsById, quotaBuckets = {} }) {
|
|
24
|
+
return (Array.isArray(eligibleSeatIds) ? eligibleSeatIds : [])
|
|
25
|
+
.slice(0, RANKING_MAX_CANDIDATES)
|
|
26
|
+
.map((seatId) => {
|
|
27
|
+
const seat = seatsById.get(seatId);
|
|
28
|
+
if (!seat) return null;
|
|
29
|
+
return {
|
|
30
|
+
seatId,
|
|
31
|
+
kind: seat.kind,
|
|
32
|
+
costClass: seat.costClass,
|
|
33
|
+
locality: seat.kind === "local" ? "local" : "hosted",
|
|
34
|
+
quotaBucket: quotaBuckets[seatId] ?? null,
|
|
35
|
+
phaseTags: [...(Array.isArray(seat.capabilities) ? seat.capabilities : [])],
|
|
36
|
+
};
|
|
37
|
+
})
|
|
38
|
+
.filter((c) => c !== null);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function candidateSetDigest(candidates) {
|
|
42
|
+
return sha256Hex(canonicalJsonString(candidates));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The ranking question (§6.4): route_rank (choice over candidate seatIds)
|
|
46
|
+
// plus rank_confidence (score 0..5). rank_confidence is AUDIT-ONLY: it is
|
|
47
|
+
// recorded in the ranking observation and never influences eligibility,
|
|
48
|
+
// ordering, or tie-breaking.
|
|
49
|
+
export function rankingQuestion(candidates) {
|
|
50
|
+
return {
|
|
51
|
+
questions: {
|
|
52
|
+
route_rank: {
|
|
53
|
+
type: "choice",
|
|
54
|
+
options: candidates.map((c) => c.seatId),
|
|
55
|
+
prompt: "Which seat class should take this work?",
|
|
56
|
+
},
|
|
57
|
+
rank_confidence: {
|
|
58
|
+
type: "score",
|
|
59
|
+
min: 0,
|
|
60
|
+
max: 5,
|
|
61
|
+
prompt: "How confident is the route ranking?",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
state: { candidates },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Apply a janus ranking response to the fallback order. Returns the ranked
|
|
69
|
+
// order only when the response is well-formed AND covers exactly the
|
|
70
|
+
// fallback set (no invented seats, none dropped); otherwise null (→
|
|
71
|
+
// deterministic fallback). rank_confidence is parsed for the audit record
|
|
72
|
+
// only.
|
|
73
|
+
export function applyRankingResponse({ response, fallbackOrder }) {
|
|
74
|
+
if (response === null || typeof response !== "object" || Array.isArray(response)) return null;
|
|
75
|
+
const answers = response.answers ?? response;
|
|
76
|
+
const rank = answers?.route_rank;
|
|
77
|
+
if (typeof rank !== "string" || !fallbackOrder.includes(rank)) return null;
|
|
78
|
+
// A single choice reorders deterministically: chosen first, remaining
|
|
79
|
+
// seats keep their deterministic preference order.
|
|
80
|
+
const ranked = [rank, ...fallbackOrder.filter((id) => id !== rank)];
|
|
81
|
+
const confidence = answers?.rank_confidence;
|
|
82
|
+
return {
|
|
83
|
+
order: ranked,
|
|
84
|
+
confidence: Number.isFinite(confidence) && confidence >= 0 && confidence <= 5 ? Number(confidence) : null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The injected adapter surface. `fetchFn` is injectable for tests (no
|
|
89
|
+
// network calls in tests). On ANY failure — fetch throws, non-2xx, timeout,
|
|
90
|
+
// malformed body — this returns null and the caller falls back.
|
|
91
|
+
export function createJanusRankAdapter({ janusUrl, fetchFn = null, timeoutMs = RANKING_TIMEOUT_MS } = {}) {
|
|
92
|
+
if (typeof janusUrl !== "string" || janusUrl === "") {
|
|
93
|
+
// No janus configured: the fallback IS the path.
|
|
94
|
+
return async () => null;
|
|
95
|
+
}
|
|
96
|
+
const doFetch = fetchFn ?? globalThis.fetch?.bind(globalThis);
|
|
97
|
+
if (typeof doFetch !== "function") return async () => null;
|
|
98
|
+
return async ({ candidates, question, requestId }) => {
|
|
99
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
100
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
101
|
+
try {
|
|
102
|
+
const res = await doFetch(`${janusUrl.replace(/\/$/, "")}/evaluate`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "content-type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ requestId, ...question }),
|
|
106
|
+
signal: controller?.signal,
|
|
107
|
+
});
|
|
108
|
+
if (!res || typeof res.ok !== "boolean" || !res.ok) return null;
|
|
109
|
+
const body = await res.json();
|
|
110
|
+
return body;
|
|
111
|
+
} catch {
|
|
112
|
+
return null; // unavailable, timeout, malformed — never blocks
|
|
113
|
+
} finally {
|
|
114
|
+
if (timer) clearTimeout(timer);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The full ranking step used by the route-decision path: rank or fall back,
|
|
120
|
+
// and record the observation for audit. Never throws; never blocks.
|
|
121
|
+
export async function rankOrFallback({ config, eligibleSeatIds, seatsById, quotaBuckets = {}, janusRank = null, now }) {
|
|
122
|
+
const preferenceOrder = (config?.preferences?.order ?? []).map((e) => e.seatId)
|
|
123
|
+
.filter((id) => eligibleSeatIds.includes(id));
|
|
124
|
+
const rest = eligibleSeatIds.filter((id) => !preferenceOrder.includes(id));
|
|
125
|
+
const fallbackOrder = [...preferenceOrder, ...rest];
|
|
126
|
+
if (fallbackOrder.length === 0) return { order: [], used: false, requestId: null, digest: null, confidence: null, outcome: "no-candidates" };
|
|
127
|
+
|
|
128
|
+
const candidates = rankingCandidates({ eligibleSeatIds: fallbackOrder, seatsById, quotaBuckets });
|
|
129
|
+
const digest = candidateSetDigest(candidates);
|
|
130
|
+
if (config?.ranking?.enabled !== true || typeof janusRank !== "function") {
|
|
131
|
+
return { order: fallbackOrder, used: false, requestId: null, digest, confidence: null, outcome: "fallback-disabled" };
|
|
132
|
+
}
|
|
133
|
+
const requestId = `rank-${now}-${digest.slice(0, 12)}`;
|
|
134
|
+
const question = rankingQuestion(candidates);
|
|
135
|
+
let response = null;
|
|
136
|
+
try {
|
|
137
|
+
response = await janusRank({ candidates, question, requestId });
|
|
138
|
+
} catch {
|
|
139
|
+
response = null;
|
|
140
|
+
}
|
|
141
|
+
const applied = applyRankingResponse({ response, fallbackOrder });
|
|
142
|
+
if (applied === null) {
|
|
143
|
+
return { order: fallbackOrder, used: false, requestId, digest, confidence: null, outcome: "fallback-failed" };
|
|
144
|
+
}
|
|
145
|
+
return { order: applied.order, used: true, requestId, digest, confidence: applied.confidence, outcome: "ranked" };
|
|
146
|
+
}
|