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.
Files changed (66) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CONTRIBUTING.md +146 -0
  3. package/LICENSE +21 -0
  4. package/README.md +126 -0
  5. package/README.ru.md +117 -0
  6. package/README.zh-CN.md +117 -0
  7. package/RELEASE.md +53 -0
  8. package/SECURITY.md +56 -0
  9. package/bin/jev.mjs +214 -0
  10. package/config/codex.mcp.toml +8 -0
  11. package/config/generic-mcp.json +13 -0
  12. package/config/hermes.mcp.yaml +9 -0
  13. package/config/jev.example.json +22 -0
  14. package/config/omp.mcp.json +14 -0
  15. package/config/providers.env.example +13 -0
  16. package/docs/COMPATIBILITY.md +15 -0
  17. package/docs/SCHEMA-VERSIONING.md +94 -0
  18. package/examples/capabilities.json +39 -0
  19. package/examples/route-request.json +28 -0
  20. package/integrations/codex/.codex-plugin/plugin.json +19 -0
  21. package/integrations/codex/.mcp.json +11 -0
  22. package/integrations/codex/AGENTS.md +1 -0
  23. package/integrations/codex/run-mcp.mjs +9 -0
  24. package/integrations/codex/skills/jev-route/SKILL.md +17 -0
  25. package/integrations/hermes/__init__.py +51 -0
  26. package/integrations/hermes/plugin.yaml +5 -0
  27. package/integrations/hermes/schemas.py +12 -0
  28. package/integrations/omp/extension.js +105 -0
  29. package/integrations/template/README.md +10 -0
  30. package/integrations/template/adapter.mjs +87 -0
  31. package/package.json +59 -0
  32. package/scripts/benchmark.mjs +35 -0
  33. package/scripts/browser-benchmark.mjs +92 -0
  34. package/scripts/browser-e2e.mjs +117 -0
  35. package/scripts/capability-e2e.mjs +31 -0
  36. package/scripts/clean-install-smoke.mjs +162 -0
  37. package/scripts/codex-mcp-smoke.mjs +119 -0
  38. package/scripts/context-filter-e2e.mjs +45 -0
  39. package/scripts/fail-open-smoke.mjs +120 -0
  40. package/scripts/feature-flags-smoke.mjs +46 -0
  41. package/scripts/mcp-receipt-smoke.mjs +98 -0
  42. package/scripts/openrouter-choice.mjs +59 -0
  43. package/scripts/replay-eval.mjs +124 -0
  44. package/scripts/smoke.mjs +34 -0
  45. package/scripts/supervision-e2e.mjs +109 -0
  46. package/src/browser.mjs +569 -0
  47. package/src/cli.mjs +33 -0
  48. package/src/config.mjs +75 -0
  49. package/src/context-filter.mjs +56 -0
  50. package/src/contract.mjs +72 -0
  51. package/src/discovery.mjs +65 -0
  52. package/src/mcp-server.mjs +210 -0
  53. package/src/providers/demo.mjs +52 -0
  54. package/src/providers/typesafe.mjs +126 -0
  55. package/src/receipts.mjs +226 -0
  56. package/src/registry.mjs +109 -0
  57. package/src/relevance-filter.mjs +99 -0
  58. package/src/route.mjs +221 -0
  59. package/src/supervision.mjs +244 -0
  60. package/test/browser.test.mjs +199 -0
  61. package/test/capability.test.mjs +54 -0
  62. package/test/context-filter.test.mjs +65 -0
  63. package/test/openrouter-provider.test.mjs +55 -0
  64. package/test/receipts.test.mjs +71 -0
  65. package/test/route.test.mjs +74 -0
  66. package/test/supervision.test.mjs +99 -0
