@tangle-network/agent-app 0.43.55 → 0.43.57

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 (352) hide show
  1. package/dist/DesignCanvas-UNL34LS2.js +12 -0
  2. package/dist/DesignCanvasEditor-FJIVYOA2.js +13 -0
  3. package/dist/IntakeInterview-3ZIQMZS6.js +8 -0
  4. package/dist/InvitationsPanel-CJ7DR4F7.js +8 -0
  5. package/dist/MembersPanel-BB4OD2KB.js +8 -0
  6. package/dist/{TimelineEditor-I35SJJAJ.js → TimelineEditor-2ECKSU6P.js} +3 -3
  7. package/dist/{VaultPane-DrQ_tH6c.d.ts → VaultPane-BDP6SB_K.d.ts} +1 -0
  8. package/dist/{access-BZ81Btt5.d.ts → access-490O6aOU.d.ts} +16 -1
  9. package/dist/app-auth/index.d.ts +6 -1
  10. package/dist/app-auth/index.js +3 -3
  11. package/dist/app-auth/index.js.map +1 -1
  12. package/dist/{apply-Gk4raT2j.d.ts → apply-6wlMOLf8.d.ts} +30 -1
  13. package/dist/assets/index.d.ts +41 -0
  14. package/dist/assets/index.js +1 -1
  15. package/dist/assistant/index.d.ts +26 -6
  16. package/dist/assistant/index.js +13 -12
  17. package/dist/assistant/index.js.map +1 -1
  18. package/dist/{attachment-validation-D0csjCvz.d.ts → attachment-validation-tPRp-PP1.d.ts} +7 -1
  19. package/dist/{auth-DuptSkWh.d.ts → auth-_FU8w01b.d.ts} +4 -1
  20. package/dist/billing/index.d.ts +8 -0
  21. package/dist/billing/index.js +1 -1
  22. package/dist/brand/index.d.ts +2 -0
  23. package/dist/brand/index.js.map +1 -1
  24. package/dist/brand-extraction/index.d.ts +3 -0
  25. package/dist/brand-extraction/index.js.map +1 -1
  26. package/dist/catalog/index.d.ts +2 -0
  27. package/dist/catalog/index.js +1 -1
  28. package/dist/chat-routes/index.d.ts +41 -10
  29. package/dist/chat-routes/index.js +15 -15
  30. package/dist/chat-routes/index.js.map +1 -1
  31. package/dist/chat-store/index.d.ts +18 -4
  32. package/dist/chat-store/index.js +4 -4
  33. package/dist/chat-store/index.js.map +1 -1
  34. package/dist/{chunk-H5FM75MR.js → chunk-22GLESKE.js} +3 -3
  35. package/dist/chunk-22GLESKE.js.map +1 -0
  36. package/dist/{chunk-WEBBJBDH.js → chunk-2DRYTJHI.js} +1 -1
  37. package/dist/chunk-2DRYTJHI.js.map +1 -0
  38. package/dist/{chunk-7EVZUIHW.js → chunk-3EJ6SFJI.js} +1 -1
  39. package/dist/chunk-3EJ6SFJI.js.map +1 -0
  40. package/dist/{chunk-HRJJH3RX.js → chunk-3OFQFW5K.js} +4 -4
  41. package/dist/chunk-3OFQFW5K.js.map +1 -0
  42. package/dist/{chunk-7SUPA2YT.js → chunk-4KKXZQPT.js} +2 -2
  43. package/dist/chunk-4KKXZQPT.js.map +1 -0
  44. package/dist/{chunk-5PTGEJZL.js → chunk-5SD6HT2V.js} +1 -1
  45. package/dist/chunk-5SD6HT2V.js.map +1 -0
  46. package/dist/{chunk-7C64WKAH.js → chunk-5SXS3YAB.js} +2 -2
  47. package/dist/{chunk-L2TG5DBW.js → chunk-5TESA4SN.js} +1 -1
  48. package/dist/chunk-5TESA4SN.js.map +1 -0
  49. package/dist/{chunk-63CE7FEZ.js → chunk-6XIAPIW6.js} +1 -1
  50. package/dist/chunk-6XIAPIW6.js.map +1 -0
  51. package/dist/{chunk-MFRCM32T.js → chunk-7CTIUCQ4.js} +1 -1
  52. package/dist/chunk-7CTIUCQ4.js.map +1 -0
  53. package/dist/{chunk-6KVH5SP5.js → chunk-7YW6EBDX.js} +2 -2
  54. package/dist/chunk-7YW6EBDX.js.map +1 -0
  55. package/dist/{chunk-ZXNXAQAH.js → chunk-AP2UXKUK.js} +1 -1
  56. package/dist/chunk-AP2UXKUK.js.map +1 -0
  57. package/dist/{chunk-K63OWUDJ.js → chunk-BPSV5AA2.js} +2 -2
  58. package/dist/chunk-BPSV5AA2.js.map +1 -0
  59. package/dist/{chunk-MFI4ZM7W.js → chunk-BT4QFCTB.js} +1 -1
  60. package/dist/chunk-BT4QFCTB.js.map +1 -0
  61. package/dist/{chunk-ZYBWGSAZ.js → chunk-BVKQKGRK.js} +1 -1
  62. package/dist/chunk-BVKQKGRK.js.map +1 -0
  63. package/dist/{chunk-RVUISYUE.js → chunk-C3NSB66C.js} +7 -7
  64. package/dist/chunk-C3NSB66C.js.map +1 -0
  65. package/dist/{chunk-BQV42AFK.js → chunk-CCVWR33L.js} +1 -1
  66. package/dist/chunk-CCVWR33L.js.map +1 -0
  67. package/dist/{chunk-MCJSS6SM.js → chunk-CQZSAR77.js} +1 -1
  68. package/dist/chunk-CQZSAR77.js.map +1 -0
  69. package/dist/{chunk-3SVAA3MA.js → chunk-DJ4VJIH5.js} +1 -1
  70. package/dist/chunk-DJ4VJIH5.js.map +1 -0
  71. package/dist/{chunk-XKBIYSSM.js → chunk-DTSPS2HV.js} +3 -3
  72. package/dist/chunk-DTSPS2HV.js.map +1 -0
  73. package/dist/{chunk-4QN75UHX.js → chunk-E7N67FYI.js} +8 -8
  74. package/dist/chunk-E7N67FYI.js.map +1 -0
  75. package/dist/{chunk-2QI7XV2T.js → chunk-FBVLEGEG.js} +1 -1
  76. package/dist/{chunk-2QI7XV2T.js.map → chunk-FBVLEGEG.js.map} +1 -1
  77. package/dist/{chunk-5RMIUJDI.js → chunk-FKCB5FTD.js} +1 -1
  78. package/dist/chunk-FKCB5FTD.js.map +1 -0
  79. package/dist/{chunk-EEPJGZJW.js → chunk-GPBRL2XR.js} +1 -1
  80. package/dist/chunk-GPBRL2XR.js.map +1 -0
  81. package/dist/{chunk-G3HCU7TA.js → chunk-GXE6KHTS.js} +1 -1
  82. package/dist/chunk-GXE6KHTS.js.map +1 -0
  83. package/dist/{chunk-65P3HJY3.js → chunk-HCOROIRT.js} +1 -1
  84. package/dist/chunk-HCOROIRT.js.map +1 -0
  85. package/dist/{chunk-54GSK2KK.js → chunk-HYTIHYNV.js} +2 -2
  86. package/dist/chunk-HYTIHYNV.js.map +1 -0
  87. package/dist/{chunk-5MG74GVQ.js → chunk-IG46XCZM.js} +3 -3
  88. package/dist/chunk-IG46XCZM.js.map +1 -0
  89. package/dist/{chunk-ND3XEGBE.js → chunk-JDEGS53O.js} +1 -1
  90. package/dist/chunk-JDEGS53O.js.map +1 -0
  91. package/dist/{chunk-7W5XSTUF.js → chunk-JML7WKWU.js} +1 -1
  92. package/dist/chunk-JML7WKWU.js.map +1 -0
  93. package/dist/{chunk-Q4TKVF3L.js → chunk-JTLYXUEV.js} +1 -1
  94. package/dist/chunk-JTLYXUEV.js.map +1 -0
  95. package/dist/{chunk-CCRVDLV3.js → chunk-JWBZ74TW.js} +2 -2
  96. package/dist/chunk-JWBZ74TW.js.map +1 -0
  97. package/dist/{chunk-ZU5GNSOJ.js → chunk-K3LIPKFV.js} +3 -3
  98. package/dist/chunk-K3LIPKFV.js.map +1 -0
  99. package/dist/{chunk-GQRL6VNU.js → chunk-LGBO24FL.js} +1 -1
  100. package/dist/chunk-LGBO24FL.js.map +1 -0
  101. package/dist/{chunk-SWUVTGMR.js → chunk-MEUNTJL5.js} +3 -3
  102. package/dist/{chunk-SWUVTGMR.js.map → chunk-MEUNTJL5.js.map} +1 -1
  103. package/dist/{chunk-YNALYIQ3.js → chunk-NLSSFLLS.js} +1 -1
  104. package/dist/chunk-NLSSFLLS.js.map +1 -0
  105. package/dist/{chunk-HFC4BTWJ.js → chunk-PTUMBMVH.js} +1 -1
  106. package/dist/chunk-PTUMBMVH.js.map +1 -0
  107. package/dist/{chunk-R4NOKZMD.js → chunk-ROLFDOMJ.js} +3 -3
  108. package/dist/{chunk-NYATNLRK.js → chunk-RT5RKAFX.js} +1 -1
  109. package/dist/chunk-RT5RKAFX.js.map +1 -0
  110. package/dist/{chunk-GNL3MG5J.js → chunk-S564OFTL.js} +2 -2
  111. package/dist/{chunk-GUSCYE3S.js → chunk-TQOFR6XY.js} +1 -1
  112. package/dist/chunk-TQOFR6XY.js.map +1 -0
  113. package/dist/{chunk-47C6PJCW.js → chunk-TZO3SOUL.js} +1 -1
  114. package/dist/chunk-TZO3SOUL.js.map +1 -0
  115. package/dist/{chunk-YGUNTIT5.js → chunk-UKTYKG57.js} +1 -1
  116. package/dist/chunk-UKTYKG57.js.map +1 -0
  117. package/dist/{chunk-23LUHHHN.js → chunk-VCOD5AOR.js} +1 -1
  118. package/dist/chunk-VCOD5AOR.js.map +1 -0
  119. package/dist/{chunk-5I72FRZY.js → chunk-VOJB7MVG.js} +2 -2
  120. package/dist/{chunk-XWJXZ6WE.js → chunk-VTWKH4YU.js} +2 -2
  121. package/dist/chunk-VTWKH4YU.js.map +1 -0
  122. package/dist/{chunk-4K7BZYO7.js → chunk-VWAEWOWK.js} +3 -3
  123. package/dist/chunk-VWAEWOWK.js.map +1 -0
  124. package/dist/{chunk-M2EBUDZ7.js → chunk-VWOPOLVO.js} +2 -2
  125. package/dist/chunk-VWOPOLVO.js.map +1 -0
  126. package/dist/{chunk-BN2USWIR.js → chunk-VYECQAGM.js} +2 -2
  127. package/dist/chunk-VYECQAGM.js.map +1 -0
  128. package/dist/{chunk-SIXYZ2FB.js → chunk-YJMCRXQQ.js} +1 -1
  129. package/dist/chunk-YJMCRXQQ.js.map +1 -0
  130. package/dist/{chunk-PJC4NXPA.js → chunk-YKBDH2UY.js} +3 -3
  131. package/dist/chunk-YKBDH2UY.js.map +1 -0
  132. package/dist/{chunk-XAWFPMAR.js → chunk-YKOM4ZLQ.js} +1 -1
  133. package/dist/chunk-YKOM4ZLQ.js.map +1 -0
  134. package/dist/{chunk-TKVJE63N.js → chunk-YRMLYFVR.js} +3 -3
  135. package/dist/chunk-YRMLYFVR.js.map +1 -0
  136. package/dist/{chunk-USYXHDKE.js → chunk-YZ6KQULN.js} +2 -2
  137. package/dist/{command-stack-B_-WNfZU.d.ts → command-stack-TNPWJCzW.d.ts} +14 -2
  138. package/dist/{completion-Kaqp_tWk.d.ts → completion-CvcNUz07.d.ts} +1 -1
  139. package/dist/config/index.d.ts +1 -1
  140. package/dist/{contract-KfqJh_au.d.ts → contract-TjB68uqA.d.ts} +17 -0
  141. package/dist/design-canvas/drizzle.d.ts +10 -2
  142. package/dist/design-canvas/drizzle.js.map +1 -1
  143. package/dist/design-canvas/index.d.ts +29 -11
  144. package/dist/design-canvas/index.js +1070 -17
  145. package/dist/design-canvas/index.js.map +1 -1
  146. package/dist/design-canvas-react/engine.d.ts +42 -6
  147. package/dist/design-canvas-react/engine.js +4 -4
  148. package/dist/design-canvas-react/index.d.ts +12 -7
  149. package/dist/design-canvas-react/index.js +11 -11
  150. package/dist/design-canvas-react/index.js.map +1 -1
  151. package/dist/design-canvas-react/lazy.d.ts +4 -4
  152. package/dist/design-canvas-react/lazy.js +1 -1
  153. package/dist/durable-chat/index.d.ts +46 -1
  154. package/dist/durable-chat/index.js +3 -3
  155. package/dist/eval/index.d.ts +1 -1
  156. package/dist/{export-presets-DAsU5Wal.d.ts → export-presets-mgVulRaV.d.ts} +10 -1
  157. package/dist/{flow-types-Cb_AblZs.d.ts → flow-types-CqomVAUN.d.ts} +1 -0
  158. package/dist/harness/index.d.ts +5 -0
  159. package/dist/harness/index.js +1 -1
  160. package/dist/index.d.ts +10 -18
  161. package/dist/index.js +30 -231
  162. package/dist/intakes/api.d.ts +3 -2
  163. package/dist/intakes/api.js +3 -3
  164. package/dist/intakes/api.js.map +1 -1
  165. package/dist/intakes/drizzle.d.ts +10 -2
  166. package/dist/intakes/drizzle.js +3 -3
  167. package/dist/intakes/drizzle.js.map +1 -1
  168. package/dist/intakes/index.d.ts +2 -2
  169. package/dist/intakes/index.js +2 -2
  170. package/dist/intakes-react/index.d.ts +2 -1
  171. package/dist/intakes-react/index.js +2 -2
  172. package/dist/intakes-react/lazy.d.ts +2 -1
  173. package/dist/intakes-react/lazy.js +1 -1
  174. package/dist/intakes-react/lazy.js.map +1 -1
  175. package/dist/integrations/index.d.ts +4 -0
  176. package/dist/integrations/index.js +1 -1
  177. package/dist/interactions/index.d.ts +13 -2
  178. package/dist/interactions/index.js +2 -2
  179. package/dist/{invitations-CZ5G2tIn.d.ts → invitations-DoYMvXea.d.ts} +11 -1
  180. package/dist/{invitations-schema-DaMO4hZE.d.ts → invitations-schema-_fI7HkJP.d.ts} +5 -1
  181. package/dist/knowledge/index.d.ts +2 -0
  182. package/dist/knowledge/index.js +1 -1
  183. package/dist/knowledge-loop/index.d.ts +4 -1
  184. package/dist/knowledge-loop/index.js +1 -1
  185. package/dist/{lazy-C7LiVeFt.d.ts → lazy-DrkVyDpe.d.ts} +2 -2
  186. package/dist/{mcp-BHfIoGLW.d.ts → mcp-BjPqQ-3C.d.ts} +4 -2
  187. package/dist/{mcp-rpc-DLw_r9PQ.d.ts → mcp-rpc-BfUOsalw.d.ts} +3 -0
  188. package/dist/missions/index.d.ts +25 -0
  189. package/dist/missions/index.js +1 -1
  190. package/dist/{model-Bzi5i63l.d.ts → model-C8U0HZxL.d.ts} +1 -0
  191. package/dist/{model-dF2h4xT9.d.ts → model-DmdkIteM.d.ts} +19 -0
  192. package/dist/{model-BBwyOuWG.d.ts → model-DnfO_PdJ.d.ts} +25 -1
  193. package/dist/model-resolution/index.d.ts +10 -0
  194. package/dist/model-resolution/index.js.map +1 -1
  195. package/dist/{operations-DLIvvD00.d.ts → operations-ByJ6yXOW.d.ts} +20 -2
  196. package/dist/{parts-1sRYSPjF.d.ts → parts-n9XUon0a.d.ts} +29 -1
  197. package/dist/plans/index.d.ts +6 -0
  198. package/dist/plans/index.js +1 -1
  199. package/dist/platform/index.d.ts +24 -2
  200. package/dist/platform/index.js +3 -3
  201. package/dist/platform/index.js.map +1 -1
  202. package/dist/preflight/index.d.ts +3 -0
  203. package/dist/preflight/index.js +1 -1
  204. package/dist/preset-cloudflare/index.d.ts +4 -1
  205. package/dist/preset-cloudflare/index.js +2 -2
  206. package/dist/redact/index.d.ts +5 -0
  207. package/dist/redact/index.js +1 -1
  208. package/dist/{roles-BC1n4t37.d.ts → roles-B4nU3jDi.d.ts} +12 -0
  209. package/dist/run/index.d.ts +3 -0
  210. package/dist/run/index.js +1 -1
  211. package/dist/run/index.js.map +1 -1
  212. package/dist/runtime/index.d.ts +11 -4
  213. package/dist/runtime/index.js +4 -4
  214. package/dist/sandbox/index.d.ts +96 -3
  215. package/dist/sandbox/index.js +6 -6
  216. package/dist/{sandbox-terminal-BIIC__CP.d.ts → sandbox-terminal-ChNEdHF8.d.ts} +5 -0
  217. package/dist/sequences/drizzle.d.ts +11 -1
  218. package/dist/sequences/drizzle.js +1 -1
  219. package/dist/sequences/drizzle.js.map +1 -1
  220. package/dist/sequences/index.d.ts +32 -7
  221. package/dist/sequences/index.js +1562 -47
  222. package/dist/sequences/index.js.map +1 -1
  223. package/dist/sequences-react/index.d.ts +32 -2
  224. package/dist/sequences-react/index.js +3 -3
  225. package/dist/sequences-react/index.js.map +1 -1
  226. package/dist/{sso-CNOsARMJ.d.ts → sso-Cm00S61u.d.ts} +13 -0
  227. package/dist/{stale-turn-lock-C8Na1cFZ.d.ts → stale-turn-lock-DucQzvXu.d.ts} +2 -0
  228. package/dist/store/index.d.ts +5 -0
  229. package/dist/store/index.js.map +1 -1
  230. package/dist/{store-gckrNq-g.d.ts → store-B7LLlk9p.d.ts} +22 -1
  231. package/dist/{store-CFyxgTlD.d.ts → store-CqfDtnPQ.d.ts} +8 -2
  232. package/dist/stream/index.d.ts +10 -4
  233. package/dist/stream/index.js +4 -4
  234. package/dist/{stream-normalizer-DWvtmY6F.d.ts → stream-normalizer-C3M--r6y.d.ts} +17 -1
  235. package/dist/studio/index.d.ts +37 -0
  236. package/dist/studio/index.js +1 -1
  237. package/dist/studio-react/index.d.ts +3 -0
  238. package/dist/studio-react/index.js +1 -1
  239. package/dist/studio-react/index.js.map +1 -1
  240. package/dist/tangle/index.d.ts +3 -0
  241. package/dist/tangle/index.js +1 -1
  242. package/dist/teams/drizzle.d.ts +8 -4
  243. package/dist/teams/drizzle.js +1 -1
  244. package/dist/teams/drizzle.js.map +1 -1
  245. package/dist/teams/index.d.ts +4 -2
  246. package/dist/teams/index.js +3 -3
  247. package/dist/teams/invitations-api.d.ts +10 -4
  248. package/dist/teams/invitations-api.js +4 -4
  249. package/dist/teams/invitations-api.js.map +1 -1
  250. package/dist/teams/members-api.d.ts +4 -2
  251. package/dist/teams/members-api.js +3 -3
  252. package/dist/teams/resend.d.ts +5 -4
  253. package/dist/teams/resend.js +1 -1
  254. package/dist/teams/resend.js.map +1 -1
  255. package/dist/teams-react/index.d.ts +7 -2
  256. package/dist/teams-react/index.js +3 -3
  257. package/dist/teams-react/lazy.d.ts +5 -2
  258. package/dist/teams-react/lazy.js +2 -2
  259. package/dist/teams-react/lazy.js.map +1 -1
  260. package/dist/theme/index.d.ts +2 -0
  261. package/dist/theme/index.js +1 -1
  262. package/dist/theme/tailwind-preset.d.ts +1 -0
  263. package/dist/theme/tailwind-preset.js.map +1 -1
  264. package/dist/theme-contract/cli.js +1 -1
  265. package/dist/theme-contract/index.d.ts +3 -0
  266. package/dist/theme-contract/index.js +1 -1
  267. package/dist/tools/index.d.ts +13 -6
  268. package/dist/tools/index.js +3 -3
  269. package/dist/trace/index.d.ts +6 -1
  270. package/dist/trace/index.js +2 -2
  271. package/dist/{turn-buffer-C9mEgoop.d.ts → turn-buffer-DGnAPKwa.d.ts} +6 -0
  272. package/dist/turn-stream/index.d.ts +28 -2
  273. package/dist/turn-stream/index.js +1 -1
  274. package/dist/turn-stream/index.js.map +1 -1
  275. package/dist/{types-BEOvc_ue.d.ts → types-CBRyqijY.d.ts} +10 -0
  276. package/dist/vault/index.d.ts +1 -1
  277. package/dist/vault/lazy.d.ts +3 -2
  278. package/dist/vault/lazy.js.map +1 -1
  279. package/dist/web/index.d.ts +4 -0
  280. package/dist/web/index.js +1 -1
  281. package/dist/web-react/index.d.ts +51 -8
  282. package/dist/web-react/index.js +9 -9
  283. package/dist/web-react/terminal.d.ts +2 -2
  284. package/dist/web-react/terminal.js +1 -1
  285. package/package.json +3 -2
  286. package/dist/DesignCanvas-5DJAFDNC.js +0 -12
  287. package/dist/DesignCanvasEditor-XUJZARSN.js +0 -13
  288. package/dist/IntakeInterview-WHVKMNQW.js +0 -8
  289. package/dist/InvitationsPanel-XUUWZBPV.js +0 -8
  290. package/dist/MembersPanel-FOVHWUQ3.js +0 -8
  291. package/dist/chunk-23LUHHHN.js.map +0 -1
  292. package/dist/chunk-3SVAA3MA.js.map +0 -1
  293. package/dist/chunk-47C6PJCW.js.map +0 -1
  294. package/dist/chunk-4K7BZYO7.js.map +0 -1
  295. package/dist/chunk-4QN75UHX.js.map +0 -1
  296. package/dist/chunk-54GSK2KK.js.map +0 -1
  297. package/dist/chunk-5MG74GVQ.js.map +0 -1
  298. package/dist/chunk-5PTGEJZL.js.map +0 -1
  299. package/dist/chunk-5RMIUJDI.js.map +0 -1
  300. package/dist/chunk-63CE7FEZ.js.map +0 -1
  301. package/dist/chunk-65P3HJY3.js.map +0 -1
  302. package/dist/chunk-6KVH5SP5.js.map +0 -1
  303. package/dist/chunk-7EVZUIHW.js.map +0 -1
  304. package/dist/chunk-7SUPA2YT.js.map +0 -1
  305. package/dist/chunk-7W5XSTUF.js.map +0 -1
  306. package/dist/chunk-BN2USWIR.js.map +0 -1
  307. package/dist/chunk-BQV42AFK.js.map +0 -1
  308. package/dist/chunk-CCRVDLV3.js.map +0 -1
  309. package/dist/chunk-EEPJGZJW.js.map +0 -1
  310. package/dist/chunk-G3HCU7TA.js.map +0 -1
  311. package/dist/chunk-GQRL6VNU.js.map +0 -1
  312. package/dist/chunk-GUSCYE3S.js.map +0 -1
  313. package/dist/chunk-H5FM75MR.js.map +0 -1
  314. package/dist/chunk-HFC4BTWJ.js.map +0 -1
  315. package/dist/chunk-HRJJH3RX.js.map +0 -1
  316. package/dist/chunk-K63OWUDJ.js.map +0 -1
  317. package/dist/chunk-L2TG5DBW.js.map +0 -1
  318. package/dist/chunk-M2EBUDZ7.js.map +0 -1
  319. package/dist/chunk-MCJSS6SM.js.map +0 -1
  320. package/dist/chunk-MFI4ZM7W.js.map +0 -1
  321. package/dist/chunk-MFRCM32T.js.map +0 -1
  322. package/dist/chunk-ND3XEGBE.js.map +0 -1
  323. package/dist/chunk-NEOV2NQ3.js +0 -1617
  324. package/dist/chunk-NEOV2NQ3.js.map +0 -1
  325. package/dist/chunk-NYATNLRK.js.map +0 -1
  326. package/dist/chunk-PJC4NXPA.js.map +0 -1
  327. package/dist/chunk-Q4TKVF3L.js.map +0 -1
  328. package/dist/chunk-RVUISYUE.js.map +0 -1
  329. package/dist/chunk-SIXYZ2FB.js.map +0 -1
  330. package/dist/chunk-TKVJE63N.js.map +0 -1
  331. package/dist/chunk-WEBBJBDH.js.map +0 -1
  332. package/dist/chunk-WFMUDX5J.js +0 -1092
  333. package/dist/chunk-WFMUDX5J.js.map +0 -1
  334. package/dist/chunk-XAWFPMAR.js.map +0 -1
  335. package/dist/chunk-XKBIYSSM.js.map +0 -1
  336. package/dist/chunk-XWJXZ6WE.js.map +0 -1
  337. package/dist/chunk-YGUNTIT5.js.map +0 -1
  338. package/dist/chunk-YNALYIQ3.js.map +0 -1
  339. package/dist/chunk-ZU5GNSOJ.js.map +0 -1
  340. package/dist/chunk-ZXNXAQAH.js.map +0 -1
  341. package/dist/chunk-ZYBWGSAZ.js.map +0 -1
  342. /package/dist/{DesignCanvas-5DJAFDNC.js.map → DesignCanvas-UNL34LS2.js.map} +0 -0
  343. /package/dist/{DesignCanvasEditor-XUJZARSN.js.map → DesignCanvasEditor-FJIVYOA2.js.map} +0 -0
  344. /package/dist/{IntakeInterview-WHVKMNQW.js.map → IntakeInterview-3ZIQMZS6.js.map} +0 -0
  345. /package/dist/{InvitationsPanel-XUUWZBPV.js.map → InvitationsPanel-CJ7DR4F7.js.map} +0 -0
  346. /package/dist/{MembersPanel-FOVHWUQ3.js.map → MembersPanel-BB4OD2KB.js.map} +0 -0
  347. /package/dist/{TimelineEditor-I35SJJAJ.js.map → TimelineEditor-2ECKSU6P.js.map} +0 -0
  348. /package/dist/{chunk-7C64WKAH.js.map → chunk-5SXS3YAB.js.map} +0 -0
  349. /package/dist/{chunk-R4NOKZMD.js.map → chunk-ROLFDOMJ.js.map} +0 -0
  350. /package/dist/{chunk-GNL3MG5J.js.map → chunk-S564OFTL.js.map} +0 -0
  351. /package/dist/{chunk-5I72FRZY.js.map → chunk-VOJB7MVG.js.map} +0 -0
  352. /package/dist/{chunk-USYXHDKE.js.map → chunk-YZ6KQULN.js.map} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runtime/model.ts"],"sourcesContent":["/**\n * Resolve the model config a Tangle agent's sandbox/runtime runs on.\n *\n * Every Tangle agent product resolves the SAME thing from env: the Tangle Router\n * (OpenAI-compatible, metered at the platform markup against a single\n * `TANGLE_API_KEY`) by default, with a direct-Anthropic BYOK escape hatch. The\n * shape feeds the sandbox SDK's `backend.model`. Lifted here so no product\n * hand-rolls the env parsing + the router default.\n */\n\nexport interface TangleModelConfig {\n /** The Tangle Router is OpenAI-compatible → driven via `openai-compat`.\n * `anthropic` is the BYOK escape hatch. */\n provider: 'openai-compat' | 'anthropic'\n model: string\n apiKey: string\n baseUrl: string\n}\n\nexport type TangleExecutionEnvironment = 'development' | 'staging' | 'production' | 'test'\nexport type TangleExecutionKeySource = 'local-env' | 'user'\nexport type TangleExecutionKeyErrorCode =\n | 'local_tangle_api_key_required'\n | 'tangle_account_not_connected'\n\nexport interface ResolveModelOptions {\n /** Env to read (defaults to process.env). */\n env?: Record<string, string | undefined>\n /** Router base URL default when `TANGLE_ROUTER_BASE_URL` is unset. */\n defaultRouterBaseUrl?: string\n}\n\nexport interface ResolveUserTangleExecutionKeyOptions {\n /** Deployment context. Only local development may fall back to env keys. */\n environment?: TangleExecutionEnvironment\n /** Env to read for the local-development fallback. */\n env?: Record<string, string | undefined>\n /** App-owned lookup for the caller's linked platform API key. */\n getUserApiKey: () => string | null | undefined | Promise<string | null | undefined>\n}\n\nexport interface ResolveUserTangleExecutionKeyForUserOptions<UserId = string> {\n userId: UserId\n environment?: TangleExecutionEnvironment\n env?: Record<string, string | undefined>\n getUserApiKey: (userId: UserId) => string | null | undefined | Promise<string | null | undefined>\n}\n\nexport interface ResolvedTangleExecutionKey {\n apiKey: string\n source: TangleExecutionKeySource\n}\n\nexport interface ResolveTangleDevOrUserKeyOptions {\n /** Deployment context. Only local development may use the env key. */\n environment?: TangleExecutionEnvironment\n /** Env to read for the local-development fallback. */\n env?: Record<string, string | undefined>\n /** App-owned lookup for the caller's linked platform API key. */\n getUserApiKey: () => string | null | undefined | Promise<string | null | undefined>\n}\n\nexport interface TangleExecutionKeyHttpError {\n status: number\n body: {\n error: string\n code: TangleExecutionKeyErrorCode\n }\n}\n\nexport interface CreateTangleRouterModelConfigOptions {\n apiKey: string\n model: string\n baseUrl?: string\n}\n\nexport interface TangleBillingEnforcementOptions {\n /** Env to read (defaults to process.env). */\n env?: Record<string, string | undefined>\n /**\n * Optional app-specific override flag, e.g. `GTM_BILLING_ENFORCEMENT`.\n * Defaults to the shared `TANGLE_BILLING_ENFORCEMENT`.\n */\n enforcementEnvVar?: string\n}\n\nexport const DEFAULT_TANGLE_ROUTER_BASE_URL = 'https://router.tangle.tools/v1'\nexport const DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR = 'TANGLE_BILLING_ENFORCEMENT'\n\nfunction requireEnv(env: Record<string, string | undefined>, name: string): string {\n const value = env[name]?.trim()\n if (!value) throw new Error(`${name} is required`)\n return value\n}\n\nexport function trimOrNull(value: string | null | undefined): string | null {\n const trimmed = value?.trim()\n return trimmed ? trimmed : null\n}\n\nfunction isTangleExecutionKeyErrorCode(value: unknown): value is TangleExecutionKeyErrorCode {\n return value === 'local_tangle_api_key_required' || value === 'tangle_account_not_connected'\n}\n\nexport class TangleExecutionKeyError extends Error {\n readonly code: TangleExecutionKeyErrorCode\n readonly status: number\n\n constructor(code: TangleExecutionKeyErrorCode, message: string, status: number) {\n super(message)\n this.name = 'TangleExecutionKeyError'\n this.code = code\n this.status = status\n }\n}\n\nexport function isTangleExecutionKeyError(error: unknown): error is TangleExecutionKeyError {\n return error instanceof TangleExecutionKeyError\n || (\n typeof error === 'object'\n && error !== null\n && (error as { name?: unknown }).name === 'TangleExecutionKeyError'\n && typeof (error as { message?: unknown }).message === 'string'\n && isTangleExecutionKeyErrorCode((error as { code?: unknown }).code)\n && typeof (error as { status?: unknown }).status === 'number'\n )\n}\n\nexport function resolveTangleExecutionEnvironment(\n env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,\n): TangleExecutionEnvironment {\n const raw = (env.APP_ENV ?? env.NODE_ENV ?? '').trim().toLowerCase()\n if (raw === 'development' || raw === 'dev' || raw === 'local') return 'development'\n if (raw === 'staging') return 'staging'\n if (raw === 'test') return 'test'\n return 'production'\n}\n\n/**\n * Shared policy for agent products that bill through the Tangle Platform.\n *\n * Local development defaults billing enforcement off so apps can use a local\n * `TANGLE_API_KEY` without requiring a browser-linked platform account. Any\n * non-development environment defaults enforcement on. Apps may pass their own\n * override flag (`FOO_BILLING_ENFORCEMENT`) while new apps can use the shared\n * `TANGLE_BILLING_ENFORCEMENT`.\n */\nexport function isTangleBillingEnforcementDisabled(\n opts: TangleBillingEnforcementOptions = {},\n): boolean {\n const env = opts.env ?? (process.env as Record<string, string | undefined>)\n const enforcementEnvVar = opts.enforcementEnvVar ?? DEFAULT_TANGLE_BILLING_ENFORCEMENT_ENV_VAR\n const override = env[enforcementEnvVar]?.trim().toLowerCase()\n\n if (override === 'disabled') return true\n if (override === 'enabled') return false\n\n return resolveTangleExecutionEnvironment(env) === 'development'\n}\n\nexport function tangleExecutionKeyHttpError(error: unknown): TangleExecutionKeyHttpError | null {\n if (!isTangleExecutionKeyError(error)) return null\n return {\n status: error.status,\n body: {\n error: error.message,\n code: error.code,\n },\n }\n}\n\n/**\n * Shared dev-aware Tangle key resolution. Local development may use a server\n * `TANGLE_API_KEY`; otherwise the caller's linked platform key is used. Returns\n * null when neither resolves, so each caller can throw its own domain error\n * (e.g. model-execution 503/401 vs hub `tangle_link_required` 412).\n */\nexport async function resolveTangleDevOrUserKey(\n opts: ResolveTangleDevOrUserKeyOptions,\n): Promise<ResolvedTangleExecutionKey | null> {\n const env = opts.env ?? (process.env as Record<string, string | undefined>)\n const environment = opts.environment ?? resolveTangleExecutionEnvironment(env)\n\n if (environment === 'development') {\n const apiKey = trimOrNull(env.TANGLE_API_KEY)\n if (apiKey) return { apiKey, source: 'local-env' }\n }\n\n const apiKey = trimOrNull(await opts.getUserApiKey())\n if (apiKey) return { apiKey, source: 'user' }\n\n return null\n}\n\n/**\n * Resolve the user-facing Tangle API key for model execution.\n *\n * Local development may use a server env key so apps remain easy to run.\n * Deployed contexts must use the caller's linked platform key; this keeps\n * model execution, billing, and account ownership aligned across products.\n */\nexport async function resolveUserTangleExecutionKey(\n opts: ResolveUserTangleExecutionKeyOptions,\n): Promise<ResolvedTangleExecutionKey> {\n const env = opts.env ?? (process.env as Record<string, string | undefined>)\n const environment = opts.environment ?? resolveTangleExecutionEnvironment(env)\n\n const resolved = await resolveTangleDevOrUserKey({ environment, env, getUserApiKey: opts.getUserApiKey })\n if (resolved) return resolved\n\n if (environment === 'development') {\n throw new TangleExecutionKeyError(\n 'local_tangle_api_key_required',\n 'TANGLE_API_KEY or a linked Tangle account is required for local Tangle model execution.',\n 503,\n )\n }\n\n throw new TangleExecutionKeyError(\n 'tangle_account_not_connected',\n 'Connect your Tangle account before invoking this agent.',\n 401,\n )\n}\n\nexport async function resolveUserTangleExecutionKeyForUser<UserId = string>(\n opts: ResolveUserTangleExecutionKeyForUserOptions<UserId>,\n): Promise<ResolvedTangleExecutionKey> {\n return resolveUserTangleExecutionKey({\n environment: opts.environment,\n env: opts.env,\n getUserApiKey: () => opts.getUserApiKey(opts.userId),\n })\n}\n\n/**\n * Build an OpenAI-compatible Tangle Router model config from an already\n * resolved execution key. This intentionally does not read TANGLE_API_KEY.\n */\nexport function createTangleRouterModelConfig(\n opts: CreateTangleRouterModelConfigOptions,\n): TangleModelConfig {\n const apiKey = opts.apiKey.trim()\n if (!apiKey) throw new Error('apiKey is required')\n const model = opts.model.trim()\n if (!model) throw new Error('model is required')\n return {\n provider: 'openai-compat',\n model,\n apiKey,\n baseUrl: (opts.baseUrl?.trim() || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\\/+$/, ''),\n }\n}\n\n/**\n * Resolve the model config from env. DEFAULT path (`MODEL_PROVIDER` unset or\n * `openai-compat`/`tangle-router`/`tcloud`): the Tangle Router, authenticated\n * with `TANGLE_API_KEY`, model from `MODEL_NAME`. BYOK path\n * (`MODEL_PROVIDER=anthropic`): direct Anthropic with `ANTHROPIC_API_KEY` +\n * `ANTHROPIC_BASE_URL`. Throws (fail-loud) on a missing required var so a\n * misconfigured deploy fails at boot, not mid-turn.\n */\nexport function resolveTangleModelConfig(opts: ResolveModelOptions = {}): TangleModelConfig {\n const env = opts.env ?? (process.env as Record<string, string | undefined>)\n const provider = env.MODEL_PROVIDER?.trim() || 'openai-compat'\n const model = requireEnv(env, 'MODEL_NAME')\n\n if (provider === 'openai-compat' || provider === 'tangle-router' || provider === 'tcloud') {\n return {\n provider: 'openai-compat',\n model,\n apiKey: requireEnv(env, 'TANGLE_API_KEY'),\n baseUrl: (env.TANGLE_ROUTER_BASE_URL?.trim() || opts.defaultRouterBaseUrl || DEFAULT_TANGLE_ROUTER_BASE_URL).replace(/\\/+$/, ''),\n }\n }\n\n if (provider === 'anthropic') {\n return {\n provider,\n model,\n apiKey: requireEnv(env, 'ANTHROPIC_API_KEY'),\n baseUrl: requireEnv(env, 'ANTHROPIC_BASE_URL'),\n }\n }\n\n throw new Error(`Unsupported MODEL_PROVIDER: ${provider} (use openai-compat for the Tangle Router, or anthropic for BYOK)`)\n}\n"],"mappings":"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","names":["apiKey"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/design-canvas-react/engine/snap.ts","../src/design-canvas-react/engine/selection.ts","../src/design-canvas-react/engine/zoom-pan.ts","../src/design-canvas-react/export-math.ts","../src/design-canvas-react/insert-builders.ts"],"sourcesContent":["/**\n * 2-axis snap engine for the design-canvas editor. Targets come from element\n * AABBs (edges + centers), page edges + center, saved guides, and grid lines\n * near the moving bounds. Grid lines are generated lazily in the neighborhood\n * of the moving element, not the full page — avoids allocating thousands of\n * targets on large pages with fine grids.\n *\n * Threshold is a SCREEN distance (pixels), divided by zoom to convert to\n * document units — what \"feels close\" is a screen distance.\n *\n * Tie-breaking: non-grid kinds beat grid on equal distance; among equals of\n * the same priority the first in iteration order wins.\n */\n\nimport { elementAabb } from '../../design-canvas/model'\nimport type { Bounds, SceneElement, ScenePage } from '../../design-canvas/model'\nimport type { EditorSceneState, SnapEngine, SnapResult, SnapTarget, SnapTargetKind, SnapTargets } from '../contracts'\n\n/** Priority rank for tie-breaking: lower = higher priority (wins the tie). */\nconst KIND_PRIORITY: Record<SnapTargetKind, number> = {\n 'guide': 0,\n 'page-edge': 1,\n 'page-center': 2,\n 'element-edge': 3,\n 'element-center': 4,\n 'grid': 5,\n}\n\nexport function createSnapEngine(): SnapEngine {\n return {\n collectTargets(state: EditorSceneState, excludeIds: string[]): SnapTargets {\n const page = state.document.pages.find((p) => p.id === state.activePageId)\n if (!page) throw new Error(`collectTargets: active page ${state.activePageId} not found`)\n\n const vertical: SnapTarget[] = []\n const horizontal: SnapTarget[] = []\n const excludeSet = new Set(excludeIds)\n\n // Page edges + center\n vertical.push({ position: 0, kind: 'page-edge' })\n vertical.push({ position: page.width, kind: 'page-edge' })\n vertical.push({ position: page.width / 2, kind: 'page-center' })\n horizontal.push({ position: 0, kind: 'page-edge' })\n horizontal.push({ position: page.height, kind: 'page-edge' })\n horizontal.push({ position: page.height / 2, kind: 'page-center' })\n\n // Saved ruler guides\n for (const pos of page.guides.vertical) {\n vertical.push({ position: pos, kind: 'guide' })\n }\n for (const pos of page.guides.horizontal) {\n horizontal.push({ position: pos, kind: 'guide' })\n }\n\n // Element edges + centers (visible, unlocked, not excluded)\n collectElementTargets(page.elements, excludeSet, vertical, horizontal)\n\n // Grid lines (deferred to apply() with the moving bounds neighborhood)\n // Grid targets are injected by collectGridTargets() when gridEnabled.\n\n return { vertical, horizontal }\n },\n\n apply(bounds: Bounds, targets: SnapTargets, thresholdPx: number, zoom: number): SnapResult {\n if (!Number.isFinite(zoom) || zoom <= 0) {\n throw new Error(`snap.apply: zoom must be a positive finite number, got ${zoom}`)\n }\n if (!Number.isFinite(thresholdPx) || thresholdPx < 0) {\n throw new Error(`snap.apply: thresholdPx must be a non-negative finite number, got ${thresholdPx}`)\n }\n const threshold = thresholdPx / zoom\n\n const snappedX = snapAxis(bounds.x, bounds.x + bounds.width / 2, bounds.x + bounds.width, targets.vertical, threshold)\n const snappedY = snapAxis(bounds.y, bounds.y + bounds.height / 2, bounds.y + bounds.height, targets.horizontal, threshold)\n\n return {\n x: snappedX !== null ? snappedX.docPosition - snappedX.elementOffset : bounds.x,\n y: snappedY !== null ? snappedY.docPosition - snappedY.elementOffset : bounds.y,\n activeVertical: snappedX !== null ? snappedX.target : null,\n activeHorizontal: snappedY !== null ? snappedY.target : null,\n }\n },\n }\n}\n\n/** Generate grid line targets within a neighborhood around the moving bounds.\n * Call this and append to `SnapTargets.vertical`/`horizontal` before\n * passing to `apply()` when `state.gridEnabled`. */\nexport function collectGridTargets(\n bounds: Bounds,\n gridSize: number,\n page: ScenePage,\n thresholdDocPx: number,\n): { vertical: SnapTarget[]; horizontal: SnapTarget[] } {\n if (!Number.isFinite(gridSize) || gridSize <= 0) {\n throw new Error(`collectGridTargets: gridSize must be positive finite, got ${gridSize}`)\n }\n const vertical: SnapTarget[] = []\n const horizontal: SnapTarget[] = []\n\n // Generate lines that fall within the bounds + threshold neighborhood\n const xMin = Math.floor((bounds.x - thresholdDocPx) / gridSize) * gridSize\n const xMax = Math.ceil((bounds.x + bounds.width + thresholdDocPx) / gridSize) * gridSize\n for (let x = xMin; x <= xMax; x += gridSize) {\n if (x >= 0 && x <= page.width) vertical.push({ position: x, kind: 'grid' })\n }\n\n const yMin = Math.floor((bounds.y - thresholdDocPx) / gridSize) * gridSize\n const yMax = Math.ceil((bounds.y + bounds.height + thresholdDocPx) / gridSize) * gridSize\n for (let y = yMin; y <= yMax; y += gridSize) {\n if (y >= 0 && y <= page.height) horizontal.push({ position: y, kind: 'grid' })\n }\n\n return { vertical, horizontal }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\ninterface SnapHit {\n /** The snap target that won. */\n target: SnapTarget\n /** The document-coordinate position of the snap line. */\n docPosition: number\n /** How far the element's reference point (left/center/right or top/mid/bottom)\n * is from the element's x/y origin — used to reconstruct the new origin. */\n elementOffset: number\n}\n\n/** Try snapping each of the three reference points (start/center/end) to the\n * nearest target. Returns the best hit across all three, or null. */\nfunction snapAxis(start: number, center: number, end: number, targets: SnapTarget[], threshold: number): SnapHit | null {\n const candidates: Array<{ value: number; offset: number }> = [\n { value: start, offset: 0 },\n { value: center, offset: center - start },\n { value: end, offset: end - start },\n ]\n\n let best: SnapHit | null = null\n let bestDistance = Infinity\n let bestPriority = Infinity\n\n for (const { value, offset } of candidates) {\n for (const target of targets) {\n const distance = Math.abs(target.position - value)\n if (distance > threshold) continue\n const priority = KIND_PRIORITY[target.kind]\n if (\n distance < bestDistance ||\n (distance === bestDistance && priority < bestPriority)\n ) {\n bestDistance = distance\n bestPriority = priority\n best = { target, docPosition: target.position, elementOffset: offset }\n }\n }\n }\n\n return best\n}\n\nfunction collectElementTargets(\n elements: SceneElement[],\n excludeIds: Set<string>,\n vertical: SnapTarget[],\n horizontal: SnapTarget[],\n): void {\n for (const el of elements) {\n if (!el.visible || el.locked) continue\n if (excludeIds.has(el.id)) continue\n\n const aabb = elementAabb(el)\n // Left, center, right\n vertical.push({ position: aabb.x, kind: 'element-edge' })\n vertical.push({ position: aabb.x + aabb.width / 2, kind: 'element-center' })\n vertical.push({ position: aabb.x + aabb.width, kind: 'element-edge' })\n // Top, middle, bottom\n horizontal.push({ position: aabb.y, kind: 'element-edge' })\n horizontal.push({ position: aabb.y + aabb.height / 2, kind: 'element-center' })\n horizontal.push({ position: aabb.y + aabb.height, kind: 'element-edge' })\n\n if (el.kind === 'group') {\n collectElementTargets(el.children, excludeIds, vertical, horizontal)\n }\n }\n}\n","/**\n * Selection math for the design-canvas editor: marquee hit-tests, keyboard\n * nudge deltas, and the duplicate offset constant. Pure functions — no DOM, no\n * Konva, no React.\n *\n * Marquee inclusion: by default any element whose AABB intersects the marquee\n * rect is included (the \"touch\" model). Pass `requireFullContainment: true` for\n * the \"surround\" model where the marquee must fully contain the element. Locked\n * and invisible elements are never selected regardless.\n */\n\nimport { boundsIntersect, elementAabb } from '../../design-canvas/model'\nimport type { Bounds, SceneElement, ScenePage } from '../../design-canvas/model'\n\n// ---------------------------------------------------------------------------\n// Point hit-test (model-space)\n// ---------------------------------------------------------------------------\n\n/**\n * Top-most selectable element whose AABB contains the document-space point, or\n * null when the point is over empty space.\n *\n * This is the authoritative \"is the pointer over an element?\" test for the\n * pointer-down gesture router. It runs against the scene model in document\n * coordinates rather than Konva's hit-graph canvas, so it is independent of\n * clip groups, listening flags, and hit-canvas redraw timing — all of which\n * could make `stage.getIntersection` misclassify a press as empty space and\n * silently start a marquee instead of an element drag.\n *\n * Z-order: later elements paint on top, so we scan the array in reverse and\n * return the first hit. Groups descend into children (top child wins); a hit on\n * a group child resolves to the group itself, matching drag/selection which\n * operate on the group as a unit. Locked and invisible elements never hit.\n */\nexport function hitTestPoint(page: ScenePage, x: number, y: number): string | null {\n return hitTestIn(page.elements, x, y, null)\n}\n\nfunction hitTestIn(\n elements: SceneElement[],\n x: number,\n y: number,\n resolveTo: string | null,\n): string | null {\n for (let i = elements.length - 1; i >= 0; i -= 1) {\n const el = elements[i]!\n if (!el.visible || el.locked) continue\n const aabb = elementAabb(el)\n if (!pointInBounds(aabb, x, y)) continue\n if (el.kind === 'group') {\n // A press anywhere inside the group AABB selects the group as a unit,\n // even over the gaps between children — the group is the draggable node.\n const child = hitTestIn(el.children, x, y, el.id)\n return child ?? el.id\n }\n return resolveTo ?? el.id\n }\n return null\n}\n\nfunction pointInBounds(b: Bounds, x: number, y: number): boolean {\n return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height\n}\n\nexport interface MarqueeSelectOptions {\n /** When true, the element's AABB must be fully inside the marquee; default is\n * intersection (any overlap selects). */\n requireFullContainment?: boolean\n}\n\n/** Returns the ids of selectable elements on `page` whose AABB intersects (or\n * is contained by) `rect`. Locked and invisible elements are excluded. */\nexport function marqueeSelect(page: ScenePage, rect: Bounds, opts: MarqueeSelectOptions = {}): string[] {\n assertValidBounds(rect, 'marqueeSelect rect')\n const result: string[] = []\n collectMarqueeIds(page.elements, rect, opts.requireFullContainment ?? false, result)\n return result\n}\n\nfunction collectMarqueeIds(\n elements: SceneElement[],\n rect: Bounds,\n requireContainment: boolean,\n result: string[],\n): void {\n for (const el of elements) {\n if (!el.visible || el.locked) continue\n const aabb = elementAabb(el)\n\n if (el.kind === 'group') {\n // Groups are transparent to marquee: descend when the group AABB intersects\n // the rect, collecting children individually. The group entity itself is\n // never added — marquee selects leaf elements, not the group wrapper.\n if (boundsIntersect(rect, aabb)) {\n collectMarqueeIds(el.children, rect, requireContainment, result)\n }\n continue\n }\n\n const hit = requireContainment ? boundsContain(rect, aabb) : boundsIntersect(rect, aabb)\n if (hit) result.push(el.id)\n }\n}\n\n/** Returns true when `outer` fully contains `inner` (edges may touch). */\nfunction boundsContain(outer: Bounds, inner: Bounds): boolean {\n return (\n inner.x >= outer.x &&\n inner.y >= outer.y &&\n inner.x + inner.width <= outer.x + outer.width &&\n inner.y + inner.height <= outer.y + outer.height\n )\n}\n\n// ---------------------------------------------------------------------------\n// Keyboard nudge\n// ---------------------------------------------------------------------------\n\n/** Pixels to move per nudge step at normal and shifted speed. */\nconst NUDGE_NORMAL_PX = 1\nconst NUDGE_SHIFT_PX = 10\n\nexport interface NudgeDelta {\n dx: number\n dy: number\n}\n\n/** Map an arrow key to a document-px delta. `shift` engages the 10× step.\n * Throws on unknown keys so callers handle only real nudge keys. */\nexport function nudgeDelta(key: 'ArrowLeft' | 'ArrowRight' | 'ArrowUp' | 'ArrowDown', shift: boolean): NudgeDelta {\n const step = shift ? NUDGE_SHIFT_PX : NUDGE_NORMAL_PX\n switch (key) {\n case 'ArrowLeft': return { dx: -step, dy: 0 }\n case 'ArrowRight': return { dx: step, dy: 0 }\n case 'ArrowUp': return { dx: 0, dy: -step }\n case 'ArrowDown': return { dx: 0, dy: step }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Duplicate offset\n// ---------------------------------------------------------------------------\n\n/** Document-px offset applied to duplicated elements so the copy is visually\n * separated from the original (matching common design-tool convention). */\nexport const DUPLICATE_OFFSET: NudgeDelta = { dx: 10, dy: 10 }\n\n// ---------------------------------------------------------------------------\n// Internal\n// ---------------------------------------------------------------------------\n\nfunction assertValidBounds(bounds: Bounds, label: string): void {\n if (!Number.isFinite(bounds.x) || !Number.isFinite(bounds.y) ||\n !Number.isFinite(bounds.width) || !Number.isFinite(bounds.height)) {\n throw new Error(`${label}: all fields must be finite numbers`)\n }\n}\n","/**\n * Zoom + pan coordinate math for the design-canvas editor. Zoom is pixels-per-\n * document-px (e.g. 2 = 200% magnification). The key invariant for wheel-zoom\n * is that the document point under the cursor stays fixed in screen space:\n *\n * docPoint = (screenPoint - pan) / zoom\n * newPan = screenPoint - docPoint * newZoom\n *\n * This module is pure math — no DOM, no Konva, no React.\n */\n\nimport type { ZoomPanMath } from '../contracts'\n\nexport interface ZoomPanConfig {\n minZoom: number\n maxZoom: number\n}\n\nexport function createZoomPanMath(config: ZoomPanConfig): ZoomPanMath {\n const { minZoom, maxZoom } = config\n if (!Number.isFinite(minZoom) || minZoom <= 0) {\n throw new Error(`minZoom must be a positive finite number, got ${minZoom}`)\n }\n if (!Number.isFinite(maxZoom) || maxZoom <= minZoom) {\n throw new Error(`maxZoom must be finite and greater than minZoom (${minZoom}), got ${maxZoom}`)\n }\n\n return {\n minZoom,\n maxZoom,\n\n zoomAtPoint(\n state: { zoom: number; panX: number; panY: number },\n factor: number,\n screenX: number,\n screenY: number,\n ): { zoom: number; panX: number; panY: number } {\n if (!Number.isFinite(factor) || factor <= 0) {\n throw new Error(`zoomAtPoint: factor must be a positive finite number, got ${factor}`)\n }\n if (!Number.isFinite(screenX) || !Number.isFinite(screenY)) {\n throw new Error(`zoomAtPoint: screenX/screenY must be finite numbers`)\n }\n if (!Number.isFinite(state.zoom) || state.zoom <= 0) {\n throw new Error(`zoomAtPoint: state.zoom must be a positive finite number, got ${state.zoom}`)\n }\n\n const newZoom = Math.min(maxZoom, Math.max(minZoom, state.zoom * factor))\n\n // The document point under the cursor must remain fixed:\n // docX = (screenX - panX) / zoom\n // newPanX = screenX - docX * newZoom\n const docX = (screenX - state.panX) / state.zoom\n const docY = (screenY - state.panY) / state.zoom\n const panX = screenX - docX * newZoom\n const panY = screenY - docY * newZoom\n\n return { zoom: newZoom, panX, panY }\n },\n\n fitPage(\n page: { width: number; height: number },\n viewport: { width: number; height: number },\n paddingPx = 48,\n ): { zoom: number; panX: number; panY: number } {\n if (!Number.isFinite(page.width) || page.width <= 0) {\n throw new Error(`fitPage: page.width must be a positive finite number, got ${page.width}`)\n }\n if (!Number.isFinite(page.height) || page.height <= 0) {\n throw new Error(`fitPage: page.height must be a positive finite number, got ${page.height}`)\n }\n if (!Number.isFinite(viewport.width) || viewport.width <= 0) {\n throw new Error(`fitPage: viewport.width must be a positive finite number, got ${viewport.width}`)\n }\n if (!Number.isFinite(viewport.height) || viewport.height <= 0) {\n throw new Error(`fitPage: viewport.height must be a positive finite number, got ${viewport.height}`)\n }\n if (!Number.isFinite(paddingPx) || paddingPx < 0) {\n throw new Error(`fitPage: paddingPx must be a non-negative finite number, got ${paddingPx}`)\n }\n\n const availW = viewport.width - paddingPx * 2\n const availH = viewport.height - paddingPx * 2\n const zoom = Math.min(maxZoom, Math.max(minZoom, Math.min(availW / page.width, availH / page.height)))\n\n // Center the page in the viewport\n const panX = (viewport.width - page.width * zoom) / 2\n const panY = (viewport.height - page.height * zoom) / 2\n\n return { zoom, panX, panY }\n },\n\n documentToScreen(\n state: { zoom: number; panX: number; panY: number },\n x: number,\n y: number,\n ): { x: number; y: number } {\n return { x: x * state.zoom + state.panX, y: y * state.zoom + state.panY }\n },\n\n screenToDocument(\n state: { zoom: number; panX: number; panY: number },\n x: number,\n y: number,\n ): { x: number; y: number } {\n if (!Number.isFinite(state.zoom) || state.zoom === 0) {\n throw new Error(`screenToDocument: zoom must be a non-zero finite number, got ${state.zoom}`)\n }\n return { x: (x - state.panX) / state.zoom, y: (y - state.panY) / state.zoom }\n },\n }\n}\n","/**\n * Pure export math for the design-canvas surface — no Konva, no DOM, no React.\n *\n * All stage-interaction code (save/restore, node visibility, toDataURL) lives\n * in export.ts and delegates the decision math here so it can be tested\n * without a canvas environment.\n */\n\nimport type { ScenePage } from '../design-canvas/model'\nimport {\n bleedAwareExportBounds,\n scaleForPreset,\n type ExportCropRect,\n type ExportPreset,\n} from '../design-canvas/export-presets'\n\n// ---------------------------------------------------------------------------\n// Re-exports so callers only import from one place\n// ---------------------------------------------------------------------------\n\nexport type { ExportCropRect, ExportPreset }\nexport { bleedAwareExportBounds, scaleForPreset }\n\n// ---------------------------------------------------------------------------\n// Overlay / transformer node name predicates\n// ---------------------------------------------------------------------------\n\n/** Returns true for any Konva node that must be hidden during export. Nodes\n * whose names start with 'overlay:' are editor-only chrome (snap lines,\n * selection indicators, rulers, bleed guides). The transformer node is\n * identified by its canonical name 'Transformer'. */\nexport function isExportHiddenNodeName(name: string): boolean {\n return name.startsWith('overlay:') || name === 'Transformer'\n}\n\n// ---------------------------------------------------------------------------\n// Crop rect + pixel ratio resolution\n// ---------------------------------------------------------------------------\n\nexport interface ResolvedExportParams {\n cropRect: ExportCropRect\n pixelRatio: number\n mimeType: 'image/png' | 'image/jpeg'\n quality: number | undefined\n}\n\n/**\n * Resolve the full set of toDataURL params from a page, format, pixel ratio,\n * bleed flag, and optional preset. When a preset is supplied it overrides\n * `pixelRatio` and `includeBleed` — the explicit args are ignored for those\n * two values so the preset is the single source of truth.\n *\n * `quality` is set to 0.92 for jpeg, undefined for png (Konva ignores it).\n */\nexport function resolveExportParams(\n page: ScenePage,\n opts: {\n format: 'png' | 'jpeg'\n pixelRatio?: number\n includeBleed?: boolean\n preset?: ExportPreset\n },\n): ResolvedExportParams {\n const includeBleed = opts.preset !== undefined\n ? opts.preset.includeBleed\n : (opts.includeBleed ?? false)\n\n const cropRect = bleedAwareExportBounds(page, includeBleed)\n\n const pixelRatio = opts.preset !== undefined\n ? scaleForPreset(opts.preset, cropRect)\n : (opts.pixelRatio ?? 1)\n\n const format = opts.preset !== undefined ? opts.preset.format : opts.format\n const mimeType: 'image/png' | 'image/jpeg' = format === 'jpeg' ? 'image/jpeg' : 'image/png'\n\n return {\n cropRect,\n pixelRatio,\n mimeType,\n quality: format === 'jpeg' ? 0.92 : undefined,\n }\n}\n\n// ---------------------------------------------------------------------------\n// CORS taint detection\n// ---------------------------------------------------------------------------\n\n/**\n * Walk image nodes to find the src of any that may have caused a SecurityError\n * when the stage called toDataURL. Returns the first offending src found, or\n * null if none can be identified.\n *\n * `imageSrcs` is a flat list of { name, src } records collected from all image\n * nodes on the stage before calling toDataURL — export.ts builds it before\n * attempting the export so that if the SecurityError fires we have the data.\n */\nexport function identifyTaintedSrc(imageSrcs: ReadonlyArray<{ name: string; src: string }>): string | null {\n // We cannot re-probe CORS from a caught SecurityError — the browser does not\n // tell us which src triggered it. Return the first src that looks like a\n // cross-origin resource the browser would taint: anything that is not a\n // same-origin relative path or a data: URL.\n for (const { src } of imageSrcs) {\n if (isCrossOriginSrc(src)) return src\n }\n return null\n}\n\n/**\n * Heuristic: a src is potentially cross-origin when it is an absolute http(s)\n * URL. Same-origin relative paths (/api/...) and data: blobs are safe.\n * This is intentionally conservative — the taint check runs only after a\n * SecurityError fires, so a false positive is \"blamed\" without being silently\n * ignored. NOTE: same-origin https:// assets will also match; if a\n * SecurityError fires, the cross-origin culprit may be a different image than\n * the one this function flags first.\n */\nexport function isCrossOriginSrc(src: string): boolean {\n return /^https?:\\/\\//i.test(src)\n}\n\n// ---------------------------------------------------------------------------\n// View state snapshot (used by save/restore helpers)\n// ---------------------------------------------------------------------------\n\n/** Minimal snapshot of stage view state that export.ts saves before mutating\n * zoom/pan/visibility and restores afterward. */\nexport interface ExportViewSnapshot {\n /** Stage scale factor before export. */\n scaleX: number\n scaleY: number\n /** Stage translation before export. */\n x: number\n y: number\n /** node name → prior visibility, for the nodes that were hidden. */\n hiddenNodeNames: string[]\n}\n\n/**\n * Build the toDataURL crop rect translated to stage output coordinates.\n * Konva's `stage.toDataURL({ x, y, width, height })` takes STAGE OUTPUT\n * coordinates, not document coordinates. When the stage has been scaled to fit\n * (zoom × stageScale), the crop rect in document px must be multiplied by the\n * combined scale to land on the right pixels.\n *\n * `stageScale` is the stage's current uniform scale (scaleX == scaleY);\n * `pixelRatio` is separate and handed directly to toDataURL — Konva multiplies\n * it internally when rendering to the backing canvas, so we must NOT include\n * it in the stage-coordinate rect.\n */\nexport function documentCropToStageCoords(\n cropRect: ExportCropRect,\n stageScale: number,\n stageX: number,\n stageY: number,\n): { x: number; y: number; width: number; height: number } {\n return {\n x: stageX + cropRect.x * stageScale,\n y: stageY + cropRect.y * stageScale,\n width: cropRect.width * stageScale,\n height: cropRect.height * stageScale,\n }\n}\n","/**\n * Pure builders for the human \"add element to the canvas\" path — the math and\n * `SceneOperation` construction behind {@link CanvasInsertPanel}, separated from\n * the React/DOM layer so every insert is unit-testable without Konva or a\n * browser. The panel calls these to produce `add_element` operations and hands\n * them to the host's `onApplyOperations` (server-validated, undoable) — the same\n * pipeline every other edit flows through.\n *\n * Media boundary: image inserts must pass {@link assertSceneMediaSrc} (remote\n * http(s) or a rooted `/api/` path — never `data:` blobs or sandbox-local\n * files). Builders that take a src assert it up front so a bad url fails here,\n * not deep in the apply layer.\n */\n\nimport { assertSceneMediaSrc } from '../design-canvas/model'\nimport type {\n EllipseElement,\n ImageElement,\n RectElement,\n SceneElement,\n TextElement,\n} from '../design-canvas/model'\nimport type { SceneOperation } from '../design-canvas/operations'\n\n/** Largest dimension (document px) a freshly inserted element is fitted to,\n * before the page-size cap applies. Keeps a huge upload from filling the page. */\nexport const MAX_INSERT_DIMENSION = 600\n\n/** Active page geometry an insert lands into — drives centering and fitting. */\nexport interface InsertPageGeometry {\n pageId: string\n width: number\n height: number\n background?: string\n}\n\n/** Mint a DOM-safe element id. Prefers `crypto.randomUUID`; falls back to a\n * time+random id where crypto is unavailable (older runtimes, some test envs). */\nexport function mintElementId(): string {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID()\n }\n return `el-${Date.now()}-${Math.random().toString(36).slice(2)}`\n}\n\n/** Fit (naturalW, naturalH) under a max-dimension cap (further bounded by the\n * page), preserving aspect ratio. Never upscales past natural size. */\nexport function fittedSize(\n naturalW: number,\n naturalH: number,\n pageWidth: number,\n pageHeight: number,\n): { width: number; height: number } {\n const cap = Math.min(MAX_INSERT_DIMENSION, pageWidth * 0.8, pageHeight * 0.8)\n const longest = Math.max(naturalW, naturalH)\n const scale = naturalW > 0 && naturalH > 0 ? Math.min(1, cap / longest) : 1\n const width = Math.max(1, Math.round((naturalW || cap) * scale))\n const height = Math.max(1, Math.round((naturalH || cap) * scale))\n return { width, height }\n}\n\n/** Top-left position that centers a (width, height) box on the page. */\nexport function centeredPosition(\n width: number,\n height: number,\n pageWidth: number,\n pageHeight: number,\n): { x: number; y: number } {\n return {\n x: Math.round((pageWidth - width) / 2),\n y: Math.round((pageHeight - height) / 2),\n }\n}\n\n/** Common element fields with a freshly minted id and unrotated/visible defaults. */\nfunction baseAttrs(name: string, x: number, y: number) {\n return { id: mintElementId(), name, x, y, rotation: 0, opacity: 1, locked: false, visible: true }\n}\n\n/** Wrap a single element in an `add_element` op for the active page. */\nfunction addElementOp(pageId: string, element: SceneElement): SceneOperation {\n return { type: 'add_element', pageId, element }\n}\n\nfunction hexLuminance(color: string | undefined): number | null {\n if (!color) return null\n const raw = color.trim()\n const hex = raw.startsWith('#') ? raw.slice(1) : ''\n const full = hex.length === 3\n ? hex.split('').map((ch) => ch + ch).join('')\n : hex.length === 6\n ? hex\n : ''\n if (!/^[0-9a-fA-F]{6}$/.test(full)) return null\n const channels = [0, 2, 4].map((start) => parseInt(full.slice(start, start + 2), 16) / 255)\n const [r, g, b] = channels.map((value) => (\n value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4)\n ))\n return 0.2126 * r! + 0.7152 * g! + 0.0722 * b!\n}\n\nfunction pageTextFill(page: InsertPageGeometry, tone: 'primary' | 'secondary'): string {\n const luminance = hexLuminance(page.background)\n if (luminance !== null && luminance < 0.35) {\n return tone === 'primary' ? '#f8fafc' : '#cbd5e1'\n }\n return tone === 'primary' ? '#111827' : '#374151'\n}\n\n/**\n * Build an `add_element` op placing an image, fitted and centered on the page.\n * `naturalSize` is the probed image dimensions (the panel reads them in the\n * browser); pass `{ width: 0, height: 0 }` when unknown to fall back to the cap.\n *\n * Throws (via `assertSceneMediaSrc`) when `src` is not http(s) or a rooted\n * `/api/` path — callers should surface the error, never insert a `data:` src.\n */\nexport function buildInsertImageOp(\n src: string,\n naturalSize: { width: number; height: number },\n page: InsertPageGeometry,\n): SceneOperation {\n assertSceneMediaSrc(src, 'image src')\n const { width, height } = fittedSize(naturalSize.width, naturalSize.height, page.width, page.height)\n const { x, y } = centeredPosition(width, height, page.width, page.height)\n const element: ImageElement = {\n ...baseAttrs('Image', x, y),\n kind: 'image',\n width,\n height,\n src,\n fit: 'contain',\n }\n return addElementOp(page.pageId, element)\n}\n\n/** A template the insert panel can drop without the agent. `build` is pure and\n * produces the operations for the active page geometry. */\nexport interface InsertTemplate {\n id: string\n label: string\n build(page: InsertPageGeometry): SceneOperation[]\n}\n\n/** The built-in starter templates (heading, body text, rectangle, ellipse).\n * Consumers can pass their own list to {@link CanvasInsertPanel}; this is the\n * default so every canvas gets a usable set out of the box. */\nexport const DEFAULT_INSERT_TEMPLATES: readonly InsertTemplate[] = [\n {\n id: 'heading',\n label: 'Heading',\n build(page) {\n const width = Math.min(480, Math.round(page.width * 0.7))\n const { x, y } = centeredPosition(width, 60, page.width, page.height)\n const element: TextElement = {\n ...baseAttrs('Heading', x, y),\n kind: 'text',\n text: 'Add a headline',\n width,\n fontFamily: 'Inter',\n fontSize: 48,\n fontStyle: 'bold',\n fill: pageTextFill(page, 'primary'),\n align: 'left',\n lineHeight: 1.1,\n letterSpacing: 0,\n }\n return [addElementOp(page.pageId, element)]\n },\n },\n {\n id: 'body',\n label: 'Body text',\n build(page) {\n const width = Math.min(420, Math.round(page.width * 0.6))\n const { x, y } = centeredPosition(width, 80, page.width, page.height)\n const element: TextElement = {\n ...baseAttrs('Body', x, y),\n kind: 'text',\n text: 'Add a paragraph of supporting copy.',\n width,\n fontFamily: 'Inter',\n fontSize: 20,\n fontStyle: 'normal',\n fill: pageTextFill(page, 'secondary'),\n align: 'left',\n lineHeight: 1.4,\n letterSpacing: 0,\n }\n return [addElementOp(page.pageId, element)]\n },\n },\n {\n id: 'rect',\n label: 'Rectangle',\n build(page) {\n const width = Math.min(320, Math.round(page.width * 0.4))\n const height = Math.min(200, Math.round(page.height * 0.3))\n const { x, y } = centeredPosition(width, height, page.width, page.height)\n const element: RectElement = {\n ...baseAttrs('Rectangle', x, y),\n kind: 'rect',\n width,\n height,\n fill: '#6366f1',\n cornerRadius: 12,\n }\n return [addElementOp(page.pageId, element)]\n },\n },\n {\n id: 'ellipse',\n label: 'Ellipse',\n build(page) {\n const size = Math.min(220, Math.round(Math.min(page.width, page.height) * 0.3))\n const { x, y } = centeredPosition(size, size, page.width, page.height)\n const element: EllipseElement = {\n ...baseAttrs('Ellipse', x, y),\n kind: 'ellipse',\n width: size,\n height: size,\n fill: '#10b981',\n }\n return [addElementOp(page.pageId, element)]\n },\n },\n]\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/chat-routes/stale-turn-lock.ts"],"sourcesContent":["/**\n * Recovery policy for a `ChatTurnLock` whose holder died.\n *\n * `createChatTurnRoutes` takes the lock as a seam (`acquire`/`release`) and a\n * lock is a single-flight guard: while it is held, a second turn on the same\n * scope is refused. Products give it a TTL measured in tens of minutes, so a\n * turn that dies without releasing wedges chat for that whole window. Every\n * app on the seam inherits that wedge, which is why the way OUT of it is\n * policy this package owns rather than something each app rediscovers.\n *\n * The policy takes PROBES, not clients: it imports no sandbox SDK, opens no\n * connection, and knows nothing about how a product finds its box or talks to\n * a sidecar. That is what makes the rules testable and what keeps the concrete\n * probes — which box key, which session id, which sidecar endpoint — in the\n * product.\n *\n * The rules, in precedence order:\n *\n * 1. The session probe answered and the execution is TERMINAL ⇒ release, once\n * the lock is past a short grace period. The authority on \"is this turn\n * still running\" is whatever is actually running it; a terminal verdict is\n * proof the lock outlived its turn — but only if the verdict is about THIS\n * turn, which is what the grace buys (see\n * {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS}).\n * 2. The session probe answered and the execution is LIVE ⇒ hold, always.\n * Nothing below may override this. The lock is doing exactly its job.\n * 3. The probes could not reach that authority at all — the sandbox could not\n * be listed, is gone, is not running, or its session probe failed ⇒ fall\n * back on the physical argument: an execution runs INSIDE the box, so a box\n * that is not there is running nothing, and the lock is releasable. Without\n * this fallback the recovery would depend on the very subsystem whose\n * failure produced the stale lock.\n *\n * Rule 3 is gated on a grace period because it is an inference, not an\n * observation — see {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}.\n */\n\n/** Where the box is, as far as the caller can see. `state` on `not-running`\n * is the platform's own status string, carried through for the log. */\nexport type StaleTurnLockSandboxProbeResult =\n | { status: 'running' }\n | { status: 'absent' }\n | { status: 'not-running'; state?: string }\n\n/** What the thing running the turn says about it. `terminal: false` means an\n * execution is LIVE — the strongest signal in the policy. `diagnostics` rides\n * through to the result and the logs unread. */\nexport type StaleTurnLockSessionProbeResult =\n | { reachable: true; terminal: boolean; diagnostics?: Record<string, unknown> }\n | { reachable: false; reason?: string }\n\n/**\n * Minimum age a lock must reach before the \"sandbox unreachable ⇒ nothing can\n * be running\" fallback may force-release it.\n *\n * The lock is acquired BEFORE the box is ensured, so during a cold workspace's\n * first turn there is a real window in which the lock is held and no box exists\n * yet — indistinguishable, from a peek, from a box that vanished. The grace\n * period has to outlast that window (create + bootstrap + whatever the product\n * hydrates) or a concurrent request steals the lock from a turn that is merely\n * still provisioning. Five minutes clears observed cold starts with room to\n * spare while cutting the worst case from a TTL-length wedge down to five\n * minutes. Raising it makes recovery slower; lowering it risks stealing a lock\n * mid-provision.\n */\nexport const DEFAULT_STALE_TURN_LOCK_GRACE_MS = 5 * 60 * 1000\n\n/**\n * Minimum age a lock must reach before a TERMINAL session verdict may release\n * it.\n *\n * The session probe is keyed on the THREAD, not on the execution the lock\n * holds: a sidecar that has nothing running reports `terminal` with\n * `activeExecutionId: null`, so there is no id to match the lock against. The\n * lock, meanwhile, is acquired BEFORE the box is ensured and before the\n * execution registers with the sidecar. Between those two moments a second\n * request that reconciles the lock asks the sidecar about a turn it has not\n * heard of yet and gets back the PREVIOUS turn's terminal state — proof about\n * the wrong execution. Releasing on that verdict hands the second request a\n * lock the first one is still using, which is two concurrent turns on a scope\n * whose single-flight guard just voted for itself.\n *\n * One minute covers the acquire → box-ensure → sidecar-registration window on\n * a warm box (the cold-box case is Rule 3's, and has its own, much longer\n * grace). Deliberately NOT\n * {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}: this branch has a positive\n * observation behind it, so it should recover fast, and stretching it to five\n * minutes would leave a genuinely dead turn wedged for the whole window that\n * the session probe exists to shortcut. Raising it delays recovery from a\n * crashed turn; lowering it narrows the registration window it protects.\n */\nexport const DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS = 60 * 1000\n\nexport interface ReconcileStaleTurnLockOptions {\n /** When the held lock was acquired (epoch ms). The grace period is measured\n * from here, so it must be the LOCK's start, not the turn's. */\n lockStartedAt: number\n /** Is the box there and running? Never provisions — a peek, not an ensure.\n * A throw is treated as unreachable, same as `absent`. */\n probeSandbox(): Promise<StaleTurnLockSandboxProbeResult>\n /** Ask the running box whether the execution is still live. Only called when\n * `probeSandbox` reported `running`. A throw is treated as unreachable. */\n probeSession(): Promise<StaleTurnLockSessionProbeResult>\n /** Release the lock, fenced by the instant the releasing evidence was\n * observed. `fence.observedAt` is snapshotted BEFORE the probe that\n * justified the release, so a store that can compare it against the held\n * lock's start refuses to delete a SUCCESSOR lock acquired while the probe\n * was in flight. A store that cannot make that comparison may ignore the\n * fence, but must not substitute its own `Date.now()` — that timestamp is\n * by construction newer than any successor and makes the check vacuous.\n *\n * Returns whether the release actually landed — `false` when the lock was\n * already gone (someone else got there first), which is reported, never\n * treated as a release. */\n release(fence: { observedAt: number }): boolean | Promise<boolean>\n /** Override {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS} (Rule 3's fallback). */\n graceMs?: number\n /** Override {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS} (Rule 1's release). */\n terminalGraceMs?: number\n /** Identity fields merged into every log line (workspace, thread, execution\n * id — whatever makes the entry findable in the product's logs). */\n context?: Record<string, unknown>\n /** Defaults to `console.warn`. Both the withheld and the force-released\n * branches log; a force-release is never silent. */\n log?(message: string, meta: Record<string, unknown>): void\n /** Injectable clock, for tests. */\n now?(): number\n}\n\nexport interface ReconcileStaleTurnLockResult {\n released: boolean\n /** Why the policy decided what it did — the probe's own diagnostics on the\n * reachable path, the unreachable reason and lock age on the fallback. */\n diagnostics: Record<string, unknown>\n}\n\nfunction messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\n/**\n * Decide whether a held lock is stale and, if so, release it.\n *\n * Never provisions and never mutates anything but the lock: a reconciliation\n * attempt on a cold workspace leaves it cold.\n */\nexport async function reconcileStaleTurnLock(\n options: ReconcileStaleTurnLockOptions,\n): Promise<ReconcileStaleTurnLockResult> {\n let sandbox: StaleTurnLockSandboxProbeResult\n try {\n sandbox = await options.probeSandbox()\n } catch (err) {\n return forceReleaseUnreachable(options, {\n unreachableReason: 'SANDBOX_PROBE_FAILED',\n unreachableDetail: messageOf(err),\n })\n }\n if (sandbox.status !== 'running') {\n return forceReleaseUnreachable(options, {\n unreachableReason: sandbox.status === 'absent' ? 'SANDBOX_ABSENT' : 'SANDBOX_NOT_RUNNING',\n ...(sandbox.status === 'not-running' && sandbox.state !== undefined\n ? { sandboxState: sandbox.state }\n : {}),\n })\n }\n\n // Snapshot the clock BEFORE the probe and use that one instant for BOTH the\n // grace decision and the release fence: the release is only valid against the\n // lock as it was when the state was observed, not after an arbitrarily slow\n // round trip. Re-reading the clock after the probe would let a slow round\n // trip age the lock past the grace on paper, and would hand the store a fence\n // newer than a successor lock acquired meanwhile.\n const observedAt = (options.now ?? Date.now)()\n let session: StaleTurnLockSessionProbeResult\n try {\n session = await options.probeSession()\n } catch (err) {\n return forceReleaseUnreachable(options, {\n unreachableReason: 'SESSION_PROBE_FAILED',\n unreachableDetail: messageOf(err),\n })\n }\n if (!session.reachable) {\n return forceReleaseUnreachable(options, {\n unreachableReason: 'SESSION_UNREACHABLE',\n ...(session.reason !== undefined ? { sessionProbeError: session.reason } : {}),\n })\n }\n\n const diagnostics: Record<string, unknown> = {\n sandboxReachable: true,\n sessionTerminal: session.terminal,\n ...(session.diagnostics ?? {}),\n }\n // The authority answered and says the execution is live: the lock is doing\n // its job. Nothing below this point may override that.\n if (!session.terminal) return { released: false, diagnostics }\n\n // Terminal, but about WHICH execution? The probe is thread-keyed, so a lock\n // younger than the registration window may be reading the previous turn's\n // verdict — hold until it is old enough that the verdict has to be its own.\n const terminalGraceMs = options.terminalGraceMs ?? DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS\n const lockAgeMs = observedAt - options.lockStartedAt\n if (lockAgeMs < terminalGraceMs) {\n const log = options.log ?? ((message, meta) => console.warn(message, meta))\n const withheld = {\n ...diagnostics,\n lockAgeMs,\n terminalReleaseGraceMs: terminalGraceMs,\n terminalReleaseWithheld: 'LOCK_WITHIN_TERMINAL_GRACE_PERIOD',\n }\n log('[chat-routes] stale turn lock held: terminal verdict but lock is younger than the registration window', {\n ...(options.context ?? {}),\n ...withheld,\n })\n return { released: false, diagnostics: withheld }\n }\n\n const released = await options.release({ observedAt })\n return {\n released,\n diagnostics: { ...diagnostics, lockAgeMs, terminalReleaseGraceMs: terminalGraceMs, released, observedAt },\n }\n}\n\n/**\n * The fallback: the session authority could not attest to the execution's\n * state because the box is gone, stopped, or unreachable. Release the lock\n * once it is old enough that it cannot belong to a turn still provisioning its\n * box — and say so either way, with the reason, the lock's age, and the grace\n * period it was measured against.\n *\n * One clock read (`at`) serves as the age gate AND the release fence here too,\n * for the same reason the terminal branch uses `observedAt` for both.\n */\nasync function forceReleaseUnreachable(\n options: ReconcileStaleTurnLockOptions,\n reason: Record<string, unknown> & { unreachableReason: string },\n): Promise<ReconcileStaleTurnLockResult> {\n const log = options.log ?? ((message, meta) => console.warn(message, meta))\n const graceMs = options.graceMs ?? DEFAULT_STALE_TURN_LOCK_GRACE_MS\n const at = (options.now ?? Date.now)()\n const lockAgeMs = at - options.lockStartedAt\n const diagnostics: Record<string, unknown> = {\n ...reason,\n sandboxReachable: false,\n lockAgeMs,\n forceReleaseGraceMs: graceMs,\n }\n\n if (lockAgeMs < graceMs) {\n log('[chat-routes] stale turn lock held: sandbox unreachable but lock is inside the grace period', {\n ...(options.context ?? {}),\n ...diagnostics,\n })\n return { released: false, diagnostics: { ...diagnostics, forceReleaseWithheld: 'LOCK_WITHIN_GRACE_PERIOD' } }\n }\n\n const released = await options.release({ observedAt: at })\n log('[chat-routes] force-released stale turn lock: sandbox unreachable, no turn can be executing', {\n ...(options.context ?? {}),\n ...diagnostics,\n released,\n })\n return { released, diagnostics: { ...diagnostics, forceReleased: released } }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runtime/openai-stream.ts","../src/runtime/loop.ts","../src/runtime/agent.ts","../src/runtime/certified-delivery.ts","../src/runtime/surface-profile.ts"],"sourcesContent":["/**\n * OpenAI-compatible stream → `LoopEvent` adapter, for NON-sandbox copilots.\n *\n * `streamAppToolLoop` takes a `streamTurn` seam that yields `LoopEvent`s. A\n * sandboxed agent produces those from its container; a browser/edge copilot\n * instead calls a model directly. The Tangle Router, the tcloud SDK, and most\n * providers all speak the OpenAI Chat Completions streaming shape — so the ONE\n * reusable piece is assembling that stream (content deltas + FRAGMENTED\n * tool-call deltas) into `LoopEvent`s. That assembly is the boilerplate every\n * copilot would re-write (and get wrong — OpenAI streams tool-call arguments in\n * pieces across chunks).\n *\n * This does NOT implement an HTTP client beyond a minimal `fetch` + SSE reader\n * (browser/edge/Node-safe, zero deps). For richer transport use the tcloud SDK\n * or the Vercel AI SDK and pipe their stream through {@link toLoopEvents}.\n */\nimport type { LoopEvent, LoopMessage, LoopToolCall } from './loop'\n\n/** Minimal OpenAI Chat Completions streaming chunk (structural — no `openai` dep). */\nexport interface OpenAIStreamChunk {\n choices?: Array<{\n delta?: {\n content?: string | null\n /** Reasoning deltas — DeepSeek/router use `reasoning_content`; some proxies use `thinking`. */\n reasoning_content?: string | null\n thinking?: string | null\n tool_calls?: Array<{\n index: number\n id?: string\n function?: { name?: string; arguments?: string }\n }>\n }\n finish_reason?: string | null\n }>\n /** Final-chunk token accounting (requires `stream_options.include_usage`). */\n usage?: {\n prompt_tokens?: number\n completion_tokens?: number\n } | null\n}\n\ninterface PartialToolCall {\n id?: string\n name: string\n args: string\n}\n\n/**\n * Map an OpenAI-compat streaming chunk iterator to `LoopEvent`s: each content\n * delta → a `text` event; tool-call deltas are accumulated by index across\n * chunks and emitted as one complete `tool_call` event when the stream finishes\n * (arguments JSON-parsed; an empty/garbled args string yields `{}` rather than\n * throwing). Works for the Tangle Router, tcloud, or any OpenAI-compat source.\n */\nexport async function* toLoopEvents(chunks: AsyncIterable<OpenAIStreamChunk>): AsyncIterable<LoopEvent> {\n const calls = new Map<number, PartialToolCall>()\n for await (const chunk of chunks) {\n // Usage rides the final chunk, which has an empty choices array — handle\n // it before the choice guard.\n if (chunk.usage?.prompt_tokens != null || chunk.usage?.completion_tokens != null) {\n yield {\n type: 'usage',\n usage: {\n promptTokens: chunk.usage.prompt_tokens ?? 0,\n completionTokens: chunk.usage.completion_tokens ?? 0,\n },\n }\n }\n const choice = chunk.choices?.[0]\n if (!choice) continue\n const content = choice.delta?.content\n if (content) yield { type: 'text', text: content }\n const reasoning = choice.delta?.reasoning_content ?? choice.delta?.thinking\n if (reasoning) yield { type: 'reasoning', text: reasoning }\n for (const tc of choice.delta?.tool_calls ?? []) {\n const cur = calls.get(tc.index) ?? { name: '', args: '' }\n if (tc.id) cur.id = tc.id\n if (tc.function?.name) cur.name += tc.function.name\n if (tc.function?.arguments) cur.args += tc.function.arguments\n calls.set(tc.index, cur)\n }\n }\n for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {\n if (!c.name) continue\n yield { type: 'tool_call', call: { toolCallId: c.id, toolName: c.name, args: safeParse(c.args) } satisfies LoopToolCall }\n }\n}\n\nfunction safeParse(s: string): Record<string, unknown> {\n if (!s.trim()) return {}\n try {\n const v = JSON.parse(s)\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {}\n } catch {\n return {}\n }\n}\n\nexport interface OpenAICompatStreamTurnOptions {\n /** OpenAI-compat base URL (e.g. the Tangle Router `https://router.tangle.tools/v1`). */\n baseUrl: string\n apiKey: string\n model: string\n /** OpenAI tool definitions — pass `buildAppToolOpenAITools(taxonomy)` so the\n * model can call the app tools. Omit for a tool-free copilot. */\n tools?: unknown[]\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra body fields (e.g. `max_tokens`). */\n extraBody?: Record<string, unknown>\n}\n\n/**\n * Build a `streamTurn` that calls an OpenAI-compatible `/chat/completions`\n * endpoint (Tangle Router / tcloud / any compat provider) with `stream: true`\n * and yields `LoopEvent`s via {@link toLoopEvents}. Browser/edge/Node-safe —\n * just `fetch` + an SSE reader. Drop straight into `streamAppToolLoop`:\n *\n * const cfg = resolveTangleModelConfig() // or { baseUrl, apiKey, model }\n * streamAppToolLoop({ streamTurn: createOpenAICompatStreamTurn({ ...cfg, tools }), executeToolCall, ... })\n */\nexport function createOpenAICompatStreamTurn(\n opts: OpenAICompatStreamTurnOptions,\n): (messages: LoopMessage[]) => AsyncIterable<LoopEvent> {\n const base = opts.baseUrl.replace(/\\/+$/, '')\n const doFetch = opts.fetchImpl ?? fetch\n return (messages) =>\n toLoopEvents(\n streamChatCompletions(doFetch, `${base}/chat/completions`, opts.apiKey, {\n model: opts.model,\n messages,\n stream: true,\n stream_options: { include_usage: true },\n ...(opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {}),\n ...(opts.temperature != null ? { temperature: opts.temperature } : {}),\n ...opts.extraBody,\n }),\n )\n}\n\n/** Stream + parse an OpenAI-compat SSE response into chunks. Tolerates `data:`\n * framing, multi-line buffers, and the terminal `[DONE]`. */\nasync function* streamChatCompletions(\n doFetch: typeof fetch,\n url: string,\n apiKey: string,\n body: Record<string, unknown>,\n): AsyncIterable<OpenAIStreamChunk> {\n const res = await doFetch(url, {\n method: 'POST',\n headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', Accept: 'text/event-stream' },\n body: JSON.stringify(body),\n })\n if (!res.ok || !res.body) {\n const text = res.body ? await res.text().catch(() => '') : ''\n throw new Error(`OpenAI-compat stream failed (HTTP ${res.status})${text ? `: ${text.slice(0, 200)}` : ''}`)\n }\n const reader = res.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n buffer += decoder.decode(value, { stream: true })\n const lines = buffer.split('\\n')\n buffer = lines.pop() ?? ''\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('data:')) continue\n const data = trimmed.slice(5).trim()\n if (data === '[DONE]') return\n try {\n yield JSON.parse(data) as OpenAIStreamChunk\n } catch {\n /* skip a partial/garbled SSE frame */\n }\n }\n }\n}\n","/**\n * The bounded agent tool-loop — owned by `@tangle-network/agent-runtime`.\n *\n * A model turn may emit tool calls (integration-hub actions, the app tools from\n * `../tools`, delegation). The loop streams a turn, collects the executable tool\n * calls, dispatches each, appends the results to history in OpenAI\n * function-calling shape, and re-runs so the model reads them — bounded by\n * `maxToolTurns`, a wall-clock `deadlineMs`, and a `maxCostUsd` budget.\n *\n * The history shape is the OpenAI function-calling contract: the assistant turn\n * that emitted tool calls is preserved as an `assistant` message carrying its\n * `tool_calls` array, and each result is its own `{ role: 'tool', tool_call_id,\n * content }` message keyed to the call. A strict model (Claude, and any\n * OpenAI-compatible provider that validates tool history) needs this to read its\n * own tool use back; folding results into a `user` message makes such models\n * re-issue the same call in a loop.\n *\n * The loop is substrate-owned (`runToolLoop` / `streamToolLoop`); the app\n * supplies `streamTurn` (wrapping its model endpoint) and `executeToolCall`\n * (routing to its integration + app-tool executors). The app-facing names below\n * are 1:1 aliases of the canonical symbols, kept so this package's consumers and\n * the in-package `createAgentRuntime` read against a single, stable vocabulary.\n *\n * This is the LEAF the runtime barrel and its children both import — keeping the\n * tool-loop vocabulary out of any import cycle. The barrel re-exports it.\n */\n\nexport {\n runToolLoop as runAppToolLoop,\n streamToolLoop as streamAppToolLoop,\n} from '@tangle-network/agent-runtime'\nexport type {\n ToolLoopCall as LoopToolCall,\n ToolLoopAssistantToolCall as LoopAssistantToolCall,\n ToolLoopMessage as LoopMessage,\n ToolLoopEvent,\n ToolLoopStopReason,\n ToolLoopResult,\n RunToolLoopOptions as AppToolLoopOptions,\n StreamToolLoopOptions as StreamAppToolLoopOptions,\n StreamToolLoopYield as StreamLoopYield,\n} from '@tangle-network/agent-runtime'\n\n/**\n * Events the app's OpenAI-compat stream adapter ({@link toLoopEvents}) yields.\n *\n * This is the app's own `Raw` event type for the streaming loop — the canonical\n * `streamToolLoop<Raw>` is generic over it. It widens the substrate's\n * tool-loop event with `reasoning` (DeepSeek/router `reasoning_content` /\n * `thinking` deltas, rendered as thinking sections) and `usage` (per-message\n * token accounting) — neither belongs in the substrate's loop contract, so they\n * stay here. The adapter maps each into the `streamTurn` seam; `text` and\n * `tool_call` drive the loop, `reasoning` / `usage` pass through to the UI.\n */\nexport type LoopEvent =\n | { type: 'text'; text: string }\n | { type: 'reasoning'; text: string }\n | { type: 'tool_call'; call: import('@tangle-network/agent-runtime').ToolLoopCall }\n | { type: 'usage'; usage: { promptTokens: number; completionTokens: number } }\n | { type: 'other'; event: unknown }\n","/**\n * `createAgentRuntime` — the in-process agent core, assembled.\n *\n * The bricks to run an agent turn WITHOUT a sandbox already exist in this\n * package, but a consumer must hand-wire five of them every time: resolve the\n * model config, build the OpenAI tool schemas from the taxonomy, build a\n * `streamTurn` over the model endpoint, build an `executeToolCall` over the\n * product's handlers, and drive `runAppToolLoop` / `streamAppToolLoop` with an\n * `isExecutableTool` predicate. That boilerplate is identical across every\n * sandbox-free surface (an edge/browser copilot, an eval harness, a Node CLI),\n * and getting it subtly wrong — e.g. NOT advertising the tools, so the model\n * never emits a `tool_call` and no side effect ever fires — is exactly the\n * failure that makes a tool-driven agent score zero off-sandbox.\n *\n * This factory bundles those five into one object configured for ONE agent:\n *\n * const runtime = createAgentRuntime({ model, taxonomy, handlers, systemPrompt })\n * const result = await runtime.run(userMessage, { ctx }) // awaitable\n * for await (const y of runtime.stream(userMessage, { ctx })) {…} // streaming\n *\n * The model is advertised the app tools (so it CAN call them); each call is\n * dispatched against the product's `handlers` (so the side effect is real); the\n * `onProduced` hook fires at the real side-effect site (so an eval/UI credits a\n * persisted proposal or artifact). Substrate-free: no `@tangle-network/sandbox`,\n * no Durable Object, no `@tangle-network/agent-runtime` import. The SAME core\n * the Cloudflare Worker runs, runnable anywhere a `fetch` to an OpenAI-compatible\n * endpoint works.\n *\n * Domain stays out: the proposal taxonomy, the handlers, and the system prompt\n * are all injected — the factory knows nothing about insurance, law, tax, etc.\n */\nimport {\n type AppToolHandlers,\n type AppToolContext,\n type AppToolOutcome,\n type AppToolProducedEvent,\n type AppToolTaxonomy,\n} from '../tools/types'\nimport { buildAppToolOpenAITools, isAppToolName } from '../tools/openai'\nimport { createAppToolRuntimeExecutor } from '../tools/runtime'\nimport {\n runAppToolLoop,\n streamAppToolLoop,\n type LoopEvent,\n type LoopMessage,\n type LoopToolCall,\n type StreamLoopYield,\n type ToolLoopEvent,\n type ToolLoopResult,\n} from './loop'\nimport { createOpenAICompatStreamTurn } from './openai-stream'\n\n/** OpenAI-compatible model endpoint (Tangle Router / tcloud / any compat\n * provider). Build from {@link resolveTangleModelConfig} or pass literals. */\nexport interface AgentRuntimeModelConfig {\n baseUrl: string\n apiKey: string\n model: string\n temperature?: number\n fetchImpl?: typeof fetch\n /** Extra request-body fields (e.g. `max_tokens`, a `reasoning` block). */\n extraBody?: Record<string, unknown>\n}\n\n/** The agent's resolved profile surfaces for one turn — the things a delivered\n * / certified `AgentProfile` can change. Profile-WIDE on purpose: certified\n * delivery folds prompt-surface + skills into `systemPrompt` AND can add\n * certified `tool` artifacts to `extraTools` (the model's advertised tools is\n * rebuilt when these change). MCP servers / memory / RAG that materialize as\n * files or servers deliver through the sandbox-provisioning seam, not here. */\nexport interface ResolvedAgentProfile {\n systemPrompt: string\n extraTools: unknown[]\n}\n\nexport interface CreateAgentRuntimeOptions {\n /** The model endpoint the turns stream from. */\n model: AgentRuntimeModelConfig\n /**\n * Optional transform applied to the resolved profile surfaces each turn —\n * the seam for certified-artifact delivery (`createCertifiedDelivery`). It is\n * profile-WIDE (not prompt-only): it returns the effective `systemPrompt` +\n * advertised `extraTools`. Kept generic + injected so this substrate-free core\n * never imports `@tangle-network/agent-runtime`. Fail-closed by contract: an\n * impl that can't reach the plane returns the base surfaces unchanged.\n */\n composeProfile?: (base: ResolvedAgentProfile) => ResolvedAgentProfile | Promise<ResolvedAgentProfile>\n /** The product's proposal taxonomy — advertises `submit_proposal`'s `type`\n * enum to the model and labels the regulated subset on the result. */\n taxonomy: AppToolTaxonomy\n /** Domain handlers persisting each tool to the product's store/vault. */\n handlers: AppToolHandlers\n /** Default agent identity / system prompt. A turn may override it. */\n systemPrompt: string\n /** Runaway-backstop cap. Default 200 — set far above any legitimate workflow.\n * For per-workflow limits use `deadlineMs` or `maxCostUsd` on the loop options. */\n maxToolTurns?: number\n /** Extra OpenAI tool definitions advertised ALONGSIDE the four app tools\n * (e.g. `integration_invoke`). Pair with {@link executeOtherTool}. */\n extraTools?: unknown[]\n /** Execute a tool that is NOT one of the four app tools (e.g. an integration\n * action). Only consulted for names {@link isOtherExecutableTool} accepts. */\n executeOtherTool?: (call: LoopToolCall, ctx: AppToolContext) => Promise<AppToolOutcome>\n /** Which non-app tool names are executable here. Required if {@link executeOtherTool} is set. */\n isOtherExecutableTool?: (toolName: string) => boolean\n}\n\nexport interface AgentTurnOptions {\n /** The trusted per-turn context (who/where the turn runs as). */\n ctx: AppToolContext\n /** Prior conversation turns, in order. */\n priorMessages?: Array<{ role: string; content: string }>\n /** Override the factory's default system prompt for this turn. */\n systemPrompt?: string\n /** Fires at the real side-effect site for each produced proposal/artifact. */\n onProduced?: (event: AppToolProducedEvent) => void\n}\n\nexport interface AgentRuntime {\n /** Run the bounded tool loop to completion; resolve with final text + every\n * executed tool outcome. */\n run(userMessage: string, turn: AgentTurnOptions): Promise<ToolLoopResult>\n /** Stream the bounded tool loop: yields each raw model event and each executed\n * tool result as it happens (for SSE re-emission + telemetry). */\n stream(userMessage: string, turn: AgentTurnOptions): AsyncGenerator<StreamLoopYield<LoopEvent>, void, unknown>\n}\n\n/**\n * Create an in-process agent runtime for one agent. See the module doc for the\n * full rationale; the short version: it advertises the app tools to the model,\n * dispatches each emitted call against `handlers`, and drives the bounded loop —\n * the whole agent core, sandbox-free.\n */\nexport function createAgentRuntime(opts: CreateAgentRuntimeOptions): AgentRuntime {\n if (opts.executeOtherTool && !opts.isOtherExecutableTool) {\n throw new Error('createAgentRuntime: isOtherExecutableTool is required when executeOtherTool is set')\n }\n\n // Tool schemas + the streamTurn are stable across turns — build once. The\n // model MUST be advertised the tools or it never emits a tool_call (the exact\n // failure that scores a tool-driven agent zero off-sandbox).\n const m = opts.model\n const buildStreamTurn = (extraTools: unknown[]) =>\n createOpenAICompatStreamTurn({\n baseUrl: m.baseUrl,\n apiKey: m.apiKey,\n model: m.model,\n tools: [...buildAppToolOpenAITools(opts.taxonomy), ...extraTools],\n temperature: m.temperature,\n fetchImpl: m.fetchImpl,\n extraBody: m.extraBody,\n })\n\n // The advertised tool set is stable across turns UNLESS a delivered profile\n // changes `extraTools` (certified-tool delivery, on the cache-refresh cadence\n // — not per turn). Memoize the streamTurn by the active extraTools identity so\n // it rebuilds only when the certified tools actually change.\n const baseExtraTools = opts.extraTools ?? []\n let activeExtraTools = baseExtraTools\n let activeStreamTurn = buildStreamTurn(baseExtraTools)\n const streamTurnFor = (extraTools: unknown[]) => {\n if (extraTools !== activeExtraTools) {\n activeExtraTools = extraTools\n activeStreamTurn = buildStreamTurn(extraTools)\n }\n return activeStreamTurn\n }\n\n // Resolve the per-turn profile surfaces, applying the optional profile\n // transform (certified-artifact delivery). Profile-wide: system prompt +\n // advertised tools.\n const resolveProfile = async (turn: AgentTurnOptions): Promise<ResolvedAgentProfile> => {\n const base: ResolvedAgentProfile = {\n systemPrompt: turn.systemPrompt ?? opts.systemPrompt,\n extraTools: baseExtraTools,\n }\n return opts.composeProfile ? opts.composeProfile(base) : base\n }\n\n const isExecutableTool = (name: string): boolean =>\n isAppToolName(name) || (opts.isOtherExecutableTool?.(name) ?? false)\n\n const buildExecutor = (turn: AgentTurnOptions) => {\n const appExecutor = createAppToolRuntimeExecutor({\n handlers: opts.handlers,\n taxonomy: opts.taxonomy,\n ctx: turn.ctx,\n onProduced: turn.onProduced,\n })\n return async (call: LoopToolCall): Promise<AppToolOutcome> => {\n if (isAppToolName(call.toolName)) return appExecutor({ toolName: call.toolName, args: call.args })\n if (opts.executeOtherTool && opts.isOtherExecutableTool?.(call.toolName)) {\n return opts.executeOtherTool(call, turn.ctx)\n }\n return { ok: false, code: 'unknown_tool', message: `No executor for tool: ${call.toolName}` }\n }\n }\n\n return {\n async run(userMessage, turn) {\n const profile = await resolveProfile(turn)\n return runAppToolLoop({\n systemPrompt: profile.systemPrompt,\n userMessage,\n priorMessages: turn.priorMessages,\n // The awaitable loop consumes only text + tool_call; the app's UI-only\n // reasoning/usage events ride the substrate's `other` channel.\n streamTurn: narrowToToolLoopEvents(streamTurnFor(profile.extraTools)),\n executeToolCall: buildExecutor(turn),\n isExecutableTool,\n maxToolTurns: opts.maxToolTurns,\n })\n },\n async *stream(userMessage, turn) {\n const profile = await resolveProfile(turn)\n yield* streamAppToolLoop<LoopEvent>({\n systemPrompt: profile.systemPrompt,\n userMessage,\n priorMessages: turn.priorMessages,\n streamTurn: streamTurnFor(profile.extraTools),\n extractText: (ev) => (ev.type === 'text' ? ev.text : ''),\n extractToolCall: (ev) => (ev.type === 'tool_call' ? ev.call : null),\n isExecutableTool,\n executeToolCall: buildExecutor(turn),\n maxToolTurns: opts.maxToolTurns,\n })\n },\n }\n}\n\n/**\n * Adapt the app's rich {@link LoopEvent} stream to the substrate awaitable\n * loop's `ToolLoopEvent` contract. The loop reads only `text` (accumulated into\n * the answer) and `tool_call` (dispatched); the app's UI-only `reasoning` /\n * `usage` events have no awaitable meaning, so they collapse onto the\n * substrate's `other` channel and are ignored by the loop.\n */\nfunction narrowToToolLoopEvents(\n streamTurn: (messages: LoopMessage[]) => AsyncIterable<LoopEvent>,\n): (messages: LoopMessage[]) => AsyncIterable<ToolLoopEvent> {\n return (messages) =>\n (async function* () {\n for await (const ev of streamTurn(messages)) {\n if (ev.type === 'text') yield { type: 'text', text: ev.text }\n else if (ev.type === 'tool_call') yield { type: 'tool_call', call: ev.call }\n else yield { type: 'other', event: ev }\n }\n })()\n}\n","/**\n * `createCertifiedDelivery` — the delivery truck for Tangle Intelligence.\n *\n * Pulls a tenant's CERTIFIED `AgentProfile` from the deployed plane\n * (`GET /v1/profiles/:target/composed`) and applies it to the agent's resolved\n * surfaces each turn, so an approved improvement reaches the running agent with\n * NO redeploy. This is the `composeProfile` transform `createAgentRuntime`\n * accepts — opt-in per product, fail-closed, cached + refreshed.\n *\n * Profile-WIDE by design (not prompt-only): the composed profile carries every\n * promoted artifact type keyed by kind. What's folded where:\n * - `prompt-surface` + `skill` → the system prompt (via `composeCertifiedPrompt`).\n * - `tool` artifacts that carry an OpenAI tool definition → `extraTools`\n * (advertised to the model; the matching executor is supplied by the product\n * via `executeOtherTool`). Until a `tool` artifact carries a runnable def,\n * it is surfaced (see `current()`) but not advertised — advertising a tool\n * with no executor would make the model call into a dead end.\n * - `mcp` / `memory` / `rag` artifacts materialize as servers/files and deliver\n * through the SANDBOX-provisioning seam, not this in-process one. The full\n * certified profile is exposed via `current()` so that seam can consume it.\n *\n * Substrate boundary: THIS module imports `@tangle-network/agent-runtime`; the\n * `createAgentRuntime` core does not (it only consumes the generic transform).\n */\n\nimport {\n composeCertifiedPrompt,\n type CertifiedProfile,\n pullCertified,\n} from '@tangle-network/agent-runtime/intelligence'\nimport type { ResolvedAgentProfile } from './agent'\n\nconst defaultRefreshMs = 300_000\n\nexport interface CertifiedDeliveryConfig {\n /** The tenant target whose certified artifacts to deliver (the agent id). */\n target: string\n /** Bearer for the plane. Reads `TANGLE_API_KEY` when omitted. */\n apiKey?: string\n /** Plane base URL. Reads `TANGLE_INTELLIGENCE_URL` then the public plane. */\n baseUrl?: string\n /** Min interval between certified-profile pulls. Default 5m. */\n refreshMs?: number\n /** fetch impl (tests / non-global-fetch runtimes). */\n fetchImpl?: typeof fetch\n}\n\nexport interface CertifiedDelivery {\n /** The `composeProfile` transform to pass to `createAgentRuntime`. Applies the\n * cached certified profile to the base surfaces; refreshes on the cadence. */\n composeProfile(base: ResolvedAgentProfile): Promise<ResolvedAgentProfile>\n /** Force a pull now (ignores the refresh window). Best-effort. */\n refresh(): Promise<void>\n /** The certified profile currently in effect (null = none promoted / pull\n * failed). Lets the sandbox-provisioning seam deliver the file/server\n * artifact types this in-process seam doesn't. */\n current(): CertifiedProfile | null\n}\n\n/**\n * Build a certified-delivery transform for one agent target. Fail-closed: a pull\n * error or 404 keeps the last-known certified profile (or null), and the agent\n * runs on its base surfaces — it never breaks because Intelligence is down.\n */\nexport function createCertifiedDelivery(config: CertifiedDeliveryConfig): CertifiedDelivery {\n const refreshMs = config.refreshMs ?? defaultRefreshMs\n let certified: CertifiedProfile | null = null\n let lastPullAt = 0\n let inflight: Promise<void> | null = null\n\n async function refresh(force = false): Promise<void> {\n if (!force && Date.now() - lastPullAt < refreshMs) return\n if (inflight) return inflight\n inflight = (async () => {\n const outcome = await pullCertified({\n target: config.target,\n apiKey: config.apiKey,\n baseUrl: config.baseUrl,\n fetchImpl: config.fetchImpl,\n })\n lastPullAt = Date.now()\n // Only replace on a real pull; a 404/error keeps the last-known profile.\n if (outcome.succeeded) certified = outcome.value\n })()\n try {\n await inflight\n } finally {\n inflight = null\n }\n }\n\n return {\n async composeProfile(base) {\n await refresh()\n return {\n // prompt-surface + skill fold into the system prompt.\n systemPrompt: composeCertifiedPrompt(base.systemPrompt, certified),\n // Certified `tool` artifacts deliver here once they carry a runnable\n // OpenAI def + the product wires the executor; until then pass through.\n extraTools: base.extraTools,\n }\n },\n refresh: () => refresh(true),\n current: () => certified,\n }\n}\n","/**\n * Surface-scoped profile overlay — the seam letting any product page (a\n * sequence editor, a brief composer, a dataset view) add MCP servers, a prompt\n * addendum, and permission tightening to the workspace agent profile for turns\n * initiated FROM that surface, without the chat orchestrator knowing any\n * surface's specifics. The orchestrator resolves `(kind, ctx)` through a\n * registry the REQUEST HANDLER constructs per request (construction is a Map\n * build — cheap) and merges the result into the base profile it was about to\n * send to the sandbox. Per-request construction is the trust mechanism, not an\n * optimization target: each `build()` closes over server-trusted request state\n * (env bindings, secrets, the AUTHENTICATED user/workspace), which on Workers\n * exists only per request — a startup-built registry would force identity\n * through the untrusted client `ctx`.\n *\n * SECURITY INVARIANT: the surface `kind` and the ids inside `ctx` arrive on\n * the client request and are pure ROUTING data — never trusted content, never\n * identity. Identity comes from the closure (see above). The registered\n * `build()` runs server-side only: it validates the routing ids against the\n * product's access control, then mints its own URLs and capability tokens from\n * server configuration (`buildHttpMcpServer` + `createCapabilityToken` in\n * ../tools). A client can therefore never inject an arbitrary MCP url, header,\n * or token into the agent profile: the overlay's `mcp` values are typed as\n * {@link SurfaceMcpServer} (= the server-built `AppToolMcpServer` entry shape),\n * and only build() constructs them.\n */\n\nimport type { AppToolMcpServer } from '../tools/mcp'\n\n/** Sandbox permission posture values, ranked deny > ask > allow for merging. */\nexport type SurfacePermissionValue = 'allow' | 'ask' | 'deny'\n\n/** The only MCP entry shape an overlay may carry: the server-built bridge\n * entry from ../tools/mcp (transport, url, headers, and capability token all\n * assembled server-side). The alias exists so overlay authors reach for the\n * builders in ../tools rather than hand-rolling `{ url: ctx.url }` shapes\n * that would let request data become a dialable endpoint. */\nexport type SurfaceMcpServer = AppToolMcpServer\n\n/** What one surface contributes to the agent profile for a single turn. */\nexport interface SurfaceOverlay {\n /** MCP servers to mount for this turn, keyed by tool-routing name. Names\n * must not collide with the base profile's — see {@link mergeSurfaceOverlay}. */\n mcp?: Record<string, SurfaceMcpServer>\n /** Appended to the base system-prompt addendum with a blank-line separator. */\n promptAddendum?: string\n /** Per-key posture the surface wants for its turns. Merging is monotone\n * fail-closed: the stricter of base/overlay wins, so a surface can tighten\n * the workspace posture but never relax it. */\n permissions?: Record<string, SurfacePermissionValue>\n}\n\n/**\n * One registered surface kind. `TCtx` is the shape build() expects — a CLAIM\n * about the client payload, not a guarantee: the registry hands build() the\n * request's `ctx` unvalidated, so build() must treat every field as an\n * untrusted id (resolve it through access control that throws on a bad or\n * foreign id) before minting anything from it.\n */\nexport interface SurfaceKindDefinition<TCtx> {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}\n\n/** The variance-erased form a registry accepts (`build` is contravariant in\n * `TCtx`, so every concrete definition is assignable to this). */\nexport type AnySurfaceKind = SurfaceKindDefinition<never>\n\n/**\n * Declare one surface kind. The `kind` string is the client-visible routing\n * key (e.g. `'sequences'`); `build` is the server-side factory that turns a\n * validated ctx into the overlay for one turn.\n */\nexport function defineSurfaceKind<TCtx>(opts: {\n kind: string\n build: (ctx: TCtx) => SurfaceOverlay | Promise<SurfaceOverlay>\n}): SurfaceKindDefinition<TCtx> {\n if (typeof opts.kind !== 'string' || opts.kind.length === 0 || /\\s/.test(opts.kind)) {\n throw new Error(`surface kind must be a non-empty string without whitespace (got ${JSON.stringify(opts.kind)})`)\n }\n if (typeof opts.build !== 'function') {\n throw new Error(`surface kind '${opts.kind}' requires a build function`)\n }\n return { kind: opts.kind, build: opts.build }\n}\n\nexport interface SurfaceRegistry {\n /** Build the overlay for one turn. Throws on an unknown kind — an unknown\n * surface is a routing bug (client and server registries drifted), and\n * silently returning an empty overlay would strip the surface's tools from\n * the turn with no signal anywhere. Build errors propagate unwrapped. */\n resolve(kind: string, ctx: unknown): Promise<SurfaceOverlay>\n}\n\n/**\n * Assemble the product's surface registry from its registered kinds. Duplicate\n * kinds throw at construction: two builders behind one routing key would make\n * the mounted toolset depend on registration order.\n */\nexport function createSurfaceRegistry(kinds: readonly AnySurfaceKind[]): SurfaceRegistry {\n const byKind = new Map<string, AnySurfaceKind>()\n for (const definition of kinds) {\n if (byKind.has(definition.kind)) {\n throw new Error(`duplicate surface kind '${definition.kind}' — each kind must be registered exactly once`)\n }\n byKind.set(definition.kind, definition)\n }\n\n return {\n async resolve(kind, ctx) {\n const definition = byKind.get(kind)\n if (!definition) {\n const known = [...byKind.keys()].join(', ') || '(none)'\n throw new Error(\n `unknown surface kind '${kind}' — registered kinds: ${known}. ` +\n 'An unknown surface is a routing bug: register the kind via defineSurfaceKind before clients can reference it.',\n )\n }\n // The trust boundary where static ctx typing ends: the request payload is\n // handed to build() as-is, and build() validates it (see SurfaceKindDefinition).\n const overlay = await definition.build(ctx as never)\n assertSurfaceOverlay(overlay, `surface kind '${kind}'`)\n return overlay\n },\n }\n}\n\n/** Base-profile slice the merge reads/writes. Real callers pass their full\n * profile object; every field outside this slice passes through untouched. */\nexport interface SurfaceMergeBase {\n mcp?: Record<string, unknown>\n systemPromptAddendum?: string\n permissions?: Record<string, SurfacePermissionValue>\n}\n\nconst PERMISSION_SEVERITY: Record<SurfacePermissionValue, number> = { allow: 0, ask: 1, deny: 2 }\n\n/**\n * Merge one surface overlay into a base profile, returning a new object\n * (the base is never mutated; untouched nested records are shared by\n * reference).\n *\n * - `mcp`: overlay servers are added under their own names. A name already\n * present on the base THROWS — a collision is two servers claiming one\n * routing name, and renaming either silently would corrupt tool routing for\n * whichever caller expected the original binding.\n * - `systemPromptAddendum`: the overlay's `promptAddendum` appends after a\n * blank-line separator (no separator when the base has no addendum).\n * - `permissions`: per key the STRICTER value wins (deny > ask > allow). A\n * surface can tighten the base posture for its turns; a base 'deny' survives\n * any overlay.\n */\nexport function mergeSurfaceOverlay<TBase extends SurfaceMergeBase>(\n base: TBase,\n overlay: SurfaceOverlay,\n): TBase & SurfaceMergeBase {\n assertSurfaceOverlay(overlay, 'surface overlay')\n const merged: SurfaceMergeBase = { ...base }\n\n if (overlay.mcp && Object.keys(overlay.mcp).length > 0) {\n const baseMcp = base.mcp ?? {}\n const collisions = Object.keys(overlay.mcp).filter((name) => name in baseMcp)\n if (collisions.length > 0) {\n throw new Error(\n `surface overlay MCP name collision: ${collisions.map((n) => `'${n}'`).join(', ')} already exist on the base profile. ` +\n 'Two servers cannot claim one name — give the surface server a distinct name.',\n )\n }\n merged.mcp = { ...baseMcp, ...overlay.mcp }\n }\n\n if (overlay.promptAddendum !== undefined) {\n merged.systemPromptAddendum = base.systemPromptAddendum\n ? `${base.systemPromptAddendum}\\n\\n${overlay.promptAddendum}`\n : overlay.promptAddendum\n }\n\n if (overlay.permissions && Object.keys(overlay.permissions).length > 0) {\n const permissions: Record<string, SurfacePermissionValue> = { ...(base.permissions ?? {}) }\n for (const [key, value] of Object.entries(overlay.permissions)) {\n const existing = permissions[key]\n permissions[key] =\n existing === undefined || PERMISSION_SEVERITY[value] > PERMISSION_SEVERITY[existing] ? value : existing\n }\n merged.permissions = permissions\n }\n\n // merged began as a shallow copy of base; only the three slice fields were\n // replaced, so the intersection type is the true shape.\n return merged as TBase & SurfaceMergeBase\n}\n\n/** Reject overlays a build() (or hand-rolled caller) malformed, with the exact\n * field named: a relative MCP url, a blank addendum, or an off-vocabulary\n * permission would otherwise surface only as an opaque sandbox failure. */\nfunction assertSurfaceOverlay(overlay: SurfaceOverlay, label: string): void {\n if (overlay.promptAddendum !== undefined) {\n if (typeof overlay.promptAddendum !== 'string' || overlay.promptAddendum.trim().length === 0) {\n throw new Error(`${label}: promptAddendum must be a non-blank string when provided`)\n }\n }\n if (overlay.mcp !== undefined) {\n for (const [name, server] of Object.entries(overlay.mcp)) {\n if (name.trim().length === 0) throw new Error(`${label}: MCP server names must be non-empty`)\n if (server.transport !== 'http') {\n throw new Error(`${label}: MCP server '${name}' must use transport 'http' (got ${JSON.stringify((server as { transport?: unknown }).transport)})`)\n }\n let parsed: URL\n try {\n parsed = new URL(server.url)\n } catch {\n throw new Error(`${label}: MCP server '${name}' url must be an absolute URL (got ${JSON.stringify(server.url)})`)\n }\n if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {\n throw new Error(`${label}: MCP server '${name}' url must be http(s) (got ${JSON.stringify(server.url)})`)\n }\n }\n }\n if (overlay.permissions !== undefined) {\n for (const [key, value] of Object.entries(overlay.permissions)) {\n if (!(value in PERMISSION_SEVERITY)) {\n throw new Error(`${label}: permission '${key}' must be 'allow' | 'ask' | 'deny' (got ${JSON.stringify(value)})`)\n }\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/knowledge-loop/index.ts"],"sourcesContent":["/**\n * `@tangle-network/agent-app/knowledge-loop` — wire the declarative\n * `AgentKnowledgeConfig` to a running, source-grounded, eval-gated knowledge\n * acquisition loop.\n *\n * This module does NOT implement a loop. `@tangle-network/agent-knowledge`\n * already ships `runKnowledgeResearchLoop` — the source-grounded,\n * propose-don't-apply primitive (sources become immutable records first; only\n * accepted `---FILE: knowledge/...---` write blocks are applied; lint +\n * validation + optional readiness gate every iteration). It is pluggable on two\n * seams: a `SourceAdapter[]` (how raw bytes/text become curated source records —\n * text by default, audio/video/image adapters added by the consumer) and a\n * `step` decider (the per-iteration policy: an agentic judge, a sandbox run, or\n * a deterministic gate — the loop deliberately bakes none).\n *\n * `createKnowledgeLoop(config.knowledge, deps)` is the thin mapper between the\n * two:\n *\n * - `config.knowledge.sources` → adapter selection. The text adapter is the\n * default; `deps.adapters` is the multimodal seam (prepend an audio/video/\n * image `SourceAdapter` and the loop ingests that media). agent-app bakes no\n * media handler — it's a parameter.\n * - `config.knowledge.loop` → `runKnowledgeResearchLoop` options. `goal` maps\n * to the loop goal, `minConfidence` to the gate threshold, `freshness` is\n * threaded to the decider so a per-source-freshness policy can read it.\n * - `deps.decide` → the pluggable gate. DEFAULT is a reviewer policy\n * (agent-knowledge's propose-don't-apply posture): a candidate carrying\n * confidence below `minConfidence` is gated OUT (its proposal text is\n * dropped — sources are still recorded, because grounding is never the thing\n * we gate); at/above threshold it is accepted and the loop applies the write\n * blocks. Swap in an agentic judge or a sandbox run by passing your own\n * `decide`.\n * - `deps.driver` → the agent-runtime turn driver (this repo's `../runtime`\n * `runAppToolLoop` seam, or any compatible driver) the decider invokes for\n * the loop's agent turns. Optional; a deterministic / sandbox decider needs\n * no model turns and omits it.\n *\n * Layering: agent-knowledge and agent-runtime are PEER dependencies, never\n * bundled. This module imports only TYPES + the loop entry from agent-knowledge\n * and stays substrate-free behind the `decide` / `driver` / `adapters` seams.\n */\n\nimport type {\n AddSourceTextInput,\n KnowledgeResearchLoopContext,\n KnowledgeResearchLoopDecision,\n KnowledgeResearchLoopResult,\n RunKnowledgeResearchLoopOptions,\n SourceAdapter,\n} from '@tangle-network/agent-knowledge'\nimport {\n runKnowledgeResearchLoop,\n textSourceAdapter,\n} from '@tangle-network/agent-knowledge'\nimport type { AgentKnowledgeConfig, KnowledgeSourceSpec } from '../config/index'\n\n/**\n * A research candidate the decider evaluates before the loop applies it. This is\n * the propose-don't-apply unit: notes + the sources discovered this iteration +\n * the proposed write blocks, each carrying a confidence the gate scores against\n * the configured `minConfidence`.\n */\nexport interface KnowledgeCandidate {\n /** Human-readable research transcript for this iteration. */\n notes?: string\n /**\n * Textual source artifacts to register as immutable sources BEFORE any\n * proposal is applied. Recording these is grounding, not the gated step — a\n * rejected candidate still keeps its sources (so the next iteration is better\n * grounded). The decider may not strip these.\n */\n sourceTexts?: AddSourceTextInput[]\n /** Local files to register as immutable sources (same grounding posture). */\n sourcePaths?: string[]\n /**\n * Safe-write-protocol text (`---FILE: knowledge/...---` blocks). This IS the\n * gated step: the default decider drops it when `confidence < minConfidence`.\n */\n proposalText?: string\n /**\n * Aggregate confidence in this candidate's proposal, in [0, 1]. The default\n * reviewer gate compares this to `minConfidence`. A candidate with no\n * `proposalText` needs no confidence (nothing is gated).\n */\n confidence?: number\n /** The researcher's signal that the goal is met; ends the loop. */\n done?: boolean\n metadata?: Record<string, unknown>\n}\n\n/** The verdict a gate returns for one candidate. */\nexport interface KnowledgeGateVerdict {\n /** Whether the candidate's proposal is accepted (applied) or gated out. */\n accepted: boolean\n /** Why — surfaced in the decision metadata for audit. */\n reason: string\n /** The confidence the gate scored (echoed for telemetry). */\n confidence: number\n /** The threshold it was scored against. */\n minConfidence: number\n}\n\n/**\n * The pluggable acquisition policy. Given the loop context (current index, lint,\n * validation, freshness target, the driver), produce a candidate AND a gate\n * verdict on it. This is the seam an agentic judge or a sandbox run plugs into;\n * the default {@link createReviewerDecider} is a confidence gate.\n */\nexport interface KnowledgeDecider {\n (input: KnowledgeDeciderInput): Promise<KnowledgeDecision> | KnowledgeDecision\n}\n\nexport interface KnowledgeDeciderInput {\n /** The agent-knowledge loop context for this iteration. */\n context: KnowledgeResearchLoopContext\n /** The acquisition goal (from `config.loop.goal` or a deps fallback). */\n goal: string\n /** The confidence threshold a proposal must clear to be applied. */\n minConfidence: number\n /** The freshness target (`config.loop.freshness`), if set. */\n freshness?: string\n /** The configured sources, for a decider that fetches/selects among them. */\n sources: KnowledgeSourceSpec[]\n /** The agent-runtime turn driver, if one was supplied to the loop. */\n driver?: KnowledgeLoopDriver\n}\n\nexport interface KnowledgeDecision {\n /** The candidate the policy produced (may be empty to end the loop). */\n candidate: KnowledgeCandidate\n /** The gate's verdict on the candidate's proposal. */\n verdict: KnowledgeGateVerdict\n}\n\n/**\n * The agent-runtime turn driver seam. This is exactly `../runtime`'s\n * `runAppToolLoop` shape (a bounded, awaitable tool-driving turn loop over a\n * model). Typed structurally so a decider can drive the loop's agent turns\n * without this module importing the runtime engine. A deterministic / sandbox\n * decider may omit it.\n */\nexport interface KnowledgeLoopDriver {\n (opts: {\n systemPrompt: string\n userMessage: string\n }): Promise<{ finalText: string }>\n}\n\nexport interface CreateKnowledgeLoopDeps {\n /**\n * The knowledge-base root the loop reads/writes (an agent-knowledge layout).\n * Required — agent-knowledge owns disk; agent-app owns only the wiring.\n */\n root: string\n /**\n * The per-iteration policy. Defaults to {@link createReviewerDecider} keyed on\n * the config's `minConfidence`. Pass your own to use an agentic judge or a\n * sandbox run.\n */\n decide?: KnowledgeDecider\n /**\n * Extra source adapters (audio/video/image/PDF/...). The text adapter is\n * always present as the fallback; these are tried first so a multimodal\n * source is claimed by its adapter. This is the multimodal seam.\n */\n adapters?: SourceAdapter[]\n /** The agent-runtime turn driver for the loop's agent turns. */\n driver?: KnowledgeLoopDriver\n /**\n * Fallback goal when `config.loop.goal` is unset. agent-knowledge requires a\n * goal; this keeps the loop runnable for a config with only requirement specs.\n */\n defaultGoal?: string\n /** Default confidence threshold when `config.loop.minConfidence` is unset. */\n defaultMinConfidence?: number\n /** Max research iterations (forwarded to agent-knowledge). Default 3. */\n maxIterations?: number\n /** Actor stamped on the loop's knowledge events. */\n actor?: string\n /** Abort the loop. */\n signal?: AbortSignal\n /** Per-step hook (forwarded to agent-knowledge's `onStep`). */\n onStep?: RunKnowledgeResearchLoopOptions['onStep']\n}\n\n/** The handle `createKnowledgeLoop` returns. */\nexport interface KnowledgeLoop {\n /** Run the acquisition loop to completion and return the agent-knowledge result. */\n run(): Promise<KnowledgeResearchLoopResult>\n /** The resolved goal the loop pursues. */\n readonly goal: string\n /** The resolved confidence gate threshold. */\n readonly minConfidence: number\n /** The adapters the loop uses (consumer extras first, text last). */\n readonly adapters: SourceAdapter[]\n}\n\nconst DEFAULT_MIN_CONFIDENCE = 0.7\nconst DEFAULT_GOAL = 'Acquire and ground the knowledge this product requires.'\n\n/**\n * The default gate — agent-knowledge's propose-don't-apply reviewer posture as a\n * confidence threshold. A candidate's `proposalText` is applied only when its\n * `confidence` is at/above `minConfidence`; otherwise the proposal is gated out\n * (sources are still recorded). A candidate with no `proposalText` is trivially\n * accepted (nothing to gate). This is the floor policy; swap in an agentic judge\n * or a sandbox-run decider via `deps.decide` for richer review.\n */\nexport function reviewCandidate(\n candidate: KnowledgeCandidate,\n minConfidence: number,\n): KnowledgeGateVerdict {\n if (!candidate.proposalText) {\n return {\n accepted: true,\n reason: 'no-proposal',\n confidence: candidate.confidence ?? 1,\n minConfidence,\n }\n }\n const confidence = candidate.confidence ?? 0\n if (confidence >= minConfidence) {\n return {\n accepted: true,\n reason: `confidence ${confidence.toFixed(2)} >= minConfidence ${minConfidence.toFixed(2)}`,\n confidence,\n minConfidence,\n }\n }\n return {\n accepted: false,\n reason: `confidence ${confidence.toFixed(2)} < minConfidence ${minConfidence.toFixed(2)}`,\n confidence,\n minConfidence,\n }\n}\n\n/**\n * Wrap a candidate-producing policy in the default reviewer gate. The policy\n * decides WHAT to propose (notes, sources, proposalText, confidence); the gate\n * decides whether the proposal is APPLIED. Use this when you have a proposer but\n * want the standard confidence gate; pass a full {@link KnowledgeDecider} to\n * `deps.decide` to own the gate too.\n */\nexport function createReviewerDecider(\n propose: (\n input: KnowledgeDeciderInput,\n ) => Promise<KnowledgeCandidate> | KnowledgeCandidate,\n): KnowledgeDecider {\n return async (input) => {\n const candidate = await propose(input)\n const verdict = reviewCandidate(candidate, input.minConfidence)\n return { candidate, verdict }\n }\n}\n\n/**\n * Apply a gate verdict to a candidate, producing the agent-knowledge decision.\n * Grounding (sources) always passes through; the gated `proposalText` is dropped\n * when the verdict rejects it. The verdict is recorded in `metadata.gate` so the\n * loop's event stream carries the audit trail.\n */\nfunction toResearchDecision(decision: KnowledgeDecision): KnowledgeResearchLoopDecision {\n const { candidate, verdict } = decision\n return {\n notes: candidate.notes,\n sourcePaths: candidate.sourcePaths,\n sourceTexts: candidate.sourceTexts,\n proposalText: verdict.accepted ? candidate.proposalText : undefined,\n done: candidate.done,\n metadata: {\n ...(candidate.metadata ?? {}),\n gate: verdict,\n },\n }\n}\n\n/**\n * The do-nothing default policy: when no `decide` is supplied, the loop runs\n * with a proposer that proposes nothing and ends immediately. A real product\n * supplies a proposer (agentic judge, sandbox run, or deterministic) via\n * `deps.decide`; this keeps `createKnowledgeLoop` total for a config that only\n * declares sources/requirements without a wired researcher yet.\n */\nconst noopDecider: KnowledgeDecider = (input) => ({\n candidate: { done: true, notes: 'no decider supplied; nothing proposed' },\n verdict: {\n accepted: true,\n reason: 'no-proposal',\n confidence: 1,\n minConfidence: input.minConfidence,\n },\n})\n\n/**\n * Build a runnable knowledge-acquisition loop from the product's\n * `AgentKnowledgeConfig` and a small set of seams. Maps config → agent-knowledge\n * `runKnowledgeResearchLoop` options; never reimplements the loop.\n */\nexport function createKnowledgeLoop(\n knowledge: AgentKnowledgeConfig,\n deps: CreateKnowledgeLoopDeps,\n): KnowledgeLoop {\n const goal = knowledge.loop?.goal ?? deps.defaultGoal ?? DEFAULT_GOAL\n const minConfidence =\n knowledge.loop?.minConfidence ?? deps.defaultMinConfidence ?? DEFAULT_MIN_CONFIDENCE\n const freshness = knowledge.loop?.freshness\n\n // Multimodal seam: consumer adapters are tried first; the text adapter is the\n // always-present fallback (it claims text/.md/.txt/.json/.csv). A config\n // source whose media an extra adapter claims is ingested by that adapter.\n const adapters: SourceAdapter[] = [...(deps.adapters ?? []), textSourceAdapter]\n\n const decide = deps.decide ?? noopDecider\n\n const run = (): Promise<KnowledgeResearchLoopResult> =>\n runKnowledgeResearchLoop({\n root: deps.root,\n goal,\n maxIterations: deps.maxIterations,\n actor: deps.actor,\n signal: deps.signal,\n onStep: deps.onStep,\n sourceOptions: { adapters },\n async step(context) {\n const decision = await decide({\n context,\n goal,\n minConfidence,\n freshness,\n sources: knowledge.sources,\n driver: deps.driver,\n })\n return toResearchDecision(decision)\n },\n })\n\n return {\n run,\n goal,\n minConfidence,\n adapters,\n }\n}\n"],"mappings":";AAkDA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAgJP,IAAM,yBAAyB;AAC/B,IAAM,eAAe;AAUd,SAAS,gBACd,WACA,eACsB;AACtB,MAAI,CAAC,UAAU,cAAc;AAC3B,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,YAAY,UAAU,cAAc;AAAA,MACpC;AAAA,IACF;AAAA,EACF;AACA,QAAM,aAAa,UAAU,cAAc;AAC3C,MAAI,cAAc,eAAe;AAC/B,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ,cAAc,WAAW,QAAQ,CAAC,CAAC,qBAAqB,cAAc,QAAQ,CAAC,CAAC;AAAA,MACxF;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA,IACL,UAAU;AAAA,IACV,QAAQ,cAAc,WAAW,QAAQ,CAAC,CAAC,oBAAoB,cAAc,QAAQ,CAAC,CAAC;AAAA,IACvF;AAAA,IACA;AAAA,EACF;AACF;AASO,SAAS,sBACd,SAGkB;AAClB,SAAO,OAAO,UAAU;AACtB,UAAM,YAAY,MAAM,QAAQ,KAAK;AACrC,UAAM,UAAU,gBAAgB,WAAW,MAAM,aAAa;AAC9D,WAAO,EAAE,WAAW,QAAQ;AAAA,EAC9B;AACF;AAQA,SAAS,mBAAmB,UAA4D;AACtF,QAAM,EAAE,WAAW,QAAQ,IAAI;AAC/B,SAAO;AAAA,IACL,OAAO,UAAU;AAAA,IACjB,aAAa,UAAU;AAAA,IACvB,aAAa,UAAU;AAAA,IACvB,cAAc,QAAQ,WAAW,UAAU,eAAe;AAAA,IAC1D,MAAM,UAAU;AAAA,IAChB,UAAU;AAAA,MACR,GAAI,UAAU,YAAY,CAAC;AAAA,MAC3B,MAAM;AAAA,IACR;AAAA,EACF;AACF;AASA,IAAM,cAAgC,CAAC,WAAW;AAAA,EAChD,WAAW,EAAE,MAAM,MAAM,OAAO,wCAAwC;AAAA,EACxE,SAAS;AAAA,IACP,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,eAAe,MAAM;AAAA,EACvB;AACF;AAOO,SAAS,oBACd,WACA,MACe;AACf,QAAM,OAAO,UAAU,MAAM,QAAQ,KAAK,eAAe;AACzD,QAAM,gBACJ,UAAU,MAAM,iBAAiB,KAAK,wBAAwB;AAChE,QAAM,YAAY,UAAU,MAAM;AAKlC,QAAM,WAA4B,CAAC,GAAI,KAAK,YAAY,CAAC,GAAI,iBAAiB;AAE9E,QAAM,SAAS,KAAK,UAAU;AAE9B,QAAM,MAAM,MACV,yBAAyB;AAAA,IACvB,MAAM,KAAK;AAAA,IACX;AAAA,IACA,eAAe,KAAK;AAAA,IACpB,OAAO,KAAK;AAAA,IACZ,QAAQ,KAAK;AAAA,IACb,QAAQ,KAAK;AAAA,IACb,eAAe,EAAE,SAAS;AAAA,IAC1B,MAAM,KAAK,SAAS;AAClB,YAAM,WAAW,MAAM,OAAO;AAAA,QAC5B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,SAAS,UAAU;AAAA,QACnB,QAAQ,KAAK;AAAA,MACf,CAAC;AACD,aAAO,mBAAmB,QAAQ;AAAA,IACpC;AAAA,EACF,CAAC;AAEH,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/billing/index.ts"],"sourcesContent":["/**\n * Per-workspace budget-capped model keys — app-owned billing, metered on Tangle.\n *\n * Each workspace (the paying entity) runs the agent on its OWN child API key\n * minted from the platform parent key. The child carries a hard USD budget the\n * Tangle Router enforces AT THE KEY — model spend can't exceed the allowance,\n * zero app-side accounting. The app charges its own subscription (e.g. 5× the\n * allowance) and re-provisions each period. Child budgets are IMMUTABLE on the\n * platform, so a new budget = a fresh key + revoke the prior (rotate).\n *\n * The mint / rotate / rollover / usage LOGIC is generic and lives here.\n * Persistence (which D1 table), secret encryption, and key provisioning are\n * SEAMS each product supplies — so this module imports no DB and no key-mgmt\n * SDK (structural contracts only, like `../tangle`). The `@tangle-network/tcloud`\n * SDK is the provisioner a product passes in; it is not a dependency here.\n */\n\n/** The key-provisioning operations the key manager needs. Wire it from the\n * platform via {@link createTcloudKeyProvisioner} rather than casting. */\nexport interface KeyProvisioner {\n createKey(input: { name: string; product: string; budgetUsd: number; expiresAt: string }): Promise<{ id?: string; key?: string }>\n revokeKey(keyId: string): Promise<unknown>\n getKey(keyId: string): Promise<{ budgetUsd?: number; budgetSpent?: number; expiresAt?: string | null }>\n}\n\n/**\n * The subset of the `@tangle-network/tcloud` `TCloudClient` the provisioner uses\n * — declared with METHOD syntax so the real client (whose `product` is a narrow\n * union and whose budgets are `number | null`) is assignable bivariantly. The\n * real SDK client satisfies this; pass it straight in.\n */\nexport interface TcloudKeyClient {\n createKey(opts: {\n name: string\n product?: string\n budgetUsd?: number\n expiresAt?: string\n parentKeyId?: string\n allowedModels?: string[]\n rpmLimit?: number\n }): Promise<{ id: string; key: string }>\n getKey(id: string): Promise<{ budgetUsd?: number | null; budgetSpent?: number; expiresAt?: string | null }>\n revokeKey(id: string): Promise<unknown>\n}\n\n/**\n * Adapt the tcloud SDK client to {@link KeyProvisioner} — the typed seam that\n * replaces the `as unknown as KeyProvisioner` cast every consumer otherwise\n * repeats. The platform already exposes child-key minting (parent→child key,\n * per-key USD budget, expiry); this maps its shapes (`product` union,\n * `number | null` budgets) onto the manager's contract (`null → undefined`).\n */\nexport function createTcloudKeyProvisioner(client: TcloudKeyClient): KeyProvisioner {\n return {\n createKey: async (input) => {\n const created = await client.createKey(input)\n return { id: created.id, key: created.key }\n },\n revokeKey: (keyId) => client.revokeKey(keyId),\n getKey: async (keyId) => {\n const info = await client.getKey(keyId)\n return {\n budgetUsd: info.budgetUsd ?? undefined,\n budgetSpent: info.budgetSpent ?? undefined,\n expiresAt: info.expiresAt ?? null,\n }\n },\n }\n}\n\n/** A stored child-key record (the app's row, shape-normalized). */\nexport interface WorkspaceKeyRecord {\n /** App row id (opaque). */\n id: string\n keyId: string\n /** The encrypted secret — decrypted via {@link KeyCrypto.decrypt}. */\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date | null\n}\n\n/** Persistence seam — the product implements this against its own D1 table. */\nexport interface WorkspaceKeyStore {\n /** Most-recent active key for the workspace, or null. */\n getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null>\n /** All active keys (to revoke priors on rotate). */\n listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>>\n /** Persist a freshly minted active key. */\n insert(record: { workspaceId: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: Date }): Promise<void>\n /** Mark a prior row revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n}\n\n/** Secret encryption seam (the app's at-rest crypto). */\nexport interface KeyCrypto {\n encrypt(secret: string): Promise<string>\n decrypt(encrypted: string): Promise<string>\n}\n\nexport interface WorkspaceKeyManagerOptions {\n provisioner: KeyProvisioner\n store: WorkspaceKeyStore\n crypto: KeyCrypto\n /** Default monthly allowance (USD) when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. Default `() => new Date()`. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\nexport interface WorkspaceModelKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string | null\n exhausted: boolean\n}\n\nexport interface WorkspaceKeyManager {\n /** The workspace's active child-key secret, provisioning one if absent/expired. */\n ensureKey(workspaceId: string, opts?: { budgetUsd?: number }): Promise<string>\n /** Mint a fresh key + revoke priors (period renewal / top-up). `rollover`\n * carries the prior key's unused budget into the new one, bounded by\n * `rolloverCapUsd`. Returns the new secret. */\n rotateKey(workspaceId: string, opts?: { budgetUsd?: number; rollover?: boolean; rolloverCapUsd?: number }): Promise<string>\n /** Live budget usage for the active key (drives the \"$X of $Y used\" panel). */\n getUsage(workspaceId: string): Promise<WorkspaceModelKeyUsage | null>\n}\n\n/** Period end = first day of next month, midnight UTC. Keys expire at the period\n * boundary so a forgotten rotation fails closed rather than running free. */\nfunction nextPeriodEnd(now: Date): Date {\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0))\n}\n\n/** A round-trip probe through the at-rest crypto seam (no platform key needed).\n * Runs BEFORE minting so a consumer with a missing/invalid FIELD_ENCRYPTION_KEY\n * fails loud with zero platform spend instead of orphaning a freshly minted key\n * when encrypt later throws. The probe never touches the real secret. */\nasync function assertCryptoUsable(crypto: KeyCrypto): Promise<void> {\n const probe = 'agent-app:key-manager:crypto-probe'\n let roundTripped: string\n try {\n roundTripped = await crypto.decrypt(await crypto.encrypt(probe))\n } catch (err) {\n throw new Error(\n 'Key encryption is misconfigured: the crypto seam threw before minting. ' +\n 'Validate FIELD_ENCRYPTION_KEY (64-char hex) at startup. No platform key was minted.',\n { cause: err },\n )\n }\n if (roundTripped !== probe) {\n throw new Error(\n 'Key encryption is misconfigured: encrypt/decrypt round-trip did not preserve the plaintext. ' +\n 'No platform key was minted.',\n )\n }\n}\n\n// ---------------------------------------------------------------------------\n// Shared-platform-balance billing\n//\n// A DIFFERENT model from the per-workspace child-key manager above: here every\n// user runs against a SHARED platform balance (id.tangle.tools), keyed by the\n// user's platform identity. The app owns no key minting — it reads the balance,\n// gates a billable turn, and deducts spend through the platform billing API.\n// Plan limits, the platform transport, and identity resolution are SEAMS the\n// product supplies; this module imports no DB and no HTTP client.\n// ---------------------------------------------------------------------------\n\n/** A user's resolved platform identity (from the app's SSO account store). */\nexport interface PlatformIdentity {\n platformUserId: string\n /** The user's per-user platform API key (reads), or null when unlinked. */\n apiKey: string | null\n}\n\n/** Spendable balance for a platform user. */\nexport interface PlatformBalanceInfo {\n balance: number\n lifetimeSpent: number\n}\n\n/** Per-product spend aggregate. */\nexport interface PlatformProductUsage {\n product: string | null\n totalSpent: number\n count: number\n}\n\n/** Plan limits — a PARAMETER per product (dollar allowance, concurrency,\n * overage policy). Never baked into the framework. */\nexport interface PlanLimit {\n monthlyBalanceUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/**\n * The platform billing transport — the product wires these to id.tangle.tools\n * (or any balance backend). Reads authenticate as the user (their `apiKey`);\n * the deduct write is a service-token call naming the target user. This module\n * never touches HTTP — it only sequences these calls.\n */\nexport interface PlatformBillingClient<Plan extends string> {\n /** Resolve the user's platform identity, or null when there is no SSO account. */\n resolveIdentity(userId: string): Promise<PlatformIdentity | null>\n /** Subscription plan for the user (via their platform key). */\n getPlan(apiKey: string): Promise<Plan>\n /** Spendable balance for the user (via their platform key). */\n getBalance(apiKey: string): Promise<PlatformBalanceInfo>\n /** Per-product usage rows for the user (via their platform key). */\n getUsageByProduct(apiKey: string): Promise<PlatformProductUsage[]>\n /** Deduct spend against the user's balance (service-token write). */\n deduct(input: { platformUserId: string; amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n}\n\nexport interface SharedBillingState<Plan extends string> {\n /** Platform user id, or null when the user has no Tangle SSO account. */\n platformUserId: string | null\n plan: Plan\n monthlyBalanceUsd: number\n remainingBalanceUsd: number\n lifetimeSpentUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\nexport interface PlatformBalanceManagerOptions<Plan extends string> {\n client: PlatformBillingClient<Plan>\n /** Plan → limits map (the product's pricing). */\n planLimits: Record<Plan, PlanLimit>\n /** The plan an unlinked / outage user falls to (fails CLOSED). */\n freePlan: Plan\n /** The product slug to attribute usage to (for `getProductUsage`). */\n productSlug: string\n}\n\nexport interface PlatformBalanceManager<Plan extends string> {\n /** Resolve the user's plan + balance. Unlinked or platform-outage users fail\n * CLOSED: free plan, zero remaining balance — a billable run is never started\n * against an unknown balance. */\n getState(userId: string): Promise<SharedBillingState<Plan>>\n /** Gate a billable turn: allowed when the plan permits overage or remaining\n * balance is positive. Returns the state so the caller deducts against it. */\n canStartBillableTurn(userId: string): Promise<{ allowed: boolean; state: SharedBillingState<Plan> }>\n /** Deduct `amountUsd` against the user's platform balance. Throws when the\n * user is not platform-linked. */\n deduct(userId: string, params: { amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n /** This product's spend for the user (drives a usage panel). */\n getProductUsage(userId: string): Promise<{ spentUsd: number; transactionCount: number }>\n}\n\nexport function createPlatformBalanceManager<Plan extends string>(\n opts: PlatformBalanceManagerOptions<Plan>,\n): PlatformBalanceManager<Plan> {\n const { client, planLimits, freePlan, productSlug } = opts\n\n const getState: PlatformBalanceManager<Plan>['getState'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n // No SSO account, or linked without a platform key: unlinked free tier with\n // zero balance. Reads require the user's key — never call them empty.\n if (!identity || !identity.apiKey) {\n const limits = planLimits[freePlan]\n return {\n platformUserId: identity?.platformUserId ?? null,\n plan: freePlan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: 0,\n lifetimeSpentUsd: 0,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n const [plan, balance] = await Promise.all([client.getPlan(identity.apiKey), client.getBalance(identity.apiKey)])\n const limits = planLimits[plan]\n return {\n platformUserId: identity.platformUserId,\n plan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: balance.balance,\n lifetimeSpentUsd: balance.lifetimeSpent,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n\n const canStartBillableTurn: PlatformBalanceManager<Plan>['canStartBillableTurn'] = async (userId) => {\n const state = await getState(userId)\n if (!state.platformUserId) return { allowed: false, state }\n const allowed = state.overageAllowed || state.remainingBalanceUsd > 0\n return { allowed, state }\n }\n\n const deduct: PlatformBalanceManager<Plan>['deduct'] = async (userId, params) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity) throw new Error('Shared billing requires a platform-linked user')\n await client.deduct({\n platformUserId: identity.platformUserId,\n amountUsd: params.amountUsd,\n type: params.type,\n description: params.description,\n referenceId: params.referenceId,\n })\n }\n\n const getProductUsage: PlatformBalanceManager<Plan>['getProductUsage'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity?.apiKey) return { spentUsd: 0, transactionCount: 0 }\n const rows = await client.getUsageByProduct(identity.apiKey)\n const product = rows.find((row) => row.product === productSlug)\n return { spentUsd: product?.totalSpent ?? 0, transactionCount: product?.count ?? 0 }\n }\n\n return { getState, canStartBillableTurn, deduct, getProductUsage }\n}\n\nexport function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager {\n const clock = opts.now ?? (() => new Date())\n const product = opts.product ?? 'router'\n\n const getUsage: WorkspaceKeyManager['getUsage'] = async (workspaceId) => {\n const active = await opts.store.getActive(workspaceId)\n if (!active) return null\n const info = await opts.provisioner.getKey(active.keyId)\n const budgetUsd = info.budgetUsd ?? active.budgetUsd\n const budgetSpent = info.budgetSpent ?? 0\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: active.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt: info.expiresAt ?? (active.expiresAt ? active.expiresAt.toISOString() : null),\n exhausted: budgetRemaining <= 0,\n }\n }\n\n const rotateKey: WorkspaceKeyManager['rotateKey'] = async (workspaceId, ropts) => {\n const now = clock()\n const allowance = ropts?.budgetUsd ?? opts.defaultBudgetUsd\n\n let budgetUsd = allowance\n if (ropts?.rollover) {\n const prior = await getUsage(workspaceId).catch(() => null)\n budgetUsd = allowance + (prior?.budgetRemaining ?? 0)\n if (ropts.rolloverCapUsd != null) budgetUsd = Math.min(budgetUsd, ropts.rolloverCapUsd)\n }\n\n // Prove the at-rest crypto seam is usable BEFORE spending a platform mint.\n // The `KeyCrypto` impl may close over an env-resolved key (createFieldCrypto)\n // that is empty/invalid and only throws at encrypt time — after the mint —\n // orphaning the child key. A round-trip probe makes a misconfigured consumer\n // fail loud with ZERO platform spend.\n await assertCryptoUsable(opts.crypto)\n\n const expiresAt = nextPeriodEnd(now)\n const created = await opts.provisioner.createKey({ name: `ws:${workspaceId}`, product, budgetUsd, expiresAt: expiresAt.toISOString() })\n if (!created.key || !created.id) throw new Error('tcloud createKey returned no key')\n const mintedKeyId = created.id\n\n // Past this point the platform has minted a budget-bearing child key. ANY\n // failure before the row is persisted (encrypt, insert) MUST revoke the\n // just-minted key, else it leaks as a zombie holding parent-key headroom —\n // a missing/invalid encryption key would otherwise burn the child budget\n // per mint attempt until the parent can no longer mint (fleet-wide outage).\n let keyEncrypted: string\n let priors: Array<{ id: string; keyId: string }>\n try {\n keyEncrypted = await opts.crypto.encrypt(created.key)\n priors = await opts.store.listActive(workspaceId)\n await opts.store.insert({ workspaceId, keyId: mintedKeyId, keyEncrypted, budgetUsd, expiresAt })\n } catch (err) {\n // Best-effort revoke of the orphan. A revoke failure must NOT mask the\n // original cause — log it loudly and rethrow the original error.\n try {\n await opts.provisioner.revokeKey(mintedKeyId)\n } catch (revokeErr) {\n console.error(\n `[workspace-key-manager] FAILED to revoke orphaned child key ${mintedKeyId} ` +\n `for workspace ${workspaceId} after a post-mint failure — it now leaks parent-key budget. ` +\n `Revoke error:`,\n revokeErr,\n )\n }\n throw err\n }\n\n for (const p of priors) {\n await opts.store.markRevoked(p.id, now)\n // Best-effort upstream revoke — the row is already revoked and an expired\n // key fails closed regardless, so a transient error is non-fatal.\n try {\n await opts.provisioner.revokeKey(p.keyId)\n } catch {\n /* non-fatal */\n }\n }\n return created.key\n }\n\n const ensureKey: WorkspaceKeyManager['ensureKey'] = async (workspaceId, eopts) => {\n const now = clock()\n const active = await opts.store.getActive(workspaceId)\n if (active && (!active.expiresAt || active.expiresAt.getTime() > now.getTime())) {\n return opts.crypto.decrypt(active.keyEncrypted)\n }\n return rotateKey(workspaceId, { budgetUsd: eopts?.budgetUsd })\n }\n\n return { ensureKey, rotateKey, getUsage }\n}\n"],"mappings":"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","names":["limits"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/trace/mission-trace.ts","../src/trace/index.ts"],"sourcesContent":["/**\n * Mission trace context — mint + thread the trace ids that join a mission's\n * step attempts and its delegated agent runs into ONE trace tree.\n *\n * ID formats are byte-compatible with agent-runtime's trace propagation\n * (`readTraceContextFromEnv` / OTLP export): 32 lowercase hex chars for a\n * trace id (16 bytes), 16 for a span id (8 bytes). The env pair from\n * {@link traceEnv} is exactly what agent-runtime's MCP subprocess reads at\n * startup (`TRACE_ID` + `PARENT_SPAN_ID`), so a delegation dispatched with it\n * parents its loop→round→iteration spans under the mission's step span.\n *\n * Pure functions, no deps. Ids are DETERMINISTIC when a key is supplied\n * (missionId / step-attempt seed) so a crashed driver re-mints the identical\n * context on re-dispatch and the re-run joins the same trace instead of\n * forking a new one; without a key they are random.\n */\n\nexport interface MissionTraceContext {\n /** 32-hex trace id shared by every span in the mission's tree. */\n traceId: string\n /** 16-hex span id of the mission root — the parent of every step span. */\n rootSpanId: string\n}\n\nexport interface StepSpanContext {\n traceId: string\n /** 16-hex span id of this step attempt (or any nested unit of work). */\n spanId: string\n /** The span this one nests under. */\n parentSpanId: string\n}\n\n/**\n * Mint a mission's trace context. With `missionId` the ids are a pure\n * function of it; omitted, both ids are random.\n */\nexport function createMissionTraceContext(missionId?: string): MissionTraceContext {\n if (missionId !== undefined && missionId !== '') {\n return {\n traceId: hex64(fnv1a64(`mission-trace:${missionId}`)) + hex64(fnv1a64(`mission-trace:2:${missionId}`)),\n rootSpanId: hex64(fnv1a64(`mission-root-span:${missionId}`)),\n }\n }\n return { traceId: randomHex(16), rootSpanId: randomHex(8) }\n}\n\n/**\n * Derive a child span context under `parent` — one per step attempt (seed\n * e.g. `\"${stepId}#${attempt}\"`), or nested under another step span. With a\n * seed the span id is deterministic for the same parent + seed; omitted, it\n * is random.\n */\nexport function childSpanContext(\n parent: MissionTraceContext | StepSpanContext,\n seed?: string,\n): StepSpanContext {\n const parentSpanId = 'rootSpanId' in parent ? parent.rootSpanId : parent.spanId\n const spanId =\n seed !== undefined && seed !== ''\n ? hex64(fnv1a64(`span:${parent.traceId}:${parentSpanId}:${seed}`))\n : randomHex(8)\n return { traceId: parent.traceId, spanId, parentSpanId }\n}\n\n/**\n * The env pair a delegation subprocess inherits — agent-runtime's\n * `readTraceContextFromEnv` reads exactly these names. `PARENT_SPAN_ID` is\n * the span the dispatched work nests under: the root for a mission context,\n * the step-attempt span for a step context.\n */\nexport function traceEnv(ctx: MissionTraceContext | StepSpanContext): {\n TRACE_ID: string\n PARENT_SPAN_ID: string\n} {\n return {\n TRACE_ID: ctx.traceId,\n PARENT_SPAN_ID: 'rootSpanId' in ctx ? ctx.rootSpanId : ctx.spanId,\n }\n}\n\n// FNV-1a 64-bit over UTF-16 code units — a stable, dependency-free digest for\n// deterministic ids. Not cryptographic; trace ids only need uniqueness within\n// a tenant's missions, not unforgeability.\nconst FNV_OFFSET = 0xcbf29ce484222325n\nconst FNV_PRIME = 0x100000001b3n\nconst MASK_64 = 0xffffffffffffffffn\n\nfunction fnv1a64(input: string): bigint {\n let hash = FNV_OFFSET\n for (let i = 0; i < input.length; i++) {\n hash ^= BigInt(input.charCodeAt(i))\n hash = (hash * FNV_PRIME) & MASK_64\n }\n return hash\n}\n\nfunction hex64(value: bigint): string {\n return value.toString(16).padStart(16, '0')\n}\n\nfunction randomHex(byteLength: number): string {\n const bytes = new Uint8Array(byteLength)\n globalThis.crypto.getRandomValues(bytes)\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n","/**\n * `@tangle-network/agent-app/trace` — flow observability for agent turns.\n *\n * The turn buffer stamps `_t` (ms since turn start) on every event, so any\n * live stream OR any historical turn replayed from a TurnEventStore can be\n * reconstructed into a span trace: pipeline overhead, model segments (with\n * thinking TTFT), tool executions, token usage, and cost. Renderers turn\n * traces and multi-run samples into ASCII waterfalls and histograms — the\n * default artifact for \"how did this run actually behave\" questions across\n * evals, hill-climbs, and production debugging.\n *\n * Span boundaries derived from a buffered stream are quantized by the\n * pump's flush window and the reader's poll cadence (~100–400ms); spans\n * carry `approx: true` to keep reports honest about that.\n */\n\nexport * from './mission-trace'\nexport * from './flow-types'\nexport * from './mission-flow'\n\nimport type { FlowSpan, FlowTrace } from './flow-types'\n\nexport interface TimedEvent {\n /** ms since turn start (`_t` stamped by pumpBufferedTurn). */\n t: number\n event: Record<string, unknown>\n}\n\n/** Parse stored turn-event lines (JSON strings with `_t`) into TimedEvents. */\nexport function timedEventsFromLines(lines: string[]): TimedEvent[] {\n const out: TimedEvent[] = []\n for (const line of lines) {\n try {\n const parsed = JSON.parse(line) as Record<string, unknown>\n if (typeof parsed._t === 'number') out.push({ t: parsed._t, event: parsed })\n } catch {\n /* skip torn lines */\n }\n }\n return out.sort((a, b) => a.t - b.t)\n}\n\nfunction innerOf(e: Record<string, unknown>): Record<string, unknown> {\n return (e.kind === 'event' ? (e.event as Record<string, unknown>) : e) ?? {}\n}\n\n/**\n * Derive a span trace from timestamped turn events. Model segments are runs\n * of text/reasoning deltas; a tool span opens at the last delta before its\n * tool_call emission and closes at the matching tool_result.\n */\nexport function buildFlowTrace(\n events: TimedEvent[],\n opts?: { pricing?: { prompt?: string | number; completion?: string | number } },\n): FlowTrace {\n const spans: FlowSpan[] = []\n let promptTokens = 0\n let completionTokens = 0\n let toolCalls = 0\n\n const first = events[0]?.t ?? 0\n if (first > 0) {\n spans.push({ kind: 'pipeline', name: 'dispatch → first event', startMs: 0, endMs: first })\n }\n\n let segStart: number | null = null\n let segEnd = 0\n let segKinds = new Set<string>()\n let lastDeltaT = first\n const openCalls = new Map<string, { name: string; emitT: number; lastDeltaT: number }>()\n\n const closeSegment = () => {\n if (segStart !== null) {\n spans.push({\n kind: 'model',\n name: segKinds.has('reasoning') ? 'model turn (reasoning + text)' : 'model turn',\n startMs: segStart,\n endMs: segEnd,\n approx: true,\n })\n segStart = null\n segKinds = new Set()\n }\n }\n\n for (const { t, event } of events) {\n const inner = innerOf(event)\n const type = String(event.kind === 'tool_result' ? 'tool_result' : (inner.type ?? ''))\n\n if (type === 'text' || type === 'reasoning') {\n if (segStart === null) segStart = t\n segEnd = t\n segKinds.add(type)\n lastDeltaT = t\n } else if (type === 'tool_call') {\n closeSegment()\n toolCalls++\n const call = (inner.call ?? inner) as Record<string, unknown>\n const id = String(call.toolCallId ?? `call_${toolCalls}`)\n openCalls.set(id, { name: String(call.toolName ?? 'tool'), emitT: t, lastDeltaT })\n } else if (type === 'tool_result') {\n const id = String(event.toolCallId ?? inner.toolCallId ?? '')\n const open = openCalls.get(id)\n if (open) {\n spans.push({\n kind: 'tool',\n name: open.name,\n // Execution happens between the end of the model turn that emitted\n // the call and the result landing in the buffer.\n startMs: open.lastDeltaT,\n endMs: t,\n approx: true,\n meta: { ok: ((event.outcome ?? inner.outcome) as { ok?: boolean } | undefined)?.ok },\n })\n openCalls.delete(id)\n }\n } else if (type === 'usage') {\n const u = (inner.usage ?? {}) as { promptTokens?: number; completionTokens?: number }\n promptTokens += u.promptTokens ?? 0\n completionTokens += u.completionTokens ?? 0\n }\n }\n closeSegment()\n\n const totalMs = events.length ? events[events.length - 1]!.t : 0\n const trace: FlowTrace = { spans, totalMs, promptTokens, completionTokens, toolCalls }\n const p = opts?.pricing\n if (p && (p.prompt != null || p.completion != null)) {\n trace.costUsd = promptTokens * Number(p.prompt ?? 0) + completionTokens * Number(p.completion ?? 0)\n }\n return trace\n}\n\nconst fmtS = (ms: number) => `${(ms / 1000).toFixed(1)}s`\n\n/** ASCII waterfall cascade — the default artifact for explaining a flow. */\nexport function renderWaterfall(trace: FlowTrace, opts?: { width?: number }): string {\n const width = opts?.width ?? 40\n const scale = trace.totalMs > 0 ? width / trace.totalMs : 0\n const lines: string[] = []\n const spans = [...trace.spans].sort((a, b) => a.startMs - b.startMs)\n for (let i = 0; i < spans.length; i++) {\n const s = spans[i]!\n const offset = Math.round(s.startMs * scale)\n const len = Math.max(1, Math.round((s.endMs - s.startMs) * scale))\n const bar = ' '.repeat(offset) + (s.kind === 'tool' ? '▓' : s.kind === 'pipeline' ? '░' : '█').repeat(len)\n const branch = i === spans.length - 1 ? '└─' : '├─'\n const dur = `${fmtS(s.endMs - s.startMs)}${s.approx ? '~' : ''}`\n lines.push(`${fmtS(s.startMs).padStart(7)} ${branch} ${bar.padEnd(width + 2)} ${s.name} (${dur})`)\n }\n const cost = trace.costUsd != null ? ` $${trace.costUsd.toFixed(trace.costUsd < 0.01 ? 6 : 4)}` : ''\n lines.push(\n `${fmtS(trace.totalMs).padStart(7)} ── total · ${trace.promptTokens}p + ${trace.completionTokens}c tok · ${trace.toolCalls} tool calls${cost}`,\n )\n return lines.join('\\n')\n}\n\nexport interface DistributionSummary {\n n: number\n min: number\n p50: number\n p90: number\n max: number\n}\n\nexport function summarize(values: number[]): DistributionSummary {\n const sorted = [...values].sort((a, b) => a - b)\n const q = (p: number) => sorted[Math.min(sorted.length - 1, Math.floor(p * sorted.length))] ?? 0\n return { n: sorted.length, min: sorted[0] ?? 0, p50: q(0.5), p90: q(0.9), max: sorted[sorted.length - 1] ?? 0 }\n}\n\n/** ASCII histogram for multi-run samples (eval latencies, costs, scores). */\nexport function renderHistogram(\n values: number[],\n opts?: { buckets?: number; width?: number; unit?: string; format?: (v: number) => string },\n): string {\n if (!values.length) return '(no samples)'\n const buckets = opts?.buckets ?? 6\n const width = opts?.width ?? 24\n const fmt = opts?.format ?? ((v: number) => `${Math.round(v)}${opts?.unit ?? ''}`)\n const s = summarize(values)\n const lo = s.min\n const hi = s.max === s.min ? s.min + 1 : s.max\n const counts = new Array<number>(buckets).fill(0)\n for (const v of values) {\n counts[Math.min(buckets - 1, Math.floor(((v - lo) / (hi - lo)) * buckets))]!++\n }\n const maxCount = Math.max(...counts)\n const lines = [\n `n=${s.n} min=${fmt(s.min)} p50=${fmt(s.p50)} p90=${fmt(s.p90)} max=${fmt(s.max)}`,\n ]\n for (let i = 0; i < buckets; i++) {\n const a = lo + ((hi - lo) * i) / buckets\n const b = lo + ((hi - lo) * (i + 1)) / buckets\n const bar = '█'.repeat(Math.max(counts[i]! > 0 ? 1 : 0, Math.round((counts[i]! / maxCount) * width)))\n lines.push(`${fmt(a).padStart(8)}-${fmt(b).padEnd(8)} ${bar} ${counts[i]}`)\n }\n return lines.join('\\n')\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/runtime/model-catalog.ts"],"sourcesContent":["/**\n * Model catalogue — computed live from the Tangle Router, never hand-curated.\n * Lifted from tuner-agent so every agent app's model picker shares one\n * filter/dedupe/rank/feature pipeline instead of re-deriving it.\n *\n * The router's /models endpoint returns every routeable model (~200), which is\n * unusable as a picker list: it mixes chat models with TTS/embedding/realtime\n * endpoints, dated snapshots alias their parents, and provider-prefixed ids\n * duplicate canonical ones. This module turns that into a product catalogue:\n *\n * filter (chat-capable, routeable) → dedupe (snapshot/prefix/:free aliases)\n * → rank (provider tier, family, version) → feature (best model per family)\n * → default (env override or first featured)\n *\n * Freshness is automatic: everything is derived from the live router response,\n * so new models surface as soon as the router lists them. The only static\n * knowledge here is slow-moving: provider display order and family name\n * patterns (e.g. \"claude-sonnet-*\", \"gpt-N\"). A new Sonnet or GPT release\n * outranks its predecessor by version comparison with zero code change; only\n * a brand-new *family name* (rare) needs a one-line rule addition.\n */\n\nexport interface RouterModel {\n id: string\n name?: string\n description?: string\n _provider?: string\n pricing?: { prompt?: string | null; completion?: string | null }\n context_length?: number\n architecture?: {\n modality?: string\n input_modalities?: string[]\n output_modalities?: string[]\n }\n supported_parameters?: string[]\n routeability?: {\n status?: string\n routeable?: boolean\n provider?: string\n }\n}\n\nexport interface CatalogModel {\n id: string\n name: string\n provider: string\n description?: string\n contextLength?: number\n pricing?: { prompt?: string; completion?: string }\n supportsTools: boolean\n supportsReasoning: boolean\n featured: boolean\n}\n\nexport interface ModelCatalog {\n defaultModelId: string | null\n fetchedAt: string\n models: CatalogModel[]\n}\n\n/** Display order. Unlisted providers sort after these, alphabetically. */\nconst PROVIDER_TIER: string[] = [\n 'anthropic',\n 'openai',\n 'google',\n 'xai',\n 'deepseek',\n 'moonshotai',\n 'moonshot',\n 'zai',\n 'z-ai',\n 'mistral',\n 'groq',\n 'nvidia',\n 'cohere',\n 'cerebras',\n]\n\n/** Non-chat endpoints that pollute the router list (matched on normalized id). */\nconst EXCLUDED_ID = /(embedding|tts|transcribe|whisper|audio|realtime|image|lyria|sora|dall-e|moderation|content-safety|search-preview|search-api|deep-research)/\n\n/**\n * Featured families, in display order. Each rule surfaces the highest-version\n * routeable model whose normalized id matches. Patterns anchor on the family\n * name and stop before specialty suffixes (codex, nano, lite, …) so the\n * mainline model wins.\n */\nconst FEATURED_RULES: Array<{ providers: string[]; match: RegExp }> = [\n { providers: ['anthropic'], match: /^claude-sonnet-[\\d-]+$/ },\n { providers: ['anthropic'], match: /^claude-opus-[\\d-]+$/ },\n { providers: ['anthropic'], match: /^claude-haiku-[\\d-]+$/ },\n { providers: ['openai'], match: /^gpt-\\d+(\\.\\d+)?$/ },\n { providers: ['openai'], match: /^gpt-\\d+(\\.\\d+)?-mini$/ },\n { providers: ['google'], match: /^gemini-[\\d.]+-pro(-preview)?$/ },\n { providers: ['google'], match: /^gemini-[\\d.]+-flash(-preview)?$/ },\n { providers: ['xai'], match: /^grok-[\\d.]+$/ },\n { providers: ['deepseek'], match: /^deepseek-(chat|v[\\d.]+(-\\w+)?)$/ },\n { providers: ['moonshotai', 'moonshot'], match: /^kimi-k[\\d.]+$/ },\n { providers: ['zai', 'z-ai'], match: /^glm-[\\d.]+$/ },\n { providers: ['mistral'], match: /^mistral-(large|medium)-?[\\d.-]*$/ },\n]\n\n/** Families known to support tool calls even when router metadata omits it\n * (dated snapshots often lack the supported_parameters of their parent). */\nconst TOOL_CAPABLE_FAMILY = /^(claude|gpt-[45]|gpt-oss|o[134]|gemini|grok|deepseek|glm|kimi|mistral|ministral|magistral|command|nemotron|llama)/\n\n/** Strip provider prefix, :free suffix, and trailing date stamps. */\nexport function normalizeModelId(id: string): string {\n let tail = id.split('/').pop() ?? id\n tail = tail.replace(/:free$/, '')\n tail = tail.replace(/-\\d{8}$/, '')\n tail = tail.replace(/-\\d{4}-\\d{2}-\\d{2}$/, '')\n return tail\n}\n\n/** All numeric groups in a normalized id, for version comparison. */\nfunction versionOf(normId: string): number[] {\n return (normId.match(/\\d+/g) ?? []).map(Number)\n}\n\nfunction compareVersions(a: number[], b: number[]): number {\n const len = Math.max(a.length, b.length)\n for (let i = 0; i < len; i++) {\n const d = (a[i] ?? -1) - (b[i] ?? -1)\n if (d !== 0) return d\n }\n return 0\n}\n\n/** Lower = preferred representative for an alias group. */\nfunction aliasPenalty(id: string): number {\n let p = 0\n if (id.includes('/')) p += 4\n if (/-\\d{8}$|-\\d{4}-\\d{2}-\\d{2}$/.test(id.replace(/:free$/, ''))) p += 2\n if (id.endsWith(':free')) p += 1\n return p\n}\n\nfunction providerRank(provider: string): number {\n const i = PROVIDER_TIER.indexOf(provider)\n return i === -1 ? PROVIDER_TIER.length : i\n}\n\nfunction isChatModel(m: RouterModel): boolean {\n const arch = m.architecture\n if (!arch?.input_modalities || !arch?.output_modalities) return true\n return arch.input_modalities.includes('text') && arch.output_modalities.includes('text')\n}\n\nfunction isRouteable(m: RouterModel): boolean {\n return m.routeability?.routeable !== false && m.routeability?.status !== 'unavailable'\n}\n\nfunction familyOf(normId: string): string {\n return normId.replace(/[\\d.]+/g, '').replace(/-+/g, '-').replace(/-$/, '')\n}\n\n/**\n * Pure catalogue pipeline. `preferredDefault` (typically the MODEL_NAME env\n * var) wins when it survives filtering; otherwise the first featured model.\n */\nexport function buildCatalog(raw: RouterModel[], opts?: { preferredDefault?: string }): ModelCatalog {\n // Filter to chat-capable, routeable, non-specialty models\n const candidates = raw.filter(\n (m) => m.id && isRouteable(m) && isChatModel(m) && !EXCLUDED_ID.test(normalizeModelId(m.id)),\n )\n\n // Dedupe alias groups (dated snapshots, provider prefixes, :free variants).\n // Within a group, merge metadata so the representative keeps the richest\n // supported_parameters claim (snapshots often omit what the parent lists).\n const groups = new Map<string, RouterModel[]>()\n for (const m of candidates) {\n const key = `${m._provider ?? ''}::${normalizeModelId(m.id)}`\n const g = groups.get(key)\n if (g) g.push(m)\n else groups.set(key, [m])\n }\n\n const reps: Array<{ model: RouterModel; normId: string; mergedParams: Set<string> }> = []\n for (const group of groups.values()) {\n group.sort((a, b) => aliasPenalty(a.id) - aliasPenalty(b.id) || a.id.length - b.id.length)\n const rep = group[0]!\n const mergedParams = new Set<string>(group.flatMap((m) => m.supported_parameters ?? []))\n reps.push({ model: rep, normId: normalizeModelId(rep.id), mergedParams })\n }\n\n // Featured: best version per family rule, in rule order\n const featuredIds: string[] = []\n for (const rule of FEATURED_RULES) {\n const matches = reps.filter(\n (r) =>\n rule.providers.includes(r.model._provider ?? '') &&\n rule.match.test(r.normId) &&\n !featuredIds.includes(r.model.id),\n )\n if (!matches.length) continue\n matches.sort(\n (a, b) =>\n compareVersions(versionOf(b.normId), versionOf(a.normId)) ||\n Number(a.normId.includes('preview')) - Number(b.normId.includes('preview')) ||\n a.model.id.length - b.model.id.length,\n )\n featuredIds.push(matches[0]!.model.id)\n }\n\n const toCatalogModel = (r: (typeof reps)[number]): CatalogModel => {\n const m = r.model\n const provider = m._provider ?? 'unknown'\n return {\n id: m.id,\n name: m.name ?? m.id,\n provider,\n description: m.description ? m.description.slice(0, 160) : undefined,\n contextLength: m.context_length,\n pricing:\n m.pricing?.prompt || m.pricing?.completion\n ? { prompt: m.pricing.prompt ?? undefined, completion: m.pricing.completion ?? undefined }\n : undefined,\n supportsTools: r.mergedParams.has('tools') || TOOL_CAPABLE_FAMILY.test(r.normId),\n supportsReasoning: r.mergedParams.has('reasoning') || r.mergedParams.has('include_reasoning'),\n featured: featuredIds.includes(m.id),\n }\n }\n\n // Sort: featured first (rule order), then provider tier → family → version desc\n const featured = featuredIds\n .map((id) => reps.find((r) => r.model.id === id)!)\n .map(toCatalogModel)\n const rest = reps\n .filter((r) => !featuredIds.includes(r.model.id))\n .sort((a, b) => {\n const pa = providerRank(a.model._provider ?? '')\n const pb = providerRank(b.model._provider ?? '')\n if (pa !== pb) return pa - pb\n const fa = familyOf(a.normId)\n const fb = familyOf(b.normId)\n if (fa !== fb) return fa.localeCompare(fb)\n return compareVersions(versionOf(b.normId), versionOf(a.normId)) || a.model.id.localeCompare(b.model.id)\n })\n .map(toCatalogModel)\n\n const models = [...featured, ...rest]\n\n const preferred = opts?.preferredDefault\n const defaultModelId =\n (preferred && models.find((m) => m.id === preferred || normalizeModelId(m.id) === normalizeModelId(preferred))?.id) ||\n featured.find((m) => m.supportsTools)?.id ||\n models[0]?.id ||\n null\n\n return { defaultModelId, fetchedAt: new Date().toISOString(), models }\n}\n\n// ── Cached fetch ─────────────────────────────────────────────────────────\n\nconst CATALOG_TTL_MS = 5 * 60 * 1000\n\nlet _cache: { catalog: ModelCatalog; at: number } | null = null\n\n/**\n * Fetch the router model list and build the catalogue, with an in-isolate\n * cache (TTL 5 min). On router failure a stale catalogue is served rather\n * than erroring the picker.\n */\nexport async function fetchModelCatalog(cfg: {\n baseUrl: string\n apiKey: string\n preferredDefault?: string\n}): Promise<ModelCatalog> {\n if (_cache && Date.now() - _cache.at < CATALOG_TTL_MS) {\n return _cache.catalog\n }\n try {\n const res = await fetch(`${cfg.baseUrl}/models`, {\n headers: { Authorization: `Bearer ${cfg.apiKey}` },\n })\n if (!res.ok) throw new Error(`Router /models returned ${res.status}`)\n const data = (await res.json()) as { data?: RouterModel[] }\n const catalog = buildCatalog(data.data ?? [], { preferredDefault: cfg.preferredDefault })\n _cache = { catalog, at: Date.now() }\n return catalog\n } catch (err) {\n if (_cache) return _cache.catalog\n throw err\n }\n}\n\n/** Test-only: clear the catalogue cache. */\nexport function __resetCatalogCache(): void {\n _cache = null\n}\n"],"mappings":"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","names":[]}