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