jev-layer 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -34
- package/CONTRIBUTING.md +146 -146
- package/LICENSE +21 -21
- package/README.md +123 -126
- package/README.ru.md +138 -117
- package/README.zh-CN.md +137 -117
- package/RELEASE.md +53 -53
- package/SECURITY.md +56 -56
- package/bin/jev.mjs +213 -213
- package/config/codex.mcp.toml +8 -8
- package/config/generic-mcp.json +13 -13
- package/config/hermes.mcp.yaml +9 -9
- package/config/jev.example.json +22 -22
- package/config/omp.mcp.json +14 -14
- package/config/providers.env.example +13 -13
- package/docs/AGENT-IMPLEMENTATION.md +250 -0
- package/docs/COMPATIBILITY.md +15 -15
- package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +177 -0
- package/docs/PROVIDERS.md +67 -0
- package/docs/SCHEMA-VERSIONING.md +96 -94
- package/docs/architecture.svg +77 -0
- package/docs/jev-layer-banner.svg +38 -0
- package/examples/capabilities.json +39 -39
- package/examples/route-request.json +28 -28
- package/integrations/codex/.codex-plugin/plugin.json +19 -19
- package/integrations/codex/.mcp.json +11 -11
- package/integrations/codex/AGENTS.md +1 -1
- package/integrations/codex/run-mcp.mjs +9 -9
- package/integrations/codex/skills/jev-route/SKILL.md +17 -17
- package/integrations/hermes/__init__.py +150 -51
- package/integrations/hermes/plugin.yaml +10 -5
- package/integrations/hermes/schemas.py +89 -12
- package/integrations/omp/extension.js +105 -105
- package/integrations/template/README.md +10 -10
- package/integrations/template/adapter.mjs +87 -87
- package/package.json +60 -59
- package/scripts/benchmark.mjs +35 -35
- package/scripts/browser-benchmark.mjs +92 -92
- package/scripts/browser-e2e.mjs +117 -117
- package/scripts/capability-e2e.mjs +31 -31
- package/scripts/clean-install-smoke.mjs +162 -162
- package/scripts/codex-mcp-smoke.mjs +119 -119
- package/scripts/context-filter-e2e.mjs +45 -45
- package/scripts/fail-open-smoke.mjs +120 -120
- package/scripts/feature-flags-smoke.mjs +46 -46
- package/scripts/mcp-receipt-smoke.mjs +113 -98
- package/scripts/model-route-report.mjs +15 -0
- package/scripts/openrouter-choice.mjs +59 -59
- package/scripts/replay-eval.mjs +124 -124
- package/scripts/smoke.mjs +34 -34
- package/scripts/supervision-e2e.mjs +138 -109
- package/src/browser.mjs +571 -569
- package/src/cli.mjs +33 -33
- package/src/config.mjs +85 -75
- package/src/context-filter.mjs +56 -56
- package/src/contract.mjs +72 -72
- package/src/discovery.mjs +65 -65
- package/src/hermes-adapter.mjs +123 -0
- package/src/mcp-server.mjs +267 -210
- package/src/model-route-metrics.mjs +89 -0
- package/src/model-routing.mjs +87 -0
- package/src/providers/demo.mjs +52 -52
- package/src/providers/index.mjs +12 -0
- package/src/providers/typesafe.mjs +126 -126
- package/src/receipts.mjs +227 -226
- package/src/registry.mjs +109 -109
- package/src/relevance-filter.mjs +99 -99
- package/src/route.mjs +219 -221
- package/src/shadow-compaction.mjs +170 -0
- package/src/supervision.mjs +268 -244
- package/test/browser.test.mjs +199 -199
- package/test/capability.test.mjs +54 -54
- package/test/context-filter.test.mjs +65 -65
- package/test/hermes-adapter.test.mjs +62 -0
- package/test/model-route-metrics.test.mjs +61 -0
- package/test/model-routing.test.mjs +59 -0
- package/test/openrouter-provider.test.mjs +55 -55
- package/test/provider-transports.test.mjs +205 -0
- package/test/providers.test.mjs +126 -0
- package/test/receipts.test.mjs +71 -71
- package/test/route.test.mjs +74 -74
- package/test/shadow-compaction.test.mjs +67 -0
- package/test/supervision.test.mjs +117 -99
package/src/route.mjs
CHANGED
|
@@ -1,221 +1,219 @@
|
|
|
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 {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
if (!
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
|
|
7
|
+
export async function routeRequest(input, options = {}) {
|
|
8
|
+
const discovered = options.discovery ? discoverCapabilities(options.discovery) : [];
|
|
9
|
+
const request = normalizeRequest(discovered.length ? { ...input, capabilities: [...(Array.isArray(input?.capabilities) ? input.capabilities : []), ...discovered] } : input);
|
|
10
|
+
if (options.engine === "jevrouter") return routeViaJevRouter(request, options);
|
|
11
|
+
|
|
12
|
+
const started = performance.now();
|
|
13
|
+
const effectivePolicy = { ...DEFAULT_POLICY, ...request.policy, ...(options.policy ?? {}) };
|
|
14
|
+
const capabilities = normalizeCapabilities(request.capabilities);
|
|
15
|
+
const { policy, assessments, eligible } = filterCapabilities(capabilities, request, effectivePolicy);
|
|
16
|
+
const projection = projectState(request, eligible, options.maxContextBytes ?? 6_000, { mode: options.contextFilterMode });
|
|
17
|
+
const base = {
|
|
18
|
+
provider: typeof options.provider === "string" ? options.provider : options.provider?.name ?? "jev-demo",
|
|
19
|
+
candidateCount: eligible.length,
|
|
20
|
+
contextBytes: projection.context_bytes,
|
|
21
|
+
started,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (process.env.JEV_LAYER_ENABLED === "0") {
|
|
25
|
+
return fallback(base, assessments, "disabled", "Jev layer disabled by JEV_LAYER_ENABLED=0", projection.state);
|
|
26
|
+
}
|
|
27
|
+
let provider;
|
|
28
|
+
try {
|
|
29
|
+
provider = resolveProvider(options.provider ?? "demo", options);
|
|
30
|
+
base.provider = provider.name;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return fallback(base, assessments, "provider_error", error instanceof Error ? error.message : String(error), projection.state);
|
|
33
|
+
}
|
|
34
|
+
if (eligible.length === 0) {
|
|
35
|
+
return decisionEnvelope({
|
|
36
|
+
status: "no_decision",
|
|
37
|
+
reason: "no capability passed availability, risk, and permission policy",
|
|
38
|
+
provider: provider.name,
|
|
39
|
+
latencyMs: elapsed(started),
|
|
40
|
+
candidateCount: 0,
|
|
41
|
+
contextBytes: projection.context_bytes,
|
|
42
|
+
candidates: decisionCandidates(assessments),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const deterministic = policy.deterministic === true
|
|
46
|
+
? deterministicCandidate(eligible, policy)
|
|
47
|
+
: null;
|
|
48
|
+
if (deterministic) {
|
|
49
|
+
const selected = deterministic.capability;
|
|
50
|
+
const probabilities = Object.fromEntries(eligible.map((candidate) => [candidate.id, candidate.id === selected.id ? 1 : 0]));
|
|
51
|
+
const candidates = decisionCandidates(assessments, probabilities, 1);
|
|
52
|
+
const requiresConfirmation = policy.confirmation_risk_levels.includes(selected.risk) || selected.requires_confirmation;
|
|
53
|
+
return decisionEnvelope({
|
|
54
|
+
status: requiresConfirmation ? "needs_confirmation" : "selected",
|
|
55
|
+
reason: deterministic.reason,
|
|
56
|
+
provider: `${provider.name}:deterministic`,
|
|
57
|
+
latencyMs: elapsed(started),
|
|
58
|
+
candidateCount: eligible.length,
|
|
59
|
+
contextBytes: projection.context_bytes,
|
|
60
|
+
candidates,
|
|
61
|
+
selected: selected.id,
|
|
62
|
+
probabilities,
|
|
63
|
+
confidence: 1,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await provider.decide({ state: projection.state, candidates: eligible });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return fallback(base, assessments, "provider_error", error instanceof Error ? error.message : String(error), projection.state);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let answer;
|
|
75
|
+
try {
|
|
76
|
+
answer = readChoiceAnswer(raw);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return fallback(base, assessments, "malformed_response", error instanceof Error ? error.message : String(error), projection.state, raw);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const selected = eligible.find((candidate) => candidate.id === answer.choice);
|
|
82
|
+
const candidates = decisionCandidates(assessments, answer.probabilities, answer.confidence);
|
|
83
|
+
if (!selected) {
|
|
84
|
+
return decisionEnvelope({
|
|
85
|
+
status: "fallback",
|
|
86
|
+
reason: `provider selected unknown or filtered capability: ${answer.choice}`,
|
|
87
|
+
provider: provider.name,
|
|
88
|
+
latencyMs: elapsed(started),
|
|
89
|
+
candidateCount: eligible.length,
|
|
90
|
+
contextBytes: projection.context_bytes,
|
|
91
|
+
candidates,
|
|
92
|
+
probabilities: answer.probabilities,
|
|
93
|
+
confidence: answer.confidence,
|
|
94
|
+
rawJev: raw,
|
|
95
|
+
fallback: { type: "invalid_selection", reason: "selection is not in the eligible candidate set" },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (answer.confidence < policy.min_confidence) {
|
|
99
|
+
return decisionEnvelope({
|
|
100
|
+
status: "fallback",
|
|
101
|
+
reason: `confidence ${answer.confidence.toFixed(3)} is below policy minimum ${policy.min_confidence.toFixed(3)}`,
|
|
102
|
+
provider: provider.name,
|
|
103
|
+
latencyMs: elapsed(started),
|
|
104
|
+
candidateCount: eligible.length,
|
|
105
|
+
contextBytes: projection.context_bytes,
|
|
106
|
+
candidates,
|
|
107
|
+
selected: selected.id,
|
|
108
|
+
probabilities: answer.probabilities,
|
|
109
|
+
confidence: answer.confidence,
|
|
110
|
+
rawJev: raw,
|
|
111
|
+
fallback: { type: "low_confidence", reason: "host must continue with normal planning" },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const requiresConfirmation = policy.confirmation_risk_levels.includes(selected.risk) || selected.requires_confirmation;
|
|
116
|
+
return decisionEnvelope({
|
|
117
|
+
status: requiresConfirmation ? "needs_confirmation" : "selected",
|
|
118
|
+
reason: requiresConfirmation ? "selected capability requires host confirmation" : "selected by bounded Jev decision",
|
|
119
|
+
provider: provider.name,
|
|
120
|
+
latencyMs: elapsed(started),
|
|
121
|
+
candidateCount: eligible.length,
|
|
122
|
+
contextBytes: projection.context_bytes,
|
|
123
|
+
candidates,
|
|
124
|
+
selected: selected.id,
|
|
125
|
+
probabilities: answer.probabilities,
|
|
126
|
+
confidence: answer.confidence,
|
|
127
|
+
rawJev: raw,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readChoiceAnswer(raw) {
|
|
132
|
+
const answers = raw?.answers;
|
|
133
|
+
if (!answers || typeof answers !== "object") throw new Error("response has no answers object");
|
|
134
|
+
const answer = answers.tool ?? Object.values(answers).find((value) => value?.type === "choice");
|
|
135
|
+
if (!answer || answer.type !== "choice" || typeof answer.choice !== "string" || !answer.probabilities || typeof answer.probabilities !== "object") {
|
|
136
|
+
throw new Error("response does not contain a valid Choice answer");
|
|
137
|
+
}
|
|
138
|
+
const probabilities = Object.fromEntries(Object.entries(answer.probabilities).map(([id, value]) => {
|
|
139
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) throw new Error(`invalid probability for ${id}`);
|
|
140
|
+
return [id, value];
|
|
141
|
+
}));
|
|
142
|
+
const confidence = typeof answer.confidence === "number" ? answer.confidence : Math.max(...Object.values(probabilities), 0);
|
|
143
|
+
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) throw new Error("invalid confidence");
|
|
144
|
+
return { choice: answer.choice, probabilities, confidence };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function fallback(base, assessments, type, reason, state, rawJev = null) {
|
|
148
|
+
return decisionEnvelope({
|
|
149
|
+
status: "fallback",
|
|
150
|
+
reason,
|
|
151
|
+
provider: base.provider,
|
|
152
|
+
latencyMs: elapsed(base.started),
|
|
153
|
+
candidateCount: base.candidateCount,
|
|
154
|
+
contextBytes: byteLength(state),
|
|
155
|
+
candidates: decisionCandidates(assessments),
|
|
156
|
+
rawJev,
|
|
157
|
+
fallback: { type, reason },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function elapsed(started) {
|
|
162
|
+
return Number((performance.now() - started).toFixed(3));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function routeViaJevRouter(request, options) {
|
|
166
|
+
const started = performance.now();
|
|
167
|
+
const capabilities = normalizeCapabilities(request.capabilities);
|
|
168
|
+
const { assessments, eligible } = filterCapabilities(capabilities, request, { ...DEFAULT_POLICY, ...request.policy, ...(options.policy ?? {}) });
|
|
169
|
+
const projection = projectState(request, eligible, options.maxContextBytes ?? 6_000, { mode: options.contextFilterMode });
|
|
170
|
+
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) });
|
|
171
|
+
|
|
172
|
+
let module;
|
|
173
|
+
try {
|
|
174
|
+
module = await import("jevrouter");
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return fallback({ provider: "jevrouter", candidateCount: eligible.length, contextBytes: projection.context_bytes, started }, assessments, "dependency_error", error instanceof Error ? error.message : String(error), projection.state);
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const result = await module.route({
|
|
180
|
+
request: request.intent,
|
|
181
|
+
context: projection.state.context,
|
|
182
|
+
candidates: eligible.map(toJevRouterCapability),
|
|
183
|
+
}, {
|
|
184
|
+
provider: options.innerProvider ?? process.env.JEVROUTER_PROVIDER ?? "demo",
|
|
185
|
+
policy: options.policy,
|
|
186
|
+
});
|
|
187
|
+
const selected = result.decision?.selected ?? null;
|
|
188
|
+
const raw = result.raw_jev ?? null;
|
|
189
|
+
const candidates = decisionCandidates(assessments, Object.fromEntries((result.decision?.candidates ?? []).map((candidate) => [candidate.id, candidate.jev_probability ?? 0])), null);
|
|
190
|
+
return decisionEnvelope({
|
|
191
|
+
status: result.status === "selected" ? "selected" : result.status === "needs_confirmation" ? "needs_confirmation" : "fallback",
|
|
192
|
+
selected,
|
|
193
|
+
reason: result.fallback?.reason ?? "selected by JevRouter",
|
|
194
|
+
provider: `jevrouter:${options.innerProvider ?? process.env.JEVROUTER_PROVIDER ?? "demo"}`,
|
|
195
|
+
latencyMs: elapsed(started),
|
|
196
|
+
candidateCount: eligible.length,
|
|
197
|
+
contextBytes: projection.context_bytes,
|
|
198
|
+
candidates,
|
|
199
|
+
rawJev: raw,
|
|
200
|
+
fallback: result.fallback?.type ? { type: result.fallback.type, reason: result.fallback.reason } : null,
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return fallback({ provider: "jevrouter", candidateCount: eligible.length, contextBytes: projection.context_bytes, started }, assessments, "provider_error", error instanceof Error ? error.message : String(error), projection.state);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function toJevRouterCapability(capability) {
|
|
208
|
+
return {
|
|
209
|
+
id: capability.id,
|
|
210
|
+
name: capability.name,
|
|
211
|
+
type: capability.kind === "mcp" ? "mcp_tool" : capability.kind,
|
|
212
|
+
description: capability.description,
|
|
213
|
+
permissions: capability.permissions,
|
|
214
|
+
risk: { level: capability.risk },
|
|
215
|
+
availability: { available: capability.available },
|
|
216
|
+
execution: { mode: capability.kind === "mcp" ? "mcp" : capability.kind, target: capability.execution.target },
|
|
217
|
+
policy: { requires_confirmation: capability.requires_confirmation },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -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
|
+
}
|