failproofai 1.0.7-beta.1 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +4 -4
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/run/route.js +7 -8
- package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/audit/page.js +5 -7
- package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +6 -6
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +14 -14
- package/.next/standalone/.next/server/app/policies/page.js +11 -13
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +7 -8
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +7 -7
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +6 -7
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +8 -41
- package/.next/standalone/.next/server/app/settings/page.js +9 -12
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__1j-zsg5._.js → [externals]__1_bftcl._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{[externals]__19_pzeq._.js → [externals]__1msfs-h._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1bf34x4._.js → [root-of-the-server]__1_r2rbg._.js} +7 -5
- package/.next/standalone/.next/server/chunks/_09dz7xv._.js +21 -21
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +1 -1
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +1 -1
- package/.next/standalone/.next/server/chunks/{_1q5i8mb._.js → _1c3k-8x._.js} +2 -2
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +16 -16
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_09z9-p7._.js +1 -1
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
- package/.next/standalone/.next/server/chunks/src_hooks_0iu54mz._.js +3 -0
- package/.next/standalone/.next/server/chunks/src_hooks_custom-hooks-loader_ts_0lnb3n3._.js +2 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-_ki57._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0yxwl6j._.js → [root-of-the-server]__0bd3mje._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cg-bgc._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0cpu_mj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1mf3zp6._.js → [root-of-the-server]__0cxe_2_._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u3w0ll._.js +22 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__17d_ffl._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__19d9tgz._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ctpynv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1jiwfsj._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1phc187._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_06imw3p._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1w_5l7t._.js → _0h_douw._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_214wgrp._.js → _1-i_gzc._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_0bn2oo8._.js → _166t73i._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_1q46vxx._.js → _1_qswah._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_1gb0ifp._.js → _1es2j7i._.js} +5 -5
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_policies_page_actions_1sp2-yo.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{node_modules_next_dist_18_d8l1._.js → node_modules_next_dist_0drixxt._.js} +4 -4
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1cv9_c4._.js +10 -0
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_pack-cli_ts_0t7me65._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +23 -56
- package/.next/standalone/.next/static/chunks/094xgi4owxaqf.js +1 -0
- package/.next/standalone/.next/static/chunks/0__8a7m868fvf.js +1 -0
- package/.next/standalone/.next/static/chunks/{12tvm75t5ffui.js → 0o6qlkgubtoex.js} +1 -1
- package/.next/standalone/.next/static/chunks/13i7-9is-vhys.js +1 -0
- package/.next/standalone/.next/static/chunks/1rz20_pz828f3.js +6 -0
- package/.next/standalone/.next/static/chunks/2k9f4tyv04809.css +1 -0
- package/.next/standalone/.next/static/chunks/{0vmd180qfntfb.js → 2klitrtzpaoe0.js} +1 -1
- package/.next/standalone/.next/static/chunks/{258668t68du6b.js → 2mdh397ghgnvv.js} +1 -1
- package/.next/standalone/.next/static/chunks/2rshywgeqsyzk.css +2 -0
- package/.next/standalone/.next/static/chunks/3pzx4chkhko9k.js +1 -0
- package/.next/standalone/.next/static/chunks/{0qrbdkv9qmvli.js → 3rh5o7e16irrm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{29fql3nbnfc9q.js → 43ufqrz8qo3h-.js} +1 -1
- package/.next/standalone/SECURITY.md +53 -0
- package/.next/standalone/app/actions/pack-actions.ts +0 -12
- package/.next/standalone/app/policies/hooks-client.tsx +0 -9
- package/.next/standalone/app/settings/page.tsx +1 -20
- package/.next/standalone/app/settings/settings-client.tsx +1 -27
- package/.next/standalone/app/settings/settings.css +0 -79
- package/.next/standalone/package.json +9 -9
- package/.next/standalone/sdk/python/skill/SKILL.md +60 -14
- package/.next/standalone/sdk/python/skill/agents/openai.yaml +2 -1
- package/.next/standalone/sdk/python/skill/references/evaluator.md +255 -0
- package/.next/standalone/sdk/python/skill/references/events.md +17 -8
- package/.next/standalone/sdk/python/skill/references/frameworks.md +3 -0
- package/.next/standalone/sdk/python/skill/references/install.md +3 -0
- package/.next/standalone/sdk/python/skill/references/integration.md +6 -2
- package/.next/standalone/sdk/python/skill/references/typescript.md +568 -0
- package/.next/standalone/sdk/typescript/CHANGELOG.md +133 -0
- package/.next/standalone/sdk/typescript/LICENSE +42 -0
- package/.next/standalone/sdk/typescript/README.md +552 -0
- package/.next/standalone/sdk/typescript/eslint.config.mjs +59 -0
- package/.next/standalone/sdk/typescript/examples/research-agent.ts +197 -0
- package/.next/standalone/sdk/typescript/integration/ai.test.ts +920 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/agent.ts +337 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package-lock.json +261 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/package.json +16 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/surfaces.ts +605 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-4/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/agent.ts +342 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package-lock.json +156 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/package.json +16 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/surfaces.ts +628 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-5/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/agent.ts +346 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package-lock.json +156 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/package.json +13 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/surfaces.ts +651 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-6/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/agent.ts +350 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package-lock.json +153 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/package.json +13 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/surfaces.ts +651 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/ai-7/tsconfig.surfaces.json +4 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/agent.ts +623 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package-lock.json +344 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/package.json +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-0.3/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent-v1.ts +99 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/agent.ts +623 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package-lock.json +336 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-1/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/agent.ts +96 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package-lock.json +441 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/package.json +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.cjs +53 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/index.mjs +55 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/langchain-dup-core/vendor/lc-weather-provider/package.json +18 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/agent.ts +659 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package-lock.json +635 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.11/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/agent.ts +659 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package-lock.json +553 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/llamaindex-0.12/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/agent.ts +877 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/mcp-server.mjs +66 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package-lock.json +6797 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/package.json +24 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-0/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/agent.ts +872 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/mcp-server.mjs +66 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package-lock.json +2540 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/mastra-1/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/actions.ts +18 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/ai/route.ts +42 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/edge/route.ts +29 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/langgraph/route.ts +21 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/llamaindex/route.ts +11 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/mastra/route.ts +19 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/api/status/route.ts +7 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/layout.tsx +9 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/app/page.tsx +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/instrumentation.ts +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/ai.ts +61 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/langgraph.ts +68 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/llamaindex.ts +98 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/lib/mastra.ts +90 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/next.config.ts +52 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package-lock.json +4343 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/nextjs/package.json +28 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/agent.ts +51 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/deno-npm.ts +76 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package-lock.json +484 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/runtimes/package.json +15 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/agent.ts +79 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/package-lock.json +740 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/types/package.json +11 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/agent.ts +197 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package-lock.json +70 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/package.json +12 -0
- package/.next/standalone/sdk/typescript/integration/fixtures/vanilla/tsconfig.json +12 -0
- package/.next/standalone/sdk/typescript/integration/global-setup.ts +29 -0
- package/.next/standalone/sdk/typescript/integration/harness.ts +451 -0
- package/.next/standalone/sdk/typescript/integration/langchain.test.ts +682 -0
- package/.next/standalone/sdk/typescript/integration/llamaindex.test.ts +709 -0
- package/.next/standalone/sdk/typescript/integration/mastra-coverage.test.ts +386 -0
- package/.next/standalone/sdk/typescript/integration/mastra.test.ts +311 -0
- package/.next/standalone/sdk/typescript/integration/nextjs.test.ts +341 -0
- package/.next/standalone/sdk/typescript/integration/runtime-parity.ts +180 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.bun.test.ts +15 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.core.test.ts +113 -0
- package/.next/standalone/sdk/typescript/integration/runtimes.deno.test.ts +19 -0
- package/.next/standalone/sdk/typescript/integration/types.test.ts +141 -0
- package/.next/standalone/sdk/typescript/integration/vanilla.test.ts +255 -0
- package/.next/standalone/sdk/typescript/package-lock.json +2640 -0
- package/.next/standalone/sdk/typescript/package.json +401 -0
- package/.next/standalone/sdk/typescript/scripts/finalize-build.mjs +123 -0
- package/.next/standalone/sdk/typescript/scripts/release.mjs +177 -0
- package/.next/standalone/sdk/typescript/src/clock.ts +58 -0
- package/.next/standalone/sdk/typescript/src/context.ts +214 -0
- package/.next/standalone/sdk/typescript/src/edge/adapter.ts +18 -0
- package/.next/standalone/sdk/typescript/src/edge/ai.ts +116 -0
- package/.next/standalone/sdk/typescript/src/edge/index.ts +238 -0
- package/.next/standalone/sdk/typescript/src/edge/langchain.ts +18 -0
- package/.next/standalone/sdk/typescript/src/edge/llamaindex.ts +12 -0
- package/.next/standalone/sdk/typescript/src/edge/mastra.ts +17 -0
- package/.next/standalone/sdk/typescript/src/edge/notice.ts +33 -0
- package/.next/standalone/sdk/typescript/src/environment.ts +75 -0
- package/.next/standalone/sdk/typescript/src/evaluator/authoring.ts +480 -0
- package/.next/standalone/sdk/typescript/src/evaluator/cli.ts +96 -0
- package/.next/standalone/sdk/typescript/src/evaluator/client.ts +421 -0
- package/.next/standalone/sdk/typescript/src/evaluator/expression.ts +1292 -0
- package/.next/standalone/sdk/typescript/src/evaluator/index.ts +144 -0
- package/.next/standalone/sdk/typescript/src/evaluator/protocol.ts +747 -0
- package/.next/standalone/sdk/typescript/src/evaluator/runtime.ts +930 -0
- package/.next/standalone/sdk/typescript/src/evaluator/sandbox-worker.ts +171 -0
- package/.next/standalone/sdk/typescript/src/evaluator/source-limits.ts +27 -0
- package/.next/standalone/sdk/typescript/src/evaluator/source.ts +509 -0
- package/.next/standalone/sdk/typescript/src/events.ts +879 -0
- package/.next/standalone/sdk/typescript/src/exit.ts +117 -0
- package/.next/standalone/sdk/typescript/src/index.ts +186 -0
- package/.next/standalone/sdk/typescript/src/integrations/ai.ts +1566 -0
- package/.next/standalone/sdk/typescript/src/integrations/compat.ts +322 -0
- package/.next/standalone/sdk/typescript/src/integrations/core.ts +1321 -0
- package/.next/standalone/sdk/typescript/src/integrations/index.ts +355 -0
- package/.next/standalone/sdk/typescript/src/integrations/langchain.ts +2340 -0
- package/.next/standalone/sdk/typescript/src/integrations/llamaindex.ts +2111 -0
- package/.next/standalone/sdk/typescript/src/integrations/mastra.ts +1802 -0
- package/.next/standalone/sdk/typescript/src/logger.ts +98 -0
- package/.next/standalone/sdk/typescript/src/next.ts +115 -0
- package/.next/standalone/sdk/typescript/src/node-require.ts +446 -0
- package/.next/standalone/sdk/typescript/src/redact.ts +305 -0
- package/.next/standalone/sdk/typescript/src/resolver.ts +120 -0
- package/.next/standalone/sdk/typescript/src/runtime.ts +29 -0
- package/.next/standalone/sdk/typescript/src/schema.ts +410 -0
- package/.next/standalone/sdk/typescript/src/scopes.ts +701 -0
- package/.next/standalone/sdk/typescript/src/shared.ts +33 -0
- package/.next/standalone/sdk/typescript/src/version.ts +5 -0
- package/.next/standalone/sdk/typescript/src/writer.ts +934 -0
- package/.next/standalone/sdk/typescript/test/adapters.test.ts +397 -0
- package/.next/standalone/sdk/typescript/test/ai.test.ts +1076 -0
- package/.next/standalone/sdk/typescript/test/copies.test.ts +204 -0
- package/.next/standalone/sdk/typescript/test/edge.test.ts +183 -0
- package/.next/standalone/sdk/typescript/test/evaluator-client.test.ts +234 -0
- package/.next/standalone/sdk/typescript/test/evaluator-protocol.test.ts +225 -0
- package/.next/standalone/sdk/typescript/test/events.test.ts +193 -0
- package/.next/standalone/sdk/typescript/test/expression.test.ts +181 -0
- package/.next/standalone/sdk/typescript/test/global-setup.ts +26 -0
- package/.next/standalone/sdk/typescript/test/helpers.ts +130 -0
- package/.next/standalone/sdk/typescript/test/integrations.test.ts +369 -0
- package/.next/standalone/sdk/typescript/test/langchain-copies.test.ts +204 -0
- package/.next/standalone/sdk/typescript/test/langchain.test.ts +999 -0
- package/.next/standalone/sdk/typescript/test/llamaindex.test.ts +1760 -0
- package/.next/standalone/sdk/typescript/test/mastra-coverage.test.ts +501 -0
- package/.next/standalone/sdk/typescript/test/mastra-lifecycle.test.ts +479 -0
- package/.next/standalone/sdk/typescript/test/mastra.test.ts +285 -0
- package/.next/standalone/sdk/typescript/test/next.test.ts +109 -0
- package/.next/standalone/sdk/typescript/test/packaging.test.ts +312 -0
- package/.next/standalone/sdk/typescript/test/redaction.test.ts +171 -0
- package/.next/standalone/sdk/typescript/test/runtimes.test.ts +101 -0
- package/.next/standalone/sdk/typescript/test/sandbox.test.ts +189 -0
- package/.next/standalone/sdk/typescript/test/scopes.test.ts +271 -0
- package/.next/standalone/sdk/typescript/test/setup.ts +19 -0
- package/.next/standalone/sdk/typescript/test/skill-snippets.test.ts +73 -0
- package/.next/standalone/sdk/typescript/test/spool-contract.test.ts +124 -0
- package/.next/standalone/sdk/typescript/test/tracker-bounds.test.ts +191 -0
- package/.next/standalone/sdk/typescript/test/wire-format.test.ts +214 -0
- package/.next/standalone/sdk/typescript/test/writer.test.ts +407 -0
- package/.next/standalone/sdk/typescript/tsconfig.build.json +15 -0
- package/.next/standalone/sdk/typescript/tsconfig.cjs.json +19 -0
- package/.next/standalone/sdk/typescript/tsconfig.json +28 -0
- package/.next/standalone/sdk/typescript/vitest.config.ts +33 -0
- package/.next/standalone/sdk/typescript/vitest.integration.config.ts +23 -0
- package/.next/standalone/server.js +1 -1
- package/bin/failproofai.mjs +2 -115
- package/dist/cli.mjs +6538 -13686
- package/dist/index.js +1 -19
- package/dist/worker.mjs +2085 -8012
- package/package.json +9 -9
- package/pi-extension/index.ts +0 -11
- package/scripts/build-policy-pack.mjs +2 -39
- package/src/hooks/builtin-policies.ts +3 -21
- package/src/hooks/cloud-enrollment-cli.ts +1 -1
- package/src/hooks/cloud-managed-policies.ts +0 -22
- package/src/hooks/custom-hooks-loader.ts +7 -45
- package/src/hooks/custom-hooks-registry.ts +1 -45
- package/src/hooks/first-run-gate.ts +0 -5
- package/src/hooks/fp-home.ts +0 -23
- package/src/hooks/handler.ts +6 -265
- package/src/hooks/hook-activity-store.ts +1 -105
- package/src/hooks/hook-telemetry.ts +0 -41
- package/src/hooks/loader-utils.ts +0 -6
- package/src/hooks/pack-cli.ts +17 -278
- package/src/hooks/pack-manifest.ts +7 -479
- package/src/hooks/pack-store.ts +10 -156
- package/src/hooks/policy-catalog.ts +0 -65
- package/src/hooks/policy-evaluator.ts +796 -940
- package/src/hooks/policy-registry.ts +0 -25
- package/src/hooks/policy-types.ts +0 -126
- package/src/hooks/worker-server.ts +26 -119
- package/src/index.ts +0 -6
- package/.next/standalone/.next/server/chunks/src_hooks_01frwmb._.js +0 -5
- package/.next/standalone/.next/server/chunks/src_hooks_18qtd42._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01bmjsj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04usis8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__056wjo4._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__059yza8._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0eip4_k._.js +0 -22
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0n0xg95._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qcb0mg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qxnccm._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0rwtwpm._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s_yomn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0soxz2z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0yrsbd_._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11mayhe._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__13d-wb6._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1dinjii._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pprgri._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1q4p5b8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1qiz0e4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_042cgl1._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0bqoto4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0uyu3jf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1feuvhb._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/app_actions_get-scheduled-audit_ts_0ei9sni._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_02tf1h4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_0w6mzq5._.js +0 -151
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_095a_79._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_15t8kqj._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_18k8rl0._.js +0 -12
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1fm2w5z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_1j0zy3v._.js +0 -3
- package/.next/standalone/.next/static/chunks/0cd-_8-c-m1ea.js +0 -6
- package/.next/standalone/.next/static/chunks/0uldbut9y2-e8.js +0 -1
- package/.next/standalone/.next/static/chunks/1u5zsejmgrir_.js +0 -1
- package/.next/standalone/.next/static/chunks/285spx855h_3r.css +0 -2
- package/.next/standalone/.next/static/chunks/2qv4hshejedtx.css +0 -1
- package/.next/standalone/.next/static/chunks/2vkvu9-opa_1z.js +0 -1
- package/.next/standalone/.next/static/chunks/37lhv7wa3ywt6.js +0 -1
- package/.next/standalone/app/actions/get-jev-config.ts +0 -409
- package/.next/standalone/app/actions/update-jev-config.ts +0 -420
- package/.next/standalone/app/components/jev-notices.tsx +0 -96
- package/.next/standalone/app/settings/jev-panel.tsx +0 -469
- package/src/hooks/effective-reviewers.ts +0 -79
- package/src/hooks/jev-activity.ts +0 -385
- package/src/hooks/jev-cli.ts +0 -1193
- package/src/hooks/policy-authority.ts +0 -333
- package/src/hooks/policy-reviewability.ts +0 -229
- package/src/hooks/semantic/combine.ts +0 -541
- package/src/hooks/semantic/compile.ts +0 -176
- package/src/hooks/semantic/decide.ts +0 -392
- package/src/hooks/semantic/envelope.ts +0 -1296
- package/src/hooks/semantic/evaluator.ts +0 -547
- package/src/hooks/semantic/facts.ts +0 -292
- package/src/hooks/semantic/intent.ts +0 -1190
- package/src/hooks/semantic/jev-client.ts +0 -643
- package/src/hooks/semantic/jev-config.ts +0 -594
- package/src/hooks/semantic/jev-review.ts +0 -374
- package/src/hooks/semantic/jev-stats.ts +0 -289
- package/src/hooks/semantic/jev-throttle.ts +0 -421
- package/src/hooks/semantic/pack-policies.ts +0 -251
- package/src/hooks/semantic/policies.ts +0 -596
- package/src/hooks/semantic/precondition-names.ts +0 -60
- package/src/hooks/semantic/preconditions.ts +0 -58
- package/src/hooks/semantic/redact.ts +0 -2910
- package/src/hooks/semantic/types.ts +0 -145
- package/src/hooks/semver-precedence.ts +0 -128
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{T8YAYM9h_64fKv705vug7 → PgeWCHmyVbjRznv2VO7KF}/_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
|
+
}
|