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.
- package/CHANGELOG.md +73 -34
- package/CONTRIBUTING.md +146 -146
- package/LICENSE +21 -21
- package/README.md +125 -126
- package/README.ru.md +140 -117
- package/README.zh-CN.md +139 -117
- package/RELEASE.md +53 -53
- package/SECURITY.md +64 -56
- package/bin/jev.mjs +215 -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 +23 -22
- package/config/omp.mcp.json +14 -14
- package/config/providers.env.example +13 -13
- package/docs/AGENT-IMPLEMENTATION.md +306 -0
- package/docs/COMPATIBILITY.md +24 -15
- package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +187 -0
- package/docs/PROVIDERS.md +109 -0
- package/docs/SCHEMA-VERSIONING.md +101 -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 +156 -51
- package/integrations/hermes/plugin.yaml +11 -5
- package/integrations/hermes/schemas.py +120 -12
- package/integrations/omp/extension.js +106 -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 +176 -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 +115 -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 +583 -569
- package/src/cli.mjs +33 -33
- package/src/computer.mjs +196 -0
- package/src/config.mjs +86 -75
- package/src/context-filter.mjs +56 -56
- package/src/contract.mjs +74 -72
- package/src/discovery.mjs +65 -65
- package/src/hermes-adapter.mjs +134 -0
- package/src/images.mjs +98 -0
- package/src/mcp-server.mjs +313 -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 +206 -126
- package/src/receipts.mjs +228 -226
- package/src/registry.mjs +109 -109
- package/src/relevance-filter.mjs +99 -99
- package/src/route.mjs +228 -221
- package/src/shadow-compaction.mjs +170 -0
- package/src/supervision.mjs +268 -244
- package/test/browser.test.mjs +222 -199
- package/test/capability.test.mjs +54 -54
- package/test/computer.test.mjs +195 -0
- package/test/context-filter.test.mjs +65 -65
- package/test/hermes-adapter.test.mjs +63 -0
- package/test/image-fixture.mjs +2 -0
- package/test/images.test.mjs +85 -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 +125 -0
- package/test/receipts.test.mjs +71 -71
- package/test/route.test.mjs +86 -74
- package/test/shadow-compaction.test.mjs +67 -0
- package/test/supervision.test.mjs +117 -99
package/scripts/browser-e2e.mjs
CHANGED
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { createInterface } from "node:readline";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { dirname, join, resolve } from "node:path";
|
|
7
|
-
import { tmpdir } from "node:os";
|
|
8
|
-
|
|
9
|
-
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
-
const casesPath = join(await mkdtemp(join(tmpdir(), "jev-browser-e2e-")), "cases.jsonl");
|
|
11
|
-
const child = spawn(process.env.JEV_NODE ?? "node", [resolve(root, "src/mcp-server.mjs")], {
|
|
12
|
-
cwd: root,
|
|
13
|
-
env: { ...process.env, JEV_BROWSER_FAST_PATH: "1", JEV_REPLAY_CASES: casesPath },
|
|
14
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
-
});
|
|
16
|
-
const lines = createInterface({ input: child.stdout });
|
|
17
|
-
const pending = new Map();
|
|
18
|
-
lines.on("line", (line) => {
|
|
19
|
-
if (!line.trim()) return;
|
|
20
|
-
const message = JSON.parse(line);
|
|
21
|
-
const waiter = pending.get(message.id);
|
|
22
|
-
if (waiter) {
|
|
23
|
-
pending.delete(message.id);
|
|
24
|
-
waiter(message);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const initialized = await request(1, "initialize", {});
|
|
30
|
-
assert.equal(initialized.result.serverInfo.name, "jev-layer");
|
|
31
|
-
await notification("notifications/initialized", {});
|
|
32
|
-
const listed = await request(2, "tools/list", {});
|
|
33
|
-
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_browser_step"));
|
|
34
|
-
|
|
35
|
-
const routed = await request(3, "tools/call", {
|
|
36
|
-
name: "jev_browser_step",
|
|
37
|
-
arguments: {
|
|
38
|
-
provider: process.env.JEV_BROWSER_PROVIDER ?? "demo",
|
|
39
|
-
enabled: true,
|
|
40
|
-
harness: "browser-e2e",
|
|
41
|
-
goal: "scroll the page downward",
|
|
42
|
-
observation: {
|
|
43
|
-
url: "https://example.test",
|
|
44
|
-
title: "Fixture",
|
|
45
|
-
visible_text: "Additional content is below the fold",
|
|
46
|
-
scroll: { up: false, down: true },
|
|
47
|
-
targets: [],
|
|
48
|
-
tabs: [],
|
|
49
|
-
},
|
|
50
|
-
policy: { min_confidence: 0 },
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
const decision = routed.result.structuredContent;
|
|
54
|
-
assert.equal(decision.status, "selected");
|
|
55
|
-
assert.equal(decision.browser_action.operation, "scroll");
|
|
56
|
-
assert.equal(decision.browser_action.target_id, null);
|
|
57
|
-
|
|
58
|
-
const started = performance.now();
|
|
59
|
-
const hostResult = { operation: decision.browser_action.operation, direction: "down", fixture: true };
|
|
60
|
-
const recorded = await request(4, "tools/call", {
|
|
61
|
-
name: "jev_record_execution",
|
|
62
|
-
arguments: {
|
|
63
|
-
correlation_id: decision.correlation_id,
|
|
64
|
-
harness: "browser-e2e",
|
|
65
|
-
capability_id: decision.selected,
|
|
66
|
-
status: "completed",
|
|
67
|
-
result: hostResult,
|
|
68
|
-
browser: { operation: "scroll", direction: "down", executed: true },
|
|
69
|
-
exit_status: 0,
|
|
70
|
-
duration_ms: Number((performance.now() - started).toFixed(3)),
|
|
71
|
-
},
|
|
72
|
-
});
|
|
73
|
-
const receipt = recorded.result.structuredContent;
|
|
74
|
-
assert.equal(receipt.correlation_id, decision.correlation_id);
|
|
75
|
-
assert.equal(receipt.host.browser.operation, "scroll");
|
|
76
|
-
assert.equal(receipt.host.exit_status, 0);
|
|
77
|
-
|
|
78
|
-
const records = (await readFile(casesPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
|
|
79
|
-
assert.deepEqual(records.map((record) => record.record_type), ["routing_case", "execution_receipt"]);
|
|
80
|
-
assert.equal(records[0].request.context.browser.goal, "scroll the page downward");
|
|
81
|
-
console.log(JSON.stringify({
|
|
82
|
-
ok: true,
|
|
83
|
-
cases_path: casesPath,
|
|
84
|
-
decision: {
|
|
85
|
-
correlation_id: decision.correlation_id,
|
|
86
|
-
selected: decision.selected,
|
|
87
|
-
operation: decision.browser_action.operation,
|
|
88
|
-
confidence: decision.confidence,
|
|
89
|
-
latency_ms: decision.receipt.latency_ms,
|
|
90
|
-
cost_usd: decision.receipt.cost_usd ?? null,
|
|
91
|
-
},
|
|
92
|
-
host_execution: { operation: hostResult.operation, exit_status: receipt.host.exit_status },
|
|
93
|
-
receipts: records.length,
|
|
94
|
-
}, null, 2));
|
|
95
|
-
} finally {
|
|
96
|
-
child.kill();
|
|
97
|
-
await new Promise((resolveExit) => child.once("close", resolveExit));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function request(id, method, params) {
|
|
101
|
-
return new Promise((resolveResponse, reject) => {
|
|
102
|
-
const timer = setTimeout(() => {
|
|
103
|
-
pending.delete(id);
|
|
104
|
-
reject(new Error(`MCP request timed out: ${method}`));
|
|
105
|
-
}, 15_000);
|
|
106
|
-
pending.set(id, (message) => {
|
|
107
|
-
clearTimeout(timer);
|
|
108
|
-
resolveResponse(message);
|
|
109
|
-
});
|
|
110
|
-
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function notification(method, params) {
|
|
115
|
-
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
116
|
-
return new Promise((resolveNotification) => setImmediate(resolveNotification));
|
|
117
|
-
}
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
|
|
9
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const casesPath = join(await mkdtemp(join(tmpdir(), "jev-browser-e2e-")), "cases.jsonl");
|
|
11
|
+
const child = spawn(process.env.JEV_NODE ?? "node", [resolve(root, "src/mcp-server.mjs")], {
|
|
12
|
+
cwd: root,
|
|
13
|
+
env: { ...process.env, JEV_BROWSER_FAST_PATH: "1", JEV_REPLAY_CASES: casesPath },
|
|
14
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
15
|
+
});
|
|
16
|
+
const lines = createInterface({ input: child.stdout });
|
|
17
|
+
const pending = new Map();
|
|
18
|
+
lines.on("line", (line) => {
|
|
19
|
+
if (!line.trim()) return;
|
|
20
|
+
const message = JSON.parse(line);
|
|
21
|
+
const waiter = pending.get(message.id);
|
|
22
|
+
if (waiter) {
|
|
23
|
+
pending.delete(message.id);
|
|
24
|
+
waiter(message);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const initialized = await request(1, "initialize", {});
|
|
30
|
+
assert.equal(initialized.result.serverInfo.name, "jev-layer");
|
|
31
|
+
await notification("notifications/initialized", {});
|
|
32
|
+
const listed = await request(2, "tools/list", {});
|
|
33
|
+
assert.ok(listed.result.tools.some((tool) => tool.name === "jev_browser_step"));
|
|
34
|
+
|
|
35
|
+
const routed = await request(3, "tools/call", {
|
|
36
|
+
name: "jev_browser_step",
|
|
37
|
+
arguments: {
|
|
38
|
+
provider: process.env.JEV_BROWSER_PROVIDER ?? "demo",
|
|
39
|
+
enabled: true,
|
|
40
|
+
harness: "browser-e2e",
|
|
41
|
+
goal: "scroll the page downward",
|
|
42
|
+
observation: {
|
|
43
|
+
url: "https://example.test",
|
|
44
|
+
title: "Fixture",
|
|
45
|
+
visible_text: "Additional content is below the fold",
|
|
46
|
+
scroll: { up: false, down: true },
|
|
47
|
+
targets: [],
|
|
48
|
+
tabs: [],
|
|
49
|
+
},
|
|
50
|
+
policy: { min_confidence: 0 },
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const decision = routed.result.structuredContent;
|
|
54
|
+
assert.equal(decision.status, "selected");
|
|
55
|
+
assert.equal(decision.browser_action.operation, "scroll");
|
|
56
|
+
assert.equal(decision.browser_action.target_id, null);
|
|
57
|
+
|
|
58
|
+
const started = performance.now();
|
|
59
|
+
const hostResult = { operation: decision.browser_action.operation, direction: "down", fixture: true };
|
|
60
|
+
const recorded = await request(4, "tools/call", {
|
|
61
|
+
name: "jev_record_execution",
|
|
62
|
+
arguments: {
|
|
63
|
+
correlation_id: decision.correlation_id,
|
|
64
|
+
harness: "browser-e2e",
|
|
65
|
+
capability_id: decision.selected,
|
|
66
|
+
status: "completed",
|
|
67
|
+
result: hostResult,
|
|
68
|
+
browser: { operation: "scroll", direction: "down", executed: true },
|
|
69
|
+
exit_status: 0,
|
|
70
|
+
duration_ms: Number((performance.now() - started).toFixed(3)),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const receipt = recorded.result.structuredContent;
|
|
74
|
+
assert.equal(receipt.correlation_id, decision.correlation_id);
|
|
75
|
+
assert.equal(receipt.host.browser.operation, "scroll");
|
|
76
|
+
assert.equal(receipt.host.exit_status, 0);
|
|
77
|
+
|
|
78
|
+
const records = (await readFile(casesPath, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
|
|
79
|
+
assert.deepEqual(records.map((record) => record.record_type), ["routing_case", "execution_receipt"]);
|
|
80
|
+
assert.equal(records[0].request.context.browser.goal, "scroll the page downward");
|
|
81
|
+
console.log(JSON.stringify({
|
|
82
|
+
ok: true,
|
|
83
|
+
cases_path: casesPath,
|
|
84
|
+
decision: {
|
|
85
|
+
correlation_id: decision.correlation_id,
|
|
86
|
+
selected: decision.selected,
|
|
87
|
+
operation: decision.browser_action.operation,
|
|
88
|
+
confidence: decision.confidence,
|
|
89
|
+
latency_ms: decision.receipt.latency_ms,
|
|
90
|
+
cost_usd: decision.receipt.cost_usd ?? null,
|
|
91
|
+
},
|
|
92
|
+
host_execution: { operation: hostResult.operation, exit_status: receipt.host.exit_status },
|
|
93
|
+
receipts: records.length,
|
|
94
|
+
}, null, 2));
|
|
95
|
+
} finally {
|
|
96
|
+
child.kill();
|
|
97
|
+
await new Promise((resolveExit) => child.once("close", resolveExit));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function request(id, method, params) {
|
|
101
|
+
return new Promise((resolveResponse, reject) => {
|
|
102
|
+
const timer = setTimeout(() => {
|
|
103
|
+
pending.delete(id);
|
|
104
|
+
reject(new Error(`MCP request timed out: ${method}`));
|
|
105
|
+
}, 15_000);
|
|
106
|
+
pending.set(id, (message) => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
resolveResponse(message);
|
|
109
|
+
});
|
|
110
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function notification(method, params) {
|
|
115
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
116
|
+
return new Promise((resolveNotification) => setImmediate(resolveNotification));
|
|
117
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { routeRequest } from "../src/route.mjs";
|
|
3
|
-
|
|
4
|
-
let providerCalls = 0;
|
|
5
|
-
const decision = await routeRequest({
|
|
6
|
-
harness: "capability-e2e",
|
|
7
|
-
intent: "use the verified model for a bounded decision",
|
|
8
|
-
context: { task: "model" },
|
|
9
|
-
policy: { deterministic: true, prefer_kind: "model", prefer_verified: true },
|
|
10
|
-
}, {
|
|
11
|
-
discovery: {
|
|
12
|
-
skills: [{ id: "skill:inspect", name: "Inspect skill", description: "Inspect context" }],
|
|
13
|
-
models: [{ id: "model:verified", name: "Verified model", description: "Make bounded decisions", verified: true, risk: "low", source: "e2e-manifest" }],
|
|
14
|
-
tools: [{ id: "tool:offline", name: "Offline tool", description: "Unavailable", available: false }],
|
|
15
|
-
},
|
|
16
|
-
provider: { name: "capability-e2e-provider", async decide() { providerCalls += 1; throw new Error("deterministic selection should not call Jev"); } },
|
|
17
|
-
});
|
|
18
|
-
assert.equal(providerCalls, 0);
|
|
19
|
-
assert.equal(decision.status, "selected");
|
|
20
|
-
assert.equal(decision.selected, "model:verified");
|
|
21
|
-
assert.equal(decision.candidates.find((candidate) => candidate.id === "model:verified").source, "e2e-manifest");
|
|
22
|
-
assert.equal(decision.candidates.find((candidate) => candidate.id === "model:verified").verified, true);
|
|
23
|
-
console.log(JSON.stringify({
|
|
24
|
-
ok: true,
|
|
25
|
-
status: decision.status,
|
|
26
|
-
selected: decision.selected,
|
|
27
|
-
provider: decision.receipt.provider,
|
|
28
|
-
deterministic: true,
|
|
29
|
-
jev_calls: providerCalls,
|
|
30
|
-
candidate_metadata: decision.candidates.find((candidate) => candidate.id === decision.selected),
|
|
31
|
-
}, null, 2));
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { routeRequest } from "../src/route.mjs";
|
|
3
|
+
|
|
4
|
+
let providerCalls = 0;
|
|
5
|
+
const decision = await routeRequest({
|
|
6
|
+
harness: "capability-e2e",
|
|
7
|
+
intent: "use the verified model for a bounded decision",
|
|
8
|
+
context: { task: "model" },
|
|
9
|
+
policy: { deterministic: true, prefer_kind: "model", prefer_verified: true },
|
|
10
|
+
}, {
|
|
11
|
+
discovery: {
|
|
12
|
+
skills: [{ id: "skill:inspect", name: "Inspect skill", description: "Inspect context" }],
|
|
13
|
+
models: [{ id: "model:verified", name: "Verified model", description: "Make bounded decisions", verified: true, risk: "low", source: "e2e-manifest" }],
|
|
14
|
+
tools: [{ id: "tool:offline", name: "Offline tool", description: "Unavailable", available: false }],
|
|
15
|
+
},
|
|
16
|
+
provider: { name: "capability-e2e-provider", async decide() { providerCalls += 1; throw new Error("deterministic selection should not call Jev"); } },
|
|
17
|
+
});
|
|
18
|
+
assert.equal(providerCalls, 0);
|
|
19
|
+
assert.equal(decision.status, "selected");
|
|
20
|
+
assert.equal(decision.selected, "model:verified");
|
|
21
|
+
assert.equal(decision.candidates.find((candidate) => candidate.id === "model:verified").source, "e2e-manifest");
|
|
22
|
+
assert.equal(decision.candidates.find((candidate) => candidate.id === "model:verified").verified, true);
|
|
23
|
+
console.log(JSON.stringify({
|
|
24
|
+
ok: true,
|
|
25
|
+
status: decision.status,
|
|
26
|
+
selected: decision.selected,
|
|
27
|
+
provider: decision.receipt.provider,
|
|
28
|
+
deterministic: true,
|
|
29
|
+
jev_calls: providerCalls,
|
|
30
|
+
candidate_metadata: decision.candidates.find((candidate) => candidate.id === decision.selected),
|
|
31
|
+
}, null, 2));
|