jev-layer 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +73 -34
  2. package/CONTRIBUTING.md +146 -146
  3. package/LICENSE +21 -21
  4. package/README.md +125 -126
  5. package/README.ru.md +140 -117
  6. package/README.zh-CN.md +139 -117
  7. package/RELEASE.md +53 -53
  8. package/SECURITY.md +64 -56
  9. package/bin/jev.mjs +215 -213
  10. package/config/codex.mcp.toml +8 -8
  11. package/config/generic-mcp.json +13 -13
  12. package/config/hermes.mcp.yaml +9 -9
  13. package/config/jev.example.json +23 -22
  14. package/config/omp.mcp.json +14 -14
  15. package/config/providers.env.example +13 -13
  16. package/docs/AGENT-IMPLEMENTATION.md +306 -0
  17. package/docs/COMPATIBILITY.md +24 -15
  18. package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +187 -0
  19. package/docs/PROVIDERS.md +109 -0
  20. package/docs/SCHEMA-VERSIONING.md +101 -94
  21. package/docs/architecture.svg +77 -0
  22. package/docs/jev-layer-banner.svg +38 -0
  23. package/examples/capabilities.json +39 -39
  24. package/examples/route-request.json +28 -28
  25. package/integrations/codex/.codex-plugin/plugin.json +19 -19
  26. package/integrations/codex/.mcp.json +11 -11
  27. package/integrations/codex/AGENTS.md +1 -1
  28. package/integrations/codex/run-mcp.mjs +9 -9
  29. package/integrations/codex/skills/jev-route/SKILL.md +17 -17
  30. package/integrations/hermes/__init__.py +156 -51
  31. package/integrations/hermes/plugin.yaml +11 -5
  32. package/integrations/hermes/schemas.py +120 -12
  33. package/integrations/omp/extension.js +106 -105
  34. package/integrations/template/README.md +10 -10
  35. package/integrations/template/adapter.mjs +87 -87
  36. package/package.json +60 -59
  37. package/scripts/benchmark.mjs +35 -35
  38. package/scripts/browser-benchmark.mjs +92 -92
  39. package/scripts/browser-e2e.mjs +117 -117
  40. package/scripts/capability-e2e.mjs +31 -31
  41. package/scripts/clean-install-smoke.mjs +176 -162
  42. package/scripts/codex-mcp-smoke.mjs +119 -119
  43. package/scripts/context-filter-e2e.mjs +45 -45
  44. package/scripts/fail-open-smoke.mjs +120 -120
  45. package/scripts/feature-flags-smoke.mjs +46 -46
  46. package/scripts/mcp-receipt-smoke.mjs +115 -98
  47. package/scripts/model-route-report.mjs +15 -0
  48. package/scripts/openrouter-choice.mjs +59 -59
  49. package/scripts/replay-eval.mjs +124 -124
  50. package/scripts/smoke.mjs +34 -34
  51. package/scripts/supervision-e2e.mjs +138 -109
  52. package/src/browser.mjs +583 -569
  53. package/src/cli.mjs +33 -33
  54. package/src/computer.mjs +196 -0
  55. package/src/config.mjs +86 -75
  56. package/src/context-filter.mjs +56 -56
  57. package/src/contract.mjs +74 -72
  58. package/src/discovery.mjs +65 -65
  59. package/src/hermes-adapter.mjs +134 -0
  60. package/src/images.mjs +98 -0
  61. package/src/mcp-server.mjs +313 -210
  62. package/src/model-route-metrics.mjs +89 -0
  63. package/src/model-routing.mjs +87 -0
  64. package/src/providers/demo.mjs +52 -52
  65. package/src/providers/index.mjs +12 -0
  66. package/src/providers/typesafe.mjs +206 -126
  67. package/src/receipts.mjs +228 -226
  68. package/src/registry.mjs +109 -109
  69. package/src/relevance-filter.mjs +99 -99
  70. package/src/route.mjs +228 -221
  71. package/src/shadow-compaction.mjs +170 -0
  72. package/src/supervision.mjs +268 -244
  73. package/test/browser.test.mjs +222 -199
  74. package/test/capability.test.mjs +54 -54
  75. package/test/computer.test.mjs +195 -0
  76. package/test/context-filter.test.mjs +65 -65
  77. package/test/hermes-adapter.test.mjs +63 -0
  78. package/test/image-fixture.mjs +2 -0
  79. package/test/images.test.mjs +85 -0
  80. package/test/model-route-metrics.test.mjs +61 -0
  81. package/test/model-routing.test.mjs +59 -0
  82. package/test/openrouter-provider.test.mjs +55 -55
  83. package/test/provider-transports.test.mjs +205 -0
  84. package/test/providers.test.mjs +125 -0
  85. package/test/receipts.test.mjs +71 -71
  86. package/test/route.test.mjs +86 -74
  87. package/test/shadow-compaction.test.mjs +67 -0
  88. package/test/supervision.test.mjs +117 -99
