@ambitresearch/genie 1.3.1

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 (301) hide show
  1. package/README.md +15 -0
  2. package/dist/auth/bearer.d.ts +49 -0
  3. package/dist/auth/bearer.d.ts.map +1 -0
  4. package/dist/auth/bearer.js +159 -0
  5. package/dist/auth/bearer.js.map +1 -0
  6. package/dist/auth/oauth/authorize.d.ts +19 -0
  7. package/dist/auth/oauth/authorize.d.ts.map +1 -0
  8. package/dist/auth/oauth/authorize.js +111 -0
  9. package/dist/auth/oauth/authorize.js.map +1 -0
  10. package/dist/auth/oauth/config.d.ts +10 -0
  11. package/dist/auth/oauth/config.d.ts.map +1 -0
  12. package/dist/auth/oauth/config.js +16 -0
  13. package/dist/auth/oauth/config.js.map +1 -0
  14. package/dist/auth/oauth/dcr.d.ts +15 -0
  15. package/dist/auth/oauth/dcr.d.ts.map +1 -0
  16. package/dist/auth/oauth/dcr.js +38 -0
  17. package/dist/auth/oauth/dcr.js.map +1 -0
  18. package/dist/auth/oauth/jwt.d.ts +13 -0
  19. package/dist/auth/oauth/jwt.d.ts.map +1 -0
  20. package/dist/auth/oauth/jwt.js +46 -0
  21. package/dist/auth/oauth/jwt.js.map +1 -0
  22. package/dist/auth/oauth/metadata.d.ts +9 -0
  23. package/dist/auth/oauth/metadata.d.ts.map +1 -0
  24. package/dist/auth/oauth/metadata.js +19 -0
  25. package/dist/auth/oauth/metadata.js.map +1 -0
  26. package/dist/auth/oauth/router.d.ts +32 -0
  27. package/dist/auth/oauth/router.d.ts.map +1 -0
  28. package/dist/auth/oauth/router.js +159 -0
  29. package/dist/auth/oauth/router.js.map +1 -0
  30. package/dist/auth/oauth/store.d.ts +55 -0
  31. package/dist/auth/oauth/store.d.ts.map +1 -0
  32. package/dist/auth/oauth/store.js +63 -0
  33. package/dist/auth/oauth/store.js.map +1 -0
  34. package/dist/auth/oauth/token.d.ts +21 -0
  35. package/dist/auth/oauth/token.d.ts.map +1 -0
  36. package/dist/auth/oauth/token.js +77 -0
  37. package/dist/auth/oauth/token.js.map +1 -0
  38. package/dist/auth/oidc/group-policy.d.ts +43 -0
  39. package/dist/auth/oidc/group-policy.d.ts.map +1 -0
  40. package/dist/auth/oidc/group-policy.js +54 -0
  41. package/dist/auth/oidc/group-policy.js.map +1 -0
  42. package/dist/auth/oidc/verifier.d.ts +42 -0
  43. package/dist/auth/oidc/verifier.d.ts.map +1 -0
  44. package/dist/auth/oidc/verifier.js +145 -0
  45. package/dist/auth/oidc/verifier.js.map +1 -0
  46. package/dist/auth/token-cli.d.ts +9 -0
  47. package/dist/auth/token-cli.d.ts.map +1 -0
  48. package/dist/auth/token-cli.js +98 -0
  49. package/dist/auth/token-cli.js.map +1 -0
  50. package/dist/cli.d.ts +3 -0
  51. package/dist/cli.d.ts.map +1 -0
  52. package/dist/cli.js +149 -0
  53. package/dist/cli.js.map +1 -0
  54. package/dist/config/redact.d.ts +20 -0
  55. package/dist/config/redact.d.ts.map +1 -0
  56. package/dist/config/redact.js +77 -0
  57. package/dist/config/redact.js.map +1 -0
  58. package/dist/config/secrets.d.ts +100 -0
  59. package/dist/config/secrets.d.ts.map +1 -0
  60. package/dist/config/secrets.js +130 -0
  61. package/dist/config/secrets.js.map +1 -0
  62. package/dist/framework/html.d.ts +103 -0
  63. package/dist/framework/html.d.ts.map +1 -0
  64. package/dist/framework/html.js +271 -0
  65. package/dist/framework/html.js.map +1 -0
  66. package/dist/framework/interface.d.ts +130 -0
  67. package/dist/framework/interface.d.ts.map +1 -0
  68. package/dist/framework/interface.js +112 -0
  69. package/dist/framework/interface.js.map +1 -0
  70. package/dist/framework/react.d.ts +133 -0
  71. package/dist/framework/react.d.ts.map +1 -0
  72. package/dist/framework/react.js +243 -0
  73. package/dist/framework/react.js.map +1 -0
  74. package/dist/framework/vue.d.ts +99 -0
  75. package/dist/framework/vue.d.ts.map +1 -0
  76. package/dist/framework/vue.js +375 -0
  77. package/dist/framework/vue.js.map +1 -0
  78. package/dist/index.d.ts +3 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +3 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/llm/client.d.ts +81 -0
  83. package/dist/llm/client.d.ts.map +1 -0
  84. package/dist/llm/client.js +141 -0
  85. package/dist/llm/client.js.map +1 -0
  86. package/dist/llm/component-response.d.ts +111 -0
  87. package/dist/llm/component-response.d.ts.map +1 -0
  88. package/dist/llm/component-response.js +167 -0
  89. package/dist/llm/component-response.js.map +1 -0
  90. package/dist/llm/generated-files.d.ts +10 -0
  91. package/dist/llm/generated-files.d.ts.map +1 -0
  92. package/dist/llm/generated-files.js +26 -0
  93. package/dist/llm/generated-files.js.map +1 -0
  94. package/dist/llm/prompts/CHANGELOG.md +70 -0
  95. package/dist/llm/prompts/generate-component.system.md +99 -0
  96. package/dist/llm/prompts/refine-component.system.md +112 -0
  97. package/dist/llm/prompts.d.ts +66 -0
  98. package/dist/llm/prompts.d.ts.map +1 -0
  99. package/dist/llm/prompts.js +78 -0
  100. package/dist/llm/prompts.js.map +1 -0
  101. package/dist/llm/retry.d.ts +183 -0
  102. package/dist/llm/retry.d.ts.map +1 -0
  103. package/dist/llm/retry.js +475 -0
  104. package/dist/llm/retry.js.map +1 -0
  105. package/dist/llm/schema.d.ts +129 -0
  106. package/dist/llm/schema.d.ts.map +1 -0
  107. package/dist/llm/schema.js +147 -0
  108. package/dist/llm/schema.js.map +1 -0
  109. package/dist/llm/validate.d.ts +107 -0
  110. package/dist/llm/validate.d.ts.map +1 -0
  111. package/dist/llm/validate.js +174 -0
  112. package/dist/llm/validate.js.map +1 -0
  113. package/dist/manifest/compiler.d.ts +112 -0
  114. package/dist/manifest/compiler.d.ts.map +1 -0
  115. package/dist/manifest/compiler.js +306 -0
  116. package/dist/manifest/compiler.js.map +1 -0
  117. package/dist/manifest/ensure.d.ts +46 -0
  118. package/dist/manifest/ensure.d.ts.map +1 -0
  119. package/dist/manifest/ensure.js +49 -0
  120. package/dist/manifest/ensure.js.map +1 -0
  121. package/dist/manifest/index.d.ts +13 -0
  122. package/dist/manifest/index.d.ts.map +1 -0
  123. package/dist/manifest/index.js +13 -0
  124. package/dist/manifest/index.js.map +1 -0
  125. package/dist/middleware/plan-guard.d.ts +167 -0
  126. package/dist/middleware/plan-guard.d.ts.map +1 -0
  127. package/dist/middleware/plan-guard.js +190 -0
  128. package/dist/middleware/plan-guard.js.map +1 -0
  129. package/dist/plans/index.d.ts +71 -0
  130. package/dist/plans/index.d.ts.map +1 -0
  131. package/dist/plans/index.js +252 -0
  132. package/dist/plans/index.js.map +1 -0
  133. package/dist/schemas/component.schema.json +114 -0
  134. package/dist/server.d.ts +96 -0
  135. package/dist/server.d.ts.map +1 -0
  136. package/dist/server.js +258 -0
  137. package/dist/server.js.map +1 -0
  138. package/dist/store/empty-manifest.d.ts +58 -0
  139. package/dist/store/empty-manifest.d.ts.map +1 -0
  140. package/dist/store/empty-manifest.js +59 -0
  141. package/dist/store/empty-manifest.js.map +1 -0
  142. package/dist/store/git-host.d.ts +120 -0
  143. package/dist/store/git-host.d.ts.map +1 -0
  144. package/dist/store/git-host.js +796 -0
  145. package/dist/store/git-host.js.map +1 -0
  146. package/dist/store/index.d.ts +5 -0
  147. package/dist/store/index.d.ts.map +1 -0
  148. package/dist/store/index.js +4 -0
  149. package/dist/store/index.js.map +1 -0
  150. package/dist/store/interface.d.ts +322 -0
  151. package/dist/store/interface.d.ts.map +1 -0
  152. package/dist/store/interface.js +100 -0
  153. package/dist/store/interface.js.map +1 -0
  154. package/dist/store/kit-files.d.ts +88 -0
  155. package/dist/store/kit-files.d.ts.map +1 -0
  156. package/dist/store/kit-files.js +199 -0
  157. package/dist/store/kit-files.js.map +1 -0
  158. package/dist/store/local.d.ts +70 -0
  159. package/dist/store/local.d.ts.map +1 -0
  160. package/dist/store/local.js +651 -0
  161. package/dist/store/local.js.map +1 -0
  162. package/dist/store/manifest.d.ts +44 -0
  163. package/dist/store/manifest.d.ts.map +1 -0
  164. package/dist/store/manifest.js +116 -0
  165. package/dist/store/manifest.js.map +1 -0
  166. package/dist/store/viewer-assets.d.ts +20 -0
  167. package/dist/store/viewer-assets.d.ts.map +1 -0
  168. package/dist/store/viewer-assets.js +118 -0
  169. package/dist/store/viewer-assets.js.map +1 -0
  170. package/dist/sync/anchor.d.ts +125 -0
  171. package/dist/sync/anchor.d.ts.map +1 -0
  172. package/dist/sync/anchor.js +210 -0
  173. package/dist/sync/anchor.js.map +1 -0
  174. package/dist/sync/index.d.ts +15 -0
  175. package/dist/sync/index.d.ts.map +1 -0
  176. package/dist/sync/index.js +15 -0
  177. package/dist/sync/index.js.map +1 -0
  178. package/dist/sync/orchestrator.d.ts +160 -0
  179. package/dist/sync/orchestrator.d.ts.map +1 -0
  180. package/dist/sync/orchestrator.js +288 -0
  181. package/dist/sync/orchestrator.js.map +1 -0
  182. package/dist/tools/bind_kit.d.ts +29 -0
  183. package/dist/tools/bind_kit.d.ts.map +1 -0
  184. package/dist/tools/bind_kit.js +70 -0
  185. package/dist/tools/bind_kit.js.map +1 -0
  186. package/dist/tools/conjure.d.ts +141 -0
  187. package/dist/tools/conjure.d.ts.map +1 -0
  188. package/dist/tools/conjure.js +655 -0
  189. package/dist/tools/conjure.js.map +1 -0
  190. package/dist/tools/conjure_screen.d.ts +174 -0
  191. package/dist/tools/conjure_screen.d.ts.map +1 -0
  192. package/dist/tools/conjure_screen.js +458 -0
  193. package/dist/tools/conjure_screen.js.map +1 -0
  194. package/dist/tools/create_kit.d.ts +32 -0
  195. package/dist/tools/create_kit.d.ts.map +1 -0
  196. package/dist/tools/create_kit.js +131 -0
  197. package/dist/tools/create_kit.js.map +1 -0
  198. package/dist/tools/create_project.d.ts +267 -0
  199. package/dist/tools/create_project.d.ts.map +1 -0
  200. package/dist/tools/create_project.js +546 -0
  201. package/dist/tools/create_project.js.map +1 -0
  202. package/dist/tools/delete_files.d.ts +93 -0
  203. package/dist/tools/delete_files.d.ts.map +1 -0
  204. package/dist/tools/delete_files.js +276 -0
  205. package/dist/tools/delete_files.js.map +1 -0
  206. package/dist/tools/delete_project.d.ts +43 -0
  207. package/dist/tools/delete_project.d.ts.map +1 -0
  208. package/dist/tools/delete_project.js +118 -0
  209. package/dist/tools/delete_project.js.map +1 -0
  210. package/dist/tools/get_kit.d.ts +28 -0
  211. package/dist/tools/get_kit.d.ts.map +1 -0
  212. package/dist/tools/get_kit.js +95 -0
  213. package/dist/tools/get_kit.js.map +1 -0
  214. package/dist/tools/get_project.d.ts +37 -0
  215. package/dist/tools/get_project.d.ts.map +1 -0
  216. package/dist/tools/get_project.js +71 -0
  217. package/dist/tools/get_project.js.map +1 -0
  218. package/dist/tools/list_components.d.ts +42 -0
  219. package/dist/tools/list_components.d.ts.map +1 -0
  220. package/dist/tools/list_components.js +125 -0
  221. package/dist/tools/list_components.js.map +1 -0
  222. package/dist/tools/list_files.d.ts +32 -0
  223. package/dist/tools/list_files.d.ts.map +1 -0
  224. package/dist/tools/list_files.js +109 -0
  225. package/dist/tools/list_files.js.map +1 -0
  226. package/dist/tools/list_kits.d.ts +14 -0
  227. package/dist/tools/list_kits.d.ts.map +1 -0
  228. package/dist/tools/list_kits.js +41 -0
  229. package/dist/tools/list_kits.js.map +1 -0
  230. package/dist/tools/list_projects.d.ts +23 -0
  231. package/dist/tools/list_projects.d.ts.map +1 -0
  232. package/dist/tools/list_projects.js +66 -0
  233. package/dist/tools/list_projects.js.map +1 -0
  234. package/dist/tools/plan.d.ts +16 -0
  235. package/dist/tools/plan.d.ts.map +1 -0
  236. package/dist/tools/plan.js +146 -0
  237. package/dist/tools/plan.js.map +1 -0
  238. package/dist/tools/preview.d.ts +289 -0
  239. package/dist/tools/preview.d.ts.map +1 -0
  240. package/dist/tools/preview.js +672 -0
  241. package/dist/tools/preview.js.map +1 -0
  242. package/dist/tools/read_file.d.ts +40 -0
  243. package/dist/tools/read_file.d.ts.map +1 -0
  244. package/dist/tools/read_file.js +114 -0
  245. package/dist/tools/read_file.js.map +1 -0
  246. package/dist/tools/refine.d.ts +220 -0
  247. package/dist/tools/refine.d.ts.map +1 -0
  248. package/dist/tools/refine.js +501 -0
  249. package/dist/tools/refine.js.map +1 -0
  250. package/dist/tools/validate.d.ts +74 -0
  251. package/dist/tools/validate.d.ts.map +1 -0
  252. package/dist/tools/validate.js +224 -0
  253. package/dist/tools/validate.js.map +1 -0
  254. package/dist/tools/write_files.d.ts +158 -0
  255. package/dist/tools/write_files.d.ts.map +1 -0
  256. package/dist/tools/write_files.js +541 -0
  257. package/dist/tools/write_files.js.map +1 -0
  258. package/dist/transport.d.ts +77 -0
  259. package/dist/transport.d.ts.map +1 -0
  260. package/dist/transport.js +448 -0
  261. package/dist/transport.js.map +1 -0
  262. package/dist/ui/card-asset-broker.d.ts +42 -0
  263. package/dist/ui/card-asset-broker.d.ts.map +1 -0
  264. package/dist/ui/card-asset-broker.js +479 -0
  265. package/dist/ui/card-asset-broker.js.map +1 -0
  266. package/dist/ui/grid-resource.d.ts +252 -0
  267. package/dist/ui/grid-resource.d.ts.map +1 -0
  268. package/dist/ui/grid-resource.js +680 -0
  269. package/dist/ui/grid-resource.js.map +1 -0
  270. package/dist/ui/viewer-static/index.html +95 -0
  271. package/dist/ui/viewer-static/viewer.css +451 -0
  272. package/dist/ui/viewer-static/viewer.js +1405 -0
  273. package/dist/validate/full-scan.d.ts +87 -0
  274. package/dist/validate/full-scan.d.ts.map +1 -0
  275. package/dist/validate/full-scan.js +299 -0
  276. package/dist/validate/full-scan.js.map +1 -0
  277. package/dist/validate/index.d.ts +15 -0
  278. package/dist/validate/index.d.ts.map +1 -0
  279. package/dist/validate/index.js +18 -0
  280. package/dist/validate/index.js.map +1 -0
  281. package/dist/validate/marker.d.ts +89 -0
  282. package/dist/validate/marker.d.ts.map +1 -0
  283. package/dist/validate/marker.js +87 -0
  284. package/dist/validate/marker.js.map +1 -0
  285. package/dist/validate/phash.d.ts +135 -0
  286. package/dist/validate/phash.d.ts.map +1 -0
  287. package/dist/validate/phash.js +307 -0
  288. package/dist/validate/phash.js.map +1 -0
  289. package/dist/validate/render.d.ts +38 -0
  290. package/dist/validate/render.d.ts.map +1 -0
  291. package/dist/validate/render.js +155 -0
  292. package/dist/validate/render.js.map +1 -0
  293. package/dist/watch/index.d.ts +12 -0
  294. package/dist/watch/index.d.ts.map +1 -0
  295. package/dist/watch/index.js +12 -0
  296. package/dist/watch/index.js.map +1 -0
  297. package/dist/watch/watcher.d.ts +65 -0
  298. package/dist/watch/watcher.d.ts.map +1 -0
  299. package/dist/watch/watcher.js +238 -0
  300. package/dist/watch/watcher.js.map +1 -0
  301. package/package.json +70 -0
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Retry/backoff middleware (M2-06, DRO-253) — wraps an async LLM handler and
3
+ * retries transient failures with exponential-jittered backoff, honouring
4
+ * `Retry-After`, capped by `GENIE_LLM_RETRY_MAX`, surfacing a typed
5
+ * `RateLimitedError | TransientError` on exhaustion, and logging one line
6
+ * per retry to **stderr** (never stdout — on the stdio MCP transport, stdout
7
+ * IS the JSON-RPC protocol stream, and any stray line there corrupts every
8
+ * client's message framing; see `client.ts` / `plans/index.ts` /
9
+ * `middleware/plan-guard.ts` for the same convention).
10
+ *
11
+ * Composed as `withRetry(createChatCompletion)` at the call sites of every
12
+ * future generation verb (`conjure` / `refine`, M2-03 / M2-04). Kept as a
13
+ * generic HOF rather than baked into `client.ts` for two reasons:
14
+ * 1. `client.ts` deliberately makes exactly one attempt per call (see the
15
+ * comment on `maxRetries: 0` there) so retry policy lives in exactly
16
+ * one place — this file — where its "max N attempts" and "each attempt
17
+ * logged" guarantees can't be silently double-counted by SDK-internal
18
+ * retries happening a layer below.
19
+ * 2. A future non-chat handler (say, an embedding call) can be wrapped
20
+ * the same way without knowing anything about the chat completions
21
+ * surface.
22
+ *
23
+ * Retryability discriminates by the openai SDK's real error class hierarchy
24
+ * (`RateLimitError` / `InternalServerError` / `APIConnectionError` /
25
+ * `APIConnectionTimeoutError`) rather than by string-matching on `err.code`
26
+ * or `err.message`: global `fetch` wraps a socket-reset root cause as
27
+ * `TypeError('fetch failed')` and the SDK re-wraps that as an
28
+ * `APIConnectionError` — meaning the ECONNRESET code lives two levels deep
29
+ * at `err.cause.cause.code` and cannot be found by looking at the outermost
30
+ * error's own properties. The instanceof checks are the primary discriminator;
31
+ * the deep-cause code check is a belt-and-braces fallback for any transport
32
+ * that surfaces a raw node error without the SDK wrapping (e.g. a pre-request
33
+ * DNS failure, or a future alternate transport).
34
+ */
35
+ import { APIConnectionError, APIConnectionTimeoutError, APIError, InternalServerError, RateLimitError, } from "openai";
36
+ // ─── Constants ───────────────────────────────────────────────────────────
37
+ /** Env var that overrides the default retry ceiling (AC5). */
38
+ export const RETRY_MAX_ENV = "GENIE_LLM_RETRY_MAX";
39
+ /** Default retry ceiling when the env var is unset / invalid (AC5). */
40
+ export const DEFAULT_RETRY_MAX = 3;
41
+ /** Base backoff delay: 1 s (AC4). */
42
+ export const BASE_DELAY_MS = 1_000;
43
+ /** Backoff cap: 30 s (AC4). */
44
+ export const MAX_DELAY_MS = 30_000;
45
+ /** Jitter multiplier applied symmetrically around the exponential value (AC4: ±20 %). */
46
+ export const JITTER_RATIO = 0.2;
47
+ /**
48
+ * Hard ceiling on an upstream-honoured `Retry-After` value once it's used as
49
+ * an actual `sleep()` duration (Copilot review on PR #126 / DRO-253).
50
+ * `MAX_DELAY_MS` only bounds OUR OWN computed exponential backoff — a value
51
+ * entirely within this module's control — but an honoured `Retry-After` is
52
+ * trusted verbatim by `parseRetryAfter`. A misconfigured or hostile upstream
53
+ * sending e.g. `Retry-After: 86400` (one day) would otherwise stall the
54
+ * in-flight call — and on the stdio MCP transport, the whole in-flight
55
+ * request — for that entire duration: `GENIE_LLM_REQUEST_TIMEOUT_MS` only
56
+ * bounds each individual HTTP request, not the sleep between retries.
57
+ *
58
+ * Set to 60 s — double `MAX_DELAY_MS` — so an explicit upstream signal is
59
+ * still granted more deference than our own guessed-at exponential
60
+ * schedule, while remaining bounded to a duration reasonable for a
61
+ * synchronous MCP tool call. (The reviewer described this figure as the
62
+ * `openai` SDK's own clamp; the SDK actually applies no such clamp — see
63
+ * `retryRequest`/`calculateDefaultRetryTimeoutMillis` in
64
+ * `openai/client.js`, which honours an unclamped `Retry-After` exactly as
65
+ * this module did before this fix. 60 s is picked on its own merits here,
66
+ * not inherited from upstream SDK behaviour.)
67
+ *
68
+ * Only the `sleep()` duration is clamped. `RateLimitedError.retryAfterMs`
69
+ * (surfaced to the caller on exhaustion, see `withRetry` below)
70
+ * deliberately keeps the TRUE, unclamped upstream value — that field's
71
+ * contract is "what did the upstream ask for" (e.g. so a UI layer can show
72
+ * "come back tomorrow"), not "how long did we actually wait".
73
+ */
74
+ export const MAX_RETRY_AFTER_MS = 60_000;
75
+ /**
76
+ * Node socket / DNS error codes that indicate a transient network failure
77
+ * worth retrying (AC2), when found on a RAW node error that bypasses the
78
+ * openai SDK's own error classes entirely (see the fallback branch at the
79
+ * bottom of `classifyError`). Kept intentionally narrow.
80
+ */
81
+ const RETRYABLE_NETWORK_CODES = new Set(["ECONNRESET", "ETIMEDOUT"]);
82
+ /**
83
+ * Node error codes that indicate a PERMANENT connection failure on an
84
+ * `APIConnectionError` — retrying won't help because the target
85
+ * fundamentally isn't reachable, as opposed to a transient peer reset or
86
+ * timeout (Copilot review on PR #126 / DRO-253). Used as a DENYLIST
87
+ * (exclude these, retry everything else) rather than an allowlist,
88
+ * deliberately:
89
+ *
90
+ * An earlier version of this fix tried the allowlist shape — only retry
91
+ * `APIConnectionError` when its cause code is in `RETRYABLE_NETWORK_CODES`
92
+ * — and it broke this file's OWN real-socket-destroy integration test
93
+ * (`recovers from a mid-flight socket destroy`, see `retry.test.ts`): a
94
+ * genuine peer-reset under Node's real `fetch`/undici stack surfaces as
95
+ * `SocketError` with code `UND_ERR_SOCKET`, one cause-level shallower than
96
+ * — and spelled entirely differently from — the `ECONNRESET` the mocked
97
+ * unit tests assume. An allowlist keyed on the wrong/incomplete code set
98
+ * would silently stop retrying the exact real-world failure this
99
+ * middleware exists to recover from. A denylist degrades safely instead:
100
+ * an unrecognised or future code still retries (matching this module's
101
+ * original, evidence-backed default), while the two well-understood
102
+ * permanent cases below stop burning the retry budget on a call that will
103
+ * never succeed:
104
+ * - `ECONNREFUSED` — nothing is listening at the target address; a
105
+ * retry a second later won't change that.
106
+ * - `ENOTFOUND` — DNS has no record for the hostname at all (distinct
107
+ * from `EAI_AGAIN`, "temporary failure in name resolution", which IS
108
+ * transient by definition and deliberately stays off this list).
109
+ *
110
+ * TLS handshake rejection is NOT enumerated here: unlike the two codes
111
+ * above, there's no single stable, version-independent error code to key
112
+ * on across Node/undici/OpenSSL versions — guessing one wrong would give
113
+ * false confidence rather than real protection. It falls through to the
114
+ * safe "retry by default" behaviour like any other unrecognised code.
115
+ */
116
+ const PERMANENT_CONNECTION_CODES = new Set(["ECONNREFUSED", "ENOTFOUND"]);
117
+ // ─── Errors ──────────────────────────────────────────────────────────────
118
+ /**
119
+ * Thrown when a 429 rate-limit response was retried the full budget and the
120
+ * upstream never gave a non-429 answer (AC6). Kept distinct from
121
+ * `TransientError` so callers that want to surface `ERR_RATE_LIMITED`
122
+ * separately from generic-transient-failure can branch on the type — matches
123
+ * the FR-046 PRD note about surfacing rate-limits distinctly.
124
+ *
125
+ * `retryAfterMs` carries the last `Retry-After` value (in milliseconds) the
126
+ * upstream sent, so a UI layer can show "come back in ~30 s" without having
127
+ * to unwrap the cause itself. Undefined if the final response carried no
128
+ * `Retry-After` header.
129
+ */
130
+ export class RateLimitedError extends Error {
131
+ attempts;
132
+ cause;
133
+ retryAfterMs;
134
+ name = "RateLimitedError";
135
+ constructor(message, attempts, cause, retryAfterMs) {
136
+ super(message, { cause });
137
+ this.attempts = attempts;
138
+ this.cause = cause;
139
+ this.retryAfterMs = retryAfterMs;
140
+ }
141
+ }
142
+ /**
143
+ * Thrown when a 5xx / network error was retried the full budget and the
144
+ * upstream never recovered (AC6). Callers that don't care about the
145
+ * distinction from `RateLimitedError` should catch `TransientError |
146
+ * RateLimitedError` as a union; both extend `Error` and both preserve the
147
+ * originating cause on `.cause`.
148
+ */
149
+ export class TransientError extends Error {
150
+ attempts;
151
+ cause;
152
+ name = "TransientError";
153
+ constructor(message, attempts, cause) {
154
+ super(message, { cause });
155
+ this.attempts = attempts;
156
+ this.cause = cause;
157
+ }
158
+ }
159
+ // ─── Env resolution (AC5) ────────────────────────────────────────────────
160
+ /**
161
+ * Resolve `GENIE_LLM_RETRY_MAX`, falling back to `DEFAULT_RETRY_MAX` when
162
+ * unset, blank, non-numeric, or negative. Mirrors the established fallback
163
+ * shape of `resolveTimeoutMs` in `client.ts` and `getPlanTTL` in
164
+ * `plans/index.ts`.
165
+ *
166
+ * A concrete `"0"` is a legitimate operator choice — one attempt, no retries
167
+ * — and must NOT be collapsed with "unset". An operator running against a
168
+ * test lab where they own the retry policy externally sets 0 and expects
169
+ * exactly 1 attempt, not 4.
170
+ *
171
+ * Requires the trimmed value to be ENTIRELY digits (Copilot review on PR
172
+ * #126 / DRO-253): `parseInt` alone silently accepts a leading-numeric
173
+ * prefix — `parseInt("5s", 10)` and `parseInt("3.5", 10)` both parse as
174
+ * valid (5 and 3 respectively) despite neither being a clean integer,
175
+ * contradicting this function's own "non-numeric → default" contract and
176
+ * silently misconfiguring an operator who fat-fingered the env var (e.g.
177
+ * copy-pasted a value with a trailing unit). The regex guard rejects
178
+ * anything that isn't purely `[0-9]+` before `parseInt` ever sees it.
179
+ */
180
+ export function resolveRetryMax(env = process.env) {
181
+ const raw = env[RETRY_MAX_ENV];
182
+ if (raw !== undefined) {
183
+ const trimmed = raw.trim();
184
+ if (/^\d+$/.test(trimmed)) {
185
+ const parsed = parseInt(trimmed, 10);
186
+ if (!isNaN(parsed) && parsed >= 0) {
187
+ return parsed;
188
+ }
189
+ }
190
+ }
191
+ return DEFAULT_RETRY_MAX;
192
+ }
193
+ // ─── Retry-After parsing (AC3) ───────────────────────────────────────────
194
+ /**
195
+ * Parse an HTTP `Retry-After` header (or the non-standard `retry-after-ms`)
196
+ * into milliseconds (AC3).
197
+ *
198
+ * Precedence:
199
+ * 1. `retry-after-ms` (millisecond precision, non-standard but common)
200
+ * 2. `retry-after` as fractional seconds (RFC 9110 delay-seconds form)
201
+ * 3. `retry-after` as HTTP-date (RFC 9110 date form) → delta from now,
202
+ * clamped to 0 if the date is in the past.
203
+ *
204
+ * Returns `null` for absent, empty, or unparseable values — the caller then
205
+ * falls back to the jittered exponential schedule (AC4).
206
+ *
207
+ * The two-level parse (seconds first, HTTP-date fallback) matches the openai
208
+ * SDK's own `retryRequest` logic in `openai/client.js` so anyone already
209
+ * relying on either header form gets consistent behaviour whether they hit
210
+ * this wrapper or the SDK's own internal retry.
211
+ */
212
+ export function parseRetryAfter(headers) {
213
+ if (!headers)
214
+ return null;
215
+ // 1. Millisecond-precision (non-standard, but the openai SDK recognises it).
216
+ const ms = headers.get("retry-after-ms");
217
+ if (ms) {
218
+ const parsed = parseFloat(ms);
219
+ if (!Number.isNaN(parsed) && parsed >= 0) {
220
+ return parsed;
221
+ }
222
+ }
223
+ // 2/3. Standard `retry-after`.
224
+ const raw = headers.get("retry-after");
225
+ if (!raw)
226
+ return null;
227
+ // Try delay-seconds form first — RFC 9110 §10.2.3 permits an integer
228
+ // (technically the ABNF is non-negative-integer, but many servers send
229
+ // fractional values and the openai SDK accepts them via parseFloat, so we
230
+ // match that lenient posture).
231
+ //
232
+ // Use a regex to distinguish "genuinely numeric" from "starts with a
233
+ // number then something else": `parseFloat("Wed 21 Oct")` returns
234
+ // `NaN`, but `parseFloat("21 Oct 2025")` returns `21`, which would
235
+ // silently interpret an HTTP-date as "21 seconds" if the two branches
236
+ // weren't strictly ordered. Round-trip stringification (`` `${n}` ===
237
+ // raw ``) is subtly wrong for values like `"7.0"` → `parseFloat` → `7`
238
+ // → `"7"`, which doesn't match; the regex sidesteps that whole class of
239
+ // bug by only recognising strings that ARE the numeric literal.
240
+ const trimmed = raw.trim();
241
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
242
+ const seconds = parseFloat(trimmed);
243
+ if (!Number.isNaN(seconds)) {
244
+ return Math.max(0, seconds * 1_000);
245
+ }
246
+ }
247
+ // Fall back to HTTP-date form.
248
+ const dateMs = Date.parse(raw);
249
+ if (!Number.isNaN(dateMs)) {
250
+ return Math.max(0, dateMs - Date.now());
251
+ }
252
+ return null;
253
+ }
254
+ // ─── Jittered exponential backoff (AC4) ──────────────────────────────────
255
+ /**
256
+ * Compute the sleep duration for the given retry `attempt` (1-indexed —
257
+ * `attempt=1` is the delay before the first retry, i.e. after the initial
258
+ * attempt failed). Base 1 s, doubles per attempt, capped at 30 s, then a
259
+ * uniform ±20 % jitter applied on top (AC4).
260
+ *
261
+ * The cap is applied to the exponential value BEFORE the jitter multiplier,
262
+ * so at high attempt counts the delay is still bounded by
263
+ * `MAX_DELAY_MS * (1 + JITTER_RATIO)` on paper — but then clamped back to
264
+ * `MAX_DELAY_MS` a second time, so the effective ceiling is exactly 30 s
265
+ * (a strict interpretation of "cap 30 s" in the spec).
266
+ *
267
+ * `random` is injectable so tests can drive deterministic values without
268
+ * needing to stub `Math.random` globally.
269
+ */
270
+ export function jitteredBackoff(attempt, random = Math.random) {
271
+ const exponential = BASE_DELAY_MS * Math.pow(2, attempt - 1);
272
+ const capped = Math.min(exponential, MAX_DELAY_MS);
273
+ const jitterMultiplier = 1 - JITTER_RATIO + random() * (JITTER_RATIO * 2);
274
+ return Math.min(Math.round(capped * jitterMultiplier), MAX_DELAY_MS);
275
+ }
276
+ // ─── Retryability discrimination (AC2) ───────────────────────────────────
277
+ /**
278
+ * Walk `error.cause` down to `MAX_DEPTH` levels, looking for a node-style
279
+ * `.code` property (`ECONNRESET`, `ETIMEDOUT`, …). Global `fetch` wraps a
280
+ * socket-reset root cause as `TypeError('fetch failed', { cause: rootErr })`
281
+ * and the openai SDK re-wraps that as `APIConnectionError`, meaning the real
282
+ * code lives at `err.cause.cause.code` — two levels below where a naive
283
+ * `err.code` check would look. Depth-limited to prevent a pathological
284
+ * self-referential cause chain from looping.
285
+ */
286
+ function findCauseCode(error) {
287
+ const MAX_DEPTH = 5;
288
+ let current = error;
289
+ for (let i = 0; i < MAX_DEPTH && current instanceof Error; i++) {
290
+ const code = current.code;
291
+ if (typeof code === "string")
292
+ return code;
293
+ current = current.cause;
294
+ }
295
+ return undefined;
296
+ }
297
+ function classifyError(error) {
298
+ if (error instanceof RateLimitError)
299
+ return "rateLimit";
300
+ if (error instanceof InternalServerError)
301
+ return "transient";
302
+ // Checked BEFORE the general APIConnectionError branch below because
303
+ // APIConnectionTimeoutError extends APIConnectionError — a client-side
304
+ // timeout has no wire-response context to inspect a cause code from, and
305
+ // maps directly to AC2's "ETIMEDOUT" bucket, so it's unconditionally
306
+ // retryable without needing the narrower cause-code check the plain
307
+ // APIConnectionError branch below applies.
308
+ if (error instanceof APIConnectionTimeoutError)
309
+ return "transient";
310
+ if (error instanceof APIConnectionError) {
311
+ // Deny-list, not allow-list (Copilot review on PR #126 / DRO-253 asked
312
+ // for *some* narrowing here — see `PERMANENT_CONNECTION_CODES`'s
313
+ // docstring for why an allowlist keyed on `RETRYABLE_NETWORK_CODES`
314
+ // broke this file's own real-socket-destroy integration test). Retry
315
+ // by default — matching this module's original, evidence-backed
316
+ // stance that the SDK only raises this class for wire-level failures
317
+ // where the request never reached a productive response — UNLESS the
318
+ // cause chain names one of the two well-understood permanent codes,
319
+ // in which case stop burning the retry budget on a call that will
320
+ // never succeed.
321
+ const networkCode = findCauseCode(error);
322
+ return networkCode && PERMANENT_CONNECTION_CODES.has(networkCode) ? "nonRetryable" : "transient";
323
+ }
324
+ // Non-openai APIError with a 5xx status (defensive — future SDK versions
325
+ // may add subclasses we haven't enumerated).
326
+ if (error instanceof APIError && typeof error.status === "number" && error.status >= 500) {
327
+ return "transient";
328
+ }
329
+ // Deep-cause network code check — catches raw node errors that bypass the
330
+ // openai SDK entirely (e.g. a pre-request rejection from a custom fetch).
331
+ const code = findCauseCode(error);
332
+ if (code && RETRYABLE_NETWORK_CODES.has(code))
333
+ return "transient";
334
+ return "nonRetryable";
335
+ }
336
+ /**
337
+ * Pull an HTTP status out of an openai APIError for logging. Non-openai
338
+ * errors and network-connection errors don't have one, so return null (AC7).
339
+ */
340
+ function statusOf(error) {
341
+ if (error instanceof APIError) {
342
+ const status = error.status;
343
+ return typeof status === "number" ? status : null;
344
+ }
345
+ return null;
346
+ }
347
+ /**
348
+ * Pull `Retry-After` out of an openai APIError's response headers (AC3).
349
+ * Only present on errors carrying wire-response context (i.e. not on
350
+ * `APIConnectionError` / `APIConnectionTimeoutError`, which never received
351
+ * a response). Returns null when absent or unparseable.
352
+ */
353
+ function retryAfterOf(error) {
354
+ if (error instanceof APIError && error.headers) {
355
+ return parseRetryAfter(error.headers);
356
+ }
357
+ return null;
358
+ }
359
+ // ─── Structured audit log (AC7) ──────────────────────────────────────────
360
+ /**
361
+ * Emit one `llm.retry` audit line per retry attempt to stderr — never
362
+ * stdout. On the stdio MCP transport stdout IS the JSON-RPC protocol
363
+ * stream, and one stray line there corrupts every client's message framing
364
+ * (see the same-convention comment in `client.ts` on the sibling
365
+ * `llm.chat_completion` log).
366
+ *
367
+ * Includes `attempt` (1-indexed, matches AC7's contract), `status` (HTTP
368
+ * status if the error was an APIError; null for network failures), and
369
+ * `retryAfter` (milliseconds honoured, null if none). Deliberately does NOT
370
+ * include the request body, prompt, or completion text — the log is what an
371
+ * operator inspects afterwards, and leaking prompt content across every
372
+ * transient retry would be a real audit-log data leak.
373
+ */
374
+ function logRetry(attempt, status, retryAfterMs) {
375
+ const line = JSON.stringify({
376
+ event: "llm.retry",
377
+ attempt,
378
+ status,
379
+ retryAfter: retryAfterMs,
380
+ });
381
+ process.stderr.write(line + "\n");
382
+ }
383
+ /**
384
+ * Default `setTimeout`-backed sleep. Extracted so both the default option
385
+ * value and any explicit `sleep` injection route through a single named
386
+ * function — makes stack traces in fake-timer tests self-explanatory.
387
+ */
388
+ function defaultSleep(ms) {
389
+ return new Promise((resolve) => setTimeout(resolve, ms));
390
+ }
391
+ /**
392
+ * Wrap an async LLM handler with retry/backoff (AC1). Returns a function
393
+ * with the SAME argument list and return type as the input handler, so
394
+ * `withRetry(createChatCompletion)` is a drop-in replacement for
395
+ * `createChatCompletion` at every call site.
396
+ *
397
+ * Order of operations per attempt:
398
+ * 1. Call the handler.
399
+ * 2. On success → return the result immediately.
400
+ * 3. On failure → classify (AC2). Non-retryable errors propagate as-is
401
+ * (the original error object, not a wrapped one — a caller that
402
+ * already has instanceof/status handling for BadRequestError et al.
403
+ * keeps working unchanged).
404
+ * 4. On retryable failure with remaining budget → emit an `llm.retry`
405
+ * log line (AC7), sleep for `Retry-After` (AC3) or the jittered
406
+ * backoff (AC4), then loop.
407
+ * 5. On retryable failure with exhausted budget → throw
408
+ * `RateLimitedError` (if the tail-cause was a 429) or `TransientError`
409
+ * (otherwise), preserving the original error on `.cause` (AC6).
410
+ *
411
+ * A handler that transitions from one retryable kind to another across
412
+ * attempts (say 429 → 503 → 500) throws whichever kind matches the LAST
413
+ * failing attempt, on the reasoning that "what the caller sees now" is the
414
+ * most useful discriminator for their fallback branch — a 429-turned-503
415
+ * indicates a genuinely degraded upstream, not a persistent rate-limit.
416
+ */
417
+ export function withRetry(handler, opts = {}) {
418
+ const sleep = opts.sleep ?? defaultSleep;
419
+ const random = opts.random ?? Math.random;
420
+ const maxRetries = opts.maxRetries ?? resolveRetryMax(opts.env ?? process.env);
421
+ return async (...args) => {
422
+ let lastError;
423
+ // attempt 0 is the initial attempt; attempts 1..maxRetries are retries.
424
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
425
+ try {
426
+ return await handler(...args);
427
+ }
428
+ catch (error) {
429
+ lastError = error;
430
+ const kind = classifyError(error);
431
+ if (kind === "nonRetryable") {
432
+ throw error;
433
+ }
434
+ const remainingRetries = maxRetries - attempt;
435
+ if (remainingRetries <= 0) {
436
+ // Budget exhausted — surface a typed error. Kind is derived from
437
+ // the LAST failing attempt, on the reasoning that "what the
438
+ // caller sees now" is the most useful discriminator (see the
439
+ // JSDoc above).
440
+ const err = error;
441
+ const attemptsTotal = attempt + 1;
442
+ if (kind === "rateLimit") {
443
+ const retryAfterMs = retryAfterOf(error);
444
+ throw new RateLimitedError(`LLM call exhausted ${attemptsTotal} attempts on rate-limit (429).`, attemptsTotal, err, retryAfterMs ?? undefined);
445
+ }
446
+ throw new TransientError(`LLM call exhausted ${attemptsTotal} attempts on transient failure.`, attemptsTotal, err);
447
+ }
448
+ // Retry — log, sleep, loop.
449
+ const status = statusOf(error);
450
+ const retryAfterMs = retryAfterOf(error);
451
+ // 1-indexed attempt number for the audit log (attempt=1 is the
452
+ // first retry, i.e. the second call overall). Logged verbatim
453
+ // (unclamped) — the log's `retryAfter` field records what the
454
+ // upstream actually asked for, same contract as
455
+ // `RateLimitedError.retryAfterMs` below.
456
+ logRetry(attempt + 1, status, retryAfterMs);
457
+ // Clamp only the value actually slept on (MAX_RETRY_AFTER_MS):
458
+ // `retryAfterMs` is trusted verbatim from the upstream response, so
459
+ // an unclamped sleep here would let a misconfigured or hostile
460
+ // upstream (e.g. `Retry-After: 86400`) stall this call — and, on
461
+ // the stdio MCP transport, the whole in-flight request — for that
462
+ // full duration. `jitteredBackoff` is already self-capping at
463
+ // MAX_DELAY_MS, so the clamp below is a no-op on that branch.
464
+ const delayMs = Math.min(retryAfterMs ?? jitteredBackoff(attempt + 1, random), MAX_RETRY_AFTER_MS);
465
+ await sleep(delayMs);
466
+ }
467
+ }
468
+ // Unreachable: the loop either returns on success or throws on the
469
+ // budget-exhaustion branch. TypeScript wants an explicit throw here
470
+ // because it can't prove the loop always terminates via one of those
471
+ // two branches — `throw` on the final `lastError` closes the type.
472
+ throw lastError;
473
+ };
474
+ }
475
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/llm/retry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `COMPONENT_SCHEMA` (M2-02) — the JSON Schema `conjure`/`refine` (M2-03/M2-04)
3
+ * demand from the model via `response_format: { type: "json_schema",
4
+ * json_schema: COMPONENT_SCHEMA }`, and that M2-07's Ajv validation pass
5
+ * (`validateComponent`) compiles against.
6
+ *
7
+ * Draft 7 only (AC2): no `anyOf`/`oneOf` discriminator pattern, and the only
8
+ * `$ref` in the whole schema is a single level deep (`manifestEntry.viewport`
9
+ * → `#/definitions/Viewport`, mirroring the RFC §9.20 shared-`$defs` shape).
10
+ * LiteLLM's structured-output passthrough to Anthropic (via tool-use) is the
11
+ * reason to keep nesting shallow — a deep `$ref` chain is exactly the shape
12
+ * that passthrough has been observed not to handle reliably.
13
+ *
14
+ * `as const satisfies JSONSchema7` (AC1) does two jobs at once: `satisfies`
15
+ * runs the object through `@types/json-schema`'s `JSONSchema7` shape so a
16
+ * typo'd keyword (e.g. `additionalProperty` instead of `additionalProperties`)
17
+ * is a compile error, while `as const` preserves the literal types `FromSchema`
18
+ * (AC7) needs to actually infer `ValidatedComponent` below rather than widening
19
+ * every string field to `string`.
20
+ */
21
+ import type { FromSchema } from "json-schema-to-ts";
22
+ export declare const COMPONENT_SCHEMA: {
23
+ readonly $schema: "http://json-schema.org/draft-07/schema#";
24
+ readonly $id: "https://genie.dev/schema/component.schema.json";
25
+ readonly title: "GenieComponent";
26
+ readonly description: string;
27
+ readonly type: "object";
28
+ readonly additionalProperties: false;
29
+ readonly required: ["componentName", "group", "files", "manifestEntry"];
30
+ readonly definitions: {
31
+ readonly Viewport: {
32
+ readonly type: "object";
33
+ readonly additionalProperties: false;
34
+ readonly required: ["width", "height"];
35
+ readonly properties: {
36
+ readonly width: {
37
+ readonly type: "integer";
38
+ readonly minimum: 1;
39
+ readonly maximum: 4096;
40
+ };
41
+ readonly height: {
42
+ readonly type: "integer";
43
+ readonly minimum: 1;
44
+ readonly maximum: 4096;
45
+ };
46
+ };
47
+ };
48
+ };
49
+ readonly properties: {
50
+ readonly componentName: {
51
+ readonly type: "string";
52
+ readonly description: "PascalCase component name, e.g. \"Button\".";
53
+ readonly pattern: "^[A-Z][A-Za-z0-9]{1,63}$";
54
+ };
55
+ readonly group: {
56
+ readonly type: "string";
57
+ readonly description: "Kebab-case UI-kit group/category, e.g. \"actions\".";
58
+ readonly pattern: "^[a-z0-9-]{1,32}$";
59
+ };
60
+ readonly files: {
61
+ readonly type: "array";
62
+ readonly description: string;
63
+ readonly minItems: 1;
64
+ readonly maxItems: 12;
65
+ readonly contains: {
66
+ readonly type: "object";
67
+ readonly properties: {
68
+ readonly path: {
69
+ readonly type: "string";
70
+ readonly pattern: "^components/[a-z0-9-]+/([A-Z][A-Za-z0-9]{1,63})/\\1\\.html$";
71
+ };
72
+ };
73
+ };
74
+ readonly items: {
75
+ readonly type: "object";
76
+ readonly additionalProperties: false;
77
+ readonly required: ["path", "content", "mimeType"];
78
+ readonly properties: {
79
+ readonly path: {
80
+ readonly type: "string";
81
+ readonly description: "Kit-relative path under components/<group>/<Name>/.";
82
+ readonly pattern: "^components/[a-z0-9-]+/[A-Z][A-Za-z0-9]+/[A-Za-z0-9._-]+$";
83
+ };
84
+ readonly content: {
85
+ readonly type: "string";
86
+ readonly description: string;
87
+ readonly minLength: 1;
88
+ readonly maxLength: 65536;
89
+ };
90
+ readonly mimeType: {
91
+ readonly type: "string";
92
+ readonly description: "MIME type of `content`, e.g. \"text/html\".";
93
+ readonly pattern: "^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$";
94
+ };
95
+ };
96
+ };
97
+ };
98
+ readonly manifestEntry: {
99
+ readonly type: "object";
100
+ readonly description: string;
101
+ readonly additionalProperties: false;
102
+ readonly required: ["viewport"];
103
+ readonly properties: {
104
+ readonly viewport: {
105
+ readonly $ref: "#/definitions/Viewport";
106
+ };
107
+ readonly subtitle: {
108
+ readonly type: "string";
109
+ readonly maxLength: 256;
110
+ };
111
+ readonly tags: {
112
+ readonly type: "array";
113
+ readonly items: {
114
+ readonly type: "string";
115
+ };
116
+ readonly maxItems: 16;
117
+ };
118
+ };
119
+ };
120
+ };
121
+ };
122
+ /**
123
+ * AC7 — TypeScript type inferred from {@link COMPONENT_SCHEMA} via
124
+ * `json-schema-to-ts`, so the schema (not a hand-maintained interface) is the
125
+ * single source of truth `conjure`/`refine`/`validateComponent` (M2-03/04/07)
126
+ * import against.
127
+ */
128
+ export type ValidatedComponent = FromSchema<typeof COMPONENT_SCHEMA>;
129
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/llm/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAwCpD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6FG,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC"}