jev-layer 0.1.0 → 0.2.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/CHANGELOG.md +45 -34
- package/CONTRIBUTING.md +146 -146
- package/LICENSE +21 -21
- package/README.md +123 -126
- package/README.ru.md +138 -117
- package/README.zh-CN.md +137 -117
- package/RELEASE.md +53 -53
- package/SECURITY.md +56 -56
- package/bin/jev.mjs +213 -213
- package/config/codex.mcp.toml +8 -8
- package/config/generic-mcp.json +13 -13
- package/config/hermes.mcp.yaml +9 -9
- package/config/jev.example.json +22 -22
- package/config/omp.mcp.json +14 -14
- package/config/providers.env.example +13 -13
- package/docs/AGENT-IMPLEMENTATION.md +250 -0
- package/docs/COMPATIBILITY.md +15 -15
- package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +177 -0
- package/docs/PROVIDERS.md +67 -0
- package/docs/SCHEMA-VERSIONING.md +96 -94
- package/docs/architecture.svg +77 -0
- package/docs/jev-layer-banner.svg +38 -0
- package/examples/capabilities.json +39 -39
- package/examples/route-request.json +28 -28
- package/integrations/codex/.codex-plugin/plugin.json +19 -19
- package/integrations/codex/.mcp.json +11 -11
- package/integrations/codex/AGENTS.md +1 -1
- package/integrations/codex/run-mcp.mjs +9 -9
- package/integrations/codex/skills/jev-route/SKILL.md +17 -17
- package/integrations/hermes/__init__.py +150 -51
- package/integrations/hermes/plugin.yaml +10 -5
- package/integrations/hermes/schemas.py +89 -12
- package/integrations/omp/extension.js +105 -105
- package/integrations/template/README.md +10 -10
- package/integrations/template/adapter.mjs +87 -87
- package/package.json +60 -59
- package/scripts/benchmark.mjs +35 -35
- package/scripts/browser-benchmark.mjs +92 -92
- package/scripts/browser-e2e.mjs +117 -117
- package/scripts/capability-e2e.mjs +31 -31
- package/scripts/clean-install-smoke.mjs +162 -162
- package/scripts/codex-mcp-smoke.mjs +119 -119
- package/scripts/context-filter-e2e.mjs +45 -45
- package/scripts/fail-open-smoke.mjs +120 -120
- package/scripts/feature-flags-smoke.mjs +46 -46
- package/scripts/mcp-receipt-smoke.mjs +113 -98
- package/scripts/model-route-report.mjs +15 -0
- package/scripts/openrouter-choice.mjs +59 -59
- package/scripts/replay-eval.mjs +124 -124
- package/scripts/smoke.mjs +34 -34
- package/scripts/supervision-e2e.mjs +138 -109
- package/src/browser.mjs +571 -569
- package/src/cli.mjs +33 -33
- package/src/config.mjs +85 -75
- package/src/context-filter.mjs +56 -56
- package/src/contract.mjs +72 -72
- package/src/discovery.mjs +65 -65
- package/src/hermes-adapter.mjs +123 -0
- package/src/mcp-server.mjs +267 -210
- package/src/model-route-metrics.mjs +89 -0
- package/src/model-routing.mjs +87 -0
- package/src/providers/demo.mjs +52 -52
- package/src/providers/index.mjs +12 -0
- package/src/providers/typesafe.mjs +126 -126
- package/src/receipts.mjs +227 -226
- package/src/registry.mjs +109 -109
- package/src/relevance-filter.mjs +99 -99
- package/src/route.mjs +219 -221
- package/src/shadow-compaction.mjs +170 -0
- package/src/supervision.mjs +268 -244
- package/test/browser.test.mjs +199 -199
- package/test/capability.test.mjs +54 -54
- package/test/context-filter.test.mjs +65 -65
- package/test/hermes-adapter.test.mjs +62 -0
- package/test/model-route-metrics.test.mjs +61 -0
- package/test/model-routing.test.mjs +59 -0
- package/test/openrouter-provider.test.mjs +55 -55
- package/test/provider-transports.test.mjs +205 -0
- package/test/providers.test.mjs +126 -0
- package/test/receipts.test.mjs +71 -71
- package/test/route.test.mjs +74 -74
- package/test/shadow-compaction.test.mjs +67 -0
- package/test/supervision.test.mjs +117 -99
|
@@ -1,98 +1,113 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { createInterface } from "node:readline";
|
|
7
|
-
|
|
8
|
-
const root = resolve(new URL("..", import.meta.url).pathname);
|
|
9
|
-
const casesPath = join(await mkdtemp(join(tmpdir(), "jev-mcp-receipt-")), "cases.jsonl");
|
|
10
|
-
const child = spawn(process.env.JEV_NODE ?? "node", [resolve(root, "src/mcp-server.mjs")], {
|
|
11
|
-
cwd: root,
|
|
12
|
-
env: { ...process.env, JEV_REPLAY_CASES: casesPath },
|
|
13
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
14
|
-
});
|
|
15
|
-
const lines = createInterface({ input: child.stdout });
|
|
16
|
-
const pending = new Map();
|
|
17
|
-
lines.on("line", (line) => {
|
|
18
|
-
if (!line.trim()) return;
|
|
19
|
-
const message = JSON.parse(line);
|
|
20
|
-
const waiter = pending.get(message.id);
|
|
21
|
-
if (waiter) {
|
|
22
|
-
pending.delete(message.id);
|
|
23
|
-
waiter(message);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const initialized = await request(1, "initialize", {});
|
|
29
|
-
assert.equal(initialized.result.serverInfo.name, "jev-layer");
|
|
30
|
-
await notification("notifications/initialized", {});
|
|
31
|
-
const listed = await request(2, "tools/list", {});
|
|
32
|
-
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_route"));
|
|
33
|
-
assert.ok(listed.result.tools.some((tool) => tool.name === "
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
assert.equal(receipt.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return new Promise((
|
|
98
|
-
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
|
|
8
|
+
const root = resolve(new URL("..", import.meta.url).pathname);
|
|
9
|
+
const casesPath = join(await mkdtemp(join(tmpdir(), "jev-mcp-receipt-")), "cases.jsonl");
|
|
10
|
+
const child = spawn(process.env.JEV_NODE ?? "node", [resolve(root, "src/mcp-server.mjs")], {
|
|
11
|
+
cwd: root,
|
|
12
|
+
env: { ...process.env, JEV_REPLAY_CASES: casesPath },
|
|
13
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
14
|
+
});
|
|
15
|
+
const lines = createInterface({ input: child.stdout });
|
|
16
|
+
const pending = new Map();
|
|
17
|
+
lines.on("line", (line) => {
|
|
18
|
+
if (!line.trim()) return;
|
|
19
|
+
const message = JSON.parse(line);
|
|
20
|
+
const waiter = pending.get(message.id);
|
|
21
|
+
if (waiter) {
|
|
22
|
+
pending.delete(message.id);
|
|
23
|
+
waiter(message);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const initialized = await request(1, "initialize", {});
|
|
29
|
+
assert.equal(initialized.result.serverInfo.name, "jev-layer");
|
|
30
|
+
await notification("notifications/initialized", {});
|
|
31
|
+
const listed = await request(2, "tools/list", {});
|
|
32
|
+
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_route"));
|
|
33
|
+
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_model_route"));
|
|
34
|
+
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_shadow_compaction"));
|
|
35
|
+
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_record_execution"));
|
|
36
|
+
|
|
37
|
+
const routed = await request(3, "tools/call", {
|
|
38
|
+
name: "jev_route",
|
|
39
|
+
arguments: {
|
|
40
|
+
provider: "demo",
|
|
41
|
+
harness: "mcp-receipt-smoke",
|
|
42
|
+
intent: "read repository metadata",
|
|
43
|
+
actor_permissions: ["read"],
|
|
44
|
+
capabilities: [{
|
|
45
|
+
id: "read_metadata",
|
|
46
|
+
kind: "tool",
|
|
47
|
+
name: "Read metadata",
|
|
48
|
+
description: "Read repository metadata without modifying files",
|
|
49
|
+
permissions: ["read"],
|
|
50
|
+
risk: "low",
|
|
51
|
+
}],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
const decision = routed.result.structuredContent;
|
|
55
|
+
assert.equal(decision.status, "selected");
|
|
56
|
+
assert.equal(decision.receipt.replay_persisted, true);
|
|
57
|
+
|
|
58
|
+
const recorded = await request(4, "tools/call", {
|
|
59
|
+
name: "jev_record_execution",
|
|
60
|
+
arguments: {
|
|
61
|
+
correlation_id: decision.correlation_id,
|
|
62
|
+
harness: "mcp-receipt-smoke",
|
|
63
|
+
capability_id: decision.selected,
|
|
64
|
+
status: "completed",
|
|
65
|
+
result: { observed: "metadata" },
|
|
66
|
+
exit_status: 0,
|
|
67
|
+
duration_ms: 3.2,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const receipt = recorded.result.structuredContent;
|
|
71
|
+
assert.equal(receipt.correlation_id, decision.correlation_id);
|
|
72
|
+
assert.equal(receipt.host.exit_status, 0);
|
|
73
|
+
assert.equal(receipt.host.duration_ms, 3.2);
|
|
74
|
+
|
|
75
|
+
const shadow = await request(5, "tools/call", {
|
|
76
|
+
name: "jev_shadow_compaction",
|
|
77
|
+
arguments: {
|
|
78
|
+
provider: "demo",
|
|
79
|
+
intent: "inspect context before compaction",
|
|
80
|
+
context: { messages: ["stale note", "Requirement: preserve /workspace/config.json"] },
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const shadowReport = shadow.result.structuredContent;
|
|
84
|
+
assert.equal(shadowReport.mode, "jev_shadow");
|
|
85
|
+
assert.equal(shadowReport.changed, false);
|
|
86
|
+
assert.equal(shadowReport.protected_count, 1);
|
|
87
|
+
|
|
88
|
+
const records = (await readFile(casesPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
|
|
89
|
+
assert.deepEqual(records.map((record) => record.record_type), ["routing_case", "execution_receipt"]);
|
|
90
|
+
console.log(JSON.stringify({ ok: true, cases_path: casesPath, receipt }, null, 2));
|
|
91
|
+
} finally {
|
|
92
|
+
child.kill();
|
|
93
|
+
await new Promise((resolveExit) => child.once("close", resolveExit));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function request(id, method, params) {
|
|
97
|
+
return new Promise((resolveResponse, reject) => {
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
pending.delete(id);
|
|
100
|
+
reject(new Error(`MCP request timed out: ${method}`));
|
|
101
|
+
}, 15_000);
|
|
102
|
+
pending.set(id, (message) => {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
resolveResponse(message);
|
|
105
|
+
});
|
|
106
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function notification(method, params) {
|
|
111
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
112
|
+
return new Promise((resolveNotification) => setImmediate(resolveNotification));
|
|
113
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { buildModelRouteReport } from "../src/model-route-metrics.mjs";
|
|
4
|
+
|
|
5
|
+
const path = process.argv[2] ?? process.env.JEV_REPLAY_CASES ?? ".jev/replay/cases.jsonl";
|
|
6
|
+
let text = "";
|
|
7
|
+
try {
|
|
8
|
+
text = await readFile(path, "utf8");
|
|
9
|
+
} catch (error) {
|
|
10
|
+
if (error?.code !== "ENOENT") throw error;
|
|
11
|
+
}
|
|
12
|
+
const records = text.split(/\r?\n/).filter(Boolean).flatMap((line) => {
|
|
13
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
14
|
+
});
|
|
15
|
+
console.log(JSON.stringify(buildModelRouteReport(records), null, 2));
|
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
import { routeRequest } from "../src/route.mjs";
|
|
2
|
-
|
|
3
|
-
const endpoint = process.env.OPENROUTER_DECISIONS_ENDPOINT ?? "https://openrouter.ai/api/alpha/decisions";
|
|
4
|
-
const model = process.env.OPENROUTER_DECISIONS_MODEL ?? "typesafe/jev-1.13";
|
|
5
|
-
const decision = await routeRequest({
|
|
6
|
-
schema_version: 1,
|
|
7
|
-
harness: "jev-layer-openrouter-smoke",
|
|
8
|
-
intent: "Find where the OpenRouter Decisions provider is implemented without modifying files",
|
|
9
|
-
context: {
|
|
10
|
-
purpose: "one real bounded Choice call",
|
|
11
|
-
secret_note: "must be redacted before provider transmission",
|
|
12
|
-
},
|
|
13
|
-
actor_permissions: ["read", "browser", "terminal:read"],
|
|
14
|
-
capabilities: [
|
|
15
|
-
{
|
|
16
|
-
id: "repo_search",
|
|
17
|
-
kind: "mcp",
|
|
18
|
-
name: "Repository search",
|
|
19
|
-
description: "Search repository files and symbols without changing files",
|
|
20
|
-
permissions: ["read"],
|
|
21
|
-
risk: "low",
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
id: "browser_inspect",
|
|
25
|
-
kind: "mcp",
|
|
26
|
-
name: "Browser inspection",
|
|
27
|
-
description: "Open a webpage and inspect visible browser state",
|
|
28
|
-
permissions: ["browser"],
|
|
29
|
-
risk: "medium",
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: "terminal_read",
|
|
33
|
-
kind: "cli",
|
|
34
|
-
name: "Read-only terminal",
|
|
35
|
-
description: "Run a read-only shell command and return its output",
|
|
36
|
-
permissions: ["terminal:read"],
|
|
37
|
-
risk: "low",
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
}, {
|
|
41
|
-
provider: "openrouter",
|
|
42
|
-
openrouter: { endpoint, model, timeoutMs: 15_000 },
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const raw = decision.raw_jev ?? {};
|
|
46
|
-
const usage = raw.usage && typeof raw.usage === "object" ? raw.usage : {};
|
|
47
|
-
console.log(JSON.stringify({
|
|
48
|
-
request: { endpoint, model, method: "POST" },
|
|
49
|
-
response: { id: raw.id ?? null, model: raw.model ?? null, provider: raw.provider ?? null },
|
|
50
|
-
status: decision.status,
|
|
51
|
-
selected: decision.selected,
|
|
52
|
-
probabilities: decision.probabilities,
|
|
53
|
-
confidence: decision.confidence,
|
|
54
|
-
latency_ms: decision.receipt.latency_ms,
|
|
55
|
-
cost_usd: typeof usage.cost === "number" ? usage.cost : null,
|
|
56
|
-
usage,
|
|
57
|
-
fallback: decision.fallback,
|
|
58
|
-
execution: decision.execution,
|
|
59
|
-
}, null, 2));
|
|
1
|
+
import { routeRequest } from "../src/route.mjs";
|
|
2
|
+
|
|
3
|
+
const endpoint = process.env.OPENROUTER_DECISIONS_ENDPOINT ?? "https://openrouter.ai/api/alpha/decisions";
|
|
4
|
+
const model = process.env.OPENROUTER_DECISIONS_MODEL ?? "typesafe/jev-1.13";
|
|
5
|
+
const decision = await routeRequest({
|
|
6
|
+
schema_version: 1,
|
|
7
|
+
harness: "jev-layer-openrouter-smoke",
|
|
8
|
+
intent: "Find where the OpenRouter Decisions provider is implemented without modifying files",
|
|
9
|
+
context: {
|
|
10
|
+
purpose: "one real bounded Choice call",
|
|
11
|
+
secret_note: "must be redacted before provider transmission",
|
|
12
|
+
},
|
|
13
|
+
actor_permissions: ["read", "browser", "terminal:read"],
|
|
14
|
+
capabilities: [
|
|
15
|
+
{
|
|
16
|
+
id: "repo_search",
|
|
17
|
+
kind: "mcp",
|
|
18
|
+
name: "Repository search",
|
|
19
|
+
description: "Search repository files and symbols without changing files",
|
|
20
|
+
permissions: ["read"],
|
|
21
|
+
risk: "low",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "browser_inspect",
|
|
25
|
+
kind: "mcp",
|
|
26
|
+
name: "Browser inspection",
|
|
27
|
+
description: "Open a webpage and inspect visible browser state",
|
|
28
|
+
permissions: ["browser"],
|
|
29
|
+
risk: "medium",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "terminal_read",
|
|
33
|
+
kind: "cli",
|
|
34
|
+
name: "Read-only terminal",
|
|
35
|
+
description: "Run a read-only shell command and return its output",
|
|
36
|
+
permissions: ["terminal:read"],
|
|
37
|
+
risk: "low",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}, {
|
|
41
|
+
provider: "openrouter",
|
|
42
|
+
openrouter: { endpoint, model, timeoutMs: 15_000 },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const raw = decision.raw_jev ?? {};
|
|
46
|
+
const usage = raw.usage && typeof raw.usage === "object" ? raw.usage : {};
|
|
47
|
+
console.log(JSON.stringify({
|
|
48
|
+
request: { endpoint, model, method: "POST" },
|
|
49
|
+
response: { id: raw.id ?? null, model: raw.model ?? null, provider: raw.provider ?? null },
|
|
50
|
+
status: decision.status,
|
|
51
|
+
selected: decision.selected,
|
|
52
|
+
probabilities: decision.probabilities,
|
|
53
|
+
confidence: decision.confidence,
|
|
54
|
+
latency_ms: decision.receipt.latency_ms,
|
|
55
|
+
cost_usd: typeof usage.cost === "number" ? usage.cost : null,
|
|
56
|
+
usage,
|
|
57
|
+
fallback: decision.fallback,
|
|
58
|
+
execution: decision.execution,
|
|
59
|
+
}, null, 2));
|
package/scripts/replay-eval.mjs
CHANGED
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { readRoutingCases, readSupervisionCases, replayCasePath } from "../src/receipts.mjs";
|
|
4
|
-
import { deterministicSupervisionPolicy } from "../src/supervision.mjs";
|
|
5
|
-
import { routeRequest } from "../src/route.mjs";
|
|
6
|
-
|
|
7
|
-
const args = parseArgs(process.argv.slice(2));
|
|
8
|
-
const inputPath = replayCasePath(args.input);
|
|
9
|
-
const provider = args.provider ?? process.env.JEV_REPLAY_PROVIDER ?? "demo";
|
|
10
|
-
const allCases = await readRoutingCases(inputPath);
|
|
11
|
-
const cases = args.limit ? allCases.slice(0, args.limit) : allCases;
|
|
12
|
-
const results = [];
|
|
13
|
-
|
|
14
|
-
for (const routingCase of cases) {
|
|
15
|
-
const decision = await routeRequest(routingCase.request, { provider });
|
|
16
|
-
if (decision.execution?.enabled) throw new Error(`replay attempted host execution for ${routingCase.case_id}`);
|
|
17
|
-
results.push({
|
|
18
|
-
source_case_id: routingCase.case_id,
|
|
19
|
-
source_correlation_id: routingCase.correlation_id,
|
|
20
|
-
replay_correlation_id: decision.correlation_id,
|
|
21
|
-
surface: routingCase.request.context?.browser ? "browser" : "generic",
|
|
22
|
-
harness: routingCase.harness,
|
|
23
|
-
selected: decision.selected,
|
|
24
|
-
confidence: decision.confidence,
|
|
25
|
-
status: decision.status,
|
|
26
|
-
fallback: decision.fallback?.type ?? null,
|
|
27
|
-
latency_ms: decision.receipt?.latency_ms ?? null,
|
|
28
|
-
cost_usd: decision.receipt?.cost_usd ?? decision.raw_jev?.usage?.cost ?? null,
|
|
29
|
-
execution: decision.execution,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const supervisionCases = args.limit
|
|
34
|
-
? (await readSupervisionCases(inputPath)).slice(0, args.limit)
|
|
35
|
-
: await readSupervisionCases(inputPath);
|
|
36
|
-
const supervisionResults = supervisionCases.map((record) => ({
|
|
37
|
-
source_case_id: record.case_id,
|
|
38
|
-
source_correlation_id: record.correlation_id,
|
|
39
|
-
harness: record.harness,
|
|
40
|
-
assessment: record.supervision.assessment,
|
|
41
|
-
recorded_action: record.supervision.action,
|
|
42
|
-
replayed_policy: deterministicSupervisionPolicy({
|
|
43
|
-
assessment: record.supervision.assessment,
|
|
44
|
-
evidence: record.request.context?.evidence ?? {},
|
|
45
|
-
}),
|
|
46
|
-
host_execution: false,
|
|
47
|
-
}));
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const summary = summarize(results);
|
|
51
|
-
summary.supervision_case_count = supervisionResults.length;
|
|
52
|
-
summary.supervision_policy_match_rate = rate(
|
|
53
|
-
supervisionResults.filter((result) => result.replayed_policy.action === result.recorded_action).length,
|
|
54
|
-
supervisionResults.length,
|
|
55
|
-
);
|
|
56
|
-
const report = {
|
|
57
|
-
ok: true,
|
|
58
|
-
source: inputPath,
|
|
59
|
-
provider,
|
|
60
|
-
replayed_cases: results.length,
|
|
61
|
-
replayed_supervision_cases: supervisionResults.length,
|
|
62
|
-
host_execution_calls: 0,
|
|
63
|
-
summary,
|
|
64
|
-
cases: results,
|
|
65
|
-
supervision_cases: supervisionResults,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
if (args.output) {
|
|
69
|
-
await mkdir(dirname(args.output), { recursive: true });
|
|
70
|
-
await writeFile(args.output, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
71
|
-
}
|
|
72
|
-
console.log(JSON.stringify(report, null, 2));
|
|
73
|
-
function summarize(results) {
|
|
74
|
-
const total = results.length;
|
|
75
|
-
const fallbackCount = results.filter((result) => result.status === "fallback").length;
|
|
76
|
-
const selectedCount = results.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length;
|
|
77
|
-
const noDecisionCount = results.filter((result) => result.status === "no_decision").length;
|
|
78
|
-
const browserResults = results.filter((result) => result.surface === "browser");
|
|
79
|
-
const confidences = results.map((result) => result.confidence).filter((value) => typeof value === "number");
|
|
80
|
-
const latencies = results.map((result) => result.latency_ms).filter((value) => typeof value === "number");
|
|
81
|
-
const costs = results.map((result) => result.cost_usd).filter((value) => typeof value === "number");
|
|
82
|
-
return {
|
|
83
|
-
selected_rate: rate(selectedCount, total),
|
|
84
|
-
fallback_rate: rate(fallbackCount, total),
|
|
85
|
-
no_decision_rate: rate(noDecisionCount, total),
|
|
86
|
-
browser_case_count: browserResults.length,
|
|
87
|
-
browser_selected_rate: rate(browserResults.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length, browserResults.length),
|
|
88
|
-
average_confidence: average(confidences),
|
|
89
|
-
average_latency_ms: average(latencies),
|
|
90
|
-
p50_latency_ms: percentile(latencies, 0.5),
|
|
91
|
-
p95_latency_ms: percentile(latencies, 0.95),
|
|
92
|
-
total_cost_usd: Number(costs.reduce((sum, value) => sum + value, 0).toFixed(9)),
|
|
93
|
-
status_counts: Object.fromEntries([...new Set(results.map((result) => result.status))].map((status) => [status, results.filter((result) => result.status === status).length])),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function parseArgs(argv) {
|
|
98
|
-
const result = {};
|
|
99
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
100
|
-
const value = argv[index];
|
|
101
|
-
if (value === "--input") result.input = argv[++index];
|
|
102
|
-
else if (value === "--provider") result.provider = argv[++index];
|
|
103
|
-
else if (value === "--limit") result.limit = Number(argv[++index]);
|
|
104
|
-
else if (value === "--output") result.output = argv[++index];
|
|
105
|
-
}
|
|
106
|
-
if (result.limit !== undefined && (!Number.isInteger(result.limit) || result.limit < 0)) throw new Error("--limit must be a non-negative integer");
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function average(values) {
|
|
111
|
-
return values.length ? Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(3)) : null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function percentile(values, fraction) {
|
|
115
|
-
if (!values.length) return null;
|
|
116
|
-
const sorted = [...values].sort((left, right) => left - right);
|
|
117
|
-
const rank = Math.max(0, Math.ceil(sorted.length * fraction) - 1);
|
|
118
|
-
return Number(sorted[Math.min(sorted.length - 1, rank)].toFixed(3));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
function rate(part, total) {
|
|
123
|
-
return total ? Number((part / total).toFixed(4)) : 0;
|
|
124
|
-
}
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { readRoutingCases, readSupervisionCases, replayCasePath } from "../src/receipts.mjs";
|
|
4
|
+
import { deterministicSupervisionPolicy } from "../src/supervision.mjs";
|
|
5
|
+
import { routeRequest } from "../src/route.mjs";
|
|
6
|
+
|
|
7
|
+
const args = parseArgs(process.argv.slice(2));
|
|
8
|
+
const inputPath = replayCasePath(args.input);
|
|
9
|
+
const provider = args.provider ?? process.env.JEV_REPLAY_PROVIDER ?? "demo";
|
|
10
|
+
const allCases = await readRoutingCases(inputPath);
|
|
11
|
+
const cases = args.limit ? allCases.slice(0, args.limit) : allCases;
|
|
12
|
+
const results = [];
|
|
13
|
+
|
|
14
|
+
for (const routingCase of cases) {
|
|
15
|
+
const decision = await routeRequest(routingCase.request, { provider });
|
|
16
|
+
if (decision.execution?.enabled) throw new Error(`replay attempted host execution for ${routingCase.case_id}`);
|
|
17
|
+
results.push({
|
|
18
|
+
source_case_id: routingCase.case_id,
|
|
19
|
+
source_correlation_id: routingCase.correlation_id,
|
|
20
|
+
replay_correlation_id: decision.correlation_id,
|
|
21
|
+
surface: routingCase.request.context?.browser ? "browser" : "generic",
|
|
22
|
+
harness: routingCase.harness,
|
|
23
|
+
selected: decision.selected,
|
|
24
|
+
confidence: decision.confidence,
|
|
25
|
+
status: decision.status,
|
|
26
|
+
fallback: decision.fallback?.type ?? null,
|
|
27
|
+
latency_ms: decision.receipt?.latency_ms ?? null,
|
|
28
|
+
cost_usd: decision.receipt?.cost_usd ?? decision.raw_jev?.usage?.cost ?? null,
|
|
29
|
+
execution: decision.execution,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const supervisionCases = args.limit
|
|
34
|
+
? (await readSupervisionCases(inputPath)).slice(0, args.limit)
|
|
35
|
+
: await readSupervisionCases(inputPath);
|
|
36
|
+
const supervisionResults = supervisionCases.map((record) => ({
|
|
37
|
+
source_case_id: record.case_id,
|
|
38
|
+
source_correlation_id: record.correlation_id,
|
|
39
|
+
harness: record.harness,
|
|
40
|
+
assessment: record.supervision.assessment,
|
|
41
|
+
recorded_action: record.supervision.action,
|
|
42
|
+
replayed_policy: deterministicSupervisionPolicy({
|
|
43
|
+
assessment: record.supervision.assessment,
|
|
44
|
+
evidence: record.request.context?.evidence ?? {},
|
|
45
|
+
}),
|
|
46
|
+
host_execution: false,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
const summary = summarize(results);
|
|
51
|
+
summary.supervision_case_count = supervisionResults.length;
|
|
52
|
+
summary.supervision_policy_match_rate = rate(
|
|
53
|
+
supervisionResults.filter((result) => result.replayed_policy.action === result.recorded_action).length,
|
|
54
|
+
supervisionResults.length,
|
|
55
|
+
);
|
|
56
|
+
const report = {
|
|
57
|
+
ok: true,
|
|
58
|
+
source: inputPath,
|
|
59
|
+
provider,
|
|
60
|
+
replayed_cases: results.length,
|
|
61
|
+
replayed_supervision_cases: supervisionResults.length,
|
|
62
|
+
host_execution_calls: 0,
|
|
63
|
+
summary,
|
|
64
|
+
cases: results,
|
|
65
|
+
supervision_cases: supervisionResults,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (args.output) {
|
|
69
|
+
await mkdir(dirname(args.output), { recursive: true });
|
|
70
|
+
await writeFile(args.output, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
|
71
|
+
}
|
|
72
|
+
console.log(JSON.stringify(report, null, 2));
|
|
73
|
+
function summarize(results) {
|
|
74
|
+
const total = results.length;
|
|
75
|
+
const fallbackCount = results.filter((result) => result.status === "fallback").length;
|
|
76
|
+
const selectedCount = results.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length;
|
|
77
|
+
const noDecisionCount = results.filter((result) => result.status === "no_decision").length;
|
|
78
|
+
const browserResults = results.filter((result) => result.surface === "browser");
|
|
79
|
+
const confidences = results.map((result) => result.confidence).filter((value) => typeof value === "number");
|
|
80
|
+
const latencies = results.map((result) => result.latency_ms).filter((value) => typeof value === "number");
|
|
81
|
+
const costs = results.map((result) => result.cost_usd).filter((value) => typeof value === "number");
|
|
82
|
+
return {
|
|
83
|
+
selected_rate: rate(selectedCount, total),
|
|
84
|
+
fallback_rate: rate(fallbackCount, total),
|
|
85
|
+
no_decision_rate: rate(noDecisionCount, total),
|
|
86
|
+
browser_case_count: browserResults.length,
|
|
87
|
+
browser_selected_rate: rate(browserResults.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length, browserResults.length),
|
|
88
|
+
average_confidence: average(confidences),
|
|
89
|
+
average_latency_ms: average(latencies),
|
|
90
|
+
p50_latency_ms: percentile(latencies, 0.5),
|
|
91
|
+
p95_latency_ms: percentile(latencies, 0.95),
|
|
92
|
+
total_cost_usd: Number(costs.reduce((sum, value) => sum + value, 0).toFixed(9)),
|
|
93
|
+
status_counts: Object.fromEntries([...new Set(results.map((result) => result.status))].map((status) => [status, results.filter((result) => result.status === status).length])),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseArgs(argv) {
|
|
98
|
+
const result = {};
|
|
99
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
100
|
+
const value = argv[index];
|
|
101
|
+
if (value === "--input") result.input = argv[++index];
|
|
102
|
+
else if (value === "--provider") result.provider = argv[++index];
|
|
103
|
+
else if (value === "--limit") result.limit = Number(argv[++index]);
|
|
104
|
+
else if (value === "--output") result.output = argv[++index];
|
|
105
|
+
}
|
|
106
|
+
if (result.limit !== undefined && (!Number.isInteger(result.limit) || result.limit < 0)) throw new Error("--limit must be a non-negative integer");
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function average(values) {
|
|
111
|
+
return values.length ? Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(3)) : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function percentile(values, fraction) {
|
|
115
|
+
if (!values.length) return null;
|
|
116
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
117
|
+
const rank = Math.max(0, Math.ceil(sorted.length * fraction) - 1);
|
|
118
|
+
return Number(sorted[Math.min(sorted.length - 1, rank)].toFixed(3));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
function rate(part, total) {
|
|
123
|
+
return total ? Number((part / total).toFixed(4)) : 0;
|
|
124
|
+
}
|