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.
Files changed (389) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +6 -6
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +6 -6
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  15. package/.next/standalone/.next/server/app/_not-found/page.js +2 -2
  16. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  20. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
  22. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/standalone/.next/server/app/api/audit/invite/route.js +3 -2
  24. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/audit/run/route.js +5 -4
  26. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/audit/status/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +4 -3
  31. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/logout/route.js +4 -4
  33. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/status/route.js +4 -4
  35. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  38. package/.next/standalone/.next/server/app/audit/page.js +2 -2
  39. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  41. package/.next/standalone/.next/server/app/index.html +1 -1
  42. package/.next/standalone/.next/server/app/index.rsc +15 -15
  43. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +14 -14
  44. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  45. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  46. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  47. package/.next/standalone/.next/server/app/page.js +2 -2
  48. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +66 -22
  51. package/.next/standalone/.next/server/app/policies/page.js +5 -4
  52. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  53. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  54. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  55. package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
  56. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  58. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  59. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  60. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +2 -2
  61. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  65. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/settings/page/server-reference-manifest.json +4 -4
  68. package/.next/standalone/.next/server/app/settings/page.js +1 -1
  69. package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  71. package/.next/standalone/.next/server/chunks/{[externals]__1qzqcio._.js → [externals]__0e32-7c._.js} +1 -1
  72. package/.next/standalone/.next/server/chunks/[externals]__1s462yl._.js +3 -0
  73. package/.next/standalone/.next/server/chunks/[root-of-the-server]__01eqny5._.js +3 -0
  74. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  75. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__1p8poxd._.js → [root-of-the-server]__0igxt32._.js} +2 -2
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0neqlu-._.js +3 -0
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0o07qi9._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0t9oqnn._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/_0bs-v8p._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/_0tovk6q._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/_0trp3yc._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/_185gyk9._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/_1ek68ln._.js +5 -6
  84. package/.next/standalone/.next/server/chunks/lib_telemetry-id_ts_1k8gj9o._.js +3 -0
  85. 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
  86. 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
  87. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  88. package/.next/standalone/.next/server/chunks/src_hooks_0xbw3bu._.js +12 -0
  89. package/.next/standalone/.next/server/chunks/src_hooks_fp-home_ts_09kv0bn._.js +1 -1
  90. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__013jr2b._.js +4 -0
  91. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01wy8d-._.js +4 -0
  92. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__02npjtd._.js +4 -0
  93. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0spkm68._.js → [root-of-the-server]__07gm3zl._.js} +21 -20
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0da85px._.js +4 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ftmoxc._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0oa1lav._.js +1 -1
  97. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0p-5p8u._.js +4 -0
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s740oi._.js +6 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__07tbbf3._.js → [root-of-the-server]__0zig5sv._.js} +2 -2
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__192nhvh._.js +5 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1p2otjt._.js +4 -0
  102. package/.next/standalone/.next/server/chunks/ssr/{_0lvyuv4._.js → _00e9tnf._.js} +2 -2
  103. package/.next/standalone/.next/server/chunks/ssr/_08x1r5t._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/ssr/{_0u5zauv._.js → _12l4a_k._.js} +1 -1
  105. package/.next/standalone/.next/server/chunks/ssr/_1u8-lu2._.js +1 -1
  106. package/.next/standalone/.next/server/chunks/ssr/_1zopuov._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +13 -13
  108. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  109. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +2 -2
  110. package/.next/standalone/.next/server/chunks/ssr/app_settings_settings-client_tsx_20lq-mq._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/src_hooks_08o7bqo._.js +10 -0
  112. package/.next/standalone/.next/server/chunks/ssr/src_hooks_builtin-policies_ts_09j2ndl._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/src_hooks_fp-home_ts_0je3xkv._.js +1 -1
  114. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  115. package/.next/standalone/.next/server/middleware.js +1 -1
  116. package/.next/standalone/.next/server/pages/404.html +1 -1
  117. package/.next/standalone/.next/server/pages/500.html +1 -1
  118. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  119. package/.next/standalone/.next/server/server-reference-manifest.json +71 -27
  120. package/.next/standalone/.next/static/chunks/{37fcpc36x9gpe.js → 0eqky-7n7dgdm.js} +1 -1
  121. package/.next/standalone/.next/static/chunks/{1ide1wl2f7zi1.js → 0sa9v5mhc8861.js} +13 -13
  122. package/.next/standalone/.next/static/chunks/1mfzsja6ftim5.css +2 -0
  123. package/.next/standalone/.next/static/chunks/{3k7mxbp9hiom0.js → 1nuqytbta44af.js} +1 -1
  124. package/.next/standalone/.next/static/chunks/1sl2xmn9inx3y.js +6 -0
  125. package/.next/standalone/.next/static/chunks/{2_2lhuojq2xop.js → 2aawsjld9ied_.js} +1 -1
  126. package/.next/standalone/.next/static/chunks/2gn7ut4svofm2.js +1 -0
  127. package/.next/standalone/.next/static/chunks/{31s1smp-g7gks.js → 2hs7ycvsav7v2.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/{0k5050ezjvv49.js → 2i2yt3hzjredm.js} +1 -1
  129. package/.next/standalone/.next/static/chunks/{3c808qrfcq0im.js → 35xg6retpe6rp.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/{2zpp1we2jv6tb.js → 3y54nw0i7dsjl.js} +1 -1
  131. package/.next/standalone/SECURITY.md +5 -4
  132. package/.next/standalone/app/actions/get-hooks-config.ts +108 -17
  133. package/.next/standalone/app/actions/pack-actions.ts +153 -0
  134. package/.next/standalone/app/audit/_components/run-progress.tsx +1 -1
  135. package/.next/standalone/app/audit/_components/share-templates.ts +13 -13
  136. package/.next/standalone/app/policies/hooks-client.tsx +320 -14
  137. package/.next/standalone/components/navbar.tsx +2 -28
  138. package/.next/standalone/components/reach-developers.tsx +8 -1
  139. package/.next/standalone/docs-old/agenteye/python-sdk-skill.mdx +1 -1
  140. package/.next/standalone/docs-old/agenteye/python-sdk.mdx +59 -35
  141. package/.next/standalone/docs-old/i18n/README.ar.md +1 -0
  142. package/.next/standalone/docs-old/i18n/README.de.md +1 -0
  143. package/.next/standalone/docs-old/i18n/README.es.md +1 -0
  144. package/.next/standalone/docs-old/i18n/README.fr.md +1 -0
  145. package/.next/standalone/docs-old/i18n/README.he.md +1 -0
  146. package/.next/standalone/docs-old/i18n/README.hi.md +1 -0
  147. package/.next/standalone/docs-old/i18n/README.it.md +1 -0
  148. package/.next/standalone/docs-old/i18n/README.ja.md +1 -0
  149. package/.next/standalone/docs-old/i18n/README.ko.md +1 -0
  150. package/.next/standalone/docs-old/i18n/README.pt-br.md +1 -0
  151. package/.next/standalone/docs-old/i18n/README.ru.md +1 -0
  152. package/.next/standalone/docs-old/i18n/README.tr.md +1 -0
  153. package/.next/standalone/docs-old/i18n/README.vi.md +1 -0
  154. package/.next/standalone/docs-old/i18n/README.zh.md +1 -0
  155. package/.next/standalone/fp-cloud-cli/CHANGELOG.md +431 -0
  156. package/.next/standalone/fp-cloud-cli/LICENSE +42 -0
  157. package/.next/standalone/fp-cloud-cli/MANIFEST.in +10 -0
  158. package/.next/standalone/fp-cloud-cli/README.md +259 -0
  159. package/.next/standalone/fp-cloud-cli/fp_cli/__init__.py +10 -0
  160. package/.next/standalone/fp-cloud-cli/fp_cli/__main__.py +4 -0
  161. package/.next/standalone/fp-cloud-cli/fp_cli/_click_compat.py +64 -0
  162. package/.next/standalone/fp-cloud-cli/fp_cli/_context.py +332 -0
  163. package/.next/standalone/fp-cloud-cli/fp_cli/_version.py +1 -0
  164. package/.next/standalone/fp-cloud-cli/fp_cli/analytics.py +432 -0
  165. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_config.py +77 -0
  166. package/.next/standalone/fp-cloud-cli/fp_cli/analytics_registry.py +83 -0
  167. package/.next/standalone/fp-cloud-cli/fp_cli/app.py +492 -0
  168. package/.next/standalone/fp-cloud-cli/fp_cli/auth.py +160 -0
  169. package/.next/standalone/fp-cloud-cli/fp_cli/client.py +1694 -0
  170. package/.next/standalone/fp-cloud-cli/fp_cli/commands/__init__.py +0 -0
  171. package/.next/standalone/fp-cloud-cli/fp_cli/commands/_write.py +214 -0
  172. package/.next/standalone/fp-cloud-cli/fp_cli/commands/agent_cmds.py +407 -0
  173. package/.next/standalone/fp-cloud-cli/fp_cli/commands/alerts_cmds.py +445 -0
  174. package/.next/standalone/fp-cloud-cli/fp_cli/commands/audits_cmds.py +1054 -0
  175. package/.next/standalone/fp-cloud-cli/fp_cli/commands/auth_cmds.py +512 -0
  176. package/.next/standalone/fp-cloud-cli/fp_cli/commands/errors_cmds.py +190 -0
  177. package/.next/standalone/fp-cloud-cli/fp_cli/commands/evals_cmds.py +161 -0
  178. package/.next/standalone/fp-cloud-cli/fp_cli/commands/events_cmds.py +159 -0
  179. package/.next/standalone/fp-cloud-cli/fp_cli/commands/fleet_cmds.py +416 -0
  180. package/.next/standalone/fp-cloud-cli/fp_cli/commands/guardrails_cmds.py +148 -0
  181. package/.next/standalone/fp-cloud-cli/fp_cli/commands/incidents_cmds.py +472 -0
  182. package/.next/standalone/fp-cloud-cli/fp_cli/commands/keys_cmds.py +407 -0
  183. package/.next/standalone/fp-cloud-cli/fp_cli/commands/list_cmds.py +63 -0
  184. package/.next/standalone/fp-cloud-cli/fp_cli/commands/orgs_cmds.py +319 -0
  185. package/.next/standalone/fp-cloud-cli/fp_cli/commands/policies_cmds.py +499 -0
  186. package/.next/standalone/fp-cloud-cli/fp_cli/commands/queries_cmds.py +378 -0
  187. package/.next/standalone/fp-cloud-cli/fp_cli/commands/sessions_cmds.py +151 -0
  188. package/.next/standalone/fp-cloud-cli/fp_cli/commands/settings_cmds.py +150 -0
  189. package/.next/standalone/fp-cloud-cli/fp_cli/commands/usage_cmds.py +35 -0
  190. package/.next/standalone/fp-cloud-cli/fp_cli/commands/users_cmds.py +404 -0
  191. package/.next/standalone/fp-cloud-cli/fp_cli/config.py +330 -0
  192. package/.next/standalone/fp-cloud-cli/fp_cli/dates.py +78 -0
  193. package/.next/standalone/fp-cloud-cli/fp_cli/enforcement.py +345 -0
  194. package/.next/standalone/fp-cloud-cli/fp_cli/errors.py +98 -0
  195. package/.next/standalone/fp-cloud-cli/fp_cli/models.py +891 -0
  196. package/.next/standalone/fp-cloud-cli/fp_cli/orgs.py +30 -0
  197. package/.next/standalone/fp-cloud-cli/fp_cli/output.py +6593 -0
  198. package/.next/standalone/fp-cloud-cli/fp_cli/permissions.py +208 -0
  199. package/.next/standalone/fp-cloud-cli/fp_cli/policy_check.py +290 -0
  200. package/.next/standalone/fp-cloud-cli/fp_cli/py.typed +0 -0
  201. package/.next/standalone/fp-cloud-cli/fp_cli/select.py +322 -0
  202. package/.next/standalone/fp-cloud-cli/fp_cli/theme.py +53 -0
  203. package/.next/standalone/fp-cloud-cli/pyproject.toml +106 -0
  204. package/.next/standalone/fp-cloud-cli/skill/SKILL.md +302 -0
  205. package/.next/standalone/fp-cloud-cli/skill/agents/openai.yaml +8 -0
  206. package/.next/standalone/fp-cloud-cli/skill/references/commands.md +254 -0
  207. package/.next/standalone/fp-cloud-cli/tests/__init__.py +0 -0
  208. package/.next/standalone/fp-cloud-cli/tests/conftest.py +92 -0
  209. package/.next/standalone/fp-cloud-cli/tests/test_alerting.py +644 -0
  210. package/.next/standalone/fp-cloud-cli/tests/test_analytics.py +385 -0
  211. package/.next/standalone/fp-cloud-cli/tests/test_audits.py +950 -0
  212. package/.next/standalone/fp-cloud-cli/tests/test_auth.py +98 -0
  213. package/.next/standalone/fp-cloud-cli/tests/test_auth_mode.py +272 -0
  214. package/.next/standalone/fp-cloud-cli/tests/test_click_compat.py +118 -0
  215. package/.next/standalone/fp-cloud-cli/tests/test_client.py +299 -0
  216. package/.next/standalone/fp-cloud-cli/tests/test_commands.py +814 -0
  217. package/.next/standalone/fp-cloud-cli/tests/test_config.py +99 -0
  218. package/.next/standalone/fp-cloud-cli/tests/test_dashboards_agent.py +340 -0
  219. package/.next/standalone/fp-cloud-cli/tests/test_dates.py +76 -0
  220. package/.next/standalone/fp-cloud-cli/tests/test_enforcement_logic.py +481 -0
  221. package/.next/standalone/fp-cloud-cli/tests/test_facets.py +170 -0
  222. package/.next/standalone/fp-cloud-cli/tests/test_failproofai_home.py +722 -0
  223. package/.next/standalone/fp-cloud-cli/tests/test_fp_home_contract.py +198 -0
  224. package/.next/standalone/fp-cloud-cli/tests/test_hardening.py +186 -0
  225. package/.next/standalone/fp-cloud-cli/tests/test_help_table_coverage.py +127 -0
  226. package/.next/standalone/fp-cloud-cli/tests/test_keys_queries.py +496 -0
  227. package/.next/standalone/fp-cloud-cli/tests/test_list.py +59 -0
  228. package/.next/standalone/fp-cloud-cli/tests/test_multivalue.py +183 -0
  229. package/.next/standalone/fp-cloud-cli/tests/test_no_customer_identifiers.py +168 -0
  230. package/.next/standalone/fp-cloud-cli/tests/test_operator.py +492 -0
  231. package/.next/standalone/fp-cloud-cli/tests/test_orgs.py +692 -0
  232. package/.next/standalone/fp-cloud-cli/tests/test_output.py +1732 -0
  233. package/.next/standalone/fp-cloud-cli/tests/test_policy_check.py +191 -0
  234. package/.next/standalone/fp-cloud-cli/tests/test_readme_matches_reality.py +125 -0
  235. package/.next/standalone/fp-cloud-cli/tests/test_review_fixes.py +161 -0
  236. package/.next/standalone/fp-cloud-cli/tests/test_telemetry_completeness.py +90 -0
  237. package/.next/standalone/fp-cloud-cli/tests/test_usage.py +115 -0
  238. package/.next/standalone/fp-cloud-cli/tests/test_v1_origin_diagnostic.py +70 -0
  239. package/.next/standalone/fp-cloud-cli/tests/test_v1_routing.py +214 -0
  240. package/.next/standalone/fp-cloud-cli/tests/test_whoami.py +104 -0
  241. package/.next/standalone/fp-cloud-cli/uv.lock +552 -0
  242. package/.next/standalone/package.json +29 -22
  243. package/.next/standalone/sdk/python/CHANGELOG.md +70 -0
  244. package/.next/standalone/sdk/python/LICENSE +42 -0
  245. package/.next/standalone/sdk/python/MANIFEST.in +21 -0
  246. package/.next/standalone/sdk/python/README.md +466 -0
  247. package/.next/standalone/sdk/python/docs/README.md +249 -0
  248. package/.next/standalone/sdk/python/docs/_shared/README.md +43 -0
  249. package/.next/standalone/sdk/python/docs/_shared/__init__.py +195 -0
  250. package/.next/standalone/sdk/python/docs/crewai/README.md +328 -0
  251. package/.next/standalone/sdk/python/docs/crewai/examples/quickstart.py +61 -0
  252. package/.next/standalone/sdk/python/docs/crewai/examples/research_crew.py +92 -0
  253. package/.next/standalone/sdk/python/docs/langgraph/README.md +481 -0
  254. package/.next/standalone/sdk/python/docs/langgraph/examples/quickstart.py +51 -0
  255. package/.next/standalone/sdk/python/docs/langgraph/examples/research_agent.py +86 -0
  256. package/.next/standalone/sdk/python/docs/langgraph/examples/supervisor_handoff.py +83 -0
  257. package/.next/standalone/sdk/python/docs/llama_index/README.md +332 -0
  258. package/.next/standalone/sdk/python/docs/llama_index/examples/quickstart.py +62 -0
  259. package/.next/standalone/sdk/python/docs/llama_index/examples/research_agent.py +93 -0
  260. package/.next/standalone/sdk/python/docs/manual/README.md +491 -0
  261. package/.next/standalone/sdk/python/docs/manual/examples/quickstart.py +57 -0
  262. package/.next/standalone/sdk/python/docs/manual/examples/research_agent.py +133 -0
  263. package/.next/standalone/sdk/python/docs/pydantic_ai/README.md +323 -0
  264. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/quickstart.py +48 -0
  265. package/.next/standalone/sdk/python/docs/pydantic_ai/examples/research_agent.py +94 -0
  266. package/.next/standalone/sdk/python/failproofai_sdk/__init__.py +131 -0
  267. package/.next/standalone/sdk/python/failproofai_sdk/_context.py +218 -0
  268. package/.next/standalone/sdk/python/failproofai_sdk/_environment.py +78 -0
  269. package/.next/standalone/sdk/python/failproofai_sdk/_events.py +752 -0
  270. package/.next/standalone/sdk/python/failproofai_sdk/_resolver.py +118 -0
  271. package/.next/standalone/sdk/python/failproofai_sdk/_runtime.py +21 -0
  272. package/.next/standalone/sdk/python/failproofai_sdk/_schema.py +317 -0
  273. package/.next/standalone/sdk/python/failproofai_sdk/_scopes.py +422 -0
  274. package/.next/standalone/sdk/python/failproofai_sdk/_version.py +1 -0
  275. package/.next/standalone/sdk/python/failproofai_sdk/_writer.py +750 -0
  276. package/.next/standalone/sdk/python/failproofai_sdk/integrations/__init__.py +274 -0
  277. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_compat.py +247 -0
  278. package/.next/standalone/sdk/python/failproofai_sdk/integrations/_core.py +1076 -0
  279. package/.next/standalone/sdk/python/failproofai_sdk/integrations/crewai.py +1669 -0
  280. package/.next/standalone/sdk/python/failproofai_sdk/integrations/langchain.py +2151 -0
  281. package/.next/standalone/sdk/python/failproofai_sdk/integrations/llama_index.py +1588 -0
  282. package/.next/standalone/sdk/python/failproofai_sdk/integrations/pydantic_ai.py +977 -0
  283. package/.next/standalone/sdk/python/failproofai_sdk/py.typed +0 -0
  284. package/.next/standalone/sdk/python/pyproject.toml +92 -0
  285. package/.next/standalone/sdk/python/skill/SKILL.md +399 -0
  286. package/.next/standalone/sdk/python/skill/agents/openai.yaml +8 -0
  287. package/.next/standalone/sdk/python/skill/references/events.md +234 -0
  288. package/.next/standalone/sdk/python/skill/references/frameworks.md +314 -0
  289. package/.next/standalone/sdk/python/skill/references/install.md +100 -0
  290. package/.next/standalone/sdk/python/skill/references/integration.md +352 -0
  291. package/.next/standalone/sdk/python/tests/__init__.py +0 -0
  292. package/.next/standalone/sdk/python/tests/conftest.py +169 -0
  293. package/.next/standalone/sdk/python/tests/integrations/__init__.py +6 -0
  294. package/.next/standalone/sdk/python/tests/integrations/test_crewai.py +1299 -0
  295. package/.next/standalone/sdk/python/tests/integrations/test_langchain.py +2338 -0
  296. package/.next/standalone/sdk/python/tests/integrations/test_llama_index.py +1576 -0
  297. package/.next/standalone/sdk/python/tests/integrations/test_pydantic_ai.py +1102 -0
  298. package/.next/standalone/sdk/python/tests/test_context.py +410 -0
  299. package/.next/standalone/sdk/python/tests/test_docs.py +411 -0
  300. package/.next/standalone/sdk/python/tests/test_durability.py +1352 -0
  301. package/.next/standalone/sdk/python/tests/test_encoding.py +571 -0
  302. package/.next/standalone/sdk/python/tests/test_integrations.py +1127 -0
  303. package/.next/standalone/sdk/python/tests/test_no_customer_identifiers.py +214 -0
  304. package/.next/standalone/sdk/python/tests/test_packaging.py +185 -0
  305. package/.next/standalone/sdk/python/tests/test_resolver_umbrella.py +178 -0
  306. package/.next/standalone/sdk/python/tests/test_scopes.py +487 -0
  307. package/.next/standalone/sdk/python/tests/test_sdk.py +923 -0
  308. package/.next/standalone/sdk/python/tests/test_server_contract.py +650 -0
  309. package/.next/standalone/sdk/python/tests/test_site_docs.py +371 -0
  310. package/.next/standalone/sdk/python/tests/test_skill_snippets.py +165 -0
  311. package/.next/standalone/sdk/python/tests/test_spool_contract.py +287 -0
  312. package/.next/standalone/sdk/python/tests/test_spool_creation.py +360 -0
  313. package/.next/standalone/sdk/python/tests/test_wire_format.py +279 -0
  314. package/.next/standalone/sdk/python/tests/test_zero_dependencies.py +378 -0
  315. package/.next/standalone/sdk/python/uv.lock +6005 -0
  316. package/.next/standalone/server.js +1 -1
  317. package/README.md +77 -21
  318. package/bin/failproofai.mjs +145 -11
  319. package/dist/cli.mjs +4482 -2205
  320. package/dist/index.js +5 -5
  321. package/dist/worker.mjs +2399 -1978
  322. package/package.json +29 -22
  323. package/policy-pack/SHA256SUMS +2 -0
  324. package/policy-pack/failproofai-pack.json +732 -0
  325. package/policy-pack/failproofai-pack.mjs +2017 -0
  326. package/scripts/build-policy-pack.mjs +121 -0
  327. package/scripts/changelog-section.py +77 -0
  328. package/scripts/prune-standalone.mjs +3 -0
  329. package/scripts/python-version.py +232 -0
  330. package/scripts/translate-docs/cli.ts +21 -0
  331. package/scripts/translate-docs/mintlify-nav.ts +85 -13
  332. package/scripts/translate-docs/translator.ts +66 -11
  333. package/src/audit/cache.ts +40 -3
  334. package/src/audit/cli.ts +1 -1
  335. package/src/audit/index.ts +1 -1
  336. package/src/audit/replay.ts +89 -3
  337. package/src/audit/report.ts +5 -5
  338. package/src/audit/schedule-cli.ts +49 -53
  339. package/src/hooks/builtin-policies.ts +217 -579
  340. package/src/hooks/cloud-enrollment-cli.ts +66 -27
  341. package/src/hooks/cloud-managed-policies.ts +6 -1
  342. package/src/hooks/configure-wizard.ts +1 -1
  343. package/src/hooks/custom-hooks-loader.ts +89 -6
  344. package/src/hooks/fp-home.ts +92 -4
  345. package/src/hooks/fp-reset.ts +26 -2
  346. package/src/hooks/handler.ts +181 -11
  347. package/src/hooks/harness-cli.ts +79 -25
  348. package/src/hooks/hook-activity-store.ts +13 -3
  349. package/src/hooks/install-prompt.ts +25 -3
  350. package/src/hooks/manager.ts +418 -172
  351. package/src/hooks/migrations.ts +2 -2
  352. package/src/hooks/pack-cli.ts +505 -0
  353. package/src/hooks/pack-failclosed.ts +189 -0
  354. package/src/hooks/pack-manifest.ts +344 -0
  355. package/src/hooks/pack-store.ts +930 -0
  356. package/src/hooks/policy-catalog.ts +535 -0
  357. package/src/hooks/policy-evaluator.ts +24 -16
  358. package/src/hooks/policy-presets.ts +4 -2
  359. package/src/hooks/policy-registry.ts +8 -2
  360. package/src/hooks/policy-types.ts +26 -0
  361. package/src/hooks/session-pause-cli.ts +33 -9
  362. package/src/hooks/tui.ts +570 -0
  363. package/src/hooks/types.ts +15 -1
  364. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_usx0j._.js +0 -3
  365. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_zk340._.js +0 -3
  366. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0agdw2a._.js +0 -3
  367. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d1u_na._.js +0 -3
  368. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0oy4kxb._.js +0 -3
  369. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zf034w._.js +0 -3
  370. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1tvn4wf._.js +0 -3
  371. package/.next/standalone/.next/server/chunks/_0lxbzdq._.js +0 -3
  372. package/.next/standalone/.next/server/chunks/_1ger0a2._.js +0 -3
  373. package/.next/standalone/.next/server/chunks/_1zuiiy3._.js +0 -3
  374. package/.next/standalone/.next/server/chunks/node_modules_posthog-node_dist_entrypoints_index_node_mjs_1iiwm5e._.js +0 -3
  375. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kc8_v5._.js +0 -4
  376. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0kgy9da._.js +0 -4
  377. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mhqk5a._.js +0 -4
  378. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0pltpn8._.js +0 -4
  379. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qkcyf-._.js +0 -4
  380. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w0x_ok._.js +0 -4
  381. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1_dfocn._.js +0 -4
  382. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1bfex62._.js +0 -5
  383. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1kzapg5._.js +0 -6
  384. package/.next/standalone/.next/static/chunks/36fw6yk1s6x_f.js +0 -1
  385. package/.next/standalone/.next/static/chunks/3tzq0260n17ae.js +0 -6
  386. package/.next/standalone/.next/static/chunks/3wq9x0c-7ld5d.css +0 -2
  387. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_buildManifest.js +0 -0
  388. /package/.next/standalone/.next/static/{ZklXxyA5ygsWSQz2u_zii → nwbyWT06Ud0XdXOrVJyvS}/_clientMiddlewareManifest.js +0 -0
  389. /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")