jev-layer 0.1.0 → 0.3.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +73 -34
  2. package/CONTRIBUTING.md +146 -146
  3. package/LICENSE +21 -21
  4. package/README.md +125 -126
  5. package/README.ru.md +140 -117
  6. package/README.zh-CN.md +139 -117
  7. package/RELEASE.md +53 -53
  8. package/SECURITY.md +64 -56
  9. package/bin/jev.mjs +215 -213
  10. package/config/codex.mcp.toml +8 -8
  11. package/config/generic-mcp.json +13 -13
  12. package/config/hermes.mcp.yaml +9 -9
  13. package/config/jev.example.json +23 -22
  14. package/config/omp.mcp.json +14 -14
  15. package/config/providers.env.example +13 -13
  16. package/docs/AGENT-IMPLEMENTATION.md +306 -0
  17. package/docs/COMPATIBILITY.md +24 -15
  18. package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +187 -0
  19. package/docs/PROVIDERS.md +109 -0
  20. package/docs/SCHEMA-VERSIONING.md +101 -94
  21. package/docs/architecture.svg +77 -0
  22. package/docs/jev-layer-banner.svg +38 -0
  23. package/examples/capabilities.json +39 -39
  24. package/examples/route-request.json +28 -28
  25. package/integrations/codex/.codex-plugin/plugin.json +19 -19
  26. package/integrations/codex/.mcp.json +11 -11
  27. package/integrations/codex/AGENTS.md +1 -1
  28. package/integrations/codex/run-mcp.mjs +9 -9
  29. package/integrations/codex/skills/jev-route/SKILL.md +17 -17
  30. package/integrations/hermes/__init__.py +156 -51
  31. package/integrations/hermes/plugin.yaml +11 -5
  32. package/integrations/hermes/schemas.py +120 -12
  33. package/integrations/omp/extension.js +106 -105
  34. package/integrations/template/README.md +10 -10
  35. package/integrations/template/adapter.mjs +87 -87
  36. package/package.json +60 -59
  37. package/scripts/benchmark.mjs +35 -35
  38. package/scripts/browser-benchmark.mjs +92 -92
  39. package/scripts/browser-e2e.mjs +117 -117
  40. package/scripts/capability-e2e.mjs +31 -31
  41. package/scripts/clean-install-smoke.mjs +176 -162
  42. package/scripts/codex-mcp-smoke.mjs +119 -119
  43. package/scripts/context-filter-e2e.mjs +45 -45
  44. package/scripts/fail-open-smoke.mjs +120 -120
  45. package/scripts/feature-flags-smoke.mjs +46 -46
  46. package/scripts/mcp-receipt-smoke.mjs +115 -98
  47. package/scripts/model-route-report.mjs +15 -0
  48. package/scripts/openrouter-choice.mjs +59 -59
  49. package/scripts/replay-eval.mjs +124 -124
  50. package/scripts/smoke.mjs +34 -34
  51. package/scripts/supervision-e2e.mjs +138 -109
  52. package/src/browser.mjs +583 -569
  53. package/src/cli.mjs +33 -33
  54. package/src/computer.mjs +196 -0
  55. package/src/config.mjs +86 -75
  56. package/src/context-filter.mjs +56 -56
  57. package/src/contract.mjs +74 -72
  58. package/src/discovery.mjs +65 -65
  59. package/src/hermes-adapter.mjs +134 -0
  60. package/src/images.mjs +98 -0
  61. package/src/mcp-server.mjs +313 -210
  62. package/src/model-route-metrics.mjs +89 -0
  63. package/src/model-routing.mjs +87 -0
  64. package/src/providers/demo.mjs +52 -52
  65. package/src/providers/index.mjs +12 -0
  66. package/src/providers/typesafe.mjs +206 -126
  67. package/src/receipts.mjs +228 -226
  68. package/src/registry.mjs +109 -109
  69. package/src/relevance-filter.mjs +99 -99
  70. package/src/route.mjs +228 -221
  71. package/src/shadow-compaction.mjs +170 -0
  72. package/src/supervision.mjs +268 -244
  73. package/test/browser.test.mjs +222 -199
  74. package/test/capability.test.mjs +54 -54
  75. package/test/computer.test.mjs +195 -0
  76. package/test/context-filter.test.mjs +65 -65
  77. package/test/hermes-adapter.test.mjs +63 -0
  78. package/test/image-fixture.mjs +2 -0
  79. package/test/images.test.mjs +85 -0
  80. package/test/model-route-metrics.test.mjs +61 -0
  81. package/test/model-routing.test.mjs +59 -0
  82. package/test/openrouter-provider.test.mjs +55 -55
  83. package/test/provider-transports.test.mjs +205 -0
  84. package/test/providers.test.mjs +125 -0
  85. package/test/receipts.test.mjs +71 -71
  86. package/test/route.test.mjs +86 -74
  87. package/test/shadow-compaction.test.mjs +67 -0
  88. package/test/supervision.test.mjs +117 -99
