jev-layer 0.1.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 (66) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/CONTRIBUTING.md +146 -0
  3. package/LICENSE +21 -0
  4. package/README.md +126 -0
  5. package/README.ru.md +117 -0
  6. package/README.zh-CN.md +117 -0
  7. package/RELEASE.md +53 -0
  8. package/SECURITY.md +56 -0
  9. package/bin/jev.mjs +214 -0
  10. package/config/codex.mcp.toml +8 -0
  11. package/config/generic-mcp.json +13 -0
  12. package/config/hermes.mcp.yaml +9 -0
  13. package/config/jev.example.json +22 -0
  14. package/config/omp.mcp.json +14 -0
  15. package/config/providers.env.example +13 -0
  16. package/docs/COMPATIBILITY.md +15 -0
  17. package/docs/SCHEMA-VERSIONING.md +94 -0
  18. package/examples/capabilities.json +39 -0
  19. package/examples/route-request.json +28 -0
  20. package/integrations/codex/.codex-plugin/plugin.json +19 -0
  21. package/integrations/codex/.mcp.json +11 -0
  22. package/integrations/codex/AGENTS.md +1 -0
  23. package/integrations/codex/run-mcp.mjs +9 -0
  24. package/integrations/codex/skills/jev-route/SKILL.md +17 -0
  25. package/integrations/hermes/__init__.py +51 -0
  26. package/integrations/hermes/plugin.yaml +5 -0
  27. package/integrations/hermes/schemas.py +12 -0
  28. package/integrations/omp/extension.js +105 -0
  29. package/integrations/template/README.md +10 -0
  30. package/integrations/template/adapter.mjs +87 -0
  31. package/package.json +59 -0
  32. package/scripts/benchmark.mjs +35 -0
  33. package/scripts/browser-benchmark.mjs +92 -0
  34. package/scripts/browser-e2e.mjs +117 -0
  35. package/scripts/capability-e2e.mjs +31 -0
  36. package/scripts/clean-install-smoke.mjs +162 -0
  37. package/scripts/codex-mcp-smoke.mjs +119 -0
  38. package/scripts/context-filter-e2e.mjs +45 -0
  39. package/scripts/fail-open-smoke.mjs +120 -0
  40. package/scripts/feature-flags-smoke.mjs +46 -0
  41. package/scripts/mcp-receipt-smoke.mjs +98 -0
  42. package/scripts/openrouter-choice.mjs +59 -0
  43. package/scripts/replay-eval.mjs +124 -0
  44. package/scripts/smoke.mjs +34 -0
  45. package/scripts/supervision-e2e.mjs +109 -0
  46. package/src/browser.mjs +569 -0
  47. package/src/cli.mjs +33 -0
  48. package/src/config.mjs +75 -0
  49. package/src/context-filter.mjs +56 -0
  50. package/src/contract.mjs +72 -0
  51. package/src/discovery.mjs +65 -0
  52. package/src/mcp-server.mjs +210 -0
  53. package/src/providers/demo.mjs +52 -0
  54. package/src/providers/typesafe.mjs +126 -0
  55. package/src/receipts.mjs +226 -0
  56. package/src/registry.mjs +109 -0
  57. package/src/relevance-filter.mjs +99 -0
  58. package/src/route.mjs +221 -0
  59. package/src/supervision.mjs +244 -0
  60. package/test/browser.test.mjs +199 -0
  61. package/test/capability.test.mjs +54 -0
  62. package/test/context-filter.test.mjs +65 -0
  63. package/test/openrouter-provider.test.mjs +55 -0
  64. package/test/receipts.test.mjs +71 -0
  65. package/test/route.test.mjs +74 -0
  66. package/test/supervision.test.mjs +99 -0
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline";
3
+ import { appendExecutionReceipt, appendRoutingCase, buildExecutionReceipt, replayCasePath } from "./receipts.mjs";
4
+ import { configuredProvider, configuredReplayPath, loadConfig } from "./config.mjs";
5
+ import { decideBrowserStep } from "./browser.mjs";
6
+ import { superviseWork } from "./supervision.mjs";
7
+ import { routeRequest } from "./route.mjs";
8
+
9
+ const { config } = await loadConfig();
10
+ const serverInfo = { name: "jev-layer", version: "0.1.0" };
11
+ const CASES_PATH = replayCasePath(configuredReplayPath(config));
12
+ const pendingDecisions = new Map();
13
+ const tools = [
14
+ {
15
+ name: "jev_route",
16
+ description: "Return one bounded Jev capability decision. The host executes the selected capability; this tool never executes it.",
17
+ inputSchema: {
18
+ type: "object",
19
+ required: ["intent", "capabilities"],
20
+ properties: {
21
+ intent: { type: "string" },
22
+ harness: { type: "string" },
23
+ context: { type: "object" },
24
+ actor_permissions: { type: "array", items: { type: "string" } },
25
+ capabilities: { type: "array", items: { type: "object" } },
26
+ policy: { type: "object" },
27
+ provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
28
+ engine: { type: "string", enum: ["native", "jevrouter"] },
29
+ },
30
+ },
31
+ },
32
+ {
33
+ name: "jev_browser_step",
34
+ description: "Choose one bounded browser action from the host's visible observation. The host executes it or hands control back; this tool never drives a browser.",
35
+ inputSchema: {
36
+ type: "object",
37
+ required: ["goal", "observation"],
38
+ properties: {
39
+ goal: { type: "string" },
40
+ harness: { type: "string" },
41
+ start_url: { type: ["string", "null"] },
42
+ observation: { type: "object" },
43
+ policy: { type: "object" },
44
+ provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
45
+ enabled: { type: "boolean" },
46
+ },
47
+ },
48
+ },
49
+ {
50
+ name: "jev_supervise",
51
+ description: "Judge bounded work-state dimensions; deterministic host policy returns continue, verify, retry, finish, or escalate.",
52
+ inputSchema: {
53
+ type: "object",
54
+ required: ["job", "observation"],
55
+ properties: {
56
+ job: { type: "object" },
57
+ observation: { type: "object" },
58
+ evidence: { type: "object" },
59
+ harness: { type: "string" },
60
+ actor_permissions: { type: "array", items: { type: "string" } },
61
+ policy: { type: "object" },
62
+ attempts: { type: "integer", minimum: 0 },
63
+ provider: { type: "string", enum: ["demo", "typesafe", "openrouter"] },
64
+ enabled: { type: "boolean" },
65
+ },
66
+ },
67
+ },
68
+ {
69
+ name: "jev_record_execution",
70
+ description: "Attach a host execution result to a Jev decision and persist the unified execution receipt.",
71
+ inputSchema: {
72
+ type: "object",
73
+ required: ["correlation_id", "status"],
74
+ properties: {
75
+ correlation_id: { type: "string" },
76
+ harness: { type: "string" },
77
+ capability_id: { type: "string" },
78
+ status: { type: "string", enum: ["completed", "failed", "not_started"] },
79
+ result: {},
80
+ error: {},
81
+ browser: { type: "object" },
82
+ exit_status: { type: ["integer", "null"] },
83
+ duration_ms: { type: ["number", "null"] },
84
+ started_at: { type: ["string", "null"] },
85
+ completed_at: { type: ["string", "null"] },
86
+ },
87
+ },
88
+ },
89
+ ];
90
+
91
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
92
+ for await (const line of input) {
93
+ if (!line.trim()) continue;
94
+ let message;
95
+ try {
96
+ message = JSON.parse(line);
97
+ const response = await handle(message);
98
+ if (response) process.stdout.write(`${JSON.stringify(response)}\n`);
99
+ } catch (error) {
100
+ const id = message?.id ?? null;
101
+ process.stdout.write(`${JSON.stringify(jsonRpcError(id, -32603, error instanceof Error ? error.message : String(error)))}\n`);
102
+ }
103
+ }
104
+
105
+ async function handle(message) {
106
+ const { id, method, params = {} } = message;
107
+ if (method === "initialize") {
108
+ return { jsonrpc: "2.0", id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo } };
109
+ }
110
+ if (method === "notifications/initialized") return null;
111
+ if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools } };
112
+ if (method !== "tools/call") return jsonRpcError(id, -32601, `method not found: ${method}`);
113
+
114
+ const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
115
+ if (params.name === "jev_route") return route(args, id);
116
+ if (params.name === "jev_browser_step") return browserStep(args, id);
117
+ if (params.name === "jev_supervise") return supervise(args, id);
118
+ if (params.name === "jev_record_execution") return recordExecution(args, id);
119
+ return jsonRpcError(id, -32602, `unknown tool: ${params.name}`);
120
+ }
121
+
122
+ async function route(args, id) {
123
+ const { provider: requestedProvider, engine = "native", ...request } = args;
124
+ const provider = configuredProvider(config, requestedProvider);
125
+ const decision = await routeRequest(request, {
126
+ provider,
127
+ engine,
128
+ contextFilterMode: request.policy?.context_filter_mode ?? config.features?.context_filter,
129
+ });
130
+ return persistDecision(request, decision, id);
131
+ }
132
+
133
+ async function browserStep(args, id) {
134
+ const { provider: requestedProvider, enabled, ...input } = args;
135
+ const routed = await decideBrowserStep(input, {
136
+ enabled: enabled ?? config.features?.browser_fast_path,
137
+ provider: configuredProvider(config, requestedProvider),
138
+ });
139
+ routed.decision.browser_action = routed.action
140
+ ? {
141
+ id: routed.action.id,
142
+ operation: routed.action.operation,
143
+ target_id: routed.action.target_id ?? null,
144
+ option_id: routed.action.option_id ?? null,
145
+ consequential: routed.action.consequential,
146
+ }
147
+ : null;
148
+ return persistDecision(routed.request, routed.decision, id);
149
+ }
150
+
151
+ async function supervise(args, id) {
152
+ const { provider: requestedProvider, enabled, ...input } = args;
153
+ const result = await superviseWork({
154
+ ...input,
155
+ enabled: enabled ?? config.features?.supervision,
156
+ provider: configuredProvider(config, requestedProvider),
157
+ receiptPath: CASES_PATH,
158
+ });
159
+ return toolResult(id, result, false);
160
+ }
161
+
162
+ async function persistDecision(request, decision, id) {
163
+ pendingDecisions.set(decision.correlation_id, { request, decision });
164
+ try {
165
+ await appendRoutingCase({ path: CASES_PATH, request, decision });
166
+ decision.receipt = { ...decision.receipt, replay_case_id: decision.correlation_id, replay_persisted: true };
167
+ } catch (error) {
168
+ decision.receipt = {
169
+ ...decision.receipt,
170
+ replay_persisted: false,
171
+ replay_error: error instanceof Error ? error.message : String(error),
172
+ };
173
+ }
174
+ return toolResult(id, decision, decision.status === "error");
175
+ }
176
+
177
+ async function recordExecution(args, id) {
178
+ const correlationId = typeof args.correlation_id === "string" ? args.correlation_id : null;
179
+ const pending = correlationId ? pendingDecisions.get(correlationId) : null;
180
+ if (!pending) return jsonRpcError(id, -32004, `unknown Jev correlation_id: ${correlationId ?? "missing"}`);
181
+ const { request, decision } = pending;
182
+ let record;
183
+ try {
184
+ ({ record } = await appendExecutionReceipt({ path: CASES_PATH, request, decision, host: args }));
185
+ } catch (error) {
186
+ record = buildExecutionReceipt({ request, decision, host: args });
187
+ record.persistence = {
188
+ persisted: false,
189
+ error: error instanceof Error ? error.message : String(error),
190
+ };
191
+ }
192
+ pendingDecisions.delete(correlationId);
193
+ return toolResult(id, record, false);
194
+ }
195
+
196
+ function toolResult(id, value, isError) {
197
+ return {
198
+ jsonrpc: "2.0",
199
+ id,
200
+ result: {
201
+ content: [{ type: "text", text: JSON.stringify(value) }],
202
+ structuredContent: value,
203
+ isError,
204
+ },
205
+ };
206
+ }
207
+
208
+ function jsonRpcError(id, code, message) {
209
+ return { jsonrpc: "2.0", id, error: { code, message } };
210
+ }
@@ -0,0 +1,52 @@
1
+ import { stableJson } from "../contract.mjs";
2
+
3
+ const TOKEN_RE = /[\p{L}\p{N}_-]+/gu;
4
+
5
+ export class DemoProvider {
6
+ name = "jev-demo";
7
+
8
+ async decide({ state, candidates }) {
9
+ const requestTokens = tokens(`${state.intent} ${stableJson(state.context ?? {})}`);
10
+ const scored = candidates.map((candidate) => {
11
+ const candidateTokens = tokens(`${candidate.id} ${candidate.name} ${candidate.description}`);
12
+ const overlap = [...requestTokens].filter((token) => candidateTokens.has(token)).length;
13
+ return { id: candidate.id, score: overlap + 0.01 };
14
+ });
15
+ const total = scored.reduce((sum, item) => sum + item.score, 0);
16
+ const probabilities = Object.fromEntries(scored.map(({ id, score }) => [id, score / total]));
17
+ const choice = [...scored].sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))[0]?.id ?? "";
18
+ const top = choice ? probabilities[choice] : 0;
19
+ const confidence = scored.length <= 1
20
+ ? 1
21
+ : Math.max(0, Math.min(1, (top - 1 / scored.length) / Math.max(1 - 1 / scored.length, 0.0001)));
22
+ return {
23
+ model: "jev-demo",
24
+ answers: {
25
+ tool: {
26
+ type: "choice",
27
+ choice,
28
+ probabilities,
29
+ confidence,
30
+ },
31
+ },
32
+ usage: { input_bytes: Buffer.byteLength(stableJson(state), "utf8"), output_bytes: 0 },
33
+ };
34
+ }
35
+
36
+ async evaluate({ state, questions }) {
37
+ const supplied = state.context?.supervision?.judgments ?? {};
38
+ const answers = Object.fromEntries(Object.keys(questions).map((id) => {
39
+ const value = typeof supplied[id] === "number" ? Math.max(0, Math.min(1, supplied[id])) : 0.5;
40
+ return [id, { type: "noul", probability: value, confidence: 1 }];
41
+ }));
42
+ return {
43
+ model: "jev-demo",
44
+ answers,
45
+ usage: { input_bytes: Buffer.byteLength(stableJson(state), "utf8"), output_bytes: 0 },
46
+ };
47
+ }
48
+ }
49
+
50
+ function tokens(value) {
51
+ return new Set(value.toLowerCase().match(TOKEN_RE) ?? []);
52
+ }
@@ -0,0 +1,126 @@
1
+ import { stableJson } from "../contract.mjs";
2
+
3
+ export class TypeSafeProvider {
4
+ name = "typesafe";
5
+
6
+ constructor({ apiKey = process.env.TYPESAFE_API_KEY, endpoint = process.env.TYPESAFE_ENDPOINT ?? "https://api.typesafe.ai/v1/systemone", model = process.env.TYPESAFE_MODEL ?? "jev-latest", timeoutMs = 2_000 } = {}) {
7
+ this.apiKey = apiKey;
8
+ this.endpoint = endpoint;
9
+ this.model = model;
10
+ this.timeoutMs = timeoutMs;
11
+ }
12
+
13
+ async decide({ state, candidates }) {
14
+ const criteria = Object.fromEntries(candidates.map((candidate) => [candidate.id, {
15
+ name: candidate.name,
16
+ kind: candidate.kind,
17
+ description: candidate.description,
18
+ risk: candidate.risk,
19
+ }]));
20
+ return this.evaluate({
21
+ state,
22
+ questions: {
23
+ tool: {
24
+ type: "choice",
25
+ instructions: "Which single capability should handle this request? Choose only from the supplied options.",
26
+ criteria,
27
+ },
28
+ },
29
+ });
30
+ }
31
+
32
+ async evaluate({ state, questions }) {
33
+ if (!this.apiKey) throw new Error("TYPESAFE_API_KEY is not configured");
34
+ const response = await fetch(this.endpoint, {
35
+ method: "POST",
36
+ headers: {
37
+ Authorization: `Bearer ${this.apiKey}`,
38
+ "Content-Type": "application/json",
39
+ },
40
+ body: JSON.stringify({ state, model: this.model, questions }),
41
+ signal: AbortSignal.timeout(this.timeoutMs),
42
+ }).catch((error) => {
43
+ if (error?.name === "TimeoutError") throw new Error(`TypeSafe request timed out after ${this.timeoutMs}ms`);
44
+ throw error;
45
+ });
46
+
47
+ if (!response.ok) {
48
+ const body = await response.text();
49
+ throw new Error(`TypeSafe returned HTTP ${response.status}: ${body.slice(0, 240)}`);
50
+ }
51
+ const raw = await response.json();
52
+ if (!raw || typeof raw !== "object" || !raw.answers || typeof raw.answers !== "object") {
53
+ throw new Error("TypeSafe response has no answers object");
54
+ }
55
+ return raw;
56
+ }
57
+ }
58
+
59
+ export class OpenRouterDecisionsProvider {
60
+ name = "openrouter:typesafe/jev-1.13";
61
+
62
+ constructor({
63
+ apiKey = process.env.OPENROUTER_API_KEY,
64
+ endpoint = process.env.OPENROUTER_DECISIONS_ENDPOINT ?? "https://openrouter.ai/api/alpha/decisions",
65
+ model = process.env.OPENROUTER_DECISIONS_MODEL ?? "typesafe/jev-1.13",
66
+ timeoutMs = 5_000,
67
+ fetchImpl = globalThis.fetch,
68
+ httpReferer = process.env.OPENROUTER_HTTP_REFERER,
69
+ appTitle = process.env.OPENROUTER_APP_TITLE,
70
+ } = {}) {
71
+ this.apiKey = apiKey;
72
+ this.endpoint = endpoint;
73
+ this.model = model;
74
+ this.timeoutMs = timeoutMs;
75
+ this.fetchImpl = fetchImpl;
76
+ this.httpReferer = httpReferer;
77
+ }
78
+ async decide({ state, candidates }) {
79
+ const criteria = Object.fromEntries(candidates.map((candidate) => [candidate.id, {
80
+ name: candidate.name,
81
+ kind: candidate.kind,
82
+ description: candidate.description,
83
+ risk: candidate.risk,
84
+ }]));
85
+ return this.evaluate({
86
+ state,
87
+ questions: {
88
+ tool: {
89
+ type: "choice",
90
+ instructions: "Which single capability should handle this request? Choose only from the supplied options.",
91
+ criteria,
92
+ },
93
+ },
94
+ });
95
+ }
96
+
97
+ async evaluate({ state, questions }) {
98
+ if (!this.apiKey) throw new Error("OPENROUTER_API_KEY is not configured");
99
+ if (typeof this.fetchImpl !== "function") throw new Error("fetch is unavailable");
100
+
101
+ const response = await this.fetchImpl(this.endpoint, {
102
+ method: "POST",
103
+ headers: {
104
+ Authorization: `Bearer ${this.apiKey}`,
105
+ "Content-Type": "application/json",
106
+ ...(this.httpReferer ? { "HTTP-Referer": this.httpReferer } : {}),
107
+ ...(this.appTitle ? { "X-Title": this.appTitle } : {}),
108
+ },
109
+ body: JSON.stringify({ model: this.model, questions, state }),
110
+ signal: AbortSignal.timeout(this.timeoutMs),
111
+ }).catch((error) => {
112
+ if (error?.name === "TimeoutError") throw new Error(`OpenRouter Decisions request timed out after ${this.timeoutMs}ms`);
113
+ throw error;
114
+ });
115
+
116
+ if (!response.ok) {
117
+ const body = await response.text();
118
+ throw new Error(`OpenRouter Decisions returned HTTP ${response.status}: ${body.slice(0, 240)}`);
119
+ }
120
+ const raw = await response.json();
121
+ if (!raw || typeof raw !== "object" || !raw.answers || typeof raw.answers !== "object") {
122
+ throw new Error("OpenRouter Decisions response has no answers object");
123
+ }
124
+ return raw;
125
+ }
126
+ }