ai-runtime-engine 1.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 (269) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +15 -0
  3. package/README.md +489 -0
  4. package/dist/artifacts/artifacts.d.ts +28 -0
  5. package/dist/artifacts/artifacts.js +46 -0
  6. package/dist/benchmark/benchmark.d.ts +23 -0
  7. package/dist/benchmark/benchmark.js +40 -0
  8. package/dist/cli/cli.d.ts +6 -0
  9. package/dist/cli/cli.js +161 -0
  10. package/dist/cli/commands/cleanup.d.ts +47 -0
  11. package/dist/cli/commands/cleanup.js +96 -0
  12. package/dist/cli/commands/config.d.ts +8 -0
  13. package/dist/cli/commands/config.js +28 -0
  14. package/dist/cli/commands/doctor.d.ts +57 -0
  15. package/dist/cli/commands/doctor.js +86 -0
  16. package/dist/cli/commands/executions.d.ts +9 -0
  17. package/dist/cli/commands/executions.js +25 -0
  18. package/dist/cli/commands/info.d.ts +43 -0
  19. package/dist/cli/commands/info.js +53 -0
  20. package/dist/cli/commands/init.d.ts +5 -0
  21. package/dist/cli/commands/init.js +75 -0
  22. package/dist/cli/commands/inspect.d.ts +16 -0
  23. package/dist/cli/commands/inspect.js +60 -0
  24. package/dist/cli/commands/phase2.d.ts +22 -0
  25. package/dist/cli/commands/phase2.js +83 -0
  26. package/dist/cli/commands/route.d.ts +14 -0
  27. package/dist/cli/commands/route.js +49 -0
  28. package/dist/cli/commands/run.d.ts +11 -0
  29. package/dist/cli/commands/run.js +37 -0
  30. package/dist/cli/commands/setup.d.ts +34 -0
  31. package/dist/cli/commands/setup.js +104 -0
  32. package/dist/cli/commands/skills.d.ts +28 -0
  33. package/dist/cli/commands/skills.js +48 -0
  34. package/dist/cli/commands/test.d.ts +7 -0
  35. package/dist/cli/commands/test.js +29 -0
  36. package/dist/cli/context.d.ts +12 -0
  37. package/dist/cli/context.js +16 -0
  38. package/dist/cli/interactive/repl.d.ts +6 -0
  39. package/dist/cli/interactive/repl.js +45 -0
  40. package/dist/cli/interactive/session.d.ts +36 -0
  41. package/dist/cli/interactive/session.js +356 -0
  42. package/dist/cli/prompt.d.ts +6 -0
  43. package/dist/cli/prompt.js +18 -0
  44. package/dist/cli/render.d.ts +7 -0
  45. package/dist/cli/render.js +14 -0
  46. package/dist/comparison/analysis.d.ts +46 -0
  47. package/dist/comparison/analysis.js +177 -0
  48. package/dist/comparison/comparator.d.ts +46 -0
  49. package/dist/comparison/comparator.js +270 -0
  50. package/dist/comparison/comparison.d.ts +140 -0
  51. package/dist/comparison/comparison.js +9 -0
  52. package/dist/comparison/render.d.ts +7 -0
  53. package/dist/comparison/render.js +66 -0
  54. package/dist/config/defaults.d.ts +52 -0
  55. package/dist/config/defaults.js +56 -0
  56. package/dist/config/load.d.ts +17 -0
  57. package/dist/config/load.js +50 -0
  58. package/dist/config/providerDefaults.d.ts +17 -0
  59. package/dist/config/providerDefaults.js +61 -0
  60. package/dist/config/schema.d.ts +9 -0
  61. package/dist/config/schema.js +78 -0
  62. package/dist/context/budget.d.ts +13 -0
  63. package/dist/context/budget.js +17 -0
  64. package/dist/context/compiler.d.ts +61 -0
  65. package/dist/context/compiler.js +125 -0
  66. package/dist/context/tokens.d.ts +19 -0
  67. package/dist/context/tokens.js +38 -0
  68. package/dist/conversations/conversations.d.ts +38 -0
  69. package/dist/conversations/conversations.js +64 -0
  70. package/dist/core/capabilities/evidence.d.ts +40 -0
  71. package/dist/core/capabilities/evidence.js +102 -0
  72. package/dist/core/capabilities/overlay.d.ts +15 -0
  73. package/dist/core/capabilities/overlay.js +0 -0
  74. package/dist/core/capabilities/taxonomy.d.ts +19 -0
  75. package/dist/core/capabilities/taxonomy.js +25 -0
  76. package/dist/core/fallback/errors.d.ts +30 -0
  77. package/dist/core/fallback/errors.js +80 -0
  78. package/dist/core/fallback/fallback.d.ts +40 -0
  79. package/dist/core/fallback/fallback.js +82 -0
  80. package/dist/core/fallback/retryPolicy.d.ts +11 -0
  81. package/dist/core/fallback/retryPolicy.js +14 -0
  82. package/dist/core/health/health.d.ts +3 -0
  83. package/dist/core/health/health.js +5 -0
  84. package/dist/core/health/monitor.d.ts +23 -0
  85. package/dist/core/health/monitor.js +82 -0
  86. package/dist/core/policies/budget.d.ts +19 -0
  87. package/dist/core/policies/budget.js +37 -0
  88. package/dist/core/registry/builtinTasks.d.ts +8 -0
  89. package/dist/core/registry/builtinTasks.js +54 -0
  90. package/dist/core/registry/registry.d.ts +18 -0
  91. package/dist/core/registry/registry.js +33 -0
  92. package/dist/core/registry/taskRegistry.d.ts +15 -0
  93. package/dist/core/registry/taskRegistry.js +30 -0
  94. package/dist/core/router/confidence.d.ts +7 -0
  95. package/dist/core/router/confidence.js +20 -0
  96. package/dist/core/router/dimensions.d.ts +16 -0
  97. package/dist/core/router/dimensions.js +60 -0
  98. package/dist/core/router/executor.d.ts +16 -0
  99. package/dist/core/router/executor.js +25 -0
  100. package/dist/core/router/filter.d.ts +34 -0
  101. package/dist/core/router/filter.js +113 -0
  102. package/dist/core/router/normalize.d.ts +30 -0
  103. package/dist/core/router/normalize.js +119 -0
  104. package/dist/core/router/request.d.ts +4 -0
  105. package/dist/core/router/request.js +21 -0
  106. package/dist/core/router/router.d.ts +32 -0
  107. package/dist/core/router/router.js +195 -0
  108. package/dist/core/router/routingPrefs.d.ts +11 -0
  109. package/dist/core/router/routingPrefs.js +30 -0
  110. package/dist/core/router/scorer.d.ts +19 -0
  111. package/dist/core/router/scorer.js +50 -0
  112. package/dist/core/router/weights.d.ts +9 -0
  113. package/dist/core/router/weights.js +31 -0
  114. package/dist/core/validation/validator.d.ts +16 -0
  115. package/dist/core/validation/validator.js +33 -0
  116. package/dist/discovery/modelCatalog.d.ts +28 -0
  117. package/dist/discovery/modelCatalog.js +105 -0
  118. package/dist/discovery/openapi.d.ts +25 -0
  119. package/dist/discovery/openapi.js +76 -0
  120. package/dist/executions/checkpoint.d.ts +26 -0
  121. package/dist/executions/checkpoint.js +114 -0
  122. package/dist/executions/execution.d.ts +51 -0
  123. package/dist/executions/execution.js +8 -0
  124. package/dist/executions/store.d.ts +52 -0
  125. package/dist/executions/store.js +124 -0
  126. package/dist/generation/generateAdapter.d.ts +17 -0
  127. package/dist/generation/generateAdapter.js +30 -0
  128. package/dist/index.d.ts +147 -0
  129. package/dist/index.js +107 -0
  130. package/dist/learning/feedback.d.ts +9 -0
  131. package/dist/learning/feedback.js +18 -0
  132. package/dist/learning/learningStore.d.ts +68 -0
  133. package/dist/learning/learningStore.js +138 -0
  134. package/dist/learning/performanceStore.d.ts +27 -0
  135. package/dist/learning/performanceStore.js +0 -0
  136. package/dist/marketplace/presets.d.ts +24 -0
  137. package/dist/marketplace/presets.js +52 -0
  138. package/dist/mcp/mcp.d.ts +31 -0
  139. package/dist/mcp/mcp.js +54 -0
  140. package/dist/memory/bm25.d.ts +16 -0
  141. package/dist/memory/bm25.js +56 -0
  142. package/dist/memory/classifier.d.ts +14 -0
  143. package/dist/memory/classifier.js +17 -0
  144. package/dist/memory/memory.d.ts +80 -0
  145. package/dist/memory/memory.js +191 -0
  146. package/dist/orchestration/executor.d.ts +35 -0
  147. package/dist/orchestration/executor.js +65 -0
  148. package/dist/orchestration/orchestrator.d.ts +42 -0
  149. package/dist/orchestration/orchestrator.js +63 -0
  150. package/dist/orchestration/plan.d.ts +37 -0
  151. package/dist/orchestration/plan.js +70 -0
  152. package/dist/orchestration/planner.d.ts +29 -0
  153. package/dist/orchestration/planner.js +69 -0
  154. package/dist/plugin/ai.d.ts +82 -0
  155. package/dist/plugin/ai.js +167 -0
  156. package/dist/probing/probe.d.ts +25 -0
  157. package/dist/probing/probe.js +63 -0
  158. package/dist/providers/factory.d.ts +18 -0
  159. package/dist/providers/factory.js +54 -0
  160. package/dist/providers/httpClient.d.ts +34 -0
  161. package/dist/providers/httpClient.js +80 -0
  162. package/dist/providers/httpProvider.d.ts +49 -0
  163. package/dist/providers/httpProvider.js +135 -0
  164. package/dist/providers/mock/demo.d.ts +13 -0
  165. package/dist/providers/mock/demo.js +58 -0
  166. package/dist/providers/mock/mockProvider.d.ts +35 -0
  167. package/dist/providers/mock/mockProvider.js +121 -0
  168. package/dist/providers/mock/scenarios.d.ts +44 -0
  169. package/dist/providers/mock/scenarios.js +30 -0
  170. package/dist/providers/provider.d.ts +26 -0
  171. package/dist/providers/provider.js +11 -0
  172. package/dist/providers/wire/anthropicWire.d.ts +6 -0
  173. package/dist/providers/wire/anthropicWire.js +83 -0
  174. package/dist/providers/wire/openaiWire.d.ts +7 -0
  175. package/dist/providers/wire/openaiWire.js +81 -0
  176. package/dist/providers/wire/registry.d.ts +8 -0
  177. package/dist/providers/wire/registry.js +20 -0
  178. package/dist/providers/wire/types.d.ts +39 -0
  179. package/dist/providers/wire/types.js +24 -0
  180. package/dist/runtime/config.d.ts +31 -0
  181. package/dist/runtime/config.js +121 -0
  182. package/dist/runtime/context.d.ts +34 -0
  183. package/dist/runtime/context.js +11 -0
  184. package/dist/runtime/events.d.ts +99 -0
  185. package/dist/runtime/events.js +82 -0
  186. package/dist/runtime/host.d.ts +27 -0
  187. package/dist/runtime/host.js +7 -0
  188. package/dist/runtime/intent/classifier.d.ts +30 -0
  189. package/dist/runtime/intent/classifier.js +60 -0
  190. package/dist/runtime/intent/signals.d.ts +19 -0
  191. package/dist/runtime/intent/signals.js +46 -0
  192. package/dist/runtime/modes/availability.d.ts +11 -0
  193. package/dist/runtime/modes/availability.js +17 -0
  194. package/dist/runtime/modes/chat.d.ts +18 -0
  195. package/dist/runtime/modes/chat.js +67 -0
  196. package/dist/runtime/modes/modeResolver.d.ts +43 -0
  197. package/dist/runtime/modes/modeResolver.js +78 -0
  198. package/dist/runtime/policy.d.ts +72 -0
  199. package/dist/runtime/policy.js +59 -0
  200. package/dist/runtime/providerView.d.ts +62 -0
  201. package/dist/runtime/providerView.js +105 -0
  202. package/dist/runtime/routing.d.ts +26 -0
  203. package/dist/runtime/routing.js +65 -0
  204. package/dist/runtime/runtime.d.ts +191 -0
  205. package/dist/runtime/runtime.js +718 -0
  206. package/dist/runtime/types.d.ts +153 -0
  207. package/dist/runtime/types.js +9 -0
  208. package/dist/runtime/workspace/detectors.d.ts +15 -0
  209. package/dist/runtime/workspace/detectors.js +57 -0
  210. package/dist/runtime/workspace/workspace.d.ts +29 -0
  211. package/dist/runtime/workspace/workspace.js +116 -0
  212. package/dist/security/credentials.d.ts +26 -0
  213. package/dist/security/credentials.js +34 -0
  214. package/dist/security/redact.d.ts +16 -0
  215. package/dist/security/redact.js +57 -0
  216. package/dist/skills/builtins/fileAnalyzer.d.ts +7 -0
  217. package/dist/skills/builtins/fileAnalyzer.js +47 -0
  218. package/dist/skills/builtins/repositoryAnalyzer.d.ts +6 -0
  219. package/dist/skills/builtins/repositoryAnalyzer.js +47 -0
  220. package/dist/skills/discovery.d.ts +61 -0
  221. package/dist/skills/discovery.js +211 -0
  222. package/dist/skills/manifest.d.ts +30 -0
  223. package/dist/skills/manifest.js +75 -0
  224. package/dist/skills/registry.d.ts +15 -0
  225. package/dist/skills/registry.js +22 -0
  226. package/dist/skills/skill.d.ts +64 -0
  227. package/dist/skills/skill.js +8 -0
  228. package/dist/store/area.d.ts +54 -0
  229. package/dist/store/area.js +164 -0
  230. package/dist/store/paths.d.ts +15 -0
  231. package/dist/store/paths.js +48 -0
  232. package/dist/store/store.d.ts +59 -0
  233. package/dist/store/store.js +140 -0
  234. package/dist/telemetry/sinks/file.d.ts +12 -0
  235. package/dist/telemetry/sinks/file.js +28 -0
  236. package/dist/telemetry/telemetry.d.ts +36 -0
  237. package/dist/telemetry/telemetry.js +63 -0
  238. package/dist/tools/builtins/filesystem.d.ts +7 -0
  239. package/dist/tools/builtins/filesystem.js +53 -0
  240. package/dist/tools/builtins/git.d.ts +10 -0
  241. package/dist/tools/builtins/git.js +66 -0
  242. package/dist/tools/builtins/shell.d.ts +17 -0
  243. package/dist/tools/builtins/shell.js +91 -0
  244. package/dist/tools/jail.d.ts +12 -0
  245. package/dist/tools/jail.js +98 -0
  246. package/dist/tools/permissions.d.ts +25 -0
  247. package/dist/tools/permissions.js +24 -0
  248. package/dist/tools/registry.d.ts +10 -0
  249. package/dist/tools/registry.js +20 -0
  250. package/dist/tools/runner.d.ts +23 -0
  251. package/dist/tools/runner.js +64 -0
  252. package/dist/tools/tool.d.ts +53 -0
  253. package/dist/tools/tool.js +24 -0
  254. package/dist/tools/untrusted.d.ts +13 -0
  255. package/dist/tools/untrusted.js +30 -0
  256. package/dist/types.d.ts +460 -0
  257. package/dist/types.js +12 -0
  258. package/dist/util/clock.d.ts +6 -0
  259. package/dist/util/clock.js +4 -0
  260. package/dist/util/extractJson.d.ts +8 -0
  261. package/dist/util/extractJson.js +54 -0
  262. package/dist/verification/verify.d.ts +26 -0
  263. package/dist/verification/verify.js +67 -0
  264. package/docs/GUIDE.md +358 -0
  265. package/docs/README.md +21 -0
  266. package/docs/architecture.md +78 -0
  267. package/docs/router.md +376 -0
  268. package/docs/security.md +55 -0
  269. package/package.json +67 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Phase 1 — task normalization. Turns a public `RunRequest` + the task registry into a
