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,1566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Vercel AI SDK (`ai`), majors 4 through 7.
|
|
3
|
+
*
|
|
4
|
+
* ## The mapping
|
|
5
|
+
*
|
|
6
|
+
* Python's rule (`sdk/python/skill/references/frameworks.md`): a construct is
|
|
7
|
+
* an agent if and only if it owns an LLM decision loop and has its own goal.
|
|
8
|
+
* In the AI SDK that is exactly one thing — a top-level operation.
|
|
9
|
+
*
|
|
10
|
+
* | AI SDK | FailproofAI |
|
|
11
|
+
* |----------------------------------------------------------|-----------------------------------------------|
|
|
12
|
+
* | `generateText` / `streamText` / `generateObject` / `streamObject` call — including one made by an agent class (`Experimental_Agent`, `ToolLoopAgent`) | `agent_start` / `agent_end`; `agent_id` = `functionId`, else the operation name (`ai.generateText`) — never a call or span id. An agent class's own `id` is dropped by the SDK before telemetry sees it: set `functionId` in its telemetry settings |
|
|
13
|
+
* | `embed` / `embedMany` with nothing enclosing it | its own run, like a bare `wrapModel` call: `agent_start`, a model pair per provider call, `agent_end` |
|
|
14
|
+
* | `embed` / `embedMany` inside `failproofai.agent()` or a tool | model pairs of the enclosing agent — no nested agent (it owns no decision loop) |
|
|
15
|
+
* | a model step (one provider call; a tool loop makes several) | `model_request` / `model_response`, paired on `request_id` |
|
|
16
|
+
* | a tool execution | `tool_use` / `tool_result`, with the MODEL's own `toolCallId` |
|
|
17
|
+
* | an operation inside `failproofai.agent()` / a tool | nested: `parent_id` = the enclosing agent |
|
|
18
|
+
* | a bare `wrapModel` call with no enclosing agent | its own run: `agent_start` named after the model, the model pair, `agent_end` |
|
|
19
|
+
*
|
|
20
|
+
* `model_response` always carries integer `input_tokens` / `output_tokens`, a
|
|
21
|
+
* STRING `stop_reason`, the model id and an integer `duration_ms`. Every major
|
|
22
|
+
* spells those differently — v4 `promptTokens`, v5 `inputTokens: 11`, v6+
|
|
23
|
+
* `inputTokens: { total: 11 }`; a finish reason is `"stop"` up to v5 and
|
|
24
|
+
* `{ unified: "stop", raw }` from v6 — and every spelling is read here.
|
|
25
|
+
*
|
|
26
|
+
* **A failure is recorded once, where it happened.** A model that throws closes
|
|
27
|
+
* its `model_response` with `error` and `stop_reason: "error"`; a tool that
|
|
28
|
+
* throws closes its `tool_result` with `error`. The enclosing agent then ends
|
|
29
|
+
* `failed` WITHOUT a separate `error` event — one is emitted only when the
|
|
30
|
+
* failure happened at the operation itself (schema validation, say) and no
|
|
31
|
+
* leaf carried it. v5+ hands a tool failure back to the model as a tool-error
|
|
32
|
+
* result and the loop carries on, so there the agent ends `success`: it
|
|
33
|
+
* recovered. v4 throws it out of `generateText`, so there it ends `failed`.
|
|
34
|
+
*
|
|
35
|
+
* **A stream that does not finish ends `cancelled`.** An aborted stream closes
|
|
36
|
+
* its open model call with `stop_reason: "cancelled"` on every major. A stream
|
|
37
|
+
* whose reader goes away — a client disconnecting from a route that returns
|
|
38
|
+
* `toUIMessageStreamResponse()`, a stream nobody reads, a v4–v6 provider
|
|
39
|
+
* stream that breaks mid-way — is never ended by the SDK at all; on v4–v6 the
|
|
40
|
+
* adapter closes it when its root span is garbage-collected (`fw_abandoned`).
|
|
41
|
+
* v7 reports none of these to an integration and hands it nothing to watch,
|
|
42
|
+
* so there the agent stays open (bounded by `MAX_OPEN_CALLS`) unless the call
|
|
43
|
+
* passes an `abortSignal` — the request's own, in a route handler.
|
|
44
|
+
*
|
|
45
|
+
* ## Where it attaches
|
|
46
|
+
*
|
|
47
|
+
* The AI SDK's surface is module-level functions exported from an ES module,
|
|
48
|
+
* whose namespace is immutable by specification — there is nothing to patch.
|
|
49
|
+
* So this adapter uses the extension points the SDK itself documents:
|
|
50
|
+
*
|
|
51
|
+
* * **v4–v6: an OpenTelemetry `Tracer`.** The SDK opens a span per operation
|
|
52
|
+
* (`ai.generateText`), per model step (`ai.generateText.doGenerate`) and per
|
|
53
|
+
* tool (`ai.toolCall`) on whatever tracer `experimental_telemetry.tracer`
|
|
54
|
+
* names, else on the global one. `FailproofTracer` implements just enough of
|
|
55
|
+
* the OTel `Tracer` interface to translate those spans — structurally, with
|
|
56
|
+
* no dependency on `@opentelemetry/api`.
|
|
57
|
+
* * **v7: a `Telemetry` integration.** v7 removed the `tracer` option and the
|
|
58
|
+
* OpenTelemetry dependency and replaced them with lifecycle callbacks
|
|
59
|
+
* (`onStart`, `onLanguageModelCallStart`/`End`, `onToolExecutionStart`/`End`,
|
|
60
|
+
* `onEnd`, `onError`, …) passed per call in `telemetry.integrations` or
|
|
61
|
+
* registered process-wide on `globalThis.AI_SDK_TELEMETRY_INTEGRATIONS`
|
|
62
|
+
* (which is all `registerTelemetry()` does). `integration` below is one.
|
|
63
|
+
*
|
|
64
|
+
* `telemetry()` returns BOTH — `{ isEnabled, functionId, tracer, integrations }`
|
|
65
|
+
* — so one call site works on every major: v4–v6 read `tracer`, v7 reads
|
|
66
|
+
* `integrations`. v6 also knows `integrations` (an earlier, smaller interface),
|
|
67
|
+
* but its events carry no `callId`, and the integration ignores any event
|
|
68
|
+
* without one, so v6 is recorded by the tracer alone and never twice.
|
|
69
|
+
*
|
|
70
|
+
* `instrument("ai")` registers the integration on v7's global list, so v7
|
|
71
|
+
* records EVERY call (its telemetry is on by default once an integration is
|
|
72
|
+
* registered). That list is additive and per-call `integrations` replace it,
|
|
73
|
+
* so it takes nothing from anybody else.
|
|
74
|
+
*
|
|
75
|
+
* On v4–v6 `instrument("ai")` records nothing by itself, and says so once. The
|
|
76
|
+
* process-wide hook there is the global OpenTelemetry tracer provider, a
|
|
77
|
+
* single slot that OpenTelemetry refuses to hand over once taken: registering
|
|
78
|
+
* ours would silently refuse the customer's own `NodeSDK.start()` later in
|
|
79
|
+
* startup and route their http/database spans to a tracer that exports
|
|
80
|
+
* nothing. So taking it is opt-in — `instrument("ai", { registerGlobalTracer:
|
|
81
|
+
* true })`, for a process with no OpenTelemetry of its own — and even then
|
|
82
|
+
* only when the slot is still empty. (v4–v6 consult the global tracer only for
|
|
83
|
+
* calls that pass `experimental_telemetry: { isEnabled: true }`; that is the
|
|
84
|
+
* SDK's rule, not ours.) The call-site `telemetry()` and `wrapModel()` are the
|
|
85
|
+
* recommended paths on v4–v6.
|
|
86
|
+
*
|
|
87
|
+
* * **`middleware()` / `wrapModel()`** see model calls only — tools run above
|
|
88
|
+
* the model layer. Combined with `telemetry()` or `instrument()` they defer,
|
|
89
|
+
* so each call is recorded once: the tracer marks a model span's callback,
|
|
90
|
+
* and the integration its `executeLanguageModelCall`, with an
|
|
91
|
+
* `AsyncLocalStorage` flag the middleware checks.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
95
|
+
import { randomUUID } from "node:crypto";
|
|
96
|
+
import { createRequire } from "node:module";
|
|
97
|
+
|
|
98
|
+
import { current as currentIdentity } from "../context.js";
|
|
99
|
+
import { logger } from "../logger.js";
|
|
100
|
+
import { resolveEsm, resolveFrom, tryRequire } from "../node-require.js";
|
|
101
|
+
import * as compat from "./compat.js";
|
|
102
|
+
import * as core from "./core.js";
|
|
103
|
+
import type { Adapter } from "./core.js";
|
|
104
|
+
|
|
105
|
+
const NAME = "ai";
|
|
106
|
+
const PACKAGE = "ai";
|
|
107
|
+
|
|
108
|
+
let tracker: core.RunTracker | null = null;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Set while the tracer or the integration is recording a MODEL call, so
|
|
112
|
+
* `middleware()` knows this call is already covered and stays quiet. Scoped to
|
|
113
|
+
* the model call, not the whole operation: a wrapped model called from inside
|
|
114
|
+
* a tool is a different call and must still be recorded.
|
|
115
|
+
*/
|
|
116
|
+
const recordingModelCall = new AsyncLocalStorage<true>();
|
|
117
|
+
|
|
118
|
+
function ensureTracker(options: Record<string, unknown> = {}): core.RunTracker {
|
|
119
|
+
tracker ??= new core.RunTracker(NAME, {
|
|
120
|
+
baseFields: core.frameworkFields(NAME, PACKAGE),
|
|
121
|
+
fieldLimit: typeof options.captureLimit === "number" ? options.captureLimit : undefined,
|
|
122
|
+
});
|
|
123
|
+
return tracker;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Reading every major's spelling
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
function asInt(value: unknown): number | undefined {
|
|
131
|
+
const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
132
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0 ? Math.round(n) : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** `11` (v5, v7's normalised usage) or `{ total: 11, … }` (v6+ provider usage). */
|
|
136
|
+
function tokenCount(value: unknown): number | undefined {
|
|
137
|
+
if (typeof value === "object" && value !== null) return asInt((value as { total?: unknown }).total);
|
|
138
|
+
return asInt(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Input/output tokens from any major's usage object. */
|
|
142
|
+
export function usageTokens(usage: unknown): { inputTokens?: number; outputTokens?: number } {
|
|
143
|
+
if (typeof usage !== "object" || usage === null) return {};
|
|
144
|
+
const u = usage as Record<string, unknown>;
|
|
145
|
+
return {
|
|
146
|
+
inputTokens: tokenCount(u.inputTokens ?? u.promptTokens ?? u.tokens),
|
|
147
|
+
outputTokens: tokenCount(u.outputTokens ?? u.completionTokens),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A finish reason as a string. v6 made it `{ unified, raw }`; the column is a
|
|
153
|
+
* string, and an object there is unfilterable.
|
|
154
|
+
*/
|
|
155
|
+
export function stopReasonOf(value: unknown): string | undefined {
|
|
156
|
+
if (typeof value === "string") return value || undefined;
|
|
157
|
+
if (Array.isArray(value)) return stopReasonOf(value[0]);
|
|
158
|
+
if (typeof value === "object" && value !== null) {
|
|
159
|
+
const v = value as { unified?: unknown; type?: unknown; raw?: unknown };
|
|
160
|
+
return stopReasonOf(v.unified ?? v.type ?? v.raw);
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Attribute values arrive as JSON strings; a non-JSON string is itself. */
|
|
166
|
+
function parseMaybeJson(value: unknown): unknown {
|
|
167
|
+
if (typeof value !== "string") return value;
|
|
168
|
+
const trimmed = value.trim();
|
|
169
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return value;
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(trimmed) as unknown;
|
|
172
|
+
} catch {
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** `""` is not content: a tool-call step has empty text and a real answer elsewhere. */
|
|
178
|
+
function nonEmpty(value: unknown): unknown {
|
|
179
|
+
if (value === "" || value === null || value === undefined) return undefined;
|
|
180
|
+
if (Array.isArray(value) && value.length === 0) return undefined;
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function toolCallOf(part: Record<string, unknown>): Record<string, unknown> {
|
|
185
|
+
return {
|
|
186
|
+
toolCallId: part.toolCallId,
|
|
187
|
+
toolName: part.toolName,
|
|
188
|
+
input: parseMaybeJson(part.input ?? part.args),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A step's tool calls in the one shape every path emits —
|
|
194
|
+
* `{ toolCallId, toolName, input }` with `input` parsed. The tracer reads them
|
|
195
|
+
* from `ai.response.toolCalls`, which each major spells its own way: v4
|
|
196
|
+
* `{ toolCallType, args: "<json>" }`, v5/v6 `generateText` `{ input: "<json>" }`,
|
|
197
|
+
* v5/v6 `streamText` `{ type: "tool-call", input: {…} }`. Passed through, the
|
|
198
|
+
* same call read three different ways on the dashboard, its input a JSON
|
|
199
|
+
* string inside JSON.
|
|
200
|
+
*
|
|
201
|
+
* @internal Exported for the unit tests.
|
|
202
|
+
*/
|
|
203
|
+
export function toolCallsOf(value: unknown): Array<Record<string, unknown>> | undefined {
|
|
204
|
+
const parsed = parseMaybeJson(value);
|
|
205
|
+
if (!Array.isArray(parsed)) return undefined;
|
|
206
|
+
const calls = parsed
|
|
207
|
+
.filter((call): call is Record<string, unknown> => typeof call === "object" && call !== null)
|
|
208
|
+
.map(toolCallOf);
|
|
209
|
+
return calls.length > 0 ? calls : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* What the model said: its text, or — for a step that only called tools — the
|
|
214
|
+
* calls. Reads v4's `{ text, toolCalls }` and v5+'s `content: [parts]`.
|
|
215
|
+
*/
|
|
216
|
+
export function responseContent(result: unknown): unknown {
|
|
217
|
+
if (typeof result !== "object" || result === null) return undefined;
|
|
218
|
+
const r = result as { content?: unknown; text?: unknown; toolCalls?: unknown };
|
|
219
|
+
if (Array.isArray(r.content)) {
|
|
220
|
+
const parts = r.content as Array<Record<string, unknown>>;
|
|
221
|
+
const text = parts
|
|
222
|
+
.filter((p) => p.type === "text" && typeof p.text === "string")
|
|
223
|
+
.map((p) => p.text as string)
|
|
224
|
+
.join("");
|
|
225
|
+
if (text) return text;
|
|
226
|
+
const calls = parts.filter((p) => p.type === "tool-call").map(toolCallOf);
|
|
227
|
+
return calls.length > 0 ? calls : undefined;
|
|
228
|
+
}
|
|
229
|
+
const text = nonEmpty(r.text);
|
|
230
|
+
if (text !== undefined) return text;
|
|
231
|
+
if (Array.isArray(r.toolCalls) && r.toolCalls.length > 0) {
|
|
232
|
+
return (r.toolCalls as Array<Record<string, unknown>>).map(toolCallOf);
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function errorOf(error: unknown): { type: string; message: string; stack?: string } {
|
|
238
|
+
if (error instanceof Error) {
|
|
239
|
+
return { type: error.name || "Error", message: error.message, stack: error.stack };
|
|
240
|
+
}
|
|
241
|
+
if (typeof error === "object" && error !== null && typeof (error as { message?: unknown }).message === "string") {
|
|
242
|
+
const e = error as { name?: unknown; message: string; stack?: unknown };
|
|
243
|
+
return {
|
|
244
|
+
type: typeof e.name === "string" && e.name ? e.name : "Error",
|
|
245
|
+
message: e.message,
|
|
246
|
+
stack: typeof e.stack === "string" ? e.stack : undefined,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return { type: typeof error, message: String(error) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const errorText = (error: unknown): string => {
|
|
253
|
+
const detail = errorOf(error);
|
|
254
|
+
return `${detail.type}: ${detail.message}`;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/** An operation's default agent name — stable, low-cardinality, never an id. */
|
|
258
|
+
function agentName(functionId: unknown, operation: unknown): string {
|
|
259
|
+
if (typeof functionId === "string" && functionId) return functionId;
|
|
260
|
+
return typeof operation === "string" && operation ? operation : "ai";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// v4–v6: the tracer
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
type Attributes = Record<string, unknown>;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* An OpenTelemetry attribute value. Spelled out rather than imported so the
|
|
271
|
+
* public types check against `ai`'s own `TelemetrySettings.metadata` without
|
|
272
|
+
* this package depending on `@opentelemetry/api`.
|
|
273
|
+
*/
|
|
274
|
+
export type AttributeValue =
|
|
275
|
+
| string
|
|
276
|
+
| number
|
|
277
|
+
| boolean
|
|
278
|
+
| Array<null | undefined | string>
|
|
279
|
+
| Array<null | undefined | number>
|
|
280
|
+
| Array<null | undefined | boolean>;
|
|
281
|
+
|
|
282
|
+
/** The operations the SDK opens a ROOT span for. Each becomes an agent. */
|
|
283
|
+
const ROOT_OPERATIONS = new Set([
|
|
284
|
+
"ai.generateText",
|
|
285
|
+
"ai.streamText",
|
|
286
|
+
"ai.generateObject",
|
|
287
|
+
"ai.streamObject",
|
|
288
|
+
"ai.embed",
|
|
289
|
+
"ai.embedMany",
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The embedding operations. An embedding call owns no decision loop, so it is
|
|
294
|
+
* an agent only when nothing encloses it — a bare `embed()` in an indexing
|
|
295
|
+
* script is its own run, the way a bare `wrapModel` call is. Inside an agent
|
|
296
|
+
* (a `failproofai.agent()` scope, or a tool of a traced operation) it is a
|
|
297
|
+
* model call of THAT agent: a nested `ai.embed` agent per retrieval drowned
|
|
298
|
+
* the real agents in the `agent_id` facet.
|
|
299
|
+
*/
|
|
300
|
+
const EMBED_OPERATIONS = new Set(["ai.embed", "ai.embedMany"]);
|
|
301
|
+
|
|
302
|
+
/** Whether an operation starting now has an agent above it to belong to. */
|
|
303
|
+
const enclosedByAgent = (parentKey: unknown): boolean =>
|
|
304
|
+
parentKey !== undefined || currentIdentity().agentId !== null;
|
|
305
|
+
|
|
306
|
+
/** The operations that ARE the model call. Each becomes a request/response pair. */
|
|
307
|
+
const MODEL_OPERATIONS = new Set([
|
|
308
|
+
"ai.generateText.doGenerate",
|
|
309
|
+
"ai.streamText.doStream",
|
|
310
|
+
"ai.generateObject.doGenerate",
|
|
311
|
+
"ai.streamObject.doStream",
|
|
312
|
+
"ai.embed.doEmbed",
|
|
313
|
+
"ai.embedMany.doEmbed",
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
let spanCounter = 0;
|
|
317
|
+
|
|
318
|
+
/** A model or tool span still open: what closing it takes, held apart from the span. */
|
|
319
|
+
interface OpenLeaf {
|
|
320
|
+
id: string;
|
|
321
|
+
parentId: string | undefined;
|
|
322
|
+
/** Set for a model call (its model id, possibly undefined); unset for a tool. */
|
|
323
|
+
model?: { id: string | undefined };
|
|
324
|
+
tool?: { toolName: string; toolCallId: string };
|
|
325
|
+
started: number;
|
|
326
|
+
closed: boolean;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* What an operation still owes the trace — its `agent_end` and the leaves
|
|
331
|
+
* under it that never closed. Plain data, deliberately holding no span, so a
|
|
332
|
+
* `FinalizationRegistry` can settle it once the root span itself is gone.
|
|
333
|
+
*/
|
|
334
|
+
interface Ledger {
|
|
335
|
+
id: string;
|
|
336
|
+
operation: string;
|
|
337
|
+
started: number;
|
|
338
|
+
tracker: core.RunTracker;
|
|
339
|
+
/** False for an enclosed embedding operation, which opened no agent. */
|
|
340
|
+
agent: boolean;
|
|
341
|
+
leaves: Map<string, OpenLeaf>;
|
|
342
|
+
done: boolean;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Close a leaf that will never end by itself, as cancelled. */
|
|
346
|
+
function closeLeaf(t: core.RunTracker, leaf: OpenLeaf): void {
|
|
347
|
+
leaf.closed = true;
|
|
348
|
+
if (leaf.model !== undefined) {
|
|
349
|
+
t.emit("modelResponse", leaf.id, {
|
|
350
|
+
parentKey: leaf.parentId,
|
|
351
|
+
model: leaf.model.id,
|
|
352
|
+
stopReason: "cancelled",
|
|
353
|
+
role: "assistant",
|
|
354
|
+
requestId: leaf.id,
|
|
355
|
+
...core.fwFields({ duration_ms: core.ms(Date.now() - leaf.started) }),
|
|
356
|
+
});
|
|
357
|
+
} else if (leaf.tool !== undefined) {
|
|
358
|
+
t.emit("toolResult", leaf.id, {
|
|
359
|
+
parentKey: leaf.parentId,
|
|
360
|
+
...leaf.tool,
|
|
361
|
+
error: "cancelled: the operation ended before the tool returned",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
t.unlink(leaf.id);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Close every leaf still open under an operation; whether there were any. */
|
|
368
|
+
function settleLeaves(t: core.RunTracker, ledger: Ledger): boolean {
|
|
369
|
+
const leaves = [...ledger.leaves.values()];
|
|
370
|
+
ledger.leaves.clear();
|
|
371
|
+
for (const leaf of leaves) closeLeaf(t, leaf);
|
|
372
|
+
return leaves.length > 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Operations whose root span was garbage-collected before anything ended it.
|
|
377
|
+
*
|
|
378
|
+
* The SDK ends a stream's root span from its result stream's `flush()`, and a
|
|
379
|
+
* `TransformStream` never flushes when its reader cancels or its source
|
|
380
|
+
* errors. So a client that disconnects from a route returning
|
|
381
|
+
* `toUIMessageStreamResponse()`, a stream nobody reads, and (on v4) a provider
|
|
382
|
+
* stream that breaks mid-way all leave the root span open forever — for the
|
|
383
|
+
* SDK's own OpenTelemetry exporters as much as for this adapter — and the
|
|
384
|
+
* agent reads as still running. Once nothing can reach the root span nothing
|
|
385
|
+
* can end it either, so that is exactly when it is safe to say it never will:
|
|
386
|
+
* the agent ends `cancelled`, marked `fw_abandoned`.
|
|
387
|
+
*/
|
|
388
|
+
const abandonedOperations =
|
|
389
|
+
typeof FinalizationRegistry === "function"
|
|
390
|
+
? new FinalizationRegistry<Ledger>((ledger) => {
|
|
391
|
+
if (ledger.done) return;
|
|
392
|
+
ledger.done = true;
|
|
393
|
+
const t = tracker;
|
|
394
|
+
// A tracker since replaced (uninstall, reinstall) no longer owns this run.
|
|
395
|
+
if (t === null || t !== ledger.tracker) return;
|
|
396
|
+
core.callSafely(
|
|
397
|
+
() => {
|
|
398
|
+
settleLeaves(t, ledger);
|
|
399
|
+
if (ledger.agent) {
|
|
400
|
+
t.endAgent(ledger.id, {
|
|
401
|
+
outcome: "cancelled",
|
|
402
|
+
...core.fwFields({
|
|
403
|
+
operation: ledger.operation,
|
|
404
|
+
abandoned: true,
|
|
405
|
+
duration_ms: core.ms(Date.now() - ledger.started),
|
|
406
|
+
}),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
t.unlink(ledger.id);
|
|
410
|
+
},
|
|
411
|
+
[],
|
|
412
|
+
`${NAME}.span.abandoned`,
|
|
413
|
+
);
|
|
414
|
+
})
|
|
415
|
+
: null;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* One OpenTelemetry span, translated as it opens and as it ends.
|
|
419
|
+
*
|
|
420
|
+
* The START events are emitted when the span OPENS, not when it ends: the SDK
|
|
421
|
+
* sets a model span's prompt and a tool span's arguments as start attributes,
|
|
422
|
+
* and in a stream the tool a model asked for runs while the model's span is
|
|
423
|
+
* still open — emitting the request at end time put it after the tool it
|
|
424
|
+
* caused.
|
|
425
|
+
*/
|
|
426
|
+
export class FailproofSpan {
|
|
427
|
+
readonly attributes: Attributes = {};
|
|
428
|
+
/** The ROOT span of this operation, where "a leaf already reported the failure" is kept. */
|
|
429
|
+
readonly root: FailproofSpan | undefined;
|
|
430
|
+
private spanName: string;
|
|
431
|
+
private readonly id: string;
|
|
432
|
+
private readonly parentId: string | undefined;
|
|
433
|
+
private readonly started = Date.now();
|
|
434
|
+
private ended = false;
|
|
435
|
+
private failure: unknown = undefined;
|
|
436
|
+
private leafFailed = false;
|
|
437
|
+
/** An enclosed embedding operation: its model calls belong to the agent above, it is no agent itself. */
|
|
438
|
+
private passThrough = false;
|
|
439
|
+
/**
|
|
440
|
+
* On a root: what the operation still owes. The SDK does not end a model
|
|
441
|
+
* span when a stream is aborted — only the root — so the root closes
|
|
442
|
+
* whatever is left when it ends, and the ledger survives the span for the
|
|
443
|
+
* case where nothing ends the root at all.
|
|
444
|
+
*/
|
|
445
|
+
private ledger: Ledger | undefined;
|
|
446
|
+
/** On a model or tool span: its entry in the root's ledger. */
|
|
447
|
+
private leaf: OpenLeaf | undefined;
|
|
448
|
+
|
|
449
|
+
constructor(spanName: string, parent: FailproofSpan | undefined, attributes: Attributes = {}) {
|
|
450
|
+
this.spanName = spanName;
|
|
451
|
+
spanCounter += 1;
|
|
452
|
+
this.id = `ai-${String(spanCounter)}-${randomUUID().slice(0, 8)}`;
|
|
453
|
+
this.parentId = parent?.spanId;
|
|
454
|
+
Object.assign(this.attributes, attributes);
|
|
455
|
+
this.root = ROOT_OPERATIONS.has(this.operation()) && !parent?.root ? this : parent?.root;
|
|
456
|
+
core.callSafely(() => {
|
|
457
|
+
this.open();
|
|
458
|
+
}, [], `${NAME}.span.start`);
|
|
459
|
+
core.callSafely(() => {
|
|
460
|
+
this.track();
|
|
461
|
+
}, [], `${NAME}.span.track`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Enter this span in its operation's ledger (or, for a root, open one). */
|
|
465
|
+
private track(): void {
|
|
466
|
+
const t = tracker;
|
|
467
|
+
if (t === null || this.root === undefined) return;
|
|
468
|
+
if (this.root === this) {
|
|
469
|
+
this.ledger = {
|
|
470
|
+
id: this.id,
|
|
471
|
+
operation: this.operation(),
|
|
472
|
+
started: this.started,
|
|
473
|
+
tracker: t,
|
|
474
|
+
agent: !this.passThrough,
|
|
475
|
+
leaves: new Map(),
|
|
476
|
+
done: false,
|
|
477
|
+
};
|
|
478
|
+
abandonedOperations?.register(this, this.ledger, this.ledger);
|
|
479
|
+
} else if (this.isLeaf && this.root.ledger !== undefined) {
|
|
480
|
+
this.leaf = {
|
|
481
|
+
id: this.id,
|
|
482
|
+
parentId: this.parentId,
|
|
483
|
+
...(this.isModelCall ? { model: { id: this.model() } } : { tool: this.tool() }),
|
|
484
|
+
started: this.started,
|
|
485
|
+
closed: false,
|
|
486
|
+
};
|
|
487
|
+
this.root.ledger.leaves.set(this.id, this.leaf);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** A model or tool span: one that opens a request/tool pair it must close. */
|
|
492
|
+
private get isLeaf(): boolean {
|
|
493
|
+
return this.isModelCall || this.operation() === "ai.toolCall";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
get spanId(): string {
|
|
497
|
+
return this.id;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Whether this span is the model call itself. */
|
|
501
|
+
get isModelCall(): boolean {
|
|
502
|
+
return MODEL_OPERATIONS.has(this.operation());
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private operation(): string {
|
|
506
|
+
const id = this.attributes["ai.operationId"];
|
|
507
|
+
return typeof id === "string" ? id : this.spanName;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private open(): void {
|
|
511
|
+
const t = tracker;
|
|
512
|
+
if (t === null) return;
|
|
513
|
+
const a = this.attributes;
|
|
514
|
+
const operation = this.operation();
|
|
515
|
+
if (EMBED_OPERATIONS.has(operation) && enclosedByAgent(this.parentId)) {
|
|
516
|
+
this.passThrough = true;
|
|
517
|
+
t.link(this.id, this.parentId);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (ROOT_OPERATIONS.has(operation)) {
|
|
521
|
+
t.startAgent(this.id, {
|
|
522
|
+
agentId: agentName(a["ai.telemetry.functionId"], operation),
|
|
523
|
+
parentKey: this.parentId,
|
|
524
|
+
...core.fwFields({ operation }),
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (MODEL_OPERATIONS.has(operation)) {
|
|
529
|
+
const messages = parseMaybeJson(a["ai.prompt.messages"] ?? a["ai.prompt"]);
|
|
530
|
+
const tools = Array.isArray(a["ai.prompt.tools"])
|
|
531
|
+
? (a["ai.prompt.tools"] as unknown[]).map(parseMaybeJson)
|
|
532
|
+
: undefined;
|
|
533
|
+
t.emit("modelRequest", this.id, {
|
|
534
|
+
parentKey: this.parentId,
|
|
535
|
+
model: this.model(),
|
|
536
|
+
messages: Array.isArray(messages) ? (messages as Array<Record<string, unknown>>) : undefined,
|
|
537
|
+
tools: tools as Array<Record<string, unknown>> | undefined,
|
|
538
|
+
requestId: this.id,
|
|
539
|
+
...core.fwFields({
|
|
540
|
+
provider: a["ai.model.provider"] ?? a["gen_ai.system"],
|
|
541
|
+
operation,
|
|
542
|
+
prompt: Array.isArray(messages) ? undefined : messages,
|
|
543
|
+
}),
|
|
544
|
+
});
|
|
545
|
+
} else if (operation === "ai.toolCall") {
|
|
546
|
+
const { toolName, toolCallId } = this.tool();
|
|
547
|
+
t.emit("toolUse", this.id, {
|
|
548
|
+
parentKey: this.parentId,
|
|
549
|
+
toolName,
|
|
550
|
+
toolCallId,
|
|
551
|
+
input: parseMaybeJson(a["ai.toolCall.args"] ?? a["ai.toolCall.input"]) ?? undefined,
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
// Any other span still has to lead its children to the agent above it.
|
|
555
|
+
t.link(this.id, this.parentId);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private model(): string | undefined {
|
|
560
|
+
const a = this.attributes;
|
|
561
|
+
const id = a["ai.model.id"] ?? a["gen_ai.request.model"] ?? a["ai.response.model"];
|
|
562
|
+
return typeof id === "string" ? id : undefined;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private tool(): { toolName: string; toolCallId: string } {
|
|
566
|
+
const a = this.attributes;
|
|
567
|
+
return {
|
|
568
|
+
toolName: typeof a["ai.toolCall.name"] === "string" ? a["ai.toolCall.name"] : "tool",
|
|
569
|
+
toolCallId: typeof a["ai.toolCall.id"] === "string" ? a["ai.toolCall.id"] : this.id,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
setAttribute(key: string, value: unknown): this {
|
|
574
|
+
this.attributes[key] = value;
|
|
575
|
+
return this;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
setAttributes(attributes: Attributes): this {
|
|
579
|
+
Object.assign(this.attributes, attributes);
|
|
580
|
+
return this;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
addEvent(): this {
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
addLink(): this {
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
addLinks(): this {
|
|
592
|
+
return this;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
setStatus(status: { code?: number; message?: string }): this {
|
|
596
|
+
// OpenTelemetry's SpanStatusCode.ERROR is 2.
|
|
597
|
+
if (status?.code === 2 && this.failure === undefined) {
|
|
598
|
+
this.failure = new Error(status.message ?? "span reported an error status");
|
|
599
|
+
}
|
|
600
|
+
return this;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
recordException(error: unknown): void {
|
|
604
|
+
this.failure = error;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
updateName(name: string): this {
|
|
608
|
+
this.spanName = name;
|
|
609
|
+
return this;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
isRecording(): boolean {
|
|
613
|
+
return !this.ended;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
spanContext(): { traceId: string; spanId: string; traceFlags: number } {
|
|
617
|
+
return { traceId: this.id.padEnd(32, "0").slice(0, 32), spanId: this.id.slice(0, 16), traceFlags: 1 };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
end(): void {
|
|
621
|
+
if (this.ended) return;
|
|
622
|
+
this.ended = true;
|
|
623
|
+
if (this.leaf !== undefined) {
|
|
624
|
+
// Closed as cancelled when its operation ended; a late end is not news.
|
|
625
|
+
if (this.leaf.closed) return;
|
|
626
|
+
this.root?.ledger?.leaves.delete(this.id);
|
|
627
|
+
}
|
|
628
|
+
if (this.ledger !== undefined) {
|
|
629
|
+
this.ledger.done = true;
|
|
630
|
+
abandonedOperations?.unregister(this.ledger);
|
|
631
|
+
}
|
|
632
|
+
const t = tracker;
|
|
633
|
+
if (t === null) return;
|
|
634
|
+
core.callSafely(() => {
|
|
635
|
+
this.emitEnd(t);
|
|
636
|
+
}, [], `${NAME}.span.end`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private emitEnd(t: core.RunTracker): void {
|
|
640
|
+
const a = this.attributes;
|
|
641
|
+
const operation = this.operation();
|
|
642
|
+
const failed = this.failure !== undefined;
|
|
643
|
+
if (failed && this.root && this.root !== this) this.root.leafFailed = true;
|
|
644
|
+
|
|
645
|
+
if (MODEL_OPERATIONS.has(operation)) {
|
|
646
|
+
const toolCalls = toolCallsOf(a["ai.response.toolCalls"]);
|
|
647
|
+
t.emit("modelResponse", this.id, {
|
|
648
|
+
parentKey: this.parentId,
|
|
649
|
+
model: (typeof a["ai.response.model"] === "string" ? a["ai.response.model"] : undefined) ?? this.model(),
|
|
650
|
+
stopReason: failed
|
|
651
|
+
? "error"
|
|
652
|
+
: stopReasonOf(a["ai.response.finishReason"] ?? a["gen_ai.response.finish_reasons"]),
|
|
653
|
+
inputTokens: asInt(
|
|
654
|
+
a["ai.usage.inputTokens"] ?? a["ai.usage.promptTokens"] ?? a["ai.usage.tokens"] ?? a["gen_ai.usage.input_tokens"],
|
|
655
|
+
),
|
|
656
|
+
outputTokens: asInt(
|
|
657
|
+
a["ai.usage.outputTokens"] ?? a["ai.usage.completionTokens"] ?? a["gen_ai.usage.output_tokens"],
|
|
658
|
+
),
|
|
659
|
+
content: failed
|
|
660
|
+
? undefined
|
|
661
|
+
: (nonEmpty(a["ai.response.text"]) ?? toolCalls ?? parseMaybeJson(nonEmpty(a["ai.response.object"]))),
|
|
662
|
+
role: "assistant",
|
|
663
|
+
requestId: this.id,
|
|
664
|
+
error: failed ? errorText(this.failure) : undefined,
|
|
665
|
+
...core.fwFields({
|
|
666
|
+
duration_ms: core.ms(Date.now() - this.started),
|
|
667
|
+
tool_calls: failed ? undefined : toolCalls,
|
|
668
|
+
response_id: a["ai.response.id"],
|
|
669
|
+
}),
|
|
670
|
+
});
|
|
671
|
+
} else if (operation === "ai.toolCall") {
|
|
672
|
+
const { toolName, toolCallId } = this.tool();
|
|
673
|
+
t.emit("toolResult", this.id, {
|
|
674
|
+
parentKey: this.parentId,
|
|
675
|
+
toolName,
|
|
676
|
+
toolCallId,
|
|
677
|
+
output: failed ? undefined : parseMaybeJson(a["ai.toolCall.result"] ?? a["ai.toolCall.output"]),
|
|
678
|
+
error: failed ? errorText(this.failure) : undefined,
|
|
679
|
+
});
|
|
680
|
+
} else if (ROOT_OPERATIONS.has(operation)) {
|
|
681
|
+
const cutOff = this.ledger !== undefined && settleLeaves(t, this.ledger);
|
|
682
|
+
// The failure is already on the model_response / tool_result that raised
|
|
683
|
+
// it; a second `error` here would count it twice.
|
|
684
|
+
if (failed && !this.leafFailed) {
|
|
685
|
+
const detail = errorOf(this.failure);
|
|
686
|
+
t.emit("error", this.id, {
|
|
687
|
+
...(this.passThrough ? { parentKey: this.parentId } : {}),
|
|
688
|
+
errorType: detail.type,
|
|
689
|
+
message: detail.message,
|
|
690
|
+
traceback: detail.stack,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
if (!this.passThrough) {
|
|
694
|
+
// An aborted stream ends its root with a model call still open and no
|
|
695
|
+
// finish reason (v5/v6 end only the root; v4 the same when the
|
|
696
|
+
// provider honours the signal). It did not succeed: it was cancelled.
|
|
697
|
+
const finishReason = stopReasonOf(a["ai.response.finishReason"]);
|
|
698
|
+
const cancelled = cutOff || (operation === "ai.streamText" && finishReason === undefined);
|
|
699
|
+
t.endAgent(this.id, {
|
|
700
|
+
outcome: failed ? "failed" : cancelled ? "cancelled" : "success",
|
|
701
|
+
...core.fwFields({
|
|
702
|
+
operation,
|
|
703
|
+
duration_ms: core.ms(Date.now() - this.started),
|
|
704
|
+
finish_reason: finishReason,
|
|
705
|
+
}),
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Every span kind leaves a parent link behind (`emit` with a `parentKey`,
|
|
710
|
+
// `startAgent` under a parent, `link` for any other span). Its last event
|
|
711
|
+
// is out, so forget it: a completed span's link kept forever is a leak,
|
|
712
|
+
// and at the tracker's FIFO cap it evicts a LIVE run's links instead.
|
|
713
|
+
t.unlink(this.id);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const activeSpan = new AsyncLocalStorage<FailproofSpan>();
|
|
718
|
+
|
|
719
|
+
type SpanCallback = (span: FailproofSpan) => unknown;
|
|
720
|
+
interface SpanOptions {
|
|
721
|
+
attributes?: Attributes;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Just enough of OpenTelemetry's `Tracer` for the AI SDK — structurally
|
|
726
|
+
* assignable to it, which is what lets `experimental_telemetry.tracer` accept
|
|
727
|
+
* it with no OpenTelemetry dependency in this package.
|
|
728
|
+
*
|
|
729
|
+
* `startActiveSpan` NEVER ends the span. That is OpenTelemetry's contract, and
|
|
730
|
+
* the SDK relies on it: its stream spans are opened with `endWhenDone: false`
|
|
731
|
+
* and ended by the SDK itself when the stream finishes. Ending on the
|
|
732
|
+
* callback's promise closed a `streamText` span the moment the stream object
|
|
733
|
+
* was returned, before a single token — and dropped half the events.
|
|
734
|
+
*/
|
|
735
|
+
export class FailproofTracer {
|
|
736
|
+
startSpan(name: string, options?: SpanOptions): FailproofSpan {
|
|
737
|
+
return new FailproofSpan(name, activeSpan.getStore(), options?.attributes ?? {});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
startActiveSpan<F extends SpanCallback>(name: string, fn: F): ReturnType<F>;
|
|
741
|
+
startActiveSpan<F extends SpanCallback>(name: string, options: SpanOptions, fn: F): ReturnType<F>;
|
|
742
|
+
startActiveSpan<F extends SpanCallback>(name: string, options: SpanOptions, context: unknown, fn: F): ReturnType<F>;
|
|
743
|
+
startActiveSpan(name: string, ...rest: unknown[]): unknown {
|
|
744
|
+
const callback = rest[rest.length - 1] as SpanCallback;
|
|
745
|
+
const options = (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null ? rest[0] : {}) as SpanOptions;
|
|
746
|
+
const span = new FailproofSpan(name, activeSpan.getStore(), options.attributes ?? {});
|
|
747
|
+
const run = (): unknown => callback(span);
|
|
748
|
+
return activeSpan.run(span, () => (span.isModelCall ? recordingModelCall.run(true, run) : run()));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** What `telemetry()` returns: every major's telemetry settings at once. */
|
|
753
|
+
export interface AiTelemetry {
|
|
754
|
+
isEnabled: true;
|
|
755
|
+
functionId?: string;
|
|
756
|
+
metadata?: Record<string, AttributeValue>;
|
|
757
|
+
/** Read by ai v4–v6. */
|
|
758
|
+
tracer: FailproofTracer;
|
|
759
|
+
/** Read by ai v7. */
|
|
760
|
+
integrations: AiTelemetryIntegration[];
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* The value to hand the SDK's telemetry option, on any major:
|
|
765
|
+
*
|
|
766
|
+
* const { text } = await generateText({
|
|
767
|
+
* model,
|
|
768
|
+
* prompt,
|
|
769
|
+
* telemetry: telemetry({ functionId: "answer-question" }), // ai 4–6: experimental_telemetry
|
|
770
|
+
* });
|
|
771
|
+
*
|
|
772
|
+
* `functionId` names the agent; without one it is named after the operation
|
|
773
|
+
* (`ai.generateText`). Keep it low-cardinality — it lands in `agent_id`, the
|
|
774
|
+
* primary dashboard facet.
|
|
775
|
+
*/
|
|
776
|
+
export function telemetry(
|
|
777
|
+
options: { functionId?: string; metadata?: Record<string, AttributeValue> } = {},
|
|
778
|
+
): AiTelemetry {
|
|
779
|
+
ensureTracker();
|
|
780
|
+
return {
|
|
781
|
+
isEnabled: true,
|
|
782
|
+
...(options.functionId === undefined ? {} : { functionId: options.functionId }),
|
|
783
|
+
...(options.metadata === undefined ? {} : { metadata: options.metadata }),
|
|
784
|
+
tracer: new FailproofTracer(),
|
|
785
|
+
integrations: [integration],
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** The bare tracer, for `experimental_telemetry: { isEnabled: true, tracer }` (ai v4–v6). */
|
|
790
|
+
export function tracer(): FailproofTracer {
|
|
791
|
+
ensureTracker();
|
|
792
|
+
return new FailproofTracer();
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ---------------------------------------------------------------------------
|
|
796
|
+
// v7: the Telemetry integration
|
|
797
|
+
// ---------------------------------------------------------------------------
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* The subset of ai v7's `Telemetry` interface this adapter implements.
|
|
801
|
+
*
|
|
802
|
+
* Parameters are `unknown` so the object is assignable to v7's `Telemetry` and
|
|
803
|
+
* v6's `TelemetryIntegration` alike without naming either; every field is read
|
|
804
|
+
* defensively below.
|
|
805
|
+
*/
|
|
806
|
+
export interface AiTelemetryIntegration {
|
|
807
|
+
onStart: (event: unknown) => void;
|
|
808
|
+
onLanguageModelCallStart: (event: unknown) => void;
|
|
809
|
+
onLanguageModelCallEnd: (event: unknown) => void;
|
|
810
|
+
onObjectStepStart: (event: unknown) => void;
|
|
811
|
+
onObjectStepEnd: (event: unknown) => void;
|
|
812
|
+
onEmbedStart: (event: unknown) => void;
|
|
813
|
+
onEmbedEnd: (event: unknown) => void;
|
|
814
|
+
onToolExecutionStart: (event: unknown) => void;
|
|
815
|
+
onToolExecutionEnd: (event: unknown) => void;
|
|
816
|
+
onEnd: (event: unknown) => void;
|
|
817
|
+
onAbort: (event: unknown) => void;
|
|
818
|
+
onError: (event: unknown) => void;
|
|
819
|
+
executeLanguageModelCall: <T>(options: { execute: () => PromiseLike<T>; callId?: string }) => Promise<T>;
|
|
820
|
+
executeTool: <T>(options: { execute: () => PromiseLike<T>; callId?: string }) => Promise<T>;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
interface Call {
|
|
824
|
+
callId: string;
|
|
825
|
+
key: string;
|
|
826
|
+
operation: string;
|
|
827
|
+
/** Model calls started and not yet ended, oldest first — one at a time in practice. */
|
|
828
|
+
pending: Array<{ id: string; started: number; model: string | undefined }>;
|
|
829
|
+
/** Tool keys whose `tool_use` is out and whose `tool_result` is not. */
|
|
830
|
+
tools: Set<string>;
|
|
831
|
+
sequence: number;
|
|
832
|
+
/** A leaf already recorded the failure; the agent's end must not repeat it. */
|
|
833
|
+
leafFailed: boolean;
|
|
834
|
+
/**
|
|
835
|
+
* Whether this call is an agent. An embedding call inside one is not: its
|
|
836
|
+
* model calls are recorded on the enclosing agent and it opens no run.
|
|
837
|
+
*/
|
|
838
|
+
agent: boolean;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const MAX_OPEN_CALLS = 10_000;
|
|
842
|
+
const calls = new Map<string, Call>();
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* The agent a tool's `execute` runs under, so an operation started INSIDE a
|
|
846
|
+
* tool — a sub-agent — nests under the operation that called the tool. v7
|
|
847
|
+
* calls `executeTool` around exactly that function, which is the one place an
|
|
848
|
+
* `AsyncLocalStorage` scope covers the nested call and nothing else.
|
|
849
|
+
*/
|
|
850
|
+
const enclosingCall = new AsyncLocalStorage<string>();
|
|
851
|
+
|
|
852
|
+
type Event7 = Record<string, unknown>;
|
|
853
|
+
|
|
854
|
+
/** v7 events carry `callId`; v6's do not, and are recorded by the tracer instead. */
|
|
855
|
+
function callOf(event: unknown): Call | undefined {
|
|
856
|
+
const id = (event as Event7 | undefined)?.callId;
|
|
857
|
+
return typeof id === "string" ? calls.get(id) : undefined;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function startModel(event: Event7, fields: Record<string, unknown>): void {
|
|
861
|
+
const t = tracker;
|
|
862
|
+
const call = callOf(event);
|
|
863
|
+
if (t === null || call === undefined) return;
|
|
864
|
+
call.sequence += 1;
|
|
865
|
+
const id = `${call.key}:m${String(call.sequence)}`;
|
|
866
|
+
call.pending.push({ id, started: Date.now(), model: typeof event.modelId === "string" ? event.modelId : undefined });
|
|
867
|
+
t.emit("modelRequest", id, {
|
|
868
|
+
parentKey: call.key,
|
|
869
|
+
model: typeof event.modelId === "string" ? event.modelId : undefined,
|
|
870
|
+
requestId: id,
|
|
871
|
+
...fields,
|
|
872
|
+
...core.fwFields({ provider: event.provider, operation: call.operation }),
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function endModel(event: Event7 | undefined, call: Call, fields: Record<string, unknown>): void {
|
|
877
|
+
const t = tracker;
|
|
878
|
+
const open = call.pending.shift();
|
|
879
|
+
if (t === null || open === undefined) return;
|
|
880
|
+
const performance = event?.performance as { responseTimeMs?: unknown } | undefined;
|
|
881
|
+
t.emit("modelResponse", open.id, {
|
|
882
|
+
parentKey: call.key,
|
|
883
|
+
// An aborted or failed call is closed with no end event: keep the model it started with.
|
|
884
|
+
model: typeof event?.modelId === "string" ? event.modelId : open.model,
|
|
885
|
+
role: "assistant",
|
|
886
|
+
requestId: open.id,
|
|
887
|
+
...fields,
|
|
888
|
+
...core.fwFields({
|
|
889
|
+
duration_ms: core.ms(asInt(performance?.responseTimeMs) ?? Date.now() - open.started),
|
|
890
|
+
response_id: event?.responseId,
|
|
891
|
+
}),
|
|
892
|
+
});
|
|
893
|
+
t.unlink(open.id);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** Close the oldest open model call as failed. */
|
|
897
|
+
function failModel(call: Call, error: unknown): void {
|
|
898
|
+
if (call.pending.length === 0) return;
|
|
899
|
+
call.leafFailed = true;
|
|
900
|
+
endModel(undefined, call, { stopReason: "error", error: errorText(error) });
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function finishCall(call: Call, outcome: string, error?: unknown, fields: Record<string, unknown> = {}): void {
|
|
904
|
+
const t = tracker;
|
|
905
|
+
calls.delete(call.callId);
|
|
906
|
+
if (t === null) return;
|
|
907
|
+
while (call.pending.length > 0) {
|
|
908
|
+
// An abort closes the model call it interrupted as cancelled, like the
|
|
909
|
+
// middleware does; "incomplete" is left for an operation that ended with
|
|
910
|
+
// a call the SDK never reported back.
|
|
911
|
+
if (error === undefined) endModel(undefined, call, { stopReason: outcome === "cancelled" ? "cancelled" : "incomplete" });
|
|
912
|
+
else failModel(call, error);
|
|
913
|
+
}
|
|
914
|
+
if (error !== undefined && !call.leafFailed) {
|
|
915
|
+
const detail = errorOf(error);
|
|
916
|
+
t.emit("error", call.key, { errorType: detail.type, message: detail.message, traceback: detail.stack });
|
|
917
|
+
}
|
|
918
|
+
if (call.agent) t.endAgent(call.key, { outcome, ...fields });
|
|
919
|
+
// Forget every link this call left: its own (a nested call's), and any tool
|
|
920
|
+
// the operation abandoned mid-run (an abort, an error).
|
|
921
|
+
t.unlink(call.key);
|
|
922
|
+
for (const key of call.tools) t.unlink(key);
|
|
923
|
+
call.tools.clear();
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const safe = (site: string, fn: (event: Event7) => void) => (event: unknown): void => {
|
|
927
|
+
if (typeof event !== "object" || event === null) return;
|
|
928
|
+
core.callSafely(fn, [event], `${NAME}.${site}`);
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* One shared, stateless-per-call object: per-call state is keyed by v7's
|
|
933
|
+
* `callId`, so the same instance serves every call, the per-call
|
|
934
|
+
* `integrations` list and the global one.
|
|
935
|
+
*/
|
|
936
|
+
export const integration: AiTelemetryIntegration = {
|
|
937
|
+
onStart: safe("onStart", (event: Event7) => {
|
|
938
|
+
const callId = event?.callId;
|
|
939
|
+
if (typeof callId !== "string" || calls.has(callId)) return;
|
|
940
|
+
// The integration is exported, so it can be registered by hand
|
|
941
|
+
// (`registerTelemetry(integration)`) without telemetry() or instrument().
|
|
942
|
+
const t = ensureTracker();
|
|
943
|
+
while (calls.size >= MAX_OPEN_CALLS) {
|
|
944
|
+
const oldest = calls.keys().next();
|
|
945
|
+
if (oldest.done) break;
|
|
946
|
+
calls.delete(oldest.value);
|
|
947
|
+
}
|
|
948
|
+
const operation = typeof event.operationId === "string" ? event.operationId : "ai";
|
|
949
|
+
const key = `ai7:${callId}`;
|
|
950
|
+
const parentKey = enclosingCall.getStore();
|
|
951
|
+
const agent = !(EMBED_OPERATIONS.has(operation) && enclosedByAgent(parentKey));
|
|
952
|
+
calls.set(callId, { callId, key, operation, pending: [], tools: new Set(), sequence: 0, leafFailed: false, agent });
|
|
953
|
+
if (!agent) {
|
|
954
|
+
t.link(key, parentKey);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
t.startAgent(key, {
|
|
958
|
+
agentId: agentName(event.functionId, operation),
|
|
959
|
+
parentKey,
|
|
960
|
+
...core.fwFields({ operation, call_id: callId }),
|
|
961
|
+
});
|
|
962
|
+
}),
|
|
963
|
+
|
|
964
|
+
onLanguageModelCallStart: safe("onLanguageModelCallStart", (event: Event7) => {
|
|
965
|
+
startModel(event, {
|
|
966
|
+
messages: Array.isArray(event.messages) ? (event.messages as Array<Record<string, unknown>>) : undefined,
|
|
967
|
+
system: event.system,
|
|
968
|
+
tools: Array.isArray(event.tools) ? (event.tools as Array<Record<string, unknown>>) : undefined,
|
|
969
|
+
});
|
|
970
|
+
}),
|
|
971
|
+
|
|
972
|
+
onLanguageModelCallEnd: safe("onLanguageModelCallEnd", (event: Event7) => {
|
|
973
|
+
const call = callOf(event);
|
|
974
|
+
if (call === undefined) return;
|
|
975
|
+
const content = responseContent(event);
|
|
976
|
+
endModel(event, call, {
|
|
977
|
+
stopReason: stopReasonOf(event.finishReason),
|
|
978
|
+
...usageTokens(event.usage),
|
|
979
|
+
content,
|
|
980
|
+
...core.fwFields({ tool_calls: Array.isArray(content) ? content : undefined }),
|
|
981
|
+
});
|
|
982
|
+
}),
|
|
983
|
+
|
|
984
|
+
onObjectStepStart: safe("onObjectStepStart", (event: Event7) => {
|
|
985
|
+
startModel(event, {
|
|
986
|
+
messages: Array.isArray(event.promptMessages) ? (event.promptMessages as Array<Record<string, unknown>>) : undefined,
|
|
987
|
+
});
|
|
988
|
+
}),
|
|
989
|
+
|
|
990
|
+
onObjectStepEnd: safe("onObjectStepEnd", (event: Event7) => {
|
|
991
|
+
const call = callOf(event);
|
|
992
|
+
if (call === undefined) return;
|
|
993
|
+
endModel(event, call, {
|
|
994
|
+
stopReason: stopReasonOf(event.finishReason),
|
|
995
|
+
...usageTokens(event.usage),
|
|
996
|
+
content: parseMaybeJson(nonEmpty(event.objectText)),
|
|
997
|
+
});
|
|
998
|
+
}),
|
|
999
|
+
|
|
1000
|
+
onEmbedStart: safe("onEmbedStart", (event: Event7) => {
|
|
1001
|
+
startModel(event, { ...core.fwFields({ values: Array.isArray(event.values) ? event.values.length : undefined }) });
|
|
1002
|
+
}),
|
|
1003
|
+
|
|
1004
|
+
onEmbedEnd: safe("onEmbedEnd", (event: Event7) => {
|
|
1005
|
+
const call = callOf(event);
|
|
1006
|
+
if (call === undefined) return;
|
|
1007
|
+
endModel(event, call, { ...usageTokens(event.usage) });
|
|
1008
|
+
}),
|
|
1009
|
+
|
|
1010
|
+
onToolExecutionStart: safe("onToolExecutionStart", (event: Event7) => {
|
|
1011
|
+
const t = tracker;
|
|
1012
|
+
const call = callOf(event);
|
|
1013
|
+
const toolCall = event.toolCall as Record<string, unknown> | undefined;
|
|
1014
|
+
if (t === null || call === undefined || !toolCall) return;
|
|
1015
|
+
const toolCallId = String(toolCall.toolCallId);
|
|
1016
|
+
call.tools.add(`${call.key}:${toolCallId}`);
|
|
1017
|
+
t.emit("toolUse", `${call.key}:${toolCallId}`, {
|
|
1018
|
+
parentKey: call.key,
|
|
1019
|
+
toolName: typeof toolCall.toolName === "string" ? toolCall.toolName : "tool",
|
|
1020
|
+
toolCallId,
|
|
1021
|
+
input: parseMaybeJson(toolCall.input ?? toolCall.args) ?? undefined,
|
|
1022
|
+
});
|
|
1023
|
+
}),
|
|
1024
|
+
|
|
1025
|
+
onToolExecutionEnd: safe("onToolExecutionEnd", (event: Event7) => {
|
|
1026
|
+
const t = tracker;
|
|
1027
|
+
const call = callOf(event);
|
|
1028
|
+
const toolCall = event.toolCall as Record<string, unknown> | undefined;
|
|
1029
|
+
if (t === null || call === undefined || !toolCall) return;
|
|
1030
|
+
const toolCallId = String(toolCall.toolCallId);
|
|
1031
|
+
const output = event.toolOutput as { type?: unknown; output?: unknown; error?: unknown } | undefined;
|
|
1032
|
+
const failed = output?.type === "tool-error";
|
|
1033
|
+
if (failed) call.leafFailed = true;
|
|
1034
|
+
t.emit("toolResult", `${call.key}:${toolCallId}`, {
|
|
1035
|
+
parentKey: call.key,
|
|
1036
|
+
toolName: typeof toolCall.toolName === "string" ? toolCall.toolName : "tool",
|
|
1037
|
+
toolCallId,
|
|
1038
|
+
output: failed ? undefined : output?.output,
|
|
1039
|
+
error: failed ? errorText(output?.error) : undefined,
|
|
1040
|
+
});
|
|
1041
|
+
call.tools.delete(`${call.key}:${toolCallId}`);
|
|
1042
|
+
t.unlink(`${call.key}:${toolCallId}`);
|
|
1043
|
+
}),
|
|
1044
|
+
|
|
1045
|
+
onEnd: safe("onEnd", (event: Event7) => {
|
|
1046
|
+
const call = callOf(event);
|
|
1047
|
+
if (call === undefined) return;
|
|
1048
|
+
// streamObject reports a schema failure here rather than throwing.
|
|
1049
|
+
const failure = event.error;
|
|
1050
|
+
finishCall(call, failure === undefined ? "success" : "failed", failure, {
|
|
1051
|
+
...core.fwFields({ operation: call.operation, finish_reason: stopReasonOf(event.finishReason) }),
|
|
1052
|
+
});
|
|
1053
|
+
}),
|
|
1054
|
+
|
|
1055
|
+
onAbort: safe("onAbort", (event: Event7) => {
|
|
1056
|
+
const call = callOf(event);
|
|
1057
|
+
if (call !== undefined) finishCall(call, "cancelled");
|
|
1058
|
+
}),
|
|
1059
|
+
|
|
1060
|
+
onError: safe("onError", (event: Event7) => {
|
|
1061
|
+
const call = callOf(event);
|
|
1062
|
+
if (call === undefined) return;
|
|
1063
|
+
failModel(call, event.error);
|
|
1064
|
+
finishCall(call, "failed", event.error, { ...core.fwFields({ operation: call.operation }) });
|
|
1065
|
+
}),
|
|
1066
|
+
|
|
1067
|
+
async executeLanguageModelCall<T>(options: { execute: () => PromiseLike<T>; callId?: string }): Promise<T> {
|
|
1068
|
+
try {
|
|
1069
|
+
return await recordingModelCall.run(true, () => options.execute());
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
// A provider call that throws never reaches `onLanguageModelCallEnd`,
|
|
1072
|
+
// and a retried one starts again: close THIS attempt here, as failed.
|
|
1073
|
+
const call = callOf(options);
|
|
1074
|
+
if (call !== undefined) core.callSafely(failModel, [call, error], `${NAME}.executeLanguageModelCall`);
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
},
|
|
1078
|
+
|
|
1079
|
+
async executeTool<T>(options: { execute: () => PromiseLike<T>; callId?: string }): Promise<T> {
|
|
1080
|
+
const key = callOf(options)?.key;
|
|
1081
|
+
return key === undefined ? await options.execute() : await enclosingCall.run(key, () => options.execute());
|
|
1082
|
+
},
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
// ---------------------------------------------------------------------------
|
|
1086
|
+
// The middleware
|
|
1087
|
+
// ---------------------------------------------------------------------------
|
|
1088
|
+
|
|
1089
|
+
interface ModelLike {
|
|
1090
|
+
modelId?: string;
|
|
1091
|
+
provider?: string;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* A language-model middleware, typed structurally so it is assignable to
|
|
1096
|
+
* `LanguageModelV1Middleware` (ai 4), `V2` (ai 5), `V3` (ai 6) and v7's
|
|
1097
|
+
* `LanguageModelMiddleware` without naming any of them. The result type is a
|
|
1098
|
+
* type parameter: whatever the model's `doGenerate` resolves to is what comes
|
|
1099
|
+
* back.
|
|
1100
|
+
*/
|
|
1101
|
+
export interface AiMiddleware {
|
|
1102
|
+
/**
|
|
1103
|
+
* ai 6 requires the literal `"v3"`; ai 4, 5 and 7 do not look at it. The
|
|
1104
|
+
* wrapping behaviour is identical across the four specifications.
|
|
1105
|
+
*/
|
|
1106
|
+
readonly specificationVersion: "v3";
|
|
1107
|
+
wrapGenerate<R>(options: { doGenerate: () => PromiseLike<R>; params: unknown; model?: unknown }): Promise<R>;
|
|
1108
|
+
wrapStream<R>(options: { doStream: () => PromiseLike<R>; params: unknown; model?: unknown }): Promise<R>;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/** Where a middleware-observed call's events go: an enclosing agent, or a run of its own. */
|
|
1112
|
+
interface Placement {
|
|
1113
|
+
requestId: string;
|
|
1114
|
+
run: string | null;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Open the model call. With an enclosing `failproofai.agent()` the call is one
|
|
1119
|
+
* step of that agent; without one it is its own run — `agent_start` named after
|
|
1120
|
+
* the model — the same answer the LangChain adapter gives a bare chat-model
|
|
1121
|
+
* call. Dropping it (no session) or pinning it to a phantom `main` agent with
|
|
1122
|
+
* no `agent_start` were the two previous answers, and both lost it.
|
|
1123
|
+
*/
|
|
1124
|
+
function openModelCall(t: core.RunTracker, params: Record<string, unknown>, model: ModelLike | undefined, streaming: boolean): Placement {
|
|
1125
|
+
const requestId = randomUUID();
|
|
1126
|
+
let run: string | null = null;
|
|
1127
|
+
if (currentIdentity().agentId === null) {
|
|
1128
|
+
run = `ai-model:${requestId}`;
|
|
1129
|
+
t.startAgent(run, {
|
|
1130
|
+
agentId: typeof model?.modelId === "string" && model.modelId ? model.modelId : "model",
|
|
1131
|
+
...core.fwFields({ provider: model?.provider }),
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
const prompt = params.prompt;
|
|
1135
|
+
t.emit("modelRequest", requestId, {
|
|
1136
|
+
parentKey: run ?? undefined,
|
|
1137
|
+
model: model?.modelId,
|
|
1138
|
+
messages: Array.isArray(prompt) ? (prompt as Array<Record<string, unknown>>) : undefined,
|
|
1139
|
+
tools: Array.isArray(params.tools) ? (params.tools as Array<Record<string, unknown>>) : undefined,
|
|
1140
|
+
requestId,
|
|
1141
|
+
...core.fwFields({ provider: model?.provider, streaming: streaming || undefined }),
|
|
1142
|
+
});
|
|
1143
|
+
return { requestId, run };
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function closeModelCall(
|
|
1147
|
+
t: core.RunTracker,
|
|
1148
|
+
placement: Placement,
|
|
1149
|
+
model: ModelLike | undefined,
|
|
1150
|
+
started: number,
|
|
1151
|
+
fields: Record<string, unknown>,
|
|
1152
|
+
outcome: string = fields.error === undefined ? "success" : "failed",
|
|
1153
|
+
): void {
|
|
1154
|
+
t.emit("modelResponse", placement.requestId, {
|
|
1155
|
+
parentKey: placement.run ?? undefined,
|
|
1156
|
+
model: model?.modelId,
|
|
1157
|
+
role: "assistant",
|
|
1158
|
+
requestId: placement.requestId,
|
|
1159
|
+
...fields,
|
|
1160
|
+
...core.fwFields({ duration_ms: core.ms(Date.now() - started) }),
|
|
1161
|
+
});
|
|
1162
|
+
t.unlink(placement.requestId);
|
|
1163
|
+
if (placement.run !== null) t.endAgent(placement.run, { outcome });
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* A language-model middleware that records `model_request` / `model_response`.
|
|
1168
|
+
*
|
|
1169
|
+
* const model = wrapLanguageModel({ model: openai("gpt-4o"), middleware: middleware() });
|
|
1170
|
+
*
|
|
1171
|
+
* Defers when `telemetry()` / `instrument()` is already recording the call, so
|
|
1172
|
+
* combining them records each model call once.
|
|
1173
|
+
*/
|
|
1174
|
+
export function middleware(options: Record<string, unknown> = {}): AiMiddleware {
|
|
1175
|
+
const t = ensureTracker(options);
|
|
1176
|
+
|
|
1177
|
+
const record = async <R>(
|
|
1178
|
+
params: unknown,
|
|
1179
|
+
rawModel: unknown,
|
|
1180
|
+
run: () => PromiseLike<R>,
|
|
1181
|
+
streaming: boolean,
|
|
1182
|
+
): Promise<R> => {
|
|
1183
|
+
if (recordingModelCall.getStore()) return await run();
|
|
1184
|
+
const model = (typeof rawModel === "object" && rawModel !== null ? rawModel : undefined) as ModelLike | undefined;
|
|
1185
|
+
const started = Date.now();
|
|
1186
|
+
const placement = core.callSafely(
|
|
1187
|
+
openModelCall,
|
|
1188
|
+
[t, (params ?? {}) as Record<string, unknown>, model, streaming],
|
|
1189
|
+
`${NAME}.middleware.request`,
|
|
1190
|
+
);
|
|
1191
|
+
if (placement === undefined) return await run();
|
|
1192
|
+
|
|
1193
|
+
let result: R;
|
|
1194
|
+
try {
|
|
1195
|
+
result = await run();
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
core.callSafely(
|
|
1198
|
+
closeModelCall,
|
|
1199
|
+
[t, placement, model, started, { stopReason: "error", error: errorText(error) }],
|
|
1200
|
+
`${NAME}.middleware.error`,
|
|
1201
|
+
);
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (streaming) return instrumentStream(t, result, placement, model, started);
|
|
1206
|
+
core.callSafely(
|
|
1207
|
+
closeModelCall,
|
|
1208
|
+
[
|
|
1209
|
+
t,
|
|
1210
|
+
placement,
|
|
1211
|
+
model,
|
|
1212
|
+
started,
|
|
1213
|
+
{
|
|
1214
|
+
stopReason: stopReasonOf((result as { finishReason?: unknown }).finishReason),
|
|
1215
|
+
...usageTokens((result as { usage?: unknown }).usage),
|
|
1216
|
+
content: responseContent(result),
|
|
1217
|
+
},
|
|
1218
|
+
],
|
|
1219
|
+
`${NAME}.middleware.response`,
|
|
1220
|
+
);
|
|
1221
|
+
return result;
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
return {
|
|
1225
|
+
specificationVersion: "v3",
|
|
1226
|
+
wrapGenerate: ({ doGenerate, params, model }) => record(params, model, doGenerate, false),
|
|
1227
|
+
wrapStream: ({ doStream, params, model }) => record(params, model, doStream, true),
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Observe a model stream so the response event carries what was actually produced.
|
|
1233
|
+
*
|
|
1234
|
+
* A stream's usage and finish reason only exist in its FINAL part, so emitting
|
|
1235
|
+
* at `doStream` return time would record every streaming call with no tokens
|
|
1236
|
+
* and no finish reason — the fields the dashboard's cost and failure views are
|
|
1237
|
+
* built on. Every chunk passes through untouched, and the model call closes
|
|
1238
|
+
* exactly once, however the stream stops:
|
|
1239
|
+
*
|
|
1240
|
+
* * it finishes — the response carries the text, tool calls, usage and finish
|
|
1241
|
+
* reason read off the parts;
|
|
1242
|
+
* * the consumer cancels it (a client that disconnected, a `streamText` that
|
|
1243
|
+
* was aborted) — `stop_reason: "cancelled"` with whatever had arrived, and a
|
|
1244
|
+
* standalone run ends `cancelled`; the cancel is passed on to the provider's
|
|
1245
|
+
* stream so its connection is released;
|
|
1246
|
+
* * it errors — `stop_reason: "error"` with the error, and a standalone run
|
|
1247
|
+
* ends `failed`.
|
|
1248
|
+
*
|
|
1249
|
+
* `pipeThrough(new TransformStream())` saw only the first: a transformer's
|
|
1250
|
+
* `flush` never runs on a cancel or an error, so those calls stayed open
|
|
1251
|
+
* forever — no `model_response`, and no `agent_end` for a standalone run.
|
|
1252
|
+
*/
|
|
1253
|
+
function instrumentStream<R>(
|
|
1254
|
+
t: core.RunTracker,
|
|
1255
|
+
result: R,
|
|
1256
|
+
placement: Placement,
|
|
1257
|
+
model: ModelLike | undefined,
|
|
1258
|
+
started: number,
|
|
1259
|
+
): R {
|
|
1260
|
+
const value = result as { stream?: ReadableStream<unknown> };
|
|
1261
|
+
if (typeof value?.stream?.getReader !== "function" || typeof ReadableStream !== "function") {
|
|
1262
|
+
core.callSafely(closeModelCall, [t, placement, model, started, {}], `${NAME}.middleware.stream`);
|
|
1263
|
+
return result;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
let text = "";
|
|
1267
|
+
let finishReason: string | undefined;
|
|
1268
|
+
let usage: { inputTokens?: number; outputTokens?: number } = {};
|
|
1269
|
+
let failure: unknown = undefined;
|
|
1270
|
+
const toolCalls: Array<Record<string, unknown>> = [];
|
|
1271
|
+
|
|
1272
|
+
const onPart = (chunk: unknown): void => {
|
|
1273
|
+
const part = chunk as Record<string, unknown>;
|
|
1274
|
+
if (part.type === "text-delta" || part.type === "text") {
|
|
1275
|
+
// v5+ `delta`, v4 `textDelta`.
|
|
1276
|
+
const delta = part.delta ?? part.textDelta ?? part.text;
|
|
1277
|
+
if (typeof delta === "string") text += delta;
|
|
1278
|
+
} else if (part.type === "tool-call") {
|
|
1279
|
+
toolCalls.push(toolCallOf(part));
|
|
1280
|
+
} else if (part.type === "finish") {
|
|
1281
|
+
finishReason = stopReasonOf(part.finishReason);
|
|
1282
|
+
usage = usageTokens(part.usage);
|
|
1283
|
+
} else if (part.type === "error") {
|
|
1284
|
+
failure = part.error;
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
const done = (error: unknown, cancelled: boolean): void => {
|
|
1289
|
+
const produced = {
|
|
1290
|
+
content: text || (toolCalls.length > 0 ? toolCalls : undefined),
|
|
1291
|
+
...core.fwFields({ streaming: true, tool_calls: toolCalls.length > 0 ? toolCalls : undefined }),
|
|
1292
|
+
};
|
|
1293
|
+
if (cancelled) {
|
|
1294
|
+
closeModelCall(t, placement, model, started, { stopReason: "cancelled", ...usage, ...produced }, "cancelled");
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
// A thrown stream error, else an in-band `error` part.
|
|
1298
|
+
const problem = error ?? failure;
|
|
1299
|
+
const fields: Record<string, unknown> =
|
|
1300
|
+
problem === undefined
|
|
1301
|
+
? { stopReason: finishReason, ...usage, ...produced }
|
|
1302
|
+
: { stopReason: "error", error: errorText(problem), ...usage };
|
|
1303
|
+
closeModelCall(t, placement, model, started, fields);
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
return {
|
|
1307
|
+
...(result as object),
|
|
1308
|
+
stream: core.observeStream(value.stream, onPart, done, `${NAME}.middleware.stream`),
|
|
1309
|
+
} as R;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Wrap a model so every call through it is recorded.
|
|
1314
|
+
*
|
|
1315
|
+
* const model = await wrapModel(openai("gpt-4o"));
|
|
1316
|
+
*
|
|
1317
|
+
* Uses the SDK's own `wrapLanguageModel`, so the returned value is exactly
|
|
1318
|
+
* what the SDK expects.
|
|
1319
|
+
*/
|
|
1320
|
+
export async function wrapModel<T>(model: T, options: Record<string, unknown> = {}): Promise<T> {
|
|
1321
|
+
const sdk = (await compat.requireModule("ai", "npm install ai")) as {
|
|
1322
|
+
wrapLanguageModel?: (arg: { model: T; middleware: unknown }) => T;
|
|
1323
|
+
experimental_wrapLanguageModel?: (arg: { model: T; middleware: unknown }) => T;
|
|
1324
|
+
};
|
|
1325
|
+
const wrap = sdk.wrapLanguageModel ?? sdk.experimental_wrapLanguageModel;
|
|
1326
|
+
if (typeof wrap !== "function") {
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
"this version of `ai` does not export wrapLanguageModel; pass " +
|
|
1329
|
+
"`experimental_telemetry: telemetry()` instead " +
|
|
1330
|
+
'(`import { telemetry } from "@failproofai/sdk/ai"`).',
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
return wrap({ model, middleware: middleware(options) });
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Wrap one tool's `execute` so its call and result are recorded.
|
|
1338
|
+
*
|
|
1339
|
+
* Only needed when you are NOT using `telemetry()` — the SDK reports every
|
|
1340
|
+
* tool it runs, which already becomes a `tool_use`/`tool_result` pair.
|
|
1341
|
+
*/
|
|
1342
|
+
export function wrapTool<T extends { execute?: (...args: never[]) => unknown }>(
|
|
1343
|
+
toolName: string,
|
|
1344
|
+
tool: T,
|
|
1345
|
+
): T {
|
|
1346
|
+
const execute = tool.execute;
|
|
1347
|
+
if (typeof execute !== "function") return tool;
|
|
1348
|
+
const t = ensureTracker();
|
|
1349
|
+
|
|
1350
|
+
const wrapped = core.wrapCallable(
|
|
1351
|
+
execute,
|
|
1352
|
+
{
|
|
1353
|
+
before: (input: unknown, meta: unknown) => {
|
|
1354
|
+
const toolCallId =
|
|
1355
|
+
(meta as { toolCallId?: string } | undefined)?.toolCallId ?? randomUUID();
|
|
1356
|
+
t.emit("toolUse", toolCallId, {
|
|
1357
|
+
toolName,
|
|
1358
|
+
toolCallId,
|
|
1359
|
+
input: (input) ?? undefined,
|
|
1360
|
+
});
|
|
1361
|
+
return toolCallId;
|
|
1362
|
+
},
|
|
1363
|
+
after: (ctx, output) => {
|
|
1364
|
+
t.emit("toolResult", ctx, { toolName, toolCallId: String(ctx), output });
|
|
1365
|
+
},
|
|
1366
|
+
onError: (ctx, error) => {
|
|
1367
|
+
t.emit("toolResult", ctx, {
|
|
1368
|
+
toolName,
|
|
1369
|
+
toolCallId: String(ctx),
|
|
1370
|
+
error: errorText(error),
|
|
1371
|
+
});
|
|
1372
|
+
},
|
|
1373
|
+
},
|
|
1374
|
+
NAME,
|
|
1375
|
+
);
|
|
1376
|
+
return { ...tool, execute: wrapped };
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** `wrapTool` across a `{ name: tool }` record, as `generateText({ tools })` takes. */
|
|
1380
|
+
export function wrapTools<T extends Record<string, { execute?: (...args: never[]) => unknown }>>(
|
|
1381
|
+
tools: T,
|
|
1382
|
+
): T {
|
|
1383
|
+
const out: Record<string, unknown> = {};
|
|
1384
|
+
for (const [name, tool] of Object.entries(tools)) out[name] = wrapTool(name, tool);
|
|
1385
|
+
return out as T;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// ---------------------------------------------------------------------------
|
|
1389
|
+
// instrument("ai")
|
|
1390
|
+
// ---------------------------------------------------------------------------
|
|
1391
|
+
|
|
1392
|
+
/** The global v7 reads; `registerTelemetry()` only ever pushes onto it. */
|
|
1393
|
+
const GLOBAL_INTEGRATIONS = "AI_SDK_TELEMETRY_INTEGRATIONS";
|
|
1394
|
+
|
|
1395
|
+
function globalIntegrations(): unknown[] | undefined {
|
|
1396
|
+
const value = (globalThis as Record<string, unknown>)[GLOBAL_INTEGRATIONS];
|
|
1397
|
+
return Array.isArray(value) ? value : undefined;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function registerIntegration(): void {
|
|
1401
|
+
const list = globalIntegrations();
|
|
1402
|
+
if (list === undefined) {
|
|
1403
|
+
(globalThis as Record<string, unknown>)[GLOBAL_INTEGRATIONS] = [integration];
|
|
1404
|
+
} else if (!list.includes(integration)) {
|
|
1405
|
+
list.push(integration);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function unregisterIntegration(): void {
|
|
1410
|
+
const list = globalIntegrations();
|
|
1411
|
+
if (list === undefined) return;
|
|
1412
|
+
const index = list.indexOf(integration);
|
|
1413
|
+
if (index !== -1) list.splice(index, 1);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
interface OtelApi {
|
|
1417
|
+
trace: {
|
|
1418
|
+
setGlobalTracerProvider(provider: unknown): boolean;
|
|
1419
|
+
getTracerProvider(): unknown;
|
|
1420
|
+
disable(): void;
|
|
1421
|
+
};
|
|
1422
|
+
ProxyTracerProvider?: new () => { getDelegate(): unknown };
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const provider = { getTracer: (): FailproofTracer => sharedTracer };
|
|
1426
|
+
const sharedTracer = new FailproofTracer();
|
|
1427
|
+
let registeredWith: OtelApi | null = null;
|
|
1428
|
+
|
|
1429
|
+
/**
|
|
1430
|
+
* `@opentelemetry/api` as the AI SDK sees it: resolved from `ai`'s own
|
|
1431
|
+
* location first — under pnpm the application cannot see a transitive
|
|
1432
|
+
* dependency, and the global registry is shared between copies anyway — then
|
|
1433
|
+
* from the application.
|
|
1434
|
+
*/
|
|
1435
|
+
function loadOtel(): OtelApi | null {
|
|
1436
|
+
for (const anchor of [resolveFrom(PACKAGE), resolveEsm(PACKAGE)]) {
|
|
1437
|
+
if (anchor === null) continue;
|
|
1438
|
+
try {
|
|
1439
|
+
return createRequire(anchor)("@opentelemetry/api") as OtelApi;
|
|
1440
|
+
} catch {
|
|
1441
|
+
// Not beside `ai` (v7 dropped the dependency); try the next anchor.
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return tryRequire<OtelApi>("@opentelemetry/api");
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/** Whether the "instrument('ai') does not cover v4–v6 by itself" note has been logged. */
|
|
1448
|
+
let advised = false;
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Register our tracer as the process-wide OpenTelemetry tracer (ai v4–v6).
|
|
1452
|
+
* Only ever on `instrument("ai", { registerGlobalTracer: true })`.
|
|
1453
|
+
*
|
|
1454
|
+
* Only when no provider is registered yet: taking over a customer's own
|
|
1455
|
+
* tracing is the opposite of what an observability library should do to
|
|
1456
|
+
* somebody else's observability. `getTracerProvider()` ALWAYS returns the
|
|
1457
|
+
* API's proxy, so the question is asked of the proxy's DELEGATE — a no-op
|
|
1458
|
+
* provider until somebody registers one.
|
|
1459
|
+
*
|
|
1460
|
+
* "Not yet" is not "never", which is why this is opt-in: a provider the
|
|
1461
|
+
* customer registers AFTER this call is refused by OpenTelemetry ("duplicate
|
|
1462
|
+
* registration"), and theirs is the one that exports. Composing instead —
|
|
1463
|
+
* handing their provider our spans too — is not possible from here: the
|
|
1464
|
+
* registration that would have to be wrapped has not happened yet, and one
|
|
1465
|
+
* that already has is held by instrumentations as a cached delegate that a
|
|
1466
|
+
* re-registration does not reach.
|
|
1467
|
+
*/
|
|
1468
|
+
function registerGlobalTracer(): "registered" | "absent" | "taken" {
|
|
1469
|
+
const api = loadOtel();
|
|
1470
|
+
if (api === null || typeof api.trace?.setGlobalTracerProvider !== "function") return "absent";
|
|
1471
|
+
const current = api.trace.getTracerProvider() as { getDelegate?: () => unknown } | undefined;
|
|
1472
|
+
const delegate = typeof current?.getDelegate === "function" ? current.getDelegate() : current;
|
|
1473
|
+
if (delegate === provider) {
|
|
1474
|
+
registeredWith = api;
|
|
1475
|
+
return "registered";
|
|
1476
|
+
}
|
|
1477
|
+
const noop = api.ProxyTracerProvider ? (new api.ProxyTracerProvider().getDelegate() as object) : undefined;
|
|
1478
|
+
if (noop !== undefined && (delegate as object | undefined)?.constructor !== noop.constructor) return "taken";
|
|
1479
|
+
if (!api.trace.setGlobalTracerProvider(provider)) return "taken";
|
|
1480
|
+
registeredWith = api;
|
|
1481
|
+
return "registered";
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
export const adapter: Adapter = {
|
|
1485
|
+
name: NAME,
|
|
1486
|
+
|
|
1487
|
+
install(options: Record<string, unknown> = {}): void {
|
|
1488
|
+
compat.checkVersion(NAME, PACKAGE, {
|
|
1489
|
+
minimum: "4.0.0",
|
|
1490
|
+
below: "8.0.0",
|
|
1491
|
+
reason: "the tracer spans, telemetry integration and middleware below are the v4–v7 shapes",
|
|
1492
|
+
});
|
|
1493
|
+
ensureTracker(options);
|
|
1494
|
+
|
|
1495
|
+
// v7: harmless on older majors, whose events carry no callId. The global
|
|
1496
|
+
// list is additive — v7 dispatches every event to every integration on it,
|
|
1497
|
+
// and a per-call `integrations` option replaces the list for that call —
|
|
1498
|
+
// so registering ours takes nothing from anybody else's.
|
|
1499
|
+
registerIntegration();
|
|
1500
|
+
|
|
1501
|
+
const major = compat.versionTuple(PACKAGE)?.[0];
|
|
1502
|
+
if (major !== undefined && major >= 7) return;
|
|
1503
|
+
|
|
1504
|
+
// v4–v6 read spans from the ONE process-wide OpenTelemetry tracer
|
|
1505
|
+
// provider, and OpenTelemetry refuses every registration after the first.
|
|
1506
|
+
// Taking that slot here would silently refuse the customer's own
|
|
1507
|
+
// `NodeSDK.start()` later on, and send their http/database/framework spans
|
|
1508
|
+
// to a tracer that exports nothing. So it is opt-in, never the default.
|
|
1509
|
+
if (options.registerGlobalTracer !== true) {
|
|
1510
|
+
if (options.registerGlobalTracer === undefined && major !== undefined && !advised) {
|
|
1511
|
+
advised = true;
|
|
1512
|
+
logger.warn(
|
|
1513
|
+
`instrument("ai") on ai ${String(major)}.x does not register a global OpenTelemetry ` +
|
|
1514
|
+
"tracer — that slot belongs to your own tracing — so by itself it records nothing " +
|
|
1515
|
+
"on ai 4–6 (it covers ai 7). Record calls at the call site with " +
|
|
1516
|
+
"`experimental_telemetry: telemetry()`, or wrap the model once with " +
|
|
1517
|
+
"`await wrapModel(model)` — both `import { telemetry, wrapModel } from " +
|
|
1518
|
+
'"@failproofai/sdk/ai"`. If this process runs no OpenTelemetry of its own, ' +
|
|
1519
|
+
'`instrument("ai", { registerGlobalTracer: true })` records every call that passes ' +
|
|
1520
|
+
"`experimental_telemetry: { isEnabled: true }`. Pass `registerGlobalTracer: false` " +
|
|
1521
|
+
"to silence this.",
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
const outcome = registerGlobalTracer();
|
|
1527
|
+
if (outcome === "registered" || major === undefined) return;
|
|
1528
|
+
compat.warn(
|
|
1529
|
+
(outcome === "taken"
|
|
1530
|
+
? "an OpenTelemetry tracer provider is already registered, so the `ai` adapter left it alone. "
|
|
1531
|
+
: "`@opentelemetry/api` is not importable, so the `ai` adapter cannot register a global tracer. ") +
|
|
1532
|
+
'Add telemetry at the call site (import { telemetry, wrapModel } from "@failproofai/sdk/ai") —\n' +
|
|
1533
|
+
" experimental_telemetry: telemetry()\n" +
|
|
1534
|
+
"or wrap the model once —\n" +
|
|
1535
|
+
" const model = await wrapModel(openai('gpt-4o'))",
|
|
1536
|
+
`${NAME}:global-tracer`,
|
|
1537
|
+
);
|
|
1538
|
+
},
|
|
1539
|
+
|
|
1540
|
+
uninstall(): void {
|
|
1541
|
+
unregisterIntegration();
|
|
1542
|
+
if (registeredWith !== null) {
|
|
1543
|
+
try {
|
|
1544
|
+
registeredWith.trace.disable();
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
logger.debug(`could not unregister the OpenTelemetry tracer provider: ${String(error)}`);
|
|
1547
|
+
}
|
|
1548
|
+
registeredWith = null;
|
|
1549
|
+
}
|
|
1550
|
+
advised = false;
|
|
1551
|
+
tracker?.closeOpenAgents();
|
|
1552
|
+
tracker?.reset();
|
|
1553
|
+
tracker = null;
|
|
1554
|
+
calls.clear();
|
|
1555
|
+
},
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* The adapter's bookkeeping, for this package's own unit tests.
|
|
1560
|
+
*
|
|
1561
|
+
* @internal Not part of the public API.
|
|
1562
|
+
*/
|
|
1563
|
+
export const _internals = {
|
|
1564
|
+
tracker: (): core.RunTracker | null => tracker,
|
|
1565
|
+
openCalls: (): number => calls.size,
|
|
1566
|
+
};
|