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
|
@@ -0,0 +1,62 @@
|
|
|
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 { spawn } from "node:child_process";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
|
|
8
|
+
const adapter = new URL("../src/hermes-adapter.mjs", import.meta.url).pathname;
|
|
9
|
+
|
|
10
|
+
function run(lines, replayPath) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const child = spawn(process.execPath, [adapter], { env: { ...process.env, JEV_REPLAY_CASES: replayPath } });
|
|
13
|
+
let out = "";
|
|
14
|
+
let err = "";
|
|
15
|
+
child.stdout.on("data", (chunk) => { out += chunk; });
|
|
16
|
+
child.stderr.on("data", (chunk) => { err += chunk; });
|
|
17
|
+
child.on("error", reject);
|
|
18
|
+
child.on("close", (code) => {
|
|
19
|
+
if (code !== 0) return reject(new Error(err || `adapter exited ${code}`));
|
|
20
|
+
resolve(out.trim().split(/\r?\n/).filter(Boolean).map(JSON.parse));
|
|
21
|
+
});
|
|
22
|
+
child.stdin.end(lines.map((line) => JSON.stringify(line)).join("\n") + "\n");
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("Hermes adapter routes a closed candidate set and records a correlated receipt", async () => {
|
|
27
|
+
const dir = await mkdtemp(join(tmpdir(), "jev-hermes-adapter-"));
|
|
28
|
+
const replayPath = join(dir, "cases.jsonl");
|
|
29
|
+
const routeRequest = { operation: "route", args: { harness: "hermes-test", intent: "Read a source file", provider: "demo", actor_permissions: ["read"], capabilities: [
|
|
30
|
+
{ id: "read", kind: "tool", name: "Read file", description: "Read a local text file", permissions: ["read"], risk: "low", available: true },
|
|
31
|
+
{ id: "write", kind: "tool", name: "Write file", description: "Mutate a local file", permissions: ["write"], risk: "high", available: true },
|
|
32
|
+
] } };
|
|
33
|
+
const [decision] = await run([routeRequest], replayPath);
|
|
34
|
+
assert.equal(decision.status, "selected");
|
|
35
|
+
assert.equal(decision.selected, "read");
|
|
36
|
+
assert.ok(decision.correlation_id);
|
|
37
|
+
assert.ok(decision._jev_request);
|
|
38
|
+
const [receipt] = await run([{ operation: "record_execution", decision, args: { correlation_id: decision.correlation_id, harness: "hermes-test", capability_id: "read", status: "completed", result: { lines: 3 }, duration_ms: 4 } }], replayPath);
|
|
39
|
+
assert.equal(receipt.record_type, "execution_receipt");
|
|
40
|
+
assert.equal(receipt.correlation_id, decision.correlation_id);
|
|
41
|
+
assert.equal(receipt.host.status, "completed");
|
|
42
|
+
const records = (await readFile(replayPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
|
|
43
|
+
assert.equal(records.filter((record) => record.record_type === "routing_case").length, 1);
|
|
44
|
+
assert.equal(records.filter((record) => record.record_type === "execution_receipt").length, 1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("Hermes adapter exposes supervision, shadow-compaction, and browser surfaces without execution", async () => {
|
|
48
|
+
const dir = await mkdtemp(join(tmpdir(), "jev-hermes-adapter-"));
|
|
49
|
+
const replayPath = join(dir, "cases.jsonl");
|
|
50
|
+
const [supervision, shadow, browser] = await run([
|
|
51
|
+
{ operation: "supervise", args: { provider: "demo", enabled: true, harness: "hermes-test", job: { goal: "verify" }, observation: { state: "done" }, evidence: { tests_passed: true } } },
|
|
52
|
+
{ operation: "shadow_compaction", args: { provider: "demo", intent: "review context", context: { messages: ["old note", "Requirement: preserve /workspace/project/config.json"] } } },
|
|
53
|
+
{ operation: "browser_step", args: { provider: "demo", enabled: true, harness: "hermes-test", goal: "Open the visible docs link", observation: { url: "https://example.test", targets: [{ id: "docs", name: "Docs", href: "/docs", visible: true, clickable: true }], tabs: [], scroll: { down: false, up: false } } } },
|
|
54
|
+
], replayPath);
|
|
55
|
+
assert.equal(supervision.status, "judged");
|
|
56
|
+
assert.ok(["continue", "verify", "retry", "finish", "escalate"].includes(supervision.action));
|
|
57
|
+
assert.equal(shadow.mode, "jev_shadow");
|
|
58
|
+
assert.equal(shadow.changed, false);
|
|
59
|
+
assert.equal(shadow.protected_count, 1);
|
|
60
|
+
assert.ok(["selected", "fallback", "no_decision", "needs_confirmation"].includes(browser.status));
|
|
61
|
+
assert.equal(browser.execution.enabled, false);
|
|
62
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { buildModelRouteReport } from "../src/model-route-metrics.mjs";
|
|
4
|
+
|
|
5
|
+
const rows = [
|
|
6
|
+
{
|
|
7
|
+
record_type: "routing_case",
|
|
8
|
+
correlation_id: "one",
|
|
9
|
+
request: { context: { model_route: { mode: "shadow", profiles: [{ id: "fast" }, { id: "frontier" }] } } },
|
|
10
|
+
decision: { status: "selected", selected: "fast", receipt: { latency_ms: 4, cost_usd: 0.001 } },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
record_type: "execution_receipt",
|
|
14
|
+
correlation_id: "one",
|
|
15
|
+
host: {
|
|
16
|
+
status: "completed",
|
|
17
|
+
duration_ms: 20,
|
|
18
|
+
result: { model_route: { actual_model_id: "fast", retry_count: 1, outcome: "verified" } },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
record_type: "routing_case",
|
|
23
|
+
correlation_id: "two",
|
|
24
|
+
request: { context: { model_route: { mode: "shadow", profiles: [{ id: "fast" }, { id: "frontier" }] } } },
|
|
25
|
+
decision: { status: "selected", selected: "frontier", receipt: { latency_ms: 6, cost_usd: 0.002 } },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
record_type: "execution_receipt",
|
|
29
|
+
correlation_id: "two",
|
|
30
|
+
host: {
|
|
31
|
+
status: "failed",
|
|
32
|
+
duration_ms: 30,
|
|
33
|
+
result: { model_route: { actual_model_id: "fast", retry_count: 2, outcome: "failed" } },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
test("model route report joins shadow recommendation to host outcome without treating it as a route change", () => {
|
|
39
|
+
const report = buildModelRouteReport(rows);
|
|
40
|
+
assert.equal(report.schema_version, 1);
|
|
41
|
+
assert.equal(report.summary.decisions, 2);
|
|
42
|
+
assert.equal(report.summary.with_host_outcome, 2);
|
|
43
|
+
assert.equal(report.summary.recommendation_match_rate, 0.5);
|
|
44
|
+
assert.equal(report.summary.verified_outcomes, 1);
|
|
45
|
+
assert.equal(report.summary.failed_outcomes, 1);
|
|
46
|
+
assert.equal(report.summary.total_retry_count, 3);
|
|
47
|
+
assert.deepEqual(report.models.map((row) => row.recommended_model_id), ["fast", "frontier"]);
|
|
48
|
+
assert.equal(report.models[0].host_actual_model_ids.fast, 1);
|
|
49
|
+
assert.equal(report.models[1].host_actual_model_ids.fast, 1);
|
|
50
|
+
assert.equal(report.models[1].recommendation_matches, 0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("model route report fails open on incomplete or unrelated receipts", () => {
|
|
54
|
+
const report = buildModelRouteReport([
|
|
55
|
+
{ record_type: "routing_case", correlation_id: "unrelated", request: { context: {} }, decision: { selected: "tool" } },
|
|
56
|
+
{ record_type: "routing_case", correlation_id: "pending", request: { context: { model_route: { mode: "shadow" } } }, decision: { selected: null } },
|
|
57
|
+
]);
|
|
58
|
+
assert.equal(report.summary.decisions, 1);
|
|
59
|
+
assert.equal(report.summary.with_host_outcome, 0);
|
|
60
|
+
assert.equal(report.models[0].recommended_model_id, "unrecommended");
|
|
61
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { recommendModelRoute } from "../src/model-routing.mjs";
|
|
4
|
+
|
|
5
|
+
const models = [
|
|
6
|
+
{
|
|
7
|
+
id: "fast",
|
|
8
|
+
provider: "openrouter",
|
|
9
|
+
model: "small-model",
|
|
10
|
+
reasoning_effort: "low",
|
|
11
|
+
description: "fast and cheap for simple classification or file inspection",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "frontier",
|
|
15
|
+
provider: "openai-codex",
|
|
16
|
+
model: "frontier-model",
|
|
17
|
+
reasoning_effort: "high",
|
|
18
|
+
description: "strong model for difficult implementation and ambiguous reasoning",
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
test("model routing returns an advisory profile and never changes the host route", async () => {
|
|
23
|
+
let captured;
|
|
24
|
+
const result = await recommendModelRoute({
|
|
25
|
+
intent: "classify a small batch of already structured feedback",
|
|
26
|
+
context: { task_size: "small", external_side_effects: false },
|
|
27
|
+
models,
|
|
28
|
+
provider: {
|
|
29
|
+
name: "model-route-test-provider",
|
|
30
|
+
async decide(input) {
|
|
31
|
+
captured = input;
|
|
32
|
+
return {
|
|
33
|
+
answers: { tool: { type: "choice", choice: "fast", probabilities: { fast: 0.92, frontier: 0.08 }, confidence: 0.92 } },
|
|
34
|
+
usage: { cost: 0.0001 },
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.equal(result.route_mode, "shadow");
|
|
41
|
+
assert.equal(result.status, "selected");
|
|
42
|
+
assert.equal(result.selected, "fast");
|
|
43
|
+
assert.equal(result.recommended_model.model, "small-model");
|
|
44
|
+
assert.equal(result.execution.enabled, false);
|
|
45
|
+
assert.equal(captured.candidates.every((candidate) => candidate.kind === "model"), true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("model routing excludes unavailable profiles and fails open with no selected profile", async () => {
|
|
49
|
+
const result = await recommendModelRoute({
|
|
50
|
+
intent: "inspect a file",
|
|
51
|
+
models: [{ ...models[0], available: false }],
|
|
52
|
+
provider: "demo",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
assert.equal(result.status, "no_decision");
|
|
56
|
+
assert.equal(result.selected, null);
|
|
57
|
+
assert.equal(result.recommended_model, null);
|
|
58
|
+
assert.equal(result.execution.enabled, false);
|
|
59
|
+
});
|
|
@@ -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,126 @@
|
|
|
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, model] of [
|
|
33
|
+
["typesafe", "https://api.typesafe.ai/v1/systemone", "jev-latest"],
|
|
34
|
+
["openrouter", "https://openrouter.ai/api/alpha/decisions", "typesafe/jev-1.13"],
|
|
35
|
+
]) {
|
|
36
|
+
test(`${provider} routes with the documented endpoint, model, 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.model, model);
|
|
57
|
+
assert.equal(body.state.intent, request.intent);
|
|
58
|
+
assert.equal(body.questions.tool.type, "choice");
|
|
59
|
+
assert.deepEqual(Object.keys(body.questions.tool.criteria), ["search", "clock"]);
|
|
60
|
+
assert.equal(body.questions.tool.criteria.search.description, request.capabilities[0].description);
|
|
61
|
+
assert.equal("messages" in body, false);
|
|
62
|
+
return Response.json({ model, answers: { tool: answer }, usage: { input_tokens: 10, output_tokens: 2 } });
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
assert.equal(calls, 1);
|
|
67
|
+
assert.equal(decision.status, "selected");
|
|
68
|
+
assert.equal(decision.selected, "search");
|
|
69
|
+
assert.deepEqual(decision.probabilities, answer.probabilities);
|
|
70
|
+
assert.equal(decision.confidence, answer.confidence);
|
|
71
|
+
assert.deepEqual(decision.execution, { enabled: false, status: "not_started" });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test(`${provider} never calls HTTP when credentials are missing or routing is deterministic`, async () => {
|
|
75
|
+
let calls = 0;
|
|
76
|
+
const options = { provider, [provider]: { apiKey: "", fetchImpl: async () => { calls += 1; throw new Error("must not call HTTP"); } } };
|
|
77
|
+
const missing = await routeRequest(request, options);
|
|
78
|
+
assert.equal(missing.status, "fallback");
|
|
79
|
+
assert.equal(missing.fallback.type, "provider_error");
|
|
80
|
+
assert.match(missing.reason, /API_KEY is not configured/);
|
|
81
|
+
const deterministic = await routeRequest({ ...request, policy: { deterministic: true, deterministic_capability_id: "search" } }, options);
|
|
82
|
+
assert.equal(deterministic.status, "selected");
|
|
83
|
+
assert.equal(calls, 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
for (const status of [401, 422, 429, 529]) {
|
|
87
|
+
test(`${provider} fails open on HTTP ${status} without retries`, async () => {
|
|
88
|
+
let calls = 0;
|
|
89
|
+
const decision = await routeRequest(request, { provider, [provider]: {
|
|
90
|
+
apiKey: "fixture-key",
|
|
91
|
+
async fetchImpl() { calls += 1; return new Response("fixture error", { status }); },
|
|
92
|
+
} });
|
|
93
|
+
assert.equal(calls, 1);
|
|
94
|
+
assert.equal(decision.status, "fallback");
|
|
95
|
+
assert.equal(decision.fallback.type, "provider_error");
|
|
96
|
+
assert.match(decision.reason, new RegExp(`HTTP ${status}`));
|
|
97
|
+
assert.equal(decision.execution.enabled, false);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const [name, response, fallback] of [
|
|
102
|
+
["invalid JSON", "{", "provider_error"],
|
|
103
|
+
["missing answers", {}, "provider_error"],
|
|
104
|
+
["wrong answer type", { answers: { tool: { type: "score" } } }, "malformed_response"],
|
|
105
|
+
["unknown capability", { answers: { tool: { ...answer, choice: "unknown" } } }, "invalid_selection"],
|
|
106
|
+
["low confidence", { answers: { tool: { ...answer, confidence: 0.1 } } }, "low_confidence"],
|
|
107
|
+
]) {
|
|
108
|
+
test(`${provider} fails open on ${name}`, async () => {
|
|
109
|
+
const decision = await routeRequest(request, { provider, [provider]: {
|
|
110
|
+
apiKey: "fixture-key",
|
|
111
|
+
fetchImpl: async () => new Response(typeof response === "string" ? response : JSON.stringify(response)),
|
|
112
|
+
} });
|
|
113
|
+
assert.equal(decision.status, "fallback");
|
|
114
|
+
assert.equal(decision.fallback.type, fallback);
|
|
115
|
+
assert.equal(decision.execution.enabled, false);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
test("unsupported providers preserve the routing fallback envelope", async () => {
|
|
121
|
+
const decision = await routeRequest(request, { provider: "unknown" });
|
|
122
|
+
assert.equal(decision.status, "fallback");
|
|
123
|
+
assert.equal(decision.fallback.type, "provider_error");
|
|
124
|
+
assert.ok(decision.correlation_id);
|
|
125
|
+
assert.equal(decision.execution.enabled, false);
|
|
126
|
+
});
|