3
+ * `NormalizedTask` (required/preferred capabilities, strategy, sensitivity) plus a provider-agnostic
4
+ * request template that the executor completes with a chosen model. An UNKNOWN task never fails: its
5
+ * requirements are inferred from the request's own hints and its confidence is stamped low.
6
+ */
7
+ function toInput(input) {
8
+ if (typeof input === 'string')
9
+ return { text: input };
10
+ return input;
11
+ }
12
+ function requirementsFrom(reqs, minEvidence) {
13
+ if (!reqs)
14
+ return [];
15
+ const out = [];
16
+ const groups = ['input', 'output', 'intelligence', 'agent'];
17
+ for (const group of groups) {
18
+ for (const key of reqs[group] ?? []) {
19
+ out.push(minEvidence ? { group, key, minEvidence } : { group, key });
20
+ }
21
+ }
22
+ return out;
23
+ }
24
+ /** Infer a minimal, lenient (`inferred`-floor) requirement set for an unknown task from request hints. */
25
+ function inferRequirements(req, parts) {
26
+ const out = [{ group: 'intelligence', key: 'reasoning', minEvidence: 'inferred' }];
27
+ const fmt = req.output?.format;
28
+ if (fmt === 'code')
29
+ out.push({ group: 'intelligence', key: 'code_generation', minEvidence: 'inferred' }, { group: 'output', key: 'code', minEvidence: 'inferred' });
30
+ if (fmt === 'json' || fmt === 'structured_output')
31
+ out.push({ group: 'output', key: fmt, minEvidence: 'inferred' });
32
+ if (parts?.some((p) => p.kind === 'image'))
33
+ out.push({ group: 'input', key: 'image', minEvidence: 'inferred' });
34
+ if (parts?.some((p) => p.kind === 'file' || p.kind === 'pdf'))
35
+ out.push({ group: 'input', key: 'file', minEvidence: 'inferred' });
36
+ if (req.tools && req.tools.length > 0)
37
+ out.push({ group: 'agent', key: 'tool_calling', minEvidence: 'inferred' });
38
+ return out;
39
+ }
40
+ /** Merge requirement lists, de-duplicating by (group,key) and keeping the stricter evidence floor. */
41
+ function mergeRequirements(...lists) {
42
+ const byKey = new Map();
43
+ for (const list of lists) {
44
+ for (const r of list) {
45
+ const k = `${r.group}:${r.key}`;
46
+ const existing = byKey.get(k);
47
+ if (!existing)
48
+ byKey.set(k, r);
49
+ else {
50
+ // keep the stricter floor
51
+ const rankMap = { unsupported: 0, unknown: 1, inferred: 2, documented: 3, verified: 4 };
52
+ const a = rankMap[existing.minEvidence ?? 'documented'];
53
+ const b = rankMap[r.minEvidence ?? 'documented'];
54
+ if (b > a)
55
+ byKey.set(k, r);
56
+ }
57
+ }
58
+ }
59
+ return [...byKey.values()];
60
+ }
61
+ export function normalize(req, tasks, config) {
62
+ const def = tasks.lookup(req.task);
63
+ const input = toInput(req.input);
64
+ const sensitivity = req.sensitivity ?? config.privacy.defaultSensitivity;
65
+ const explicitReqs = requirementsFrom(req.requirements);
66
+ let required;
67
+ let preferred;
68
+ let minContextWindow;
69
+ let output = req.output ?? def?.output;
70
+ let strategy;
71
+ let requireLocal = false;
72
+ let confidence;
73
+ if (def) {
74
+ required = mergeRequirements(def.required, explicitReqs);
75
+ preferred = def.preferred ?? [];
76
+ minContextWindow = req.requirements?.minContextWindow ?? def.minContextWindow;
77
+ strategy = req.strategy ?? def.defaultStrategy ?? config.strategy;
78
+ requireLocal = def.privacy?.requireLocal ?? false;
79
+ confidence = 1;
80
+ }
81
+ else {
82
+ // Unknown task: infer from hints, keep it lenient, and lower confidence.
83
+ required = mergeRequirements(inferRequirements(req, input.parts), explicitReqs);
84
+ preferred = [];
85
+ minContextWindow = req.requirements?.minContextWindow;
86
+ strategy = req.strategy ?? config.strategy;
87
+ confidence = 0.5;
88
+ }
89
+ if (req.provider)
90
+ strategy = 'provider-specific';
91
+ const template = {
92
+ taskId: def?.id ?? req.task,
93
+ input,
94
+ timeoutMs: config.timeoutMs,
95
+ sensitivity,
96
+ };
97
+ if (req.system !== undefined)
98
+ template.system = req.system;
99
+ if (output !== undefined)
100
+ template.output = output;
101
+ if (req.tools !== undefined)
102
+ template.tools = req.tools;
103
+ if (req.params !== undefined)
104
+ template.params = req.params;
105
+ const task = {
106
+ id: def?.id ?? req.task,
107
+ required,
108
+ preferred,
109
+ strategy,
110
+ sensitivity,
111
+ requireLocal,
112
+ confidence,
113
+ };
114
+ if (minContextWindow !== undefined)
115
+ task.minContextWindow = minContextWindow;
116
+ if (output !== undefined)
117
+ task.output = output;
118
+ return { task, template };
119
+ }
@@ -0,0 +1,4 @@
1
+ /** Complete a request template into a concrete AIRequest for one model. */
2
+ import type { AIRequest } from '../../types.js';
3
+ import type { RequestTemplate } from './normalize.js';
4
+ export declare function buildRequest(template: RequestTemplate, model: string, signal?: AbortSignal): AIRequest;
@@ -0,0 +1,21 @@
1
+ /** Complete a request template into a concrete AIRequest for one model. */
2
+ export function buildRequest(template, model, signal) {
3
+ const req = {
4
+ taskId: template.taskId,
5
+ model,
6
+ input: template.input,
7
+ timeoutMs: template.timeoutMs,
8
+ sensitivity: template.sensitivity,
9
+ };
10
+ if (template.system !== undefined)
11
+ req.system = template.system;
12
+ if (template.output !== undefined)
13
+ req.output = template.output;
14
+ if (template.tools !== undefined)
15
+ req.tools = template.tools;
16
+ if (template.params !== undefined)
17
+ req.params = template.params;
18
+ if (signal !== undefined)
19
+ req.signal = signal;
20
+ return req;
21
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The router — the vendor-agnostic 7-phase pipeline. It holds only a provider registry, a task
3
+ * registry, resolved config, and a telemetry sink; it reaches providers exclusively through the
4
+ * `AIProvider` contract. No vendor name appears here.
5
+ *
6
+ * 1 normalize → 2 filter → 3 score → 4 select → 5 execute → 6 (validate: later stage) → 7 fallback
7
+ */
8
+ import type { ProviderRegistry } from '../registry/registry.js';
9
+ import type { TaskRegistry } from '../registry/taskRegistry.js';
10
+ import type { ResolvedConfig } from '../../config/defaults.js';
11
+ import type { TelemetrySink } from '../../telemetry/telemetry.js';
12
+ import type { HealthMonitor } from '../health/monitor.js';
13
+ import type { PerformanceStore } from '../../learning/performanceStore.js';
14
+ import type { CapabilityOverlay } from '../capabilities/overlay.js';
15
+ import type { Clock } from '../../util/clock.js';
16
+ import type { RunRequest, RunResult } from '../../types.js';
17
+ export interface RouterDeps {
18
+ registry: ProviderRegistry;
19
+ tasks: TaskRegistry;
20
+ config: ResolvedConfig;
21
+ telemetry: TelemetrySink;
22
+ health?: HealthMonitor;
23
+ performance?: PerformanceStore;
24
+ overlay?: CapabilityOverlay;
25
+ clock?: Clock;
26
+ }
27
+ export declare class Router {
28
+ private readonly deps;
29
+ private readonly clock;
30
+ constructor(deps: RouterDeps);
31
+ run(req: RunRequest): Promise<RunResult>;
32
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * The router — the vendor-agnostic 7-phase pipeline. It holds only a provider registry, a task
3
+ * registry, resolved config, and a telemetry sink; it reaches providers exclusively through the
4
+ * `AIProvider` contract. No vendor name appears here.
5
+ *
6
+ * 1 normalize → 2 filter → 3 score → 4 select → 5 execute → 6 (validate: later stage) → 7 fallback
7
+ */
8
+ import { mergeProfiles } from '../capabilities/evidence.js';
9
+ import { systemClock } from '../../util/clock.js';
10
+ import { AIError } from '../fallback/errors.js';
11
+ import { normalize } from './normalize.js';
12
+ import { filterCandidates } from './filter.js';
13
+ import { scoreCandidates } from './scorer.js';
14
+ import { runWithFallback } from '../fallback/fallback.js';
15
+ import { deriveConfidence } from './confidence.js';
16
+ import { validate as validateResponse } from '../validation/validator.js';
17
+ import { BudgetTracker } from '../policies/budget.js';
18
+ import { buildRequest } from './request.js';
19
+ import { runVerification } from '../../verification/verify.js';
20
+ export class Router {
21
+ deps;
22
+ clock;
23
+ constructor(deps) {
24
+ this.deps = deps;
25
+ this.clock = deps.clock ?? systemClock;
26
+ }
27
+ async run(req) {
28
+ const runStarted = this.clock.now();
29
+ const { registry, tasks, config, telemetry } = this.deps;
30
+ const getProvider = (id) => registry.get(id);
31
+ // Phase 1 — normalize
32
+ const { task, template } = normalize(req, tasks, config);
33
+ const pin = { provider: req.provider, model: req.model };
34
+ // Team policy (org-level guardrails) merged over per-run constraints.
35
+ const policy = config.policy;
36
+ if (policy.requireLocal)
37
+ task.requireLocal = true;
38
+ if (policy.strategy && !req.provider && req.strategy === undefined)
39
+ task.strategy = policy.strategy;
40
+ const effectiveConstraints = {
41
+ ...req.constraints,
42
+ denyProviders: [...(req.constraints?.denyProviders ?? []), ...(policy.denyProviders ?? [])],
43
+ ...(policy.allowProviders ? { allowProviders: intersectAllow(req.constraints?.allowProviders, policy.allowProviders) } : req.constraints?.allowProviders ? { allowProviders: req.constraints.allowProviders } : {}),
44
+ ...(minDefined(req.constraints?.maxCostUsd, policy.maxCostUsd) !== undefined ? { maxCostUsd: minDefined(req.constraints?.maxCostUsd, policy.maxCostUsd) } : {}),
45
+ };
46
+ // Gather candidates from enabled providers (provider-owned model + capability resolution)
47
+ const candidates = [];
48
+ for (const provider of registry.enabled()) {
49
+ const models = await provider.listModels();
50
+ for (const model of models) {
51
+ if (!model.enabled)
52
+ continue;
53
+ const overlayProfile = this.deps.overlay?.get(provider.id, model.id);
54
+ const enriched = overlayProfile ? { ...model, capabilities: mergeProfiles(model.capabilities, overlayProfile) } : model;
55
+ candidates.push({ providerId: provider.id, model: enriched });
56
+ }
57
+ }
58
+ const preFilter = [];
59
+ if (pin.provider && !candidates.some((c) => c.providerId === pin.provider)) {
60
+ preFilter.push({ providerId: pin.provider, model: pin.model ?? '*', reason: 'pinned provider not configured or not enabled' });
61
+ }
62
+ // Phase 2 — filter
63
+ const { eligible, filteredOut } = await filterCandidates({
64
+ candidates,
65
+ task,
66
+ config,
67
+ template,
68
+ getProvider,
69
+ pin,
70
+ ...(this.deps.health ? { health: this.deps.health } : {}),
71
+ constraints: effectiveConstraints,
72
+ ...(req.routing ? { routing: req.routing } : {}),
73
+ });
74
+ // Phase 3 — score (blends learned performance when available)
75
+ const history = this.deps.performance
76
+ ? (pid, mid) => this.deps.performance.forTask(task.id, pid, mid)
77
+ : undefined;
78
+ const scored = scoreCandidates(eligible, task, config.weights, task.strategy, { ...(history ? { history } : {}), ...(req.routing ? { prefer: req.routing } : {}) });
79
+ const ranked = scored.map((s) => ({
80
+ providerId: s.candidate.providerId,
81
+ model: s.candidate.model.id,
82
+ score: s.score,
83
+ breakdown: s.breakdown,
84
+ }));
85
+ const baseReport = {
86
+ taskId: task.id,
87
+ strategy: task.strategy,
88
+ weights: config.weights,
89
+ requiredCapabilities: task.required,
90
+ consideredCount: candidates.length,
91
+ filteredOut: [...preFilter, ...filteredOut],
92
+ ranked,
93
+ attempts: [],
94
+ };
95
+ // No eligible candidate — actionable NO_CAPABLE_PROVIDER
96
+ if (scored.length === 0) {
97
+ const error = new AIError(describeNoCapable(task.id, baseReport.filteredOut), {
98
+ category: 'NO_CAPABLE_PROVIDER',
99
+ retryable: false,
100
+ });
101
+ telemetry.emit({ type: 'route.decision', ts: this.clock.now(), taskId: task.id, strategy: task.strategy, considered: candidates.length });
102
+ const result = { ok: false, error: error.toInfo(), confidence: 0, routing: baseReport };
103
+ telemetry.emit({ type: 'route.result', ts: this.clock.now(), taskId: task.id, ok: false, confidence: 0, totalLatencyMs: this.clock.now() - runStarted, fallbackCount: 0 });
104
+ return result;
105
+ }
106
+ telemetry.emit({
107
+ type: 'route.decision',
108
+ ts: this.clock.now(),
109
+ taskId: task.id,
110
+ strategy: task.strategy,
111
+ considered: candidates.length,
112
+ selected: { providerId: scored[0].candidate.providerId, model: scored[0].candidate.model.id },
113
+ topScore: scored[0].score,
114
+ });
115
+ // Phases 4/5/7 — select top, execute, fall back
116
+ const onAttempt = (record) => {
117
+ this.deps.health?.recordOutcome(record.providerId, record.outcome === 'success', record.category);
118
+ this.deps.performance?.record(task.id, record.providerId, record.model, record.outcome === 'success', record.latencyMs);
119
+ telemetry.emit({ type: 'route.attempt', ts: this.clock.now(), taskId: task.id, providerId: record.providerId, model: record.model, outcome: record.outcome, ...(record.category !== undefined ? { category: record.category } : {}), latencyMs: record.latencyMs });
120
+ };
121
+ // Budget guardrail (per-run override merged over config)
122
+ const maxCalls = req.budget?.maxCalls ?? config.budget.maxCalls;
123
+ const maxCostUsd = req.budget?.maxCostUsd ?? config.budget.maxCostUsd;
124
+ const budget = maxCalls !== undefined || maxCostUsd !== undefined ? new BudgetTracker(maxCalls, maxCostUsd) : undefined;
125
+ const costOf = async (pid, mid) => {
126
+ const p = getProvider(pid);
127
+ if (!p)
128
+ return 0;
129
+ const est = await p.estimate(buildRequest(template, mid));
130
+ return est.estCost?.amount ?? 0;
131
+ };
132
+ const fb = await runWithFallback({
133
+ ranked: scored,
134
+ template,
135
+ getProvider,
136
+ maxFallbacks: config.maxFallbacks,
137
+ clock: this.clock,
138
+ onAttempt,
139
+ validate: (response) => validateResponse({ response, ...(template.output ? { output: template.output } : {}), ...(template.tools ? { tools: template.tools } : {}) }),
140
+ ...(budget ? { budget, costOf } : {}),
141
+ });
142
+ baseReport.attempts = fb.attempts;
143
+ const fallbackCount = Math.max(0, fb.attempts.length - 1);
144
+ if (!fb.ok) {
145
+ telemetry.emit({ type: 'route.result', ts: this.clock.now(), taskId: task.id, ok: false, confidence: 0, totalLatencyMs: this.clock.now() - runStarted, fallbackCount });
146
+ return { ok: false, error: fb.error.toInfo(), confidence: 0, routing: baseReport };
147
+ }
148
+ baseReport.selected = { providerId: fb.used.candidate.providerId, model: fb.used.candidate.model.id };
149
+ if (fb.validation)
150
+ baseReport.validation = fb.validation;
151
+ let confidence = deriveConfidence(fb.used.score, fb.used.candidate.model, task);
152
+ // Phase-2 multi-model verification (opt-in): a second model judges the primary answer.
153
+ const wantVerify = req.verification ?? config.verification.enabled;
154
+ if (wantVerify) {
155
+ const verification = await runVerification({
156
+ task,
157
+ template,
158
+ primary: { providerId: fb.used.candidate.providerId, model: fb.used.candidate.model.id, response: fb.response },
159
+ ranked: scored,
160
+ getProvider,
161
+ ...(budget ? { budget, costOf } : {}),
162
+ });
163
+ if (verification) {
164
+ baseReport.verification = verification;
165
+ // A disagreeing verifier lowers confidence; agreement modestly raises it.
166
+ if (verification.agreement === 'disagree')
167
+ confidence = Math.min(confidence, config.minConfidence * 0.9);
168
+ else if (verification.agreement === 'agree')
169
+ confidence = Math.min(1, confidence + 0.05);
170
+ }
171
+ }
172
+ baseReport.belowConfidenceThreshold = confidence < config.minConfidence;
173
+ telemetry.emit({ type: 'route.result', ts: this.clock.now(), taskId: task.id, ok: true, confidence, totalLatencyMs: this.clock.now() - runStarted, fallbackCount });
174
+ return { ok: true, response: fb.response, confidence, routing: baseReport };
175
+ }
176
+ }
177
+ /** Intersect a per-run allow list with a policy allow list (both must permit). */
178
+ function intersectAllow(runAllow, policyAllow) {
179
+ if (!runAllow)
180
+ return policyAllow;
181
+ return runAllow.filter((id) => policyAllow.includes(id));
182
+ }
183
+ function minDefined(a, b) {
184
+ if (a === undefined)
185
+ return b;
186
+ if (b === undefined)
187
+ return a;
188
+ return Math.min(a, b);
189
+ }
190
+ function describeNoCapable(taskId, filteredOut) {
191
+ if (filteredOut.length === 0)
192
+ return `No provider is configured for task '${taskId}'.`;
193
+ const lines = filteredOut.map((f) => ` - ${f.providerId}${f.model && f.model !== '*' ? `/${f.model}` : ''}: ${f.reason}`);
194
+ return `No compatible AI provider is available for task '${taskId}'.\nExcluded:\n${lines.join('\n')}`;
195
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Shared matchers for user exclude/prefer routing (Phase 9). EXCLUDE is consumed by the filter (a hard
3
+ * gate); PREFER by the scorer (a soft nudge). A model entry is either `provider:model` (exact) or a bare
4
+ * `model` id (matches that model under any provider). Kept tiny and pure so both the filter and scorer
5
+ * apply identical matching semantics.
6
+ */
7
+ import type { RoutingPreferences } from '../../types.js';
8
+ /** True when routing hard-excludes this (provider, model) — by provider id or a model entry. */
9
+ export declare function isExcluded(routing: RoutingPreferences | undefined, providerId: string, modelId: string): boolean;
10
+ /** True when routing soft-prefers this (provider, model) — by provider id or a model entry. */
11
+ export declare function isPreferred(routing: RoutingPreferences | undefined, providerId: string, modelId: string): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared matchers for user exclude/prefer routing (Phase 9). EXCLUDE is consumed by the filter (a hard
3
+ * gate); PREFER by the scorer (a soft nudge). A model entry is either `provider:model` (exact) or a bare
4
+ * `model` id (matches that model under any provider). Kept tiny and pure so both the filter and scorer
5
+ * apply identical matching semantics.
6
+ */
7
+ function modelEntryMatches(entry, providerId, modelId) {
8
+ // Bare whole-string match FIRST, so a model id that itself contains a colon (e.g. a Bedrock
9
+ // "…-v1:0" or an OpenRouter "…:free") is still matched. Otherwise fall back to `provider:model`.
10
+ if (entry === modelId)
11
+ return true;
12
+ const i = entry.indexOf(':');
13
+ return i > 0 && entry.slice(0, i) === providerId && entry.slice(i + 1) === modelId;
14
+ }
15
+ /** True when routing hard-excludes this (provider, model) — by provider id or a model entry. */
16
+ export function isExcluded(routing, providerId, modelId) {
17
+ if (!routing)
18
+ return false;
19
+ if (routing.excludeProviders?.includes(providerId))
20
+ return true;
21
+ return (routing.excludeModels ?? []).some((e) => modelEntryMatches(e, providerId, modelId));
22
+ }
23
+ /** True when routing soft-prefers this (provider, model) — by provider id or a model entry. */
24
+ export function isPreferred(routing, providerId, modelId) {
25
+ if (!routing)
26
+ return false;
27
+ if (routing.preferProviders?.includes(providerId))
28
+ return true;
29
+ return (routing.preferModels ?? []).some((e) => modelEntryMatches(e, providerId, modelId));
30
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Phase 3 — scoring. Combines the per-dimension scores with strategy-adjusted weights into a single
3
+ * 0..1 score per candidate, normalized by the weight sum so weights need not total 100. Deterministic
4
+ * ordering: score desc, then quality, then latency, then providerId/model for stability.
5
+ */
6
+ import type { Candidate, NormalizedTask, RoutingPreferences, ScoreBreakdown, ScoreWeights, Strategy } from '../../types.js';
7
+ import type { HistoryLookup } from '../../learning/performanceStore.js';
8
+ export interface ScoredCandidate {
9
+ candidate: Candidate;
10
+ score: number;
11
+ breakdown: ScoreBreakdown;
12
+ }
13
+ export interface ScoreOptions {
14
+ preferences?: Record<string, number>;
15
+ /** User prefer routing — a SOFT nudge to the userPreference dimension (never a hard override). */
16
+ prefer?: RoutingPreferences;
17
+ history?: HistoryLookup;
18
+ }
19
+ export declare function scoreCandidates(candidates: Candidate[], task: NormalizedTask, baseWeights: ScoreWeights, strategy: Strategy, options?: ScoreOptions): ScoredCandidate[];
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Phase 3 — scoring. Combines the per-dimension scores with strategy-adjusted weights into a single
3
+ * 0..1 score per candidate, normalized by the weight sum so weights need not total 100. Deterministic
4
+ * ordering: score desc, then quality, then latency, then providerId/model for stability.
5
+ */
6
+ import * as dim from './dimensions.js';
7
+ import { strategyWeights, weightSum } from './weights.js';
8
+ import { isPreferred } from './routingPrefs.js';
9
+ /** The userPreference input for a candidate: 1 when user-preferred, else any provider-level preference. */
10
+ function preferenceValue(candidate, options) {
11
+ if (isPreferred(options.prefer, candidate.providerId, candidate.model.id))
12
+ return 1;
13
+ return options.preferences?.[candidate.providerId];
14
+ }
15
+ export function scoreCandidates(candidates, task, baseWeights, strategy, options = {}) {
16
+ const w = strategyWeights(strategy, baseWeights);
17
+ const total = weightSum(w) || 1;
18
+ const scored = candidates.map((candidate) => {
19
+ const m = candidate.model;
20
+ const observed = options.history?.(candidate.providerId, m.id);
21
+ const breakdown = {
22
+ capabilityFit: dim.capabilityFit(m, task),
23
+ quality: dim.quality(m),
24
+ reliability: dim.reliability(m),
25
+ historicalSuccess: dim.historicalSuccess(m, observed),
26
+ latency: dim.latency(m, observed),
27
+ cost: dim.cost(m),
28
+ userPreference: dim.userPreference(m, preferenceValue(candidate, options)),
29
+ };
30
+ const score = (w.capabilityFit * breakdown.capabilityFit +
31
+ w.quality * breakdown.quality +
32
+ w.reliability * breakdown.reliability +
33
+ w.historicalSuccess * breakdown.historicalSuccess +
34
+ w.latency * breakdown.latency +
35
+ w.cost * breakdown.cost +
36
+ w.userPreference * breakdown.userPreference) /
37
+ total;
38
+ return { candidate, score, breakdown };
39
+ });
40
+ scored.sort((a, b) => {
41
+ if (b.score !== a.score)
42
+ return b.score - a.score;
43
+ if (b.breakdown.quality !== a.breakdown.quality)
44
+ return b.breakdown.quality - a.breakdown.quality;
45
+ if (b.breakdown.latency !== a.breakdown.latency)
46
+ return b.breakdown.latency - a.breakdown.latency;
47
+ return `${a.candidate.providerId}/${a.candidate.model.id}`.localeCompare(`${b.candidate.providerId}/${b.candidate.model.id}`);
48
+ });
49
+ return scored;
50
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Strategy → weight adjustments. Scores are normalized by the sum of weights, so a preset simply
3
+ * amplifies the dimension a strategy cares about; strategies that are really FILTERS (local-only,
4
+ * cloud-only, privacy-first, provider-specific) leave the weights at their base and let the filter
5
+ * phase do the work.
6
+ */
7
+ import type { ScoreWeights, Strategy } from '../../types.js';
8
+ export declare function strategyWeights(strategy: Strategy, base: ScoreWeights): ScoreWeights;
9
+ export declare function weightSum(w: ScoreWeights): number;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Strategy → weight adjustments. Scores are normalized by the sum of weights, so a preset simply
3
+ * amplifies the dimension a strategy cares about; strategies that are really FILTERS (local-only,
4
+ * cloud-only, privacy-first, provider-specific) leave the weights at their base and let the filter
5
+ * phase do the work.
6
+ */
7
+ export function strategyWeights(strategy, base) {
8
+ const w = { ...base };
9
+ switch (strategy) {
10
+ case 'fastest':
11
+ w.latency = base.latency * 4;
12
+ break;
13
+ case 'cheapest':
14
+ w.cost = base.cost * 4;
15
+ break;
16
+ case 'highest-quality':
17
+ w.quality = base.quality * 2.5;
18
+ break;
19
+ case 'best':
20
+ case 'local-only':
21
+ case 'cloud-only':
22
+ case 'privacy-first':
23
+ case 'provider-specific':
24
+ case 'fallback-only':
25
+ break;
26
+ }
27
+ return w;
28
+ }
29
+ export function weightSum(w) {
30
+ return w.capabilityFit + w.quality + w.reliability + w.historicalSuccess + w.latency + w.cost + w.userPreference;
31
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Phase 6 — response validation. Checks the normalized response against the task's declared output
3
+ * contract: JSON presence, optional schema (any object exposing `safeParse`, e.g. a zod schema),
4
+ * tool-call validity, and a basic non-empty signal. Returns a structured report; the router treats a
5
+ * failing report as a reason to fall through to the next candidate.
6
+ */
7
+ import type { AIResponse, OutputFormat, ToolSpec, ValidationReport } from '../../types.js';
8
+ export interface ValidateInput {
9
+ response: AIResponse;
10
+ output?: {
11
+ format: OutputFormat;
12
+ schema?: unknown;
13
+ };
14
+ tools?: ToolSpec[];
15
+ }
16
+ export declare function validate(input: ValidateInput): ValidationReport;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Phase 6 — response validation. Checks the normalized response against the task's declared output
3
+ * contract: JSON presence, optional schema (any object exposing `safeParse`, e.g. a zod schema),
4
+ * tool-call validity, and a basic non-empty signal. Returns a structured report; the router treats a
5
+ * failing report as a reason to fall through to the next candidate.
6
+ */
7
+ function isSafeParser(schema) {
8
+ return typeof schema === 'object' && schema !== null && typeof schema.safeParse === 'function';
9
+ }
10
+ export function validate(input) {
11
+ const checks = [];
12
+ const { response, output, tools } = input;
13
+ const fmt = output?.format;
14
+ if (fmt === 'json' || fmt === 'structured_output') {
15
+ const hasJson = response.json !== undefined;
16
+ checks.push({ name: 'json-present', ok: hasJson, ...(hasJson ? {} : { why: 'expected JSON output but none was produced' }) });
17
+ if (hasJson && output?.schema && isSafeParser(output.schema)) {
18
+ const result = output.schema.safeParse(response.json);
19
+ checks.push({ name: 'schema', ok: result.success, ...(result.success ? {} : { why: 'output did not match the provided schema' }) });
20
+ }
21
+ }
22
+ else if (response.finishReason !== 'tool_calls') {
23
+ // A tool-call response legitimately has no text; only require text otherwise.
24
+ const hasText = Boolean(response.text && response.text.trim());
25
+ checks.push({ name: 'text-present', ok: hasText, ...(hasText ? {} : { why: 'empty text response' }) });
26
+ }
27
+ if (tools && tools.length > 0 && response.finishReason === 'tool_calls') {
28
+ const calls = response.toolCalls ?? [];
29
+ const ok = calls.length > 0 && calls.every((c) => tools.some((t) => t.name === c.name));
30
+ checks.push({ name: 'tool-calls', ok, ...(ok ? {} : { why: 'missing or unknown tool call' }) });
31
+ }
32
+ return { ok: checks.every((c) => c.ok), checks };
33
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Hand-curated, `documented`-tier facts for well-known models. This is the ONLY place vendor model
3
+ * knowledge lives, and it is in the discovery layer — providers call `resolveModelMetadata`, `core/`
4
+ * never imports this. A model with no catalog entry and no operator-declared capabilities resolves to
5
+ * an all-`unknown` profile (routable only via an explicit pin), honoring the never-promote rule.
6
+ */
7
+ import type { BooleanCapabilityGroup } from '../core/capabilities/taxonomy.js';
8
+ import type { DeclaredCapabilities, ModelMetadata, Pricing, ProviderKind, QualityTier } from '../types.js';
9
+ export interface CatalogEntry {
10
+ caps: Partial<Record<BooleanCapabilityGroup, string[]>>;
11
+ contextWindow?: number;
12
+ maxOutputTokens?: number;
13
+ pricing?: Pricing;
14
+ quality?: {
15
+ tier?: QualityTier;
16
+ score?: number;
17
+ };
18
+ latencyP50Ms?: number;
19
+ successRate?: number;
20
+ family?: string;
21
+ displayName?: string;
22
+ }
23
+ export declare function lookupCatalog(kind: ProviderKind, modelId: string): CatalogEntry | undefined;
24
+ /**
25
+ * Build normalized ModelMetadata for one model by merging catalog facts with operator-declared
26
+ * capabilities (both `documented`). No source → all-`unknown` capabilities and `source: 'unknown'`.
27
+ */
28
+ export declare function resolveModelMetadata(providerId: string, kind: ProviderKind, modelId: string, declared?: DeclaredCapabilities, enabled?: boolean): ModelMetadata;