@@ -0,0 +1,226 @@
1
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { stableJson } from "./contract.mjs";
4
+ import { sanitizeContext } from "./context-filter.mjs";
5
+
6
+ const MAX_RESULT_BYTES = 8_000;
7
+
8
+ export function replayCasePath(filePath = process.env.JEV_REPLAY_CASES) {
9
+ return resolve(filePath || ".jev/replay/cases.jsonl");
10
+ }
11
+
12
+ export function buildRoutingCase({ request, decision, recordedAt = new Date().toISOString() }) {
13
+ const correlationId = decision.correlation_id ?? decision.receipt?.correlation_id ?? decision.receipt?.request_id;
14
+ if (!correlationId) throw new TypeError("decision correlation_id is required");
15
+ return {
16
+ record_type: "routing_case",
17
+ schema_version: 1,
18
+ case_id: correlationId,
19
+ correlation_id: correlationId,
20
+ recorded_at: recordedAt,
21
+ harness: request.harness ?? "unknown",
22
+ request: replayRequest(request),
23
+ decision: decisionSummary(decision),
24
+ };
25
+ }
26
+
27
+ export function buildExecutionReceipt({ request, decision, host, recordedAt = new Date().toISOString() }) {
28
+ const correlationId = decision.correlation_id ?? decision.receipt?.correlation_id ?? decision.receipt?.request_id;
29
+ if (!correlationId) throw new TypeError("decision correlation_id is required");
30
+ if (!host || typeof host !== "object") throw new TypeError("host execution is required");
31
+ const exitStatus = Number.isInteger(host.exit_status) ? host.exit_status : null;
32
+ const status = typeof host.status === "string"
33
+ ? host.status
34
+ : exitStatus === null || exitStatus === 0 ? "completed" : "failed";
35
+ return {
36
+ record_type: "execution_receipt",
37
+ schema_version: 1,
38
+ receipt_id: `${correlationId}:execution`,
39
+ correlation_id: correlationId,
40
+ recorded_at: recordedAt,
41
+ harness: host.harness ?? request.harness ?? "unknown",
42
+ candidates: decision.candidates ?? [],
43
+ selected: decision.selected ?? null,
44
+ confidence: decision.confidence ?? null,
45
+ probabilities: decision.probabilities ?? {},
46
+ fallback: decision.fallback ?? null,
47
+ jev: {
48
+ correlation_id: correlationId,
49
+ request_id: decision.receipt?.request_id ?? correlationId,
50
+ provider: decision.receipt?.provider ?? null,
51
+ latency_ms: decision.receipt?.latency_ms ?? null,
52
+ cost_usd: decision.receipt?.cost_usd ?? decision.raw_jev?.usage?.cost ?? null,
53
+ status: decision.status ?? null,
54
+ },
55
+ host: {
56
+ capability_id: host.capability_id ?? decision.selected ?? null,
57
+ status,
58
+ result: boundedValue(host.result ?? host.host_result ?? null),
59
+ error: boundedValue(host.error ?? null),
60
+ exit_status: exitStatus,
61
+ duration_ms: finiteNumber(host.duration_ms),
62
+ started_at: host.started_at ?? null,
63
+ completed_at: host.completed_at ?? recordedAt,
64
+ browser: boundedValue(host.browser ?? null),
65
+ },
66
+ };
67
+ }
68
+
69
+ export async function appendRoutingCase({ path, request, decision }) {
70
+ const record = buildRoutingCase({ request, decision });
71
+ await appendRecord(path, record);
72
+ return { record, path: replayCasePath(path) };
73
+ }
74
+
75
+ export async function appendExecutionReceipt({ path, request, decision, host }) {
76
+ const record = buildExecutionReceipt({ request, decision, host });
77
+ await appendRecord(path, record);
78
+ return { record, path: replayCasePath(path) };
79
+ }
80
+
81
+ export function buildSupervisionReceipt({ request, result, recordedAt = new Date().toISOString() }) {
82
+ const correlationId = result.receipt?.correlation_id;
83
+ if (!correlationId) throw new TypeError("supervision result correlation_id is required");
84
+ return {
85
+ record_type: "supervision_case",
86
+ schema_version: 1,
87
+ case_id: correlationId,
88
+ correlation_id: correlationId,
89
+ recorded_at: recordedAt,
90
+ harness: request.harness ?? "unknown",
91
+ request: {
92
+ schema_version: request.schema_version ?? 1,
93
+ harness: request.harness ?? "unknown",
94
+ intent: request.intent,
95
+ context: sanitizeContext(request.context ?? {}),
96
+ actor_permissions: request.actor_permissions ?? [],
97
+ policy: request.policy ?? {},
98
+ },
99
+ supervision: {
100
+ status: result.status ?? null,
101
+ action: result.action ?? "continue",
102
+ reason: result.reason ?? null,
103
+ assessment: result.assessment ?? null,
104
+ policy: result.policy ?? null,
105
+ jev: {
106
+ provider: result.receipt.provider ?? null,
107
+ latency_ms: result.receipt.latency_ms ?? null,
108
+ cost_usd: result.receipt.cost_usd ?? null,
109
+ dimensions: result.receipt.dimensions ?? null,
110
+ },
111
+ },
112
+ };
113
+ }
114
+
115
+ export async function appendSupervisionReceipt({ path, request, result }) {
116
+ const record = buildSupervisionReceipt({ request, result });
117
+ await appendRecord(path, record);
118
+ return { record, path: replayCasePath(path) };
119
+ }
120
+
121
+ export async function readRoutingCases(path) {
122
+ const resolved = replayCasePath(path);
123
+ let text;
124
+ try {
125
+ text = await readFile(resolved, "utf8");
126
+ } catch (error) {
127
+ if (error?.code === "ENOENT") return [];
128
+ throw error;
129
+ }
130
+ return text.split(/\r?\n/).filter(Boolean).flatMap((line, index) => {
131
+ try {
132
+ const record = JSON.parse(line);
133
+ return record.record_type === "routing_case" ? [{ ...record, source_line: index + 1 }] : [];
134
+ } catch {
135
+ return [];
136
+ }
137
+ });
138
+ }
139
+
140
+ export async function readSupervisionCases(path) {
141
+ const resolved = replayCasePath(path);
142
+ let text;
143
+ try {
144
+ text = await readFile(resolved, "utf8");
145
+ } catch (error) {
146
+ if (error?.code === "ENOENT") return [];
147
+ throw error;
148
+ }
149
+ return text.split(/\r?\n/).filter(Boolean).flatMap((line, index) => {
150
+ try {
151
+ const record = JSON.parse(line);
152
+ return record.record_type === "supervision_case" ? [{ ...record, source_line: index + 1 }] : [];
153
+ } catch {
154
+ return [];
155
+ }
156
+ });
157
+ }
158
+
159
+ function replayRequest(request) {
160
+ return {
161
+ schema_version: request.schema_version ?? 1,
162
+ harness: request.harness ?? "unknown",
163
+ intent: request.intent,
164
+ context: sanitizeContext(request.context ?? {}),
165
+ actor: request.actor,
166
+ actor_permissions: Array.isArray(request.actor_permissions) ? request.actor_permissions.filter((value) => typeof value === "string") : undefined,
167
+ policy: request.policy && typeof request.policy === "object" ? request.policy : {},
168
+ capabilities: Array.isArray(request.capabilities) ? request.capabilities.map(replayCapability) : [],
169
+ };
170
+ }
171
+
172
+ function replayCapability(capability) {
173
+ const risk = capability.risk?.level ?? capability.risk ?? "medium";
174
+ return {
175
+ id: capability.id,
176
+ kind: capability.kind ?? capability.type ?? "tool",
177
+ name: capability.name,
178
+ description: capability.description,
179
+ permissions: Array.isArray(capability.permissions) ? capability.permissions.filter((value) => typeof value === "string") : [],
180
+ risk,
181
+ available: capability.available,
182
+ availability: capability.availability,
183
+ source: typeof capability.source === "string" ? capability.source : null,
184
+ verified: capability.verified === true,
185
+ metadata: capability.metadata && typeof capability.metadata === "object" ? capability.metadata : {},
186
+ policy: capability.policy,
187
+ };
188
+ }
189
+
190
+ function decisionSummary(decision) {
191
+ return {
192
+ status: decision.status ?? null,
193
+ selected: decision.selected ?? null,
194
+ confidence: decision.confidence ?? null,
195
+ probabilities: decision.probabilities ?? {},
196
+ fallback: decision.fallback ?? null,
197
+ candidates: decision.candidates ?? [],
198
+ receipt: {
199
+ correlation_id: decision.receipt?.correlation_id ?? decision.correlation_id ?? null,
200
+ request_id: decision.receipt?.request_id ?? null,
201
+ provider: decision.receipt?.provider ?? null,
202
+ latency_ms: decision.receipt?.latency_ms ?? null,
203
+ cost_usd: decision.receipt?.cost_usd ?? decision.raw_jev?.usage?.cost ?? null,
204
+ candidate_count: decision.receipt?.candidate_count ?? null,
205
+ context_bytes: decision.receipt?.context_bytes ?? null,
206
+ },
207
+ };
208
+ }
209
+
210
+ async function appendRecord(path, record) {
211
+ const resolved = replayCasePath(path);
212
+ await mkdir(dirname(resolved), { recursive: true });
213
+ await appendFile(resolved, `${stableJson(record)}\n`, "utf8");
214
+ }
215
+
216
+ function finiteNumber(value) {
217
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
218
+ }
219
+
220
+ function boundedValue(value) {
221
+ if (value === null || value === undefined) return null;
222
+ if (typeof value === "string") return value.length > MAX_RESULT_BYTES ? `${value.slice(0, MAX_RESULT_BYTES)}…` : value;
223
+ const sanitized = sanitizeContext(value);
224
+ const encoded = stableJson(sanitized);
225
+ return encoded.length > MAX_RESULT_BYTES ? `${encoded.slice(0, MAX_RESULT_BYTES)}…` : sanitized;
226
+ }
@@ -0,0 +1,109 @@
1
+ import { CAPABILITY_KINDS, RISK_ORDER } from "./contract.mjs";
2
+
3
+ const KIND_ALIASES = Object.freeze({ mcp_tool: "mcp" });
4
+
5
+ export const DEFAULT_POLICY = Object.freeze({
6
+ min_confidence: 0.62,
7
+ max_risk: "high",
8
+ confirmation_risk_levels: ["medium", "high"],
9
+ });
10
+
11
+ export function normalizeCapability(raw, index = 0) {
12
+ if (!raw || typeof raw !== "object") throw new TypeError(`capabilities[${index}] must be an object`);
13
+ if (typeof raw.id !== "string" || raw.id.trim() === "") throw new TypeError(`capabilities[${index}].id is required`);
14
+ if (typeof raw.name !== "string" || raw.name.trim() === "") throw new TypeError(`capabilities[${index}].name is required`);
15
+ if (typeof raw.description !== "string" || raw.description.trim() === "") throw new TypeError(`capabilities[${index}].description is required`);
16
+
17
+ const rawKind = raw.kind ?? raw.type ?? raw.execution?.mode ?? "tool";
18
+ const kind = KIND_ALIASES[rawKind] ?? rawKind;
19
+ if (!CAPABILITY_KINDS.has(kind)) throw new TypeError(`capabilities[${index}].kind is unsupported: ${rawKind}`);
20
+
21
+ const risk = raw.risk?.level ?? raw.risk ?? "medium";
22
+ if (!(risk in RISK_ORDER)) throw new TypeError(`capabilities[${index}].risk is unsupported: ${risk}`);
23
+
24
+ return {
25
+ id: raw.id.trim(),
26
+ kind,
27
+ name: raw.name.trim(),
28
+ description: raw.description.trim(),
29
+ permissions: Array.isArray(raw.permissions) ? raw.permissions.filter((value) => typeof value === "string") : [],
30
+ risk,
31
+ available: raw.available !== false && raw.availability?.available !== false,
32
+ availability_reason: raw.availability?.reason ?? null,
33
+ source: typeof raw.source === "string" ? raw.source : null,
34
+ verified: raw.verified === true,
35
+ metadata: normalizeMetadata(raw.metadata ?? raw.discovery),
36
+ execution: {
37
+ mode: "host",
38
+ target: typeof raw.execution?.target === "string" ? raw.execution.target : undefined,
39
+ },
40
+ requires_confirmation: Boolean(raw.policy?.requires_confirmation) || risk !== "low",
41
+ };
42
+ }
43
+
44
+ export function normalizeCapabilities(rawCapabilities) {
45
+ const seen = new Set();
46
+ return rawCapabilities.map((raw, index) => {
47
+ const capability = normalizeCapability(raw, index);
48
+ if (seen.has(capability.id)) throw new TypeError(`duplicate capability id: ${capability.id}`);
49
+ seen.add(capability.id);
50
+ return capability;
51
+ });
52
+ }
53
+
54
+ export function filterCapabilities(capabilities, request, policy = {}) {
55
+ const effective = { ...DEFAULT_POLICY, ...policy };
56
+ const maxRisk = effective.max_risk ?? "high";
57
+ const actorPermissions = Array.isArray(request.actor_permissions) ? new Set(request.actor_permissions) : null;
58
+ const assessments = capabilities.map((capability) => {
59
+ let reason = null;
60
+ if (!capability.available) reason = capability.availability_reason || "capability unavailable";
61
+ else if (RISK_ORDER[capability.risk] > RISK_ORDER[maxRisk]) reason = `risk ${capability.risk} exceeds policy maximum ${maxRisk}`;
62
+ else if (actorPermissions && capability.permissions.some((permission) => !actorPermissions.has(permission))) reason = "actor lacks required capability permissions";
63
+ return { capability, filtered: Boolean(reason), filter_reason: reason };
64
+ });
65
+ return {
66
+ policy: effective,
67
+ assessments,
68
+ eligible: assessments.filter((assessment) => !assessment.filtered).map((assessment) => assessment.capability),
69
+ };
70
+ }
71
+ export function decisionCandidates(assessments, probabilities = {}, confidence = null) {
72
+ return assessments.map(({ capability, filtered, filter_reason }) => ({
73
+ id: capability.id,
74
+ kind: capability.kind,
75
+ name: capability.name,
76
+ risk: capability.risk,
77
+ available: capability.available,
78
+ filtered,
79
+ filter_reason,
80
+ probability: typeof probabilities[capability.id] === "number" ? probabilities[capability.id] : null,
81
+ confidence: confidence ?? null,
82
+ requires_confirmation: capability.requires_confirmation,
83
+ source: capability.source,
84
+ verified: capability.verified,
85
+ metadata: capability.metadata,
86
+ }));
87
+ }
88
+
89
+ export function deterministicCandidate(capabilities, policy = {}) {
90
+ let eligible = capabilities;
91
+ const requestedId = typeof policy.deterministic_capability_id === "string" ? policy.deterministic_capability_id : null;
92
+ if (requestedId) eligible = eligible.filter((capability) => capability.id === requestedId);
93
+ if (typeof policy.prefer_kind === "string") eligible = eligible.filter((capability) => capability.kind === policy.prefer_kind);
94
+ if (typeof policy.prefer_source === "string") eligible = eligible.filter((capability) => capability.source === policy.prefer_source);
95
+ if (policy.prefer_verified === true) eligible = eligible.filter((capability) => capability.verified);
96
+ if (eligible.length !== 1) return null;
97
+ return {
98
+ capability: eligible[0],
99
+ reason: requestedId ? "explicit capability policy" : "one capability remains after deterministic policy",
100
+ };
101
+ }
102
+
103
+ function normalizeMetadata(value) {
104
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
105
+ const allowed = ["skill", "mcp", "cli", "dsh", "subagent", "model", "manifest", "source_ref"];
106
+ return Object.fromEntries(allowed
107
+ .filter((key) => typeof value[key] === "string" || typeof value[key] === "boolean")
108
+ .map((key) => [key, value[key]]));
109
+ }
@@ -0,0 +1,99 @@
1
+ const MODES = new Set(["shadow", "conservative"]);
2
+ const SIGNALS = [
3
+ /(?:^|[\s"'`])(?:\/[\w.\-~]+|~\/[^\s"'`]+|[A-Za-z]:\\[^\s"'`]+)/u,
4
+ /\b(?:error|exception|traceback|failed|failure|exit\s+status|stderr)\b/i,
5
+ /`[^`\n]+`/u,
6
+ /(?:^|\n)\s*(?:[$#])\s*[A-Za-z0-9_.-]+/u,
7
+ /\b(?:must|required|requirement|acceptance|should|need\s+to)\b/i,
8
+ ];
9
+ const LIST_KEYS = new Set(["messages", "events", "logs", "tool_results", "history", "transcript"]);
10
+
11
+ export function normalizeRelevanceMode(mode) {
12
+ if (mode === true) return "shadow";
13
+ if (typeof mode !== "string") return null;
14
+ const normalized = mode.trim().toLowerCase();
15
+ return MODES.has(normalized) ? normalized : null;
16
+ }
17
+
18
+ export function filterContext(context, { mode = "shadow", recent = 8 } = {}) {
19
+ const normalizedMode = normalizeRelevanceMode(mode);
20
+ if (!normalizedMode) return { context, report: disabledReport() };
21
+ const source = context && typeof context === "object" ? context : {};
22
+ const report = {
23
+ mode: normalizedMode,
24
+ changed: false,
25
+ considered: 0,
26
+ kept: 0,
27
+ dropped: 0,
28
+ pinned: 0,
29
+ pinned_reasons: [],
30
+ };
31
+ if (normalizedMode === "shadow") {
32
+ inspectValue(source, report);
33
+ return { context: source, report };
34
+ }
35
+
36
+ const output = Array.isArray(source) ? source.slice() : { ...source };
37
+ for (const [key, value] of Object.entries(source)) {
38
+ if (!LIST_KEYS.has(key) || !Array.isArray(value)) continue;
39
+ const filtered = filterList(value, normalizedMode, recent, report);
40
+ output[key] = filtered;
41
+ }
42
+ if (report.dropped > 0) {
43
+ report.changed = true;
44
+ output._jev_relevance = {
45
+ mode: normalizedMode,
46
+ dropped: report.dropped,
47
+ pinned: report.pinned,
48
+ };
49
+ }
50
+ return { context: output, report };
51
+ }
52
+
53
+ export function isPinnedEvidence(value) {
54
+ const text = typeof value === "string" ? value : JSON.stringify(value ?? "");
55
+ const reasons = [];
56
+ if (SIGNALS[0].test(text)) reasons.push("path");
57
+ if (SIGNALS[1].test(text)) reasons.push("error");
58
+ if (SIGNALS[2].test(text) || SIGNALS[3].test(text)) reasons.push("command");
59
+ if (SIGNALS[4].test(text)) reasons.push("requirement");
60
+ return { pinned: reasons.length > 0, reasons };
61
+ }
62
+
63
+ function filterList(items, mode, recent, report) {
64
+ const keepFrom = Math.max(0, items.length - Math.max(0, recent));
65
+ return items.filter((item, index) => {
66
+ report.considered += 1;
67
+ const evidence = isPinnedEvidence(item);
68
+ const keep = evidence.pinned || index >= keepFrom || mode === "conservative" && index === items.length - 1;
69
+ if (evidence.pinned) {
70
+ report.pinned += 1;
71
+ report.pinned_reasons.push(...evidence.reasons);
72
+ }
73
+ if (keep) report.kept += 1;
74
+ else report.dropped += 1;
75
+ return keep;
76
+ });
77
+ }
78
+
79
+ function inspectValue(value, report) {
80
+ if (Array.isArray(value)) {
81
+ for (const item of value) {
82
+ report.considered += 1;
83
+ const evidence = isPinnedEvidence(item);
84
+ if (evidence.pinned) {
85
+ report.pinned += 1;
86
+ report.pinned_reasons.push(...evidence.reasons);
87
+ }
88
+ inspectValue(item, report);
89
+ }
90
+ return;
91
+ }
92
+ if (value && typeof value === "object") {
93
+ for (const child of Object.values(value)) inspectValue(child, report);
94
+ }
95
+ }
96
+
97
+ function disabledReport() {
98
+ return { mode: null, changed: false, considered: 0, kept: 0, dropped: 0, pinned: 0, pinned_reasons: [] };
99
+ }
package/src/route.mjs ADDED
@@ -0,0 +1,221 @@
1
+ import { byteLength, decisionEnvelope, normalizeRequest } from "./contract.mjs";
2
+ import { projectState } from "./context-filter.mjs";
3
+ import { discoverCapabilities } from "./discovery.mjs";
4
+ import { decisionCandidates, DEFAULT_POLICY, deterministicCandidate, filterCapabilities, normalizeCapabilities } from "./registry.mjs";
5
+ import { DemoProvider } from "./providers/demo.mjs";
6
+ import { OpenRouterDecisionsProvider, TypeSafeProvider } from "./providers/typesafe.mjs";
7
+
8
+ export async function routeRequest(input, options = {}) {
9
+ const discovered = options.discovery ? discoverCapabilities(options.discovery) : [];
10
+ const request = normalizeRequest(discovered.length ? { ...input, capabilities: [...(Array.isArray(input?.capabilities) ? input.capabilities : []), ...discovered] } : input);
11
+ if (options.engine === "jevrouter") return routeViaJevRouter(request, options);
12
+
13
+ const started = performance.now();
14
+ const effectivePolicy = { ...DEFAULT_POLICY, ...request.policy, ...(options.policy ?? {}) };
15
+ const capabilities = normalizeCapabilities(request.capabilities);
16
+ const { policy, assessments, eligible } = filterCapabilities(capabilities, request, effectivePolicy);
17
+ const projection = projectState(request, eligible, options.maxContextBytes ?? 6_000, { mode: options.contextFilterMode });
18
+ const provider = resolveProvider(options.provider ?? "demo", options);
19
+ const base = {
20
+ provider: provider.name,
21
+ candidateCount: eligible.length,
22
+ contextBytes: projection.context_bytes,
23
+ started,
24
+ };
25
+
26
+ if (process.env.JEV_LAYER_ENABLED === "0") {
27
+ return fallback(base, assessments, "disabled", "Jev layer disabled by JEV_LAYER_ENABLED=0", projection.state);
28
+ }
29
+ if (eligible.length === 0) {
30
+ return decisionEnvelope({
31
+ status: "no_decision",
32
+ reason: "no capability passed availability, risk, and permission policy",
33
+ provider: provider.name,
34
+ latencyMs: elapsed(started),
35
+ candidateCount: 0,
36
+ contextBytes: projection.context_bytes,
37
+ candidates: decisionCandidates(assessments),
38
+ });
39
+ }
40
+ const deterministic = policy.deterministic === true
41
+ ? deterministicCandidate(eligible, policy)
42
+ : null;
43
+ if (deterministic) {
44
+ const selected = deterministic.capability;
45
+ const probabilities = Object.fromEntries(eligible.map((candidate) => [candidate.id, candidate.id === selected.id ? 1 : 0]));
46
+ const candidates = decisionCandidates(assessments, probabilities, 1);
47
+ const requiresConfirmation = policy.confirmation_risk_levels.includes(selected.risk) || selected.requires_confirmation;
48
+ return decisionEnvelope({
49
+ status: requiresConfirmation ? "needs_confirmation" : "selected",
50
+ reason: deterministic.reason,
51
+ provider: `${provider.name}:deterministic`,
52
+ latencyMs: elapsed(started),
53
+ candidateCount: eligible.length,
54
+ contextBytes: projection.context_bytes,
55
+ candidates,
56
+ selected: selected.id,
57
+ probabilities,
58
+ confidence: 1,
59
+ });
60
+ }
61
+
62
+ let raw;
63
+ try {
64
+ raw = await provider.decide({ state: projection.state, candidates: eligible });
65
+ } catch (error) {
66
+ return fallback(base, assessments, "provider_error", error instanceof Error ? error.message : String(error), projection.state);
67
+ }
68
+
69
+ let answer;
70
+ try {
71
+ answer = readChoiceAnswer(raw);
72
+ } catch (error) {
73
+ return fallback(base, assessments, "malformed_response", error instanceof Error ? error.message : String(error), projection.state, raw);
74
+ }
75
+
76
+ const selected = eligible.find((candidate) => candidate.id === answer.choice);
77
+ const candidates = decisionCandidates(assessments, answer.probabilities, answer.confidence);
78
+ if (!selected) {
79
+ return decisionEnvelope({
80
+ status: "fallback",
81
+ reason: `provider selected unknown or filtered capability: ${answer.choice}`,
82
+ provider: provider.name,
83
+ latencyMs: elapsed(started),
84
+ candidateCount: eligible.length,
85
+ contextBytes: projection.context_bytes,
86
+ candidates,
87
+ probabilities: answer.probabilities,
88
+ confidence: answer.confidence,
89
+ rawJev: raw,
90
+ fallback: { type: "invalid_selection", reason: "selection is not in the eligible candidate set" },
91
+ });
92
+ }
93
+ if (answer.confidence < policy.min_confidence) {
94
+ return decisionEnvelope({
95
+ status: "fallback",
96
+ reason: `confidence ${answer.confidence.toFixed(3)} is below policy minimum ${policy.min_confidence.toFixed(3)}`,
97
+ provider: provider.name,
98
+ latencyMs: elapsed(started),
99
+ candidateCount: eligible.length,
100
+ contextBytes: projection.context_bytes,
101
+ candidates,
102
+ selected: selected.id,
103
+ probabilities: answer.probabilities,
104
+ confidence: answer.confidence,
105
+ rawJev: raw,
106
+ fallback: { type: "low_confidence", reason: "host must continue with normal planning" },
107
+ });
108
+ }
109
+
110
+ const requiresConfirmation = policy.confirmation_risk_levels.includes(selected.risk) || selected.requires_confirmation;
111
+ return decisionEnvelope({
112
+ status: requiresConfirmation ? "needs_confirmation" : "selected",
113
+ reason: requiresConfirmation ? "selected capability requires host confirmation" : "selected by bounded Jev decision",
114
+ provider: provider.name,
115
+ latencyMs: elapsed(started),
116
+ candidateCount: eligible.length,
117
+ contextBytes: projection.context_bytes,
118
+ candidates,
119
+ selected: selected.id,
120
+ probabilities: answer.probabilities,
121
+ confidence: answer.confidence,
122
+ rawJev: raw,
123
+ });
124
+ }
125
+
126
+ function resolveProvider(provider, options) {
127
+ if (provider && typeof provider === "object" && typeof provider.decide === "function") return provider;
128
+ if (provider === "demo") return new DemoProvider();
129
+ if (provider === "openrouter") return new OpenRouterDecisionsProvider(options.openrouter);
130
+ throw new Error(`unsupported provider: ${provider}`);
131
+ }
132
+
133
+ function readChoiceAnswer(raw) {
134
+ const answers = raw?.answers;
135
+ if (!answers || typeof answers !== "object") throw new Error("response has no answers object");
136
+ const answer = answers.tool ?? Object.values(answers).find((value) => value?.type === "choice");
137
+ if (!answer || answer.type !== "choice" || typeof answer.choice !== "string" || !answer.probabilities || typeof answer.probabilities !== "object") {
138
+ throw new Error("response does not contain a valid Choice answer");
139
+ }
140
+ const probabilities = Object.fromEntries(Object.entries(answer.probabilities).map(([id, value]) => {
141
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) throw new Error(`invalid probability for ${id}`);
142
+ return [id, value];
143
+ }));
144
+ const confidence = typeof answer.confidence === "number" ? answer.confidence : Math.max(...Object.values(probabilities), 0);
145
+ if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) throw new Error("invalid confidence");
146
+ return { choice: answer.choice, probabilities, confidence };
147
+ }
148
+
149
+ function fallback(base, assessments, type, reason, state, rawJev = null) {
150
+ return decisionEnvelope({
151
+ status: "fallback",
152
+ reason,
153
+ provider: base.provider,
154
+ latencyMs: elapsed(base.started),
155
+ candidateCount: base.candidateCount,
156
+ contextBytes: byteLength(state),
157
+ candidates: decisionCandidates(assessments),
158
+ rawJev,
159
+ fallback: { type, reason },
160
+ });
161
+ }
162
+
163
+ function elapsed(started) {
164
+ return Number((performance.now() - started).toFixed(3));
165
+ }
166
+
167
+ async function routeViaJevRouter(request, options) {
168
+ const started = performance.now();
169
+ const capabilities = normalizeCapabilities(request.capabilities);
170
+ const { assessments, eligible } = filterCapabilities(capabilities, request, { ...DEFAULT_POLICY, ...request.policy, ...(options.policy ?? {}) });
171
+ const projection = projectState(request, eligible, options.maxContextBytes ?? 6_000, { mode: options.contextFilterMode });
172
+ if (eligible.length === 0) return decisionEnvelope({ status: "no_decision", reason: "no eligible capability", provider: "jevrouter", latencyMs: elapsed(started), candidateCount: 0, contextBytes: projection.context_bytes, candidates: decisionCandidates(assessments) });
173
+
174
+ let module;
175
+ try {
176
+ module = await import("jevrouter");
177
+ } catch (error) {
178
+ return fallback({ provider: "jevrouter", candidateCount: eligible.length, contextBytes: projection.context_bytes, started }, assessments, "dependency_error", error instanceof Error ? error.message : String(error), projection.state);
179
+ }
180
+ try {
181
+ const result = await module.route({
182
+ request: request.intent,
183
+ context: projection.state.context,
184
+ candidates: eligible.map(toJevRouterCapability),
185
+ }, {
186
+ provider: options.innerProvider ?? process.env.JEVROUTER_PROVIDER ?? "demo",
187
+ policy: options.policy,
188
+ });
189
+ const selected = result.decision?.selected ?? null;
190
+ const raw = result.raw_jev ?? null;
191
+ const candidates = decisionCandidates(assessments, Object.fromEntries((result.decision?.candidates ?? []).map((candidate) => [candidate.id, candidate.jev_probability ?? 0])), null);
192
+ return decisionEnvelope({
193
+ status: result.status === "selected" ? "selected" : result.status === "needs_confirmation" ? "needs_confirmation" : "fallback",
194
+ selected,
195
+ reason: result.fallback?.reason ?? "selected by JevRouter",
196
+ provider: `jevrouter:${options.innerProvider ?? process.env.JEVROUTER_PROVIDER ?? "demo"}`,
197
+ latencyMs: elapsed(started),
198
+ candidateCount: eligible.length,
199
+ contextBytes: projection.context_bytes,
200
+ candidates,
201
+ rawJev: raw,
202
+ fallback: result.fallback?.type ? { type: result.fallback.type, reason: result.fallback.reason } : null,
203
+ });
204
+ } catch (error) {
205
+ return fallback({ provider: "jevrouter", candidateCount: eligible.length, contextBytes: projection.context_bytes, started }, assessments, "provider_error", error instanceof Error ? error.message : String(error), projection.state);
206
+ }
207
+ }
208
+
209
+ function toJevRouterCapability(capability) {
210
+ return {
211
+ id: capability.id,
212
+ name: capability.name,
213
+ type: capability.kind === "mcp" ? "mcp_tool" : capability.kind,
214
+ description: capability.description,
215
+ permissions: capability.permissions,
216
+ risk: { level: capability.risk },
217
+ availability: { available: capability.available },
218
+ execution: { mode: capability.kind === "mcp" ? "mcp" : capability.kind, target: capability.execution.target },
219
+ policy: { requires_confirmation: capability.requires_confirmation },
220
+ };
221
+ }