Skip to content

Commit cd204d7

Browse files
committed
feat(control-plane): expose routing policy telemetry
1 parent 49c0bd8 commit cd204d7

4 files changed

Lines changed: 127 additions & 0 deletions

File tree

DESIGN.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,3 +1777,15 @@ Rationale:
17771777
- Routing policy was already tested as a control-plane module; this promotes it into the actual model-call path without granting any new promotion authority.
17781778
- Operator views should reflect the route actually attempted, not reconstruct model choice from environment defaults after the fact.
17791779
- Capability-first routing keeps frontend labels, backend execution, and prompt language aligned: use the best policy-permitted model available on this surface, then record what was actually selected.
1780+
1781+
### D-0057: Operator-Visible Routing Policy Telemetry
1782+
1783+
Decision:
1784+
- Project routing-policy sub-receipts through router receipts and `/api/operator/receipts-telemetry`.
1785+
- Summarize policy status and selected tier from persisted receipt facts, not from current environment defaults.
1786+
- Keep the projection bounded to route IDs, model IDs, selected tier, attempt status, provider invocation, and receipt hashes.
1787+
1788+
Rationale:
1789+
- The operator cockpit needs to show whether a response was routed, blocked, downgraded, or served by a provider without requiring code inspection.
1790+
- Persisted receipts may be older than current configuration, so telemetry must describe what happened at the time of execution rather than what the current model router would choose now.
1791+
- This remains observability only. It does not prove output quality, provider availability beyond the attempted call, or Council promotion authority.

NEXT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ These are future focuses for public collaborators, not current completion claims
3030

3131
## Completed
3232

33+
- W-0132: Exposed routing-policy sub-receipts in operator telemetry (`agent_server.mjs`, `scripts/operator_telemetry_routes_test.mjs`, `DESIGN.md` [D-0057]). Router receipts now carry a sanitized `routing_policy` projection when dispatch supplies one, and `/api/operator/receipts-telemetry` summarizes routing-policy status and selected tier from receipt facts rather than reconstructing model choice from current environment defaults. This is cockpit observability only, not provider-quality proof or Council promotion authority. (Verification: `npm run test:operator-telemetry`; `npm run test:router`; `npm run check:council`)
34+
3335
- W-0131: Routed live chat and utility provider calls through the capability-first routing policy (`lib/dispatch.mjs`, `lib/routing_policy.mjs`, `lib/gemini_client.mjs`, `scripts/dynamic_router_test.mjs`, `scripts/routing_policy_test.mjs`, `DESIGN.md` [D-0056]). Dispatch now plans Gemini/OpenAI-compatible/Ollama-style calls with surface-bound route evidence before invocation, emits routing-policy metadata in execution receipts, preserves redirect and isolation block reasons, and sends Gemini max-output limits as actual generation controls. This is live dispatch admission and observability, not Council promotion authority. (Verification: `npm run test:routing-policy`; `npm run test:router`; `npm run check:council`)
3436

3537
- W-0126: Replaced the Python/Z3 bridge with a deterministic policy scorer. Removed the synchronous Python subprocess and Z3 SMT solver from memory conflict resolution. Implemented a pure JavaScript deterministic scorer based on confidence + 0.02 * reinforcement with a strict 0.05 dominance margin. Hard governance invariants are enforced as hard predicates before scoring. Re-anchored our roadmap around Anthropic's Building Effective Agents doctrine. (Verification: npm run check:council)

agent_server.mjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,36 @@ function normalizeFreeText(value, maxChars = 20_000) {
437437
return String(value ?? "").trim().slice(0, Math.max(1, Number(maxChars) || 20_000));
438438
}
439439

