@aglyn/plugins-ai 1.0.0-beta.230 → 1.0.0-beta.232

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 (305) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.js +8 -4
  3. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  4. package/src/lib/components/ai-assist-provider-on-host.component.d.ts +66 -0
  5. package/src/lib/components/ai-assist-provider-on-host.component.js +78 -0
  6. package/src/lib/components/ai-assist-provider-on-host.component.js.map +1 -0
  7. package/src/lib/components/ai-assist-provider.component.js +9 -10
  8. package/src/lib/components/ai-assist-provider.component.js.map +1 -1
  9. package/src/lib/components/ai-job-follow.component.js +1 -0
  10. package/src/lib/components/ai-job-follow.component.js.map +1 -1
  11. package/src/lib/components/ai-job-links.js +13 -0
  12. package/src/lib/components/ai-job-links.js.map +1 -1
  13. package/src/lib/components/ai-job-plan.component.js +3 -1
  14. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  15. package/src/lib/components/ai-jobs-list-page.component.js +1 -0
  16. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  17. package/src/lib/components/ai-media-create.component.d.ts +20 -7
  18. package/src/lib/components/ai-media-create.component.js +107 -55
  19. package/src/lib/components/ai-media-create.component.js.map +1 -1
  20. package/src/lib/components/ai-permissions-on-host.component.d.ts +3 -46
  21. package/src/lib/components/ai-permissions-on-host.component.js +5 -53
  22. package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
  23. package/src/lib/components/ai-seo-fields-card.component.js +1 -0
  24. package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
  25. package/src/lib/components/ai-site-build-page.component.js +20 -8
  26. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  27. package/src/lib/components/ai-site-memory-card.component.d.ts +32 -0
  28. package/src/lib/components/ai-site-memory-card.component.js +187 -0
  29. package/src/lib/components/ai-site-memory-card.component.js.map +1 -0
  30. package/src/lib/components/ai-site-start-card.component.js +80 -77
  31. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  32. package/src/lib/components/ai-theme-proposal-card.component.js +4 -0
  33. package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
  34. package/src/lib/components/ai-usage-strip.component.js +1 -0
  35. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  36. package/src/lib/components/assist-panel.component.d.ts +10 -0
  37. package/src/lib/components/assist-panel.component.js +119 -9
  38. package/src/lib/components/assist-panel.component.js.map +1 -1
  39. package/src/lib/components/assist-signals-page.component.js +2 -0
  40. package/src/lib/components/assist-signals-page.component.js.map +1 -1
  41. package/src/lib/components/billing-ai-allotments.component.js +3 -0
  42. package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
  43. package/src/lib/components/billing-ai-top-users.component.js +1 -0
  44. package/src/lib/components/billing-ai-top-users.component.js.map +1 -1
  45. package/src/lib/components/billing-assist-overage-card.component.js +2 -0
  46. package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
  47. package/src/lib/components/member-ai-usage-card.component.js +10 -0
  48. package/src/lib/components/member-ai-usage-card.component.js.map +1 -1
  49. package/src/lib/components/staff-org-ai-card.component.js +1 -0
  50. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  51. package/src/lib/components/staff-org-ai-conversations.component.d.ts +23 -0
  52. package/src/lib/components/staff-org-ai-conversations.component.js +346 -0
  53. package/src/lib/components/staff-org-ai-conversations.component.js.map +1 -0
  54. package/src/lib/jobs/ai-build-capabilities.d.ts +13 -1
  55. package/src/lib/jobs/ai-build-capabilities.js +101 -7
  56. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  57. package/src/lib/jobs/ai-build-unit-outcome.js +4 -2
  58. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
  59. package/src/lib/jobs/ai-business-profile-prefill.d.ts +29 -0
  60. package/src/lib/jobs/ai-business-profile-prefill.js +85 -0
  61. package/src/lib/jobs/ai-business-profile-prefill.js.map +1 -0
  62. package/src/lib/jobs/ai-job-auto-confirm.d.ts +8 -0
  63. package/src/lib/jobs/ai-job-auto-confirm.js +30 -6
  64. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -1
  65. package/src/lib/jobs/ai-job-build-step.js +14 -1
  66. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  67. package/src/lib/jobs/ai-job-edit-step.d.ts +225 -0
  68. package/src/lib/jobs/ai-job-edit-step.js +631 -0
  69. package/src/lib/jobs/ai-job-edit-step.js.map +1 -0
  70. package/src/lib/jobs/ai-job-generation.d.ts +8 -0
  71. package/src/lib/jobs/ai-job-generation.js +13 -2
  72. package/src/lib/jobs/ai-job-generation.js.map +1 -1
  73. package/src/lib/jobs/ai-job-insight-step.js +29 -5
  74. package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
  75. package/src/lib/jobs/ai-job-layout-language.d.ts +78 -0
  76. package/src/lib/jobs/ai-job-layout-language.js +200 -0
  77. package/src/lib/jobs/ai-job-layout-language.js.map +1 -0
  78. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +13 -2
  79. package/src/lib/jobs/ai-job-layout-site-pages.js +18 -2
  80. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -1
  81. package/src/lib/jobs/ai-job-layout-step.d.ts +5 -0
  82. package/src/lib/jobs/ai-job-layout-step.js +107 -6
  83. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  84. package/src/lib/jobs/ai-job-logic-step.d.ts +20 -0
  85. package/src/lib/jobs/ai-job-logic-step.js +87 -6
  86. package/src/lib/jobs/ai-job-logic-step.js.map +1 -1
  87. package/src/lib/jobs/ai-job-page-language.d.ts +194 -0
  88. package/src/lib/jobs/ai-job-page-language.js +418 -0
  89. package/src/lib/jobs/ai-job-page-language.js.map +1 -0
  90. package/src/lib/jobs/ai-job-page-sections.d.ts +37 -1
  91. package/src/lib/jobs/ai-job-page-sections.js +93 -11
  92. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  93. package/src/lib/jobs/ai-job-page-step.d.ts +4 -4
  94. package/src/lib/jobs/ai-job-page-step.js +115 -14
  95. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  96. package/src/lib/jobs/ai-job-plan-step.d.ts +20 -6
  97. package/src/lib/jobs/ai-job-plan-step.js +62 -22
  98. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  99. package/src/lib/jobs/ai-job-site-content.d.ts +163 -0
  100. package/src/lib/jobs/ai-job-site-content.js +443 -0
  101. package/src/lib/jobs/ai-job-site-content.js.map +1 -0
  102. package/src/lib/jobs/ai-job-site-look.d.ts +112 -0
  103. package/src/lib/jobs/ai-job-site-look.js +322 -0
  104. package/src/lib/jobs/ai-job-site-look.js.map +1 -0
  105. package/src/lib/jobs/ai-job-site-step.d.ts +30 -5
  106. package/src/lib/jobs/ai-job-site-step.js +183 -27
  107. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  108. package/src/lib/jobs/ai-job-workflow-step.js +2 -2
  109. package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
  110. package/src/lib/jobs/ai-jobs-notify.d.ts +6 -1
  111. package/src/lib/jobs/ai-jobs-notify.js +15 -3
  112. package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
  113. package/src/lib/jobs/ai-jobs.d.ts +1 -1
  114. package/src/lib/jobs/ai-jobs.js +17 -8
  115. package/src/lib/jobs/ai-jobs.js.map +1 -1
  116. package/src/lib/jobs/ai-layout-full-height.d.ts +40 -0
  117. package/src/lib/jobs/ai-layout-full-height.js +82 -0
  118. package/src/lib/jobs/ai-layout-full-height.js.map +1 -0
  119. package/src/lib/jobs/ai-layout-site-facts.d.ts +54 -0
  120. package/src/lib/jobs/ai-layout-site-facts.js +153 -0
  121. package/src/lib/jobs/ai-layout-site-facts.js.map +1 -0
  122. package/src/lib/layout-language/ai-layout-compiler.d.ts +154 -0
  123. package/src/lib/layout-language/ai-layout-compiler.js +1235 -0
  124. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -0
  125. package/src/lib/layout-language/ai-layout-copy.d.ts +73 -0
  126. package/src/lib/layout-language/ai-layout-copy.js +137 -0
  127. package/src/lib/layout-language/ai-layout-copy.js.map +1 -0
  128. package/src/lib/layout-language/ai-layout-frame.d.ts +52 -0
  129. package/src/lib/layout-language/ai-layout-frame.js +458 -0
  130. package/src/lib/layout-language/ai-layout-frame.js.map +1 -0
  131. package/src/lib/layout-language/ai-layout-gaps.d.ts +63 -0
  132. package/src/lib/layout-language/ai-layout-gaps.js +345 -0
  133. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -0
  134. package/src/lib/layout-language/ai-layout-language.d.ts +168 -0
  135. package/src/lib/layout-language/ai-layout-language.js +529 -0
  136. package/src/lib/layout-language/ai-layout-language.js.map +1 -0
  137. package/src/lib/layout-language/ai-layout-links.d.ts +80 -0
  138. package/src/lib/layout-language/ai-layout-links.js +109 -0
  139. package/src/lib/layout-language/ai-layout-links.js.map +1 -0
  140. package/src/lib/layout-language/ai-layout-pictures.d.ts +144 -0
  141. package/src/lib/layout-language/ai-layout-pictures.js +214 -0
  142. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -0
  143. package/src/lib/layout-language/ai-layout-store.d.ts +41 -0
  144. package/src/lib/layout-language/ai-layout-store.js +75 -0
  145. package/src/lib/layout-language/ai-layout-store.js.map +1 -0
  146. package/src/lib/layout-language/ai-layout-tree.d.ts +59 -0
  147. package/src/lib/layout-language/ai-layout-tree.js +84 -0
  148. package/src/lib/layout-language/ai-layout-tree.js.map +1 -0
  149. package/src/lib/lazy-widget.d.ts +45 -0
  150. package/src/lib/lazy-widget.js +53 -0
  151. package/src/lib/lazy-widget.js.map +1 -0
  152. package/src/lib/model/ai-build-job.js +2 -1
  153. package/src/lib/model/ai-build-job.js.map +1 -1
  154. package/src/lib/model/ai-build-progress.d.ts +20 -3
  155. package/src/lib/model/ai-build-progress.js +52 -7
  156. package/src/lib/model/ai-build-progress.js.map +1 -1
  157. package/src/lib/model/ai-edit-job.d.ts +109 -0
  158. package/src/lib/model/ai-edit-job.js +157 -0
  159. package/src/lib/model/ai-edit-job.js.map +1 -0
  160. package/src/lib/model/ai-host-publish-context.d.ts +51 -0
  161. package/src/lib/model/ai-host-publish-context.js +104 -0
  162. package/src/lib/model/ai-host-publish-context.js.map +1 -0
  163. package/src/lib/model/ai-job-activity.d.ts +22 -0
  164. package/src/lib/model/ai-job-activity.js +23 -1
  165. package/src/lib/model/ai-job-activity.js.map +1 -1
  166. package/src/lib/model/ai-job-failure-copy.d.ts +38 -1
  167. package/src/lib/model/ai-job-failure-copy.js +56 -1
  168. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  169. package/src/lib/model/ai-job-notice.d.ts +3 -1
  170. package/src/lib/model/ai-job-notice.js +31 -9
  171. package/src/lib/model/ai-job-notice.js.map +1 -1
  172. package/src/lib/model/ai-jobs.types.d.ts +22 -1
  173. package/src/lib/model/ai-jobs.types.js.map +1 -1
  174. package/src/lib/model/ai-logic-job.d.ts +18 -1
  175. package/src/lib/model/ai-logic-job.js +24 -3
  176. package/src/lib/model/ai-logic-job.js.map +1 -1
  177. package/src/lib/model/ai-media-credits.d.ts +8 -4
  178. package/src/lib/model/ai-media-credits.js +11 -4
  179. package/src/lib/model/ai-media-credits.js.map +1 -1
  180. package/src/lib/model/ai-media-kinds.d.ts +68 -0
  181. package/src/lib/model/ai-media-kinds.js +98 -0
  182. package/src/lib/model/ai-media-kinds.js.map +1 -0
  183. package/src/lib/model/ai-site-build-progress.d.ts +9 -1
  184. package/src/lib/model/ai-site-build-progress.js +54 -14
  185. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  186. package/src/lib/model/ai-site-context.d.ts +111 -0
  187. package/src/lib/model/ai-site-context.js +222 -0
  188. package/src/lib/model/ai-site-context.js.map +1 -0
  189. package/src/lib/model/ai-site-job.d.ts +18 -2
  190. package/src/lib/model/ai-site-job.js +21 -5
  191. package/src/lib/model/ai-site-job.js.map +1 -1
  192. package/src/lib/model/ai-site-kinds.d.ts +164 -0
  193. package/src/lib/model/ai-site-kinds.js +1942 -0
  194. package/src/lib/model/ai-site-kinds.js.map +1 -0
  195. package/src/lib/model/ai-site-look.d.ts +150 -0
  196. package/src/lib/model/ai-site-look.js +1236 -0
  197. package/src/lib/model/ai-site-look.js.map +1 -0
  198. package/src/lib/model/ai-site-memory.d.ts +80 -0
  199. package/src/lib/model/ai-site-memory.js +171 -0
  200. package/src/lib/model/ai-site-memory.js.map +1 -0
  201. package/src/lib/model/ai-site-start-seo.d.ts +1 -1
  202. package/src/lib/model/ai-site-start.d.ts +15 -3
  203. package/src/lib/model/ai-site-start.js +21 -4
  204. package/src/lib/model/ai-site-start.js.map +1 -1
  205. package/src/lib/model/assist-follow-up.d.ts +92 -0
  206. package/src/lib/model/assist-follow-up.js +128 -0
  207. package/src/lib/model/assist-follow-up.js.map +1 -0
  208. package/src/lib/plugin-config.js +2 -0
  209. package/src/lib/plugin-config.js.map +1 -1
  210. package/src/lib/plugin.js +99 -48
  211. package/src/lib/plugin.js.map +1 -1
  212. package/src/lib/providers/anthropic.d.ts +9 -1
  213. package/src/lib/providers/anthropic.js +143 -9
  214. package/src/lib/providers/anthropic.js.map +1 -1
  215. package/src/lib/providers/catalog.d.ts +38 -8
  216. package/src/lib/providers/catalog.js +106 -14
  217. package/src/lib/providers/catalog.js.map +1 -1
  218. package/src/lib/providers/contract.d.ts +29 -0
  219. package/src/lib/providers/contract.js.map +1 -1
  220. package/src/lib/providers/image-contract.d.ts +22 -1
  221. package/src/lib/providers/image-contract.js +40 -1
  222. package/src/lib/providers/image-contract.js.map +1 -1
  223. package/src/lib/providers/image-endpoints.d.ts +24 -0
  224. package/src/lib/providers/image-endpoints.js +28 -0
  225. package/src/lib/providers/image-endpoints.js.map +1 -0
  226. package/src/lib/providers/model-choice.js +23 -2
  227. package/src/lib/providers/model-choice.js.map +1 -1
  228. package/src/lib/providers/routing.js +18 -0
  229. package/src/lib/providers/routing.js.map +1 -1
  230. package/src/lib/providers/vertex-image-endpoint.d.ts +48 -0
  231. package/src/lib/providers/vertex-image-endpoint.js +80 -0
  232. package/src/lib/providers/vertex-image-endpoint.js.map +1 -0
  233. package/src/lib/providers/vertex-image.d.ts +13 -27
  234. package/src/lib/providers/vertex-image.js +15 -42
  235. package/src/lib/providers/vertex-image.js.map +1 -1
  236. package/src/lib/runtime/ai-blog-post-generation.d.ts +62 -0
  237. package/src/lib/runtime/ai-blog-post-generation.js +89 -0
  238. package/src/lib/runtime/ai-blog-post-generation.js.map +1 -0
  239. package/src/lib/runtime/ai-dev-env.d.ts +53 -0
  240. package/src/lib/runtime/ai-dev-env.js +72 -0
  241. package/src/lib/runtime/ai-dev-env.js.map +1 -0
  242. package/src/lib/runtime/ai-dev-replay.d.ts +116 -0
  243. package/src/lib/runtime/ai-dev-replay.js +313 -0
  244. package/src/lib/runtime/ai-dev-replay.js.map +1 -0
  245. package/src/lib/runtime/ai-doctrine-validators.d.ts +82 -0
  246. package/src/lib/runtime/ai-doctrine-validators.js +436 -4
  247. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  248. package/src/lib/runtime/ai-doctrine.js +16 -3
  249. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  250. package/src/lib/runtime/ai-eval-live.js +4 -0
  251. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  252. package/src/lib/runtime/ai-eval.d.ts +13 -1
  253. package/src/lib/runtime/ai-eval.js +84 -1
  254. package/src/lib/runtime/ai-eval.js.map +1 -1
  255. package/src/lib/runtime/ai-icon-library.d.ts +257 -0
  256. package/src/lib/runtime/ai-icon-library.js +254 -0
  257. package/src/lib/runtime/ai-icon-library.js.map +1 -0
  258. package/src/lib/runtime/ai-live-batch.d.ts +45 -0
  259. package/src/lib/runtime/ai-live-batch.js +89 -0
  260. package/src/lib/runtime/ai-live-batch.js.map +1 -0
  261. package/src/lib/runtime/ai-node-tree.d.ts +9 -0
  262. package/src/lib/runtime/ai-node-tree.js +30 -4
  263. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  264. package/src/lib/runtime/ai-palette.d.ts +6 -0
  265. package/src/lib/runtime/ai-palette.generated.js +70 -0
  266. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  267. package/src/lib/runtime/ai-palette.js.map +1 -1
  268. package/src/lib/runtime/ai-runtime.js +46 -2
  269. package/src/lib/runtime/ai-runtime.js.map +1 -1
  270. package/src/lib/runtime/site-context.d.ts +58 -0
  271. package/src/lib/runtime/site-context.js +127 -0
  272. package/src/lib/runtime/site-context.js.map +1 -0
  273. package/src/lib/server/ai-admin-conversations.d.ts +22 -0
  274. package/src/lib/server/ai-admin-conversations.js +197 -0
  275. package/src/lib/server/ai-admin-conversations.js.map +1 -0
  276. package/src/lib/server/ai-media-image.d.ts +5 -5
  277. package/src/lib/server/ai-media-image.js +25 -11
  278. package/src/lib/server/ai-media-image.js.map +1 -1
  279. package/src/lib/server/ai-media-raster-prompt.d.ts +34 -0
  280. package/src/lib/server/ai-media-raster-prompt.js +53 -0
  281. package/src/lib/server/ai-media-raster-prompt.js.map +1 -0
  282. package/src/lib/server/assist-chat.d.ts +11 -0
  283. package/src/lib/server/assist-chat.js +57 -18
  284. package/src/lib/server/assist-chat.js.map +1 -1
  285. package/src/lib/server/assist-edit-applied.js +16 -3
  286. package/src/lib/server/assist-edit-applied.js.map +1 -1
  287. package/src/lib/server/assist-view-context.d.ts +19 -1
  288. package/src/lib/server/assist-view-context.js +38 -1
  289. package/src/lib/server/assist-view-context.js.map +1 -1
  290. package/src/lib/server.js +10 -0
  291. package/src/lib/server.js.map +1 -1
  292. package/src/lib/subprocessors.js +17 -1
  293. package/src/lib/subprocessors.js.map +1 -1
  294. package/src/lib/tools/ai-blog-post-tool.d.ts +60 -0
  295. package/src/lib/tools/ai-blog-post-tool.js +175 -0
  296. package/src/lib/tools/ai-blog-post-tool.js.map +1 -0
  297. package/src/lib/tools/ai-workflow-tool.d.ts +6 -2
  298. package/src/lib/tools/ai-workflow-tool.js +16 -14
  299. package/src/lib/tools/ai-workflow-tool.js.map +1 -1
  300. package/src/lib/usage/assist-usage.d.ts +3 -0
  301. package/src/lib/usage/assist-usage.js +4 -2
  302. package/src/lib/usage/assist-usage.js.map +1 -1
  303. package/src/lib/usage/staff-org-ai-conversations.d.ts +103 -0
  304. package/src/lib/usage/staff-org-ai-conversations.js +152 -0
  305. package/src/lib/usage/staff-org-ai-conversations.js.map +1 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { AiProvider, AiProviderRequest, AiResult, AiStreamEvent, AiSystemBlock, AiTool } from '../providers/contract';