@@ -1,55 +1,55 @@
1
- import assert from "node:assert/strict";
2
- import { test } from "node:test";
3
- import { OpenRouterDecisionsProvider } from "../src/providers/typesafe.mjs";
4
-
5
- const candidates = [{
6
- id: "search",
7
- kind: "mcp",
8
- name: "Repository search",
9
- description: "Search repository files",
10
- risk: "low",
11
- }];
12
-
13
- test("OpenRouter provider sends Decisions Choice payload, not chat completion payload", async () => {
14
- let requestUrl;
15
- let requestInit;
16
- const provider = new OpenRouterDecisionsProvider({
17
- apiKey: "test-key",
18
- endpoint: "https://openrouter.ai/api/alpha/decisions",
19
- model: "typesafe/jev-1.13",
20
- fetchImpl: async (url, init) => {
21
- requestUrl = url;
22
- requestInit = init;
23
- return new Response(JSON.stringify({
24
- id: "decision-test",
25
- model: "typesafe/jev-1.13",
26
- provider: "TypeSafe",
27
- answers: {
28
- tool: {
29
- type: "choice",
30
- choice: "search",
31
- probabilities: { search: 1 },
32
- confidence: 1,
33
- },
34
- },
35
- usage: { input_tokens: 10, output_tokens: 2, cost: 0.000001 },
36
- }), { status: 200, headers: { "content-type": "application/json" } });
37
- },
38
- });
39
-
40
- const raw = await provider.decide({
41
- state: { intent: "Search repository files" },
42
- candidates,
43
- });
44
- const body = JSON.parse(requestInit.body);
45
- assert.equal(requestUrl, "https://openrouter.ai/api/alpha/decisions");
46
- assert.equal(requestInit.method, "POST");
47
- assert.equal(requestInit.headers.Authorization, "Bearer test-key");
48
- assert.equal(body.model, "typesafe/jev-1.13");
49
- assert.equal(body.questions.tool.type, "choice");
50
- assert.equal(body.questions.tool.criteria.search.description, "Search repository files");
51
- assert.deepEqual(body.state, { intent: "Search repository files" });
52
- assert.equal("messages" in body, false);
53
- assert.equal(raw.answers.tool.choice, "search");
54
- assert.equal(raw.usage.cost, 0.000001);
55
- });
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { OpenRouterDecisionsProvider } from "../src/providers/typesafe.mjs";
4
+
5
+ const candidates = [{
6
+ id: "search",
7
+ kind: "mcp",
8
+ name: "Repository search",
9
+ description: "Search repository files",
10
+ risk: "low",
11
+ }];
12
+
13
+ test("OpenRouter provider sends Decisions Choice payload, not chat completion payload", async () => {
14
+ let requestUrl;
15
+ let requestInit;
16
+ const provider = new OpenRouterDecisionsProvider({
17
+ apiKey: "test-key",
18
+ endpoint: "https://openrouter.ai/api/alpha/decisions",
19
+ model: "typesafe/jev-1.13",
20
+ fetchImpl: async (url, init) => {
21
+ requestUrl = url;
22
+ requestInit = init;
23
+ return new Response(JSON.stringify({
24
+ id: "decision-test",
25
+ model: "typesafe/jev-1.13",
26
+ provider: "TypeSafe",
27
+ answers: {
28
+ tool: {
29
+ type: "choice",
30
+ choice: "search",
31
+ probabilities: { search: 1 },
32
+ confidence: 1,
33
+ },
34
+ },
35
+ usage: { input_tokens: 10, output_tokens: 2, cost: 0.000001 },
36
+ }), { status: 200, headers: { "content-type": "application/json" } });
37
+ },
38
+ });
39
+
40
+ const raw = await provider.decide({
41
+ state: { intent: "Search repository files" },
42
+ candidates,
43
+ });
44
+ const body = JSON.parse(requestInit.body);
45
+ assert.equal(requestUrl, "https://openrouter.ai/api/alpha/decisions");
46
+ assert.equal(requestInit.method, "POST");
47
+ assert.equal(requestInit.headers.Authorization, "Bearer test-key");
48
+ assert.equal(body.model, "typesafe/jev-1.13");
49
+ assert.equal(body.questions.tool.type, "choice");
50
+ assert.equal(body.questions.tool.criteria.search.description, "Search repository files");
51
+ assert.deepEqual(body.state, { intent: "Search repository files" });
52
+ assert.equal("messages" in body, false);
53
+ assert.equal(raw.answers.tool.choice, "search");
54
+ assert.equal(raw.usage.cost, 0.000001);
55
+ });
@@ -0,0 +1,205 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { once } from "node:events";
4
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { createServer } from "node:http";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+ import { test } from "node:test";
9
+ import { fileURLToPath } from "node:url";
10
+ import { routeRequest } from "../src/route.mjs";
11
+
12
+ const root = fileURLToPath(new URL("..", import.meta.url));
13
+ const request = {
14
+ intent: "Search repository files",
15
+ actor_permissions: ["read"],
16
+ capabilities: [
17
+ { id: "search", name: "Repository search", description: "Search repository files", risk: "low", permissions: ["read"] },
18
+ { id: "clock", name: "Clock", description: "Report the current time", risk: "low", permissions: ["read"] },
19
+ ],
20
+ };
21
+ const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(JEV_|TYPESAFE_|OPENROUTER_)/.test(key)));
22
+ const hasPython = spawnSync("python3", ["--version"]).status === 0;
23
+
24
+ test("provider configuration reaches CLI, MCP and adapters over local HTTP", { timeout: 30_000 }, async (t) => {
25
+ const workspace = await mkdtemp(join(tmpdir(), "jev-providers-"));
26
+ t.after(() => rm(workspace, { recursive: true, force: true }));
27
+ const calls = [];
28
+ const server = createServer(async (incoming, outgoing) => {
29
+ let text = "";
30
+ for await (const chunk of incoming) text += chunk;
31
+ const body = JSON.parse(text);
32
+ calls.push({ url: incoming.url, headers: incoming.headers, body });
33
+ if (incoming.url === "/timeout") return;
34
+ const answers = Object.fromEntries(Object.entries(body.questions).map(([id, question]) => {
35
+ if (question.type === "noul") return [id, { type: "noul", noul: 0.1 }];
36
+ const ids = Object.keys(question.criteria);
37
+ const choice = ids.includes("search") ? "search" : ids[0];
38
+ return [id, { type: "choice", choice, confidence: 1, probabilities: Object.fromEntries(ids.map((key) => [key, key === choice ? 1 : 0])) }];
39
+ }));
40
+ outgoing.setHeader("Content-Type", "application/json");
41
+ outgoing.end(JSON.stringify({ model: body.model, answers, usage: { input_tokens: 10, output_tokens: 2 } }));
42
+ });
43
+ server.listen(0, "127.0.0.1");
44
+ await once(server, "listening");
45
+ t.after(() => { server.closeAllConnections(); server.close(); });
46
+ const endpoint = `http://127.0.0.1:${server.address().port}`;
47
+ const configPath = join(workspace, "config.json");
48
+ const casesPath = join(workspace, "cases.jsonl");
49
+ const env = {
50
+ ...cleanEnv,
51
+ JEV_CONFIG: configPath,
52
+ JEV_REPLAY_CASES: casesPath,
53
+ TYPESAFE_API_KEY: "fixture-typesafe",
54
+ TYPESAFE_ENDPOINT: `${endpoint}/typesafe`,
55
+ TYPESAFE_MODEL: "fixture-typesafe-model",
56
+ OPENROUTER_API_KEY: "fixture-openrouter",
57
+ OPENROUTER_DECISIONS_ENDPOINT: `${endpoint}/openrouter`,
58
+ OPENROUTER_DECISIONS_MODEL: "fixture-openrouter-model",
59
+ JEV_NODE: process.execPath,
60
+ PYTHONDONTWRITEBYTECODE: "1",
61
+ };
62
+
63
+ async function run(args, input, overrides = {}, command = process.execPath) {
64
+ const child = spawn(command, args, { cwd: workspace, env: { ...env, ...overrides }, stdio: ["pipe", "pipe", "pipe"], timeout: 10_000 });
65
+ let stdout = "";
66
+ let stderr = "";
67
+ child.stdout.on("data", (chunk) => { stdout += chunk; });
68
+ child.stderr.on("data", (chunk) => { stderr += chunk; });
69
+ child.stdin.end(input);
70
+ const [code] = await once(child, "close");
71
+ assert.equal(code, 0, stderr);
72
+ assert.equal(stderr, "");
73
+ return stdout.trim().split(/\r?\n/).map((line) => JSON.parse(line));
74
+ }
75
+
76
+ async function cli(payload = request, args = [], overrides = {}) {
77
+ return (await run([join(root, "bin/jev.mjs"), "cli", ...args], JSON.stringify(payload), overrides))[0];
78
+ }
79
+
80
+ async function mcp(payload = request, overrides = {}, name = "jev_route") {
81
+ const messages = [
82
+ { jsonrpc: "2.0", id: 1, method: "initialize", params: {} },
83
+ { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name, arguments: payload } },
84
+ ];
85
+ const responses = await run([join(root, "bin/jev.mjs"), "mcp"], messages.map((message) => JSON.stringify(message)).join("\n") + "\n", overrides);
86
+ assert.equal(responses[0].result.serverInfo.name, "jev-layer");
87
+ assert.equal(responses[1].error, undefined);
88
+ return responses[1].result.structuredContent;
89
+ }
90
+
91
+ function selected(decision, provider, before) {
92
+ assert.equal(decision.status, "selected");
93
+ assert.equal(decision.selected, "search");
94
+ assert.ok(decision.correlation_id);
95
+ assert.deepEqual(decision.execution, { enabled: false, status: "not_started" });
96
+ assert.equal(calls.length, before + (provider === "demo" ? 0 : 1));
97
+ if (provider === "demo") return;
98
+ assert.equal(calls.at(-1).url, `/${provider}`);
99
+ assert.equal(calls.at(-1).headers.authorization, `Bearer fixture-${provider}`);
100
+ assert.equal(calls.at(-1).body.model, `fixture-${provider}-model`);
101
+ if (provider === "openrouter") assert.equal(decision.receipt.provider, "openrouter:fixture-openrouter-model");
102
+ }
103
+
104
+ for (const provider of ["demo", "openrouter", "typesafe"]) {
105
+ for (const transport of ["cli", "mcp"]) {
106
+ await t.test(`${provider} via ${transport}: explicit selection overrides environment and config`, async () => {
107
+ await writeFile(configPath, JSON.stringify({ provider: "unknown" }));
108
+ const before = calls.length;
109
+ const decision = transport === "cli"
110
+ ? await cli({ ...request, provider }, ["--provider", "unknown"], { JEV_LAYER_PROVIDER: "unknown" })
111
+ : await mcp({ ...request, provider }, { JEV_LAYER_PROVIDER: "unknown" });
112
+ selected(decision, provider, before);
113
+ });
114
+ await t.test(`${provider} via ${transport}: environment overrides config`, async () => {
115
+ await writeFile(configPath, JSON.stringify({ provider: "unknown" }));
116
+ const before = calls.length;
117
+ const decision = transport === "cli"
118
+ ? await cli(request, [], { JEV_LAYER_PROVIDER: provider })
119
+ : await mcp(request, { JEV_LAYER_PROVIDER: provider });
120
+ selected(decision, provider, before);
121
+ });
122
+ await t.test(`${provider} via ${transport}: project configuration`, async () => {
123
+ await writeFile(configPath, JSON.stringify({ provider }));
124
+ const before = calls.length;
125
+ selected(await (transport === "cli" ? cli() : mcp()), provider, before);
126
+ });
127
+ }
128
+ await t.test(`${provider} via CLI flag overrides environment and config`, async () => {
129
+ await writeFile(configPath, JSON.stringify({ provider: "unknown" }));
130
+ const before = calls.length;
131
+ selected(await cli(request, ["--provider", provider], { JEV_LAYER_PROVIDER: "unknown" }), provider, before);
132
+ });
133
+ await t.test(`${provider} remains disabled without HTTP`, async () => {
134
+ const before = calls.length;
135
+ for (const decision of [await cli({ ...request, provider }, [], { JEV_LAYER_ENABLED: "0" }), await mcp({ ...request, provider }, { JEV_LAYER_ENABLED: "0" })]) {
136
+ assert.equal(decision.fallback.type, "disabled");
137
+ assert.equal(decision.execution.enabled, false);
138
+ }
139
+ assert.equal(calls.length, before);
140
+ });
141
+ }
142
+
143
+ for (const provider of ["openrouter", "typesafe"]) {
144
+ await writeFile(configPath, JSON.stringify({ provider, providers: { [provider]: {
145
+ api_key_env: "FIXTURE_KEY", endpoint_env: "FIXTURE_ENDPOINT", model_env: "FIXTURE_MODEL",
146
+ } } }));
147
+ const aliases = { FIXTURE_KEY: `fixture-${provider}`, FIXTURE_ENDPOINT: `${endpoint}/${provider}`, FIXTURE_MODEL: `fixture-${provider}-model` };
148
+ const guardedEnv = { ...aliases, TYPESAFE_API_KEY: "", OPENROUTER_API_KEY: "", TYPESAFE_ENDPOINT: `${endpoint}/wrong`, OPENROUTER_DECISIONS_ENDPOINT: `${endpoint}/wrong` };
149
+ for (const transport of ["cli", "mcp"]) {
150
+ await t.test(`${provider} via ${transport}: configured environment variable names`, async () => {
151
+ const before = calls.length;
152
+ selected(await (transport === "cli" ? cli(request, [], guardedEnv) : mcp(request, guardedEnv)), provider, before);
153
+ });
154
+ await t.test(`${provider} via ${transport}: missing configured key fails open`, async () => {
155
+ const before = calls.length;
156
+ const decision = await (transport === "cli" ? cli(request) : mcp());
157
+ assert.equal(decision.fallback.type, "provider_error");
158
+ assert.equal(calls.length, before);
159
+ });
160
+ }
161
+ await t.test(`${provider} configuration reaches MCP supervision and browser decisions`, async () => {
162
+ const before = calls.length;
163
+ const supervised = await mcp({ enabled: true, job: {}, observation: {} }, guardedEnv, "jev_supervise");
164
+ assert.equal(supervised.status, "judged");
165
+ const browser = await mcp({ enabled: true, goal: "Scroll down", observation: { scroll: { down: true } } }, guardedEnv, "jev_browser_step");
166
+ assert.equal(browser.status, "selected");
167
+ assert.equal(browser.selected, "browser:scroll:down");
168
+ assert.equal(browser.execution.enabled, false);
169
+ assert.equal(calls.length, before + 2);
170
+ assert.equal(calls.at(-1).url, `/${provider}`);
171
+ });
172
+ await t.test(`${provider} times out an actual stalled HTTP request`, async () => {
173
+ const decision = await routeRequest(request, { provider, [provider]: { apiKey: "fixture-key", endpoint: `${endpoint}/timeout`, timeoutMs: 50 } });
174
+ assert.equal(decision.fallback.type, "provider_error");
175
+ assert.match(decision.reason, /timed out/);
176
+ assert.equal(decision.execution.enabled, false);
177
+ });
178
+
179
+ await t.test(`${provider} project configuration reaches the OMP Node adapter`, async () => {
180
+ const before = calls.length;
181
+ const code = `
182
+ import extension from ${JSON.stringify(new URL("../integrations/omp/extension.js", import.meta.url).href)};
183
+ const field = { optional() { return this; } };
184
+ const zod = { string: () => field, unknown: () => field, array: () => field, object: () => field };
185
+ let tool;
186
+ extension({ zod, registerTool(value) { tool = value; } });
187
+ console.log(JSON.stringify((await tool.execute("fixture", ${JSON.stringify(request)})).details));
188
+ `;
189
+ selected((await run(["--input-type=module", "-e", code], "", guardedEnv))[0], provider, before);
190
+ });
191
+ await t.test(`${provider} project configuration reaches the Hermes adapter`, { skip: !hasPython }, async () => {
192
+ const before = calls.length;
193
+ const code = "import importlib.util, json, pathlib, sys\np = pathlib.Path(sys.argv[1])\nspec = importlib.util.spec_from_file_location('fixture_hermes', p, submodule_search_locations=[str(p.parent)])\nmodule = importlib.util.module_from_spec(spec)\nsys.modules[spec.name] = module\nspec.loader.exec_module(module)\nprint(module.jev_route(json.load(sys.stdin)))";
194
+ selected((await run(["-c", code, join(root, "integrations/hermes/__init__.py")], JSON.stringify(request), guardedEnv, "python3"))[0], provider, before);
195
+ });
196
+ }
197
+
198
+ await t.test("MCP persists routing cases with their correlation ids", async () => {
199
+ const records = (await readFile(casesPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
200
+ assert.ok(records.some((record) => record.decision?.receipt?.provider === "typesafe"));
201
+ for (const record of records.filter((record) => record.record_type === "routing_case")) {
202
+ assert.equal(record.case_id, record.correlation_id);
203
+ }
204
+ });
205
+ });
@@ -0,0 +1,125 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { routeRequest } from "../src/route.mjs";
4
+
5
+ // Keep these offline checks independent of a developer's provider environment.
6
+ for (const key of Object.keys(process.env)) {
7
+ if (/^(JEV_|TYPESAFE_|OPENROUTER_)/.test(key)) delete process.env[key];
8
+ }
9
+
10
+ const request = {
11
+ intent: "Search repository files",
12
+ actor_permissions: ["read"],
13
+ capabilities: [
14
+ { id: "search", name: "Repository search", description: "Search repository files", permissions: ["read"], risk: "low" },
15
+ { id: "clock", name: "Clock", description: "Report the current time", permissions: ["read"], risk: "low" },
16
+ ],
17
+ };
18
+ const answer = { type: "choice", choice: "search", probabilities: { search: 0.9, clock: 0.1 }, confidence: 0.8 };
19
+
20
+ test("demo routes deterministically without a network call or credentials", async (t) => {
21
+ t.mock.method(globalThis, "fetch", () => { throw new Error("demo must remain offline"); });
22
+ const first = await routeRequest(request, { provider: "demo" });
23
+ const second = await routeRequest(request, { provider: "demo" });
24
+ assert.equal(first.status, "selected");
25
+ assert.equal(first.selected, "search");
26
+ assert.deepEqual(first.probabilities, second.probabilities);
27
+ assert.equal(first.receipt.provider, "jev-demo");
28
+ assert.equal(first.execution.enabled, false);
29
+ assert.equal(globalThis.fetch.mock.callCount(), 0);
30
+ });
31
+
32
+ for (const [provider, endpoint] of [
33
+ ["typesafe", "https://api.typesafe.ai/v1/systemone"],
34
+ ["openrouter", "https://openrouter.ai/api/alpha/decisions"],
35
+ ]) {
36
+ test(`${provider} routes with the documented endpoint, auth and Choice contract`, async () => {
37
+ let calls = 0;
38
+ const decision = await routeRequest(request, {
39
+ provider,
40
+ [provider]: {
41
+ apiKey: "fixture-key",
42
+ httpReferer: "https://example.test",
43
+ appTitle: "Jev fixture",
44
+ async fetchImpl(url, init) {
45
+ calls += 1;
46
+ assert.equal(url, endpoint);
47
+ assert.equal(init.method, "POST");
48
+ assert.equal(init.headers.Authorization, "Bearer fixture-key");
49
+ assert.equal(init.headers["Content-Type"], "application/json");
50
+ assert.ok(init.signal instanceof AbortSignal);
51
+ if (provider === "openrouter") {
52
+ assert.equal(init.headers["HTTP-Referer"], "https://example.test");
53
+ assert.equal(init.headers["X-Title"], "Jev fixture");
54
+ }
55
+ const body = JSON.parse(init.body);
56
+ assert.equal(body.state.intent, request.intent);
57
+ assert.equal(body.questions.tool.type, "choice");
58
+ assert.deepEqual(Object.keys(body.questions.tool.criteria), ["search", "clock"]);
59
+ assert.equal(body.questions.tool.criteria.search.description, request.capabilities[0].description);
60
+ assert.equal("messages" in body, false);
61
+ return Response.json({ model: body.model, answers: { tool: answer }, usage: { input_tokens: 10, output_tokens: 2 } });
62
+ },
63
+ },
64
+ });
65
+ assert.equal(calls, 1);
66
+ assert.equal(decision.status, "selected");
67
+ assert.equal(decision.selected, "search");
68
+ assert.deepEqual(decision.probabilities, answer.probabilities);
69
+ assert.equal(decision.confidence, answer.confidence);
70
+ assert.deepEqual(decision.execution, { enabled: false, status: "not_started" });
71
+ });
72
+
73
+ test(`${provider} never calls HTTP when credentials are missing or routing is deterministic`, async () => {
74
+ let calls = 0;
75
+ const options = { provider, [provider]: { apiKey: "", fetchImpl: async () => { calls += 1; throw new Error("must not call HTTP"); } } };
76
+ const missing = await routeRequest(request, options);
77
+ assert.equal(missing.status, "fallback");
78
+ assert.equal(missing.fallback.type, "provider_error");
79
+ assert.match(missing.reason, /API_KEY is not configured/);
80
+ const deterministic = await routeRequest({ ...request, policy: { deterministic: true, deterministic_capability_id: "search" } }, options);
81
+ assert.equal(deterministic.status, "selected");
82
+ assert.equal(calls, 0);
83
+ });
84
+
85
+ for (const status of [401, 422, 429, 529]) {
86
+ test(`${provider} fails open on HTTP ${status} without retries`, async () => {
87
+ let calls = 0;
88
+ const decision = await routeRequest(request, { provider, [provider]: {
89
+ apiKey: "fixture-key",
90
+ async fetchImpl() { calls += 1; return new Response("fixture error", { status }); },
91
+ } });
92
+ assert.equal(calls, 1);
93
+ assert.equal(decision.status, "fallback");
94
+ assert.equal(decision.fallback.type, "provider_error");
95
+ assert.match(decision.reason, new RegExp(`HTTP ${status}`));
96
+ assert.equal(decision.execution.enabled, false);
97
+ });
98
+ }
99
+
100
+ for (const [name, response, fallback] of [
101
+ ["invalid JSON", "{", "provider_error"],
102
+ ["missing answers", {}, "provider_error"],
103
+ ["wrong answer type", { answers: { tool: { type: "score" } } }, "malformed_response"],
104
+ ["unknown capability", { answers: { tool: { ...answer, choice: "unknown" } } }, "invalid_selection"],
105
+ ["low confidence", { answers: { tool: { ...answer, confidence: 0.1 } } }, "low_confidence"],
106
+ ]) {
107
+ test(`${provider} fails open on ${name}`, async () => {
108
+ const decision = await routeRequest(request, { provider, [provider]: {
109
+ apiKey: "fixture-key",
110
+ fetchImpl: async () => new Response(typeof response === "string" ? response : JSON.stringify(response)),
111
+ } });
112
+ assert.equal(decision.status, "fallback");
113
+ assert.equal(decision.fallback.type, fallback);
114
+ assert.equal(decision.execution.enabled, false);
115
+ });
116
+ }
117
+ }
118
+
119
+ test("unsupported providers preserve the routing fallback envelope", async () => {
120
+ const decision = await routeRequest(request, { provider: "unknown" });
121
+ assert.equal(decision.status, "fallback");
122
+ assert.equal(decision.fallback.type, "provider_error");
123
+ assert.ok(decision.correlation_id);
124
+ assert.equal(decision.execution.enabled, false);
125
+ });
@@ -1,71 +1,71 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtemp, readFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { test } from "node:test";
6
- import { appendExecutionReceipt, appendRoutingCase, readRoutingCases } from "../src/receipts.mjs";
7
- import { routeRequest } from "../src/route.mjs";
8
-
9
- const request = {
10
- harness: "receipt-test",
11
- intent: "inspect the repository",
12
- context: { workspace: "test" },
13
- actor_permissions: ["read"],
14
- capabilities: [{
15
- id: "read_repo",
16
- kind: "tool",
17
- name: "Read repository",
18
- description: "Read repository metadata without modifying files",
19
- permissions: ["read"],
20
- risk: "low",
21
- }],
22
- };
23
-
24
- test("execution receipt carries one correlation id through host completion", async () => {
25
- const provider = {
26
- name: "receipt-test-provider",
27
- async decide() {
28
- return {
29
- answers: {
30
- tool: {
31
- type: "choice",
32
- choice: "read_repo",
33
- probabilities: { read_repo: 1 },
34
- confidence: 1,
35
- },
36
- },
37
- usage: { cost: 0.000123 },
38
- };
39
- },
40
- };
41
- const decision = await routeRequest(request, { provider });
42
- const path = join(await mkdtemp(join(tmpdir(), "jev-receipts-")), "cases.jsonl");
43
- await appendRoutingCase({ path, request, decision });
44
- const completed = await appendExecutionReceipt({
45
- path,
46
- request,
47
- decision,
48
- host: {
49
- harness: "receipt-test",
50
- capability_id: "read_repo",
51
- status: "completed",
52
- result: { bytes: 42 },
53
- exit_status: 0,
54
- duration_ms: 12.5,
55
- },
56
- });
57
-
58
- assert.equal(decision.correlation_id, decision.receipt.correlation_id);
59
- assert.equal(decision.receipt.cost_usd, 0.000123);
60
- assert.equal(completed.record.correlation_id, decision.correlation_id);
61
- assert.equal(completed.record.jev.cost_usd, 0.000123);
62
- assert.equal(completed.record.host.exit_status, 0);
63
- assert.equal(completed.record.host.duration_ms, 12.5);
64
- assert.deepEqual(completed.record.host.result, { bytes: 42 });
65
-
66
- const routingCases = await readRoutingCases(path);
67
- assert.equal(routingCases.length, 1);
68
- assert.equal(routingCases[0].case_id, decision.correlation_id);
69
- const firstRecord = JSON.parse((await readFile(path, "utf8")).split(/\r?\n/)[0]);
70
- assert.equal(firstRecord.record_type, "routing_case");
71
- });
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import { appendExecutionReceipt, appendRoutingCase, readRoutingCases } from "../src/receipts.mjs";
7
+ import { routeRequest } from "../src/route.mjs";
8
+
9
+ const request = {
10
+ harness: "receipt-test",
11
+ intent: "inspect the repository",
12
+ context: { workspace: "test" },
13
+ actor_permissions: ["read"],
14
+ capabilities: [{
15
+ id: "read_repo",
16
+ kind: "tool",
17
+ name: "Read repository",
18
+ description: "Read repository metadata without modifying files",
19
+ permissions: ["read"],
20
+ risk: "low",
21
+ }],
22
+ };
23
+
24
+ test("execution receipt carries one correlation id through host completion", async () => {
25
+ const provider = {
26
+ name: "receipt-test-provider",
27
+ async decide() {
28
+ return {
29
+ answers: {
30
+ tool: {
31
+ type: "choice",
32
+ choice: "read_repo",
33
+ probabilities: { read_repo: 1 },
34
+ confidence: 1,
35
+ },
36
+ },
37
+ usage: { cost: 0.000123 },
38
+ };
39
+ },
40
+ };
41
+ const decision = await routeRequest(request, { provider });
42
+ const path = join(await mkdtemp(join(tmpdir(), "jev-receipts-")), "cases.jsonl");
43
+ await appendRoutingCase({ path, request, decision });
44
+ const completed = await appendExecutionReceipt({
45
+ path,
46
+ request,
47
+ decision,
48
+ host: {
49
+ harness: "receipt-test",
50
+ capability_id: "read_repo",
51
+ status: "completed",
52
+ result: { bytes: 42 },
53
+ exit_status: 0,
54
+ duration_ms: 12.5,
55
+ },
56
+ });
57
+
58
+ assert.equal(decision.correlation_id, decision.receipt.correlation_id);
59
+ assert.equal(decision.receipt.cost_usd, 0.000123);
60
+ assert.equal(completed.record.correlation_id, decision.correlation_id);
61
+ assert.equal(completed.record.jev.cost_usd, 0.000123);
62
+ assert.equal(completed.record.host.exit_status, 0);
63
+ assert.equal(completed.record.host.duration_ms, 12.5);
64
+ assert.deepEqual(completed.record.host.result, { bytes: 42 });
65
+
66
+ const routingCases = await readRoutingCases(path);
67
+ assert.equal(routingCases.length, 1);
68
+ assert.equal(routingCases[0].case_id, decision.correlation_id);
69
+ const firstRecord = JSON.parse((await readFile(path, "utf8")).split(/\r?\n/)[0]);
70
+ assert.equal(firstRecord.record_type, "routing_case");
71
+ });