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,1076 @@
|
|
|
1
|
+
"""The parts every framework adapter shares: failure policy, patching, identity.
|
|
2
|
+
|
|
3
|
+
An adapter under `failproofai_sdk/integrations/` is supposed to be a **translation
|
|
4
|
+
table** and nothing else. Everything that is genuinely hard — never raising into
|
|
5
|
+
the customer's call stack, restoring exactly what we replaced, mapping a
|
|
6
|
+
framework's run ids onto Failproof AI identity, keeping payloads inside the store's
|
|
7
|
+
patience — lives here, in one copy. If an adapter needs something added to this
|
|
8
|
+
module, that is a signal the core is wrong, not that the adapter is special.
|
|
9
|
+
|
|
10
|
+
Three things in here are load-bearing and easy to "fix" into a bug:
|
|
11
|
+
|
|
12
|
+
* `safe()` catches `Exception`, **never `BaseException`** — see the comment on
|
|
13
|
+
it before you change that.
|
|
14
|
+
* `RunTracker` never touches contextvars. `ContextVar.reset(token)` raises
|
|
15
|
+
across asyncio tasks as well as threads, so a callback surface whose start and
|
|
16
|
+
end are separate calls can never hold a token between them.
|
|
17
|
+
* `fw_fields()` is a safety rule, not a style rule. `_schema._build()` merges
|
|
18
|
+
extra fields **last**, so an extra named `tool_name` silently overwrites the
|
|
19
|
+
declared one and changes the promoted column.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import dataclasses
|
|
23
|
+
import functools
|
|
24
|
+
import logging
|
|
25
|
+
import re
|
|
26
|
+
import threading
|
|
27
|
+
import uuid
|
|
28
|
+
from collections.abc import Mapping, Sequence
|
|
29
|
+
from collections.abc import Set as AbcSet
|
|
30
|
+
from typing import Any, Callable, Protocol
|
|
31
|
+
|
|
32
|
+
from failproofai_sdk import _context, _runtime, _schema
|
|
33
|
+
from failproofai_sdk._context import DEFAULT_AGENT_ID, Identity
|
|
34
|
+
from failproofai_sdk._events import _RESERVED
|
|
35
|
+
from failproofai_sdk._version import __version__
|
|
36
|
+
from failproofai_sdk.integrations import _compat
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("failproofai_sdk.integrations")
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Adapter",
|
|
42
|
+
"RunTracker",
|
|
43
|
+
"Patcher",
|
|
44
|
+
"safe",
|
|
45
|
+
"call_safely",
|
|
46
|
+
"wrap_callable",
|
|
47
|
+
"is_wrapped",
|
|
48
|
+
"unwrap",
|
|
49
|
+
"strict",
|
|
50
|
+
"set_strict",
|
|
51
|
+
"reset_failures",
|
|
52
|
+
"truncate",
|
|
53
|
+
"payload",
|
|
54
|
+
"fw_fields",
|
|
55
|
+
"guard_extras",
|
|
56
|
+
"framework_fields",
|
|
57
|
+
"normalize_agent_id",
|
|
58
|
+
"ms",
|
|
59
|
+
"FIELD_LIMIT",
|
|
60
|
+
"EVENT_BUDGET",
|
|
61
|
+
"FORBIDDEN_EXTRAS",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# The adapter protocol
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
class Adapter(Protocol):
|
|
70
|
+
"""What `failproofai_sdk/integrations/<framework>.py` must expose as `adapter`.
|
|
71
|
+
|
|
72
|
+
`module` is the framework module that must already be in `sys.modules` for
|
|
73
|
+
auto-detection to pick this adapter up; the registry keeps its own copy of
|
|
74
|
+
that mapping so detection never has to import anything.
|
|
75
|
+
|
|
76
|
+
`install()` must save the **original attribute object** it replaces (use
|
|
77
|
+
`Patcher`), and `uninstall()` must restore that saved object rather than
|
|
78
|
+
re-importing or reconstructing it.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
module: str
|
|
83
|
+
|
|
84
|
+
def install(self, **options: Any) -> None:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def uninstall(self) -> None:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# Failure policy
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
# Everything under `integrations/` obeys one rule: never raise into the
|
|
96
|
+
# customer's call stack. Observability that takes the process down with it is
|
|
97
|
+
# worse than no observability. FAILPROOFAI_SDK_STRICT=1 inverts that for tests and for
|
|
98
|
+
# debugging an adapter that has gone quiet — without it you can only ever prove
|
|
99
|
+
# "it didn't crash", never "it swallowed the right thing".
|
|
100
|
+
_strict: bool | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def strict() -> bool:
|
|
104
|
+
global _strict
|
|
105
|
+
if _strict is None:
|
|
106
|
+
_strict = _compat._env_flag("FAILPROOFAI_SDK_STRICT")
|
|
107
|
+
return _strict
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def set_strict(value: bool | None) -> None:
|
|
111
|
+
"""Override the flag. `None` re-reads `FAILPROOFAI_SDK_STRICT`."""
|
|
112
|
+
global _strict
|
|
113
|
+
_strict = value
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# After this many failures at one call site we stop calling it. A broken adapter
|
|
117
|
+
# should cost one log line, not 40% of the process and a full disk.
|
|
118
|
+
_MAX_FAILURES = 3
|
|
119
|
+
|
|
120
|
+
_failures: dict[str, int] = {}
|
|
121
|
+
_disabled: set[str] = set()
|
|
122
|
+
_failure_lock = threading.Lock()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def reset_failures() -> None:
|
|
126
|
+
"""Re-enable every degraded call site (tests; also `uninstrument()`)."""
|
|
127
|
+
with _failure_lock:
|
|
128
|
+
_failures.clear()
|
|
129
|
+
_disabled.clear()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def is_degraded(site: str) -> bool:
|
|
133
|
+
return site in _disabled
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _site_of(fn: Callable[..., Any]) -> str:
|
|
137
|
+
module = getattr(fn, "__module__", None) or "?"
|
|
138
|
+
qualname = getattr(fn, "__qualname__", None) or getattr(fn, "__name__", None) or repr(fn)
|
|
139
|
+
return f"{module}.{qualname}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def call_safely(fn: Callable[..., Any], args: tuple, kwargs: dict, site: str) -> Any:
|
|
143
|
+
"""Call `fn`, swallowing `Exception` and degrading a repeatedly failing site.
|
|
144
|
+
|
|
145
|
+
Catches `Exception` and **not** `BaseException` on purpose.
|
|
146
|
+
`asyncio.CancelledError` has been a `BaseException` since Python 3.8, as is
|
|
147
|
+
`KeyboardInterrupt` and `SystemExit`; swallowing those would silently break
|
|
148
|
+
cancellation in every instrumented async application — the task gets
|
|
149
|
+
cancelled, our handler eats the CancelledError, and the framework carries on
|
|
150
|
+
running work that was supposed to stop. `GeneratorExit` is the same story
|
|
151
|
+
for generators. If you are here to "fix" this to `BaseException`, don't.
|
|
152
|
+
"""
|
|
153
|
+
if site in _disabled:
|
|
154
|
+
return None
|
|
155
|
+
try:
|
|
156
|
+
return fn(*args, **kwargs)
|
|
157
|
+
except Exception:
|
|
158
|
+
if strict():
|
|
159
|
+
raise
|
|
160
|
+
count = 0
|
|
161
|
+
newly_disabled = False
|
|
162
|
+
with _failure_lock:
|
|
163
|
+
count = _failures.get(site, 0) + 1
|
|
164
|
+
_failures[site] = count
|
|
165
|
+
if count >= _MAX_FAILURES and site not in _disabled:
|
|
166
|
+
_disabled.add(site)
|
|
167
|
+
newly_disabled = True
|
|
168
|
+
if count == 1:
|
|
169
|
+
# Logged once per site, with the traceback. Repeats are silent:
|
|
170
|
+
# a hook that fails on every token of a streaming response would
|
|
171
|
+
# otherwise become the log volume.
|
|
172
|
+
logger.warning(
|
|
173
|
+
"failproofai_sdk: instrumentation hook %s failed; the instrumented call was "
|
|
174
|
+
"not affected. Set FAILPROOFAI_SDK_STRICT=1 to re-raise.",
|
|
175
|
+
site,
|
|
176
|
+
exc_info=True,
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
logger.debug("failproofai_sdk: instrumentation hook %s failed again (%d)", site, count)
|
|
180
|
+
if newly_disabled:
|
|
181
|
+
logger.error(
|
|
182
|
+
"failproofai_sdk: instrumentation hook %s failed %d times and is now disabled "
|
|
183
|
+
"for the rest of this process. Events from it will be missing.",
|
|
184
|
+
site,
|
|
185
|
+
count,
|
|
186
|
+
)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def safe(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
191
|
+
"""Decorator form of `call_safely`. Put it on every callback an adapter exposes."""
|
|
192
|
+
site = _site_of(fn)
|
|
193
|
+
|
|
194
|
+
@functools.wraps(fn)
|
|
195
|
+
def _failproofai_safe(*args: Any, **kwargs: Any) -> Any:
|
|
196
|
+
return call_safely(fn, args, kwargs, site)
|
|
197
|
+
|
|
198
|
+
_failproofai_safe.__failproofai_safe__ = True # type: ignore[attr-defined]
|
|
199
|
+
return _failproofai_safe
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _safe_call(fn: Callable[..., Any] | None, *args: Any, **kwargs: Any) -> Any:
|
|
203
|
+
if fn is None:
|
|
204
|
+
return None
|
|
205
|
+
return call_safely(fn, args, kwargs, _site_of(fn))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Shape A — wrapper surfaces
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
def wrap_callable(
|
|
213
|
+
original: Callable[..., Any],
|
|
214
|
+
*,
|
|
215
|
+
before: Callable[..., Any] | None = None,
|
|
216
|
+
after: Callable[..., Any] | None = None,
|
|
217
|
+
on_error: Callable[..., Any] | None = None,
|
|
218
|
+
) -> Callable[..., Any]:
|
|
219
|
+
"""Wrap a framework callable so start and end are one frame.
|
|
220
|
+
|
|
221
|
+
* `before(*args, **kwargs)` -> an opaque ctx handed back to the others;
|
|
222
|
+
* `after(ctx, result)`;
|
|
223
|
+
* `on_error(ctx, exc)` — then the exception is re-raised, always.
|
|
224
|
+
|
|
225
|
+
The structural guarantee, which is the whole reason this is a function and
|
|
226
|
+
not hand-written try/except in five adapters: **the user's call sits in
|
|
227
|
+
exactly one `try`, whose only job is to re-raise.** Nothing we do can change
|
|
228
|
+
what the wrapped callable returns or raises, because every one of our own
|
|
229
|
+
calls is outside that block and inside `call_safely`. That is auditable in
|
|
230
|
+
nine lines, and there is a test asserting the exception comes back out with
|
|
231
|
+
its `is` identity intact even when all three hooks raise.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
@functools.wraps(original)
|
|
235
|
+
def _failproofai_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
236
|
+
ctx = _safe_call(before, *args, **kwargs)
|
|
237
|
+
try:
|
|
238
|
+
result = original(*args, **kwargs)
|
|
239
|
+
except BaseException as exc: # noqa: BLE001 - re-raised unconditionally
|
|
240
|
+
_safe_call(on_error, ctx, exc)
|
|
241
|
+
raise
|
|
242
|
+
_safe_call(after, ctx, result)
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
_failproofai_wrapper.__failproofai_wrapped__ = original # type: ignore[attr-defined]
|
|
246
|
+
return _failproofai_wrapper
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def is_wrapped(obj: Any) -> bool:
|
|
250
|
+
return hasattr(obj, "__failproofai_wrapped__")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def unwrap(obj: Any) -> Any:
|
|
254
|
+
"""The object we replaced, or `obj` itself if we never wrapped it."""
|
|
255
|
+
return getattr(obj, "__failproofai_wrapped__", obj)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# Install / uninstall discipline
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
class Patcher:
|
|
263
|
+
"""Records what an `install()` replaced so `uninstall()` can put it back.
|
|
264
|
+
|
|
265
|
+
Two rules, both of which exist because instrumentation libraries are
|
|
266
|
+
routinely installed alongside each other:
|
|
267
|
+
|
|
268
|
+
1. **Restore the saved object, never a re-import.** Re-importing to restore
|
|
269
|
+
hands back whatever the *current* value of the attribute's source is,
|
|
270
|
+
which is how two instrumentation libraries silently un-patch each other.
|
|
271
|
+
2. **If the attribute is no longer ours, leave it alone.** Somebody patched
|
|
272
|
+
on top of us; restoring would delete their patch. We log at WARNING and
|
|
273
|
+
keep our record, so the customer can see it happened.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
__slots__ = ("_records", "_lock")
|
|
277
|
+
|
|
278
|
+
def __init__(self) -> None:
|
|
279
|
+
self._records: list[tuple[Any, str, Any, Any, bool]] = []
|
|
280
|
+
self._lock = threading.Lock()
|
|
281
|
+
|
|
282
|
+
def patch(self, obj: Any, attr: str, new: Any) -> None:
|
|
283
|
+
"""Set `obj.attr = new`, remembering the exact object replaced."""
|
|
284
|
+
existed = hasattr(obj, attr)
|
|
285
|
+
original = getattr(obj, attr, None)
|
|
286
|
+
try:
|
|
287
|
+
new.__failproofai_wrapped__ = original
|
|
288
|
+
except (AttributeError, TypeError):
|
|
289
|
+
# builtins, slots, C functions — the marker is best-effort, the
|
|
290
|
+
# identity check below falls back to `is` against what we stored.
|
|
291
|
+
pass
|
|
292
|
+
setattr(obj, attr, new)
|
|
293
|
+
with self._lock:
|
|
294
|
+
self._records.append((obj, attr, original, new, existed))
|
|
295
|
+
|
|
296
|
+
def restore_all(self) -> None:
|
|
297
|
+
"""Undo every patch, newest first. Never raises."""
|
|
298
|
+
with self._lock:
|
|
299
|
+
records = list(reversed(self._records))
|
|
300
|
+
self._records.clear()
|
|
301
|
+
for obj, attr, original, installed, existed in records:
|
|
302
|
+
try:
|
|
303
|
+
current = getattr(obj, attr, None)
|
|
304
|
+
if current is not installed:
|
|
305
|
+
logger.warning(
|
|
306
|
+
"failproofai_sdk: not restoring %s.%s — it is no longer the object "
|
|
307
|
+
"failproofai_sdk installed (something else patched on top). Leaving "
|
|
308
|
+
"the current value in place rather than deleting their patch.",
|
|
309
|
+
getattr(obj, "__name__", type(obj).__name__),
|
|
310
|
+
attr,
|
|
311
|
+
)
|
|
312
|
+
continue
|
|
313
|
+
if existed:
|
|
314
|
+
setattr(obj, attr, original)
|
|
315
|
+
else:
|
|
316
|
+
delattr(obj, attr)
|
|
317
|
+
except Exception:
|
|
318
|
+
logger.warning(
|
|
319
|
+
"failproofai_sdk: failed to restore %s.%s", obj, attr, exc_info=True
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def __len__(self) -> int:
|
|
323
|
+
return len(self._records)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ---------------------------------------------------------------------------
|
|
327
|
+
# Payload discipline
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
TRUNCATION_MARKER = "…[truncated]"
|
|
331
|
+
FIELD_LIMIT = 8192
|
|
332
|
+
|
|
333
|
+
#: How many MAX-SIZE fields one event may carry before `payload()` starts
|
|
334
|
+
#: dropping keys. The budget is DERIVED from the field limit rather than being a
|
|
335
|
+
#: second independent number, because the two are not independent: raising one
|
|
336
|
+
#: without the other silently changes how much survives.
|
|
337
|
+
#:
|
|
338
|
+
#: Measured against real traffic before choosing it — a live event from each of
|
|
339
|
+
#: the five framework adapters carries 7-8 `fw_*` fields, so 16 leaves roughly
|
|
340
|
+
#: 2x headroom at the theoretical maximum. It matches the ratio the LangChain
|
|
341
|
+
#: adapter shipped with (32 KiB budget over a 2 KiB field limit), which is where
|
|
342
|
+
#: the number comes from; it is a preserved property, not a fresh guess.
|
|
343
|
+
#:
|
|
344
|
+
#: This matters because of HOW `payload()` runs out: past the budget it does not
|
|
345
|
+
#: shorten the next field, it OMITS THE KEY (see the `remaining <= 0` branch).
|
|
346
|
+
#: A caller raising `field_limit` therefore has to raise the budget in step or
|
|
347
|
+
#: it trades shortened values for missing ones, which is strictly worse — the
|
|
348
|
+
#: event stops saying that anything is absent.
|
|
349
|
+
_FIELDS_PER_EVENT = 16
|
|
350
|
+
|
|
351
|
+
EVENT_BUDGET = FIELD_LIMIT * _FIELDS_PER_EVENT
|
|
352
|
+
_MAX_ITEMS = 100
|
|
353
|
+
_MAX_DEPTH = 6
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class _Cut:
|
|
357
|
+
"""Mutable 'did we cut anything' flag, threaded through the recursion."""
|
|
358
|
+
|
|
359
|
+
__slots__ = ("hit",)
|
|
360
|
+
|
|
361
|
+
def __init__(self) -> None:
|
|
362
|
+
self.hit = False
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class _Budget:
|
|
366
|
+
"""Remaining bytes for a whole event, spent as `_truncate` emits.
|
|
367
|
+
|
|
368
|
+
Mirrors `_size`'s accounting exactly — 8 for a scalar, `len` for a string,
|
|
369
|
+
plus `len(str(k))` per mapping key — because `payload()` compares what it
|
|
370
|
+
emits against `_size`, and a budget that counted differently would let the
|
|
371
|
+
two disagree about whether a field fits.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
__slots__ = ("remaining",)
|
|
375
|
+
|
|
376
|
+
def __init__(self, total: int) -> None:
|
|
377
|
+
self.remaining = total
|
|
378
|
+
|
|
379
|
+
def spend(self, n: int) -> None:
|
|
380
|
+
self.remaining -= n
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def spent_out(self) -> bool:
|
|
384
|
+
return self.remaining <= 0
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def truncate(value: Any, limit: int = FIELD_LIMIT) -> Any:
|
|
388
|
+
"""Shrink a payload value to something a column store will tolerate.
|
|
389
|
+
|
|
390
|
+
Framework payloads are prompts, retrieved documents and tool outputs — the
|
|
391
|
+
three largest strings in the process. None of these are promoted columns, so
|
|
392
|
+
querying them means `JSONExtract` over the payload, which has already caused
|
|
393
|
+
a production memory blowup in the events store here. Payload discipline is not optional.
|
|
394
|
+
"""
|
|
395
|
+
return _truncate(value, limit, _Cut(), 0)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _truncate(value: Any, limit: int, cut: _Cut, depth: int, budget: "_Budget | None" = None) -> Any:
|
|
399
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
400
|
+
if budget is not None:
|
|
401
|
+
budget.spend(8)
|
|
402
|
+
return value
|
|
403
|
+
if isinstance(value, str):
|
|
404
|
+
if len(value) > limit:
|
|
405
|
+
cut.hit = True
|
|
406
|
+
value = value[: max(limit - len(TRUNCATION_MARKER), 0)] + TRUNCATION_MARKER
|
|
407
|
+
# The per-field limit bounds ONE string; the budget bounds the whole
|
|
408
|
+
# event. A structure whose leaves each fit under the limit used to sail
|
|
409
|
+
# past the budget entirely, because `payload()` re-truncated with
|
|
410
|
+
# `remaining` as the per-STRING limit — which changes nothing when every
|
|
411
|
+
# leaf is already shorter than it.
|
|
412
|
+
if budget is not None:
|
|
413
|
+
if len(value) > budget.remaining:
|
|
414
|
+
cut.hit = True
|
|
415
|
+
keep = max(budget.remaining - len(TRUNCATION_MARKER), 0)
|
|
416
|
+
value = value[:keep] + TRUNCATION_MARKER
|
|
417
|
+
budget.spend(len(value))
|
|
418
|
+
return value
|
|
419
|
+
if isinstance(value, bytes):
|
|
420
|
+
return _truncate(value.decode("utf-8", "replace"), limit, cut, depth, budget)
|
|
421
|
+
if depth >= _MAX_DEPTH:
|
|
422
|
+
cut.hit = True
|
|
423
|
+
return _truncate(repr(value), limit, cut, _MAX_DEPTH, budget)
|
|
424
|
+
# `Mapping`/`Sequence`, not `dict`/`list`. The concrete types missed every
|
|
425
|
+
# mapping a framework actually hands us that is not literally a dict —
|
|
426
|
+
# `MappingProxyType` (what `model_json_schema()` and any frozen config
|
|
427
|
+
# returns), `ChainMap`, and every third-party mapping — and those fell
|
|
428
|
+
# through to the repr branch at the bottom. A tool's JSON schema then
|
|
429
|
+
# reached the events store as the STRING
|
|
430
|
+
# `"mappingproxy({'title': 'From Unit', 'type': 'string'})"`: valid JSON
|
|
431
|
+
# holding a Python repr, so `JSONExtract` over it returns nothing and the
|
|
432
|
+
# field is unqueryable rather than merely ugly. Verified in a real stored
|
|
433
|
+
# row — a crewai `model_request.tools[0]…properties.from_unit`.
|
|
434
|
+
if isinstance(value, Mapping):
|
|
435
|
+
out = {}
|
|
436
|
+
for i, (k, v) in enumerate(value.items()):
|
|
437
|
+
if i >= _MAX_ITEMS:
|
|
438
|
+
cut.hit = True
|
|
439
|
+
out["…"] = f"[{len(value) - _MAX_ITEMS} more keys truncated]"
|
|
440
|
+
break
|
|
441
|
+
if budget is not None:
|
|
442
|
+
if budget.spent_out:
|
|
443
|
+
cut.hit = True
|
|
444
|
+
out["…"] = f"[{len(value) - i} more keys truncated]"
|
|
445
|
+
break
|
|
446
|
+
budget.spend(len(str(k)))
|
|
447
|
+
out[str(k)] = _truncate(v, limit, cut, depth + 1, budget)
|
|
448
|
+
return out
|
|
449
|
+
# `str`/`bytes` are Sequences too and are handled above, so they cannot
|
|
450
|
+
# reach here; `Set` is a separate ABC and is not a `Sequence`.
|
|
451
|
+
if isinstance(value, (Sequence, AbcSet)):
|
|
452
|
+
items = list(value)
|
|
453
|
+
out_list = []
|
|
454
|
+
for i, v in enumerate(items[:_MAX_ITEMS]):
|
|
455
|
+
if budget is not None and budget.spent_out:
|
|
456
|
+
cut.hit = True
|
|
457
|
+
out_list.append(f"[{len(items) - i} more items truncated]")
|
|
458
|
+
break
|
|
459
|
+
out_list.append(_truncate(v, limit, cut, depth + 1, budget))
|
|
460
|
+
else:
|
|
461
|
+
if len(items) > _MAX_ITEMS:
|
|
462
|
+
cut.hit = True
|
|
463
|
+
out_list.append(f"[{len(items) - _MAX_ITEMS} more items truncated]")
|
|
464
|
+
return out_list
|
|
465
|
+
# A dataclass or a pydantic model is DATA, and every framework hands us
|
|
466
|
+
# them: a tool's argument model, its structured return, a settings object on
|
|
467
|
+
# a model request. They have no JSON shape by the checks above, so they were
|
|
468
|
+
# rendered — `Weather(city='Faro', celsius=21)` — which is a Python repr
|
|
469
|
+
# sitting inside a JSON string, unqueryable by `JSONExtract` and unfilterable
|
|
470
|
+
# in the dashboard. Each adapter was starting to unwrap them itself; doing it
|
|
471
|
+
# once here means an adapter that has not thought about it still records
|
|
472
|
+
# something readable.
|
|
473
|
+
shaped = _as_mapping(value)
|
|
474
|
+
if shaped is not None:
|
|
475
|
+
# Same depth, not depth + 1: the object is REPLACED by its mapping
|
|
476
|
+
# rather than nested inside one, and the Mapping branch above does the
|
|
477
|
+
# descending (and the per-field limits) from here.
|
|
478
|
+
return _truncate(shaped, limit, cut, depth)
|
|
479
|
+
|
|
480
|
+
# An object with no JSON shape is rendered, not cut — `fw_truncated` means
|
|
481
|
+
# "data was lost", and a repr that fits has lost nothing a JSON encoder
|
|
482
|
+
# would have kept.
|
|
483
|
+
return _truncate(repr(value), limit, cut, _MAX_DEPTH)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _as_mapping(value: Any) -> "dict | None":
|
|
487
|
+
"""A dataclass instance or pydantic model as a plain dict, or None.
|
|
488
|
+
|
|
489
|
+
Shallow on purpose. `dataclasses.asdict` and `model_dump` both recurse and
|
|
490
|
+
both COPY, so on a large object they duplicate the whole tree before
|
|
491
|
+
`_truncate` gets to decide it only wanted the first 8 KB. Reading the top
|
|
492
|
+
level and handing it back lets the existing walk apply the field limit, the
|
|
493
|
+
item cap and the depth cap on the way down, as it does for a dict.
|
|
494
|
+
|
|
495
|
+
Everything here can execute the caller's own code — a pydantic validator, a
|
|
496
|
+
property behind `getattr` — so all of it is guarded, and a failure falls
|
|
497
|
+
through to `repr`, which is what happened before this existed.
|
|
498
|
+
"""
|
|
499
|
+
if isinstance(value, type): # the CLASS, not an instance of it
|
|
500
|
+
return None
|
|
501
|
+
if dataclasses.is_dataclass(value):
|
|
502
|
+
try:
|
|
503
|
+
return {f.name: getattr(value, f.name) for f in dataclasses.fields(value)}
|
|
504
|
+
except Exception:
|
|
505
|
+
return None
|
|
506
|
+
# `model_dump` and not `dict`: pydantic v2 names it distinctively, whereas
|
|
507
|
+
# half the objects in a typical process have some attribute called `dict`
|
|
508
|
+
# and calling it would be a coin flip.
|
|
509
|
+
dump = getattr(value, "model_dump", None)
|
|
510
|
+
if callable(dump):
|
|
511
|
+
try:
|
|
512
|
+
dumped = dump()
|
|
513
|
+
except Exception:
|
|
514
|
+
return None
|
|
515
|
+
return dumped if isinstance(dumped, Mapping) else None
|
|
516
|
+
return None
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _size(value: Any, _depth: int = 0) -> int:
|
|
520
|
+
if value is None or isinstance(value, (bool, int, float)):
|
|
521
|
+
return 8
|
|
522
|
+
if isinstance(value, str):
|
|
523
|
+
return len(value)
|
|
524
|
+
if _depth >= _MAX_DEPTH:
|
|
525
|
+
return len(repr(value))
|
|
526
|
+
# Same ABCs as `_truncate`, for the same reason: a size computed off `repr`
|
|
527
|
+
# for a value that `_truncate` will expand into JSON budgets the wrong
|
|
528
|
+
# number, and the budget is what decides which fields survive.
|
|
529
|
+
if isinstance(value, Mapping):
|
|
530
|
+
return sum(len(str(k)) + _size(v, _depth + 1) for k, v in value.items())
|
|
531
|
+
if isinstance(value, (Sequence, AbcSet)):
|
|
532
|
+
return sum(_size(v, _depth + 1) for v in value)
|
|
533
|
+
return len(repr(value))
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def payload(
|
|
537
|
+
fields: dict,
|
|
538
|
+
*,
|
|
539
|
+
limit: int = FIELD_LIMIT,
|
|
540
|
+
budget: int = EVENT_BUDGET,
|
|
541
|
+
cut: "_Cut | None" = None,
|
|
542
|
+
) -> dict:
|
|
543
|
+
"""Apply the per-field limit and the per-event budget to a dict of extras.
|
|
544
|
+
|
|
545
|
+
Anything cut sets `fw_truncated=True`, so a surprising-looking payload in
|
|
546
|
+
the dashboard is self-explaining rather than a mystery.
|
|
547
|
+
"""
|
|
548
|
+
# Shared with the caller when it also truncated something — `_emit_now`
|
|
549
|
+
# cuts the DECLARED parameters itself, and `fw_truncated` has to mean "this
|
|
550
|
+
# event lost data", not "one of its metadata extras did".
|
|
551
|
+
cut = cut if cut is not None else _Cut()
|
|
552
|
+
spend = _Budget(budget)
|
|
553
|
+
|
|
554
|
+
# SMALLEST FIRST, spent in that order and emitted in the caller's. The
|
|
555
|
+
# budget binds either way, but insertion order decides WHICH keys survive
|
|
556
|
+
# it, and the adapters put the big payload before the metadata: an oversized
|
|
557
|
+
# `fw_inputs` consumed the whole event and took `fw_run_id` and `fw_node`
|
|
558
|
+
# with it — the two fields that say which run the payload belongs to. Sizing
|
|
559
|
+
# first costs a walk over the node count (`_size` is O(1) per string), not
|
|
560
|
+
# over the character count, so it is cheap even for the payloads this exists
|
|
561
|
+
# to contain. Ties keep insertion order, so a set of equal-sized fields still
|
|
562
|
+
# fills up in the order it arrived and the last of them are omitted whole.
|
|
563
|
+
sized = sorted(fields.items(), key=lambda kv: (_size(kv[1]) + len(kv[0]), ))
|
|
564
|
+
kept: dict[str, Any] = {}
|
|
565
|
+
for key, value in sized:
|
|
566
|
+
if spend.spent_out:
|
|
567
|
+
# Past the budget a field does not arrive short, it does not arrive
|
|
568
|
+
# at all — which is why the limit and the budget cannot move
|
|
569
|
+
# independently, and why `fw_truncated` has to be set here.
|
|
570
|
+
cut.hit = True
|
|
571
|
+
continue
|
|
572
|
+
spend.spend(len(key))
|
|
573
|
+
kept[key] = _truncate(value, limit, cut, 0, spend)
|
|
574
|
+
|
|
575
|
+
out: dict[str, Any] = {k: kept[k] for k in fields if k in kept}
|
|
576
|
+
if cut.hit:
|
|
577
|
+
out["fw_truncated"] = True
|
|
578
|
+
return out
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# Every field name declared on any event dataclass, plus the five names
|
|
582
|
+
# `_events._RESERVED` blocks. Derived rather than hand-listed so that adding a
|
|
583
|
+
# field to `_schema.py` cannot leave a stale copy here.
|
|
584
|
+
def _declared_field_names() -> frozenset[str]:
|
|
585
|
+
names: set[str] = set(_RESERVED)
|
|
586
|
+
for obj in vars(_schema).values():
|
|
587
|
+
if dataclasses.is_dataclass(obj) and isinstance(obj, type):
|
|
588
|
+
names.update(f.name for f in dataclasses.fields(obj))
|
|
589
|
+
names.discard("extra_fields")
|
|
590
|
+
return frozenset(names)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
# Deliberate exceptions: these are top-level by design. `duration_ms` is how an
|
|
594
|
+
# adapter reports a model call's real latency (it is not a declared parameter of
|
|
595
|
+
# `model_response`, and `durationOf` prefers it); `usage` is read by both the
|
|
596
|
+
# server summary and the dashboard as a token fallback; `request_id` pairs
|
|
597
|
+
# model events; `framework*` label every event.
|
|
598
|
+
ALLOWED_TOP_LEVEL = frozenset(
|
|
599
|
+
{
|
|
600
|
+
"request_id",
|
|
601
|
+
"duration_ms",
|
|
602
|
+
"usage",
|
|
603
|
+
"traceback",
|
|
604
|
+
"framework",
|
|
605
|
+
"framework_version",
|
|
606
|
+
"integration_version",
|
|
607
|
+
}
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# An extra whose name collides with a declared field SILENTLY OVERWRITES it:
|
|
611
|
+
# `_schema._build()` ends with `result.update(extra)`. An adapter reflecting a
|
|
612
|
+
# framework's kwargs into extras would then change `tool_name`, `model`,
|
|
613
|
+
# `outcome` or `input_tokens` — i.e. the promoted columns and the
|
|
614
|
+
# server's computed summary — and every test would still pass.
|
|
615
|
+
FORBIDDEN_EXTRAS = _declared_field_names() - ALLOWED_TOP_LEVEL
|
|
616
|
+
|
|
617
|
+
_FW_PREFIX = "fw_"
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def fw_fields(**kw: Any) -> dict:
|
|
621
|
+
"""Build the `fw_*` extra-field namespace.
|
|
622
|
+
|
|
623
|
+
fw_fields(run_id=run_id, node="retrieve", tags=None)
|
|
624
|
+
-> {"fw_run_id": "...", "fw_node": "retrieve"}
|
|
625
|
+
|
|
626
|
+
Keys are prefixed unless they already are, or are one of the deliberate
|
|
627
|
+
top-level names. `None` values are dropped (the schema omits None optionals
|
|
628
|
+
anyway, and an extra explicitly set to None would still occupy a key).
|
|
629
|
+
Flat only — `payload_key_expr` on the server is single-level, so a nested
|
|
630
|
+
dict is not queryable.
|
|
631
|
+
|
|
632
|
+
Values are NOT truncated here. They used to be, at the module-level
|
|
633
|
+
`FIELD_LIMIT` bound at import — so `instrument(..., capture_limit=32768)`
|
|
634
|
+
raised the ceiling for `input`/`output`/`messages`/`content` and left every
|
|
635
|
+
`fw_*` extra pinned at 8192, because the value was already cut before
|
|
636
|
+
`_emit_now` saw it and `payload()` can only cut further, never restore. Half
|
|
637
|
+
the event honoured the option and half did not, with nothing saying which.
|
|
638
|
+
Every call site splats the result into a tracker emit, so `payload()` — which
|
|
639
|
+
receives the tracker's real limit — is the one place that bounds them now.
|
|
640
|
+
"""
|
|
641
|
+
out: dict[str, Any] = {}
|
|
642
|
+
for key, value in kw.items():
|
|
643
|
+
if value is None:
|
|
644
|
+
continue
|
|
645
|
+
if key in ALLOWED_TOP_LEVEL or key.startswith(_FW_PREFIX):
|
|
646
|
+
name = key
|
|
647
|
+
else:
|
|
648
|
+
name = _FW_PREFIX + key
|
|
649
|
+
out[name] = value
|
|
650
|
+
return guard_extras(out)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def guard_extras(fields: dict) -> dict:
|
|
654
|
+
"""Strip (or, in strict mode, reject) extras that would shadow a real field.
|
|
655
|
+
|
|
656
|
+
Called on every emit, so even an adapter that builds its extras by hand
|
|
657
|
+
cannot silently rewrite a promoted column.
|
|
658
|
+
"""
|
|
659
|
+
bad = FORBIDDEN_EXTRAS & fields.keys()
|
|
660
|
+
if not bad:
|
|
661
|
+
return fields
|
|
662
|
+
names = sorted(bad)
|
|
663
|
+
message = (
|
|
664
|
+
f"failproofai_sdk: extra fields {names} would overwrite declared event fields "
|
|
665
|
+
f"(schema merges extras last). Namespace them as fw_* instead."
|
|
666
|
+
)
|
|
667
|
+
if strict():
|
|
668
|
+
raise ValueError(message)
|
|
669
|
+
logger.warning("%s Dropping them.", message)
|
|
670
|
+
return {k: v for k, v in fields.items() if k not in bad}
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def framework_fields(name: str, dist: str | None = None) -> dict:
|
|
674
|
+
"""The `framework` / `framework_version` / `integration_version` triple.
|
|
675
|
+
|
|
676
|
+
Payload-only, so **not** server-side filterable; promoting it later is a
|
|
677
|
+
five-file hand-mirrored change, so it is done on demand, not speculatively.
|
|
678
|
+
"""
|
|
679
|
+
out = {"framework": name, "integration_version": __version__}
|
|
680
|
+
version = _compat.version_string(dist) if dist else None
|
|
681
|
+
if version:
|
|
682
|
+
out["framework_version"] = version
|
|
683
|
+
return out
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
_ID_SEPARATORS = re.compile(r"[\s\-_.:/]+")
|
|
687
|
+
_EMBEDDED_UUID = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
|
688
|
+
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
|
|
689
|
+
_HEX = frozenset("0123456789abcdefABCDEF")
|
|
690
|
+
_AGENT_ID_LIMIT = 64
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def normalize_agent_id(raw: Any, default: str = DEFAULT_AGENT_ID) -> str:
|
|
694
|
+
"""Turn a framework's label into something safe for `agent_id`.
|
|
695
|
+
|
|
696
|
+
`agent_id` is a `LowCardinality(String)` column and the primary facet on
|
|
697
|
+
every dashboard surface. A UUID in it poisons that facet permanently —
|
|
698
|
+
LowCardinality degrades, and the filter dropdown fills with one entry per
|
|
699
|
+
run. So a value that looks like an id becomes `default` and the real id goes
|
|
700
|
+
to `fw_agent_id` / `fw_run_id` where it belongs.
|
|
701
|
+
"""
|
|
702
|
+
if raw is None:
|
|
703
|
+
return default
|
|
704
|
+
text = " ".join(str(raw).split())
|
|
705
|
+
if not text:
|
|
706
|
+
return default
|
|
707
|
+
if _looks_like_id(text):
|
|
708
|
+
return default
|
|
709
|
+
text = _strip_embedded_id(text)
|
|
710
|
+
if not text:
|
|
711
|
+
return default
|
|
712
|
+
return text[:_AGENT_ID_LIMIT]
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _looks_like_id(text: str) -> bool:
|
|
716
|
+
"""True for UUIDs and long bare hex strings."""
|
|
717
|
+
try:
|
|
718
|
+
uuid.UUID(text)
|
|
719
|
+
return True
|
|
720
|
+
except (ValueError, AttributeError, TypeError):
|
|
721
|
+
pass
|
|
722
|
+
bare = text.replace("-", "").replace("_", "")
|
|
723
|
+
return len(bare) >= 16 and all(c in _HEX for c in bare)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _strip_embedded_id(text: str) -> str:
|
|
727
|
+
"""Drop a per-run id that a readable prefix is carrying.
|
|
728
|
+
|
|
729
|
+
`_looks_like_id` only fires on a value that is an id ALL THE WAY THROUGH, so
|
|
730
|
+
it caught a bare UUID and missed `agent-<uuid>`, `crew_<uuid>`,
|
|
731
|
+
`task-3f9a1c…` — a readable name with a per-run suffix, which is the shape
|
|
732
|
+
frameworks actually produce and precisely the one the docs warn against
|
|
733
|
+
("a role containing a UUID, timestamp, or per-run suffix"). Those went
|
|
734
|
+
through untouched, one distinct value per run, into a
|
|
735
|
+
`LowCardinality(String)` column that is the primary facet on every dashboard
|
|
736
|
+
surface. That is the same poisoning the whole-string guard exists to stop,
|
|
737
|
+
reached by the more common route.
|
|
738
|
+
|
|
739
|
+
Stripping rather than falling back to `default`: `agent-<uuid>` still knows
|
|
740
|
+
it is an agent, and collapsing every such label to `main` would throw away
|
|
741
|
+
the one readable thing in it. A segment is dropped only if it is a UUID or a
|
|
742
|
+
hex run of 16+ characters, so a name like `agent-v2` or `step-3` is
|
|
743
|
+
untouched — and a value with nothing left after stripping falls back, which
|
|
744
|
+
is what the caller wanted for a bare id anyway.
|
|
745
|
+
"""
|
|
746
|
+
# Dashed UUIDs first, and as a substring: splitting on separators would
|
|
747
|
+
# break `task-3f9a1c2b-...` into five segments none of which is an id on its
|
|
748
|
+
# own, so the most standard shape of all would survive the segment pass.
|
|
749
|
+
stripped = _EMBEDDED_UUID.sub(" ", text)
|
|
750
|
+
parts = [p for p in _ID_SEPARATORS.split(stripped) if p]
|
|
751
|
+
kept = [p for p in parts if not _looks_like_id(p)]
|
|
752
|
+
# Nothing was an id: hand back the ORIGINAL, separators and all. Rejoining
|
|
753
|
+
# on spaces would rewrite every `node_a_b` in the process into `node a b`,
|
|
754
|
+
# which is a rename of the primary facet in exchange for nothing. (The
|
|
755
|
+
# empty segments a leading or trailing separator produces are dropped
|
|
756
|
+
# before the comparison, or `node_x_` alone would look like a change.)
|
|
757
|
+
if stripped == text and len(kept) == len(parts):
|
|
758
|
+
return text
|
|
759
|
+
return " ".join(kept).strip()
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def ms(delta_seconds: Any) -> int:
|
|
763
|
+
"""Whole milliseconds, as an `int`.
|
|
764
|
+
|
|
765
|
+
The server stores `duration_ms` as a u32 and its JSON parser drops floats
|
|
766
|
+
(`as_u64()` -> None), so a float silently NULLs the column: the dashboard
|
|
767
|
+
then shows no duration and nobody sees an error. Negative deltas (clock
|
|
768
|
+
adjustments, a framework handing us an end before its start) clamp to 0.
|
|
769
|
+
"""
|
|
770
|
+
seconds = getattr(delta_seconds, "total_seconds", None)
|
|
771
|
+
value = seconds() if callable(seconds) else float(delta_seconds)
|
|
772
|
+
return max(round(value * 1000), 0)
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
# ---------------------------------------------------------------------------
|
|
776
|
+
# Shape B — callback surfaces
|
|
777
|
+
# ---------------------------------------------------------------------------
|
|
778
|
+
|
|
779
|
+
@dataclasses.dataclass(slots=True)
|
|
780
|
+
class _Run:
|
|
781
|
+
identity: Identity
|
|
782
|
+
parent_key: Any
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
class RunTracker:
|
|
786
|
+
"""Maps a framework's own run ids onto Failproof AI identity.
|
|
787
|
+
|
|
788
|
+
This is Shape B: the surface where a start and its end are **separate
|
|
789
|
+
callbacks**, possibly on different threads (CrewAI dispatches handlers on a
|
|
790
|
+
ten-worker pool) or different asyncio tasks. Such an adapter can never use
|
|
791
|
+
contextvars — `ContextVar.reset(token)` raises `ValueError: Token was created
|
|
792
|
+
in a different Context` across tasks as well as threads, so a token cannot be
|
|
793
|
+
held between two callbacks. Instead we keep the mapping here and pass
|
|
794
|
+
`session_id=` / `agent_id=` **explicitly** on every emit.
|
|
795
|
+
|
|
796
|
+
Bounded (`max_open`, FIFO eviction) because orphaned starts are normal: a
|
|
797
|
+
crashed run, a stream nobody consumed, a framework that forgot an end
|
|
798
|
+
callback. Unbounded, that is a memory leak in a long-lived server.
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
__slots__ = (
|
|
802
|
+
"name", "_max_open", "_base_fields", "_runs", "_links", "_lock", "_warned",
|
|
803
|
+
"_field_limit", "_budget",
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
def __init__(
|
|
807
|
+
self,
|
|
808
|
+
name: str,
|
|
809
|
+
*,
|
|
810
|
+
max_open: int = 10_000,
|
|
811
|
+
base_fields: dict | None = None,
|
|
812
|
+
field_limit: int | None = None,
|
|
813
|
+
) -> None:
|
|
814
|
+
self.name = name
|
|
815
|
+
self._max_open = max_open
|
|
816
|
+
self._base_fields = dict(base_fields or {})
|
|
817
|
+
# One place decides how much of a value survives, for both halves of an
|
|
818
|
+
# event: the declared parameters (`input`, `output`, `messages`) and the
|
|
819
|
+
# `fw_*` extras. They used to be truncated by two different rules — the
|
|
820
|
+
# adapter's own constant on the way in, `FIELD_LIMIT` here — so an
|
|
821
|
+
# adapter that tightened its limit still had its declared fields cut at
|
|
822
|
+
# the core default, and raising the adapter's constant changed only half
|
|
823
|
+
# the event.
|
|
824
|
+
self._field_limit = FIELD_LIMIT if field_limit is None else int(field_limit)
|
|
825
|
+
self._budget = self._field_limit * _FIELDS_PER_EVENT
|
|
826
|
+
self._runs: dict[Any, _Run] = {}
|
|
827
|
+
self._links: dict[Any, Any] = {}
|
|
828
|
+
# RLock: `start_agent` resolves a parent while already holding it.
|
|
829
|
+
self._lock = threading.RLock()
|
|
830
|
+
self._warned = False
|
|
831
|
+
|
|
832
|
+
# -- identity ---------------------------------------------------------
|
|
833
|
+
|
|
834
|
+
def identity(self, key: Any, parent_key: Any = None, *, warn: bool = True) -> Identity | None:
|
|
835
|
+
"""Resolve a run to an Failproof AI identity, in this order:
|
|
836
|
+
|
|
837
|
+
1. the exact `key`;
|
|
838
|
+
2. the `parent_key` chain, walked through every link we have seen — a
|
|
839
|
+
framework's own parent_run_id is a *better* parent chain than a
|
|
840
|
+
contextvar stack, because it survives task hops and thread pools;
|
|
841
|
+
3. **`failproofai_sdk.current()`** — this is the whole interop story. An
|
|
842
|
+
adapter running inside a hand-written `with failproofai_sdk.agent("planner")`
|
|
843
|
+
joins that same session and gets `parent_id="planner"`, so mixing the
|
|
844
|
+
manual API and an adapter produces one tree, not two;
|
|
845
|
+
4. otherwise the event is dropped and we log **once**.
|
|
846
|
+
"""
|
|
847
|
+
with self._lock:
|
|
848
|
+
if key is not None:
|
|
849
|
+
run = self._runs.get(key)
|
|
850
|
+
if run is not None:
|
|
851
|
+
return run.identity
|
|
852
|
+
walked = self._walk(parent_key)
|
|
853
|
+
if walked is not None:
|
|
854
|
+
return walked
|
|
855
|
+
|
|
856
|
+
ambient = self._ambient(coerce_agent=True)
|
|
857
|
+
if ambient is not None:
|
|
858
|
+
return ambient
|
|
859
|
+
|
|
860
|
+
if warn:
|
|
861
|
+
self._warn_unresolved(key)
|
|
862
|
+
return None
|
|
863
|
+
|
|
864
|
+
@staticmethod
|
|
865
|
+
def _ambient(*, coerce_agent: bool) -> Identity | None:
|
|
866
|
+
"""Step 3: the identity a hand-written scope has bound, if any.
|
|
867
|
+
|
|
868
|
+
The one copy of this. When resolving an event we coerce a missing
|
|
869
|
+
agent_id to `main`, but when resolving a *parent* we must not: inside a
|
|
870
|
+
bare `with failproofai_sdk.session(...)` there is no open agent, and claiming
|
|
871
|
+
`parent_id="main"` would point at an agent that never emitted an
|
|
872
|
+
`agent_start` — which makes the dashboard synthesize a never-ending root
|
|
873
|
+
span that stays `ongoing` forever.
|
|
874
|
+
"""
|
|
875
|
+
cur = _context.current()
|
|
876
|
+
if cur.session_id is None:
|
|
877
|
+
return None
|
|
878
|
+
return Identity(
|
|
879
|
+
session_id=cur.session_id,
|
|
880
|
+
agent_id=cur.agent_id or (DEFAULT_AGENT_ID if coerce_agent else None),
|
|
881
|
+
parent_id=cur.parent_id,
|
|
882
|
+
depth=cur.depth,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def _walk(self, parent_key: Any) -> Identity | None:
|
|
886
|
+
"""Caller holds the lock."""
|
|
887
|
+
seen: set[Any] = set()
|
|
888
|
+
key = parent_key
|
|
889
|
+
while key is not None and key not in seen:
|
|
890
|
+
seen.add(key)
|
|
891
|
+
run = self._runs.get(key)
|
|
892
|
+
if run is not None:
|
|
893
|
+
return run.identity
|
|
894
|
+
key = self._links.get(key)
|
|
895
|
+
return None
|
|
896
|
+
|
|
897
|
+
def _warn_unresolved(self, key: Any) -> None:
|
|
898
|
+
if self._warned:
|
|
899
|
+
return
|
|
900
|
+
self._warned = True
|
|
901
|
+
logger.warning(
|
|
902
|
+
"failproofai_sdk: %s could not resolve a session for run %r and is dropping its "
|
|
903
|
+
"events. Wrap the call in `with failproofai_sdk.session():` (or "
|
|
904
|
+
"`with failproofai_sdk.agent(...):`) if you want them attributed. This is logged "
|
|
905
|
+
"once per tracker.",
|
|
906
|
+
self.name,
|
|
907
|
+
key,
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
def link(self, key: Any, parent_key: Any) -> None:
|
|
911
|
+
"""Record a run's parent without making it an agent.
|
|
912
|
+
|
|
913
|
+
Intermediate framework runs (a LangChain chain, a CrewAI task) do not
|
|
914
|
+
become Failproof AI spans, but their children still need to find the agent
|
|
915
|
+
above them. This is what makes step 2 of `identity()` work more than one
|
|
916
|
+
hop up.
|
|
917
|
+
"""
|
|
918
|
+
if key is None or parent_key is None or key == parent_key:
|
|
919
|
+
return
|
|
920
|
+
with self._lock:
|
|
921
|
+
self._evict(self._links)
|
|
922
|
+
self._links[key] = parent_key
|
|
923
|
+
|
|
924
|
+
def _evict(self, table: dict) -> None:
|
|
925
|
+
"""Caller holds the lock. FIFO — dicts keep insertion order."""
|
|
926
|
+
while len(table) >= self._max_open:
|
|
927
|
+
table.pop(next(iter(table)), None)
|
|
928
|
+
|
|
929
|
+
# -- agents -----------------------------------------------------------
|
|
930
|
+
|
|
931
|
+
def start_agent(
|
|
932
|
+
self,
|
|
933
|
+
key: Any,
|
|
934
|
+
*,
|
|
935
|
+
agent_id: str,
|
|
936
|
+
parent_key: Any = None,
|
|
937
|
+
session_id: str | None = None,
|
|
938
|
+
goal: str | None = None,
|
|
939
|
+
**fields: Any,
|
|
940
|
+
) -> Identity:
|
|
941
|
+
"""Register a run as an agent and emit `agent_start`."""
|
|
942
|
+
parent = self._resolve_parent(parent_key)
|
|
943
|
+
sid = session_id or (parent.session_id if parent else None) or uuid.uuid4().hex
|
|
944
|
+
aid = normalize_agent_id(agent_id)
|
|
945
|
+
identity = Identity(
|
|
946
|
+
session_id=sid,
|
|
947
|
+
agent_id=aid,
|
|
948
|
+
parent_id=parent.agent_id if parent else None,
|
|
949
|
+
depth=(parent.depth + 1) if parent else 1,
|
|
950
|
+
)
|
|
951
|
+
with self._lock:
|
|
952
|
+
self._evict(self._runs)
|
|
953
|
+
self._runs[key] = _Run(identity=identity, parent_key=parent_key)
|
|
954
|
+
if parent_key is not None:
|
|
955
|
+
self._evict(self._links)
|
|
956
|
+
self._links[key] = parent_key
|
|
957
|
+
self._emit(
|
|
958
|
+
"agent_start",
|
|
959
|
+
identity,
|
|
960
|
+
goal=truncate(goal) if goal is not None else None,
|
|
961
|
+
parent_id=identity.parent_id,
|
|
962
|
+
**fields,
|
|
963
|
+
)
|
|
964
|
+
return identity
|
|
965
|
+
|
|
966
|
+
def end_agent(
|
|
967
|
+
self,
|
|
968
|
+
key: Any,
|
|
969
|
+
*,
|
|
970
|
+
outcome: str = "success",
|
|
971
|
+
summary: str | None = None,
|
|
972
|
+
**fields: Any,
|
|
973
|
+
) -> None:
|
|
974
|
+
"""Emit `agent_end` and forget the run.
|
|
975
|
+
|
|
976
|
+
`outcome` is `"failed"`, never `"failure"` — the server only counts
|
|
977
|
+
`error|failed|timeout|rejected` as a failure.
|
|
978
|
+
"""
|
|
979
|
+
with self._lock:
|
|
980
|
+
run = self._runs.pop(key, None)
|
|
981
|
+
identity = run.identity if run is not None else self.identity(key)
|
|
982
|
+
if identity is None:
|
|
983
|
+
return
|
|
984
|
+
self._emit(
|
|
985
|
+
"agent_end",
|
|
986
|
+
identity,
|
|
987
|
+
outcome=outcome,
|
|
988
|
+
summary=truncate(summary) if summary is not None else None,
|
|
989
|
+
**fields,
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
def open_agents(self) -> tuple[Any, ...]:
|
|
993
|
+
with self._lock:
|
|
994
|
+
return tuple(self._runs)
|
|
995
|
+
|
|
996
|
+
def close_open_agents(self, *, outcome: str = "cancelled") -> None:
|
|
997
|
+
"""Close every still-open agent, newest first.
|
|
998
|
+
|
|
999
|
+
A session that dies with an open `agent_start` renders as `ongoing`
|
|
1000
|
+
forever, so teardown closes what it opened.
|
|
1001
|
+
"""
|
|
1002
|
+
for key in reversed(self.open_agents()):
|
|
1003
|
+
self.end_agent(key, outcome=outcome)
|
|
1004
|
+
|
|
1005
|
+
def reset(self) -> None:
|
|
1006
|
+
with self._lock:
|
|
1007
|
+
self._runs.clear()
|
|
1008
|
+
self._links.clear()
|
|
1009
|
+
self._warned = False
|
|
1010
|
+
|
|
1011
|
+
# -- everything else --------------------------------------------------
|
|
1012
|
+
|
|
1013
|
+
def emit(self, method: str, key: Any, *, parent_key: Any = None, **fields: Any) -> None:
|
|
1014
|
+
"""Emit any `failproofai_sdk.event.*` method against a run's identity.
|
|
1015
|
+
|
|
1016
|
+
tracker.emit("tool_use", run_id, parent_key=parent_run_id,
|
|
1017
|
+
tool_name=name, tool_call_id=str(run_id))
|
|
1018
|
+
|
|
1019
|
+
Drops the event (with one warning) when nothing resolves, rather than
|
|
1020
|
+
inventing a session id: a synthesized session splits one run into many.
|
|
1021
|
+
"""
|
|
1022
|
+
if parent_key is not None:
|
|
1023
|
+
self.link(key, parent_key)
|
|
1024
|
+
identity = self.identity(key, parent_key)
|
|
1025
|
+
if identity is None:
|
|
1026
|
+
return
|
|
1027
|
+
self._emit(method, identity, **fields)
|
|
1028
|
+
|
|
1029
|
+
def _emit(self, method: str, identity: Identity, **fields: Any) -> None:
|
|
1030
|
+
call_safely(self._emit_now, (method, identity, fields), {}, f"{self.name}.{method}")
|
|
1031
|
+
|
|
1032
|
+
def _emit_now(self, method: str, identity: Identity, fields: dict) -> None:
|
|
1033
|
+
# Two kinds of keyword here, and the split is by NAME, not by meaning:
|
|
1034
|
+
# `fw_*` (plus whatever the adapter set as base fields) are payload
|
|
1035
|
+
# extras and go through the guard and the size budget; everything else
|
|
1036
|
+
# is a real parameter of the `event.*` method — `tool_name`, `input`,
|
|
1037
|
+
# `outcome` — and is passed straight through. Those still get truncated,
|
|
1038
|
+
# because `input`/`output`/`messages`/`content` are exactly the fields a
|
|
1039
|
+
# framework fills with a 200KB prompt.
|
|
1040
|
+
declared: dict[str, Any] = {}
|
|
1041
|
+
extras: dict[str, Any] = {}
|
|
1042
|
+
# ONE `_Cut` across both halves. `truncate()` used to build a throwaway
|
|
1043
|
+
# one per declared field and discard it, so `fw_truncated` — the only
|
|
1044
|
+
# machine-readable "this event lost data" signal, and the field an
|
|
1045
|
+
# operator filters on to find where — was set when a small `fw_*` extra
|
|
1046
|
+
# was cut and NOT when the prompt or the completion was. Exactly the
|
|
1047
|
+
# wrong way round: `output` is cut on essentially every real tool loop.
|
|
1048
|
+
cut = _Cut()
|
|
1049
|
+
for key, value in fields.items():
|
|
1050
|
+
if value is None:
|
|
1051
|
+
continue
|
|
1052
|
+
if key.startswith(_FW_PREFIX):
|
|
1053
|
+
extras[key] = value
|
|
1054
|
+
else:
|
|
1055
|
+
declared[key] = _truncate(value, self._field_limit, cut, 0)
|
|
1056
|
+
merged = payload(
|
|
1057
|
+
guard_extras({**self._base_fields, **extras}),
|
|
1058
|
+
limit=self._field_limit,
|
|
1059
|
+
budget=self._budget,
|
|
1060
|
+
cut=cut,
|
|
1061
|
+
)
|
|
1062
|
+
# A base field named like a real parameter would be a duplicate keyword
|
|
1063
|
+
# (TypeError inside the customer's callback); the explicit value wins.
|
|
1064
|
+
merged = {k: v for k, v in merged.items() if k not in declared}
|
|
1065
|
+
emit = getattr(_runtime.event, method)
|
|
1066
|
+
emit(session_id=identity.session_id, agent_id=identity.agent_id, **declared, **merged)
|
|
1067
|
+
|
|
1068
|
+
def _resolve_parent(self, parent_key: Any) -> Identity | None:
|
|
1069
|
+
with self._lock:
|
|
1070
|
+
walked = self._walk(parent_key)
|
|
1071
|
+
if walked is not None:
|
|
1072
|
+
return walked
|
|
1073
|
+
# Same three steps as `identity()`, minus the exact-key lookup (a run
|
|
1074
|
+
# cannot be its own parent) and minus the warning (a root agent with no
|
|
1075
|
+
# ambient scope is normal, not a dropped event).
|
|
1076
|
+
return self._ambient(coerce_agent=False)
|