jev-layer 0.1.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 +34 -0
- package/CONTRIBUTING.md +146 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/README.ru.md +117 -0
- package/README.zh-CN.md +117 -0
- package/RELEASE.md +53 -0
- package/SECURITY.md +56 -0
- package/bin/jev.mjs +214 -0
- package/config/codex.mcp.toml +8 -0
- package/config/generic-mcp.json +13 -0
- package/config/hermes.mcp.yaml +9 -0
- package/config/jev.example.json +22 -0
- package/config/omp.mcp.json +14 -0
- package/config/providers.env.example +13 -0
- package/docs/COMPATIBILITY.md +15 -0
- package/docs/SCHEMA-VERSIONING.md +94 -0
- package/examples/capabilities.json +39 -0
- package/examples/route-request.json +28 -0
- package/integrations/codex/.codex-plugin/plugin.json +19 -0
- package/integrations/codex/.mcp.json +11 -0
- package/integrations/codex/AGENTS.md +1 -0
- package/integrations/codex/run-mcp.mjs +9 -0
- package/integrations/codex/skills/jev-route/SKILL.md +17 -0
- package/integrations/hermes/__init__.py +51 -0
- package/integrations/hermes/plugin.yaml +5 -0
- package/integrations/hermes/schemas.py +12 -0
- package/integrations/omp/extension.js +105 -0
- package/integrations/template/README.md +10 -0
- package/integrations/template/adapter.mjs +87 -0
- package/package.json +59 -0
- package/scripts/benchmark.mjs +35 -0
- package/scripts/browser-benchmark.mjs +92 -0
- package/scripts/browser-e2e.mjs +117 -0
- package/scripts/capability-e2e.mjs +31 -0
- package/scripts/clean-install-smoke.mjs +162 -0
- package/scripts/codex-mcp-smoke.mjs +119 -0
- package/scripts/context-filter-e2e.mjs +45 -0
- package/scripts/fail-open-smoke.mjs +120 -0
- package/scripts/feature-flags-smoke.mjs +46 -0
- package/scripts/mcp-receipt-smoke.mjs +98 -0
- package/scripts/openrouter-choice.mjs +59 -0
- package/scripts/replay-eval.mjs +124 -0
- package/scripts/smoke.mjs +34 -0
- package/scripts/supervision-e2e.mjs +109 -0
- package/src/browser.mjs +569 -0
- package/src/cli.mjs +33 -0
- package/src/config.mjs +75 -0
- package/src/context-filter.mjs +56 -0
- package/src/contract.mjs +72 -0
- package/src/discovery.mjs +65 -0
- package/src/mcp-server.mjs +210 -0
- package/src/providers/demo.mjs +52 -0
- package/src/providers/typesafe.mjs +126 -0
- package/src/receipts.mjs +226 -0
- package/src/registry.mjs +109 -0
- package/src/relevance-filter.mjs +99 -0
- package/src/route.mjs +221 -0
- package/src/supervision.mjs +244 -0
- package/test/browser.test.mjs +199 -0
- package/test/capability.test.mjs +54 -0
- package/test/context-filter.test.mjs +65 -0
- package/test/openrouter-provider.test.mjs +55 -0
- package/test/receipts.test.mjs +71 -0
- package/test/route.test.mjs +74 -0
- package/test/supervision.test.mjs +99 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { stableJson } from "./contract.mjs";
|
|
3
|
+
import { DemoProvider } from "./providers/demo.mjs";
|
|
4
|
+
import { OpenRouterDecisionsProvider, TypeSafeProvider } from "./providers/typesafe.mjs";
|
|
5
|
+
import { appendSupervisionReceipt } from "./receipts.mjs";
|
|
6
|
+
|
|
7
|
+
export const SUPERVISION_ENV = "JEV_SUPERVISION";
|
|
8
|
+
export const SUPERVISION_DIMENSIONS = Object.freeze([
|
|
9
|
+
"requirements_addressed",
|
|
10
|
+
"verification_needed",
|
|
11
|
+
"meaningful_progress",
|
|
12
|
+
"worker_stuck",
|
|
13
|
+
"work_off_track",
|
|
14
|
+
"completion",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const MAX_STATE_BYTES = 8_000;
|
|
18
|
+
|
|
19
|
+
export function supervisionEnabled(options = {}) {
|
|
20
|
+
if (options.enabled === true) return true;
|
|
21
|
+
if (options.enabled === false) return false;
|
|
22
|
+
return process.env[SUPERVISION_ENV] === "1";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildSupervisionRequest({
|
|
26
|
+
job = {},
|
|
27
|
+
observation = {},
|
|
28
|
+
evidence = {},
|
|
29
|
+
harness = "unknown",
|
|
30
|
+
actor_permissions = [],
|
|
31
|
+
policy = {},
|
|
32
|
+
} = {}) {
|
|
33
|
+
return {
|
|
34
|
+
schema_version: 1,
|
|
35
|
+
harness,
|
|
36
|
+
intent: "Assess supervised work against its requirements and verification evidence.",
|
|
37
|
+
actor_permissions: Array.isArray(actor_permissions) ? actor_permissions.filter((value) => typeof value === "string") : [],
|
|
38
|
+
policy: { ...policy },
|
|
39
|
+
context: {
|
|
40
|
+
job: bounded(job),
|
|
41
|
+
observation: bounded(observation),
|
|
42
|
+
evidence: bounded(evidence),
|
|
43
|
+
supervision: {
|
|
44
|
+
judgments: bounded(evidence?.judgments ?? {}),
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function supervisionQuestions() {
|
|
51
|
+
return Object.fromEntries(SUPERVISION_DIMENSIONS.map((dimension) => [dimension, {
|
|
52
|
+
type: "noul",
|
|
53
|
+
instructions: questionInstructions(dimension),
|
|
54
|
+
criteria: {
|
|
55
|
+
true: "The statement is supported by the supplied work state and evidence.",
|
|
56
|
+
false: "The statement is not supported, is contradicted, or is unknown.",
|
|
57
|
+
},
|
|
58
|
+
}]));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeAssessment(raw) {
|
|
62
|
+
const answers = raw?.answers;
|
|
63
|
+
if (!answers || typeof answers !== "object") throw new Error("supervision response has no answers object");
|
|
64
|
+
const assessment = {};
|
|
65
|
+
for (const dimension of SUPERVISION_DIMENSIONS) {
|
|
66
|
+
const answer = answers[dimension];
|
|
67
|
+
const probability = readProbability(answer);
|
|
68
|
+
if (probability === null) throw new Error(`supervision response has no valid ${dimension} judgment`);
|
|
69
|
+
assessment[dimension] = Number(probability.toFixed(4));
|
|
70
|
+
}
|
|
71
|
+
return assessment;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function deterministicSupervisionPolicy({ assessment, evidence = {}, attempts = 0, policy = {} } = {}) {
|
|
75
|
+
const values = assessment && typeof assessment === "object" ? assessment : {};
|
|
76
|
+
const threshold = Number.isFinite(policy.threshold) ? Math.max(0, Math.min(1, policy.threshold)) : 0.7;
|
|
77
|
+
const maxRetries = Number.isInteger(policy.max_retries) && policy.max_retries >= 0 ? policy.max_retries : 2;
|
|
78
|
+
const verificationEvidence = evidence.verification_passed === true || evidence.tests_passed === true;
|
|
79
|
+
|
|
80
|
+
if (value(values.worker_stuck) >= threshold) {
|
|
81
|
+
return attempts < maxRetries
|
|
82
|
+
? { action: "retry", reason: "worker appears stuck and retry budget remains", policy_source: "deterministic_host_policy" }
|
|
83
|
+
: { action: "escalate", reason: "worker appears stuck and retry budget is exhausted", policy_source: "deterministic_host_policy" };
|
|
84
|
+
}
|
|
85
|
+
if (value(values.work_off_track) >= threshold) {
|
|
86
|
+
return attempts < maxRetries
|
|
87
|
+
? { action: "retry", reason: "work appears off track and retry budget remains", policy_source: "deterministic_host_policy" }
|
|
88
|
+
: { action: "escalate", reason: "work appears off track and retry budget is exhausted", policy_source: "deterministic_host_policy" };
|
|
89
|
+
}
|
|
90
|
+
if (value(values.verification_needed) >= threshold || (value(values.completion) >= threshold && !verificationEvidence)) {
|
|
91
|
+
return { action: "verify", reason: "verification is required before continuing or finishing", policy_source: "deterministic_host_policy" };
|
|
92
|
+
}
|
|
93
|
+
if (value(values.completion) >= threshold && value(values.requirements_addressed) >= threshold) {
|
|
94
|
+
return { action: "finish", reason: "completion and requirement coverage are supported", policy_source: "deterministic_host_policy" };
|
|
95
|
+
}
|
|
96
|
+
return { action: "continue", reason: value(values.meaningful_progress) >= threshold ? "meaningful progress is supported" : "continue while evidence is incomplete", policy_source: "deterministic_host_policy" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function superviseWork({
|
|
100
|
+
job,
|
|
101
|
+
observation,
|
|
102
|
+
evidence,
|
|
103
|
+
harness,
|
|
104
|
+
actor_permissions,
|
|
105
|
+
policy,
|
|
106
|
+
provider = "demo",
|
|
107
|
+
enabled,
|
|
108
|
+
attempts = 0,
|
|
109
|
+
receiptPath,
|
|
110
|
+
} = {}) {
|
|
111
|
+
const started = performance.now();
|
|
112
|
+
const request = buildSupervisionRequest({ job, observation, evidence, harness, actor_permissions, policy });
|
|
113
|
+
const base = {
|
|
114
|
+
status: "fallback",
|
|
115
|
+
action: "continue",
|
|
116
|
+
reason: "supervision unavailable",
|
|
117
|
+
assessment: null,
|
|
118
|
+
policy: null,
|
|
119
|
+
request,
|
|
120
|
+
metrics: { wall_time_ms: 0, jev_calls: 0, cost_usd: 0, failures: 0 },
|
|
121
|
+
receipt: null,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (!supervisionEnabled({ enabled })) return finalize(base, started, "disabled", 0);
|
|
125
|
+
let resolved;
|
|
126
|
+
try {
|
|
127
|
+
resolved = resolveProvider(provider);
|
|
128
|
+
if (!resolved || typeof resolved.evaluate !== "function") throw new Error("provider does not support supervision evaluation");
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return finalize(base, started, "provider_error", 1, error);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let raw;
|
|
134
|
+
try {
|
|
135
|
+
raw = await resolved.evaluate({ state: request, questions: supervisionQuestions() });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
return finalize(base, started, "provider_error", 1, error);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let assessment;
|
|
141
|
+
try {
|
|
142
|
+
assessment = normalizeAssessment(raw);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return finalize({ ...base, raw_jev: raw }, started, "malformed_response", 1, error);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const selectedPolicy = deterministicSupervisionPolicy({ assessment, evidence, attempts, policy });
|
|
148
|
+
const result = {
|
|
149
|
+
status: "judged",
|
|
150
|
+
action: selectedPolicy.action,
|
|
151
|
+
reason: selectedPolicy.reason,
|
|
152
|
+
assessment,
|
|
153
|
+
policy: selectedPolicy,
|
|
154
|
+
request,
|
|
155
|
+
metrics: {
|
|
156
|
+
wall_time_ms: Number((performance.now() - started).toFixed(3)),
|
|
157
|
+
jev_calls: 1,
|
|
158
|
+
cost_usd: finiteCost(raw?.usage?.cost),
|
|
159
|
+
failures: 0,
|
|
160
|
+
},
|
|
161
|
+
receipt: {
|
|
162
|
+
correlation_id: randomUUID(),
|
|
163
|
+
provider: resolved.name ?? providerName(provider),
|
|
164
|
+
latency_ms: Number((performance.now() - started).toFixed(3)),
|
|
165
|
+
cost_usd: finiteCost(raw?.usage?.cost),
|
|
166
|
+
dimensions: SUPERVISION_DIMENSIONS.length,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
if (receiptPath) {
|
|
170
|
+
try {
|
|
171
|
+
const appended = await appendSupervisionReceipt({ path: receiptPath, request, result });
|
|
172
|
+
result.receipt_path = appended.path;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
result.receipt_error = error instanceof Error ? error.message : String(error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resolveProvider(provider) {
|
|
181
|
+
if (provider && typeof provider === "object") return provider;
|
|
182
|
+
if (provider === "demo") return new DemoProvider();
|
|
183
|
+
if (provider === "openrouter") return new OpenRouterDecisionsProvider();
|
|
184
|
+
if (provider === "typesafe") return new TypeSafeProvider();
|
|
185
|
+
throw new Error(`unsupported provider: ${provider}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function finalize(result, started, reason, jevCalls, error = null) {
|
|
189
|
+
const message = error instanceof Error ? error.message : error ? String(error) : reason;
|
|
190
|
+
return {
|
|
191
|
+
...result,
|
|
192
|
+
reason: message,
|
|
193
|
+
metrics: {
|
|
194
|
+
...result.metrics,
|
|
195
|
+
wall_time_ms: Number((performance.now() - started).toFixed(3)),
|
|
196
|
+
jev_calls: jevCalls,
|
|
197
|
+
failures: reason === "disabled" ? 0 : 1,
|
|
198
|
+
},
|
|
199
|
+
fallback: { type: reason, reason: message },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readProbability(answer) {
|
|
204
|
+
if (typeof answer === "boolean") return answer ? 1 : 0;
|
|
205
|
+
if (typeof answer === "number" && Number.isFinite(answer) && answer >= 0 && answer <= 1) return answer;
|
|
206
|
+
if (!answer || typeof answer !== "object") return null;
|
|
207
|
+
for (const key of ["probability", "p_true", "score", "value", "noul"]) {
|
|
208
|
+
const value = answer[key];
|
|
209
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1) return value;
|
|
210
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function questionInstructions(dimension) {
|
|
216
|
+
const labels = {
|
|
217
|
+
requirements_addressed: "The worker has addressed the stated requirements.",
|
|
218
|
+
verification_needed: "Additional verification is needed before trusting the current work.",
|
|
219
|
+
meaningful_progress: "The worker has made meaningful progress toward the goal.",
|
|
220
|
+
worker_stuck: "The worker is stuck and is not making useful progress.",
|
|
221
|
+
work_off_track: "The worker's current work is materially off track.",
|
|
222
|
+
completion: "The work is complete for the stated requirements.",
|
|
223
|
+
};
|
|
224
|
+
return `Judge whether this statement is supported: ${labels[dimension]}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function bounded(value) {
|
|
228
|
+
const encoded = stableJson(value ?? {});
|
|
229
|
+
if (Buffer.byteLength(encoded, "utf8") <= MAX_STATE_BYTES) return value ?? {};
|
|
230
|
+
return { truncated: true, preview: encoded.slice(0, MAX_STATE_BYTES) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function value(assessment, key) {
|
|
234
|
+
const candidate = key === undefined ? assessment : assessment?.[key];
|
|
235
|
+
return typeof candidate === "number" && Number.isFinite(candidate) ? candidate : 0;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function finiteCost(value) {
|
|
239
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function providerName(provider) {
|
|
243
|
+
return typeof provider === "string" ? provider : "unknown";
|
|
244
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
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 { buildBrowserRequest, browserActions, decideBrowserStep, runBrowserFastPath } from "../src/browser.mjs";
|
|
7
|
+
import { readRoutingCases } from "../src/receipts.mjs";
|
|
8
|
+
import { routeRequest } from "../src/route.mjs";
|
|
9
|
+
function providerFor(choices) {
|
|
10
|
+
let call = 0;
|
|
11
|
+
return {
|
|
12
|
+
name: "browser-test-provider",
|
|
13
|
+
async decide({ candidates }) {
|
|
14
|
+
const requested = choices[call++] ?? "browser:handoff";
|
|
15
|
+
const choice = candidates.some((candidate) => candidate.id === requested)
|
|
16
|
+
? requested
|
|
17
|
+
: candidates.find((candidate) => candidate.id === "browser:handoff")?.id ?? candidates[0]?.id;
|
|
18
|
+
return {
|
|
19
|
+
answers: { tool: { type: "choice", choice, probabilities: Object.fromEntries(candidates.map((candidate) => [candidate.id, candidate.id === choice ? 1 : 0])), confidence: 1 } },
|
|
20
|
+
usage: { cost: 0.0001 },
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("browser request exposes only bounded structured actions", () => {
|
|
27
|
+
const request = buildBrowserRequest({
|
|
28
|
+
goal: "inspect the visible page",
|
|
29
|
+
observation: {
|
|
30
|
+
url: "https://example.test",
|
|
31
|
+
visible_text: "Visible content",
|
|
32
|
+
scroll: { up: false, down: true },
|
|
33
|
+
targets: [{ id: "submit", name: "Submit", role: "button", clickable: true }, { id: "country", name: "Country", options: [{ id: "de", label: "Germany" }] }],
|
|
34
|
+
tabs: [{ id: "tab-1", title: "Current", active: true }, { id: "tab-2", title: "Other", active: false }],
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:scroll:down"));
|
|
38
|
+
assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:click:submit"));
|
|
39
|
+
assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:select:country:de"));
|
|
40
|
+
assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:switch_tab:tab-2"));
|
|
41
|
+
assert.ok(request.capabilities.every((candidate) => candidate.permissions.includes("browser")));
|
|
42
|
+
assert.equal(request.capabilities.some((candidate) => candidate.id.includes("type")), false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("browser progress removes completed targets and exposes link navigation", () => {
|
|
46
|
+
const request = buildBrowserRequest({
|
|
47
|
+
goal: "click the action then navigate",
|
|
48
|
+
observation: {
|
|
49
|
+
url: "https://example.test/start",
|
|
50
|
+
targets: [
|
|
51
|
+
{ id: "action", role: "button", name: "Action", clickable: true },
|
|
52
|
+
{ id: "next", role: "a", name: "Next", href: "/next", clickable: true },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
progress: {
|
|
56
|
+
executed_actions: [{ id: "browser:click:action", operation: "click", target_id: "action", status: "completed" }],
|
|
57
|
+
used_targets: ["action"],
|
|
58
|
+
current_url: "https://example.test/start",
|
|
59
|
+
last_action: { id: "browser:click:action", operation: "click", target_id: "action", status: "completed" },
|
|
60
|
+
last_result: { status: "completed", state_changed: true },
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
assert.equal(request.capabilities.some((candidate) => candidate.id === "browser:click:action"), false);
|
|
64
|
+
assert.ok(request.capabilities.some((candidate) => candidate.id === "browser:navigate:https%3A%2F%2Fexample.test%2Fnext:next"));
|
|
65
|
+
assert.equal(request.context.browser.progress.goal, "click the action then navigate");
|
|
66
|
+
assert.equal(request.context.browser.progress.current_url, "https://example.test/start");
|
|
67
|
+
assert.equal(request.context.browser.progress.last_action.target_id, "action");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("browser progress infers actionable roles and deterministic goal order", () => {
|
|
71
|
+
const request = buildBrowserRequest({
|
|
72
|
+
goal: "Click Increment, then select Beta, then navigate to the next page",
|
|
73
|
+
observation: {
|
|
74
|
+
url: "https://example.test/start",
|
|
75
|
+
targets: [
|
|
76
|
+
{ id: "increment", role: "button", name: "Increment" },
|
|
77
|
+
{ id: "choice", role: "select", name: "Choice", options: [{ id: "beta", value: "beta", label: "Beta" }] },
|
|
78
|
+
{ id: "next", role: "a", name: "Next", href: "/next" },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
assert.deepEqual(request.context.browser.progress.remaining_expected_progress, [
|
|
83
|
+
"browser:click:increment",
|
|
84
|
+
"browser:select:choice:beta",
|
|
85
|
+
"browser:navigate:https%3A%2F%2Fexample.test%2Fnext:next",
|
|
86
|
+
]);
|
|
87
|
+
assert.deepEqual(request.capabilities.map((candidate) => candidate.id), ["browser:click:increment", "browser:handoff"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("browser progress only permits repeated scroll after measurable progress", () => {
|
|
91
|
+
const base = {
|
|
92
|
+
goal: "scroll down",
|
|
93
|
+
observation: { url: "https://example.test", scroll: { down: true, position: 100, max: 1000 } },
|
|
94
|
+
};
|
|
95
|
+
const blocked = browserActions({
|
|
96
|
+
...base,
|
|
97
|
+
progress: {
|
|
98
|
+
last_action: { id: "browser:scroll:down", operation: "scroll", direction: "down", status: "completed" },
|
|
99
|
+
last_result: { status: "completed", state_changed: false, scroll_progress: false },
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
assert.equal(blocked.some((candidate) => candidate.id === "browser:scroll:down"), false);
|
|
103
|
+
const allowed = browserActions({
|
|
104
|
+
...base,
|
|
105
|
+
progress: {
|
|
106
|
+
last_action: { id: "browser:scroll:down", operation: "scroll", direction: "down", status: "completed" },
|
|
107
|
+
last_result: { status: "completed", state_changed: true, scroll_progress: true },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
assert.ok(allowed.some((candidate) => candidate.id === "browser:scroll:down"));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("browser fast-path is opt-in and fails open without an executor", async () => {
|
|
114
|
+
const disabled = await decideBrowserStep({ goal: "inspect", observation: {} }, { enabled: false, provider: "demo" });
|
|
115
|
+
assert.equal(disabled.decision.status, "fallback");
|
|
116
|
+
assert.equal(disabled.decision.fallback.type, "browser_fast_path_disabled");
|
|
117
|
+
const unavailable = await runBrowserFastPath({ goal: "inspect", enabled: true, provider: "demo" });
|
|
118
|
+
assert.equal(unavailable.status, "handoff");
|
|
119
|
+
assert.equal(unavailable.reason, "browser_executor_unavailable");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("browser fast-path executes safe actions then hands control back", async () => {
|
|
123
|
+
const path = join(await mkdtemp(join(tmpdir(), "jev-browser-")), "cases.jsonl");
|
|
124
|
+
const executed = [];
|
|
125
|
+
const observations = [
|
|
126
|
+
{ url: "https://example.test", scroll: { up: false, down: true }, tabs: [{ id: "tab-1", active: true }, { id: "tab-2", title: "Other", active: false }] },
|
|
127
|
+
{ url: "https://example.test", scroll: { up: false, down: false }, tabs: [{ id: "tab-1", active: true }, { id: "tab-2", title: "Other", active: false }] },
|
|
128
|
+
];
|
|
129
|
+
const executor = {
|
|
130
|
+
async observe() { return observations.shift() ?? observations.at(-1) ?? {}; },
|
|
131
|
+
async execute(action) {
|
|
132
|
+
executed.push(action);
|
|
133
|
+
return { result: { operation: action.operation }, observation: observations.shift() ?? {}, handoff: executed.length === 2 };
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
const result = await runBrowserFastPath({
|
|
137
|
+
goal: "scroll and switch to the other tab",
|
|
138
|
+
harness: "browser-test",
|
|
139
|
+
enabled: true,
|
|
140
|
+
provider: providerFor(["browser:scroll:down", "browser:switch_tab:tab-2"]),
|
|
141
|
+
executor,
|
|
142
|
+
receiptPath: path,
|
|
143
|
+
});
|
|
144
|
+
assert.equal(result.status, "handoff");
|
|
145
|
+
assert.equal(result.reason, "host_handoff");
|
|
146
|
+
assert.deepEqual(executed.map((action) => action.operation), ["scroll", "switch_tab"]);
|
|
147
|
+
assert.equal(result.metrics.jev_calls, 2);
|
|
148
|
+
assert.equal(result.metrics.browser_actions, 2);
|
|
149
|
+
assert.equal(result.metrics.failures, 0);
|
|
150
|
+
assert.equal(result.metrics.cost_usd, 0.0002);
|
|
151
|
+
const records = (await readFile(path, "utf8")).trim().split(/\r?\n/).map(JSON.parse);
|
|
152
|
+
assert.equal(records.length, 4);
|
|
153
|
+
assert.equal(records.filter((record) => record.record_type === "routing_case").length, 2);
|
|
154
|
+
assert.equal(records.filter((record) => record.record_type === "execution_receipt").length, 2);
|
|
155
|
+
assert.equal(records[1].host.browser.operation, "scroll");
|
|
156
|
+
assert.equal(records[3].host.browser.operation, "switch_tab");
|
|
157
|
+
const replayCases = await readRoutingCases(path);
|
|
158
|
+
const replayed = await Promise.all(replayCases.map((routingCase) => routeRequest(routingCase.request, { provider: "demo" })));
|
|
159
|
+
assert.equal(replayed.length, 2);
|
|
160
|
+
assert.ok(replayed.every((decision) => decision.execution.enabled === false));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("consequential browser action hands off for host approval", async () => {
|
|
164
|
+
let executeCalls = 0;
|
|
165
|
+
const result = await runBrowserFastPath({
|
|
166
|
+
goal: "click the visible action",
|
|
167
|
+
enabled: true,
|
|
168
|
+
provider: providerFor(["browser:click:action"]),
|
|
169
|
+
executor: {
|
|
170
|
+
async observe() { return { targets: [{ id: "action", name: "Action", clickable: true }] }; },
|
|
171
|
+
async approve() { return false; },
|
|
172
|
+
async execute() { executeCalls += 1; return {}; },
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
assert.equal(result.status, "handoff");
|
|
176
|
+
assert.equal(result.reason, "host_confirmation_required");
|
|
177
|
+
assert.equal(executeCalls, 0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("native select uses the optional host select executor", async () => {
|
|
181
|
+
let selectCalls = 0;
|
|
182
|
+
let executeCalls = 0;
|
|
183
|
+
const result = await runBrowserFastPath({
|
|
184
|
+
goal: "select beta",
|
|
185
|
+
enabled: true,
|
|
186
|
+
maxSteps: 1,
|
|
187
|
+
provider: providerFor(["browser:select:choice:beta"]),
|
|
188
|
+
executor: {
|
|
189
|
+
async observe() { return { targets: [{ id: "choice", role: "select", options: [{ id: "beta", label: "Beta" }] }] }; },
|
|
190
|
+
async approve() { return true; },
|
|
191
|
+
async select(action) { selectCalls += 1; return { result: { value: action.option_id }, observation: { targets: [] } }; },
|
|
192
|
+
async execute() { executeCalls += 1; },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
assert.equal(result.reason, "browser_step_budget_exhausted");
|
|
196
|
+
assert.equal(result.metrics.browser_actions, 1);
|
|
197
|
+
assert.equal(selectCalls, 1);
|
|
198
|
+
assert.equal(executeCalls, 0);
|
|
199
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { discoverCapabilities } from "../src/discovery.mjs";
|
|
4
|
+
import { routeRequest } from "../src/route.mjs";
|
|
5
|
+
|
|
6
|
+
function discovered() {
|
|
7
|
+
return discoverCapabilities({
|
|
8
|
+
skills: [{ id: "skill:inspect", name: "Inspect skill", description: "Inspect work", verified: true }],
|
|
9
|
+
mcp: [{ id: "mcp:search", name: "Search MCP", description: "Search files" }],
|
|
10
|
+
cli: [{ id: "cli:git", name: "Git CLI", command: "git", description: "Run git" }],
|
|
11
|
+
dsh: [{ id: "dsh:worker", name: "Worker DSH", description: "Delegate work" }],
|
|
12
|
+
subagents: [{ id: "subagent:review", name: "Review subagent", description: "Review code" }],
|
|
13
|
+
models: [{ id: "model:jev", name: "Jev model", description: "Make bounded decisions" }],
|
|
14
|
+
tools: [{ id: "tool:read", name: "Read tool", description: "Read files" }],
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("discovery normalizes skills, MCP, CLI, DSH, subagent, model, and tool candidates", () => {
|
|
19
|
+
const capabilities = discovered();
|
|
20
|
+
assert.deepEqual(new Set(capabilities.map((capability) => capability.kind)), new Set(["skill", "mcp", "cli", "dsh", "subagent", "model", "tool"]));
|
|
21
|
+
assert.equal(capabilities.find((capability) => capability.id === "cli:git").metadata.cli, "git");
|
|
22
|
+
assert.equal(capabilities.find((capability) => capability.id === "skill:inspect").verified, true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("explicit deterministic policy selects one discovered capability before Jev", async () => {
|
|
26
|
+
let providerCalls = 0;
|
|
27
|
+
const decision = await routeRequest({
|
|
28
|
+
intent: "make a bounded model decision",
|
|
29
|
+
context: { request: "model" },
|
|
30
|
+
policy: { deterministic: true, prefer_kind: "model", prefer_verified: true },
|
|
31
|
+
}, {
|
|
32
|
+
discovery: { models: [{ id: "model:jev", name: "Jev model", description: "Make bounded decisions", verified: true, risk: "low" }] },
|
|
33
|
+
provider: { name: "must-not-run", async decide() { providerCalls += 1; throw new Error("deterministic route should not call Jev"); } },
|
|
34
|
+
});
|
|
35
|
+
assert.equal(providerCalls, 0);
|
|
36
|
+
assert.equal(decision.status, "selected");
|
|
37
|
+
assert.equal(decision.selected, "model:jev");
|
|
38
|
+
assert.equal(decision.receipt.provider, "must-not-run:deterministic");
|
|
39
|
+
assert.equal(decision.candidates[0].verified, true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("availability policy excludes an unavailable discovered capability", async () => {
|
|
43
|
+
const decision = await routeRequest({
|
|
44
|
+
intent: "inspect",
|
|
45
|
+
context: {},
|
|
46
|
+
policy: { deterministic: true, deterministic_capability_id: "tool:offline" },
|
|
47
|
+
}, {
|
|
48
|
+
discovery: { tools: [{ id: "tool:offline", name: "Offline", description: "Unavailable", available: false }] },
|
|
49
|
+
provider: "demo",
|
|
50
|
+
});
|
|
51
|
+
assert.equal(decision.status, "no_decision");
|
|
52
|
+
assert.equal(decision.candidates[0].filtered, true);
|
|
53
|
+
assert.equal(decision.candidates[0].filter_reason, "capability unavailable");
|
|
54
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { filterContext } from "../src/relevance-filter.mjs";
|
|
4
|
+
import { projectState } from "../src/context-filter.mjs";
|
|
5
|
+
import { routeRequest } from "../src/route.mjs";
|
|
6
|
+
|
|
7
|
+
const capabilities = [{ id: "inspect", kind: "tool", name: "Inspect", description: "Inspect the supplied context", risk: "low" }];
|
|
8
|
+
|
|
9
|
+
function messages() {
|
|
10
|
+
return [
|
|
11
|
+
...Array.from({ length: 10 }, (_, index) => `old unrelated note ${index}`),
|
|
12
|
+
"Requirement: preserve /workspace/project/config.json exactly",
|
|
13
|
+
"command: `npm test` failed with exit status 1; Error: fixture failed",
|
|
14
|
+
"recent worker update",
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test("shadow mode never mutates context and reports evidence", () => {
|
|
19
|
+
const context = { messages: messages() };
|
|
20
|
+
const before = JSON.stringify(context);
|
|
21
|
+
const result = filterContext(context, { mode: "shadow", recent: 2 });
|
|
22
|
+
assert.strictEqual(result.context, context);
|
|
23
|
+
assert.equal(JSON.stringify(context), before);
|
|
24
|
+
assert.equal(result.report.mode, "shadow");
|
|
25
|
+
assert.equal(result.report.changed, false);
|
|
26
|
+
assert.ok(result.report.pinned >= 2);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("conservative mode drops stale noise but preserves exact evidence", () => {
|
|
30
|
+
const result = filterContext({ messages: messages() }, { mode: "conservative", recent: 2 });
|
|
31
|
+
assert.equal(result.report.mode, "conservative");
|
|
32
|
+
assert.equal(result.report.changed, true);
|
|
33
|
+
assert.ok(result.report.dropped > 0);
|
|
34
|
+
assert.ok(result.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"));
|
|
35
|
+
assert.ok(result.context.messages.includes("command: `npm test` failed with exit status 1; Error: fixture failed"));
|
|
36
|
+
assert.ok(!result.context.messages.includes("old unrelated note 0"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("route projection applies explicit filtering without losing pinned evidence", async () => {
|
|
40
|
+
let captured;
|
|
41
|
+
const decision = await routeRequest({
|
|
42
|
+
intent: "inspect project state",
|
|
43
|
+
policy: { context_filter_mode: "conservative" },
|
|
44
|
+
context: { messages: messages() },
|
|
45
|
+
capabilities,
|
|
46
|
+
}, {
|
|
47
|
+
provider: {
|
|
48
|
+
name: "context-filter-test-provider",
|
|
49
|
+
async decide({ state }) {
|
|
50
|
+
captured = state;
|
|
51
|
+
return { answers: { tool: { type: "choice", choice: "inspect", probabilities: { inspect: 1 }, confidence: 1 } } };
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
assert.equal(decision.status, "selected");
|
|
56
|
+
assert.equal(captured.context_filter.mode, "conservative");
|
|
57
|
+
assert.ok(captured.context.messages.includes("Requirement: preserve /workspace/project/config.json exactly"));
|
|
58
|
+
assert.ok(captured.context.messages.includes("command: `npm test` failed with exit status 1; Error: fixture failed"));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("no mode keeps the existing projection contract", () => {
|
|
62
|
+
const result = projectState({ intent: "inspect", context: { messages: ["plain"] } }, capabilities, 6_000);
|
|
63
|
+
assert.equal(result.state.context_filter, undefined);
|
|
64
|
+
assert.deepEqual(result.state.context, { messages: ["plain"] });
|
|
65
|
+
});
|
|
@@ -0,0 +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
|
+
});
|