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,2111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LlamaIndex.TS (`llamaindex`, `@llamaindex/core`, `@llamaindex/workflow`).
|
|
3
|
+
*
|
|
4
|
+
* The TypeScript counterpart of the Python SDK's `integrations/llama_index.py`,
|
|
5
|
+
* and it must draw the same tree for the same program:
|
|
6
|
+
*
|
|
7
|
+
* | LlamaIndex.TS | FailproofAI |
|
|
8
|
+
* |---------------------------------------|-----------------------------------------------|
|
|
9
|
+
* | `AgentWorkflow` run (`agent()`) | session + `agent_start`/`agent_end` |
|
|
10
|
+
* | nested run (inside a tool, a scope) | nested `agent_start`/`agent_end` |
|
|
11
|
+
* | `multiAgent()` handoff | nested agent per agent holding the turn |
|
|
12
|
+
* | workflow step | `hook_triggered`/`hook_completed`, `trigger_event="workflow_step"` |
|
|
13
|
+
* | legacy `LLMAgent` / `AgentRunner` task| `agent_start`/`agent_end` across ALL its steps |
|
|
14
|
+
* | `createWorkflow()` workflow (core ≥1.1)| `agent_start`/`agent_end` (`"Workflow"`), steps as hooks |
|
|
15
|
+
* | LLM chat | `model_request`/`model_response` on `request_id` |
|
|
16
|
+
* | tool call | `tool_use`/`tool_result`, the model's call id |
|
|
17
|
+
* | retrieval | `tool_use`/`tool_result` named after the retriever's class, output summarised |
|
|
18
|
+
* | top-level chat engine / query engine / retriever / `llm.chat()` / tool | its own root run, named after its class |
|
|
19
|
+
*
|
|
20
|
+
* `agent_id` is the agent's `name` (`"Agent"` when unnamed, as in Python), the
|
|
21
|
+
* class name for a multi-agent workflow or a legacy runner — never an id.
|
|
22
|
+
*
|
|
23
|
+
* ## Two extension points, because LlamaIndex.TS has two halves
|
|
24
|
+
*
|
|
25
|
+
* **The callback bus** (`Settings.callbackManager`, from `@llamaindex/core/global`)
|
|
26
|
+
* carries everything below the agent: `llm-start`/`llm-stream`/`llm-end`,
|
|
27
|
+
* `llm-tool-call`/`llm-tool-result`, `retrieve-*`, `query-*`, and the legacy
|
|
28
|
+
* runner's `agent-start`/`agent-end`. Subscribing is the whole integration for
|
|
29
|
+
* that half, and uninstrumenting is unsubscribing.
|
|
30
|
+
*
|
|
31
|
+
* **The workflow runtime** (`@llamaindex/workflow` ≥1.1, on
|
|
32
|
+
* `@llamaindex/workflow-core`) emits NOTHING on that bus: an `agent().run()`
|
|
33
|
+
* has no run boundary and no step events there at all. What it does have is the
|
|
34
|
+
* middleware surface its own first-party middleware is built on — a context's
|
|
35
|
+
* `__internal__call_context` (wraps every step handler invocation; this is how
|
|
36
|
+
* `withTraceEvents` works) and `__internal__call_send_event` (sees every event
|
|
37
|
+
* a step sends; this is how `withState` works). So `AgentWorkflow.prototype.runStream`
|
|
38
|
+
* — which `run()` also goes through — is wrapped to open the run and to attach
|
|
39
|
+
* those two subscriptions to the context it creates. That is a prototype patch,
|
|
40
|
+
* so it records agents built before `instrument()` too.
|
|
41
|
+
*
|
|
42
|
+
* Three smaller hooks fill what neither half says, each on one object and each
|
|
43
|
+
* undone by `uninstrument()`:
|
|
44
|
+
*
|
|
45
|
+
* * **Invocation boundaries.** A chat engine's `chat()` dispatches nothing of
|
|
46
|
+
* its own, so its retrieval and its model call used to become two root runs
|
|
47
|
+
* in two sessions. Every `@wrapEventCaller` method runs as
|
|
48
|
+
* `storage.run(new EventCaller(...), fn)` on ONE module-private storage in
|
|
49
|
+
* `@llamaindex/core/global`; an own `run` on that storage (found through the
|
|
50
|
+
* exported `getEventCaller()`, see `eventCallerStorage`) sees each
|
|
51
|
+
* invocation start and return. A top-level invocation is then one run named
|
|
52
|
+
* after its class that ends when it returns, and an invocation that THROWS
|
|
53
|
+
* is the failure signal `wrapLLMEvent` and the legacy runner lack.
|
|
54
|
+
* * **Retriever names.** `retrieve-start` carries only the query; the patched
|
|
55
|
+
* `BaseRetriever.prototype.retrieve` binds the retriever for it.
|
|
56
|
+
* * **Plain workflows.** workflow-core ≥1.1 runs every step handler as
|
|
57
|
+
* `AsyncContext.Variable#run(handlerContext, …)`, a class it exports; its
|
|
58
|
+
* prototype `run` sees every step of every context. See `plainStep` for why
|
|
59
|
+
* such a run ends when its context goes idle.
|
|
60
|
+
*
|
|
61
|
+
* ## Correlation, which LlamaIndex.TS does not give us
|
|
62
|
+
*
|
|
63
|
+
* The bus events carry an id that pairs start with end and nothing that says
|
|
64
|
+
* which run they belong to. Two signals do, and we use both:
|
|
65
|
+
*
|
|
66
|
+
* * our own `AsyncLocalStorage` frame, bound around every workflow step we
|
|
67
|
+
* wrap. The bus dispatches in a `queueMicrotask`, which Node runs in the
|
|
68
|
+
* dispatcher's async context, so a handler sees the step that caused it;
|
|
69
|
+
* * LlamaIndex's own `EventCaller` (`event.reason`), set by `@wrapEventCaller`
|
|
70
|
+
* — on `AgentRunner.chat`, `BaseQueryEngine.query` and every first-party
|
|
71
|
+
* provider's `chat`. `withEventCaller` binds a FRESH `EventCaller` per
|
|
72
|
+
* invocation in LlamaIndex's own `AsyncLocalStorage`, chained through
|
|
73
|
+
* `.parent` to the invocation it ran inside. A legacy task or a query run is
|
|
74
|
+
* registered under the `EventCaller` of the invocation that opened it, and
|
|
75
|
+
* an event belongs to it when that exact object is on the event's chain.
|
|
76
|
+
*
|
|
77
|
+
* The invocation, NOT the object that owns it. One query engine (or one
|
|
78
|
+
* `LLMAgent`) built at startup and serving every request is the normal
|
|
79
|
+
* deployment, so the owner is the same object for every concurrent call; keyed
|
|
80
|
+
* by owner, request B's `query-start` found A's run, treated itself as nested
|
|
81
|
+
* and recorded nothing of its own. The `EventCaller` is per call and already
|
|
82
|
+
* flows through the async context, which is why it is used rather than a
|
|
83
|
+
* prototype patch of `query`/`chat`: `@wrapEventCaller` binds the method onto
|
|
84
|
+
* each INSTANCE at construction (`this.query = (...) => withEventCaller(...)`),
|
|
85
|
+
* so a prototype patch would miss every engine built before `instrument()`.
|
|
86
|
+
*
|
|
87
|
+
* Only a bus that carries no `EventCaller` (a build without one; the unit
|
|
88
|
+
* tests' stand-ins) falls back to matching the owner objects in
|
|
89
|
+
* `computedCallers`, and there a run owned by the object STARTING a new run is
|
|
90
|
+
* never taken as its parent: without the chain a concurrent sibling on a shared
|
|
91
|
+
* object is indistinguishable from re-entry, and the sibling is the common case.
|
|
92
|
+
*
|
|
93
|
+
* When both our frame and a caller match, the deeper run wins. When neither
|
|
94
|
+
* does, the call is a root run of its own — the LangChain/Python precedent for a
|
|
95
|
+
* bare model call — and nests under an enclosing `failproofai.session()`/
|
|
96
|
+
* `agent()` scope if there is one.
|
|
97
|
+
*
|
|
98
|
+
* ## Known gaps, each one the framework's and each one documented, not faked
|
|
99
|
+
*
|
|
100
|
+
* * A model call has no failure signal of its own: `wrapLLMEvent` has no error
|
|
101
|
+
* path (no `llm-end` when `chat()` throws). A provider whose `chat` is
|
|
102
|
+
* `@wrapEventCaller` (every first-party one) fails with its invocation, and
|
|
103
|
+
* inside a workflow the failed step closes it. A STREAM that fails while it
|
|
104
|
+
* is being read, outside a workflow, has returned already: that leaf stays
|
|
105
|
+
* open until the reaper (`staleAfter`) or `uninstrument()`.
|
|
106
|
+
* * A provider error mid-stream inside `agent().run()` escapes LlamaIndex's
|
|
107
|
+
* workflow runtime as an UNHANDLED rejection and `run()` never settles —
|
|
108
|
+
* with or without this SDK. The failed step still closes the run, failed.
|
|
109
|
+
* * `callTool` dispatches no `llm-tool-result` when a tool throws. In a
|
|
110
|
+
* workflow the runtime's own tool-result event closes it with the error; in
|
|
111
|
+
* a legacy agent the next model call does, from the tool-result message.
|
|
112
|
+
* * No embedding events exist on the TS bus, so `embeddings: true` has
|
|
113
|
+
* nothing to record.
|
|
114
|
+
* * Streamed calls carry token usage only when the provider sends it:
|
|
115
|
+
* `@llamaindex/openai` requests it only with
|
|
116
|
+
* `additionalChatOptions: { stream_options: { include_usage: true } }`.
|
|
117
|
+
* * A plain `createWorkflow()` workflow is a run only on workflow-core ≥1.1
|
|
118
|
+
* resolvable from the application (not the floor's `@llama-flow/core`, not
|
|
119
|
+
* an unhoisted pnpm layout): elsewhere its model calls are loose root runs.
|
|
120
|
+
* Its run ends when the context goes idle, so a workflow that waits for an
|
|
121
|
+
* event from outside records each burst as its own run, and it is named
|
|
122
|
+
* `"Workflow"` — the runtime has no name to give it (wrap it in
|
|
123
|
+
* `failproofai.agent("name", …)` to name the parent).
|
|
124
|
+
* * No human-in-the-loop pairs: the TS runtime has no waiting-for-event signal.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
128
|
+
import { randomUUID } from "node:crypto";
|
|
129
|
+
import { performance } from "node:perf_hooks";
|
|
130
|
+
|
|
131
|
+
import { logger } from "../logger.js";
|
|
132
|
+
import * as compat from "./compat.js";
|
|
133
|
+
import * as core from "./core.js";
|
|
134
|
+
import type { Adapter } from "./core.js";
|
|
135
|
+
|
|
136
|
+
const NAME = "llamaindex";
|
|
137
|
+
const PACKAGE = "llamaindex";
|
|
138
|
+
const CORE_PACKAGE = "@llamaindex/core";
|
|
139
|
+
const WORKFLOW_PACKAGE = "@llamaindex/workflow";
|
|
140
|
+
const ASYNC_CONTEXT_MODULE = "@llamaindex/workflow-core/async-context";
|
|
141
|
+
const INSTALL = "npm install llamaindex";
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 0.11.4 is a CAPABILITY floor: it is the first `llamaindex` whose agent API
|
|
145
|
+
* (`agent()` / `multiAgent()`) runs on the `@llamaindex/workflow` 1.1 runtime
|
|
146
|
+
* this adapter wraps. 0.9–0.11.3 ship workflow 1.0, a different class-based
|
|
147
|
+
* runtime with none of the surfaces above — their workflow agents would record
|
|
148
|
+
* no run and no steps, only loose model and tool calls.
|
|
149
|
+
*/
|
|
150
|
+
export const MIN_VERSION = "0.11.4";
|
|
151
|
+
export const BELOW_VERSION = "1.0.0";
|
|
152
|
+
const WORKFLOW_MIN = "1.1.0";
|
|
153
|
+
const WORKFLOW_BELOW = "2.0.0";
|
|
154
|
+
|
|
155
|
+
const MAX_NODES_IN_SUMMARY = 5;
|
|
156
|
+
/**
|
|
157
|
+
* Open runs and leaves are bounded, oldest evicted first: orphans are normal (a
|
|
158
|
+
* stream nobody consumed, a legacy task whose step threw and so never sent
|
|
159
|
+
* `agent-end`) and a long-lived server must not keep every one of them.
|
|
160
|
+
*/
|
|
161
|
+
const MAX_OPEN = 10_000;
|
|
162
|
+
|
|
163
|
+
// Token key aliases, widest first. LlamaIndex normalises nothing, so this is
|
|
164
|
+
// the union of what the provider packages actually put in `raw` — the Python
|
|
165
|
+
// adapter's list, plus the camelCase spellings TS providers use.
|
|
166
|
+
const INPUT_TOKEN_KEYS = [
|
|
167
|
+
"prompt_tokens",
|
|
168
|
+
"input_tokens",
|
|
169
|
+
"inputTokens",
|
|
170
|
+
"promptTokens",
|
|
171
|
+
"prompt_token_count",
|
|
172
|
+
"promptTokenCount",
|
|
173
|
+
] as const;
|
|
174
|
+
const OUTPUT_TOKEN_KEYS = [
|
|
175
|
+
"completion_tokens",
|
|
176
|
+
"output_tokens",
|
|
177
|
+
"outputTokens",
|
|
178
|
+
"completionTokens",
|
|
179
|
+
"candidates_token_count",
|
|
180
|
+
"candidatesTokenCount",
|
|
181
|
+
] as const;
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Pure helpers — no framework import in any of these
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
type Json = Record<string, unknown>;
|
|
188
|
+
|
|
189
|
+
function isObject(value: unknown): value is Json {
|
|
190
|
+
return typeof value === "object" && value !== null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** A property read that cannot throw (getters on framework classes can). */
|
|
194
|
+
function read(target: unknown, key: string): unknown {
|
|
195
|
+
if (!isObject(target) && typeof target !== "function") return undefined;
|
|
196
|
+
try {
|
|
197
|
+
return (target as Json)[key];
|
|
198
|
+
} catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function nonEmpty(value: unknown): string | undefined {
|
|
204
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A correlation id out of a payload field whose type the framework does not
|
|
209
|
+
* promise. An object would render as `[object Object]` and correlate with every
|
|
210
|
+
* other one; a fresh id leaves the pair merely unpaired instead.
|
|
211
|
+
*/
|
|
212
|
+
function asId(...candidates: unknown[]): string {
|
|
213
|
+
for (const candidate of candidates) {
|
|
214
|
+
if (typeof candidate === "string" && candidate !== "") return candidate;
|
|
215
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) return String(candidate);
|
|
216
|
+
}
|
|
217
|
+
return randomUUID();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function firstInt(source: Json, keys: readonly string[]): number | undefined {
|
|
221
|
+
for (const key of keys) {
|
|
222
|
+
const value = source[key];
|
|
223
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
|
|
224
|
+
}
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface Usage {
|
|
229
|
+
usage?: Json;
|
|
230
|
+
inputTokens?: number;
|
|
231
|
+
outputTokens?: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Token usage from a `ChatResponse`, streaming or not.
|
|
236
|
+
*
|
|
237
|
+
* Conservative on purpose, like the Python adapter: the token numbers are set
|
|
238
|
+
* ONLY when a key we recognise is present, while the raw usage object always
|
|
239
|
+
* ships as `usage` so a provider that names its counters something new still
|
|
240
|
+
* reports something the server can fall back to.
|
|
241
|
+
*
|
|
242
|
+
* A streamed response is the case that used to lose everything: `wrapLLMEvent`
|
|
243
|
+
* hands `llm-end` a `raw` that is the ARRAY of chunks, and providers put the
|
|
244
|
+
* usage on one chunk — OpenAI on the last, content-less one — so reading
|
|
245
|
+
* `raw.usage` found nothing on every streamed call, which is every
|
|
246
|
+
* `FunctionAgent` call.
|
|
247
|
+
*/
|
|
248
|
+
export function usageOf(response: unknown): Usage {
|
|
249
|
+
const raw = read(response, "raw");
|
|
250
|
+
const candidates: unknown[] = [];
|
|
251
|
+
const fromChunk = (chunk: unknown): void => {
|
|
252
|
+
const chunkRaw = read(chunk, "raw");
|
|
253
|
+
candidates.push(
|
|
254
|
+
read(chunkRaw, "usage"),
|
|
255
|
+
read(chunkRaw, "usage_metadata"),
|
|
256
|
+
read(chunkRaw, "usageMetadata"),
|
|
257
|
+
read(read(chunk, "options"), "usage"),
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
if (Array.isArray(raw)) {
|
|
261
|
+
// Newest chunk first: providers that report running totals end with the final one.
|
|
262
|
+
for (let i = raw.length - 1; i >= 0; i -= 1) fromChunk(raw[i]);
|
|
263
|
+
} else {
|
|
264
|
+
candidates.push(read(raw, "usage"), read(raw, "usage_metadata"), read(raw, "usageMetadata"));
|
|
265
|
+
}
|
|
266
|
+
candidates.push(read(read(read(response, "message"), "options"), "usage"), read(response, "usage"));
|
|
267
|
+
for (const candidate of candidates) {
|
|
268
|
+
if (!isObject(candidate) || Array.isArray(candidate) || Object.keys(candidate).length === 0) continue;
|
|
269
|
+
return {
|
|
270
|
+
usage: candidate,
|
|
271
|
+
inputTokens: firstInt(candidate, INPUT_TOKEN_KEYS),
|
|
272
|
+
outputTokens: firstInt(candidate, OUTPUT_TOKEN_KEYS),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function stopReasonOf(response: unknown): string | undefined {
|
|
279
|
+
const raw = read(response, "raw");
|
|
280
|
+
const sources = Array.isArray(raw) ? [...raw].reverse().map((chunk) => read(chunk, "raw")) : [raw];
|
|
281
|
+
for (const source of sources) {
|
|
282
|
+
const choice = (read(source, "choices") as unknown[] | undefined)?.[0];
|
|
283
|
+
const value =
|
|
284
|
+
nonEmpty(read(choice, "finish_reason")) ??
|
|
285
|
+
nonEmpty(read(source, "stop_reason")) ??
|
|
286
|
+
nonEmpty(read(source, "finishReason"));
|
|
287
|
+
if (value) return value;
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** A retrieval result small enough to store: count, scores, a prefix of the top few. */
|
|
293
|
+
export function summarizeNodes(nodes: unknown): { num_nodes: number; top: Json[] } {
|
|
294
|
+
const items = Array.isArray(nodes) ? nodes : [];
|
|
295
|
+
const top = items.slice(0, MAX_NODES_IN_SUMMARY).map((item) => {
|
|
296
|
+
const node = read(item, "node") ?? item;
|
|
297
|
+
let text: unknown;
|
|
298
|
+
const getContent = read(node, "getContent");
|
|
299
|
+
if (typeof getContent === "function") {
|
|
300
|
+
try {
|
|
301
|
+
text = (getContent as () => unknown).call(node);
|
|
302
|
+
} catch {
|
|
303
|
+
text = undefined;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
text ??= read(node, "text");
|
|
307
|
+
const score = read(item, "score");
|
|
308
|
+
return {
|
|
309
|
+
id: nonEmpty(read(node, "id_")) ?? nonEmpty(read(node, "id")),
|
|
310
|
+
score: typeof score === "number" ? score : undefined,
|
|
311
|
+
text: core.truncate(typeof text === "string" ? text : "", 200),
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
return { num_nodes: items.length, top };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function messagesOf(messages: unknown): Json[] | undefined {
|
|
318
|
+
if (!Array.isArray(messages)) return undefined;
|
|
319
|
+
return messages.map((message) => ({
|
|
320
|
+
role: nonEmpty(read(message, "role")) ?? "user",
|
|
321
|
+
content: read(message, "content"),
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** The text of a query or a `QueryBundle`. */
|
|
326
|
+
function queryText(query: unknown): unknown {
|
|
327
|
+
if (typeof query === "string") return query;
|
|
328
|
+
return read(query, "query") ?? read(query, "queryStr") ?? query;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The text of whatever a run returned — `EngineResponse`, a message, a string. */
|
|
332
|
+
function textOf(value: unknown): string | undefined {
|
|
333
|
+
if (typeof value === "string") return value;
|
|
334
|
+
const content = read(read(value, "message"), "content") ?? read(value, "response") ?? read(value, "content");
|
|
335
|
+
if (typeof content === "string") return content;
|
|
336
|
+
if (Array.isArray(content)) {
|
|
337
|
+
const parts = content.map((part) => read(part, "text")).filter((part) => typeof part === "string");
|
|
338
|
+
if (parts.length > 0) return parts.join("");
|
|
339
|
+
}
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function errorText(error: unknown): string {
|
|
344
|
+
if (error instanceof Error) return `${error.name || "Error"}: ${error.message}`;
|
|
345
|
+
return String(error);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function className(value: unknown): string | undefined {
|
|
349
|
+
const name = read(read(value, "constructor"), "name");
|
|
350
|
+
return typeof name === "string" && name !== "" && name !== "Object" && name !== "Function"
|
|
351
|
+
? name
|
|
352
|
+
: undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** `event.reason.computedCallers` — the objects inside whose `@wrapEventCaller` calls this ran. */
|
|
356
|
+
function callersOf(event: unknown): unknown[] {
|
|
357
|
+
const callers = read(read(event, "reason"), "computedCallers");
|
|
358
|
+
return Array.isArray(callers) ? callers : [];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The `EventCaller` chain of an event's `reason`, innermost first — or `null`
|
|
363
|
+
* when the reason is not an `EventCaller` (no `caller` field), in which case
|
|
364
|
+
* only the owner objects in `computedCallers` are known.
|
|
365
|
+
*/
|
|
366
|
+
function invocationChain(reason: unknown): object[] | null {
|
|
367
|
+
if (!isObject(reason)) return null;
|
|
368
|
+
try {
|
|
369
|
+
if (!("caller" in reason)) return null;
|
|
370
|
+
} catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const chain: object[] = [];
|
|
374
|
+
const seen = new Set<unknown>();
|
|
375
|
+
let node: unknown = reason;
|
|
376
|
+
while (isObject(node) && !seen.has(node)) {
|
|
377
|
+
seen.add(node);
|
|
378
|
+
chain.push(node);
|
|
379
|
+
node = read(node, "parent");
|
|
380
|
+
}
|
|
381
|
+
return chain;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Where an event came from: LlamaIndex's invocation chain, and the owner objects on it. */
|
|
385
|
+
interface Origin {
|
|
386
|
+
chain: object[] | null;
|
|
387
|
+
callers: unknown[];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function originOf(event: unknown): Origin {
|
|
391
|
+
return { chain: invocationChain(read(event, "reason")), callers: callersOf(event) };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The model name for a bus event.
|
|
396
|
+
*
|
|
397
|
+
* `llm-start` carries only `{id, messages}`: `wrapLLMEvent` never passes the
|
|
398
|
+
* model. Every first-party provider decorates `chat` with `@wrapEventCaller`
|
|
399
|
+
* too, so the LLM instance itself is the nearest caller, and its
|
|
400
|
+
* `metadata.model` is the name. A legacy runner above it has the LLM as `.llm`.
|
|
401
|
+
*/
|
|
402
|
+
function modelFromCallers(event: unknown): string | undefined {
|
|
403
|
+
for (const caller of callersOf(event)) {
|
|
404
|
+
const model = nonEmpty(read(read(caller, "metadata"), "model"));
|
|
405
|
+
if (model) return model;
|
|
406
|
+
const viaLlm = nonEmpty(read(read(read(caller, "llm"), "metadata"), "model"));
|
|
407
|
+
if (viaLlm) return viaLlm;
|
|
408
|
+
}
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function detail(event: unknown): Json {
|
|
413
|
+
// A `CustomEvent`, so the payload is on `.detail` — with a fallback to the
|
|
414
|
+
// event itself, for a build that dispatched the payload directly.
|
|
415
|
+
const payload = read(event, "detail") ?? event;
|
|
416
|
+
return isObject(payload) ? payload : {};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function numberOption(value: unknown, fallback: number): number {
|
|
420
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// State
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
/** One agent span we opened: a workflow run, a sub-agent, a legacy task, a bare call. */
|
|
428
|
+
interface Run {
|
|
429
|
+
key: string;
|
|
430
|
+
agentId: string;
|
|
431
|
+
depth: number;
|
|
432
|
+
/** The run this one is nested in, when it is a multi-agent sub-agent. */
|
|
433
|
+
root: Run | null;
|
|
434
|
+
leaves: Set<string>;
|
|
435
|
+
usedToolIds: Map<string, number>;
|
|
436
|
+
/** Sub-agent currently holding the turn (multi-agent workflows only). */
|
|
437
|
+
sub: Run | null;
|
|
438
|
+
subSeq: number;
|
|
439
|
+
/** The object whose `@wrapEventCaller` calls belong to this run, if any. */
|
|
440
|
+
owner: object | null;
|
|
441
|
+
/** LlamaIndex's `EventCaller` for the ONE invocation that opened this run, if any. */
|
|
442
|
+
caller: object | null;
|
|
443
|
+
/** Workflow step keys linked to this run and not yet ended. */
|
|
444
|
+
steps: Set<string>;
|
|
445
|
+
/** Legacy runs have no end signal until their last step; bare runs end with their leaf. */
|
|
446
|
+
bare: boolean;
|
|
447
|
+
/**
|
|
448
|
+
* Ends when the LlamaIndex invocation in `caller` returns — a chat engine, a
|
|
449
|
+
* bare model call — because nothing on the bus marks its end. (Query and
|
|
450
|
+
* legacy runs have their own end event and take only a FAILURE from there.)
|
|
451
|
+
*/
|
|
452
|
+
byInvocation: boolean;
|
|
453
|
+
/** Plain-workflow runs: step handlers still running, and the last step's output text. */
|
|
454
|
+
inFlight: number;
|
|
455
|
+
output?: string;
|
|
456
|
+
/** A run whose end arrived while a streamed leaf was still open. */
|
|
457
|
+
ending: { outcome: string; summary?: string } | null;
|
|
458
|
+
lastContent?: string;
|
|
459
|
+
model?: string;
|
|
460
|
+
ended: boolean;
|
|
461
|
+
/** `performance.now()` of the last event that touched this run; what the reaper reads. */
|
|
462
|
+
lastActivity: number;
|
|
463
|
+
/** Drops this run from whichever index found it (legacy tasks, queries). */
|
|
464
|
+
forget?: () => void;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
interface Leaf {
|
|
468
|
+
key: string;
|
|
469
|
+
kind: "model" | "tool" | "retrieval";
|
|
470
|
+
run: Run;
|
|
471
|
+
parentKey: string;
|
|
472
|
+
name: string;
|
|
473
|
+
callId: string;
|
|
474
|
+
rawId?: string;
|
|
475
|
+
/** Who opened a tool leaf: the callback bus, or the workflow runtime's own event. */
|
|
476
|
+
source?: "bus" | "workflow";
|
|
477
|
+
started: number;
|
|
478
|
+
model?: string;
|
|
479
|
+
firstChunk?: number;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** What the adapter's `AsyncLocalStorage` carries through a workflow step. */
|
|
483
|
+
interface Frame {
|
|
484
|
+
key: string;
|
|
485
|
+
run: Run;
|
|
486
|
+
model?: string;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
interface Located {
|
|
490
|
+
parentKey: string;
|
|
491
|
+
run: Run;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export interface LlamaIndexOptions {
|
|
495
|
+
captureMessages: boolean;
|
|
496
|
+
steps: boolean;
|
|
497
|
+
embeddings: boolean;
|
|
498
|
+
staleAfter: number;
|
|
499
|
+
reaperInterval: number;
|
|
500
|
+
captureLimit?: number;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function parseOptions(options: Record<string, unknown>): LlamaIndexOptions {
|
|
504
|
+
return {
|
|
505
|
+
captureMessages: options.captureMessages !== false,
|
|
506
|
+
steps: options.steps !== false,
|
|
507
|
+
embeddings: options.embeddings === true,
|
|
508
|
+
staleAfter: numberOption(options.staleAfter, 600),
|
|
509
|
+
reaperInterval: numberOption(options.reaperInterval, 30),
|
|
510
|
+
captureLimit: typeof options.captureLimit === "number" ? options.captureLimit : undefined,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
interface Bus {
|
|
515
|
+
on: (event: string, handler: (event: unknown) => void) => unknown;
|
|
516
|
+
off?: (event: string, handler: (event: unknown) => void) => unknown;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** One loaded copy of `@llamaindex/core/global` (or of the umbrella re-exporting it). */
|
|
520
|
+
export interface GlobalModule {
|
|
521
|
+
Settings?: { callbackManager?: unknown };
|
|
522
|
+
getEventCaller?: () => unknown;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** One loaded copy of `@llamaindex/core/retriever`. */
|
|
526
|
+
export interface RetrieverModule {
|
|
527
|
+
BaseRetriever?: { prototype: object };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** One loaded copy of `@llamaindex/workflow-core/async-context` (workflow-core ≥1.1). */
|
|
531
|
+
export interface AsyncContextModule {
|
|
532
|
+
AsyncContext?: { Variable?: { prototype: object } };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** One loaded copy of `@llamaindex/workflow`. */
|
|
536
|
+
export interface WorkflowModule {
|
|
537
|
+
AgentWorkflow?: { prototype: object; name?: string };
|
|
538
|
+
stopAgentEvent?: { include: (event: unknown) => boolean };
|
|
539
|
+
agentToolCallEvent?: { include: (event: unknown) => boolean };
|
|
540
|
+
agentToolCallResultEvent?: { include: (event: unknown) => boolean };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
interface Subscribable {
|
|
544
|
+
subscribe: (callback: (...args: never[]) => unknown) => unknown;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
interface HandlerContext {
|
|
548
|
+
handler: (...args: unknown[]) => unknown;
|
|
549
|
+
[key: string]: unknown;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
class State {
|
|
553
|
+
readonly options: LlamaIndexOptions;
|
|
554
|
+
readonly tracker: core.RunTracker;
|
|
555
|
+
readonly frames = new AsyncLocalStorage<Frame>();
|
|
556
|
+
private readonly runs = new Map<string, Run>();
|
|
557
|
+
private readonly leaves = new Map<string, Leaf>();
|
|
558
|
+
private readonly owners = new WeakMap<object, Run[]>();
|
|
559
|
+
/** Runs by the `EventCaller` of the invocation that opened them. */
|
|
560
|
+
private readonly invocations = new WeakMap<object, Run>();
|
|
561
|
+
/** Legacy task: first step id -> run. */
|
|
562
|
+
private readonly tasks = new Map<string, Run>();
|
|
563
|
+
/** Root query-engine runs, by `query-start` id. */
|
|
564
|
+
private readonly queries = new Map<string, Run>();
|
|
565
|
+
/** The retriever whose `retrieve()` is running, for the retrieval's name. */
|
|
566
|
+
readonly retrievers = new AsyncLocalStorage<object>();
|
|
567
|
+
/** `EventCaller`s whose invocation we watch start and end (see `invocation`). */
|
|
568
|
+
private readonly observed = new WeakSet<object>();
|
|
569
|
+
/** `EventCaller`s whose invocation has already returned or thrown. */
|
|
570
|
+
private readonly returned = new WeakSet<object>();
|
|
571
|
+
/** Model leaves opened directly inside an invocation (not in a workflow step), by its `EventCaller`. */
|
|
572
|
+
private readonly invocationLeaves = new WeakMap<object, Set<string>>();
|
|
573
|
+
/** Errors a leaf already carries, so the run they end does not report them again. */
|
|
574
|
+
private readonly carried = new WeakSet<object>();
|
|
575
|
+
/** Plain-workflow runs, by the workflow-core root handler context of their context. */
|
|
576
|
+
private readonly plainRuns = new WeakMap<object, Run>();
|
|
577
|
+
/** Step handlers of `AgentWorkflow`s, which are recorded as agent runs instead. */
|
|
578
|
+
private readonly agentHandlers = new WeakSet<object>();
|
|
579
|
+
private readonly globals: GlobalModule[];
|
|
580
|
+
private reaper: ReturnType<typeof setInterval> | null = null;
|
|
581
|
+
private seq = 0;
|
|
582
|
+
active = true;
|
|
583
|
+
|
|
584
|
+
constructor(options: LlamaIndexOptions, globals: GlobalModule[], frameworkPackage: string) {
|
|
585
|
+
this.options = options;
|
|
586
|
+
this.globals = globals;
|
|
587
|
+
this.tracker = new core.RunTracker(NAME, {
|
|
588
|
+
baseFields: core.frameworkFields(NAME, frameworkPackage),
|
|
589
|
+
fieldLimit: options.captureLimit,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** The one gate every payload goes through — `captureMessages: false` drops them all. */
|
|
594
|
+
capture<T>(value: T): T | undefined {
|
|
595
|
+
return this.options.captureMessages ? value : undefined;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private nextKey(prefix: string): string {
|
|
599
|
+
this.seq += 1;
|
|
600
|
+
return `${prefix}#${this.seq}`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// -- where does this event belong -----------------------------------------
|
|
604
|
+
|
|
605
|
+
private live(run: Run | undefined | null): Run | null {
|
|
606
|
+
return run && !run.ended ? run : null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** The innermost run opened by an invocation on this chain — exact, per call. */
|
|
610
|
+
private invocationRun(chain: object[]): Run | null {
|
|
611
|
+
for (const node of chain) {
|
|
612
|
+
const run = this.live(this.invocations.get(node));
|
|
613
|
+
if (run) return run;
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Fallback for a bus without `EventCaller`s: the newest live run of an owner
|
|
620
|
+
* object on the caller list. `starting` is the owner of a run being opened
|
|
621
|
+
* now; its own runs are skipped, because without the chain they are far more
|
|
622
|
+
* likely concurrent siblings on a shared object than this call's parent.
|
|
623
|
+
*/
|
|
624
|
+
private ownerRun(callers: unknown[], starting?: unknown): Run | null {
|
|
625
|
+
for (const caller of callers) {
|
|
626
|
+
if (!isObject(caller) || caller === starting) continue;
|
|
627
|
+
const stack = this.owners.get(caller);
|
|
628
|
+
const run = this.live(stack?.[stack.length - 1]);
|
|
629
|
+
if (run) return run;
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* The run an event belongs to: our step frame or LlamaIndex's invocation
|
|
636
|
+
* chain, whichever is deeper. `origin` defaults to the chain bound right now,
|
|
637
|
+
* for callers (a workflow starting) that have no event to read it from.
|
|
638
|
+
* `starting` is the owner of a run about to be opened (see `ownerRun`).
|
|
639
|
+
*/
|
|
640
|
+
locate(origin?: Origin, starting?: unknown): Located | null {
|
|
641
|
+
const from = origin ?? this.boundOrigin();
|
|
642
|
+
const frame = this.frames.getStore();
|
|
643
|
+
const fromFrame = frame && this.live(frame.run) ? { parentKey: frame.key, run: frame.run } : null;
|
|
644
|
+
// With an `EventCaller` chain, ONLY the chain decides: a run whose owner is
|
|
645
|
+
// on the caller list but whose invocation is not on the chain belongs to a
|
|
646
|
+
// different call — a concurrent request on a shared engine — never this one.
|
|
647
|
+
const owner = from.chain ? this.invocationRun(from.chain) : this.ownerRun(from.callers, starting);
|
|
648
|
+
const fromOwner = owner ? { parentKey: (owner.sub ?? owner).key, run: owner.sub ?? owner } : null;
|
|
649
|
+
if (fromFrame && fromOwner) return fromOwner.run.depth > fromFrame.run.depth ? fromOwner : fromFrame;
|
|
650
|
+
return fromFrame ?? fromOwner;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private boundOrigin(): Origin {
|
|
654
|
+
for (const module of this.globals) {
|
|
655
|
+
try {
|
|
656
|
+
const caller = module.getEventCaller?.();
|
|
657
|
+
if (!isObject(caller)) continue;
|
|
658
|
+
const callers = read(caller, "computedCallers");
|
|
659
|
+
return { chain: invocationChain(caller), callers: Array.isArray(callers) ? callers : [] };
|
|
660
|
+
} catch {
|
|
661
|
+
// An older build without `getEventCaller`; the frame is still consulted.
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return { chain: null, callers: [] };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// -- runs ---------------------------------------------------------------
|
|
668
|
+
|
|
669
|
+
openRun(
|
|
670
|
+
prefix: string,
|
|
671
|
+
agentId: string,
|
|
672
|
+
options: {
|
|
673
|
+
parent?: Located | null;
|
|
674
|
+
owner?: object | null;
|
|
675
|
+
caller?: object | null;
|
|
676
|
+
bare?: boolean;
|
|
677
|
+
byInvocation?: boolean;
|
|
678
|
+
goal?: unknown;
|
|
679
|
+
root?: Run | null;
|
|
680
|
+
fields?: Json;
|
|
681
|
+
},
|
|
682
|
+
): Run {
|
|
683
|
+
while (this.runs.size >= MAX_OPEN) {
|
|
684
|
+
const oldest = this.runs.values().next();
|
|
685
|
+
if (oldest.done) break;
|
|
686
|
+
this.finishRun(oldest.value, "cancelled", undefined, "evicted");
|
|
687
|
+
}
|
|
688
|
+
const key = this.nextKey(prefix);
|
|
689
|
+
const identity = this.tracker.startAgent(key, {
|
|
690
|
+
agentId,
|
|
691
|
+
parentKey: options.parent?.parentKey,
|
|
692
|
+
goal: this.options.captureMessages && typeof options.goal === "string" ? options.goal : undefined,
|
|
693
|
+
...core.fwFields({ run_id: key, ...(options.fields ?? {}) }),
|
|
694
|
+
});
|
|
695
|
+
const run: Run = {
|
|
696
|
+
key,
|
|
697
|
+
agentId: identity.agentId ?? agentId,
|
|
698
|
+
depth: identity.depth,
|
|
699
|
+
root: options.root ?? null,
|
|
700
|
+
leaves: new Set(),
|
|
701
|
+
usedToolIds: new Map(),
|
|
702
|
+
sub: null,
|
|
703
|
+
subSeq: 0,
|
|
704
|
+
owner: options.owner ?? null,
|
|
705
|
+
caller: options.caller ?? null,
|
|
706
|
+
steps: new Set(),
|
|
707
|
+
bare: options.bare ?? false,
|
|
708
|
+
byInvocation: options.byInvocation ?? false,
|
|
709
|
+
inFlight: 0,
|
|
710
|
+
ending: null,
|
|
711
|
+
ended: false,
|
|
712
|
+
lastActivity: performance.now(),
|
|
713
|
+
};
|
|
714
|
+
this.runs.set(key, run);
|
|
715
|
+
if (run.owner) {
|
|
716
|
+
const stack = this.owners.get(run.owner) ?? [];
|
|
717
|
+
stack.push(run);
|
|
718
|
+
this.owners.set(run.owner, stack);
|
|
719
|
+
}
|
|
720
|
+
if (run.caller) this.invocations.set(run.caller, run);
|
|
721
|
+
return run;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* End a run. A success whose streamed model call is still being consumed is
|
|
726
|
+
* DEFERRED until that leaf closes, so the tokens are not lost; anything else
|
|
727
|
+
* force-closes what is open first, because an `agent_end` with an open leaf
|
|
728
|
+
* under it leaves the session `ongoing` forever.
|
|
729
|
+
*/
|
|
730
|
+
finishRun(run: Run, outcome: string, summary?: string, reason = "run_ended"): void {
|
|
731
|
+
if (run.ended) return;
|
|
732
|
+
if (run.sub) this.finishRun(run.sub, outcome, undefined, reason);
|
|
733
|
+
if (outcome === "success" && run.leaves.size > 0 && reason === "run_ended") {
|
|
734
|
+
run.ending = { outcome, summary };
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
// Ended BEFORE its leaves are force-closed: closing the last leaf of a bare
|
|
738
|
+
// run would otherwise settle it as a success in the middle of this.
|
|
739
|
+
run.ended = true;
|
|
740
|
+
for (const key of [...run.leaves]) {
|
|
741
|
+
const leaf = this.leaves.get(key);
|
|
742
|
+
if (leaf) this.closeLeaf(leaf, { closedBy: reason });
|
|
743
|
+
}
|
|
744
|
+
this.runs.delete(run.key);
|
|
745
|
+
run.forget?.();
|
|
746
|
+
if (run.owner) {
|
|
747
|
+
const stack = this.owners.get(run.owner);
|
|
748
|
+
const index = stack?.lastIndexOf(run) ?? -1;
|
|
749
|
+
if (stack && index !== -1) stack.splice(index, 1);
|
|
750
|
+
if (stack?.length === 0) this.owners.delete(run.owner);
|
|
751
|
+
}
|
|
752
|
+
if (run.caller && this.invocations.get(run.caller) === run) this.invocations.delete(run.caller);
|
|
753
|
+
if (run.root && run.root.sub === run) run.root.sub = null;
|
|
754
|
+
const text = summary ?? (outcome === "success" ? run.lastContent : undefined);
|
|
755
|
+
this.tracker.endAgent(run.key, {
|
|
756
|
+
outcome,
|
|
757
|
+
summary: this.options.captureMessages || outcome !== "success" ? text : undefined,
|
|
758
|
+
...core.fwFields({ run_id: run.key }),
|
|
759
|
+
});
|
|
760
|
+
// Every tracker link this run made goes with it — its own and any step
|
|
761
|
+
// still in flight. A leaked link is worse than memory: at the tracker's
|
|
762
|
+
// FIFO cap, the next eviction takes a LIVE run's link and its events drop.
|
|
763
|
+
for (const step of run.steps) this.tracker.unlink(step);
|
|
764
|
+
run.steps.clear();
|
|
765
|
+
this.tracker.unlink(run.key);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* A deferred or bare run whose last leaf just closed. A bare run IS its one
|
|
770
|
+
* call, so it ends the way that call did: a failure, a reaped orphan
|
|
771
|
+
* (`cancelled` — we never learned how it ended), or a success.
|
|
772
|
+
*/
|
|
773
|
+
private settle(run: Run, outcome: string): void {
|
|
774
|
+
if (run.ended || run.leaves.size > 0) return;
|
|
775
|
+
if (run.ending) this.finishRun(run, run.ending.outcome, run.ending.summary);
|
|
776
|
+
else if (run.bare) this.finishRun(run, outcome, undefined, outcome === "success" ? "run_ended" : "leaf");
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// -- leaves -------------------------------------------------------------
|
|
780
|
+
|
|
781
|
+
private openLeaf(leaf: Leaf): void {
|
|
782
|
+
while (this.leaves.size >= MAX_OPEN) {
|
|
783
|
+
const oldest = this.leaves.values().next();
|
|
784
|
+
if (oldest.done) break;
|
|
785
|
+
this.closeLeaf(oldest.value, { closedBy: "evicted" });
|
|
786
|
+
}
|
|
787
|
+
this.leaves.set(leaf.key, leaf);
|
|
788
|
+
leaf.run.leaves.add(leaf.key);
|
|
789
|
+
leaf.run.lastActivity = performance.now();
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
closeLeaf(leaf: Leaf, result: { output?: unknown; error?: string; response?: unknown; closedBy?: string }): void {
|
|
793
|
+
if (!this.leaves.delete(leaf.key)) return;
|
|
794
|
+
leaf.run.leaves.delete(leaf.key);
|
|
795
|
+
leaf.run.lastActivity = performance.now();
|
|
796
|
+
const extras = core.fwFields({ run_id: leaf.run.key, closed_by: result.closedBy });
|
|
797
|
+
if (leaf.kind === "model") {
|
|
798
|
+
const response = result.response;
|
|
799
|
+
const usage = usageOf(response);
|
|
800
|
+
const content = read(read(response, "message"), "content");
|
|
801
|
+
if (typeof content === "string" && content !== "") leaf.run.lastContent = content;
|
|
802
|
+
const now = performance.now();
|
|
803
|
+
this.tracker.emit("modelResponse", leaf.key, {
|
|
804
|
+
parentKey: leaf.parentKey,
|
|
805
|
+
model: leaf.model,
|
|
806
|
+
requestId: leaf.callId,
|
|
807
|
+
role: response === undefined ? undefined : "assistant",
|
|
808
|
+
content: this.capture(content),
|
|
809
|
+
stopReason: stopReasonOf(response),
|
|
810
|
+
inputTokens: usage.inputTokens,
|
|
811
|
+
outputTokens: usage.outputTokens,
|
|
812
|
+
usage: usage.usage,
|
|
813
|
+
error: result.error,
|
|
814
|
+
// Always an int: `modelResponse` does not measure its own duration.
|
|
815
|
+
duration_ms: core.ms(now - leaf.started),
|
|
816
|
+
...extras,
|
|
817
|
+
...core.fwFields({
|
|
818
|
+
ttft_ms: leaf.firstChunk === undefined ? undefined : core.ms(leaf.firstChunk - leaf.started),
|
|
819
|
+
chunks: Array.isArray(read(response, "raw")) ? (read(response, "raw") as unknown[]).length : undefined,
|
|
820
|
+
}),
|
|
821
|
+
});
|
|
822
|
+
} else {
|
|
823
|
+
this.tracker.emit("toolResult", leaf.key, {
|
|
824
|
+
parentKey: leaf.parentKey,
|
|
825
|
+
toolName: leaf.name,
|
|
826
|
+
toolCallId: leaf.callId,
|
|
827
|
+
output: result.error === undefined ? this.capture(result.output) : undefined,
|
|
828
|
+
error: result.error,
|
|
829
|
+
...extras,
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
this.tracker.unlink(leaf.key);
|
|
833
|
+
this.settle(leaf.run, result.error !== undefined ? "failed" : result.closedBy ? "cancelled" : "success");
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// -- the bus --------------------------------------------------------------
|
|
837
|
+
|
|
838
|
+
/** True when the outermost invocation on the chain is one we can see end. */
|
|
839
|
+
private observedOuter(origin: Origin): boolean {
|
|
840
|
+
const outer = origin.chain?.[origin.chain.length - 1];
|
|
841
|
+
return outer !== undefined && this.observed.has(outer) && !this.returned.has(outer);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** Where a bus event goes; opens a root run when it belongs to nothing. */
|
|
845
|
+
private placeOrOpen(event: unknown, prefix: string, agentId: () => string, origin = originOf(event)): Located {
|
|
846
|
+
const located = this.locate(origin);
|
|
847
|
+
if (located) return located;
|
|
848
|
+
const run = this.openRoot(origin, prefix, agentId);
|
|
849
|
+
return { parentKey: run.key, run };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* The root run for an event nothing encloses: Python's rule, "any other
|
|
854
|
+
* top-level instrumented call opens the session and becomes its root agent,
|
|
855
|
+
* named after its class".
|
|
856
|
+
*
|
|
857
|
+
* The top-level call is the OUTERMOST invocation on the event's `EventCaller`
|
|
858
|
+
* chain — a `ContextChatEngine.chat`, not the retriever or the model call it
|
|
859
|
+
* made — so everything one chat does lands in one run instead of one session
|
|
860
|
+
* per retrieval and per model call. The run ends when that invocation
|
|
861
|
+
* returns, which only `invocation()` sees; an invocation it did not see start
|
|
862
|
+
* (no hook on this build, or begun before `instrument()`) falls back to a
|
|
863
|
+
* bare run that ends with its one leaf, as every root run used to.
|
|
864
|
+
*/
|
|
865
|
+
private openRoot(origin: Origin, prefix: string, agentId: () => string): Run {
|
|
866
|
+
const outer = origin.chain?.[origin.chain.length - 1];
|
|
867
|
+
if (outer && this.observedOuter(origin)) {
|
|
868
|
+
return this.openRun(prefix, className(read(outer, "caller")) ?? agentId(), {
|
|
869
|
+
parent: null,
|
|
870
|
+
caller: outer,
|
|
871
|
+
byInvocation: true,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
return this.openRun(prefix, agentId(), { parent: null, bare: true });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Run one LlamaIndex invocation — the callback `withEventCaller` binds an
|
|
879
|
+
* `EventCaller` around — and observe how it ends. Called from the hook on
|
|
880
|
+
* LlamaIndex's own event-caller storage (`hookInvocations`).
|
|
881
|
+
*
|
|
882
|
+
* A native promise is REPLACED by one that settles identically, never merely
|
|
883
|
+
* observed: attaching a rejection handler to the caller's own promise would
|
|
884
|
+
* mark it handled, and a rejection the application never handled would stop
|
|
885
|
+
* being reported. Anything else is returned untouched and only a synchronous
|
|
886
|
+
* throw is seen.
|
|
887
|
+
*/
|
|
888
|
+
invocation(caller: object, fn: () => unknown): unknown {
|
|
889
|
+
this.observed.add(caller);
|
|
890
|
+
let result: unknown;
|
|
891
|
+
try {
|
|
892
|
+
result = fn();
|
|
893
|
+
} catch (error) {
|
|
894
|
+
this.returnedFrom(caller, undefined, error, true);
|
|
895
|
+
throw error;
|
|
896
|
+
}
|
|
897
|
+
if (result instanceof Promise && result.constructor === Promise) {
|
|
898
|
+
return result.then(
|
|
899
|
+
(value: unknown) => {
|
|
900
|
+
this.returnedFrom(caller, value);
|
|
901
|
+
return value;
|
|
902
|
+
},
|
|
903
|
+
(error: unknown) => {
|
|
904
|
+
this.returnedFrom(caller, undefined, error, true);
|
|
905
|
+
throw error;
|
|
906
|
+
},
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
if (!(isObject(result) && typeof (result as { then?: unknown }).then === "function")) {
|
|
910
|
+
this.returnedFrom(caller, result);
|
|
911
|
+
}
|
|
912
|
+
return result;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
private returnedFrom(caller: object, value: unknown, error?: unknown, failed = false): void {
|
|
916
|
+
core.callSafely(
|
|
917
|
+
() => {
|
|
918
|
+
if (this.active) this.invocationEnded(caller, value, error, failed);
|
|
919
|
+
},
|
|
920
|
+
[],
|
|
921
|
+
`${NAME}.invocationEnded`,
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* An invocation returned or threw. A failure is the ONLY failure signal a
|
|
927
|
+
* model call, a query or a legacy task has — `wrapLLMEvent` has no error path
|
|
928
|
+
* and a failed step dispatches no `agent-end` — so it closes what that call
|
|
929
|
+
* left open. A success ends only a run that has no end event of its own.
|
|
930
|
+
*/
|
|
931
|
+
private invocationEnded(caller: object, value: unknown, error: unknown, failed: boolean): void {
|
|
932
|
+
this.returned.add(caller);
|
|
933
|
+
const text = failed ? errorText(error) : undefined;
|
|
934
|
+
if (failed) {
|
|
935
|
+
for (const key of this.invocationLeaves.get(caller) ?? []) {
|
|
936
|
+
const leaf = this.leaves.get(key);
|
|
937
|
+
if (!leaf) continue;
|
|
938
|
+
this.closeLeaf(leaf, { error: text });
|
|
939
|
+
if (isObject(error)) this.carried.add(error);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
this.invocationLeaves.delete(caller);
|
|
943
|
+
const run = this.live(this.invocations.get(caller));
|
|
944
|
+
if (!run) return;
|
|
945
|
+
if (failed) {
|
|
946
|
+
for (const key of [...run.leaves]) {
|
|
947
|
+
const leaf = this.leaves.get(key);
|
|
948
|
+
if (!leaf) continue;
|
|
949
|
+
this.closeLeaf(leaf, { error: text });
|
|
950
|
+
if (isObject(error)) this.carried.add(error);
|
|
951
|
+
}
|
|
952
|
+
// One report per failure: a leaf that already carries it is the report.
|
|
953
|
+
if (!(isObject(error) && this.carried.has(error))) this.reportError(run, error);
|
|
954
|
+
this.finishRun(run, "failed", text);
|
|
955
|
+
} else if (run.byInvocation) {
|
|
956
|
+
this.finishRun(run, "success", this.options.captureMessages ? textOf(value) : undefined);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
llmStart(event: unknown): void {
|
|
961
|
+
const payload = detail(event);
|
|
962
|
+
const id = asId(payload.id);
|
|
963
|
+
const frame = this.frames.getStore();
|
|
964
|
+
const origin = originOf(event);
|
|
965
|
+
const place = this.placeOrOpen(event, "llm", () => className(callersOf(event)[0]) ?? "llm", origin);
|
|
966
|
+
this.closeAnsweredTools(place.run, payload.messages);
|
|
967
|
+
const model = modelFromCallers(event) ?? (frame && frame.run === place.run ? frame.model : undefined) ?? place.run.model;
|
|
968
|
+
const leaf: Leaf = {
|
|
969
|
+
key: `llm:${id}`,
|
|
970
|
+
kind: "model",
|
|
971
|
+
run: place.run,
|
|
972
|
+
parentKey: place.parentKey,
|
|
973
|
+
name: model ?? "llm",
|
|
974
|
+
callId: id,
|
|
975
|
+
started: performance.now(),
|
|
976
|
+
model,
|
|
977
|
+
};
|
|
978
|
+
this.openLeaf(leaf);
|
|
979
|
+
// A model call that throws has no `llm-end`; its invocation throwing is the
|
|
980
|
+
// only signal. Inside a workflow step the step's failure already closes it.
|
|
981
|
+
const invocation = origin.chain?.[0];
|
|
982
|
+
if (invocation && !frame) {
|
|
983
|
+
const keys = this.invocationLeaves.get(invocation) ?? new Set<string>();
|
|
984
|
+
keys.add(leaf.key);
|
|
985
|
+
this.invocationLeaves.set(invocation, keys);
|
|
986
|
+
}
|
|
987
|
+
this.tracker.emit("modelRequest", leaf.key, {
|
|
988
|
+
parentKey: leaf.parentKey,
|
|
989
|
+
model,
|
|
990
|
+
requestId: id,
|
|
991
|
+
messages: this.capture(messagesOf(payload.messages)),
|
|
992
|
+
...core.fwFields({ run_id: place.run.key }),
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
llmStream(event: unknown): void {
|
|
997
|
+
const leaf = this.leaves.get(`llm:${asId(detail(event).id)}`);
|
|
998
|
+
if (leaf && leaf.firstChunk === undefined) leaf.firstChunk = performance.now();
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
llmEnd(event: unknown): void {
|
|
1002
|
+
const payload = detail(event);
|
|
1003
|
+
const leaf = this.leaves.get(`llm:${asId(payload.id)}`);
|
|
1004
|
+
if (leaf) this.closeLeaf(leaf, { response: payload.response });
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Close tool leaves the model has already been shown the result of.
|
|
1009
|
+
*
|
|
1010
|
+
* `callTool` dispatches no `llm-tool-result` when a tool throws, so without
|
|
1011
|
+
* this a failing tool in a legacy agent stays open for the life of the run.
|
|
1012
|
+
* The next model call carries the result as a `toolResult` message — with
|
|
1013
|
+
* `isError` — which is the framework's own record of how the tool ended.
|
|
1014
|
+
*/
|
|
1015
|
+
private closeAnsweredTools(run: Run, messages: unknown): void {
|
|
1016
|
+
if (run.leaves.size === 0 || !Array.isArray(messages)) return;
|
|
1017
|
+
const answers = new Map<string, Json>();
|
|
1018
|
+
for (const message of messages) {
|
|
1019
|
+
const result = read(read(message, "options"), "toolResult");
|
|
1020
|
+
const id = nonEmpty(read(result, "id"));
|
|
1021
|
+
if (id && isObject(result)) answers.set(id, result);
|
|
1022
|
+
}
|
|
1023
|
+
for (const key of [...run.leaves]) {
|
|
1024
|
+
const leaf = this.leaves.get(key);
|
|
1025
|
+
if (!leaf || leaf.kind !== "tool" || !leaf.rawId) continue;
|
|
1026
|
+
const answer = answers.get(leaf.rawId);
|
|
1027
|
+
if (!answer) continue;
|
|
1028
|
+
this.closeLeaf(leaf, answer.isError === true ? { error: cleanToolError(answer.result) } : { output: answer.result });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private toolCallId(run: Run, rawId: string | undefined): string {
|
|
1033
|
+
if (!rawId) return randomUUID();
|
|
1034
|
+
const seen = run.usedToolIds.get(rawId) ?? 0;
|
|
1035
|
+
run.usedToolIds.set(rawId, seen + 1);
|
|
1036
|
+
// A repeat of the same provider id within a run would pair wrongly.
|
|
1037
|
+
return seen === 0 ? rawId : `${rawId}#${seen}`;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
toolCall(event: unknown): void {
|
|
1041
|
+
const call = read(detail(event), "toolCall");
|
|
1042
|
+
const name = nonEmpty(read(call, "name")) ?? "tool";
|
|
1043
|
+
const rawId = nonEmpty(read(call, "id"));
|
|
1044
|
+
const located = this.locate(originOf(event));
|
|
1045
|
+
// Inside a workflow the runtime's own `agentToolCallEvent` opened this call
|
|
1046
|
+
// already, synchronously and before the tool ran; this is the same call.
|
|
1047
|
+
if (located && this.findTool(rawId, located.run, "workflow")) return;
|
|
1048
|
+
const place = located ?? this.placeOrOpen(event, "tool", () => name);
|
|
1049
|
+
this.openTool(place, name, rawId, read(call, "input"), "bus");
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
private openTool(place: Located, name: string, rawId: string | undefined, input: unknown, source: Leaf["source"]): void {
|
|
1053
|
+
const leaf: Leaf = {
|
|
1054
|
+
key: this.nextKey("tool"),
|
|
1055
|
+
kind: "tool",
|
|
1056
|
+
run: place.run,
|
|
1057
|
+
parentKey: place.parentKey,
|
|
1058
|
+
name,
|
|
1059
|
+
callId: this.toolCallId(place.run, rawId),
|
|
1060
|
+
rawId,
|
|
1061
|
+
source,
|
|
1062
|
+
started: performance.now(),
|
|
1063
|
+
};
|
|
1064
|
+
this.openLeaf(leaf);
|
|
1065
|
+
this.tracker.emit("toolUse", leaf.key, {
|
|
1066
|
+
parentKey: leaf.parentKey,
|
|
1067
|
+
toolName: name,
|
|
1068
|
+
toolCallId: leaf.callId,
|
|
1069
|
+
input: this.capture(isObject(input) && !Array.isArray(input) ? input : input === undefined ? undefined : { input }),
|
|
1070
|
+
...core.fwFields({ run_id: place.run.key, tool_id: rawId !== leaf.callId ? rawId : undefined }),
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** The open tool leaf for a provider tool call id, in the run this event belongs to first. */
|
|
1075
|
+
private findTool(rawId: string | undefined, run: Run | null, source?: Leaf["source"]): Leaf | undefined {
|
|
1076
|
+
if (!rawId) return undefined;
|
|
1077
|
+
const search = (keys: Iterable<string>): Leaf | undefined => {
|
|
1078
|
+
for (const key of keys) {
|
|
1079
|
+
const leaf = this.leaves.get(key);
|
|
1080
|
+
if (leaf?.kind === "tool" && leaf.rawId === rawId && (!source || leaf.source === source)) return leaf;
|
|
1081
|
+
}
|
|
1082
|
+
return undefined;
|
|
1083
|
+
};
|
|
1084
|
+
if (source) return run ? search(run.leaves) : undefined;
|
|
1085
|
+
return (run ? search(run.leaves) : undefined) ?? search(this.leaves.keys());
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
toolResult(event: unknown): void {
|
|
1089
|
+
const payload = detail(event);
|
|
1090
|
+
const leaf = this.findTool(nonEmpty(read(read(payload, "toolCall"), "id")), this.locate(originOf(event))?.run ?? null);
|
|
1091
|
+
if (!leaf) return;
|
|
1092
|
+
const result = read(payload, "toolResult");
|
|
1093
|
+
const failed = read(result, "isError") === true;
|
|
1094
|
+
this.closeLeaf(leaf, failed ? { error: cleanToolError(read(result, "output")) } : { output: read(result, "output") });
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
retrieveStart(event: unknown): void {
|
|
1098
|
+
const payload = detail(event);
|
|
1099
|
+
const id = asId(payload.id);
|
|
1100
|
+
// The retriever's class, as Python names it (`VectorIndexRetriever`). The
|
|
1101
|
+
// event does not carry the retriever; the patched `retrieve()` it was
|
|
1102
|
+
// dispatched from does, and the dispatch runs in that call's async context.
|
|
1103
|
+
const name = className(this.retrievers.getStore()) ?? "retriever";
|
|
1104
|
+
const place = this.placeOrOpen(event, "retrieve", () => name);
|
|
1105
|
+
const leaf: Leaf = {
|
|
1106
|
+
key: `retrieve:${id}`,
|
|
1107
|
+
kind: "retrieval",
|
|
1108
|
+
run: place.run,
|
|
1109
|
+
parentKey: place.parentKey,
|
|
1110
|
+
name,
|
|
1111
|
+
callId: id,
|
|
1112
|
+
started: performance.now(),
|
|
1113
|
+
};
|
|
1114
|
+
this.openLeaf(leaf);
|
|
1115
|
+
this.tracker.emit("toolUse", leaf.key, {
|
|
1116
|
+
parentKey: leaf.parentKey,
|
|
1117
|
+
toolName: leaf.name,
|
|
1118
|
+
toolCallId: id,
|
|
1119
|
+
input: this.capture({ query: queryText(payload.query) }),
|
|
1120
|
+
...core.fwFields({ run_id: place.run.key, kind: "retrieval" }),
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
retrieveEnd(event: unknown): void {
|
|
1125
|
+
const payload = detail(event);
|
|
1126
|
+
const leaf = this.leaves.get(`retrieve:${asId(payload.id)}`);
|
|
1127
|
+
if (leaf) this.closeLeaf(leaf, { output: summarizeNodes(payload.nodes) });
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* A query engine call is a root run when nothing encloses it — Python's
|
|
1132
|
+
* top-level `query_engine.query()` — and nothing at all inside a run, where
|
|
1133
|
+
* its retrievals and model calls are what is worth seeing.
|
|
1134
|
+
*/
|
|
1135
|
+
queryStart(event: unknown): void {
|
|
1136
|
+
const origin = originOf(event);
|
|
1137
|
+
const owner = origin.callers[0];
|
|
1138
|
+
if (this.locate(origin, owner)) return;
|
|
1139
|
+
// Called from inside ANOTHER invocation nothing encloses — a chat engine
|
|
1140
|
+
// whose first act is a query: that invocation is the root, and the query
|
|
1141
|
+
// is inside it (recording nothing of its own, as inside any run).
|
|
1142
|
+
if (origin.chain && origin.chain.length > 1 && this.observedOuter(origin)) {
|
|
1143
|
+
this.openRoot(origin, "invocation", () => className(owner) ?? "query_engine");
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const payload = detail(event);
|
|
1147
|
+
const run = this.openRun(`query:${asId(payload.id)}`, className(owner) ?? "query_engine", {
|
|
1148
|
+
owner: isObject(owner) ? owner : null,
|
|
1149
|
+
caller: origin.chain?.[0] ?? null,
|
|
1150
|
+
goal: queryText(payload.query),
|
|
1151
|
+
});
|
|
1152
|
+
const id = asId(payload.id);
|
|
1153
|
+
this.queries.set(id, run);
|
|
1154
|
+
run.forget = () => this.queries.delete(id);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
queryEnd(event: unknown): void {
|
|
1158
|
+
const payload = detail(event);
|
|
1159
|
+
const id = asId(payload.id);
|
|
1160
|
+
const run = this.queries.get(id);
|
|
1161
|
+
if (!run) return;
|
|
1162
|
+
this.finishRun(run, "success", this.options.captureMessages ? textOf(payload.response) : undefined);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/**
|
|
1166
|
+
* Legacy `AgentRunner` (`LLMAgent`, `OpenAIAgent`, `ReActAgent`, …).
|
|
1167
|
+
*
|
|
1168
|
+
* `agent-start` fires for EVERY step and `agent-end` only after the last one,
|
|
1169
|
+
* both carrying the step. Pairing them by step id — what this adapter used to
|
|
1170
|
+
* do — opened a span per step and closed one, so every multi-step run left
|
|
1171
|
+
* one agent open forever and split into two sessions. A task is identified by
|
|
1172
|
+
* its FIRST step instead, reached by walking `prevStep`.
|
|
1173
|
+
*/
|
|
1174
|
+
agentStart(event: unknown): void {
|
|
1175
|
+
const step = read(detail(event), "startStep");
|
|
1176
|
+
if (!isObject(step)) return;
|
|
1177
|
+
const first = firstStep(step);
|
|
1178
|
+
const firstId = asId(read(first, "id"));
|
|
1179
|
+
const running = this.tasks.get(firstId);
|
|
1180
|
+
if (running) {
|
|
1181
|
+
running.lastActivity = performance.now();
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
const origin = originOf(event);
|
|
1185
|
+
const owner = origin.callers[0];
|
|
1186
|
+
let parent = this.locate(origin, owner);
|
|
1187
|
+
if (!parent && origin.chain && origin.chain.length > 1 && this.observedOuter(origin)) {
|
|
1188
|
+
// A legacy agent called from inside another un-run invocation nests
|
|
1189
|
+
// under it, the way a sub-agent nests under a workflow.
|
|
1190
|
+
const root = this.openRoot(origin, "invocation", () => "invocation");
|
|
1191
|
+
parent = { parentKey: root.key, run: root };
|
|
1192
|
+
}
|
|
1193
|
+
const run = this.openRun(`task:${firstId}`, className(owner) ?? "AgentRunner", {
|
|
1194
|
+
parent,
|
|
1195
|
+
owner: isObject(owner) ? owner : null,
|
|
1196
|
+
caller: origin.chain?.[0] ?? null,
|
|
1197
|
+
goal: textOf((read(read(read(step, "context"), "store"), "messages") as unknown[] | undefined)?.at(-1)),
|
|
1198
|
+
});
|
|
1199
|
+
run.model = nonEmpty(read(read(read(read(step, "context"), "llm"), "metadata"), "model"));
|
|
1200
|
+
this.tasks.set(firstId, run);
|
|
1201
|
+
run.forget = () => this.tasks.delete(firstId);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
agentEnd(event: unknown): void {
|
|
1205
|
+
const step = read(detail(event), "endStep");
|
|
1206
|
+
if (!isObject(step)) return;
|
|
1207
|
+
const firstId = asId(read(firstStep(step), "id"));
|
|
1208
|
+
const run = this.tasks.get(firstId);
|
|
1209
|
+
if (run) this.finishRun(run, "success");
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// -- workflows ------------------------------------------------------------
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Open the run for an `AgentWorkflow.runStream()` call and return the hook to
|
|
1216
|
+
* attach to the context it is about to create.
|
|
1217
|
+
*/
|
|
1218
|
+
beginWorkflow(
|
|
1219
|
+
workflow: Json,
|
|
1220
|
+
userInput: unknown,
|
|
1221
|
+
module: WorkflowModule,
|
|
1222
|
+
): { run: Run; attach: (context: unknown) => void } {
|
|
1223
|
+
// Its steps are recorded through its context (`attachContext`), never
|
|
1224
|
+
// again as a plain workflow's.
|
|
1225
|
+
for (const handler of handlerNames(workflow).keys()) {
|
|
1226
|
+
if (isObject(handler) || typeof handler === "function") this.agentHandlers.add(handler);
|
|
1227
|
+
}
|
|
1228
|
+
const agents = read(workflow, "agents");
|
|
1229
|
+
const size = agents instanceof Map ? agents.size : 1;
|
|
1230
|
+
const rootName = nonEmpty(read(workflow, "rootAgentName"));
|
|
1231
|
+
const agentId = size <= 1 && rootName ? rootName : (className(workflow) ?? "AgentWorkflow");
|
|
1232
|
+
const run = this.openRun("workflow", agentId, {
|
|
1233
|
+
parent: this.locate(),
|
|
1234
|
+
goal: typeof userInput === "string" ? userInput : textOf(userInput),
|
|
1235
|
+
fields: { workflow: className(workflow), agent_name: size <= 1 ? rootName : undefined },
|
|
1236
|
+
});
|
|
1237
|
+
return {
|
|
1238
|
+
run,
|
|
1239
|
+
attach: (context: unknown) => {
|
|
1240
|
+
this.attachContext(run, workflow, context, module);
|
|
1241
|
+
},
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
failWorkflow(run: Run | null, error: unknown): void {
|
|
1246
|
+
if (!run) return;
|
|
1247
|
+
// Nothing below the run saw this failure, so the run reports it — once.
|
|
1248
|
+
this.reportError(run, error);
|
|
1249
|
+
this.finishRun(run, "failed", errorText(error));
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
/**
|
|
1253
|
+
* An `error` event, for a failure no leaf or hook already carries. The
|
|
1254
|
+
* Python adapter's rule: a second report of one failure double-counts on the
|
|
1255
|
+
* session's error total, so only the innermost place that saw it reports it.
|
|
1256
|
+
*/
|
|
1257
|
+
private reportError(run: Run, error: unknown): void {
|
|
1258
|
+
this.tracker.emit("error", run.key, {
|
|
1259
|
+
errorType: error instanceof Error ? error.name || "Error" : typeof error,
|
|
1260
|
+
message: error instanceof Error ? error.message || error.name : String(error),
|
|
1261
|
+
traceback: error instanceof Error ? error.stack : undefined,
|
|
1262
|
+
...core.fwFields({ run_id: run.key }),
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
private attachContext(run: Run, workflow: Json, context: unknown, module: WorkflowModule): void {
|
|
1267
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- the subscriber is a named function (its name is its degradation site)
|
|
1268
|
+
const state = this;
|
|
1269
|
+
const callContext = read(context, "__internal__call_context") as Subscribable | undefined;
|
|
1270
|
+
const sendEvent = read(context, "__internal__call_send_event") as Subscribable | undefined;
|
|
1271
|
+
if (typeof sendEvent?.subscribe === "function") {
|
|
1272
|
+
sendEvent.subscribe(
|
|
1273
|
+
core.safe(NAME, function workflowEvent(this: void, sent: unknown): void {
|
|
1274
|
+
state.workflowEvent(run, sent, module);
|
|
1275
|
+
}),
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (typeof callContext?.subscribe !== "function") {
|
|
1279
|
+
compat.warn(
|
|
1280
|
+
"this @llamaindex/workflow context has no __internal__call_context, so workflow steps " +
|
|
1281
|
+
"are not recorded (the run, its model calls and its tool calls still are).",
|
|
1282
|
+
`${NAME}:call_context`,
|
|
1283
|
+
);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const names = handlerNames(workflow);
|
|
1287
|
+
callContext.subscribe(((handlerContext: HandlerContext, next: (context: HandlerContext) => void) => {
|
|
1288
|
+
// `next` MUST be called exactly once whatever happens here: a subscriber
|
|
1289
|
+
// that throws before it would silently stop the customer's workflow.
|
|
1290
|
+
let wrapped: HandlerContext = handlerContext;
|
|
1291
|
+
try {
|
|
1292
|
+
const original = handlerContext.handler;
|
|
1293
|
+
if (typeof original === "function") {
|
|
1294
|
+
const name = names.get(original) ?? nonEmpty(original.name) ?? "step";
|
|
1295
|
+
const inputs = handlerContext.inputs;
|
|
1296
|
+
const input = Array.isArray(inputs) ? (inputs[0] as unknown) : undefined;
|
|
1297
|
+
wrapped = { ...handlerContext, handler: this.wrapStep(run, workflow, name, original, input) };
|
|
1298
|
+
}
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
core.callSafely(
|
|
1301
|
+
() => {
|
|
1302
|
+
throw error;
|
|
1303
|
+
},
|
|
1304
|
+
[],
|
|
1305
|
+
`${NAME}.wrapStep`,
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
next(wrapped);
|
|
1309
|
+
}));
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
workflowEvent(run: Run, sent: unknown, module: WorkflowModule): void {
|
|
1313
|
+
// A context outlives uninstall(); its subscriptions cannot be removed.
|
|
1314
|
+
if (!this.active) return;
|
|
1315
|
+
if (module.stopAgentEvent?.include(sent)) {
|
|
1316
|
+
const data = read(sent, "data");
|
|
1317
|
+
const result = read(data, "result");
|
|
1318
|
+
this.finishRun(run, "success", this.options.captureMessages ? (textOf(result) ?? textOf(read(data, "message"))) : undefined);
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (module.agentToolCallEvent?.include(sent)) {
|
|
1322
|
+
// The runtime announces every tool call before running it — the only
|
|
1323
|
+
// signal on releases whose `AgentWorkflow` calls tools directly rather
|
|
1324
|
+
// than through `callTool` (workflow 1.1.5 dispatches nothing on the bus).
|
|
1325
|
+
const data = read(sent, "data");
|
|
1326
|
+
const owner = run.sub ?? run;
|
|
1327
|
+
const frame = this.frames.getStore();
|
|
1328
|
+
const place = frame && frame.run === owner ? { parentKey: frame.key, run: owner } : { parentKey: owner.key, run: owner };
|
|
1329
|
+
const name = nonEmpty(read(data, "toolName")) ?? "tool";
|
|
1330
|
+
this.openTool(place, name, nonEmpty(read(data, "toolId")), read(data, "toolKwargs"), "workflow");
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (module.agentToolCallResultEvent?.include(sent)) {
|
|
1334
|
+
// The runtime's own record of how a tool ended. It is the ONLY signal for
|
|
1335
|
+
// a tool that threw: `callTool` dispatches no `llm-tool-result` then.
|
|
1336
|
+
const data = read(sent, "data");
|
|
1337
|
+
const leaf = this.findTool(nonEmpty(read(data, "toolId")), run.sub ?? run);
|
|
1338
|
+
if (!leaf) return;
|
|
1339
|
+
const output = read(data, "toolOutput");
|
|
1340
|
+
if (read(output, "isError") === true) {
|
|
1341
|
+
this.closeLeaf(leaf, { error: cleanToolError(read(output, "result")) });
|
|
1342
|
+
} else {
|
|
1343
|
+
this.closeLeaf(leaf, { output: read(data, "raw") ?? read(output, "result") });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
private wrapStep(
|
|
1349
|
+
run: Run,
|
|
1350
|
+
workflow: Json,
|
|
1351
|
+
name: string,
|
|
1352
|
+
original: (...args: unknown[]) => unknown,
|
|
1353
|
+
input: unknown,
|
|
1354
|
+
): (...args: unknown[]) => unknown {
|
|
1355
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- the wrapper needs its own `this`
|
|
1356
|
+
const state = this;
|
|
1357
|
+
return function failproofaiStep(this: unknown, ...args: unknown[]): unknown {
|
|
1358
|
+
if (!state.active) return original.apply(this, args);
|
|
1359
|
+
const step = core.callSafely(() => state.stepStart(run, workflow, name, input), [], `${NAME}.stepStart`);
|
|
1360
|
+
if (!step) return original.apply(this, args);
|
|
1361
|
+
const finish = (value: unknown, error?: unknown): void => {
|
|
1362
|
+
core.callSafely(() => {
|
|
1363
|
+
state.stepEnd(step, value, error);
|
|
1364
|
+
}, [], `${NAME}.stepEnd`);
|
|
1365
|
+
};
|
|
1366
|
+
let result: unknown;
|
|
1367
|
+
try {
|
|
1368
|
+
result = state.frames.run(step.frame, () => original.apply(this, args));
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
finish(undefined, error);
|
|
1371
|
+
throw error;
|
|
1372
|
+
}
|
|
1373
|
+
if (isObject(result) && typeof (result as { then?: unknown }).then === "function") {
|
|
1374
|
+
return (result as unknown as PromiseLike<unknown>).then(
|
|
1375
|
+
(value) => {
|
|
1376
|
+
finish(value);
|
|
1377
|
+
return value;
|
|
1378
|
+
},
|
|
1379
|
+
(error: unknown) => {
|
|
1380
|
+
finish(undefined, error);
|
|
1381
|
+
throw error;
|
|
1382
|
+
},
|
|
1383
|
+
);
|
|
1384
|
+
}
|
|
1385
|
+
finish(result);
|
|
1386
|
+
return result;
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* One step handler of a plain `createWorkflow()` workflow, seen as
|
|
1392
|
+
* workflow-core binds its handler context (`hookAsyncContext`).
|
|
1393
|
+
*
|
|
1394
|
+
* A plain workflow has no run boundary of its own: the application creates
|
|
1395
|
+
* the context, sends it events and stops reading whenever it likes. So the
|
|
1396
|
+
* run is the context's BURST of activity — it opens with the first step
|
|
1397
|
+
* handler of a context and ends once no step of it is running and none was
|
|
1398
|
+
* started by the last one's output (checked a macrotask later, after the
|
|
1399
|
+
* runtime has dispatched that output). A workflow that then waits for an
|
|
1400
|
+
* event from outside (human-in-the-loop) records the next burst as a new run.
|
|
1401
|
+
*/
|
|
1402
|
+
plainStep(handlerContext: Json, proceed: () => unknown): unknown {
|
|
1403
|
+
const original = handlerContext.handler;
|
|
1404
|
+
if (typeof original !== "function" || this.agentHandlers.has(original)) return proceed();
|
|
1405
|
+
const root = rootContext(handlerContext);
|
|
1406
|
+
const inputs = handlerContext.inputs;
|
|
1407
|
+
const input = Array.isArray(inputs) ? (inputs[0] as unknown) : undefined;
|
|
1408
|
+
let run = this.live(this.plainRuns.get(root));
|
|
1409
|
+
if (!run) {
|
|
1410
|
+
const data = read(input, "data");
|
|
1411
|
+
run = this.openRun("workflow", "Workflow", {
|
|
1412
|
+
parent: this.locate(),
|
|
1413
|
+
goal: typeof data === "string" ? data : textOf(data),
|
|
1414
|
+
fields: { workflow: "Workflow" },
|
|
1415
|
+
});
|
|
1416
|
+
this.plainRuns.set(root, run);
|
|
1417
|
+
}
|
|
1418
|
+
const label = eventLabel(input);
|
|
1419
|
+
const name = nonEmpty(read(original, "name")) ?? (label ? `handle:${label}` : "step");
|
|
1420
|
+
const step = this.stepStart(run, {}, name, input, true);
|
|
1421
|
+
if (!step) return proceed();
|
|
1422
|
+
const owner = run;
|
|
1423
|
+
owner.inFlight += 1;
|
|
1424
|
+
let done = false;
|
|
1425
|
+
const end = (value: unknown, error?: unknown): void => {
|
|
1426
|
+
if (done) return;
|
|
1427
|
+
done = true;
|
|
1428
|
+
owner.inFlight -= 1;
|
|
1429
|
+
core.callSafely(
|
|
1430
|
+
() => {
|
|
1431
|
+
const data = read(value, "data");
|
|
1432
|
+
if (error === undefined && typeof data === "string") owner.output = data;
|
|
1433
|
+
this.stepEnd(step, value, error);
|
|
1434
|
+
if (owner.inFlight === 0 && !owner.ended) this.whenQuiet(owner);
|
|
1435
|
+
},
|
|
1436
|
+
[],
|
|
1437
|
+
`${NAME}.plainStepEnd`,
|
|
1438
|
+
);
|
|
1439
|
+
};
|
|
1440
|
+
// The handler object is this invocation's own, so its handler is replaced
|
|
1441
|
+
// in place — the same thing middleware does — rather than wrapped around.
|
|
1442
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- the wrapper needs its own `this`
|
|
1443
|
+
const state = this;
|
|
1444
|
+
handlerContext.handler = function failproofaiStep(this: unknown, ...args: unknown[]): unknown {
|
|
1445
|
+
let result: unknown;
|
|
1446
|
+
try {
|
|
1447
|
+
result = state.frames.run(step.frame, () => (original as (...a: unknown[]) => unknown).apply(this, args));
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
end(undefined, error);
|
|
1450
|
+
throw error;
|
|
1451
|
+
}
|
|
1452
|
+
if (isObject(result) && typeof (result as { then?: unknown }).then === "function") {
|
|
1453
|
+
return (result as unknown as PromiseLike<unknown>).then(
|
|
1454
|
+
(value) => {
|
|
1455
|
+
end(value);
|
|
1456
|
+
return value;
|
|
1457
|
+
},
|
|
1458
|
+
(error: unknown) => {
|
|
1459
|
+
end(undefined, error);
|
|
1460
|
+
throw error;
|
|
1461
|
+
},
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
end(result);
|
|
1465
|
+
return result;
|
|
1466
|
+
};
|
|
1467
|
+
return proceed();
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* End a plain-workflow run if it is still idle once the runtime has
|
|
1472
|
+
* dispatched the last step's output. Two microtask hops: the step's end runs
|
|
1473
|
+
* in a reaction to the handler's promise, the runtime's `sendEvent` in the
|
|
1474
|
+
* reaction to the promise returned in its place — queued one hop later, and
|
|
1475
|
+
* starting any next handler synchronously. Any later and the run would end
|
|
1476
|
+
* after the caller that awaited it (and after an `agent()` scope around it).
|
|
1477
|
+
*/
|
|
1478
|
+
private whenQuiet(run: Run): void {
|
|
1479
|
+
queueMicrotask(() => {
|
|
1480
|
+
queueMicrotask(() => {
|
|
1481
|
+
core.callSafely(
|
|
1482
|
+
() => {
|
|
1483
|
+
if (this.active && !run.ended && run.inFlight === 0) {
|
|
1484
|
+
this.finishRun(run, "success", this.options.captureMessages ? run.output : undefined);
|
|
1485
|
+
}
|
|
1486
|
+
},
|
|
1487
|
+
[],
|
|
1488
|
+
`${NAME}.whenQuiet`,
|
|
1489
|
+
);
|
|
1490
|
+
});
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
private stepStart(run: Run, workflow: Json, name: string, event: unknown, plain = false): Step | null {
|
|
1495
|
+
if (run.ended) return null;
|
|
1496
|
+
const data = read(event, "data");
|
|
1497
|
+
// Only an AgentWorkflow's events name the agent holding the turn; a plain
|
|
1498
|
+
// workflow's data is the application's own, whatever its field names.
|
|
1499
|
+
const agentName = plain
|
|
1500
|
+
? undefined
|
|
1501
|
+
: (nonEmpty(read(data, "currentAgentName")) ?? nonEmpty(read(data, "agentName")));
|
|
1502
|
+
const owner = plain ? run : this.subAgent(run, agentName);
|
|
1503
|
+
run.lastActivity = owner.lastActivity = performance.now();
|
|
1504
|
+
const key = this.nextKey(`${run.key}:step`);
|
|
1505
|
+
this.tracker.link(key, owner.key);
|
|
1506
|
+
owner.steps.add(key);
|
|
1507
|
+
const agents = read(workflow, "agents");
|
|
1508
|
+
const agent = agents instanceof Map ? agents.get(agentName ?? read(workflow, "rootAgentName")) : undefined;
|
|
1509
|
+
const model = nonEmpty(read(read(read(agent, "llm"), "metadata"), "model"));
|
|
1510
|
+
if (this.options.steps) {
|
|
1511
|
+
this.tracker.emit("hookTriggered", key, {
|
|
1512
|
+
parentKey: owner.key,
|
|
1513
|
+
hookName: name,
|
|
1514
|
+
hookId: key,
|
|
1515
|
+
triggerEvent: "workflow_step",
|
|
1516
|
+
input: this.capture(eventData(data)),
|
|
1517
|
+
...core.fwFields({ run_id: run.key, step: name, input_event: eventLabel(event), agent_name: agentName }),
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
return { key, name, run, owner, frame: { key, run: owner, model } };
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
private stepEnd(step: Step, value: unknown, error?: unknown): void {
|
|
1524
|
+
step.run.lastActivity = step.owner.lastActivity = performance.now();
|
|
1525
|
+
const failed = error !== undefined;
|
|
1526
|
+
let reported = false;
|
|
1527
|
+
if (failed) {
|
|
1528
|
+
// Whatever this step had open failed with it — a model call that threw
|
|
1529
|
+
// has no `llm-end` at all.
|
|
1530
|
+
for (const key of [...step.owner.leaves]) {
|
|
1531
|
+
const leaf = this.leaves.get(key);
|
|
1532
|
+
if (leaf && leaf.parentKey === step.key) {
|
|
1533
|
+
this.closeLeaf(leaf, { error: errorText(error) });
|
|
1534
|
+
reported = true;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
if (this.options.steps) {
|
|
1539
|
+
this.tracker.emit("hookCompleted", step.key, {
|
|
1540
|
+
parentKey: step.owner.key,
|
|
1541
|
+
hookName: step.name,
|
|
1542
|
+
hookId: step.key,
|
|
1543
|
+
outcome: failed ? "failed" : "success",
|
|
1544
|
+
output: failed ? undefined : this.capture(eventData(read(value, "data") ?? value)),
|
|
1545
|
+
error: failed ? errorText(error) : undefined,
|
|
1546
|
+
...core.fwFields({ run_id: step.run.key, step: step.name, output_event: eventLabel(value) }),
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
// An AgentWorkflow is a single chain of steps: a step that throws ends the
|
|
1550
|
+
// run, and the runtime never settles `run()` to tell us so. Under
|
|
1551
|
+
// `steps: false` there is no failed hook, so unless a leaf carried it the
|
|
1552
|
+
// run reports it — as the Python adapter does.
|
|
1553
|
+
if (failed) {
|
|
1554
|
+
if (!reported && !this.options.steps) this.reportError(step.owner, error);
|
|
1555
|
+
this.finishRun(step.run, "failed", errorText(error));
|
|
1556
|
+
}
|
|
1557
|
+
// Last: the step's leaves and its hook resolve their session through it.
|
|
1558
|
+
step.owner.steps.delete(step.key);
|
|
1559
|
+
this.tracker.unlink(step.key);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* The nested agent a multi-agent step belongs to, or the run itself.
|
|
1564
|
+
*
|
|
1565
|
+
* The Python adapter's rule: each distinct agent name opens a nested agent
|
|
1566
|
+
* under the workflow and a handoff closes the previous one. The name is
|
|
1567
|
+
* sticky — tool steps carry `agentName`, not `currentAgentName`, and a step
|
|
1568
|
+
* with neither keeps whichever agent holds the turn.
|
|
1569
|
+
*/
|
|
1570
|
+
private subAgent(run: Run, name: string | undefined): Run {
|
|
1571
|
+
if (!name || name === run.agentId || (run.sub && run.sub.agentId === name)) return run.sub ?? run;
|
|
1572
|
+
if (run.sub) this.finishRun(run.sub, "success");
|
|
1573
|
+
run.subSeq += 1;
|
|
1574
|
+
const sub = this.openRun(`${run.key}:sub${run.subSeq}`, name, {
|
|
1575
|
+
parent: { parentKey: run.key, run },
|
|
1576
|
+
root: run,
|
|
1577
|
+
fields: { agent_name: name, workflow: run.agentId },
|
|
1578
|
+
});
|
|
1579
|
+
run.sub = sub;
|
|
1580
|
+
return sub;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// -- teardown -------------------------------------------------------------
|
|
1584
|
+
|
|
1585
|
+
/**
|
|
1586
|
+
* Close what nobody is going to close. Returns how many leaves and runs.
|
|
1587
|
+
*
|
|
1588
|
+
* Leaves first — a model call that threw has no `llm-end` — then runs that
|
|
1589
|
+
* have been silent for `staleAfter`: LlamaIndex.TS never signals a legacy
|
|
1590
|
+
* task whose step threw (no `agent-end`) or a `runStream()` nobody drained,
|
|
1591
|
+
* and a run left open is a session the dashboard shows as `ongoing` forever.
|
|
1592
|
+
* Python reaps only leaves because its span handler sees every drop; here
|
|
1593
|
+
* the run-level signal genuinely does not exist.
|
|
1594
|
+
*/
|
|
1595
|
+
sweep(now = performance.now()): number {
|
|
1596
|
+
const cutoff = now - this.options.staleAfter * 1000;
|
|
1597
|
+
let closed = 0;
|
|
1598
|
+
for (const leaf of [...this.leaves.values()]) {
|
|
1599
|
+
if (leaf.started > cutoff) continue;
|
|
1600
|
+
this.closeLeaf(leaf, { closedBy: "stale" });
|
|
1601
|
+
closed += 1;
|
|
1602
|
+
}
|
|
1603
|
+
for (const run of [...this.runs.values()].reverse()) {
|
|
1604
|
+
if (run.ended || run.root !== null || run.leaves.size > 0 || run.lastActivity > cutoff) continue;
|
|
1605
|
+
this.finishRun(run, "cancelled", undefined, "stale");
|
|
1606
|
+
closed += 1;
|
|
1607
|
+
}
|
|
1608
|
+
return closed;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
startReaper(): void {
|
|
1612
|
+
if (this.options.reaperInterval <= 0 || this.reaper !== null) return;
|
|
1613
|
+
this.reaper = setInterval(() => {
|
|
1614
|
+
core.callSafely(() => this.sweep(), [], `${NAME}.reaper`);
|
|
1615
|
+
}, this.options.reaperInterval * 1000);
|
|
1616
|
+
this.reaper.unref();
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/** What is still held, for tests: every per-run table, and the tracker. */
|
|
1620
|
+
residue(): { runs: number; leaves: number; tasks: number; queries: number; tracker: core.RunTracker } {
|
|
1621
|
+
return {
|
|
1622
|
+
runs: this.runs.size,
|
|
1623
|
+
leaves: this.leaves.size,
|
|
1624
|
+
tasks: this.tasks.size,
|
|
1625
|
+
queries: this.queries.size,
|
|
1626
|
+
tracker: this.tracker,
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
shutdown(): void {
|
|
1631
|
+
this.active = false;
|
|
1632
|
+
if (this.reaper !== null) clearInterval(this.reaper);
|
|
1633
|
+
this.reaper = null;
|
|
1634
|
+
// Newest first, so a sub-agent closes before the workflow that opened it.
|
|
1635
|
+
for (const run of [...this.runs.values()].reverse()) this.finishRun(run, "cancelled", undefined, "uninstrument");
|
|
1636
|
+
this.leaves.clear();
|
|
1637
|
+
this.tasks.clear();
|
|
1638
|
+
this.queries.clear();
|
|
1639
|
+
this.tracker.reset();
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
interface Step {
|
|
1644
|
+
key: string;
|
|
1645
|
+
name: string;
|
|
1646
|
+
run: Run;
|
|
1647
|
+
owner: Run;
|
|
1648
|
+
frame: Frame;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function firstStep(step: Json): Json {
|
|
1652
|
+
let current = step;
|
|
1653
|
+
const seen = new Set<unknown>();
|
|
1654
|
+
for (;;) {
|
|
1655
|
+
const previous = read(current, "prevStep");
|
|
1656
|
+
if (!isObject(previous) || seen.has(previous)) return current;
|
|
1657
|
+
seen.add(previous);
|
|
1658
|
+
current = previous;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/** A workflow-core handler context's root: one per `createContext()`, so one per context. */
|
|
1663
|
+
function rootContext(handlerContext: Json): object {
|
|
1664
|
+
const root = read(handlerContext, "root");
|
|
1665
|
+
if (isObject(root)) return root;
|
|
1666
|
+
let current: Json = handlerContext;
|
|
1667
|
+
const seen = new Set<unknown>();
|
|
1668
|
+
for (;;) {
|
|
1669
|
+
const previous = read(current, "prev");
|
|
1670
|
+
if (!isObject(previous) || seen.has(previous)) return current;
|
|
1671
|
+
seen.add(previous);
|
|
1672
|
+
current = previous;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
/** Step handler functions are instance arrow fields, so they are named by the field. */
|
|
1677
|
+
function handlerNames(workflow: Json): Map<unknown, string> {
|
|
1678
|
+
const names = new Map<unknown, string>();
|
|
1679
|
+
try {
|
|
1680
|
+
for (const key of Object.keys(workflow)) {
|
|
1681
|
+
const value = read(workflow, key);
|
|
1682
|
+
if (typeof value === "function") names.set(value, key);
|
|
1683
|
+
}
|
|
1684
|
+
} catch {
|
|
1685
|
+
// A frozen or exotic object; unnamed steps fall back to the function name.
|
|
1686
|
+
}
|
|
1687
|
+
return names;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
/** A workflow event's label: workflow-core tags each event with `Symbol.toStringTag`. */
|
|
1691
|
+
function eventLabel(event: unknown): string | undefined {
|
|
1692
|
+
if (!isObject(event)) return undefined;
|
|
1693
|
+
try {
|
|
1694
|
+
const tag = /^\[object (.+)\]$/.exec(Object.prototype.toString.call(event))?.[1];
|
|
1695
|
+
return tag !== undefined && tag !== "Object" && !tag.startsWith("WorkflowEvent") ? tag : undefined;
|
|
1696
|
+
} catch {
|
|
1697
|
+
return undefined;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/** A workflow event's payload, minus the run's whole state object. */
|
|
1702
|
+
function eventData(data: unknown): unknown {
|
|
1703
|
+
if (!isObject(data) || Array.isArray(data)) return data;
|
|
1704
|
+
const rest = { ...data };
|
|
1705
|
+
delete rest.state;
|
|
1706
|
+
return rest;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/**
|
|
1710
|
+
* `AgentWorkflow` stores a thrown tool as `Error: ${new Error(String(output))}`,
|
|
1711
|
+
* where `output` is already `prettifyError`'s rendering — three nested
|
|
1712
|
+
* prefixes for one failure. Keep the innermost, as `<name>: <message>`.
|
|
1713
|
+
*
|
|
1714
|
+
* `prettifyError` has two spellings: `Error: <message>` on older releases and
|
|
1715
|
+
* `Error(<name>): <message>` on llamaindex 0.12, which recorded verbatim read
|
|
1716
|
+
* "Error: Error(Error): unknown region: latam".
|
|
1717
|
+
*/
|
|
1718
|
+
function cleanToolError(result: unknown): string {
|
|
1719
|
+
let text = typeof result === "string" ? result : String(result);
|
|
1720
|
+
while (/^Error: (\w*Error: |Error\(\w*\): )/.test(text)) text = text.slice("Error: ".length);
|
|
1721
|
+
const named = /^Error\((\w*)\): ([\s\S]*)$/.exec(text);
|
|
1722
|
+
if (named) text = `${named[1] || "Error"}: ${named[2]}`;
|
|
1723
|
+
return text;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// ---------------------------------------------------------------------------
|
|
1727
|
+
// Attachment
|
|
1728
|
+
// ---------------------------------------------------------------------------
|
|
1729
|
+
|
|
1730
|
+
interface Installed {
|
|
1731
|
+
state: State;
|
|
1732
|
+
unsubscribe: Array<() => void>;
|
|
1733
|
+
patcher: core.Patcher;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
let installed: Installed | null = null;
|
|
1737
|
+
|
|
1738
|
+
const BUS_EVENTS: Array<[string, keyof State]> = [
|
|
1739
|
+
["llm-start", "llmStart"],
|
|
1740
|
+
["llm-stream", "llmStream"],
|
|
1741
|
+
["llm-end", "llmEnd"],
|
|
1742
|
+
["llm-tool-call", "toolCall"],
|
|
1743
|
+
["llm-tool-result", "toolResult"],
|
|
1744
|
+
["retrieve-start", "retrieveStart"],
|
|
1745
|
+
["retrieve-end", "retrieveEnd"],
|
|
1746
|
+
["query-start", "queryStart"],
|
|
1747
|
+
["query-end", "queryEnd"],
|
|
1748
|
+
["agent-start", "agentStart"],
|
|
1749
|
+
["agent-end", "agentEnd"],
|
|
1750
|
+
];
|
|
1751
|
+
|
|
1752
|
+
/**
|
|
1753
|
+
* Subscribe to every copy of the bus and patch every copy of `AgentWorkflow`.
|
|
1754
|
+
*
|
|
1755
|
+
* Separate from `install()` so a test can hand it stand-ins for the framework
|
|
1756
|
+
* modules; `install()` is only about FINDING the right copies.
|
|
1757
|
+
*
|
|
1758
|
+
* THROWS while an earlier attach is still installed, before touching anything.
|
|
1759
|
+
* Overwriting it would orphan that install's bus subscriptions and prototype
|
|
1760
|
+
* patch — `uninstall()` only reaches the latest — so they would record for the
|
|
1761
|
+
* life of the process, every event twice. A throw rather than a no-op because a
|
|
1762
|
+
* no-op would hand back a handle for an install that did not happen, with
|
|
1763
|
+
* options that were never applied. `instrument()` never reaches this: it skips
|
|
1764
|
+
* an adapter that is already active.
|
|
1765
|
+
*
|
|
1766
|
+
* @internal Not part of the public API.
|
|
1767
|
+
*/
|
|
1768
|
+
export function attach(
|
|
1769
|
+
rawOptions: Record<string, unknown>,
|
|
1770
|
+
modules: {
|
|
1771
|
+
globals: GlobalModule[];
|
|
1772
|
+
workflows: WorkflowModule[];
|
|
1773
|
+
retrievers?: RetrieverModule[];
|
|
1774
|
+
asyncContexts?: AsyncContextModule[];
|
|
1775
|
+
frameworkPackage?: string;
|
|
1776
|
+
},
|
|
1777
|
+
): { sweep: (now?: number) => number; residue: () => ReturnType<State["residue"]> } {
|
|
1778
|
+
if (installed !== null) {
|
|
1779
|
+
throw new Error(
|
|
1780
|
+
"the llamaindex adapter is already installed; uninstrument(\"llamaindex\") (or " +
|
|
1781
|
+
"adapter.uninstall()) before attaching again.",
|
|
1782
|
+
);
|
|
1783
|
+
}
|
|
1784
|
+
const options = parseOptions(rawOptions);
|
|
1785
|
+
const state = new State(options, modules.globals, modules.frameworkPackage ?? PACKAGE);
|
|
1786
|
+
const current: Installed = { state, unsubscribe: [], patcher: new core.Patcher() };
|
|
1787
|
+
installed = current;
|
|
1788
|
+
|
|
1789
|
+
const buses = new Set<Bus>();
|
|
1790
|
+
for (const module of modules.globals) {
|
|
1791
|
+
const bus = module.Settings?.callbackManager as Bus | undefined;
|
|
1792
|
+
if (bus && typeof bus.on === "function") buses.add(bus);
|
|
1793
|
+
}
|
|
1794
|
+
if (buses.size === 0) {
|
|
1795
|
+
throw new Error(
|
|
1796
|
+
"Settings.callbackManager is missing or has no `on` — this build of LlamaIndex does not " +
|
|
1797
|
+
"expose the callback bus this adapter subscribes to.",
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
for (const bus of buses) {
|
|
1801
|
+
if (typeof bus.off !== "function") {
|
|
1802
|
+
compat.warn(
|
|
1803
|
+
"this build of LlamaIndex has no `callbackManager.off`, so uninstrument() cannot detach " +
|
|
1804
|
+
"the handlers. They stay subscribed for the life of the process and emit nothing.",
|
|
1805
|
+
`${NAME}:off`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
for (const [event, method] of BUS_EVENTS) {
|
|
1809
|
+
// Named per event BEFORE `safe()` reads the name: the name is the
|
|
1810
|
+
// degradation site, and one shared site would let a handler that keeps
|
|
1811
|
+
// failing on, say, `retrieve-end` switch off `llm-start` with it.
|
|
1812
|
+
const named = {
|
|
1813
|
+
[method](raw: unknown): void {
|
|
1814
|
+
if (!state.active) return;
|
|
1815
|
+
(state[method] as (event: unknown) => void).call(state, raw);
|
|
1816
|
+
},
|
|
1817
|
+
}[method]!;
|
|
1818
|
+
const handler = core.safe(NAME, named);
|
|
1819
|
+
bus.on(event, handler);
|
|
1820
|
+
current.unsubscribe.push(() => {
|
|
1821
|
+
bus.off?.(event, handler);
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
if (options.embeddings) {
|
|
1826
|
+
logger.debug(
|
|
1827
|
+
"llamaindex: embeddings=true has nothing to record — LlamaIndex.TS dispatches no embedding events.",
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const storages = new Set<AsyncLocalStorage<unknown>>();
|
|
1832
|
+
for (const module of modules.globals) {
|
|
1833
|
+
const storage = eventCallerStorage(module);
|
|
1834
|
+
if (storage) storages.add(storage);
|
|
1835
|
+
}
|
|
1836
|
+
for (const storage of storages) hookInvocations(current, storage);
|
|
1837
|
+
compat.probe(NAME, "EventCaller storage", () => storages.size > 0 || modules.globals.every((m) => !m.getEventCaller));
|
|
1838
|
+
for (const module of modules.retrievers ?? []) {
|
|
1839
|
+
patchRetriever(current, module);
|
|
1840
|
+
}
|
|
1841
|
+
for (const module of modules.workflows) {
|
|
1842
|
+
patchWorkflow(current, module);
|
|
1843
|
+
}
|
|
1844
|
+
for (const module of modules.asyncContexts ?? []) {
|
|
1845
|
+
hookAsyncContext(current, module);
|
|
1846
|
+
}
|
|
1847
|
+
state.startReaper();
|
|
1848
|
+
logger.debug(`llamaindex adapter subscribed on ${buses.size} bus(es), ${current.patcher.size} patch(es)`);
|
|
1849
|
+
return { sweep: (now?: number) => state.sweep(now), residue: () => state.residue() };
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* LlamaIndex's own `AsyncLocalStorage` of `EventCaller`s — module-private in
|
|
1854
|
+
* `@llamaindex/core/global`, so found by watching which storage one call of
|
|
1855
|
+
* the exported `getEventCaller()` reads. The prototype is swapped back before
|
|
1856
|
+
* this returns: the window is one synchronous call, with no other code in it.
|
|
1857
|
+
*
|
|
1858
|
+
* @internal Exported for the unit tests.
|
|
1859
|
+
*/
|
|
1860
|
+
export function eventCallerStorage(module: GlobalModule): AsyncLocalStorage<unknown> | null {
|
|
1861
|
+
const getEventCaller = module.getEventCaller;
|
|
1862
|
+
if (typeof getEventCaller !== "function") return null;
|
|
1863
|
+
const proto = AsyncLocalStorage.prototype;
|
|
1864
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- restored as the same unbound function
|
|
1865
|
+
const original = proto.getStore;
|
|
1866
|
+
let found: unknown = null;
|
|
1867
|
+
proto.getStore = function getStore(this: AsyncLocalStorage<unknown>): unknown {
|
|
1868
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias -- recording WHICH storage is read is the point
|
|
1869
|
+
found ??= this;
|
|
1870
|
+
return original.call(this);
|
|
1871
|
+
};
|
|
1872
|
+
try {
|
|
1873
|
+
getEventCaller();
|
|
1874
|
+
} catch {
|
|
1875
|
+
// An exotic build; no storage, so no invocation boundaries.
|
|
1876
|
+
} finally {
|
|
1877
|
+
proto.getStore = original;
|
|
1878
|
+
}
|
|
1879
|
+
return found instanceof AsyncLocalStorage ? (found as AsyncLocalStorage<unknown>) : null;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/**
|
|
1883
|
+
* See every LlamaIndex invocation start and end: `withEventCaller` runs each
|
|
1884
|
+
* `@wrapEventCaller` method (a chat engine's `chat`, a query engine's `query`,
|
|
1885
|
+
* a provider's `chat`, `AgentRunner.chat`) as `storage.run(new EventCaller(...), fn)`.
|
|
1886
|
+
* An own `run` on that ONE storage object wraps `fn`; no other storage in the
|
|
1887
|
+
* process is touched, and uninstall restores it.
|
|
1888
|
+
*
|
|
1889
|
+
* This is the run boundary the callback bus lacks: without it a chat engine
|
|
1890
|
+
* has no start or end event at all, so its retrieval and its model call were
|
|
1891
|
+
* two unrelated root runs in two sessions.
|
|
1892
|
+
*/
|
|
1893
|
+
function hookInvocations(current: Installed, storage: AsyncLocalStorage<unknown>): void {
|
|
1894
|
+
const state = current.state;
|
|
1895
|
+
// Whatever `run` this storage has now (normally the prototype's), called with the storage as `this`.
|
|
1896
|
+
const original = Reflect.get(storage, "run") as (this: unknown, ...args: unknown[]) => unknown;
|
|
1897
|
+
const replacement = function run(this: unknown, store: unknown, callback: unknown, ...args: unknown[]): unknown {
|
|
1898
|
+
if (!state.active || typeof callback !== "function" || !isObject(store) || !("caller" in store)) {
|
|
1899
|
+
return original.call(this, store, callback, ...args);
|
|
1900
|
+
}
|
|
1901
|
+
const fn = callback as (...a: unknown[]) => unknown;
|
|
1902
|
+
return original.call(
|
|
1903
|
+
this,
|
|
1904
|
+
store,
|
|
1905
|
+
function invocation(this: unknown, ...inner: unknown[]): unknown {
|
|
1906
|
+
return state.invocation(store, () => fn.apply(this, inner));
|
|
1907
|
+
},
|
|
1908
|
+
...args,
|
|
1909
|
+
);
|
|
1910
|
+
};
|
|
1911
|
+
current.patcher.patch(storage, "run", replacement);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* `BaseRetriever.prototype.retrieve` — a plain prototype method, so this also
|
|
1916
|
+
* covers retrievers built before `instrument()` — binds the retriever for the
|
|
1917
|
+
* `retrieve-start` it dispatches, which carries only the query.
|
|
1918
|
+
*/
|
|
1919
|
+
function patchRetriever(current: Installed, module: RetrieverModule): void {
|
|
1920
|
+
const proto = module.BaseRetriever?.prototype as Record<string, unknown> | undefined;
|
|
1921
|
+
const ok = compat.probe(NAME, "BaseRetriever.retrieve", () => typeof proto?.retrieve === "function");
|
|
1922
|
+
if (!ok || !proto) return;
|
|
1923
|
+
const original = proto.retrieve as (...args: unknown[]) => unknown;
|
|
1924
|
+
const state = current.state;
|
|
1925
|
+
const replacement = function retrieve(this: object, ...args: unknown[]): unknown {
|
|
1926
|
+
if (!state.active || !isObject(this)) return original.apply(this, args);
|
|
1927
|
+
return state.retrievers.run(this, () => original.apply(this, args));
|
|
1928
|
+
};
|
|
1929
|
+
current.patcher.patch(proto, "retrieve", replacement);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* Plain `createWorkflow()` workflows. workflow-core runs every step handler as
|
|
1934
|
+
* `handlerContextAsyncLocalStorage.run(handlerContext, …)`, and from 1.1 that
|
|
1935
|
+
* storage is an `AsyncContext.Variable` — a class exported from
|
|
1936
|
+
* `@llamaindex/workflow-core/async-context`, so its prototype `run` sees every
|
|
1937
|
+
* handler of every context, including workflows built before `instrument()`.
|
|
1938
|
+
* (Before 1.1, and on `@llama-flow/core`, it is a closure: nothing to hook.)
|
|
1939
|
+
* Only a value shaped like a handler context is acted on; every other use of
|
|
1940
|
+
* the class passes straight through.
|
|
1941
|
+
*/
|
|
1942
|
+
function hookAsyncContext(current: Installed, module: AsyncContextModule): void {
|
|
1943
|
+
const proto = module.AsyncContext?.Variable?.prototype as Record<string, unknown> | undefined;
|
|
1944
|
+
if (typeof proto?.run !== "function") {
|
|
1945
|
+
logger.debug("llamaindex: this workflow-core has no AsyncContext.Variable; plain workflows are not runs.");
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
const original = proto.run as (value: unknown, fn: () => unknown) => unknown;
|
|
1949
|
+
const state = current.state;
|
|
1950
|
+
const replacement = function run(this: unknown, value: unknown, fn: () => unknown): unknown {
|
|
1951
|
+
if (!state.active || !isHandlerContext(value)) return original.call(this, value, fn);
|
|
1952
|
+
let proceeded = false;
|
|
1953
|
+
const proceed = (): unknown => {
|
|
1954
|
+
proceeded = true;
|
|
1955
|
+
return original.call(this, value, fn);
|
|
1956
|
+
};
|
|
1957
|
+
try {
|
|
1958
|
+
return state.plainStep(value, proceed);
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
// A failure of ours before the handler ran must not stop the workflow;
|
|
1961
|
+
// one from the handler (after `proceed`) is the application's own.
|
|
1962
|
+
if (proceeded) throw error;
|
|
1963
|
+
core.callSafely(
|
|
1964
|
+
() => {
|
|
1965
|
+
throw error;
|
|
1966
|
+
},
|
|
1967
|
+
[],
|
|
1968
|
+
`${NAME}.plainStep`,
|
|
1969
|
+
);
|
|
1970
|
+
return original.call(this, value, fn);
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
current.patcher.patch(proto, "run", replacement);
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
/** workflow-core's per-invocation handler context, by shape. */
|
|
1977
|
+
function isHandlerContext(value: unknown): value is Json {
|
|
1978
|
+
return (
|
|
1979
|
+
isObject(value) &&
|
|
1980
|
+
typeof read(value, "handler") === "function" &&
|
|
1981
|
+
Array.isArray(read(value, "inputs")) &&
|
|
1982
|
+
read(value, "next") instanceof Set &&
|
|
1983
|
+
"prev" in value
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
function patchWorkflow(current: Installed, module: WorkflowModule): void {
|
|
1988
|
+
const proto = module.AgentWorkflow?.prototype as Record<string, unknown> | undefined;
|
|
1989
|
+
const ok = compat.probe(NAME, "AgentWorkflow.runStream", () => typeof proto?.runStream === "function");
|
|
1990
|
+
if (!ok || !proto) return;
|
|
1991
|
+
const original = proto.runStream as (...args: unknown[]) => unknown;
|
|
1992
|
+
const state = current.state;
|
|
1993
|
+
const replacement = function runStream(this: Json, ...args: unknown[]): unknown {
|
|
1994
|
+
if (!state.active) return original.apply(this, args);
|
|
1995
|
+
const wf = read(this, "workflow") as Json | undefined;
|
|
1996
|
+
const createContext = read(wf, "createContext");
|
|
1997
|
+
const own = wf ? Object.getOwnPropertyDescriptor(wf, "createContext") : undefined;
|
|
1998
|
+
// Without the context there is no end signal, so an opened run would stay
|
|
1999
|
+
// open until the reaper: record the calls inside it as loose runs instead.
|
|
2000
|
+
const observable = compat.probe(
|
|
2001
|
+
NAME,
|
|
2002
|
+
"AgentWorkflow.workflow.createContext",
|
|
2003
|
+
() => typeof createContext === "function" && own?.writable === true,
|
|
2004
|
+
);
|
|
2005
|
+
if (!observable) return original.apply(this, args);
|
|
2006
|
+
let attach: ((context: unknown) => void) | undefined;
|
|
2007
|
+
let run: Run | null = null;
|
|
2008
|
+
core.callSafely(
|
|
2009
|
+
() => {
|
|
2010
|
+
({ run, attach } = state.beginWorkflow(this, args[0], module));
|
|
2011
|
+
},
|
|
2012
|
+
[],
|
|
2013
|
+
`${NAME}.beginWorkflow`,
|
|
2014
|
+
);
|
|
2015
|
+
const intercept = attach !== undefined;
|
|
2016
|
+
if (intercept) {
|
|
2017
|
+
// `runStream` creates the context and sends the start event in one
|
|
2018
|
+
// synchronous call, and the first step runs inside that send. So the
|
|
2019
|
+
// subscriptions go on the context the moment it exists, for exactly the
|
|
2020
|
+
// duration of this call.
|
|
2021
|
+
wf!.createContext = function createContextOnce(this: unknown, ...inner: unknown[]): unknown {
|
|
2022
|
+
const context = (createContext as (...a: unknown[]) => unknown).apply(this, inner);
|
|
2023
|
+
core.callSafely(() => attach!(context), [], `${NAME}.attachContext`);
|
|
2024
|
+
return context;
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
try {
|
|
2028
|
+
return original.apply(this, args);
|
|
2029
|
+
} catch (error) {
|
|
2030
|
+
core.callSafely(() => state.failWorkflow(run, error), [], `${NAME}.failWorkflow`);
|
|
2031
|
+
throw error;
|
|
2032
|
+
} finally {
|
|
2033
|
+
if (intercept && own) Object.defineProperty(wf!, "createContext", own);
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
current.patcher.patch(proto, "runStream", replacement);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// ---------------------------------------------------------------------------
|
|
2040
|
+
// The adapter
|
|
2041
|
+
// ---------------------------------------------------------------------------
|
|
2042
|
+
|
|
2043
|
+
async function loadCopies(specifier: string): Promise<unknown[] | null> {
|
|
2044
|
+
try {
|
|
2045
|
+
return await compat.requireModuleCopies(specifier, INSTALL);
|
|
2046
|
+
} catch {
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
export const adapter: Adapter = {
|
|
2052
|
+
name: NAME,
|
|
2053
|
+
|
|
2054
|
+
async install(options: Record<string, unknown> = {}): Promise<void> {
|
|
2055
|
+
compat.checkVersion(NAME, PACKAGE, {
|
|
2056
|
+
minimum: MIN_VERSION,
|
|
2057
|
+
below: BELOW_VERSION,
|
|
2058
|
+
reason: "the first release whose agent() runs on the @llamaindex/workflow 1.1 runtime",
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
// `@llamaindex/core/global` is where the bus singleton lives, and it is the
|
|
2062
|
+
// one module every LlamaIndex install has — an app on `@llamaindex/core` +
|
|
2063
|
+
// `@llamaindex/workflow` alone never installs the umbrella. The umbrella
|
|
2064
|
+
// re-exports the same `Settings`, so it is only the fallback for a layout
|
|
2065
|
+
// (pnpm, strict) where the app cannot resolve the scoped package itself.
|
|
2066
|
+
let globals = (await loadCopies("@llamaindex/core/global")) as GlobalModule[] | null;
|
|
2067
|
+
let frameworkPackage = compat.versionString(PACKAGE) !== null ? PACKAGE : CORE_PACKAGE;
|
|
2068
|
+
let retrievers = (await loadCopies("@llamaindex/core/retriever")) as RetrieverModule[] | null;
|
|
2069
|
+
if (!globals?.some((module) => module.Settings)) {
|
|
2070
|
+
globals = (await compat.requireModuleCopies(PACKAGE, INSTALL)) as GlobalModule[];
|
|
2071
|
+
frameworkPackage = PACKAGE;
|
|
2072
|
+
// The umbrella re-exports `BaseRetriever` from the same copy of core.
|
|
2073
|
+
retrievers = globals as RetrieverModule[];
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// The workflow package is optional: a legacy-agent or query-engine app does
|
|
2077
|
+
// not have it, and that is not a reason to record nothing.
|
|
2078
|
+
let workflows: WorkflowModule[] = [];
|
|
2079
|
+
if (compat.versionString(WORKFLOW_PACKAGE) !== null) {
|
|
2080
|
+
compat.checkVersion(NAME, WORKFLOW_PACKAGE, {
|
|
2081
|
+
minimum: WORKFLOW_MIN,
|
|
2082
|
+
below: WORKFLOW_BELOW,
|
|
2083
|
+
reason: "1.1 moved agent workflows onto @llamaindex/workflow-core",
|
|
2084
|
+
});
|
|
2085
|
+
workflows = ((await loadCopies(WORKFLOW_PACKAGE)) ?? []) as WorkflowModule[];
|
|
2086
|
+
}
|
|
2087
|
+
// Plain workflows: workflow-core ≥1.1 only (see `hookAsyncContext`). Resolved
|
|
2088
|
+
// from the application, so a layout that does not hoist it (pnpm) leaves
|
|
2089
|
+
// plain workflows unrecorded as runs — their model calls still are.
|
|
2090
|
+
const asyncContexts = ((await loadCopies(ASYNC_CONTEXT_MODULE)) ?? []) as AsyncContextModule[];
|
|
2091
|
+
attach(options, { globals, workflows, retrievers: retrievers ?? [], asyncContexts, frameworkPackage });
|
|
2092
|
+
},
|
|
2093
|
+
|
|
2094
|
+
uninstall(): void {
|
|
2095
|
+
const current = installed;
|
|
2096
|
+
installed = null;
|
|
2097
|
+
if (current === null) return;
|
|
2098
|
+
for (const unsubscribe of current.unsubscribe) {
|
|
2099
|
+
try {
|
|
2100
|
+
unsubscribe();
|
|
2101
|
+
} catch {
|
|
2102
|
+
// Detaching is best-effort; `state.active = false` below is what
|
|
2103
|
+
// actually stops events being recorded.
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
current.patcher.restoreAll();
|
|
2107
|
+
core.callSafely(() => {
|
|
2108
|
+
current.state.shutdown();
|
|
2109
|
+
}, [], `${NAME}.shutdown`);
|
|
2110
|
+
},
|
|
2111
|
+
};
|