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,105 @@
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 { emptyProfile, mergeProfiles, profileFromDeclared } from '../core/capabilities/evidence.js';
8
+ const TEXT_CODE = ['reasoning', 'code_generation', 'code_analysis', 'summarization', 'extraction', 'classification'];
9
+ const TOOLS = ['tool_calling', 'function_calling'];
10
+ /** Keyed by provider KIND so any provider of that kind benefits, then by model id. */
11
+ const CATALOG = {
12
+ gemini: {
13
+ 'gemini-3.6-flash': {
14
+ caps: { intelligence: [...TEXT_CODE, 'planning'], output: ['text', 'json', 'structured_output', 'code'], input: ['text', 'image', 'pdf', 'file'], agent: TOOLS },
15
+ contextWindow: 1_000_000,
16
+ pricing: { inputPerMTok: 0.15, outputPerMTok: 0.6, currency: 'USD' },
17
+ quality: { tier: 'strong' },
18
+ latencyP50Ms: 900,
19
+ successRate: 0.98,
20
+ },
21
+ 'gemini-2.5-flash-lite': {
22
+ caps: { intelligence: TEXT_CODE, output: ['text', 'json', 'code'], input: ['text', 'image'], agent: TOOLS },
23
+ contextWindow: 1_000_000,
24
+ pricing: { inputPerMTok: 0.075, outputPerMTok: 0.3, currency: 'USD' },
25
+ quality: { tier: 'mid' },
26
+ latencyP50Ms: 600,
27
+ successRate: 0.97,
28
+ },
29
+ },
30
+ groq: {
31
+ 'llama-3.3-70b': {
32
+ caps: { intelligence: TEXT_CODE, output: ['text', 'json', 'code'], input: ['text'], agent: TOOLS },
33
+ contextWindow: 128_000,
34
+ pricing: { inputPerMTok: 0.59, outputPerMTok: 0.79, currency: 'USD' },
35
+ quality: { tier: 'strong' },
36
+ latencyP50Ms: 300,
37
+ successRate: 0.97,
38
+ },
39
+ },
40
+ anthropic: {
41
+ 'claude-sonnet-5': {
42
+ caps: { intelligence: [...TEXT_CODE, 'planning'], output: ['text', 'json', 'structured_output', 'code'], input: ['text', 'image', 'pdf'], agent: TOOLS },
43
+ contextWindow: 200_000,
44
+ maxOutputTokens: 64_000,
45
+ pricing: { inputPerMTok: 3, outputPerMTok: 15, currency: 'USD' },
46
+ quality: { tier: 'frontier' },
47
+ latencyP50Ms: 1500,
48
+ successRate: 0.99,
49
+ },
50
+ 'claude-haiku-4-5': {
51
+ caps: { intelligence: TEXT_CODE, output: ['text', 'json', 'code'], input: ['text', 'image'], agent: TOOLS },
52
+ contextWindow: 200_000,
53
+ pricing: { inputPerMTok: 0.8, outputPerMTok: 4, currency: 'USD' },
54
+ quality: { tier: 'strong' },
55
+ latencyP50Ms: 700,
56
+ successRate: 0.98,
57
+ },
58
+ },
59
+ ollama: {
60
+ 'llama3.1': {
61
+ caps: { intelligence: ['reasoning', 'code_generation', 'code_analysis', 'summarization'], output: ['text', 'json', 'code'], input: ['text'] },
62
+ contextWindow: 128_000,
63
+ quality: { tier: 'mid' },
64
+ latencyP50Ms: 3500,
65
+ successRate: 0.95,
66
+ },
67
+ },
68
+ };
69
+ export function lookupCatalog(kind, modelId) {
70
+ return CATALOG[kind]?.[modelId];
71
+ }
72
+ /**
73
+ * Build normalized ModelMetadata for one model by merging catalog facts with operator-declared
74
+ * capabilities (both `documented`). No source → all-`unknown` capabilities and `source: 'unknown'`.
75
+ */
76
+ export function resolveModelMetadata(providerId, kind, modelId, declared, enabled = true) {
77
+ const entry = lookupCatalog(kind, modelId);
78
+ const hasFacts = Boolean(entry) || Boolean(declared);
79
+ const evidence = hasFacts ? 'documented' : 'unknown';
80
+ let capabilities = emptyProfile(entry?.contextWindow ?? declared?.contextWindow ?? 0);
81
+ if (entry)
82
+ capabilities = mergeProfiles(capabilities, profileFromDeclared({ ...entry.caps, contextWindow: entry.contextWindow }, 'documented'));
83
+ if (declared) {
84
+ capabilities = mergeProfiles(capabilities, profileFromDeclared({ input: declared.input, output: declared.output, intelligence: declared.intelligence, agent: declared.agent, contextWindow: declared.contextWindow }, 'documented'));
85
+ }
86
+ const model = { providerId, id: modelId, capabilities, enabled, source: evidence };
87
+ const contextWindow = entry?.contextWindow ?? declared?.contextWindow;
88
+ if (contextWindow !== undefined)
89
+ model.contextWindow = contextWindow;
90
+ if (entry?.maxOutputTokens !== undefined)
91
+ model.maxOutputTokens = entry.maxOutputTokens;
92
+ if (entry?.family !== undefined)
93
+ model.family = entry.family;
94
+ if (entry?.displayName !== undefined)
95
+ model.displayName = entry.displayName;
96
+ if (entry?.pricing !== undefined)
97
+ model.pricing = entry.pricing;
98
+ if (entry?.quality !== undefined)
99
+ model.quality = { evidence: 'documented', ...(entry.quality.tier !== undefined ? { tier: entry.quality.tier } : {}), ...(entry.quality.score !== undefined ? { score: entry.quality.score } : {}) };
100
+ if (entry?.latencyP50Ms !== undefined)
101
+ model.latency = { p50Ms: entry.latencyP50Ms, evidence: 'documented' };
102
+ if (entry?.successRate !== undefined)
103
+ model.reliability = { successRate: entry.successRate, evidence: 'documented' };
104
+ return model;
105
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * OpenAPI-based provider discovery. DETERMINISTIC inspection of an OpenAPI 3.x
3
+ * document to propose how an unknown provider maps onto the router. The spec is treated as EVIDENCE,
4
+ * not proof — a suggested config still requires the operator to confirm and to supply the key via an
5
+ * env var. No LLM and no secrets are involved (AI-assisted discovery is a later phase).
6
+ */
7
+ import type { ProviderConfig } from '../types.js';
8
+ export interface OpenApiEndpoint {
9
+ path: string;
10
+ methods: string[];
11
+ }
12
+ export interface OpenApiAnalysis {
13
+ title?: string;
14
+ version?: string;
15
+ baseUrl?: string;
16
+ authMode: 'bearer' | 'apiKey-header' | 'none' | 'unknown';
17
+ authHeaderName?: string;
18
+ wireShape: 'openai' | 'unknown';
19
+ chatEndpoint?: string;
20
+ endpoints: OpenApiEndpoint[];
21
+ suggestedProviderConfig?: ProviderConfig;
22
+ notes: string[];
23
+ }
24
+ export declare function analyzeOpenApi(spec: unknown): OpenApiAnalysis;
25
+ export declare function analyzeOpenApiFile(path: string): OpenApiAnalysis;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * OpenAPI-based provider discovery. DETERMINISTIC inspection of an OpenAPI 3.x
3
+ * document to propose how an unknown provider maps onto the router. The spec is treated as EVIDENCE,
4
+ * not proof — a suggested config still requires the operator to confirm and to supply the key via an
5
+ * env var. No LLM and no secrets are involved (AI-assisted discovery is a later phase).
6
+ */
7
+ import { readFileSync } from 'node:fs';
8
+ import { parse as parseYaml } from 'yaml';
9
+ const CHAT_HINT = /(chat\/completions|\/completions|\/generate|\/v1\/messages)/i;
10
+ const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
11
+ export function analyzeOpenApi(spec) {
12
+ const doc = (spec ?? {});
13
+ const notes = [];
14
+ const baseUrl = doc.servers?.[0]?.url;
15
+ if (!baseUrl)
16
+ notes.push('No servers[].url found — baseUrl must be supplied manually.');
17
+ const endpoints = Object.entries(doc.paths ?? {}).map(([path, ops]) => ({
18
+ path,
19
+ methods: Object.keys(ops ?? {}).filter((m) => HTTP_METHODS.includes(m.toLowerCase())),
20
+ }));
21
+ const chat = endpoints.find((e) => CHAT_HINT.test(e.path));
22
+ const wireShape = chat && /chat\/completions/i.test(chat.path) ? 'openai' : 'unknown';
23
+ if (chat)
24
+ notes.push(`Detected a chat-like endpoint: ${chat.path}`);
25
+ else
26
+ notes.push('No chat/completions-style endpoint detected — a custom mapping may be required.');
27
+ // Auth scheme
28
+ let authMode = 'unknown';
29
+ let authHeaderName;
30
+ const schemes = doc.components?.securitySchemes ?? {};
31
+ for (const scheme of Object.values(schemes)) {
32
+ if (scheme.type === 'http' && scheme.scheme === 'bearer') {
33
+ authMode = 'bearer';
34
+ break;
35
+ }
36
+ if (scheme.type === 'apiKey' && scheme.in === 'header') {
37
+ authMode = 'apiKey-header';
38
+ authHeaderName = scheme.name;
39
+ break;
40
+ }
41
+ if (scheme.type === 'http' && scheme.scheme === 'none')
42
+ authMode = 'none';
43
+ }
44
+ if (authMode === 'unknown')
45
+ notes.push('Could not determine an auth scheme — set it in the provider config.');
46
+ const analysis = {
47
+ ...(doc.info?.title ? { title: doc.info.title } : {}),
48
+ ...(doc.info?.version ? { version: doc.info.version } : {}),
49
+ ...(baseUrl ? { baseUrl } : {}),
50
+ authMode,
51
+ ...(authHeaderName ? { authHeaderName } : {}),
52
+ wireShape,
53
+ ...(chat ? { chatEndpoint: chat.path } : {}),
54
+ endpoints,
55
+ notes,
56
+ };
57
+ // Suggest a config only when we found an OpenAI-compatible chat endpoint and a base URL.
58
+ if (wireShape === 'openai' && baseUrl) {
59
+ const base = baseUrl.replace(/\/chat\/completions$/i, '').replace(/\/+$/, '');
60
+ analysis.suggestedProviderConfig = {
61
+ id: 'openapi-provider',
62
+ kind: 'openai-compatible',
63
+ baseUrl: base,
64
+ apiKeyEnv: 'OPENAPI_PROVIDER_API_KEY',
65
+ wireShape: 'openai',
66
+ models: 'auto',
67
+ };
68
+ notes.push('Suggested an openai-compatible provider config — review it, set models, and supply the key via OPENAPI_PROVIDER_API_KEY.');
69
+ }
70
+ return analysis;
71
+ }
72
+ export function analyzeOpenApiFile(path) {
73
+ const text = readFileSync(path, 'utf8');
74
+ const raw = path.endsWith('.json') ? JSON.parse(text) : parseYaml(text);
75
+ return analyzeOpenApi(raw);
76
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Checkpoint capture + reconciliation. A checkpoint fingerprints the workspace (git HEAD, hashes of
3
+ * files the plan touches, the runtime config, and the versions of the skills in play). On resume we
4
+ * recompute and compare: any difference is DRIFT, which forces a replan rather than blindly continuing.
5
+ */
6
+ import type { Clock } from '../util/clock.js';
7
+ import type { Skill } from '../skills/skill.js';
8
+ import type { ExecutionPlan } from '../orchestration/plan.js';
9
+ import type { Checkpoint } from './execution.js';
10
+ /** git HEAD sha (resolving a symbolic ref), or undefined when not a repo. The ref path is jailed to
11
+ * `.git` so a crafted HEAD (`ref: ../../etc/x`) can't read outside the workspace. */
12
+ export declare function gitHead(root: string): string | undefined;
13
+ export interface CaptureInput {
14
+ root: string;
15
+ plan?: ExecutionPlan;
16
+ skills: Skill[];
17
+ completedSteps: string[];
18
+ clock?: Clock;
19
+ }
20
+ export declare function captureCheckpoint(input: CaptureInput): Checkpoint;
21
+ export interface Reconciliation {
22
+ drifted: boolean;
23
+ reasons: string[];
24
+ }
25
+ /** Recompute the fingerprint and compare to a checkpoint. Any difference is drift. */
26
+ export declare function reconcile(checkpoint: Checkpoint, root: string, skills: Skill[]): Reconciliation;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Checkpoint capture + reconciliation. A checkpoint fingerprints the workspace (git HEAD, hashes of
3
+ * files the plan touches, the runtime config, and the versions of the skills in play). On resume we
4
+ * recompute and compare: any difference is DRIFT, which forces a replan rather than blindly continuing.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { systemClock } from '../util/clock.js';
10
+ import { resolveInJail } from '../tools/jail.js';
11
+ function sha(text) {
12
+ return createHash('sha256').update(text).digest('hex').slice(0, 16);
13
+ }
14
+ /** git HEAD sha (resolving a symbolic ref), or undefined when not a repo. The ref path is jailed to
15
+ * `.git` so a crafted HEAD (`ref: ../../etc/x`) can't read outside the workspace. */
16
+ export function gitHead(root) {
17
+ const gitDir = join(root, '.git');
18
+ const headPath = join(gitDir, 'HEAD');
19
+ if (!existsSync(headPath))
20
+ return undefined;
21
+ try {
22
+ const head = readFileSync(headPath, 'utf8').trim();
23
+ const m = /^ref:\s*(.+)$/.exec(head);
24
+ if (!m)
25
+ return head; // detached HEAD → raw sha
26
+ const ref = m[1].trim();
27
+ // Reject a ref that isn't a plain in-.git path (no absolute, no traversal).
28
+ if (ref.startsWith('/') || ref.includes('..') || ref.includes('\0'))
29
+ return head;
30
+ const refPath = resolveInJail(gitDir, ref);
31
+ return existsSync(refPath) ? readFileSync(refPath, 'utf8').trim() : head;
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ function hashFile(root, rel) {
38
+ try {
39
+ const abs = resolveInJail(root, rel);
40
+ return existsSync(abs) ? sha(readFileSync(abs, 'utf8')) : '∅'; // absent → a distinct, comparable marker
41
+ }
42
+ catch {
43
+ return '∅';
44
+ }
45
+ }
46
+ /** Files the plan touches (step inputs with a string `path`), plus the config, are fingerprinted. */
47
+ function planPaths(plan) {
48
+ const paths = new Set();
49
+ for (const step of plan?.steps ?? []) {
50
+ const input = step.input;
51
+ if (input && typeof input.path === 'string')
52
+ paths.add(input.path);
53
+ }
54
+ return [...paths];
55
+ }
56
+ function configHash(root) {
57
+ for (const rel of ['.ai-runtime/config.yaml', '.ai-runtime/config.yml', 'ai-runtime.yaml']) {
58
+ const abs = join(root, rel);
59
+ if (existsSync(abs)) {
60
+ try {
61
+ return sha(readFileSync(abs, 'utf8'));
62
+ }
63
+ catch {
64
+ return undefined;
65
+ }
66
+ }
67
+ }
68
+ return undefined;
69
+ }
70
+ export function captureCheckpoint(input) {
71
+ const clock = input.clock ?? systemClock;
72
+ const fileHashes = {};
73
+ for (const p of planPaths(input.plan))
74
+ fileHashes[p] = hashFile(input.root, p);
75
+ const skillVersions = {};
76
+ for (const s of input.skills)
77
+ skillVersions[s.id] = s.version;
78
+ const head = gitHead(input.root);
79
+ const cfg = configHash(input.root);
80
+ return {
81
+ at: clock.now(),
82
+ planVersion: input.plan?.version ?? 0,
83
+ ...(head !== undefined ? { gitHead: head } : {}),
84
+ fileHashes,
85
+ ...(cfg !== undefined ? { configHash: cfg } : {}),
86
+ skillVersions,
87
+ completedSteps: [...input.completedSteps],
88
+ };
89
+ }
90
+ /** Recompute the fingerprint and compare to a checkpoint. Any difference is drift. */
91
+ export function reconcile(checkpoint, root, skills) {
92
+ const reasons = [];
93
+ // Compare directly so an ADDITION (absent at capture, present now — e.g. git initialized, config
94
+ // added) also counts as drift, not just a change to something already captured.
95
+ const head = gitHead(root);
96
+ if (checkpoint.gitHead !== head)
97
+ reasons.push(`git HEAD changed (${checkpoint.gitHead ?? 'none'} → ${head ?? 'none'})`);
98
+ const cfg = configHash(root);
99
+ if (checkpoint.configHash !== cfg)
100
+ reasons.push('runtime config changed');
101
+ for (const [rel, hash] of Object.entries(checkpoint.fileHashes)) {
102
+ if (hashFile(root, rel) !== hash)
103
+ reasons.push(`file changed: ${rel}`);
104
+ }
105
+ const versions = new Map(skills.map((s) => [s.id, s.version]));
106
+ for (const [id, ver] of Object.entries(checkpoint.skillVersions)) {
107
+ const cur = versions.get(id);
108
+ if (cur === undefined)
109
+ reasons.push(`skill ${id} was removed`);
110
+ else if (cur !== ver)
111
+ reasons.push(`skill ${id} version changed (${ver} → ${cur})`);
112
+ }
113
+ return { drifted: reasons.length > 0, reasons };
114
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Execution — a persistent, resumable unit of orchestration work. It carries the plan, per-step
3
+ * progress, observations, artifacts, checkpoints, and a best-effort LEASE that keeps two owners from
4
+ * running the same execution (heartbeat-renewed while running; not a hard OS mutex — see store.ts).
5
+ * Waiting states (approval / clarification) persist across process exit and resume on answer.
6
+ */
7
+ import type { ExecutionPlan } from '../orchestration/plan.js';
8
+ import type { StepObservation } from '../orchestration/executor.js';
9
+ import type { ArtifactRef } from '../runtime/types.js';
10
+ export type ExecutionStatus = 'planned' | 'running' | 'paused' | 'waiting_for_input' | 'waiting_for_clarification' | 'completed' | 'failed' | 'cancelled';
11
+ /** A time-boxed claim on an execution by one process. */
12
+ export interface Lease {
13
+ owner: string;
14
+ heartbeatAt: number;
15
+ expiresAt: number;
16
+ }
17
+ /** A fingerprint of the workspace at a point in time, used to detect drift on resume. */
18
+ export interface Checkpoint {
19
+ at: number;
20
+ planVersion: number;
21
+ gitHead?: string;
22
+ fileHashes: Record<string, string>;
23
+ configHash?: string;
24
+ skillVersions: Record<string, string>;
25
+ completedSteps: string[];
26
+ }
27
+ /** What an execution is waiting for (approval or clarification), persisted so a restart can resume it. */
28
+ export interface PendingInput {
29
+ kind: 'approval' | 'clarification';
30
+ question?: string;
31
+ action?: string;
32
+ }
33
+ export interface Execution {
34
+ id: string;
35
+ requestId?: string;
36
+ goal: string;
37
+ mode: string;
38
+ planVersion: number;
39
+ plan?: ExecutionPlan;
40
+ status: ExecutionStatus;
41
+ completedSteps: string[];
42
+ observations: StepObservation[];
43
+ artifacts: ArtifactRef[];
44
+ checkpoints: Checkpoint[];
45
+ pending?: PendingInput;
46
+ createdAt: number;
47
+ updatedAt: number;
48
+ lease?: Lease;
49
+ }
50
+ export declare const RESUMABLE: ReadonlySet<ExecutionStatus>;
51
+ export declare const TERMINAL: ReadonlySet<ExecutionStatus>;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Execution — a persistent, resumable unit of orchestration work. It carries the plan, per-step
3
+ * progress, observations, artifacts, checkpoints, and a best-effort LEASE that keeps two owners from
4
+ * running the same execution (heartbeat-renewed while running; not a hard OS mutex — see store.ts).
5
+ * Waiting states (approval / clarification) persist across process exit and resume on answer.
6
+ */
7
+ export const RESUMABLE = new Set(['planned', 'running', 'paused', 'waiting_for_input', 'waiting_for_clarification']);
8
+ export const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ExecutionStore — persists executions and coordinates owners via a BEST-EFFORT lease. A live lease
3
+ * held by another owner blocks acquisition; an expired lease is stolen (process-death recovery);
4
+ * `commit` refuses to overwrite an execution a live different owner holds; `acquire` re-reads after
5
+ * writing to detect a lost race. This narrows — but on a shared filesystem does not fully eliminate —
6
+ * concurrent access (there is no OS file lock; a true hard mutex is post-1.0). Long runs must heartbeat
7
+ * to keep the lease alive. Idempotency: a requestId maps to one execution (reserve it under the store
8
+ * lock before running).
9
+ */
10
+ import type { Clock } from '../util/clock.js';
11
+ import type { Area } from '../store/area.js';
12
+ import type { Execution, ExecutionStatus } from './execution.js';
13
+ export interface ExecutionStoreOptions {
14
+ clock?: Clock;
15
+ owner?: string;
16
+ heartbeatMs?: number;
17
+ expiryMs?: number;
18
+ }
19
+ export interface AcquireResult {
20
+ ok: boolean;
21
+ execution?: Execution;
22
+ reason?: string;
23
+ }
24
+ export declare class ExecutionStore {
25
+ private readonly area;
26
+ readonly owner: string;
27
+ private readonly clock;
28
+ private readonly expiryMs;
29
+ readonly heartbeatMs: number;
30
+ private counter;
31
+ constructor(area: Area, options?: ExecutionStoreOptions);
32
+ get enabled(): boolean;
33
+ private nextId;
34
+ /** Create + persist a new execution (owned by this store). */
35
+ create(fields: Omit<Execution, 'id' | 'createdAt' | 'updatedAt' | 'completedSteps' | 'observations' | 'artifacts' | 'checkpoints'> & Partial<Pick<Execution, 'completedSteps' | 'observations' | 'artifacts' | 'checkpoints'>>): Execution;
36
+ save(exec: Execution): void;
37
+ get(id: string): Execution | undefined;
38
+ list(): Execution[];
39
+ /** Idempotency: find an existing execution for a caller requestId. */
40
+ byRequestId(requestId: string): Execution | undefined;
41
+ private leaseLive;
42
+ /** Claim an execution. Fails if a DIFFERENT owner holds a live lease; steals an expired one. Re-reads
43
+ * after writing to detect losing a concurrent steal (best-effort — no OS lock). */
44
+ acquire(id: string): AcquireResult;
45
+ /** Write only if we still hold the lease (or it's free/expired) — never clobber a live different owner. */
46
+ commit(exec: Execution): boolean;
47
+ /** Renew this owner's lease. Returns false if we no longer hold it. */
48
+ heartbeat(id: string): boolean;
49
+ release(id: string): void;
50
+ setStatus(id: string, status: ExecutionStatus): void;
51
+ delete(id: string): void;
52
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * ExecutionStore — persists executions and coordinates owners via a BEST-EFFORT lease. A live lease
3
+ * held by another owner blocks acquisition; an expired lease is stolen (process-death recovery);
4
+ * `commit` refuses to overwrite an execution a live different owner holds; `acquire` re-reads after
5
+ * writing to detect a lost race. This narrows — but on a shared filesystem does not fully eliminate —
6
+ * concurrent access (there is no OS file lock; a true hard mutex is post-1.0). Long runs must heartbeat
7
+ * to keep the lease alive. Idempotency: a requestId maps to one execution (reserve it under the store
8
+ * lock before running).
9
+ */
10
+ import { hostname } from 'node:os';
11
+ import { systemClock } from '../util/clock.js';
12
+ export class ExecutionStore {
13
+ area;
14
+ owner;
15
+ clock;
16
+ expiryMs;
17
+ heartbeatMs;
18
+ counter = 0;
19
+ constructor(area, options = {}) {
20
+ this.area = area;
21
+ this.clock = options.clock ?? systemClock;
22
+ this.expiryMs = options.expiryMs ?? 60_000;
23
+ this.heartbeatMs = options.heartbeatMs ?? 15_000;
24
+ this.owner = options.owner ?? `${hostname()}:${process.pid}:${Math.random().toString(36).slice(2, 8)}`;
25
+ }
26
+ get enabled() {
27
+ return this.area.enabled;
28
+ }
29
+ nextId() {
30
+ return `exec_${this.clock.now().toString(36)}_${process.pid.toString(36)}_${(this.counter += 1).toString(36)}`;
31
+ }
32
+ /** Create + persist a new execution (owned by this store). */
33
+ create(fields) {
34
+ const now = this.clock.now();
35
+ const exec = {
36
+ id: this.nextId(),
37
+ completedSteps: [],
38
+ observations: [],
39
+ artifacts: [],
40
+ checkpoints: [],
41
+ ...fields,
42
+ createdAt: now,
43
+ updatedAt: now,
44
+ lease: { owner: this.owner, heartbeatAt: now, expiresAt: now + this.expiryMs },
45
+ };
46
+ this.area.writeJson(exec.id, exec);
47
+ return exec;
48
+ }
49
+ save(exec) {
50
+ exec.updatedAt = this.clock.now();
51
+ this.area.writeJson(exec.id, exec);
52
+ }
53
+ get(id) {
54
+ return this.area.tryReadJson(id);
55
+ }
56
+ list() {
57
+ return this.area
58
+ .listIds()
59
+ .map((id) => this.area.tryReadJson(id))
60
+ .filter((e) => e !== undefined)
61
+ .sort((a, b) => b.updatedAt - a.updatedAt);
62
+ }
63
+ /** Idempotency: find an existing execution for a caller requestId. */
64
+ byRequestId(requestId) {
65
+ return this.list().find((e) => e.requestId === requestId);
66
+ }
67
+ leaseLive(exec, now) {
68
+ return !!exec.lease && exec.lease.expiresAt > now;
69
+ }
70
+ /** Claim an execution. Fails if a DIFFERENT owner holds a live lease; steals an expired one. Re-reads
71
+ * after writing to detect losing a concurrent steal (best-effort — no OS lock). */
72
+ acquire(id) {
73
+ const exec = this.get(id);
74
+ if (!exec)
75
+ return { ok: false, reason: 'not found' };
76
+ const now = this.clock.now();
77
+ if (this.leaseLive(exec, now) && exec.lease.owner !== this.owner) {
78
+ return { ok: false, reason: `held by ${exec.lease.owner} until ${exec.lease.expiresAt}` };
79
+ }
80
+ exec.lease = { owner: this.owner, heartbeatAt: now, expiresAt: now + this.expiryMs };
81
+ this.save(exec);
82
+ // Confirm we actually won: if another owner's write landed after ours, back off.
83
+ const confirmed = this.get(id);
84
+ if (confirmed?.lease?.owner !== this.owner)
85
+ return { ok: false, reason: 'lost the acquire race' };
86
+ return { ok: true, execution: confirmed };
87
+ }
88
+ /** Write only if we still hold the lease (or it's free/expired) — never clobber a live different owner. */
89
+ commit(exec) {
90
+ const disk = this.get(exec.id);
91
+ if (disk?.lease && disk.lease.owner !== this.owner && disk.lease.expiresAt > this.clock.now())
92
+ return false;
93
+ this.save(exec);
94
+ return true;
95
+ }
96
+ /** Renew this owner's lease. Returns false if we no longer hold it. */
97
+ heartbeat(id) {
98
+ const exec = this.get(id);
99
+ if (!exec || exec.lease?.owner !== this.owner)
100
+ return false;
101
+ const now = this.clock.now();
102
+ exec.lease.heartbeatAt = now;
103
+ exec.lease.expiresAt = now + this.expiryMs;
104
+ this.save(exec);
105
+ return true;
106
+ }
107
+ release(id) {
108
+ const exec = this.get(id);
109
+ if (exec && exec.lease?.owner === this.owner) {
110
+ delete exec.lease;
111
+ this.save(exec);
112
+ }
113
+ }
114
+ setStatus(id, status) {
115
+ const exec = this.get(id);
116
+ if (exec) {
117
+ exec.status = status;
118
+ this.save(exec);
119
+ }
120
+ }
121
+ delete(id) {
122
+ this.area.remove(id);
123
+ }
124
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Dynamic adapter generation. Given an OpenAPI analysis, produce a validated
3
+ * ProviderConfig that plugs into the existing HttpProvider — so "generating an adapter" means emitting
4
+ * CONFIG, not code, which keeps it safe: no arbitrary code is created or executed, and the result is
5
+ * validated before use. The operator still supplies the key via an env var and confirms the mapping.
6
+ */
7
+ import { type OpenApiAnalysis } from '../discovery/openapi.js';
8
+ import type { ProviderConfig } from '../types.js';
9
+ export interface GenerateOptions {
10
+ id?: string;
11
+ apiKeyEnv?: string;
12
+ models?: string[];
13
+ privacyClass?: 'cloud' | 'local';
14
+ }
15
+ /** Turn an OpenAPI analysis into a ProviderConfig. Throws if the spec yielded no usable mapping. */
16
+ export declare function generateProviderConfig(analysis: OpenApiAnalysis, overrides?: GenerateOptions): ProviderConfig;
17
+ export declare function generateProviderConfigFromFile(specPath: string, overrides?: GenerateOptions): ProviderConfig;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Dynamic adapter generation. Given an OpenAPI analysis, produce a validated
3
+ * ProviderConfig that plugs into the existing HttpProvider — so "generating an adapter" means emitting
4
+ * CONFIG, not code, which keeps it safe: no arbitrary code is created or executed, and the result is
5
+ * validated before use. The operator still supplies the key via an env var and confirms the mapping.
6
+ */
7
+ import { AIError } from '../core/fallback/errors.js';
8
+ import { analyzeOpenApiFile } from '../discovery/openapi.js';
9
+ /** Turn an OpenAPI analysis into a ProviderConfig. Throws if the spec yielded no usable mapping. */
10
+ export function generateProviderConfig(analysis, overrides = {}) {
11
+ const base = analysis.suggestedProviderConfig;
12
+ if (!base) {
13
+ throw new AIError('OpenAPI spec did not yield an OpenAI-compatible mapping — a manual adapter config is required', { category: 'CONFIG' });
14
+ }
15
+ const config = {
16
+ ...base,
17
+ ...(overrides.id ? { id: overrides.id } : {}),
18
+ ...(overrides.apiKeyEnv ? { apiKeyEnv: overrides.apiKeyEnv } : {}),
19
+ ...(overrides.privacyClass ? { privacyClass: overrides.privacyClass } : {}),
20
+ ...(overrides.models ? { models: overrides.models } : {}),
21
+ };
22
+ if (config.models === 'auto' && !overrides.models) {
23
+ // Discovery of the model list is not yet implemented; require an explicit model list to route.
24
+ delete config.models;
25
+ }
26
+ return config;
27
+ }
28
+ export function generateProviderConfigFromFile(specPath, overrides = {}) {
29
+ return generateProviderConfig(analyzeOpenApiFile(specPath), overrides);
30
+ }