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,923 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the failproofai_sdk SDK.
|
|
3
|
+
|
|
4
|
+
Unit tests use EventNamespace with a mock writer (no disk I/O).
|
|
5
|
+
Integration tests use configure() + real tmpdir + flush_now().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
import failproofai_sdk
|
|
16
|
+
from failproofai_sdk._events import EventNamespace
|
|
17
|
+
from failproofai_sdk._schema import (
|
|
18
|
+
AgentEndEvent,
|
|
19
|
+
AgentPauseEvent,
|
|
20
|
+
AgentResumeEvent,
|
|
21
|
+
AgentStartEvent,
|
|
22
|
+
ErrorEvent,
|
|
23
|
+
HookCompletedEvent,
|
|
24
|
+
HookTriggeredEvent,
|
|
25
|
+
HumanInputEvent,
|
|
26
|
+
HumanInterruptEvent,
|
|
27
|
+
HumanPauseEvent,
|
|
28
|
+
HumanWaitEvent,
|
|
29
|
+
ModelRequestEvent,
|
|
30
|
+
ModelResponseEvent,
|
|
31
|
+
ToolResultEvent,
|
|
32
|
+
ToolUseEvent,
|
|
33
|
+
)
|
|
34
|
+
from failproofai_sdk._writer import EventWriter
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Helpers
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
class MockWriter:
|
|
42
|
+
"""Collects submitted entries without touching disk."""
|
|
43
|
+
|
|
44
|
+
def __init__(self):
|
|
45
|
+
self.entries: list[dict] = []
|
|
46
|
+
|
|
47
|
+
def submit(self, entry: dict) -> None:
|
|
48
|
+
self.entries.append(entry)
|
|
49
|
+
|
|
50
|
+
def last(self) -> dict:
|
|
51
|
+
return self.entries[-1]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture()
|
|
55
|
+
def mock_writer():
|
|
56
|
+
return MockWriter()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture()
|
|
60
|
+
def ns(mock_writer):
|
|
61
|
+
return EventNamespace(mock_writer)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Reserved field validation
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize("reserved", ["timestamp", "type"])
|
|
69
|
+
def test_reserved_fields_raise_via_extra(ns, reserved):
|
|
70
|
+
# timestamp and type aren't in the explicit signature so they land in **fields and hit our validator
|
|
71
|
+
with pytest.raises(ValueError, match="Reserved field"):
|
|
72
|
+
ns.agent_start(session_id="s1", agent_id="a1", **{reserved: "bad"})
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.parametrize("reserved", ["session_id", "agent_id"])
|
|
76
|
+
def test_reserved_fields_blocked_by_signature(ns, reserved):
|
|
77
|
+
# session_id and agent_id are explicit params; Python raises TypeError on duplicate keyword
|
|
78
|
+
with pytest.raises(TypeError):
|
|
79
|
+
ns.agent_start(session_id="s1", agent_id="a1", **{reserved: "bad"})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# duration_ms rejection on paired end events
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def test_tool_result_rejects_duration_ms(ns):
|
|
87
|
+
with pytest.raises(ValueError, match="duration_ms"):
|
|
88
|
+
ns.tool_result(session_id="s1", agent_id="a1", tool_name="t", tool_call_id="tc1", duration_ms=99)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_hook_completed_rejects_duration_ms(ns):
|
|
92
|
+
with pytest.raises(ValueError, match="duration_ms"):
|
|
93
|
+
ns.hook_completed(session_id="s1", agent_id="a1", hook_name="h", hook_id="hid1", duration_ms=99)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Null field omission
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def test_null_fields_omitted_tool_use(ns, mock_writer):
|
|
101
|
+
ns.tool_use(session_id="s1", agent_id="a1", tool_name="search", tool_call_id="tc1")
|
|
102
|
+
d = mock_writer.last()
|
|
103
|
+
assert "input" not in d
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_null_fields_omitted_model_request(ns, mock_writer):
|
|
107
|
+
ns.model_request(session_id="s1", agent_id="a1")
|
|
108
|
+
d = mock_writer.last()
|
|
109
|
+
for f in ("model", "messages", "system", "tools"):
|
|
110
|
+
assert f not in d
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_null_fields_omitted_model_response(ns, mock_writer):
|
|
114
|
+
ns.model_response(session_id="s1", agent_id="a1")
|
|
115
|
+
d = mock_writer.last()
|
|
116
|
+
for f in ("model", "stop_reason", "input_tokens", "output_tokens", "content", "role"):
|
|
117
|
+
assert f not in d
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_model_request_fields_captured(ns, mock_writer):
|
|
121
|
+
messages = [{"role": "user", "content": "hello"}]
|
|
122
|
+
tools = [{"name": "search", "input_schema": {"type": "object"}}]
|
|
123
|
+
ns.model_request(
|
|
124
|
+
session_id="s1", agent_id="a1", model="claude-opus-4-6",
|
|
125
|
+
messages=messages, system="You are helpful.", tools=tools,
|
|
126
|
+
)
|
|
127
|
+
d = mock_writer.last()
|
|
128
|
+
assert d["messages"] == messages
|
|
129
|
+
assert d["system"] == "You are helpful."
|
|
130
|
+
assert d["tools"] == tools
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_model_response_content_as_string(ns, mock_writer):
|
|
134
|
+
ns.model_response(
|
|
135
|
+
session_id="s1", agent_id="a1",
|
|
136
|
+
content="plain text completion", role="assistant",
|
|
137
|
+
)
|
|
138
|
+
d = mock_writer.last()
|
|
139
|
+
assert d["content"] == "plain text completion"
|
|
140
|
+
assert d["role"] == "assistant"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_model_response_content_as_blocks(ns, mock_writer):
|
|
144
|
+
blocks = [
|
|
145
|
+
{"type": "text", "text": "I'll search..."},
|
|
146
|
+
{"type": "tool_use", "id": "toolu_01", "name": "search", "input": {"q": "x"}},
|
|
147
|
+
]
|
|
148
|
+
ns.model_response(session_id="s1", agent_id="a1", content=blocks, role="assistant")
|
|
149
|
+
d = mock_writer.last()
|
|
150
|
+
assert d["content"] == blocks
|
|
151
|
+
assert d["role"] == "assistant"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_null_fields_omitted_agent_start(ns, mock_writer):
|
|
155
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
156
|
+
d = mock_writer.last()
|
|
157
|
+
assert "goal" not in d
|
|
158
|
+
assert "parent_id" not in d
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_null_fields_omitted_error(ns, mock_writer):
|
|
162
|
+
ns.error(session_id="s1", agent_id="a1", error_type="ValueError", message="oops")
|
|
163
|
+
d = mock_writer.last()
|
|
164
|
+
assert "traceback" not in d
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
# Field ordering
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def test_field_ordering(ns, mock_writer):
|
|
172
|
+
ns.agent_start(session_id="s1", agent_id="a1", goal="do stuff")
|
|
173
|
+
d = mock_writer.last()
|
|
174
|
+
keys = list(d.keys())
|
|
175
|
+
assert keys[0] == "timestamp"
|
|
176
|
+
assert keys[1] == "session_id"
|
|
177
|
+
assert keys[2] == "agent_id"
|
|
178
|
+
assert keys[3] == "type"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_custom_fields_at_end(ns, mock_writer):
|
|
182
|
+
ns.agent_end(session_id="s1", agent_id="a1", outcome="success", message="done", custom_key="val")
|
|
183
|
+
d = mock_writer.last()
|
|
184
|
+
keys = list(d.keys())
|
|
185
|
+
assert keys.index("message") > keys.index("outcome")
|
|
186
|
+
assert keys.index("custom_key") > keys.index("outcome")
|
|
187
|
+
assert d["message"] == "done"
|
|
188
|
+
assert d["custom_key"] == "val"
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Auto duration computation
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
def test_tool_use_result_duration(ns, mock_writer):
|
|
196
|
+
ns.tool_use(session_id="s1", agent_id="a1", tool_name="search", tool_call_id="tc1")
|
|
197
|
+
time.sleep(0.01)
|
|
198
|
+
ns.tool_result(session_id="s1", agent_id="a1", tool_name="search", tool_call_id="tc1", output="ok")
|
|
199
|
+
d = mock_writer.last()
|
|
200
|
+
assert "duration_ms" in d
|
|
201
|
+
assert d["duration_ms"] >= 10.0 # at least 10ms
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_hook_triggered_completed_duration(ns, mock_writer):
|
|
205
|
+
ns.hook_triggered(session_id="s1", agent_id="a1", hook_name="pre_tool_use", hook_id="hid1")
|
|
206
|
+
time.sleep(0.01)
|
|
207
|
+
ns.hook_completed(session_id="s1", agent_id="a1", hook_name="pre_tool_use", hook_id="hid1", outcome="success")
|
|
208
|
+
d = mock_writer.last()
|
|
209
|
+
assert "duration_ms" in d
|
|
210
|
+
assert d["duration_ms"] >= 10.0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# Missing correlation ID → duration_ms absent
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def test_tool_result_without_prior_tool_use(ns, mock_writer):
|
|
218
|
+
ns.tool_result(session_id="s1", agent_id="a1", tool_name="search", tool_call_id="no-prior")
|
|
219
|
+
d = mock_writer.last()
|
|
220
|
+
assert "duration_ms" not in d
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_hook_completed_without_prior_triggered(ns, mock_writer):
|
|
224
|
+
ns.hook_completed(session_id="s1", agent_id="a1", hook_name="h", hook_id="no-prior", outcome="success")
|
|
225
|
+
d = mock_writer.last()
|
|
226
|
+
assert "duration_ms" not in d
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
# All 9 event types — type field smoke test
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
@pytest.mark.parametrize("method,kwargs,expected_type", [
|
|
234
|
+
("tool_use", {"tool_name": "t", "tool_call_id": "c1"}, "tool_use"),
|
|
235
|
+
("tool_result", {"tool_name": "t", "tool_call_id": "c2"}, "tool_result"),
|
|
236
|
+
("model_request", {}, "model_request"),
|
|
237
|
+
("model_response", {}, "model_response"),
|
|
238
|
+
("agent_start", {}, "agent_start"),
|
|
239
|
+
("agent_end", {}, "agent_end"),
|
|
240
|
+
("hook_triggered", {"hook_name": "h", "hook_id": "hid"}, "hook_triggered"),
|
|
241
|
+
("hook_completed", {"hook_name": "h", "hook_id": "hid2"}, "hook_completed"),
|
|
242
|
+
("error", {"error_type": "ValueError", "message": "oops"}, "error"),
|
|
243
|
+
("human_wait", {"input_id": "inp1"}, "human_wait"),
|
|
244
|
+
("human_input", {"input_id": "inp2"}, "human_input"),
|
|
245
|
+
("human_pause", {}, "human_pause"),
|
|
246
|
+
("human_interrupt", {}, "human_interrupt"),
|
|
247
|
+
("agent_pause", {"pause_id": "p1"}, "agent_pause"),
|
|
248
|
+
("agent_resume", {"pause_id": "p2"}, "agent_resume"),
|
|
249
|
+
])
|
|
250
|
+
def test_event_types(ns, mock_writer, method, kwargs, expected_type):
|
|
251
|
+
getattr(ns, method)(session_id="s1", agent_id="a1", **kwargs)
|
|
252
|
+
assert mock_writer.last()["type"] == expected_type
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
# Schema to_dict directly — field values
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
def test_tool_use_schema():
|
|
260
|
+
e = ToolUseEvent(
|
|
261
|
+
timestamp="2026-03-31T14:22:01.123456Z",
|
|
262
|
+
session_id="run-abc",
|
|
263
|
+
agent_id="planner",
|
|
264
|
+
tool_name="web_search",
|
|
265
|
+
tool_call_id="toolu_01",
|
|
266
|
+
input={"query": "test"},
|
|
267
|
+
)
|
|
268
|
+
d = e.to_dict()
|
|
269
|
+
assert d["type"] == "tool_use"
|
|
270
|
+
assert d["tool_name"] == "web_search"
|
|
271
|
+
assert d["input"] == {"query": "test"}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_tool_result_with_duration_schema():
|
|
275
|
+
e = ToolResultEvent(
|
|
276
|
+
timestamp="2026-03-31T14:22:02.456789Z",
|
|
277
|
+
session_id="run-abc",
|
|
278
|
+
agent_id="planner",
|
|
279
|
+
tool_name="web_search",
|
|
280
|
+
tool_call_id="toolu_01",
|
|
281
|
+
output={"results": ["x"]},
|
|
282
|
+
duration_ms=1333.6,
|
|
283
|
+
)
|
|
284
|
+
d = e.to_dict()
|
|
285
|
+
assert d["duration_ms"] == 1333.6
|
|
286
|
+
assert d["output"] == {"results": ["x"]}
|
|
287
|
+
assert "error" not in d
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_error_schema():
|
|
291
|
+
e = ErrorEvent(
|
|
292
|
+
timestamp="2026-03-31T14:22:08.500000Z",
|
|
293
|
+
session_id="run-abc",
|
|
294
|
+
agent_id="planner",
|
|
295
|
+
error_type="TimeoutError",
|
|
296
|
+
message="timed out",
|
|
297
|
+
traceback="Traceback...",
|
|
298
|
+
)
|
|
299
|
+
d = e.to_dict()
|
|
300
|
+
assert d["error_type"] == "TimeoutError"
|
|
301
|
+
assert d["traceback"] == "Traceback..."
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Human-in-the-loop event tests
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def test_human_input_rejects_duration_ms(ns):
|
|
309
|
+
with pytest.raises(ValueError, match="duration_ms"):
|
|
310
|
+
ns.human_input(session_id="s1", agent_id="a1", input_id="inp1", duration_ms=99)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_human_wait_input_duration(ns, mock_writer):
|
|
314
|
+
ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
|
|
315
|
+
time.sleep(0.01)
|
|
316
|
+
ns.human_input(session_id="s1", agent_id="a1", input_id="inp1", response="approved")
|
|
317
|
+
d = mock_writer.last()
|
|
318
|
+
assert "duration_ms" in d
|
|
319
|
+
assert d["duration_ms"] >= 10.0
|
|
320
|
+
assert d["response"] == "approved"
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_human_input_without_prior_wait(ns, mock_writer):
|
|
324
|
+
ns.human_input(session_id="s1", agent_id="a1", input_id="no-prior")
|
|
325
|
+
d = mock_writer.last()
|
|
326
|
+
assert "duration_ms" not in d
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
# Agent pause / resume event tests
|
|
331
|
+
# ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
def test_agent_resume_rejects_duration_ms(ns):
|
|
334
|
+
with pytest.raises(ValueError, match="duration_ms"):
|
|
335
|
+
ns.agent_resume(session_id="s1", agent_id="a1", pause_id="p1", duration_ms=99)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_agent_pause_resume_duration(ns, mock_writer):
|
|
339
|
+
ns.agent_pause(session_id="s1", agent_id="a1", pause_id="p1", reason="waiting_for_user")
|
|
340
|
+
time.sleep(0.01)
|
|
341
|
+
ns.agent_resume(session_id="s1", agent_id="a1", pause_id="p1")
|
|
342
|
+
d = mock_writer.last()
|
|
343
|
+
assert d["type"] == "agent_resume"
|
|
344
|
+
assert d["pause_id"] == "p1"
|
|
345
|
+
assert "duration_ms" in d
|
|
346
|
+
assert d["duration_ms"] >= 10.0
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def test_agent_resume_without_prior_pause(ns, mock_writer):
|
|
350
|
+
ns.agent_resume(session_id="s1", agent_id="a1", pause_id="no-prior")
|
|
351
|
+
d = mock_writer.last()
|
|
352
|
+
assert d["type"] == "agent_resume"
|
|
353
|
+
assert d["pause_id"] == "no-prior"
|
|
354
|
+
assert "duration_ms" not in d
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_agent_pause_fields(ns, mock_writer):
|
|
358
|
+
ns.agent_pause(session_id="s1", agent_id="a1", pause_id="p1", reason="user_requested", user_id="usr_9")
|
|
359
|
+
d = mock_writer.last()
|
|
360
|
+
assert d["type"] == "agent_pause"
|
|
361
|
+
assert d["pause_id"] == "p1"
|
|
362
|
+
assert d["reason"] == "user_requested"
|
|
363
|
+
assert d["user_id"] == "usr_9"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def test_agent_pause_null_fields_omitted(ns, mock_writer):
|
|
367
|
+
ns.agent_pause(session_id="s1", agent_id="a1", pause_id="p1")
|
|
368
|
+
d = mock_writer.last()
|
|
369
|
+
assert d["pause_id"] == "p1"
|
|
370
|
+
assert "reason" not in d
|
|
371
|
+
assert "user_id" not in d
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_agent_pause_resume_cross_namespace_link():
|
|
375
|
+
# Pause in one EventNamespace ("process"), resume in another: the in-memory
|
|
376
|
+
# _pending map does not carry over, so duration_ms is absent — but the
|
|
377
|
+
# shared pause_id still links the two events for downstream pairing.
|
|
378
|
+
w1, w2 = MockWriter(), MockWriter()
|
|
379
|
+
EventNamespace(w1).agent_pause(session_id="s1", agent_id="a1", pause_id="cross-1")
|
|
380
|
+
EventNamespace(w2).agent_resume(session_id="s1", agent_id="a1", pause_id="cross-1")
|
|
381
|
+
pause_d, resume_d = w1.last(), w2.last()
|
|
382
|
+
assert pause_d["type"] == "agent_pause" and pause_d["pause_id"] == "cross-1"
|
|
383
|
+
assert resume_d["type"] == "agent_resume" and resume_d["pause_id"] == "cross-1"
|
|
384
|
+
assert "duration_ms" not in resume_d # no in-process pending → no auto-duration
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def test_agent_pause_id_isolated_from_tool_call_id(ns, mock_writer):
|
|
388
|
+
# A pause_id equal to a tool_call_id must NOT cross-link in the shared
|
|
389
|
+
# _pending map (pause keys are namespaced), else resume would steal the
|
|
390
|
+
# tool's start time and emit a bogus duration.
|
|
391
|
+
ns.tool_use(session_id="s1", agent_id="a1", tool_name="t", tool_call_id="x1")
|
|
392
|
+
ns.agent_resume(session_id="s1", agent_id="a1", pause_id="x1")
|
|
393
|
+
d = mock_writer.last()
|
|
394
|
+
assert d["type"] == "agent_resume"
|
|
395
|
+
assert "duration_ms" not in d
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_agent_pause_resume_schema():
|
|
399
|
+
p = AgentPauseEvent(
|
|
400
|
+
timestamp="2026-07-15T10:00:00.000000Z", session_id="s", agent_id="a",
|
|
401
|
+
pause_id="p1", reason="waiting_for_user",
|
|
402
|
+
)
|
|
403
|
+
pd = p.to_dict()
|
|
404
|
+
assert pd["type"] == "agent_pause"
|
|
405
|
+
assert pd["pause_id"] == "p1"
|
|
406
|
+
assert pd["reason"] == "waiting_for_user"
|
|
407
|
+
assert "user_id" not in pd # None-dropped
|
|
408
|
+
|
|
409
|
+
r = AgentResumeEvent(
|
|
410
|
+
timestamp="2026-07-15T10:05:00.000000Z", session_id="s", agent_id="a",
|
|
411
|
+
pause_id="p1", duration_ms=300000.0,
|
|
412
|
+
)
|
|
413
|
+
rd = r.to_dict()
|
|
414
|
+
assert rd["type"] == "agent_resume"
|
|
415
|
+
assert rd["pause_id"] == "p1"
|
|
416
|
+
assert rd["duration_ms"] == 300000.0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_human_wait_options_in_output(ns, mock_writer):
|
|
420
|
+
ns.human_wait(
|
|
421
|
+
session_id="s1", agent_id="a1", input_id="inp1",
|
|
422
|
+
prompt="Choose an action", options=["approve", "reject", "defer"],
|
|
423
|
+
)
|
|
424
|
+
d = mock_writer.last()
|
|
425
|
+
assert d["options"] == ["approve", "reject", "defer"]
|
|
426
|
+
assert d["prompt"] == "Choose an action"
|
|
427
|
+
assert d["input_id"] == "inp1"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_human_wait_no_options_omitted(ns, mock_writer):
|
|
431
|
+
ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
|
|
432
|
+
d = mock_writer.last()
|
|
433
|
+
assert "options" not in d
|
|
434
|
+
assert "prompt" not in d
|
|
435
|
+
assert "reason" not in d
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_null_fields_omitted_human_pause(ns, mock_writer):
|
|
439
|
+
ns.human_pause(session_id="s1", agent_id="a1")
|
|
440
|
+
d = mock_writer.last()
|
|
441
|
+
assert "reason" not in d
|
|
442
|
+
assert "user_id" not in d
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def test_null_fields_omitted_human_interrupt(ns, mock_writer):
|
|
446
|
+
ns.human_interrupt(session_id="s1", agent_id="a1")
|
|
447
|
+
d = mock_writer.last()
|
|
448
|
+
assert "reason" not in d
|
|
449
|
+
assert "user_id" not in d
|
|
450
|
+
assert "at_step" not in d
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def test_human_pause_fields(ns, mock_writer):
|
|
454
|
+
ns.human_pause(session_id="s1", agent_id="a1", reason="user_requested", user_id="usr_42")
|
|
455
|
+
d = mock_writer.last()
|
|
456
|
+
assert d["type"] == "human_pause"
|
|
457
|
+
assert d["reason"] == "user_requested"
|
|
458
|
+
assert d["user_id"] == "usr_42"
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def test_human_interrupt_fields(ns, mock_writer):
|
|
462
|
+
ns.human_interrupt(
|
|
463
|
+
session_id="s1", agent_id="a1",
|
|
464
|
+
reason="output_incorrect", user_id="usr_42", at_step="tool_use:web_search",
|
|
465
|
+
)
|
|
466
|
+
d = mock_writer.last()
|
|
467
|
+
assert d["type"] == "human_interrupt"
|
|
468
|
+
assert d["reason"] == "output_incorrect"
|
|
469
|
+
assert d["at_step"] == "tool_use:web_search"
|
|
470
|
+
assert d["user_id"] == "usr_42"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_human_wait_schema():
|
|
474
|
+
e = HumanWaitEvent(
|
|
475
|
+
timestamp="2026-05-11T10:00:00.000000Z",
|
|
476
|
+
session_id="run-abc",
|
|
477
|
+
agent_id="planner",
|
|
478
|
+
input_id="inp-01",
|
|
479
|
+
prompt="Do you approve?",
|
|
480
|
+
options=["approve", "reject"],
|
|
481
|
+
)
|
|
482
|
+
d = e.to_dict()
|
|
483
|
+
assert d["type"] == "human_wait"
|
|
484
|
+
assert d["input_id"] == "inp-01"
|
|
485
|
+
assert d["prompt"] == "Do you approve?"
|
|
486
|
+
assert d["options"] == ["approve", "reject"]
|
|
487
|
+
assert "reason" not in d
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def test_human_input_schema():
|
|
491
|
+
e = HumanInputEvent(
|
|
492
|
+
timestamp="2026-05-11T10:00:05.000000Z",
|
|
493
|
+
session_id="run-abc",
|
|
494
|
+
agent_id="planner",
|
|
495
|
+
input_id="inp-01",
|
|
496
|
+
response="approve",
|
|
497
|
+
duration_ms=5000.0,
|
|
498
|
+
)
|
|
499
|
+
d = e.to_dict()
|
|
500
|
+
assert d["type"] == "human_input"
|
|
501
|
+
assert d["input_id"] == "inp-01"
|
|
502
|
+
assert d["response"] == "approve"
|
|
503
|
+
assert d["duration_ms"] == 5000.0
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def test_human_wait_input_id_in_base_dict(ns, mock_writer):
|
|
507
|
+
# input_id must appear before environment in field ordering
|
|
508
|
+
ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
|
|
509
|
+
keys = list(mock_writer.last().keys())
|
|
510
|
+
assert keys.index("input_id") < keys.index("environment")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def test_human_input_input_id_in_base_dict(ns, mock_writer):
|
|
514
|
+
ns.human_input(session_id="s1", agent_id="a1", input_id="inp1")
|
|
515
|
+
keys = list(mock_writer.last().keys())
|
|
516
|
+
assert keys.index("input_id") < keys.index("environment")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def test_human_wait_custom_fields(ns, mock_writer):
|
|
520
|
+
ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1", tenant="acme")
|
|
521
|
+
d = mock_writer.last()
|
|
522
|
+
assert d["tenant"] == "acme"
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def test_human_interrupt_pending_cleared_after_human_input(ns, mock_writer):
|
|
526
|
+
# Emit wait then input twice with same input_id — second input should have no duration
|
|
527
|
+
ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
|
|
528
|
+
ns.human_input(session_id="s1", agent_id="a1", input_id="inp1")
|
|
529
|
+
ns.human_input(session_id="s1", agent_id="a1", input_id="inp1")
|
|
530
|
+
d = mock_writer.last()
|
|
531
|
+
assert "duration_ms" not in d
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def test_environment_in_human_events(ns, mock_writer):
|
|
535
|
+
import failproofai_sdk._environment as env_mod
|
|
536
|
+
env_mod.set_environment("production")
|
|
537
|
+
ns.human_wait(session_id="s1", agent_id="a1", input_id="inp1")
|
|
538
|
+
ns.human_input(session_id="s1", agent_id="a1", input_id="inp1")
|
|
539
|
+
ns.human_pause(session_id="s1", agent_id="a1")
|
|
540
|
+
ns.human_interrupt(session_id="s1", agent_id="a1")
|
|
541
|
+
for entry in mock_writer.entries[-4:]:
|
|
542
|
+
assert entry["environment"] == "production"
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def test_pending_dict_is_bounded(ns):
|
|
546
|
+
"""Orphaned tool_use calls (no matching tool_result) must not grow
|
|
547
|
+
`_pending` without bound — at the cap, the oldest entry is evicted FIFO."""
|
|
548
|
+
from failproofai_sdk._events import _PENDING_CAP, _tool_key
|
|
549
|
+
|
|
550
|
+
for i in range(_PENDING_CAP + 50):
|
|
551
|
+
ns.tool_use(
|
|
552
|
+
session_id="s1",
|
|
553
|
+
agent_id="a1",
|
|
554
|
+
tool_name="t",
|
|
555
|
+
tool_call_id=f"call-{i}",
|
|
556
|
+
)
|
|
557
|
+
assert len(ns._pending) == _PENDING_CAP
|
|
558
|
+
# Earliest IDs were evicted.
|
|
559
|
+
assert _tool_key("s1", "call-0") not in ns._pending
|
|
560
|
+
assert _tool_key("s1", "call-49") not in ns._pending
|
|
561
|
+
# Most-recent IDs are still tracked.
|
|
562
|
+
assert _tool_key("s1", f"call-{_PENDING_CAP + 49}") in ns._pending
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# ---------------------------------------------------------------------------
|
|
566
|
+
# EventWriter — file writing integration tests
|
|
567
|
+
# ---------------------------------------------------------------------------
|
|
568
|
+
|
|
569
|
+
def test_writer_creates_jsonl_file(tmp_path):
|
|
570
|
+
import failproofai_sdk._resolver as resolver
|
|
571
|
+
original = resolver._base_dir
|
|
572
|
+
try:
|
|
573
|
+
resolver.set_base_dir(tmp_path)
|
|
574
|
+
writer = EventWriter(flush_interval=60) # won't auto-flush during test
|
|
575
|
+
writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "agent_start"})
|
|
576
|
+
writer.flush_now()
|
|
577
|
+
|
|
578
|
+
events_dir = tmp_path / "events"
|
|
579
|
+
jsonl_files = list(events_dir.glob("*.jsonl"))
|
|
580
|
+
assert len(jsonl_files) == 1
|
|
581
|
+
|
|
582
|
+
lines = jsonl_files[0].read_text().strip().splitlines()
|
|
583
|
+
assert len(lines) == 1
|
|
584
|
+
parsed = json.loads(lines[0])
|
|
585
|
+
assert parsed["type"] == "agent_start"
|
|
586
|
+
finally:
|
|
587
|
+
resolver.set_base_dir(original)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def test_writer_no_tmp_files_after_flush(tmp_path):
|
|
591
|
+
import failproofai_sdk._resolver as resolver
|
|
592
|
+
original = resolver._base_dir
|
|
593
|
+
try:
|
|
594
|
+
resolver.set_base_dir(tmp_path)
|
|
595
|
+
writer = EventWriter(flush_interval=60)
|
|
596
|
+
writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "tool_use"})
|
|
597
|
+
writer.flush_now()
|
|
598
|
+
|
|
599
|
+
events_dir = tmp_path / "events"
|
|
600
|
+
tmp_files = list(events_dir.glob("*.tmp"))
|
|
601
|
+
assert tmp_files == []
|
|
602
|
+
finally:
|
|
603
|
+
resolver.set_base_dir(original)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def test_writer_multiple_events_in_one_file(tmp_path):
|
|
607
|
+
import failproofai_sdk._resolver as resolver
|
|
608
|
+
original = resolver._base_dir
|
|
609
|
+
try:
|
|
610
|
+
resolver.set_base_dir(tmp_path)
|
|
611
|
+
writer = EventWriter(flush_interval=60)
|
|
612
|
+
for i in range(5):
|
|
613
|
+
writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "model_request", "i": i})
|
|
614
|
+
writer.flush_now()
|
|
615
|
+
|
|
616
|
+
events_dir = tmp_path / "events"
|
|
617
|
+
jsonl_files = list(events_dir.glob("*.jsonl"))
|
|
618
|
+
assert len(jsonl_files) == 1
|
|
619
|
+
lines = jsonl_files[0].read_text().strip().splitlines()
|
|
620
|
+
assert len(lines) == 5
|
|
621
|
+
finally:
|
|
622
|
+
resolver.set_base_dir(original)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def test_writer_coerces_unserializable_payload_values(tmp_path):
|
|
626
|
+
import failproofai_sdk._resolver as resolver
|
|
627
|
+
original = resolver._base_dir
|
|
628
|
+
try:
|
|
629
|
+
resolver.set_base_dir(tmp_path)
|
|
630
|
+
writer = EventWriter(flush_interval=60)
|
|
631
|
+
value = datetime(2026, 7, 17, 12, 34, 56, tzinfo=timezone.utc)
|
|
632
|
+
writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "tool_result", "output": value})
|
|
633
|
+
writer.flush_now()
|
|
634
|
+
|
|
635
|
+
path = next((tmp_path / "events").glob("*.jsonl"))
|
|
636
|
+
assert json.loads(path.read_text())["output"] == str(value)
|
|
637
|
+
assert writer._thread.is_alive()
|
|
638
|
+
finally:
|
|
639
|
+
resolver.set_base_dir(original)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def test_writer_requeues_batch_after_write_failure(monkeypatch):
|
|
643
|
+
writer = EventWriter(flush_interval=60)
|
|
644
|
+
entries = [{"i": 1}, {"i": 2}]
|
|
645
|
+
for entry in entries:
|
|
646
|
+
writer.submit(entry)
|
|
647
|
+
|
|
648
|
+
monkeypatch.setattr(writer, "_write_batch", lambda _entries: (_ for _ in ()).throw(OSError("disk full")))
|
|
649
|
+
with pytest.raises(OSError, match="disk full"):
|
|
650
|
+
writer.flush_now()
|
|
651
|
+
assert list(writer._queue) == entries
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def test_writer_no_flush_when_empty(tmp_path):
|
|
655
|
+
import failproofai_sdk._resolver as resolver
|
|
656
|
+
original = resolver._base_dir
|
|
657
|
+
try:
|
|
658
|
+
resolver.set_base_dir(tmp_path)
|
|
659
|
+
writer = EventWriter(flush_interval=60)
|
|
660
|
+
writer.flush_now() # nothing in queue
|
|
661
|
+
|
|
662
|
+
events_dir = tmp_path / "events"
|
|
663
|
+
assert not events_dir.exists() or list(events_dir.glob("*")) == []
|
|
664
|
+
finally:
|
|
665
|
+
resolver.set_base_dir(original)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
# ---------------------------------------------------------------------------
|
|
669
|
+
# configure() integration
|
|
670
|
+
# ---------------------------------------------------------------------------
|
|
671
|
+
|
|
672
|
+
def test_configure_custom_base_dir(tmp_path):
|
|
673
|
+
failproofai_sdk.configure(base_dir=tmp_path, flush_interval=60)
|
|
674
|
+
failproofai_sdk.event.agent_start(session_id="s1", agent_id="a1", goal="test configure")
|
|
675
|
+
failproofai_sdk._writer.flush_now()
|
|
676
|
+
|
|
677
|
+
events_dir = tmp_path / "events"
|
|
678
|
+
jsonl_files = list(events_dir.glob("*.jsonl"))
|
|
679
|
+
assert len(jsonl_files) == 1
|
|
680
|
+
parsed = json.loads(jsonl_files[0].read_text().strip().splitlines()[0])
|
|
681
|
+
assert parsed["goal"] == "test configure"
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
# ---------------------------------------------------------------------------
|
|
685
|
+
# atexit flush — events must survive process exit without explicit flush_now()
|
|
686
|
+
# ---------------------------------------------------------------------------
|
|
687
|
+
|
|
688
|
+
def test_atexit_flushes_on_process_exit(tmp_path):
|
|
689
|
+
import subprocess
|
|
690
|
+
import sys
|
|
691
|
+
script = f"""
|
|
692
|
+
import failproofai_sdk
|
|
693
|
+
import failproofai_sdk._resolver as resolver
|
|
694
|
+
from pathlib import Path
|
|
695
|
+
resolver.set_base_dir(Path(r'{tmp_path}'))
|
|
696
|
+
failproofai_sdk.event.agent_start(session_id='s1', agent_id='a1')
|
|
697
|
+
# intentionally no flush_now() — atexit must handle it
|
|
698
|
+
"""
|
|
699
|
+
result = subprocess.run([sys.executable, "-c", script], capture_output=True)
|
|
700
|
+
assert result.returncode == 0, result.stderr.decode()
|
|
701
|
+
events_dir = tmp_path / "events"
|
|
702
|
+
jsonl_files = list(events_dir.glob("*.jsonl"))
|
|
703
|
+
assert len(jsonl_files) == 1
|
|
704
|
+
parsed = json.loads(jsonl_files[0].read_text().strip().splitlines()[0])
|
|
705
|
+
assert parsed["type"] == "agent_start"
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
# ---------------------------------------------------------------------------
|
|
709
|
+
# Timestamp format
|
|
710
|
+
# ---------------------------------------------------------------------------
|
|
711
|
+
|
|
712
|
+
def test_timestamp_format(ns, mock_writer):
|
|
713
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
714
|
+
d = mock_writer.last()
|
|
715
|
+
ts = d["timestamp"]
|
|
716
|
+
# ISO 8601 with microsecond precision, UTC: 2026-04-01T12:34:56.789012Z
|
|
717
|
+
assert ts.endswith("Z")
|
|
718
|
+
assert "T" in ts
|
|
719
|
+
# Must be parseable
|
|
720
|
+
from datetime import datetime, timezone
|
|
721
|
+
dt = datetime.fromisoformat(ts.rstrip("Z") + "+00:00")
|
|
722
|
+
assert dt.tzinfo is not None
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
# ---------------------------------------------------------------------------
|
|
726
|
+
# Base dir resolution — set_base_dir() > ~/.failproofai/custom-agents
|
|
727
|
+
# ---------------------------------------------------------------------------
|
|
728
|
+
|
|
729
|
+
def test_base_dir_from_env_var_is_ignored(monkeypatch, tmp_path):
|
|
730
|
+
"""`AGENTEYE_HOME` no longer resolves the spool; see `_resolver`."""
|
|
731
|
+
import failproofai_sdk._resolver as resolver
|
|
732
|
+
from pathlib import Path
|
|
733
|
+
monkeypatch.setattr(resolver, "_base_dir", None)
|
|
734
|
+
monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
|
|
735
|
+
monkeypatch.delenv("FAILPROOFAI_HOME", raising=False)
|
|
736
|
+
monkeypatch.setenv("AGENTEYE_HOME", str(tmp_path / "ignored"))
|
|
737
|
+
assert resolver.get_base_dir() == tmp_path / ".failproofai" / "custom-agents"
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def test_base_dir_from_failproofai_home(monkeypatch, tmp_path):
|
|
741
|
+
"""The one variable that moves it — and it stays inside the umbrella."""
|
|
742
|
+
import failproofai_sdk._resolver as resolver
|
|
743
|
+
monkeypatch.setattr(resolver, "_base_dir", None)
|
|
744
|
+
monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path))
|
|
745
|
+
assert resolver.get_base_dir() == tmp_path / "custom-agents"
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def test_set_base_dir_overrides_env_var(monkeypatch, tmp_path):
|
|
749
|
+
import failproofai_sdk._resolver as resolver
|
|
750
|
+
other = tmp_path / "explicit"
|
|
751
|
+
monkeypatch.setattr(resolver, "_base_dir", None)
|
|
752
|
+
monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path / "from-env"))
|
|
753
|
+
resolver.set_base_dir(other)
|
|
754
|
+
try:
|
|
755
|
+
assert resolver.get_base_dir() == other
|
|
756
|
+
finally:
|
|
757
|
+
resolver.set_base_dir(None)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def test_default_when_neither_set(monkeypatch):
|
|
761
|
+
import failproofai_sdk._resolver as resolver
|
|
762
|
+
monkeypatch.setattr(resolver, "_base_dir", None)
|
|
763
|
+
monkeypatch.delenv("AGENTEYE_HOME", raising=False)
|
|
764
|
+
monkeypatch.delenv("FAILPROOFAI_HOME", raising=False)
|
|
765
|
+
assert resolver.get_base_dir() == Path.home() / ".failproofai" / "custom-agents"
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def test_writer_uses_failproofai_home_base_dir(monkeypatch, tmp_path):
|
|
769
|
+
"""End-to-end: the writer spools under $FAILPROOFAI_HOME/custom-agents."""
|
|
770
|
+
import failproofai_sdk._resolver as resolver
|
|
771
|
+
monkeypatch.setattr(resolver, "_base_dir", None)
|
|
772
|
+
monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path))
|
|
773
|
+
writer = EventWriter(flush_interval=60)
|
|
774
|
+
writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "agent_start"})
|
|
775
|
+
writer.flush_now()
|
|
776
|
+
|
|
777
|
+
jsonl_files = list((tmp_path / "custom-agents" / "events").glob("*.jsonl"))
|
|
778
|
+
assert len(jsonl_files) == 1
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def test_writer_ignores_agenteye_home(monkeypatch, tmp_path):
|
|
782
|
+
"""End-to-end counterpart: the retired variable moves nothing on disk.
|
|
783
|
+
|
|
784
|
+
Asserting the batch's real location, not just what `get_base_dir()` returns
|
|
785
|
+
— the resolver could be right while the writer still resolved separately.
|
|
786
|
+
"""
|
|
787
|
+
import failproofai_sdk._resolver as resolver
|
|
788
|
+
monkeypatch.setattr(resolver, "_base_dir", None)
|
|
789
|
+
monkeypatch.setenv("FAILPROOFAI_HOME", str(tmp_path / "umbrella"))
|
|
790
|
+
monkeypatch.setenv("AGENTEYE_HOME", str(tmp_path / "legacy"))
|
|
791
|
+
writer = EventWriter(flush_interval=60)
|
|
792
|
+
writer.submit({"timestamp": "t", "session_id": "s1", "agent_id": "a1", "type": "agent_start"})
|
|
793
|
+
writer.flush_now()
|
|
794
|
+
|
|
795
|
+
assert not (tmp_path / "legacy").exists(), "the retired variable still steered the writer"
|
|
796
|
+
assert len(list((tmp_path / "umbrella" / "custom-agents" / "events").glob("*.jsonl"))) == 1
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
# ---------------------------------------------------------------------------
|
|
800
|
+
# Environment
|
|
801
|
+
# ---------------------------------------------------------------------------
|
|
802
|
+
|
|
803
|
+
@pytest.fixture(autouse=True)
|
|
804
|
+
def _reset_environment():
|
|
805
|
+
"""Reset the global environment state between every test."""
|
|
806
|
+
import failproofai_sdk._environment as env_mod
|
|
807
|
+
original = env_mod._environment
|
|
808
|
+
yield
|
|
809
|
+
env_mod._environment = original
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def test_environment_defaults_to_dev(ns, mock_writer):
|
|
813
|
+
import failproofai_sdk._environment as env_mod
|
|
814
|
+
env_mod._environment = None
|
|
815
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
816
|
+
assert mock_writer.last()["environment"] == "dev"
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def test_environment_from_configure(tmp_path, mock_writer):
|
|
820
|
+
import failproofai_sdk._environment as env_mod
|
|
821
|
+
env_mod.set_environment("staging")
|
|
822
|
+
ns = EventNamespace(mock_writer)
|
|
823
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
824
|
+
assert mock_writer.last()["environment"] == "staging"
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def test_environment_from_env_var(ns, mock_writer, monkeypatch):
|
|
828
|
+
import failproofai_sdk._environment as env_mod
|
|
829
|
+
env_mod._environment = None
|
|
830
|
+
monkeypatch.setenv("AGENTEYE_ENVIRONMENT", "production")
|
|
831
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
832
|
+
assert mock_writer.last()["environment"] == "production"
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def test_configure_overrides_env_var(ns, mock_writer, monkeypatch):
|
|
836
|
+
import failproofai_sdk._environment as env_mod
|
|
837
|
+
monkeypatch.setenv("AGENTEYE_ENVIRONMENT", "production")
|
|
838
|
+
env_mod.set_environment("staging")
|
|
839
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
840
|
+
assert mock_writer.last()["environment"] == "staging"
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def test_a_comma_in_configure_environment_is_refused_rather_than_dropped(
|
|
844
|
+
ns, mock_writer
|
|
845
|
+
):
|
|
846
|
+
# Ingest splits `environment` on commas to build its facets, so a line whose
|
|
847
|
+
# environment contains one is discarded WHOLE: 200 with
|
|
848
|
+
# `{"accepted":0,"skipped":N}`, the daemon deletes the delivered batch, and
|
|
849
|
+
# the run is simply never in the dashboard. Measured against the running
|
|
850
|
+
# stack before this check existed. `failproofaid` already refuses a comma in
|
|
851
|
+
# `collector.environment`; the SDK writes the same field and did not.
|
|
852
|
+
import failproofai_sdk._environment as env_mod
|
|
853
|
+
|
|
854
|
+
with pytest.raises(ValueError, match="comma"):
|
|
855
|
+
env_mod.set_environment("prod,eu")
|
|
856
|
+
# And the rejected call must not have taken effect.
|
|
857
|
+
env_mod._environment = None
|
|
858
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
859
|
+
assert "," not in mock_writer.last()["environment"]
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def test_a_comma_in_the_env_var_warns_and_falls_back_instead_of_raising(
|
|
863
|
+
ns, mock_writer, monkeypatch, caplog
|
|
864
|
+
):
|
|
865
|
+
# An env var is read lazily, inside `to_dict()` on whatever event happens to
|
|
866
|
+
# be next — raising there would take the caller's agent down from a line of
|
|
867
|
+
# telemetry, which a library in someone else's process must not do. Warn,
|
|
868
|
+
# and land the events under a visibly wrong environment rather than nowhere.
|
|
869
|
+
import logging
|
|
870
|
+
|
|
871
|
+
import failproofai_sdk._environment as env_mod
|
|
872
|
+
|
|
873
|
+
env_mod._environment = None
|
|
874
|
+
monkeypatch.setenv("AGENTEYE_ENVIRONMENT", "prod,eu")
|
|
875
|
+
with caplog.at_level(logging.WARNING, logger="failproofai_sdk"):
|
|
876
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
877
|
+
assert mock_writer.last()["environment"] == "dev"
|
|
878
|
+
assert "comma" in caplog.text
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def test_environment_is_reserved(ns):
|
|
882
|
+
with pytest.raises(ValueError, match="Reserved field"):
|
|
883
|
+
ns.agent_start(session_id="s1", agent_id="a1", environment="bad")
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def test_environment_field_ordering(ns, mock_writer):
|
|
887
|
+
import failproofai_sdk._environment as env_mod
|
|
888
|
+
env_mod.set_environment("qa")
|
|
889
|
+
ns.agent_start(session_id="s1", agent_id="a1", goal="test")
|
|
890
|
+
keys = list(mock_writer.last().keys())
|
|
891
|
+
assert keys.index("environment") > keys.index("type")
|
|
892
|
+
assert keys.index("environment") < keys.index("goal")
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def test_environment_in_all_event_types(ns, mock_writer):
|
|
896
|
+
import failproofai_sdk._environment as env_mod
|
|
897
|
+
env_mod.set_environment("canary")
|
|
898
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
899
|
+
ns.tool_use(session_id="s1", agent_id="a1", tool_name="t", tool_call_id="tc1")
|
|
900
|
+
ns.error(session_id="s1", agent_id="a1", error_type="E", message="m")
|
|
901
|
+
for entry in mock_writer.entries:
|
|
902
|
+
assert entry["environment"] == "canary"
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def test_environment_integration(tmp_path):
|
|
906
|
+
import failproofai_sdk._resolver as resolver
|
|
907
|
+
import failproofai_sdk._environment as env_mod
|
|
908
|
+
original_dir = resolver._base_dir
|
|
909
|
+
try:
|
|
910
|
+
resolver.set_base_dir(tmp_path)
|
|
911
|
+
env_mod.set_environment("integration-test")
|
|
912
|
+
writer = EventWriter(flush_interval=60)
|
|
913
|
+
ns = EventNamespace(writer)
|
|
914
|
+
ns.agent_start(session_id="s1", agent_id="a1")
|
|
915
|
+
writer.flush_now()
|
|
916
|
+
|
|
917
|
+
events_dir = tmp_path / "events"
|
|
918
|
+
jsonl_files = list(events_dir.glob("*.jsonl"))
|
|
919
|
+
assert len(jsonl_files) == 1
|
|
920
|
+
parsed = json.loads(jsonl_files[0].read_text().strip().splitlines()[0])
|
|
921
|
+
assert parsed["environment"] == "integration-test"
|
|
922
|
+
finally:
|
|
923
|
+
resolver.set_base_dir(original_dir)
|