failproofai 1.0.7-beta.1 → 1.0.7-beta.2

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 (324) 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 +3 -3
  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.nft.json +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  10. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  11. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  18. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  21. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  30. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  32. package/.next/standalone/.next/server/app/index.html +1 -1
  33. package/.next/standalone/.next/server/app/index.rsc +15 -15
  34. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  35. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  36. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  37. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  38. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
  41. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  44. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  45. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  47. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  48. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  51. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  52. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  53. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +7 -7
  54. package/.next/standalone/.next/server/app/settings/page.js +2 -2
  55. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  57. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0l3yhx4._.js +2 -2
  58. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
  59. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
  60. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
  61. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
  62. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  63. package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +2 -2
  65. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +2 -2
  66. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yxwl6j._.js → [root-of-the-server]__0l44ual._.js} +2 -2
  67. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +2 -2
  68. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +2 -2
  69. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +2 -2
  70. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +2 -2
  71. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +2 -2
  72. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +2 -2
  73. package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +1 -1
  74. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +1 -1
  76. package/.next/standalone/.next/server/chunks/ssr/{_214wgrp._.js → _1mel6y1._.js} +2 -2
  77. package/.next/standalone/.next/server/chunks/ssr/{_0bn2oo8._.js → _1v-jvrv._.js} +1 -1
  78. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  79. package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
  80. package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +1 -1
  81. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  82. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  83. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  84. package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +1 -1
  86. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +1 -1
  87. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  89. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  90. package/.next/standalone/.next/server/pages/404.html +1 -1
  91. package/.next/standalone/.next/server/pages/500.html +1 -1
  92. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  93. package/.next/standalone/.next/server/server-reference-manifest.json +22 -22
  94. package/.next/standalone/.next/static/chunks/043j99m8ykg__.css +2 -0
  95. package/.next/standalone/.next/static/chunks/0fqd7m_u81mi5.js +1 -0
  96. package/.next/standalone/.next/static/chunks/129ag2bw93bdh.js +1 -0
  97. package/.next/standalone/.next/static/chunks/{29fql3nbnfc9q.js → 1qd741hzlmjbo.js} +1 -1
  98. package/.next/standalone/.next/static/chunks/{0vmd180qfntfb.js → 2_pltstd8-xgs.js} +1 -1
  99. package/.next/standalone/.next/static/chunks/{1u5zsejmgrir_.js → 2c8j9l6j_b1ci.js} +1 -1
  100. package/.next/standalone/.next/static/chunks/3-k569wzcli8q.js +1 -0
  101. package/.next/standalone/.next/static/chunks/{258668t68du6b.js → 3brze37td_wnc.js} +1 -1
  102. package/.next/standalone/.next/static/chunks/3otmypm6j_xfo.js +6 -0
  103. package/.next/standalone/.next/static/chunks/{12tvm75t5ffui.js → 3ugmd_7dyn0id.js} +1 -1
  104. package/.next/standalone/.next/static/chunks/3yxro_r2_o9ad.js +69 -0
  105. package/.next/standalone/PROBE-FOLLOWUP.md +186 -0
  106. package/.next/standalone/app/actions/get-jev-config.ts +38 -23
  107. package/.next/standalone/app/actions/update-jev-config.ts +1 -1
  108. package/.next/standalone/app/settings/jev-panel.tsx +33 -29
  109. package/.next/standalone/package.json +10 -10
  110. package/.next/standalone/sdk/python/skill/SKILL.md +60 -14
  111. package/.next/standalone/sdk/python/skill/agents/openai.yaml +2 -1
  112. package/.next/standalone/sdk/python/skill/references/evaluator.md +255 -0
  113. package/.next/standalone/sdk/python/skill/references/events.md +17 -8
  114. package/.next/standalone/sdk/python/skill/references/frameworks.md +3 -0
  115. package/.next/standalone/sdk/python/skill/references/install.md +3 -0
  116. package/.next/standalone/sdk/python/skill/references/integration.md +6 -2
  117. package/.next/standalone/sdk/python/skill/references/typescript.md +568 -0
  118. package/.next/standalone/sdk/typescript/CHANGELOG.md +119 -0
  119. package/.next/standalone/sdk/typescript/LICENSE +42 -0
  120. package/.next/standalone/sdk/typescript/README.md +552 -0
  121. package/.next/standalone/sdk/typescript/eslint.config.mjs +59 -0
  122. package/.next/standalone/sdk/typescript/examples/research-agent.ts +197 -0
  123. package/.next/standalone/sdk/typescript/integration/ai.test.ts +920 -0
  124. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/agent.ts +337 -0
  125. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +281 -0
  126. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +13 -0
  127. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/surfaces.ts +605 -0
  128. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.json +12 -0
  129. package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.surfaces.json +4 -0
  130. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/agent.ts +342 -0
  131. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +168 -0
  132. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +13 -0
  133. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/surfaces.ts +628 -0
  134. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.json +12 -0
  135. package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.surfaces.json +4 -0
  136. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/agent.ts +346 -0
  137. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +156 -0
  138. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +13 -0
  139. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/surfaces.ts +651 -0
  140. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.json +12 -0
  141. package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.surfaces.json +4 -0
  142. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/agent.ts +350 -0
  143. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +153 -0
  144. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +13 -0
  145. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/surfaces.ts +651 -0
  146. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.json +12 -0
  147. package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.surfaces.json +4 -0
  148. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/agent.ts +623 -0
  149. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +463 -0
  150. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +15 -0
  151. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/tsconfig.json +12 -0
  152. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent-v1.ts +99 -0
  153. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent.ts +623 -0
  154. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +336 -0
  155. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +15 -0
  156. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/tsconfig.json +12 -0
  157. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/agent.ts +96 -0
  158. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +579 -0
  159. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +15 -0
  160. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/tsconfig.json +12 -0
  161. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.cjs +53 -0
  162. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.mjs +55 -0
  163. package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/package.json +18 -0
  164. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/agent.ts +659 -0
  165. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +635 -0
  166. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +15 -0
  167. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/tsconfig.json +12 -0
  168. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/agent.ts +659 -0
  169. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +553 -0
  170. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +15 -0
  171. package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/tsconfig.json +12 -0
  172. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/agent.ts +877 -0
  173. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/mcp-server.mjs +66 -0
  174. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +6417 -0
  175. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +15 -0
  176. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/tsconfig.json +12 -0
  177. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/agent.ts +872 -0
  178. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/mcp-server.mjs +66 -0
  179. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +2540 -0
  180. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +15 -0
  181. package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/tsconfig.json +12 -0
  182. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/actions.ts +18 -0
  183. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/ai/route.ts +42 -0
  184. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/edge/route.ts +29 -0
  185. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/langgraph/route.ts +21 -0
  186. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/llamaindex/route.ts +11 -0
  187. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/mastra/route.ts +19 -0
  188. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/status/route.ts +7 -0
  189. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/layout.tsx +9 -0
  190. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/page.tsx +15 -0
  191. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/instrumentation.ts +15 -0
  192. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/ai.ts +61 -0
  193. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/langgraph.ts +68 -0
  194. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/llamaindex.ts +98 -0
  195. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/mastra.ts +90 -0
  196. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/next.config.ts +52 -0
  197. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +4343 -0
  198. package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +28 -0
  199. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/agent.ts +51 -0
  200. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/deno-npm.ts +76 -0
  201. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +484 -0
  202. package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +15 -0
  203. package/.next/standalone/sdk/typescript/integration/fixtures/types/agent.ts +79 -0
  204. package/.next/standalone/sdk/typescript/integration/fixtures/types/package-lock.json +740 -0
  205. package/.next/standalone/sdk/typescript/integration/fixtures/types/package.json +11 -0
  206. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/agent.ts +197 -0
  207. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package-lock.json +70 -0
  208. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package.json +12 -0
  209. package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/tsconfig.json +12 -0
  210. package/.next/standalone/sdk/typescript/integration/global-setup.ts +29 -0
  211. package/.next/standalone/sdk/typescript/integration/harness.ts +451 -0
  212. package/.next/standalone/sdk/typescript/integration/langchain.test.ts +682 -0
  213. package/.next/standalone/sdk/typescript/integration/llamaindex.test.ts +709 -0
  214. package/.next/standalone/sdk/typescript/integration/mastra-coverage.test.ts +386 -0
  215. package/.next/standalone/sdk/typescript/integration/mastra.test.ts +311 -0
  216. package/.next/standalone/sdk/typescript/integration/nextjs.test.ts +341 -0
  217. package/.next/standalone/sdk/typescript/integration/runtime-parity.ts +180 -0
  218. package/.next/standalone/sdk/typescript/integration/runtimes.bun.test.ts +15 -0
  219. package/.next/standalone/sdk/typescript/integration/runtimes.core.test.ts +113 -0
  220. package/.next/standalone/sdk/typescript/integration/runtimes.deno.test.ts +19 -0
  221. package/.next/standalone/sdk/typescript/integration/types.test.ts +141 -0
  222. package/.next/standalone/sdk/typescript/integration/vanilla.test.ts +255 -0
  223. package/.next/standalone/sdk/typescript/package-lock.json +2640 -0
  224. package/.next/standalone/sdk/typescript/package.json +401 -0
  225. package/.next/standalone/sdk/typescript/scripts/finalize-build.mjs +123 -0
  226. package/.next/standalone/sdk/typescript/scripts/release.mjs +177 -0
  227. package/.next/standalone/sdk/typescript/src/clock.ts +58 -0
  228. package/.next/standalone/sdk/typescript/src/context.ts +214 -0
  229. package/.next/standalone/sdk/typescript/src/edge/adapter.ts +18 -0
  230. package/.next/standalone/sdk/typescript/src/edge/ai.ts +116 -0
  231. package/.next/standalone/sdk/typescript/src/edge/index.ts +238 -0
  232. package/.next/standalone/sdk/typescript/src/edge/langchain.ts +18 -0
  233. package/.next/standalone/sdk/typescript/src/edge/llamaindex.ts +12 -0
  234. package/.next/standalone/sdk/typescript/src/edge/mastra.ts +17 -0
  235. package/.next/standalone/sdk/typescript/src/edge/notice.ts +33 -0
  236. package/.next/standalone/sdk/typescript/src/environment.ts +75 -0
  237. package/.next/standalone/sdk/typescript/src/evaluator/authoring.ts +480 -0
  238. package/.next/standalone/sdk/typescript/src/evaluator/cli.ts +96 -0
  239. package/.next/standalone/sdk/typescript/src/evaluator/client.ts +421 -0
  240. package/.next/standalone/sdk/typescript/src/evaluator/expression.ts +1292 -0
  241. package/.next/standalone/sdk/typescript/src/evaluator/index.ts +144 -0
  242. package/.next/standalone/sdk/typescript/src/evaluator/protocol.ts +747 -0
  243. package/.next/standalone/sdk/typescript/src/evaluator/runtime.ts +930 -0
  244. package/.next/standalone/sdk/typescript/src/evaluator/sandbox-worker.ts +171 -0
  245. package/.next/standalone/sdk/typescript/src/evaluator/source-limits.ts +27 -0
  246. package/.next/standalone/sdk/typescript/src/evaluator/source.ts +509 -0
  247. package/.next/standalone/sdk/typescript/src/events.ts +879 -0
  248. package/.next/standalone/sdk/typescript/src/exit.ts +117 -0
  249. package/.next/standalone/sdk/typescript/src/index.ts +186 -0
  250. package/.next/standalone/sdk/typescript/src/integrations/ai.ts +1566 -0
  251. package/.next/standalone/sdk/typescript/src/integrations/compat.ts +322 -0
  252. package/.next/standalone/sdk/typescript/src/integrations/core.ts +1321 -0
  253. package/.next/standalone/sdk/typescript/src/integrations/index.ts +355 -0
  254. package/.next/standalone/sdk/typescript/src/integrations/langchain.ts +2340 -0
  255. package/.next/standalone/sdk/typescript/src/integrations/llamaindex.ts +2111 -0
  256. package/.next/standalone/sdk/typescript/src/integrations/mastra.ts +1802 -0
  257. package/.next/standalone/sdk/typescript/src/logger.ts +98 -0
  258. package/.next/standalone/sdk/typescript/src/next.ts +115 -0
  259. package/.next/standalone/sdk/typescript/src/node-require.ts +446 -0
  260. package/.next/standalone/sdk/typescript/src/redact.ts +305 -0
  261. package/.next/standalone/sdk/typescript/src/resolver.ts +120 -0
  262. package/.next/standalone/sdk/typescript/src/runtime.ts +29 -0
  263. package/.next/standalone/sdk/typescript/src/schema.ts +410 -0
  264. package/.next/standalone/sdk/typescript/src/scopes.ts +701 -0
  265. package/.next/standalone/sdk/typescript/src/shared.ts +33 -0
  266. package/.next/standalone/sdk/typescript/src/version.ts +5 -0
  267. package/.next/standalone/sdk/typescript/src/writer.ts +934 -0
  268. package/.next/standalone/sdk/typescript/test/adapters.test.ts +397 -0
  269. package/.next/standalone/sdk/typescript/test/ai.test.ts +1076 -0
  270. package/.next/standalone/sdk/typescript/test/copies.test.ts +204 -0
  271. package/.next/standalone/sdk/typescript/test/edge.test.ts +183 -0
  272. package/.next/standalone/sdk/typescript/test/evaluator-client.test.ts +234 -0
  273. package/.next/standalone/sdk/typescript/test/evaluator-protocol.test.ts +225 -0
  274. package/.next/standalone/sdk/typescript/test/events.test.ts +193 -0
  275. package/.next/standalone/sdk/typescript/test/expression.test.ts +181 -0
  276. package/.next/standalone/sdk/typescript/test/global-setup.ts +26 -0
  277. package/.next/standalone/sdk/typescript/test/helpers.ts +130 -0
  278. package/.next/standalone/sdk/typescript/test/integrations.test.ts +369 -0
  279. package/.next/standalone/sdk/typescript/test/langchain-copies.test.ts +204 -0
  280. package/.next/standalone/sdk/typescript/test/langchain.test.ts +999 -0
  281. package/.next/standalone/sdk/typescript/test/llamaindex.test.ts +1760 -0
  282. package/.next/standalone/sdk/typescript/test/mastra-coverage.test.ts +501 -0
  283. package/.next/standalone/sdk/typescript/test/mastra-lifecycle.test.ts +479 -0
  284. package/.next/standalone/sdk/typescript/test/mastra.test.ts +285 -0
  285. package/.next/standalone/sdk/typescript/test/next.test.ts +109 -0
  286. package/.next/standalone/sdk/typescript/test/packaging.test.ts +312 -0
  287. package/.next/standalone/sdk/typescript/test/redaction.test.ts +171 -0
  288. package/.next/standalone/sdk/typescript/test/runtimes.test.ts +101 -0
  289. package/.next/standalone/sdk/typescript/test/sandbox.test.ts +189 -0
  290. package/.next/standalone/sdk/typescript/test/scopes.test.ts +271 -0
  291. package/.next/standalone/sdk/typescript/test/setup.ts +19 -0
  292. package/.next/standalone/sdk/typescript/test/skill-snippets.test.ts +73 -0
  293. package/.next/standalone/sdk/typescript/test/spool-contract.test.ts +124 -0
  294. package/.next/standalone/sdk/typescript/test/tracker-bounds.test.ts +191 -0
  295. package/.next/standalone/sdk/typescript/test/wire-format.test.ts +214 -0
  296. package/.next/standalone/sdk/typescript/test/writer.test.ts +407 -0
  297. package/.next/standalone/sdk/typescript/tsconfig.build.json +15 -0
  298. package/.next/standalone/sdk/typescript/tsconfig.cjs.json +19 -0
  299. package/.next/standalone/sdk/typescript/tsconfig.json +28 -0
  300. package/.next/standalone/sdk/typescript/vitest.config.ts +33 -0
  301. package/.next/standalone/sdk/typescript/vitest.integration.config.ts +23 -0
  302. package/.next/standalone/server.js +1 -1
  303. package/README.md +2 -2
  304. package/dist/cli.mjs +208 -27
  305. package/dist/worker.mjs +120 -14
  306. package/package.json +10 -10
  307. package/scripts/build-policy-pack.mjs +14 -0
  308. package/src/audit/features.ts +3 -2
  309. package/src/hooks/builtin-policies.ts +156 -5
  310. package/src/hooks/jev-cli.ts +57 -1
  311. package/src/hooks/manager.ts +1 -1
  312. package/src/hooks/pack-cli.ts +150 -18
  313. package/src/hooks/pack-store.ts +1 -1
  314. package/src/hooks/policy-catalog.ts +148 -9
  315. package/src/hooks/types.ts +1 -1
  316. package/.next/standalone/.next/static/chunks/0cd-_8-c-m1ea.js +0 -6
  317. package/.next/standalone/.next/static/chunks/0qrbdkv9qmvli.js +0 -69
  318. package/.next/standalone/.next/static/chunks/0uldbut9y2-e8.js +0 -1
  319. package/.next/standalone/.next/static/chunks/285spx855h_3r.css +0 -2
  320. package/.next/standalone/.next/static/chunks/2vkvu9-opa_1z.js +0 -1
  321. package/.next/standalone/.next/static/chunks/37lhv7wa3ywt6.js +0 -1
  322. /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → gbEOjBgZAxF2UIUwZVHNu}/_buildManifest.js +0 -0
  323. /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → gbEOjBgZAxF2UIUwZVHNu}/_clientMiddlewareManifest.js +0 -0
  324. /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → gbEOjBgZAxF2UIUwZVHNu}/_ssgManifest.js +0 -0
