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.
Files changed (83) hide show
  1. package/CHANGELOG.md +45 -34
  2. package/CONTRIBUTING.md +146 -146
  3. package/LICENSE +21 -21
  4. package/README.md +123 -126
  5. package/README.ru.md +138 -117
  6. package/README.zh-CN.md +137 -117
  7. package/RELEASE.md +53 -53
  8. package/SECURITY.md +56 -56
  9. package/bin/jev.mjs +213 -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 +22 -22
  14. package/config/omp.mcp.json +14 -14
  15. package/config/providers.env.example +13 -13
  16. package/docs/AGENT-IMPLEMENTATION.md +250 -0
  17. package/docs/COMPATIBILITY.md +15 -15
  18. package/docs/HERMES-LOCAL-AGENT-HANDOFF.ru.md +177 -0
  19. package/docs/PROVIDERS.md +67 -0
  20. package/docs/SCHEMA-VERSIONING.md +96 -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 +150 -51
  31. package/integrations/hermes/plugin.yaml +10 -5
  32. package/integrations/hermes/schemas.py +89 -12
  33. package/integrations/omp/extension.js +105 -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 +162 -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 +113 -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 +571 -569
  53. package/src/cli.mjs +33 -33
  54. package/src/config.mjs +85 -75
  55. package/src/context-filter.mjs +56 -56
  56. package/src/contract.mjs +72 -72
  57. package/src/discovery.mjs +65 -65
  58. package/src/hermes-adapter.mjs +123 -0
  59. package/src/mcp-server.mjs +267 -210
  60. package/src/model-route-metrics.mjs +89 -0
  61. package/src/model-routing.mjs +87 -0
  62. package/src/providers/demo.mjs +52 -52
  63. package/src/providers/index.mjs +12 -0
  64. package/src/providers/typesafe.mjs +126 -126
  65. package/src/receipts.mjs +227 -226
  66. package/src/registry.mjs +109 -109
  67. package/src/relevance-filter.mjs +99 -99
  68. package/src/route.mjs +219 -221
  69. package/src/shadow-compaction.mjs +170 -0
  70. package/src/supervision.mjs +268 -244
  71. package/test/browser.test.mjs +199 -199
  72. package/test/capability.test.mjs +54 -54
  73. package/test/context-filter.test.mjs +65 -65
  74. package/test/hermes-adapter.test.mjs +62 -0
  75. package/test/model-route-metrics.test.mjs +61 -0
  76. package/test/model-routing.test.mjs +59 -0
  77. package/test/openrouter-provider.test.mjs +55 -55
  78. package/test/provider-transports.test.mjs +205 -0
  79. package/test/providers.test.mjs +126 -0
  80. package/test/receipts.test.mjs +71 -71
  81. package/test/route.test.mjs +74 -74
  82. package/test/shadow-compaction.test.mjs +67 -0
  83. 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 configuredReplayPath(config) {
50
- return process.env.JEV_REPLAY_CASES ?? config?.replay_cases ?? DEFAULT_CONFIG.replay_cases;
51
- }
52
-
53
- export async function pathExists(path) {
54
- try {
55
- await access(path);
56
- return true;
57
- } catch {
58
- return false;
59
- }
60
- }
61
-
62
- function mergeConfig(parsed) {
63
- return {
64
- ...DEFAULT_CONFIG,
65
- ...parsed,
66
- features: {
67
- ...DEFAULT_CONFIG.features,
68
- ...(parsed.features && typeof parsed.features === "object" ? parsed.features : {}),
69
- },
70
- providers: {
71
- ...DEFAULT_CONFIG.providers,
72
- ...(parsed.providers && typeof parsed.providers === "object" ? parsed.providers : {}),
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
+ }
@@ -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
+ }