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