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,1669 @@
1
+ """CrewAI adapter — written against crewai 1.15.8 (2026-07-29), floor 1.13.0.
2
+
3
+ import failproofai_sdk, crewai
4
+ failproofai_sdk.instrument("crewai")
5
+ crew.kickoff()
6
+
7
+ Everything here is a translation table over `crewai.events`, with exactly one
8
+ exception (6. below). Identity, correlation, payload budgets and the never-raise
9
+ policy live in `_core`.
10
+
11
+ Six things about CrewAI that this file is shaped by, each verified against the
12
+ installed package rather than recalled:
13
+
14
+ 1. **The module is `crewai.events`, not `crewai.utilities.events`.** The old
15
+ shim was removed in 1.0.0.
16
+
17
+ 2. **Handlers are keyed by EXACT type, with no MRO walk**
18
+ (`event_bus._sync_handlers.get(type(event))`), so there is no `BaseEvent`
19
+ catch-all to register. Every event class is registered individually, which
20
+ is also what makes the anti-drift test possible.
21
+
22
+ 3. **Our handlers are `async def`, and that is a correctness requirement, not a
23
+ style choice.** `CrewAIEventsBus.emit()` dispatches *sync* handlers onto a
24
+ ten-worker `ThreadPoolExecutor`; with more than one worker, submission order
25
+ is not execution order. Measured on this version, 500 events through a sync
26
+ handler come back **out of order every single run**, while the same 500
27
+ through an async handler are strictly ordered — async handlers are scheduled
28
+ with `run_coroutine_threadsafe` onto one background event loop, and
29
+ `call_soon_threadsafe` is FIFO. Out-of-order handling would break two of the
30
+ four rendering invariants at once: a `tool_result` could be written before
31
+ its `tool_use`, and an `agent_start` before the root's. Keep these `async`.
32
+ They must also never `await` anything, or that ordering guarantee is gone.
33
+
34
+ 4. **`BaseEvent` already carries the span tree** — `event_id`,
35
+ `parent_event_id`, and `started_event_id` on every `*Completed`/`*Failed`
36
+ event. Verified populated on tool, LLM, agent, task and crew events here, so
37
+ nesting is *read*, never reconstructed. (A per-`(role, tool)` LIFO is kept as
38
+ a fallback for the case where the pairing stack has been unwound by an
39
+ exception — that does happen, see 5.)
40
+
41
+ 5. **CrewAI's own agent executor emits `FlowStartedEvent`/`FlowFinishedEvent`
42
+ with `flow_name="AgentExecutor"`, nested inside every single
43
+ `agent_execution_started`.** Mapping flow events to agents unconditionally —
44
+ which is what the obvious reading of the API suggests — would put a spurious
45
+ `AgentExecutor` agent inside every agent execution, doubling the span tree and
46
+ poisoning the `agent_id` facet. A flow whose parent is an agent execution is
47
+ therefore a pass-through link, not an agent. Its `flow_finished` is also
48
+ *missing* when the agent's LLM call raises, so it can never be relied on to
49
+ close anything.
50
+
51
+ 6. **Not everything CrewAI does is on the bus.** `human_input=True` on a Task
52
+ goes through `crewai.core.providers.human_input`, which calls `input()` and
53
+ emits no event of any kind — verified on 1.15.16. That one surface is
54
+ instrumented by wrapping (`_patch_task_human_input`), which is the only patch
55
+ in this file; everything else here is a subscription.
56
+
57
+ **Two kinds of node.** `_nodes` holds more than the spans that become agents:
58
+ tool calls and hook spans are recorded as *links* because CrewAI hangs real work
59
+ underneath them — a `delegate_work_to_coworker` call parents the coworker's whole
60
+ agent execution, and a flow method parents a `Crew.kickoff()` inside it. A leaf
61
+ that is not in `_nodes` is invisible to `_parent_key`, which then falls back to
62
+ the open root; the visible symptom is a flattened tree or a second session, never
63
+ an error.
64
+
65
+ `agent_id` is the crew name or the agent **role** — `LowCardinality(String)`,
66
+ the primary dashboard facet. CrewAI's `agent.id` is a UUID and goes to
67
+ `fw_agent_id`, never here.
68
+ """
69
+
70
+ import json
71
+ import logging
72
+ import threading
73
+ import uuid
74
+ from collections import OrderedDict
75
+ from datetime import datetime, timezone
76
+ from typing import Any
77
+
78
+ from failproofai_sdk.integrations import _compat
79
+ from failproofai_sdk.integrations._core import (
80
+ RunTracker,
81
+ framework_fields,
82
+ fw_fields,
83
+ ms,
84
+ normalize_agent_id,
85
+ safe,
86
+ truncate,
87
+ )
88
+
89
+ logger = logging.getLogger("failproofai_sdk.integrations")
90
+
91
+ __all__ = ["adapter", "FailproofAICrewListener"]
92
+
93
+ NAME = "crewai"
94
+ DIST = "crewai"
95
+
96
+ # 1.13.0 is the release where `started_event_id` and a normalized `usage` dict
97
+ # are both present; below it the span tree has to be reconstructed by hand.
98
+ MIN_VERSION = "1.13.0"
99
+ # Ceiling: without one, a clean build a year from now pulls the next major, the
100
+ # event classes shift, and this adapter stops recording while raising nothing.
101
+ BELOW_VERSION = "2"
102
+
103
+ _INSTALL_HINT = (
104
+ "failproofai_sdk: cannot instrument 'crewai' because 'crewai.events' is not importable. "
105
+ "Install it with: pip install 'failproofai_sdk[crewai]' (or install crewai>=1.13 directly). "
106
+ "Note that the events package moved: it is 'crewai.events', not "
107
+ "'crewai.utilities.events', which was removed in crewai 1.0.0."
108
+ )
109
+
110
+ try: # only ever executed by `instrument("crewai")` — `import failproofai_sdk` never gets here
111
+ from crewai.events import event_types as _ct
112
+ from crewai import events as _ce
113
+ from crewai.events.base_event_listener import BaseEventListener
114
+ from crewai.events.event_bus import crewai_event_bus
115
+ except ImportError as _exc: # pragma: no cover - exercised by uninstalling crewai
116
+ raise ImportError(_INSTALL_HINT) from _exc
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # The translation table
121
+ # ---------------------------------------------------------------------------
122
+
123
+ # event class name -> translator method name. One entry per class, because the
124
+ # bus does no MRO walk. Ordered as the dashboard reads them: structure, then
125
+ # leaves.
126
+ TABLE: tuple[tuple[str, str], ...] = (
127
+ # --- structure: these become agent_start / agent_end -------------------
128
+ ("CrewKickoffStartedEvent", "on_crew_started"),
129
+ ("CrewKickoffCompletedEvent", "on_crew_completed"),
130
+ ("CrewKickoffFailedEvent", "on_crew_failed"),
131
+ ("FlowStartedEvent", "on_flow_started"),
132
+ ("FlowFinishedEvent", "on_flow_finished"),
133
+ # A flow whose method raises emits `FlowFailedEvent` and NEVER
134
+ # `FlowFinishedEvent`. Without this row the flow's `agent_start` is never
135
+ # closed and the session renders `ongoing` forever — the worst outcome
136
+ # available here, and the one every other `*Failed` row in this table exists
137
+ # to avoid.
138
+ ("FlowFailedEvent", "on_flow_failed"),
139
+ ("AgentExecutionStartedEvent", "on_agent_started"),
140
+ ("AgentExecutionCompletedEvent", "on_agent_completed"),
141
+ ("AgentExecutionErrorEvent", "on_agent_error"),
142
+ # `Agent.kickoff()` — crewai's LiteAgent surface, an agent run with no Crew
143
+ # and no Task. It emits its OWN execution events, not the AgentExecution
144
+ # ones, and it is a ROOT: `parent_event_id` is None on it. Without these
145
+ # rows such a run produces no agent span at all — every LLM and tool event
146
+ # falls through to whatever ambient scope exists (agent_id "main"), or is
147
+ # dropped entirely when there is none. Nothing raises either way.
148
+ ("LiteAgentExecutionStartedEvent", "on_lite_agent_started"),
149
+ ("LiteAgentExecutionCompletedEvent", "on_agent_completed"),
150
+ ("LiteAgentExecutionErrorEvent", "on_agent_error"),
151
+ # --- structure we deliberately do NOT turn into spans ------------------
152
+ # A CrewAI Task is a subset of the agent execution that runs it; emitting
153
+ # both would double every row and render them as siblings. The task is
154
+ # recorded as a link so its children still find the crew above them, and
155
+ # its id/name ride along on the agent's own events as fw_task_*.
156
+ ("TaskStartedEvent", "on_task_started"),
157
+ ("TaskCompletedEvent", "on_task_finished"),
158
+ ("TaskFailedEvent", "on_task_finished"),
159
+ # --- framework machinery around the agent: hook pairs ------------------
160
+ ("MethodExecutionStartedEvent", "on_method_started"),
161
+ ("MethodExecutionFinishedEvent", "on_method_finished"),
162
+ ("MethodExecutionFailedEvent", "on_method_failed"),
163
+ ("LLMGuardrailStartedEvent", "on_guardrail_started"),
164
+ ("LLMGuardrailCompletedEvent", "on_guardrail_completed"),
165
+ # --- human in the loop -------------------------------------------------
166
+ # A crew blocked on a person is otherwise an unexplained gap: no event
167
+ # fires for the whole wait, so the session reads as `ongoing` and its
168
+ # active duration absorbs however long the human took. The LangChain and
169
+ # LlamaIndex adapters both map their HITL surface onto the same four
170
+ # events, and this is the crewai one — `crewai.flow.runtime` emits the
171
+ # pair around every `@human_feedback` method.
172
+ ("HumanFeedbackRequestedEvent", "on_human_requested"),
173
+ ("HumanFeedbackReceivedEvent", "on_human_received"),
174
+ # --- things the agent calls: tool pairs --------------------------------
175
+ ("ToolUsageStartedEvent", "on_tool_started"),
176
+ ("ToolUsageFinishedEvent", "on_tool_finished"),
177
+ ("ToolUsageErrorEvent", "on_tool_error"),
178
+ ("MemoryQueryStartedEvent", "on_store_started"),
179
+ ("MemoryQueryCompletedEvent", "on_store_finished"),
180
+ ("MemoryQueryFailedEvent", "on_store_failed"),
181
+ ("MemorySaveStartedEvent", "on_store_started"),
182
+ ("MemorySaveCompletedEvent", "on_store_finished"),
183
+ ("MemorySaveFailedEvent", "on_store_failed"),
184
+ ("MemoryRetrievalStartedEvent", "on_store_started"),
185
+ ("MemoryRetrievalCompletedEvent", "on_store_finished"),
186
+ ("MemoryRetrievalFailedEvent", "on_store_failed"),
187
+ ("KnowledgeQueryStartedEvent", "on_store_started"),
188
+ ("KnowledgeQueryCompletedEvent", "on_store_finished"),
189
+ ("KnowledgeQueryFailedEvent", "on_store_failed"),
190
+ ("KnowledgeRetrievalStartedEvent", "on_store_started"),
191
+ ("KnowledgeRetrievalCompletedEvent", "on_store_finished"),
192
+ ("KnowledgeSearchQueryFailedEvent", "on_store_failed"),
193
+ # --- model calls -------------------------------------------------------
194
+ ("LLMCallStartedEvent", "on_llm_started"),
195
+ ("LLMCallCompletedEvent", "on_llm_completed"),
196
+ ("LLMCallFailedEvent", "on_llm_failed"),
197
+ # A streaming chunk emits NOTHING. A 500-token response would otherwise be
198
+ # 500 stored rows and 500 rail rows against a five-lane cap. It is
199
+ # folded into fw_chunks / fw_ttft_ms / fw_streamed on the model_response.
200
+ ("LLMStreamChunkEvent", "on_llm_chunk"),
201
+ )
202
+
203
+ # Memory and knowledge operations are retrieval calls the agent makes, so they
204
+ # are tools, named for the surface they hit rather than the class that fired.
205
+ STORE_TOOLS: dict[str, str] = {
206
+ "MemoryQueryStartedEvent": "memory.query",
207
+ "MemoryQueryCompletedEvent": "memory.query",
208
+ "MemoryQueryFailedEvent": "memory.query",
209
+ "MemorySaveStartedEvent": "memory.save",
210
+ "MemorySaveCompletedEvent": "memory.save",
211
+ "MemorySaveFailedEvent": "memory.save",
212
+ "MemoryRetrievalStartedEvent": "memory.retrieve",
213
+ "MemoryRetrievalCompletedEvent": "memory.retrieve",
214
+ "MemoryRetrievalFailedEvent": "memory.retrieve",
215
+ "KnowledgeQueryStartedEvent": "knowledge.query",
216
+ "KnowledgeQueryCompletedEvent": "knowledge.query",
217
+ "KnowledgeQueryFailedEvent": "knowledge.query",
218
+ "KnowledgeRetrievalStartedEvent": "knowledge.search",
219
+ "KnowledgeRetrievalCompletedEvent": "knowledge.search",
220
+ "KnowledgeSearchQueryFailedEvent": "knowledge.search",
221
+ }
222
+
223
+ # Flow names CrewAI uses for its own internal machinery. Belt and braces on top
224
+ # of the structural check in `on_flow_started` — the structural one is the rule,
225
+ # this is the fallback if a future release re-parents the executor flow.
226
+ INTERNAL_FLOW_NAMES = frozenset({"AgentExecutor"})
227
+
228
+ # Node kinds. "crew"/"flow"/"agent" are real Failproof AI agents; the rest are
229
+ # links that exist so a child can find the agent above it.
230
+ _AGENT_KINDS = frozenset({"crew", "flow", "agent"})
231
+
232
+ _MAX_NODES = 20_000
233
+ _MAX_LEAVES = 10_000
234
+ _MAX_STREAMS = 1_000
235
+
236
+
237
+ class _Node:
238
+ """One CrewAI span, as far as we care about it."""
239
+
240
+ __slots__ = ("kind", "parent", "task", "agent_id")
241
+
242
+ def __init__(self, kind, parent, task=None, agent_id=None):
243
+ self.kind = kind
244
+ self.parent = parent
245
+ self.task = task # (task_id, task_name), inherited from the parent
246
+ self.agent_id = agent_id
247
+
248
+
249
+ class _Leaf:
250
+ """An emitted-but-unclosed leaf span: a tool, a model call, a hook.
251
+
252
+ `agent_end` force-closes open *pauses* but not tools, models or humans, so a
253
+ run that dies mid-tool leaves the session `ongoing` forever. Every open leaf
254
+ is remembered here and closed when the span that owns it ends.
255
+ """
256
+
257
+ __slots__ = ("method", "key", "parent_key", "match", "started", "fields")
258
+
259
+ def __init__(self, method, key, parent_key, match, started, fields):
260
+ self.method = method
261
+ self.key = key
262
+ self.parent_key = parent_key
263
+ self.match = match # for the LIFO fallback when started_event_id is absent
264
+ self.started = started
265
+ self.fields = fields
266
+
267
+
268
+ class _CrewAIAdapter:
269
+ """The object `failproofai_sdk.integrations` loads as `adapter`."""
270
+
271
+ name = NAME
272
+ module = "crewai"
273
+
274
+ def __init__(self) -> None:
275
+ # RLock: teardown resolves parents while already holding it.
276
+ self._lock = threading.RLock()
277
+ self._tracker: RunTracker | None = None
278
+ self._listener: "FailproofAICrewListener | None" = None
279
+ # (class, attribute, original descriptor, installed function) for the
280
+ # one wrapped surface — see `_patch_task_human_input`.
281
+ self._patched: list[tuple] = []
282
+ self._nodes: "OrderedDict[str, _Node]" = OrderedDict()
283
+ self._leaves: "OrderedDict[str, _Leaf]" = OrderedDict()
284
+ self._streams: "OrderedDict[str, list]" = OrderedDict()
285
+ # pause id -> (emit key, parent key, prompt). Bounded like the rest:
286
+ # a run that is cancelled while a human is still thinking never sends
287
+ # the matching `received`, and unbounded that is a leak in a gateway.
288
+ self._pauses: "OrderedDict[str, tuple]" = OrderedDict()
289
+ self._roots: list[str] = []
290
+ self._session_id: str | None = None
291
+
292
+ # -- install / uninstall ------------------------------------------------
293
+
294
+ def install(self, **options: Any) -> None:
295
+ """Register the listener on the module-level `crewai_event_bus`.
296
+
297
+ `options` is whatever was passed to `failproofai_sdk.instrument()`, and the same
298
+ dict reaches every adapter, so unknown keys are ignored rather than
299
+ raising a TypeError that would take out the other frameworks.
300
+ """
301
+ _compat.check_version(
302
+ NAME,
303
+ DIST,
304
+ minimum=MIN_VERSION,
305
+ below=BELOW_VERSION,
306
+ reason="started_event_id on *Completed events, and a normalized usage dict",
307
+ )
308
+ self._session_id = options.get("session_id")
309
+ self._tracker = RunTracker(
310
+ NAME,
311
+ base_fields=framework_fields(NAME, DIST),
312
+ )
313
+ # Must be kept alive in a module-level global. `BaseEventListener` holds
314
+ # no reference back from the bus other than the bound handlers, so a
315
+ # listener that goes out of scope keeps "working" only by accident.
316
+ self._listener = FailproofAICrewListener(self)
317
+ self._patch_task_human_input()
318
+
319
+ def uninstall(self) -> None:
320
+ listener, self._listener = self._listener, None
321
+ if listener is not None:
322
+ listener.teardown()
323
+ self._restore_patches()
324
+ # A run that is still open when the customer uninstruments would render
325
+ # `ongoing` forever. Close it, then forget everything.
326
+ self._close_everything(outcome="cancelled")
327
+ with self._lock:
328
+ self._nodes.clear()
329
+ self._leaves.clear()
330
+ self._streams.clear()
331
+ self._pauses.clear()
332
+ self._roots.clear()
333
+ if self._tracker is not None:
334
+ self._tracker.reset()
335
+ self._tracker = None
336
+
337
+ # -- bookkeeping --------------------------------------------------------
338
+
339
+ def _note(self, event_id, parent, kind, task=None, agent_id=None) -> None:
340
+ with self._lock:
341
+ while len(self._nodes) >= _MAX_NODES:
342
+ self._nodes.pop(next(iter(self._nodes)), None)
343
+ inherited = task
344
+ if inherited is None and parent is not None:
345
+ node = self._nodes.get(parent)
346
+ inherited = node.task if node is not None else None
347
+ self._nodes[event_id] = _Node(kind, parent, inherited, agent_id)
348
+
349
+ def _parent_key(self, parent_event_id, *, allow_root_fallback: bool = True):
350
+ """Which run a child should hang off.
351
+
352
+ CrewAI's `parent_event_id` comes off a contextvar scope stack. On 1.15.16
353
+ it survives crewai's own thread pool — measured: an `async_execution`
354
+ task's events all arrive with it set — so the fallback is for the events
355
+ whose parent span is genuinely gone: one that arrives after
356
+ `_close_span` popped its parent, or after `_MAX_NODES` evicted it.
357
+ Falling back to the open root keeps those inside the session instead of
358
+ minting a second one, which is the failure that splits one run into many.
359
+
360
+ Root-capable events (`crew_kickoff_started`, `flow_started`) pass
361
+ `allow_root_fallback=False`: for them a missing parent genuinely means
362
+ "this is the top".
363
+ """
364
+ with self._lock:
365
+ if parent_event_id is not None and parent_event_id in self._nodes:
366
+ return parent_event_id
367
+ if not allow_root_fallback or not self._roots:
368
+ return None
369
+ roots = list(self._roots)
370
+ if len(roots) == 1:
371
+ return roots[0]
372
+ return self._root_for_current_context(roots)
373
+
374
+ def _root_for_current_context(self, roots):
375
+ """Which open root an orphaned event belongs to, with several open.
376
+
377
+ `_roots` is process-global, so two crews kicked off on two threads leave
378
+ two entries in it and `roots[-1]` is a coin flip — one that files one
379
+ run's events under the OTHER run's session id. That is silent
380
+ cross-session corruption, which is strictly worse than a missing row,
381
+ and it is reachable without any `async_execution`: a late
382
+ `ToolUsageFinished` whose agent span has already been closed, or an
383
+ eviction at `_MAX_NODES`, is enough to reach this fallback.
384
+
385
+ `CrewAIEventsBus.emit` copies the EMITTING thread's contextvars onto the
386
+ handler task, so an ambient `failproofai_sdk.session()` is a real signal about
387
+ which of the open runs we are inside. Prefer the newest root that agrees
388
+ with it; only guess when there is no ambient session to check against.
389
+ """
390
+ tracker = self._tracker
391
+ if tracker is None:
392
+ return roots[-1]
393
+ ambient = tracker.identity(None, None, warn=False)
394
+ if ambient is None:
395
+ return roots[-1]
396
+ for key in reversed(roots):
397
+ identity = tracker.identity(key, warn=False)
398
+ if identity is not None and identity.session_id == ambient.session_id:
399
+ return key
400
+ return roots[-1]
401
+
402
+ def _task_of(self, event):
403
+ """(task_id, task_name) for an event, from the event or its ancestors."""
404
+ task_id = getattr(event, "task_id", None)
405
+ task_name = getattr(event, "task_name", None)
406
+ if task_id or task_name:
407
+ return (task_id, task_name)
408
+ with self._lock:
409
+ node = self._nodes.get(getattr(event, "parent_event_id", None))
410
+ return node.task if node is not None else None
411
+
412
+ # -- leaf spans ---------------------------------------------------------
413
+
414
+ def _open_leaf(self, method, key, parent_key, match, started, fields) -> None:
415
+ with self._lock:
416
+ while len(self._leaves) >= _MAX_LEAVES:
417
+ self._leaves.pop(next(iter(self._leaves)), None)
418
+ self._leaves[key] = _Leaf(method, key, parent_key, match, started, fields)
419
+
420
+ def _pop_leaf(self, start_id, match):
421
+ """The open leaf this ending event closes.
422
+
423
+ `started_event_id` is the authority — it is populated on every
424
+ `*Completed`/`*Failed` event on this version. The `match` scan is the
425
+ documented fallback for the case where the scope stack was unwound by an
426
+ exception before the ending event was emitted: last-opened wins, which is
427
+ the right answer for a nested retry.
428
+ """
429
+ with self._lock:
430
+ if start_id is not None:
431
+ leaf = self._leaves.pop(start_id, None)
432
+ if leaf is not None:
433
+ return leaf
434
+ for key in reversed(self._leaves):
435
+ if self._leaves[key].match == match:
436
+ return self._leaves.pop(key)
437
+ return None
438
+
439
+ def _descends_from(self, node_id, ancestor) -> bool:
440
+ seen = set()
441
+ key = node_id
442
+ while key is not None and key not in seen:
443
+ if key == ancestor:
444
+ return True
445
+ seen.add(key)
446
+ node = self._nodes.get(key)
447
+ key = node.parent if node is not None else None
448
+ return False
449
+
450
+ def _close_span(self, node_id, *, outcome, summary=None, **fields) -> None:
451
+ """End an agent span, after closing everything still open beneath it."""
452
+ tracker = self._tracker
453
+ if tracker is None or node_id is None:
454
+ return
455
+ with self._lock:
456
+ leaves = [
457
+ leaf
458
+ for leaf in reversed(list(self._leaves.values()))
459
+ if self._descends_from(leaf.parent_key, node_id)
460
+ ]
461
+ for leaf in leaves:
462
+ self._leaves.pop(leaf.key, None)
463
+ descendants = [
464
+ key
465
+ for key in reversed(tracker.open_agents())
466
+ if key != node_id and self._descends_from(key, node_id)
467
+ ]
468
+ for leaf in leaves:
469
+ self._force_close(leaf)
470
+ for key in descendants:
471
+ tracker.end_agent(key, outcome="cancelled", **fw_fields(closed_by="teardown"))
472
+ tracker.end_agent(node_id, outcome=outcome, summary=summary, **fields)
473
+ with self._lock:
474
+ self._roots = [key for key in self._roots if key != node_id]
475
+ for key in [k for k in self._nodes if self._descends_from(k, node_id)]:
476
+ self._nodes.pop(key, None)
477
+
478
+ def _force_close(self, leaf) -> None:
479
+ """Close a leaf whose framework never reported an end."""
480
+ tracker = self._tracker
481
+ if tracker is None:
482
+ return
483
+ fields = dict(leaf.fields)
484
+ fields.update(fw_fields(incomplete=True, closed_by="teardown"))
485
+ if leaf.method == "hook_completed":
486
+ fields["outcome"] = "cancelled"
487
+ if leaf.method == "model_response":
488
+ # Invariant: EVERY model_response carries an int duration_ms, including
489
+ # the ones we synthesize. Without it the dashboard shows no duration
490
+ # for exactly the calls that went wrong.
491
+ fields["duration_ms"] = ms(datetime.now(timezone.utc) - leaf.started)
492
+ tracker.emit(leaf.method, leaf.key, parent_key=leaf.parent_key, **fields)
493
+
494
+ def _close_everything(self, *, outcome: str) -> None:
495
+ for node_id in list(reversed(self._roots)):
496
+ self._close_span(node_id, outcome=outcome)
497
+ tracker = self._tracker
498
+ if tracker is None:
499
+ return
500
+ with self._lock:
501
+ leaves = list(reversed(list(self._leaves.values())))
502
+ self._leaves.clear()
503
+ for leaf in leaves:
504
+ self._force_close(leaf)
505
+ tracker.close_open_agents(outcome=outcome)
506
+
507
+ # -- crew ---------------------------------------------------------------
508
+
509
+ @safe
510
+ def on_crew_started(self, source, event) -> None:
511
+ tracker = self._tracker
512
+ if tracker is None:
513
+ return
514
+ # A fresh uuid4 per kickoff, NOT crew.id: crew.id is stable across
515
+ # kickoffs, so reusing it would merge every run of the same crew into one
516
+ # never-ending session. RunTracker mints it when no parent and no
517
+ # ambient scope supply one.
518
+ parent = self._parent_key(event.parent_event_id, allow_root_fallback=False)
519
+ agent_id = normalize_agent_id(getattr(event, "crew_name", None), default="crew")
520
+ tracker.start_agent(
521
+ event.event_id,
522
+ agent_id=agent_id,
523
+ parent_key=parent,
524
+ session_id=self._session_id if parent is None else None,
525
+ goal=_first_text(getattr(event, "inputs", None)),
526
+ **fw_fields(
527
+ kind="crew",
528
+ crew_name=getattr(event, "crew_name", None),
529
+ inputs=getattr(event, "inputs", None),
530
+ event_id=event.event_id,
531
+ ),
532
+ )
533
+ self._note(event.event_id, parent, "crew", agent_id=agent_id)
534
+ with self._lock:
535
+ if parent is None:
536
+ self._roots.append(event.event_id)
537
+
538
+ @safe
539
+ def on_crew_completed(self, source, event) -> None:
540
+ self._close_span(
541
+ self._crew_key(event),
542
+ outcome="success",
543
+ summary=_text(getattr(event, "output", None)),
544
+ **fw_fields(total_tokens=getattr(event, "total_tokens", None)),
545
+ )
546
+
547
+ @safe
548
+ def on_crew_failed(self, source, event) -> None:
549
+ # The error is reported on the span that OWNS it. No standalone `error`
550
+ # event: `sessionSummary.errorCount` counts both, so emitting one here
551
+ # would double-count every failed crew.
552
+ self._close_span(
553
+ self._crew_key(event),
554
+ outcome="failed",
555
+ summary=_text(getattr(event, "error", None)),
556
+ **fw_fields(error=_text(getattr(event, "error", None))),
557
+ )
558
+
559
+ def _crew_key(self, event):
560
+ """The crew span this ending event closes.
561
+
562
+ `started_event_id` is set explicitly by `Crew._finish_execution`, so it
563
+ is reliable; the root fallback exists because a session left with an open
564
+ root `agent_start` renders `ongoing` forever, which is the single worst
565
+ outcome available here.
566
+ """
567
+ with self._lock:
568
+ node_id = getattr(event, "started_event_id", None)
569
+ if node_id is not None and node_id in self._nodes:
570
+ return node_id
571
+ roots = list(self._roots)
572
+ if not roots:
573
+ return None
574
+ # Same reasoning as `_root_for_current_context`: closing "the newest
575
+ # root" while a second crew is open on another thread ends the WRONG
576
+ # crew, which reads as one run finishing early and one hanging forever.
577
+ return roots[0] if len(roots) == 1 else self._root_for_current_context(roots)
578
+
579
+ # -- flows --------------------------------------------------------------
580
+
581
+ @safe
582
+ def on_flow_started(self, source, event) -> None:
583
+ tracker = self._tracker
584
+ if tracker is None:
585
+ return
586
+ parent_id = getattr(event, "parent_event_id", None)
587
+ with self._lock:
588
+ parent_node = self._nodes.get(parent_id) if parent_id else None
589
+ internal = (parent_node is not None and parent_node.kind == "agent") or (
590
+ getattr(event, "flow_name", None) in INTERNAL_FLOW_NAMES
591
+ )
592
+ if internal:
593
+ # CrewAI's own agent executor. Link it so the LLM and tool events
594
+ # underneath resolve to the agent, but emit nothing: it is not a
595
+ # flow the user wrote, and its flow_finished is missing entirely
596
+ # when the agent's LLM call raises.
597
+ parent = self._parent_key(parent_id)
598
+ tracker.link(event.event_id, parent)
599
+ self._note(event.event_id, parent, "flow_internal")
600
+ return
601
+ parent = self._parent_key(parent_id, allow_root_fallback=False)
602
+ agent_id = normalize_agent_id(getattr(event, "flow_name", None), default="flow")
603
+ tracker.start_agent(
604
+ event.event_id,
605
+ agent_id=agent_id,
606
+ parent_key=parent,
607
+ session_id=self._session_id if parent is None else None,
608
+ goal=_first_text(getattr(event, "inputs", None)),
609
+ **fw_fields(
610
+ kind="flow",
611
+ flow_name=getattr(event, "flow_name", None),
612
+ inputs=getattr(event, "inputs", None),
613
+ event_id=event.event_id,
614
+ ),
615
+ )
616
+ self._note(event.event_id, parent, "flow", agent_id=agent_id)
617
+ with self._lock:
618
+ if parent is None:
619
+ self._roots.append(event.event_id)
620
+
621
+ @safe
622
+ def on_flow_finished(self, source, event) -> None:
623
+ node_id = self._flow_key(event)
624
+ if node_id is None:
625
+ return
626
+ self._close_span(
627
+ node_id, outcome="success", summary=_text(getattr(event, "result", None))
628
+ )
629
+
630
+ @safe
631
+ def on_flow_failed(self, source, event) -> None:
632
+ node_id = self._flow_key(event)
633
+ if node_id is None:
634
+ return
635
+ error = _text(getattr(event, "error", None))
636
+ self._close_span(
637
+ node_id,
638
+ outcome="failed",
639
+ summary=error,
640
+ **fw_fields(error=error),
641
+ )
642
+
643
+ def _flow_key(self, event):
644
+ """The flow span this ending event closes, or None if there is nothing to close.
645
+
646
+ `flow_internal` is the AgentExecutor pass-through link (see 5. in the
647
+ module docstring): it opened no span, so its end just drops the node.
648
+ """
649
+ node_id = getattr(event, "started_event_id", None)
650
+ with self._lock:
651
+ node = self._nodes.get(node_id) if node_id else None
652
+ if node is not None and node.kind == "flow_internal":
653
+ self._nodes.pop(node_id, None)
654
+ return None
655
+ if node is None:
656
+ return None
657
+ return node_id
658
+
659
+ # -- tasks: recorded, never emitted -------------------------------------
660
+
661
+ @safe
662
+ def on_task_started(self, source, event) -> None:
663
+ tracker = self._tracker
664
+ if tracker is None:
665
+ return
666
+ parent = self._parent_key(event.parent_event_id)
667
+ tracker.link(event.event_id, parent)
668
+ task = (getattr(event, "task_id", None), getattr(event, "task_name", None))
669
+ self._note(event.event_id, parent, "task", task=task)
670
+
671
+ @safe
672
+ def on_task_finished(self, source, event) -> None:
673
+ with self._lock:
674
+ self._nodes.pop(event.started_event_id, None)
675
+
676
+ # -- agents -------------------------------------------------------------
677
+
678
+ @safe
679
+ def on_agent_started(self, source, event) -> None:
680
+ tracker = self._tracker
681
+ if tracker is None:
682
+ return
683
+ agent = getattr(event, "agent", None)
684
+ parent = self._parent_key(event.parent_event_id)
685
+ # `event.agent_role` is None on this event (only the tool and LLM events
686
+ # get it filled in), so the role comes off the agent object. The UUID in
687
+ # `agent.id` must never reach agent_id.
688
+ role = getattr(event, "agent_role", None) or getattr(agent, "role", None)
689
+ agent_id = normalize_agent_id(role, default="agent")
690
+ task = self._task_of(event) or (None, None)
691
+ tracker.start_agent(
692
+ event.event_id,
693
+ agent_id=agent_id,
694
+ parent_key=parent,
695
+ goal=_text(getattr(agent, "goal", None)),
696
+ **fw_fields(
697
+ kind="agent",
698
+ agent_id=str(getattr(agent, "id", "")) or None,
699
+ agent_role=role,
700
+ task_id=task[0],
701
+ task_name=task[1],
702
+ tools=_tool_names(getattr(event, "tools", None)),
703
+ allow_delegation=getattr(agent, "allow_delegation", None),
704
+ event_id=event.event_id,
705
+ ),
706
+ )
707
+ self._note(event.event_id, parent, "agent", task=task, agent_id=agent_id)
708
+
709
+ @safe
710
+ def on_lite_agent_started(self, source, event) -> None:
711
+ """`Agent.kickoff()`. A root, like a crew or a user-written flow.
712
+
713
+ The role is only in `agent_info` here — `agent_role` is None on this
714
+ event, exactly as it is on `AgentExecutionStartedEvent`, and
715
+ `agent_info["id"]` is a UUID that must not reach `agent_id`.
716
+ """
717
+ tracker = self._tracker
718
+ if tracker is None:
719
+ return
720
+ info = getattr(event, "agent_info", None)
721
+ info = info if isinstance(info, dict) else {}
722
+ parent = self._parent_key(
723
+ getattr(event, "parent_event_id", None), allow_root_fallback=False
724
+ )
725
+ role = getattr(event, "agent_role", None) or info.get("role")
726
+ agent_id = normalize_agent_id(role, default="agent")
727
+ tracker.start_agent(
728
+ event.event_id,
729
+ agent_id=agent_id,
730
+ parent_key=parent,
731
+ session_id=self._session_id if parent is None else None,
732
+ goal=_text(info.get("goal")),
733
+ **fw_fields(
734
+ kind="agent",
735
+ lite=True,
736
+ agent_id=str(info.get("id") or "") or None,
737
+ agent_role=role,
738
+ tools=_tool_names(getattr(event, "tools", None)),
739
+ event_id=event.event_id,
740
+ ),
741
+ )
742
+ self._note(event.event_id, parent, "agent", agent_id=agent_id)
743
+ with self._lock:
744
+ if parent is None:
745
+ self._roots.append(event.event_id)
746
+
747
+ @safe
748
+ def on_agent_completed(self, source, event) -> None:
749
+ self._close_span(
750
+ event.started_event_id,
751
+ outcome="success",
752
+ summary=_text(getattr(event, "output", None)),
753
+ )
754
+
755
+ @safe
756
+ def on_agent_error(self, source, event) -> None:
757
+ self._close_span(
758
+ event.started_event_id,
759
+ outcome="failed",
760
+ summary=_text(getattr(event, "error", None)),
761
+ **fw_fields(error=_text(getattr(event, "error", None))),
762
+ )
763
+
764
+ # -- flow methods and guardrails: hook pairs ----------------------------
765
+
766
+ @safe
767
+ def on_method_started(self, source, event) -> None:
768
+ self._hook_start(
769
+ event,
770
+ hook_name=getattr(event, "method_name", None) or "flow_method",
771
+ trigger_event="flow_method",
772
+ input=_dictify(getattr(event, "params", None)),
773
+ extra=fw_fields(flow_name=getattr(event, "flow_name", None)),
774
+ )
775
+
776
+ @safe
777
+ def on_method_finished(self, source, event) -> None:
778
+ self._hook_end(event, outcome="success", output=_text(getattr(event, "result", None)))
779
+
780
+ @safe
781
+ def on_method_failed(self, source, event) -> None:
782
+ self._hook_end(
783
+ event, outcome="failed", error=_text(getattr(event, "error", None))
784
+ )
785
+
786
+ # -- human in the loop --------------------------------------------------
787
+ #
788
+ # The same four events, in the same order, as the LangChain and LlamaIndex
789
+ # adapters. Neither pair is redundant and neither is optional:
790
+ #
791
+ # human_wait -> human_input carries the prompt, the answer and the
792
+ # pendingHuman count;
793
+ # agent_pause -> agent_resume is the ONLY thing that feeds pausedMs, so
794
+ # without it the wait is billed as active
795
+ # time on the agent.
796
+ #
797
+ # PAIRING is the whole difficulty here. Verified against crewai 1.15.16:
798
+ # `crewai.flow.runtime` constructs both events with **no correlation id at
799
+ # all** — `request_id` is None on both and `started_event_id` is None on the
800
+ # received one, so there is nothing to join on. What they do share is
801
+ # `(flow_name, method_name)`, and the interaction is strictly sequential:
802
+ # the runtime emits `requested`, blocks on `input()`, then emits `received`.
803
+ #
804
+ # So the lookup is `request_id` -> `(flow_name, method_name)` -> most
805
+ # recently opened, in that order. The last fallback is sound rather than a
806
+ # guess, because a blocking console prompt cannot interleave with another.
807
+ # `request_id` is tried first anyway: the enterprise async provider in
808
+ # `crewai.flow.async_feedback` does set it, and that one CAN interleave.
809
+
810
+ @staticmethod
811
+ def _pause_lookup_keys(event) -> tuple:
812
+ """Join keys for one HITL event, most specific first."""
813
+ keys = []
814
+ request_id = getattr(event, "request_id", None)
815
+ if request_id:
816
+ keys.append(("request", str(request_id)))
817
+ started = getattr(event, "started_event_id", None)
818
+ if started:
819
+ keys.append(("request", str(started)))
820
+ flow = getattr(event, "flow_name", None)
821
+ method = getattr(event, "method_name", None)
822
+ if flow or method:
823
+ keys.append(("method", str(flow), str(method)))
824
+ return tuple(keys)
825
+
826
+ @safe
827
+ def on_human_requested(self, source, event) -> None:
828
+ tracker = self._tracker
829
+ if tracker is None:
830
+ return
831
+ # The id the SDK pairs on. `event_id` is always present and unique;
832
+ # `request_id` is usually None, so it cannot serve as this.
833
+ pause_id = str(getattr(event, "event_id", None) or uuid.uuid4().hex)
834
+ parent = self._parent_key(getattr(event, "parent_event_id", None))
835
+ prompt = _text(getattr(event, "message", None))
836
+ record = (pause_id, parent, prompt)
837
+
838
+ with self._lock:
839
+ while len(self._pauses) >= _MAX_NODES:
840
+ self._pauses.pop(next(iter(self._pauses)), None)
841
+ for key in self._pause_lookup_keys(event):
842
+ self._pauses[key] = record
843
+ # Always keyed by its own id too, so a `received` that does carry an
844
+ # id finds it even when flow and method are both blank.
845
+ self._pauses[("request", pause_id)] = record
846
+
847
+ options = [str(o) for o in (getattr(event, "emit", None) or [])] or None
848
+ extra = fw_fields(
849
+ flow_name=getattr(event, "flow_name", None),
850
+ method_name=getattr(event, "method_name", None),
851
+ output=_text(getattr(event, "output", None)),
852
+ )
853
+ tracker.emit(
854
+ "human_wait",
855
+ pause_id,
856
+ parent_key=parent,
857
+ input_id=pause_id,
858
+ prompt=prompt,
859
+ options=options,
860
+ reason="crewai_human_feedback",
861
+ **extra,
862
+ )
863
+ tracker.emit(
864
+ "agent_pause",
865
+ pause_id,
866
+ parent_key=parent,
867
+ pause_id=pause_id,
868
+ reason="crewai_human_feedback",
869
+ **extra,
870
+ )
871
+
872
+ @safe
873
+ def on_human_received(self, source, event) -> None:
874
+ tracker = self._tracker
875
+ if tracker is None:
876
+ return
877
+ feedback = _text(getattr(event, "feedback", None))
878
+ extra = fw_fields(
879
+ flow_name=getattr(event, "flow_name", None),
880
+ method_name=getattr(event, "method_name", None),
881
+ outcome_hint=getattr(event, "outcome", None),
882
+ )
883
+
884
+ record = None
885
+ with self._lock:
886
+ for key in self._pause_lookup_keys(event):
887
+ record = self._pauses.pop(key, None)
888
+ if record is not None:
889
+ break
890
+ if record is None and self._pauses:
891
+ # Sequential fallback: the most recently opened pause.
892
+ _, record = self._pauses.popitem(last=True)
893
+ if record is not None:
894
+ # Drop this record's other alias keys so it cannot pair twice.
895
+ for key in [k for k, v in self._pauses.items() if v is record]:
896
+ self._pauses.pop(key, None)
897
+
898
+ if record is None:
899
+ # Feedback for a request we never saw — a flow resumed in another
900
+ # process, or one that predates instrument(). Record the answer but
901
+ # NOT agent_resume: closing a pause that never opened subtracts a
902
+ # pausedMs interval that was never added. `input_id` falls back to
903
+ # this event's own id and is NEVER None, which is a hard TypeError
904
+ # on `human_input` rather than a quietly dropped field.
905
+ own_id = str(getattr(event, "event_id", None) or uuid.uuid4().hex)
906
+ tracker.emit(
907
+ "human_input",
908
+ own_id,
909
+ parent_key=self._parent_key(getattr(event, "parent_event_id", None)),
910
+ input_id=own_id,
911
+ response=feedback,
912
+ **fw_fields(orphaned=True),
913
+ **extra,
914
+ )
915
+ return
916
+
917
+ pause_id, parent, _prompt = record
918
+ # agent_resume FIRST: the dashboard closes the pause on it, and
919
+ # `duration_ms` on both closing events is measured from the matching
920
+ # opening one.
921
+ tracker.emit(
922
+ "agent_resume",
923
+ pause_id,
924
+ parent_key=parent,
925
+ pause_id=pause_id,
926
+ reason="crewai_human_feedback",
927
+ **extra,
928
+ )
929
+ tracker.emit(
930
+ "human_input",
931
+ pause_id,
932
+ parent_key=parent,
933
+ input_id=pause_id,
934
+ response=feedback,
935
+ **extra,
936
+ )
937
+
938
+ # -- the OTHER human-in-the-loop surface: Task(human_input=True) ---------
939
+ #
940
+ # crewai has TWO of them and only one is on the event bus.
941
+ # `@human_feedback` on a Flow method emits the pair mapped above.
942
+ # `human_input=True` on a crew Task — the surface crewai's own Crew
943
+ # documentation teaches, and the one most people mean by "HITL in crewai" —
944
+ # runs through `crewai.core.providers.human_input.SyncHumanInputProvider`,
945
+ # which prints a rich panel, calls `input()`, and emits NOTHING. Verified
946
+ # against crewai 1.15.16: no event class in `crewai.events` fires for it.
947
+ #
948
+ # Subscribing therefore cannot reach it, so this one surface is instrumented
949
+ # by WRAPPING. It is the only patch in this adapter. Three properties keep
950
+ # that acceptable:
951
+ #
952
+ # * `_prompt_input` is the narrowest possible seam — it is exactly the
953
+ # blocking call, so the pause interval is the human's wait and nothing
954
+ # else. Wrapping `handle_feedback` instead would fold the agent's
955
+ # re-invocation LLM calls into pausedMs.
956
+ # * the call is inside one `try` whose only job is to re-raise, and both
957
+ # of our own hooks are `@safe`, so nothing here can change what the
958
+ # customer's crew returns or raises.
959
+ # * if crewai moves it, `_compat.probe` disables this pair and the rest of
960
+ # the adapter is untouched.
961
+
962
+ _HITL_PROMPT = (
963
+ "crewai is blocked on human feedback for this task's result "
964
+ "(an empty answer accepts it)."
965
+ )
966
+ _HITL_REASON = "crewai_task_human_input"
967
+
968
+ def _patch_task_human_input(self) -> None:
969
+ try:
970
+ from crewai.core.providers.human_input import SyncHumanInputProvider
971
+ except Exception: # pragma: no cover - exercised by a crewai that moved it
972
+ _compat.probe(NAME, "Task(human_input=True)", lambda: False)
973
+ return
974
+ for attr, is_async in (("_prompt_input", False), ("_prompt_input_async", True)):
975
+ original = getattr(SyncHumanInputProvider, attr, None)
976
+ if original is None:
977
+ _compat.probe(NAME, f"SyncHumanInputProvider.{attr}", lambda: False)
978
+ continue
979
+ if getattr(original, "__failproofai_wrapped__", None) is not None:
980
+ # Already ours — an install that ran without its uninstall.
981
+ # Wrapping again would emit the pause twice per prompt.
982
+ continue
983
+ wrapper = self._wrap_prompt(original, is_async=is_async)
984
+ wrapper.__failproofai_wrapped__ = original
985
+ # `staticmethod(...)`, not the bare function: `_prompt_input` is a
986
+ # staticmethod and the call site is `self._prompt_input(crew)`, so a
987
+ # plain function would bind and arrive with `self` as `crew`.
988
+ descriptor = SyncHumanInputProvider.__dict__.get(attr)
989
+ setattr(SyncHumanInputProvider, attr, staticmethod(wrapper))
990
+ self._patched.append((SyncHumanInputProvider, attr, descriptor, wrapper))
991
+
992
+ def _restore_patches(self) -> None:
993
+ for klass, attr, descriptor, installed in reversed(self._patched):
994
+ try:
995
+ if getattr(klass, attr, None) is not installed:
996
+ # Somebody patched on top of us; restoring would delete
997
+ # their patch. Same rule as `_core.Patcher`.
998
+ logger.warning(
999
+ "failproofai_sdk: not restoring %s.%s — it is no longer the object "
1000
+ "failproofai_sdk installed.",
1001
+ klass.__name__,
1002
+ attr,
1003
+ )
1004
+ continue
1005
+ if descriptor is not None:
1006
+ setattr(klass, attr, descriptor)
1007
+ else:
1008
+ delattr(klass, attr)
1009
+ except Exception: # pragma: no cover - teardown must never raise
1010
+ logger.warning(
1011
+ "failproofai_sdk: failed to restore %s.%s", klass, attr, exc_info=True
1012
+ )
1013
+ self._patched.clear()
1014
+
1015
+ def _wrap_prompt(self, original, *, is_async):
1016
+ adapter = self
1017
+
1018
+ if is_async:
1019
+ async def _failproofai_prompt(*args, **kwargs):
1020
+ record = adapter._human_wait_start(args[0] if args else None)
1021
+ try:
1022
+ answer = await original(*args, **kwargs)
1023
+ except BaseException as exc: # noqa: BLE001 - re-raised below
1024
+ adapter._human_wait_end(record, None, exc)
1025
+ raise
1026
+ adapter._human_wait_end(record, answer, None)
1027
+ return answer
1028
+ else:
1029
+ def _failproofai_prompt(*args, **kwargs):
1030
+ record = adapter._human_wait_start(args[0] if args else None)
1031
+ try:
1032
+ answer = original(*args, **kwargs)
1033
+ except BaseException as exc: # noqa: BLE001 - re-raised below
1034
+ adapter._human_wait_end(record, None, exc)
1035
+ raise
1036
+ adapter._human_wait_end(record, answer, None)
1037
+ return answer
1038
+
1039
+ _failproofai_prompt.__name__ = getattr(original, "__name__", "_prompt_input")
1040
+ _failproofai_prompt.__qualname__ = "failproofai_sdk.crewai.task_human_input"
1041
+ return _failproofai_prompt
1042
+
1043
+ @safe
1044
+ def _human_wait_start(self, crew):
1045
+ tracker = self._tracker
1046
+ if tracker is None:
1047
+ return None
1048
+ pause_id = uuid.uuid4().hex
1049
+ parent = self._current_agent_key()
1050
+ extra = fw_fields(
1051
+ surface="task_human_input",
1052
+ crew_name=getattr(crew, "name", None),
1053
+ )
1054
+ tracker.emit(
1055
+ "human_wait",
1056
+ pause_id,
1057
+ parent_key=parent,
1058
+ input_id=pause_id,
1059
+ prompt=self._HITL_PROMPT,
1060
+ reason=self._HITL_REASON,
1061
+ **extra,
1062
+ )
1063
+ tracker.emit(
1064
+ "agent_pause",
1065
+ pause_id,
1066
+ parent_key=parent,
1067
+ pause_id=pause_id,
1068
+ reason=self._HITL_REASON,
1069
+ **extra,
1070
+ )
1071
+ return (pause_id, parent)
1072
+
1073
+ @safe
1074
+ def _human_wait_end(self, record, answer, error):
1075
+ tracker = self._tracker
1076
+ if tracker is None or record is None:
1077
+ return
1078
+ pause_id, parent = record
1079
+ extra = fw_fields(surface="task_human_input", error=_text(str(error)) if error else None)
1080
+ # agent_resume FIRST, for the same reason as the flow pair above.
1081
+ tracker.emit(
1082
+ "agent_resume",
1083
+ pause_id,
1084
+ parent_key=parent,
1085
+ pause_id=pause_id,
1086
+ reason=self._HITL_REASON,
1087
+ **extra,
1088
+ )
1089
+ tracker.emit(
1090
+ "human_input",
1091
+ pause_id,
1092
+ parent_key=parent,
1093
+ input_id=pause_id,
1094
+ response=_text(answer),
1095
+ **extra,
1096
+ )
1097
+
1098
+ def _current_agent_key(self):
1099
+ """The innermost open agent span — what a blocking prompt sits inside.
1100
+
1101
+ There is no event to read a parent off here, so it comes from the node
1102
+ table. Newest agent wins (a console prompt cannot interleave with
1103
+ another), except that with two crews open on two threads the newest is a
1104
+ coin flip, so an ambient session narrows it the same way
1105
+ `_root_for_current_context` does.
1106
+ """
1107
+ tracker = self._tracker
1108
+ with self._lock:
1109
+ candidates = [key for key, node in self._nodes.items() if node.kind == "agent"]
1110
+ if not candidates:
1111
+ return self._parent_key(None)
1112
+ if len(candidates) == 1 or tracker is None:
1113
+ return candidates[-1]
1114
+ ambient = tracker.identity(None, None, warn=False)
1115
+ if ambient is None:
1116
+ return candidates[-1]
1117
+ for key in reversed(candidates):
1118
+ identity = tracker.identity(key, warn=False)
1119
+ if identity is not None and identity.session_id == ambient.session_id:
1120
+ return key
1121
+ return candidates[-1]
1122
+
1123
+ @safe
1124
+ def on_guardrail_started(self, source, event) -> None:
1125
+ self._hook_start(
1126
+ event,
1127
+ hook_name=getattr(event, "guardrail_name", None) or "guardrail",
1128
+ trigger_event="guardrail",
1129
+ input=None,
1130
+ extra=fw_fields(
1131
+ guardrail_type=getattr(event, "guardrail_type", None),
1132
+ retry_count=getattr(event, "retry_count", None),
1133
+ ),
1134
+ )
1135
+
1136
+ @safe
1137
+ def on_guardrail_completed(self, source, event) -> None:
1138
+ # "rejected" is in the server's failure vocabulary
1139
+ # (error|failed|timeout|rejected), so a tripped guardrail paints red
1140
+ # rather than reading as a successful hook that happened to say no.
1141
+ passed = bool(getattr(event, "success", False))
1142
+ self._hook_end(
1143
+ event,
1144
+ outcome="success" if passed else "rejected",
1145
+ output=_text(getattr(event, "result", None)),
1146
+ error=_text(getattr(event, "error", None)),
1147
+ extra=fw_fields(retry_count=getattr(event, "retry_count", None)),
1148
+ )
1149
+
1150
+ def _hook_start(self, event, *, hook_name, trigger_event, input, extra) -> None:
1151
+ tracker = self._tracker
1152
+ if tracker is None:
1153
+ return
1154
+ parent = self._parent_key(event.parent_event_id)
1155
+ fields = dict(
1156
+ hook_name=hook_name,
1157
+ hook_id=event.event_id,
1158
+ trigger_event=trigger_event,
1159
+ input=input,
1160
+ **extra,
1161
+ )
1162
+ tracker.emit("hook_triggered", event.event_id, parent_key=parent, **fields)
1163
+ # A flow method is a PARENT: a `Crew.kickoff()` inside one arrives with
1164
+ # `parent_event_id` set to this event. Unless the id is a node,
1165
+ # `_parent_key` cannot see it, `on_crew_started` reads "no parent" and
1166
+ # mints a SECOND ROOT — a whole separate session for a crew that ran
1167
+ # inside the flow. See `_tool_start` for the same rule.
1168
+ self._note(event.event_id, parent, "hook")
1169
+ self._open_leaf(
1170
+ "hook_completed",
1171
+ event.event_id,
1172
+ parent,
1173
+ ("hook", hook_name),
1174
+ event.timestamp,
1175
+ {"hook_name": hook_name, "hook_id": event.event_id},
1176
+ )
1177
+
1178
+ def _hook_end(self, event, *, outcome, output=None, error=None, extra=None) -> None:
1179
+ tracker = self._tracker
1180
+ if tracker is None:
1181
+ return
1182
+ leaf = self._pop_leaf(event.started_event_id, ("hook", _hook_name_of(event)))
1183
+ if leaf is None:
1184
+ return
1185
+ with self._lock:
1186
+ self._nodes.pop(leaf.key, None)
1187
+ # duration_ms is auto-computed by the SDK from the hook_triggered we
1188
+ # emitted, and is hard-rejected from callers on hook_completed.
1189
+ tracker.emit(
1190
+ "hook_completed",
1191
+ leaf.key,
1192
+ parent_key=leaf.parent_key,
1193
+ hook_name=leaf.fields["hook_name"],
1194
+ hook_id=leaf.fields["hook_id"],
1195
+ outcome=outcome,
1196
+ output=output,
1197
+ error=error,
1198
+ **(extra or {}),
1199
+ )
1200
+
1201
+ # -- tools --------------------------------------------------------------
1202
+
1203
+ @safe
1204
+ def on_tool_started(self, source, event) -> None:
1205
+ self._tool_start(
1206
+ event,
1207
+ tool_name=getattr(event, "tool_name", None) or "tool",
1208
+ input=_dictify(getattr(event, "tool_args", None)),
1209
+ extra=fw_fields(
1210
+ tool_class=getattr(event, "tool_class", None),
1211
+ run_attempts=getattr(event, "run_attempts", None),
1212
+ agent_role=getattr(event, "agent_role", None),
1213
+ agent_id=getattr(event, "agent_id", None),
1214
+ ),
1215
+ )
1216
+
1217
+ @safe
1218
+ def on_tool_finished(self, source, event) -> None:
1219
+ self._tool_end(
1220
+ event,
1221
+ output=_text(getattr(event, "output", None)),
1222
+ error=None,
1223
+ extra=fw_fields(
1224
+ from_cache=getattr(event, "from_cache", None),
1225
+ run_attempts=getattr(event, "run_attempts", None),
1226
+ ),
1227
+ )
1228
+
1229
+ @safe
1230
+ def on_tool_error(self, source, event) -> None:
1231
+ # A tool failure the agent loop catches and retries is not a run-level
1232
+ # error, so it is reported on the tool_result and nowhere else.
1233
+ self._tool_end(
1234
+ event,
1235
+ output=None,
1236
+ error=_text(getattr(event, "error", None)) or "tool failed",
1237
+ extra=fw_fields(run_attempts=getattr(event, "run_attempts", None)),
1238
+ )
1239
+
1240
+ @safe
1241
+ def on_store_started(self, source, event) -> None:
1242
+ name = STORE_TOOLS.get(type(event).__name__, "memory")
1243
+ self._tool_start(
1244
+ event,
1245
+ tool_name=name,
1246
+ input=_dictify(getattr(event, "query", None) or getattr(event, "value", None)),
1247
+ extra=fw_fields(limit=getattr(event, "limit", None), store=name),
1248
+ )
1249
+
1250
+ @safe
1251
+ def on_store_finished(self, source, event) -> None:
1252
+ results = (
1253
+ getattr(event, "results", None)
1254
+ or getattr(event, "memory_content", None)
1255
+ or getattr(event, "retrieved_knowledge", None)
1256
+ )
1257
+ self._tool_end(
1258
+ event,
1259
+ output=truncate(results),
1260
+ error=None,
1261
+ extra=fw_fields(
1262
+ query_time_ms=getattr(event, "query_time_ms", None),
1263
+ save_time_ms=getattr(event, "save_time_ms", None),
1264
+ retrieval_time_ms=getattr(event, "retrieval_time_ms", None),
1265
+ ),
1266
+ )
1267
+
1268
+ @safe
1269
+ def on_store_failed(self, source, event) -> None:
1270
+ self._tool_end(
1271
+ event,
1272
+ output=None,
1273
+ error=_text(getattr(event, "error", None)) or "store operation failed",
1274
+ extra={},
1275
+ )
1276
+
1277
+ def _tool_start(self, event, *, tool_name, input, extra) -> None:
1278
+ tracker = self._tracker
1279
+ if tracker is None:
1280
+ return
1281
+ parent = self._parent_key(event.parent_event_id)
1282
+ # tool_call_id is CrewAI's own event_id, verbatim: it is a uuid4, so it
1283
+ # cannot collide inside `_pending`, and it lines our rows up with the
1284
+ # framework's own logs.
1285
+ tracker.emit(
1286
+ "tool_use",
1287
+ event.event_id,
1288
+ parent_key=parent,
1289
+ tool_name=tool_name,
1290
+ tool_call_id=event.event_id,
1291
+ input=input,
1292
+ **extra,
1293
+ )
1294
+ # A tool call is a PARENT, not only a leaf. `delegate_work_to_coworker`
1295
+ # and `ask_question_to_coworker` run a whole coworker underneath the
1296
+ # call: verified on crewai 1.15.16, the delegate's
1297
+ # `AgentExecutionStartedEvent.parent_event_id` is exactly this event's id.
1298
+ # Unless that id is a node, `_parent_key` misses it, falls back to the
1299
+ # open root, and every delegated agent is re-parented onto the CREW —
1300
+ # manager and coworker rendered as siblings, which is the whole
1301
+ # hierarchical process flattened into one level.
1302
+ self._note(event.event_id, parent, "tool")
1303
+ self._open_leaf(
1304
+ "tool_result",
1305
+ event.event_id,
1306
+ parent,
1307
+ ("tool", getattr(event, "agent_role", None), tool_name),
1308
+ event.timestamp,
1309
+ {"tool_name": tool_name, "tool_call_id": event.event_id},
1310
+ )
1311
+
1312
+ def _tool_end(self, event, *, output, error, extra) -> None:
1313
+ tracker = self._tracker
1314
+ if tracker is None:
1315
+ return
1316
+ name = getattr(event, "tool_name", None) or STORE_TOOLS.get(
1317
+ type(event).__name__, "tool"
1318
+ )
1319
+ leaf = self._pop_leaf(
1320
+ event.started_event_id, ("tool", getattr(event, "agent_role", None), name)
1321
+ )
1322
+ if leaf is None:
1323
+ return
1324
+ with self._lock:
1325
+ self._nodes.pop(leaf.key, None)
1326
+ # duration_ms is hard-rejected on tool_result and is computed by the SDK
1327
+ # from the tool_use we emitted a moment ago.
1328
+ tracker.emit(
1329
+ "tool_result",
1330
+ leaf.key,
1331
+ parent_key=leaf.parent_key,
1332
+ tool_name=leaf.fields["tool_name"],
1333
+ tool_call_id=leaf.fields["tool_call_id"],
1334
+ output=output,
1335
+ error=error,
1336
+ **extra,
1337
+ )
1338
+
1339
+ # -- model calls --------------------------------------------------------
1340
+
1341
+ @safe
1342
+ def on_llm_started(self, source, event) -> None:
1343
+ tracker = self._tracker
1344
+ if tracker is None:
1345
+ return
1346
+ parent = self._parent_key(event.parent_event_id)
1347
+ call_id = getattr(event, "call_id", None) or event.event_id
1348
+ task = self._task_of(event) or (None, None)
1349
+ tracker.emit(
1350
+ "model_request",
1351
+ event.event_id,
1352
+ parent_key=parent,
1353
+ model=getattr(event, "model", None),
1354
+ messages=_messages(getattr(event, "messages", None)),
1355
+ tools=_listify(getattr(event, "tools", None)),
1356
+ request_id=call_id,
1357
+ **fw_fields(
1358
+ call_id=call_id,
1359
+ temperature=getattr(event, "temperature", None),
1360
+ max_tokens=getattr(event, "max_tokens", None),
1361
+ stream=getattr(event, "stream", None),
1362
+ task_id=task[0],
1363
+ task_name=task[1],
1364
+ ),
1365
+ )
1366
+ self._open_leaf(
1367
+ "model_response",
1368
+ event.event_id,
1369
+ parent,
1370
+ ("llm", call_id),
1371
+ event.timestamp,
1372
+ {"model": getattr(event, "model", None), "request_id": call_id},
1373
+ )
1374
+
1375
+ @safe
1376
+ def on_llm_completed(self, source, event) -> None:
1377
+ usage = getattr(event, "usage", None)
1378
+ input_tokens, output_tokens, normalized = _tokens(usage)
1379
+ self._model_end(
1380
+ event,
1381
+ content=_text(getattr(event, "response", None)),
1382
+ stop_reason=getattr(event, "finish_reason", None),
1383
+ error=None,
1384
+ extra=dict(
1385
+ input_tokens=input_tokens,
1386
+ output_tokens=output_tokens,
1387
+ usage=normalized or None,
1388
+ **fw_fields(
1389
+ usage_raw=usage if isinstance(usage, dict) else None,
1390
+ response_id=getattr(event, "response_id", None),
1391
+ call_type=_enum_value(getattr(event, "call_type", None)),
1392
+ ),
1393
+ ),
1394
+ )
1395
+
1396
+ @safe
1397
+ def on_llm_failed(self, source, event) -> None:
1398
+ # Reported on the model_response, which is why `error` was added to that
1399
+ # event: a failed LLM call otherwise paints the row red but not the span.
1400
+ self._model_end(
1401
+ event,
1402
+ content=None,
1403
+ stop_reason="error",
1404
+ error=_text(getattr(event, "error", None)) or "llm call failed",
1405
+ extra={},
1406
+ )
1407
+
1408
+ def _model_end(self, event, *, content, stop_reason, error, extra) -> None:
1409
+ tracker = self._tracker
1410
+ if tracker is None:
1411
+ return
1412
+ call_id = getattr(event, "call_id", None)
1413
+ leaf = self._pop_leaf(event.started_event_id, ("llm", call_id))
1414
+ if leaf is None:
1415
+ return
1416
+ stream = self._pop_stream(call_id)
1417
+ fields = dict(extra)
1418
+ if stream is not None:
1419
+ chunks, first_ts = stream
1420
+ fields.update(
1421
+ fw_fields(
1422
+ streamed=True,
1423
+ chunks=chunks,
1424
+ ttft_ms=ms(first_ts - leaf.started) if first_ts is not None else None,
1425
+ )
1426
+ )
1427
+ # duration_ms is NOT guarded on model_response, and `durationOf` prefers
1428
+ # the closing event's value over end-start — which is what keeps model
1429
+ # durations honest even when the dashboard's FIFO pairing brackets the
1430
+ # wrong pair. It must be an int: the server's JSON parser drops floats
1431
+ # and would silently NULL the column.
1432
+ tracker.emit(
1433
+ "model_response",
1434
+ leaf.key,
1435
+ parent_key=leaf.parent_key,
1436
+ model=getattr(event, "model", None) or leaf.fields["model"],
1437
+ request_id=leaf.fields["request_id"],
1438
+ content=content,
1439
+ stop_reason=stop_reason,
1440
+ error=error,
1441
+ duration_ms=ms(event.timestamp - leaf.started),
1442
+ **fields,
1443
+ )
1444
+
1445
+ @safe
1446
+ def on_llm_chunk(self, source, event) -> None:
1447
+ """Emits nothing. Counts chunks and stamps time-to-first-token, once."""
1448
+ key = getattr(event, "call_id", None) or getattr(event, "parent_event_id", None)
1449
+ if key is None:
1450
+ return
1451
+ with self._lock:
1452
+ entry = self._streams.get(key)
1453
+ if entry is None:
1454
+ while len(self._streams) >= _MAX_STREAMS:
1455
+ self._streams.pop(next(iter(self._streams)), None)
1456
+ self._streams[key] = [1, event.timestamp]
1457
+ else:
1458
+ entry[0] += 1
1459
+
1460
+ def _pop_stream(self, call_id):
1461
+ if call_id is None:
1462
+ return None
1463
+ with self._lock:
1464
+ entry = self._streams.pop(call_id, None)
1465
+ return (entry[0], entry[1]) if entry else None
1466
+
1467
+
1468
+ # ---------------------------------------------------------------------------
1469
+ # The listener
1470
+ # ---------------------------------------------------------------------------
1471
+
1472
+ def event_class(name: str):
1473
+ """The event class `name`, or None.
1474
+
1475
+ TWO namespaces, because crewai has two and neither is complete.
1476
+ `crewai.events.event_types` is the flat re-export the bulk of the table
1477
+ resolves against, but the flow events — `HumanFeedbackRequestedEvent` among
1478
+ them — are NOT in it; they are reachable only through `crewai.events`, whose
1479
+ module-level `__getattr__` imports them lazily from
1480
+ `crewai.events.types.flow_events`.
1481
+
1482
+ Resolving against `event_types` alone is not an error you would see: the
1483
+ lookup returns None, `probe()` disables that one hook, and the adapter
1484
+ carries on recording everything else. The HITL events were mapped and
1485
+ silently never registered until this existed. `getattr`, never `dir()` —
1486
+ a lazy re-export does not appear in `dir()`.
1487
+ """
1488
+ return getattr(_ct, name, None) or getattr(_ce, name, None)
1489
+
1490
+
1491
+ class FailproofAICrewListener(BaseEventListener):
1492
+ """Registers one handler per event class on the module-level bus.
1493
+
1494
+ `BaseEventListener.__init__` is what performs the registration (it calls
1495
+ `setup_listeners` with the `crewai_event_bus` singleton), so constructing
1496
+ the instance *is* the install step — and the instance has to be kept alive
1497
+ by the adapter or it is garbage collected and silently stops working.
1498
+
1499
+ Every handler is `async def` for the ordering reason in the module
1500
+ docstring, and does nothing but call a `safe`-wrapped translator, so a bug
1501
+ in this file can never take down the customer's crew.
1502
+ """
1503
+
1504
+ def __init__(self, adapter: _CrewAIAdapter) -> None:
1505
+ self._adapter = adapter
1506
+ self._registered: list[tuple[type, Any]] = []
1507
+ super().__init__()
1508
+
1509
+ def setup_listeners(self, crewai_event_bus) -> None:
1510
+ for class_name, method_name in TABLE:
1511
+ klass = event_class(class_name)
1512
+ if klass is None:
1513
+ # Tier 3: one missing event class disables one hook, never the
1514
+ # whole adapter — the other 90% of the events are still correct.
1515
+ _compat.probe(NAME, class_name, lambda: False)
1516
+ continue
1517
+ handler = self._make_handler(getattr(self._adapter, method_name), method_name)
1518
+ crewai_event_bus.register_handler(klass, handler)
1519
+ self._registered.append((klass, handler))
1520
+
1521
+ @staticmethod
1522
+ def _make_handler(translator, method_name):
1523
+ async def _failproofai_handler(source, event):
1524
+ translator(source, event)
1525
+
1526
+ _failproofai_handler.__name__ = f"failproofai_{method_name}"
1527
+ _failproofai_handler.__qualname__ = f"failproofai_sdk.crewai.{method_name}"
1528
+ return _failproofai_handler
1529
+
1530
+ def teardown(self) -> None:
1531
+ """`crewai_event_bus.off()` for everything `setup_listeners` added."""
1532
+ for event_class, handler in self._registered:
1533
+ try:
1534
+ crewai_event_bus.off(event_class, handler)
1535
+ except Exception: # pragma: no cover - teardown must never raise
1536
+ logger.warning(
1537
+ "failproofai_sdk: could not unregister the crewai handler for %s",
1538
+ getattr(event_class, "__name__", event_class),
1539
+ exc_info=True,
1540
+ )
1541
+ self._registered.clear()
1542
+
1543
+ def handlers(self) -> tuple[tuple[type, Any], ...]:
1544
+ """(event class, handler) pairs currently registered. For tests."""
1545
+ return tuple(self._registered)
1546
+
1547
+
1548
+ # ---------------------------------------------------------------------------
1549
+ # Payload helpers
1550
+ # ---------------------------------------------------------------------------
1551
+
1552
+ def _text(value) -> "str | None":
1553
+ if value is None:
1554
+ return None
1555
+ if isinstance(value, str):
1556
+ return truncate(value)
1557
+ raw = getattr(value, "raw", None)
1558
+ return truncate(raw if isinstance(raw, str) else str(value))
1559
+
1560
+
1561
+ def _first_text(mapping) -> "str | None":
1562
+ """A goal string for a crew or flow, from whatever inputs it was given."""
1563
+ if isinstance(mapping, dict):
1564
+ for value in mapping.values():
1565
+ if isinstance(value, str) and value.strip():
1566
+ return truncate(value)
1567
+ return None
1568
+
1569
+
1570
+ def _dictify(value) -> "dict | None":
1571
+ """`input=` is declared `dict | None`; a bare string would break the shape.
1572
+
1573
+ CrewAI hands `tool_args` over as the raw JSON string the model produced, so
1574
+ it is decoded when it decodes: an object in the payload is queryable with
1575
+ `payload_key_expr`, a JSON-string-inside-a-string is not.
1576
+ """
1577
+ if value is None:
1578
+ return None
1579
+ if isinstance(value, dict):
1580
+ return truncate(value)
1581
+ if isinstance(value, str):
1582
+ stripped = value.strip()
1583
+ if stripped.startswith("{"):
1584
+ try:
1585
+ decoded = json.loads(stripped)
1586
+ except ValueError:
1587
+ decoded = None
1588
+ if isinstance(decoded, dict):
1589
+ return truncate(decoded)
1590
+ return {"input": truncate(value)}
1591
+ return {"input": truncate(str(value))}
1592
+
1593
+
1594
+ def _listify(value) -> "list | None":
1595
+ if value is None:
1596
+ return None
1597
+ return truncate(list(value) if isinstance(value, (list, tuple)) else [value])
1598
+
1599
+
1600
+ def _messages(value) -> "list | None":
1601
+ """`messages=` is declared `list[dict]`; CrewAI also allows a bare string."""
1602
+ if value is None:
1603
+ return None
1604
+ if isinstance(value, str):
1605
+ return [{"role": "user", "content": truncate(value)}]
1606
+ return _listify(value)
1607
+
1608
+
1609
+ def _tool_names(tools) -> "list | None":
1610
+ if not tools:
1611
+ return None
1612
+ return [getattr(tool, "name", None) or type(tool).__name__ for tool in tools]
1613
+
1614
+
1615
+ def _enum_value(value):
1616
+ return getattr(value, "value", value) if value is not None else None
1617
+
1618
+
1619
+ def _hook_name_of(event) -> "str | None":
1620
+ return (
1621
+ getattr(event, "method_name", None)
1622
+ or getattr(event, "guardrail_name", None)
1623
+ or "guardrail"
1624
+ )
1625
+
1626
+
1627
+ # The usage dict is whatever the provider returned, so both spellings are in the
1628
+ # wild and neither is guaranteed. Reading only one of them reports zero tokens
1629
+ # for half the providers, at HTTP 200, forever.
1630
+ _INPUT_KEYS = ("prompt_tokens", "input_tokens", "promptTokens", "inputTokens")
1631
+ _OUTPUT_KEYS = ("completion_tokens", "output_tokens", "completionTokens", "outputTokens")
1632
+ _TOTAL_KEYS = ("total_tokens", "totalTokens")
1633
+
1634
+
1635
+ def _tokens(usage):
1636
+ """(input_tokens, output_tokens, normalized_usage_dict)."""
1637
+ if not isinstance(usage, dict):
1638
+ return None, None, {}
1639
+ input_tokens = _count(usage, _INPUT_KEYS)
1640
+ output_tokens = _count(usage, _OUTPUT_KEYS)
1641
+ total = _count(usage, _TOTAL_KEYS)
1642
+ if total is None and (input_tokens is not None or output_tokens is not None):
1643
+ total = (input_tokens or 0) + (output_tokens or 0)
1644
+ normalized = {}
1645
+ for key, value in (
1646
+ ("input_tokens", input_tokens),
1647
+ ("output_tokens", output_tokens),
1648
+ ("total_tokens", total),
1649
+ ):
1650
+ if value is not None:
1651
+ normalized[key] = value
1652
+ return input_tokens, output_tokens, normalized
1653
+
1654
+
1655
+ def _count(usage, keys):
1656
+ for key in keys:
1657
+ value = usage.get(key)
1658
+ if isinstance(value, bool):
1659
+ continue
1660
+ if isinstance(value, int):
1661
+ return value
1662
+ if isinstance(value, float):
1663
+ return int(value)
1664
+ if isinstance(value, str) and value.isdigit():
1665
+ return int(value)
1666
+ return None
1667
+
1668
+
1669
+ adapter = _CrewAIAdapter()