18
+ import { type AiDevEnv as Env, type AiDevReplayMode } from './ai-dev-env';
19
+ /**
20
+ * THE DEVELOPMENT REPLAY CACHE (AGL-3660).
21
+ *
22
+ * Almost every dollar the provider bills in a month is a development run: a
23
+ * live eval, the layout screenshots fed by one, a local guided start. Most of
24
+ * the requests in a second run of the same eval are byte-identical to the
25
+ * first — the change under test was to a compiler, a check or one prompt, not
26
+ * to all of them — and each identical request bought the same sample twice.
27
+ *
28
+ * So, outside production only, the runtime keys each request by a hash of
29
+ * everything that decides the answer — the provider, the model, the system
30
+ * blocks, the tools, the messages, the ceiling, thinking and effort — and keeps
31
+ * the provider's answer under `.cache/ai-replay/` (gitignored). The next
32
+ * identical request is answered from disk for nothing; a request whose prompt
33
+ * changed by one byte has a new key and goes to the provider. A replayed answer
34
+ * is a real answer the same model gave to the same bytes, so an eval that
35
+ * passes on replays has still seen every CHANGED prompt answered live: the
36
+ * replay never stands in for a prompt the provider has not answered.
37
+ *
38
+ * `AGLYN_AI_REPLAY`:
39
+ * - `1` / `on`: replay what is recorded, record what is not.
40
+ * - `refresh`: every request live, its answer recorded over the old one.
41
+ * - `off` / `0`: nothing read, nothing written.
42
+ * - unset: on under the live-eval launcher (`AI_EVAL_LIVE_LAUNCHER`), off
43
+ * everywhere else, so a dev server or a spec that never asked for it
44
+ * behaves exactly as before.
45
+ *
46
+ * It never runs in a deployed server. `NODE_ENV=production`, `VERCEL_ENV`
47
+ * of `production` or `preview`, or a Cloud Run service (`K_SERVICE`) turn it
48
+ * off whatever the variable says, and the first request says so in the log:
49
+ * a recorded answer handed to a customer would be another customer's page.
50
+ *
51
+ * Every live eval's table carries `aiDevReplayStats()`, so a run that was
52
+ * all replays reads as one: `live: 0` is not a live pass.
53
+ */
54
+ /** JSON with every object's keys in sorted order: the same value, the same bytes, whatever built it. */
55
+ export declare function aiCanonicalJson(value: unknown): string;
56
+ /** What a request's answer depends on. The key, the signal and the cache lifetime are not part of it. */
57
+ export interface AiReplayRequest {
58
+ model: string;
59
+ system: readonly AiSystemBlock[];
60
+ messages: AiProviderRequest['messages'];
61
+ tools?: readonly AiTool[];
62
+ maxTokens: number;
63
+ thinking?: AiProviderRequest['thinking'];
64
+ effort?: AiProviderRequest['effort'];
65
+ }
66
+ /** The request's replay key: a SHA-256 of everything that decides the answer. */
67
+ export declare function aiDevReplayKey(providerId: string, stream: boolean, request: AiReplayRequest): string;
68
+ /**
69
+ * The prefix a provider caches: the model, the tools and every system block
70
+ * through the last breakpoint. Two requests with the same prefix key read one
71
+ * cache entry; `null` when the request marks no breakpoint.
72
+ */
73
+ export declare function aiCachedPrefixKey(request: Pick<AiReplayRequest, 'model' | 'system' | 'tools'>): string | null;
74
+ /** Where recorded answers are kept. */
75
+ export declare function aiDevReplayDir(env?: Env): string;
76
+ /** How many of this process's requests were sent, and how many were answered from disk. */
77
+ export interface AiDevReplayStats {
78
+ mode: AiDevReplayMode;
79
+ /** Requests sent to the provider. */
80
+ live: number;
81
+ /** Requests answered from a recording. */
82
+ replayed: number;
83
+ /** What the live requests cost at the provider's list rates. */
84
+ liveProviderUsd: number;
85
+ /** What the replayed ones would have cost, had they been sent. */
86
+ savedProviderUsd: number;
87
+ }
88
+ /** This process's count so far: what a live eval prints beside its table. */
89
+ export declare function aiDevReplayStats(env?: Env): AiDevReplayStats;
90
+ /** Starts the count again; a spec that runs two rounds reports each. */
91
+ export declare function resetAiDevReplayStats(): void;
92
+ /** Counts one request a live path sent, at what the provider billed for it; `discount` is a batch's half. */
93
+ export declare function aiDevCountLive(usage: AiResult['usage'], model: string, discount?: number): void;
94
+ /**
95
+ * One request through the replay cache: answered from disk when recorded and
96
+ * the mode replays, otherwise sent through `send` and recorded. `send` is the
97
+ * live path — the provider, or the batch queue — and counts its own spend.
98
+ */
99
+ export declare function aiDevReplayComplete(provider: Pick<AiProvider, 'id'>, request: AiReplayRequest, mode: Exclude<AiDevReplayMode, 'off'>, send: () => Promise<AiResult>, env?: Env): Promise<AiResult>;
100
+ /** The streaming half of {@link aiDevReplayComplete}. */
101
+ export declare function aiDevReplayStream(provider: Pick<AiProvider, 'id'>, request: AiReplayRequest, mode: Exclude<AiDevReplayMode, 'off'>, send: () => Promise<AsyncIterable<AiStreamEvent>>, env?: Env): Promise<AsyncIterable<AiStreamEvent>>;
102
+ export declare function aiDevWarmPrefixFirst<T>(prefixKey: string | null, ttlMs: number, send: () => Promise<T>, now?: () => number): Promise<T>;
103
+ /** Forgets every warmed prefix; for specs. */
104
+ export declare function resetAiDevWarmPrefixes(): void;
105
+ /**
106
+ * The line every live eval's table carries (AGL-3660): how many of its
107
+ * requests went to the provider and how many were answered from disk, what
108
+ * the live ones cost at list, and how many batches carried them. A table
109
+ * whose `live` is 0 proved nothing new — every prompt in it had been answered
110
+ * before — and is not a live pass; `AGLYN_AI_REPLAY=refresh` asks again.
111
+ */
112
+ export declare function aiLiveRunLedger(env?: Env): AiDevReplayStats & {
113
+ batches: number;
114
+ batchedRequests: number;
115
+ allReplayed: boolean;
116
+ };
@@ -0,0 +1,313 @@
1
+ import { _ as _async_iterator } from "@swc/helpers/_/_async_iterator";
2
+ import { _ as _await_async_generator } from "@swc/helpers/_/_await_async_generator";
3
+ import { _ as _extends } from "@swc/helpers/_/_extends";
4
+ import { _ as _wrap_async_generator } from "@swc/helpers/_/_wrap_async_generator";
5
+ /**
6
+ * @license
7
+ * Copyright 2026 Aglyn LLC
8
+ *
9
+ * Licensed under the Apache License, Version 2.0 (the "License");
10
+ * you may not use this file except in compliance with the License.
11
+ * You may obtain a copy of the License at
12
+ *
13
+ * http://www.apache.org/licenses/LICENSE-2.0
14
+ *
15
+ * Unless required by applicable law or agreed to in writing, software
16
+ * distributed under the License is distributed on an "AS IS" BASIS,
17
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ * See the License for the specific language governing permissions and
19
+ * limitations under the License.
20
+ */ import { createHash } from "node:crypto";
21
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
22
+ import { dirname, join } from "node:path";
23
+ import { estimateAiProviderCostUsd } from "../providers/catalog.js";
24
+ import { aiLiveBatchStats } from "./ai-live-batch.js";
25
+ import { AI_REPLAY_DIR_ENV, aiDevReplayMode } from "./ai-dev-env.js";
26
+ /** The key format's version: bumped when what a key covers changes, so no old answer is read under a new meaning. */ const KEY_VERSION = 1;
27
+ /**
28
+ * THE DEVELOPMENT REPLAY CACHE (AGL-3660).
29
+ *
30
+ * Almost every dollar the provider bills in a month is a development run: a
31
+ * live eval, the layout screenshots fed by one, a local guided start. Most of
32
+ * the requests in a second run of the same eval are byte-identical to the
33
+ * first — the change under test was to a compiler, a check or one prompt, not
34
+ * to all of them — and each identical request bought the same sample twice.
35
+ *
36
+ * So, outside production only, the runtime keys each request by a hash of
37
+ * everything that decides the answer — the provider, the model, the system
38
+ * blocks, the tools, the messages, the ceiling, thinking and effort — and keeps
39
+ * the provider's answer under `.cache/ai-replay/` (gitignored). The next
40
+ * identical request is answered from disk for nothing; a request whose prompt
41
+ * changed by one byte has a new key and goes to the provider. A replayed answer
42
+ * is a real answer the same model gave to the same bytes, so an eval that
43
+ * passes on replays has still seen every CHANGED prompt answered live: the
44
+ * replay never stands in for a prompt the provider has not answered.
45
+ *
46
+ * `AGLYN_AI_REPLAY`:
47
+ * - `1` / `on`: replay what is recorded, record what is not.
48
+ * - `refresh`: every request live, its answer recorded over the old one.
49
+ * - `off` / `0`: nothing read, nothing written.
50
+ * - unset: on under the live-eval launcher (`AI_EVAL_LIVE_LAUNCHER`), off
51
+ * everywhere else, so a dev server or a spec that never asked for it
52
+ * behaves exactly as before.
53
+ *
54
+ * It never runs in a deployed server. `NODE_ENV=production`, `VERCEL_ENV`
55
+ * of `production` or `preview`, or a Cloud Run service (`K_SERVICE`) turn it
56
+ * off whatever the variable says, and the first request says so in the log:
57
+ * a recorded answer handed to a customer would be another customer's page.
58
+ *
59
+ * Every live eval's table carries `aiDevReplayStats()`, so a run that was
60
+ * all replays reads as one: `live: 0` is not a live pass.
61
+ */ // ── The key ──────────────────────────────────────────────────────────────
62
+ /** JSON with every object's keys in sorted order: the same value, the same bytes, whatever built it. */ export function aiCanonicalJson(value) {
63
+ return JSON.stringify(sortKeys(value));
64
+ }
65
+ function sortKeys(value) {
66
+ if (Array.isArray(value)) return value.map(sortKeys);
67
+ if (value && typeof value === 'object') {
68
+ return Object.fromEntries(Object.keys(value).sort().filter((key)=>value[key] !== undefined).map((key)=>[
69
+ key,
70
+ sortKeys(value[key])
71
+ ]));
72
+ }
73
+ return value;
74
+ }
75
+ /** The request's replay key: a SHA-256 of everything that decides the answer. */ export function aiDevReplayKey(providerId, stream, request) {
76
+ var _request_tools, _request_thinking, _request_effort;
77
+ return createHash('sha256').update(aiCanonicalJson({
78
+ v: KEY_VERSION,
79
+ provider: providerId,
80
+ stream,
81
+ model: request.model,
82
+ // A breakpoint changes no answer, so the key reads the text alone.
83
+ system: request.system.map((block)=>block.text),
84
+ tools: (_request_tools = request.tools) != null ? _request_tools : [],
85
+ messages: request.messages,
86
+ maxTokens: request.maxTokens,
87
+ thinking: (_request_thinking = request.thinking) != null ? _request_thinking : null,
88
+ effort: (_request_effort = request.effort) != null ? _request_effort : null
89
+ })).digest('hex');
90
+ }
91
+ /**
92
+ * The prefix a provider caches: the model, the tools and every system block
93
+ * through the last breakpoint. Two requests with the same prefix key read one
94
+ * cache entry; `null` when the request marks no breakpoint.
95
+ */ export function aiCachedPrefixKey(request) {
96
+ var _request_tools;
97
+ const last = request.system.map((block)=>Boolean(block.cacheBreakpoint)).lastIndexOf(true);
98
+ if (last < 0) return null;
99
+ return createHash('sha256').update(aiCanonicalJson({
100
+ model: request.model,
101
+ tools: (_request_tools = request.tools) != null ? _request_tools : [],
102
+ system: request.system.slice(0, last + 1).map((block)=>block.text)
103
+ })).digest('hex');
104
+ }
105
+ // ── The store ────────────────────────────────────────────────────────────
106
+ /** The repository root: the nearest directory up from here holding `nx.json`, else the working directory. */ function repositoryRoot(start = process.cwd()) {
107
+ let dir = start;
108
+ for(;;){
109
+ if (existsSync(join(dir, 'nx.json'))) return dir;
110
+ const parent = dirname(dir);
111
+ if (parent === dir) return start;
112
+ dir = parent;
113
+ }
114
+ }
115
+ /** Where recorded answers are kept. */ export function aiDevReplayDir(env = process.env) {
116
+ return env[AI_REPLAY_DIR_ENV] || join(repositoryRoot(), '.cache', 'ai-replay');
117
+ }
118
+ function pathFor(dir, key) {
119
+ return join(dir, key.slice(0, 2), `${key}.json`);
120
+ }
121
+ function readRecording(dir, key) {
122
+ const path = pathFor(dir, key);
123
+ if (!existsSync(path)) return null;
124
+ try {
125
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
126
+ return (parsed == null ? void 0 : parsed.v) === KEY_VERSION && parsed.key === key ? parsed : null;
127
+ } catch (unused) {
128
+ return null;
129
+ }
130
+ }
131
+ function writeRecording(dir, recording) {
132
+ const path = pathFor(dir, recording.key);
133
+ mkdirSync(dirname(path), {
134
+ recursive: true
135
+ });
136
+ // Written aside and renamed, so a run killed mid-write leaves no half file to replay.
137
+ const temp = `${path}.${process.pid}.tmp`;
138
+ writeFileSync(temp, JSON.stringify(recording));
139
+ renameSync(temp, path);
140
+ }
141
+ const stats = {
142
+ mode: 'off',
143
+ live: 0,
144
+ replayed: 0,
145
+ liveProviderUsd: 0,
146
+ savedProviderUsd: 0
147
+ };
148
+ /** This process's count so far: what a live eval prints beside its table. */ export function aiDevReplayStats(env = process.env) {
149
+ return _extends({}, stats, {
150
+ mode: aiDevReplayMode(env),
151
+ liveProviderUsd: round(stats.liveProviderUsd),
152
+ savedProviderUsd: round(stats.savedProviderUsd)
153
+ });
154
+ }
155
+ /** Starts the count again; a spec that runs two rounds reports each. */ export function resetAiDevReplayStats() {
156
+ stats.live = 0;
157
+ stats.replayed = 0;
158
+ stats.liveProviderUsd = 0;
159
+ stats.savedProviderUsd = 0;
160
+ }
161
+ function round(usd) {
162
+ return Math.round(usd * 1000000) / 1000000;
163
+ }
164
+ function usageCost(usage, model) {
165
+ return estimateAiProviderCostUsd(usage, model);
166
+ }
167
+ /** Counts one request a live path sent, at what the provider billed for it; `discount` is a batch's half. */ export function aiDevCountLive(usage, model, discount = 1) {
168
+ stats.live += 1;
169
+ stats.liveProviderUsd += usageCost(usage, model) * discount;
170
+ }
171
+ // ── The seam ─────────────────────────────────────────────────────────────
172
+ function replayEvents(events) {
173
+ return _wrap_async_generator(function*() {
174
+ for (const event of events)yield event;
175
+ })();
176
+ }
177
+ /** A live stream passed through as it arrives, and recorded once it is done. */ function recordEvents(source, onDone) {
178
+ return _wrap_async_generator(function*() {
179
+ const events = [];
180
+ let completed = false;
181
+ {
182
+ var _iteratorAbruptCompletion = false, _didIteratorError = false, _iteratorError;
183
+ try {
184
+ for(var _iterator = _async_iterator(source), _step; _iteratorAbruptCompletion = !(_step = yield _await_async_generator(_iterator.next())).done; _iteratorAbruptCompletion = false){
185
+ let _value = _step.value;
186
+ const event = _value;
187
+ events.push(event);
188
+ if (event.type === 'done') completed = true;
189
+ yield event;
190
+ }
191
+ } catch (err) {
192
+ _didIteratorError = true;
193
+ _iteratorError = err;
194
+ } finally{
195
+ try {
196
+ if (_iteratorAbruptCompletion && _iterator.return != null) {
197
+ yield _await_async_generator(_iterator.return());
198
+ }
199
+ } finally{
200
+ if (_didIteratorError) {
201
+ throw _iteratorError;
202
+ }
203
+ }
204
+ }
205
+ }
206
+ // A stream cut short, or one that carried a provider error, is not an
207
+ // answer worth keeping: the next run asks again.
208
+ if (completed && !events.some((event)=>event.type === 'error')) onDone(events);
209
+ })();
210
+ }
211
+ /**
212
+ * One request through the replay cache: answered from disk when recorded and
213
+ * the mode replays, otherwise sent through `send` and recorded. `send` is the
214
+ * live path — the provider, or the batch queue — and counts its own spend.
215
+ */ export async function aiDevReplayComplete(provider, request, mode, send, env = process.env) {
216
+ const dir = aiDevReplayDir(env);
217
+ const key = aiDevReplayKey(provider.id, false, request);
218
+ const recorded = mode === 'replay' ? readRecording(dir, key) : null;
219
+ if ((recorded == null ? void 0 : recorded.kind) === 'result') {
220
+ stats.replayed += 1;
221
+ stats.savedProviderUsd += usageCost(recorded.result.usage, request.model);
222
+ return recorded.result;
223
+ }
224
+ const result = await send();
225
+ writeRecording(dir, {
226
+ v: KEY_VERSION,
227
+ key,
228
+ model: request.model,
229
+ recordedAt: new Date().toISOString(),
230
+ kind: 'result',
231
+ result
232
+ });
233
+ return result;
234
+ }
235
+ /** The streaming half of {@link aiDevReplayComplete}. */ export async function aiDevReplayStream(provider, request, mode, send, env = process.env) {
236
+ const dir = aiDevReplayDir(env);
237
+ const key = aiDevReplayKey(provider.id, true, request);
238
+ const recorded = mode === 'replay' ? readRecording(dir, key) : null;
239
+ if ((recorded == null ? void 0 : recorded.kind) === 'stream') {
240
+ stats.replayed += 1;
241
+ const done = recorded.events.find((event)=>event.type === 'done');
242
+ if ((done == null ? void 0 : done.type) === 'done') stats.savedProviderUsd += usageCost(done.usage, request.model);
243
+ return replayEvents(recorded.events);
244
+ }
245
+ const live = await send();
246
+ return recordEvents(live, (events)=>{
247
+ const done = events.find((event)=>event.type === 'done');
248
+ if ((done == null ? void 0 : done.type) === 'done') aiDevCountLive(done.usage, request.model);
249
+ writeRecording(dir, {
250
+ v: KEY_VERSION,
251
+ key,
252
+ model: request.model,
253
+ recordedAt: new Date().toISOString(),
254
+ kind: 'stream',
255
+ events
256
+ });
257
+ });
258
+ }
259
+ // ── Warming a shared prefix before a fan-out ─────────────────────────────
260
+ /**
261
+ * A cache entry is readable only once the request that writes it has started
262
+ * answering, so N requests sent at once over one prefix all pay to write it
263
+ * and none reads it — which is exactly what a live eval's `Promise.all` over
264
+ * its briefs did. In a development run the first request over a prefix goes
265
+ * alone; the others wait for it and then read what it wrote. A prefix
266
+ * answered within the cache's lifetime is not held again.
267
+ *
268
+ * Development only, like everything here: production's beat runs one step at
269
+ * a time, and holding a customer's request behind another's would trade
270
+ * their seconds for our cents.
271
+ */ const warming = new Map();
272
+ const warmedAt = new Map();
273
+ export async function aiDevWarmPrefixFirst(prefixKey, ttlMs, send, now = Date.now) {
274
+ if (!prefixKey) return send();
275
+ const warmed = warmedAt.get(prefixKey);
276
+ if (warmed !== undefined && now() - warmed < ttlMs) return send();
277
+ const pending = warming.get(prefixKey);
278
+ if (pending) {
279
+ await pending.catch(()=>undefined);
280
+ return send();
281
+ }
282
+ const first = send();
283
+ warming.set(prefixKey, first);
284
+ try {
285
+ const result = await first;
286
+ warmedAt.set(prefixKey, now());
287
+ return result;
288
+ } finally{
289
+ warming.delete(prefixKey);
290
+ }
291
+ }
292
+ /** Forgets every warmed prefix; for specs. */ export function resetAiDevWarmPrefixes() {
293
+ warming.clear();
294
+ warmedAt.clear();
295
+ }
296
+ // ── What a live eval prints ──────────────────────────────────────────────
297
+ /**
298
+ * The line every live eval's table carries (AGL-3660): how many of its
299
+ * requests went to the provider and how many were answered from disk, what
300
+ * the live ones cost at list, and how many batches carried them. A table
301
+ * whose `live` is 0 proved nothing new — every prompt in it had been answered
302
+ * before — and is not a live pass; `AGLYN_AI_REPLAY=refresh` asks again.
303
+ */ export function aiLiveRunLedger(env = process.env) {
304
+ const replay = aiDevReplayStats(env);
305
+ const batch = aiLiveBatchStats();
306
+ return _extends({}, replay, {
307
+ batches: batch.batches,
308
+ batchedRequests: batch.requests,
309
+ allReplayed: replay.live === 0 && replay.replayed > 0
310
+ });
311
+ }
312
+
313
+ //# sourceMappingURL=ai-dev-replay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/runtime/ai-dev-replay.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { createHash } from 'node:crypto'\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { estimateAiProviderCostUsd } from '../providers/catalog'\nimport type {\n AiProvider,\n AiProviderRequest,\n AiResult,\n AiStreamEvent,\n AiSystemBlock,\n AiTool,\n} from '../providers/contract'\nimport { aiLiveBatchStats } from './ai-live-batch'\nimport { AI_REPLAY_DIR_ENV, aiDevReplayMode, type AiDevEnv as Env, type AiDevReplayMode } from './ai-dev-env'\n\n/** The key format's version: bumped when what a key covers changes, so no old answer is read under a new meaning. */\nconst KEY_VERSION = 1\n\n/**\n * THE DEVELOPMENT REPLAY CACHE (AGL-3660).\n *\n * Almost every dollar the provider bills in a month is a development run: a\n * live eval, the layout screenshots fed by one, a local guided start. Most of\n * the requests in a second run of the same eval are byte-identical to the\n * first — the change under test was to a compiler, a check or one prompt, not\n * to all of them — and each identical request bought the same sample twice.\n *\n * So, outside production only, the runtime keys each request by a hash of\n * everything that decides the answer — the provider, the model, the system\n * blocks, the tools, the messages, the ceiling, thinking and effort — and keeps\n * the provider's answer under `.cache/ai-replay/` (gitignored). The next\n * identical request is answered from disk for nothing; a request whose prompt\n * changed by one byte has a new key and goes to the provider. A replayed answer\n * is a real answer the same model gave to the same bytes, so an eval that\n * passes on replays has still seen every CHANGED prompt answered live: the\n * replay never stands in for a prompt the provider has not answered.\n *\n * `AGLYN_AI_REPLAY`:\n * - `1` / `on`: replay what is recorded, record what is not.\n * - `refresh`: every request live, its answer recorded over the old one.\n * - `off` / `0`: nothing read, nothing written.\n * - unset: on under the live-eval launcher (`AI_EVAL_LIVE_LAUNCHER`), off\n * everywhere else, so a dev server or a spec that never asked for it\n * behaves exactly as before.\n *\n * It never runs in a deployed server. `NODE_ENV=production`, `VERCEL_ENV`\n * of `production` or `preview`, or a Cloud Run service (`K_SERVICE`) turn it\n * off whatever the variable says, and the first request says so in the log:\n * a recorded answer handed to a customer would be another customer's page.\n *\n * Every live eval's table carries `aiDevReplayStats()`, so a run that was\n * all replays reads as one: `live: 0` is not a live pass.\n */\n\n// ── The key ──────────────────────────────────────────────────────────────\n\n/** JSON with every object's keys in sorted order: the same value, the same bytes, whatever built it. */\nexport function aiCanonicalJson(value: unknown): string {\n return JSON.stringify(sortKeys(value))\n}\n\nfunction sortKeys(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(sortKeys)\n if (value && typeof value === 'object') {\n return Object.fromEntries(\n Object.keys(value as Record<string, unknown>)\n .sort()\n .filter((key) => (value as Record<string, unknown>)[key] !== undefined)\n .map((key) => [key, sortKeys((value as Record<string, unknown>)[key])]),\n )\n }\n return value\n}\n\n/** What a request's answer depends on. The key, the signal and the cache lifetime are not part of it. */\nexport interface AiReplayRequest {\n model: string\n system: readonly AiSystemBlock[]\n messages: AiProviderRequest['messages']\n tools?: readonly AiTool[]\n maxTokens: number\n thinking?: AiProviderRequest['thinking']\n effort?: AiProviderRequest['effort']\n}\n\n/** The request's replay key: a SHA-256 of everything that decides the answer. */\nexport function aiDevReplayKey(providerId: string, stream: boolean, request: AiReplayRequest): string {\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n v: KEY_VERSION,\n provider: providerId,\n stream,\n model: request.model,\n // A breakpoint changes no answer, so the key reads the text alone.\n system: request.system.map((block) => block.text),\n tools: request.tools ?? [],\n messages: request.messages,\n maxTokens: request.maxTokens,\n thinking: request.thinking ?? null,\n effort: request.effort ?? null,\n }),\n )\n .digest('hex')\n}\n\n/**\n * The prefix a provider caches: the model, the tools and every system block\n * through the last breakpoint. Two requests with the same prefix key read one\n * cache entry; `null` when the request marks no breakpoint.\n */\nexport function aiCachedPrefixKey(request: Pick<AiReplayRequest, 'model' | 'system' | 'tools'>): string | null {\n const last = request.system.map((block) => Boolean(block.cacheBreakpoint)).lastIndexOf(true)\n if (last < 0) return null\n return createHash('sha256')\n .update(\n aiCanonicalJson({\n model: request.model,\n tools: request.tools ?? [],\n system: request.system.slice(0, last + 1).map((block) => block.text),\n }),\n )\n .digest('hex')\n}\n\n// ── The store ────────────────────────────────────────────────────────────\n\n/** The repository root: the nearest directory up from here holding `nx.json`, else the working directory. */\nfunction repositoryRoot(start: string = process.cwd()): string {\n let dir = start\n for (;;) {\n if (existsSync(join(dir, 'nx.json'))) return dir\n const parent = dirname(dir)\n if (parent === dir) return start\n dir = parent\n }\n}\n\n/** Where recorded answers are kept. */\nexport function aiDevReplayDir(env: Env = process.env): string {\n return env[AI_REPLAY_DIR_ENV] || join(repositoryRoot(), '.cache', 'ai-replay')\n}\n\ntype Recording =\n | { v: number; key: string; model: string; recordedAt: string; kind: 'result'; result: AiResult }\n | { v: number; key: string; model: string; recordedAt: string; kind: 'stream'; events: AiStreamEvent[] }\n\nfunction pathFor(dir: string, key: string): string {\n return join(dir, key.slice(0, 2), `${key}.json`)\n}\n\nfunction readRecording(dir: string, key: string): Recording | null {\n const path = pathFor(dir, key)\n if (!existsSync(path)) return null\n try {\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as Recording\n return parsed?.v === KEY_VERSION && parsed.key === key ? parsed : null\n } catch {\n return null\n }\n}\n\nfunction writeRecording(dir: string, recording: Recording): void {\n const path = pathFor(dir, recording.key)\n mkdirSync(dirname(path), { recursive: true })\n // Written aside and renamed, so a run killed mid-write leaves no half file to replay.\n const temp = `${path}.${process.pid}.tmp`\n writeFileSync(temp, JSON.stringify(recording))\n renameSync(temp, path)\n}\n\n// ── What a run did ───────────────────────────────────────────────────────\n\n/** How many of this process's requests were sent, and how many were answered from disk. */\nexport interface AiDevReplayStats {\n mode: AiDevReplayMode\n /** Requests sent to the provider. */\n live: number\n /** Requests answered from a recording. */\n replayed: number\n /** What the live requests cost at the provider's list rates. */\n liveProviderUsd: number\n /** What the replayed ones would have cost, had they been sent. */\n savedProviderUsd: number\n}\n\nconst stats: AiDevReplayStats = {\n mode: 'off',\n live: 0,\n replayed: 0,\n liveProviderUsd: 0,\n savedProviderUsd: 0,\n}\n\n/** This process's count so far: what a live eval prints beside its table. */\nexport function aiDevReplayStats(env: Env = process.env): AiDevReplayStats {\n return {\n ...stats,\n mode: aiDevReplayMode(env),\n liveProviderUsd: round(stats.liveProviderUsd),\n savedProviderUsd: round(stats.savedProviderUsd),\n }\n}\n\n/** Starts the count again; a spec that runs two rounds reports each. */\nexport function resetAiDevReplayStats(): void {\n stats.live = 0\n stats.replayed = 0\n stats.liveProviderUsd = 0\n stats.savedProviderUsd = 0\n}\n\nfunction round(usd: number): number {\n return Math.round(usd * 1_000_000) / 1_000_000\n}\n\nfunction usageCost(usage: AiResult['usage'], model: string): number {\n return estimateAiProviderCostUsd(usage, model)\n}\n\n/** Counts one request a live path sent, at what the provider billed for it; `discount` is a batch's half. */\nexport function aiDevCountLive(usage: AiResult['usage'], model: string, discount = 1): void {\n stats.live += 1\n stats.liveProviderUsd += usageCost(usage, model) * discount\n}\n\n// ── The seam ─────────────────────────────────────────────────────────────\n\nasync function* replayEvents(events: readonly AiStreamEvent[]): AsyncGenerator<AiStreamEvent> {\n for (const event of events) yield event\n}\n\n/** A live stream passed through as it arrives, and recorded once it is done. */\nasync function* recordEvents(\n source: AsyncIterable<AiStreamEvent>,\n onDone: (events: AiStreamEvent[]) => void,\n): AsyncGenerator<AiStreamEvent> {\n const events: AiStreamEvent[] = []\n let completed = false\n for await (const event of source) {\n events.push(event)\n if (event.type === 'done') completed = true\n yield event\n }\n // A stream cut short, or one that carried a provider error, is not an\n // answer worth keeping: the next run asks again.\n if (completed && !events.some((event) => event.type === 'error')) onDone(events)\n}\n\n/**\n * One request through the replay cache: answered from disk when recorded and\n * the mode replays, otherwise sent through `send` and recorded. `send` is the\n * live path — the provider, or the batch queue — and counts its own spend.\n */\nexport async function aiDevReplayComplete(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AiResult>,\n env: Env = process.env,\n): Promise<AiResult> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, false, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'result') {\n stats.replayed += 1\n stats.savedProviderUsd += usageCost(recorded.result.usage, request.model)\n return recorded.result\n }\n const result = await send()\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'result',\n result,\n })\n return result\n}\n\n/** The streaming half of {@link aiDevReplayComplete}. */\nexport async function aiDevReplayStream(\n provider: Pick<AiProvider, 'id'>,\n request: AiReplayRequest,\n mode: Exclude<AiDevReplayMode, 'off'>,\n send: () => Promise<AsyncIterable<AiStreamEvent>>,\n env: Env = process.env,\n): Promise<AsyncIterable<AiStreamEvent>> {\n const dir = aiDevReplayDir(env)\n const key = aiDevReplayKey(provider.id, true, request)\n const recorded = mode === 'replay' ? readRecording(dir, key) : null\n if (recorded?.kind === 'stream') {\n stats.replayed += 1\n const done = recorded.events.find((event) => event.type === 'done')\n if (done?.type === 'done') stats.savedProviderUsd += usageCost(done.usage, request.model)\n return replayEvents(recorded.events)\n }\n const live = await send()\n return recordEvents(live, (events) => {\n const done = events.find((event) => event.type === 'done')\n if (done?.type === 'done') aiDevCountLive(done.usage, request.model)\n writeRecording(dir, {\n v: KEY_VERSION,\n key,\n model: request.model,\n recordedAt: new Date().toISOString(),\n kind: 'stream',\n events,\n })\n })\n}\n\n// ── Warming a shared prefix before a fan-out ─────────────────────────────\n\n/**\n * A cache entry is readable only once the request that writes it has started\n * answering, so N requests sent at once over one prefix all pay to write it\n * and none reads it — which is exactly what a live eval's `Promise.all` over\n * its briefs did. In a development run the first request over a prefix goes\n * alone; the others wait for it and then read what it wrote. A prefix\n * answered within the cache's lifetime is not held again.\n *\n * Development only, like everything here: production's beat runs one step at\n * a time, and holding a customer's request behind another's would trade\n * their seconds for our cents.\n */\nconst warming = new Map<string, Promise<unknown>>()\nconst warmedAt = new Map<string, number>()\n\nexport async function aiDevWarmPrefixFirst<T>(\n prefixKey: string | null,\n ttlMs: number,\n send: () => Promise<T>,\n now: () => number = Date.now,\n): Promise<T> {\n if (!prefixKey) return send()\n const warmed = warmedAt.get(prefixKey)\n if (warmed !== undefined && now() - warmed < ttlMs) return send()\n const pending = warming.get(prefixKey)\n if (pending) {\n await pending.catch(() => undefined)\n return send()\n }\n const first = send()\n warming.set(prefixKey, first)\n try {\n const result = await first\n warmedAt.set(prefixKey, now())\n return result\n } finally {\n warming.delete(prefixKey)\n }\n}\n\n/** Forgets every warmed prefix; for specs. */\nexport function resetAiDevWarmPrefixes(): void {\n warming.clear()\n warmedAt.clear()\n}\n\n// ── What a live eval prints ──────────────────────────────────────────────\n\n/**\n * The line every live eval's table carries (AGL-3660): how many of its\n * requests went to the provider and how many were answered from disk, what\n * the live ones cost at list, and how many batches carried them. A table\n * whose `live` is 0 proved nothing new — every prompt in it had been answered\n * before — and is not a live pass; `AGLYN_AI_REPLAY=refresh` asks again.\n */\nexport function aiLiveRunLedger(env: Env = process.env): AiDevReplayStats & {\n batches: number\n batchedRequests: number\n allReplayed: boolean\n} {\n const replay = aiDevReplayStats(env)\n const batch = aiLiveBatchStats()\n return {\n ...replay,\n batches: batch.batches,\n batchedRequests: batch.requests,\n allReplayed: replay.live === 0 && replay.replayed > 0,\n }\n}\n"],"names":["createHash","existsSync","mkdirSync","readFileSync","renameSync","writeFileSync","dirname","join","estimateAiProviderCostUsd","aiLiveBatchStats","AI_REPLAY_DIR_ENV","aiDevReplayMode","KEY_VERSION","aiCanonicalJson","value","JSON","stringify","sortKeys","Array","isArray","map","Object","fromEntries","keys","sort","filter","key","undefined","aiDevReplayKey","providerId","stream","request","update","v","provider","model","system","block","text","tools","messages","maxTokens","thinking","effort","digest","aiCachedPrefixKey","last","Boolean","cacheBreakpoint","lastIndexOf","slice","repositoryRoot","start","process","cwd","dir","parent","aiDevReplayDir","env","pathFor","readRecording","path","parsed","parse","writeRecording","recording","recursive","temp","pid","stats","mode","live","replayed","liveProviderUsd","savedProviderUsd","aiDevReplayStats","round","resetAiDevReplayStats","usd","Math","usageCost","usage","aiDevCountLive","discount","replayEvents","events","event","recordEvents","source","onDone","completed","push","type","some","aiDevReplayComplete","send","id","recorded","kind","result","recordedAt","Date","toISOString","aiDevReplayStream","done","find","warming","Map","warmedAt","aiDevWarmPrefixFirst","prefixKey","ttlMs","now","warmed","get","pending","catch","first","set","delete","resetAiDevWarmPrefixes","clear","aiLiveRunLedger","replay","batch","batches","batchedRequests","requests","allReplayed"],"mappings":"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"}
@@ -382,6 +382,53 @@ export declare function detectAdHocWidths(tree: AiDoctrineTree, outputKind: AiOu
382
382
  * absent, and only what was written can name them.
383
383
  */
384
384
  export declare function detectUnresponsiveGrids(tree: AiDoctrineTree, outputKind: AiOutputKind, written?: (id: string) => unknown): AiDoctrineViolation[];
385
+ /**
386
+ * Rule 12's `grid-item-size`, settled before the check reads it where it has
387
+ * one answer (AGL-3596), as `aiSettlePlanRefs` settles a plan. The re-ask for
388
+ * it states the very size to write, and a production Home page was refused and
389
+ * refunded after its re-ask still left a container's items unsized: the model
390
+ * breaks a different rule each run, and a rule whose answer the validator
391
+ * already knows is applied rather than asked for.
392
+ *
393
+ * For every Grid container, its items are read exactly as
394
+ * `detectUnresponsiveGrids` counts them, a Box or a Stack holding nothing but
395
+ * sized Grid items lending its items to the container:
396
+ *
397
+ * - an item whose size is missing, or is not full width on a phone, takes the
398
+ * size the model wrote as an object of breakpoints where that one is full
399
+ * width on a phone, and otherwise the size the re-ask would ask for
400
+ * (`gridItemSizeFor`);
401
+ * - an element that is no Grid, directly in the container, is wrapped in a new
402
+ * Grid item of that size, minted the way the palette validator mints ids;
403
+ * - a container of several items none of which steps down to columns at a
404
+ * larger width takes that size on every item, which is what the re-ask asks;
405
+ * - a `container` written as the text "true" in any case or spacing
406
+ * (`grid-container-text`) is true, since it can mean nothing else.
407
+ *
408
+ * A size that is already full width on a phone is kept. Nothing else rule 12
409
+ * names is touched: a Grid that is not a container may be meant as a row or as
410
+ * a stack (`grid-not-container`, `grid-as-stack`), and a sized item inside a
411
+ * Box (`grid-item-outside-container`) is moved only by changing the Box.
412
+ *
413
+ * `written` is the node as the model wrote it, by an id of `tree`, as
414
+ * `detectUnresponsiveGrids` reads it, for a tree the palette validator has
415
+ * read; a tree as the model wrote it is read as it stands, which is how the
416
+ * tree check settles a layout, a template or a component (`aiSettleWrittenGridItems`).
417
+ * A tree with nothing to settle comes back
418
+ * as the same object, so settling is idempotent. A node that is changed is
419
+ * copied, never edited in place.
420
+ */
421
+ export declare function aiSettleGridItems<T extends AiDoctrineTree>(tree: T, options?: {
422
+ written?: (id: string) => unknown;
423
+ mintId?: () => string;
424
+ }): T;
425
+ /**
426
+ * `aiSettleGridItems` on a tree as the model wrote it, before the palette
427
+ * validator reads it (AGL-3596): the tree check every kind but a page section
428
+ * goes through stores what the validator makes of its answer, so the answer
429
+ * is settled first. Anything that is no flat node map comes back as it was.
430
+ */
431
+ export declare function aiSettleWrittenGridItems(input: unknown): unknown;
385
432
  /** Whether an element is one rule 10 holds to a destination: a Button or a Screen Link. */
386
433
  export declare function isAiLinkElement(componentId: unknown): boolean;
387
434
  /**
@@ -576,6 +623,41 @@ export declare function estimateAiOutputLoad(score: AiOutputScore, outputKind: A
576
623
  * validator drops anything, so a re-ask can quote the node at fault.
577
624
  */
578
625
  export declare function detectDisagreeingNodes(input: unknown): AiDoctrineViolation[];
626
+ /**
627
+ * A node map that does not agree with itself, settled where it has one
628
+ * reading (AGL-3596), before `detectDisagreeingNodes` reads it. A guided
629
+ * start's Home page stopped on each of these after its re-ask, and each is
630
+ * one line the model left out, not a choice it made.
631
+ *
632
+ * - A name listed under `nodes` that the map never holds is taken out of the
633
+ * list: there is nothing written to place there.
634
+ * - An element written and listed by nobody goes where the model said it
635
+ * goes, when its own `parentId` names an element the tree reaches that can
636
+ * hold it. Failing that, a single such element goes in the single element
637
+ * the tree reaches that can hold elements and holds none, which is the shape
638
+ * of a row whose item was written beside it. Failing both, it is dropped,
639
+ * with everything under it: nothing on the page holds it, and none of it
640
+ * would ever be shown.
641
+ * - A repeated item (`repeat`) is the exception: dropping it would lose every
642
+ * card it draws, so one that cannot be placed is left for the re-ask.
643
+ *
644
+ * Reads and returns the tree as the model wrote it; an input with nothing to
645
+ * settle comes back as the same object.
646
+ */
647
+ export declare function aiSettleDisagreeingNodes(input: unknown): unknown;
648
+ /**
649
+ * A heading written past the headline ceiling, settled where it has one
650
+ * reading (AGL-3596). The palette validator cuts such a line where the
651
+ * ceiling falls, mid-word, and the check refuses the cut (rule 14,
652
+ * `copy-cut-at-ceiling`); live guided starts' heroes stopped on it after
653
+ * their re-ask. A heading whose first sentence or clause ends within the
654
+ * ceiling is a whole line already, so it is kept to that, its later words
655
+ * dropped: "Free inspections after a storm, with photos of every shingle we
656
+ * replace and a written…" becomes "Free inspections after a storm". A heading
657
+ * with no such boundary, or whose part within is a stub, is left for the
658
+ * re-ask. Reads and returns the tree as the model wrote it.
659
+ */
660
+ export declare function aiSettleCutHeadings(input: unknown): unknown;
579
661
  export interface AiDoctrineTreeReport {
580
662
  ok: boolean;
581
663
  /** The palette validator's result: the tree to store, its repairs and its id map. */