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/cli.mjs
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { createInterface } from "node:readline";
|
|
4
|
-
import { configuredProvider, loadConfig } from "./config.mjs";
|
|
5
|
-
import { routeRequest } from "./route.mjs";
|
|
6
|
-
|
|
7
|
-
const args = parseArgs(process.argv.slice(2));
|
|
8
|
-
const { config } = await loadConfig();
|
|
9
|
-
const input = args.input ? await readFile(args.input, "utf8") : null;
|
|
10
|
-
const lines = input === null ? createInterface({ input: process.stdin, crlfDelay: Infinity }) : [input];
|
|
11
|
-
|
|
12
|
-
for await (const line of lines) {
|
|
13
|
-
if (!line.trim()) continue;
|
|
14
|
-
try {
|
|
15
|
-
const request = JSON.parse(line);
|
|
16
|
-
const { provider = args.provider ?? configuredProvider(config), engine = args.engine ?? "native", ...payload } = request;
|
|
17
|
-
const decision = await routeRequest(payload, { provider, engine });
|
|
18
|
-
process.stdout.write(`${JSON.stringify(decision)}\n`);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
process.stdout.write(`${JSON.stringify({ status: "error", reason: error instanceof Error ? error.message : String(error), execution: { enabled: false, status: "not_started" } })}\n`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function parseArgs(argv) {
|
|
25
|
-
const result = {};
|
|
26
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
27
|
-
const value = argv[index];
|
|
28
|
-
if (value === "--input") result.input = argv[++index];
|
|
29
|
-
else if (value === "--provider") result.provider = argv[++index];
|
|
30
|
-
else if (value === "--engine") result.engine = argv[++index];
|
|
31
|
-
}
|
|
32
|
-
return result;
|
|
33
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { configuredProvider, loadConfig } from "./config.mjs";
|
|
5
|
+
import { routeRequest } from "./route.mjs";
|
|
6
|
+
|
|
7
|
+
const args = parseArgs(process.argv.slice(2));
|
|
8
|
+
const { config } = await loadConfig();
|
|
9
|
+
const input = args.input ? await readFile(args.input, "utf8") : null;
|
|
10
|
+
const lines = input === null ? createInterface({ input: process.stdin, crlfDelay: Infinity }) : [input];
|
|
11
|
+
|
|
12
|
+
for await (const line of lines) {
|
|
13
|
+
if (!line.trim()) continue;
|
|
14
|
+
try {
|
|
15
|
+
const request = JSON.parse(line);
|
|
16
|
+
const { provider = args.provider ?? configuredProvider(config), engine = args.engine ?? "native", ...payload } = request;
|
|
17
|
+
const decision = await routeRequest(payload, { provider, engine, config });
|
|
18
|
+
process.stdout.write(`${JSON.stringify(decision)}\n`);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
process.stdout.write(`${JSON.stringify({ status: "error", reason: error instanceof Error ? error.message : String(error), execution: { enabled: false, status: "not_started" } })}\n`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const result = {};
|
|
26
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
27
|
+
const value = argv[index];
|
|
28
|
+
if (value === "--input") result.input = argv[++index];
|
|
29
|
+
else if (value === "--provider") result.provider = argv[++index];
|
|
30
|
+
else if (value === "--engine") result.engine = argv[++index];
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -1,75 +1,85 @@
|
|
|
1
|
-
import { access, readFile } from "node:fs/promises";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_CONFIG = Object.freeze({
|
|
5
|
-
schema_version: 1,
|
|
6
|
-
provider: "demo",
|
|
7
|
-
replay_cases: ".jev/replay/cases.jsonl",
|
|
8
|
-
features: {
|
|
9
|
-
browser_fast_path: false,
|
|
10
|
-
supervision: false,
|
|
11
|
-
context_filter: null,
|
|
12
|
-
},
|
|
13
|
-
providers: {
|
|
14
|
-
openrouter: {
|
|
15
|
-
api_key_env: "OPENROUTER_API_KEY",
|
|
16
|
-
endpoint_env: "OPENROUTER_DECISIONS_ENDPOINT",
|
|
17
|
-
model_env: "OPENROUTER_DECISIONS_MODEL",
|
|
18
|
-
},
|
|
19
|
-
typesafe: {
|
|
20
|
-
api_key_env: "TYPESAFE_API_KEY",
|
|
21
|
-
endpoint_env: "TYPESAFE_ENDPOINT",
|
|
22
|
-
model_env: "TYPESAFE_MODEL",
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
export async function loadConfig({ cwd = process.cwd(), path = process.env.JEV_CONFIG } = {}) {
|
|
28
|
-
const configPath = resolve(path || join(cwd, ".jev", "config.json"));
|
|
29
|
-
try {
|
|
30
|
-
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
31
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
32
|
-
return { config: { ...DEFAULT_CONFIG }, path: configPath, error: "config must be a JSON object" };
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
config: mergeConfig(parsed),
|
|
36
|
-
path: configPath,
|
|
37
|
-
error: null,
|
|
38
|
-
};
|
|
39
|
-
} catch (error) {
|
|
40
|
-
if (error?.code === "ENOENT") return { config: { ...DEFAULT_CONFIG }, path: configPath, error: null };
|
|
41
|
-
return { config: { ...DEFAULT_CONFIG }, path: configPath, error: error instanceof Error ? error.message : String(error) };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function configuredProvider(config, explicitProvider) {
|
|
46
|
-
return explicitProvider ?? process.env.JEV_LAYER_PROVIDER ?? config?.provider ?? DEFAULT_CONFIG.provider;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function
|
|
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
|
-
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_CONFIG = Object.freeze({
|
|
5
|
+
schema_version: 1,
|
|
6
|
+
provider: "demo",
|
|
7
|
+
replay_cases: ".jev/replay/cases.jsonl",
|
|
8
|
+
features: {
|
|
9
|
+
browser_fast_path: false,
|
|
10
|
+
supervision: false,
|
|
11
|
+
context_filter: null,
|
|
12
|
+
},
|
|
13
|
+
providers: {
|
|
14
|
+
openrouter: {
|
|
15
|
+
api_key_env: "OPENROUTER_API_KEY",
|
|
16
|
+
endpoint_env: "OPENROUTER_DECISIONS_ENDPOINT",
|
|
17
|
+
model_env: "OPENROUTER_DECISIONS_MODEL",
|
|
18
|
+
},
|
|
19
|
+
typesafe: {
|
|
20
|
+
api_key_env: "TYPESAFE_API_KEY",
|
|
21
|
+
endpoint_env: "TYPESAFE_ENDPOINT",
|
|
22
|
+
model_env: "TYPESAFE_MODEL",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export async function loadConfig({ cwd = process.cwd(), path = process.env.JEV_CONFIG } = {}) {
|
|
28
|
+
const configPath = resolve(path || join(cwd, ".jev", "config.json"));
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
31
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
32
|
+
return { config: { ...DEFAULT_CONFIG }, path: configPath, error: "config must be a JSON object" };
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
config: mergeConfig(parsed),
|
|
36
|
+
path: configPath,
|
|
37
|
+
error: null,
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error?.code === "ENOENT") return { config: { ...DEFAULT_CONFIG }, path: configPath, error: null };
|
|
41
|
+
return { config: { ...DEFAULT_CONFIG }, path: configPath, error: error instanceof Error ? error.message : String(error) };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function configuredProvider(config, explicitProvider) {
|
|
46
|
+
return explicitProvider ?? process.env.JEV_LAYER_PROVIDER ?? config?.provider ?? DEFAULT_CONFIG.provider;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function configuredProviderOptions(config, provider) {
|
|
50
|
+
if (!config?.providers?.[provider]) return {};
|
|
51
|
+
const names = { ...DEFAULT_CONFIG.providers[provider], ...config.providers[provider] };
|
|
52
|
+
return {
|
|
53
|
+
apiKey: process.env[names.api_key_env] ?? "",
|
|
54
|
+
endpoint: process.env[names.endpoint_env],
|
|
55
|
+
model: process.env[names.model_env],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function configuredReplayPath(config) {
|
|
60
|
+
return process.env.JEV_REPLAY_CASES ?? config?.replay_cases ?? DEFAULT_CONFIG.replay_cases;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function pathExists(path) {
|
|
64
|
+
try {
|
|
65
|
+
await access(path);
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mergeConfig(parsed) {
|
|
73
|
+
return {
|
|
74
|
+
...DEFAULT_CONFIG,
|
|
75
|
+
...parsed,
|
|
76
|
+
features: {
|
|
77
|
+
...DEFAULT_CONFIG.features,
|
|
78
|
+
...(parsed.features && typeof parsed.features === "object" ? parsed.features : {}),
|
|
79
|
+
},
|
|
80
|
+
providers: {
|
|
81
|
+
...DEFAULT_CONFIG.providers,
|
|
82
|
+
...(parsed.providers && typeof parsed.providers === "object" ? parsed.providers : {}),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
package/src/context-filter.mjs
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
import { byteLength, stableJson } from "./contract.mjs";
|
|
2
|
-
import { filterContext, normalizeRelevanceMode } from "./relevance-filter.mjs";
|
|
3
|
-
const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|cookie|credential|private[_-]?key|session)/i;
|
|
4
|
-
const MAX_STRING = 2_000;
|
|
5
|
-
const MAX_DEPTH = 4;
|
|
6
|
-
|
|
7
|
-
export function sanitizeContext(value, depth = 0, key = "") {
|
|
8
|
-
if (SECRET_KEY.test(key)) return "[REDACTED]";
|
|
9
|
-
if (depth > MAX_DEPTH) return "[DEPTH_LIMIT]";
|
|
10
|
-
if (typeof value === "string") return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…` : value;
|
|
11
|
-
if (value === null || typeof value === "number" || typeof value === "boolean") return value;
|
|
12
|
-
if (Array.isArray(value)) return value.slice(0, 32).map((item) => sanitizeContext(item, depth + 1, key));
|
|
13
|
-
if (value && typeof value === "object") {
|
|
14
|
-
return Object.fromEntries(Object.entries(value).slice(0, 64).map(([childKey, childValue]) => [childKey, sanitizeContext(childValue, depth + 1, childKey)]));
|
|
15
|
-
}
|
|
16
|
-
return String(value);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function projectState(request, capabilities, maxBytes = 6_000, options = {}) {
|
|
20
|
-
const configuredMode = options.mode ?? request.policy?.context_filter_mode ?? process.env.JEV_CONTEXT_FILTER;
|
|
21
|
-
const relevanceMode = normalizeRelevanceMode(configuredMode);
|
|
22
|
-
const relevance = filterContext(request.context, { mode: configuredMode, recent: options.recent });
|
|
23
|
-
const state = {
|
|
24
|
-
intent: request.intent.length > MAX_STRING ? `${request.intent.slice(0, MAX_STRING)}…` : request.intent,
|
|
25
|
-
context: sanitizeContext(relevance.context),
|
|
26
|
-
capabilities: capabilities.map((capability) => ({
|
|
27
|
-
id: capability.id,
|
|
28
|
-
kind: capability.kind,
|
|
29
|
-
name: capability.name,
|
|
30
|
-
description: capability.description,
|
|
31
|
-
risk: capability.risk,
|
|
32
|
-
})),
|
|
33
|
-
};
|
|
34
|
-
if (relevanceMode) state.context_filter = relevance.report;
|
|
35
|
-
|
|
36
|
-
let encoded = stableJson(state);
|
|
37
|
-
if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: false };
|
|
38
|
-
if (relevanceMode) {
|
|
39
|
-
state.context_filter = { ...state.context_filter, over_budget: true };
|
|
40
|
-
return { state, context_bytes: byteLength(encoded), truncated: true };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
state.context = {};
|
|
44
|
-
state.context_truncated = true;
|
|
45
|
-
encoded = stableJson(state);
|
|
46
|
-
if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: true };
|
|
47
|
-
|
|
48
|
-
state.capabilities = state.capabilities.map(({ id, kind, name, risk }) => ({ id, kind, name, risk }));
|
|
49
|
-
encoded = stableJson(state);
|
|
50
|
-
if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: true };
|
|
51
|
-
|
|
52
|
-
state.capabilities = state.capabilities.slice(0, 32);
|
|
53
|
-
state.capabilities_truncated = true;
|
|
54
|
-
state.intent = state.intent.slice(0, Math.max(128, maxBytes - byteLength(stableJson(state))));
|
|
55
|
-
return { state, context_bytes: byteLength(stableJson(state)), truncated: true };
|
|
56
|
-
}
|
|
1
|
+
import { byteLength, stableJson } from "./contract.mjs";
|
|
2
|
+
import { filterContext, normalizeRelevanceMode } from "./relevance-filter.mjs";
|
|
3
|
+
const SECRET_KEY = /(token|secret|password|passwd|api[_-]?key|authorization|cookie|credential|private[_-]?key|session)/i;
|
|
4
|
+
const MAX_STRING = 2_000;
|
|
5
|
+
const MAX_DEPTH = 4;
|
|
6
|
+
|
|
7
|
+
export function sanitizeContext(value, depth = 0, key = "") {
|
|
8
|
+
if (SECRET_KEY.test(key)) return "[REDACTED]";
|
|
9
|
+
if (depth > MAX_DEPTH) return "[DEPTH_LIMIT]";
|
|
10
|
+
if (typeof value === "string") return value.length > MAX_STRING ? `${value.slice(0, MAX_STRING)}…` : value;
|
|
11
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") return value;
|
|
12
|
+
if (Array.isArray(value)) return value.slice(0, 32).map((item) => sanitizeContext(item, depth + 1, key));
|
|
13
|
+
if (value && typeof value === "object") {
|
|
14
|
+
return Object.fromEntries(Object.entries(value).slice(0, 64).map(([childKey, childValue]) => [childKey, sanitizeContext(childValue, depth + 1, childKey)]));
|
|
15
|
+
}
|
|
16
|
+
return String(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function projectState(request, capabilities, maxBytes = 6_000, options = {}) {
|
|
20
|
+
const configuredMode = options.mode ?? request.policy?.context_filter_mode ?? process.env.JEV_CONTEXT_FILTER;
|
|
21
|
+
const relevanceMode = normalizeRelevanceMode(configuredMode);
|
|
22
|
+
const relevance = filterContext(request.context, { mode: configuredMode, recent: options.recent });
|
|
23
|
+
const state = {
|
|
24
|
+
intent: request.intent.length > MAX_STRING ? `${request.intent.slice(0, MAX_STRING)}…` : request.intent,
|
|
25
|
+
context: sanitizeContext(relevance.context),
|
|
26
|
+
capabilities: capabilities.map((capability) => ({
|
|
27
|
+
id: capability.id,
|
|
28
|
+
kind: capability.kind,
|
|
29
|
+
name: capability.name,
|
|
30
|
+
description: capability.description,
|
|
31
|
+
risk: capability.risk,
|
|
32
|
+
})),
|
|
33
|
+
};
|
|
34
|
+
if (relevanceMode) state.context_filter = relevance.report;
|
|
35
|
+
|
|
36
|
+
let encoded = stableJson(state);
|
|
37
|
+
if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: false };
|
|
38
|
+
if (relevanceMode) {
|
|
39
|
+
state.context_filter = { ...state.context_filter, over_budget: true };
|
|
40
|
+
return { state, context_bytes: byteLength(encoded), truncated: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
state.context = {};
|
|
44
|
+
state.context_truncated = true;
|
|
45
|
+
encoded = stableJson(state);
|
|
46
|
+
if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: true };
|
|
47
|
+
|
|
48
|
+
state.capabilities = state.capabilities.map(({ id, kind, name, risk }) => ({ id, kind, name, risk }));
|
|
49
|
+
encoded = stableJson(state);
|
|
50
|
+
if (byteLength(encoded) <= maxBytes) return { state, context_bytes: byteLength(encoded), truncated: true };
|
|
51
|
+
|
|
52
|
+
state.capabilities = state.capabilities.slice(0, 32);
|
|
53
|
+
state.capabilities_truncated = true;
|
|
54
|
+
state.intent = state.intent.slice(0, Math.max(128, maxBytes - byteLength(stableJson(state))));
|
|
55
|
+
return { state, context_bytes: byteLength(stableJson(state)), truncated: true };
|
|
56
|
+
}
|
package/src/contract.mjs
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
export const RISK_ORDER = Object.freeze({ low: 0, medium: 1, high: 2, critical: 3 });
|
|
4
|
-
export const CAPABILITY_KINDS = new Set(["skill", "tool", "mcp", "cli", "dsh", "subagent", "model"]);
|
|
5
|
-
|
|
6
|
-
export function stableJson(value) {
|
|
7
|
-
return JSON.stringify(sortJson(value));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function sortJson(value) {
|
|
11
|
-
if (Array.isArray(value)) return value.map(sortJson);
|
|
12
|
-
if (value && typeof value === "object") {
|
|
13
|
-
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])]));
|
|
14
|
-
}
|
|
15
|
-
return value;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function sha256(value) {
|
|
19
|
-
return `sha256:${createHash("sha256").update(typeof value === "string" ? value : stableJson(value)).digest("hex")}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function byteLength(value) {
|
|
23
|
-
return Buffer.byteLength(typeof value === "string" ? value : stableJson(value), "utf8");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function normalizeRequest(input) {
|
|
27
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
28
|
-
throw new TypeError("route request must be an object");
|
|
29
|
-
}
|
|
30
|
-
const intent = typeof input.intent === "string" ? input.intent : input.request;
|
|
31
|
-
if (typeof intent !== "string" || intent.trim() === "") {
|
|
32
|
-
throw new TypeError("route request requires a non-empty intent");
|
|
33
|
-
}
|
|
34
|
-
if (!Array.isArray(input.capabilities)) {
|
|
35
|
-
throw new TypeError("route request requires capabilities[]");
|
|
36
|
-
}
|
|
37
|
-
return {
|
|
38
|
-
schema_version: input.schema_version ?? 1,
|
|
39
|
-
harness: typeof input.harness === "string" ? input.harness : "unknown",
|
|
40
|
-
intent: intent.trim(),
|
|
41
|
-
context: input.context && typeof input.context === "object" ? input.context : {},
|
|
42
|
-
actor: typeof input.actor === "string" ? input.actor : undefined,
|
|
43
|
-
actor_permissions: Array.isArray(input.actor_permissions) ? input.actor_permissions.filter((value) => typeof value === "string") : undefined,
|
|
44
|
-
capabilities: input.capabilities,
|
|
45
|
-
policy: input.policy && typeof input.policy === "object" ? input.policy : {},
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function decisionEnvelope({ status, selected, reason, provider, latencyMs, candidateCount, contextBytes, candidates = [], probabilities = {}, confidence = null, rawJev = null, fallback = null, requestId = randomUUID(), correlationId = requestId, costUsd = rawJev?.usage?.cost ?? null }) {
|
|
50
|
-
return {
|
|
51
|
-
schema_version: 1,
|
|
52
|
-
correlation_id: correlationId,
|
|
53
|
-
status,
|
|
54
|
-
selected: selected ?? null,
|
|
55
|
-
confidence,
|
|
56
|
-
probabilities,
|
|
57
|
-
reason,
|
|
58
|
-
candidates,
|
|
59
|
-
fallback,
|
|
60
|
-
execution: { enabled: false, status: "not_started" },
|
|
61
|
-
raw_jev: rawJev,
|
|
62
|
-
receipt: {
|
|
63
|
-
correlation_id: correlationId,
|
|
64
|
-
request_id: requestId,
|
|
65
|
-
provider,
|
|
66
|
-
latency_ms: latencyMs,
|
|
67
|
-
cost_usd: costUsd,
|
|
68
|
-
candidate_count: candidateCount,
|
|
69
|
-
context_bytes: contextBytes,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const RISK_ORDER = Object.freeze({ low: 0, medium: 1, high: 2, critical: 3 });
|
|
4
|
+
export const CAPABILITY_KINDS = new Set(["skill", "tool", "mcp", "cli", "dsh", "subagent", "model"]);
|
|
5
|
+
|
|
6
|
+
export function stableJson(value) {
|
|
7
|
+
return JSON.stringify(sortJson(value));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sortJson(value) {
|
|
11
|
+
if (Array.isArray(value)) return value.map(sortJson);
|
|
12
|
+
if (value && typeof value === "object") {
|
|
13
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])]));
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sha256(value) {
|
|
19
|
+
return `sha256:${createHash("sha256").update(typeof value === "string" ? value : stableJson(value)).digest("hex")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function byteLength(value) {
|
|
23
|
+
return Buffer.byteLength(typeof value === "string" ? value : stableJson(value), "utf8");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function normalizeRequest(input) {
|
|
27
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
28
|
+
throw new TypeError("route request must be an object");
|
|
29
|
+
}
|
|
30
|
+
const intent = typeof input.intent === "string" ? input.intent : input.request;
|
|
31
|
+
if (typeof intent !== "string" || intent.trim() === "") {
|
|
32
|
+
throw new TypeError("route request requires a non-empty intent");
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(input.capabilities)) {
|
|
35
|
+
throw new TypeError("route request requires capabilities[]");
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
schema_version: input.schema_version ?? 1,
|
|
39
|
+
harness: typeof input.harness === "string" ? input.harness : "unknown",
|
|
40
|
+
intent: intent.trim(),
|
|
41
|
+
context: input.context && typeof input.context === "object" ? input.context : {},
|
|
42
|
+
actor: typeof input.actor === "string" ? input.actor : undefined,
|
|
43
|
+
actor_permissions: Array.isArray(input.actor_permissions) ? input.actor_permissions.filter((value) => typeof value === "string") : undefined,
|
|
44
|
+
capabilities: input.capabilities,
|
|
45
|
+
policy: input.policy && typeof input.policy === "object" ? input.policy : {},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function decisionEnvelope({ status, selected, reason, provider, latencyMs, candidateCount, contextBytes, candidates = [], probabilities = {}, confidence = null, rawJev = null, fallback = null, requestId = randomUUID(), correlationId = requestId, costUsd = rawJev?.usage?.cost ?? null }) {
|
|
50
|
+
return {
|
|
51
|
+
schema_version: 1,
|
|
52
|
+
correlation_id: correlationId,
|
|
53
|
+
status,
|
|
54
|
+
selected: selected ?? null,
|
|
55
|
+
confidence,
|
|
56
|
+
probabilities,
|
|
57
|
+
reason,
|
|
58
|
+
candidates,
|
|
59
|
+
fallback,
|
|
60
|
+
execution: { enabled: false, status: "not_started" },
|
|
61
|
+
raw_jev: rawJev,
|
|
62
|
+
receipt: {
|
|
63
|
+
correlation_id: correlationId,
|
|
64
|
+
request_id: requestId,
|
|
65
|
+
provider,
|
|
66
|
+
latency_ms: latencyMs,
|
|
67
|
+
cost_usd: costUsd,
|
|
68
|
+
candidate_count: candidateCount,
|
|
69
|
+
context_bytes: contextBytes,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/discovery.mjs
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
const DISCOVERY_SOURCES = Object.freeze([
|
|
4
|
-
["skills", "skill"],
|
|
5
|
-
["mcp", "mcp"],
|
|
6
|
-
["cli", "cli"],
|
|
7
|
-
["dsh", "dsh"],
|
|
8
|
-
["tools", "tool"],
|
|
9
|
-
["subagents", "subagent"],
|
|
10
|
-
["models", "model"],
|
|
11
|
-
]);
|
|
12
|
-
|
|
13
|
-
export function discoverCapabilities(input = {}) {
|
|
14
|
-
const entries = [];
|
|
15
|
-
for (const [field, kind] of DISCOVERY_SOURCES) {
|
|
16
|
-
const values = Array.isArray(input[field]) ? input[field] : [];
|
|
17
|
-
for (const value of values) entries.push(normalizeDiscovered(value, kind, input.source ?? `${kind}-discovery`));
|
|
18
|
-
}
|
|
19
|
-
const manifests = Array.isArray(input.manifests) ? input.manifests : [];
|
|
20
|
-
for (const manifest of manifests) {
|
|
21
|
-
const values = Array.isArray(manifest?.capabilities) ? manifest.capabilities : [manifest];
|
|
22
|
-
for (const value of values) {
|
|
23
|
-
const kind = value?.kind ?? value?.type ?? manifest?.kind ?? "tool";
|
|
24
|
-
entries.push(normalizeDiscovered(value, kind, value?.source ?? manifest?.source ?? "manifest"));
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
const seen = new Set();
|
|
28
|
-
return entries.filter((entry) => {
|
|
29
|
-
if (seen.has(entry.id)) return false;
|
|
30
|
-
seen.add(entry.id);
|
|
31
|
-
return true;
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function loadCapabilityManifest(path, options = {}) {
|
|
36
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
37
|
-
const manifest = Array.isArray(parsed) ? { capabilities: parsed } : parsed;
|
|
38
|
-
return discoverCapabilities({ manifests: [{ ...manifest, source: options.source ?? manifest.source ?? "manifest" }] });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function normalizeDiscovered(value, kind, source) {
|
|
42
|
-
if (!value || typeof value !== "object") throw new TypeError("discovered capability must be an object");
|
|
43
|
-
const id = stringValue(value.id ?? value.name, `${kind}-capability`);
|
|
44
|
-
const name = stringValue(value.name ?? value.id, id);
|
|
45
|
-
const description = stringValue(value.description ?? value.summary, `${kind} capability ${name}`);
|
|
46
|
-
const metadata = {
|
|
47
|
-
...(value.metadata && typeof value.metadata === "object" ? value.metadata : {}),
|
|
48
|
-
[kind]: stringValue(value.target ?? value.command ?? value.name, name),
|
|
49
|
-
};
|
|
50
|
-
return {
|
|
51
|
-
...value,
|
|
52
|
-
id,
|
|
53
|
-
kind,
|
|
54
|
-
name,
|
|
55
|
-
description,
|
|
56
|
-
source: stringValue(value.source, source),
|
|
57
|
-
verified: value.verified === true,
|
|
58
|
-
metadata,
|
|
59
|
-
available: value.available !== false && value.availability?.available !== false,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function stringValue(value, fallback) {
|
|
64
|
-
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
65
|
-
}
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
const DISCOVERY_SOURCES = Object.freeze([
|
|
4
|
+
["skills", "skill"],
|
|
5
|
+
["mcp", "mcp"],
|
|
6
|
+
["cli", "cli"],
|
|
7
|
+
["dsh", "dsh"],
|
|
8
|
+
["tools", "tool"],
|
|
9
|
+
["subagents", "subagent"],
|
|
10
|
+
["models", "model"],
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
export function discoverCapabilities(input = {}) {
|
|
14
|
+
const entries = [];
|
|
15
|
+
for (const [field, kind] of DISCOVERY_SOURCES) {
|
|
16
|
+
const values = Array.isArray(input[field]) ? input[field] : [];
|
|
17
|
+
for (const value of values) entries.push(normalizeDiscovered(value, kind, input.source ?? `${kind}-discovery`));
|
|
18
|
+
}
|
|
19
|
+
const manifests = Array.isArray(input.manifests) ? input.manifests : [];
|
|
20
|
+
for (const manifest of manifests) {
|
|
21
|
+
const values = Array.isArray(manifest?.capabilities) ? manifest.capabilities : [manifest];
|
|
22
|
+
for (const value of values) {
|
|
23
|
+
const kind = value?.kind ?? value?.type ?? manifest?.kind ?? "tool";
|
|
24
|
+
entries.push(normalizeDiscovered(value, kind, value?.source ?? manifest?.source ?? "manifest"));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
return entries.filter((entry) => {
|
|
29
|
+
if (seen.has(entry.id)) return false;
|
|
30
|
+
seen.add(entry.id);
|
|
31
|
+
return true;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function loadCapabilityManifest(path, options = {}) {
|
|
36
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
37
|
+
const manifest = Array.isArray(parsed) ? { capabilities: parsed } : parsed;
|
|
38
|
+
return discoverCapabilities({ manifests: [{ ...manifest, source: options.source ?? manifest.source ?? "manifest" }] });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeDiscovered(value, kind, source) {
|
|
42
|
+
if (!value || typeof value !== "object") throw new TypeError("discovered capability must be an object");
|
|
43
|
+
const id = stringValue(value.id ?? value.name, `${kind}-capability`);
|
|
44
|
+
const name = stringValue(value.name ?? value.id, id);
|
|
45
|
+
const description = stringValue(value.description ?? value.summary, `${kind} capability ${name}`);
|
|
46
|
+
const metadata = {
|
|
47
|
+
...(value.metadata && typeof value.metadata === "object" ? value.metadata : {}),
|
|
48
|
+
[kind]: stringValue(value.target ?? value.command ?? value.name, name),
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
...value,
|
|
52
|
+
id,
|
|
53
|
+
kind,
|
|
54
|
+
name,
|
|
55
|
+
description,
|
|
56
|
+
source: stringValue(value.source, source),
|
|
57
|
+
verified: value.verified === true,
|
|
58
|
+
metadata,
|
|
59
|
+
available: value.available !== false && value.availability?.available !== false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stringValue(value, fallback) {
|
|
64
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallback;
|
|
65
|
+
}
|