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,82 @@
1
+ /**
2
+ * Per-provider health tracker. Seeded from healthCheck() and updated by execution outcomes: a
3
+ * rate-limit or quota error puts the provider in a timed cooldown so subsequent routing skips it;
4
+ * a success restores it. State transitions emit `health.transition` telemetry. Cooldowns persist on
5
+ * the AI instance across run() calls, which is what makes "back off a throttled provider" real.
6
+ */
7
+ import { systemClock } from '../../util/clock.js';
8
+ import { redactString } from '../../security/redact.js';
9
+ import { isRoutableState } from './health.js';
10
+ function categoryToState(category) {
11
+ switch (category) {
12
+ case 'RATE_LIMIT':
13
+ return 'RATE_LIMITED';
14
+ case 'QUOTA':
15
+ return 'QUOTA_EXCEEDED';
16
+ case 'AUTHENTICATION':
17
+ case 'AUTHORIZATION':
18
+ return 'AUTH_FAILED';
19
+ case 'NETWORK':
20
+ return 'NETWORK_ERROR';
21
+ case 'TIMEOUT':
22
+ case 'PROVIDER':
23
+ return 'DEGRADED';
24
+ case 'MODEL_NOT_FOUND':
25
+ return 'MODEL_UNAVAILABLE';
26
+ default:
27
+ return 'DEGRADED';
28
+ }
29
+ }
30
+ export class HealthMonitor {
31
+ telemetry;
32
+ cooldownMs;
33
+ state = new Map();
34
+ clock;
35
+ constructor(telemetry, clock = systemClock, cooldownMs = 60_000) {
36
+ this.telemetry = telemetry;
37
+ this.cooldownMs = cooldownMs;
38
+ this.clock = clock;
39
+ }
40
+ get(providerId) {
41
+ return this.state.get(providerId);
42
+ }
43
+ all() {
44
+ return [...this.state.values()];
45
+ }
46
+ /** Optimistic: an unseen provider is routable; a cooled-down one becomes routable again once elapsed. */
47
+ routable(providerId) {
48
+ const s = this.state.get(providerId);
49
+ if (!s)
50
+ return true;
51
+ if (s.cooldownUntil !== undefined)
52
+ return this.clock.now() >= s.cooldownUntil;
53
+ return s.routable;
54
+ }
55
+ seed(status) {
56
+ this.transition(status.providerId, status.state, status.detail);
57
+ }
58
+ recordOutcome(providerId, ok, category) {
59
+ if (ok) {
60
+ this.transition(providerId, 'AVAILABLE');
61
+ return;
62
+ }
63
+ const to = category ? categoryToState(category) : 'DEGRADED';
64
+ const cooldown = to === 'RATE_LIMITED' || to === 'QUOTA_EXCEEDED' ? this.clock.now() + this.cooldownMs : undefined;
65
+ this.transition(providerId, to, undefined, cooldown);
66
+ }
67
+ transition(providerId, to, detail, cooldownUntil) {
68
+ const prev = this.state.get(providerId);
69
+ const status = {
70
+ providerId,
71
+ state: to,
72
+ routable: isRoutableState(to) && cooldownUntil === undefined,
73
+ checkedAt: this.clock.now(),
74
+ ...(detail !== undefined ? { detail: redactString(detail) } : {}),
75
+ ...(cooldownUntil !== undefined ? { cooldownUntil } : {}),
76
+ };
77
+ this.state.set(providerId, status);
78
+ if (this.telemetry && prev?.state !== to) {
79
+ this.telemetry.emit({ type: 'health.transition', ts: this.clock.now(), providerId, from: prev?.state ?? 'UNKNOWN', to });
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Per-run spend guardrail. Bounds a single run()'s total calls and/or estimated cost across the
3
+ * primary attempt, fallbacks, and verification. A `maxCalls`/cost ceiling: when the budget is spent,
4
+ * the run STOPS with a distinct BUDGET category rather than being mistaken for a provider failure and
5
+ * retried elsewhere.
6
+ */
7
+ export declare class BudgetTracker {
8
+ private readonly maxCalls?;
9
+ private readonly maxCostUsd?;
10
+ private calls;
11
+ private costUsd;
12
+ constructor(maxCalls?: number | undefined, maxCostUsd?: number | undefined);
13
+ get callsUsed(): number;
14
+ get costUsed(): number;
15
+ get active(): boolean;
16
+ /** Would one more call costing at most `estCostUsd` stay within budget? */
17
+ canSpend(estCostUsd?: number): boolean;
18
+ recordCall(costUsd?: number): void;
19
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Per-run spend guardrail. Bounds a single run()'s total calls and/or estimated cost across the
3
+ * primary attempt, fallbacks, and verification. A `maxCalls`/cost ceiling: when the budget is spent,
4
+ * the run STOPS with a distinct BUDGET category rather than being mistaken for a provider failure and
5
+ * retried elsewhere.
6
+ */
7
+ export class BudgetTracker {
8
+ maxCalls;
9
+ maxCostUsd;
10
+ calls = 0;
11
+ costUsd = 0;
12
+ constructor(maxCalls, maxCostUsd) {
13
+ this.maxCalls = maxCalls;
14
+ this.maxCostUsd = maxCostUsd;
15
+ }
16
+ get callsUsed() {
17
+ return this.calls;
18
+ }
19
+ get costUsed() {
20
+ return this.costUsd;
21
+ }
22
+ get active() {
23
+ return this.maxCalls !== undefined || this.maxCostUsd !== undefined;
24
+ }
25
+ /** Would one more call costing at most `estCostUsd` stay within budget? */
26
+ canSpend(estCostUsd = 0) {
27
+ if (this.maxCalls !== undefined && this.calls >= this.maxCalls)
28
+ return false;
29
+ if (this.maxCostUsd !== undefined && this.costUsd + estCostUsd > this.maxCostUsd)
30
+ return false;
31
+ return true;
32
+ }
33
+ recordCall(costUsd = 0) {
34
+ this.calls += 1;
35
+ this.costUsd += costUsd;
36
+ }
37
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Built-in task definitions — the QA Brain integration seam expressed as data. Each named task maps
3
+ * to the capabilities a model must have to serve it. Grounded in the actual QA Brain skills
4
+ * (url-to-test, trace analysis, test-healer, framework-converter, feature-analyzer, jira-to-test).
5
+ * Users extend or override these via `ai.registerTask()`, `new AI({ tasks })`, or YAML `tasks:`.
6
+ */
7
+ import type { TaskDefinition } from '../../types.js';
8
+ export declare const BUILTIN_TASKS: TaskDefinition[];
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Built-in task definitions — the QA Brain integration seam expressed as data. Each named task maps
3
+ * to the capabilities a model must have to serve it. Grounded in the actual QA Brain skills
4
+ * (url-to-test, trace analysis, test-healer, framework-converter, feature-analyzer, jira-to-test).
5
+ * Users extend or override these via `ai.registerTask()`, `new AI({ tasks })`, or YAML `tasks:`.
6
+ */
7
+ const req = (group, key, minEvidence) => minEvidence ? { group, key, minEvidence } : { group, key };
8
+ export const BUILTIN_TASKS = [
9
+ {
10
+ id: 'generate-playwright-tests',
11
+ aliases: ['url-to-test'],
12
+ description: 'Generate Playwright tests from a URL, ticket, or spec.',
13
+ required: [req('intelligence', 'code_generation'), req('intelligence', 'reasoning'), req('output', 'code')],
14
+ preferred: [req('agent', 'tool_calling')],
15
+ minContextWindow: 32_000,
16
+ output: { format: 'code' },
17
+ defaultStrategy: 'highest-quality',
18
+ },
19
+ {
20
+ id: 'playwright-trace-analysis',
21
+ aliases: ['trace-analysis', 'analyze-playwright-trace'],
22
+ description: 'Analyze a Playwright trace and suggest a fix.',
23
+ required: [req('intelligence', 'code_analysis'), req('intelligence', 'reasoning')],
24
+ preferred: [req('input', 'image'), req('input', 'file')],
25
+ minContextWindow: 128_000,
26
+ defaultStrategy: 'best',
27
+ },
28
+ {
29
+ id: 'test-healer',
30
+ description: 'Repair a broken test given the failure and the current code.',
31
+ required: [req('intelligence', 'code_analysis'), req('intelligence', 'code_generation')],
32
+ minContextWindow: 64_000,
33
+ output: { format: 'code' },
34
+ },
35
+ {
36
+ id: 'framework-converter',
37
+ description: 'Convert a test/script from one framework to another.',
38
+ required: [req('intelligence', 'code_generation'), req('intelligence', 'code_analysis')],
39
+ minContextWindow: 64_000,
40
+ output: { format: 'code' },
41
+ },
42
+ {
43
+ id: 'feature-analyzer',
44
+ description: 'Analyze a feature for test coverage and intent.',
45
+ required: [req('intelligence', 'reasoning'), req('intelligence', 'summarization'), req('intelligence', 'extraction')],
46
+ minContextWindow: 32_000,
47
+ },
48
+ {
49
+ id: 'jira-to-test',
50
+ aliases: ['ticket-router'],
51
+ description: 'Turn a Jira ticket into test cases / routing intent.',
52
+ required: [req('intelligence', 'reasoning'), req('intelligence', 'extraction'), req('intelligence', 'classification')],
53
+ },
54
+ ];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The provider registry. Holds provider instances and their enabled state. The router only ever
3
+ * reaches providers through here, and only through the `AIProvider` contract — so a new vendor is
4
+ * just another registered instance, never a special case.
5
+ */
6
+ import type { AIProvider } from '../../providers/provider.js';
7
+ export declare class ProviderRegistry {
8
+ private readonly entries;
9
+ register(provider: AIProvider, enabled?: boolean): void;
10
+ setEnabled(id: string, enabled: boolean): void;
11
+ has(id: string): boolean;
12
+ get(id: string): AIProvider | undefined;
13
+ /** Every registered provider, regardless of enabled state. */
14
+ all(): AIProvider[];
15
+ /** Only the providers eligible to serve traffic. */
16
+ enabled(): AIProvider[];
17
+ isEnabled(id: string): boolean;
18
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The provider registry. Holds provider instances and their enabled state. The router only ever
3
+ * reaches providers through here, and only through the `AIProvider` contract — so a new vendor is
4
+ * just another registered instance, never a special case.
5
+ */
6
+ export class ProviderRegistry {
7
+ entries = new Map();
8
+ register(provider, enabled = true) {
9
+ this.entries.set(provider.id, { provider, enabled });
10
+ }
11
+ setEnabled(id, enabled) {
12
+ const entry = this.entries.get(id);
13
+ if (entry)
14
+ entry.enabled = enabled;
15
+ }
16
+ has(id) {
17
+ return this.entries.has(id);
18
+ }
19
+ get(id) {
20
+ return this.entries.get(id)?.provider;
21
+ }
22
+ /** Every registered provider, regardless of enabled state. */
23
+ all() {
24
+ return [...this.entries.values()].map((e) => e.provider);
25
+ }
26
+ /** Only the providers eligible to serve traffic. */
27
+ enabled() {
28
+ return [...this.entries.values()].filter((e) => e.enabled).map((e) => e.provider);
29
+ }
30
+ isEnabled(id) {
31
+ return this.entries.get(id)?.enabled ?? false;
32
+ }
33
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The task registry: named task -> TaskDefinition, resolving aliases and merging sources by
3
+ * precedence. Built-ins are the floor; config tasks and `registerTask()` override by id.
4
+ */
5
+ import type { TaskDefinition } from '../../types.js';
6
+ export declare class TaskRegistry {
7
+ private readonly byId;
8
+ private readonly aliasToId;
9
+ constructor(extra?: TaskDefinition[]);
10
+ /** Register (or override by id) a task definition and index its aliases. */
11
+ register(def: TaskDefinition): void;
12
+ /** Look up a task by id or alias. Returns undefined for an unknown task (caller infers). */
13
+ lookup(nameOrAlias: string): TaskDefinition | undefined;
14
+ list(): TaskDefinition[];
15
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The task registry: named task -> TaskDefinition, resolving aliases and merging sources by
3
+ * precedence. Built-ins are the floor; config tasks and `registerTask()` override by id.
4
+ */
5
+ import { BUILTIN_TASKS } from './builtinTasks.js';
6
+ export class TaskRegistry {
7
+ byId = new Map();
8
+ aliasToId = new Map();
9
+ constructor(extra = []) {
10
+ for (const def of BUILTIN_TASKS)
11
+ this.register(def);
12
+ for (const def of extra)
13
+ this.register(def);
14
+ }
15
+ /** Register (or override by id) a task definition and index its aliases. */
16
+ register(def) {
17
+ this.byId.set(def.id, def);
18
+ this.aliasToId.set(def.id, def.id);
19
+ for (const alias of def.aliases ?? [])
20
+ this.aliasToId.set(alias, def.id);
21
+ }
22
+ /** Look up a task by id or alias. Returns undefined for an unknown task (caller infers). */
23
+ lookup(nameOrAlias) {
24
+ const id = this.aliasToId.get(nameOrAlias);
25
+ return id ? this.byId.get(id) : undefined;
26
+ }
27
+ list() {
28
+ return [...this.byId.values()];
29
+ }
30
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Confidence derivation. Blends the winning score with how well-evidenced the selected model's
3
+ * required capabilities are and how confident normalization was about the task. Monotonic: a higher
4
+ * score, better evidence, or a known (vs inferred) task each raises confidence.
5
+ */
6
+ import type { ModelMetadata, NormalizedTask } from '../../types.js';
7
+ export declare function deriveConfidence(topScore: number, model: ModelMetadata, task: NormalizedTask): number;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Confidence derivation. Blends the winning score with how well-evidenced the selected model's
3
+ * required capabilities are and how confident normalization was about the task. Monotonic: a higher
4
+ * score, better evidence, or a known (vs inferred) task each raises confidence.
5
+ */
6
+ import { capabilitySatisfies, getCapability, rankOf } from '../capabilities/evidence.js';
7
+ const clamp01 = (n) => (n < 0 ? 0 : n > 1 ? 1 : n);
8
+ export function deriveConfidence(topScore, model, task) {
9
+ let evFactor = 1;
10
+ if (task.required.length > 0) {
11
+ const sum = task.required.reduce((acc, r) => {
12
+ if (!capabilitySatisfies(model.capabilities, r))
13
+ return acc; // pinned/unknown-capability model: penalize
14
+ const cap = getCapability(model.capabilities, r.group, r.key);
15
+ return acc + rankOf(cap.evidence) / 4;
16
+ }, 0);
17
+ evFactor = sum / task.required.length;
18
+ }
19
+ return clamp01(topScore * (0.5 + 0.5 * evFactor) * task.confidence);
20
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Per-dimension scoring functions, each returning 0..1. They read ONLY normalized ModelMetadata
3
+ * (capabilities resolved by the provider, quality/reliability/latency/pricing threaded on upstream),
4
+ * so `core/scoring` never imports vendor data. `historicalSuccess` is a static placeholder here and
5
+ * is wired to telemetry in a later stage.
6
+ */
7
+ import type { ModelMetadata, NormalizedTask } from '../../types.js';
8
+ import type { ObservedPerf } from '../../learning/performanceStore.js';
9
+ /** How well the model meets required capabilities (evidence-weighted) plus a preferred-coverage bonus. */
10
+ export declare function capabilityFit(model: ModelMetadata, task: NormalizedTask): number;
11
+ export declare function quality(model: ModelMetadata): number;
12
+ export declare function reliability(model: ModelMetadata): number;
13
+ export declare function historicalSuccess(model: ModelMetadata, observed?: ObservedPerf): number;
14
+ export declare function latency(model: ModelMetadata, observed?: ObservedPerf): number;
15
+ export declare function cost(model: ModelMetadata): number;
16
+ export declare function userPreference(_model: ModelMetadata, preference?: number): number;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Per-dimension scoring functions, each returning 0..1. They read ONLY normalized ModelMetadata
3
+ * (capabilities resolved by the provider, quality/reliability/latency/pricing threaded on upstream),
4
+ * so `core/scoring` never imports vendor data. `historicalSuccess` is a static placeholder here and
5
+ * is wired to telemetry in a later stage.
6
+ */
7
+ import { rankOf } from '../capabilities/evidence.js';
8
+ import { capabilitySatisfies, getCapability } from '../capabilities/evidence.js';
9
+ const clamp01 = (n) => (n < 0 ? 0 : n > 1 ? 1 : n);
10
+ const TIER_SCORE = { frontier: 1, strong: 0.8, mid: 0.6, small: 0.4 };
11
+ /** How well the model meets required capabilities (evidence-weighted) plus a preferred-coverage bonus. */
12
+ export function capabilityFit(model, task) {
13
+ const evFactor = (key, group) => {
14
+ const cap = getCapability(model.capabilities, group, key);
15
+ return cap.value ? rankOf(cap.evidence) / 4 : 0;
16
+ };
17
+ const requiredScore = task.required.length === 0
18
+ ? 1
19
+ : task.required.reduce((acc, r) => acc + (capabilitySatisfies(model.capabilities, r) ? evFactor(r.key, r.group) : 0), 0) /
20
+ task.required.length;
21
+ if (task.preferred.length === 0)
22
+ return clamp01(requiredScore);
23
+ const preferredScore = task.preferred.filter((r) => capabilitySatisfies(model.capabilities, r)).length / task.preferred.length;
24
+ return clamp01(0.8 * requiredScore + 0.2 * preferredScore);
25
+ }
26
+ export function quality(model) {
27
+ if (model.quality?.score !== undefined)
28
+ return clamp01(model.quality.score);
29
+ if (model.quality?.tier)
30
+ return TIER_SCORE[model.quality.tier];
31
+ return 0.5;
32
+ }
33
+ export function reliability(model) {
34
+ return clamp01(model.reliability?.successRate ?? 0.85);
35
+ }
36
+ export function historicalSuccess(model, observed) {
37
+ // Prefer OBSERVED success rate once we have enough samples; else declared reliability.
38
+ if (observed)
39
+ return clamp01(observed.successRate);
40
+ return clamp01(model.reliability?.successRate ?? 0.8);
41
+ }
42
+ export function latency(model, observed) {
43
+ // Prefer OBSERVED latency once learned; else the catalog's p50.
44
+ const p50 = observed?.avgLatencyMs ?? model.latency?.p50Ms;
45
+ if (p50 === undefined)
46
+ return 0.5;
47
+ return clamp01(1 / (1 + p50 / 2000));
48
+ }
49
+ export function cost(model) {
50
+ const p = model.pricing;
51
+ if (!p)
52
+ return 1; // local / unpriced treated as cheapest
53
+ const sum = (p.inputPerMTok ?? 0) + (p.outputPerMTok ?? 0);
54
+ if (sum === 0)
55
+ return 1;
56
+ return clamp01(1 / (1 + sum));
57
+ }
58
+ export function userPreference(_model, preference) {
59
+ return clamp01(preference ?? 0.5);
60
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Phase 5 — single-attempt execution. Calls the provider through the contract and normalizes ANY
3
+ * thrown value into an AIError carrying the provider/model, so the fallback controller never sees a
4
+ * raw vendor error.
5
+ */
6
+ import type { AIProvider } from '../../providers/provider.js';
7
+ import type { AIRequest, AIResponse } from '../../types.js';
8
+ import { AIError } from '../fallback/errors.js';
9
+ export type ExecOutcome = {
10
+ ok: true;
11
+ response: AIResponse;
12
+ } | {
13
+ ok: false;
14
+ error: AIError;
15
+ };
16
+ export declare function executeOnce(provider: AIProvider, request: AIRequest): Promise<ExecOutcome>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Phase 5 — single-attempt execution. Calls the provider through the contract and normalizes ANY
3
+ * thrown value into an AIError carrying the provider/model, so the fallback controller never sees a
4
+ * raw vendor error.
5
+ */
6
+ import { AIError, toAIError } from '../fallback/errors.js';
7
+ export async function executeOnce(provider, request) {
8
+ try {
9
+ const response = await provider.execute(request);
10
+ return { ok: true, response };
11
+ }
12
+ catch (e) {
13
+ const base = toAIError(e);
14
+ const error = base.providerId
15
+ ? base
16
+ : new AIError(base.message, {
17
+ category: base.category,
18
+ status: base.status,
19
+ retryable: base.retryable,
20
+ providerId: provider.id,
21
+ model: request.model,
22
+ });
23
+ return { ok: false, error };
24
+ }
25
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Phase 2 — filter. Excludes candidates that cannot or must not serve the task, recording a concrete
3
+ * reason for each so a zero-eligible outcome yields an actionable NO_CAPABLE_PROVIDER. Order matters
4
+ * only for which reason is reported first; every exclusion is a hard gate:
5
+ * deny/allow → pin → capability (relaxed when pinned) → context window → privacy → hard cost/latency.
6
+ *
7
+ * PRIVACY is enforced here UNCONDITIONALLY — even a pinned/provider-specific request passes through
8
+ * it, so high-sensitivity input can never reach a cloud provider without an explicit allowance.
9
+ */
10
+ import type { AIProvider } from '../../providers/provider.js';
11
+ import type { Candidate, ExclusionReason, NormalizedTask, RunRequest } from '../../types.js';
12
+ import type { ResolvedConfig } from '../../config/defaults.js';
13
+ import type { HealthMonitor } from '../health/monitor.js';
14
+ import type { RequestTemplate } from './normalize.js';
15
+ export interface FilterInput {
16
+ candidates: Candidate[];
17
+ task: NormalizedTask;
18
+ config: ResolvedConfig;
19
+ template: RequestTemplate;
20
+ getProvider: (id: string) => AIProvider | undefined;
21
+ pin: {
22
+ provider?: string;
23
+ model?: string;
24
+ };
25
+ constraints?: RunRequest['constraints'];
26
+ /** User exclude routing (a hard filter, recorded in the trace — never overridden by learning). */
27
+ routing?: RunRequest['routing'];
28
+ health?: HealthMonitor;
29
+ }
30
+ export interface FilterResult {
31
+ eligible: Candidate[];
32
+ filteredOut: ExclusionReason[];
33
+ }
34
+ export declare function filterCandidates(input: FilterInput): Promise<FilterResult>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Phase 2 — filter. Excludes candidates that cannot or must not serve the task, recording a concrete
3
+ * reason for each so a zero-eligible outcome yields an actionable NO_CAPABLE_PROVIDER. Order matters
4
+ * only for which reason is reported first; every exclusion is a hard gate:
5
+ * deny/allow → pin → capability (relaxed when pinned) → context window → privacy → hard cost/latency.
6
+ *
7
+ * PRIVACY is enforced here UNCONDITIONALLY — even a pinned/provider-specific request passes through
8
+ * it, so high-sensitivity input can never reach a cloud provider without an explicit allowance.
9
+ */
10
+ import { capabilitySatisfies } from '../capabilities/evidence.js';
11
+ import { buildRequest } from './request.js';
12
+ import { isExcluded } from './routingPrefs.js';
13
+ function privacyFloor(task, p) {
14
+ let floor = task.requireLocal || task.strategy === 'local-only' || !p.allowCloud;
15
+ if (task.sensitivity === 'high' && !p.sensitiveDataAllowedOnCloud)
16
+ floor = true;
17
+ if (task.sensitivity === 'unknown' && p.treatUnknownAsSensitive && !p.sensitiveDataAllowedOnCloud)
18
+ floor = true;
19
+ return floor;
20
+ }
21
+ export async function filterCandidates(input) {
22
+ const { candidates, task, config, pin, constraints } = input;
23
+ const eligible = [];
24
+ const filteredOut = [];
25
+ const pinned = Boolean(pin.provider);
26
+ const requireLocal = privacyFloor(task, config.privacy);
27
+ const wantCostLatency = constraints?.maxCostUsd !== undefined || constraints?.maxLatencyMs !== undefined;
28
+ for (const candidate of candidates) {
29
+ const provider = input.getProvider(candidate.providerId);
30
+ const modelId = candidate.model.id;
31
+ const exclude = (reason) => {
32
+ filteredOut.push({ providerId: candidate.providerId, model: modelId, reason });
33
+ };
34
+ if (!provider) {
35
+ exclude('provider not registered');
36
+ continue;
37
+ }
38
+ // health / cooldown
39
+ if (input.health && !input.health.routable(candidate.providerId)) {
40
+ const state = input.health.get(candidate.providerId)?.state ?? 'UNKNOWN';
41
+ exclude(`provider not routable (health=${state})`);
42
+ continue;
43
+ }
44
+ // user exclude routing (a HARD filter — but the candidate stays visible in the trace, and learning
45
+ // can never re-admit it). Checked before pin so it applies even to a pinned provider.
46
+ if (isExcluded(input.routing, candidate.providerId, modelId)) {
47
+ exclude(`⊘ excluded by user configuration`);
48
+ continue;
49
+ }
50
+ // deny / allow lists
51
+ if (constraints?.denyProviders?.includes(candidate.providerId)) {
52
+ exclude(`excluded by denyProviders`);
53
+ continue;
54
+ }
55
+ if (constraints?.allowProviders && !constraints.allowProviders.includes(candidate.providerId)) {
56
+ exclude(`not in allowProviders`);
57
+ continue;
58
+ }
59
+ // pin
60
+ if (pin.provider && candidate.providerId !== pin.provider) {
61
+ exclude(`not the pinned provider '${pin.provider}'`);
62
+ continue;
63
+ }
64
+ if (pin.model && modelId !== pin.model) {
65
+ exclude(`not the pinned model '${pin.model}'`);
66
+ continue;
67
+ }
68
+ // capability — relaxed when the caller pinned a provider (route on their assertion)
69
+ if (!pinned) {
70
+ const unmet = task.required.find((r) => !capabilitySatisfies(candidate.model.capabilities, r));
71
+ if (unmet) {
72
+ exclude(`missing required capability ${unmet.group}.${unmet.key}`);
73
+ continue;
74
+ }
75
+ }
76
+ // context window
77
+ if (task.minContextWindow !== undefined && candidate.model.contextWindow !== undefined && candidate.model.contextWindow < task.minContextWindow) {
78
+ exclude(`context window ${candidate.model.contextWindow} < required ${task.minContextWindow}`);
79
+ continue;
80
+ }
81
+ // privacy (unconditional)
82
+ if (requireLocal && provider.privacyClass !== 'local') {
83
+ exclude(`privacy: high/undeclared sensitivity may not use cloud provider (privacyClass=${provider.privacyClass})`);
84
+ continue;
85
+ }
86
+ if (task.strategy === 'cloud-only' && provider.privacyClass !== 'cloud') {
87
+ exclude(`strategy cloud-only excludes local provider`);
88
+ continue;
89
+ }
90
+ if (!config.privacy.allowLocal && provider.privacyClass === 'local') {
91
+ exclude(`local providers disabled by policy`);
92
+ continue;
93
+ }
94
+ // hard cost / latency constraints
95
+ if (wantCostLatency) {
96
+ const estimate = await provider.estimate(buildRequest(input.template, modelId));
97
+ if (constraints?.maxCostUsd !== undefined && estimate.estCost && estimate.estCost.amount > constraints.maxCostUsd) {
98
+ exclude(`estimated cost ${estimate.estCost.amount} > maxCostUsd ${constraints.maxCostUsd}`);
99
+ continue;
100
+ }
101
+ if (constraints?.maxLatencyMs !== undefined && estimate.estLatencyMs?.p50 !== undefined && estimate.estLatencyMs.p50 > constraints.maxLatencyMs) {
102
+ exclude(`estimated latency ${estimate.estLatencyMs.p50}ms > maxLatencyMs ${constraints.maxLatencyMs}`);
103
+ continue;
104
+ }
105
+ if (!estimate.contextFits) {
106
+ exclude(`input does not fit model context window`);
107
+ continue;
108
+ }
109
+ }
110
+ eligible.push(candidate);
111
+ }
112
+ return { eligible, filteredOut };
113
+ }
@@ -0,0 +1,30 @@
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
+ import type { AIRequest, InputPart, NormalizedTask, OutputFormat, RunRequest, Sensitivity, ToolSpec } from '../../types.js';
8
+ import type { ResolvedConfig } from '../../config/defaults.js';
9
+ import type { TaskRegistry } from '../registry/taskRegistry.js';
10
+ export interface RequestTemplate {
11
+ taskId: string;
12
+ input: {
13
+ text?: string;
14
+ parts?: InputPart[];
15
+ };
16
+ system?: string;
17
+ output?: {
18
+ format: OutputFormat;
19
+ schema?: unknown;
20
+ };
21
+ tools?: ToolSpec[];
22
+ params?: AIRequest['params'];
23
+ timeoutMs: number;
24
+ sensitivity: Sensitivity;
25
+ }
26
+ export interface NormalizeResult {
27
+ task: NormalizedTask;
28
+ template: RequestTemplate;
29
+ }
30
+ export declare function normalize(req: RunRequest, tasks: TaskRegistry, config: ResolvedConfig): NormalizeResult;