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,38 @@
1
+ /**
2
+ * Token ESTIMATION for context budgeting. These are heuristic estimates (≈ chars / charsPerToken),
3
+ * deliberately NOT conflated with provider-reported usage (which is authoritative and separate). The
4
+ * estimator can be calibrated over time from observed (chars, actualTokens) pairs via an EWMA, so the
5
+ * ratio drifts toward reality per deployment without ever claiming to be exact.
6
+ */
7
+ const DEFAULT_CHARS_PER_TOKEN = 4;
8
+ const EWMA_ALPHA = 0.2;
9
+ export class TokenEstimator {
10
+ charsPerToken;
11
+ constructor(charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
12
+ this.charsPerToken = charsPerToken > 0 ? charsPerToken : DEFAULT_CHARS_PER_TOKEN;
13
+ }
14
+ /** Heuristic token estimate for a string. Always ≥ 0; a non-empty string estimates ≥ 1. */
15
+ estimate(text) {
16
+ if (!text)
17
+ return 0;
18
+ return Math.max(1, Math.ceil(text.length / this.charsPerToken));
19
+ }
20
+ /** The chars→token ratio a token budget maps to (for compression sizing). */
21
+ charsForTokens(tokens) {
22
+ return Math.max(0, Math.floor(tokens * this.charsPerToken));
23
+ }
24
+ /** Nudge the ratio toward an observed (chars, actualTokens) sample. Ignores degenerate samples. */
25
+ calibrate(chars, actualTokens) {
26
+ if (chars <= 0 || actualTokens <= 0)
27
+ return;
28
+ const observed = chars / actualTokens;
29
+ this.charsPerToken = (1 - EWMA_ALPHA) * this.charsPerToken + EWMA_ALPHA * observed;
30
+ }
31
+ ratio() {
32
+ return this.charsPerToken;
33
+ }
34
+ }
35
+ /** Convenience: a one-off estimate with the default ratio. */
36
+ export function estimateTokens(text) {
37
+ return new TokenEstimator().estimate(text);
38
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Conversation store — full original turns as JSONL, one log + one meta record per conversation. This
3
+ * is persistent local USER DATA (recoverable originals), deliberately separate from telemetry (which
4
+ * never stores prompts) and from memory (durable facts). Resolved secret VALUES are still scrubbed so a
5
+ * pasted key can't be persisted, but user prose is kept intact.
6
+ */
7
+ import type { Clock } from '../util/clock.js';
8
+ import type { Area } from '../store/area.js';
9
+ export type TurnRole = 'user' | 'assistant' | 'system';
10
+ export interface ConversationTurn {
11
+ role: TurnRole;
12
+ text: string;
13
+ ts: number;
14
+ runId?: string;
15
+ }
16
+ export interface ConversationMeta {
17
+ id: string;
18
+ title: string;
19
+ createdAt: number;
20
+ updatedAt: number;
21
+ turns: number;
22
+ }
23
+ export declare class ConversationStore {
24
+ private readonly area;
25
+ private readonly clock;
26
+ private counter;
27
+ constructor(area: Area, clock?: Clock);
28
+ get enabled(): boolean;
29
+ /** Start a conversation; returns its id. */
30
+ start(id?: string): string;
31
+ /** Append a turn (originals preserved; only resolved secrets scrubbed). Titles derive from turn 1. */
32
+ append(id: string, role: TurnRole, text: string, runId?: string): void;
33
+ turns(id: string): ConversationTurn[];
34
+ meta(id: string): ConversationMeta | undefined;
35
+ /** All conversations, newest first. A corrupt meta file is skipped, not fatal. */
36
+ list(): ConversationMeta[];
37
+ delete(id: string): void;
38
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Conversation store — full original turns as JSONL, one log + one meta record per conversation. This
3
+ * is persistent local USER DATA (recoverable originals), deliberately separate from telemetry (which
4
+ * never stores prompts) and from memory (durable facts). Resolved secret VALUES are still scrubbed so a
5
+ * pasted key can't be persisted, but user prose is kept intact.
6
+ */
7
+ import { systemClock } from '../util/clock.js';
8
+ import { redactString } from '../security/redact.js';
9
+ function deriveTitle(text) {
10
+ const clean = text.replace(/\s+/g, ' ').trim();
11
+ if (!clean)
12
+ return 'Untitled';
13
+ const words = clean.split(' ').slice(0, 8).join(' ');
14
+ return words.length > 60 ? words.slice(0, 57) + '…' : words;
15
+ }
16
+ export class ConversationStore {
17
+ area;
18
+ clock;
19
+ counter = 0;
20
+ constructor(area, clock = systemClock) {
21
+ this.area = area;
22
+ this.clock = clock;
23
+ }
24
+ get enabled() {
25
+ return this.area.enabled;
26
+ }
27
+ /** Start a conversation; returns its id. */
28
+ start(id) {
29
+ const cid = id ?? `conv_${this.clock.now().toString(36)}_${process.pid.toString(36)}_${(this.counter += 1).toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
30
+ const now = this.clock.now();
31
+ const meta = { id: cid, title: 'Untitled', createdAt: now, updatedAt: now, turns: 0 };
32
+ this.area.writeJson(cid, meta);
33
+ return cid;
34
+ }
35
+ /** Append a turn (originals preserved; only resolved secrets scrubbed). Titles derive from turn 1. */
36
+ append(id, role, text, runId) {
37
+ const safe = redactString(text);
38
+ const turn = { role, text: safe, ts: this.clock.now(), ...(runId ? { runId } : {}) };
39
+ this.area.appendLine(id, turn);
40
+ const meta = this.area.tryReadJson(id) ?? { id, title: 'Untitled', createdAt: turn.ts, updatedAt: turn.ts, turns: 0 };
41
+ meta.turns += 1;
42
+ meta.updatedAt = turn.ts;
43
+ if (meta.title === 'Untitled' && role === 'user')
44
+ meta.title = deriveTitle(safe);
45
+ this.area.writeJson(id, meta);
46
+ }
47
+ turns(id) {
48
+ return this.area.readLines(id);
49
+ }
50
+ meta(id) {
51
+ return this.area.tryReadJson(id);
52
+ }
53
+ /** All conversations, newest first. A corrupt meta file is skipped, not fatal. */
54
+ list() {
55
+ return this.area
56
+ .listIds()
57
+ .map((id) => this.area.tryReadJson(id))
58
+ .filter((m) => m !== undefined)
59
+ .sort((a, b) => b.updatedAt - a.updatedAt);
60
+ }
61
+ delete(id) {
62
+ this.area.remove(id);
63
+ }
64
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The evidence engine — the heart of the "never claim a capability with false certainty" rule.
3
+ *
4
+ * Two invariants enforced here and covered by tests:
5
+ * 1. `unknown` and `unsupported` NEVER satisfy a requirement.
6
+ * 2. Merging capability facts never PROMOTES evidence beyond what a source actually asserted;
7
+ * the higher-ranked source wins, ties keep the first, and a positive claim can only be
8
+ * overridden by an equally- or higher-ranked source.
9
+ */
10
+ import type { Capability, CapabilityProfile, CapabilityRequirement, Evidence } from '../../types.js';
11
+ import type { BooleanCapabilityGroup } from './taxonomy.js';
12
+ export declare function rankOf(evidence: Evidence): number;
13
+ /** The capability returned for anything we have no fact about: false @ unknown. Never satisfies. */
14
+ export declare function unknownCapability(): Capability;
15
+ /** An empty profile — every lookup falls back to `unknown`. */
16
+ export declare function emptyProfile(contextWindow?: number): CapabilityProfile;
17
+ /** Look up one boolean capability, returning the `unknown` default when absent. */
18
+ export declare function getCapability(profile: CapabilityProfile, group: BooleanCapabilityGroup, key: string): Capability;
19
+ /**
20
+ * Does `profile` satisfy `req`? True only when the capability is positively present AND its evidence
21
+ * rank meets the requirement's floor (default `documented`). `unknown`/`unsupported` can never pass
22
+ * because their rank (1/0) is below any real floor and their value is false.
23
+ */
24
+ export declare function capabilitySatisfies(profile: CapabilityProfile, req: CapabilityRequirement): boolean;
25
+ /**
26
+ * Merge two capability facts without ever inventing certainty. The source with the higher evidence
27
+ * rank wins outright. On a tie, `a` is kept. This means a `documented:true` is never silently
28
+ * upgraded to `verified:true` by a second `documented` source — evidence only moves when a
29
+ * genuinely higher-ranked source says so.
30
+ */
31
+ export declare function mergeCapability<V>(a: Capability<V>, b: Capability<V>): Capability<V>;
32
+ /** Merge two full profiles fact-by-fact under the never-promote rule. */
33
+ export declare function mergeProfiles(a: CapabilityProfile, b: CapabilityProfile): CapabilityProfile;
34
+ /**
35
+ * Build a profile from a plain list of supported keys, all tagged at one evidence level. Used by
36
+ * providers/config that self-declare capabilities. Anything not listed stays `unknown` (absent).
37
+ */
38
+ export declare function profileFromDeclared(declared: Partial<Record<BooleanCapabilityGroup, string[]>> & {
39
+ contextWindow?: number;
40
+ }, evidence: Evidence): CapabilityProfile;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The evidence engine — the heart of the "never claim a capability with false certainty" rule.
3
+ *
4
+ * Two invariants enforced here and covered by tests:
5
+ * 1. `unknown` and `unsupported` NEVER satisfy a requirement.
6
+ * 2. Merging capability facts never PROMOTES evidence beyond what a source actually asserted;
7
+ * the higher-ranked source wins, ties keep the first, and a positive claim can only be
8
+ * overridden by an equally- or higher-ranked source.
9
+ */
10
+ import { EVIDENCE_RANK } from '../../types.js';
11
+ export function rankOf(evidence) {
12
+ return EVIDENCE_RANK[evidence];
13
+ }
14
+ /** The capability returned for anything we have no fact about: false @ unknown. Never satisfies. */
15
+ export function unknownCapability() {
16
+ return { value: false, evidence: 'unknown' };
17
+ }
18
+ /** An empty profile — every lookup falls back to `unknown`. */
19
+ export function emptyProfile(contextWindow = 0) {
20
+ return {
21
+ input: {},
22
+ output: {},
23
+ intelligence: {},
24
+ agent: {},
25
+ context: { contextWindow: { value: contextWindow, evidence: contextWindow > 0 ? 'documented' : 'unknown' } },
26
+ };
27
+ }
28
+ /** Look up one boolean capability, returning the `unknown` default when absent. */
29
+ export function getCapability(profile, group, key) {
30
+ const bucket = profile[group];
31
+ return bucket[key] ?? unknownCapability();
32
+ }
33
+ /**
34
+ * Does `profile` satisfy `req`? True only when the capability is positively present AND its evidence
35
+ * rank meets the requirement's floor (default `documented`). `unknown`/`unsupported` can never pass
36
+ * because their rank (1/0) is below any real floor and their value is false.
37
+ */
38
+ export function capabilitySatisfies(profile, req) {
39
+ const cap = getCapability(profile, req.group, req.key);
40
+ if (!cap.value)
41
+ return false;
42
+ const floor = rankOf(req.minEvidence ?? 'documented');
43
+ return rankOf(cap.evidence) >= floor;
44
+ }
45
+ /**
46
+ * Merge two capability facts without ever inventing certainty. The source with the higher evidence
47
+ * rank wins outright. On a tie, `a` is kept. This means a `documented:true` is never silently
48
+ * upgraded to `verified:true` by a second `documented` source — evidence only moves when a
49
+ * genuinely higher-ranked source says so.
50
+ */
51
+ export function mergeCapability(a, b) {
52
+ return rankOf(b.evidence) > rankOf(a.evidence) ? b : a;
53
+ }
54
+ function mergeBucket(a, b) {
55
+ const out = {};
56
+ for (const key of new Set([...Object.keys(a), ...Object.keys(b)])) {
57
+ const av = a[key];
58
+ const bv = b[key];
59
+ if (av && bv)
60
+ out[key] = mergeCapability(av, bv);
61
+ else if (av)
62
+ out[key] = av;
63
+ else if (bv)
64
+ out[key] = bv;
65
+ }
66
+ return out;
67
+ }
68
+ /** Merge two full profiles fact-by-fact under the never-promote rule. */
69
+ export function mergeProfiles(a, b) {
70
+ return {
71
+ input: mergeBucket(a.input, b.input),
72
+ output: mergeBucket(a.output, b.output),
73
+ intelligence: mergeBucket(a.intelligence, b.intelligence),
74
+ agent: mergeBucket(a.agent, b.agent),
75
+ context: {
76
+ contextWindow: mergeCapability(a.context.contextWindow, b.context.contextWindow),
77
+ maxOutputTokens: a.context.maxOutputTokens && b.context.maxOutputTokens
78
+ ? mergeCapability(a.context.maxOutputTokens, b.context.maxOutputTokens)
79
+ : (a.context.maxOutputTokens ?? b.context.maxOutputTokens),
80
+ },
81
+ };
82
+ }
83
+ /**
84
+ * Build a profile from a plain list of supported keys, all tagged at one evidence level. Used by
85
+ * providers/config that self-declare capabilities. Anything not listed stays `unknown` (absent).
86
+ */
87
+ export function profileFromDeclared(declared, evidence) {
88
+ const profile = emptyProfile(declared.contextWindow ?? 0);
89
+ const groups = ['input', 'output', 'intelligence', 'agent'];
90
+ for (const group of groups) {
91
+ const keys = declared[group];
92
+ if (!keys)
93
+ continue;
94
+ const bucket = profile[group];
95
+ for (const key of keys)
96
+ bucket[key] = { value: true, evidence };
97
+ }
98
+ if (declared.contextWindow && declared.contextWindow > 0) {
99
+ profile.context.contextWindow = { value: declared.contextWindow, evidence };
100
+ }
101
+ return profile;
102
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A runtime capability overlay. Verified probe results (and the one sanctioned runtime DEMOTION —
3
+ * a gateway that rejects response_format) live here, keyed by (provider, model), and are merged over
4
+ * the catalog/declared profile at routing time under the same never-promote merge rule. This keeps the
5
+ * static catalog deterministic while letting evidence improve as the system actually exercises models.
6
+ */
7
+ import type { CapabilityProfile } from '../../types.js';
8
+ export declare class CapabilityOverlay {
9
+ private readonly map;
10
+ private key;
11
+ get(providerId: string, model: string): CapabilityProfile | undefined;
12
+ /** Merge a probed profile into the overlay for a model (never-promote merge). */
13
+ set(providerId: string, model: string, profile: CapabilityProfile): void;
14
+ size(): number;
15
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The normalized capability taxonomy. This is the vocabulary the whole router speaks;
3
+ * provider adapters map their vendor-specific truth INTO these terms so that `core/` never
4
+ * has to know a vendor name. Adding a term here is the only sanctioned way to grow the taxonomy.
5
+ */
6
+ export declare const INPUT_MODALITIES: readonly ["text", "image", "audio", "video", "file", "pdf"];
7
+ export type InputModality = (typeof INPUT_MODALITIES)[number];
8
+ export declare const OUTPUT_MODALITIES: readonly ["text", "json", "structured_output", "code", "image", "audio"];
9
+ export type OutputModality = (typeof OUTPUT_MODALITIES)[number];
10
+ export declare const INTELLIGENCE_SKILLS: readonly ["reasoning", "classification", "summarization", "code_generation", "code_analysis", "planning", "translation", "extraction"];
11
+ export type IntelligenceSkill = (typeof INTELLIGENCE_SKILLS)[number];
12
+ export declare const AGENT_CAPABILITIES: readonly ["tool_calling", "function_calling", "mcp", "agentic_loop"];
13
+ export type AgentCapability = (typeof AGENT_CAPABILITIES)[number];
14
+ /** The five groups a capability requirement can address. `context` is numeric, handled separately. */
15
+ export type CapabilityGroup = 'input' | 'output' | 'intelligence' | 'agent' | 'context';
16
+ /** Groups whose keys are booleans (everything but the numeric `context`). */
17
+ export type BooleanCapabilityGroup = 'input' | 'output' | 'intelligence' | 'agent';
18
+ /** All valid keys for a boolean group, as a runtime lookup used by config validation. */
19
+ export declare const CAPABILITY_KEYS: Record<BooleanCapabilityGroup, readonly string[]>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The normalized capability taxonomy. This is the vocabulary the whole router speaks;
3
+ * provider adapters map their vendor-specific truth INTO these terms so that `core/` never
4
+ * has to know a vendor name. Adding a term here is the only sanctioned way to grow the taxonomy.
5
+ */
6
+ export const INPUT_MODALITIES = ['text', 'image', 'audio', 'video', 'file', 'pdf'];
7
+ export const OUTPUT_MODALITIES = ['text', 'json', 'structured_output', 'code', 'image', 'audio'];
8
+ export const INTELLIGENCE_SKILLS = [
9
+ 'reasoning',
10
+ 'classification',
11
+ 'summarization',
12
+ 'code_generation',
13
+ 'code_analysis',
14
+ 'planning',
15
+ 'translation',
16
+ 'extraction',
17
+ ];
18
+ export const AGENT_CAPABILITIES = ['tool_calling', 'function_calling', 'mcp', 'agentic_loop'];
19
+ /** All valid keys for a boolean group, as a runtime lookup used by config validation. */
20
+ export const CAPABILITY_KEYS = {
21
+ input: INPUT_MODALITIES,
22
+ output: OUTPUT_MODALITIES,
23
+ intelligence: INTELLIGENCE_SKILLS,
24
+ agent: AGENT_CAPABILITIES,
25
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * One authoritative error taxonomy for the whole router. A compact {status, retryable, category}
3
+ * shape is exactly what fallback needs: `category` spans the full ErrorCategory set and the error
4
+ * carries provider/model.
5
+ *
6
+ * SECURITY: an AIError stores only a category, a status, and a provider/model — never a key, never
7
+ * a request URL. Messages are assumed to reach a sink, so callers pass already-redacted text.
8
+ */
9
+ import type { ErrorCategory, RouterErrorInfo } from '../../types.js';
10
+ export interface AIErrorOptions {
11
+ category?: ErrorCategory;
12
+ status?: number | null;
13
+ retryable?: boolean;
14
+ providerId?: string;
15
+ model?: string;
16
+ }
17
+ export declare class AIError extends Error {
18
+ readonly category: ErrorCategory;
19
+ readonly status: number | null;
20
+ readonly retryable: boolean;
21
+ readonly providerId?: string;
22
+ readonly model?: string;
23
+ constructor(message: string, opts?: AIErrorOptions);
24
+ toInfo(): RouterErrorInfo;
25
+ }
26
+ export declare function isRetryable(category: ErrorCategory): boolean;
27
+ /** Map an HTTP status to a category. Provider adapters may refine this with body inspection. */
28
+ export declare function statusToCategory(status: number): ErrorCategory;
29
+ /** Coerce any thrown value into an AIError without losing an existing category. */
30
+ export declare function toAIError(err: unknown, fallback?: AIErrorOptions): AIError;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * One authoritative error taxonomy for the whole router. A compact {status, retryable, category}
3
+ * shape is exactly what fallback needs: `category` spans the full ErrorCategory set and the error
4
+ * carries provider/model.
5
+ *
6
+ * SECURITY: an AIError stores only a category, a status, and a provider/model — never a key, never
7
+ * a request URL. Messages are assumed to reach a sink, so callers pass already-redacted text.
8
+ */
9
+ export class AIError extends Error {
10
+ category;
11
+ status;
12
+ retryable;
13
+ providerId;
14
+ model;
15
+ constructor(message, opts = {}) {
16
+ super(message);
17
+ this.name = 'AIError';
18
+ this.category = opts.category ?? 'UNKNOWN';
19
+ this.status = opts.status ?? null;
20
+ this.retryable = opts.retryable ?? isRetryable(this.category);
21
+ if (opts.providerId !== undefined)
22
+ this.providerId = opts.providerId;
23
+ if (opts.model !== undefined)
24
+ this.model = opts.model;
25
+ }
26
+ toInfo() {
27
+ const info = {
28
+ category: this.category,
29
+ message: this.message,
30
+ retryable: this.retryable,
31
+ };
32
+ if (this.providerId !== undefined)
33
+ info.providerId = this.providerId;
34
+ if (this.model !== undefined)
35
+ info.model = this.model;
36
+ return info;
37
+ }
38
+ }
39
+ /**
40
+ * Retryable-vs-terminal by category. Retryable failures are the transient wire/limit ones;
41
+ * everything caused by a bad request, bad key, or missing capability is terminal — retrying
42
+ * it against the same provider only wastes calls (the never-re-hammer rule).
43
+ */
44
+ const RETRYABLE = new Set([
45
+ 'RATE_LIMIT',
46
+ 'QUOTA',
47
+ 'TIMEOUT',
48
+ 'NETWORK',
49
+ 'PROVIDER',
50
+ ]);
51
+ export function isRetryable(category) {
52
+ return RETRYABLE.has(category);
53
+ }
54
+ /** Map an HTTP status to a category. Provider adapters may refine this with body inspection. */
55
+ export function statusToCategory(status) {
56
+ if (status === 401)
57
+ return 'AUTHENTICATION';
58
+ if (status === 403)
59
+ return 'AUTHORIZATION';
60
+ if (status === 404)
61
+ return 'MODEL_NOT_FOUND';
62
+ if (status === 408)
63
+ return 'TIMEOUT';
64
+ if (status === 422)
65
+ return 'INVALID_REQUEST';
66
+ if (status === 429)
67
+ return 'RATE_LIMIT';
68
+ if (status >= 500)
69
+ return 'PROVIDER';
70
+ if (status >= 400)
71
+ return 'INVALID_REQUEST';
72
+ return 'UNKNOWN';
73
+ }
74
+ /** Coerce any thrown value into an AIError without losing an existing category. */
75
+ export function toAIError(err, fallback = {}) {
76
+ if (err instanceof AIError)
77
+ return err;
78
+ const message = err instanceof Error ? err.message : String(err);
79
+ return new AIError(message, fallback);
80
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Cross-provider fallback. Walks the ranked candidates; on failure it moves to the next candidate,
3
+ * but never re-hammers a provider that already returned a NON-retryable error (that provider's later
4
+ * candidates are skipped). A STOP-category error aborts the whole run. If nothing succeeds the result
5
+ * is ALL_PROVIDERS_FAILED, carrying the full attempt trace and the last underlying error.
6
+ */
7
+ import type { AIProvider } from '../../providers/provider.js';
8
+ import type { AIResponse, AttemptRecord, ValidationReport } from '../../types.js';
9
+ import type { ScoredCandidate } from '../router/scorer.js';
10
+ import type { RequestTemplate } from '../router/normalize.js';
11
+ import { AIError } from './errors.js';
12
+ import type { BudgetTracker } from '../policies/budget.js';
13
+ import type { Clock } from '../../util/clock.js';
14
+ export interface FallbackInput {
15
+ ranked: ScoredCandidate[];
16
+ template: RequestTemplate;
17
+ getProvider: (id: string) => AIProvider | undefined;
18
+ maxFallbacks: number;
19
+ signal?: AbortSignal;
20
+ clock?: Clock;
21
+ onAttempt?: (record: AttemptRecord) => void;
22
+ /** Optional Phase-6 validation. A failing report drops this candidate and continues (no poisoning). */
23
+ validate?: (response: AIResponse, model: string) => ValidationReport;
24
+ /** Optional spend guardrail. When it cannot afford the next call, the run STOPS with BUDGET. */
25
+ budget?: BudgetTracker;
26
+ /** Estimated USD cost of a candidate call, consulted before spending (used with `budget`). */
27
+ costOf?: (providerId: string, model: string) => Promise<number>;
28
+ }
29
+ export type FallbackResult = {
30
+ ok: true;
31
+ response: AIResponse;
32
+ used: ScoredCandidate;
33
+ attempts: AttemptRecord[];
34
+ validation?: ValidationReport;
35
+ } | {
36
+ ok: false;
37
+ error: AIError;
38
+ attempts: AttemptRecord[];
39
+ };
40
+ export declare function runWithFallback(input: FallbackInput): Promise<FallbackResult>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Cross-provider fallback. Walks the ranked candidates; on failure it moves to the next candidate,
3
+ * but never re-hammers a provider that already returned a NON-retryable error (that provider's later
4
+ * candidates are skipped). A STOP-category error aborts the whole run. If nothing succeeds the result
5
+ * is ALL_PROVIDERS_FAILED, carrying the full attempt trace and the last underlying error.
6
+ */
7
+ import { buildRequest } from '../router/request.js';
8
+ import { executeOnce } from '../router/executor.js';
9
+ import { AIError } from './errors.js';
10
+ import { attemptOutcome, isStop } from './retryPolicy.js';
11
+ import { systemClock } from '../../util/clock.js';
12
+ export async function runWithFallback(input) {
13
+ const clock = input.clock ?? systemClock;
14
+ const attempts = [];
15
+ const poisoned = new Set(); // providers that returned a non-retryable error
16
+ let lastError;
17
+ let tried = 0;
18
+ const limit = input.maxFallbacks + 1;
19
+ for (const scored of input.ranked) {
20
+ if (tried >= limit)
21
+ break;
22
+ const { providerId, model } = scored.candidate;
23
+ if (poisoned.has(providerId))
24
+ continue;
25
+ const provider = input.getProvider(providerId);
26
+ if (!provider)
27
+ continue;
28
+ // Budget gate: if we cannot afford this call, stop the whole run with BUDGET (not a fallback).
29
+ const estCost = input.budget?.active && input.costOf ? await input.costOf(providerId, model.id) : 0;
30
+ if (input.budget && !input.budget.canSpend(estCost)) {
31
+ lastError = new AIError(`run budget exhausted (calls=${input.budget.callsUsed}, cost=${input.budget.costUsed.toFixed(4)})`, { category: 'BUDGET', retryable: false });
32
+ break;
33
+ }
34
+ tried += 1;
35
+ const started = clock.now();
36
+ const request = buildRequest(input.template, model.id, input.signal);
37
+ const outcome = await executeOnce(provider, request);
38
+ const latencyMs = clock.now() - started;
39
+ input.budget?.recordCall(estCost);
40
+ if (outcome.ok) {
41
+ const validation = input.validate?.(outcome.response, model.id);
42
+ if (validation && !validation.ok) {
43
+ // Response-specific failure: drop this candidate but do NOT poison the provider.
44
+ const why = validation.checks.filter((c) => !c.ok).map((c) => c.why).filter(Boolean).join('; ');
45
+ const record = { providerId, model: model.id, outcome: 'non-retryable', category: 'RESPONSE_VALIDATION', latencyMs };
46
+ attempts.push(record);
47
+ input.onAttempt?.(record);
48
+ lastError = new AIError(`response failed validation: ${why}`, { category: 'RESPONSE_VALIDATION', retryable: false, providerId, model: model.id });
49
+ continue;
50
+ }
51
+ attempts.push({ providerId, model: model.id, outcome: 'success', latencyMs });
52
+ input.onAttempt?.(attempts[attempts.length - 1]);
53
+ return { ok: true, response: outcome.response, used: scored, attempts, ...(validation ? { validation } : {}) };
54
+ }
55
+ const record = {
56
+ providerId,
57
+ model: model.id,
58
+ outcome: attemptOutcome(outcome.error),
59
+ category: outcome.error.category,
60
+ latencyMs,
61
+ };
62
+ attempts.push(record);
63
+ input.onAttempt?.(record);
64
+ lastError = outcome.error;
65
+ if (record.outcome === 'non-retryable')
66
+ poisoned.add(providerId);
67
+ if (isStop(outcome.error.category))
68
+ break;
69
+ }
70
+ // A stop-category error (budget/config/privacy) surfaces as-is; otherwise aggregate as ALL_PROVIDERS_FAILED.
71
+ const error = lastError !== undefined && isStop(lastError.category)
72
+ ? lastError
73
+ : lastError !== undefined
74
+ ? new AIError(`all providers failed — last: ${lastError.message}`, {
75
+ category: 'ALL_PROVIDERS_FAILED',
76
+ retryable: false,
77
+ ...(lastError.providerId !== undefined ? { providerId: lastError.providerId } : {}),
78
+ ...(lastError.model !== undefined ? { model: lastError.model } : {}),
79
+ })
80
+ : new AIError('no candidate could be executed', { category: 'ALL_PROVIDERS_FAILED', retryable: false });
81
+ return { ok: false, error, attempts };
82
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Fallback classification. Distinct from the SAME-provider retry (which lives in the HTTP layer):
3
+ * this governs CROSS-provider traversal. A failure means "try the next candidate",
4
+ * except for a few categories that make trying anything else pointless (budget spent, config broken,
5
+ * a privacy policy that forbade the route) — those stop the whole run.
6
+ */
7
+ import type { AttemptRecord, ErrorCategory } from '../../types.js';
8
+ import type { AIError } from './errors.js';
9
+ /** Should this error abort the entire run rather than fall through to the next provider? */
10
+ export declare function isStop(category: ErrorCategory): boolean;
11
+ export declare function attemptOutcome(err: AIError): AttemptRecord['outcome'];
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Fallback classification. Distinct from the SAME-provider retry (which lives in the HTTP layer):
3
+ * this governs CROSS-provider traversal. A failure means "try the next candidate",
4
+ * except for a few categories that make trying anything else pointless (budget spent, config broken,
5
+ * a privacy policy that forbade the route) — those stop the whole run.
6
+ */
7
+ const STOP = new Set(['BUDGET', 'CONFIG', 'PRIVACY_VIOLATION']);
8
+ /** Should this error abort the entire run rather than fall through to the next provider? */
9
+ export function isStop(category) {
10
+ return STOP.has(category);
11
+ }
12
+ export function attemptOutcome(err) {
13
+ return err.retryable ? 'retryable' : 'non-retryable';
14
+ }
@@ -0,0 +1,3 @@
1
+ /** Health-state predicate: which states may still receive traffic. Unknown/unchecked is optimistic. */
2
+ import type { HealthState } from '../../types.js';
3
+ export declare function isRoutableState(state: HealthState): boolean;
@@ -0,0 +1,5 @@
1
+ /** Health-state predicate: which states may still receive traffic. Unknown/unchecked is optimistic. */
2
+ const ROUTABLE = new Set(['AVAILABLE', 'CONFIGURED', 'DEGRADED', 'UNKNOWN']);
3
+ export function isRoutableState(state) {
4
+ return ROUTABLE.has(state);
5
+ }
@@ -0,0 +1,23 @@
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 type { ErrorCategory, HealthStatus } from '../../types.js';
8
+ import type { Clock } from '../../util/clock.js';
9
+ import type { TelemetrySink } from '../../telemetry/telemetry.js';
10
+ export declare class HealthMonitor {
11
+ private readonly telemetry?;
12
+ private readonly cooldownMs;
13
+ private readonly state;
14
+ private readonly clock;
15
+ constructor(telemetry?: TelemetrySink | undefined, clock?: Clock, cooldownMs?: number);
16
+ get(providerId: string): HealthStatus | undefined;
17
+ all(): HealthStatus[];
18
+ /** Optimistic: an unseen provider is routable; a cooled-down one becomes routable again once elapsed. */
19
+ routable(providerId: string): boolean;
20
+ seed(status: HealthStatus): void;
21
+ recordOutcome(providerId: string, ok: boolean, category?: ErrorCategory): void;
22
+ private transition;
23
+ }