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
package/src/route.mjs CHANGED
@@ -1,221 +1,228 @@
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
- }
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 { resolveProvider } from "./providers/index.mjs";
6
+ import { normalizeImageAssets } from "./images.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" && !input.assets && !input.assets_omitted) 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 base = {
19
+ provider: typeof options.provider === "string" ? options.provider : options.provider?.name ?? "jev-demo",
20
+ candidateCount: eligible.length,
21
+ contextBytes: projection.context_bytes,
22
+ started,
23
+ };
24
+
25
+ if (process.env.JEV_LAYER_ENABLED === "0") {
26
+ return fallback(base, assessments, "disabled", "Jev layer disabled by JEV_LAYER_ENABLED=0", projection.state);
27
+ }
28
+ let provider;
29
+ try {
30
+ provider = resolveProvider(options.provider ?? "demo", options);
31
+ base.provider = provider.name;
32
+ if (request.assets_omitted) throw new Error("visual replay requires the original images; stored screenshots are intentionally omitted");
33
+ const assets = normalizeImageAssets(request.assets);
34
+ if (Object.keys(assets).length) {
35
+ if (options.engine === "jevrouter" || provider.supportsImages !== true) throw new Error("provider does not support image decisions");
36
+ projection.state.visual_assets = Object.keys(assets);
37
+ projection.context_bytes = byteLength(projection.state);
38
+ }
39
+ request.assets = assets;
40
+ } catch (error) {
41
+ return fallback(base, assessments, "provider_error", error instanceof Error ? error.message : String(error), projection.state);
42
+ }
43
+ if (eligible.length === 0) {
44
+ return decisionEnvelope({
45
+ status: "no_decision",
46
+ reason: "no capability passed availability, risk, and permission policy",
47
+ provider: provider.name,
48
+ latencyMs: elapsed(started),
49
+ candidateCount: 0,
50
+ contextBytes: projection.context_bytes,
51
+ candidates: decisionCandidates(assessments),
52
+ });
53
+ }
54
+ const deterministic = policy.deterministic === true
55
+ ? deterministicCandidate(eligible, policy)
56
+ : null;
57
+ if (deterministic) {
58
+ const selected = deterministic.capability;
59
+ const probabilities = Object.fromEntries(eligible.map((candidate) => [candidate.id, candidate.id === selected.id ? 1 : 0]));
60
+ const candidates = decisionCandidates(assessments, probabilities, 1);
61
+ const requiresConfirmation = policy.confirmation_risk_levels.includes(selected.risk) || selected.requires_confirmation;
62
+ return decisionEnvelope({
63
+ status: requiresConfirmation ? "needs_confirmation" : "selected",
64
+ reason: deterministic.reason,
65
+ provider: `${provider.name}:deterministic`,
66
+ latencyMs: elapsed(started),
67
+ candidateCount: eligible.length,
68
+ contextBytes: projection.context_bytes,
69
+ candidates,
70
+ selected: selected.id,
71
+ probabilities,
72
+ confidence: 1,
73
+ });
74
+ }
75
+
76
+ let raw;
77
+ try {
78
+ raw = await provider.decide({ state: projection.state, candidates: eligible, assets: request.assets });
79
+ } catch (error) {
80
+ return fallback(base, assessments, "provider_error", error instanceof Error ? error.message : String(error), projection.state);
81
+ }
82
+
83
+ let answer;
84
+ try {
85
+ answer = readChoiceAnswer(raw);
86
+ } catch (error) {
87
+ return fallback(base, assessments, "malformed_response", error instanceof Error ? error.message : String(error), projection.state, raw);
88
+ }
89
+
90
+ const selected = eligible.find((candidate) => candidate.id === answer.choice);
91
+ const candidates = decisionCandidates(assessments, answer.probabilities, answer.confidence);
92
+ if (!selected) {
93
+ return decisionEnvelope({
94
+ status: "fallback",
95
+ reason: `provider selected unknown or filtered capability: ${answer.choice}`,
96
+ provider: provider.name,
97
+ latencyMs: elapsed(started),
98
+ candidateCount: eligible.length,
99
+ contextBytes: projection.context_bytes,
100
+ candidates,
101
+ probabilities: answer.probabilities,
102
+ confidence: answer.confidence,
103
+ rawJev: raw,
104
+ fallback: { type: "invalid_selection", reason: "selection is not in the eligible candidate set" },
105
+ });
106
+ }
107
+ if (answer.confidence < policy.min_confidence) {
108
+ return decisionEnvelope({
109
+ status: "fallback",
110
+ reason: `confidence ${answer.confidence.toFixed(3)} is below policy minimum ${policy.min_confidence.toFixed(3)}`,
111
+ provider: provider.name,
112
+ latencyMs: elapsed(started),
113
+ candidateCount: eligible.length,
114
+ contextBytes: projection.context_bytes,
115
+ candidates,
116
+ selected: selected.id,
117
+ probabilities: answer.probabilities,
118
+ confidence: answer.confidence,
119
+ rawJev: raw,
120
+ fallback: { type: "low_confidence", reason: "host must continue with normal planning" },
121
+ });
122
+ }
123
+
124
+ const requiresConfirmation = policy.confirmation_risk_levels.includes(selected.risk) || selected.requires_confirmation;
125
+ return decisionEnvelope({
126
+ status: requiresConfirmation ? "needs_confirmation" : "selected",
127
+ reason: requiresConfirmation ? "selected capability requires host confirmation" : "selected by bounded Jev decision",
128
+ provider: provider.name,
129
+ latencyMs: elapsed(started),
130
+ candidateCount: eligible.length,
131
+ contextBytes: projection.context_bytes,
132
+ candidates,
133
+ selected: selected.id,
134
+ probabilities: answer.probabilities,
135
+ confidence: answer.confidence,
136
+ rawJev: raw,
137
+ });
138
+ }
139
+
140
+ function readChoiceAnswer(raw) {
141
+ const answers = raw?.answers;
142
+ if (!answers || typeof answers !== "object") throw new Error("response has no answers object");
143
+ const answer = answers.tool ?? Object.values(answers).find((value) => value?.type === "choice");
144
+ if (!answer || answer.type !== "choice" || typeof answer.choice !== "string" || !answer.probabilities || typeof answer.probabilities !== "object") {
145
+ throw new Error("response does not contain a valid Choice answer");
146
+ }
147
+ const probabilities = Object.fromEntries(Object.entries(answer.probabilities).map(([id, value]) => {
148
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) throw new Error(`invalid probability for ${id}`);
149
+ return [id, value];
150
+ }));
151
+ const confidence = typeof answer.confidence === "number" ? answer.confidence : Math.max(...Object.values(probabilities), 0);
152
+ if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) throw new Error("invalid confidence");
153
+ return { choice: answer.choice, probabilities, confidence };
154
+ }
155
+
156
+ function fallback(base, assessments, type, reason, state, rawJev = null) {
157
+ return decisionEnvelope({
158
+ status: "fallback",
159
+ reason,
160
+ provider: base.provider,
161
+ latencyMs: elapsed(base.started),
162
+ candidateCount: base.candidateCount,
163
+ contextBytes: byteLength(state),
164
+ candidates: decisionCandidates(assessments),
165
+ rawJev,
166
+ fallback: { type, reason },
167
+ });
168
+ }
169
+
170
+ function elapsed(started) {
171
+ return Number((performance.now() - started).toFixed(3));
172
+ }
173
+
174
+ async function routeViaJevRouter(request, options) {
175
+ const started = performance.now();
176
+ const capabilities = normalizeCapabilities(request.capabilities);
177
+ const { assessments, eligible } = filterCapabilities(capabilities, request, { ...DEFAULT_POLICY, ...request.policy, ...(options.policy ?? {}) });
178
+ const projection = projectState(request, eligible, options.maxContextBytes ?? 6_000, { mode: options.contextFilterMode });
179
+ 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) });
180
+
181
+ let module;
182
+ try {
183
+ module = await import("jevrouter");
184
+ } catch (error) {
185
+ return fallback({ provider: "jevrouter", candidateCount: eligible.length, contextBytes: projection.context_bytes, started }, assessments, "dependency_error", error instanceof Error ? error.message : String(error), projection.state);
186
+ }
187
+ try {
188
+ const result = await module.route({
189
+ request: request.intent,
190
+ context: projection.state.context,
191
+ candidates: eligible.map(toJevRouterCapability),
192
+ }, {
193
+ provider: options.innerProvider ?? process.env.JEVROUTER_PROVIDER ?? "demo",
194
+ policy: options.policy,
195
+ });
196
+ const selected = result.decision?.selected ?? null;
197
+ const raw = result.raw_jev ?? null;
198
+ const candidates = decisionCandidates(assessments, Object.fromEntries((result.decision?.candidates ?? []).map((candidate) => [candidate.id, candidate.jev_probability ?? 0])), null);
199
+ return decisionEnvelope({
200
+ status: result.status === "selected" ? "selected" : result.status === "needs_confirmation" ? "needs_confirmation" : "fallback",
201
+ selected,
202
+ reason: result.fallback?.reason ?? "selected by JevRouter",
203
+ provider: `jevrouter:${options.innerProvider ?? process.env.JEVROUTER_PROVIDER ?? "demo"}`,
204
+ latencyMs: elapsed(started),
205
+ candidateCount: eligible.length,
206
+ contextBytes: projection.context_bytes,
207
+ candidates,
208
+ rawJev: raw,
209
+ fallback: result.fallback?.type ? { type: result.fallback.type, reason: result.fallback.reason } : null,
210
+ });
211
+ } catch (error) {
212
+ return fallback({ provider: "jevrouter", candidateCount: eligible.length, contextBytes: projection.context_bytes, started }, assessments, "provider_error", error instanceof Error ? error.message : String(error), projection.state);
213
+ }
214
+ }
215
+
216
+ function toJevRouterCapability(capability) {
217
+ return {
218
+ id: capability.id,
219
+ name: capability.name,
220
+ type: capability.kind === "mcp" ? "mcp_tool" : capability.kind,
221
+ description: capability.description,
222
+ permissions: capability.permissions,
223
+ risk: { level: capability.risk },
224
+ availability: { available: capability.available },
225
+ execution: { mode: capability.kind === "mcp" ? "mcp" : capability.kind, target: capability.execution.target },
226
+ policy: { requires_confirmation: capability.requires_confirmation },
227
+ };
228
+ }
@@ -0,0 +1,170 @@
1
+ import { sanitizeContext } from "./context-filter.mjs";
2
+ import { isPinnedEvidence } from "./relevance-filter.mjs";
3
+ import { resolveProvider } from "./providers/index.mjs";
4
+
5
+ const LIST_KEYS = new Set(["messages", "events", "logs", "tool_results", "history", "transcript"]);
6
+ const DEFAULT_BATCH_SIZE = 8;
7
+ const DEFAULT_KEEP_THRESHOLD = 0.5;
8
+ const DEFAULT_MIN_CONFIDENCE = 0.7;
9
+ const MAX_ITEMS = 64;
10
+ const MAX_PREVIEW = 320;
11
+
12
+ /**
13
+ * Produce advisory Jev compaction decisions without mutating context.
14
+ * Pinned evidence is never sent as droppable and provider failure retains all items.
15
+ */
16
+ export async function buildShadowCompactionReport({
17
+ intent = "Assess which context records must remain available to a later agent.",
18
+ context = {},
19
+ provider = "demo",
20
+ config,
21
+ batchSize = DEFAULT_BATCH_SIZE,
22
+ keepThreshold = DEFAULT_KEEP_THRESHOLD,
23
+ minConfidence = DEFAULT_MIN_CONFIDENCE,
24
+ } = {}) {
25
+ const items = collectContextItems(context);
26
+ const normalizedBatchSize = boundedInteger(batchSize, DEFAULT_BATCH_SIZE, 1, DEFAULT_BATCH_SIZE);
27
+ const normalizedThreshold = boundedNumber(keepThreshold, DEFAULT_KEEP_THRESHOLD);
28
+ const normalizedConfidence = boundedNumber(minConfidence, DEFAULT_MIN_CONFIDENCE);
29
+ const protectedItems = items.filter((item) => item.pinned);
30
+ const evaluable = items.filter((item) => !item.pinned);
31
+ const decisions = new Map(protectedItems.map((item) => [item.id, {
32
+ decision: "keep",
33
+ probability: 1,
34
+ confidence: 1,
35
+ reason: `pinned:${item.pinned_reasons.join(",")}`,
36
+ }]));
37
+ let costUsd = 0;
38
+ let failed = null;
39
+
40
+ try {
41
+ const resolved = resolveProvider(provider, { config });
42
+ if (!resolved || typeof resolved.evaluate !== "function") throw new Error("provider does not support batched evaluation");
43
+ for (const batch of batches(evaluable, normalizedBatchSize)) {
44
+ const raw = await resolved.evaluate({
45
+ state: {
46
+ intent: boundedText(intent),
47
+ compaction: {
48
+ mode: "shadow",
49
+ instruction: "For each item, decide whether dropping it would lose a requirement, exact identifier, path, command, error, user decision, execution receipt, or other evidence needed later. Be conservative: keep when uncertain.",
50
+ items: batch.map(publicItem),
51
+ },
52
+ },
53
+ questions: Object.fromEntries(batch.map((item, index) => [`item_${index}`, keepQuestion()])),
54
+ });
55
+ costUsd += finiteCost(raw?.usage?.cost) ?? 0;
56
+ for (const [index, item] of batch.entries()) {
57
+ const answer = readNoul(raw?.answers?.[`item_${index}`]);
58
+ if (!answer) throw new Error(`provider response has no valid item_${index} judgment`);
59
+ const decision = answer.confidence < normalizedConfidence || answer.probability >= normalizedThreshold ? "keep" : "drop_candidate";
60
+ decisions.set(item.id, {
61
+ decision,
62
+ probability: answer.probability,
63
+ confidence: answer.confidence,
64
+ reason: answer.confidence < normalizedConfidence ? "low_confidence" : decision === "keep" ? "provider_keep" : "provider_drop_candidate",
65
+ });
66
+ }
67
+ }
68
+ } catch (error) {
69
+ failed = error instanceof Error ? error.message : String(error);
70
+ for (const item of evaluable) {
71
+ decisions.set(item.id, { decision: "keep", probability: null, confidence: null, reason: "provider_error" });
72
+ }
73
+ }
74
+
75
+ const reportedItems = items.map((item) => ({ ...publicItem(item), ...decisions.get(item.id) }));
76
+ const droppableCount = reportedItems.filter((item) => item.decision === "drop_candidate").length;
77
+ return {
78
+ schema_version: 1,
79
+ mode: "jev_shadow",
80
+ status: failed ? "fallback" : "judged",
81
+ changed: false,
82
+ candidate_count: reportedItems.length,
83
+ evaluated_count: evaluable.length,
84
+ protected_count: protectedItems.length,
85
+ retained_count: reportedItems.length - droppableCount,
86
+ droppable_count: droppableCount,
87
+ keep_threshold: normalizedThreshold,
88
+ min_confidence: normalizedConfidence,
89
+ cost_usd: failed ? null : Number(costUsd.toFixed(8)),
90
+ fallback: failed ? { type: "provider_error", reason: failed } : null,
91
+ items: reportedItems,
92
+ };
93
+ }
94
+
95
+ export function collectContextItems(context = {}) {
96
+ const source = context && typeof context === "object" ? context : {};
97
+ const items = [];
98
+ for (const [list, value] of Object.entries(source)) {
99
+ if (!LIST_KEYS.has(list) || !Array.isArray(value)) continue;
100
+ for (const [index, original] of value.entries()) {
101
+ if (items.length >= MAX_ITEMS) return items;
102
+ const sanitized = sanitizeContext(original);
103
+ const evidence = isPinnedEvidence(sanitized);
104
+ items.push({
105
+ id: `${list}[${index}]`,
106
+ list,
107
+ index,
108
+ value: sanitized,
109
+ pinned: evidence.pinned,
110
+ pinned_reasons: evidence.reasons,
111
+ });
112
+ }
113
+ }
114
+ return items;
115
+ }
116
+
117
+ function publicItem(item) {
118
+ return {
119
+ id: item.id,
120
+ list: item.list,
121
+ index: item.index,
122
+ pinned: item.pinned,
123
+ pinned_reasons: item.pinned_reasons,
124
+ preview: boundedText(typeof item.value === "string" ? item.value : JSON.stringify(item.value)),
125
+ };
126
+ }
127
+
128
+ function keepQuestion() {
129
+ return {
130
+ type: "noul",
131
+ instructions: "Would dropping this context record lose information needed by a later agent? Return true to keep it and false only when it is safely stale or redundant.",
132
+ criteria: {
133
+ true: "Keep: it contains a requirement, decision, identifier, path, command, error, execution evidence, or context that may matter later; uncertainty means keep.",
134
+ false: "Drop candidate: it is safely stale or redundant and losing it would not affect later work.",
135
+ },
136
+ };
137
+ }
138
+
139
+ function readNoul(answer) {
140
+ if (!answer || typeof answer !== "object") return null;
141
+ const probability = ["probability", "p_true", "score", "value", "noul"].map((key) => answer[key]).find(validProbability);
142
+ if (!validProbability(probability)) return null;
143
+ const confidence = validProbability(answer.confidence) ? answer.confidence : probability;
144
+ return { probability, confidence };
145
+ }
146
+
147
+ function batches(items, size) {
148
+ return Array.from({ length: Math.ceil(items.length / size) }, (_, index) => items.slice(index * size, (index + 1) * size));
149
+ }
150
+
151
+ function boundedText(value) {
152
+ const text = String(value ?? "");
153
+ return text.length > MAX_PREVIEW ? `${text.slice(0, MAX_PREVIEW)}…` : text;
154
+ }
155
+
156
+ function boundedNumber(value, fallback) {
157
+ return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : fallback;
158
+ }
159
+
160
+ function boundedInteger(value, fallback, min, max) {
161
+ return Number.isInteger(value) ? Math.max(min, Math.min(max, value)) : fallback;
162
+ }
163
+
164
+ function validProbability(value) {
165
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
166
+ }
167
+
168
+ function finiteCost(value) {
169
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
170
+ }