failproofai 1.0.7-beta.1 → 1.0.7

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 (429) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +4 -4
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/audit/run/route.js +7 -8
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  33. package/.next/standalone/.next/server/app/audit/page.js +5 -7
  34. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  36. package/.next/standalone/.next/server/app/index.html +1 -1
  37. package/.next/standalone/.next/server/app/index.rsc +15 -15
  38. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  39. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  40. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  41. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  42. package/.next/standalone/.next/server/app/page.js +6 -6
  43. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  45. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  46. package/.next/standalone/.next/server/app/policies/page.js +11 -13
  47. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  48. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  50. package/.next/standalone/.next/server/app/project/[name]/page.js +7 -8
  51. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  54. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  55. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +7 -7
  56. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  59. package/.next/standalone/.next/server/app/projects/page.js +6 -7
  60. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  62. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -41
  63. package/.next/standalone/.next/server/app/settings/page.js +9 -12
  64. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  66. package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__1_bftcl._.js} +1 -1
  67. package/.next/standalone/.next/server/chunks/{[externals]__19_pzeq._.js → [externals]__1msfs-h._.js} +1 -1
  68. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  69. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1bf34x4._.js → [root-of-the-server]__1_r2rbg._.js} +7 -5
  70. package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
  71. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  72. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  73. package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1c3k-8x._.js} +2 -2
  74. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
  75. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -4
  79. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +4 -0
  80. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  81. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  82. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  83. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yxwl6j._.js → [root-of-the-server]__0bd3mje._.js} +2 -2
  84. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +5 -0
  85. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__0cxe_2_._.js} +3 -3
  87. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  88. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  89. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +22 -0
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +5 -0
  93. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +3 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +5 -0
  98. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/ssr/{_1w_5l7t._.js → _0h_douw._.js} +1 -1
  100. package/.next/standalone/.next/server/chunks/ssr/{_214wgrp._.js → _1-i_gzc._.js} +2 -2
  101. package/.next/standalone/.next/server/chunks/ssr/{_0bn2oo8._.js → _166t73i._.js} +1 -1
  102. package/.next/standalone/.next/server/chunks/ssr/{_1q46vxx._.js → _1_qswah._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifp._.js → _1es2j7i._.js} +5 -5
  104. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +3 -0
  105. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  107. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
  111. package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_18_d8l1._.js → node_modules_next_dist_0drixxt._.js} +4 -4
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
  116. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  117. package/.next/standalone/.next/server/pages/404.html +1 -1
  118. package/.next/standalone/.next/server/pages/500.html +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  120. package/.next/standalone/.next/server/server-reference-manifest.json +23 -56
  121. package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +1 -0
  122. package/.next/standalone/.next/static/chunks/0__8a7m868fvf.js +1 -0
  123. package/.next/standalone/.next/static/chunks/{12tvm75t5ffui.js → 0o6qlkgubtoex.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/13i7-9is-vhys.js +1 -0
  125. package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +6 -0
  126. package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
  127. package/.next/standalone/.next/static/chunks/{0vmd180qfntfb.js → 2klitrtzpaoe0.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{258668t68du6b.js → 2mdh397ghgnvv.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +2 -0
  130. package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +1 -0
  131. package/.next/standalone/.next/static/chunks/{0qrbdkv9qmvli.js → 3rh5o7e16irrm.js} +1 -1
  132. package/.next/standalone/.next/static/chunks/{29fql3nbnfc9q.js → 43ufqrz8qo3h-.js} +1 -1
  133. package/.next/standalone/SECURITY.md +53 -0
  134. package/.next/standalone/app/actions/pack-actions.ts +0 -12
  135. package/.next/standalone/app/policies/hooks-client.tsx +0 -9
  136. package/.next/standalone/app/settings/page.tsx +1 -20
  137. package/.next/standalone/app/settings/settings-client.tsx +1 -27
  138. package/.next/standalone/app/settings/settings.css +0 -79
  139. package/.next/standalone/package.json +9 -9
  140. package/.next/standalone/sdk/python/skill/SKILL.md +60 -14
  141. package/.next/standalone/sdk/python/skill/agents/openai.yaml +2 -1
  142. package/.next/standalone/sdk/python/skill/references/evaluator.md +255 -0
  143. package/.next/standalone/sdk/python/skill/references/events.md +17 -8
  144. package/.next/standalone/sdk/python/skill/references/frameworks.md +3 -0
  145. package/.next/standalone/sdk/python/skill/references/install.md +3 -0
  146. package/.next/standalone/sdk/python/skill/references/integration.md +6 -2
  147. package/.next/standalone/sdk/python/skill/references/typescript.md +568 -0
  148. package/.next/standalone/sdk/typescript/CHANGELOG.md +133 -0
  149. package/.next/standalone/sdk/typescript/LICENSE +42 -0
  150. package/.next/standalone/sdk/typescript/README.md +552 -0
  151. package/.next/standalone/sdk/typescript/eslint.config.mjs +59 -0
  152. package/.next/standalone/sdk/typescript/examples/research-agent.ts +197 -0
  153. package/.next/standalone/sdk/typescript/integration/ai.test.ts +920 -0
  154. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/agent.ts +337 -0
  155. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +261 -0
  156. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +16 -0
  157. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/surfaces.ts +605 -0
  158. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.json +12 -0
  159. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.surfaces.json +4 -0
  160. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/agent.ts +342 -0
  161. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +156 -0
  162. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +16 -0
  163. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/surfaces.ts +628 -0
  164. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.json +12 -0
  165. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.surfaces.json +4 -0
  166. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/agent.ts +346 -0
  167. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +156 -0
  168. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +13 -0
  169. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/surfaces.ts +651 -0
  170. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.json +12 -0
  171. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.surfaces.json +4 -0
  172. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/agent.ts +350 -0
  173. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +153 -0
  174. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +13 -0
  175. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/surfaces.ts +651 -0
  176. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.json +12 -0
  177. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.surfaces.json +4 -0
  178. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/agent.ts +623 -0
  179. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +344 -0
  180. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +19 -0
  181. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/tsconfig.json +12 -0
  182. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent-v1.ts +99 -0
  183. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent.ts +623 -0
  184. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +336 -0
  185. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +15 -0
  186. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/tsconfig.json +12 -0
  187. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/agent.ts +96 -0
  188. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +441 -0
  189. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +19 -0
  190. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/tsconfig.json +12 -0
  191. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.cjs +53 -0
  192. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.mjs +55 -0
  193. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/package.json +18 -0
  194. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/agent.ts +659 -0
  195. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +635 -0
  196. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +15 -0
  197. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/tsconfig.json +12 -0
  198. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/agent.ts +659 -0
  199. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +553 -0
  200. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +15 -0
  201. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/tsconfig.json +12 -0
  202. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/agent.ts +877 -0
  203. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/mcp-server.mjs +66 -0
  204. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +6797 -0
  205. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +24 -0
  206. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/tsconfig.json +12 -0
  207. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/agent.ts +872 -0
  208. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/mcp-server.mjs +66 -0
  209. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +2540 -0
  210. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +15 -0
  211. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/tsconfig.json +12 -0
  212. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/actions.ts +18 -0
  213. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/ai/route.ts +42 -0
  214. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/edge/route.ts +29 -0
  215. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/langgraph/route.ts +21 -0
  216. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/llamaindex/route.ts +11 -0
  217. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/mastra/route.ts +19 -0
  218. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/status/route.ts +7 -0
  219. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/layout.tsx +9 -0
  220. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/page.tsx +15 -0
  221. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/instrumentation.ts +15 -0
  222. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/ai.ts +61 -0
  223. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/langgraph.ts +68 -0
  224. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/llamaindex.ts +98 -0
  225. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/mastra.ts +90 -0
  226. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/next.config.ts +52 -0
  227. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +4343 -0
  228. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +28 -0
  229. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/agent.ts +51 -0
  230. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/deno-npm.ts +76 -0
  231. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +484 -0
  232. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +15 -0
  233. package/.next/standalone/sdk/typescript/integration/fixtures/types/agent.ts +79 -0
  234. package/.next/standalone/sdk/typescript/integration/fixtures/types/package-lock.json +740 -0
  235. package/.next/standalone/sdk/typescript/integration/fixtures/types/package.json +11 -0
  236. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/agent.ts +197 -0
  237. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package-lock.json +70 -0
  238. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package.json +12 -0
  239. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/tsconfig.json +12 -0
  240. package/.next/standalone/sdk/typescript/integration/global-setup.ts +29 -0
  241. package/.next/standalone/sdk/typescript/integration/harness.ts +451 -0
  242. package/.next/standalone/sdk/typescript/integration/langchain.test.ts +682 -0
  243. package/.next/standalone/sdk/typescript/integration/llamaindex.test.ts +709 -0
  244. package/.next/standalone/sdk/typescript/integration/mastra-coverage.test.ts +386 -0
  245. package/.next/standalone/sdk/typescript/integration/mastra.test.ts +311 -0
  246. package/.next/standalone/sdk/typescript/integration/nextjs.test.ts +341 -0
  247. package/.next/standalone/sdk/typescript/integration/runtime-parity.ts +180 -0
  248. package/.next/standalone/sdk/typescript/integration/runtimes.bun.test.ts +15 -0
  249. package/.next/standalone/sdk/typescript/integration/runtimes.core.test.ts +113 -0
  250. package/.next/standalone/sdk/typescript/integration/runtimes.deno.test.ts +19 -0
  251. package/.next/standalone/sdk/typescript/integration/types.test.ts +141 -0
  252. package/.next/standalone/sdk/typescript/integration/vanilla.test.ts +255 -0
  253. package/.next/standalone/sdk/typescript/package-lock.json +2640 -0
  254. package/.next/standalone/sdk/typescript/package.json +401 -0
  255. package/.next/standalone/sdk/typescript/scripts/finalize-build.mjs +123 -0
  256. package/.next/standalone/sdk/typescript/scripts/release.mjs +177 -0
  257. package/.next/standalone/sdk/typescript/src/clock.ts +58 -0
  258. package/.next/standalone/sdk/typescript/src/context.ts +214 -0
  259. package/.next/standalone/sdk/typescript/src/edge/adapter.ts +18 -0
  260. package/.next/standalone/sdk/typescript/src/edge/ai.ts +116 -0
  261. package/.next/standalone/sdk/typescript/src/edge/index.ts +238 -0
  262. package/.next/standalone/sdk/typescript/src/edge/langchain.ts +18 -0
  263. package/.next/standalone/sdk/typescript/src/edge/llamaindex.ts +12 -0
  264. package/.next/standalone/sdk/typescript/src/edge/mastra.ts +17 -0
  265. package/.next/standalone/sdk/typescript/src/edge/notice.ts +33 -0
  266. package/.next/standalone/sdk/typescript/src/environment.ts +75 -0
  267. package/.next/standalone/sdk/typescript/src/evaluator/authoring.ts +480 -0
  268. package/.next/standalone/sdk/typescript/src/evaluator/cli.ts +96 -0
  269. package/.next/standalone/sdk/typescript/src/evaluator/client.ts +421 -0
  270. package/.next/standalone/sdk/typescript/src/evaluator/expression.ts +1292 -0
  271. package/.next/standalone/sdk/typescript/src/evaluator/index.ts +144 -0
  272. package/.next/standalone/sdk/typescript/src/evaluator/protocol.ts +747 -0
  273. package/.next/standalone/sdk/typescript/src/evaluator/runtime.ts +930 -0
  274. package/.next/standalone/sdk/typescript/src/evaluator/sandbox-worker.ts +171 -0
  275. package/.next/standalone/sdk/typescript/src/evaluator/source-limits.ts +27 -0
  276. package/.next/standalone/sdk/typescript/src/evaluator/source.ts +509 -0
  277. package/.next/standalone/sdk/typescript/src/events.ts +879 -0
  278. package/.next/standalone/sdk/typescript/src/exit.ts +117 -0
  279. package/.next/standalone/sdk/typescript/src/index.ts +186 -0
  280. package/.next/standalone/sdk/typescript/src/integrations/ai.ts +1566 -0
  281. package/.next/standalone/sdk/typescript/src/integrations/compat.ts +322 -0
  282. package/.next/standalone/sdk/typescript/src/integrations/core.ts +1321 -0
  283. package/.next/standalone/sdk/typescript/src/integrations/index.ts +355 -0
  284. package/.next/standalone/sdk/typescript/src/integrations/langchain.ts +2340 -0
  285. package/.next/standalone/sdk/typescript/src/integrations/llamaindex.ts +2111 -0
  286. package/.next/standalone/sdk/typescript/src/integrations/mastra.ts +1802 -0
  287. package/.next/standalone/sdk/typescript/src/logger.ts +98 -0
  288. package/.next/standalone/sdk/typescript/src/next.ts +115 -0
  289. package/.next/standalone/sdk/typescript/src/node-require.ts +446 -0
  290. package/.next/standalone/sdk/typescript/src/redact.ts +305 -0
  291. package/.next/standalone/sdk/typescript/src/resolver.ts +120 -0
  292. package/.next/standalone/sdk/typescript/src/runtime.ts +29 -0
  293. package/.next/standalone/sdk/typescript/src/schema.ts +410 -0
  294. package/.next/standalone/sdk/typescript/src/scopes.ts +701 -0
  295. package/.next/standalone/sdk/typescript/src/shared.ts +33 -0
  296. package/.next/standalone/sdk/typescript/src/version.ts +5 -0
  297. package/.next/standalone/sdk/typescript/src/writer.ts +934 -0
  298. package/.next/standalone/sdk/typescript/test/adapters.test.ts +397 -0
  299. package/.next/standalone/sdk/typescript/test/ai.test.ts +1076 -0
  300. package/.next/standalone/sdk/typescript/test/copies.test.ts +204 -0
  301. package/.next/standalone/sdk/typescript/test/edge.test.ts +183 -0
  302. package/.next/standalone/sdk/typescript/test/evaluator-client.test.ts +234 -0
  303. package/.next/standalone/sdk/typescript/test/evaluator-protocol.test.ts +225 -0
  304. package/.next/standalone/sdk/typescript/test/events.test.ts +193 -0
  305. package/.next/standalone/sdk/typescript/test/expression.test.ts +181 -0
  306. package/.next/standalone/sdk/typescript/test/global-setup.ts +26 -0
  307. package/.next/standalone/sdk/typescript/test/helpers.ts +130 -0
  308. package/.next/standalone/sdk/typescript/test/integrations.test.ts +369 -0
  309. package/.next/standalone/sdk/typescript/test/langchain-copies.test.ts +204 -0
  310. package/.next/standalone/sdk/typescript/test/langchain.test.ts +999 -0
  311. package/.next/standalone/sdk/typescript/test/llamaindex.test.ts +1760 -0
  312. package/.next/standalone/sdk/typescript/test/mastra-coverage.test.ts +501 -0
  313. package/.next/standalone/sdk/typescript/test/mastra-lifecycle.test.ts +479 -0
  314. package/.next/standalone/sdk/typescript/test/mastra.test.ts +285 -0
  315. package/.next/standalone/sdk/typescript/test/next.test.ts +109 -0
  316. package/.next/standalone/sdk/typescript/test/packaging.test.ts +312 -0
  317. package/.next/standalone/sdk/typescript/test/redaction.test.ts +171 -0
  318. package/.next/standalone/sdk/typescript/test/runtimes.test.ts +101 -0
  319. package/.next/standalone/sdk/typescript/test/sandbox.test.ts +189 -0
  320. package/.next/standalone/sdk/typescript/test/scopes.test.ts +271 -0
  321. package/.next/standalone/sdk/typescript/test/setup.ts +19 -0
  322. package/.next/standalone/sdk/typescript/test/skill-snippets.test.ts +73 -0
  323. package/.next/standalone/sdk/typescript/test/spool-contract.test.ts +124 -0
  324. package/.next/standalone/sdk/typescript/test/tracker-bounds.test.ts +191 -0
  325. package/.next/standalone/sdk/typescript/test/wire-format.test.ts +214 -0
  326. package/.next/standalone/sdk/typescript/test/writer.test.ts +407 -0
  327. package/.next/standalone/sdk/typescript/tsconfig.build.json +15 -0
  328. package/.next/standalone/sdk/typescript/tsconfig.cjs.json +19 -0
  329. package/.next/standalone/sdk/typescript/tsconfig.json +28 -0
  330. package/.next/standalone/sdk/typescript/vitest.config.ts +33 -0
  331. package/.next/standalone/sdk/typescript/vitest.integration.config.ts +23 -0
  332. package/.next/standalone/server.js +1 -1
  333. package/bin/failproofai.mjs +2 -115
  334. package/dist/cli.mjs +6538 -13686
  335. package/dist/index.js +1 -19
  336. package/dist/worker.mjs +2085 -8012
  337. package/package.json +9 -9
  338. package/pi-extension/index.ts +0 -11
  339. package/scripts/build-policy-pack.mjs +2 -39
  340. package/src/hooks/builtin-policies.ts +3 -21
  341. package/src/hooks/cloud-enrollment-cli.ts +1 -1
  342. package/src/hooks/cloud-managed-policies.ts +0 -22
  343. package/src/hooks/custom-hooks-loader.ts +7 -45
  344. package/src/hooks/custom-hooks-registry.ts +1 -45
  345. package/src/hooks/first-run-gate.ts +0 -5
  346. package/src/hooks/fp-home.ts +0 -23
  347. package/src/hooks/handler.ts +6 -265
  348. package/src/hooks/hook-activity-store.ts +1 -105
  349. package/src/hooks/hook-telemetry.ts +0 -41
  350. package/src/hooks/loader-utils.ts +0 -6
  351. package/src/hooks/pack-cli.ts +17 -278
  352. package/src/hooks/pack-manifest.ts +7 -479
  353. package/src/hooks/pack-store.ts +10 -156
  354. package/src/hooks/policy-catalog.ts +0 -65
  355. package/src/hooks/policy-evaluator.ts +796 -940
  356. package/src/hooks/policy-registry.ts +0 -25
  357. package/src/hooks/policy-types.ts +0 -126
  358. package/src/hooks/worker-server.ts +26 -119
  359. package/src/index.ts +0 -6
  360. package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
  361. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
  362. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +0 -3
  363. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +0 -4
  364. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
  365. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +0 -3
  366. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +0 -22
  367. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
  368. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +0 -3
  369. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +0 -5
  370. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
  371. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
  372. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +0 -3
  373. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
  374. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
  375. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +0 -3
  376. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +0 -3
  377. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +0 -4
  378. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
  379. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +0 -3
  380. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +0 -3
  381. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +0 -3
  382. package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +0 -3
  383. package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
  384. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +0 -3
  385. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +0 -3
  386. package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0w6mzq5._.js +0 -151
  387. package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
  388. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
  389. package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
  390. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
  391. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
  392. package/.next/standalone/.next/static/chunks/0cd-_8-c-m1ea.js +0 -6
  393. package/.next/standalone/.next/static/chunks/0uldbut9y2-e8.js +0 -1
  394. package/.next/standalone/.next/static/chunks/1u5zsejmgrir_.js +0 -1
  395. package/.next/standalone/.next/static/chunks/285spx855h_3r.css +0 -2
  396. package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +0 -1
  397. package/.next/standalone/.next/static/chunks/2vkvu9-opa_1z.js +0 -1
  398. package/.next/standalone/.next/static/chunks/37lhv7wa3ywt6.js +0 -1
  399. package/.next/standalone/app/actions/get-jev-config.ts +0 -409
  400. package/.next/standalone/app/actions/update-jev-config.ts +0 -420
  401. package/.next/standalone/app/components/jev-notices.tsx +0 -96
  402. package/.next/standalone/app/settings/jev-panel.tsx +0 -469
  403. package/src/hooks/effective-reviewers.ts +0 -79
  404. package/src/hooks/jev-activity.ts +0 -385
  405. package/src/hooks/jev-cli.ts +0 -1193
  406. package/src/hooks/policy-authority.ts +0 -333
  407. package/src/hooks/policy-reviewability.ts +0 -229
  408. package/src/hooks/semantic/combine.ts +0 -541
  409. package/src/hooks/semantic/compile.ts +0 -176
  410. package/src/hooks/semantic/decide.ts +0 -392
  411. package/src/hooks/semantic/envelope.ts +0 -1296
  412. package/src/hooks/semantic/evaluator.ts +0 -547
  413. package/src/hooks/semantic/facts.ts +0 -292
  414. package/src/hooks/semantic/intent.ts +0 -1190
  415. package/src/hooks/semantic/jev-client.ts +0 -643
  416. package/src/hooks/semantic/jev-config.ts +0 -594
  417. package/src/hooks/semantic/jev-review.ts +0 -374
  418. package/src/hooks/semantic/jev-stats.ts +0 -289
  419. package/src/hooks/semantic/jev-throttle.ts +0 -421
  420. package/src/hooks/semantic/pack-policies.ts +0 -251
  421. package/src/hooks/semantic/policies.ts +0 -596
  422. package/src/hooks/semantic/precondition-names.ts +0 -60
  423. package/src/hooks/semantic/preconditions.ts +0 -58
  424. package/src/hooks/semantic/redact.ts +0 -2910
  425. package/src/hooks/semantic/types.ts +0 -145
  426. package/src/hooks/semver-precedence.ts +0 -128
  427. /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_buildManifest.js +0 -0
  428. /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_clientMiddlewareManifest.js +0 -0
  429. /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_ssgManifest.js +0 -0
@@ -0,0 +1,1760 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { performance } from "node:perf_hooks";
3
+
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { setLogger } from "../src/logger.js";
7
+ import * as compat from "../src/integrations/compat.js";
8
+ import * as core from "../src/integrations/core.js";
9
+ import {
10
+ adapter,
11
+ attach,
12
+ eventCallerStorage,
13
+ parseOptions,
14
+ summarizeNodes,
15
+ usageOf,
16
+ type AsyncContextModule,
17
+ type GlobalModule,
18
+ type RetrieverModule,
19
+ type WorkflowModule,
20
+ } from "../src/integrations/llamaindex.js";
21
+ import { runtime } from "../src/runtime.js";
22
+ import { agent as agentScope, session } from "../src/scopes.js";
23
+ import { flushed, useSpool } from "./helpers.js";
24
+ import type { Spool } from "./helpers.js";
25
+
26
+ /**
27
+ * The LlamaIndex.TS adapter's logic, against stand-ins for the two framework
28
+ * surfaces it attaches to: the callback bus and the workflow runtime's context
29
+ * middleware. `integration/llamaindex.test.ts` proves the same against real
30
+ * releases; this proves the cases a real scripted run cannot reach cheaply —
31
+ * a model call that throws, a stale leaf, a subscriber that is not there.
32
+ */
33
+
34
+ type Event = Record<string, unknown>;
35
+
36
+ /** `Settings.callbackManager`, dispatching synchronously with an EventCaller chain. */
37
+ class FakeBus {
38
+ private readonly handlers = new Map<string, Array<(event: unknown) => void>>();
39
+ on(event: string, handler: (event: unknown) => void): this {
40
+ this.handlers.set(event, [...(this.handlers.get(event) ?? []), handler]);
41
+ return this;
42
+ }
43
+ off(event: string, handler: (event: unknown) => void): this {
44
+ this.handlers.set(
45
+ event,
46
+ (this.handlers.get(event) ?? []).filter((h) => h !== handler),
47
+ );
48
+ return this;
49
+ }
50
+ emit(event: string, detail: Event, callers: unknown[] = []): void {
51
+ for (const handler of this.handlers.get(event) ?? []) {
52
+ handler({ detail, reason: { computedCallers: callers } });
53
+ }
54
+ }
55
+ /** Dispatch with a real-shaped `EventCaller` as `reason` (see `EventCaller` below). */
56
+ emitReason(event: string, detail: Event, reason: unknown): void {
57
+ for (const handler of this.handlers.get(event) ?? []) {
58
+ handler({ detail, reason });
59
+ }
60
+ }
61
+ count(): number {
62
+ return [...this.handlers.values()].reduce((sum, list) => sum + list.length, 0);
63
+ }
64
+ }
65
+
66
+ function subscribable() {
67
+ const subs = new Set<(...args: never[]) => unknown>();
68
+ return {
69
+ subs,
70
+ subscribe(callback: (...args: never[]) => unknown) {
71
+ subs.add(callback);
72
+ return () => subs.delete(callback);
73
+ },
74
+ };
75
+ }
76
+
77
+ /** A workflow-core context, reduced to the two middleware hooks and a step runner. */
78
+ function fakeContext() {
79
+ const callContext = subscribable();
80
+ const sendEvent = subscribable();
81
+ return {
82
+ __internal__call_context: callContext,
83
+ __internal__call_send_event: sendEvent,
84
+ step(handler: (...args: unknown[]) => unknown, input: unknown): unknown {
85
+ const cbs = [...callContext.subs] as Array<(c: unknown, next: (c: unknown) => void) => void>;
86
+ let i = 0;
87
+ let result: unknown;
88
+ const next = (context: unknown): void => {
89
+ if (i === cbs.length) {
90
+ const c = context as { handler: (...a: unknown[]) => unknown; inputs: unknown[] };
91
+ result = c.handler(this, ...c.inputs);
92
+ return;
93
+ }
94
+ cbs[i++]!(context, next);
95
+ };
96
+ next({ handler, inputs: [input] });
97
+ return result;
98
+ },
99
+ send(event: unknown): void {
100
+ for (const sub of sendEvent.subs) (sub as (e: unknown, h: unknown) => void)(event, {});
101
+ },
102
+ };
103
+ }
104
+
105
+ type Ctx = ReturnType<typeof fakeContext>;
106
+ const ev = (kind: string, data: Event = {}) => ({ kind, data });
107
+ const is = (kind: string) => ({ include: (event: unknown) => (event as { kind?: unknown })?.kind === kind });
108
+
109
+ /** An `AgentWorkflow`: step handlers are instance arrow fields, as in the real one. */
110
+ class AgentWorkflow {
111
+ workflow: { createContext: () => Ctx };
112
+ agents: Map<string, { llm: { metadata: { model: string } } }>;
113
+ rootAgentName: string;
114
+ done: Promise<void> = Promise.resolve();
115
+ input: unknown;
116
+ program: (ctx: Ctx, self: AgentWorkflow) => Promise<void>;
117
+ handleInputStep: (ctx: unknown, event: unknown) => Promise<void> = async () => {};
118
+ runAgentStep: (ctx: unknown, event: unknown) => Promise<void> = async () => {};
119
+ executeToolCalls: (ctx: unknown, event: unknown) => Promise<void> = async () => {};
120
+
121
+ constructor(names: string[], program: AgentWorkflow["program"]) {
122
+ this.workflow = { createContext: () => fakeContext() };
123
+ this.agents = new Map(names.map((name) => [name, { llm: { metadata: { model: `${name}-model` } } }]));
124
+ this.rootAgentName = names[0]!;
125
+ this.program = program;
126
+ }
127
+
128
+ runStream(input: unknown): string {
129
+ this.input = input;
130
+ const ctx = this.workflow.createContext();
131
+ this.done = this.program(ctx, this);
132
+ return "stream";
133
+ }
134
+ }
135
+
136
+ const workflowModule = (): WorkflowModule => ({
137
+ AgentWorkflow,
138
+ stopAgentEvent: is("stop"),
139
+ agentToolCallEvent: is("toolCall"),
140
+ agentToolCallResultEvent: is("toolResult"),
141
+ });
142
+
143
+ let spool: Spool;
144
+ let bus: FakeBus;
145
+
146
+ function install(options: Record<string, unknown> = {}, workflows: WorkflowModule[] = [workflowModule()]) {
147
+ bus = new FakeBus();
148
+ return attach({ reaperInterval: 0, ...options }, {
149
+ globals: [{ Settings: { callbackManager: bus } }],
150
+ workflows,
151
+ });
152
+ }
153
+
154
+ const shape = (events: Event[]): string[] =>
155
+ events.map((e) => [e.agent_id, e.type, (e.hook_name ?? e.tool_name ?? "") as string].join(" ").trim());
156
+
157
+ beforeEach(() => {
158
+ spool = useSpool();
159
+ core.resetFailures();
160
+ core.setStrict(true);
161
+ });
162
+ afterEach(async () => {
163
+ adapter.uninstall();
164
+ core.setStrict(null);
165
+ compat.setStrictIntegrations(null);
166
+ compat.resetWarnings();
167
+ await spool.cleanup();
168
+ setLogger(null);
169
+ });
170
+
171
+ describe("usage extraction", () => {
172
+ it("finds a streamed call's usage on the chunk that carries it", () => {
173
+ // `wrapLLMEvent` hands `llm-end` the ARRAY of chunks as `raw`; OpenAI puts
174
+ // the usage on the last, content-less one.
175
+ const usage = usageOf({
176
+ raw: [{ delta: "hi", raw: { choices: [] } }, { delta: "", raw: { usage: { prompt_tokens: 7, completion_tokens: 3 } } }],
177
+ });
178
+ expect(usage).toEqual({ usage: { prompt_tokens: 7, completion_tokens: 3 }, inputTokens: 7, outputTokens: 3 });
179
+ });
180
+
181
+ it("reads a non-streamed response and the camelCase spellings", () => {
182
+ expect(usageOf({ raw: { usage: { inputTokens: 4, outputTokens: 2 } } })).toMatchObject({ inputTokens: 4, outputTokens: 2 });
183
+ expect(usageOf({ raw: { usageMetadata: { promptTokenCount: 9, candidatesTokenCount: 1 } } })).toMatchObject({
184
+ inputTokens: 9,
185
+ outputTokens: 1,
186
+ });
187
+ });
188
+
189
+ it("ships an unrecognised usage object without inventing token counts", () => {
190
+ expect(usageOf({ raw: { usage: { units: 12 } } })).toEqual({ usage: { units: 12 }, inputTokens: undefined, outputTokens: undefined });
191
+ expect(usageOf({ raw: null })).toEqual({});
192
+ expect(usageOf(undefined)).toEqual({});
193
+ });
194
+ });
195
+
196
+ describe("summarizeNodes", () => {
197
+ it("keeps the count, the scores and a prefix of the top few", () => {
198
+ const nodes = Array.from({ length: 7 }, (_, i) => ({
199
+ node: { id_: `n${i}`, getContent: () => "x".repeat(500) },
200
+ score: i / 10,
201
+ }));
202
+ const summary = summarizeNodes(nodes);
203
+ expect(summary.num_nodes).toBe(7);
204
+ expect(summary.top).toHaveLength(5);
205
+ expect(summary.top[0]).toMatchObject({ id: "n0", score: 0 });
206
+ expect(String(summary.top[0]!.text).length).toBeLessThanOrEqual(200);
207
+ expect(summarizeNodes(undefined)).toEqual({ num_nodes: 0, top: [] });
208
+ });
209
+ });
210
+
211
+ describe("options", () => {
212
+ it("mirrors the Python adapter's options, in camelCase", () => {
213
+ expect(parseOptions({})).toEqual({
214
+ captureMessages: true,
215
+ steps: true,
216
+ embeddings: false,
217
+ staleAfter: 600,
218
+ reaperInterval: 30,
219
+ captureLimit: undefined,
220
+ });
221
+ expect(parseOptions({ captureMessages: false, steps: false, embeddings: true, staleAfter: 5, reaperInterval: 0 })).toMatchObject({
222
+ captureMessages: false,
223
+ steps: false,
224
+ embeddings: true,
225
+ staleAfter: 5,
226
+ reaperInterval: 0,
227
+ });
228
+ });
229
+ });
230
+
231
+ describe("the callback bus", () => {
232
+ class LLMAgent {
233
+ llm = { metadata: { model: "legacy-model" } };
234
+ }
235
+
236
+ it("records a legacy task as ONE agent across every step, not one per step", async () => {
237
+ install();
238
+ const runner = new LLMAgent();
239
+ const step1 = { id: "s1", prevStep: null, context: { store: { messages: [{ content: "weather?" }] } } };
240
+ const step2 = { id: "s2", prevStep: step1, context: {} };
241
+ // `agent-start` fires for EVERY step, `agent-end` only after the last.
242
+ bus.emit("agent-start", { startStep: step1 }, [runner]);
243
+ bus.emit("llm-start", { id: "m1", messages: [] }, [runner]);
244
+ bus.emit("llm-end", { id: "m1", response: { message: { content: "" }, raw: { usage: { prompt_tokens: 1, completion_tokens: 1 } } } }, [runner]);
245
+ bus.emit("llm-tool-call", { toolCall: { id: "t1", name: "get_weather", input: { city: "Rome" } } }, [runner]);
246
+ bus.emit("agent-start", { startStep: step2 }, [runner]);
247
+ // `callTool` dispatched no `llm-tool-result` — the tool threw. The next model
248
+ // call carries its result, with `isError`.
249
+ bus.emit(
250
+ "llm-start",
251
+ { id: "m2", messages: [{ role: "user", content: "x", options: { toolResult: { id: "t1", result: "Error: boom", isError: true } } }] },
252
+ [runner],
253
+ );
254
+ bus.emit("llm-end", { id: "m2", response: { message: { content: "It is sunny." } } }, [runner]);
255
+ bus.emit("agent-end", { endStep: step2 }, [runner]);
256
+
257
+ const events = await flushed(spool);
258
+ expect(shape(events)).toEqual([
259
+ "LLMAgent agent_start",
260
+ "LLMAgent model_request",
261
+ "LLMAgent model_response",
262
+ "LLMAgent tool_use get_weather",
263
+ "LLMAgent tool_result get_weather",
264
+ "LLMAgent model_request",
265
+ "LLMAgent model_response",
266
+ "LLMAgent agent_end",
267
+ ]);
268
+ expect(new Set(events.map((e) => e.session_id)).size).toBe(1);
269
+ expect(events.find((e) => e.type === "tool_result")!.error).toBe("Error: boom");
270
+ expect(events.find((e) => e.type === "model_request")!.model).toBe("legacy-model");
271
+ const end = events.find((e) => e.type === "agent_end")!;
272
+ expect(end.outcome).toBe("success");
273
+ expect(end.summary).toBe("It is sunny.");
274
+ expect(events[0]!.goal).toBe("weather?");
275
+ });
276
+
277
+ it("defers a run's end until its streamed model call has been consumed", async () => {
278
+ install();
279
+ const runner = new LLMAgent();
280
+ const step = { id: "s1", prevStep: null };
281
+ bus.emit("agent-start", { startStep: step }, [runner]);
282
+ bus.emit("llm-start", { id: "m1", messages: [] }, [runner]);
283
+ bus.emit("llm-stream", { id: "m1", chunk: { delta: "a" } }, [runner]);
284
+ // The last step returns a stream: `agent-end` fires before anyone reads it.
285
+ bus.emit("agent-end", { endStep: step }, [runner]);
286
+ bus.emit(
287
+ "llm-end",
288
+ { id: "m1", response: { message: { content: "a" }, raw: [{ raw: {} }, { raw: { usage: { input_tokens: 5, output_tokens: 2 } } }] } },
289
+ [runner],
290
+ );
291
+ const events = await flushed(spool);
292
+ expect(shape(events)).toEqual(["LLMAgent agent_start", "LLMAgent model_request", "LLMAgent model_response", "LLMAgent agent_end"]);
293
+ const response = events.find((e) => e.type === "model_response")!;
294
+ expect([response.input_tokens, response.output_tokens]).toEqual([5, 2]);
295
+ expect(response.fw_chunks).toBe(2);
296
+ expect(typeof response.fw_ttft_ms).toBe("number");
297
+ expect(typeof response.duration_ms).toBe("number");
298
+ });
299
+
300
+ it("makes a bare model call its own run, named after the model's class", async () => {
301
+ install();
302
+ class OpenAI {
303
+ metadata = { model: "gpt-x" };
304
+ }
305
+ const llm = new OpenAI();
306
+ bus.emit("llm-start", { id: "m1", messages: [{ role: "user", content: "hi" }] }, [llm]);
307
+ bus.emit("llm-end", { id: "m1", response: { message: { content: "hello" } } }, [llm]);
308
+ const events = await flushed(spool);
309
+ expect(shape(events)).toEqual(["OpenAI agent_start", "OpenAI model_request", "OpenAI model_response", "OpenAI agent_end"]);
310
+ expect(events.find((e) => e.type === "model_request")!.model).toBe("gpt-x");
311
+ expect(events[1]!.request_id).toBe("m1");
312
+ expect(events[2]!.request_id).toBe("m1");
313
+ });
314
+
315
+ it("joins an enclosing session() scope instead of inventing one", async () => {
316
+ install();
317
+ await session({ sessionId: "req-9" }, () => {
318
+ bus.emit("llm-start", { id: "m1", messages: [] });
319
+ bus.emit("llm-end", { id: "m1", response: {} });
320
+ });
321
+ const events = await flushed(spool);
322
+ expect(shape(events)).toEqual(["llm agent_start", "llm model_request", "llm model_response", "llm agent_end"]);
323
+ expect(new Set(events.map((e) => e.session_id))).toEqual(new Set(["req-9"]));
324
+ });
325
+
326
+ it("records a top-level query engine call as a run, with its retrieval inside it", async () => {
327
+ install();
328
+ class RetrieverQueryEngine {}
329
+ const engine = new RetrieverQueryEngine();
330
+ bus.emit("query-start", { id: "q1", query: "what?" }, [engine]);
331
+ bus.emit("retrieve-start", { id: "r1", query: { query: "what?" } }, [engine]);
332
+ bus.emit("retrieve-end", { id: "r1", nodes: [{ node: { id_: "a", text: "alpha" }, score: 0.9 }] }, [engine]);
333
+ bus.emit("synthesize-start", { id: "y1" }, [engine]);
334
+ bus.emit("query-end", { id: "q1", response: { message: { content: "answer" } } }, [engine]);
335
+ const events = await flushed(spool);
336
+ expect(shape(events)).toEqual([
337
+ "RetrieverQueryEngine agent_start",
338
+ "RetrieverQueryEngine tool_use retriever",
339
+ "RetrieverQueryEngine tool_result retriever",
340
+ "RetrieverQueryEngine agent_end",
341
+ ]);
342
+ expect(events[2]!.output).toEqual({ num_nodes: 1, top: [{ id: "a", score: 0.9, text: "alpha" }] });
343
+ expect(events[3]!.summary).toBe("answer");
344
+ });
345
+
346
+ it("suffixes a repeated tool call id within one run rather than pairing it wrongly", async () => {
347
+ install();
348
+ const runner = new LLMAgent();
349
+ bus.emit("agent-start", { startStep: { id: "s1" } }, [runner]);
350
+ for (let i = 0; i < 2; i += 1) {
351
+ bus.emit("llm-tool-call", { toolCall: { id: "c1", name: "t", input: {} } }, [runner]);
352
+ bus.emit("llm-tool-result", { toolCall: { id: "c1" }, toolResult: { output: i, isError: false } }, [runner]);
353
+ }
354
+ bus.emit("agent-end", { endStep: { id: "s1" } }, [runner]);
355
+ const events = await flushed(spool);
356
+ expect(events.filter((e) => e.type === "tool_use").map((e) => e.tool_call_id)).toEqual(["c1", "c1#1"]);
357
+ expect(events.filter((e) => e.type === "tool_result").map((e) => e.tool_call_id)).toEqual(["c1", "c1#1"]);
358
+ });
359
+
360
+ it("drops every payload under captureMessages: false, keeping structure and tokens", async () => {
361
+ install({ captureMessages: false });
362
+ bus.emit("llm-start", { id: "m1", messages: [{ role: "user", content: "secret" }] });
363
+ bus.emit("llm-end", { id: "m1", response: { message: { content: "secret" }, raw: { usage: { prompt_tokens: 2, completion_tokens: 1 } } } });
364
+ const events = await flushed(spool);
365
+ expect(JSON.stringify(events)).not.toContain("secret");
366
+ expect(events.find((e) => e.type === "model_response")!.input_tokens).toBe(2);
367
+ });
368
+
369
+ it("closes a leaf nobody will close once it is stale, and ends the bare run it opened", async () => {
370
+ // A model call that throws has no `llm-end`: wrapLLMEvent has no error path.
371
+ const handle = install({ staleAfter: 60 });
372
+ bus.emit("llm-start", { id: "m1", messages: [] });
373
+ expect(handle.sweep(performance.now())).toBe(0);
374
+ expect(handle.sweep(performance.now() + 61_000)).toBe(1);
375
+ const events = await flushed(spool);
376
+ expect(shape(events)).toEqual(["llm agent_start", "llm model_request", "llm model_response", "llm agent_end"]);
377
+ expect(events[2]!.fw_closed_by).toBe("stale");
378
+ // We never learned how that call ended; it is not a success.
379
+ expect(events[3]!.outcome).toBe("cancelled");
380
+ });
381
+
382
+ it("ends a legacy task that will never send agent-end once it has been silent too long", async () => {
383
+ // A legacy step that throws dispatches nothing: no `llm-end`, no `agent-end`.
384
+ const handle = install({ staleAfter: 60 });
385
+ const runner = new LLMAgent();
386
+ bus.emit("agent-start", { startStep: { id: "s1" } }, [runner]);
387
+ expect(handle.sweep(performance.now() + 30_000)).toBe(0);
388
+ expect(handle.sweep(performance.now() + 61_000)).toBe(1);
389
+ const events = await flushed(spool);
390
+ expect(shape(events)).toEqual(["LLMAgent agent_start", "LLMAgent agent_end"]);
391
+ expect(events[1]).toMatchObject({ outcome: "cancelled", fw_run_id: expect.any(String) as unknown });
392
+ // And a later event for that task starts nothing stale: it is forgotten.
393
+ bus.emit("agent-end", { endStep: { id: "s1" } }, [runner]);
394
+ expect(await flushed(spool)).toHaveLength(2);
395
+ });
396
+
397
+ it("makes a bare tool call its own run, named after the tool", async () => {
398
+ install();
399
+ bus.emit("llm-tool-call", { toolCall: { id: "c1", name: "get_weather", input: { city: "Rome" } } });
400
+ bus.emit("llm-tool-result", { toolCall: { id: "c1" }, toolResult: { output: "sunny", isError: false } });
401
+ const events = await flushed(spool);
402
+ expect(shape(events)).toEqual([
403
+ "get_weather agent_start",
404
+ "get_weather tool_use get_weather",
405
+ "get_weather tool_result get_weather",
406
+ "get_weather agent_end",
407
+ ]);
408
+ expect(events[2]!.output).toBe("sunny");
409
+ expect(events[3]!.outcome).toBe("success");
410
+ });
411
+
412
+ it("detaches from the bus on uninstall and closes what is open as cancelled", async () => {
413
+ install();
414
+ bus.emit("llm-start", { id: "m1", messages: [] });
415
+ expect(bus.count()).toBeGreaterThan(0);
416
+ adapter.uninstall();
417
+ expect(bus.count()).toBe(0);
418
+ const events = await flushed(spool);
419
+ expect(shape(events)).toEqual(["llm agent_start", "llm model_request", "llm model_response", "llm agent_end"]);
420
+ expect(events[3]!.outcome).toBe("cancelled");
421
+ });
422
+ });
423
+
424
+ describe("the workflow runtime", () => {
425
+ const model = (id: string, content = "", callers: unknown[] = []) => {
426
+ bus.emit("llm-start", { id, messages: [] }, callers);
427
+ bus.emit("llm-end", { id, response: { message: { content }, raw: { usage: { prompt_tokens: 3, completion_tokens: 1 } } } }, callers);
428
+ };
429
+
430
+ it("records a run as the agent, its steps as hooks, and closes it on the stop event", async () => {
431
+ install();
432
+ const wf = new AgentWorkflow(["Agent"], async (ctx, self) => {
433
+ await ctx.step(self.handleInputStep, ev("start", { userInput: "q" }));
434
+ await ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "Agent" }));
435
+ await ctx.step(self.executeToolCalls, ev("toolCalls", { agentName: "Agent" }));
436
+ ctx.send(ev("stop", { result: "done", state: { memory: "huge" } }));
437
+ });
438
+ wf.runAgentStep = async () => {
439
+ model("m1");
440
+ };
441
+ wf.executeToolCalls = async () => {
442
+ // The runtime announces the call, THEN `callTool` dispatches on the bus
443
+ // too (workflow ≥1.1.2x): one call, recorded once.
444
+ current!.send(ev("toolCall", { toolName: "get_weather", toolId: "call_1", toolKwargs: { city: "Paris" } }));
445
+ bus.emit("llm-tool-call", { toolCall: { id: "call_1", name: "get_weather", input: { city: "Paris" } } });
446
+ bus.emit("llm-tool-result", { toolCall: { id: "call_1" }, toolResult: { output: "sunny", isError: false } });
447
+ current!.send(ev("toolResult", { toolId: "call_1", toolOutput: { result: "sunny", isError: false }, raw: "sunny" }));
448
+ };
449
+ let current: Ctx | null = null;
450
+ const create = wf.workflow.createContext;
451
+ wf.workflow.createContext = () => (current = create());
452
+ expect(wf.runStream("q")).toBe("stream");
453
+ await wf.done;
454
+
455
+ const events = await flushed(spool);
456
+ expect(shape(events)).toEqual([
457
+ "Agent agent_start",
458
+ "Agent hook_triggered handleInputStep",
459
+ "Agent hook_completed handleInputStep",
460
+ "Agent hook_triggered runAgentStep",
461
+ "Agent model_request",
462
+ "Agent model_response",
463
+ "Agent hook_completed runAgentStep",
464
+ "Agent hook_triggered executeToolCalls",
465
+ "Agent tool_use get_weather",
466
+ "Agent tool_result get_weather",
467
+ "Agent hook_completed executeToolCalls",
468
+ "Agent agent_end",
469
+ ]);
470
+ expect(events.find((e) => e.type === "hook_triggered")!.trigger_event).toBe("workflow_step");
471
+ // No `llm-start` caller chain here: the model comes from the agent the step names.
472
+ expect(events.find((e) => e.type === "model_request")!.model).toBe("Agent-model");
473
+ expect(events.find((e) => e.type === "tool_result")!.output).toBe("sunny");
474
+ expect(events.at(-1)!.summary).toBe("done");
475
+ expect(events[0]!.goal).toBe("q");
476
+ });
477
+
478
+ it("opens a nested agent per agent holding the turn, closing it on handoff", async () => {
479
+ install();
480
+ const wf = new AgentWorkflow(["triage", "forecaster"], async (ctx, self) => {
481
+ await ctx.step(self.handleInputStep, ev("start"));
482
+ await ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "triage" }));
483
+ // A tool step names `agentName`, not `currentAgentName`; a step naming
484
+ // neither keeps whoever holds the turn.
485
+ await ctx.step(self.executeToolCalls, ev("toolCalls", { agentName: "triage" }));
486
+ await ctx.step(self.handleInputStep, ev("other"));
487
+ await ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "forecaster" }));
488
+ ctx.send(ev("stop", { result: "ok" }));
489
+ });
490
+ wf.runAgentStep = async () => {
491
+ model(`m${Math.random()}`);
492
+ };
493
+ wf.runStream("q");
494
+ await wf.done;
495
+ const events = await flushed(spool);
496
+ expect(shape(events)).toEqual([
497
+ "AgentWorkflow agent_start",
498
+ "AgentWorkflow hook_triggered handleInputStep",
499
+ "AgentWorkflow hook_completed handleInputStep",
500
+ "triage agent_start",
501
+ "triage hook_triggered runAgentStep",
502
+ "triage model_request",
503
+ "triage model_response",
504
+ "triage hook_completed runAgentStep",
505
+ "triage hook_triggered executeToolCalls",
506
+ "triage hook_completed executeToolCalls",
507
+ "triage hook_triggered handleInputStep",
508
+ "triage hook_completed handleInputStep",
509
+ "triage agent_end",
510
+ "forecaster agent_start",
511
+ "forecaster hook_triggered runAgentStep",
512
+ "forecaster model_request",
513
+ "forecaster model_response",
514
+ "forecaster hook_completed runAgentStep",
515
+ "forecaster agent_end",
516
+ "AgentWorkflow agent_end",
517
+ ]);
518
+ for (const start of events.filter((e) => e.type === "agent_start").slice(1)) {
519
+ expect(start.parent_id).toBe("AgentWorkflow");
520
+ }
521
+ });
522
+
523
+ it("fails the step, its open model call and the run when a step throws", async () => {
524
+ setLogger({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() });
525
+ install();
526
+ const wf = new AgentWorkflow(["Agent"], async (ctx, self) => {
527
+ await Promise.resolve(ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "Agent" }))).catch(() => undefined);
528
+ });
529
+ wf.runAgentStep = async () => {
530
+ // A provider that throws: `llm-start` went out, `llm-end` never will.
531
+ bus.emit("llm-start", { id: "m1", messages: [] });
532
+ throw new Error("model exploded");
533
+ };
534
+ wf.runStream("q");
535
+ await wf.done;
536
+ const events = await flushed(spool);
537
+ expect(shape(events)).toEqual([
538
+ "Agent agent_start",
539
+ "Agent hook_triggered runAgentStep",
540
+ "Agent model_request",
541
+ "Agent model_response",
542
+ "Agent hook_completed runAgentStep",
543
+ "Agent agent_end",
544
+ ]);
545
+ expect(events[3]!.error).toMatch(/model exploded/);
546
+ expect(events[4]!.outcome).toBe("failed");
547
+ expect(events[5]!.outcome).toBe("failed");
548
+ expect(events.filter((e) => e.type === "error")).toEqual([]);
549
+ });
550
+
551
+ it("reports a step failure no hook or leaf carries as ONE error event (steps: false)", async () => {
552
+ setLogger({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() });
553
+ install({ steps: false });
554
+ const wf = new AgentWorkflow(["Agent"], async (ctx, self) => {
555
+ await Promise.resolve(ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "Agent" }))).catch(() => undefined);
556
+ });
557
+ wf.runAgentStep = async () => {
558
+ throw new TypeError("bad state");
559
+ };
560
+ wf.runStream("q");
561
+ await wf.done;
562
+ const events = await flushed(spool);
563
+ expect(shape(events)).toEqual(["Agent agent_start", "Agent error", "Agent agent_end"]);
564
+ expect(events[1]).toMatchObject({ error_type: "TypeError", message: "bad state" });
565
+ expect(events[2]).toMatchObject({ outcome: "failed", summary: "TypeError: bad state" });
566
+ });
567
+
568
+ it("reports a runStream() that throws before any step ran", async () => {
569
+ install();
570
+ const wf = new AgentWorkflow(["Agent"], async () => {});
571
+ wf.workflow.createContext = () => {
572
+ throw new Error("No agents added to workflow");
573
+ };
574
+ expect(() => wf.runStream("q")).toThrow("No agents added to workflow");
575
+ const events = await flushed(spool);
576
+ expect(shape(events)).toEqual(["Agent agent_start", "Agent error", "Agent agent_end"]);
577
+ expect(events[2]!.outcome).toBe("failed");
578
+ });
579
+
580
+ it("records a tool that threw from the runtime's own tool-result event", async () => {
581
+ install();
582
+ let current: Ctx | null = null;
583
+ const wf = new AgentWorkflow(["Agent"], async (ctx, self) => {
584
+ current = ctx;
585
+ await ctx.step(self.executeToolCalls, ev("toolCalls", { agentName: "Agent" }));
586
+ ctx.send(ev("stop", { result: "ok" }));
587
+ });
588
+ wf.executeToolCalls = async () => {
589
+ current!.send(ev("toolCall", { toolName: "broken", toolId: "c1", toolKwargs: {} }));
590
+ // `callTool` catches the throw and dispatches no `llm-tool-result`.
591
+ current!.send(ev("toolResult", { toolId: "c1", toolOutput: { result: "Error: Error: Error: tool exploded", isError: true } }));
592
+ };
593
+ wf.runStream("q");
594
+ await wf.done;
595
+ const events = await flushed(spool);
596
+ const result = events.find((e) => e.type === "tool_result")!;
597
+ expect(result.error).toBe("Error: tool exploded");
598
+ expect(result.tool_call_id).toBe("c1");
599
+ expect(events.at(-1)!.outcome).toBe("success");
600
+ });
601
+
602
+ it.each([
603
+ // llamaindex 0.12's prettifyError writes `Error(<name>): <message>`, and the
604
+ // workflow prefixes `Error: ` again — recorded verbatim, it read
605
+ // "Error: Error(Error): unknown region: latam".
606
+ ["Error: Error(Error): unknown region: latam", "Error: unknown region: latam"],
607
+ ["Error: Error(TypeError): bad input", "TypeError: bad input"],
608
+ ["Error: Error: Error(RangeError): out of range", "RangeError: out of range"],
609
+ ])("records %j from a failed tool as %j", async (raw, expected) => {
610
+ install();
611
+ let current: Ctx | null = null;
612
+ const wf = new AgentWorkflow(["Agent"], async (ctx, self) => {
613
+ current = ctx;
614
+ await ctx.step(self.executeToolCalls, ev("toolCalls", { agentName: "Agent" }));
615
+ ctx.send(ev("stop", { result: "ok" }));
616
+ });
617
+ wf.executeToolCalls = async () => {
618
+ current!.send(ev("toolCall", { toolName: "broken", toolId: "c1", toolKwargs: {} }));
619
+ current!.send(ev("toolResult", { toolId: "c1", toolOutput: { result: raw, isError: true } }));
620
+ };
621
+ wf.runStream("q");
622
+ await wf.done;
623
+ const events = await flushed(spool);
624
+ expect(events.find((e) => e.type === "tool_result")!.error).toBe(expected);
625
+ });
626
+
627
+ it("keeps correlating under steps: false, emitting no hooks", async () => {
628
+ install({ steps: false });
629
+ const wf = new AgentWorkflow(["Agent"], async (ctx, self) => {
630
+ await ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "Agent" }));
631
+ ctx.send(ev("stop", { result: "ok" }));
632
+ });
633
+ wf.runAgentStep = async () => {
634
+ model("m1");
635
+ };
636
+ wf.runStream("q");
637
+ await wf.done;
638
+ expect(shape(await flushed(spool))).toEqual([
639
+ "Agent agent_start",
640
+ "Agent model_request",
641
+ "Agent model_response",
642
+ "Agent agent_end",
643
+ ]);
644
+ });
645
+
646
+ it("records the run but not its steps when the context has no call-context hook", async () => {
647
+ setLogger({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() });
648
+ compat.setStrictIntegrations(false);
649
+ install();
650
+ const wf = new AgentWorkflow(["Agent"], async (ctx) => {
651
+ ctx.send(ev("stop", { result: "ok" }));
652
+ });
653
+ wf.workflow.createContext = () => {
654
+ const ctx = fakeContext();
655
+ delete (ctx as Partial<Ctx>).__internal__call_context;
656
+ return ctx;
657
+ };
658
+ wf.runStream("q");
659
+ await wf.done;
660
+ expect(shape(await flushed(spool))).toEqual(["Agent agent_start", "Agent agent_end"]);
661
+ });
662
+
663
+ it("restores the prototype and cancels an open run on uninstall", async () => {
664
+ const proto = AgentWorkflow.prototype as unknown as Record<string, unknown>;
665
+ const original = proto.runStream;
666
+ install();
667
+ expect(proto.runStream).not.toBe(original);
668
+ const wf = new AgentWorkflow(["Agent"], async () => {});
669
+ wf.runStream("q");
670
+ adapter.uninstall();
671
+ expect(proto.runStream).toBe(original);
672
+ const events = await flushed(spool);
673
+ expect(shape(events)).toEqual(["Agent agent_start", "Agent agent_end"]);
674
+ expect(events[1]!.outcome).toBe("cancelled");
675
+ });
676
+
677
+ it("records nothing from a context that outlives uninstall()", async () => {
678
+ install();
679
+ let release!: () => void;
680
+ const gate = new Promise<void>((resolve) => (release = resolve));
681
+ let current: Ctx | null = null;
682
+ const wf = new AgentWorkflow(["Agent"], async (ctx, self) => {
683
+ current = ctx;
684
+ await gate;
685
+ ctx.send(ev("toolCall", { toolName: "late", toolId: "c9", toolKwargs: {} }));
686
+ await ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "Agent" }));
687
+ ctx.send(ev("stop", { result: "ok" }));
688
+ });
689
+ wf.runAgentStep = async () => {
690
+ model("m-late");
691
+ };
692
+ wf.runStream("q");
693
+ adapter.uninstall();
694
+ release();
695
+ await wf.done;
696
+ expect(current).not.toBeNull();
697
+ expect(shape(await flushed(spool))).toEqual(["Agent agent_start", "Agent agent_end"]);
698
+ });
699
+
700
+ it("leaves a workflow whose context it cannot reach unrecorded as a run, and says so", async () => {
701
+ const warn = vi.fn();
702
+ setLogger({ debug: vi.fn(), info: vi.fn(), warn, error: vi.fn() });
703
+ compat.setStrictIntegrations(false);
704
+ install();
705
+ const wf = new AgentWorkflow(["Agent"], async (ctx, self) => {
706
+ await ctx.step(self.runAgentStep, ev("setup"));
707
+ });
708
+ // A `createContext` on a prototype, not an own writable property.
709
+ const create = wf.workflow.createContext;
710
+ wf.workflow = Object.create({ createContext: create }) as AgentWorkflow["workflow"];
711
+ wf.runAgentStep = async () => {
712
+ model("m1");
713
+ };
714
+ wf.runStream("q");
715
+ await wf.done;
716
+ // No run that would never end — the model call is recorded as its own run.
717
+ expect(shape(await flushed(spool))).toEqual(["llm agent_start", "llm model_request", "llm model_response", "llm agent_end"]);
718
+ expect(warn).toHaveBeenCalled();
719
+ });
720
+
721
+ it("never changes what runStream returns or throws", () => {
722
+ install();
723
+ const wf = new AgentWorkflow(["Agent"], async () => {});
724
+ const failure = new Error("No agents added to workflow");
725
+ wf.workflow.createContext = () => {
726
+ throw failure;
727
+ };
728
+ expect(() => wf.runStream("q")).toThrow(failure);
729
+ // The createContext override is removed again, whatever happened.
730
+ expect(Object.getOwnPropertyDescriptor(wf.workflow, "createContext")!.value).toBeTypeOf("function");
731
+ });
732
+ });
733
+
734
+ /**
735
+ * LlamaIndex's own `EventCaller`, as `@llamaindex/core/global` builds it: a
736
+ * FRESH object per `@wrapEventCaller` invocation, chained to the invocation it
737
+ * ran inside. Two concurrent `engine.query()` calls share `caller` (the engine)
738
+ * and nothing else.
739
+ */
740
+ class EventCaller {
741
+ constructor(
742
+ readonly caller: unknown,
743
+ readonly parent: EventCaller | null = null,
744
+ ) {}
745
+ get computedCallers(): unknown[] {
746
+ const callers: unknown[] = [];
747
+ // eslint-disable-next-line @typescript-eslint/no-this-alias -- walking the chain from here
748
+ for (let node: EventCaller | null = this; node !== null; node = node.parent) callers.push(node.caller);
749
+ return callers;
750
+ }
751
+ }
752
+
753
+ /** Dispatch the way the real bus does: `reason` is the EventCaller bound at dispatch. */
754
+ function emitIn(event: string, detail: Event, reason: EventCaller | null): void {
755
+ bus.emitReason(event, detail, reason);
756
+ }
757
+
758
+ const bySession = (events: Event[]): Map<unknown, string[]> => {
759
+ const out = new Map<unknown, string[]>();
760
+ for (const e of events) out.set(e.session_id, [...(out.get(e.session_id) ?? []), shape([e])[0]!]);
761
+ return out;
762
+ };
763
+
764
+ class LLMAgentStandIn {
765
+ llm = { metadata: { model: "legacy-model" } };
766
+ }
767
+ class OpenAIStandIn {
768
+ metadata = { model: "gpt-x" };
769
+ }
770
+ class RetrieverQueryEngineStandIn {}
771
+
772
+ describe("concurrent runs on ONE shared object", () => {
773
+ const answer = (text: string) => ({ message: { content: text }, raw: { usage: { prompt_tokens: 1, completion_tokens: 1 } } });
774
+
775
+ it("keeps two interleaved queries on one engine in two sessions", async () => {
776
+ install();
777
+ const engine = new RetrieverQueryEngineStandIn();
778
+ const llm = new OpenAIStandIn();
779
+ // One engine, built once, serving two requests at the same time.
780
+ const a = new EventCaller(engine);
781
+ const b = new EventCaller(engine);
782
+ emitIn("query-start", { id: "qA", query: "question A" }, a);
783
+ emitIn("query-start", { id: "qB", query: "question B" }, b);
784
+ emitIn("retrieve-start", { id: "rB", query: "retrieval B" }, b);
785
+ emitIn("retrieve-start", { id: "rA", query: "retrieval A" }, a);
786
+ emitIn("retrieve-end", { id: "rA", nodes: [] }, a);
787
+ emitIn("retrieve-end", { id: "rB", nodes: [{ node: { id_: "b" } }] }, b);
788
+ emitIn("llm-start", { id: "mB", messages: [{ role: "user", content: "B" }] }, new EventCaller(llm, b));
789
+ emitIn("llm-start", { id: "mA", messages: [{ role: "user", content: "A" }] }, new EventCaller(llm, a));
790
+ emitIn("llm-end", { id: "mA", response: answer("answer A") }, new EventCaller(llm, a));
791
+ emitIn("llm-end", { id: "mB", response: answer("answer B") }, new EventCaller(llm, b));
792
+ emitIn("query-end", { id: "qB", response: { response: "answer B" } }, b);
793
+ emitIn("query-end", { id: "qA", response: { response: "answer A" } }, a);
794
+
795
+ const events = await flushed(spool);
796
+ const sessions = bySession(events);
797
+ expect(sessions.size).toBe(2);
798
+ const run = [
799
+ "RetrieverQueryEngineStandIn agent_start",
800
+ "RetrieverQueryEngineStandIn tool_use retriever",
801
+ "RetrieverQueryEngineStandIn tool_result retriever",
802
+ "RetrieverQueryEngineStandIn model_request",
803
+ "RetrieverQueryEngineStandIn model_response",
804
+ "RetrieverQueryEngineStandIn agent_end",
805
+ ];
806
+ for (const shapeOf of sessions.values()) expect(shapeOf).toEqual(run);
807
+ const sessionOf = (pred: (e: Event) => boolean) => events.find(pred)!.session_id;
808
+ const sa = sessionOf((e) => e.type === "agent_start" && e.goal === "question A");
809
+ const sb = sessionOf((e) => e.type === "agent_start" && e.goal === "question B");
810
+ expect(sa).not.toBe(sb);
811
+ expect(sessionOf((e) => e.type === "tool_use" && JSON.stringify(e.input).includes("retrieval A"))).toBe(sa);
812
+ expect(sessionOf((e) => e.type === "tool_use" && JSON.stringify(e.input).includes("retrieval B"))).toBe(sb);
813
+ expect(sessionOf((e) => e.type === "model_response" && e.content === "answer A")).toBe(sa);
814
+ expect(sessionOf((e) => e.type === "model_response" && e.content === "answer B")).toBe(sb);
815
+ expect(sessionOf((e) => e.type === "agent_end" && e.summary === "answer A")).toBe(sa);
816
+ expect(sessionOf((e) => e.type === "agent_end" && e.summary === "answer B")).toBe(sb);
817
+ for (const e of events.filter((x) => x.type === "agent_start")) expect(e.parent_id ?? null).toBeNull();
818
+ });
819
+
820
+ it("does not nest a second query under the first when the bus gives only the caller list", async () => {
821
+ // The reviewer's repro: no EventCaller chain, only `computedCallers`. A run
822
+ // that the SAME object owns cannot be told apart from a concurrent sibling
823
+ // there, and a sibling is the normal case — so it is never taken as the parent.
824
+ install();
825
+ const engine = new RetrieverQueryEngineStandIn();
826
+ bus.emit("query-start", { id: "qA", query: "question from user A" }, [engine]);
827
+ bus.emit("query-start", { id: "qB", query: "question from user B" }, [engine]);
828
+ bus.emit("query-end", { id: "qB", response: "answer B" }, [engine]);
829
+ bus.emit("query-end", { id: "qA", response: "answer A" }, [engine]);
830
+ const events = await flushed(spool);
831
+ const starts = events.filter((e) => e.type === "agent_start");
832
+ expect(starts.map((e) => e.goal)).toEqual(["question from user A", "question from user B"]);
833
+ expect(new Set(events.map((e) => e.session_id)).size).toBe(2);
834
+ const ends = events.filter((e) => e.type === "agent_end");
835
+ expect(ends.map((e) => e.summary)).toEqual(["answer B", "answer A"]);
836
+ expect(ends[0]!.session_id).toBe(starts[1]!.session_id);
837
+ expect(ends[1]!.session_id).toBe(starts[0]!.session_id);
838
+ });
839
+
840
+ it("keeps two interleaved legacy chats on one LLMAgent in two sessions", async () => {
841
+ install();
842
+ const runner = new LLMAgentStandIn();
843
+ const llm = new OpenAIStandIn();
844
+ const a = new EventCaller(runner);
845
+ const b = new EventCaller(runner);
846
+ const stepA = { id: "a1", prevStep: null, context: { store: { messages: [{ content: "goal A" }] } } };
847
+ const stepB = { id: "b1", prevStep: null, context: { store: { messages: [{ content: "goal B" }] } } };
848
+ emitIn("agent-start", { startStep: stepA }, a);
849
+ emitIn("agent-start", { startStep: stepB }, b);
850
+ // B is the newest run on the runner; A's calls must still go to A.
851
+ emitIn("llm-start", { id: "mA", messages: [] }, new EventCaller(llm, a));
852
+ emitIn("llm-end", { id: "mA", response: answer("") }, new EventCaller(llm, a));
853
+ emitIn("llm-tool-call", { toolCall: { id: "tA", name: "tool_a", input: {} } }, a);
854
+ emitIn("llm-start", { id: "mB", messages: [] }, new EventCaller(llm, b));
855
+ emitIn("llm-tool-result", { toolCall: { id: "tA" }, toolResult: { output: "ra", isError: false } }, a);
856
+ emitIn("llm-end", { id: "mB", response: answer("done B") }, new EventCaller(llm, b));
857
+ emitIn("agent-end", { endStep: stepB }, b);
858
+ emitIn("llm-start", { id: "mA2", messages: [] }, new EventCaller(llm, a));
859
+ emitIn("llm-end", { id: "mA2", response: answer("done A") }, new EventCaller(llm, a));
860
+ emitIn("agent-end", { endStep: stepA }, a);
861
+
862
+ const events = await flushed(spool);
863
+ const starts = events.filter((e) => e.type === "agent_start");
864
+ expect(starts.map((e) => e.goal)).toEqual(["goal A", "goal B"]);
865
+ for (const start of starts) expect(start.parent_id ?? null).toBeNull();
866
+ const [sa, sb] = starts.map((e) => e.session_id);
867
+ expect(sa).not.toBe(sb);
868
+ const sessions = bySession(events);
869
+ expect(sessions.get(sa)).toEqual([
870
+ "LLMAgentStandIn agent_start",
871
+ "LLMAgentStandIn model_request",
872
+ "LLMAgentStandIn model_response",
873
+ "LLMAgentStandIn tool_use tool_a",
874
+ "LLMAgentStandIn tool_result tool_a",
875
+ "LLMAgentStandIn model_request",
876
+ "LLMAgentStandIn model_response",
877
+ "LLMAgentStandIn agent_end",
878
+ ]);
879
+ expect(sessions.get(sb)).toEqual([
880
+ "LLMAgentStandIn agent_start",
881
+ "LLMAgentStandIn model_request",
882
+ "LLMAgentStandIn model_response",
883
+ "LLMAgentStandIn agent_end",
884
+ ]);
885
+ expect(events.find((e) => e.type === "agent_end" && e.session_id === sa)!.summary).toBe("done A");
886
+ expect(events.find((e) => e.type === "agent_end" && e.session_id === sb)!.summary).toBe("done B");
887
+ });
888
+
889
+ it("still nests a query INSIDE another invocation's chain", async () => {
890
+ // A sub-engine queried from inside an outer engine's query (SubQuestion-
891
+ // QueryEngine), and the SAME engine re-entered from inside its own query:
892
+ // both run inside the outer invocation, so neither opens a run.
893
+ install();
894
+ const outer = new RetrieverQueryEngineStandIn();
895
+ const inner = new RetrieverQueryEngineStandIn();
896
+ const top = new EventCaller(outer);
897
+ emitIn("query-start", { id: "q1", query: "outer" }, top);
898
+ const sub = new EventCaller(inner, top);
899
+ emitIn("query-start", { id: "q2", query: "inner" }, sub);
900
+ emitIn("retrieve-start", { id: "r2", query: "inner" }, sub);
901
+ emitIn("retrieve-end", { id: "r2", nodes: [] }, sub);
902
+ emitIn("query-end", { id: "q2", response: "inner answer" }, sub);
903
+ const again = new EventCaller(outer, top);
904
+ emitIn("query-start", { id: "q3", query: "again" }, again);
905
+ emitIn("query-end", { id: "q3", response: "again answer" }, again);
906
+ emitIn("query-end", { id: "q1", response: "outer answer" }, top);
907
+ const events = await flushed(spool);
908
+ expect(shape(events)).toEqual([
909
+ "RetrieverQueryEngineStandIn agent_start",
910
+ "RetrieverQueryEngineStandIn tool_use retriever",
911
+ "RetrieverQueryEngineStandIn tool_result retriever",
912
+ "RetrieverQueryEngineStandIn agent_end",
913
+ ]);
914
+ expect(new Set(events.map((e) => e.session_id)).size).toBe(1);
915
+ });
916
+
917
+ it("keeps two concurrent workflow runs of one shared agent apart", async () => {
918
+ install();
919
+ const gates: Array<() => void> = [];
920
+ const wait = () => new Promise<void>((resolve) => gates.push(resolve));
921
+ const wf = new AgentWorkflow(["Agent"], async (ctx, self) => {
922
+ const input = String(self.input);
923
+ await ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "Agent", q: input }));
924
+ ctx.send(ev("stop", { result: `answer ${input}` }));
925
+ });
926
+ wf.runAgentStep = async (_ctx, event) => {
927
+ const q = String((event as { data: { q: string } }).data.q);
928
+ await wait();
929
+ bus.emit("llm-start", { id: `m-${q}`, messages: [{ role: "user", content: q }] });
930
+ await wait();
931
+ bus.emit("llm-end", { id: `m-${q}`, response: answer(`reply ${q}`) });
932
+ };
933
+ wf.runStream("A");
934
+ const doneA = wf.done;
935
+ wf.runStream("B");
936
+ const doneB = wf.done;
937
+ // Interleave: each round releases the waiting calls newest first, so B's
938
+ // model call starts before A's.
939
+ for (let i = 0; i < 10; i += 1) {
940
+ await new Promise((resolve) => setTimeout(resolve, 0));
941
+ for (const release of gates.splice(0).reverse()) release();
942
+ }
943
+ await Promise.all([doneA, doneB]);
944
+ const events = await flushed(spool);
945
+ const starts = events.filter((e) => e.type === "agent_start");
946
+ expect(starts.map((e) => e.goal)).toEqual(["A", "B"]);
947
+ const [sa, sb] = starts.map((e) => e.session_id);
948
+ expect(sa).not.toBe(sb);
949
+ const requests = events.filter((e) => e.type === "model_request");
950
+ expect(requests.map((e) => e.session_id)).toEqual([sb, sa]);
951
+ const sessions = bySession(events);
952
+ const run = [
953
+ "Agent agent_start",
954
+ "Agent hook_triggered runAgentStep",
955
+ "Agent model_request",
956
+ "Agent model_response",
957
+ "Agent hook_completed runAgentStep",
958
+ "Agent agent_end",
959
+ ];
960
+ expect(sessions.get(sa)).toEqual(run);
961
+ expect(sessions.get(sb)).toEqual(run);
962
+ expect(events.find((e) => e.type === "model_response" && e.content === "reply A")!.session_id).toBe(sa);
963
+ expect(events.find((e) => e.type === "model_response" && e.content === "reply B")!.session_id).toBe(sb);
964
+ expect(events.find((e) => e.type === "agent_end" && e.session_id === sa)!.summary).toBe("answer A");
965
+ expect(events.find((e) => e.type === "agent_end" && e.session_id === sb)!.summary).toBe("answer B");
966
+ });
967
+ });
968
+
969
+ describe("attach()", () => {
970
+ it("refuses to replace an install that is still live, leaving it removable", () => {
971
+ install();
972
+ const live = bus;
973
+ const subscribed = live.count();
974
+ const proto = AgentWorkflow.prototype as unknown as Record<string, unknown>;
975
+ const patched = proto.runStream;
976
+ const second = new FakeBus();
977
+ expect(() =>
978
+ attach({ reaperInterval: 0 }, { globals: [{ Settings: { callbackManager: second } }], workflows: [workflowModule()] }),
979
+ ).toThrow(/already installed/);
980
+ // Nothing of the refused attach happened, and the live one is untouched.
981
+ expect(second.count()).toBe(0);
982
+ expect(live.count()).toBe(subscribed);
983
+ expect(proto.runStream).toBe(patched);
984
+ // So uninstall() still reaches every subscription and the patch.
985
+ adapter.uninstall();
986
+ expect(live.count()).toBe(0);
987
+ expect(proto.runStream).not.toBe(patched);
988
+ // And once removed, attaching again works.
989
+ install();
990
+ expect(bus.count()).toBe(subscribed);
991
+ });
992
+ });
993
+
994
+ describe("state after a run ends", () => {
995
+ it("leaves no residue after 20k completed runs of every kind", async () => {
996
+ // Every per-run entry — the adapter's own maps AND the tracker's links —
997
+ // must go when its run ends. A leaked link is not just memory: at the
998
+ // tracker's FIFO cap the next eviction takes a LIVE run's link, and its
999
+ // events drop.
1000
+ const original = runtime.event;
1001
+ runtime.event = new Proxy({}, { get: () => () => undefined }) as typeof runtime.event;
1002
+ try {
1003
+ const handle = install({ staleAfter: 60 });
1004
+ const engine = new RetrieverQueryEngineStandIn();
1005
+ const runner = new LLMAgentStandIn();
1006
+ const llm = new OpenAIStandIn();
1007
+ const N = 20_000;
1008
+ for (let i = 0; i < N; i += 1) {
1009
+ switch (i % 6) {
1010
+ case 0: {
1011
+ // A query, with its retrieval and model call.
1012
+ const q = new EventCaller(engine);
1013
+ emitIn("query-start", { id: `q${i}`, query: "q" }, q);
1014
+ emitIn("retrieve-start", { id: `r${i}`, query: "q" }, q);
1015
+ emitIn("retrieve-end", { id: `r${i}`, nodes: [] }, q);
1016
+ emitIn("llm-start", { id: `m${i}`, messages: [] }, new EventCaller(llm, q));
1017
+ emitIn("llm-end", { id: `m${i}`, response: {} }, new EventCaller(llm, q));
1018
+ emitIn("query-end", { id: `q${i}`, response: "a" }, q);
1019
+ break;
1020
+ }
1021
+ case 1: {
1022
+ // A two-step legacy task with a tool call.
1023
+ const c = new EventCaller(runner);
1024
+ const s1 = { id: `s${i}a`, prevStep: null };
1025
+ const s2 = { id: `s${i}b`, prevStep: s1 };
1026
+ emitIn("agent-start", { startStep: s1 }, c);
1027
+ emitIn("llm-tool-call", { toolCall: { id: `t${i}`, name: "t", input: {} } }, c);
1028
+ emitIn("llm-tool-result", { toolCall: { id: `t${i}` }, toolResult: { output: 1, isError: false } }, c);
1029
+ emitIn("agent-start", { startStep: s2 }, c);
1030
+ emitIn("agent-end", { endStep: s2 }, c);
1031
+ break;
1032
+ }
1033
+ case 2: {
1034
+ // A bare model call.
1035
+ bus.emit("llm-start", { id: `b${i}`, messages: [] }, [llm]);
1036
+ bus.emit("llm-end", { id: `b${i}`, response: {} }, [llm]);
1037
+ break;
1038
+ }
1039
+ case 3:
1040
+ case 4: {
1041
+ // A workflow run: steps, a model call, a handoff to a sub-agent —
1042
+ // once with hooks and once under steps: false's twin path (a step
1043
+ // that throws, so no hook_completed success).
1044
+ const fail = i % 6 === 4;
1045
+ const wf = new AgentWorkflow(["triage", "forecaster"], async (ctx, self) => {
1046
+ await ctx.step(self.handleInputStep, ev("start"));
1047
+ await ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "triage" }));
1048
+ await ctx.step(self.runAgentStep, ev("setup", { currentAgentName: "forecaster" }));
1049
+ ctx.send(ev("stop", { result: "ok" }));
1050
+ });
1051
+ wf.runAgentStep = async () => {
1052
+ bus.emit("llm-start", { id: `w${i}`, messages: [] });
1053
+ bus.emit("llm-end", { id: `w${i}`, response: {} });
1054
+ if (fail) throw new Error("step failed");
1055
+ };
1056
+ wf.runStream("q");
1057
+ await wf.done.catch(() => undefined);
1058
+ break;
1059
+ }
1060
+ default: {
1061
+ // A task that never ends on its own, with a model call left open:
1062
+ // the reaper closes both.
1063
+ const c = new EventCaller(runner);
1064
+ emitIn("agent-start", { startStep: { id: `o${i}`, prevStep: null } }, c);
1065
+ emitIn("llm-start", { id: `o${i}`, messages: [] }, new EventCaller(llm, c));
1066
+ handle.sweep(performance.now() + 61_000);
1067
+ }
1068
+ }
1069
+ }
1070
+ const residue = handle.residue();
1071
+ expect({ runs: residue.runs, leaves: residue.leaves, tasks: residue.tasks, queries: residue.queries }).toEqual({
1072
+ runs: 0,
1073
+ leaves: 0,
1074
+ tasks: 0,
1075
+ queries: 0,
1076
+ });
1077
+ expect(residue.tracker.openAgents()).toEqual([]);
1078
+ expect((residue.tracker as unknown as { links: Map<unknown, unknown> }).links.size).toBe(0);
1079
+ } finally {
1080
+ runtime.event = original;
1081
+ }
1082
+ }, 60_000);
1083
+ });
1084
+
1085
+ // ---------------------------------------------------------------------------
1086
+ // Invocation boundaries, retriever names and plain workflows
1087
+ // ---------------------------------------------------------------------------
1088
+
1089
+ /**
1090
+ * `flushed()`, made deterministic: `flushNow()` hands back a flush the
1091
+ * interval timer already started — one that drained the queue BEFORE the
1092
+ * newest events — so a single call can return without them. The second call
1093
+ * waits for a flush that started after it.
1094
+ */
1095
+ async function drained(): Promise<Array<Record<string, unknown>>> {
1096
+ await runtime.writer.flushNow();
1097
+ return flushed(spool);
1098
+ }
1099
+
1100
+ /**
1101
+ * `@llamaindex/core/global` as the adapter meets it: a REAL `AsyncLocalStorage`
1102
+ * of `EventCaller`s (module-private there too), `getEventCaller`, and the
1103
+ * `withEventCaller` every `@wrapEventCaller` method runs through.
1104
+ */
1105
+ function coreGlobal() {
1106
+ const storage = new AsyncLocalStorage<EventCaller>();
1107
+ const getEventCaller = (): EventCaller | null => storage.getStore() ?? null;
1108
+ const withEventCaller = <T>(caller: unknown, fn: () => T): T =>
1109
+ storage.run(new EventCaller(caller, getEventCaller()), fn);
1110
+ return { storage, getEventCaller, withEventCaller };
1111
+ }
1112
+
1113
+ let li: ReturnType<typeof coreGlobal>;
1114
+
1115
+ function installCore(
1116
+ options: Record<string, unknown> = {},
1117
+ extra: { retrievers?: RetrieverModule[]; asyncContexts?: AsyncContextModule[]; workflows?: WorkflowModule[] } = {},
1118
+ ) {
1119
+ bus = new FakeBus();
1120
+ li = coreGlobal();
1121
+ const globals: GlobalModule[] = [{ Settings: { callbackManager: bus }, getEventCaller: li.getEventCaller }];
1122
+ return attach({ reaperInterval: 0, ...options }, { workflows: [], ...extra, globals });
1123
+ }
1124
+
1125
+ /** Dispatch the way `CallbackManager.dispatchEvent` does: with the EventCaller bound NOW. */
1126
+ const dispatch = (event: string, detail: Event): void => bus.emitReason(event, detail, li.getEventCaller());
1127
+ const tick = () => new Promise<void>((resolve) => setTimeout(resolve, 1));
1128
+ /** A property read for an identity check, so a method is compared, never called. */
1129
+ const member = (target: object, key: string): unknown => (target as Record<string, unknown>)[key];
1130
+
1131
+ /** A first-party provider: `chat` decorated `@wrapEventCaller @wrapLLMEvent`. */
1132
+ class OpenAI {
1133
+ metadata = { model: "gpt-x" };
1134
+ constructor(private readonly reply = "It is sunny.") {}
1135
+ chat(options: { fail?: string; stream?: boolean } = {}): Promise<unknown> {
1136
+ return li.withEventCaller(this, async () => {
1137
+ const id = `m-${Math.random()}`;
1138
+ dispatch("llm-start", { id, messages: [{ role: "user", content: "weather?" }] });
1139
+ await tick();
1140
+ if (options.fail) throw new Error(options.fail);
1141
+ const usage = { prompt_tokens: 9, completion_tokens: 4 };
1142
+ if (options.stream) {
1143
+ const reply = this.reply;
1144
+ return (async function* () {
1145
+ yield { delta: reply };
1146
+ await tick();
1147
+ dispatch("llm-end", { id, response: { message: { content: reply }, raw: [{ raw: { usage } }] } });
1148
+ })();
1149
+ }
1150
+ dispatch("llm-end", { id, response: { message: { content: this.reply }, raw: { usage } } });
1151
+ return { message: { content: this.reply } };
1152
+ });
1153
+ }
1154
+ }
1155
+
1156
+ /** A chat engine: `chat` is `@wrapEventCaller` and dispatches nothing of its own. */
1157
+ class ContextChatEngine {
1158
+ constructor(private readonly llm: OpenAI) {}
1159
+ chat(options: { fail?: string; stream?: boolean; retrieveFails?: boolean } = {}): Promise<unknown> {
1160
+ return li.withEventCaller(this, async () => {
1161
+ const rid = `r-${Math.random()}`;
1162
+ dispatch("retrieve-start", { id: rid, query: { query: "weather?" } });
1163
+ await tick();
1164
+ dispatch("retrieve-end", { id: rid, nodes: [{ node: { id_: "n1", text: "Paris is sunny." }, score: 1 }] });
1165
+ const response = await this.llm.chat(options);
1166
+ return options.stream ? response : { message: { content: (response as { message: { content: string } }).message.content } };
1167
+ });
1168
+ }
1169
+ }
1170
+
1171
+ describe("invocation boundaries", () => {
1172
+ it("records a chat engine — no bus event of its own — as ONE run named after it", async () => {
1173
+ installCore();
1174
+ class SimpleChat {
1175
+ constructor(private readonly llm: OpenAI) {}
1176
+ chat(): Promise<unknown> {
1177
+ return li.withEventCaller(this, async () => {
1178
+ const r = rid();
1179
+ dispatch("retrieve-start", { id: r, query: { query: "weather?" } });
1180
+ dispatch("retrieve-end", { id: r, nodes: [] });
1181
+ await tick();
1182
+ return this.llm.chat();
1183
+ });
1184
+ }
1185
+ }
1186
+ let n = 0;
1187
+ const rid = () => `r${(n += 1)}`;
1188
+ const out = await new SimpleChat(new OpenAI()).chat();
1189
+ expect(out).toEqual({ message: { content: "It is sunny." } });
1190
+ const events = await drained();
1191
+ expect(shape(events)).toEqual([
1192
+ "SimpleChat agent_start",
1193
+ "SimpleChat tool_use retriever",
1194
+ "SimpleChat tool_result retriever",
1195
+ "SimpleChat model_request",
1196
+ "SimpleChat model_response",
1197
+ "SimpleChat agent_end",
1198
+ ]);
1199
+ expect(new Set(events.map((e) => e.session_id)).size).toBe(1);
1200
+ const end = events.at(-1)!;
1201
+ expect(end).toMatchObject({ outcome: "success", summary: "It is sunny." });
1202
+ expect(events.find((e) => e.type === "model_request")!.model).toBe("gpt-x");
1203
+ });
1204
+
1205
+ it("ends a streamed chat when its stream has been read, not when chat() returns", async () => {
1206
+ installCore();
1207
+ class StreamChat {
1208
+ constructor(private readonly llm: OpenAI) {}
1209
+ chat(): Promise<unknown> {
1210
+ return li.withEventCaller(this, () => this.llm.chat({ stream: true }));
1211
+ }
1212
+ }
1213
+ const stream = (await new StreamChat(new OpenAI()).chat()) as AsyncIterable<unknown>;
1214
+ let events = await drained();
1215
+ // chat() has returned; the model call is still being read.
1216
+ expect(events.filter((e) => e.type === "agent_end")).toEqual([]);
1217
+ for await (const chunk of stream) {
1218
+ void chunk;
1219
+ // drain
1220
+ }
1221
+ events = await drained();
1222
+ expect(shape(events)).toEqual([
1223
+ "StreamChat agent_start",
1224
+ "StreamChat model_request",
1225
+ "StreamChat model_response",
1226
+ "StreamChat agent_end",
1227
+ ]);
1228
+ expect([events[2]!.input_tokens, events[2]!.output_tokens]).toEqual([9, 4]);
1229
+ expect(events[3]).toMatchObject({ outcome: "success", summary: "It is sunny." });
1230
+ });
1231
+
1232
+ it("closes a model call that threw on its response, and fails the run it ended — reported once", async () => {
1233
+ // `wrapLLMEvent` has no error path: no `llm-end`. The invocation throwing
1234
+ // is the only signal, and it used to leave both open until the reaper.
1235
+ installCore();
1236
+ const engine = new ContextChatEngine(new OpenAI());
1237
+ await expect(engine.chat({ fail: "rate limited" })).rejects.toThrow("rate limited");
1238
+ const events = await drained();
1239
+ expect(shape(events)).toEqual([
1240
+ "ContextChatEngine agent_start",
1241
+ "ContextChatEngine tool_use retriever",
1242
+ "ContextChatEngine tool_result retriever",
1243
+ "ContextChatEngine model_request",
1244
+ "ContextChatEngine model_response",
1245
+ "ContextChatEngine agent_end",
1246
+ ]);
1247
+ expect(events.find((e) => e.type === "model_response")!.error).toBe("Error: rate limited");
1248
+ const end = events.at(-1)!;
1249
+ expect(end).toMatchObject({ outcome: "failed", summary: "Error: rate limited" });
1250
+ // The leaves carry the failure; an `error` event on top would count it twice.
1251
+ expect(events.filter((e) => e.type === "error")).toEqual([]);
1252
+ });
1253
+
1254
+ it("fails a bare model call that threw, instead of leaving it for the reaper", async () => {
1255
+ installCore();
1256
+ await expect(new OpenAI().chat({ fail: "boom" })).rejects.toThrow("boom");
1257
+ const events = await drained();
1258
+ expect(shape(events)).toEqual(["OpenAI agent_start", "OpenAI model_request", "OpenAI model_response", "OpenAI agent_end"]);
1259
+ expect(events[2]!.error).toBe("Error: boom");
1260
+ expect(events[3]!.outcome).toBe("failed");
1261
+ });
1262
+
1263
+ it("fails a legacy task whose step threw — it never sends agent-end — straight away", async () => {
1264
+ installCore();
1265
+ class LLMAgent {
1266
+ llm = { metadata: { model: "legacy-model" } };
1267
+ chat(): Promise<unknown> {
1268
+ return li.withEventCaller(this, async () => {
1269
+ dispatch("agent-start", { startStep: { id: "s1", prevStep: null } });
1270
+ await tick();
1271
+ throw new Error("step exploded");
1272
+ });
1273
+ }
1274
+ }
1275
+ await expect(new LLMAgent().chat()).rejects.toThrow("step exploded");
1276
+ const events = await drained();
1277
+ expect(shape(events)).toEqual(["LLMAgent agent_start", "LLMAgent error", "LLMAgent agent_end"]);
1278
+ expect(events[1]).toMatchObject({ error_type: "Error", message: "step exploded" });
1279
+ expect(events[2]).toMatchObject({ outcome: "failed", summary: "Error: step exploded" });
1280
+ });
1281
+
1282
+ it("roots a query made first inside another invocation at that invocation", async () => {
1283
+ // CondenseQuestionChatEngine's shape, when its first act is the query.
1284
+ installCore();
1285
+ class QueryEngine {
1286
+ query(): Promise<unknown> {
1287
+ return li.withEventCaller(this, async () => {
1288
+ dispatch("query-start", { id: "q1", query: "weather?" });
1289
+ await tick();
1290
+ dispatch("query-end", { id: "q1", response: { message: { content: "notes" } } });
1291
+ return "notes";
1292
+ });
1293
+ }
1294
+ }
1295
+ class CondenseChat {
1296
+ chat(): Promise<unknown> {
1297
+ return li.withEventCaller(this, async () => {
1298
+ await new QueryEngine().query();
1299
+ return new OpenAI("final").chat();
1300
+ });
1301
+ }
1302
+ }
1303
+ await new CondenseChat().chat();
1304
+ const events = await drained();
1305
+ expect(shape(events)).toEqual([
1306
+ "CondenseChat agent_start",
1307
+ "CondenseChat model_request",
1308
+ "CondenseChat model_response",
1309
+ "CondenseChat agent_end",
1310
+ ]);
1311
+ expect(events.at(-1)!.summary).toBe("final");
1312
+ });
1313
+
1314
+ it("keeps concurrent chats on ONE shared engine in separate sessions", async () => {
1315
+ installCore();
1316
+ const engine = new ContextChatEngine(new OpenAI());
1317
+ await Promise.all(Array.from({ length: 10 }, () => engine.chat()));
1318
+ const events = await drained();
1319
+ const sessions = bySession(events);
1320
+ expect(sessions.size).toBe(10);
1321
+ for (const list of sessions.values()) {
1322
+ expect(list).toEqual([
1323
+ "ContextChatEngine agent_start",
1324
+ "ContextChatEngine tool_use retriever",
1325
+ "ContextChatEngine tool_result retriever",
1326
+ "ContextChatEngine model_request",
1327
+ "ContextChatEngine model_response",
1328
+ "ContextChatEngine agent_end",
1329
+ ]);
1330
+ }
1331
+ });
1332
+
1333
+ it("never changes what an invocation returns or throws", async () => {
1334
+ installCore();
1335
+ const value = { answer: 42 };
1336
+ expect(li.withEventCaller({}, () => value)).toBe(value);
1337
+ const iterable = (async function* () {
1338
+ yield 1;
1339
+ })();
1340
+ expect(li.withEventCaller({}, () => iterable)).toBe(iterable);
1341
+ await expect(li.withEventCaller({}, async () => value)).resolves.toBe(value);
1342
+ const error = new Error("nope");
1343
+ await expect(li.withEventCaller({}, () => Promise.reject(error))).rejects.toBe(error);
1344
+ expect(() =>
1345
+ li.withEventCaller({}, () => {
1346
+ throw error;
1347
+ }),
1348
+ ).toThrow(error);
1349
+ // A thenable that is not a native promise is handed back untouched.
1350
+ const thenable = { then: (resolve: (v: number) => void) => resolve(7) };
1351
+ expect(li.withEventCaller({}, () => thenable)).toBe(thenable);
1352
+ // And the EventCaller is still the one the callback sees.
1353
+ expect(li.withEventCaller("me", () => li.getEventCaller()!.caller)).toBe("me");
1354
+ });
1355
+
1356
+ it("still reports a rejection nobody handles, as the application would have seen it", async () => {
1357
+ installCore();
1358
+ const seen: unknown[] = [];
1359
+ const onUnhandled = (reason: unknown) => seen.push(reason);
1360
+ process.on("unhandledRejection", onUnhandled);
1361
+ try {
1362
+ const error = new Error("nobody catches me");
1363
+ void li.withEventCaller({}, () => Promise.reject(error));
1364
+ await new Promise((resolve) => setTimeout(resolve, 20));
1365
+ expect(seen).toContain(error);
1366
+ } finally {
1367
+ process.off("unhandledRejection", onUnhandled);
1368
+ }
1369
+ });
1370
+
1371
+ it("finds LlamaIndex's storage, touches nothing else, and restores it on uninstall", () => {
1372
+ const g = coreGlobal();
1373
+ const getStore = member(AsyncLocalStorage.prototype, "getStore");
1374
+ expect(eventCallerStorage({ getEventCaller: g.getEventCaller })).toBe(g.storage);
1375
+ expect(member(AsyncLocalStorage.prototype, "getStore")).toBe(getStore);
1376
+ expect(eventCallerStorage({})).toBeNull();
1377
+ expect(eventCallerStorage({ getEventCaller: () => null })).toBeNull();
1378
+
1379
+ installCore();
1380
+ const protoRun = member(AsyncLocalStorage.prototype, "run");
1381
+ expect(Object.prototype.hasOwnProperty.call(li.storage, "run")).toBe(true);
1382
+ expect(member(li.storage, "run")).not.toBe(protoRun);
1383
+ expect(member(new AsyncLocalStorage(), "run")).toBe(protoRun);
1384
+ adapter.uninstall();
1385
+ expect(member(li.storage, "run")).toBe(protoRun);
1386
+ // Uninstalled: an invocation opens nothing.
1387
+ return new OpenAI().chat().then(async () => {
1388
+ expect(await drained()).toEqual([]);
1389
+ });
1390
+ });
1391
+
1392
+ it("falls back to a bare run for an invocation it never saw start", async () => {
1393
+ // A chain built outside the hooked storage (an invocation already running
1394
+ // at instrument() time): no end signal, so the one-leaf rule.
1395
+ installCore();
1396
+ const engine = new RetrieverQueryEngineStandIn();
1397
+ const outer = new EventCaller(engine);
1398
+ emitIn("llm-start", { id: "m1", messages: [] }, new EventCaller(new OpenAIStandIn(), outer));
1399
+ emitIn("llm-end", { id: "m1", response: { message: { content: "x" } } }, new EventCaller(new OpenAIStandIn(), outer));
1400
+ const events = await drained();
1401
+ expect(shape(events)).toEqual([
1402
+ "OpenAIStandIn agent_start",
1403
+ "OpenAIStandIn model_request",
1404
+ "OpenAIStandIn model_response",
1405
+ "OpenAIStandIn agent_end",
1406
+ ]);
1407
+ });
1408
+ });
1409
+
1410
+ describe("retrieval names", () => {
1411
+ /** `@llamaindex/core/retriever`: `retrieve()` dispatches, `_retrieve()` is the subclass's. */
1412
+ class BaseRetriever {
1413
+ async retrieve(query: string): Promise<unknown[]> {
1414
+ const id = `r-${query}`;
1415
+ dispatch("retrieve-start", { id, query: { query } });
1416
+ const nodes = await this._retrieve(query);
1417
+ dispatch("retrieve-end", { id, nodes });
1418
+ return nodes;
1419
+ }
1420
+ async _retrieve(query: string): Promise<unknown[]> {
1421
+ void query;
1422
+ return [];
1423
+ }
1424
+ }
1425
+ class VectorIndexRetriever extends BaseRetriever {
1426
+ override async _retrieve(query: string): Promise<unknown[]> {
1427
+ await tick();
1428
+ return [{ node: { id_: "n1", text: `notes on ${query}` }, score: 0.5 }];
1429
+ }
1430
+ }
1431
+
1432
+ it("names a retrieval after the retriever's class, as Python does — even one built earlier", async () => {
1433
+ const retriever = new VectorIndexRetriever();
1434
+ const original = member(BaseRetriever.prototype, "retrieve");
1435
+ installCore({}, { retrievers: [{ BaseRetriever }] });
1436
+ expect(member(BaseRetriever.prototype, "retrieve")).not.toBe(original);
1437
+ await retriever.retrieve("Paris");
1438
+ const events = await drained();
1439
+ expect(shape(events)).toEqual([
1440
+ "VectorIndexRetriever agent_start",
1441
+ "VectorIndexRetriever tool_use VectorIndexRetriever",
1442
+ "VectorIndexRetriever tool_result VectorIndexRetriever",
1443
+ "VectorIndexRetriever agent_end",
1444
+ ]);
1445
+ expect(events[1]!.input).toEqual({ query: "Paris" });
1446
+ expect(events[2]!.output).toEqual({ num_nodes: 1, top: [{ id: "n1", score: 0.5, text: "notes on Paris" }] });
1447
+ adapter.uninstall();
1448
+ expect(member(BaseRetriever.prototype, "retrieve")).toBe(original);
1449
+ });
1450
+
1451
+ it("keeps each concurrent retrieval's own name", async () => {
1452
+ class KeywordRetriever extends BaseRetriever {}
1453
+ installCore({}, { retrievers: [{ BaseRetriever }] });
1454
+ await Promise.all([new VectorIndexRetriever().retrieve("a"), new KeywordRetriever().retrieve("b")]);
1455
+ const events = await drained();
1456
+ const uses = events.filter((e) => e.type === "tool_use").map((e) => [e.tool_name, (e.input as { query: string }).query]);
1457
+ expect(uses.sort()).toEqual([
1458
+ ["KeywordRetriever", "b"],
1459
+ ["VectorIndexRetriever", "a"],
1460
+ ]);
1461
+ });
1462
+ });
1463
+
1464
+ /**
1465
+ * workflow-core ≥1.1, reduced to what the adapter meets: an exported
1466
+ * `AsyncContext.Variable` class, and a runtime that runs each step handler as
1467
+ * `handlerVariable.run(handlerContext, …)` and sends the handler's output on
1468
+ * once its promise settles — through the runtime's OWN `.then`, as the real
1469
+ * one does, which is why a run cannot end the moment a step does.
1470
+ */
1471
+ function workflowCore() {
1472
+ class Variable {
1473
+ private readonly als = new AsyncLocalStorage<unknown>();
1474
+ run<T>(value: unknown, fn: () => T): T {
1475
+ return this.als.run(value, fn);
1476
+ }
1477
+ }
1478
+ const module: AsyncContextModule = { AsyncContext: { Variable } };
1479
+ type Ev = { kind: string; data?: unknown };
1480
+ const createWorkflow = () => {
1481
+ const listeners = new Map<string, (ctx: unknown, event: Ev) => unknown>();
1482
+ return {
1483
+ handle(kind: string, handler: (ctx: unknown, event: Ev) => unknown) {
1484
+ listeners.set(kind, handler);
1485
+ },
1486
+ createContext() {
1487
+ const handlerVariable = new Variable();
1488
+ const sent: Ev[] = [];
1489
+ const root: Record<string, unknown> = { handler: null, inputs: [], outputs: [], prev: null, next: new Set() };
1490
+ const send = (event: Ev, parent: Record<string, unknown>): void => {
1491
+ sent.push(event);
1492
+ const handler = listeners.get(event.kind);
1493
+ if (!handler) return;
1494
+ const hc: Record<string, unknown> = {
1495
+ handler,
1496
+ inputs: [event],
1497
+ outputs: [],
1498
+ prev: parent,
1499
+ next: new Set(),
1500
+ pending: null,
1501
+ get root() {
1502
+ return root;
1503
+ },
1504
+ };
1505
+ (parent.next as Set<unknown>).add(hc);
1506
+ handlerVariable.run(hc, () => {
1507
+ const result = (hc.handler as (c: unknown, e: Ev) => unknown)({}, event);
1508
+ if (result instanceof Promise) {
1509
+ hc.pending = result.then((out: Ev | undefined) => {
1510
+ if (out) send(out, hc);
1511
+ return out;
1512
+ });
1513
+ } else if (result) {
1514
+ send(result as Ev, hc);
1515
+ }
1516
+ });
1517
+ };
1518
+ return {
1519
+ sendEvent: (event: Ev) => send(event, root),
1520
+ sent,
1521
+ async until(kind: string): Promise<void> {
1522
+ while (!sent.some((e) => e.kind === kind)) await new Promise((resolve) => setImmediate(resolve));
1523
+ },
1524
+ };
1525
+ },
1526
+ };
1527
+ };
1528
+ return { module, Variable, createWorkflow };
1529
+ }
1530
+
1531
+ describe("plain workflows", () => {
1532
+ const twoSteps = (wc: ReturnType<typeof workflowCore>, fail = false) => {
1533
+ const wf = wc.createWorkflow();
1534
+ wf.handle("start", async function research(_ctx, event) {
1535
+ await tick();
1536
+ return { kind: "researched", data: `notes on ${String(event.data)}` };
1537
+ });
1538
+ wf.handle("researched", async function answer() {
1539
+ await new OpenAI("It is sunny in Paris.").chat();
1540
+ if (fail) throw new Error("answer failed");
1541
+ return { kind: "stop", data: "It is sunny in Paris." };
1542
+ });
1543
+ return wf;
1544
+ };
1545
+
1546
+ it("records a createWorkflow() run as an agent with its steps as hooks", async () => {
1547
+ const wc = workflowCore();
1548
+ installCore({}, { asyncContexts: [wc.module] });
1549
+ const ctx = twoSteps(wc).createContext();
1550
+ ctx.sendEvent({ kind: "start", data: "weather in Paris?" });
1551
+ await ctx.until("stop");
1552
+ const events = await drained();
1553
+ expect(shape(events)).toEqual([
1554
+ "Workflow agent_start",
1555
+ "Workflow hook_triggered research",
1556
+ "Workflow hook_completed research",
1557
+ "Workflow hook_triggered answer",
1558
+ "Workflow model_request",
1559
+ "Workflow model_response",
1560
+ "Workflow hook_completed answer",
1561
+ "Workflow agent_end",
1562
+ ]);
1563
+ expect(events[0]!.goal).toBe("weather in Paris?");
1564
+ expect(events.find((e) => e.type === "hook_triggered")!.trigger_event).toBe("workflow_step");
1565
+ expect(events.at(-1)).toMatchObject({ outcome: "success", summary: "It is sunny in Paris." });
1566
+ expect(new Set(events.map((e) => e.session_id)).size).toBe(1);
1567
+ });
1568
+
1569
+ it("ends the run before the code awaiting it goes on, inside an agent() scope", async () => {
1570
+ const wc = workflowCore();
1571
+ installCore({}, { asyncContexts: [wc.module] });
1572
+ await agentScope("forecast_flow", async () => {
1573
+ const ctx = twoSteps(wc).createContext();
1574
+ ctx.sendEvent({ kind: "start", data: "q" });
1575
+ await ctx.until("stop");
1576
+ });
1577
+ const events = await drained();
1578
+ expect(shape(events).filter((s) => s.includes("agent_"))).toEqual([
1579
+ "forecast_flow agent_start",
1580
+ "Workflow agent_start",
1581
+ "Workflow agent_end",
1582
+ "forecast_flow agent_end",
1583
+ ]);
1584
+ expect(events.find((e) => e.agent_id === "Workflow" && e.type === "agent_start")!.parent_id).toBe("forecast_flow");
1585
+ });
1586
+
1587
+ it("fails the step and the run when a step throws", async () => {
1588
+ const wc = workflowCore();
1589
+ installCore({}, { asyncContexts: [wc.module] });
1590
+ const ctx = twoSteps(wc, true).createContext();
1591
+ const onUnhandled = () => undefined;
1592
+ process.on("unhandledRejection", onUnhandled);
1593
+ try {
1594
+ ctx.sendEvent({ kind: "start", data: "q" });
1595
+ await new Promise((resolve) => setTimeout(resolve, 30));
1596
+ } finally {
1597
+ process.off("unhandledRejection", onUnhandled);
1598
+ }
1599
+ const events = await drained();
1600
+ const failed = events.find((e) => e.type === "hook_completed" && e.hook_name === "answer")!;
1601
+ expect(failed).toMatchObject({ outcome: "failed", error: "Error: answer failed" });
1602
+ expect(events.at(-1)).toMatchObject({ type: "agent_end", outcome: "failed" });
1603
+ expect(events.filter((e) => e.type === "error")).toEqual([]);
1604
+ });
1605
+
1606
+ it("keeps two concurrent contexts of one workflow in two sessions", async () => {
1607
+ const wc = workflowCore();
1608
+ installCore({}, { asyncContexts: [wc.module] });
1609
+ const wf = twoSteps(wc);
1610
+ const a = wf.createContext();
1611
+ const b = wf.createContext();
1612
+ a.sendEvent({ kind: "start", data: "A" });
1613
+ b.sendEvent({ kind: "start", data: "B" });
1614
+ await Promise.all([a.until("stop"), b.until("stop")]);
1615
+ const events = await drained();
1616
+ const sessions = bySession(events);
1617
+ expect(sessions.size).toBe(2);
1618
+ for (const list of sessions.values()) expect(list[0]).toBe("Workflow agent_start");
1619
+ expect(events.filter((e) => e.type === "agent_start").map((e) => e.goal).sort()).toEqual(["A", "B"]);
1620
+ });
1621
+
1622
+ it("records a later burst (an event sent from outside) as a new run", async () => {
1623
+ // Documented: a plain workflow has no end of its own, so idle is the end.
1624
+ const wc = workflowCore();
1625
+ installCore({}, { asyncContexts: [wc.module] });
1626
+ const wf = wc.createWorkflow();
1627
+ wf.handle("ask", async function ask(_ctx, event) {
1628
+ await tick();
1629
+ return { kind: "asked", data: event.data };
1630
+ });
1631
+ const ctx = wf.createContext();
1632
+ ctx.sendEvent({ kind: "ask", data: "first" });
1633
+ await ctx.until("asked");
1634
+ await tick();
1635
+ ctx.sendEvent({ kind: "ask", data: "second" });
1636
+ await new Promise((resolve) => setTimeout(resolve, 10));
1637
+ const events = await drained();
1638
+ expect(events.filter((e) => e.type === "agent_start").map((e) => e.goal)).toEqual(["first", "second"]);
1639
+ expect(events.filter((e) => e.type === "agent_end")).toHaveLength(2);
1640
+ });
1641
+
1642
+ it("leaves an AgentWorkflow's steps to the agent path, and any other value alone", async () => {
1643
+ const wc = workflowCore();
1644
+ installCore({}, { asyncContexts: [wc.module], workflows: [workflowModule()] });
1645
+ const agentWf = new AgentWorkflow(["Agent"], async () => {});
1646
+ agentWf.runStream("q");
1647
+ const wf = wc.createWorkflow();
1648
+ wf.handle("start", agentWf.runAgentStep);
1649
+ const ctx = wf.createContext();
1650
+ ctx.sendEvent({ kind: "start" });
1651
+ await tick();
1652
+ const variable = new wc.Variable();
1653
+ expect(variable.run({ not: "a handler context" }, () => 5)).toBe(5);
1654
+ const events = await drained();
1655
+ expect(events.filter((e) => e.agent_id === "Workflow")).toEqual([]);
1656
+ });
1657
+
1658
+ it("restores the Variable prototype on uninstall", () => {
1659
+ const wc = workflowCore();
1660
+ const original = (wc.Variable.prototype as unknown as Record<string, unknown>).run;
1661
+ installCore({}, { asyncContexts: [wc.module] });
1662
+ expect((wc.Variable.prototype as unknown as Record<string, unknown>).run).not.toBe(original);
1663
+ adapter.uninstall();
1664
+ expect((wc.Variable.prototype as unknown as Record<string, unknown>).run).toBe(original);
1665
+ });
1666
+ });
1667
+
1668
+ describe("@llamaindex/openai streaming usage", () => {
1669
+ it("records usage from the final chunk that stream_options.include_usage adds", async () => {
1670
+ // @llamaindex/openai (0.1.61 – 0.4.23) streams chat completions and, when
1671
+ // the request carries `stream_options: {include_usage: true}` (set it with
1672
+ // `new OpenAI({additionalChatOptions: {stream_options: {include_usage: true}}})`),
1673
+ // yields OpenAI's content-less usage chunk as `{raw: part, delta: ""}` —
1674
+ // no `options`. Without that option OpenAI never sends it, and there is no
1675
+ // number to record. These are the exact chunks it yields.
1676
+ installCore();
1677
+ const part = (choices: unknown[], usage: unknown = null) => ({
1678
+ id: "chatcmpl-1",
1679
+ object: "chat.completion.chunk",
1680
+ model: "gpt-4o-mini",
1681
+ choices,
1682
+ usage,
1683
+ });
1684
+ const chunks = [
1685
+ { raw: part([{ index: 0, delta: { role: "assistant", content: "It is" }, finish_reason: null }]), options: {}, delta: "It is" },
1686
+ { raw: part([{ index: 0, delta: { content: " sunny." }, finish_reason: null }]), options: {}, delta: " sunny." },
1687
+ { raw: part([{ index: 0, delta: {}, finish_reason: "stop" }]), options: {}, delta: "" },
1688
+ {
1689
+ raw: part([], {
1690
+ prompt_tokens: 21,
1691
+ completion_tokens: 4,
1692
+ total_tokens: 25,
1693
+ prompt_tokens_details: { cached_tokens: 0 },
1694
+ }),
1695
+ delta: "",
1696
+ },
1697
+ ];
1698
+ const llm = new OpenAIStandIn();
1699
+ // What `wrapLLMEvent` hands `llm-end` for a stream: every chunk as `raw`.
1700
+ await li.withEventCaller(llm, async () => {
1701
+ dispatch("llm-start", { id: "s1", messages: [{ role: "user", content: "weather?" }] });
1702
+ for (const chunk of chunks) dispatch("llm-stream", { id: "s1", chunk });
1703
+ dispatch("llm-end", { id: "s1", response: { message: { content: "It is sunny.", role: "assistant", options: {} }, raw: chunks } });
1704
+ });
1705
+ const events = await drained();
1706
+ const response = events.find((e) => e.type === "model_response")!;
1707
+ expect([response.input_tokens, response.output_tokens]).toEqual([21, 4]);
1708
+ expect(response.usage).toMatchObject({ prompt_tokens: 21, completion_tokens: 4, total_tokens: 25 });
1709
+ expect(response.stop_reason).toBe("stop");
1710
+ expect(response.fw_chunks).toBe(4);
1711
+ });
1712
+
1713
+ it("records no token counts — and invents none — for the same stream without include_usage", async () => {
1714
+ installCore();
1715
+ const chunks = [
1716
+ { raw: { choices: [{ delta: { content: "hi" }, finish_reason: null }] }, options: {}, delta: "hi" },
1717
+ { raw: { choices: [{ delta: {}, finish_reason: "stop" }] }, options: {}, delta: "" },
1718
+ ];
1719
+ bus.emit("llm-start", { id: "s2", messages: [] }, [new OpenAIStandIn()]);
1720
+ bus.emit("llm-end", { id: "s2", response: { message: { content: "hi" }, raw: chunks } }, [new OpenAIStandIn()]);
1721
+ const response = (await drained()).find((e) => e.type === "model_response")!;
1722
+ expect(response.input_tokens).toBeUndefined();
1723
+ expect(response.output_tokens).toBeUndefined();
1724
+ expect(response.stop_reason).toBe("stop");
1725
+ });
1726
+ });
1727
+
1728
+ describe("state after invocation runs and plain workflows end", () => {
1729
+ it("leaves no residue after thousands of chat and workflow runs", async () => {
1730
+ const original = runtime.event;
1731
+ runtime.event = new Proxy({}, { get: () => () => undefined }) as typeof runtime.event;
1732
+ try {
1733
+ const wc = workflowCore();
1734
+ const handle = installCore({}, { asyncContexts: [wc.module] });
1735
+ const engine = new ContextChatEngine(new OpenAI());
1736
+ const wf = wc.createWorkflow();
1737
+ wf.handle("start", async function step() {
1738
+ await new OpenAI().chat();
1739
+ return { kind: "stop" };
1740
+ });
1741
+ for (let i = 0; i < 300; i += 1) {
1742
+ const ctx = wf.createContext();
1743
+ ctx.sendEvent({ kind: "start" });
1744
+ await Promise.all([engine.chat(), engine.chat({ stream: true }).then(async (s) => {
1745
+ for await (const chunk of s as AsyncIterable<unknown>) {
1746
+ void chunk;
1747
+ // drain
1748
+ }
1749
+ }), engine.chat({ fail: "x" }).catch(() => undefined), ctx.until("stop")]);
1750
+ }
1751
+ await new Promise((resolve) => setTimeout(resolve, 10));
1752
+ const residue = handle.residue();
1753
+ expect({ runs: residue.runs, leaves: residue.leaves }).toEqual({ runs: 0, leaves: 0 });
1754
+ expect(residue.tracker.openAgents()).toEqual([]);
1755
+ expect((residue.tracker as unknown as { links: Map<unknown, unknown> }).links.size).toBe(0);
1756
+ } finally {
1757
+ runtime.event = original;
1758
+ }
1759
+ }, 60_000);
1760
+ });