440+
function summarizeRoutingPolicyMetadata(policy) {
441+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) return null;
442+
const attempts = Array.isArray(policy.attempts) ? policy.attempts.slice(0, 4).map((attempt) => ({
443+
route_id: normalizeFreeText(attempt?.route_id || "", 160),
444+
model_id: normalizeFreeText(attempt?.model_id || "", 160),
445+
status: normalizeIdentifier(attempt?.status || "unknown", "unknown"),
446+
error: attempt?.error ? normalizeIdentifier(attempt.error, "unknown_error") : null,
447+
sent_model: normalizeFreeText(attempt?.sent_model || "", 160) || null,
448+
reported_model: normalizeFreeText(attempt?.reported_model || "", 160) || null,
449+
usage_known: attempt?.usage_known === true,
450+
status_code: Number(attempt?.status_code) > 0 ? Number(attempt.status_code) : null,
451+
})) : [];
452+
453+
return {
454+
status: normalizeIdentifier(policy.status || "unknown", "unknown"),
455+
task_class: normalizeIdentifier(policy.task_class || "unknown", "unknown"),
456+
surface_id: normalizeIdentifier(policy.surface_id || "unknown", "unknown"),
457+
requested_model: normalizeFreeText(policy.requested_model || "", 160) || null,
458+
selected_tier: normalizeIdentifier(policy.selected_tier || "unknown", "unknown").toUpperCase(),
459+
selected_model_or_route: normalizeFreeText(policy.selected_model_or_route || "", 160) || null,
460+
provider_invoked: policy.provider_invoked === true,
461+
downgrade_reason: policy.downgrade_reason ? normalizeIdentifier(policy.downgrade_reason, "unknown") : null,
462+
fail_closed_reason: policy.fail_closed_reason ? normalizeIdentifier(policy.fail_closed_reason, "unknown") : null,
463+
routing_receipt_sha256: /^[a-f0-9]{64}$/i.test(String(policy.routing_receipt_sha256 || ""))
464+
? String(policy.routing_receipt_sha256).toLowerCase()
465+
: null,
466+
attempts,
467+
};
468+
}
469+
440470
function countJsonlRows(filePath) {
441471
try {
442472
const raw = fs.readFileSync(filePath, "utf8").trim();
@@ -1020,6 +1050,7 @@ export async function createRuntime(opts = {}) {
10201050
}
10211051

10221052
function summarizeReceipt(receipt = {}) {
1053+
const routingPolicy = summarizeRoutingPolicyMetadata(receipt.routing_policy);
10231054
return {
10241055
schema_version: receipt.schema_version || "",
10251056
task_id: receipt.task_id || "",
@@ -1031,6 +1062,7 @@ export async function createRuntime(opts = {}) {
10311062
latency_ms: Number.isFinite(Number(receipt.latency_ms)) ? Math.max(0, Math.round(Number(receipt.latency_ms))) : 0,
10321063
provider_health: receipt.provider_health || "unknown",
10331064
persisted: Boolean(receipt.persisted),
1065+
routing_policy: routingPolicy,
10341066
};
10351067
}
10361068

@@ -1098,6 +1130,8 @@ export async function createRuntime(opts = {}) {
10981130
const trustZonesSummary = {};
10991131
const costBandsSummary = {};
11001132
const latencyBandsSummary = {};
1133+
const routingPolicyStatuses = {};
1134+
const selectedTiersSummary = {};
11011135
let totalLatencyMs = 0;
11021136
let latencyCount = 0;
11031137

@@ -1110,6 +1144,12 @@ export async function createRuntime(opts = {}) {
11101144
costBandsSummary[costBand] = (costBandsSummary[costBand] || 0) + 1;
11111145
const band = latencyBand(r.latency_ms);
11121146
latencyBandsSummary[band] = (latencyBandsSummary[band] || 0) + 1;
1147+
if (r.routing_policy) {
1148+
const policyStatus = r.routing_policy.status || "unknown";
1149+
routingPolicyStatuses[policyStatus] = (routingPolicyStatuses[policyStatus] || 0) + 1;
1150+
const selectedTier = r.routing_policy.selected_tier || "UNKNOWN";
1151+
selectedTiersSummary[selectedTier] = (selectedTiersSummary[selectedTier] || 0) + 1;
1152+
}
11131153
if (typeof r.latency_ms === "number" && r.latency_ms > 0) {
11141154
totalLatencyMs += r.latency_ms;
11151155
latencyCount++;
@@ -1125,6 +1165,8 @@ export async function createRuntime(opts = {}) {
11251165
trust_zones: trustZonesSummary,
11261166
cost_bands: costBandsSummary,
11271167
latency_bands: latencyBandsSummary,
1168+
routing_policy_statuses: routingPolicyStatuses,
1169+
selected_tiers: selectedTiersSummary,
11281170
avg_latency_ms: latencyCount > 0 ? Math.round(totalLatencyMs / latencyCount) : 0,
11291171
},
11301172
pareto_frontier: paretoFrontier,
@@ -1409,6 +1451,7 @@ export async function createRuntime(opts = {}) {
14091451
path: "none",
14101452
blocked_reason: noExecutionReason
14111453
};
1454+
const routingPolicy = summarizeRoutingPolicyMetadata(executionMetadata?.routing_policy);
14121455

14131456
const receipt = {
14141457
schema_version: "dizzy.router_receipt.v1",
@@ -1425,6 +1468,7 @@ export async function createRuntime(opts = {}) {
14251468
provider_health: typeof executionMetadata?.provider_health === "string" ? executionMetadata.provider_health : (isNoneModel ? "unconfigured" : "healthy"),
14261469
reason: executionMetadata?.reason || (isNoneModel ? `no_model_execution:${noExecutionReason}` : "default_triage_routing_to_chat"),
14271470
fallback,
1471+
routing_policy: routingPolicy,
14281472
trust_zone: capabilities.trust_zone || "paid_public",
14291473
durable_memory_allowed: capabilities.durable_memory_allowed ?? false,
14301474
voice_consent: false,

scripts/operator_telemetry_routes_test.mjs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import assert from "node:assert/strict";
2+
import fs from "node:fs";
3+
import os from "node:os";
4+
import path from "node:path";
25
import { startServer } from "../agent_server.mjs";
36
import { TENSION_MAP_SCHEMA } from "../lib/tension_map_engine.mjs";
47
import { JOB_BOARD_INGRESS_SCHEMA } from "../lib/job_board_ingress.mjs";
@@ -15,6 +18,45 @@ async function getJson(url, token = TEST_AUTH_TOKEN) {
1518
return { status: res.status, data };
1619
}
1720

21+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dizzy-operator-telemetry-"));
22+
const receiptPath = path.join(tempRoot, "router_receipts.jsonl");
23+
const oldReceiptPath = process.env.DIZZY_ROUTER_RECEIPT_PATH;
24+
process.env.DIZZY_ROUTER_RECEIPT_PATH = receiptPath;
25+
26+
const routingReceiptHash = "a".repeat(64);
27+
fs.writeFileSync(receiptPath, JSON.stringify({
28+
schema_version: "dizzy.router_receipt.v1",
29+
task_id: "routing-telemetry-fixture",
30+
timestamp: "2026-09-13T19:50:00.000Z",
31+
task_class: "route_classify",
32+
chosen_model: "openai_compat:gemma3:4b",
33+
trust_zone: "paid_public",
34+
estimated_cost_band: "low",
35+
data_boundary: "local_machine",
36+
model_origin_risk: "low",
37+
latency_ms: 42,
38+
provider_health: "healthy",
39+
persisted: true,
40+
routing_policy: {
41+
status: "SUCCEEDED",
42+
task_class: "chat",
43+
surface_id: "dispatch",
44+
requested_model: "gemma3:4b",
45+
selected_tier: "T2",
46+
selected_model_or_route: "openai_compat:gemma3:4b",
47+
provider_invoked: true,
48+
routing_receipt_sha256: routingReceiptHash,
49+
attempts: [{
50+
route_id: "openai_compat:gemma3:4b",
51+
model_id: "gemma3:4b",
52+
status: "SUCCEEDED",
53+
sent_model: "gemma3:4b",
54+
reported_model: "gemma3:4b",
55+
usage_known: false,
56+
}],
57+
},
58+
}) + "\n", "utf8");
59+
1860
const runtime = await startServer({
1961
port: 0,
2062
bindHost: "127.0.0.1",
@@ -109,7 +151,34 @@ try {
109151
console.log(" [PASS] Test 5: unconfigured council bridge status fails closed");
110152
}
111153

154+
// Test 6: receipts telemetry exposes capability-first routing facts without relying on env reconstruction
155+
{
156+
const { status, data } = await getJson(`${baseUrl}/api/operator/receipts-telemetry`);
157+
assert.equal(status, 200);
158+
assert.equal(data.ok, true);
159+
assert.equal(data.receipt_count, 1);
160+
assert.equal(data.summary.routing_policy_statuses.succeeded, 1);
161+
assert.equal(data.summary.selected_tiers.T2, 1);
162+
const receipt = data.recent_receipts[0];
163+
assert.equal(receipt.task_id, "routing-telemetry-fixture");
164+
assert.equal(receipt.routing_policy.status, "succeeded");
165+
assert.equal(receipt.routing_policy.task_class, "chat");
166+
assert.equal(receipt.routing_policy.surface_id, "dispatch");
167+
assert.equal(receipt.routing_policy.selected_tier, "T2");
168+
assert.equal(receipt.routing_policy.selected_model_or_route, "openai_compat:gemma3:4b");
169+
assert.equal(receipt.routing_policy.provider_invoked, true);
170+
assert.equal(receipt.routing_policy.routing_receipt_sha256, routingReceiptHash);
171+
assert.equal(receipt.routing_policy.attempts[0].sent_model, "gemma3:4b");
172+
console.log(" [PASS] Test 6: receipts telemetry exposes routing policy facts");
173+
}
174+
112175
console.log("\n[test:operator-telemetry-routes] ALL TESTS PASSED CLEANLY.\n");
113176
} finally {
114177
await runtime.stop?.();
178+
if (oldReceiptPath === undefined) {
179+
delete process.env.DIZZY_ROUTER_RECEIPT_PATH;
180+
} else {
181+
process.env.DIZZY_ROUTER_RECEIPT_PATH = oldReceiptPath;
182+
}
183+
fs.rmSync(tempRoot, { recursive: true, force: true });
115184
}

0 commit comments

Comments
 (0)