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
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { buildModelRouteReport } from "../src/model-route-metrics.mjs";
4
+
5
+ const path = process.argv[2] ?? process.env.JEV_REPLAY_CASES ?? ".jev/replay/cases.jsonl";
6
+ let text = "";
7
+ try {
8
+ text = await readFile(path, "utf8");
9
+ } catch (error) {
10
+ if (error?.code !== "ENOENT") throw error;
11
+ }
12
+ const records = text.split(/\r?\n/).filter(Boolean).flatMap((line) => {
13
+ try { return [JSON.parse(line)]; } catch { return []; }
14
+ });
15
+ console.log(JSON.stringify(buildModelRouteReport(records), null, 2));
@@ -1,59 +1,59 @@
1
- import { routeRequest } from "../src/route.mjs";
2
-
3
- const endpoint = process.env.OPENROUTER_DECISIONS_ENDPOINT ?? "https://openrouter.ai/api/alpha/decisions";
4
- const model = process.env.OPENROUTER_DECISIONS_MODEL ?? "typesafe/jev-1.13";
5
- const decision = await routeRequest({
6
- schema_version: 1,
7
- harness: "jev-layer-openrouter-smoke",
8
- intent: "Find where the OpenRouter Decisions provider is implemented without modifying files",
9
- context: {
10
- purpose: "one real bounded Choice call",
11
- secret_note: "must be redacted before provider transmission",
12
- },
13
- actor_permissions: ["read", "browser", "terminal:read"],
14
- capabilities: [
15
- {
16
- id: "repo_search",
17
- kind: "mcp",
18
- name: "Repository search",
19
- description: "Search repository files and symbols without changing files",
20
- permissions: ["read"],
21
- risk: "low",
22
- },
23
- {
24
- id: "browser_inspect",
25
- kind: "mcp",
26
- name: "Browser inspection",
27
- description: "Open a webpage and inspect visible browser state",
28
- permissions: ["browser"],
29
- risk: "medium",
30
- },
31
- {
32
- id: "terminal_read",
33
- kind: "cli",
34
- name: "Read-only terminal",
35
- description: "Run a read-only shell command and return its output",
36
- permissions: ["terminal:read"],
37
- risk: "low",
38
- },
39
- ],
40
- }, {
41
- provider: "openrouter",
42
- openrouter: { endpoint, model, timeoutMs: 15_000 },
43
- });
44
-
45
- const raw = decision.raw_jev ?? {};
46
- const usage = raw.usage && typeof raw.usage === "object" ? raw.usage : {};
47
- console.log(JSON.stringify({
48
- request: { endpoint, model, method: "POST" },
49
- response: { id: raw.id ?? null, model: raw.model ?? null, provider: raw.provider ?? null },
50
- status: decision.status,
51
- selected: decision.selected,
52
- probabilities: decision.probabilities,
53
- confidence: decision.confidence,
54
- latency_ms: decision.receipt.latency_ms,
55
- cost_usd: typeof usage.cost === "number" ? usage.cost : null,
56
- usage,
57
- fallback: decision.fallback,
58
- execution: decision.execution,
59
- }, null, 2));
1
+ import { routeRequest } from "../src/route.mjs";
2
+
3
+ const endpoint = process.env.OPENROUTER_DECISIONS_ENDPOINT ?? "https://openrouter.ai/api/alpha/decisions";
4
+ const model = process.env.OPENROUTER_DECISIONS_MODEL ?? "typesafe/jev-1.14";
5
+ const decision = await routeRequest({
6
+ schema_version: 1,
7
+ harness: "jev-layer-openrouter-smoke",
8
+ intent: "Find where the OpenRouter Decisions provider is implemented without modifying files",
9
+ context: {
10
+ purpose: "one real bounded Choice call",
11
+ secret_note: "must be redacted before provider transmission",
12
+ },
13
+ actor_permissions: ["read", "browser", "terminal:read"],
14
+ capabilities: [
15
+ {
16
+ id: "repo_search",
17
+ kind: "mcp",
18
+ name: "Repository search",
19
+ description: "Search repository files and symbols without changing files",
20
+ permissions: ["read"],
21
+ risk: "low",
22
+ },
23
+ {
24
+ id: "browser_inspect",
25
+ kind: "mcp",
26
+ name: "Browser inspection",
27
+ description: "Open a webpage and inspect visible browser state",
28
+ permissions: ["browser"],
29
+ risk: "medium",
30
+ },
31
+ {
32
+ id: "terminal_read",
33
+ kind: "cli",
34
+ name: "Read-only terminal",
35
+ description: "Run a read-only shell command and return its output",
36
+ permissions: ["terminal:read"],
37
+ risk: "low",
38
+ },
39
+ ],
40
+ }, {
41
+ provider: "openrouter",
42
+ openrouter: { endpoint, model, timeoutMs: 15_000 },
43
+ });
44
+
45
+ const raw = decision.raw_jev ?? {};
46
+ const usage = raw.usage && typeof raw.usage === "object" ? raw.usage : {};
47
+ console.log(JSON.stringify({
48
+ request: { endpoint, model, method: "POST" },
49
+ response: { id: raw.id ?? null, model: raw.model ?? null, provider: raw.provider ?? null },
50
+ status: decision.status,
51
+ selected: decision.selected,
52
+ probabilities: decision.probabilities,
53
+ confidence: decision.confidence,
54
+ latency_ms: decision.receipt.latency_ms,
55
+ cost_usd: typeof usage.cost === "number" ? usage.cost : null,
56
+ usage,
57
+ fallback: decision.fallback,
58
+ execution: decision.execution,
59
+ }, null, 2));
@@ -1,124 +1,124 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
3
- import { readRoutingCases, readSupervisionCases, replayCasePath } from "../src/receipts.mjs";
4
- import { deterministicSupervisionPolicy } from "../src/supervision.mjs";
5
- import { routeRequest } from "../src/route.mjs";
6
-
7
- const args = parseArgs(process.argv.slice(2));
8
- const inputPath = replayCasePath(args.input);
9
- const provider = args.provider ?? process.env.JEV_REPLAY_PROVIDER ?? "demo";
10
- const allCases = await readRoutingCases(inputPath);
11
- const cases = args.limit ? allCases.slice(0, args.limit) : allCases;
12
- const results = [];
13
-
14
- for (const routingCase of cases) {
15
- const decision = await routeRequest(routingCase.request, { provider });
16
- if (decision.execution?.enabled) throw new Error(`replay attempted host execution for ${routingCase.case_id}`);
17
- results.push({
18
- source_case_id: routingCase.case_id,
19
- source_correlation_id: routingCase.correlation_id,
20
- replay_correlation_id: decision.correlation_id,
21
- surface: routingCase.request.context?.browser ? "browser" : "generic",
22
- harness: routingCase.harness,
23
- selected: decision.selected,
24
- confidence: decision.confidence,
25
- status: decision.status,
26
- fallback: decision.fallback?.type ?? null,
27
- latency_ms: decision.receipt?.latency_ms ?? null,
28
- cost_usd: decision.receipt?.cost_usd ?? decision.raw_jev?.usage?.cost ?? null,
29
- execution: decision.execution,
30
- });
31
- }
32
-
33
- const supervisionCases = args.limit
34
- ? (await readSupervisionCases(inputPath)).slice(0, args.limit)
35
- : await readSupervisionCases(inputPath);
36
- const supervisionResults = supervisionCases.map((record) => ({
37
- source_case_id: record.case_id,
38
- source_correlation_id: record.correlation_id,
39
- harness: record.harness,
40
- assessment: record.supervision.assessment,
41
- recorded_action: record.supervision.action,
42
- replayed_policy: deterministicSupervisionPolicy({
43
- assessment: record.supervision.assessment,
44
- evidence: record.request.context?.evidence ?? {},
45
- }),
46
- host_execution: false,
47
- }));
48
-
49
-
50
- const summary = summarize(results);
51
- summary.supervision_case_count = supervisionResults.length;
52
- summary.supervision_policy_match_rate = rate(
53
- supervisionResults.filter((result) => result.replayed_policy.action === result.recorded_action).length,
54
- supervisionResults.length,
55
- );
56
- const report = {
57
- ok: true,
58
- source: inputPath,
59
- provider,
60
- replayed_cases: results.length,
61
- replayed_supervision_cases: supervisionResults.length,
62
- host_execution_calls: 0,
63
- summary,
64
- cases: results,
65
- supervision_cases: supervisionResults,
66
- };
67
-
68
- if (args.output) {
69
- await mkdir(dirname(args.output), { recursive: true });
70
- await writeFile(args.output, `${JSON.stringify(report, null, 2)}\n`, "utf8");
71
- }
72
- console.log(JSON.stringify(report, null, 2));
73
- function summarize(results) {
74
- const total = results.length;
75
- const fallbackCount = results.filter((result) => result.status === "fallback").length;
76
- const selectedCount = results.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length;
77
- const noDecisionCount = results.filter((result) => result.status === "no_decision").length;
78
- const browserResults = results.filter((result) => result.surface === "browser");
79
- const confidences = results.map((result) => result.confidence).filter((value) => typeof value === "number");
80
- const latencies = results.map((result) => result.latency_ms).filter((value) => typeof value === "number");
81
- const costs = results.map((result) => result.cost_usd).filter((value) => typeof value === "number");
82
- return {
83
- selected_rate: rate(selectedCount, total),
84
- fallback_rate: rate(fallbackCount, total),
85
- no_decision_rate: rate(noDecisionCount, total),
86
- browser_case_count: browserResults.length,
87
- browser_selected_rate: rate(browserResults.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length, browserResults.length),
88
- average_confidence: average(confidences),
89
- average_latency_ms: average(latencies),
90
- p50_latency_ms: percentile(latencies, 0.5),
91
- p95_latency_ms: percentile(latencies, 0.95),
92
- total_cost_usd: Number(costs.reduce((sum, value) => sum + value, 0).toFixed(9)),
93
- status_counts: Object.fromEntries([...new Set(results.map((result) => result.status))].map((status) => [status, results.filter((result) => result.status === status).length])),
94
- };
95
- }
96
-
97
- function parseArgs(argv) {
98
- const result = {};
99
- for (let index = 0; index < argv.length; index += 1) {
100
- const value = argv[index];
101
- if (value === "--input") result.input = argv[++index];
102
- else if (value === "--provider") result.provider = argv[++index];
103
- else if (value === "--limit") result.limit = Number(argv[++index]);
104
- else if (value === "--output") result.output = argv[++index];
105
- }
106
- if (result.limit !== undefined && (!Number.isInteger(result.limit) || result.limit < 0)) throw new Error("--limit must be a non-negative integer");
107
- return result;
108
- }
109
-
110
- function average(values) {
111
- return values.length ? Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(3)) : null;
112
- }
113
-
114
- function percentile(values, fraction) {
115
- if (!values.length) return null;
116
- const sorted = [...values].sort((left, right) => left - right);
117
- const rank = Math.max(0, Math.ceil(sorted.length * fraction) - 1);
118
- return Number(sorted[Math.min(sorted.length - 1, rank)].toFixed(3));
119
- }
120
-
121
-
122
- function rate(part, total) {
123
- return total ? Number((part / total).toFixed(4)) : 0;
124
- }
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { readRoutingCases, readSupervisionCases, replayCasePath } from "../src/receipts.mjs";
4
+ import { deterministicSupervisionPolicy } from "../src/supervision.mjs";
5
+ import { routeRequest } from "../src/route.mjs";
6
+
7
+ const args = parseArgs(process.argv.slice(2));
8
+ const inputPath = replayCasePath(args.input);
9
+ const provider = args.provider ?? process.env.JEV_REPLAY_PROVIDER ?? "demo";
10
+ const allCases = await readRoutingCases(inputPath);
11
+ const cases = args.limit ? allCases.slice(0, args.limit) : allCases;
12
+ const results = [];
13
+
14
+ for (const routingCase of cases) {
15
+ const decision = await routeRequest(routingCase.request, { provider });
16
+ if (decision.execution?.enabled) throw new Error(`replay attempted host execution for ${routingCase.case_id}`);
17
+ results.push({
18
+ source_case_id: routingCase.case_id,
19
+ source_correlation_id: routingCase.correlation_id,
20
+ replay_correlation_id: decision.correlation_id,
21
+ surface: routingCase.request.context?.browser ? "browser" : "generic",
22
+ harness: routingCase.harness,
23
+ selected: decision.selected,
24
+ confidence: decision.confidence,
25
+ status: decision.status,
26
+ fallback: decision.fallback?.type ?? null,
27
+ latency_ms: decision.receipt?.latency_ms ?? null,
28
+ cost_usd: decision.receipt?.cost_usd ?? decision.raw_jev?.usage?.cost ?? null,
29
+ execution: decision.execution,
30
+ });
31
+ }
32
+
33
+ const supervisionCases = args.limit
34
+ ? (await readSupervisionCases(inputPath)).slice(0, args.limit)
35
+ : await readSupervisionCases(inputPath);
36
+ const supervisionResults = supervisionCases.map((record) => ({
37
+ source_case_id: record.case_id,
38
+ source_correlation_id: record.correlation_id,
39
+ harness: record.harness,
40
+ assessment: record.supervision.assessment,
41
+ recorded_action: record.supervision.action,
42
+ replayed_policy: deterministicSupervisionPolicy({
43
+ assessment: record.supervision.assessment,
44
+ evidence: record.request.context?.evidence ?? {},
45
+ }),
46
+ host_execution: false,
47
+ }));
48
+
49
+
50
+ const summary = summarize(results);
51
+ summary.supervision_case_count = supervisionResults.length;
52
+ summary.supervision_policy_match_rate = rate(
53
+ supervisionResults.filter((result) => result.replayed_policy.action === result.recorded_action).length,
54
+ supervisionResults.length,
55
+ );
56
+ const report = {
57
+ ok: true,
58
+ source: inputPath,
59
+ provider,
60
+ replayed_cases: results.length,
61
+ replayed_supervision_cases: supervisionResults.length,
62
+ host_execution_calls: 0,
63
+ summary,
64
+ cases: results,
65
+ supervision_cases: supervisionResults,
66
+ };
67
+
68
+ if (args.output) {
69
+ await mkdir(dirname(args.output), { recursive: true });
70
+ await writeFile(args.output, `${JSON.stringify(report, null, 2)}\n`, "utf8");
71
+ }
72
+ console.log(JSON.stringify(report, null, 2));
73
+ function summarize(results) {
74
+ const total = results.length;
75
+ const fallbackCount = results.filter((result) => result.status === "fallback").length;
76
+ const selectedCount = results.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length;
77
+ const noDecisionCount = results.filter((result) => result.status === "no_decision").length;
78
+ const browserResults = results.filter((result) => result.surface === "browser");
79
+ const confidences = results.map((result) => result.confidence).filter((value) => typeof value === "number");
80
+ const latencies = results.map((result) => result.latency_ms).filter((value) => typeof value === "number");
81
+ const costs = results.map((result) => result.cost_usd).filter((value) => typeof value === "number");
82
+ return {
83
+ selected_rate: rate(selectedCount, total),
84
+ fallback_rate: rate(fallbackCount, total),
85
+ no_decision_rate: rate(noDecisionCount, total),
86
+ browser_case_count: browserResults.length,
87
+ browser_selected_rate: rate(browserResults.filter((result) => result.status === "selected" || result.status === "needs_confirmation").length, browserResults.length),
88
+ average_confidence: average(confidences),
89
+ average_latency_ms: average(latencies),
90
+ p50_latency_ms: percentile(latencies, 0.5),
91
+ p95_latency_ms: percentile(latencies, 0.95),
92
+ total_cost_usd: Number(costs.reduce((sum, value) => sum + value, 0).toFixed(9)),
93
+ status_counts: Object.fromEntries([...new Set(results.map((result) => result.status))].map((status) => [status, results.filter((result) => result.status === status).length])),
94
+ };
95
+ }
96
+
97
+ function parseArgs(argv) {
98
+ const result = {};
99
+ for (let index = 0; index < argv.length; index += 1) {
100
+ const value = argv[index];
101
+ if (value === "--input") result.input = argv[++index];
102
+ else if (value === "--provider") result.provider = argv[++index];
103
+ else if (value === "--limit") result.limit = Number(argv[++index]);
104
+ else if (value === "--output") result.output = argv[++index];
105
+ }
106
+ if (result.limit !== undefined && (!Number.isInteger(result.limit) || result.limit < 0)) throw new Error("--limit must be a non-negative integer");
107
+ return result;
108
+ }
109
+
110
+ function average(values) {
111
+ return values.length ? Number((values.reduce((sum, value) => sum + value, 0) / values.length).toFixed(3)) : null;
112
+ }
113
+
114
+ function percentile(values, fraction) {
115
+ if (!values.length) return null;
116
+ const sorted = [...values].sort((left, right) => left - right);
117
+ const rank = Math.max(0, Math.ceil(sorted.length * fraction) - 1);
118
+ return Number(sorted[Math.min(sorted.length - 1, rank)].toFixed(3));
119
+ }
120
+
121
+
122
+ function rate(part, total) {
123
+ return total ? Number((part / total).toFixed(4)) : 0;
124
+ }
package/scripts/smoke.mjs CHANGED
@@ -1,34 +1,34 @@
1
- import assert from "node:assert/strict";
2
- import { readFile } from "node:fs/promises";
3
- import { routeRequest } from "../src/route.mjs";
4
-
5
- const request = JSON.parse(await readFile(new URL("../examples/route-request.json", import.meta.url), "utf8"));
6
- const selected = await routeRequest(request, { provider: "demo" });
7
- assert.equal(selected.status, "selected");
8
- assert.equal(selected.selected, "repo_search");
9
- assert.equal(selected.execution.enabled, false);
10
- assert.equal(selected.execution.status, "not_started");
11
- assert.ok(selected.receipt.context_bytes > 0);
12
- assert.equal(JSON.stringify(selected.raw_jev).includes("must-not-reach-provider"), false);
13
-
14
- const unavailable = await routeRequest({
15
- ...request,
16
- intent: "Modify a file",
17
- capabilities: request.capabilities.map((capability) => ({ ...capability, availability: capability.id === "repo_search" ? { available: false, reason: "offline" } : undefined })),
18
- }, { provider: "demo" });
19
- assert.equal(unavailable.status, "no_decision");
20
-
21
- const providerFailure = await routeRequest(request, {
22
- provider: { name: "failing-test-provider", async decide() { throw new Error("synthetic provider failure"); } },
23
- });
24
- assert.equal(providerFailure.status, "fallback");
25
- assert.equal(providerFailure.fallback.type, "provider_error");
26
- assert.equal(providerFailure.execution.enabled, false);
27
-
28
- console.log(JSON.stringify({
29
- ok: true,
30
- selected: selected.selected,
31
- unavailable_status: unavailable.status,
32
- provider_failure: providerFailure.fallback.type,
33
- execution_started: selected.execution.enabled,
34
- }));
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { routeRequest } from "../src/route.mjs";
4
+
5
+ const request = JSON.parse(await readFile(new URL("../examples/route-request.json", import.meta.url), "utf8"));
6
+ const selected = await routeRequest(request, { provider: "demo" });
7
+ assert.equal(selected.status, "selected");
8
+ assert.equal(selected.selected, "repo_search");
9
+ assert.equal(selected.execution.enabled, false);
10
+ assert.equal(selected.execution.status, "not_started");
11
+ assert.ok(selected.receipt.context_bytes > 0);
12
+ assert.equal(JSON.stringify(selected.raw_jev).includes("must-not-reach-provider"), false);
13
+
14
+ const unavailable = await routeRequest({
15
+ ...request,
16
+ intent: "Modify a file",
17
+ capabilities: request.capabilities.map((capability) => ({ ...capability, availability: capability.id === "repo_search" ? { available: false, reason: "offline" } : undefined })),
18
+ }, { provider: "demo" });
19
+ assert.equal(unavailable.status, "no_decision");
20
+
21
+ const providerFailure = await routeRequest(request, {
22
+ provider: { name: "failing-test-provider", async decide() { throw new Error("synthetic provider failure"); } },
23
+ });
24
+ assert.equal(providerFailure.status, "fallback");
25
+ assert.equal(providerFailure.fallback.type, "provider_error");
26
+ assert.equal(providerFailure.execution.enabled, false);
27
+
28
+ console.log(JSON.stringify({
29
+ ok: true,
30
+ selected: selected.selected,
31
+ unavailable_status: unavailable.status,
32
+ provider_failure: providerFailure.fallback.type,
33
+ execution_started: selected.execution.enabled,
34
+ }));