@@ -0,0 +1,2340 @@
1
+ /**
2
+ * LangChain.js and LangGraph.js.
3
+ *
4
+ * The TypeScript twin of the Python SDK's `integrations/langchain.py`, and it
5
+ * is held to that adapter's output: both SDKs write into one pipe and the
6
+ * dashboard cannot tell which language wrote an event, so for the same program
7
+ * they must draw the same tree. Every mapping decision below is Python's, and
8
+ * where JavaScript forced a different mechanism the comment says why. Verified
9
+ * against `@langchain/core` 0.3.80 + `@langchain/langgraph` 0.4.10 and
10
+ * `@langchain/core` 1.2.12 + `@langchain/langgraph` 1.4.17 — the two fixtures
11
+ * under `integration/fixtures/langchain-*`, which run this file from the packed
12
+ * tarball as both ES module and CommonJS.
13
+ *
14
+ * ## The mapping
15
+ *
16
+ * LangChain / LangGraph FailproofAI
17
+ * --------------------------- ------------------------------------------
18
+ * root run (no parent) agent_start / agent_end
19
+ * LangGraph node hook_triggered / hook_completed,
20
+ * trigger_event="graph_node"
21
+ * compiled subgraph nested agent_start ("root/node")
22
+ * tool run tool_use / tool_result (the MODEL's id)
23
+ * retriever run tool_use / tool_result (summarised)
24
+ * chat model / LLM run model_request / model_response
25
+ * interrupt() human_wait + agent_pause
26
+ * Command({ resume }) agent_resume + human_input
27
+ * intermediate chains nothing (see `includeChains`)
28
+ *
29
+ * **A LangGraph node is a hook, not a nested agent.** `agent_id` is a
30
+ * `LowCardinality(String)` column and the primary facet on every dashboard
31
+ * surface, and a session is labelled with the first `agent_id` it saw — so
32
+ * promoting `retrieve`, `grade_documents` and `should_continue` to agents would
33
+ * drown the facet and name the session after whichever node ran first. Hook
34
+ * spans draw identically on the timeline, and `/hooks` becomes a per-node
35
+ * latency page for free. (The first release of this adapter made every node an
36
+ * agent, which is exactly the trace this paragraph exists to prevent.)
37
+ *
38
+ * ## Where it attaches
39
+ *
40
+ * `CallbackManager.configure` and `CallbackManager._configureSync` — the two
41
+ * functions every runnable calls to build the manager for an invocation, and
42
+ * the one LangGraph's Pregel loop calls for the graph itself. Patching them
43
+ * attaches the handler to every `invoke`/`stream`/`batch` in the process
44
+ * without the caller passing `callbacks:` anywhere. LangChain.js has no
45
+ * supported global-handler registry (Python has `register_configure_hook`), so
46
+ * this is the only placement that works without editing call sites. Both are
47
+ * patched on EVERY loaded copy of `@langchain/core` — see
48
+ * `compat.requireModuleCopies` for why there can be two — and on every copy
49
+ * nested under a dependency that pinned its own (see `nestedManagers`).
50
+ *
51
+ * `langchainHandler()` is the patch-free path: the same handler, passed
52
+ * explicitly. It works with or without `instrument()`, and the two together do
53
+ * not double-record, because attaching is idempotent on a marker the handler
54
+ * carries rather than on object identity.
55
+ *
56
+ * ## Why the handler is awaited
57
+ *
58
+ * `awaitHandlers: true` is not optional, for the same reason Python sets
59
+ * `run_inline = True`. Without it LangChain queues every callback on a
60
+ * background promise queue: callbacks land after the call that caused them
61
+ * returned, possibly after the process flushed its spool, and — the part that
62
+ * is not recoverable — in whatever async context the queue happens to run in,
63
+ * so an enclosing `failproofai.agent()` scope is invisible to them and a graph
64
+ * that should nest under it becomes a second, unrelated session. Every
65
+ * callback here is synchronous bookkeeping plus an in-memory `submit`, so
66
+ * awaiting it costs nothing measurable.
67
+ *
68
+ * ## Control flow is not failure
69
+ *
70
+ * LangGraph reports an `interrupt()` through the same `handleChainError` as a
71
+ * genuine failure — the node run "errors" with a `GraphInterrupt`. Reporting it
72
+ * would paint a red error plus `agent_end(outcome="failed")` on every human
73
+ * approval. Anything LangGraph marks `is_bubble_up` (`GraphInterrupt`,
74
+ * `NodeInterrupt`, `ParentCommand`, `GraphDrained`), or whose name says it is
75
+ * one, is treated as control flow and emits the human-in-the-loop pairs
76
+ * instead.
77
+ */
78
+
79
+ import { join } from "node:path";
80
+ import { pathToFileURL } from "node:url";
81
+
82
+ import { sessionId as ambientSessionId } from "../context.js";
83
+ import { logger } from "../logger.js";
84
+ import { isCancellation as isScopeCancellation } from "../scopes.js";
85
+ import {
86
+ entryIsCommonJs,
87
+ importModule,
88
+ isRequired,
89
+ nestedCopies,
90
+ nodeRequire,
91
+ resolveExportsAt,
92
+ resolveFrom,
93
+ } from "../node-require.js";
94
+ import * as compat from "./compat.js";
95
+ import * as core from "./core.js";
96
+ import type { Adapter } from "./core.js";
97
+
98
+ const NAME = "langchain";
99
+ const PACKAGE = "@langchain/core";
100
+ const GRAPH_PACKAGE = "@langchain/langgraph";
101
+
102
+ /**
103
+ * The documented per-call session key — the same string the Python SDK reads,
104
+ * so one config object means the same thing to both:
105
+ *
106
+ * graph.invoke(input, { metadata: { failproofai_sdk_session_id: requestId } })
107
+ */
108
+ export const SESSION_METADATA_KEY = "failproofai_sdk_session_id";
109
+
110
+ /**
111
+ * Checked in order after the explicit key. `thread_id` is last because a thread
112
+ * is a *conversation*: two turns on one thread are two runs, and a caller who
113
+ * wants them merged says so with one of the earlier keys.
114
+ */
115
+ const SESSION_METADATA_FALLBACKS = ["session_id", "conversation_id", "thread_id"] as const;
116
+
117
+ /**
118
+ * LangSmith's convention for "machinery, not user-visible work". Demoted rather
119
+ * than dropped: never a span, but kept in the parent chain so its children
120
+ * still find the agent above them.
121
+ */
122
+ const HIDDEN_TAG = "langsmith:hidden";
123
+
124
+ /**
125
+ * A run of one of these types is a leaf — a model call, a tool call, a
126
+ * retrieval — and is never a LangGraph node's own run: whatever is handed to
127
+ * `addNode`, the node's own run is a chain run and the thing passed runs as its
128
+ * child.
129
+ */
130
+ const LEAF_RUN_TYPES: ReadonlySet<string> = new Set(["llm", "chat_model", "tool", "retriever"]);
131
+
132
+ /** LangChain tags every step of a `RunnableSequence` `seq:step:N`; a node is `graph:step:N`. */
133
+ const INNER_STEP_TAG = "seq:step:";
134
+
135
+ /**
136
+ * Name-based fallback for `is_bubble_up`. Getting this wrong is expensive and
137
+ * silent — a red error on every human approval — so it is worth a second check.
138
+ */
139
+ const CONTROL_FLOW_NAMES: ReadonlySet<string> = new Set([
140
+ "GraphBubbleUp",
141
+ "GraphInterrupt",
142
+ "NodeInterrupt",
143
+ "ParentCommand",
144
+ "GraphDrained",
145
+ ]);
146
+
147
+ /**
148
+ * JavaScript's own cancellation: an `AbortSignal` firing inside a run, which is
149
+ * how a stream whose consumer went away and a request whose client
150
+ * disconnected both end. The analogue of Python's `GeneratorExit` /
151
+ * `CancelledError`, and for the same reason: a stopped stream is not a crashed
152
+ * one, and must not flip a healthy session to failed.
153
+ */
154
+ const CANCELLATION_NAMES: ReadonlySet<string> = new Set(["AbortError"]);
155
+
156
+ /**
157
+ * LangGraph.js (>= 1.x) routes interrupt/resume lifecycle events to any handler
158
+ * carrying this marker — its `GraphCallbackHandler.isInstance` is a duck-typed
159
+ * check on exactly this registered symbol. Setting it on a plain object means
160
+ * the handler gets `handleInterrupt`/`handleResume` without this module ever
161
+ * importing LangGraph. On 0.x the symbol means nothing and the exception-path
162
+ * fallback produces the same pairs.
163
+ */
164
+ const GRAPH_CALLBACK_HANDLER = Symbol.for("langgraph.graph_callback_handler");
165
+
166
+ /**
167
+ * Marks our handler, so "is it already attached?" is answered by what the
168
+ * handler IS rather than which object it is. Identity is not enough: a manager
169
+ * built from `callbacks: [langchainHandler()]` and then passed through the
170
+ * patched `configure` would otherwise carry it once per path.
171
+ */
172
+ const HANDLER_MARK = Symbol.for("failproofai.langchain.handler");
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Options
176
+ // ---------------------------------------------------------------------------
177
+
178
+ /** Everything `instrument("langchain", ...)` and `langchainHandler()` accept. */
179
+ export interface LangChainOptions {
180
+ /** Pin every run to this session id (step 1 of the resolution order). */
181
+ sessionId?: string;
182
+ /** Drop prompts, messages and outputs; keep structure, durations and tokens. */
183
+ captureContent?: boolean;
184
+ /** Record these intermediate chains, by run name, as `trigger_event="pipeline"` hooks. */
185
+ includeChains?: string | Iterable<string>;
186
+ /** LangGraph interrupt/resume lifecycle callbacks (LangGraph.js >= 1). Default on. */
187
+ graphCallbacks?: boolean;
188
+ /** Per-value truncation ceiling. Default: the core field limit. */
189
+ captureLimit?: number | string;
190
+ }
191
+
192
+ interface Options {
193
+ sessionId: string | null;
194
+ includeChains: ReadonlySet<string>;
195
+ captureContent: boolean;
196
+ graphCallbacks: boolean;
197
+ captureLimit: number;
198
+ }
199
+
200
+ const KNOWN_OPTIONS: ReadonlySet<string> = new Set([
201
+ "sessionId",
202
+ "includeChains",
203
+ "captureContent",
204
+ "graphCallbacks",
205
+ "captureLimit",
206
+ ]);
207
+
208
+ function defaultOptions(): Options {
209
+ return {
210
+ sessionId: null,
211
+ includeChains: new Set(),
212
+ captureContent: true,
213
+ graphCallbacks: true,
214
+ captureLimit: core.FIELD_LIMIT,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Validate `captureLimit`, falling back rather than throwing.
220
+ *
221
+ * `instrument()` with no name installs every detected adapter with the same
222
+ * options object, so a value meant for — or mistyped for — another framework
223
+ * must never take this one down. `Infinity`, the obvious spelling of "capture
224
+ * everything", is not an integer and falls back too (it is what broke the
225
+ * Python adapter's startup under strict mode).
226
+ */
227
+ export function captureLimitOf(value: unknown): number {
228
+ if (value === undefined || value === null) return core.FIELD_LIMIT;
229
+ const limit = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
230
+ if (typeof limit !== "number" || !Number.isInteger(limit)) {
231
+ logger.warn(`langchain captureLimit=${display(value)} is not an integer; using ${core.FIELD_LIMIT}`);
232
+ return core.FIELD_LIMIT;
233
+ }
234
+ if (limit < 1) {
235
+ logger.warn(`langchain captureLimit=${limit} must be >= 1; using ${core.FIELD_LIMIT}`);
236
+ return core.FIELD_LIMIT;
237
+ }
238
+ return limit;
239
+ }
240
+
241
+ /** `LangChainOptions` (or the shared `instrument()` bag) -> the validated `Options`. */
242
+ export function readOptions(options: Record<string, unknown> = {}): Options {
243
+ const unknown = Object.keys(options).filter((key) => !KNOWN_OPTIONS.has(key));
244
+ if (unknown.length > 0) {
245
+ // Not fatal: a bare `instrument()` hands every adapter the same options,
246
+ // so one meant for another framework legitimately arrives here.
247
+ logger.debug(`langchain adapter ignoring options ${JSON.stringify(unknown.sort())}`);
248
+ }
249
+ const include = options.includeChains;
250
+ let chains: string[] = [];
251
+ if (typeof include === "string") chains = [include];
252
+ else if (include !== null && typeof include === "object" && Symbol.iterator in include) {
253
+ chains = [...(include as Iterable<unknown>)].map(String);
254
+ }
255
+ const sid = options.sessionId;
256
+ return {
257
+ sessionId: sid === undefined || sid === null || sid === "" ? null : display(sid),
258
+ includeChains: new Set(chains),
259
+ captureContent: options.captureContent === undefined ? true : Boolean(options.captureContent),
260
+ graphCallbacks: options.graphCallbacks === undefined ? true : Boolean(options.graphCallbacks),
261
+ captureLimit: captureLimitOf(options.captureLimit),
262
+ };
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // State
267
+ // ---------------------------------------------------------------------------
268
+
269
+ /**
270
+ * One FailproofAI session, which may outlive a single `.invoke()`.
271
+ *
272
+ * It has to: a human-in-the-loop graph runs `invoke()`, interrupts, and is
273
+ * resumed by a *second* `invoke()` minutes later. Both are the same session and
274
+ * the same root agent, and the agent stays open across the gap so that
275
+ * `agent_pause` -> `agent_resume` measures the wait.
276
+ */
277
+ interface Session {
278
+ sessionId: string;
279
+ agentKey: string;
280
+ agentId: string;
281
+ /** pause id -> the prompt it asked, for `human_input.fw_prompt`. */
282
+ openPauses: Map<string, string | undefined>;
283
+ reportedError: boolean;
284
+ /** When its root ended with a pause still open; null while running. */
285
+ pausedAt: number | null;
286
+ }
287
+
288
+ /**
289
+ * Bookkeeping for a resume whose pause was opened in ANOTHER process. Set on
290
+ * the ROOT run only, and only when this process has no open pause of its own to
291
+ * close. See `closeRemotePause`.
292
+ */
293
+ interface RemoteResume {
294
+ value: unknown;
295
+ /** level key (checkpoint-ns prefix) -> the `langgraph_step` of the first node seen there. */
296
+ levels: Map<string, unknown>;
297
+ /** The deepest level LangGraph said is resuming (`handleResume`, LangGraph.js >= 1). */
298
+ deepest: string | null;
299
+ done: Set<string>;
300
+ }
301
+
302
+ type RunType = "chain" | "llm" | "chat_model" | "tool" | "retriever";
303
+ type Kind = "" | "root" | "node" | "tool" | "retriever" | "model" | "chain" | "subgraph";
304
+
305
+ /** What we need about a LangChain run after its start callback returns. */
306
+ interface RunInfo {
307
+ id: string;
308
+ parent: string | null;
309
+ name: string;
310
+ runType: RunType;
311
+ started: number;
312
+ hidden: boolean;
313
+ kind: Kind;
314
+ /** Set only on a ROOT run that is itself a leaf — a bare `model.invoke()`, a standalone tool. */
315
+ leafKind: "" | "model" | "tool" | "retriever";
316
+ root: string | null;
317
+ session: Session | null;
318
+ node: string | null;
319
+ toolCallId: string | null;
320
+ model: string | null;
321
+ ttftMs: number | null;
322
+ chunks: number;
323
+ remote: RemoteResume | null;
324
+ tags: string[];
325
+ meta: Record<string, unknown>;
326
+ /** Chain inputs, kept for `goal` and for recovering a tool call's id on core 0.3. */
327
+ inputs: unknown;
328
+ /** Tool-call ids already handed to a child tool run (0.3 id recovery). */
329
+ claimed: Set<string> | null;
330
+ /** ROOT only: bumped by every callback under this root (see `reapIfAbandoned`). */
331
+ activity: number;
332
+ }
333
+
334
+ interface StartArgs {
335
+ id: string;
336
+ parent: string | null;
337
+ name: string;
338
+ runType: RunType;
339
+ tags: string[];
340
+ meta: Record<string, unknown>;
341
+ inputs: unknown;
342
+ /** The chat model's messages, still as `BaseMessage` objects. */
343
+ messages?: unknown[];
344
+ invocationParams?: Record<string, unknown>;
345
+ toolCallId?: string;
346
+ }
347
+
348
+ interface EndArgs {
349
+ outputs?: unknown;
350
+ error?: unknown;
351
+ response?: unknown;
352
+ }
353
+
354
+ const MAX_RUNS = 10_000;
355
+ const MAX_SESSIONS = 1_000;
356
+
357
+ /**
358
+ * How long this process keeps a run that paused on a human.
359
+ *
360
+ * An interrupted graph deliberately leaves its agent open so the resume can
361
+ * continue it — but the resume usually lands on ANOTHER worker, which already
362
+ * handles it (`closeRemotePause`), and then this process would hold the agent
363
+ * forever: a slot in the tracker that live runs need, plus a linear cost on
364
+ * every lookup that scanned open agents. After this long it is forgotten
365
+ * without emitting anything. A resume that does arrive here later takes the
366
+ * same path a cross-worker resume does, so nothing is lost but the in-process
367
+ * shortcut.
368
+ */
369
+ export const PAUSED_SESSION_TTL_MS = 15 * 60_000;
370
+
371
+ /**
372
+ * All cross-callback state, module level on purpose: one handler object serves
373
+ * every callback manager in the process, and a start and its end are separate
374
+ * calls on possibly different async branches.
375
+ */
376
+ class State {
377
+ /**
378
+ * The kill switch `uninstrument()` flips. Restoring `configure` stops new
379
+ * managers getting the handler, but a handler object somebody already holds
380
+ * (from `langchainHandler()`) or a manager already built would keep
381
+ * recording; this makes every entry point a no-op instead.
382
+ */
383
+ enabled = false;
384
+ installed = false;
385
+ options: Options = defaultOptions();
386
+ tracker: core.RunTracker = State.newTracker(core.FIELD_LIMIT);
387
+ runs = new Map<string, RunInfo>();
388
+ sessions = new Map<string, Session>();
389
+
390
+ static newTracker(limit: number): core.RunTracker {
391
+ return new core.RunTracker(NAME, { baseFields: baseFields(), fieldLimit: limit });
392
+ }
393
+
394
+ configure(options: Options): void {
395
+ this.options = options;
396
+ this.tracker = State.newTracker(options.captureLimit);
397
+ this.runs.clear();
398
+ this.sessions.clear();
399
+ }
400
+
401
+ reset(): void {
402
+ this.tracker.reset();
403
+ this.runs.clear();
404
+ this.sessions.clear();
405
+ }
406
+
407
+ /**
408
+ * FIFO; a `Map` keeps insertion order. Orphaned entries are normal — a
409
+ * cancelled stream, a crashed node, a framework that skipped an end callback
410
+ * — and unbounded, each table is a memory leak in a long-lived server.
411
+ */
412
+ evict(): void {
413
+ while (this.runs.size >= MAX_RUNS) {
414
+ const oldest = this.runs.keys().next();
415
+ if (oldest.done) break;
416
+ this.runs.delete(oldest.value);
417
+ this.tracker.unlink(oldest.value);
418
+ }
419
+ while (this.sessions.size >= MAX_SESSIONS) {
420
+ const oldest = this.sessions.keys().next();
421
+ if (oldest.done) break;
422
+ this.dropSession(oldest.value);
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Forget sessions paused longer than `PAUSED_SESSION_TTL_MS`.
428
+ *
429
+ * Paused sessions are the only ones that outlive their root, and a `Map`
430
+ * keeps insertion order, so a sweep from the front stops at the first one
431
+ * that is either not paused or not yet stale.
432
+ */
433
+ sweepPaused(now: number): void {
434
+ for (const [id, session] of this.sessions) {
435
+ if (session.pausedAt === null) continue;
436
+ if (now - session.pausedAt <= PAUSED_SESSION_TTL_MS) break;
437
+ this.dropSession(id);
438
+ }
439
+ }
440
+
441
+ /** Remove a session; a paused one's agent is forgotten, never closed. */
442
+ dropSession(id: string): void {
443
+ const session = this.sessions.get(id);
444
+ this.sessions.delete(id);
445
+ if (session !== undefined && session.pausedAt !== null) this.tracker.forget(session.agentKey);
446
+ }
447
+ }
448
+
449
+ function baseFields(): Record<string, unknown> {
450
+ const fields = core.frameworkFields(NAME, PACKAGE);
451
+ const graphVersion = compat.versionString(GRAPH_PACKAGE);
452
+ if (graphVersion) fields.fw_langgraph_version = graphVersion;
453
+ return fields;
454
+ }
455
+
456
+ const state = new State();
457
+ let patcher: core.Patcher | null = null;
458
+
459
+ // ---------------------------------------------------------------------------
460
+ // Small readers
461
+ // ---------------------------------------------------------------------------
462
+
463
+ type Loose = Record<string, unknown>;
464
+
465
+ const isObject = (value: unknown): value is Loose => typeof value === "object" && value !== null;
466
+
467
+ function isPlainObject(value: unknown): value is Loose {
468
+ if (!isObject(value)) return false;
469
+ const proto = Object.getPrototypeOf(value) as unknown;
470
+ return proto === Object.prototype || proto === null;
471
+ }
472
+
473
+ /** `runName` first, then the serialized `name`, then the last `id` segment (the class name). */
474
+ function runNameOf(serialized: unknown, runName: unknown, fallback: string): string {
475
+ if (typeof runName === "string" && runName) return runName;
476
+ const value = serialized as { name?: unknown; id?: unknown } | undefined;
477
+ if (typeof value?.name === "string" && value.name) return value.name;
478
+ const id = value?.id;
479
+ if (Array.isArray(id) && id.length > 0) {
480
+ const last = id[id.length - 1] as unknown;
481
+ if (typeof last === "string" && last) return last;
482
+ }
483
+ return fallback;
484
+ }
485
+
486
+ /** A LangChain message's type (`human`, `ai`, `tool`, `system`), or null for anything else. */
487
+ function messageType(value: unknown): string | null {
488
+ if (!isObject(value)) return null;
489
+ for (const method of ["getType", "_getType"]) {
490
+ const fn: unknown = value[method];
491
+ if (typeof fn === "function") {
492
+ try {
493
+ const type = (fn as () => unknown).call(value);
494
+ if (typeof type === "string" && type) return type;
495
+ } catch {
496
+ // A message whose type getter throws is still not worth failing over.
497
+ }
498
+ }
499
+ }
500
+ return null;
501
+ }
502
+
503
+ /**
504
+ * The payload view of a LangChain value: messages as `{type, content, ...}`,
505
+ * anything else `Serializable` as its constructor kwargs.
506
+ *
507
+ * `truncate()` would otherwise reach these through `toJSON()`, which LangChain
508
+ * defines as its SERIALISATION envelope — `{lc: 1, type: "constructor", id:
509
+ * [...], kwargs}` — so every graph-state payload would render as class paths
510
+ * with the content buried one level down. The Python adapter gets the
511
+ * equivalent of this view from pydantic's `model_dump`.
512
+ */
513
+ function plain(value: unknown, depth = 0): unknown {
514
+ if (!isObject(value) || depth > 6) return value;
515
+ if (Array.isArray(value)) return value.map((item) => plain(item, depth + 1));
516
+ const type = messageType(value);
517
+ if (type !== null) {
518
+ const out: Loose = { type, content: plain(value.content, depth + 1) };
519
+ for (const key of ["name", "id", "tool_call_id", "status"]) {
520
+ if (value[key] !== undefined && value[key] !== null) out[key] = value[key];
521
+ }
522
+ for (const key of ["tool_calls", "usage_metadata"]) {
523
+ const field = value[key];
524
+ if (field !== undefined && field !== null && !(Array.isArray(field) && field.length === 0)) {
525
+ out[key] = plain(field, depth + 1);
526
+ }
527
+ }
528
+ return out;
529
+ }
530
+ if (isPlainObject(value)) {
531
+ const out: Loose = {};
532
+ for (const [key, item] of Object.entries(value)) out[key] = plain(item, depth + 1);
533
+ return out;
534
+ }
535
+ const kwargs = (value as Loose).lc_kwargs;
536
+ if (isObject(kwargs)) return plain(kwargs, depth + 1);
537
+ // A LangGraph `Command` / `Send` — what every `createAgent` model node
538
+ // returns. Not `Serializable`, so `truncate()` would dump it through its own
539
+ // `toJSON()` and every message inside through LangChain's envelope. Take the
540
+ // dump here instead and give its contents the same payload view.
541
+ const dump = (value as { toJSON?: unknown }).toJSON;
542
+ if (typeof dump === "function") {
543
+ try {
544
+ const dumped: unknown = (dump as () => unknown).call(value);
545
+ if (isPlainObject(dumped) && dumped.lc === undefined) return plain(dumped, depth + 1);
546
+ } catch {
547
+ // A throwing `toJSON` is the value's problem; `truncate` renders it.
548
+ }
549
+ }
550
+ return value;
551
+ }
552
+
553
+ const limit = (): number => state.options.captureLimit;
554
+
555
+ /** Payload discipline for the big three: inputs, outputs, graph state. */
556
+ function shrink(value: unknown): unknown {
557
+ if (!state.options.captureContent || value === undefined) return undefined;
558
+ return core.truncate(plain(value), limit());
559
+ }
560
+
561
+ function tagsOf(tags: unknown): string[] {
562
+ return Array.isArray(tags) ? tags.filter((tag): tag is string => typeof tag === "string") : [];
563
+ }
564
+
565
+ function metaOf(metadata: unknown): Record<string, unknown> {
566
+ return isObject(metadata) ? { ...metadata } : {};
567
+ }
568
+
569
+ /**
570
+ * The LangGraph node name iff this run is the node's OWN run.
571
+ *
572
+ * Every inner runnable inherits `langgraph_node` from the node that contains
573
+ * it, so the metadata alone matches the node, the chat model inside it, the
574
+ * tool it called and each conditional-edge function. Only the node's own run
575
+ * is NAMED after the node — but the name is the user's to choose on both
576
+ * sides, and the Python adapter verified three collisions that each deleted
577
+ * the most valuable event in the trace: a node named after its tool swallowed
578
+ * the tool pair, a node named after its model swallowed the model pair, and an
579
+ * inner runnable carrying the node's name doubled the node's visits. So the run
580
+ * must also be SHAPED like a node's own run: a non-leaf run type, and not an
581
+ * inner step of a `RunnableSequence`. Both are exclusions, so if LangGraph ever
582
+ * stops emitting `seq:step:` tags this degrades to a duplicate span rather than
583
+ * to no spans.
584
+ */
585
+ export function nodeOf(
586
+ run: { name: string; runType: string; tags: readonly string[] },
587
+ meta: Record<string, unknown>,
588
+ ): string | null {
589
+ const node = meta.langgraph_node;
590
+ if (typeof node !== "string" || !node || node !== run.name) return null;
591
+ if (LEAF_RUN_TYPES.has(run.runType)) return null;
592
+ if (run.tags.some((tag) => tag.startsWith(INNER_STEP_TAG))) return null;
593
+ return node;
594
+ }
595
+
596
+ /**
597
+ * `langgraph_checkpoint_ns` split into its `name:uuid` segments. One segment
598
+ * for a top-level node, `child:uuid|inner:uuid` inside a compiled subgraph: the
599
+ * number beyond the first is the nesting depth, and the leading segments name
600
+ * the subgraphs — which is how nested agents get their ids without recognising
601
+ * a compiled graph from a callback.
602
+ */
603
+ function nsParts(meta: Record<string, unknown>): string[] {
604
+ const ns = meta.langgraph_checkpoint_ns;
605
+ return typeof ns === "string" && ns ? ns.split("|") : [];
606
+ }
607
+
608
+ function errorName(error: unknown): string {
609
+ if (isObject(error)) {
610
+ if (typeof error.name === "string" && error.name) return error.name;
611
+ const ctor = (error as { constructor?: { name?: unknown } }).constructor?.name;
612
+ if (typeof ctor === "string" && ctor) return ctor;
613
+ }
614
+ return "Error";
615
+ }
616
+
617
+ function errorMessage(error: unknown): string {
618
+ if (error instanceof Error) return error.message;
619
+ if (isObject(error) && typeof error.message === "string") return error.message;
620
+ return String(error);
621
+ }
622
+
623
+ /** Names on the error AND its class chain — a subclass keeps its parent's meaning. */
624
+ function errorNames(error: unknown): string[] {
625
+ const names = [errorName(error)];
626
+ let proto: unknown = isObject(error) ? Object.getPrototypeOf(error) : null;
627
+ while (isObject(proto)) {
628
+ const ctor = (proto as { constructor?: { name?: unknown } }).constructor?.name;
629
+ if (typeof ctor === "string") names.push(ctor);
630
+ proto = Object.getPrototypeOf(proto);
631
+ }
632
+ return names;
633
+ }
634
+
635
+ export function isControlFlow(error: unknown): boolean {
636
+ if (!isObject(error)) return false;
637
+ if (error.is_bubble_up === true) return true;
638
+ return errorNames(error).some((name) => CONTROL_FLOW_NAMES.has(name));
639
+ }
640
+
641
+ export function isCancellation(error: unknown): boolean {
642
+ if (!isObject(error)) return false;
643
+ if (isScopeCancellation(error)) return true;
644
+ if (errorNames(error).some((name) => CANCELLATION_NAMES.has(name))) return true;
645
+ // LangChain's own abort sentinels, bare `Error`s identified only by their
646
+ // text: core's `raceWithSignal` rejects with "Aborted" when the signal
647
+ // carries no reason of its own, and LangGraph.js 0.x fails the graph's root
648
+ // run with "Abort".
649
+ return (
650
+ error instanceof Error &&
651
+ error.name === "Error" &&
652
+ (error.message === "Aborted" || error.message === "Abort")
653
+ );
654
+ }
655
+
656
+ function outcomeOf(error: unknown): string {
657
+ if (error === undefined || error === null) return "success";
658
+ if (isControlFlow(error)) return "paused";
659
+ // Before `failed`, or an abandoned stream reads as a crash.
660
+ if (isCancellation(error)) return "cancelled";
661
+ return "failed";
662
+ }
663
+
664
+ /**
665
+ * The error as one short line — `"Error: model exploded"` — never the stack.
666
+ * The stack belongs on an `error` event's `traceback`; inline in
667
+ * `tool_result.error` it is unreadable.
668
+ */
669
+ function errorText(error: unknown): string | undefined {
670
+ if (error === undefined || error === null || isControlFlow(error)) return undefined;
671
+ return core.truncate(`${errorName(error)}: ${errorMessage(error)}`, limit()) as string;
672
+ }
673
+
674
+ // ---------------------------------------------------------------------------
675
+ // Session resolution
676
+ // ---------------------------------------------------------------------------
677
+
678
+ /**
679
+ * Pick the session id for a root run, first that produces a value wins:
680
+ *
681
+ * 1. `instrument("langchain", { sessionId })`;
682
+ * 2. `metadata.failproofai_sdk_session_id` on the call — the documented
683
+ * per-call key, the one to use in a web service;
684
+ * 3. an enclosing `failproofai.session()` / `failproofai.agent()` scope, so a
685
+ * hand-written outer bracket and the adapter produce ONE session;
686
+ * 4. `metadata.session_id | conversation_id | thread_id`;
687
+ * 5. the root run id.
688
+ *
689
+ * Never synthesised from scratch: a made-up id splits one run into many
690
+ * sessions, a silent wrong answer rather than a loud one.
691
+ */
692
+ function resolveSessionId(id: string, meta: Record<string, unknown>): string {
693
+ if (state.options.sessionId) return state.options.sessionId;
694
+ const explicit = meta[SESSION_METADATA_KEY];
695
+ if (explicit !== undefined && explicit !== null && explicit !== "") return display(explicit);
696
+ const ambient = ambientSessionId();
697
+ if (ambient) return ambient;
698
+ for (const key of SESSION_METADATA_FALLBACKS) {
699
+ const value = meta[key];
700
+ if (value !== undefined && value !== null && value !== "") return display(value);
701
+ }
702
+ return id;
703
+ }
704
+
705
+ // ---------------------------------------------------------------------------
706
+ // Emission helpers
707
+ // ---------------------------------------------------------------------------
708
+
709
+ function emit(method: core.EventMethod, info: RunInfo, fields: Record<string, unknown>): void {
710
+ state.tracker.emit(method, info.id, { parentKey: info.parent, ...fields });
711
+ }
712
+
713
+ function emitOnAgent(session: Session, method: core.EventMethod, fields: Record<string, unknown>): void {
714
+ state.tracker.emit(method, session.agentKey, fields);
715
+ }
716
+
717
+ /**
718
+ * The `fw_*` extras every event from this adapter carries. Namespaced as a
719
+ * SAFETY rule: the schema merges extras last, so an extra called `tool_name`
720
+ * or `outcome` would silently overwrite the declared field.
721
+ */
722
+ function fwCommon(info: RunInfo): Record<string, unknown> {
723
+ const meta = info.meta;
724
+ return core.fwFields({
725
+ run_id: info.id,
726
+ parent_run_id: info.parent ?? undefined,
727
+ node: info.node ?? meta.langgraph_node,
728
+ step: meta.langgraph_step,
729
+ checkpoint_ns: meta.langgraph_checkpoint_ns,
730
+ thread_id: meta.thread_id,
731
+ tags: info.tags.length > 0 ? info.tags : undefined,
732
+ hidden: info.hidden ? true : undefined,
733
+ });
734
+ }
735
+
736
+ // ---------------------------------------------------------------------------
737
+ // Start
738
+ // ---------------------------------------------------------------------------
739
+
740
+ function onStart(args: StartArgs): void {
741
+ if (!state.enabled) return;
742
+ state.evict();
743
+ if (args.parent === null) state.sweepPaused(Date.now());
744
+ const info: RunInfo = {
745
+ id: args.id,
746
+ parent: args.parent,
747
+ name: args.name,
748
+ runType: args.runType,
749
+ started: Date.now(),
750
+ hidden: args.tags.includes(HIDDEN_TAG),
751
+ kind: "",
752
+ leafKind: "",
753
+ root: null,
754
+ session: null,
755
+ node: null,
756
+ toolCallId: null,
757
+ model: null,
758
+ ttftMs: null,
759
+ chunks: 0,
760
+ remote: null,
761
+ tags: args.tags,
762
+ meta: args.meta,
763
+ inputs: args.runType === "chain" ? args.inputs : undefined,
764
+ claimed: null,
765
+ activity: 0,
766
+ };
767
+ state.runs.set(info.id, info);
768
+ // Every run is linked, span or not: a tool three runnables deep still finds
769
+ // the agent above it by walking the chain, and an intermediate chain that
770
+ // emits nothing would otherwise break the walk.
771
+ state.tracker.link(info.id, info.parent);
772
+
773
+ if (info.parent === null) {
774
+ // A LangGraph node never runs outside its graph. One arriving as a root
775
+ // means the graph's own run began before the callback was installed.
776
+ if (nodeOf(info, info.meta) !== null) warnOrphan(info);
777
+ startRoot(info, args);
778
+ return;
779
+ }
780
+
781
+ const holder = state.runs.get(info.parent);
782
+ if (holder === undefined) warnOrphan(info);
783
+ info.root = holder?.root ?? null;
784
+ info.session = holder?.session ?? null;
785
+ touch(info.root);
786
+
787
+ const node = nodeOf(info, info.meta);
788
+ if (node !== null) {
789
+ info.kind = "node";
790
+ info.node = node;
791
+ startNode(info, args);
792
+ return;
793
+ }
794
+ if (info.runType === "llm" || info.runType === "chat_model") {
795
+ info.kind = "model";
796
+ startModel(info, args);
797
+ return;
798
+ }
799
+ if (info.runType === "tool") {
800
+ info.kind = "tool";
801
+ startTool(info, args);
802
+ return;
803
+ }
804
+ if (info.runType === "retriever") {
805
+ info.kind = "retriever";
806
+ startRetriever(info, args);
807
+ return;
808
+ }
809
+ // Everything else — RunnableSequence, prompt templates, output parsers,
810
+ // conditional-edge functions, a compiled subgraph's own run. Linked above and
811
+ // otherwise invisible unless explicitly allowlisted.
812
+ if (info.name && state.options.includeChains.has(info.name) && !info.hidden) {
813
+ info.kind = "chain";
814
+ emit("hookTriggered", info, {
815
+ hookName: info.name,
816
+ hookId: info.id,
817
+ triggerEvent: "pipeline",
818
+ input: shrink(args.inputs),
819
+ ...fwCommon(info),
820
+ });
821
+ }
822
+ }
823
+
824
+ /**
825
+ * A run whose parent this adapter never saw start. The usual cause is an
826
+ * `instrument()` that was not awaited: the graph's root run began before the
827
+ * callback was installed, so its children arrive with a parent nobody knows
828
+ * and a node or a model call ends up as the session's agent — a wrong trace,
829
+ * with nothing said. Warned once per process; the trace itself cannot be
830
+ * repaired after the fact.
831
+ */
832
+ let warnedOrphan = false;
833
+
834
+ function warnOrphan(info: RunInfo): void {
835
+ if (warnedOrphan || info.hidden) return;
836
+ warnedOrphan = true;
837
+ logger.warn(
838
+ `a LangChain run (${JSON.stringify(info.name)}) started under a parent run the langchain ` +
839
+ "adapter never saw (or, being a graph node, with no parent at all), so its trace is " +
840
+ "missing the root. Most often `instrument()` was not " +
841
+ "awaited before the run began — `await failproofai.instrument()` at startup, before the " +
842
+ "first invoke/stream.",
843
+ );
844
+ }
845
+
846
+ /** @internal Re-arm the once-per-process orphan warning, for tests. */
847
+ export function resetOrphanWarning(): void {
848
+ warnedOrphan = false;
849
+ }
850
+
851
+ /**
852
+ * The root run becomes the session's agent — and its FIRST event. A session is
853
+ * labelled by the first `agent_id` it saw, and the dashboard parents every leaf
854
+ * to the open agent with the same `agent_id`, synthesising a never-ending root
855
+ * span when there is none — so this must never be skipped or preceded.
856
+ */
857
+ function startRoot(info: RunInfo, args: StartArgs): void {
858
+ info.kind = "root";
859
+ info.root = info.id;
860
+ const sessionId = resolveSessionId(info.id, info.meta);
861
+
862
+ const existing = state.sessions.get(sessionId);
863
+ if (
864
+ existing !== undefined &&
865
+ existing.openPauses.size > 0 &&
866
+ state.tracker.isOpen(existing.agentKey) &&
867
+ isContinuation(args.inputs, info.meta)
868
+ ) {
869
+ // A resume: the previous `.invoke()` interrupted, we deliberately left its
870
+ // agent open, and this continues it. Both halves of the test are needed.
871
+ // "The session's agent is still open" is also true of two roots that merely
872
+ // OVERLAP under one session id — `.batch()`, two requests on one
873
+ // conversation id — and reading those as a resume folds one root into the
874
+ // other and drops its events. And an open pause bounds the window without
875
+ // closing it: any other run on the same session id while a human thinks —
876
+ // a different graph, a background summariser — would be recorded as the
877
+ // human's answer. LangGraph only continues an interrupted thread through a
878
+ // `Command` or a `null` input, so that is what is required.
879
+ info.session = existing;
880
+ state.tracker.link(info.id, existing.agentKey);
881
+ resume(existing, args.inputs);
882
+ return;
883
+ }
884
+
885
+ const identity = state.tracker.startAgent(info.id, {
886
+ agentId: core.normalizeAgentId(info.name, "agent"),
887
+ sessionId,
888
+ goal: goalOf(args),
889
+ ...fwCommon(info),
890
+ });
891
+ const session: Session = {
892
+ sessionId: identity.sessionId ?? sessionId,
893
+ agentKey: info.id,
894
+ agentId: identity.agentId ?? "agent",
895
+ openPauses: new Map(),
896
+ reportedError: false,
897
+ pausedAt: null,
898
+ };
899
+ info.session = session;
900
+ state.sessions.set(session.sessionId, session);
901
+
902
+ // A resume, but nothing in THIS process is paused — so the pause was opened
903
+ // by another process. That is the ordinary deployment shape (one worker
904
+ // serves the interrupt, whichever worker picks up the approval resumes
905
+ // against the shared checkpointer), and without this its human_wait and
906
+ // agent_pause stay open forever. See `closeRemotePause`.
907
+ const answer = resumeValue(args.inputs);
908
+ if (answer !== undefined && answer !== null) {
909
+ info.remote = { value: answer, levels: new Map(), deepest: null, done: new Set() };
910
+ }
911
+
912
+ // A root run that is ITSELF a leaf is recorded as one too. A bare
913
+ // `model.invoke()` handled only as a root produced agent_start/agent_end and
914
+ // nothing else — no model name, no tokens, no latency — while the trace
915
+ // still looked populated. The agent span stays; the leaf pair lands inside.
916
+ if (info.runType === "llm" || info.runType === "chat_model") {
917
+ info.leafKind = "model";
918
+ startModel(info, args);
919
+ } else if (info.runType === "tool") {
920
+ info.leafKind = "tool";
921
+ startTool(info, args);
922
+ } else if (info.runType === "retriever") {
923
+ info.leafKind = "retriever";
924
+ startRetriever(info, args);
925
+ }
926
+ }
927
+
928
+ function goalOf(args: StartArgs): string | undefined {
929
+ if (!state.options.captureContent) return undefined;
930
+ // A chat model's input is a list of message BATCHES; its goal is the last
931
+ // message of the last one, exactly as a graph's is the last of its state.
932
+ const batches = args.messages;
933
+ const lastBatch = Array.isArray(batches) && Array.isArray(batches[batches.length - 1])
934
+ ? (batches[batches.length - 1] as unknown[])
935
+ : batches;
936
+ const inputs = args.runType === "chat_model" ? { messages: lastBatch } : args.inputs;
937
+ if (inputs === undefined || inputs === null) return undefined;
938
+ if (isObject(inputs) && Array.isArray(inputs.messages) && inputs.messages.length > 0) {
939
+ const last = inputs.messages[inputs.messages.length - 1] as unknown;
940
+ const content = isObject(last) ? last.content : undefined;
941
+ if (typeof content === "string" && content) return core.truncate(content, 512) as string;
942
+ }
943
+ if (typeof inputs === "string") return core.truncate(inputs, 512) as string;
944
+ try {
945
+ return core.truncate(JSON.stringify(plain(inputs)), 512) as string;
946
+ } catch {
947
+ return undefined;
948
+ }
949
+ }
950
+
951
+ /**
952
+ * A LangGraph node -> `hook_triggered`. Also where a compiled SUBGRAPH becomes
953
+ * a nested agent: a node whose checkpoint namespace is more than one segment
954
+ * deep runs inside one, and its parent run IS the subgraph's own run. Deriving
955
+ * it here nests to any depth without recognising a compiled graph.
956
+ */
957
+ function startNode(info: RunInfo, args: StartArgs): void {
958
+ const parts = nsParts(info.meta);
959
+ if (parts.length > 1 && info.parent !== null) ensureSubgraphAgent(info, parts.slice(0, -1));
960
+
961
+ const remote = remoteOf(info);
962
+ if (remote !== null) {
963
+ // First node seen at a level wins: LangGraph re-runs the interrupted tasks
964
+ // in the level's first superstep and nothing else, so anything at a later
965
+ // step is ordinary downstream work.
966
+ const level = parts.slice(0, -1).join("|");
967
+ if (!remote.levels.has(level)) remote.levels.set(level, info.meta.langgraph_step);
968
+ }
969
+
970
+ if (info.hidden) return;
971
+ emit("hookTriggered", info, {
972
+ hookName: info.node,
973
+ hookId: info.id,
974
+ triggerEvent: "graph_node",
975
+ input: shrink(args.inputs),
976
+ ...fwCommon(info),
977
+ });
978
+ }
979
+
980
+ function ensureSubgraphAgent(info: RunInfo, prefix: string[]): void {
981
+ const key = info.parent;
982
+ if (key === null || state.tracker.isOpen(key)) return;
983
+ const holder = state.runs.get(key);
984
+ const session = info.session;
985
+ if (holder === undefined || session === null) return;
986
+ const names = prefix.filter(Boolean).map((part) => part.split(":")[0]!);
987
+ state.tracker.startAgent(key, {
988
+ agentId: [session.agentId, ...names].join("/"),
989
+ parentKey: holder.parent,
990
+ sessionId: session.sessionId,
991
+ ...core.fwFields({ run_id: key, subgraph: names[names.length - 1], kind: "subgraph" }),
992
+ });
993
+ holder.kind = "subgraph";
994
+ holder.session = session;
995
+ }
996
+
997
+ function startTool(info: RunInfo, args: StartArgs): void {
998
+ // The MODEL's tool-call id when there is one, so a tool_use joins to the
999
+ // `tool_calls[]` entry that asked for it and to the provider's own logs.
1000
+ info.toolCallId = args.toolCallId ?? recoverToolCallId(info, args.inputs) ?? info.id;
1001
+ if (info.hidden) return;
1002
+ emit("toolUse", info, {
1003
+ toolName: info.name || "tool",
1004
+ toolCallId: info.toolCallId,
1005
+ input: state.options.captureContent ? toolInput(args.inputs) : undefined,
1006
+ ...fwCommon(info),
1007
+ });
1008
+ }
1009
+
1010
+ /** `handleToolStart` hands over a string; the tool's arguments are JSON inside it. */
1011
+ function toolInput(input: unknown): Record<string, unknown> | undefined {
1012
+ if (input === undefined || input === null) return undefined;
1013
+ if (isPlainObject(input)) return core.truncate(plain(input), limit()) as Record<string, unknown>;
1014
+ return { input: core.truncate(plain(input), limit()) };
1015
+ }
1016
+
1017
+ function parseToolInput(input: unknown): unknown {
1018
+ if (typeof input !== "string") return input;
1019
+ const text = input.trim();
1020
+ if (!text.startsWith("{")) return input;
1021
+ try {
1022
+ return JSON.parse(text) as unknown;
1023
+ } catch {
1024
+ return input;
1025
+ }
1026
+ }
1027
+
1028
+ /** Order-insensitive JSON, for comparing a tool call's `args` with the tool's parsed input. */
1029
+ function canonical(value: unknown): string {
1030
+ const sort = (item: unknown): unknown => {
1031
+ if (Array.isArray(item)) return item.map(sort);
1032
+ if (isPlainObject(item)) {
1033
+ return Object.fromEntries(
1034
+ Object.keys(item)
1035
+ .sort()
1036
+ .map((key) => [key, sort(item[key])]),
1037
+ );
1038
+ }
1039
+ return item;
1040
+ };
1041
+ try {
1042
+ return JSON.stringify(sort(value)) ?? "";
1043
+ } catch {
1044
+ return "";
1045
+ }
1046
+ }
1047
+
1048
+ /**
1049
+ * The model's tool-call id, on a core that does not pass one.
1050
+ *
1051
+ * `@langchain/core` 1.x hands `handleToolStart` the id as its eighth argument;
1052
+ * 0.3 does not, and neither does it put it anywhere a callback can read at
1053
+ * start. Without it every `tool_use` on 0.3 carried the tool's RUN id, which
1054
+ * joins to nothing: not the assistant message's `tool_calls[]`, not the
1055
+ * provider's logs.
1056
+ *
1057
+ * It is recoverable, exactly, in the case that matters — a tool run under a
1058
+ * `ToolNode` or any runnable fed the conversation. The nearest ancestor whose
1059
+ * input carries messages holds the assistant message that asked for this call,
1060
+ * and its `tool_calls[]` entry names the tool and carries the same arguments
1061
+ * the tool was invoked with. Each id is claimed once per ancestor, so two calls
1062
+ * to one tool with identical arguments still get two different ids. When there
1063
+ * is no such ancestor, or no entry matches, the run id stands, as before.
1064
+ */
1065
+ function recoverToolCallId(info: RunInfo, input: unknown): string | null {
1066
+ const args = canonical(parseToolInput(input));
1067
+ let key = info.parent;
1068
+ const seen = new Set<string>();
1069
+ while (key !== null && !seen.has(key)) {
1070
+ seen.add(key);
1071
+ const holder = state.runs.get(key);
1072
+ if (holder === undefined) return null;
1073
+ const inputs = holder.inputs;
1074
+ const messages = Array.isArray(inputs) ? inputs : isObject(inputs) ? inputs.messages : undefined;
1075
+ if (Array.isArray(messages)) {
1076
+ holder.claimed ??= new Set();
1077
+ const claimed = holder.claimed;
1078
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
1079
+ const calls = (messages[i] as { tool_calls?: unknown } | undefined)?.tool_calls;
1080
+ if (!Array.isArray(calls) || calls.length === 0) continue;
1081
+ const open = (calls as Array<{ id?: unknown; name?: unknown; args?: unknown }>).filter(
1082
+ (call) => typeof call.id === "string" && call.name === info.name && !claimed.has(call.id),
1083
+ );
1084
+ const match = open.find((call) => canonical(call.args) === args) ?? (open.length === 1 ? open[0] : undefined);
1085
+ if (match === undefined) return null;
1086
+ claimed.add(match.id as string);
1087
+ return match.id as string;
1088
+ }
1089
+ return null;
1090
+ }
1091
+ key = holder.parent;
1092
+ }
1093
+ return null;
1094
+ }
1095
+
1096
+ function startRetriever(info: RunInfo, args: StartArgs): void {
1097
+ info.toolCallId = info.id;
1098
+ if (info.hidden) return;
1099
+ emit("toolUse", info, {
1100
+ toolName: `retriever:${info.name || "retriever"}`,
1101
+ toolCallId: info.toolCallId,
1102
+ input: state.options.captureContent ? { query: core.truncate(args.inputs, limit()) } : undefined,
1103
+ ...fwCommon(info),
1104
+ });
1105
+ }
1106
+
1107
+ function startModel(info: RunInfo, args: StartArgs): void {
1108
+ info.model = modelNameOf(info, args.invocationParams);
1109
+ if (info.hidden) return;
1110
+ const messages =
1111
+ args.runType === "chat_model"
1112
+ ? normalizeMessages(args.messages)
1113
+ : promptsAsMessages((args.inputs as { prompts?: unknown } | undefined)?.prompts);
1114
+ emit("modelRequest", info, {
1115
+ // The correlation id the dashboard pairs a request with its response on.
1116
+ requestId: info.id,
1117
+ model: info.model,
1118
+ messages: state.options.captureContent ? messages : undefined,
1119
+ tools: toolsOf(args.invocationParams),
1120
+ ...fwCommon(info),
1121
+ });
1122
+ }
1123
+
1124
+ /**
1125
+ * `ls_model_name` first — the LangSmith standard key a real provider
1126
+ * integration sets — then the invocation params, then the model's own name.
1127
+ * The fallbacks are load-bearing: fakes and some community integrations set no
1128
+ * `ls_model_name` at all.
1129
+ */
1130
+ function modelNameOf(info: RunInfo, params: Record<string, unknown> | undefined): string {
1131
+ const name = info.meta.ls_model_name;
1132
+ if (typeof name === "string" && name) return name;
1133
+ for (const key of ["model_name", "model", "modelName", "model_id", "deployment_name"]) {
1134
+ const value = params?.[key];
1135
+ if (typeof value === "string" && value) return value;
1136
+ }
1137
+ return info.name || "unknown";
1138
+ }
1139
+
1140
+ function toolsOf(params: Record<string, unknown> | undefined): Array<Record<string, unknown>> | undefined {
1141
+ const tools = params?.tools;
1142
+ if (!Array.isArray(tools) || tools.length === 0) return undefined;
1143
+ return core.truncate(tools, limit()) as Array<Record<string, unknown>>;
1144
+ }
1145
+
1146
+ const ROLES: Record<string, string> = { human: "user", ai: "assistant", system: "system", tool: "tool" };
1147
+
1148
+ /**
1149
+ * Chat messages as `{role, content, tool_calls?}`, from the LAST batch. They
1150
+ * arrive as real `BaseMessage` objects here, which is the one place their
1151
+ * roles are still intact.
1152
+ */
1153
+ export function normalizeMessages(batches: unknown): Array<Record<string, unknown>> | undefined {
1154
+ if (!Array.isArray(batches) || batches.length === 0) return undefined;
1155
+ const last = batches[batches.length - 1] as unknown;
1156
+ const batch = Array.isArray(last) ? last : batches;
1157
+ return batch.map((message: unknown) => {
1158
+ const kind = messageType(message) ?? (isObject(message) && typeof message.role === "string" ? message.role : "");
1159
+ const value = isObject(message) ? message : {};
1160
+ const entry: Record<string, unknown> = {
1161
+ role: ROLES[kind] ?? (kind || "user"),
1162
+ content: core.truncate(plain(value.content ?? ""), limit()),
1163
+ };
1164
+ if (Array.isArray(value.tool_calls) && value.tool_calls.length > 0) {
1165
+ entry.tool_calls = core.truncate(plain(value.tool_calls), limit());
1166
+ }
1167
+ return entry;
1168
+ });
1169
+ }
1170
+
1171
+ function promptsAsMessages(prompts: unknown): Array<Record<string, unknown>> | undefined {
1172
+ if (!Array.isArray(prompts)) return undefined;
1173
+ return prompts.map((prompt: unknown) => ({ role: "user", content: core.truncate(prompt, limit()) }));
1174
+ }
1175
+
1176
+ // ---------------------------------------------------------------------------
1177
+ // End
1178
+ // ---------------------------------------------------------------------------
1179
+
1180
+ function onEnd(id: string, end: EndArgs): void {
1181
+ const info = state.runs.get(id);
1182
+ if (info === undefined) return;
1183
+ const error = end.error;
1184
+ touch(info.root);
1185
+
1186
+ if (info.kind === "root") {
1187
+ // Close the leaf pair first when the root was also a leaf: the dashboard
1188
+ // closes the agent span at `agent_end`, so a `model_response` after it is
1189
+ // attributed to nothing.
1190
+ if (info.leafKind) {
1191
+ endLeaf(info.leafKind, info, end);
1192
+ // ...and that leaf OWNS the failure, exactly as a nested one does.
1193
+ // Without this a failing top-level `tool.invoke()` counted twice: once as
1194
+ // `tool_result.error` and again as a standalone `error` event.
1195
+ if (info.session !== null && error !== undefined && error !== null && !isControlFlow(error)) {
1196
+ info.session.reportedError = true;
1197
+ }
1198
+ }
1199
+ endRoot(info, error);
1200
+ return;
1201
+ }
1202
+
1203
+ state.runs.delete(id);
1204
+
1205
+ if (info.kind === "subgraph" || state.tracker.isOpen(id)) {
1206
+ state.tracker.endAgent(id, { outcome: outcomeOf(error), summary: errorText(error) });
1207
+ }
1208
+ // `owned` records whether a SPAN was actually emitted for this run, which is
1209
+ // what decides `reportedError` below.
1210
+ let owned = false;
1211
+ if (info.kind === "node" || info.kind === "chain") {
1212
+ endHook(info, end);
1213
+ owned = !info.hidden;
1214
+ } else if (info.kind === "tool" || info.kind === "retriever" || info.kind === "model") {
1215
+ endLeaf(info.kind, info, end);
1216
+ owned = !info.hidden;
1217
+ }
1218
+
1219
+ if (info.kind === "node") {
1220
+ // Strictly BEFORE the suspend below: a node that answers one interrupt and
1221
+ // raises the next must close the old pause before opening the new one.
1222
+ closeRemotePause(info);
1223
+ }
1224
+
1225
+ // The exception-path HITL: every LangGraph interrupt surfaces here, as the
1226
+ // node's `handleChainError`. Outside the span handling on purpose, so it
1227
+ // still fires for a hidden node and for a subgraph that bubbled the
1228
+ // interrupt up. `suspend` dedups on the interrupt id, so this and
1229
+ // `handleInterrupt` cannot double-emit.
1230
+ const interrupts = interruptsOf(error);
1231
+ if (interrupts.length > 0 && info.session !== null) suspend(info.session, interrupts);
1232
+
1233
+ if (owned && error !== undefined && error !== null && !isControlFlow(error) && info.session !== null) {
1234
+ // Only when a span reported it. The failures nobody owned — a
1235
+ // RunnableSequence step, an output parser, a hidden run — must still reach
1236
+ // the root's one standalone `error` event; the ones a span DID report must
1237
+ // not be counted twice.
1238
+ info.session.reportedError = true;
1239
+ }
1240
+
1241
+ if (isCancellation(error) && info.root !== null) reapIfAbandoned(info.root);
1242
+ // Last, after every event above has resolved through it. See `RunTracker.unlink`.
1243
+ state.tracker.unlink(id);
1244
+ }
1245
+
1246
+ function touch(rootId: string | null): void {
1247
+ const root = rootId !== null ? state.runs.get(rootId) : undefined;
1248
+ if (root !== undefined) root.activity += 1;
1249
+ }
1250
+
1251
+ /** How long an aborted root may stay silent before it is closed for LangGraph. */
1252
+ export const ABANDONED_ROOT_GRACE_MS = 3_000;
1253
+
1254
+ /**
1255
+ * Close a root that was aborted and will never be told so.
1256
+ *
1257
+ * An `AbortSignal` firing inside `graph.invoke()` on LangGraph.js 1.x ends the
1258
+ * node's run with an `AbortError` and then abandons the graph's OWN run: the
1259
+ * invoke races the signal and returns, the stream generator is never resumed,
1260
+ * and no `handleChainEnd` or `handleChainError` ever arrives for the root
1261
+ * (VERIFIED on 1.4.17; 0.4.10 does report it, as `Error("Abort")`). Without
1262
+ * this the session reads as running forever — the JavaScript face of the
1263
+ * `GeneratorExit` case the Python adapter closes as `cancelled`.
1264
+ *
1265
+ * It cannot be closed at the node's error: a node's own `AbortError` — its
1266
+ * fetch timed out — can be retried, and the graph carries on. So the root is
1267
+ * closed only if, a grace period later, it is still open, nothing under it is
1268
+ * still running, and NOTHING under it has happened since: any retry, any new
1269
+ * node, any end callback bumps `activity` and the reap stands down. The timer
1270
+ * is `unref`'d, so it never holds a process open; a script that exits first
1271
+ * leaves the root open exactly as a crash would.
1272
+ */
1273
+ function reapIfAbandoned(rootId: string): void {
1274
+ const root = state.runs.get(rootId);
1275
+ if (root === undefined) return;
1276
+ const seen = root.activity;
1277
+ const timer = setTimeout(() => {
1278
+ core.callSafely(
1279
+ () => {
1280
+ if (!state.enabled || state.runs.get(rootId) !== root || root.activity !== seen) return;
1281
+ for (const info of state.runs.values()) if (info.root === rootId && info.id !== rootId) return;
1282
+ const abort = new Error("the run was aborted and LangGraph never closed it");
1283
+ abort.name = "AbortError";
1284
+ endRoot(root, abort);
1285
+ },
1286
+ [],
1287
+ `${NAME}.reapIfAbandoned`,
1288
+ );
1289
+ }, ABANDONED_ROOT_GRACE_MS);
1290
+ timer.unref?.();
1291
+ }
1292
+
1293
+ function endLeaf(kind: "tool" | "retriever" | "model", info: RunInfo, end: EndArgs): void {
1294
+ if (info.hidden) return;
1295
+ if (kind === "tool") endTool(info, end);
1296
+ else if (kind === "retriever") endRetriever(info, end);
1297
+ else endModel(info, end);
1298
+ }
1299
+
1300
+ function endHook(info: RunInfo, end: EndArgs): void {
1301
+ if (info.hidden) return;
1302
+ emit("hookCompleted", info, {
1303
+ hookName: info.node ?? info.name,
1304
+ hookId: info.id,
1305
+ // "paused" for an interrupt: the node did not fail, it stopped to ask a
1306
+ // human. "failed", never "failure" — the server counts only
1307
+ // error|failed|timeout|rejected.
1308
+ outcome: outcomeOf(end.error),
1309
+ output: shrink(end.outputs),
1310
+ error: errorText(end.error),
1311
+ ...fwCommon(info),
1312
+ });
1313
+ }
1314
+
1315
+ /**
1316
+ * The tool's actual result, plus an error when it failed quietly.
1317
+ *
1318
+ * A tool invoked with a `ToolCall` — what every tool loop does — returns a
1319
+ * `ToolMessage`, not a string, and rendering that object is not the result.
1320
+ * `status: "error"` is the second half: a tool whose exception the framework
1321
+ * turned into a message for the model has no error anywhere else, so without
1322
+ * this the failure had no representation at all.
1323
+ */
1324
+ export function toolOutput(output: unknown): { output: unknown; failed?: string } {
1325
+ if (messageType(output) !== "tool") return { output };
1326
+ const message = output as { content?: unknown; status?: unknown };
1327
+ if (message.status === "error") {
1328
+ const text = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
1329
+ return { output: message.content, failed: core.truncate(text, limit()) as string };
1330
+ }
1331
+ return { output: message.content };
1332
+ }
1333
+
1334
+ function endTool(info: RunInfo, end: EndArgs): void {
1335
+ const { output, failed } = toolOutput(end.outputs);
1336
+ emit("toolResult", info, {
1337
+ toolName: info.name || "tool",
1338
+ toolCallId: info.toolCallId ?? info.id,
1339
+ output: shrink(output),
1340
+ error: errorText(end.error) ?? failed,
1341
+ ...fwCommon(info),
1342
+ });
1343
+ }
1344
+
1345
+ function endRetriever(info: RunInfo, end: EndArgs): void {
1346
+ emit("toolResult", info, {
1347
+ toolName: `retriever:${info.name || "retriever"}`,
1348
+ toolCallId: info.toolCallId ?? info.id,
1349
+ output: summarizeDocuments(end.outputs),
1350
+ error: errorText(end.error),
1351
+ ...fwCommon(info),
1352
+ });
1353
+ }
1354
+
1355
+ /**
1356
+ * `{n, sources}` — never the document text. Twenty 4 KB chunks per retrieval
1357
+ * would put 80 KB of prose into one event on every hop of every RAG loop. The
1358
+ * count is structure and survives `captureContent: false`; the sources do not,
1359
+ * because a source is a document path and on regulated data that path is
1360
+ * content.
1361
+ */
1362
+ export function summarizeDocuments(documents: unknown): Record<string, unknown> | undefined {
1363
+ if (!Array.isArray(documents)) return undefined;
1364
+ if (!state.options.captureContent) return { n: documents.length };
1365
+ const sources = documents.slice(0, 10).map((doc: unknown, index) => {
1366
+ const meta = isObject(doc) && isObject(doc.metadata) ? doc.metadata : {};
1367
+ const source = meta.source ?? meta.id ?? meta.file_path;
1368
+ return core.truncate(source ? display(source) : `doc[${index}]`, 256);
1369
+ });
1370
+ return { n: documents.length, sources };
1371
+ }
1372
+
1373
+ function endModel(info: RunInfo, end: EndArgs): void {
1374
+ const usage = usageOf(end.response);
1375
+ const completion = completionOf(end.response);
1376
+ const extras: Record<string, unknown> = { ...fwCommon(info) };
1377
+ if (info.chunks > 0) {
1378
+ Object.assign(extras, core.fwFields({ streamed: true, chunks: info.chunks, ttft_ms: info.ttftMs ?? 0 }));
1379
+ }
1380
+ emit("modelResponse", info, {
1381
+ requestId: info.id,
1382
+ model: info.model,
1383
+ stopReason: end.error !== undefined && end.error !== null ? "error" : completion.stopReason,
1384
+ content: state.options.captureContent ? completion.content : undefined,
1385
+ role: completion.role,
1386
+ inputTokens: usage?.input_tokens,
1387
+ outputTokens: usage?.output_tokens,
1388
+ // Shipped as an object as well: both server-side summaries fall back to
1389
+ // `payload.usage` for tokens.
1390
+ usage,
1391
+ error: errorText(end.error),
1392
+ // ALWAYS set, always an integer. The dashboard prefers the closing event's
1393
+ // duration, which is what keeps model durations honest when concurrent
1394
+ // calls pair up by arrival, and a float would NULL the u32 column.
1395
+ duration_ms: core.ms(Date.now() - info.started),
1396
+ ...extras,
1397
+ });
1398
+ }
1399
+
1400
+ interface Usage {
1401
+ input_tokens?: number;
1402
+ output_tokens?: number;
1403
+ total_tokens?: number;
1404
+ input_token_details?: unknown;
1405
+ output_token_details?: unknown;
1406
+ }
1407
+
1408
+ const asInt = (value: unknown): number | undefined =>
1409
+ typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : undefined;
1410
+
1411
+ function firstGeneration(response: unknown): Loose | undefined {
1412
+ const generations = (response as { generations?: unknown } | undefined)?.generations;
1413
+ if (!Array.isArray(generations) || generations.length === 0) return undefined;
1414
+ const first = generations[0] as unknown;
1415
+ const generation = Array.isArray(first) ? (first[0] as unknown) : first;
1416
+ return isObject(generation) ? generation : undefined;
1417
+ }
1418
+
1419
+ /**
1420
+ * Token counts from wherever this provider put them.
1421
+ *
1422
+ * `usage_metadata` on the generated message is primary — LangChain's standard
1423
+ * shape and the only one carrying cache/reasoning detail. The fallbacks are
1424
+ * `llmOutput`: `tokenUsage` / `estimatedTokenUsage` (camelCase, the JS OpenAI
1425
+ * integration) and `token_usage` / `usage` (snake_case). Reading one place only
1426
+ * is how an adapter ends up with an empty token column for half the providers,
1427
+ * at 200 OK, with nothing logged.
1428
+ */
1429
+ export function usageOf(response: unknown): Usage | undefined {
1430
+ const message = firstGeneration(response)?.message;
1431
+ const data = isObject(message) ? message.usage_metadata : undefined;
1432
+ if (isObject(data) && Object.keys(data).length > 0) {
1433
+ const usage: Usage = {
1434
+ input_tokens: asInt(data.input_tokens),
1435
+ output_tokens: asInt(data.output_tokens),
1436
+ total_tokens: asInt(data.total_tokens),
1437
+ };
1438
+ for (const key of ["input_token_details", "output_token_details"] as const) {
1439
+ if (isObject(data[key]) && Object.keys(data[key]).length > 0) usage[key] = { ...data[key] };
1440
+ }
1441
+ return compact(usage);
1442
+ }
1443
+ const output = (response as { llmOutput?: unknown } | undefined)?.llmOutput;
1444
+ if (!isObject(output)) return undefined;
1445
+ for (const raw of [output.tokenUsage, output.estimatedTokenUsage, output.token_usage, output.usage]) {
1446
+ if (!isObject(raw)) continue;
1447
+ const usage: Usage = {
1448
+ input_tokens: asInt(raw.promptTokens ?? raw.prompt_tokens ?? raw.input_tokens),
1449
+ output_tokens: asInt(raw.completionTokens ?? raw.completion_tokens ?? raw.output_tokens),
1450
+ total_tokens: asInt(raw.totalTokens ?? raw.total_tokens),
1451
+ };
1452
+ if (usage.input_tokens === undefined && usage.output_tokens === undefined) continue;
1453
+ usage.total_tokens ??= (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
1454
+ return compact(usage);
1455
+ }
1456
+ return undefined;
1457
+ }
1458
+
1459
+ function compact(usage: Usage): Usage | undefined {
1460
+ const out = Object.fromEntries(Object.entries(usage).filter(([, value]) => value !== undefined)) as Usage;
1461
+ return Object.keys(out).length > 0 ? out : undefined;
1462
+ }
1463
+
1464
+ function completionOf(response: unknown): { content?: unknown; role?: string; stopReason?: string } {
1465
+ const generation = firstGeneration(response);
1466
+ if (generation === undefined) return {};
1467
+ const message = isObject(generation.message) ? generation.message : undefined;
1468
+ const content = message?.content ?? generation.text;
1469
+ let stopReason: string | undefined;
1470
+ for (const source of [generation.generationInfo, message?.response_metadata]) {
1471
+ if (!isObject(source)) continue;
1472
+ for (const key of ["finish_reason", "stop_reason", "finishReason", "stopReason"]) {
1473
+ const value = source[key];
1474
+ if (typeof value === "string" && value) {
1475
+ stopReason = value;
1476
+ break;
1477
+ }
1478
+ }
1479
+ if (stopReason !== undefined) break;
1480
+ }
1481
+ return {
1482
+ content: core.truncate(plain(content), limit()),
1483
+ role: message !== undefined ? "assistant" : undefined,
1484
+ stopReason,
1485
+ };
1486
+ }
1487
+
1488
+ function endRoot(info: RunInfo, error: unknown): void {
1489
+ const session = info.session;
1490
+ state.runs.delete(info.id);
1491
+ closeOpenLeaves(info.id);
1492
+ // A resumed root is linked to the session's agent rather than being one, so
1493
+ // `endAgent` below would not clear its link.
1494
+ state.tracker.unlink(info.id);
1495
+ if (session === null) return;
1496
+
1497
+ if (session.openPauses.size > 0) {
1498
+ // Interrupted, waiting on a human. Deliberately no `agent_end`: closing the
1499
+ // agent force-closes the open pause, zeroing the one interval that
1500
+ // measures how long the human took. The resuming `.invoke()` closes it —
1501
+ // here, or on another worker, in which case this process forgets it after
1502
+ // `PAUSED_SESSION_TTL_MS` (see `State.sweepPaused`).
1503
+ session.pausedAt = Date.now();
1504
+ return;
1505
+ }
1506
+
1507
+ const cancelled = isCancellation(error);
1508
+ const failed = error !== undefined && error !== null && !isControlFlow(error) && !cancelled;
1509
+
1510
+ if (failed && !session.reportedError) {
1511
+ // Nothing below reported this failure, so nobody owns it — a standalone
1512
+ // `error` is the only way it reaches the Errors surface. Strictly before
1513
+ // `agent_end`, which closes the span it would be attributed to.
1514
+ emitOnAgent(session, "error", {
1515
+ errorType: errorName(error),
1516
+ // The bare message: the server renders `<error_type>: <message>`, and the
1517
+ // prefixed form would read "Error: Error: ...".
1518
+ message: core.truncate(errorMessage(error), limit()) || errorName(error),
1519
+ traceback: error instanceof Error && error.stack ? core.truncate(error.stack, core.FIELD_LIMIT) : undefined,
1520
+ ...fwCommon(info),
1521
+ });
1522
+ }
1523
+
1524
+ state.tracker.endAgent(session.agentKey, {
1525
+ outcome: cancelled ? "cancelled" : failed ? "failed" : "success",
1526
+ summary: failed ? errorText(error) : undefined,
1527
+ ...fwCommon(info),
1528
+ });
1529
+ if (state.sessions.get(session.sessionId) === session) state.sessions.delete(session.sessionId);
1530
+ }
1531
+
1532
+ /**
1533
+ * Close every leaf still open under this root. `agent_end` force-closes open
1534
+ * pauses but not tools, models or hooks, so a run that dies mid-tool — a hard
1535
+ * cancellation, a killed stream, a framework that skipped an end callback —
1536
+ * would otherwise leave the session `ongoing` forever.
1537
+ */
1538
+ function closeOpenLeaves(rootId: string): void {
1539
+ const stale = [...state.runs.values()].filter((info) => info.root === rootId && info.id !== rootId);
1540
+ for (const info of stale.reverse()) {
1541
+ state.runs.delete(info.id);
1542
+ if (info.hidden || !info.kind) {
1543
+ state.tracker.unlink(info.id);
1544
+ continue;
1545
+ }
1546
+ const marker = core.fwFields({ incomplete: true });
1547
+ core.callSafely(
1548
+ () => {
1549
+ if (info.kind === "tool" || info.kind === "retriever") {
1550
+ emit("toolResult", info, {
1551
+ toolName: info.kind === "retriever" ? `retriever:${info.name || "retriever"}` : info.name || "tool",
1552
+ toolCallId: info.toolCallId ?? info.id,
1553
+ ...marker,
1554
+ });
1555
+ } else if (info.kind === "node" || info.kind === "chain") {
1556
+ emit("hookCompleted", info, {
1557
+ hookName: info.node ?? info.name,
1558
+ hookId: info.id,
1559
+ outcome: "cancelled",
1560
+ ...marker,
1561
+ });
1562
+ } else if (info.kind === "model") {
1563
+ emit("modelResponse", info, {
1564
+ requestId: info.id,
1565
+ model: info.model,
1566
+ stopReason: "incomplete",
1567
+ duration_ms: core.ms(Date.now() - info.started),
1568
+ ...marker,
1569
+ });
1570
+ } else if (info.kind === "subgraph") {
1571
+ state.tracker.endAgent(info.id, { outcome: "cancelled", ...marker });
1572
+ }
1573
+ },
1574
+ [],
1575
+ `${NAME}.closeOpenLeaves`,
1576
+ );
1577
+ state.tracker.unlink(info.id);
1578
+ }
1579
+ }
1580
+
1581
+ // ---------------------------------------------------------------------------
1582
+ // Human in the loop
1583
+ // ---------------------------------------------------------------------------
1584
+
1585
+ interface InterruptLike {
1586
+ id?: unknown;
1587
+ value?: unknown;
1588
+ }
1589
+
1590
+ /**
1591
+ * The interrupts a `GraphInterrupt` carries. `ParentCommand` and
1592
+ * `GraphDrained` are bubble-ups too but carry a command / a reason, which is
1593
+ * what the `value` check keeps out.
1594
+ */
1595
+ function interruptsOf(error: unknown): InterruptLike[] {
1596
+ if (!isControlFlow(error)) return [];
1597
+ const interrupts = (error as { interrupts?: unknown }).interrupts;
1598
+ if (!Array.isArray(interrupts)) return [];
1599
+ return interrupts.filter((item): item is InterruptLike => isObject(item) && "value" in item);
1600
+ }
1601
+
1602
+ /**
1603
+ * `human_wait` + `agent_pause`, one pair per interrupt, in that order. Both are
1604
+ * required: only `agent_pause` -> `agent_resume` feeds the session's paused
1605
+ * time, and only `human_wait` -> `human_input` carries the prompt and the
1606
+ * answer.
1607
+ */
1608
+ function suspend(session: Session, interrupts: readonly InterruptLike[]): void {
1609
+ interrupts.forEach((interrupt, index) => {
1610
+ const pauseId =
1611
+ typeof interrupt.id === "string" && interrupt.id ? interrupt.id : `${session.agentKey}:${index}`;
1612
+ if (session.openPauses.has(pauseId)) return;
1613
+ const { prompt, options } = promptOf(interrupt.value);
1614
+ session.openPauses.set(pauseId, prompt);
1615
+ // `captureContent: false` covers these: in a real HITL graph the interrupt
1616
+ // payload IS the record being approved, and the answer is a human's free
1617
+ // text — the two most sensitive strings in the run.
1618
+ const capture = state.options.captureContent;
1619
+ emitOnAgent(session, "humanWait", {
1620
+ inputId: pauseId,
1621
+ prompt: capture ? prompt : undefined,
1622
+ options: capture ? options : undefined,
1623
+ reason: "langgraph_interrupt",
1624
+ ...core.fwFields({ interrupt_id: pauseId, kind: "interrupt" }),
1625
+ });
1626
+ emitOnAgent(session, "agentPause", {
1627
+ pauseId,
1628
+ reason: "langgraph_interrupt",
1629
+ ...core.fwFields({ interrupt_id: pauseId }),
1630
+ });
1631
+ });
1632
+ }
1633
+
1634
+ /** A value as text: strings as-is, everything else as JSON of its payload view. */
1635
+ function display(value: unknown): string {
1636
+ if (typeof value === "string") return value;
1637
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1638
+ return value.toString();
1639
+ }
1640
+ try {
1641
+ return JSON.stringify(plain(value)) ?? String(value);
1642
+ } catch {
1643
+ return String(value);
1644
+ }
1645
+ }
1646
+
1647
+ export function promptOf(value: unknown): { prompt?: string; options?: string[] } {
1648
+ if (value === undefined || value === null) return {};
1649
+ if (isPlainObject(value)) {
1650
+ const prompt = value.prompt ?? value.question ?? value.message;
1651
+ const options = Array.isArray(value.options) ? value.options.map((option: unknown) => display(option)) : undefined;
1652
+ const text = prompt !== undefined && prompt !== null ? display(prompt) : display(value);
1653
+ return { prompt: core.truncate(text, limit()) as string, options };
1654
+ }
1655
+ return { prompt: core.truncate(display(value), limit()) as string };
1656
+ }
1657
+
1658
+ /** `agent_resume` + `human_input`, in that order, one pair per open pause. */
1659
+ function resume(session: Session, inputs: unknown): void {
1660
+ session.pausedAt = null;
1661
+ if (session.openPauses.size === 0) return;
1662
+ const answers = resumeValue(inputs);
1663
+ const capture = state.options.captureContent;
1664
+ for (const [pauseId, prompt] of [...session.openPauses]) {
1665
+ session.openPauses.delete(pauseId);
1666
+ emitOnAgent(session, "agentResume", {
1667
+ pauseId,
1668
+ reason: "langgraph_resume",
1669
+ ...core.fwFields({ interrupt_id: pauseId }),
1670
+ });
1671
+ emitOnAgent(session, "humanInput", {
1672
+ inputId: pauseId,
1673
+ response: capture ? answerFor(answers, pauseId) : undefined,
1674
+ ...core.fwFields({ interrupt_id: pauseId, prompt: capture ? prompt : undefined }),
1675
+ });
1676
+ }
1677
+ }
1678
+
1679
+ const MISSING = Symbol("missing");
1680
+
1681
+ function isCommand(value: unknown): value is { resume?: unknown } {
1682
+ if (!isObject(value)) return false;
1683
+ if (value.lg_name === "Command") return true;
1684
+ // Duck-typed fallback for a moved or renamed `Command`.
1685
+ return "resume" in value && "goto" in value;
1686
+ }
1687
+
1688
+ /**
1689
+ * What `.invoke()` was called with, when it was NOT fresh state.
1690
+ *
1691
+ * LangGraph.js hands the root's `handleChainStart` a `Command` itself (it is an
1692
+ * object, so `_coerceToDict` passes it through), and wraps anything that is not
1693
+ * an object under a single `input` key — `{input: null}` for
1694
+ * `invoke(null, config)`. Fresh state arrives as the state object. So a
1695
+ * `Command`, or an `input` key, is what separates "steering an existing
1696
+ * checkpointed run" from "starting a new one".
1697
+ */
1698
+ function steeringValue(inputs: unknown): unknown {
1699
+ if (isCommand(inputs)) return inputs;
1700
+ if (isPlainObject(inputs) && "input" in inputs) return inputs.input;
1701
+ return MISSING;
1702
+ }
1703
+
1704
+ /** Is this run a LangGraph invocation at all? A resume always is. */
1705
+ function isGraphRun(meta: Record<string, unknown>): boolean {
1706
+ return (
1707
+ "langgraph_checkpoint_ns" in meta ||
1708
+ "checkpoint_ns" in meta ||
1709
+ "thread_id" in meta ||
1710
+ "langgraph_step" in meta
1711
+ );
1712
+ }
1713
+
1714
+ /**
1715
+ * True when this root run continues an interrupted thread: a `Command`, or a
1716
+ * `null` input to a graph. A bare `null` alone is not enough — any runnable
1717
+ * invoked with no argument produces the same `{input: null}` shape, and
1718
+ * reading an unrelated heartbeat as the human's answer would fabricate an
1719
+ * approval nobody gave.
1720
+ */
1721
+ function isContinuation(inputs: unknown, meta: Record<string, unknown>): boolean {
1722
+ const value = steeringValue(inputs);
1723
+ if (value === MISSING) return false;
1724
+ if (value === undefined || value === null) return isGraphRun(meta);
1725
+ return isCommand(value);
1726
+ }
1727
+
1728
+ /** The value handed to `Command({ resume })`, read off the root run's input. */
1729
+ function resumeValue(inputs: unknown): unknown {
1730
+ const value = steeringValue(inputs);
1731
+ return value !== MISSING && isCommand(value) ? value.resume : undefined;
1732
+ }
1733
+
1734
+ function answerFor(answers: unknown, pauseId: string): string | undefined {
1735
+ if (answers === undefined || answers === null) return undefined;
1736
+ if (isPlainObject(answers) && pauseId in answers) {
1737
+ return core.truncate(display(answers[pauseId]), limit()) as string;
1738
+ }
1739
+ return core.truncate(display(answers), limit()) as string;
1740
+ }
1741
+
1742
+ // ---------------------------------------------------------------------------
1743
+ // Human in the loop, resumed by a DIFFERENT PROCESS
1744
+ // ---------------------------------------------------------------------------
1745
+ //
1746
+ // Everything above keys the pause on the interrupt object this process saw,
1747
+ // which assumes the process that paused is the one that resumes. Real HITL is
1748
+ // not shaped like that: one worker serves the interrupt, a human answers later,
1749
+ // and any worker may pick the approval up. The resuming process has no session
1750
+ // and no open pauses, so nothing correlated and the pause stayed open forever.
1751
+ //
1752
+ // It is recoverable, exactly, because an interrupt's id is not random:
1753
+ // LangGraph.js's `interrupt()` sets it to `XXH3(checkpoint_ns)` — a pure
1754
+ // function of the interrupted task's namespace, which is
1755
+ // `metadata.langgraph_checkpoint_ns` on the node's run and identical across the
1756
+ // two invocations. So the resuming process can rebuild the id the pausing
1757
+ // process used with no shared state.
1758
+ //
1759
+ // Which node re-ran BECAUSE it was interrupted: only the first superstep of a
1760
+ // level re-runs interrupted tasks, and the deepest resuming level is the graph
1761
+ // that actually paused — which excludes a subgraph's HOST node, a normal node
1762
+ // one level up. LangGraph.js >= 1 names that level in `handleResume`; on 0.x,
1763
+ // where there is no such event, the same answer is read off the runs
1764
+ // themselves: decided at node END, a host node has always seen its subgraph's
1765
+ // deeper nodes by then.
1766
+
1767
+ type Hash = (input: string) => string;
1768
+ let xxh3: Hash | null | undefined;
1769
+
1770
+ /**
1771
+ * LangGraph's own XXH3, loaded from the installed package on first use.
1772
+ *
1773
+ * Not an export — LangGraph keeps it internal — so this reads the file beside
1774
+ * its `package.json`. That is the price of an id that matches the one
1775
+ * `interrupt()` produced byte for byte; a reimplementation would have to match
1776
+ * too, and would not be told when LangGraph changed. A missing or changed file
1777
+ * disables ONLY the cross-process resume: the probe below checks the function
1778
+ * still returns the 32-hex-digit shape `interrupt()` stamps.
1779
+ */
1780
+ function interruptHash(): Hash | null {
1781
+ if (xxh3 !== undefined) return xxh3;
1782
+ xxh3 = null;
1783
+ const manifest = resolveFrom(`${GRAPH_PACKAGE}/package.json`);
1784
+ if (manifest === null) return xxh3;
1785
+ compat.probe(NAME, "langgraph interrupt ids", () => {
1786
+ const module = nodeRequire(join(manifest, "..", "dist", "hash.cjs")) as { XXH3?: unknown };
1787
+ const fn = module.XXH3;
1788
+ if (typeof fn !== "function") return false;
1789
+ const sample = String((fn as (text: string) => unknown)("failproofai"));
1790
+ if (!/^[0-9a-f]{32}$/.test(sample)) return false;
1791
+ xxh3 = (text: string) => String((fn as (value: string) => unknown)(text));
1792
+ return true;
1793
+ });
1794
+ return xxh3;
1795
+ }
1796
+
1797
+ /** The id LangGraph's `interrupt()` gave a task with this checkpoint namespace. */
1798
+ export function interruptIdOf(ns: string): string | null {
1799
+ if (!ns) return null;
1800
+ const hash = interruptHash();
1801
+ if (hash === null) return null;
1802
+ try {
1803
+ return hash(ns);
1804
+ } catch {
1805
+ return null;
1806
+ }
1807
+ }
1808
+
1809
+ function remoteOf(info: RunInfo): RemoteResume | null {
1810
+ return info.root !== null ? (state.runs.get(info.root)?.remote ?? null) : null;
1811
+ }
1812
+
1813
+ /** `agent_resume` + `human_input` for a pause this process never opened. */
1814
+ function closeRemotePause(info: RunInfo): void {
1815
+ const remote = remoteOf(info);
1816
+ const session = info.session;
1817
+ if (remote === null || session === null) return;
1818
+ const parts = nsParts(info.meta);
1819
+ const level = parts.slice(0, -1).join("|");
1820
+ if (remote.deepest !== null) {
1821
+ if (level !== remote.deepest) return;
1822
+ } else {
1823
+ // No lifecycle event named the resuming level (LangGraph.js 0.x). A level
1824
+ // strictly deeper than this one has run, so this node is a subgraph's host,
1825
+ // not the task that paused.
1826
+ const depth = parts.length - 1;
1827
+ for (const seen of remote.levels.keys()) {
1828
+ if ((seen ? seen.split("|").length : 0) > depth) return;
1829
+ }
1830
+ }
1831
+ if (info.meta.langgraph_step !== remote.levels.get(level)) return;
1832
+ const ns = info.meta.langgraph_checkpoint_ns;
1833
+ const pauseId = typeof ns === "string" ? interruptIdOf(ns) : null;
1834
+ if (pauseId === null || remote.done.has(pauseId)) return;
1835
+ remote.done.add(pauseId);
1836
+ const marker = core.fwFields({ interrupt_id: pauseId, resumed_elsewhere: true });
1837
+ emitOnAgent(session, "agentResume", { pauseId, reason: "langgraph_resume", ...marker });
1838
+ emitOnAgent(session, "humanInput", {
1839
+ inputId: pauseId,
1840
+ response: state.options.captureContent ? answerFor(remote.value, pauseId) : undefined,
1841
+ ...marker,
1842
+ });
1843
+ }
1844
+
1845
+ /** LangGraph.js >= 1 lifecycle: an interrupt, delivered with the root's run id. */
1846
+ function onInterrupt(event: unknown): void {
1847
+ if (!state.enabled || !isObject(event)) return;
1848
+ const info = typeof event.runId === "string" ? state.runs.get(event.runId) : undefined;
1849
+ if (info?.session == null) return;
1850
+ const interrupts = Array.isArray(event.interrupts) ? event.interrupts : [];
1851
+ suspend(
1852
+ info.session,
1853
+ interrupts.filter((item): item is InterruptLike => isObject(item) && "value" in item),
1854
+ );
1855
+ }
1856
+
1857
+ /**
1858
+ * LangGraph.js >= 1 lifecycle: a Pregel level is resuming. Normally a no-op for
1859
+ * the in-process case — the resuming root already closed the pause at start —
1860
+ * and load-bearing for the cross-process one, because it names the level that
1861
+ * is resuming, once per level, deepest last.
1862
+ */
1863
+ function onResume(event: unknown): void {
1864
+ if (!state.enabled || !isObject(event)) return;
1865
+ const info = typeof event.runId === "string" ? state.runs.get(event.runId) : undefined;
1866
+ if (info === undefined) return;
1867
+ const remote = remoteOf(info);
1868
+ if (remote !== null) {
1869
+ const ns = Array.isArray(event.checkpointNs) ? event.checkpointNs.map(String) : [];
1870
+ const level = ns.join("|");
1871
+ if (remote.deepest === null || ns.length >= (remote.deepest ? remote.deepest.split("|").length : 0)) {
1872
+ remote.deepest = level;
1873
+ }
1874
+ }
1875
+ if (info.session !== null) resume(info.session, undefined);
1876
+ }
1877
+
1878
+ function onToken(id: string): void {
1879
+ const info = state.runs.get(id);
1880
+ if (info === undefined) return;
1881
+ // Folded into the closing `model_response`, NEVER an event: a 500-token
1882
+ // response would otherwise be 500 stored rows.
1883
+ info.chunks += 1;
1884
+ info.ttftMs ??= core.ms(Date.now() - info.started);
1885
+ }
1886
+
1887
+ // ---------------------------------------------------------------------------
1888
+ // The handler
1889
+ // ---------------------------------------------------------------------------
1890
+
1891
+ type Handler = Record<string | symbol, unknown>;
1892
+
1893
+ function nullable(value: unknown): string | null {
1894
+ return typeof value === "string" && value ? value : null;
1895
+ }
1896
+
1897
+ /**
1898
+ * The one handler. A plain object in LangChain's `BaseCallbackHandler` shape
1899
+ * rather than a subclass, so this module never imports LangChain — the class
1900
+ * would have to come from the application's copy, of which there can be two.
1901
+ *
1902
+ * Every method is a thin argument-order adapter onto `onStart`/`onEnd`, wrapped
1903
+ * in `core.safe`. The argument order is the one `CallbackManager` DISPATCHES
1904
+ * with, which is not always the order its own `.d.ts` declares (1.x's
1905
+ * `handleChainStart` declaration puts `runType` fourth; the call site passes
1906
+ * `parentRunId` fourth, as 0.3 did).
1907
+ */
1908
+ function buildHandler(): Handler {
1909
+ const wrap = <Args extends unknown[]>(fn: (...args: Args) => void): ((...args: Args) => void) =>
1910
+ core.safe(NAME, fn);
1911
+
1912
+ return {
1913
+ name: "failproofai",
1914
+ [HANDLER_MARK]: true,
1915
+ // See the module comment: without this LangChain backgrounds every
1916
+ // callback, out of our async context and possibly past the final flush.
1917
+ awaitHandlers: true,
1918
+ ignoreLLM: false,
1919
+ ignoreChain: false,
1920
+ ignoreAgent: false,
1921
+ ignoreRetriever: false,
1922
+ // Python's adapter records no custom events; neither does this one.
1923
+ ignoreCustomEvent: true,
1924
+ // Normally false so an adapter bug can never take down the graph. Under
1925
+ // FAILPROOFAI_SDK_STRICT it follows strict mode — otherwise LangChain's own
1926
+ // handler firewall swallows the re-raise and the escape hatch does nothing.
1927
+ get raiseError(): boolean {
1928
+ return core.strict();
1929
+ },
1930
+ get [GRAPH_CALLBACK_HANDLER](): boolean {
1931
+ return state.options.graphCallbacks;
1932
+ },
1933
+
1934
+ handleChainStart: wrap(function handleChainStart(
1935
+ serialized: unknown,
1936
+ inputs: unknown,
1937
+ runId: string,
1938
+ parentRunId?: string,
1939
+ tags?: unknown,
1940
+ metadata?: unknown,
1941
+ _runType?: unknown,
1942
+ runName?: unknown,
1943
+ ) {
1944
+ onStart({
1945
+ id: runId,
1946
+ parent: nullable(parentRunId),
1947
+ name: runNameOf(serialized, runName, "chain"),
1948
+ runType: "chain",
1949
+ tags: tagsOf(tags),
1950
+ meta: metaOf(metadata),
1951
+ inputs,
1952
+ });
1953
+ }),
1954
+ handleChainEnd: wrap(function handleChainEnd(outputs: unknown, runId: string) {
1955
+ onEnd(runId, { outputs });
1956
+ }),
1957
+ handleChainError: wrap(function handleChainError(error: unknown, runId: string) {
1958
+ onEnd(runId, { error: error ?? new Error("chain failed") });
1959
+ }),
1960
+
1961
+ handleLLMStart: wrap(function handleLLMStart(
1962
+ serialized: unknown,
1963
+ prompts: unknown,
1964
+ runId: string,
1965
+ parentRunId?: string,
1966
+ extraParams?: unknown,
1967
+ tags?: unknown,
1968
+ metadata?: unknown,
1969
+ runName?: unknown,
1970
+ ) {
1971
+ onStart({
1972
+ id: runId,
1973
+ parent: nullable(parentRunId),
1974
+ name: runNameOf(serialized, runName, "llm"),
1975
+ runType: "llm",
1976
+ tags: tagsOf(tags),
1977
+ meta: metaOf(metadata),
1978
+ inputs: { prompts },
1979
+ invocationParams: (extraParams as { invocation_params?: Record<string, unknown> } | undefined)
1980
+ ?.invocation_params,
1981
+ });
1982
+ }),
1983
+ handleChatModelStart: wrap(function handleChatModelStart(
1984
+ serialized: unknown,
1985
+ messages: unknown,
1986
+ runId: string,
1987
+ parentRunId?: string,
1988
+ extraParams?: unknown,
1989
+ tags?: unknown,
1990
+ metadata?: unknown,
1991
+ runName?: unknown,
1992
+ ) {
1993
+ onStart({
1994
+ id: runId,
1995
+ parent: nullable(parentRunId),
1996
+ name: runNameOf(serialized, runName, "chat_model"),
1997
+ runType: "chat_model",
1998
+ tags: tagsOf(tags),
1999
+ meta: metaOf(metadata),
2000
+ inputs: messages,
2001
+ messages: Array.isArray(messages) ? messages : undefined,
2002
+ invocationParams: (extraParams as { invocation_params?: Record<string, unknown> } | undefined)
2003
+ ?.invocation_params,
2004
+ });
2005
+ }),
2006
+ handleLLMNewToken: wrap(function handleLLMNewToken(_token: unknown, _idx: unknown, runId: string) {
2007
+ if (state.enabled) onToken(runId);
2008
+ }),
2009
+ handleLLMEnd: wrap(function handleLLMEnd(output: unknown, runId: string) {
2010
+ onEnd(runId, { response: output });
2011
+ }),
2012
+ handleLLMError: wrap(function handleLLMError(error: unknown, runId: string) {
2013
+ onEnd(runId, { error: error ?? new Error("model call failed") });
2014
+ }),
2015
+
2016
+ handleToolStart: wrap(function handleToolStart(
2017
+ serialized: unknown,
2018
+ input: unknown,
2019
+ runId: string,
2020
+ parentRunId?: string,
2021
+ tags?: unknown,
2022
+ metadata?: unknown,
2023
+ runName?: unknown,
2024
+ toolCallId?: unknown,
2025
+ ) {
2026
+ onStart({
2027
+ id: runId,
2028
+ parent: nullable(parentRunId),
2029
+ name: runNameOf(serialized, runName, "tool"),
2030
+ runType: "tool",
2031
+ tags: tagsOf(tags),
2032
+ meta: metaOf(metadata),
2033
+ inputs: parseToolInput(input),
2034
+ toolCallId: nullable(toolCallId) ?? undefined,
2035
+ });
2036
+ }),
2037
+ handleToolEnd: wrap(function handleToolEnd(output: unknown, runId: string) {
2038
+ onEnd(runId, { outputs: output });
2039
+ }),
2040
+ handleToolError: wrap(function handleToolError(error: unknown, runId: string) {
2041
+ onEnd(runId, { error: error ?? new Error("tool failed") });
2042
+ }),
2043
+
2044
+ handleRetrieverStart: wrap(function handleRetrieverStart(
2045
+ serialized: unknown,
2046
+ query: unknown,
2047
+ runId: string,
2048
+ parentRunId?: string,
2049
+ tags?: unknown,
2050
+ metadata?: unknown,
2051
+ name?: unknown,
2052
+ ) {
2053
+ onStart({
2054
+ id: runId,
2055
+ parent: nullable(parentRunId),
2056
+ name: runNameOf(serialized, name, "retriever"),
2057
+ runType: "retriever",
2058
+ tags: tagsOf(tags),
2059
+ meta: metaOf(metadata),
2060
+ inputs: query,
2061
+ });
2062
+ }),
2063
+ handleRetrieverEnd: wrap(function handleRetrieverEnd(documents: unknown, runId: string) {
2064
+ onEnd(runId, { outputs: documents });
2065
+ }),
2066
+ handleRetrieverError: wrap(function handleRetrieverError(error: unknown, runId: string) {
2067
+ onEnd(runId, { error: error ?? new Error("retriever failed") });
2068
+ }),
2069
+
2070
+ handleInterrupt: wrap(function handleInterrupt(event: unknown) {
2071
+ onInterrupt(event);
2072
+ }),
2073
+ handleResume: wrap(function handleResume(event: unknown) {
2074
+ onResume(event);
2075
+ }),
2076
+ };
2077
+ }
2078
+
2079
+ let handler: Handler | null = null;
2080
+
2081
+ function theHandler(): Handler {
2082
+ handler ??= buildHandler();
2083
+ return handler;
2084
+ }
2085
+
2086
+ interface CallbackManagerLike {
2087
+ handlers?: unknown[];
2088
+ addHandler?: (handler: unknown, inherit?: boolean) => void;
2089
+ }
2090
+
2091
+ interface CallbackManagerCtor {
2092
+ new (): CallbackManagerLike;
2093
+ configure?: (...args: unknown[]) => unknown;
2094
+ _configureSync?: (...args: unknown[]) => unknown;
2095
+ }
2096
+
2097
+ function isOurs(value: unknown): boolean {
2098
+ return isObject(value) && (value as Record<symbol, unknown>)[HANDLER_MARK] === true;
2099
+ }
2100
+
2101
+ /**
2102
+ * Attach our handler to a manager `configure` just built — unless one is
2103
+ * already there. LangChain calls `configure` for every invocation and a child
2104
+ * manager inherits its parent's handlers, so a blind `addHandler` would emit
2105
+ * every event once per attachment.
2106
+ */
2107
+ function attach(manager: unknown): unknown {
2108
+ const value = manager as CallbackManagerLike | undefined | null;
2109
+ if (!value || typeof value.addHandler !== "function") return manager;
2110
+ if (!state.enabled) return manager;
2111
+ if (Array.isArray(value.handlers) && value.handlers.some(isOurs)) {
2112
+ return manager;
2113
+ }
2114
+ value.addHandler(theHandler(), true);
2115
+ return manager;
2116
+ }
2117
+
2118
+ /**
2119
+ * `configure`'s first argument — the inheritable handlers — with ours added.
2120
+ * An array (or nothing) gets the handler appended; a `CallbackManager` is left
2121
+ * alone and the manager `configure` derives from it is handled by `attach`.
2122
+ */
2123
+ function withHandler(inheritable: unknown): unknown {
2124
+ if (inheritable === undefined || inheritable === null) return [theHandler()];
2125
+ if (Array.isArray(inheritable)) {
2126
+ return inheritable.some(isOurs) ? inheritable : [...(inheritable as unknown[]), theHandler()];
2127
+ }
2128
+ return inheritable;
2129
+ }
2130
+
2131
+ // ---------------------------------------------------------------------------
2132
+ // Install / uninstall
2133
+ // ---------------------------------------------------------------------------
2134
+
2135
+ /** Close every span still open at teardown, leaves before agents. */
2136
+ function closeEverything(): void {
2137
+ const roots = new Set<string>();
2138
+ for (const info of state.runs.values()) if (info.root !== null) roots.add(info.root);
2139
+ for (const root of roots) closeOpenLeaves(root);
2140
+ state.tracker.closeOpenAgents("cancelled");
2141
+ }
2142
+
2143
+ /**
2144
+ * The `CallbackManager` of every OTHER installed copy of `@langchain/core` —
2145
+ * the ones nested under a dependency that pinned its own version.
2146
+ *
2147
+ * That layout is ordinary: a provider or community package declaring
2148
+ * `@langchain/core` as a hard dependency on a range the application's copy does
2149
+ * not satisfy gets its own copy at `node_modules/<pkg>/node_modules/
2150
+ * @langchain/core`, and everything it exports — its chat model, its tools, its
2151
+ * retrievers — is built on that copy. Resolution from the application can never
2152
+ * reach it. A run such a class starts INSIDE one of the application's runs was
2153
+ * always recorded: the child is handed the parent's manager, handler included.
2154
+ * But one it starts as a ROOT — `providerModel.invoke()`, a provider's tool or
2155
+ * retriever called directly — went through the nested copy's own, unpatched
2156
+ * `configure`, and was recorded nowhere, silently, while `instrument()`
2157
+ * reported success (VERIFIED: `integration/fixtures/langchain-dup-core`).
2158
+ *
2159
+ * LangChain offers no cross-copy hook to use instead. Its one registration
2160
+ * point, `registerConfigureHook`, keys its list on a module-private
2161
+ * `Symbol("lc:configure_hooks")` — each copy reads only its own — and stores
2162
+ * it in the current async context, not globally. So the copies are found on
2163
+ * disk (`nestedCopies`) and loaded the way the application will load them: the
2164
+ * build its module system reaches, plus the CommonJS build if something has
2165
+ * already `require`d it — `requireModuleCopies`' rule, for the same reasons. A
2166
+ * copy outside the declared range is left alone rather than patched blind.
2167
+ *
2168
+ * The one arrangement this cannot see is the one `requireModuleCopies` cannot:
2169
+ * an ES-module application whose CommonJS-only dependency `require`s its nested
2170
+ * copy AFTER `instrument()`. `langchainHandler()` covers it.
2171
+ */
2172
+ async function nestedManagers(): Promise<CallbackManagerCtor[]> {
2173
+ const found: CallbackManagerCtor[] = [];
2174
+ for (const root of nestedCopies(PACKAGE)) {
2175
+ await core.callSafely(
2176
+ async () => {
2177
+ const version = (nodeRequire(join(root, "package.json")) as { version?: unknown }).version;
2178
+ const parts = compat.parseVersion(typeof version === "string" ? version : "");
2179
+ if (parts.length === 0 || (parts[0] ?? 0) >= 2 || ((parts[0] ?? 0) === 0 && (parts[1] ?? 0) < 3)) {
2180
+ logger.debug(`langchain adapter leaving ${root} (${String(version)}) alone: outside >=0.3.0 <2.0.0`);
2181
+ return;
2182
+ }
2183
+ const cjs = resolveExportsAt(root, "./callbacks/manager", "require");
2184
+ const esm = resolveExportsAt(root, "./callbacks/manager", "import");
2185
+ const modules: unknown[] = [];
2186
+ if (esm === null || entryIsCommonJs()) {
2187
+ if (cjs !== null) modules.push(nodeRequire(cjs));
2188
+ } else {
2189
+ modules.push(await importModule(pathToFileURL(esm).href));
2190
+ if (cjs !== null && cjs !== esm && isRequired(cjs)) modules.push(nodeRequire(cjs));
2191
+ }
2192
+ for (const module of modules) {
2193
+ const CallbackManager = (module as { CallbackManager?: unknown }).CallbackManager;
2194
+ if (typeof CallbackManager === "function") found.push(CallbackManager as CallbackManagerCtor);
2195
+ }
2196
+ },
2197
+ [],
2198
+ `${NAME}.nestedManagers`,
2199
+ );
2200
+ }
2201
+ return found;
2202
+ }
2203
+
2204
+ export const adapter: Adapter = {
2205
+ name: NAME,
2206
+
2207
+ async install(options: Record<string, unknown> = {}): Promise<void> {
2208
+ // Every loaded copy: the ES-module and CommonJS builds of @langchain/core
2209
+ // are two different CallbackManager classes. See `requireModuleCopies`.
2210
+ const primary = (
2211
+ (await compat.requireModuleCopies(
2212
+ "@langchain/core/callbacks/manager",
2213
+ "npm install @langchain/core",
2214
+ )) as Array<{ CallbackManager?: CallbackManagerCtor }>
2215
+ ).map((module) => module.CallbackManager);
2216
+ if (primary.some((CallbackManager) => typeof CallbackManager !== "function")) {
2217
+ throw new Error("@langchain/core/callbacks/manager does not export CallbackManager");
2218
+ }
2219
+ // ...and every copy nested under a dependency. See `nestedManagers`.
2220
+ const managers = [...new Set([...primary, ...(await nestedManagers())])] as CallbackManagerCtor[];
2221
+
2222
+ compat.checkVersion(NAME, PACKAGE, {
2223
+ minimum: "0.3.0",
2224
+ below: "2.0.0",
2225
+ reason: "the callback argument order and run metadata below are the 0.3+ shape",
2226
+ });
2227
+ // LangGraph is optional — plain LangChain is instrumented without it — so
2228
+ // only an INSTALLED LangGraph outside the range warns.
2229
+ compat.checkVersion(NAME, GRAPH_PACKAGE, {
2230
+ minimum: "0.4.0",
2231
+ below: "2.0.0",
2232
+ reason: "the node metadata and interrupt shape below are LangGraph.js 0.4+",
2233
+ });
2234
+
2235
+ state.configure(readOptions(options));
2236
+ state.enabled = true;
2237
+ state.installed = true;
2238
+ patcher = new core.Patcher();
2239
+
2240
+ // Both entry points, and at least one must take. A version that routed
2241
+ // through the other would install cleanly and record nothing — the single
2242
+ // most expensive failure an adapter can have, because everything looks fine.
2243
+ let patched = 0;
2244
+ for (const CallbackManager of managers) {
2245
+ for (const method of ["configure", "_configureSync"] as const) {
2246
+ const original = CallbackManager[method];
2247
+ if (typeof original !== "function") continue;
2248
+ if (
2249
+ !compat.probe(NAME, `CallbackManager.${method}`, () =>
2250
+ Object.getOwnPropertyDescriptor(CallbackManager, method)?.writable !== false,
2251
+ )
2252
+ ) {
2253
+ continue;
2254
+ }
2255
+ const replacement = function failproofaiConfigure(this: unknown, ...args: unknown[]): unknown {
2256
+ // Our handler goes IN, as one of the inheritable handlers, rather
2257
+ // than onto whatever comes out. `configure` returns undefined when
2258
+ // there are no handlers at all — the ordinary case for an un-traced
2259
+ // process, exactly the process we are here to trace — and it only
2260
+ // applies the call's tags and metadata to a manager it builds. The
2261
+ // first release attached to a bare `new CallbackManager()` in that
2262
+ // case, which carried the handler and none of the metadata: every
2263
+ // `thread_id` and `failproofai_sdk_session_id` was silently dropped,
2264
+ // so neither could ever choose the session.
2265
+ if (state.enabled) args[0] = withHandler(args[0]);
2266
+ const built = original.apply(this, args);
2267
+ if (typeof (built as PromiseLike<unknown> | undefined)?.then === "function") {
2268
+ return (built as PromiseLike<unknown>).then(attach);
2269
+ }
2270
+ return attach(built);
2271
+ };
2272
+ if (patcher.patch(CallbackManager, method, replacement)) patched += 1;
2273
+ }
2274
+ }
2275
+
2276
+ if (patched === 0) {
2277
+ throw new Error(
2278
+ "could not patch CallbackManager.configure — this build of @langchain/core exposes " +
2279
+ "neither a writable `configure` nor `_configureSync`. Pass the handler explicitly " +
2280
+ "instead: `chain.invoke(input, { callbacks: [langchainHandler()] })`.",
2281
+ );
2282
+ }
2283
+ logger.debug(`langchain adapter attached to ${patched} callback-manager entry point(s)`);
2284
+ },
2285
+
2286
+ uninstall(): void {
2287
+ // The switch goes FIRST: teardown below emits through the tracker directly,
2288
+ // and nothing may re-enter `onStart` while it does.
2289
+ state.enabled = false;
2290
+ state.installed = false;
2291
+ patcher?.restoreAll();
2292
+ patcher = null;
2293
+ closeEverything();
2294
+ state.reset();
2295
+ state.options = defaultOptions();
2296
+ },
2297
+ };
2298
+
2299
+ let warnedOptions = false;
2300
+
2301
+ /**
2302
+ * The raw handler, for passing explicitly instead of — or as well as —
2303
+ * `instrument()`:
2304
+ *
2305
+ * import { langchainHandler } from "@failproofai/sdk/langchain";
2306
+ * await graph.invoke(input, { callbacks: [langchainHandler()] });
2307
+ *
2308
+ * Works without `instrument()`: the patch-free path, and the documented
2309
+ * fallback for a bundled application where the `@langchain/core` in
2310
+ * `node_modules` is not the copy that runs. Takes the same options as
2311
+ * `instrument("langchain", options)`; while `instrument()` is active its
2312
+ * options govern, and passing different ones here warns once.
2313
+ *
2314
+ * Using it alongside `instrument()` does not double-record: the patched
2315
+ * `configure` sees the handler is already on the manager and adds nothing.
2316
+ * `uninstrument()` disables it too, until it is asked for again.
2317
+ */
2318
+ /** @internal Table sizes, for the tests that prove a finished request leaves nothing behind. */
2319
+ export function _stats(): { runs: number; sessions: number; tracker: { runs: number; links: number } } {
2320
+ return { runs: state.runs.size, sessions: state.sessions.size, tracker: state.tracker.stats() };
2321
+ }
2322
+
2323
+ export function langchainHandler(options?: LangChainOptions): Record<string, unknown> {
2324
+ if (state.installed) {
2325
+ if (options !== undefined && !warnedOptions) {
2326
+ warnedOptions = true;
2327
+ logger.warn(
2328
+ "langchainHandler() options are ignored while instrument('langchain') is active; " +
2329
+ "the options passed to instrument() govern.",
2330
+ );
2331
+ }
2332
+ } else if (!state.enabled) {
2333
+ state.configure(readOptions((options ?? {}) as Record<string, unknown>));
2334
+ state.enabled = true;
2335
+ } else if (options !== undefined) {
2336
+ // Already recording through an earlier call: new options, same runs.
2337
+ state.options = readOptions(options as Record<string, unknown>);
2338
+ }
2339
+ return theHandler();
2340
+ }