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,1102 @@
|
|
|
1
|
+
"""The Pydantic AI adapter, against the real framework and a fake model.
|
|
2
|
+
|
|
3
|
+
Everything here runs a genuine `pydantic_ai.Agent` — `TestModel` and
|
|
4
|
+
`FunctionModel` ship in the package for exactly this — and then asserts on the
|
|
5
|
+
**JSONL the writer actually wrote**, not on mock call args. A mock-based test
|
|
6
|
+
of an adapter proves that the adapter calls the functions the test says it
|
|
7
|
+
calls, which is the one thing that was never in doubt.
|
|
8
|
+
|
|
9
|
+
The single highest-value test in this file is `TestAntiDrift`. Every other test
|
|
10
|
+
here would still pass if Pydantic AI renamed `wrap_tool_execute` tomorrow: our
|
|
11
|
+
override would simply never be called, the framework would run fine, and we
|
|
12
|
+
would silently record nothing. Only reflection over the real base class catches
|
|
13
|
+
that.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import dataclasses
|
|
18
|
+
import inspect
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
import uuid
|
|
24
|
+
|
|
25
|
+
import pytest
|
|
26
|
+
|
|
27
|
+
import failproofai_sdk
|
|
28
|
+
from failproofai_sdk import _runtime, _schema
|
|
29
|
+
from failproofai_sdk.integrations import _compat, _core
|
|
30
|
+
|
|
31
|
+
pytestmark = pytest.mark.framework
|
|
32
|
+
|
|
33
|
+
_REQUIRE_FRAMEWORKS = os.environ.get("AGENTEYE_TESTS_REQUIRE_FRAMEWORKS", "").strip().lower() in {
|
|
34
|
+
"1",
|
|
35
|
+
"true",
|
|
36
|
+
"yes",
|
|
37
|
+
"on",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
from pydantic_ai import Agent, RunContext
|
|
42
|
+
from pydantic_ai.capabilities import AbstractCapability
|
|
43
|
+
from pydantic_ai.messages import ModelResponse, TextPart, ToolCallPart
|
|
44
|
+
from pydantic_ai.models import ModelRequestContext
|
|
45
|
+
from pydantic_ai.models.function import FunctionModel
|
|
46
|
+
from pydantic_ai.models.test import TestModel
|
|
47
|
+
from pydantic_ai.usage import RequestUsage, RunUsage
|
|
48
|
+
except ImportError: # pragma: no cover - exercised only on a bare environment
|
|
49
|
+
# `pytest.importorskip` is fail-open: misspell the module and every test in
|
|
50
|
+
# the file skips while CI stays green having tested nothing. The framework
|
|
51
|
+
# CI leg sets AGENTEYE_TESTS_REQUIRE_FRAMEWORKS=1 to turn that into a hard
|
|
52
|
+
# failure.
|
|
53
|
+
if _REQUIRE_FRAMEWORKS:
|
|
54
|
+
raise
|
|
55
|
+
pytest.skip("pydantic-ai is not installed", allow_module_level=True)
|
|
56
|
+
|
|
57
|
+
from failproofai_sdk.integrations import pydantic_ai as adapter # noqa: E402
|
|
58
|
+
from failproofai_sdk.integrations.pydantic_ai import FailproofAI # noqa: E402
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Fixtures and helpers
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
@pytest.fixture()
|
|
66
|
+
def emitted(tmp_path):
|
|
67
|
+
"""Read back the real JSONL the writer produced during this test.
|
|
68
|
+
|
|
69
|
+
The flush interval goes to an hour because event filenames only have
|
|
70
|
+
millisecond resolution: two flushes inside the same millisecond write to the
|
|
71
|
+
same path and the second clobbers the first. So the background thread is
|
|
72
|
+
parked and every flush in this file is explicit.
|
|
73
|
+
"""
|
|
74
|
+
_runtime.writer.set_flush_interval(3600)
|
|
75
|
+
# Drain anything a previous test left queued, then start from an empty
|
|
76
|
+
# directory: the autouse fixture has already pointed base_dir at tmp_path.
|
|
77
|
+
failproofai_sdk._writer.flush_now()
|
|
78
|
+
events_dir = tmp_path / "events"
|
|
79
|
+
if events_dir.exists():
|
|
80
|
+
shutil.rmtree(events_dir)
|
|
81
|
+
|
|
82
|
+
def read():
|
|
83
|
+
failproofai_sdk._writer.flush_now()
|
|
84
|
+
rows = []
|
|
85
|
+
for path in sorted(events_dir.glob("*.jsonl")):
|
|
86
|
+
rows.extend(
|
|
87
|
+
json.loads(line)
|
|
88
|
+
for line in path.read_text(encoding="utf-8").splitlines()
|
|
89
|
+
if line.strip()
|
|
90
|
+
)
|
|
91
|
+
return rows
|
|
92
|
+
|
|
93
|
+
return read
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.fixture()
|
|
97
|
+
def instrumented():
|
|
98
|
+
_core.set_strict(False)
|
|
99
|
+
_compat.set_strict_integrations(False)
|
|
100
|
+
_core.reset_failures()
|
|
101
|
+
assert failproofai_sdk.instrument("pydantic_ai") == ("pydantic_ai",)
|
|
102
|
+
try:
|
|
103
|
+
yield
|
|
104
|
+
finally:
|
|
105
|
+
failproofai_sdk.uninstrument("pydantic_ai")
|
|
106
|
+
_core.set_strict(None)
|
|
107
|
+
_compat.set_strict_integrations(None)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def types_of(rows):
|
|
111
|
+
return [row["type"] for row in rows]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def of_type(rows, kind):
|
|
115
|
+
return [row for row in rows if row["type"] == kind]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def one_shot_tool_model(tool_name, args, *, tool_call_id="call-1", text="done"):
|
|
119
|
+
"""A model that calls one tool, then answers. Deterministic, no network."""
|
|
120
|
+
state = {"n": 0}
|
|
121
|
+
|
|
122
|
+
def respond(messages, info):
|
|
123
|
+
state["n"] += 1
|
|
124
|
+
if state["n"] == 1:
|
|
125
|
+
return ModelResponse(parts=[ToolCallPart(tool_name, args, tool_call_id=tool_call_id)])
|
|
126
|
+
return ModelResponse(parts=[TextPart(text)])
|
|
127
|
+
|
|
128
|
+
return FunctionModel(respond)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def weather_agent_with_tool(**kwargs):
|
|
132
|
+
agent = Agent(
|
|
133
|
+
one_shot_tool_model("get_weather", {"city": "london"}),
|
|
134
|
+
name="weather_agent",
|
|
135
|
+
**kwargs,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@agent.tool_plain
|
|
139
|
+
def get_weather(city: str) -> str:
|
|
140
|
+
return f"sunny in {city}"
|
|
141
|
+
|
|
142
|
+
return agent
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
# The shape of a representative run
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def test_the_event_type_sequence_for_a_tool_using_run(instrumented, emitted):
|
|
150
|
+
result = weather_agent_with_tool().run_sync("weather in london?")
|
|
151
|
+
|
|
152
|
+
assert result.output == "done"
|
|
153
|
+
assert types_of(emitted()) == [
|
|
154
|
+
"agent_start",
|
|
155
|
+
"model_request",
|
|
156
|
+
"model_response",
|
|
157
|
+
"tool_use",
|
|
158
|
+
"tool_result",
|
|
159
|
+
"model_request",
|
|
160
|
+
"model_response",
|
|
161
|
+
"agent_end",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_the_root_agent_start_is_the_sessions_first_event(instrumented, emitted):
|
|
166
|
+
weather_agent_with_tool().run_sync("go")
|
|
167
|
+
rows = emitted()
|
|
168
|
+
|
|
169
|
+
# `agent_sessions.agent_id = any(...)` over an ORDER BY (session_id, ts)
|
|
170
|
+
# table returns the FIRST-by-time agent_id, so anything emitted ahead of the
|
|
171
|
+
# root agent_start becomes the name of the whole session in the list view.
|
|
172
|
+
assert rows[0]["type"] == "agent_start"
|
|
173
|
+
assert rows[0]["agent_id"] == "weather_agent"
|
|
174
|
+
assert len({row["session_id"] for row in rows}) == 1
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_an_async_run_produces_the_same_sequence(instrumented, emitted):
|
|
178
|
+
agent = weather_agent_with_tool()
|
|
179
|
+
|
|
180
|
+
async def main():
|
|
181
|
+
return await agent.run("go")
|
|
182
|
+
|
|
183
|
+
result = asyncio.run(main())
|
|
184
|
+
|
|
185
|
+
assert result.output == "done"
|
|
186
|
+
assert types_of(emitted()) == [
|
|
187
|
+
"agent_start",
|
|
188
|
+
"model_request",
|
|
189
|
+
"model_response",
|
|
190
|
+
"tool_use",
|
|
191
|
+
"tool_result",
|
|
192
|
+
"model_request",
|
|
193
|
+
"model_response",
|
|
194
|
+
"agent_end",
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Correlation and duration
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def test_model_events_pair_on_request_id_and_carry_an_int_duration(instrumented, emitted):
|
|
203
|
+
weather_agent_with_tool().run_sync("go")
|
|
204
|
+
rows = emitted()
|
|
205
|
+
|
|
206
|
+
requests = of_type(rows, "model_request")
|
|
207
|
+
responses = of_type(rows, "model_response")
|
|
208
|
+
assert len(requests) == len(responses) == 2
|
|
209
|
+
|
|
210
|
+
request_ids = [row["request_id"] for row in requests]
|
|
211
|
+
assert request_ids == [row["request_id"] for row in responses]
|
|
212
|
+
assert len(set(request_ids)) == 2, "request_id must be unique per model call"
|
|
213
|
+
|
|
214
|
+
for response in responses:
|
|
215
|
+
# `durationOf` prefers the closing event's duration_ms over end-start,
|
|
216
|
+
# which is what keeps model durations honest even when the dashboard's
|
|
217
|
+
# FIFO pairing brackets the wrong pair. A float silently NULLs the
|
|
218
|
+
# promoted u32 column, so the type is the assertion.
|
|
219
|
+
assert type(response["duration_ms"]) is int
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_tool_events_pair_on_tool_call_id_and_carry_a_duration(instrumented, emitted):
|
|
223
|
+
weather_agent_with_tool().run_sync("go")
|
|
224
|
+
rows = emitted()
|
|
225
|
+
|
|
226
|
+
(use,) = of_type(rows, "tool_use")
|
|
227
|
+
(result,) = of_type(rows, "tool_result")
|
|
228
|
+
assert use["tool_call_id"] == result["tool_call_id"] == "call-1"
|
|
229
|
+
assert use["tool_name"] == result["tool_name"] == "get_weather"
|
|
230
|
+
# Auto-computed by the SDK from the shared tool_call_id: if the adapter ever
|
|
231
|
+
# passed two different ids the pairing would break and this would be absent.
|
|
232
|
+
assert type(result["duration_ms"]) is int
|
|
233
|
+
assert result["output"] == "sunny in london"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_the_tool_input_is_captured(instrumented, emitted):
|
|
237
|
+
weather_agent_with_tool().run_sync("go")
|
|
238
|
+
(use,) = of_type(emitted(), "tool_use")
|
|
239
|
+
assert use["input"] == {"city": "london"}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_capture_content_off_drops_payloads_but_keeps_the_structure(emitted):
|
|
243
|
+
_core.set_strict(False)
|
|
244
|
+
failproofai_sdk.instrument("pydantic_ai", capture_content=False)
|
|
245
|
+
try:
|
|
246
|
+
weather_agent_with_tool().run_sync("go")
|
|
247
|
+
finally:
|
|
248
|
+
failproofai_sdk.uninstrument("pydantic_ai")
|
|
249
|
+
_core.set_strict(None)
|
|
250
|
+
rows = emitted()
|
|
251
|
+
|
|
252
|
+
assert types_of(rows) == [
|
|
253
|
+
"agent_start",
|
|
254
|
+
"model_request",
|
|
255
|
+
"model_response",
|
|
256
|
+
"tool_use",
|
|
257
|
+
"tool_result",
|
|
258
|
+
"model_request",
|
|
259
|
+
"model_response",
|
|
260
|
+
"agent_end",
|
|
261
|
+
]
|
|
262
|
+
assert "goal" not in rows[0]
|
|
263
|
+
assert "input" not in of_type(rows, "tool_use")[0]
|
|
264
|
+
assert "output" not in of_type(rows, "tool_result")[0]
|
|
265
|
+
assert "content" not in of_type(rows, "model_response")[0]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_usage_is_reported_both_as_ints_and_as_a_normalized_dict(instrumented, emitted):
|
|
269
|
+
weather_agent_with_tool().run_sync("go")
|
|
270
|
+
rows = emitted()
|
|
271
|
+
|
|
272
|
+
response = of_type(rows, "model_response")[0]
|
|
273
|
+
assert isinstance(response["input_tokens"], int)
|
|
274
|
+
assert isinstance(response["output_tokens"], int)
|
|
275
|
+
# Both event_summary.rs and sessionSummary.ts fall back to `usage` when the
|
|
276
|
+
# promoted ints are missing, so it ships too.
|
|
277
|
+
assert response["usage"]["input_tokens"] == response["input_tokens"]
|
|
278
|
+
|
|
279
|
+
(end,) = of_type(rows, "agent_end")
|
|
280
|
+
assert end["usage"]["requests"] == 2
|
|
281
|
+
assert end["usage"]["tool_calls"] == 1
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
# Identity, labelling, payload hygiene
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
def test_agent_ids_are_readable_names_and_never_uuids(instrumented, emitted):
|
|
289
|
+
weather_agent_with_tool().run_sync("go")
|
|
290
|
+
rows = emitted()
|
|
291
|
+
|
|
292
|
+
ids = {row["agent_id"] for row in rows}
|
|
293
|
+
assert ids == {"weather_agent"}
|
|
294
|
+
for value in ids:
|
|
295
|
+
with pytest.raises(ValueError):
|
|
296
|
+
# agent_id is a LowCardinality(String) and the primary facet on every
|
|
297
|
+
# dashboard surface; a UUID in it poisons that facet permanently.
|
|
298
|
+
uuid.UUID(value)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_an_unnamed_agent_is_labelled_from_its_variable_not_its_run_id(instrumented, emitted):
|
|
302
|
+
forecast_agent = Agent(TestModel(call_tools=[]))
|
|
303
|
+
forecast_agent.run_sync("go")
|
|
304
|
+
|
|
305
|
+
assert {row["agent_id"] for row in emitted()} == {"forecast_agent"}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_every_event_carries_the_framework_triple(instrumented, emitted):
|
|
309
|
+
weather_agent_with_tool().run_sync("go")
|
|
310
|
+
rows = emitted()
|
|
311
|
+
|
|
312
|
+
assert rows, "no events were emitted at all"
|
|
313
|
+
for row in rows:
|
|
314
|
+
assert row["framework"] == "pydantic_ai", row["type"]
|
|
315
|
+
assert row["framework_version"], row["type"]
|
|
316
|
+
assert row["integration_version"], row["type"]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
TYPE_OF_DATACLASS = {
|
|
320
|
+
re.sub(r"(?<!^)(?=[A-Z])", "_", name[: -len("Event")]).lower(): obj
|
|
321
|
+
for name, obj in vars(_schema).items()
|
|
322
|
+
if isinstance(obj, type) and name.endswith("Event")
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def test_the_type_table_covers_the_whole_schema():
|
|
327
|
+
# Guards the test below: a bad CamelCase split would make it vacuous.
|
|
328
|
+
assert len(TYPE_OF_DATACLASS) == 15
|
|
329
|
+
assert {"tool_use", "model_response", "agent_end", "hook_triggered", "error"} <= set(
|
|
330
|
+
TYPE_OF_DATACLASS
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_no_event_carries_a_field_that_shadows_a_declared_one(instrumented, emitted):
|
|
335
|
+
"""`_schema._build()` ends with `result.update(extra)`.
|
|
336
|
+
|
|
337
|
+
So an extra field named `tool_name`, `model`, `outcome` or `input_tokens`
|
|
338
|
+
silently overwrites the declared one — changing the promoted the events store
|
|
339
|
+
column and the server's computed summary — while every other test still
|
|
340
|
+
passes. Every key we emit must therefore be a declared field of that event's
|
|
341
|
+
own dataclass, a deliberate top-level name, or `fw_*`.
|
|
342
|
+
"""
|
|
343
|
+
weather_agent_with_tool().run_sync("go")
|
|
344
|
+
rows = emitted()
|
|
345
|
+
assert rows
|
|
346
|
+
|
|
347
|
+
reserved = {"timestamp", "session_id", "agent_id", "type", "environment"}
|
|
348
|
+
for row in rows:
|
|
349
|
+
declared = {
|
|
350
|
+
field.name for field in dataclasses.fields(TYPE_OF_DATACLASS[row["type"]])
|
|
351
|
+
}
|
|
352
|
+
for key in row:
|
|
353
|
+
assert (
|
|
354
|
+
key in declared
|
|
355
|
+
or key in reserved
|
|
356
|
+
or key in _core.ALLOWED_TOP_LEVEL
|
|
357
|
+
or key.startswith("fw_")
|
|
358
|
+
), f"{row['type']}.{key} is neither declared, allow-listed, nor fw_-namespaced"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_framework_detail_rides_in_the_fw_namespace(instrumented, emitted):
|
|
362
|
+
weather_agent_with_tool().run_sync("go")
|
|
363
|
+
rows = emitted()
|
|
364
|
+
|
|
365
|
+
start = rows[0]
|
|
366
|
+
assert start["fw_conversation_id"]
|
|
367
|
+
uuid.UUID(start["fw_run_id"]) # the real run id is kept, just not as agent_id
|
|
368
|
+
assert of_type(rows, "model_request")[0]["fw_run_step"] == 1
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ---------------------------------------------------------------------------
|
|
372
|
+
# Failure paths
|
|
373
|
+
# ---------------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
def test_a_failing_tool_is_reported_on_its_own_span_and_the_run_once(instrumented, emitted):
|
|
376
|
+
"""ONE report per failure, and the leaf owns it.
|
|
377
|
+
|
|
378
|
+
This asserted the opposite until the row list below was corrected: the run
|
|
379
|
+
emitted a standalone `error` for an exception `tool_result.error` had already
|
|
380
|
+
carried, so `sessionSummary.errorCount` read 2 for a single failing tool. The
|
|
381
|
+
identical failure under LangChain, LlamaIndex or CrewAI reported 1 — all
|
|
382
|
+
three suppress the second report explicitly, and say why — so error rates
|
|
383
|
+
were not comparable across frameworks and Pydantic AI runs looked twice as
|
|
384
|
+
failure-prone as they were.
|
|
385
|
+
"""
|
|
386
|
+
agent = Agent(one_shot_tool_model("explode", {"x": "1"}), name="boom_agent", retries=0)
|
|
387
|
+
|
|
388
|
+
@agent.tool_plain
|
|
389
|
+
def explode(x: str) -> str:
|
|
390
|
+
raise RuntimeError("kaboom")
|
|
391
|
+
|
|
392
|
+
with pytest.raises(RuntimeError):
|
|
393
|
+
agent.run_sync("go")
|
|
394
|
+
rows = emitted()
|
|
395
|
+
|
|
396
|
+
assert types_of(rows) == [
|
|
397
|
+
"agent_start",
|
|
398
|
+
"model_request",
|
|
399
|
+
"model_response",
|
|
400
|
+
"tool_use",
|
|
401
|
+
"tool_result",
|
|
402
|
+
"agent_end",
|
|
403
|
+
]
|
|
404
|
+
(result,) = of_type(rows, "tool_result")
|
|
405
|
+
assert result["error"] == "RuntimeError: kaboom"
|
|
406
|
+
|
|
407
|
+
# The span that owns the failure has reported it, so the run does not report
|
|
408
|
+
# it again. Zero standalone `error` events, not one.
|
|
409
|
+
assert of_type(rows, "error") == []
|
|
410
|
+
|
|
411
|
+
# "failed", never "failure" — the server counts only
|
|
412
|
+
# error|failed|timeout|rejected as a failure.
|
|
413
|
+
(end,) = of_type(rows, "agent_end")
|
|
414
|
+
assert end["outcome"] == "failed"
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_a_failing_model_request_closes_its_own_span_with_the_error(instrumented, emitted):
|
|
418
|
+
def explode(messages, info):
|
|
419
|
+
raise RuntimeError("provider is down")
|
|
420
|
+
|
|
421
|
+
agent = Agent(FunctionModel(explode), name="model_down_agent")
|
|
422
|
+
with pytest.raises(RuntimeError):
|
|
423
|
+
agent.run_sync("go")
|
|
424
|
+
rows = emitted()
|
|
425
|
+
|
|
426
|
+
assert types_of(rows) == [
|
|
427
|
+
"agent_start",
|
|
428
|
+
"model_request",
|
|
429
|
+
"model_response",
|
|
430
|
+
"agent_end",
|
|
431
|
+
]
|
|
432
|
+
(response,) = of_type(rows, "model_response")
|
|
433
|
+
# Invariant: a model_request always gets a model_response, even when the call
|
|
434
|
+
# blew up — otherwise the leaf never closes and the session reads `ongoing`
|
|
435
|
+
# forever.
|
|
436
|
+
assert response["error"] == "RuntimeError: provider is down"
|
|
437
|
+
assert response["request_id"] == of_type(rows, "model_request")[0]["request_id"]
|
|
438
|
+
assert type(response["duration_ms"]) is int
|
|
439
|
+
# The model span carries it; the run must not report it a second time.
|
|
440
|
+
assert of_type(rows, "error") == []
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def test_a_tool_retry_does_not_fail_the_run(instrumented, emitted):
|
|
444
|
+
from pydantic_ai import ModelRetry
|
|
445
|
+
|
|
446
|
+
calls = {"model": 0, "tool": 0}
|
|
447
|
+
|
|
448
|
+
def respond(messages, info):
|
|
449
|
+
calls["model"] += 1
|
|
450
|
+
if calls["model"] <= 2:
|
|
451
|
+
return ModelResponse(
|
|
452
|
+
parts=[ToolCallPart("flaky", {"x": "1"}, tool_call_id=f"call-{calls['model']}")]
|
|
453
|
+
)
|
|
454
|
+
return ModelResponse(parts=[TextPart("recovered")])
|
|
455
|
+
|
|
456
|
+
agent = Agent(FunctionModel(respond), name="retry_agent")
|
|
457
|
+
|
|
458
|
+
@agent.tool_plain
|
|
459
|
+
def flaky(x: str) -> str:
|
|
460
|
+
calls["tool"] += 1
|
|
461
|
+
if calls["tool"] == 1:
|
|
462
|
+
raise ModelRetry("try again")
|
|
463
|
+
return "ok"
|
|
464
|
+
|
|
465
|
+
assert agent.run_sync("go").output == "recovered"
|
|
466
|
+
rows = emitted()
|
|
467
|
+
|
|
468
|
+
# The retry is visible on the tool span that owned it...
|
|
469
|
+
results = of_type(rows, "tool_result")
|
|
470
|
+
assert results[0]["error"].startswith("ToolRetryError")
|
|
471
|
+
assert "error" not in results[1], "the recovered attempt was marked failed"
|
|
472
|
+
# ...and nowhere else: the run recovered, so it is not a run-level error.
|
|
473
|
+
assert of_type(rows, "error") == []
|
|
474
|
+
assert of_type(rows, "agent_end")[0]["outcome"] == "success"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def test_a_cancelled_run_is_not_an_error(instrumented, emitted):
|
|
478
|
+
async def main():
|
|
479
|
+
started = asyncio.Event()
|
|
480
|
+
agent = Agent(
|
|
481
|
+
FunctionModel(
|
|
482
|
+
lambda messages, info: ModelResponse(
|
|
483
|
+
parts=[ToolCallPart("hang", {}, tool_call_id="call-hang")]
|
|
484
|
+
)
|
|
485
|
+
),
|
|
486
|
+
name="cancelled_agent",
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
@agent.tool_plain
|
|
490
|
+
async def hang() -> str: # pragma: no cover - cancelled before it returns
|
|
491
|
+
started.set()
|
|
492
|
+
await asyncio.sleep(30)
|
|
493
|
+
return "never"
|
|
494
|
+
|
|
495
|
+
task = asyncio.create_task(agent.run("go"))
|
|
496
|
+
await started.wait()
|
|
497
|
+
task.cancel()
|
|
498
|
+
with pytest.raises(asyncio.CancelledError):
|
|
499
|
+
await task
|
|
500
|
+
|
|
501
|
+
asyncio.run(main())
|
|
502
|
+
rows = emitted()
|
|
503
|
+
|
|
504
|
+
# A cancellation is not a failure: it must not pollute the Errors surface.
|
|
505
|
+
assert of_type(rows, "error") == []
|
|
506
|
+
assert of_type(rows, "agent_end")[0]["outcome"] == "cancelled"
|
|
507
|
+
# Every open leaf still closes — a run that dies with an open tool_use leaves
|
|
508
|
+
# the session `ongoing` forever. The tool runs in its own task and its own
|
|
509
|
+
# hook returns *after* `wrap_run` does, so the run closes it on the way out;
|
|
510
|
+
# see `test_a_cancelled_leaf_closes_before_the_agent_it_belongs_to`.
|
|
511
|
+
assert len(of_type(rows, "tool_use")) == len(of_type(rows, "tool_result")) == 1
|
|
512
|
+
assert of_type(rows, "tool_result")[0].get("error") is None
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
# ---------------------------------------------------------------------------
|
|
516
|
+
# A leaf that outlives its own run
|
|
517
|
+
#
|
|
518
|
+
# Shape A promises the start and the end of a span sit in one frame, and for the
|
|
519
|
+
# run itself that holds. It does not hold *between* frames: the graph awaits a
|
|
520
|
+
# `gather` of tool tasks, so a cancellation unwinds the run body the moment that
|
|
521
|
+
# future is cancelled while each tool task's own `CancelledError` is delivered a
|
|
522
|
+
# loop iteration later. Measured against pydantic-ai 2.32 before the fix, a
|
|
523
|
+
# `wait_for` timeout put `tool_result` 1ms *after* `agent_end`, and a timeout
|
|
524
|
+
# inside the provider call did the same to `model_response` — which is the one
|
|
525
|
+
# thing every other emit in the adapter is careful never to do, because the
|
|
526
|
+
# dashboard closes the agent span at `agent_end`.
|
|
527
|
+
# ---------------------------------------------------------------------------
|
|
528
|
+
|
|
529
|
+
def test_a_cancelled_leaf_closes_before_the_agent_it_belongs_to(instrumented, emitted):
|
|
530
|
+
async def main():
|
|
531
|
+
started = asyncio.Event()
|
|
532
|
+
agent = Agent(
|
|
533
|
+
FunctionModel(
|
|
534
|
+
lambda messages, info: ModelResponse(
|
|
535
|
+
parts=[ToolCallPart("hang", {}, tool_call_id="call-hang")]
|
|
536
|
+
)
|
|
537
|
+
),
|
|
538
|
+
name="late_leaf_agent",
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
@agent.tool_plain
|
|
542
|
+
async def hang() -> str: # pragma: no cover - cancelled before it returns
|
|
543
|
+
started.set()
|
|
544
|
+
await asyncio.sleep(30)
|
|
545
|
+
return "never"
|
|
546
|
+
|
|
547
|
+
task = asyncio.create_task(agent.run("go"))
|
|
548
|
+
await started.wait()
|
|
549
|
+
task.cancel()
|
|
550
|
+
with pytest.raises(asyncio.CancelledError):
|
|
551
|
+
await task
|
|
552
|
+
# Give the tool task's own unwind a chance to run: the whole point is
|
|
553
|
+
# that it lands after the run, and a duplicate would appear here.
|
|
554
|
+
await asyncio.sleep(0.05)
|
|
555
|
+
|
|
556
|
+
asyncio.run(main())
|
|
557
|
+
rows = emitted()
|
|
558
|
+
kinds = types_of(rows)
|
|
559
|
+
|
|
560
|
+
assert kinds.index("tool_result") < kinds.index("agent_end")
|
|
561
|
+
# Exactly one, not two: the tool's own hook still runs afterwards and must
|
|
562
|
+
# not emit a second close for a span that is already closed.
|
|
563
|
+
assert kinds.count("tool_result") == 1
|
|
564
|
+
(result,) = of_type(rows, "tool_result")
|
|
565
|
+
assert result["tool_call_id"] == of_type(rows, "tool_use")[0]["tool_call_id"]
|
|
566
|
+
# Marked, so a leaf that never reported its own outcome is distinguishable
|
|
567
|
+
# from one that completed.
|
|
568
|
+
assert result["fw_incomplete"] is True
|
|
569
|
+
# A cancellation is still not a failure.
|
|
570
|
+
assert result.get("error") is None
|
|
571
|
+
assert of_type(rows, "agent_end")[0]["outcome"] == "cancelled"
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def test_a_cancelled_model_request_closes_before_the_agent_it_belongs_to(instrumented, emitted):
|
|
575
|
+
async def main():
|
|
576
|
+
started = asyncio.Event()
|
|
577
|
+
|
|
578
|
+
async def never(messages, info): # pragma: no cover - cancelled mid-call
|
|
579
|
+
started.set()
|
|
580
|
+
await asyncio.sleep(30)
|
|
581
|
+
return ModelResponse(parts=[TextPart("never")])
|
|
582
|
+
|
|
583
|
+
agent = Agent(FunctionModel(never), name="late_model_agent")
|
|
584
|
+
task = asyncio.create_task(agent.run("go"))
|
|
585
|
+
await started.wait()
|
|
586
|
+
task.cancel()
|
|
587
|
+
with pytest.raises(asyncio.CancelledError):
|
|
588
|
+
await task
|
|
589
|
+
await asyncio.sleep(0.05)
|
|
590
|
+
|
|
591
|
+
asyncio.run(main())
|
|
592
|
+
rows = emitted()
|
|
593
|
+
kinds = types_of(rows)
|
|
594
|
+
|
|
595
|
+
assert kinds.index("model_response") < kinds.index("agent_end")
|
|
596
|
+
assert kinds.count("model_response") == 1
|
|
597
|
+
(response,) = of_type(rows, "model_response")
|
|
598
|
+
assert response["request_id"] == of_type(rows, "model_request")[0]["request_id"]
|
|
599
|
+
assert response["fw_incomplete"] is True
|
|
600
|
+
# `duration_ms` is still an int on the synthesized close: the server's JSON
|
|
601
|
+
# parser drops floats, so a float silently NULLs the column.
|
|
602
|
+
assert type(response["duration_ms"]) is int
|
|
603
|
+
assert response.get("error") is None
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def test_a_streamed_model_response_says_it_was_streamed(instrumented, emitted):
|
|
607
|
+
"""`duration_ms` on a streamed response is the whole `async with` block.
|
|
608
|
+
|
|
609
|
+
Pydantic AI hands the completed `ModelResponse` back only once the caller
|
|
610
|
+
leaves `agent.run_stream(...)`, so the consumer's own time is inside the
|
|
611
|
+
number — measured against a live gateway, 1.5s of `asyncio.sleep` in the
|
|
612
|
+
consumer moved a 3294ms response to 4677ms. No hook closes the span any
|
|
613
|
+
earlier, so the flag rides on the response as well as the request: a latency
|
|
614
|
+
percentile can exclude these rows instead of averaging UI time into a p95.
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
# TestModel, not FunctionModel: only the former can serve a streamed request
|
|
618
|
+
# without a hand-written `stream_function`.
|
|
619
|
+
agent = Agent(TestModel(), name="streamed_agent")
|
|
620
|
+
|
|
621
|
+
async def main():
|
|
622
|
+
async with agent.run_stream("go") as result:
|
|
623
|
+
async for _ in result.stream_text(delta=True):
|
|
624
|
+
pass
|
|
625
|
+
await result.get_output()
|
|
626
|
+
|
|
627
|
+
asyncio.run(main())
|
|
628
|
+
rows = emitted()
|
|
629
|
+
|
|
630
|
+
responses = of_type(rows, "model_response")
|
|
631
|
+
assert responses, types_of(rows)
|
|
632
|
+
assert [r.get("fw_streaming") for r in responses] == [True] * len(responses)
|
|
633
|
+
assert all(type(r["duration_ms"]) is int for r in responses)
|
|
634
|
+
|
|
635
|
+
# ...and a non-streamed run still says so, or the flag means nothing.
|
|
636
|
+
agent.run_sync("go")
|
|
637
|
+
later = of_type(emitted(), "model_response")[len(responses):]
|
|
638
|
+
assert later
|
|
639
|
+
assert [r.get("fw_streaming") for r in later] == [False] * len(later)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def test_a_completed_leaf_is_never_marked_incomplete(instrumented, emitted):
|
|
643
|
+
"""The teardown path must not leak into the ordinary one."""
|
|
644
|
+
weather_agent_with_tool().run_sync("weather in london?")
|
|
645
|
+
rows = emitted()
|
|
646
|
+
|
|
647
|
+
assert [r for r in rows if r.get("fw_incomplete")] == []
|
|
648
|
+
assert types_of(rows).index("tool_result") < types_of(rows).index("agent_end")
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def test_uninstrumenting_mid_run_closes_the_run_exactly_once(instrumented, emitted):
|
|
652
|
+
"""`uninstrument()` and the run's own end are both allowed to go first.
|
|
653
|
+
|
|
654
|
+
Before the fix they both went: teardown emitted `agent_end` (`cancelled`)
|
|
655
|
+
and the run then emitted a second `agent_end` (`success`) against a span the
|
|
656
|
+
dashboard had already closed, with the tool's `tool_result` stranded between
|
|
657
|
+
them.
|
|
658
|
+
"""
|
|
659
|
+
|
|
660
|
+
async def main():
|
|
661
|
+
started = asyncio.Event()
|
|
662
|
+
release = asyncio.Event()
|
|
663
|
+
agent = Agent(
|
|
664
|
+
one_shot_tool_model("wait", {}, tool_call_id="call-wait"),
|
|
665
|
+
name="torn_down_agent",
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
@agent.tool_plain
|
|
669
|
+
async def wait() -> str:
|
|
670
|
+
started.set()
|
|
671
|
+
await release.wait()
|
|
672
|
+
return "finished"
|
|
673
|
+
|
|
674
|
+
task = asyncio.create_task(agent.run("go"))
|
|
675
|
+
await started.wait()
|
|
676
|
+
failproofai_sdk.uninstrument("pydantic_ai")
|
|
677
|
+
release.set()
|
|
678
|
+
await task
|
|
679
|
+
|
|
680
|
+
asyncio.run(main())
|
|
681
|
+
rows = emitted()
|
|
682
|
+
kinds = types_of(rows)
|
|
683
|
+
|
|
684
|
+
assert kinds.count("agent_start") == 1
|
|
685
|
+
assert kinds.count("agent_end") == 1, kinds
|
|
686
|
+
assert of_type(rows, "agent_end")[0]["outcome"] == "cancelled"
|
|
687
|
+
# ...and the leaf still closed, inside the span it belongs to.
|
|
688
|
+
assert kinds.count("tool_result") == 1
|
|
689
|
+
assert kinds.index("tool_result") < kinds.index("agent_end")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
# ---------------------------------------------------------------------------
|
|
693
|
+
# What `tool_result.output` actually says
|
|
694
|
+
# ---------------------------------------------------------------------------
|
|
695
|
+
|
|
696
|
+
def _tool_returning(value, *, name="produce"):
|
|
697
|
+
agent = Agent(one_shot_tool_model(name, {}), name="output_shape_agent")
|
|
698
|
+
agent.tool_plain(lambda: value, name=name)
|
|
699
|
+
return agent
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def test_a_tool_returning_a_pydantic_model_is_recorded_as_its_fields(instrumented, emitted):
|
|
703
|
+
from pydantic import BaseModel
|
|
704
|
+
|
|
705
|
+
class Weather(BaseModel):
|
|
706
|
+
city: str
|
|
707
|
+
celsius: int
|
|
708
|
+
|
|
709
|
+
_tool_returning(Weather(city="Faro", celsius=21)).run_sync("go")
|
|
710
|
+
|
|
711
|
+
(result,) = of_type(emitted(), "tool_result")
|
|
712
|
+
# Not "Weather(city='Faro', celsius=21)": `truncate` reprs an object with no
|
|
713
|
+
# JSON shape, and this one has one.
|
|
714
|
+
assert result["output"] == {"city": "Faro", "celsius": 21}
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def test_a_tool_returning_a_dataclass_is_recorded_as_its_fields(instrumented, emitted):
|
|
718
|
+
@dataclasses.dataclass
|
|
719
|
+
class Point:
|
|
720
|
+
x: int
|
|
721
|
+
y: int
|
|
722
|
+
|
|
723
|
+
_tool_returning(Point(1, 2)).run_sync("go")
|
|
724
|
+
|
|
725
|
+
(result,) = of_type(emitted(), "tool_result")
|
|
726
|
+
assert result["output"] == {"x": 1, "y": 2}
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def test_a_tool_returning_ToolReturn_is_recorded_as_its_return_value(instrumented, emitted):
|
|
730
|
+
"""`ToolReturn` is an envelope, and the envelope is not the answer.
|
|
731
|
+
|
|
732
|
+
`return_value` is what goes back to the model; `metadata` is documented as
|
|
733
|
+
never being shown to it at all. Recording the repr of the whole thing buries
|
|
734
|
+
the one and publishes the other.
|
|
735
|
+
"""
|
|
736
|
+
from pydantic_ai.messages import ToolReturn
|
|
737
|
+
|
|
738
|
+
_tool_returning(
|
|
739
|
+
ToolReturn(
|
|
740
|
+
return_value={"answer": 42},
|
|
741
|
+
content="the model sees this",
|
|
742
|
+
metadata={"secret": "not for the model"},
|
|
743
|
+
)
|
|
744
|
+
).run_sync("go")
|
|
745
|
+
|
|
746
|
+
(result,) = of_type(emitted(), "tool_result")
|
|
747
|
+
assert result["output"] == {"answer": 42}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def test_an_ordinary_tool_return_value_is_untouched(instrumented, emitted):
|
|
751
|
+
"""The unwrapping is narrow: only shapes that have a JSON form."""
|
|
752
|
+
_tool_returning("sunny in london").run_sync("go")
|
|
753
|
+
|
|
754
|
+
(result,) = of_type(emitted(), "tool_result")
|
|
755
|
+
assert result["output"] == "sunny in london"
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
class _NoJsonShape:
|
|
759
|
+
def __repr__(self) -> str:
|
|
760
|
+
return "<opaque handle>"
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def test_an_object_with_no_json_shape_is_handed_through_untouched():
|
|
764
|
+
"""The unwrapping must not become a second, worse serializer.
|
|
765
|
+
|
|
766
|
+
Anything that is not a Pydantic model, a dataclass or a `ToolReturn` comes
|
|
767
|
+
back byte-identical, so `_core.truncate` keeps deciding what happens to it —
|
|
768
|
+
including the `repr` fallback it documents for an object with no JSON shape.
|
|
769
|
+
(Unit-level: pydantic-ai itself refuses to send such a value to a model, so
|
|
770
|
+
there is no end-to-end run that reaches this line.)
|
|
771
|
+
"""
|
|
772
|
+
opaque = _NoJsonShape()
|
|
773
|
+
assert adapter._tool_output(opaque) is opaque
|
|
774
|
+
assert _core.truncate(adapter._tool_output(opaque)) == "<opaque handle>"
|
|
775
|
+
|
|
776
|
+
# A class object is not an instance, and `dataclasses.is_dataclass` is True
|
|
777
|
+
# for both.
|
|
778
|
+
@dataclasses.dataclass
|
|
779
|
+
class Shape:
|
|
780
|
+
x: int
|
|
781
|
+
|
|
782
|
+
assert adapter._tool_output(Shape) is Shape
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
# ---------------------------------------------------------------------------
|
|
786
|
+
# Interop with the hand-written API
|
|
787
|
+
# ---------------------------------------------------------------------------
|
|
788
|
+
|
|
789
|
+
def test_an_enclosing_agenteye_scope_owns_the_session(instrumented, emitted):
|
|
790
|
+
with failproofai_sdk.agent("planner", goal="what is the weather?"):
|
|
791
|
+
weather_agent_with_tool().run_sync("go")
|
|
792
|
+
rows = emitted()
|
|
793
|
+
|
|
794
|
+
assert len({row["session_id"] for row in rows}) == 1, "the adapter split the session in two"
|
|
795
|
+
assert types_of(rows)[0] == "agent_start"
|
|
796
|
+
assert rows[0]["agent_id"] == "planner"
|
|
797
|
+
(nested,) = [
|
|
798
|
+
row for row in rows if row["type"] == "agent_start" and row["agent_id"] == "weather_agent"
|
|
799
|
+
]
|
|
800
|
+
assert nested["parent_id"] == "planner"
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def test_a_nested_agent_run_nests(instrumented, emitted):
|
|
804
|
+
researcher = Agent(TestModel(call_tools=[]), name="researcher")
|
|
805
|
+
supervisor = Agent(one_shot_tool_model("delegate", {"q": "x"}), name="supervisor")
|
|
806
|
+
|
|
807
|
+
@supervisor.tool_plain
|
|
808
|
+
async def delegate(q: str) -> str:
|
|
809
|
+
return (await researcher.run(q)).output
|
|
810
|
+
|
|
811
|
+
supervisor.run_sync("go")
|
|
812
|
+
rows = emitted()
|
|
813
|
+
|
|
814
|
+
assert len({row["session_id"] for row in rows}) == 1
|
|
815
|
+
starts = {row["agent_id"]: row for row in of_type(rows, "agent_start")}
|
|
816
|
+
assert set(starts) == {"supervisor", "researcher"}
|
|
817
|
+
assert starts["supervisor"].get("parent_id") is None
|
|
818
|
+
assert starts["researcher"]["parent_id"] == "supervisor"
|
|
819
|
+
# Every event carries the agent_id of an agent whose agent_start is open —
|
|
820
|
+
# otherwise the dashboard synthesizes a never-ending root span.
|
|
821
|
+
assert {row["agent_id"] for row in rows} == {"supervisor", "researcher"}
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def test_the_conversation_id_becomes_the_session_id(instrumented, emitted):
|
|
825
|
+
agent = Agent(TestModel(call_tools=[]), name="chatty")
|
|
826
|
+
agent.run_sync("first", conversation_id="conversation-42")
|
|
827
|
+
agent.run_sync("second", conversation_id="conversation-42")
|
|
828
|
+
rows = emitted()
|
|
829
|
+
|
|
830
|
+
# A conversation spanning several runs is ONE Failproof AI session; two runs are
|
|
831
|
+
# two agent spans inside it.
|
|
832
|
+
assert {row["session_id"] for row in rows} == {"conversation-42"}
|
|
833
|
+
assert len(of_type(rows, "agent_start")) == 2
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
# ---------------------------------------------------------------------------
|
|
837
|
+
# The adapter must never break the host agent
|
|
838
|
+
# ---------------------------------------------------------------------------
|
|
839
|
+
|
|
840
|
+
class Boom:
|
|
841
|
+
"""Every attribute is a callable that raises."""
|
|
842
|
+
|
|
843
|
+
def __getattr__(self, name):
|
|
844
|
+
def explode(*args, **kwargs):
|
|
845
|
+
raise RuntimeError(f"translator exploded in {name}")
|
|
846
|
+
|
|
847
|
+
return explode
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def test_a_translator_that_raises_on_every_call_leaves_the_run_intact(
|
|
851
|
+
instrumented, emitted, monkeypatch
|
|
852
|
+
):
|
|
853
|
+
monkeypatch.setattr(adapter, "_tracker", Boom())
|
|
854
|
+
|
|
855
|
+
result = weather_agent_with_tool().run_sync("weather in london?")
|
|
856
|
+
|
|
857
|
+
assert result.output == "done", "the adapter changed what the framework returned"
|
|
858
|
+
assert emitted() == [], "a broken translator still managed to emit"
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def test_a_failing_capability_injection_does_not_break_agent_construction(
|
|
862
|
+
instrumented, emitted, monkeypatch
|
|
863
|
+
):
|
|
864
|
+
def explode(kwargs):
|
|
865
|
+
raise RuntimeError("injection exploded")
|
|
866
|
+
|
|
867
|
+
monkeypatch.setattr(adapter, "_inject", explode)
|
|
868
|
+
|
|
869
|
+
# Construction still succeeds and the run is untouched; it simply records
|
|
870
|
+
# nothing, because the capability never got attached.
|
|
871
|
+
assert weather_agent_with_tool().run_sync("go").output == "done"
|
|
872
|
+
assert emitted() == []
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def test_strict_mode_turns_the_swallow_into_a_raise(instrumented, monkeypatch):
|
|
876
|
+
"""Without this, "it didn't crash" is the only provable property.
|
|
877
|
+
|
|
878
|
+
FAILPROOFAI_SDK_STRICT=1 is what makes the never-raise policy testable at all — and
|
|
879
|
+
it doubles as the production switch for debugging an adapter gone quiet.
|
|
880
|
+
"""
|
|
881
|
+
monkeypatch.setattr(adapter, "_tracker", Boom())
|
|
882
|
+
_core.set_strict(True)
|
|
883
|
+
try:
|
|
884
|
+
with pytest.raises(RuntimeError, match="translator exploded"):
|
|
885
|
+
weather_agent_with_tool().run_sync("go")
|
|
886
|
+
finally:
|
|
887
|
+
_core.set_strict(False)
|
|
888
|
+
_core.reset_failures()
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
# ---------------------------------------------------------------------------
|
|
892
|
+
# Install / uninstall discipline
|
|
893
|
+
# ---------------------------------------------------------------------------
|
|
894
|
+
|
|
895
|
+
def test_install_patches_agent_init_and_uninstall_restores_the_saved_object():
|
|
896
|
+
original = Agent.__init__
|
|
897
|
+
failproofai_sdk.instrument("pydantic_ai")
|
|
898
|
+
try:
|
|
899
|
+
assert Agent.__init__ is not original
|
|
900
|
+
assert _core.is_wrapped(Agent.__init__)
|
|
901
|
+
# The SAVED object, never a re-import: re-importing to restore hands back
|
|
902
|
+
# whatever the attribute's source currently holds, which is how two
|
|
903
|
+
# instrumentation libraries silently un-patch each other.
|
|
904
|
+
assert _core.unwrap(Agent.__init__) is original
|
|
905
|
+
finally:
|
|
906
|
+
failproofai_sdk.uninstrument("pydantic_ai")
|
|
907
|
+
assert Agent.__init__ is original
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def test_instrumenting_twice_is_a_no_op():
|
|
911
|
+
failproofai_sdk.instrument("pydantic_ai")
|
|
912
|
+
try:
|
|
913
|
+
assert failproofai_sdk.instrument("pydantic_ai") == ()
|
|
914
|
+
finally:
|
|
915
|
+
failproofai_sdk.uninstrument("pydantic_ai")
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def test_an_explicit_capability_is_not_duplicated(instrumented, emitted):
|
|
919
|
+
agent = Agent(TestModel(call_tools=[]), name="explicit", capabilities=[FailproofAI()])
|
|
920
|
+
agent.run_sync("go")
|
|
921
|
+
|
|
922
|
+
assert types_of(emitted()) == ["agent_start", "model_request", "model_response", "agent_end"]
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def test_an_agent_built_while_instrumented_goes_inert_after_uninstrument(emitted):
|
|
926
|
+
failproofai_sdk.instrument("pydantic_ai")
|
|
927
|
+
agent = weather_agent_with_tool()
|
|
928
|
+
failproofai_sdk.uninstrument("pydantic_ai")
|
|
929
|
+
|
|
930
|
+
# We cannot retro-remove the capability object from an already-built agent,
|
|
931
|
+
# so uninstall() makes it a pass-through instead. Anything less means
|
|
932
|
+
# uninstrument() does not actually stop the recording.
|
|
933
|
+
assert agent.run_sync("go").output == "done"
|
|
934
|
+
assert emitted() == []
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def test_unknown_options_are_ignored_rather_than_fatal(emitted):
|
|
938
|
+
# `failproofai_sdk.instrument()` with no name fans the same **options out to every
|
|
939
|
+
# detected adapter, so a keyword meant for LangChain must not take this one
|
|
940
|
+
# down.
|
|
941
|
+
_core.set_strict(True)
|
|
942
|
+
try:
|
|
943
|
+
assert failproofai_sdk.instrument("pydantic_ai", langgraph_node_hooks=False) == ("pydantic_ai",)
|
|
944
|
+
finally:
|
|
945
|
+
failproofai_sdk.uninstrument("pydantic_ai")
|
|
946
|
+
_core.set_strict(None)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
# ---------------------------------------------------------------------------
|
|
950
|
+
# Structural anti-drift — the test that catches a silent upstream rename
|
|
951
|
+
# ---------------------------------------------------------------------------
|
|
952
|
+
|
|
953
|
+
OVERRIDES = {
|
|
954
|
+
name: obj
|
|
955
|
+
for name, obj in vars(FailproofAI).items()
|
|
956
|
+
if inspect.isfunction(obj) and not name.startswith("_")
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
class TestAntiDrift:
|
|
961
|
+
"""Reflection over the REAL base class.
|
|
962
|
+
|
|
963
|
+
If upstream renames a callback, our override becomes dead code that is never
|
|
964
|
+
called — the framework keeps working, every fake-based test above keeps
|
|
965
|
+
passing, and we record nothing. Nothing but reflection catches that.
|
|
966
|
+
"""
|
|
967
|
+
|
|
968
|
+
def test_the_override_set_is_not_empty(self):
|
|
969
|
+
# Guards every test below: `for name in {}` passes vacuously.
|
|
970
|
+
assert set(OVERRIDES) == {
|
|
971
|
+
"get_ordering",
|
|
972
|
+
"wrap_run",
|
|
973
|
+
"wrap_model_request",
|
|
974
|
+
"wrap_tool_execute",
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
@pytest.mark.parametrize("name", sorted(OVERRIDES))
|
|
978
|
+
def test_each_override_still_exists_on_the_base_class(self, name):
|
|
979
|
+
assert hasattr(AbstractCapability, name), (
|
|
980
|
+
f"FailproofAI.{name} no longer overrides anything on AbstractCapability — "
|
|
981
|
+
"it is dead code that will never be called."
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
@pytest.mark.parametrize("name", sorted(OVERRIDES))
|
|
985
|
+
def test_each_override_actually_replaces_the_base_implementation(self, name):
|
|
986
|
+
assert getattr(AbstractCapability, name) is not OVERRIDES[name]
|
|
987
|
+
|
|
988
|
+
@pytest.mark.parametrize("name", sorted(OVERRIDES))
|
|
989
|
+
def test_every_parameter_we_declare_still_exists_on_the_base_signature(self, name):
|
|
990
|
+
ours = inspect.signature(OVERRIDES[name]).parameters
|
|
991
|
+
theirs = inspect.signature(getattr(AbstractCapability, name)).parameters
|
|
992
|
+
for parameter in ours.values():
|
|
993
|
+
if parameter.kind in (
|
|
994
|
+
inspect.Parameter.VAR_POSITIONAL,
|
|
995
|
+
inspect.Parameter.VAR_KEYWORD,
|
|
996
|
+
):
|
|
997
|
+
continue
|
|
998
|
+
assert parameter.name in theirs, (
|
|
999
|
+
f"FailproofAI.{name} declares {parameter.name!r}, which "
|
|
1000
|
+
f"AbstractCapability.{name} no longer accepts"
|
|
1001
|
+
)
|
|
1002
|
+
assert parameter.kind == theirs[parameter.name].kind, (
|
|
1003
|
+
f"FailproofAI.{name} takes {parameter.name!r} as {parameter.kind}, "
|
|
1004
|
+
f"the base now takes it as {theirs[parameter.name].kind}"
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
@pytest.mark.parametrize("name", sorted(OVERRIDES))
|
|
1008
|
+
def test_every_hook_we_override_is_still_async_where_the_base_is(self, name):
|
|
1009
|
+
# A `def` where the base has `async def` produces a coroutine the
|
|
1010
|
+
# framework awaits into a TypeError — or worse, never awaits at all.
|
|
1011
|
+
assert inspect.iscoroutinefunction(OVERRIDES[name]) == inspect.iscoroutinefunction(
|
|
1012
|
+
getattr(AbstractCapability, name)
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
def test_agent_init_still_takes_a_keyword_only_capabilities_argument(self):
|
|
1016
|
+
# This is the install mechanism. If `capabilities` stops being a
|
|
1017
|
+
# keyword-only parameter of Agent.__init__, `install()` silently attaches
|
|
1018
|
+
# nothing at all.
|
|
1019
|
+
parameter = inspect.signature(Agent.__init__).parameters["capabilities"]
|
|
1020
|
+
assert parameter.kind is inspect.Parameter.KEYWORD_ONLY
|
|
1021
|
+
|
|
1022
|
+
def test_there_is_still_no_supported_global_capability_default(self):
|
|
1023
|
+
# If upstream ever grows a public one, `install()` should stop patching
|
|
1024
|
+
# `Agent.__init__` and use it — patching a constructor cannot reach
|
|
1025
|
+
# agents that already exist.
|
|
1026
|
+
assert not hasattr(Agent, "capabilities_all")
|
|
1027
|
+
assert not hasattr(Agent, "instrument_all_capabilities")
|
|
1028
|
+
|
|
1029
|
+
@pytest.mark.parametrize(
|
|
1030
|
+
"attribute",
|
|
1031
|
+
["run_id", "conversation_id", "prompt", "agent", "run_step", "metadata", "usage"],
|
|
1032
|
+
)
|
|
1033
|
+
def test_run_context_still_carries_the_fields_we_read(self, attribute):
|
|
1034
|
+
assert attribute in inspect.get_annotations(RunContext, eval_str=False)
|
|
1035
|
+
|
|
1036
|
+
@pytest.mark.parametrize(
|
|
1037
|
+
"attribute", ["model", "messages", "model_request_parameters", "streaming", "model_id"]
|
|
1038
|
+
)
|
|
1039
|
+
def test_model_request_context_still_carries_the_fields_we_read(self, attribute):
|
|
1040
|
+
assert attribute in inspect.get_annotations(ModelRequestContext, eval_str=False)
|
|
1041
|
+
|
|
1042
|
+
@pytest.mark.parametrize("attribute", ["input_tokens", "output_tokens"])
|
|
1043
|
+
def test_usage_still_uses_the_2_0_token_names(self, attribute):
|
|
1044
|
+
# 2.0 renamed request_tokens/response_tokens. Reading the old names would
|
|
1045
|
+
# report zero tokens on every event, at HTTP 200.
|
|
1046
|
+
assert hasattr(RunUsage(), attribute)
|
|
1047
|
+
assert hasattr(RequestUsage(), attribute)
|
|
1048
|
+
|
|
1049
|
+
@pytest.mark.parametrize("attribute", ["tool_name", "tool_call_id", "args"])
|
|
1050
|
+
def test_tool_call_part_still_carries_the_fields_we_read(self, attribute):
|
|
1051
|
+
assert hasattr(ToolCallPart("t", {}, tool_call_id="x"), attribute)
|
|
1052
|
+
|
|
1053
|
+
def test_at_least_one_control_flow_exception_is_still_recognised(self):
|
|
1054
|
+
# If every name in the list disappeared, `_CONTROL_FLOW` would go empty
|
|
1055
|
+
# and control flow would start being reported as run failures.
|
|
1056
|
+
assert adapter._CONTROL_FLOW
|
|
1057
|
+
|
|
1058
|
+
def test_we_deliberately_do_not_override_the_two_hooks_with_side_effects(self):
|
|
1059
|
+
capability = FailproofAI()
|
|
1060
|
+
# Overriding wrap_run_event_stream makes `agent.run()` switch itself into
|
|
1061
|
+
# streaming mode; overriding wrap_node_run flips has_wrap_node_run and
|
|
1062
|
+
# buys nothing but a doubled row count.
|
|
1063
|
+
assert capability.has_wrap_run_event_stream is False
|
|
1064
|
+
assert capability.has_wrap_node_run is False
|
|
1065
|
+
|
|
1066
|
+
def test_the_capability_is_constructible_and_orders_itself_outermost(self):
|
|
1067
|
+
ordering = FailproofAI().get_ordering()
|
|
1068
|
+
assert ordering.position == "outermost"
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def test_a_run_failure_no_leaf_owns_still_gets_its_one_error_event(instrumented, emitted):
|
|
1072
|
+
"""The other half of the suppression, and the one that must not over-correct.
|
|
1073
|
+
|
|
1074
|
+
Suppressing the run's `error` whenever a leaf reported one is right; doing it
|
|
1075
|
+
unconditionally would leave a failure that no span owns with no error row at
|
|
1076
|
+
all, so it reaches the Errors surface through nothing. Here the model and the
|
|
1077
|
+
tool both succeed and the failure comes from output validation, so no leaf
|
|
1078
|
+
carries it and `_end_run` is the only reporter left.
|
|
1079
|
+
"""
|
|
1080
|
+
from pydantic_ai import ModelRetry
|
|
1081
|
+
|
|
1082
|
+
agent = Agent(
|
|
1083
|
+
FunctionModel(lambda messages, info: ModelResponse(parts=[TextPart("nope")])),
|
|
1084
|
+
name="validator_agent",
|
|
1085
|
+
output_type=str,
|
|
1086
|
+
retries=0,
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
@agent.output_validator
|
|
1090
|
+
def reject(value: str) -> str:
|
|
1091
|
+
raise ModelRetry("never acceptable")
|
|
1092
|
+
|
|
1093
|
+
with pytest.raises(Exception):
|
|
1094
|
+
agent.run_sync("go")
|
|
1095
|
+
rows = emitted()
|
|
1096
|
+
|
|
1097
|
+
errors = of_type(rows, "error")
|
|
1098
|
+
assert len(errors) == 1, f"expected exactly one standalone error, got {types_of(rows)}"
|
|
1099
|
+
assert of_type(rows, "agent_end")[0]["outcome"] == "failed"
|
|
1100
|
+
# Strictly before agent_end: the dashboard closes the agent span there and
|
|
1101
|
+
# anything after it is attributed to nothing.
|
|
1102
|
+
assert types_of(rows).index("error") < types_of(rows).index("agent_end")
|