@@ -1,210 +1,313 @@
1
- #!/usr/bin/env node
2
- import { createInterface } from "node:readline";
3
- import { appendExecutionReceipt, appendRoutingCase, buildExecutionReceipt, replayCasePath } from "./receipts.mjs";
4
- import { configuredProvider, configuredReplayPath, loadConfig } from "./config.mjs";
5
- import { decideBrowserStep } from "./browser.mjs";
6
- import { superviseWork } from "./supervision.mjs";
7
- import { routeRequest } from "./route.mjs";
8
-
9
- const { config } = await loadConfig();
10
- const serverInfo = { name: "jev-layer", version: "0.1.0" };
11
- const CASES_PATH = replayCasePath(configuredReplayPath(config));
12
- const pendingDecisions = new Map();
13
- const tools = [
14
- {
15
- name: "jev_route",
16
- description: "Return one bounded Jev capability decision. The host executes the selected capability; this tool never executes it.",
17
- inputSchema: {
18
- type: "object",
19
- required: ["intent", "capabilities"],
20
- properties: {
21
- intent: { type: "string" },
22
- harness: { type: "string" },
23
- context: { type: "object" },
24
- actor_permissions: { type: "array", items: { type: "string" } },
25
- capabilities: { type: "array", items: { type: "object" } },
26
- policy: { type: "object" },
27
- provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
28
- engine: { type: "string", enum: ["native", "jevrouter"] },
29
- },
30
- },
31
- },
32
- {
33
- name: "jev_browser_step",
34
- description: "Choose one bounded browser action from the host's visible observation. The host executes it or hands control back; this tool never drives a browser.",
35
- inputSchema: {
36
- type: "object",
37
- required: ["goal", "observation"],
38
- properties: {
39
- goal: { type: "string" },
40
- harness: { type: "string" },
41
- start_url: { type: ["string", "null"] },
42
- observation: { type: "object" },
43
- policy: { type: "object" },
44
- provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
45
- enabled: { type: "boolean" },
46
- },
47
- },
48
- },
49
- {
50
- name: "jev_supervise",
51
- description: "Judge bounded work-state dimensions; deterministic host policy returns continue, verify, retry, finish, or escalate.",
52
- inputSchema: {
53
- type: "object",
54
- required: ["job", "observation"],
55
- properties: {
56
- job: { type: "object" },
57
- observation: { type: "object" },
58
- evidence: { type: "object" },
59
- harness: { type: "string" },
60
- actor_permissions: { type: "array", items: { type: "string" } },
61
- policy: { type: "object" },
62
- attempts: { type: "integer", minimum: 0 },
63
- provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
64
- enabled: { type: "boolean" },
65
- },
66
- },
67
- },
68
- {
69
- name: "jev_record_execution",
70
- description: "Attach a host execution result to a Jev decision and persist the unified execution receipt.",
71
- inputSchema: {
72
- type: "object",
73
- required: ["correlation_id", "status"],
74
- properties: {
75
- correlation_id: { type: "string" },
76
- harness: { type: "string" },
77
- capability_id: { type: "string" },
78
- status: { type: "string", enum: ["completed", "failed", "not_started"] },
79
- result: {},
80
- error: {},
81
- browser: { type: "object" },
82
- exit_status: { type: ["integer", "null"] },
83
- duration_ms: { type: ["number", "null"] },
84
- started_at: { type: ["string", "null"] },
85
- completed_at: { type: ["string", "null"] },
86
- },
87
- },
88
- },
89
- ];
90
-
91
- const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
92
- for await (const line of input) {
93
- if (!line.trim()) continue;
94
- let message;
95
- try {
96
- message = JSON.parse(line);
97
- const response = await handle(message);
98
- if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
99
- } catch (error) {
100
- const id = message?.id ?? null;
101
- process.stdout.write(`${JSON.stringify(jsonRpcError(id, -32603, error instanceof Error ? error.message : String(error)))}\n`);
102
- }
103
- }
104
-
105
- async function handle(message) {
106
- const { id, method, params = {} } = message;
107
- if (method === "initialize") {
108
- return { jsonrpc: "2.0", id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo } };
109
- }
110
- if (method === "notifications/initialized") return null;
111
- if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools } };
112
- if (method !== "tools/call") return jsonRpcError(id, -32601, `method not found: ${method}`);
113
-
114
- const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
115
- if (params.name === "jev_route") return route(args, id);
116
- if (params.name === "jev_browser_step") return browserStep(args, id);
117
- if (params.name === "jev_supervise") return supervise(args, id);
118
- if (params.name === "jev_record_execution") return recordExecution(args, id);
119
- return jsonRpcError(id, -32602, `unknown tool: ${params.name}`);
120
- }
121
-
122
- async function route(args, id) {
123
- const { provider: requestedProvider, engine = "native", ...request } = args;
124
- const provider = configuredProvider(config, requestedProvider);
125
- const decision = await routeRequest(request, {
126
- provider,
127
- engine,
128
- contextFilterMode: request.policy?.context_filter_mode ?? config.features?.context_filter,
129
- });
130
- return persistDecision(request, decision, id);
131
- }
132
-
133
- async function browserStep(args, id) {
134
- const { provider: requestedProvider, enabled, ...input } = args;
135
- const routed = await decideBrowserStep(input, {
136
- enabled: enabled ?? config.features?.browser_fast_path,
137
- provider: configuredProvider(config, requestedProvider),
138
- });
139
- routed.decision.browser_action = routed.action
140
- ? {
141
- id: routed.action.id,
142
- operation: routed.action.operation,
143
- target_id: routed.action.target_id ?? null,
144
- option_id: routed.action.option_id ?? null,
145
- consequential: routed.action.consequential,
146
- }
147
- : null;
148
- return persistDecision(routed.request, routed.decision, id);
149
- }
150
-
151
- async function supervise(args, id) {
152
- const { provider: requestedProvider, enabled, ...input } = args;
153
- const result = await superviseWork({
154
- ...input,
155
- enabled: enabled ?? config.features?.supervision,
156
- provider: configuredProvider(config, requestedProvider),
157
- receiptPath: CASES_PATH,
158
- });
159
- return toolResult(id, result, false);
160
- }
161
-
162
- async function persistDecision(request, decision, id) {
163
- pendingDecisions.set(decision.correlation_id, { request, decision });
164
- try {
165
- await appendRoutingCase({ path: CASES_PATH, request, decision });
166
- decision.receipt = { ...decision.receipt, replay_case_id: decision.correlation_id, replay_persisted: true };
167
- } catch (error) {
168
- decision.receipt = {
169
- ...decision.receipt,
170
- replay_persisted: false,
171
- replay_error: error instanceof Error ? error.message : String(error),
172
- };
173
- }
174
- return toolResult(id, decision, decision.status === "error");
175
- }
176
-
177
- async function recordExecution(args, id) {
178
- const correlationId = typeof args.correlation_id === "string" ? args.correlation_id : null;
179
- const pending = correlationId ? pendingDecisions.get(correlationId) : null;
180
- if (!pending) return jsonRpcError(id, -32004, `unknown Jev correlation_id: ${correlationId ?? "missing"}`);
181
- const { request, decision } = pending;
182
- let record;
183
- try {
184
- ({ record } = await appendExecutionReceipt({ path: CASES_PATH, request, decision, host: args }));
185
- } catch (error) {
186
- record = buildExecutionReceipt({ request, decision, host: args });
187
- record.persistence = {
188
- persisted: false,
189
- error: error instanceof Error ? error.message : String(error),
190
- };
191
- }
192
- pendingDecisions.delete(correlationId);
193
- return toolResult(id, record, false);
194
- }
195
-
196
- function toolResult(id, value, isError) {
197
- return {
198
- jsonrpc: "2.0",
199
- id,
200
- result: {
201
- content: [{ type: "text", text: JSON.stringify(value) }],
202
- structuredContent: value,
203
- isError,
204
- },
205
- };
206
- }
207
-
208
- function jsonRpcError(id, code, message) {
209
- return { jsonrpc: "2.0", id, error: { code, message } };
210
- }
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline";
3
+ import { readFile } from "node:fs/promises";
4
+ import { appendExecutionReceipt, appendRoutingCase, buildExecutionReceipt, replayCasePath } from "./receipts.mjs";
5
+ import { configuredProvider, configuredReplayPath, loadConfig } from "./config.mjs";
6
+ import { decideBrowserStep } from "./browser.mjs";
7
+ import { decideComputerStep } from "./computer.mjs";
8
+ import { superviseWork } from "./supervision.mjs";
9
+ import { buildShadowCompactionReport } from "./shadow-compaction.mjs";
10
+ import { recommendModelRoute } from "./model-routing.mjs";
11
+ import { routeRequest } from "./route.mjs";
12
+
13
+ const { config } = await loadConfig();
14
+ const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
15
+ const serverInfo = { name: "jev-layer", version };
16
+ const CASES_PATH = replayCasePath(configuredReplayPath(config));
17
+ const pendingDecisions = new Map();
18
+ const tools = [
19
+ {
20
+ name: "jev_route",
21
+ description: "Return one bounded Jev capability decision. The host executes the selected capability; this tool never executes it.",
22
+ inputSchema: {
23
+ type: "object",
24
+ required: ["intent", "capabilities"],
25
+ properties: {
26
+ intent: { type: "string" },
27
+ harness: { type: "string" },
28
+ context: { type: "object" },
29
+ assets: { type: "object", description: "Inline PNG/JPEG assets keyed by reference; images are passed outside the text projection." },
30
+ actor_permissions: { type: "array", items: { type: "string" } },
31
+ capabilities: { type: "array", items: { type: "object" } },
32
+ policy: { type: "object" },
33
+ provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
34
+ engine: { type: "string", enum: ["native", "jevrouter"] },
35
+ },
36
+ },
37
+ },
38
+ {
39
+ name: "jev_browser_step",
40
+ description: "Choose one bounded browser action from the host's visible observation. The host executes it or hands control back; this tool never drives a browser.",
41
+ inputSchema: {
42
+ type: "object",
43
+ required: ["goal", "observation"],
44
+ properties: {
45
+ goal: { type: "string" },
46
+ harness: { type: "string" },
47
+ start_url: { type: ["string", "null"] },
48
+ observation: { type: "object", description: "Host snapshot: url, visible_text, targets, tabs, scroll, and optional screenshot {format,data_base64,width,height}." },
49
+ progress: { type: "object" },
50
+ actor_permissions: { type: "array", items: { type: "string" } },
51
+ policy: { type: "object" },
52
+ provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
53
+ enabled: { type: "boolean" },
54
+ },
55
+ },
56
+ },
57
+ {
58
+ name: "jev_computer_step",
59
+ description: "Recommend one host-declared desktop action, optionally grounding a click in a screenshot. Advisory only: host validates freshness, permissions, approval and execution.",
60
+ inputSchema: {
61
+ type: "object",
62
+ required: ["goal", "observation", "actions", "actor_permissions"],
63
+ properties: {
64
+ goal: { type: "string" },
65
+ harness: { type: "string" },
66
+ observation: { type: "object", description: "Host screenshot {format,data_base64,width,height}, optional application and visible_text." },
67
+ actions: { type: "array", maxItems: 32, items: {
68
+ type: "object", required: ["id", "operation"],
69
+ properties: {
70
+ id: { type: "string" },
71
+ operation: { type: "string", enum: ["click", "type", "key", "scroll", "handoff"] },
72
+ args: { type: "object", description: "Exact host-declared arguments; Jev does not generate typing or key input." },
73
+ target: { type: "string", description: "Unique visible click target to locate when no x/y are supplied." },
74
+ description: { type: "string" },
75
+ risk: { type: "string", enum: ["low", "medium", "high", "critical"] },
76
+ available: { type: "boolean" },
77
+ availability: { type: "object" },
78
+ permissions: { type: "array", items: { type: "string" } },
79
+ policy: { type: "object" },
80
+ },
81
+ } },
82
+ actor_permissions: { type: "array", items: { type: "string" }, description: "Must explicitly include computer; never grants host permission." },
83
+ policy: { type: "object" },
84
+ provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
85
+ enabled: { type: "boolean" },
86
+ },
87
+ },
88
+ },
89
+ {
90
+ name: "jev_supervise",
91
+ description: "Judge bounded work-state dimensions; deterministic host policy returns continue, verify, retry, finish, or escalate.",
92
+ inputSchema: {
93
+ type: "object",
94
+ required: ["job", "observation"],
95
+ properties: {
96
+ job: { type: "object" },
97
+ observation: { type: "object" },
98
+ evidence: { type: "object" },
99
+ harness: { type: "string" },
100
+ actor_permissions: { type: "array", items: { type: "string" } },
101
+ policy: { type: "object" },
102
+ attempts: { type: "integer", minimum: 0 },
103
+ provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
104
+ enabled: { type: "boolean" },
105
+ },
106
+ },
107
+ },
108
+ {
109
+ name: "jev_model_route",
110
+ description: "Recommend one host-declared model profile for the next call. This is shadow-only: it never changes provider, model, reasoning, or execution.",
111
+ inputSchema: {
112
+ type: "object",
113
+ required: ["intent", "models"],
114
+ properties: {
115
+ intent: { type: "string" },
116
+ context: { type: "object" },
117
+ models: { type: "array", minItems: 1, items: { type: "object" } },
118
+ harness: { type: "string" },
119
+ policy: { type: "object" },
120
+ provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
121
+ },
122
+ },
123
+ },
124
+ {
125
+ name: "jev_shadow_compaction",
126
+ description: "Report conservative Jev keep/drop candidates for host-supplied context. This tool never changes, summarizes, or deletes context.",
127
+ inputSchema: {
128
+ type: "object",
129
+ required: ["intent", "context"],
130
+ properties: {
131
+ intent: { type: "string" },
132
+ context: { type: "object" },
133
+ provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
134
+ batch_size: { type: "integer", minimum: 1, maximum: 8 },
135
+ keep_threshold: { type: "number", minimum: 0, maximum: 1 },
136
+ min_confidence: { type: "number", minimum: 0, maximum: 1 },
137
+ },
138
+ },
139
+ },
140
+ {
141
+ name: "jev_record_execution",
142
+ description: "Attach a host execution result to a Jev decision and persist the unified execution receipt.",
143
+ inputSchema: {
144
+ type: "object",
145
+ required: ["correlation_id", "status"],
146
+ properties: {
147
+ correlation_id: { type: "string" },
148
+ harness: { type: "string" },
149
+ capability_id: { type: "string" },
150
+ status: { type: "string", enum: ["completed", "failed", "not_started"] },
151
+ result: {},
152
+ error: {},
153
+ browser: { type: "object" },
154
+ exit_status: { type: ["integer", "null"] },
155
+ duration_ms: { type: ["number", "null"] },
156
+ started_at: { type: ["string", "null"] },
157
+ completed_at: { type: ["string", "null"] },
158
+ },
159
+ },
160
+ },
161
+ ];
162
+
163
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
164
+ for await (const line of input) {
165
+ if (!line.trim()) continue;
166
+ let message;
167
+ try {
168
+ message = JSON.parse(line);
169
+ const response = await handle(message);
170
+ if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
171
+ } catch (error) {
172
+ const id = message?.id ?? null;
173
+ process.stdout.write(`${JSON.stringify(jsonRpcError(id, -32603, error instanceof Error ? error.message : String(error)))}\n`);
174
+ }
175
+ }
176
+
177
+ async function handle(message) {
178
+ const { id, method, params = {} } = message;
179
+ if (method === "initialize") {
180
+ return { jsonrpc: "2.0", id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo } };
181
+ }
182
+ if (method === "notifications/initialized") return null;
183
+ if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools } };
184
+ if (method !== "tools/call") return jsonRpcError(id, -32601, `method not found: ${method}`);
185
+
186
+ const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
187
+ if (params.name === "jev_route") return route(args, id);
188
+ if (params.name === "jev_browser_step") return browserStep(args, id);
189
+ if (params.name === "jev_computer_step") return computerStep(args, id);
190
+ if (params.name === "jev_supervise") return supervise(args, id);
191
+ if (params.name === "jev_model_route") return modelRoute(args, id);
192
+ if (params.name === "jev_shadow_compaction") return shadowCompaction(args, id);
193
+ if (params.name === "jev_record_execution") return recordExecution(args, id);
194
+ return jsonRpcError(id, -32602, `unknown tool: ${params.name}`);
195
+ }
196
+
197
+ async function route(args, id) {
198
+ const { provider: requestedProvider, engine = "native", ...request } = args;
199
+ const provider = configuredProvider(config, requestedProvider);
200
+ const decision = await routeRequest(request, {
201
+ provider,
202
+ engine,
203
+ config,
204
+ contextFilterMode: request.policy?.context_filter_mode ?? config.features?.context_filter,
205
+ });
206
+ return persistDecision(request, decision, id);
207
+ }
208
+
209
+ async function browserStep(args, id) {
210
+ const { provider: requestedProvider, enabled, ...input } = args;
211
+ const routed = await decideBrowserStep(input, {
212
+ config,
213
+ enabled: enabled ?? config.features?.browser_fast_path,
214
+ provider: configuredProvider(config, requestedProvider),
215
+ });
216
+ routed.decision.browser_action = routed.action
217
+ ? { ...routed.action, target_id: routed.action.target_id ?? null, option_id: routed.action.option_id ?? null }
218
+ : null;
219
+ return persistDecision(routed.request, routed.decision, id);
220
+ }
221
+
222
+ async function computerStep(args, id) {
223
+ const { provider: requestedProvider, enabled, ...input } = args;
224
+ const routed = await decideComputerStep(input, {
225
+ config,
226
+ enabled: enabled ?? (process.env.JEV_COMPUTER_USE === "1" || config.features?.computer_use),
227
+ provider: configuredProvider(config, requestedProvider),
228
+ });
229
+ routed.decision.computer_action = routed.action;
230
+ return persistDecision(routed.request ?? { harness: input.harness ?? "computer", intent: input.goal }, routed.decision, id);
231
+ }
232
+
233
+ async function supervise(args, id) {
234
+ const { provider: requestedProvider, enabled, ...input } = args;
235
+ const result = await superviseWork({
236
+ ...input,
237
+ config,
238
+ enabled: enabled ?? config.features?.supervision,
239
+ provider: configuredProvider(config, requestedProvider),
240
+ receiptPath: CASES_PATH,
241
+ });
242
+ return toolResult(id, result, false);
243
+ }
244
+
245
+ async function modelRoute(args, id) {
246
+ const { provider: requestedProvider, ...input } = args;
247
+ const result = await recommendModelRoute({
248
+ ...input,
249
+ provider: configuredProvider(config, requestedProvider),
250
+ });
251
+ return toolResult(id, result, false);
252
+ }
253
+
254
+ async function shadowCompaction(args, id) {
255
+ const { provider: requestedProvider, ...input } = args;
256
+ const result = await buildShadowCompactionReport({
257
+ ...input,
258
+ provider: configuredProvider(config, requestedProvider),
259
+ });
260
+ return toolResult(id, result, false);
261
+ }
262
+
263
+ async function persistDecision(request, decision, id) {
264
+ const { assets, ...retainedRequest } = request;
265
+ if (Object.keys(assets ?? {}).length) retainedRequest.assets_omitted = true;
266
+ pendingDecisions.set(decision.correlation_id, { request: retainedRequest, decision });
267
+ try {
268
+ await appendRoutingCase({ path: CASES_PATH, request, decision });
269
+ decision.receipt = { ...decision.receipt, replay_case_id: decision.correlation_id, replay_persisted: true };
270
+ } catch (error) {
271
+ decision.receipt = {
272
+ ...decision.receipt,
273
+ replay_persisted: false,
274
+ replay_error: error instanceof Error ? error.message : String(error),
275
+ };
276
+ }
277
+ return toolResult(id, decision, decision.status === "error");
278
+ }
279
+
280
+ async function recordExecution(args, id) {
281
+ const correlationId = typeof args.correlation_id === "string" ? args.correlation_id : null;
282
+ const pending = correlationId ? pendingDecisions.get(correlationId) : null;
283
+ if (!pending) return jsonRpcError(id, -32004, `unknown Jev correlation_id: ${correlationId ?? "missing"}`);
284
+ const { request, decision } = pending;
285
+ let record;
286
+ try {
287
+ ({ record } = await appendExecutionReceipt({ path: CASES_PATH, request, decision, host: args }));
288
+ } catch (error) {
289
+ record = buildExecutionReceipt({ request, decision, host: args });
290
+ record.persistence = {
291
+ persisted: false,
292
+ error: error instanceof Error ? error.message : String(error),
293
+ };
294
+ }
295
+ pendingDecisions.delete(correlationId);
296
+ return toolResult(id, record, false);
297
+ }
298
+
299
+ function toolResult(id, value, isError) {
300
+ return {
301
+ jsonrpc: "2.0",
302
+ id,
303
+ result: {
304
+ content: [{ type: "text", text: JSON.stringify(value) }],
305
+ structuredContent: value,
306
+ isError,
307
+ },
308
+ };
309
+ }
310
+
311
+ function jsonRpcError(id, code, message) {
312
+ return { jsonrpc: "2.0", id, error: { code, message } };
313
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Read-only analysis for model-route shadow cases. Recommendations stay
3
+ * advisory: this module joins evidence; it never changes host model settings.
4
+ */
5
+ export function buildModelRouteReport(records = []) {
6
+ const routes = new Map();
7
+ const executions = new Map();
8
+ for (const record of Array.isArray(records) ? records : []) {
9
+ if (!record || typeof record !== "object" || typeof record.correlation_id !== "string") continue;
10
+ if (record.record_type === "routing_case" && record.request?.context?.model_route?.mode === "shadow") routes.set(record.correlation_id, record);
11
+ if (record.record_type === "execution_receipt") executions.set(record.correlation_id, record);
12
+ }
13
+
14
+ const rows = [...routes.values()].map((route) => toRow(route, executions.get(route.correlation_id))).sort((a, b) => a.recommended_model_id.localeCompare(b.recommended_model_id));
15
+ return {
16
+ schema_version: 1,
17
+ mode: "shadow",
18
+ generated_at: new Date().toISOString(),
19
+ summary: summarize(rows),
20
+ models: groupByRecommendation(rows),
21
+ };
22
+ }
23
+
24
+ function toRow(route, execution) {
25
+ const result = execution?.host?.result?.model_route;
26
+ const recommended = string(route.decision?.selected, "unrecommended");
27
+ const actual = string(result?.actual_model_id, null);
28
+ const outcome = string(result?.outcome, execution ? string(execution.host?.status, "unknown") : null);
29
+ return {
30
+ correlation_id: route.correlation_id,
31
+ recommended_model_id: recommended,
32
+ actual_model_id: actual,
33
+ recommendation_match: actual !== null && actual === recommended,
34
+ outcome,
35
+ retry_count: boundedInteger(result?.retry_count),
36
+ host_duration_ms: finite(execution?.host?.duration_ms),
37
+ jev_latency_ms: finite(route.decision?.receipt?.latency_ms),
38
+ jev_cost_usd: finite(route.decision?.receipt?.cost_usd),
39
+ };
40
+ }
41
+
42
+ function summarize(rows) {
43
+ const matched = rows.filter((row) => row.recommendation_match).length;
44
+ const actual = rows.filter((row) => row.actual_model_id !== null).length;
45
+ return {
46
+ decisions: rows.length,
47
+ with_host_outcome: rows.filter((row) => row.outcome !== null).length,
48
+ with_actual_model: actual,
49
+ recommendation_matches: matched,
50
+ recommendation_match_rate: actual ? matched / actual : null,
51
+ verified_outcomes: rows.filter((row) => row.outcome === "verified" || row.outcome === "completed").length,
52
+ failed_outcomes: rows.filter((row) => row.outcome === "failed").length,
53
+ total_retry_count: rows.reduce((sum, row) => sum + (row.retry_count ?? 0), 0),
54
+ total_jev_cost_usd: sum(rows.map((row) => row.jev_cost_usd)),
55
+ average_jev_latency_ms: average(rows.map((row) => row.jev_latency_ms)),
56
+ average_host_duration_ms: average(rows.map((row) => row.host_duration_ms)),
57
+ };
58
+ }
59
+
60
+ function groupByRecommendation(rows) {
61
+ const grouped = new Map();
62
+ for (const row of rows) {
63
+ const group = grouped.get(row.recommended_model_id) ?? {
64
+ recommended_model_id: row.recommended_model_id,
65
+ decisions: 0,
66
+ with_host_outcome: 0,
67
+ recommendation_matches: 0,
68
+ verified_outcomes: 0,
69
+ failed_outcomes: 0,
70
+ total_retry_count: 0,
71
+ host_actual_model_ids: {},
72
+ };
73
+ group.decisions += 1;
74
+ if (row.outcome !== null) group.with_host_outcome += 1;
75
+ if (row.recommendation_match) group.recommendation_matches += 1;
76
+ if (row.outcome === "verified" || row.outcome === "completed") group.verified_outcomes += 1;
77
+ if (row.outcome === "failed") group.failed_outcomes += 1;
78
+ group.total_retry_count += row.retry_count ?? 0;
79
+ if (row.actual_model_id) group.host_actual_model_ids[row.actual_model_id] = (group.host_actual_model_ids[row.actual_model_id] ?? 0) + 1;
80
+ grouped.set(row.recommended_model_id, group);
81
+ }
82
+ return [...grouped.values()].sort((a, b) => a.recommended_model_id.localeCompare(b.recommended_model_id));
83
+ }
84
+
85
+ function string(value, fallback) { return typeof value === "string" && value.trim() ? value.trim() : fallback; }
86
+ function finite(value) { return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null; }
87
+ function boundedInteger(value) { return Number.isInteger(value) && value >= 0 ? value : null; }
88
+ function sum(values) { return values.reduce((total, value) => total + (value ?? 0), 0); }
89
+ function average(values) { const present = values.filter((value) => value !== null); return present.length ? sum(present) / present.length : null; }