failproofai 1.0.1 → 1.0.2-beta.0
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 +6 -6
- 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 +2 -2
- 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 +2 -2
- 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 +3 -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 +5 -4
- 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 +2 -2
- 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 +4 -3
- 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 +4 -4
- 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 +4 -4
- 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 +2 -2
- 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 +2 -2
- 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 +66 -22
- package/.next/standalone/.next/server/app/policies/page.js +5 -4
- 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 +2 -2
- 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 +2 -2
- 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 +2 -2
- 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 +4 -4
- package/.next/standalone/.next/server/app/settings/page.js +1 -1
- 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]__1qzqcio._.js → [externals]__0e32-7c._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[externals]__1s462yl._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
- package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -6
- package/.next/standalone/.next/server/chunks/lib_telemetry-id_ts_1k8gj9o._.js +3 -0
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_09d5c79._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_04op_ou._.js} +1 -1
- package/.next/standalone/.next/server/chunks/{node_modules_posthog-node_dist_entrypoints_index_node_mjs_1d2g73o._.js → node_modules_posthog-node_dist_entrypoints_index_node_mjs_0dr20uz._.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_0xbw3bu._.js +12 -0
- package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
- 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]__0spkm68._.js → [root-of-the-server]__07gm3zl._.js} +21 -20
- 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]__0oa1lav._.js +1 -1
- 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]__0s740oi._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07tbbf3._.js → [root-of-the-server]__0zig5sv._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{_0lvyuv4._.js → _00e9tnf._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _12l4a_k._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +13 -13
- 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 +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_hooks_08o7bqo._.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/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware.js +1 -1
- 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 +71 -27
- package/.next/standalone/.next/static/chunks/{37fcpc36x9gpe.js → 0eqky-7n7dgdm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 0sa9v5mhc8861.js} +13 -13
- package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
- package/.next/standalone/.next/static/chunks/{3k7mxbp9hiom0.js → 1nuqytbta44af.js} +1 -1
- package/.next/standalone/.next/static/chunks/1sl2xmn9inx3y.js +6 -0
- package/.next/standalone/.next/static/chunks/{2_2lhuojq2xop.js → 2aawsjld9ied_.js} +1 -1
- package/.next/standalone/.next/static/chunks/2gn7ut4svofm2.js +1 -0
- package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2hs7ycvsav7v2.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 2i2yt3hzjredm.js} +1 -1
- package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 35xg6retpe6rp.js} +1 -1
- package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 3y54nw0i7dsjl.js} +1 -1
- package/.next/standalone/SECURITY.md +5 -4
- package/.next/standalone/app/actions/get-hooks-config.ts +108 -17
- package/.next/standalone/app/actions/pack-actions.ts +153 -0
- package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
- package/.next/standalone/app/audit/_components/share-templates.ts +13 -13
- package/.next/standalone/app/policies/hooks-client.tsx +320 -14
- package/.next/standalone/components/navbar.tsx +2 -28
- package/.next/standalone/components/reach-developers.tsx +8 -1
- package/.next/standalone/docs-old/agenteye/python-sdk-skill.mdx +1 -1
- package/.next/standalone/docs-old/agenteye/python-sdk.mdx +59 -35
- package/.next/standalone/docs-old/i18n/README.ar.md +1 -0
- package/.next/standalone/docs-old/i18n/README.de.md +1 -0
- package/.next/standalone/docs-old/i18n/README.es.md +1 -0
- package/.next/standalone/docs-old/i18n/README.fr.md +1 -0
- package/.next/standalone/docs-old/i18n/README.he.md +1 -0
- package/.next/standalone/docs-old/i18n/README.hi.md +1 -0
- package/.next/standalone/docs-old/i18n/README.it.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ja.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ko.md +1 -0
- package/.next/standalone/docs-old/i18n/README.pt-br.md +1 -0
- package/.next/standalone/docs-old/i18n/README.ru.md +1 -0
- package/.next/standalone/docs-old/i18n/README.tr.md +1 -0
- package/.next/standalone/docs-old/i18n/README.vi.md +1 -0
- package/.next/standalone/docs-old/i18n/README.zh.md +1 -0
- package/.next/standalone/fp-cloud-cli/CHANGELOG.md +431 -0
- package/.next/standalone/fp-cloud-cli/LICENSE +42 -0
- package/.next/standalone/fp-cloud-cli/MANIFEST.in +10 -0
- package/.next/standalone/fp-cloud-cli/README.md +259 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/__init__.py +10 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/__main__.py +4 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +64 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_context.py +332 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics.py +432 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics_config.py +77 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/analytics_registry.py +83 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/app.py +492 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +160 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1694 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/__init__.py +0 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +214 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +407 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +445 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +1054 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/auth_cmds.py +512 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/errors_cmds.py +190 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/evals_cmds.py +161 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +159 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +416 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/guardrails_cmds.py +148 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +472 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +407 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +63 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +319 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +499 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +378 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +151 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +150 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +35 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +404 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/config.py +330 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/dates.py +78 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +345 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +98 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/models.py +891 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/orgs.py +30 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/output.py +6593 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +208 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +290 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/py.typed +0 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/select.py +322 -0
- package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +53 -0
- package/.next/standalone/fp-cloud-cli/pyproject.toml +106 -0
- package/.next/standalone/fp-cloud-cli/skill/SKILL.md +302 -0
- package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +8 -0
- package/.next/standalone/fp-cloud-cli/skill/references/commands.md +254 -0
- package/.next/standalone/fp-cloud-cli/tests/__init__.py +0 -0
- package/.next/standalone/fp-cloud-cli/tests/conftest.py +92 -0
- package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +644 -0
- package/.next/standalone/fp-cloud-cli/tests/test_analytics.py +385 -0
- package/.next/standalone/fp-cloud-cli/tests/test_audits.py +950 -0
- package/.next/standalone/fp-cloud-cli/tests/test_auth.py +98 -0
- package/.next/standalone/fp-cloud-cli/tests/test_auth_mode.py +272 -0
- package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +118 -0
- package/.next/standalone/fp-cloud-cli/tests/test_client.py +299 -0
- package/.next/standalone/fp-cloud-cli/tests/test_commands.py +814 -0
- package/.next/standalone/fp-cloud-cli/tests/test_config.py +99 -0
- package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +340 -0
- package/.next/standalone/fp-cloud-cli/tests/test_dates.py +76 -0
- package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +481 -0
- package/.next/standalone/fp-cloud-cli/tests/test_facets.py +170 -0
- package/.next/standalone/fp-cloud-cli/tests/test_failproofai_home.py +722 -0
- package/.next/standalone/fp-cloud-cli/tests/test_fp_home_contract.py +198 -0
- package/.next/standalone/fp-cloud-cli/tests/test_hardening.py +186 -0
- package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +127 -0
- package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +496 -0
- package/.next/standalone/fp-cloud-cli/tests/test_list.py +59 -0
- package/.next/standalone/fp-cloud-cli/tests/test_multivalue.py +183 -0
- package/.next/standalone/fp-cloud-cli/tests/test_no_customer_identifiers.py +168 -0
- package/.next/standalone/fp-cloud-cli/tests/test_operator.py +492 -0
- package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +692 -0
- package/.next/standalone/fp-cloud-cli/tests/test_output.py +1732 -0
- package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +191 -0
- package/.next/standalone/fp-cloud-cli/tests/test_readme_matches_reality.py +125 -0
- package/.next/standalone/fp-cloud-cli/tests/test_review_fixes.py +161 -0
- package/.next/standalone/fp-cloud-cli/tests/test_telemetry_completeness.py +90 -0
- package/.next/standalone/fp-cloud-cli/tests/test_usage.py +115 -0
- package/.next/standalone/fp-cloud-cli/tests/test_v1_origin_diagnostic.py +70 -0
- package/.next/standalone/fp-cloud-cli/tests/test_v1_routing.py +214 -0
- package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +104 -0
- package/.next/standalone/fp-cloud-cli/uv.lock +552 -0
- package/.next/standalone/package.json +29 -22
- package/.next/standalone/sdk/python/CHANGELOG.md +70 -0
- package/.next/standalone/sdk/python/LICENSE +42 -0
- package/.next/standalone/sdk/python/MANIFEST.in +21 -0
- package/.next/standalone/sdk/python/README.md +466 -0
- package/.next/standalone/sdk/python/docs/README.md +249 -0
- package/.next/standalone/sdk/python/docs/_shared/README.md +43 -0
- package/.next/standalone/sdk/python/docs/_shared/__init__.py +195 -0
- package/.next/standalone/sdk/python/docs/crewai/README.md +328 -0
- package/.next/standalone/sdk/python/docs/crewai/examples/quickstart.py +61 -0
- package/.next/standalone/sdk/python/docs/crewai/examples/research_crew.py +92 -0
- package/.next/standalone/sdk/python/docs/langgraph/README.md +481 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/quickstart.py +51 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/research_agent.py +86 -0
- package/.next/standalone/sdk/python/docs/langgraph/examples/supervisor_handoff.py +83 -0
- package/.next/standalone/sdk/python/docs/llama_index/README.md +332 -0
- package/.next/standalone/sdk/python/docs/llama_index/examples/quickstart.py +62 -0
- package/.next/standalone/sdk/python/docs/llama_index/examples/research_agent.py +93 -0
- package/.next/standalone/sdk/python/docs/manual/README.md +491 -0
- package/.next/standalone/sdk/python/docs/manual/examples/quickstart.py +57 -0
- package/.next/standalone/sdk/python/docs/manual/examples/research_agent.py +133 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/README.md +323 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/examples/quickstart.py +48 -0
- package/.next/standalone/sdk/python/docs/pydantic_ai/examples/research_agent.py +94 -0
- package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +131 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_context.py +218 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_environment.py +78 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_events.py +752 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_resolver.py +118 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_runtime.py +21 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_schema.py +317 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_scopes.py +422 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -0
- package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +750 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +274 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/_compat.py +247 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/_core.py +1076 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/crewai.py +1669 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/langchain.py +2151 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +1588 -0
- package/.next/standalone/sdk/python/failproofai_sdk/integrations/pydantic_ai.py +977 -0
- package/.next/standalone/sdk/python/failproofai_sdk/py.typed +0 -0
- package/.next/standalone/sdk/python/pyproject.toml +92 -0
- package/.next/standalone/sdk/python/skill/SKILL.md +399 -0
- package/.next/standalone/sdk/python/skill/agents/openai.yaml +8 -0
- package/.next/standalone/sdk/python/skill/references/events.md +234 -0
- package/.next/standalone/sdk/python/skill/references/frameworks.md +314 -0
- package/.next/standalone/sdk/python/skill/references/install.md +100 -0
- package/.next/standalone/sdk/python/skill/references/integration.md +352 -0
- package/.next/standalone/sdk/python/tests/__init__.py +0 -0
- package/.next/standalone/sdk/python/tests/conftest.py +169 -0
- package/.next/standalone/sdk/python/tests/integrations/__init__.py +6 -0
- package/.next/standalone/sdk/python/tests/integrations/test_crewai.py +1299 -0
- package/.next/standalone/sdk/python/tests/integrations/test_langchain.py +2338 -0
- package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +1576 -0
- package/.next/standalone/sdk/python/tests/integrations/test_pydantic_ai.py +1102 -0
- package/.next/standalone/sdk/python/tests/test_context.py +410 -0
- package/.next/standalone/sdk/python/tests/test_docs.py +411 -0
- package/.next/standalone/sdk/python/tests/test_durability.py +1352 -0
- package/.next/standalone/sdk/python/tests/test_encoding.py +571 -0
- package/.next/standalone/sdk/python/tests/test_integrations.py +1127 -0
- package/.next/standalone/sdk/python/tests/test_no_customer_identifiers.py +214 -0
- package/.next/standalone/sdk/python/tests/test_packaging.py +185 -0
- package/.next/standalone/sdk/python/tests/test_resolver_umbrella.py +178 -0
- package/.next/standalone/sdk/python/tests/test_scopes.py +487 -0
- package/.next/standalone/sdk/python/tests/test_sdk.py +923 -0
- package/.next/standalone/sdk/python/tests/test_server_contract.py +650 -0
- package/.next/standalone/sdk/python/tests/test_site_docs.py +371 -0
- package/.next/standalone/sdk/python/tests/test_skill_snippets.py +165 -0
- package/.next/standalone/sdk/python/tests/test_spool_contract.py +287 -0
- package/.next/standalone/sdk/python/tests/test_spool_creation.py +360 -0
- package/.next/standalone/sdk/python/tests/test_wire_format.py +279 -0
- package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +378 -0
- package/.next/standalone/sdk/python/uv.lock +6005 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +77 -21
- package/bin/failproofai.mjs +145 -11
- package/dist/cli.mjs +4482 -2205
- package/dist/index.js +5 -5
- package/dist/worker.mjs +2399 -1978
- package/package.json +29 -22
- package/policy-pack/SHA256SUMS +2 -0
- package/policy-pack/failproofai-pack.json +732 -0
- package/policy-pack/failproofai-pack.mjs +2017 -0
- package/scripts/build-policy-pack.mjs +121 -0
- package/scripts/changelog-section.py +77 -0
- package/scripts/prune-standalone.mjs +3 -0
- package/scripts/python-version.py +232 -0
- package/scripts/translate-docs/cli.ts +21 -0
- package/scripts/translate-docs/mintlify-nav.ts +85 -13
- package/scripts/translate-docs/translator.ts +66 -11
- package/src/audit/cache.ts +40 -3
- package/src/audit/cli.ts +1 -1
- package/src/audit/index.ts +1 -1
- package/src/audit/replay.ts +89 -3
- package/src/audit/report.ts +5 -5
- package/src/audit/schedule-cli.ts +49 -53
- package/src/hooks/builtin-policies.ts +217 -579
- package/src/hooks/cloud-enrollment-cli.ts +66 -27
- package/src/hooks/cloud-managed-policies.ts +6 -1
- package/src/hooks/configure-wizard.ts +1 -1
- package/src/hooks/custom-hooks-loader.ts +89 -6
- package/src/hooks/fp-home.ts +92 -4
- package/src/hooks/fp-reset.ts +26 -2
- package/src/hooks/handler.ts +181 -11
- package/src/hooks/harness-cli.ts +79 -25
- package/src/hooks/hook-activity-store.ts +13 -3
- package/src/hooks/install-prompt.ts +25 -3
- package/src/hooks/manager.ts +418 -172
- package/src/hooks/migrations.ts +2 -2
- package/src/hooks/pack-cli.ts +505 -0
- package/src/hooks/pack-failclosed.ts +189 -0
- package/src/hooks/pack-manifest.ts +344 -0
- package/src/hooks/pack-store.ts +930 -0
- package/src/hooks/policy-catalog.ts +535 -0
- package/src/hooks/policy-evaluator.ts +24 -16
- package/src/hooks/policy-presets.ts +4 -2
- package/src/hooks/policy-registry.ts +8 -2
- package/src/hooks/policy-types.ts +26 -0
- package/src/hooks/session-pause-cli.ts +33 -9
- package/src/hooks/tui.ts +570 -0
- package/src/hooks/types.ts +15 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
- package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
- package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
- package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.js +0 -1
- package/.next/standalone/.next/static/chunks/3tzq0260n17ae.js +0 -6
- package/.next/standalone/.next/static/chunks/3wq9x0c-7ld5d.css +0 -2
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
"""Failproof AI adapter for Pydantic AI, built on `AbstractCapability`.
|
|
2
|
+
|
|
3
|
+
Written against **pydantic-ai-slim 2.20.0** (2026-07-29), read out of the
|
|
4
|
+
installed package rather than from memory. Every v1 tutorial is wrong for this
|
|
5
|
+
version: `Agent(instrument=...)` was **removed in 2.0.0b1**, and
|
|
6
|
+
`opentelemetry-api` became a core, non-optional dependency of pydantic-ai-slim.
|
|
7
|
+
|
|
8
|
+
Why the capability protocol and not an OTel span processor
|
|
9
|
+
----------------------------------------------------------
|
|
10
|
+
`pydantic_ai.capabilities.AbstractCapability` is a full middleware protocol —
|
|
11
|
+
`wrap_run`, `wrap_model_request`, `wrap_tool_execute` and friends — and
|
|
12
|
+
Pydantic's own `Instrumentation` capability is just another consumer of it with
|
|
13
|
+
no privileged access. So `capabilities=[FailproofAI()]`:
|
|
14
|
+
|
|
15
|
+
* hands us typed `RunContext` / `ModelResponse` / `RunUsage` objects instead of
|
|
16
|
+
stringly-typed span attributes;
|
|
17
|
+
* needs no OTel **SDK** on the user's machine (only the API package, which is
|
|
18
|
+
already a hard dependency);
|
|
19
|
+
* cannot double-count against the user's own tracing, because we are a sibling
|
|
20
|
+
of `Instrumentation` rather than a second exporter on the same spans.
|
|
21
|
+
|
|
22
|
+
The OTel route would additionally have had to read **both** `gen_ai.usage.*`
|
|
23
|
+
(model spans) and `gen_ai.aggregated_usage.*` (run spans) — the latter is a
|
|
24
|
+
Pydantic extension that every existing vendor silently drops, which is how
|
|
25
|
+
agent-run token counts come out as zero elsewhere. We read `RunUsage` directly
|
|
26
|
+
and the question does not arise.
|
|
27
|
+
|
|
28
|
+
Adapter shape
|
|
29
|
+
-------------
|
|
30
|
+
This is **Shape A** end to end: every capability hook is a wrapper whose start
|
|
31
|
+
and end sit in one frame, so the adapter can bind Failproof AI identity onto
|
|
32
|
+
contextvars for the duration of a run (`failproofai_sdk.session(...)`) and nested
|
|
33
|
+
`agent.run()` calls — an agent invoked from inside another agent's tool — nest
|
|
34
|
+
themselves with no bookkeeping. `RunTracker` still owns the id/parent/session
|
|
35
|
+
resolution and every emit, so `framework` / `framework_version` /
|
|
36
|
+
`integration_version` land on **every** event, including `agent_end`.
|
|
37
|
+
|
|
38
|
+
One thing Shape A does **not** give you here, and `_open_runs` is the whole of
|
|
39
|
+
what this module keeps to cover it: a leaf hook can return *after* the run hook
|
|
40
|
+
that contains it. Cancellation is the reliable case — the graph awaits a
|
|
41
|
+
`gather` of tool tasks, so the run body unwinds the moment that future is
|
|
42
|
+
cancelled while each tool task's own `CancelledError` lands a loop iteration
|
|
43
|
+
later. `_close_spans` therefore closes whatever leaves are still open before
|
|
44
|
+
`agent_end`, and `_claim_span` / `_claim_run` make the late handler (and a
|
|
45
|
+
concurrent `uninstall()`) a no-op rather than a duplicate.
|
|
46
|
+
|
|
47
|
+
How it installs
|
|
48
|
+
---------------
|
|
49
|
+
There is no supported global capability default in 2.20: `Agent.instrument_all()`
|
|
50
|
+
sets `Agent._instrument_default`, which is consulted *only* for the built-in
|
|
51
|
+
`Instrumentation` capability, and the one real auto-injection list
|
|
52
|
+
(`pydantic_ai.agent._AUTO_INJECT_CAPABILITY_TYPES`) is a private module
|
|
53
|
+
constant. So `install()` wraps the public `Agent.__init__` and appends our
|
|
54
|
+
capability to its keyword-only `capabilities=` argument, saving the original
|
|
55
|
+
function object through `_core.Patcher` so `uninstall()` restores exactly that
|
|
56
|
+
object.
|
|
57
|
+
|
|
58
|
+
Consequence, and it is worth knowing: an `Agent` **constructed while
|
|
59
|
+
instrumented keeps the capability object forever** — we cannot retro-remove
|
|
60
|
+
ourselves from an already-built agent. `uninstall()` therefore also flips a
|
|
61
|
+
module-level flag that makes every hook a straight pass-through, so an agent
|
|
62
|
+
built before `uninstrument()` stops recording rather than recording into a
|
|
63
|
+
half-dismantled adapter. By the same token, agents constructed *before*
|
|
64
|
+
`instrument()` are not instrumented; construct them after, or pass
|
|
65
|
+
`capabilities=[FailproofAI()]` yourself.
|
|
66
|
+
|
|
67
|
+
What is deliberately not captured
|
|
68
|
+
---------------------------------
|
|
69
|
+
* **Graph nodes** (`UserPromptNode`, `ModelRequestNode`, `CallToolsNode`) get no
|
|
70
|
+
events. They are Pydantic AI's own loop machinery, not user-authored steps —
|
|
71
|
+
unlike a LangGraph node, which is a `hook_*` pair because the user wrote it.
|
|
72
|
+
Everything a node does that is worth seeing is already covered by the model
|
|
73
|
+
and tool spans, and `wrap_node_run` would double the row count for nothing.
|
|
74
|
+
Not overriding it also keeps `AbstractCapability.has_wrap_node_run` False.
|
|
75
|
+
* **`wrap_run_event_stream`** is not overridden either, and that one is a
|
|
76
|
+
landmine: overriding it makes `agent.run()` switch itself into streaming mode.
|
|
77
|
+
Per-token events are forbidden anyway (a 500-token response would be 500
|
|
78
|
+
stored rows against a 5-lane rail).
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
import dataclasses
|
|
82
|
+
import logging
|
|
83
|
+
import threading
|
|
84
|
+
import time
|
|
85
|
+
import traceback as _traceback
|
|
86
|
+
import uuid
|
|
87
|
+
from typing import Any
|
|
88
|
+
|
|
89
|
+
import failproofai_sdk
|
|
90
|
+
from failproofai_sdk._scopes import _is_cancellation
|
|
91
|
+
from failproofai_sdk.integrations import _compat, _core
|
|
92
|
+
from failproofai_sdk.integrations._core import (
|
|
93
|
+
RunTracker,
|
|
94
|
+
framework_fields,
|
|
95
|
+
fw_fields,
|
|
96
|
+
ms,
|
|
97
|
+
normalize_agent_id,
|
|
98
|
+
safe,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
logger = logging.getLogger("failproofai_sdk.integrations")
|
|
102
|
+
|
|
103
|
+
NAME = "pydantic_ai"
|
|
104
|
+
MODULE = "pydantic_ai"
|
|
105
|
+
DIST = "pydantic-ai-slim"
|
|
106
|
+
EXTRA = "pydantic-ai"
|
|
107
|
+
|
|
108
|
+
# 2.0 is a CAPABILITY floor, not a guess: it is the release that removed
|
|
109
|
+
# `Agent(instrument=...)` and introduced `AbstractCapability` — the entire
|
|
110
|
+
# surface this module is built on. The ceiling is deliberate: without one, a
|
|
111
|
+
# clean build after the next major shifts the hook names and this adapter stops
|
|
112
|
+
# recording while raising nothing at all.
|
|
113
|
+
MIN_VERSION = "2.0"
|
|
114
|
+
BELOW_VERSION = "3"
|
|
115
|
+
_VERSION_REASON = "2.0 removed Agent(instrument=...) and added the capabilities middleware protocol"
|
|
116
|
+
|
|
117
|
+
# This module is only ever imported by `instrument("pydantic_ai")`, so a
|
|
118
|
+
# module-level framework import is fine here and nowhere else — `import failproofai_sdk`
|
|
119
|
+
# still pulls in zero third-party packages. `require_module` first so the failure
|
|
120
|
+
# is the tier-1 ImportError carrying the literal install command, rather than a
|
|
121
|
+
# bare `No module named 'pydantic_ai'`.
|
|
122
|
+
_compat.require_module("pydantic_ai.capabilities", dist=DIST, extra=EXTRA)
|
|
123
|
+
|
|
124
|
+
from pydantic_ai.capabilities import ( # noqa: E402 (deliberately after the probe)
|
|
125
|
+
AbstractCapability,
|
|
126
|
+
CapabilityOrdering,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# One tracker for the process. `base_fields` is what puts `framework` on every
|
|
130
|
+
# single event this adapter emits, agent_end and error included.
|
|
131
|
+
_tracker = RunTracker(NAME, base_fields=framework_fields(NAME, DIST))
|
|
132
|
+
|
|
133
|
+
# Flipped by install()/uninstall(). Agents built while instrumented keep the
|
|
134
|
+
# capability instance, so this flag — not the patch — is what actually stops the
|
|
135
|
+
# recording. Read on every hook, written only under the registry lock.
|
|
136
|
+
_enabled = False
|
|
137
|
+
_capability: "FailproofAI | None" = None
|
|
138
|
+
_patcher = _core.Patcher()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# Control flow that is not failure
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
def _control_flow_types() -> tuple:
|
|
146
|
+
"""Exceptions Pydantic AI raises to *steer* a run, not to report a failure.
|
|
147
|
+
|
|
148
|
+
Resolved by name at import time and tolerant of every one of them being
|
|
149
|
+
absent: this list is exactly the kind of thing a minor release renames, and
|
|
150
|
+
a missing name must degrade to "treat it as an error" rather than to an
|
|
151
|
+
`AttributeError` inside the customer's run.
|
|
152
|
+
|
|
153
|
+
`ModelRetry` / `ToolRetryError` / `ToolFailedError` are **not** here on
|
|
154
|
+
purpose. They mean an attempt genuinely failed and the model was asked to
|
|
155
|
+
try again, which is exactly what a tool span's `error` field is for; the run
|
|
156
|
+
itself still ends `success` if the retry works.
|
|
157
|
+
"""
|
|
158
|
+
import pydantic_ai.exceptions as exceptions
|
|
159
|
+
|
|
160
|
+
found = []
|
|
161
|
+
for name in (
|
|
162
|
+
"SkipToolExecution",
|
|
163
|
+
"SkipToolValidation",
|
|
164
|
+
"SkipModelRequest",
|
|
165
|
+
"CallDeferred",
|
|
166
|
+
"ApprovalRequired",
|
|
167
|
+
):
|
|
168
|
+
candidate = getattr(exceptions, name, None)
|
|
169
|
+
if isinstance(candidate, type) and issubclass(candidate, BaseException):
|
|
170
|
+
found.append(candidate)
|
|
171
|
+
return tuple(found)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
_CONTROL_FLOW = _control_flow_types()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _is_control_flow(exc: BaseException) -> bool:
|
|
178
|
+
return _is_cancellation(type(exc)) or isinstance(exc, _CONTROL_FLOW)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _describe(exc: BaseException) -> str:
|
|
182
|
+
text = str(exc)
|
|
183
|
+
return f"{type(exc).__name__}: {text}" if text else type(exc).__name__
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
_TRACEBACK_MARKER = "[older frames truncated]…\n"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _format_traceback(exc: BaseException) -> str:
|
|
190
|
+
"""The traceback, trimmed from the FRONT if it is too long.
|
|
191
|
+
|
|
192
|
+
Every truncation in `_core` keeps the head, which is exactly wrong here: a
|
|
193
|
+
traceback's last line is the exception itself, and Pydantic AI's async graph
|
|
194
|
+
stack is comfortably longer than the 8KB field limit. Keeping the head would
|
|
195
|
+
ship 8KB of framework frames and drop the one line anybody reads.
|
|
196
|
+
"""
|
|
197
|
+
text = "".join(_traceback.format_exception(type(exc), exc, exc.__traceback__))
|
|
198
|
+
if len(text) <= _core.FIELD_LIMIT:
|
|
199
|
+
return text
|
|
200
|
+
tail = text[-(_core.FIELD_LIMIT - len(_TRACEBACK_MARKER)):]
|
|
201
|
+
return _TRACEBACK_MARKER + tail
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# Reading the framework's objects
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def _run_key(ctx: Any) -> Any:
|
|
209
|
+
"""A stable per-run key. `run_id` is set for every real run; the fallback
|
|
210
|
+
only matters for a synthetic `RunContext` that is not backed by one."""
|
|
211
|
+
run_id = getattr(ctx, "run_id", None)
|
|
212
|
+
return ("pydantic_ai.run", run_id if run_id else id(ctx))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _agent_name(ctx: Any) -> str:
|
|
216
|
+
"""`agent_id` must stay low-cardinality and human readable.
|
|
217
|
+
|
|
218
|
+
Pydantic AI infers `Agent.name` from the assigning call frame on the first
|
|
219
|
+
run, so this is normally the variable name (`weather_agent`).
|
|
220
|
+
`normalize_agent_id` turns anything id-shaped — or nothing at all — into
|
|
221
|
+
`main` and the real id rides in `fw_run_id`.
|
|
222
|
+
"""
|
|
223
|
+
agent = getattr(ctx, "agent", None)
|
|
224
|
+
return normalize_agent_id(getattr(agent, "name", None))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _text(value: Any) -> str | None:
|
|
228
|
+
if value is None:
|
|
229
|
+
return None
|
|
230
|
+
return value if isinstance(value, str) else str(value)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _model_name(model: Any) -> str | None:
|
|
234
|
+
return getattr(model, "model_name", None) if model is not None else None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _render_part(part: Any, capture_content: bool) -> dict:
|
|
238
|
+
out: dict[str, Any] = {"part_kind": getattr(part, "part_kind", None) or type(part).__name__}
|
|
239
|
+
tool_name = getattr(part, "tool_name", None)
|
|
240
|
+
if tool_name:
|
|
241
|
+
out["tool_name"] = tool_name
|
|
242
|
+
tool_call_id = getattr(part, "tool_call_id", None)
|
|
243
|
+
if tool_call_id:
|
|
244
|
+
out["tool_call_id"] = tool_call_id
|
|
245
|
+
if capture_content:
|
|
246
|
+
content = getattr(part, "content", None)
|
|
247
|
+
if content is None:
|
|
248
|
+
content = getattr(part, "args", None)
|
|
249
|
+
if content is not None:
|
|
250
|
+
out["content"] = content
|
|
251
|
+
return out
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# Pydantic AI hands `wrap_model_request` the WHOLE conversation on every step,
|
|
255
|
+
# so shipping it verbatim makes a 30-step run quadratic in payload size — and
|
|
256
|
+
# these are prompts, i.e. the largest strings in the process. `truncate` caps
|
|
257
|
+
# each string, but nothing caps the count, so the tail is capped here. The head
|
|
258
|
+
# of a long conversation is the least interesting part of a model request.
|
|
259
|
+
_MESSAGE_LIMIT = 20
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _render_messages(messages: Any, capture_content: bool) -> "tuple[list[dict] | None, int]":
|
|
263
|
+
"""(rendered tail, number of older messages omitted)."""
|
|
264
|
+
if not messages:
|
|
265
|
+
return None, 0
|
|
266
|
+
messages = list(messages)
|
|
267
|
+
omitted = max(len(messages) - _MESSAGE_LIMIT, 0)
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
"kind": getattr(message, "kind", None),
|
|
271
|
+
"parts": [_render_part(p, capture_content) for p in getattr(message, "parts", ()) or ()],
|
|
272
|
+
}
|
|
273
|
+
for message in messages[omitted:]
|
|
274
|
+
], omitted
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _render_tools(params: Any, capture_content: bool) -> list[dict] | None:
|
|
278
|
+
tools: list[dict] = []
|
|
279
|
+
for group in ("function_tools", "output_tools", "native_tools"):
|
|
280
|
+
for tool in getattr(params, group, None) or ():
|
|
281
|
+
entry: dict[str, Any] = {
|
|
282
|
+
"name": getattr(tool, "name", None) or type(tool).__name__,
|
|
283
|
+
"kind": group,
|
|
284
|
+
}
|
|
285
|
+
if capture_content:
|
|
286
|
+
description = getattr(tool, "description", None)
|
|
287
|
+
if description:
|
|
288
|
+
entry["description"] = description
|
|
289
|
+
tools.append(entry)
|
|
290
|
+
return tools or None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# `RunUsage` renamed `request_tokens`/`response_tokens` to
|
|
294
|
+
# `input_tokens`/`output_tokens` in 2.0. Reading the new names only is correct
|
|
295
|
+
# for the declared floor, and the anti-drift test asserts they still exist.
|
|
296
|
+
_USAGE_KEYS = (
|
|
297
|
+
"input_tokens",
|
|
298
|
+
"output_tokens",
|
|
299
|
+
"total_tokens",
|
|
300
|
+
"cache_read_tokens",
|
|
301
|
+
"cache_write_tokens",
|
|
302
|
+
"requests",
|
|
303
|
+
"tool_calls",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _usage_dict(usage: Any) -> dict | None:
|
|
308
|
+
"""The normalized `usage` blob. Both `event_summary.rs` and
|
|
309
|
+
`sessionSummary.ts` fall back to it when the top-level ints are absent."""
|
|
310
|
+
if usage is None:
|
|
311
|
+
return None
|
|
312
|
+
out = {}
|
|
313
|
+
for key in _USAGE_KEYS:
|
|
314
|
+
value = getattr(usage, key, None)
|
|
315
|
+
if isinstance(value, int) and not isinstance(value, bool) and value:
|
|
316
|
+
out[key] = value
|
|
317
|
+
return out or None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _token(usage: Any, key: str) -> int | None:
|
|
321
|
+
value = getattr(usage, key, None)
|
|
322
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _tool_output(result: Any) -> Any:
|
|
326
|
+
"""What the tool actually produced, unwrapped and JSON-shaped.
|
|
327
|
+
|
|
328
|
+
Two lossy renderings happen without this, and `tool_result.output` is the
|
|
329
|
+
single most-read field in a tool loop:
|
|
330
|
+
|
|
331
|
+
* A tool may return `ToolReturn`, an **envelope** — `return_value` is what
|
|
332
|
+
goes back to the model, `content` is an extra user-prompt part and
|
|
333
|
+
`metadata` is deliberately never shown to the model at all. The envelope
|
|
334
|
+
has no JSON shape, so it rendered as
|
|
335
|
+
`ToolReturn(return_value={'answer': 42}, content=…, metadata=…)`: the
|
|
336
|
+
answer buried inside a repr, next to a field the model never saw.
|
|
337
|
+
* A tool returning a Pydantic model or a dataclass — the documented way to
|
|
338
|
+
return structured data — rendered as `Weather(city='Faro', celsius=21)`
|
|
339
|
+
rather than `{"city": "Faro", "celsius": 21}`, so nothing downstream can
|
|
340
|
+
read a field out of it.
|
|
341
|
+
|
|
342
|
+
`_core.truncate` reprs an object with "no JSON shape", which is the right
|
|
343
|
+
default for an arbitrary object and wrong for these: they have one, and this
|
|
344
|
+
module is the only place that knows it. It is unwrapped with the object's
|
|
345
|
+
own `model_dump` / `dataclasses.asdict` rather than `pydantic_core`, because
|
|
346
|
+
this package declares no runtime dependencies and `tests/test_zero_dependencies.py`
|
|
347
|
+
reads the AST — a guarded import would still be an import. Anything else is
|
|
348
|
+
handed through untouched, and every failure falls back to the original value,
|
|
349
|
+
which `truncate` then reprs exactly as it does today.
|
|
350
|
+
"""
|
|
351
|
+
value = getattr(result, "return_value", result) if _is_tool_return(result) else result
|
|
352
|
+
if value is None or isinstance(value, (str, bytes, bool, int, float, list, tuple, dict)):
|
|
353
|
+
return value
|
|
354
|
+
if _is_base_model(value):
|
|
355
|
+
for mode in ("json", None):
|
|
356
|
+
try:
|
|
357
|
+
return value.model_dump(mode=mode) if mode else value.model_dump()
|
|
358
|
+
except Exception:
|
|
359
|
+
continue
|
|
360
|
+
return value
|
|
361
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
362
|
+
try:
|
|
363
|
+
return dataclasses.asdict(value)
|
|
364
|
+
except Exception:
|
|
365
|
+
return value
|
|
366
|
+
return value
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _is_tool_return(value: Any) -> bool:
|
|
370
|
+
return getattr(value, "kind", None) == "tool-return" and hasattr(value, "return_value")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _is_base_model(value: Any) -> bool:
|
|
374
|
+
"""Duck-typed: `model_dump` alone also matches a TypedDict helper or a mock."""
|
|
375
|
+
return hasattr(value, "model_dump") and hasattr(value, "model_fields_set")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
# Per-hook state
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
class _RunState:
|
|
383
|
+
__slots__ = ("key", "scope", "spans", "closed", "reported_error")
|
|
384
|
+
|
|
385
|
+
def __init__(self, key: Any) -> None:
|
|
386
|
+
self.key = key
|
|
387
|
+
self.scope: Any = None
|
|
388
|
+
# Leaf spans this run has opened and not yet closed, newest last. This
|
|
389
|
+
# exists because a leaf can outlive its own run — see `_close_spans`.
|
|
390
|
+
self.spans: dict[int, "_SpanState"] = {}
|
|
391
|
+
self.closed = False
|
|
392
|
+
# Set when a leaf span closed carrying a non-None `error`. The run must
|
|
393
|
+
# not then emit a standalone `error` event for the same exception: all
|
|
394
|
+
# three sibling adapters suppress it (langchain's
|
|
395
|
+
# `session.reported_error`, llama_index's `run.errors == 0`, crewai
|
|
396
|
+
# emitting none at all) and for the same stated reason — one failure
|
|
397
|
+
# counted twice on `sessionSummary.errorCount`.
|
|
398
|
+
self.reported_error = False
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class _SpanState:
|
|
402
|
+
__slots__ = ("key", "kind", "correlation_id", "started", "extra", "managed")
|
|
403
|
+
|
|
404
|
+
def __init__(
|
|
405
|
+
self, key: Any, kind: str, correlation_id: str, extra: dict | None = None
|
|
406
|
+
) -> None:
|
|
407
|
+
self.key = key
|
|
408
|
+
self.kind = kind
|
|
409
|
+
self.correlation_id = correlation_id
|
|
410
|
+
# perf_counter, not wall clock: a clock adjustment mid-run would
|
|
411
|
+
# otherwise produce a negative duration.
|
|
412
|
+
self.started = time.perf_counter()
|
|
413
|
+
self.extra = extra or {}
|
|
414
|
+
# True once this span is registered against a live run, i.e. once the
|
|
415
|
+
# run's teardown is able to close it instead of us.
|
|
416
|
+
self.managed = False
|
|
417
|
+
|
|
418
|
+
def elapsed_ms(self) -> int:
|
|
419
|
+
return ms(time.perf_counter() - self.started)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# Runs whose `wrap_run` frame is still open, keyed by `_run_key`. The adapter is
|
|
423
|
+
# otherwise stateless — this table exists for one reason, and it is the whole of
|
|
424
|
+
# `_close_spans` below: a leaf can outlive the run that opened it.
|
|
425
|
+
_open_runs: "dict[Any, _RunState]" = {}
|
|
426
|
+
_runs_lock = threading.Lock()
|
|
427
|
+
|
|
428
|
+
# Bounded for the same reason `RunTracker` is: a `wrap_run` coroutine that is
|
|
429
|
+
# garbage-collected before it resumes runs neither of its end branches, and an
|
|
430
|
+
# unbounded table of those is a leak in a long-lived server. FIFO — dicts keep
|
|
431
|
+
# insertion order.
|
|
432
|
+
_MAX_OPEN_RUNS = 10_000
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _register_run(state: _RunState) -> None:
|
|
436
|
+
with _runs_lock:
|
|
437
|
+
while len(_open_runs) >= _MAX_OPEN_RUNS:
|
|
438
|
+
_open_runs.pop(next(iter(_open_runs)), None)
|
|
439
|
+
_open_runs[state.key] = state
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _claim_run(state: _RunState) -> bool:
|
|
443
|
+
"""True for the first caller to close this run; False for every later one.
|
|
444
|
+
|
|
445
|
+
Two things close a run — `wrap_run`'s own end, and `uninstall()` tearing
|
|
446
|
+
down mid-flight — and both must be able to go first. Without this, an
|
|
447
|
+
`uninstrument()` called while a run is in flight emitted `agent_end`
|
|
448
|
+
(`cancelled`) and the run then emitted a second `agent_end` (`success`)
|
|
449
|
+
against a span the dashboard had already closed.
|
|
450
|
+
"""
|
|
451
|
+
with _runs_lock:
|
|
452
|
+
_open_runs.pop(state.key, None)
|
|
453
|
+
if state.closed:
|
|
454
|
+
return False
|
|
455
|
+
state.closed = True
|
|
456
|
+
return True
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _register_span(span: _SpanState) -> None:
|
|
460
|
+
with _runs_lock:
|
|
461
|
+
run = _open_runs.get(span.key)
|
|
462
|
+
if run is not None:
|
|
463
|
+
run.spans[id(span)] = span
|
|
464
|
+
span.managed = True
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _mark_reported_error(span: _SpanState) -> None:
|
|
468
|
+
"""Record that a LEAF span carried this run's failure.
|
|
469
|
+
|
|
470
|
+
`span.key` is the owning run's key, and the run stays in `_open_runs` until
|
|
471
|
+
`_claim_run`, so it is still reachable here. `_end_run` reads the flag to
|
|
472
|
+
decide whether a standalone `error` event would be a second report of a
|
|
473
|
+
failure the leaf has already made.
|
|
474
|
+
"""
|
|
475
|
+
with _runs_lock:
|
|
476
|
+
run = _open_runs.get(span.key)
|
|
477
|
+
if run is not None:
|
|
478
|
+
run.reported_error = True
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _claim_span(span: _SpanState) -> bool:
|
|
482
|
+
"""True if this caller owns the span's closing event.
|
|
483
|
+
|
|
484
|
+
False only when the run's teardown already emitted it. A span that was never
|
|
485
|
+
registered against a run (its `wrap_run` never opened one) is unmanaged and
|
|
486
|
+
always closes itself, so an adapter half-failure loses no leaf.
|
|
487
|
+
"""
|
|
488
|
+
if not span.managed:
|
|
489
|
+
return True
|
|
490
|
+
with _runs_lock:
|
|
491
|
+
run = _open_runs.get(span.key)
|
|
492
|
+
if run is None:
|
|
493
|
+
return False
|
|
494
|
+
return run.spans.pop(id(span), None) is not None
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def _close_spans(state: _RunState, exc: "BaseException | None") -> None:
|
|
498
|
+
"""Close every leaf this run opened and did not close, newest first.
|
|
499
|
+
|
|
500
|
+
A leaf can outlive its own run, and on the cancellation path it reliably
|
|
501
|
+
does. `asyncio.wait_for` cancels the caller's task; the graph is awaiting
|
|
502
|
+
a `gather` of tool tasks, so the run body unwinds as soon as that future
|
|
503
|
+
is cancelled while each tool task's own `CancelledError` is delivered on
|
|
504
|
+
a later loop iteration. `wrap_run` therefore returns *before*
|
|
505
|
+
`wrap_tool_execute` does, and the same is true of `wrap_model_request`
|
|
506
|
+
when the cancellation lands inside the provider call. Measured against
|
|
507
|
+
pydantic-ai 2.32: a `wait_for` timeout produced `agent_end` at
|
|
508
|
+
`.565709` and the matching `tool_result` at `.566689`, and a timeout
|
|
509
|
+
during a model request put `model_response` after `agent_end` too — the
|
|
510
|
+
one thing every other emit in this file is careful never to do, because
|
|
511
|
+
the dashboard closes the agent span at `agent_end` and anything after it
|
|
512
|
+
is attributed to nothing. Worse, the ambient identity the late leaf
|
|
513
|
+
resolves through is unbound by then in some interleavings, and the event
|
|
514
|
+
is dropped outright: a `tool_use` with no `tool_result` at all.
|
|
515
|
+
|
|
516
|
+
So the run closes them, `_claim_span` stops the real handler emitting a
|
|
517
|
+
duplicate when it finally unwinds, and `fw_incomplete` says the leaf did
|
|
518
|
+
not report its own outcome.
|
|
519
|
+
"""
|
|
520
|
+
with _runs_lock:
|
|
521
|
+
pending = list(state.spans.values())
|
|
522
|
+
state.spans.clear()
|
|
523
|
+
if not pending:
|
|
524
|
+
return
|
|
525
|
+
# A cancellation is not a failure, exactly as on `agent_end`.
|
|
526
|
+
error = None if exc is None or _is_control_flow(exc) else _describe(exc)
|
|
527
|
+
for span in reversed(pending):
|
|
528
|
+
if span.kind == "tool":
|
|
529
|
+
_tracker.emit(
|
|
530
|
+
"tool_result",
|
|
531
|
+
None,
|
|
532
|
+
parent_key=span.key,
|
|
533
|
+
tool_name=span.extra.get("tool_name"),
|
|
534
|
+
tool_call_id=span.correlation_id,
|
|
535
|
+
error=error,
|
|
536
|
+
**fw_fields(incomplete=True),
|
|
537
|
+
)
|
|
538
|
+
else:
|
|
539
|
+
_tracker.emit(
|
|
540
|
+
"model_response",
|
|
541
|
+
None,
|
|
542
|
+
parent_key=span.key,
|
|
543
|
+
model=span.extra.get("model"),
|
|
544
|
+
role="assistant",
|
|
545
|
+
request_id=span.correlation_id,
|
|
546
|
+
error=error,
|
|
547
|
+
duration_ms=span.elapsed_ms(),
|
|
548
|
+
**fw_fields(incomplete=True, streaming=span.extra.get("streaming")),
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _close_open_runs() -> None:
|
|
553
|
+
"""Close every run this adapter still has open, newest first.
|
|
554
|
+
|
|
555
|
+
Teardown, not a hot path: `uninstall()` is the only caller. Each run's
|
|
556
|
+
leaves close before its `agent_end`, and `_claim_run` makes the run's own
|
|
557
|
+
`wrap_run` frame a no-op when it eventually unwinds.
|
|
558
|
+
"""
|
|
559
|
+
with _runs_lock:
|
|
560
|
+
states = list(_open_runs.values())
|
|
561
|
+
for state in reversed(states):
|
|
562
|
+
_core.call_safely(_close_run, (state,), {}, f"{__name__}.uninstall")
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _close_run(state: _RunState) -> None:
|
|
566
|
+
if not _claim_run(state):
|
|
567
|
+
return
|
|
568
|
+
_close_spans(state, None)
|
|
569
|
+
_tracker.end_agent(state.key, outcome="cancelled")
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
# ---------------------------------------------------------------------------
|
|
573
|
+
# The capability
|
|
574
|
+
# ---------------------------------------------------------------------------
|
|
575
|
+
|
|
576
|
+
class FailproofAI(AbstractCapability):
|
|
577
|
+
"""Record a Pydantic AI agent run into Failproof AI.
|
|
578
|
+
|
|
579
|
+
from failproofai_sdk.integrations.pydantic_ai import FailproofAI
|
|
580
|
+
agent = Agent("openai:gpt-5.6-sol", capabilities=[FailproofAI()])
|
|
581
|
+
|
|
582
|
+
or, for every agent constructed from now on::
|
|
583
|
+
|
|
584
|
+
failproofai_sdk.instrument("pydantic_ai")
|
|
585
|
+
|
|
586
|
+
Event mapping:
|
|
587
|
+
|
|
588
|
+
| Pydantic AI | Failproof AI |
|
|
589
|
+
|----------------------|-------------------------------------------|
|
|
590
|
+
| `wrap_run` | `agent_start` / `agent_end` (+ `error`) |
|
|
591
|
+
| `wrap_model_request` | `model_request` / `model_response` |
|
|
592
|
+
| `wrap_tool_execute` | `tool_use` / `tool_result` |
|
|
593
|
+
| graph nodes | nothing — see the module docstring |
|
|
594
|
+
|
|
595
|
+
`session_id` is the run's `conversation_id` (a conversation spanning several
|
|
596
|
+
runs is one Failproof AI session, which is the point), unless a hand-written
|
|
597
|
+
`failproofai_sdk.session(...)` / `failproofai_sdk.agent(...)` is already open — that always
|
|
598
|
+
wins, so mixing the manual API with this adapter produces one tree, not two.
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
def __init__(
|
|
602
|
+
self,
|
|
603
|
+
*,
|
|
604
|
+
capture_content: bool = True,
|
|
605
|
+
session_id: str | None = None,
|
|
606
|
+
id: str | None = None,
|
|
607
|
+
) -> None:
|
|
608
|
+
# `AbstractCapability` is a `@dataclass(init=False)`, so it contributes
|
|
609
|
+
# class-level defaults (`id`, `defer_loading`, …) and no `__init__` to
|
|
610
|
+
# chain to; assigning the ones we care about is the supported pattern.
|
|
611
|
+
self.capture_content = capture_content
|
|
612
|
+
self.session_id = session_id
|
|
613
|
+
self.id = id
|
|
614
|
+
|
|
615
|
+
def get_ordering(self) -> CapabilityOrdering:
|
|
616
|
+
"""Outermost, so our spans bracket every other capability's work.
|
|
617
|
+
|
|
618
|
+
The built-in `Instrumentation` declares the same tier; ties break on the
|
|
619
|
+
user's list order, which is fine — we are not exchanging state with it.
|
|
620
|
+
"""
|
|
621
|
+
return CapabilityOrdering(position="outermost")
|
|
622
|
+
|
|
623
|
+
# -- run --------------------------------------------------------------
|
|
624
|
+
|
|
625
|
+
async def wrap_run(self, ctx, *, handler):
|
|
626
|
+
if not _enabled:
|
|
627
|
+
return await handler()
|
|
628
|
+
state = self._begin_run(ctx)
|
|
629
|
+
try:
|
|
630
|
+
result = await handler()
|
|
631
|
+
except BaseException as exc:
|
|
632
|
+
# Note the structure: the framework's call sits in exactly one
|
|
633
|
+
# `try`, whose only job is to re-raise. Everything of ours is
|
|
634
|
+
# outside it and inside `safe()`, so nothing we do can change what
|
|
635
|
+
# the run returns or raises.
|
|
636
|
+
self._end_run(state, None, exc)
|
|
637
|
+
raise
|
|
638
|
+
self._end_run(state, result, None)
|
|
639
|
+
return result
|
|
640
|
+
|
|
641
|
+
@safe
|
|
642
|
+
def _begin_run(self, ctx) -> _RunState:
|
|
643
|
+
key = _run_key(ctx)
|
|
644
|
+
ambient = failproofai_sdk.current()
|
|
645
|
+
# An already-bound session wins. Handing `start_agent` an explicit
|
|
646
|
+
# session_id would otherwise split a hand-written outer scope's run into
|
|
647
|
+
# a second session.
|
|
648
|
+
session_id = self.session_id or (
|
|
649
|
+
None if ambient.session_id else (getattr(ctx, "conversation_id", None) or getattr(ctx, "run_id", None))
|
|
650
|
+
)
|
|
651
|
+
identity = _tracker.start_agent(
|
|
652
|
+
key,
|
|
653
|
+
agent_id=_agent_name(ctx),
|
|
654
|
+
session_id=session_id,
|
|
655
|
+
goal=_text(getattr(ctx, "prompt", None)) if self.capture_content else None,
|
|
656
|
+
**fw_fields(
|
|
657
|
+
run_id=getattr(ctx, "run_id", None),
|
|
658
|
+
conversation_id=getattr(ctx, "conversation_id", None),
|
|
659
|
+
model=_model_name(getattr(ctx, "model", None)),
|
|
660
|
+
metadata=getattr(ctx, "metadata", None),
|
|
661
|
+
),
|
|
662
|
+
)
|
|
663
|
+
state = _RunState(key)
|
|
664
|
+
_register_run(state)
|
|
665
|
+
try:
|
|
666
|
+
scope = failproofai_sdk.session(identity.session_id, agent_id=identity.agent_id)
|
|
667
|
+
scope.__enter__()
|
|
668
|
+
state.scope = scope
|
|
669
|
+
except Exception: # pragma: no cover - two contextvar sets cannot fail
|
|
670
|
+
# Deliberately not re-raised: the agent is already open, and the
|
|
671
|
+
# state we return is what closes it. A lost contextvar binding costs
|
|
672
|
+
# nesting for a nested run; a lost `agent_end` costs a session that
|
|
673
|
+
# renders `ongoing` forever.
|
|
674
|
+
logger.debug("failproofai_sdk: could not bind pydantic-ai run identity", exc_info=True)
|
|
675
|
+
return state
|
|
676
|
+
|
|
677
|
+
@safe
|
|
678
|
+
def _end_run(self, state: "_RunState | None", result: Any, exc: "BaseException | None") -> None:
|
|
679
|
+
if state is None:
|
|
680
|
+
return
|
|
681
|
+
if not _claim_run(state):
|
|
682
|
+
# `uninstall()` tore this run down already and emitted its agent_end.
|
|
683
|
+
# A second one would close a span the dashboard has already closed.
|
|
684
|
+
if state.scope is not None:
|
|
685
|
+
state.scope.__exit__(None, None, None)
|
|
686
|
+
state.scope = None
|
|
687
|
+
return
|
|
688
|
+
try:
|
|
689
|
+
# Strictly before agent_end, for the same reason the `error` event
|
|
690
|
+
# below is: the dashboard closes the agent span at agent_end.
|
|
691
|
+
_core.call_safely(_close_spans, (state, exc), {}, f"{__name__}.close_spans")
|
|
692
|
+
usage = _usage_dict(getattr(result, "usage", None)) if result is not None else None
|
|
693
|
+
if exc is None:
|
|
694
|
+
outcome = "success"
|
|
695
|
+
summary = _text(getattr(result, "output", None)) if self.capture_content else None
|
|
696
|
+
elif _is_control_flow(exc):
|
|
697
|
+
# A cancellation is not an error: it must not pollute the Errors
|
|
698
|
+
# surface, so no `error` event and `outcome="cancelled"`.
|
|
699
|
+
outcome, summary = "cancelled", None
|
|
700
|
+
else:
|
|
701
|
+
outcome, summary = "failed", None
|
|
702
|
+
# Strictly BEFORE agent_end — the dashboard closes the agent span
|
|
703
|
+
# at agent_end and anything after it is attributed to nothing.
|
|
704
|
+
#
|
|
705
|
+
# Only when no leaf owns it. The comment here used to assert
|
|
706
|
+
# "no leaf span owns it", which is false for the escaping case:
|
|
707
|
+
# `_end_tool` and `_end_model` have ALREADY set `error` on the
|
|
708
|
+
# `tool_result`/`model_response` for this very exception, so a
|
|
709
|
+
# failing `@agent.tool_plain` produced BOTH `tool_result(error=…)`
|
|
710
|
+
# and a standalone `error`. `sessionSummary.errorCount` then read
|
|
711
|
+
# 2 for Pydantic AI and 1 for the identical failure under any
|
|
712
|
+
# other adapter, so error rates were not comparable across
|
|
713
|
+
# frameworks and Pydantic AI runs looked twice as failure-prone.
|
|
714
|
+
if not state.reported_error:
|
|
715
|
+
_tracker.emit(
|
|
716
|
+
"error",
|
|
717
|
+
state.key,
|
|
718
|
+
error_type=type(exc).__name__,
|
|
719
|
+
message=str(exc),
|
|
720
|
+
traceback=_format_traceback(exc),
|
|
721
|
+
)
|
|
722
|
+
# "failed", never "failure": the server counts only
|
|
723
|
+
# error|failed|timeout|rejected as a failure.
|
|
724
|
+
_tracker.end_agent(state.key, outcome=outcome, summary=summary, usage=usage)
|
|
725
|
+
finally:
|
|
726
|
+
if state.scope is not None:
|
|
727
|
+
state.scope.__exit__(None, None, None)
|
|
728
|
+
state.scope = None
|
|
729
|
+
|
|
730
|
+
# -- model ------------------------------------------------------------
|
|
731
|
+
|
|
732
|
+
async def wrap_model_request(self, ctx, *, request_context, handler):
|
|
733
|
+
if not _enabled:
|
|
734
|
+
return await handler(request_context)
|
|
735
|
+
state = self._begin_model(ctx, request_context)
|
|
736
|
+
try:
|
|
737
|
+
response = await handler(request_context)
|
|
738
|
+
except BaseException as exc:
|
|
739
|
+
self._end_model(ctx, state, None, exc)
|
|
740
|
+
raise
|
|
741
|
+
self._end_model(ctx, state, response, None)
|
|
742
|
+
return response
|
|
743
|
+
|
|
744
|
+
@safe
|
|
745
|
+
def _begin_model(self, ctx, request_context) -> _SpanState:
|
|
746
|
+
# `request_id` is generated here and carried onto the response. The
|
|
747
|
+
# dashboard pairs model events FIFO per agent_id when it is absent, which
|
|
748
|
+
# mis-pairs the moment two model calls overlap.
|
|
749
|
+
state = _SpanState(_run_key(ctx), "model", uuid.uuid4().hex)
|
|
750
|
+
model = getattr(request_context, "model", None)
|
|
751
|
+
state.extra["model"] = _model_name(model)
|
|
752
|
+
# Carried onto the response because that is the event `duration_ms`
|
|
753
|
+
# rides on — see the note there.
|
|
754
|
+
state.extra["streaming"] = getattr(request_context, "streaming", None)
|
|
755
|
+
_register_span(state)
|
|
756
|
+
messages, omitted = _render_messages(
|
|
757
|
+
getattr(request_context, "messages", None), self.capture_content
|
|
758
|
+
)
|
|
759
|
+
_tracker.emit(
|
|
760
|
+
"model_request",
|
|
761
|
+
None,
|
|
762
|
+
parent_key=state.key,
|
|
763
|
+
model=state.extra["model"],
|
|
764
|
+
system=getattr(model, "system", None),
|
|
765
|
+
messages=messages,
|
|
766
|
+
tools=_render_tools(
|
|
767
|
+
getattr(request_context, "model_request_parameters", None), self.capture_content
|
|
768
|
+
),
|
|
769
|
+
request_id=state.correlation_id,
|
|
770
|
+
**fw_fields(
|
|
771
|
+
run_step=getattr(ctx, "run_step", None),
|
|
772
|
+
streaming=getattr(request_context, "streaming", None),
|
|
773
|
+
model_id=getattr(request_context, "model_id", None),
|
|
774
|
+
messages_omitted=omitted or None,
|
|
775
|
+
),
|
|
776
|
+
)
|
|
777
|
+
return state
|
|
778
|
+
|
|
779
|
+
@safe
|
|
780
|
+
def _end_model(self, ctx, state: "_SpanState | None", response: Any, exc: "BaseException | None") -> None:
|
|
781
|
+
if state is None or not _claim_span(state):
|
|
782
|
+
return
|
|
783
|
+
usage = getattr(response, "usage", None) if response is not None else None
|
|
784
|
+
error = None
|
|
785
|
+
if exc is not None and not _is_control_flow(exc):
|
|
786
|
+
error = _describe(exc)
|
|
787
|
+
_mark_reported_error(state)
|
|
788
|
+
_tracker.emit(
|
|
789
|
+
"model_response",
|
|
790
|
+
None,
|
|
791
|
+
parent_key=state.key,
|
|
792
|
+
model=_model_name(response) or state.extra.get("model"),
|
|
793
|
+
stop_reason=getattr(response, "finish_reason", None),
|
|
794
|
+
input_tokens=_token(usage, "input_tokens"),
|
|
795
|
+
output_tokens=_token(usage, "output_tokens"),
|
|
796
|
+
content=(
|
|
797
|
+
[_render_part(p, True) for p in getattr(response, "parts", ()) or ()]
|
|
798
|
+
if response is not None and self.capture_content
|
|
799
|
+
else None
|
|
800
|
+
),
|
|
801
|
+
role="assistant",
|
|
802
|
+
request_id=state.correlation_id,
|
|
803
|
+
error=error,
|
|
804
|
+
# Always an int, always present. `duration_ms` is not guarded on
|
|
805
|
+
# model_response and the dashboard prefers the closing event's value
|
|
806
|
+
# over end-start, which is what keeps model durations honest even
|
|
807
|
+
# when FIFO pairing brackets the wrong pair. A float would silently
|
|
808
|
+
# NULL the column.
|
|
809
|
+
#
|
|
810
|
+
# On a STREAMED request this number is the whole `async with
|
|
811
|
+
# agent.run_stream(...)` block, consumer included, and not the model
|
|
812
|
+
# call: Pydantic AI hands `wrap_model_request`'s handler its
|
|
813
|
+
# `ModelResponse` only once the caller leaves that block, and
|
|
814
|
+
# `after_model_request` fires later still (measured: 818.7ms vs
|
|
815
|
+
# 317.6ms for the drain). There is no earlier hook — `stream_text`
|
|
816
|
+
# is the caller's own loop and `wrap_run_event_stream` cannot be
|
|
817
|
+
# overridden here without switching `agent.run()` into streaming
|
|
818
|
+
# mode. Measured against the gateway, 1.5s of `asyncio.sleep` in the
|
|
819
|
+
# consumer moved a 3294ms response to 4677ms. So the number cannot
|
|
820
|
+
# be made honest, only made *identifiable*: `fw_streaming` rides on
|
|
821
|
+
# the response as well as the request, so a latency percentile can
|
|
822
|
+
# exclude the rows where it is true rather than quietly averaging
|
|
823
|
+
# somebody's UI render time into the model's p95.
|
|
824
|
+
duration_ms=state.elapsed_ms(),
|
|
825
|
+
usage=_usage_dict(usage),
|
|
826
|
+
**fw_fields(
|
|
827
|
+
provider=getattr(response, "provider_name", None),
|
|
828
|
+
provider_response_id=getattr(response, "provider_response_id", None),
|
|
829
|
+
run_step=getattr(ctx, "run_step", None),
|
|
830
|
+
streaming=state.extra.get("streaming"),
|
|
831
|
+
),
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# -- tools ------------------------------------------------------------
|
|
835
|
+
|
|
836
|
+
async def wrap_tool_execute(self, ctx, *, call, tool_def, args, handler):
|
|
837
|
+
if not _enabled:
|
|
838
|
+
return await handler(args)
|
|
839
|
+
state = self._begin_tool(ctx, call, tool_def, args)
|
|
840
|
+
try:
|
|
841
|
+
result = await handler(args)
|
|
842
|
+
except BaseException as exc:
|
|
843
|
+
self._end_tool(state, None, exc)
|
|
844
|
+
raise
|
|
845
|
+
self._end_tool(state, result, None)
|
|
846
|
+
return result
|
|
847
|
+
|
|
848
|
+
@safe
|
|
849
|
+
def _begin_tool(self, ctx, call, tool_def, args) -> _SpanState:
|
|
850
|
+
# The framework's own id, verbatim: it is what appears in the provider's
|
|
851
|
+
# logs, and inventing a synthetic one would destroy that correspondence.
|
|
852
|
+
# `duration_ms` on tool_result is auto-computed by the SDK from this key,
|
|
853
|
+
# so tool_use and tool_result must agree on it exactly.
|
|
854
|
+
tool_call_id = getattr(call, "tool_call_id", None) or uuid.uuid4().hex
|
|
855
|
+
tool_name = getattr(call, "tool_name", None) or getattr(tool_def, "name", None) or "tool"
|
|
856
|
+
state = _SpanState(_run_key(ctx), "tool", tool_call_id, {"tool_name": tool_name})
|
|
857
|
+
_register_span(state)
|
|
858
|
+
_tracker.emit(
|
|
859
|
+
"tool_use",
|
|
860
|
+
None,
|
|
861
|
+
parent_key=state.key,
|
|
862
|
+
tool_name=tool_name,
|
|
863
|
+
tool_call_id=tool_call_id,
|
|
864
|
+
input=args if self.capture_content and isinstance(args, dict) else None,
|
|
865
|
+
**fw_fields(
|
|
866
|
+
tool_kind=getattr(tool_def, "kind", None),
|
|
867
|
+
run_step=getattr(ctx, "run_step", None),
|
|
868
|
+
),
|
|
869
|
+
)
|
|
870
|
+
return state
|
|
871
|
+
|
|
872
|
+
@safe
|
|
873
|
+
def _end_tool(self, state: "_SpanState | None", result: Any, exc: "BaseException | None") -> None:
|
|
874
|
+
if state is None or not _claim_span(state):
|
|
875
|
+
return
|
|
876
|
+
error = None
|
|
877
|
+
if exc is not None and not _is_control_flow(exc):
|
|
878
|
+
error = _describe(exc)
|
|
879
|
+
_mark_reported_error(state)
|
|
880
|
+
# No `error` event here, ever. A tool failure the agent loop catches is
|
|
881
|
+
# not a run-level error, and one that escapes is reported exactly once —
|
|
882
|
+
# HERE, on this span, which is why `_end_run` is now told about it.
|
|
883
|
+
_tracker.emit(
|
|
884
|
+
"tool_result",
|
|
885
|
+
None,
|
|
886
|
+
parent_key=state.key,
|
|
887
|
+
tool_name=state.extra.get("tool_name"),
|
|
888
|
+
tool_call_id=state.correlation_id,
|
|
889
|
+
output=_tool_output(result) if self.capture_content else None,
|
|
890
|
+
error=error,
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
# ---------------------------------------------------------------------------
|
|
895
|
+
# install / uninstall
|
|
896
|
+
# ---------------------------------------------------------------------------
|
|
897
|
+
|
|
898
|
+
def _inject(kwargs: dict) -> None:
|
|
899
|
+
capability = _capability
|
|
900
|
+
if capability is None:
|
|
901
|
+
return
|
|
902
|
+
existing = kwargs.get("capabilities")
|
|
903
|
+
items = list(existing) if existing else []
|
|
904
|
+
if any(isinstance(item, FailproofAI) for item in items):
|
|
905
|
+
# Somebody passed `capabilities=[FailproofAI()]` explicitly. Theirs wins;
|
|
906
|
+
# two of us would double every event.
|
|
907
|
+
return
|
|
908
|
+
items.append(capability)
|
|
909
|
+
kwargs["capabilities"] = items
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def _wrap_init(original):
|
|
913
|
+
"""`Agent.__init__` with our capability appended to `capabilities=`.
|
|
914
|
+
|
|
915
|
+
Not `_core.wrap_callable`: that shape deliberately cannot touch the wrapped
|
|
916
|
+
callable's arguments (its whole guarantee is that the user's call is
|
|
917
|
+
untouched), and injecting a capability is precisely an argument mutation.
|
|
918
|
+
The `Patcher` still owns the save/restore and the "somebody patched on top
|
|
919
|
+
of us" check, so the install discipline is unchanged.
|
|
920
|
+
"""
|
|
921
|
+
|
|
922
|
+
def _failproofai_agent_init(self, *args, **kwargs):
|
|
923
|
+
_core.call_safely(_inject, (kwargs,), {}, f"{__name__}.Agent.__init__")
|
|
924
|
+
return original(self, *args, **kwargs)
|
|
925
|
+
|
|
926
|
+
_failproofai_agent_init.__name__ = getattr(original, "__name__", "__init__")
|
|
927
|
+
_failproofai_agent_init.__qualname__ = getattr(original, "__qualname__", "Agent.__init__")
|
|
928
|
+
_failproofai_agent_init.__doc__ = getattr(original, "__doc__", None)
|
|
929
|
+
return _failproofai_agent_init
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
class _Adapter:
|
|
933
|
+
name = NAME
|
|
934
|
+
module = MODULE
|
|
935
|
+
|
|
936
|
+
def install(self, **options: Any) -> None:
|
|
937
|
+
"""Append `FailproofAI()` to every `Agent` constructed from now on.
|
|
938
|
+
|
|
939
|
+
Unknown options are ignored rather than rejected: `failproofai_sdk.instrument()`
|
|
940
|
+
with no framework name fans the same `**options` out to every detected
|
|
941
|
+
adapter, so a keyword meant for LangChain must not take this one down.
|
|
942
|
+
"""
|
|
943
|
+
global _enabled, _capability
|
|
944
|
+
|
|
945
|
+
_compat.check_version(
|
|
946
|
+
NAME, DIST, minimum=MIN_VERSION, below=BELOW_VERSION, reason=_VERSION_REASON
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
from pydantic_ai import Agent
|
|
950
|
+
|
|
951
|
+
_capability = FailproofAI(
|
|
952
|
+
capture_content=bool(options.get("capture_content", True)),
|
|
953
|
+
session_id=options.get("session_id"),
|
|
954
|
+
)
|
|
955
|
+
_patcher.patch(Agent, "__init__", _wrap_init(Agent.__init__))
|
|
956
|
+
_enabled = True
|
|
957
|
+
|
|
958
|
+
def uninstall(self) -> None:
|
|
959
|
+
global _enabled, _capability
|
|
960
|
+
|
|
961
|
+
_enabled = False
|
|
962
|
+
_capability = None
|
|
963
|
+
_patcher.restore_all()
|
|
964
|
+
# Close every agent still open, so a session torn down mid-run does not
|
|
965
|
+
# render `ongoing` forever. Runs this adapter opened go through
|
|
966
|
+
# `_close_run` — leaves first, and `_claim_run` so the run's own
|
|
967
|
+
# `wrap_run` frame does not emit a second `agent_end` when it finally
|
|
968
|
+
# unwinds. `close_open_agents()` stays as the backstop for a key that
|
|
969
|
+
# reached the tracker without reaching `_open_runs`.
|
|
970
|
+
_close_open_runs()
|
|
971
|
+
_tracker.close_open_agents()
|
|
972
|
+
_tracker.reset()
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
adapter = _Adapter()
|
|
976
|
+
install = adapter.install
|
|
977
|
+
uninstall = adapter.uninstall
|