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,1321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts every framework adapter shares: failure policy, patching, identity.
|
|
3
|
+
*
|
|
4
|
+
* An adapter under `integrations/` is supposed to be a **translation table**
|
|
5
|
+
* and nothing else. Everything that is genuinely hard — never throwing into the
|
|
6
|
+
* customer's call stack, restoring exactly what we replaced, mapping a
|
|
7
|
+
* framework's run ids onto FailproofAI identity, keeping payloads inside the
|
|
8
|
+
* store's patience — lives here, in one copy. If an adapter needs something
|
|
9
|
+
* added to this module, that is a signal the core is wrong, not that the
|
|
10
|
+
* adapter is special.
|
|
11
|
+
*
|
|
12
|
+
* Three things in here are load-bearing and easy to "fix" into a bug:
|
|
13
|
+
*
|
|
14
|
+
* * `safe()` must also catch a REJECTED PROMISE, not just a synchronous throw.
|
|
15
|
+
* Half of every framework's callback surface is `async`, and a try/catch does
|
|
16
|
+
* not see a rejection.
|
|
17
|
+
* * `RunTracker` never touches `AsyncLocalStorage`. A callback surface whose
|
|
18
|
+
* start and end are separate calls has no single async subtree to bind in, so
|
|
19
|
+
* identity is carried in a map and passed EXPLICITLY on every emit.
|
|
20
|
+
* * `fwFields()` is a safety rule, not a style rule. The schema merges extra
|
|
21
|
+
* fields **last**, so an extra named `tool_name` silently overwrites the
|
|
22
|
+
* declared one and changes the promoted column.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
|
|
27
|
+
import { DEFAULT_AGENT_ID, current as currentIdentity } from "../context.js";
|
|
28
|
+
import type { Identity } from "../context.js";
|
|
29
|
+
import { nowMicros } from "../clock.js";
|
|
30
|
+
import { fatalSuffix, onProcessExit, type OpenItem } from "../exit.js";
|
|
31
|
+
import { logException, logger } from "../logger.js";
|
|
32
|
+
import { runtime } from "../runtime.js";
|
|
33
|
+
import { DECLARED_FIELD_NAMES } from "../schema.js";
|
|
34
|
+
import { VERSION } from "../version.js";
|
|
35
|
+
import { envFlag, versionString } from "./compat.js";
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// The adapter contract
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What `integrations/<framework>.ts` must export as `adapter`.
|
|
43
|
+
*
|
|
44
|
+
* `install()` must save the **original attribute object** it replaces (use
|
|
45
|
+
* `Patcher`), and `uninstall()` must restore that saved object rather than
|
|
46
|
+
* re-importing or reconstructing it.
|
|
47
|
+
*/
|
|
48
|
+
export interface Adapter {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
install(options?: Record<string, unknown>): Promise<void> | void;
|
|
51
|
+
uninstall(): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Failure policy
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Everything under `integrations/` obeys one rule: never throw into the
|
|
60
|
+
* customer's call stack. Observability that takes the process down with it is
|
|
61
|
+
* worse than no observability. `FAILPROOFAI_SDK_STRICT=1` inverts that for
|
|
62
|
+
* tests and for debugging an adapter that has gone quiet — without it you can
|
|
63
|
+
* only ever prove "it didn't crash", never "it swallowed the right thing".
|
|
64
|
+
*/
|
|
65
|
+
let strictValue: boolean | null = null;
|
|
66
|
+
|
|
67
|
+
export function strict(): boolean {
|
|
68
|
+
strictValue ??= envFlag("FAILPROOFAI_SDK_STRICT");
|
|
69
|
+
return strictValue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Override the flag. `null` re-reads `FAILPROOFAI_SDK_STRICT`. */
|
|
73
|
+
export function setStrict(value: boolean | null): void {
|
|
74
|
+
strictValue = value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* After this many failures at one call site we stop calling it. A broken
|
|
79
|
+
* adapter should cost one log line, not 40% of the process and a full disk.
|
|
80
|
+
*/
|
|
81
|
+
const MAX_FAILURES = 3;
|
|
82
|
+
|
|
83
|
+
const failures = new Map<string, number>();
|
|
84
|
+
const disabled = new Set<string>();
|
|
85
|
+
|
|
86
|
+
/** Re-enable every degraded call site (tests; also `uninstrument()`). */
|
|
87
|
+
export function resetFailures(): void {
|
|
88
|
+
failures.clear();
|
|
89
|
+
disabled.clear();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isDegraded(site: string): boolean {
|
|
93
|
+
return disabled.has(site);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function recordFailure(site: string, error: unknown): void {
|
|
97
|
+
const count = (failures.get(site) ?? 0) + 1;
|
|
98
|
+
failures.set(site, count);
|
|
99
|
+
const newlyDisabled = count >= MAX_FAILURES && !disabled.has(site);
|
|
100
|
+
if (newlyDisabled) disabled.add(site);
|
|
101
|
+
|
|
102
|
+
if (count === 1) {
|
|
103
|
+
// Logged once per site, with the stack. Repeats are silent: a hook that
|
|
104
|
+
// fails on every token of a streaming response would otherwise become the
|
|
105
|
+
// log volume.
|
|
106
|
+
logException(
|
|
107
|
+
`instrumentation hook ${site} failed; the instrumented call was not affected. ` +
|
|
108
|
+
"Set FAILPROOFAI_SDK_STRICT=1 to re-throw.",
|
|
109
|
+
error,
|
|
110
|
+
);
|
|
111
|
+
} else {
|
|
112
|
+
logger.debug(`instrumentation hook ${site} failed again (${count})`);
|
|
113
|
+
}
|
|
114
|
+
if (newlyDisabled) {
|
|
115
|
+
logger.error(
|
|
116
|
+
`instrumentation hook ${site} failed ${count} times and is now disabled for the rest ` +
|
|
117
|
+
"of this process. Events from it will be missing.",
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Call `fn`, swallowing any failure and degrading a repeatedly failing site.
|
|
124
|
+
*
|
|
125
|
+
* A REJECTED PROMISE counts. Most framework callback surfaces are `async`, so a
|
|
126
|
+
* bare try/catch sees nothing at all when the body fails — the rejection lands
|
|
127
|
+
* as an unhandled rejection in the customer's process instead, which in Node 15+
|
|
128
|
+
* terminates it by default. A telemetry hook must never be able to do that.
|
|
129
|
+
*/
|
|
130
|
+
export function callSafely<T>(fn: (...args: never[]) => T, args: unknown[], site: string): T | undefined {
|
|
131
|
+
if (disabled.has(site)) return undefined;
|
|
132
|
+
let result: T;
|
|
133
|
+
try {
|
|
134
|
+
result = (fn as (...a: unknown[]) => T)(...args);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (strict()) throw error;
|
|
137
|
+
recordFailure(site, error);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
typeof result === "object" &&
|
|
142
|
+
result !== null &&
|
|
143
|
+
typeof (result as unknown as PromiseLike<unknown>).then === "function"
|
|
144
|
+
) {
|
|
145
|
+
return (result as unknown as Promise<unknown>).catch((error: unknown) => {
|
|
146
|
+
if (strict()) throw error;
|
|
147
|
+
recordFailure(site, error);
|
|
148
|
+
return undefined;
|
|
149
|
+
}) as unknown as T;
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function siteOf(fn: unknown, namespace: string): string {
|
|
155
|
+
const named = (fn as { name?: unknown }).name;
|
|
156
|
+
return `${namespace}.${typeof named === "string" && named !== "" ? named : "anonymous"}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Wrap a callback an adapter exposes so it can never throw into the framework. */
|
|
160
|
+
export function safe<Args extends unknown[], Result>(
|
|
161
|
+
namespace: string,
|
|
162
|
+
fn: (...args: Args) => Result,
|
|
163
|
+
): (...args: Args) => Result | undefined {
|
|
164
|
+
const site = siteOf(fn, namespace);
|
|
165
|
+
const wrapped = (...args: Args): Result | undefined =>
|
|
166
|
+
callSafely(fn as unknown as (...a: never[]) => Result, args, site);
|
|
167
|
+
Object.defineProperty(wrapped, "name", { value: fn.name, configurable: true });
|
|
168
|
+
return wrapped;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function safeCall(fn: ((...args: unknown[]) => unknown) | undefined, args: unknown[], site: string): unknown {
|
|
172
|
+
if (fn === undefined) return undefined;
|
|
173
|
+
return callSafely(fn as (...a: never[]) => unknown, args, site);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Shape A — wrapper surfaces
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
export interface WrapHooks {
|
|
181
|
+
before?: (...args: unknown[]) => unknown;
|
|
182
|
+
after?: (ctx: unknown, result: unknown) => unknown;
|
|
183
|
+
onError?: (ctx: unknown, error: unknown) => unknown;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const WRAPPED = Symbol.for("failproofai.wrapped");
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Wrap a framework callable so start and end are one frame.
|
|
190
|
+
*
|
|
191
|
+
* The structural guarantee, which is the whole reason this is a function and
|
|
192
|
+
* not hand-written try/catch in four adapters: **the user's call sits in
|
|
193
|
+
* exactly one `try`, whose only job is to re-throw.** Nothing we do can change
|
|
194
|
+
* what the wrapped callable returns or throws, because every one of our own
|
|
195
|
+
* calls is outside that block and inside `callSafely`.
|
|
196
|
+
*
|
|
197
|
+
* Async is handled explicitly rather than by luck: when the original returns a
|
|
198
|
+
* thenable we attach our hooks to ITS settlement and hand the caller back a
|
|
199
|
+
* promise that settles exactly as theirs did — same value, same rejection
|
|
200
|
+
* reason, same identity.
|
|
201
|
+
*/
|
|
202
|
+
export function wrapCallable<T extends (...args: never[]) => unknown>(
|
|
203
|
+
original: T,
|
|
204
|
+
hooks: WrapHooks,
|
|
205
|
+
namespace = "wrap",
|
|
206
|
+
): T {
|
|
207
|
+
const site = `${namespace}.${original.name || "anonymous"}`;
|
|
208
|
+
const wrapper = function failproofaiWrapper(this: unknown, ...args: unknown[]): unknown {
|
|
209
|
+
const ctx = safeCall(hooks.before, args, site);
|
|
210
|
+
let result: unknown;
|
|
211
|
+
try {
|
|
212
|
+
result = (original as unknown as (...a: unknown[]) => unknown).apply(this, args);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
safeCall(hooks.onError, [ctx, error], site);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
if (
|
|
218
|
+
typeof result === "object" &&
|
|
219
|
+
result !== null &&
|
|
220
|
+
typeof (result as PromiseLike<unknown>).then === "function"
|
|
221
|
+
) {
|
|
222
|
+
return (result as PromiseLike<unknown>).then(
|
|
223
|
+
(value) => {
|
|
224
|
+
safeCall(hooks.after, [ctx, value], site);
|
|
225
|
+
return value;
|
|
226
|
+
},
|
|
227
|
+
(error: unknown) => {
|
|
228
|
+
safeCall(hooks.onError, [ctx, error], site);
|
|
229
|
+
throw error;
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
safeCall(hooks.after, [ctx, result], site);
|
|
234
|
+
return result;
|
|
235
|
+
};
|
|
236
|
+
Object.defineProperty(wrapper, "name", {
|
|
237
|
+
value: original.name,
|
|
238
|
+
configurable: true,
|
|
239
|
+
});
|
|
240
|
+
(wrapper as unknown as Record<symbol, unknown>)[WRAPPED] = original;
|
|
241
|
+
return wrapper as unknown as T;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function isWrapped(value: unknown): boolean {
|
|
245
|
+
return (
|
|
246
|
+
typeof value === "function" && (value as unknown as Record<symbol, unknown>)[WRAPPED] !== undefined
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The object we replaced, or `value` itself if we never wrapped it. */
|
|
251
|
+
export function unwrap<T>(value: T): T {
|
|
252
|
+
if (typeof value !== "function") return value;
|
|
253
|
+
const original = (value as unknown as Record<symbol, unknown>)[WRAPPED];
|
|
254
|
+
return (original as T) ?? value;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// Observing a stream without owning it
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Pass `source` through untouched, handing each part to `onPart` and calling
|
|
263
|
+
* `done` exactly once — when the stream ends, when it errors, or when the
|
|
264
|
+
* reader cancels it.
|
|
265
|
+
*
|
|
266
|
+
* `done(undefined, false)` is a clean end; `done(error, false)` a stream that
|
|
267
|
+
* errored; `done(reason, true)` a consumer that cancelled (the reason is
|
|
268
|
+
* whatever it passed to `cancel()`, or a generic error when it passed nothing).
|
|
269
|
+
* The cancel is forwarded to `source`, so a provider connection is released.
|
|
270
|
+
*
|
|
271
|
+
* A pull-based re-stream rather than `pipeThrough(new TransformStream())`: a
|
|
272
|
+
* transformer's `flush` runs only on a clean end, with no hook for a consumer
|
|
273
|
+
* that walks away or a source that errors — and a model call observed that
|
|
274
|
+
* way stays open forever in exactly the cases worth recording.
|
|
275
|
+
*
|
|
276
|
+
* `onPart` and `done` run under `callSafely(site)`: an observer that throws
|
|
277
|
+
* never breaks the caller's stream.
|
|
278
|
+
*/
|
|
279
|
+
export function observeStream<T>(
|
|
280
|
+
source: ReadableStream<T>,
|
|
281
|
+
onPart: (part: T) => void,
|
|
282
|
+
done: (error: unknown, cancelled: boolean) => void,
|
|
283
|
+
site: string,
|
|
284
|
+
): ReadableStream<T> {
|
|
285
|
+
const reader = source.getReader();
|
|
286
|
+
let finished = false;
|
|
287
|
+
const finish = (error: unknown, cancelled: boolean): void => {
|
|
288
|
+
if (finished) return;
|
|
289
|
+
finished = true;
|
|
290
|
+
callSafely(done, [error, cancelled], site);
|
|
291
|
+
};
|
|
292
|
+
return new ReadableStream<T>({
|
|
293
|
+
async pull(controller) {
|
|
294
|
+
let chunk: { done: boolean; value?: T };
|
|
295
|
+
try {
|
|
296
|
+
chunk = await reader.read();
|
|
297
|
+
} catch (error) {
|
|
298
|
+
finish(error, false);
|
|
299
|
+
controller.error(error);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (chunk.done) {
|
|
303
|
+
finish(undefined, false);
|
|
304
|
+
controller.close();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
callSafely(onPart, [chunk.value], site);
|
|
308
|
+
controller.enqueue(chunk.value as T);
|
|
309
|
+
},
|
|
310
|
+
cancel(reason) {
|
|
311
|
+
finish(reason ?? new Error("stream cancelled"), true);
|
|
312
|
+
return reader.cancel(reason);
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// Install / uninstall discipline
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
interface PatchRecord {
|
|
322
|
+
target: object;
|
|
323
|
+
property: string;
|
|
324
|
+
original: unknown;
|
|
325
|
+
installed: unknown;
|
|
326
|
+
existed: boolean;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Records what an `install()` replaced so `uninstall()` can put it back.
|
|
331
|
+
*
|
|
332
|
+
* Two rules, both of which exist because instrumentation libraries are
|
|
333
|
+
* routinely installed alongside each other:
|
|
334
|
+
*
|
|
335
|
+
* 1. **Restore the saved object, never a re-import.** Re-importing to restore
|
|
336
|
+
* hands back whatever the *current* value of the attribute's source is,
|
|
337
|
+
* which is how two instrumentation libraries silently un-patch each other.
|
|
338
|
+
* 2. **If the attribute is no longer ours, leave it alone.** Somebody patched
|
|
339
|
+
* on top of us; restoring would delete their patch. We log at WARN and keep
|
|
340
|
+
* our record, so the customer can see it happened.
|
|
341
|
+
*/
|
|
342
|
+
export class Patcher {
|
|
343
|
+
private records: PatchRecord[] = [];
|
|
344
|
+
|
|
345
|
+
/** Set `target[property] = replacement`, remembering the exact object replaced. */
|
|
346
|
+
patch(target: object, property: string, replacement: unknown): boolean {
|
|
347
|
+
const existed = property in target;
|
|
348
|
+
const original = (target as Record<string, unknown>)[property];
|
|
349
|
+
// An ESM namespace object and a frozen class both refuse assignment —
|
|
350
|
+
// silently in sloppy mode, loudly here. Reporting it lets the caller fall
|
|
351
|
+
// back to a supported wrapping API instead of believing it installed.
|
|
352
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, property);
|
|
353
|
+
if (descriptor && !descriptor.configurable && !descriptor.writable) return false;
|
|
354
|
+
try {
|
|
355
|
+
(target as Record<string, unknown>)[property] = replacement;
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
if ((target as Record<string, unknown>)[property] !== replacement) return false;
|
|
360
|
+
this.records.push({ target, property, original, installed: replacement, existed });
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Undo every patch, newest first. Never throws. */
|
|
365
|
+
restoreAll(): void {
|
|
366
|
+
const records = [...this.records].reverse();
|
|
367
|
+
this.records = [];
|
|
368
|
+
for (const { target, property, original, installed, existed } of records) {
|
|
369
|
+
try {
|
|
370
|
+
const currentValue = (target as Record<string, unknown>)[property];
|
|
371
|
+
if (currentValue !== installed) {
|
|
372
|
+
logger.warn(
|
|
373
|
+
`not restoring ${describeTarget(target)}.${property} — it is no longer the object ` +
|
|
374
|
+
"this SDK installed (something else patched on top). Leaving the current value " +
|
|
375
|
+
"in place rather than deleting their patch.",
|
|
376
|
+
);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (existed) {
|
|
380
|
+
(target as Record<string, unknown>)[property] = original;
|
|
381
|
+
} else {
|
|
382
|
+
delete (target as Record<string, unknown>)[property];
|
|
383
|
+
}
|
|
384
|
+
} catch (error) {
|
|
385
|
+
logException(`failed to restore ${describeTarget(target)}.${property}`, error);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
get size(): number {
|
|
391
|
+
return this.records.length;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function describeTarget(target: object): string {
|
|
396
|
+
const named = target as { name?: unknown; constructor?: { name?: unknown } };
|
|
397
|
+
if (typeof named.name === "string") return named.name;
|
|
398
|
+
if (typeof named.constructor?.name === "string") return named.constructor.name;
|
|
399
|
+
return "object";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// Payload discipline
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
export const TRUNCATION_MARKER = "…[truncated]";
|
|
407
|
+
export const FIELD_LIMIT = 8192;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* How many MAX-SIZE fields one event may carry before `payload()` starts
|
|
411
|
+
* dropping keys. The budget is DERIVED from the field limit rather than being a
|
|
412
|
+
* second independent number, because the two are not independent: raising one
|
|
413
|
+
* without the other silently changes how much survives.
|
|
414
|
+
*
|
|
415
|
+
* This matters because of HOW `payload()` runs out: past the budget it does not
|
|
416
|
+
* shorten the next field, it OMITS THE KEY. A caller raising `fieldLimit`
|
|
417
|
+
* therefore has to raise the budget in step or it trades shortened values for
|
|
418
|
+
* missing ones, which is strictly worse — the event stops saying that anything
|
|
419
|
+
* is absent.
|
|
420
|
+
*/
|
|
421
|
+
const FIELDS_PER_EVENT = 16;
|
|
422
|
+
|
|
423
|
+
export const EVENT_BUDGET = FIELD_LIMIT * FIELDS_PER_EVENT;
|
|
424
|
+
const MAX_ITEMS = 100;
|
|
425
|
+
const MAX_DEPTH = 6;
|
|
426
|
+
|
|
427
|
+
/** Mutable "did we cut anything" flag, threaded through the recursion. */
|
|
428
|
+
class Cut {
|
|
429
|
+
hit = false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Remaining bytes for a whole event, spent as `truncateValue` emits. */
|
|
433
|
+
class Budget {
|
|
434
|
+
remaining: number;
|
|
435
|
+
constructor(total: number) {
|
|
436
|
+
this.remaining = total;
|
|
437
|
+
}
|
|
438
|
+
spend(n: number): void {
|
|
439
|
+
this.remaining -= n;
|
|
440
|
+
}
|
|
441
|
+
get spentOut(): boolean {
|
|
442
|
+
return this.remaining <= 0;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Shrink a payload value to something a column store will tolerate.
|
|
448
|
+
*
|
|
449
|
+
* Framework payloads are prompts, retrieved documents and tool outputs — the
|
|
450
|
+
* three largest strings in the process. None of these are promoted columns, so
|
|
451
|
+
* querying them means a JSON extraction over the payload, which has already
|
|
452
|
+
* caused a production memory blowup in the events store. Payload discipline is
|
|
453
|
+
* not optional.
|
|
454
|
+
*/
|
|
455
|
+
export function truncate(value: unknown, limit: number = FIELD_LIMIT): unknown {
|
|
456
|
+
return truncateValue(value, limit, new Cut(), 0);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function truncateValue(
|
|
460
|
+
value: unknown,
|
|
461
|
+
limit: number,
|
|
462
|
+
cut: Cut,
|
|
463
|
+
depth: number,
|
|
464
|
+
budget?: Budget,
|
|
465
|
+
): unknown {
|
|
466
|
+
if (value === null || value === undefined) {
|
|
467
|
+
budget?.spend(8);
|
|
468
|
+
return value === undefined ? null : value;
|
|
469
|
+
}
|
|
470
|
+
const kind = typeof value;
|
|
471
|
+
if (kind === "boolean" || kind === "number") {
|
|
472
|
+
budget?.spend(8);
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
if (kind === "bigint") {
|
|
476
|
+
budget?.spend(8);
|
|
477
|
+
return (value as bigint).toString();
|
|
478
|
+
}
|
|
479
|
+
if (kind === "function" || kind === "symbol") {
|
|
480
|
+
return truncateValue(render(value), limit, cut, MAX_DEPTH, budget);
|
|
481
|
+
}
|
|
482
|
+
if (kind === "string") {
|
|
483
|
+
let text = value as string;
|
|
484
|
+
if (text.length > limit) {
|
|
485
|
+
cut.hit = true;
|
|
486
|
+
text = text.slice(0, Math.max(limit - TRUNCATION_MARKER.length, 0)) + TRUNCATION_MARKER;
|
|
487
|
+
}
|
|
488
|
+
// The per-field limit bounds ONE string; the budget bounds the whole event.
|
|
489
|
+
// A structure whose leaves each fit under the limit would otherwise sail
|
|
490
|
+
// past the budget entirely.
|
|
491
|
+
if (budget) {
|
|
492
|
+
if (text.length > budget.remaining) {
|
|
493
|
+
cut.hit = true;
|
|
494
|
+
const keep = Math.max(budget.remaining - TRUNCATION_MARKER.length, 0);
|
|
495
|
+
text = text.slice(0, keep) + TRUNCATION_MARKER;
|
|
496
|
+
}
|
|
497
|
+
budget.spend(text.length);
|
|
498
|
+
}
|
|
499
|
+
return text;
|
|
500
|
+
}
|
|
501
|
+
if (depth >= MAX_DEPTH) {
|
|
502
|
+
cut.hit = true;
|
|
503
|
+
return truncateValue(render(value), limit, cut, MAX_DEPTH, budget);
|
|
504
|
+
}
|
|
505
|
+
if (value instanceof Date) {
|
|
506
|
+
return truncateValue(
|
|
507
|
+
Number.isNaN(value.getTime()) ? null : value.toISOString(),
|
|
508
|
+
limit,
|
|
509
|
+
cut,
|
|
510
|
+
depth,
|
|
511
|
+
budget,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (Array.isArray(value) || value instanceof Set) {
|
|
515
|
+
const items = Array.isArray(value) ? value : [...value];
|
|
516
|
+
const out: unknown[] = [];
|
|
517
|
+
for (const [i, item] of items.slice(0, MAX_ITEMS).entries()) {
|
|
518
|
+
if (budget?.spentOut) {
|
|
519
|
+
cut.hit = true;
|
|
520
|
+
out.push(`[${items.length - i} more items truncated]`);
|
|
521
|
+
return out;
|
|
522
|
+
}
|
|
523
|
+
out.push(truncateValue(item, limit, cut, depth + 1, budget));
|
|
524
|
+
}
|
|
525
|
+
if (items.length > MAX_ITEMS) {
|
|
526
|
+
cut.hit = true;
|
|
527
|
+
out.push(`[${items.length - MAX_ITEMS} more items truncated]`);
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
const mapping = asMapping(value);
|
|
532
|
+
if (mapping !== null) {
|
|
533
|
+
const entries = Object.entries(mapping);
|
|
534
|
+
const out: Record<string, unknown> = {};
|
|
535
|
+
for (const [i, [key, item]] of entries.entries()) {
|
|
536
|
+
if (i >= MAX_ITEMS) {
|
|
537
|
+
cut.hit = true;
|
|
538
|
+
out["…"] = `[${entries.length - MAX_ITEMS} more keys truncated]`;
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
if (budget) {
|
|
542
|
+
if (budget.spentOut) {
|
|
543
|
+
cut.hit = true;
|
|
544
|
+
out["…"] = `[${entries.length - i} more keys truncated]`;
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
547
|
+
budget.spend(key.length);
|
|
548
|
+
}
|
|
549
|
+
out[key] = truncateValue(item, limit, cut, depth + 1, budget);
|
|
550
|
+
}
|
|
551
|
+
return out;
|
|
552
|
+
}
|
|
553
|
+
// An object with no JSON shape is rendered, not cut — `fw_truncated` means
|
|
554
|
+
// "data was lost", and a rendering that fits has lost nothing a JSON encoder
|
|
555
|
+
// would have kept.
|
|
556
|
+
return truncateValue(render(value), limit, cut, MAX_DEPTH, budget);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* A plain object view of `value`, or null.
|
|
561
|
+
*
|
|
562
|
+
* Shallow on purpose. A deep clone would duplicate the whole tree before
|
|
563
|
+
* `truncateValue` gets to decide it only wanted the first 8 KB. Reading the top
|
|
564
|
+
* level and handing it back lets the existing walk apply the field limit, the
|
|
565
|
+
* item cap and the depth cap on the way down.
|
|
566
|
+
*
|
|
567
|
+
* Everything here can execute the caller's own code — a getter, a `toJSON`, a
|
|
568
|
+
* Zod schema's accessor — so all of it is guarded, and a failure falls through
|
|
569
|
+
* to `render`.
|
|
570
|
+
*/
|
|
571
|
+
function asMapping(value: unknown): Record<string, unknown> | null {
|
|
572
|
+
if (typeof value !== "object" || value === null) return null;
|
|
573
|
+
try {
|
|
574
|
+
if (value instanceof Map) {
|
|
575
|
+
const out: Record<string, unknown> = {};
|
|
576
|
+
for (const [key, item] of value) out[String(key)] = item;
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
if (value instanceof Error) {
|
|
580
|
+
return { name: value.name, message: value.message };
|
|
581
|
+
}
|
|
582
|
+
if (ArrayBuffer.isView(value)) return null;
|
|
583
|
+
const toJSON = (value as { toJSON?: unknown }).toJSON;
|
|
584
|
+
if (typeof toJSON === "function") {
|
|
585
|
+
const dumped: unknown = (toJSON as () => unknown).call(value);
|
|
586
|
+
return typeof dumped === "object" && dumped !== null && !Array.isArray(dumped)
|
|
587
|
+
? (dumped as Record<string, unknown>)
|
|
588
|
+
: null;
|
|
589
|
+
}
|
|
590
|
+
// Own enumerable properties only. Walking the prototype chain would pull in
|
|
591
|
+
// a framework class's accessors, half of which are lazy and some of which
|
|
592
|
+
// make network calls.
|
|
593
|
+
return { ...(value as Record<string, unknown>) };
|
|
594
|
+
} catch {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function render(value: unknown): string {
|
|
600
|
+
try {
|
|
601
|
+
if (typeof value === "object" && value !== null) {
|
|
602
|
+
const name = value.constructor?.name;
|
|
603
|
+
return name && name !== "Object" ? `[${name}]` : "[object]";
|
|
604
|
+
}
|
|
605
|
+
if (typeof value === "symbol") return value.toString();
|
|
606
|
+
if (typeof value === "function") return `[function ${value.name || "anonymous"}]`;
|
|
607
|
+
return String(value);
|
|
608
|
+
} catch {
|
|
609
|
+
return "[unrenderable]";
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function sizeOf(value: unknown, depth = 0): number {
|
|
614
|
+
if (value === null || value === undefined) return 8;
|
|
615
|
+
const kind = typeof value;
|
|
616
|
+
if (kind === "boolean" || kind === "number" || kind === "bigint") return 8;
|
|
617
|
+
if (kind === "string") return (value as string).length;
|
|
618
|
+
if (depth >= MAX_DEPTH) return render(value).length;
|
|
619
|
+
try {
|
|
620
|
+
if (Array.isArray(value)) {
|
|
621
|
+
let total = 0;
|
|
622
|
+
for (const item of value) total += sizeOf(item, depth + 1);
|
|
623
|
+
return total;
|
|
624
|
+
}
|
|
625
|
+
if (value instanceof Set) {
|
|
626
|
+
let total = 0;
|
|
627
|
+
for (const item of value) total += sizeOf(item, depth + 1);
|
|
628
|
+
return total;
|
|
629
|
+
}
|
|
630
|
+
const mapping = asMapping(value);
|
|
631
|
+
if (mapping !== null) {
|
|
632
|
+
let total = 0;
|
|
633
|
+
for (const [key, item] of Object.entries(mapping)) total += key.length + sizeOf(item, depth + 1);
|
|
634
|
+
return total;
|
|
635
|
+
}
|
|
636
|
+
} catch {
|
|
637
|
+
return 16;
|
|
638
|
+
}
|
|
639
|
+
return render(value).length;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Apply the per-field limit and the per-event budget to a set of extras.
|
|
644
|
+
*
|
|
645
|
+
* Anything cut sets `fw_truncated=true`, so a surprising-looking payload in the
|
|
646
|
+
* dashboard is self-explaining rather than a mystery.
|
|
647
|
+
*/
|
|
648
|
+
export function payload(
|
|
649
|
+
fields: Record<string, unknown>,
|
|
650
|
+
options: { limit?: number; budget?: number; cut?: Cut } = {},
|
|
651
|
+
): Record<string, unknown> {
|
|
652
|
+
const limit = options.limit ?? FIELD_LIMIT;
|
|
653
|
+
// Shared with the caller when it also truncated something — the tracker cuts
|
|
654
|
+
// the DECLARED parameters itself, and `fw_truncated` has to mean "this event
|
|
655
|
+
// lost data", not "one of its metadata extras did".
|
|
656
|
+
const cut = options.cut ?? new Cut();
|
|
657
|
+
const spend = new Budget(options.budget ?? EVENT_BUDGET);
|
|
658
|
+
|
|
659
|
+
// SMALLEST FIRST, spent in that order and emitted in the caller's. The budget
|
|
660
|
+
// binds either way, but insertion order decides WHICH keys survive it, and
|
|
661
|
+
// the adapters put the big payload before the metadata: an oversized
|
|
662
|
+
// `fw_inputs` would consume the whole event and take `fw_run_id` and
|
|
663
|
+
// `fw_node` with it — the two fields that say which run the payload belongs
|
|
664
|
+
// to. Sizing first costs a walk over the node count, not the character count.
|
|
665
|
+
const sized = Object.entries(fields)
|
|
666
|
+
.map(([key, value]) => ({ key, value, size: sizeOf(value) + key.length }))
|
|
667
|
+
.sort((a, b) => a.size - b.size);
|
|
668
|
+
|
|
669
|
+
const kept = new Map<string, unknown>();
|
|
670
|
+
for (const { key, value } of sized) {
|
|
671
|
+
if (spend.spentOut) {
|
|
672
|
+
// Past the budget a field does not arrive short, it does not arrive at
|
|
673
|
+
// all — which is why the limit and the budget cannot move independently.
|
|
674
|
+
cut.hit = true;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
spend.spend(key.length);
|
|
678
|
+
kept.set(key, truncateValue(value, limit, cut, 0, spend));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const out: Record<string, unknown> = {};
|
|
682
|
+
for (const key of Object.keys(fields)) {
|
|
683
|
+
if (kept.has(key)) out[key] = kept.get(key);
|
|
684
|
+
}
|
|
685
|
+
if (cut.hit) out.fw_truncated = true;
|
|
686
|
+
return out;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Deliberate exceptions: these are top-level by design. `duration_ms` is how an
|
|
691
|
+
* adapter reports a model call's real latency; `usage` is read by both the
|
|
692
|
+
* server summary and the dashboard as a token fallback; `request_id` pairs
|
|
693
|
+
* model events; `framework*` label every event.
|
|
694
|
+
*/
|
|
695
|
+
export const ALLOWED_TOP_LEVEL: ReadonlySet<string> = new Set([
|
|
696
|
+
"request_id",
|
|
697
|
+
"duration_ms",
|
|
698
|
+
"usage",
|
|
699
|
+
"traceback",
|
|
700
|
+
"framework",
|
|
701
|
+
"framework_version",
|
|
702
|
+
"integration_version",
|
|
703
|
+
]);
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* An extra whose name collides with a declared field SILENTLY OVERWRITES it:
|
|
707
|
+
* the schema ends with a merge of the extras. An adapter reflecting a
|
|
708
|
+
* framework's options into extras would then change `tool_name`, `model`,
|
|
709
|
+
* `outcome` or `input_tokens` — i.e. the promoted columns and the server's
|
|
710
|
+
* computed summary — and every test would still pass.
|
|
711
|
+
*/
|
|
712
|
+
export const FORBIDDEN_EXTRAS: ReadonlySet<string> = new Set(
|
|
713
|
+
[...DECLARED_FIELD_NAMES].filter((name) => !ALLOWED_TOP_LEVEL.has(name)),
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
const FW_PREFIX = "fw_";
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Build the `fw_*` extra-field namespace.
|
|
720
|
+
*
|
|
721
|
+
* fwFields({ run_id: runId, node: "retrieve", tags: undefined })
|
|
722
|
+
* -> { fw_run_id: "...", fw_node: "retrieve" }
|
|
723
|
+
*
|
|
724
|
+
* Keys are prefixed unless they already are, or are one of the deliberate
|
|
725
|
+
* top-level names. Nullish values are dropped (the schema omits absent
|
|
726
|
+
* optionals anyway, and an extra explicitly set to null would still occupy a
|
|
727
|
+
* key, reach the wire as JSON null, and NULL out a promoted column).
|
|
728
|
+
*
|
|
729
|
+
* Flat only — the server's payload key expression is single-level, so a nested
|
|
730
|
+
* object is not queryable.
|
|
731
|
+
*
|
|
732
|
+
* Values are NOT truncated here. Doing it at this layer would pin every `fw_*`
|
|
733
|
+
* extra at the module-level default while `instrument(..., { captureLimit })`
|
|
734
|
+
* raised the ceiling for the declared fields — half the event honouring the
|
|
735
|
+
* option and half not, with nothing saying which. `payload()`, which receives
|
|
736
|
+
* the tracker's real limit, is the one place that bounds them.
|
|
737
|
+
*/
|
|
738
|
+
export function fwFields(fields: Record<string, unknown>): Record<string, unknown> {
|
|
739
|
+
const out: Record<string, unknown> = {};
|
|
740
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
741
|
+
if (value === null || value === undefined) continue;
|
|
742
|
+
const name = ALLOWED_TOP_LEVEL.has(key) || key.startsWith(FW_PREFIX) ? key : FW_PREFIX + key;
|
|
743
|
+
out[name] = value;
|
|
744
|
+
}
|
|
745
|
+
return guardExtras(out);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Strip (or, in strict mode, reject) extras that would shadow a real field.
|
|
750
|
+
*
|
|
751
|
+
* Called on every emit, so even an adapter that builds its extras by hand
|
|
752
|
+
* cannot silently rewrite a promoted column.
|
|
753
|
+
*/
|
|
754
|
+
export function guardExtras(fields: Record<string, unknown>): Record<string, unknown> {
|
|
755
|
+
const bad = Object.keys(fields).filter((key) => FORBIDDEN_EXTRAS.has(key));
|
|
756
|
+
if (bad.length === 0) return fields;
|
|
757
|
+
const message =
|
|
758
|
+
`extra fields ${JSON.stringify(bad.sort())} would overwrite declared event fields ` +
|
|
759
|
+
"(the schema merges extras last). Namespace them as fw_* instead.";
|
|
760
|
+
if (strict()) throw new Error(message);
|
|
761
|
+
logger.warn(`${message} Dropping them.`);
|
|
762
|
+
const out: Record<string, unknown> = {};
|
|
763
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
764
|
+
if (!FORBIDDEN_EXTRAS.has(key)) out[key] = value;
|
|
765
|
+
}
|
|
766
|
+
return out;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* The `framework` / `framework_version` / `integration_version` triple.
|
|
771
|
+
*
|
|
772
|
+
* Payload-only, so **not** server-side filterable; promoting it later is a
|
|
773
|
+
* hand-mirrored change across several files, so it is done on demand, not
|
|
774
|
+
* speculatively.
|
|
775
|
+
*/
|
|
776
|
+
export function frameworkFields(name: string, pkg?: string): Record<string, unknown> {
|
|
777
|
+
const out: Record<string, unknown> = { framework: name, integration_version: VERSION };
|
|
778
|
+
const version = pkg ? versionString(pkg) : null;
|
|
779
|
+
if (version) out.framework_version = version;
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const ID_SEPARATORS = /[\s\-_.:/]+/;
|
|
784
|
+
const EMBEDDED_UUID = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
|
|
785
|
+
const UUID_EXACT = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
786
|
+
const HEX_ONLY = /^[0-9a-fA-F]+$/;
|
|
787
|
+
const AGENT_ID_LIMIT = 64;
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Turn a framework's label into something safe for `agentId`.
|
|
791
|
+
*
|
|
792
|
+
* `agent_id` is a `LowCardinality(String)` column and the primary facet on
|
|
793
|
+
* every dashboard surface. A UUID in it poisons that facet permanently —
|
|
794
|
+
* LowCardinality degrades, and the filter dropdown fills with one entry per
|
|
795
|
+
* run. So a value that looks like an id becomes `fallback` and the real id goes
|
|
796
|
+
* to `fw_agent_id` / `fw_run_id` where it belongs.
|
|
797
|
+
*/
|
|
798
|
+
export function normalizeAgentId(raw: unknown, fallback: string = DEFAULT_AGENT_ID): string {
|
|
799
|
+
if (raw === null || raw === undefined) return fallback;
|
|
800
|
+
const text = (typeof raw === "string" ? raw : render(raw)).split(/\s+/).filter(Boolean).join(" ");
|
|
801
|
+
if (text === "") return fallback;
|
|
802
|
+
if (looksLikeId(text)) return fallback;
|
|
803
|
+
const stripped = stripEmbeddedId(text);
|
|
804
|
+
if (stripped === "") return fallback;
|
|
805
|
+
return stripped.slice(0, AGENT_ID_LIMIT);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** True for UUIDs and long bare hex strings. */
|
|
809
|
+
function looksLikeId(text: string): boolean {
|
|
810
|
+
if (UUID_EXACT.test(text)) return true;
|
|
811
|
+
const bare = text.replaceAll("-", "").replaceAll("_", "");
|
|
812
|
+
return bare.length >= 16 && HEX_ONLY.test(bare);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Drop a per-run id that a readable prefix is carrying.
|
|
817
|
+
*
|
|
818
|
+
* `looksLikeId` only fires on a value that is an id ALL THE WAY THROUGH, so it
|
|
819
|
+
* catches a bare UUID and misses `agent-<uuid>`, `crew_<uuid>`, `task-3f9a1c…`
|
|
820
|
+
* — a readable name with a per-run suffix, which is the shape frameworks
|
|
821
|
+
* actually produce and precisely the one the docs warn against. Those would go
|
|
822
|
+
* through untouched, one distinct value per run, into a `LowCardinality(String)`
|
|
823
|
+
* column that is the primary facet on every dashboard surface.
|
|
824
|
+
*
|
|
825
|
+
* Stripping rather than falling back: `agent-<uuid>` still knows it is an
|
|
826
|
+
* agent, and collapsing every such label to `main` would throw away the one
|
|
827
|
+
* readable thing in it. A segment is dropped only if it is a UUID or a hex run
|
|
828
|
+
* of 16+ characters, so a name like `agent-v2` or `step-3` is untouched.
|
|
829
|
+
*/
|
|
830
|
+
function stripEmbeddedId(text: string): string {
|
|
831
|
+
// Dashed UUIDs first, and as a substring: splitting on separators would break
|
|
832
|
+
// `task-3f9a1c2b-...` into five segments none of which is an id on its own,
|
|
833
|
+
// so the most standard shape of all would survive the segment pass.
|
|
834
|
+
const stripped = text.replace(EMBEDDED_UUID, " ");
|
|
835
|
+
const parts = stripped.split(ID_SEPARATORS).filter(Boolean);
|
|
836
|
+
const kept = parts.filter((part) => !looksLikeId(part));
|
|
837
|
+
// Nothing was an id: hand back the ORIGINAL, separators and all. Rejoining on
|
|
838
|
+
// spaces would rewrite every `node_a_b` in the process into `node a b`, which
|
|
839
|
+
// is a rename of the primary facet in exchange for nothing.
|
|
840
|
+
if (stripped === text && kept.length === parts.length) return text;
|
|
841
|
+
return kept.join(" ").trim();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Whole milliseconds, as an integer.
|
|
846
|
+
*
|
|
847
|
+
* The server stores `duration_ms` as a u32 and its JSON parser drops
|
|
848
|
+
* non-integers, so a float silently NULLs the column: the dashboard then shows
|
|
849
|
+
* no duration and nobody sees an error. Negative deltas (clock adjustments, a
|
|
850
|
+
* framework handing us an end before its start) clamp to 0.
|
|
851
|
+
*/
|
|
852
|
+
export function ms(deltaMs: number): number {
|
|
853
|
+
return Math.max(Math.round(deltaMs), 0);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ---------------------------------------------------------------------------
|
|
857
|
+
// Shape B — callback surfaces
|
|
858
|
+
// ---------------------------------------------------------------------------
|
|
859
|
+
|
|
860
|
+
interface Run {
|
|
861
|
+
identity: Identity;
|
|
862
|
+
parentKey: unknown;
|
|
863
|
+
/**
|
|
864
|
+
* The run joined an enclosing `agent()` scope of the same name instead of
|
|
865
|
+
* opening its own agent, so it emits neither `agent_start` nor `agent_end`.
|
|
866
|
+
*/
|
|
867
|
+
joined?: boolean;
|
|
868
|
+
/** When it opened, in the event clock's microseconds: the exit path's ordering. */
|
|
869
|
+
opened?: number;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export type EventMethod =
|
|
873
|
+
| "toolUse"
|
|
874
|
+
| "toolResult"
|
|
875
|
+
| "modelRequest"
|
|
876
|
+
| "modelResponse"
|
|
877
|
+
| "agentStart"
|
|
878
|
+
| "agentEnd"
|
|
879
|
+
| "agentPause"
|
|
880
|
+
| "agentResume"
|
|
881
|
+
| "hookTriggered"
|
|
882
|
+
| "hookCompleted"
|
|
883
|
+
| "error"
|
|
884
|
+
| "humanWait"
|
|
885
|
+
| "humanInput"
|
|
886
|
+
| "humanPause"
|
|
887
|
+
| "humanInterrupt";
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Maps a framework's own run ids onto FailproofAI identity.
|
|
891
|
+
*
|
|
892
|
+
* This is Shape B: the surface where a start and its end are **separate
|
|
893
|
+
* callbacks**, possibly on different async branches. Such an adapter can never
|
|
894
|
+
* use `AsyncLocalStorage` — there is no single subtree to run the pair inside,
|
|
895
|
+
* and `enterWith` in the start callback would bind identity into whatever
|
|
896
|
+
* unrelated context happened to dispatch it. Instead we keep the mapping here
|
|
897
|
+
* and pass `sessionId` / `agentId` **explicitly** on every emit.
|
|
898
|
+
*
|
|
899
|
+
* Bounded (`maxOpen`, FIFO eviction) because orphaned starts are normal: a
|
|
900
|
+
* crashed run, a stream nobody consumed, a framework that forgot an end
|
|
901
|
+
* callback. Unbounded, that is a memory leak in a long-lived server.
|
|
902
|
+
*/
|
|
903
|
+
/** The event methods that end the run they are keyed on. */
|
|
904
|
+
const CLOSING_METHODS: ReadonlySet<string> = new Set(["toolResult", "modelResponse", "hookCompleted"]);
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
export class RunTracker {
|
|
908
|
+
readonly name: string;
|
|
909
|
+
private readonly maxOpen: number;
|
|
910
|
+
private readonly baseFields: Record<string, unknown>;
|
|
911
|
+
private readonly fieldLimit: number;
|
|
912
|
+
private readonly budget: number;
|
|
913
|
+
private readonly runs = new Map<unknown, Run>();
|
|
914
|
+
private readonly links = new Map<unknown, unknown>();
|
|
915
|
+
/**
|
|
916
|
+
* Open pauses per agent key. A run paused on a human (a LangGraph interrupt,
|
|
917
|
+
* a suspended Mastra workflow) is deliberately left open: another process
|
|
918
|
+
* may take the answer and resume it from the checkpoint. Closing it at this
|
|
919
|
+
* process's exit would end a run that is not over.
|
|
920
|
+
*/
|
|
921
|
+
private readonly pauses = new Map<unknown, number>();
|
|
922
|
+
private warned = false;
|
|
923
|
+
|
|
924
|
+
constructor(
|
|
925
|
+
name: string,
|
|
926
|
+
options: {
|
|
927
|
+
maxOpen?: number;
|
|
928
|
+
baseFields?: Record<string, unknown>;
|
|
929
|
+
fieldLimit?: number;
|
|
930
|
+
} = {},
|
|
931
|
+
) {
|
|
932
|
+
this.name = name;
|
|
933
|
+
this.maxOpen = options.maxOpen ?? 10_000;
|
|
934
|
+
this.baseFields = { ...(options.baseFields ?? {}) };
|
|
935
|
+
// One place decides how much of a value survives, for both halves of an
|
|
936
|
+
// event: the declared parameters (`input`, `output`, `messages`) and the
|
|
937
|
+
// `fw_*` extras. Two different rules would mean raising the adapter's limit
|
|
938
|
+
// changed only half the event.
|
|
939
|
+
this.fieldLimit = options.fieldLimit ?? FIELD_LIMIT;
|
|
940
|
+
this.budget = this.fieldLimit * FIELDS_PER_EVENT;
|
|
941
|
+
// A framework run still open when the process exits (a deploy's SIGTERM
|
|
942
|
+
// mid-graph) would otherwise render as running forever. Held weakly, so a
|
|
943
|
+
// tracker an adapter drops is not kept alive by this registration.
|
|
944
|
+
const self = new WeakRef(this);
|
|
945
|
+
const unregister = onProcessExit(() => {
|
|
946
|
+
const tracker = self.deref();
|
|
947
|
+
if (tracker === undefined) {
|
|
948
|
+
unregister();
|
|
949
|
+
return [];
|
|
950
|
+
}
|
|
951
|
+
return tracker.openAtExit();
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// -- identity ---------------------------------------------------------
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Resolve a run to an identity, in this order:
|
|
959
|
+
*
|
|
960
|
+
* 1. the exact `key`;
|
|
961
|
+
* 2. the `parentKey` chain, walked through every link we have seen — a
|
|
962
|
+
* framework's own parent run id is a *better* parent chain than an
|
|
963
|
+
* ambient stack, because it survives async hops;
|
|
964
|
+
* 3. **`failproofai.current()`** — this is the whole interop story. An
|
|
965
|
+
* adapter running inside a hand-written `agent("planner", ...)` joins that
|
|
966
|
+
* same session and gets `parentId: "planner"`, so mixing the manual API
|
|
967
|
+
* and an adapter produces one tree, not two;
|
|
968
|
+
* 4. otherwise the event is dropped and we log **once**.
|
|
969
|
+
*/
|
|
970
|
+
identity(key: unknown, parentKey?: unknown, warnOnMiss = true): Identity | null {
|
|
971
|
+
if (key !== undefined && key !== null) {
|
|
972
|
+
const run = this.runs.get(key);
|
|
973
|
+
if (run) return run.identity;
|
|
974
|
+
}
|
|
975
|
+
const walked = this.walk(parentKey);
|
|
976
|
+
if (walked) return walked;
|
|
977
|
+
|
|
978
|
+
const ambient = RunTracker.ambient(true);
|
|
979
|
+
if (ambient) return ambient;
|
|
980
|
+
|
|
981
|
+
if (warnOnMiss) this.warnUnresolved(key);
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Step 3: the identity a hand-written scope has bound, if any.
|
|
987
|
+
*
|
|
988
|
+
* When resolving an event we coerce a missing agent id to `main`, but when
|
|
989
|
+
* resolving a *parent* we must not: inside a bare `session(...)` there is no
|
|
990
|
+
* open agent, and claiming `parentId: "main"` would point at an agent that
|
|
991
|
+
* never emitted an `agent_start` — which makes the dashboard synthesize a
|
|
992
|
+
* never-ending root span that stays `ongoing` forever.
|
|
993
|
+
*/
|
|
994
|
+
private static ambient(coerceAgent: boolean): Identity | null {
|
|
995
|
+
const identity = currentIdentity();
|
|
996
|
+
if (identity.sessionId === null) return null;
|
|
997
|
+
return {
|
|
998
|
+
sessionId: identity.sessionId,
|
|
999
|
+
agentId: identity.agentId ?? (coerceAgent ? DEFAULT_AGENT_ID : null),
|
|
1000
|
+
parentId: identity.parentId,
|
|
1001
|
+
depth: identity.depth,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
private walk(parentKey: unknown): Identity | null {
|
|
1006
|
+
const seen = new Set<unknown>();
|
|
1007
|
+
let key = parentKey;
|
|
1008
|
+
while (key !== undefined && key !== null && !seen.has(key)) {
|
|
1009
|
+
seen.add(key);
|
|
1010
|
+
const run = this.runs.get(key);
|
|
1011
|
+
if (run) return run.identity;
|
|
1012
|
+
key = this.links.get(key);
|
|
1013
|
+
}
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
private warnUnresolved(key: unknown): void {
|
|
1018
|
+
if (this.warned) return;
|
|
1019
|
+
this.warned = true;
|
|
1020
|
+
logger.warn(
|
|
1021
|
+
`${this.name} could not resolve a session for run ${String(key)} and is dropping its ` +
|
|
1022
|
+
"events. Wrap the call in `await failproofai.session(fn)` (or " +
|
|
1023
|
+
"`await failproofai.agent('name', fn)`) if you want them attributed. This is logged " +
|
|
1024
|
+
"once per tracker.",
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Record a run's parent without making it an agent.
|
|
1030
|
+
*
|
|
1031
|
+
* Intermediate framework runs (a LangChain chain, a Mastra step) do not
|
|
1032
|
+
* become spans, but their children still need to find the agent above them.
|
|
1033
|
+
* This is what makes step 2 of `identity()` work more than one hop up.
|
|
1034
|
+
*/
|
|
1035
|
+
link(key: unknown, parentKey: unknown): void {
|
|
1036
|
+
if (key === undefined || key === null) return;
|
|
1037
|
+
if (parentKey === undefined || parentKey === null || key === parentKey) return;
|
|
1038
|
+
// Delete first so a re-link refreshes the entry's age: the FIFO cap below
|
|
1039
|
+
// must evict the runs that have been around longest, not the ones that
|
|
1040
|
+
// happened to be linked first.
|
|
1041
|
+
this.links.delete(key);
|
|
1042
|
+
this.evict(this.links);
|
|
1043
|
+
this.links.set(key, parentKey);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Forget a run's link. Call it when the run ENDS.
|
|
1048
|
+
*
|
|
1049
|
+
* A link is only needed while the run is live — it is how that run's own
|
|
1050
|
+
* closing event, and its children's, find the agent above. Kept past that,
|
|
1051
|
+
* the table fills to its cap with runs that finished long ago, and the FIFO
|
|
1052
|
+
* cap then evicts the links of runs that are STILL RUNNING: on a busy server
|
|
1053
|
+
* a model call that outlived ~10k other runs lost its `model_response` to
|
|
1054
|
+
* "could not resolve a session". `emit()` does this itself for the closing
|
|
1055
|
+
* event types; an adapter that links a run which emits nothing (an
|
|
1056
|
+
* intermediate chain) must call it.
|
|
1057
|
+
*/
|
|
1058
|
+
unlink(key: unknown): void {
|
|
1059
|
+
this.links.delete(key);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/** FIFO — a `Map` keeps insertion order. */
|
|
1063
|
+
private evict(table: Map<unknown, unknown>): void {
|
|
1064
|
+
while (table.size >= this.maxOpen) {
|
|
1065
|
+
const oldest = table.keys().next();
|
|
1066
|
+
if (oldest.done) break;
|
|
1067
|
+
table.delete(oldest.value);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// -- agents -----------------------------------------------------------
|
|
1072
|
+
|
|
1073
|
+
/** Register a run as an agent and emit `agent_start`. */
|
|
1074
|
+
startAgent(
|
|
1075
|
+
key: unknown,
|
|
1076
|
+
options: {
|
|
1077
|
+
agentId: string;
|
|
1078
|
+
parentKey?: unknown;
|
|
1079
|
+
sessionId?: string;
|
|
1080
|
+
goal?: string;
|
|
1081
|
+
} & Record<string, unknown>,
|
|
1082
|
+
): Identity {
|
|
1083
|
+
const { agentId, parentKey, sessionId, goal, ...fields } = options;
|
|
1084
|
+
const parent = this.resolveParent(parentKey);
|
|
1085
|
+
const sid = sessionId ?? parent?.sessionId ?? randomUUID().replace(/-/g, "");
|
|
1086
|
+
const aid = normalizeAgentId(agentId);
|
|
1087
|
+
// `await agent("support", () => graph.invoke(...))` around a graph also
|
|
1088
|
+
// named "support" is the user saying "this run IS my agent", not "my agent
|
|
1089
|
+
// contains an agent of the same name". Opening a second one gave every run
|
|
1090
|
+
// two agent_start/agent_end pairs and an agent listed as its own parent.
|
|
1091
|
+
// So a framework root that lands directly inside a hand-written scope of
|
|
1092
|
+
// the same name, in the same session, joins it. Differently named, it
|
|
1093
|
+
// still nests — that is a real tree.
|
|
1094
|
+
if (
|
|
1095
|
+
parent !== null &&
|
|
1096
|
+
parent.agentId === aid &&
|
|
1097
|
+
parent.sessionId === sid &&
|
|
1098
|
+
this.walk(parentKey) === null
|
|
1099
|
+
) {
|
|
1100
|
+
this.runs.delete(key);
|
|
1101
|
+
this.evict(this.runs);
|
|
1102
|
+
this.runs.set(key, { identity: parent, parentKey, joined: true });
|
|
1103
|
+
this.link(key, parentKey);
|
|
1104
|
+
return parent;
|
|
1105
|
+
}
|
|
1106
|
+
const identity: Identity = {
|
|
1107
|
+
sessionId: sid,
|
|
1108
|
+
agentId: aid,
|
|
1109
|
+
parentId: parent?.agentId ?? null,
|
|
1110
|
+
depth: parent ? parent.depth + 1 : 1,
|
|
1111
|
+
};
|
|
1112
|
+
this.runs.delete(key);
|
|
1113
|
+
this.evict(this.runs);
|
|
1114
|
+
this.runs.set(key, { identity, parentKey, opened: nowMicros() });
|
|
1115
|
+
this.link(key, parentKey);
|
|
1116
|
+
this.emitWith("agentStart", identity, {
|
|
1117
|
+
goal: goal === undefined ? undefined : truncate(goal, this.fieldLimit),
|
|
1118
|
+
parentId: identity.parentId,
|
|
1119
|
+
...fields,
|
|
1120
|
+
});
|
|
1121
|
+
return identity;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Emit `agent_end` and forget the run.
|
|
1126
|
+
*
|
|
1127
|
+
* `outcome` is `"failed"`, never `"failure"` — the server only counts
|
|
1128
|
+
* `error|failed|timeout|rejected` as a failure.
|
|
1129
|
+
*/
|
|
1130
|
+
endAgent(
|
|
1131
|
+
key: unknown,
|
|
1132
|
+
options: { outcome?: string; summary?: string } & Record<string, unknown> = {},
|
|
1133
|
+
): void {
|
|
1134
|
+
const { outcome = "success", summary, ...fields } = options;
|
|
1135
|
+
const run = this.runs.get(key);
|
|
1136
|
+
this.runs.delete(key);
|
|
1137
|
+
this.pauses.delete(key);
|
|
1138
|
+
const identity = run?.identity ?? this.identity(key);
|
|
1139
|
+
this.links.delete(key);
|
|
1140
|
+
if (identity === null) return;
|
|
1141
|
+
// A joined run's agent belongs to the enclosing scope, which ends it — and
|
|
1142
|
+
// records the failure, if the error propagates out of the framework call.
|
|
1143
|
+
if (run?.joined === true) return;
|
|
1144
|
+
this.emitWith("agentEnd", identity, {
|
|
1145
|
+
outcome,
|
|
1146
|
+
summary: summary === undefined ? undefined : truncate(summary, this.fieldLimit),
|
|
1147
|
+
...fields,
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
openAgents(): unknown[] {
|
|
1152
|
+
return [...this.runs.keys()];
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/** Whether `key` is an open agent. O(1) — `openAgents()` copies every key. */
|
|
1156
|
+
isOpen(key: unknown): boolean {
|
|
1157
|
+
return this.runs.has(key);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* Drop an agent and its link WITHOUT emitting anything.
|
|
1162
|
+
*
|
|
1163
|
+
* For an agent this process will never close but must stop holding: a run
|
|
1164
|
+
* paused on a human and resumed by another worker. Closing it here would put
|
|
1165
|
+
* a second `agent_end` into a session the other worker ends; keeping it
|
|
1166
|
+
* would hold a slot in the table live runs need.
|
|
1167
|
+
*/
|
|
1168
|
+
forget(key: unknown): void {
|
|
1169
|
+
this.runs.delete(key);
|
|
1170
|
+
this.links.delete(key);
|
|
1171
|
+
this.pauses.delete(key);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/** @internal Table sizes, for the tests that prove nothing is retained. */
|
|
1175
|
+
stats(): { runs: number; links: number } {
|
|
1176
|
+
return { runs: this.runs.size, links: this.links.size };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Close every still-open agent, newest first.
|
|
1181
|
+
*
|
|
1182
|
+
* A session that dies with an open `agent_start` renders as `ongoing`
|
|
1183
|
+
* forever, so teardown closes what it opened.
|
|
1184
|
+
*/
|
|
1185
|
+
closeOpenAgents(outcome = "cancelled"): void {
|
|
1186
|
+
for (const key of this.openAgents().reverse()) this.endAgent(key, { outcome });
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* The process is exiting: end every open agent as `failed`, newest first —
|
|
1191
|
+
* except one paused on a human, which is waiting, not abandoned (`pauses`).
|
|
1192
|
+
*/
|
|
1193
|
+
closeAtExit(exitCode = 0): void {
|
|
1194
|
+
for (const item of this.openAtExit().sort((x, y) => y.opened - x.opened)) item.close(exitCode);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* Every open agent, for the exit path to close in one most-recent-first
|
|
1199
|
+
* order with everything else still open (`exit.ts`) — except one paused on a
|
|
1200
|
+
* human, which is waiting, not abandoned (`pauses`), and one that joined a
|
|
1201
|
+
* hand-written scope, which that scope ends.
|
|
1202
|
+
*/
|
|
1203
|
+
openAtExit(): OpenItem[] {
|
|
1204
|
+
const items: OpenItem[] = [];
|
|
1205
|
+
for (const [key, run] of this.runs) {
|
|
1206
|
+
if ((this.pauses.get(key) ?? 0) > 0 || run.joined === true) continue;
|
|
1207
|
+
items.push({
|
|
1208
|
+
opened: run.opened ?? 0,
|
|
1209
|
+
close: (exitCode) => {
|
|
1210
|
+
const current = this.runs.get(key);
|
|
1211
|
+
if (current === undefined) return; // ended normally in the meantime
|
|
1212
|
+
const message = `the process exited (code ${exitCode})${fatalSuffix()} while this run was still running`;
|
|
1213
|
+
// `error` strictly before `agent_end`, as a scope that threw would.
|
|
1214
|
+
this.emitWith("error", current.identity, { errorType: "ProcessExit", message });
|
|
1215
|
+
this.endAgent(key, { outcome: "failed", summary: "the process exited while this run was open" });
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
return items;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
reset(): void {
|
|
1224
|
+
this.runs.clear();
|
|
1225
|
+
this.links.clear();
|
|
1226
|
+
this.pauses.clear();
|
|
1227
|
+
this.warned = false;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// -- everything else --------------------------------------------------
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Emit any `failproofai.event.*` method against a run's identity.
|
|
1234
|
+
*
|
|
1235
|
+
* Drops the event (with one warning) when nothing resolves, rather than
|
|
1236
|
+
* inventing a session id: a synthesized session splits one run into many.
|
|
1237
|
+
*/
|
|
1238
|
+
emit(
|
|
1239
|
+
method: EventMethod,
|
|
1240
|
+
key: unknown,
|
|
1241
|
+
fields: { parentKey?: unknown } & Record<string, unknown> = {},
|
|
1242
|
+
): void {
|
|
1243
|
+
const { parentKey, ...rest } = fields;
|
|
1244
|
+
if (parentKey !== undefined && parentKey !== null) this.link(key, parentKey);
|
|
1245
|
+
if (method === "agentPause") this.pauses.set(key, (this.pauses.get(key) ?? 0) + 1);
|
|
1246
|
+
if (method === "agentResume") {
|
|
1247
|
+
const open = (this.pauses.get(key) ?? 0) - 1;
|
|
1248
|
+
if (open > 0) this.pauses.set(key, open);
|
|
1249
|
+
else this.pauses.delete(key);
|
|
1250
|
+
}
|
|
1251
|
+
const identity = this.identity(key, parentKey);
|
|
1252
|
+
// A closing event ends the run it is keyed on, so its link is done with.
|
|
1253
|
+
// Its children closed before it, and anything later that still names it
|
|
1254
|
+
// resolves through its parent chain or the ambient scope — see `unlink`.
|
|
1255
|
+
if (CLOSING_METHODS.has(method)) this.links.delete(key);
|
|
1256
|
+
if (identity === null) return;
|
|
1257
|
+
this.emitWith(method, identity, rest);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
private emitWith(method: EventMethod, identity: Identity, fields: Record<string, unknown>): void {
|
|
1261
|
+
callSafely(
|
|
1262
|
+
() => {
|
|
1263
|
+
this.emitNow(method, identity, fields);
|
|
1264
|
+
},
|
|
1265
|
+
[],
|
|
1266
|
+
`${this.name}.${method}`,
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
private emitNow(method: EventMethod, identity: Identity, fields: Record<string, unknown>): void {
|
|
1271
|
+
// Two kinds of key here, and the split is by NAME, not by meaning: `fw_*`
|
|
1272
|
+
// (plus whatever the adapter set as base fields) are payload extras and go
|
|
1273
|
+
// through the guard and the size budget; everything else is a real option
|
|
1274
|
+
// of the `event.*` method — `toolName`, `input`, `outcome` — and is passed
|
|
1275
|
+
// straight through. Those still get truncated, because `input`, `output`,
|
|
1276
|
+
// `messages` and `content` are exactly the fields a framework fills with a
|
|
1277
|
+
// 200 KB prompt.
|
|
1278
|
+
const declared: Record<string, unknown> = {};
|
|
1279
|
+
const extras: Record<string, unknown> = {};
|
|
1280
|
+
// ONE `Cut` across both halves. A throwaway one per declared field would
|
|
1281
|
+
// set `fw_truncated` — the only machine-readable "this event lost data"
|
|
1282
|
+
// signal — when a small `fw_*` extra was cut and NOT when the prompt or the
|
|
1283
|
+
// completion was. Exactly the wrong way round: `output` is cut on
|
|
1284
|
+
// essentially every real tool loop.
|
|
1285
|
+
const cut = new Cut();
|
|
1286
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1287
|
+
if (value === undefined || value === null) continue;
|
|
1288
|
+
if (key.startsWith(FW_PREFIX) || ALLOWED_TOP_LEVEL.has(key)) {
|
|
1289
|
+
extras[key] = value;
|
|
1290
|
+
} else {
|
|
1291
|
+
declared[key] = truncateValue(value, this.fieldLimit, cut, 0);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const merged = payload(guardExtras({ ...this.baseFields, ...extras }), {
|
|
1295
|
+
limit: this.fieldLimit,
|
|
1296
|
+
budget: this.budget,
|
|
1297
|
+
cut,
|
|
1298
|
+
});
|
|
1299
|
+
// A base field named like a real option would be a duplicate key; the
|
|
1300
|
+
// explicit value wins.
|
|
1301
|
+
for (const key of Object.keys(declared)) delete merged[key];
|
|
1302
|
+
|
|
1303
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method -- called with `.call` below, so `this` is explicit
|
|
1304
|
+
const emit = runtime.event[method] as (options: Record<string, unknown>) => void;
|
|
1305
|
+
emit.call(runtime.event, {
|
|
1306
|
+
sessionId: identity.sessionId,
|
|
1307
|
+
agentId: identity.agentId,
|
|
1308
|
+
...declared,
|
|
1309
|
+
...merged,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
private resolveParent(parentKey: unknown): Identity | null {
|
|
1314
|
+
const walked = this.walk(parentKey);
|
|
1315
|
+
if (walked) return walked;
|
|
1316
|
+
// Same three steps as `identity()`, minus the exact-key lookup (a run
|
|
1317
|
+
// cannot be its own parent) and minus the warning (a root agent with no
|
|
1318
|
+
// ambient scope is normal, not a dropped event).
|
|
1319
|
+
return RunTracker.ambient(false);
|
|
1320
|
+
}
|
|
1321
|
+
}
|