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,1576 @@
1
+ """The LlamaIndex adapter, against the real framework and a stub model.
2
+
3
+ No network, no API key, no mock of our own code: every assertion below reads
4
+ the **JSONL the writer actually produced** after driving a real
5
+ `FunctionAgent` / `Workflow`. Asserting on mock call args would happily pass
6
+ against an adapter that emits nothing the ingest pipeline can use.
7
+
8
+ The single highest-value test in the file is
9
+ `test_our_overrides_still_exist_on_the_framework_base_classes`. Every other
10
+ test here can stay green while the adapter is completely dead: if upstream
11
+ renames `prepare_to_exit_span`, our override is never called, the span handler
12
+ records nothing, and a fake-based suite notices nothing at all.
13
+ """
14
+
15
+ import asyncio
16
+ import inspect
17
+ import json
18
+ import os
19
+ from dataclasses import fields as dataclass_fields
20
+ from typing import Any, List, Sequence
21
+
22
+ import pytest
23
+
24
+ import failproofai_sdk
25
+ from failproofai_sdk import _runtime, _schema
26
+ from failproofai_sdk.integrations import _compat, _core
27
+
28
+ # `pytest.importorskip` is fail-open: misspell the module and every leg skips
29
+ # while CI stays green having tested nothing. The framework CI leg sets
30
+ # AGENTEYE_TESTS_REQUIRE_FRAMEWORKS=1, which turns the skip into a hard error.
31
+ if os.environ.get("AGENTEYE_TESTS_REQUIRE_FRAMEWORKS", "").strip().lower() in {"1", "true", "yes"}:
32
+ import llama_index.core # noqa: F401
33
+ else:
34
+ pytest.importorskip("llama_index.core", reason="llama-index-core is not installed")
35
+
36
+ from llama_index.core.base.llms.types import ( # noqa: E402
37
+ ChatMessage,
38
+ ChatResponse,
39
+ ChatResponseAsyncGen,
40
+ ChatResponseGen,
41
+ CompletionResponse,
42
+ CompletionResponseAsyncGen,
43
+ CompletionResponseGen,
44
+ LLMMetadata,
45
+ MessageRole,
46
+ )
47
+ from llama_index.core.llms.callbacks import llm_chat_callback, llm_completion_callback # noqa: E402
48
+ from llama_index.core.llms.function_calling import FunctionCallingLLM # noqa: E402
49
+ from llama_index.core.tools import ToolSelection # noqa: E402
50
+ from llama_index.core.tools.types import BaseTool # noqa: E402
51
+ from pydantic import Field, PrivateAttr # noqa: E402
52
+
53
+ from failproofai_sdk.integrations import llama_index as adapter_module # noqa: E402
54
+
55
+ pytestmark = pytest.mark.framework
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # A stub function-calling model. Real FunctionAgent, fake LLM.
60
+ # ---------------------------------------------------------------------------
61
+
62
+ class StubLLM(FunctionCallingLLM):
63
+ """Replays a script of tool calls, then answers. Never touches a network."""
64
+
65
+ model: str = "stub-model-1"
66
+ script: List[Any] = Field(default_factory=list)
67
+ final: str = "done"
68
+ _turn: int = PrivateAttr(default=0)
69
+
70
+ @property
71
+ def metadata(self) -> LLMMetadata:
72
+ return LLMMetadata(
73
+ model_name=self.model, is_chat_model=True, is_function_calling_model=True
74
+ )
75
+
76
+ def _next(self) -> ChatResponse:
77
+ turn = self._turn
78
+ self._turn += 1
79
+ if turn < len(self.script):
80
+ name, kwargs = self.script[turn]
81
+ message = ChatMessage(
82
+ role=MessageRole.ASSISTANT,
83
+ content="",
84
+ additional_kwargs={
85
+ "tool_calls": [{"id": f"call_{turn}", "name": name, "kwargs": kwargs}]
86
+ },
87
+ )
88
+ else:
89
+ message = ChatMessage(role=MessageRole.ASSISTANT, content=self.final)
90
+ return ChatResponse(
91
+ message=message,
92
+ raw={"usage": {"prompt_tokens": 11, "completion_tokens": 7, "total_tokens": 18}},
93
+ )
94
+
95
+ @llm_chat_callback()
96
+ def chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatResponse:
97
+ return self._next()
98
+
99
+ @llm_chat_callback()
100
+ async def achat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatResponse:
101
+ return self._next()
102
+
103
+ @llm_chat_callback()
104
+ def stream_chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatResponseGen:
105
+ def gen():
106
+ yield self._next()
107
+
108
+ return gen()
109
+
110
+ @llm_chat_callback()
111
+ async def astream_chat(
112
+ self, messages: Sequence[ChatMessage], **kwargs: Any
113
+ ) -> ChatResponseAsyncGen:
114
+ async def gen():
115
+ yield self._next()
116
+
117
+ return gen()
118
+
119
+ @llm_completion_callback()
120
+ def complete(self, prompt: str, formatted: bool = False, **kwargs: Any) -> CompletionResponse:
121
+ return CompletionResponse(text=self.final)
122
+
123
+ @llm_completion_callback()
124
+ async def acomplete(
125
+ self, prompt: str, formatted: bool = False, **kwargs: Any
126
+ ) -> CompletionResponse:
127
+ return CompletionResponse(text=self.final)
128
+
129
+ @llm_completion_callback()
130
+ def stream_complete(
131
+ self, prompt: str, formatted: bool = False, **kwargs: Any
132
+ ) -> CompletionResponseGen:
133
+ def gen():
134
+ yield CompletionResponse(text=self.final, delta=self.final)
135
+
136
+ return gen()
137
+
138
+ @llm_completion_callback()
139
+ async def astream_complete(
140
+ self, prompt: str, formatted: bool = False, **kwargs: Any
141
+ ) -> CompletionResponseAsyncGen:
142
+ async def gen():
143
+ yield CompletionResponse(text=self.final, delta=self.final)
144
+
145
+ return gen()
146
+
147
+ def _prepare_chat_with_tools(
148
+ self,
149
+ tools: Sequence["BaseTool"],
150
+ user_msg: Any = None,
151
+ chat_history: Any = None,
152
+ verbose: bool = False,
153
+ allow_parallel_tool_calls: bool = False,
154
+ tool_required: bool = False,
155
+ **kwargs: Any,
156
+ ) -> dict:
157
+ messages = list(chat_history or [])
158
+ if user_msg is not None:
159
+ messages.append(
160
+ ChatMessage(role=MessageRole.USER, content=user_msg)
161
+ if isinstance(user_msg, str)
162
+ else user_msg
163
+ )
164
+ return {"messages": messages, **kwargs}
165
+
166
+ def get_tool_calls_from_response(
167
+ self, response: ChatResponse, error_on_no_tool_call: bool = True, **kwargs: Any
168
+ ) -> List[ToolSelection]:
169
+ calls = response.message.additional_kwargs.get("tool_calls", [])
170
+ if not calls and error_on_no_tool_call:
171
+ raise ValueError("no tool calls")
172
+ return [
173
+ ToolSelection(tool_id=c["id"], tool_name=c["name"], tool_kwargs=c["kwargs"])
174
+ for c in calls
175
+ ]
176
+
177
+
178
+ def add(a: int, b: int) -> int:
179
+ """Add two numbers."""
180
+ return a + b
181
+
182
+
183
+ def boom(x: int) -> int:
184
+ """Always raises."""
185
+ raise RuntimeError("tool exploded")
186
+
187
+
188
+ async def _await(awaitable):
189
+ return await awaitable
190
+
191
+
192
+ def drive(coro_factory):
193
+ """Run a workflow from sync test code.
194
+
195
+ `Workflow.run()` schedules tasks eagerly, so it must be *called* with a
196
+ loop already running — `asyncio.run(workflow.run())` raises
197
+ "no running event loop" before the adapter ever sees anything.
198
+ """
199
+
200
+ async def _main():
201
+ return await coro_factory()
202
+
203
+ return asyncio.run(_main())
204
+
205
+
206
+ def run_agent(agent, prompt: str) -> str:
207
+ return str(drive(lambda: agent.run(prompt)))
208
+
209
+
210
+ def run_workflow(workflow, **kwargs) -> str:
211
+ return str(drive(lambda: workflow.run(**kwargs)))
212
+
213
+
214
+ def calculator(llm, **kwargs):
215
+ from llama_index.core.agent.workflow import FunctionAgent
216
+
217
+ kwargs.setdefault("tools", [add])
218
+ kwargs.setdefault("streaming", False)
219
+ return FunctionAgent(name="calc", description="does math", llm=llm, **kwargs)
220
+
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # Fixtures
224
+ # ---------------------------------------------------------------------------
225
+
226
+ @pytest.fixture()
227
+ def instrumented(tmp_path):
228
+ """Instrument for one test, then put everything back.
229
+
230
+ The flush interval is enormous on purpose: the writer names files with
231
+ millisecond resolution, so two automatic flushes inside the same
232
+ millisecond overwrite each other. Tests flush once, explicitly, at the end.
233
+ """
234
+ _core.set_strict(False)
235
+ _compat.set_strict_integrations(False)
236
+ _core.reset_failures()
237
+ _runtime.writer.set_flush_interval(3600)
238
+ assert failproofai_sdk.instrument("llama_index") == ("llama_index",)
239
+ try:
240
+ yield adapter_module.adapter
241
+ finally:
242
+ failproofai_sdk.uninstrument("llama_index")
243
+ _core.set_strict(None)
244
+ _compat.set_strict_integrations(None)
245
+ _core.reset_failures()
246
+
247
+
248
+ @pytest.fixture()
249
+ def instrumented_without_steps(tmp_path):
250
+ """`instrumented`, with the workflow-step hooks switched off.
251
+
252
+ Spelled out rather than parameterising the fixture above: `instrumented` is
253
+ used by nearly every test in this file, and a signature change there is a
254
+ change to all of them.
255
+ """
256
+ _core.set_strict(False)
257
+ _compat.set_strict_integrations(False)
258
+ _core.reset_failures()
259
+ _runtime.writer.set_flush_interval(3600)
260
+ assert failproofai_sdk.instrument("llama_index", steps=False) == ("llama_index",)
261
+ try:
262
+ yield adapter_module.adapter
263
+ finally:
264
+ failproofai_sdk.uninstrument("llama_index")
265
+ _core.set_strict(None)
266
+ _compat.set_strict_integrations(None)
267
+ _core.reset_failures()
268
+
269
+
270
+ @pytest.fixture(autouse=True)
271
+ def _isolate_writer_queue():
272
+ """Start and end every test with an empty writer queue.
273
+
274
+ The queue is process-global while `base_dir` is per-test, so events a test
275
+ never flushed are written into the NEXT test's directory the first time
276
+ anything flushes — which reads as a mystery second session id and is
277
+ exactly the kind of cross-test bleed this suite exists to rule out.
278
+ """
279
+ _runtime.writer._queue.clear()
280
+ yield
281
+ _runtime.writer._queue.clear()
282
+
283
+
284
+ @pytest.fixture(autouse=True)
285
+ def _no_silent_adapter_failure(request):
286
+ """Fail the test if the adapter swallowed an exception.
287
+
288
+ `safe()` exists so a bug costs one log line instead of the process — which
289
+ also means a broken adapter passes every behavioural test that only checks
290
+ "the run finished". This makes that invisible failure visible in the suite.
291
+ """
292
+ yield
293
+ if _EXPECTS_FAILURE in request.node.name:
294
+ return
295
+ assert not _core._disabled, f"adapter self-disabled a call site: {_core._disabled}"
296
+ assert not _core._failures, f"adapter swallowed exceptions: {_core._failures}"
297
+
298
+
299
+ # pyproject sets `--strict-markers`, so a bespoke marker is not available here:
300
+ # the two tests that deliberately make the adapter fail are named instead.
301
+ _EXPECTS_FAILURE = "raises_on_every_call"
302
+
303
+
304
+ def read_events(tmp_path) -> list[dict]:
305
+ _runtime.writer.flush_now()
306
+ rows: list[dict] = []
307
+ for path in sorted((tmp_path / "events").glob("*.jsonl")):
308
+ rows.extend(json.loads(line) for line in path.read_text().splitlines() if line)
309
+ return rows
310
+
311
+
312
+ def types_of(events: list[dict]) -> list[str]:
313
+ return [event["type"] for event in events]
314
+
315
+
316
+ def only(events: list[dict], kind: str) -> list[dict]:
317
+ return [event for event in events if event["type"] == kind]
318
+
319
+
320
+ # The exact sequence a one-tool FunctionAgent turn produces. Written out rather
321
+ # than computed: a change here should require a human to look at it.
322
+ ONE_TOOL_SEQUENCE = [
323
+ "agent_start",
324
+ "hook_triggered", # init_run
325
+ "hook_completed",
326
+ "hook_triggered", # setup_agent
327
+ "hook_completed",
328
+ "hook_triggered", # run_agent_step
329
+ "model_request",
330
+ "model_response",
331
+ "hook_completed",
332
+ "hook_triggered", # parse_agent_output
333
+ "hook_completed",
334
+ "hook_triggered", # call_tool
335
+ "tool_use",
336
+ "tool_result",
337
+ "hook_completed",
338
+ "hook_triggered", # aggregate_tool_results
339
+ "hook_completed",
340
+ "hook_triggered", # setup_agent
341
+ "hook_completed",
342
+ "hook_triggered", # run_agent_step
343
+ "model_request",
344
+ "model_response",
345
+ "hook_completed",
346
+ "hook_triggered", # parse_agent_output
347
+ "hook_completed",
348
+ "agent_end",
349
+ ]
350
+
351
+
352
+ # ---------------------------------------------------------------------------
353
+ # The representative run
354
+ # ---------------------------------------------------------------------------
355
+
356
+ @pytest.mark.parametrize("streaming", [False, True], ids=["blocking", "streaming"])
357
+ def test_exact_event_sequence_for_a_one_tool_run(instrumented, tmp_path, streaming):
358
+ llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5")
359
+ agent = calculator(llm, streaming=streaming)
360
+ assert str(run_agent(agent, "what is 2+3?")) == "5"
361
+
362
+ events = read_events(tmp_path)
363
+ # Streaming produces the same shape: the model span exits the instant the
364
+ # generator is created, so the response is parked and closed by
365
+ # LLMChatEndEvent rather than by the span.
366
+ assert types_of(events) == ONE_TOOL_SEQUENCE
367
+
368
+
369
+ def test_the_root_agent_start_is_the_sessions_first_event(instrumented, tmp_path):
370
+ llm = StubLLM(script=[("add", {"a": 1, "b": 1})], final="2")
371
+ run_agent(calculator(llm), "1+1?")
372
+
373
+ events = read_events(tmp_path)
374
+ # `agent_sessions.agent_id = any(...)` over an ORDER BY (session_id, ts)
375
+ # table returns the FIRST agent_id by time. If anything preceded the root
376
+ # agent_start the sessions list would name a workflow step instead.
377
+ assert events[0]["type"] == "agent_start"
378
+ assert events[0]["agent_id"] == "calc"
379
+ assert {event["session_id"] for event in events} == {events[0]["session_id"]}
380
+
381
+
382
+ def test_every_event_carries_the_framework_triple(instrumented, tmp_path):
383
+ llm = StubLLM(script=[("add", {"a": 1, "b": 1})], final="2")
384
+ run_agent(calculator(llm), "1+1?")
385
+
386
+ for event in read_events(tmp_path):
387
+ assert event["framework"] == "llama_index"
388
+ assert event["framework_version"]
389
+ assert event["integration_version"]
390
+
391
+
392
+ def test_agent_ids_are_readable_names_not_span_ids(instrumented, tmp_path):
393
+ llm = StubLLM(script=[("add", {"a": 1, "b": 1})], final="2")
394
+ run_agent(calculator(llm), "1+1?")
395
+
396
+ for event in read_events(tmp_path):
397
+ agent_id = event["agent_id"]
398
+ # agent_id is LowCardinality(String) and the global dashboard facet; a
399
+ # span id in it fills the filter dropdown with one entry per run.
400
+ assert agent_id == "calc"
401
+ assert "-" not in agent_id and len(agent_id) < 32
402
+
403
+
404
+ def test_model_requests_and_responses_pair_on_request_id(instrumented, tmp_path):
405
+ llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5")
406
+ run_agent(calculator(llm), "2+3?")
407
+
408
+ events = read_events(tmp_path)
409
+ requests = only(events, "model_request")
410
+ responses = only(events, "model_response")
411
+ assert len(requests) == len(responses) == 2
412
+ assert [r["request_id"] for r in requests] == [r["request_id"] for r in responses]
413
+ assert len({r["request_id"] for r in requests}) == 2
414
+
415
+ for response in responses:
416
+ # Invariant 3: an int, always. The server stores duration_ms as a u32
417
+ # and its JSON parser drops floats, so a float silently NULLs it.
418
+ assert isinstance(response["duration_ms"], int)
419
+ assert not isinstance(response["duration_ms"], bool)
420
+ assert response["model"] == "stub-model-1"
421
+
422
+
423
+ def test_model_name_comes_from_metadata_not_the_gutted_model_dict(instrumented, tmp_path):
424
+ llm = StubLLM(script=[], final="hi")
425
+ run_agent(calculator(llm), "hi")
426
+
427
+ events = read_events(tmp_path)
428
+ assert {e["model"] for e in only(events, "model_request")} == {"stub-model-1"}
429
+ # `to_payload()` replaced `to_dict()` in 0.14.23 and the "model" key is gone.
430
+ start_payload = llm.to_payload()
431
+ assert "model" not in start_payload
432
+
433
+
434
+ def test_tokens_are_extracted_and_the_raw_usage_dict_ships_too(instrumented, tmp_path):
435
+ llm = StubLLM(script=[], final="hi")
436
+ run_agent(calculator(llm), "hi")
437
+
438
+ response = only(read_events(tmp_path), "model_response")[0]
439
+ assert response["input_tokens"] == 11
440
+ assert response["output_tokens"] == 7
441
+ # Both event_summary.rs and sessionSummary.ts fall back to `usage`.
442
+ assert response["usage"]["total_tokens"] == 18
443
+
444
+
445
+ def test_tool_use_and_tool_result_pair_and_carry_a_duration(instrumented, tmp_path):
446
+ llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5")
447
+ run_agent(calculator(llm), "2+3?")
448
+
449
+ events = read_events(tmp_path)
450
+ uses = only(events, "tool_use")
451
+ results = only(events, "tool_result")
452
+ assert len(uses) == len(results) == 1
453
+ assert uses[0]["tool_call_id"] == results[0]["tool_call_id"] == "call_0"
454
+ assert uses[0]["tool_name"] == results[0]["tool_name"] == "add"
455
+ assert uses[0]["input"] == {"a": 2, "b": 3}
456
+ assert results[0]["output"] == "5"
457
+ assert isinstance(results[0]["duration_ms"], int)
458
+
459
+
460
+ def test_tool_call_id_is_the_frameworks_own_id(instrumented, tmp_path):
461
+ llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5")
462
+ run_agent(calculator(llm), "2+3?")
463
+
464
+ use = only(read_events(tmp_path), "tool_use")[0]
465
+ # Passing the framework's id through verbatim is what makes our events line
466
+ # up with the customer's provider logs.
467
+ assert use["tool_call_id"] == "call_0"
468
+ assert use["fw_tool_id"] == "call_0"
469
+
470
+
471
+ def test_workflow_steps_become_hooks_not_agents(instrumented, tmp_path):
472
+ llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5")
473
+ run_agent(calculator(llm), "2+3?")
474
+
475
+ events = read_events(tmp_path)
476
+ hooks = only(events, "hook_triggered")
477
+ names = [hook["hook_name"] for hook in hooks]
478
+ assert names[:3] == ["init_run", "setup_agent", "run_agent_step"]
479
+ assert all(hook["trigger_event"] == "workflow_step" for hook in hooks)
480
+ # Steps as agents would drown the agent_id facet with `parse_agent_output`.
481
+ assert {e["agent_id"] for e in events} == {"calc"}
482
+ for completed in only(events, "hook_completed"):
483
+ assert isinstance(completed["duration_ms"], int)
484
+
485
+
486
+ # ---------------------------------------------------------------------------
487
+ # Rendering invariants
488
+ # ---------------------------------------------------------------------------
489
+
490
+ def assert_rendering_invariants(events: list[dict]) -> None:
491
+ """The four properties that make the dashboard draw the session correctly."""
492
+ assert events, "no events at all"
493
+ assert events[0]["type"] == "agent_start", "invariant 2: root agent_start must be first"
494
+
495
+ open_agents: dict[str, int] = {}
496
+ open_leaves: dict[tuple[str, str], int] = {}
497
+ for event in events:
498
+ kind = event["type"]
499
+ agent_id = event["agent_id"]
500
+ if kind == "agent_start":
501
+ open_agents[agent_id] = open_agents.get(agent_id, 0) + 1
502
+ else:
503
+ # Invariant 1: an event under an agent_id with no open agent_start
504
+ # makes executionGraph synthesize a root span that stays `ongoing`.
505
+ assert open_agents.get(agent_id, 0) > 0, f"{kind} under a closed agent {agent_id!r}"
506
+ if kind == "agent_end":
507
+ open_agents[agent_id] -= 1
508
+ elif kind == "tool_use":
509
+ open_leaves[("tool", event["tool_call_id"])] = 1
510
+ elif kind == "tool_result":
511
+ tool_leaf = open_leaves.pop(("tool", event["tool_call_id"]), None)
512
+ assert tool_leaf, "unpaired tool_result"
513
+ elif kind == "model_request":
514
+ open_leaves[("model", event["request_id"])] = 1
515
+ elif kind == "model_response":
516
+ model_leaf = open_leaves.pop(("model", event["request_id"]), None)
517
+ assert model_leaf, "unpaired model_response"
518
+ assert isinstance(event["duration_ms"], int), "invariant 3"
519
+ elif kind == "hook_triggered":
520
+ open_leaves[("hook", event["hook_id"])] = 1
521
+ elif kind == "hook_completed":
522
+ hook_leaf = open_leaves.pop(("hook", event["hook_id"]), None)
523
+ assert hook_leaf, "unpaired hook_completed"
524
+
525
+ assert not any(open_agents.values()), f"agents left open: {open_agents}"
526
+ # Invariant 4: agent_end force-closes open pauses but NOT tools or models,
527
+ # so a leaf left open renders the session `ongoing` forever.
528
+ assert not open_leaves, f"leaves left open: {sorted(open_leaves)}"
529
+
530
+
531
+ @pytest.mark.parametrize("streaming", [False, True], ids=["blocking", "streaming"])
532
+ def test_rendering_invariants_hold_for_a_normal_run(instrumented, tmp_path, streaming):
533
+ llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5")
534
+ run_agent(calculator(llm, streaming=streaming), "2+3?")
535
+ assert_rendering_invariants(read_events(tmp_path))
536
+
537
+
538
+ def test_rendering_invariants_hold_when_a_tool_fails(instrumented, tmp_path):
539
+ llm = StubLLM(script=[("boom", {"x": 1})], final="recovered")
540
+ agent = calculator(llm, tools=[boom])
541
+ run_agent(agent, "blow up")
542
+ assert_rendering_invariants(read_events(tmp_path))
543
+
544
+
545
+ # ---------------------------------------------------------------------------
546
+ # Failure paths
547
+ # ---------------------------------------------------------------------------
548
+
549
+ def test_a_failing_tool_is_reported_on_the_tool_and_not_double_counted(instrumented, tmp_path):
550
+ llm = StubLLM(script=[("boom", {"x": 1})], final="recovered")
551
+ result = run_agent(calculator(llm, tools=[boom]), "blow up")
552
+ assert str(result) == "recovered"
553
+
554
+ events = read_events(tmp_path)
555
+ result_event = only(events, "tool_result")[0]
556
+ assert result_event["error"] == "RuntimeError: tool exploded"
557
+ # The span that owns the failure reports it. A standalone `error` event
558
+ # would double-count on sessionSummary.errorCount.
559
+ assert only(events, "error") == []
560
+ # The agent recovered, so the run did not fail.
561
+ assert only(events, "agent_end")[0]["outcome"] == "success"
562
+
563
+
564
+ def test_a_failing_workflow_step_reports_once_and_fails_the_agent(instrumented, tmp_path):
565
+ from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step
566
+
567
+ class Bad(Workflow):
568
+ @step
569
+ async def go(self, ev: StartEvent) -> StopEvent:
570
+ raise ValueError("workflow blew up")
571
+
572
+ with pytest.raises(ValueError):
573
+ run_workflow(Bad(timeout=5))
574
+
575
+ events = read_events(tmp_path)
576
+ completed = only(events, "hook_completed")[0]
577
+ assert completed["outcome"] == "failed"
578
+ assert completed["error"] == "ValueError: workflow blew up"
579
+ assert only(events, "error") == [], "the step owns this failure; do not count it twice"
580
+ assert only(events, "agent_end")[0]["outcome"] == "failed"
581
+ assert_rendering_invariants(events)
582
+
583
+
584
+ def test_a_run_level_failure_nobody_owns_gets_one_error_event(instrumented, tmp_path):
585
+ from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step
586
+
587
+ class Slow(Workflow):
588
+ @step
589
+ async def go(self, ev: StartEvent) -> StopEvent:
590
+ await asyncio.sleep(5)
591
+ return StopEvent(result="never")
592
+
593
+ with pytest.raises(Exception):
594
+ run_workflow(Slow(timeout=0.3))
595
+
596
+ events = read_events(tmp_path)
597
+ errors = only(events, "error")
598
+ # Nothing below the run reported the timeout, so exactly one standalone
599
+ # `error` carries it — and it comes strictly BEFORE agent_end, because the
600
+ # graph closes the agent span at agent_end and an error after it is
601
+ # attributed to nothing.
602
+ assert len(errors) == 1
603
+ assert errors[0]["error_type"] == "WorkflowTimeoutError"
604
+ assert types_of(events).index("error") < types_of(events).index("agent_end")
605
+ assert only(events, "agent_end")[0]["outcome"] == "failed"
606
+ assert_rendering_invariants(events)
607
+
608
+
609
+ def test_a_failed_agent_end_says_what_killed_the_run(instrumented, tmp_path):
610
+ """`outcome="failed"` is not a reason, and `summary` is a promoted column.
611
+
612
+ The failing step's `hook_completed` does carry the error, but that is
613
+ payload-only — and with `steps=False` it is not emitted at all, which leaves
614
+ a failed run with its cause recorded precisely nowhere.
615
+ """
616
+ from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step
617
+
618
+ class Bad(Workflow):
619
+ @step
620
+ async def go(self, ev: StartEvent) -> StopEvent:
621
+ raise ValueError("workflow blew up")
622
+
623
+ with pytest.raises(ValueError):
624
+ run_workflow(Bad(timeout=5))
625
+
626
+ end = only(read_events(tmp_path), "agent_end")[0]
627
+ assert end["outcome"] == "failed"
628
+ assert end["summary"] == "ValueError: workflow blew up"
629
+
630
+
631
+ def test_a_timed_out_agent_end_says_what_killed_the_run(instrumented, tmp_path):
632
+ """Same promise on the other failure path, where there IS an error event."""
633
+ from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step
634
+
635
+ class Slow(Workflow):
636
+ @step
637
+ async def go(self, ev: StartEvent) -> StopEvent:
638
+ await asyncio.sleep(5)
639
+ return StopEvent(result="never")
640
+
641
+ with pytest.raises(Exception):
642
+ run_workflow(Slow(timeout=0.3))
643
+
644
+ end = only(read_events(tmp_path), "agent_end")[0]
645
+ assert end["outcome"] == "failed"
646
+ assert end["summary"].startswith("WorkflowTimeoutError:")
647
+
648
+
649
+ def test_a_translator_that_raises_on_every_call_does_not_break_the_run(
650
+ instrumented, tmp_path, monkeypatch, caplog
651
+ ):
652
+ """The whole failure policy, proved rather than asserted.
653
+
654
+ This also covers the LlamaIndex-specific hazard: the dispatcher wraps every
655
+ handler call in `except BaseException: pass` **with no logging**, so an
656
+ adapter bug is invisible unless we log it ourselves.
657
+ """
658
+ state = instrumented.state
659
+
660
+ def explode(*args, **kwargs):
661
+ raise RuntimeError("translator is broken")
662
+
663
+ monkeypatch.setattr(type(state), "span_enter", explode)
664
+ monkeypatch.setattr(type(state), "span_exit", explode)
665
+ monkeypatch.setattr(type(state), "span_drop", explode)
666
+ monkeypatch.setattr(type(state), "model_start", explode)
667
+ monkeypatch.setattr(type(state), "model_end", explode)
668
+
669
+ caplog.set_level("WARNING", logger="failproofai_sdk.integrations")
670
+ llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5")
671
+ assert str(run_agent(calculator(llm), "2+3?")) == "5"
672
+
673
+ logged = " ".join(record.getMessage() for record in caplog.records)
674
+ # BOTH handlers, named individually: the span handler alone satisfying this
675
+ # would let an unwrapped `handle()` through, and the event handler is where
676
+ # every model event comes from.
677
+ # Match the qualname exactly: `.handle` alone is a substring of
678
+ # `.handler_classes`, which appears in every site name in this module.
679
+ assert "FailproofAISpanHandler.new_span" in logged, (
680
+ "the span handler swallowed its exception silently"
681
+ )
682
+ assert "FailproofAIEventHandler.handle" in logged, (
683
+ "the EVENT handler swallowed its exception without logging — the "
684
+ "dispatcher already does that for us, with no traceback and no name"
685
+ )
686
+ _core.reset_failures()
687
+
688
+
689
+ def test_strict_mode_still_cannot_take_the_run_down(instrumented, tmp_path, monkeypatch):
690
+ """FAILPROOFAI_SDK_STRICT re-raises out of `safe()` — and the dispatcher eats it.
691
+
692
+ Worth pinning: strict mode is a debugging switch, not a way to make an
693
+ instrumented LlamaIndex application fail fast. Anyone reaching for it to
694
+ "make errors loud in prod" should read this test first.
695
+ """
696
+ state = instrumented.state
697
+ _core.set_strict(True)
698
+
699
+ def explode(*args, **kwargs):
700
+ raise RuntimeError("translator is broken")
701
+
702
+ monkeypatch.setattr(type(state), "span_enter", explode)
703
+ llm = StubLLM(script=[], final="ok")
704
+ assert str(run_agent(calculator(llm), "hi")) == "ok"
705
+
706
+
707
+ # ---------------------------------------------------------------------------
708
+ # Human in the loop
709
+ # ---------------------------------------------------------------------------
710
+
711
+ def test_human_in_the_loop_emits_both_pairs(instrumented, tmp_path):
712
+ from llama_index.core.workflow import Context, HumanResponseEvent, InputRequiredEvent
713
+
714
+ async def ask_human(ctx: Context, question: str) -> str:
715
+ """Ask the human a question."""
716
+ answer = await ctx.wait_for_event(
717
+ HumanResponseEvent,
718
+ waiter_id="ask",
719
+ waiter_event=InputRequiredEvent(prefix=question),
720
+ )
721
+ return answer.response
722
+
723
+ async def converse() -> str:
724
+ llm = StubLLM(script=[("ask_human", {"question": "ok?"})], final="the human said yes")
725
+ agent = calculator(llm, tools=[ask_human])
726
+ handler = agent.run("ask them")
727
+ async for event in handler.stream_events():
728
+ if isinstance(event, InputRequiredEvent):
729
+ handler.ctx.send_event(HumanResponseEvent(response="yes"))
730
+ return str(await handler)
731
+
732
+ assert asyncio.run(converse()) == "the human said yes"
733
+
734
+ events = read_events(tmp_path)
735
+ kinds = types_of(events)
736
+ # Both pairs, in this order. Only agent_pause/agent_resume feeds the
737
+ # graph's paused time; only human_wait/human_input carries the prompt and
738
+ # the pending-human badge. Neither alone is sufficient.
739
+ assert kinds.index("human_wait") < kinds.index("agent_pause")
740
+ assert kinds.index("agent_pause") < kinds.index("agent_resume")
741
+ assert kinds.index("agent_resume") < kinds.index("human_input")
742
+
743
+ wait = only(events, "human_wait")[0]
744
+ pause = only(events, "agent_pause")[0]
745
+ resume = only(events, "agent_resume")[0]
746
+ given = only(events, "human_input")[0]
747
+ assert wait["input_id"] == pause["pause_id"] == resume["pause_id"] == given["input_id"]
748
+ assert wait["prompt"] and "ok?" in wait["prompt"]
749
+ assert isinstance(resume["duration_ms"], int)
750
+ assert isinstance(given["duration_ms"], int)
751
+ assert_rendering_invariants(events)
752
+
753
+
754
+ def test_a_paused_tool_does_not_collide_with_its_retry(instrumented, tmp_path):
755
+ """LlamaIndex re-runs a paused tool from scratch, reusing its tool_id.
756
+
757
+ Two `tool_use`/`tool_result` pairs sharing one `tool_call_id` in a session
758
+ would pair wrongly and report a nonsense duration, so the retry is suffixed.
759
+ """
760
+ from llama_index.core.workflow import Context, HumanResponseEvent, InputRequiredEvent
761
+
762
+ async def ask_human(ctx: Context, question: str) -> str:
763
+ """Ask the human a question."""
764
+ answer = await ctx.wait_for_event(
765
+ HumanResponseEvent,
766
+ waiter_id="ask",
767
+ waiter_event=InputRequiredEvent(prefix=question),
768
+ )
769
+ return answer.response
770
+
771
+ async def converse() -> None:
772
+ llm = StubLLM(script=[("ask_human", {"question": "ok?"})], final="done")
773
+ handler = calculator(llm, tools=[ask_human]).run("ask them")
774
+ async for event in handler.stream_events():
775
+ if isinstance(event, InputRequiredEvent):
776
+ handler.ctx.send_event(HumanResponseEvent(response="yes"))
777
+ await handler
778
+
779
+ asyncio.run(converse())
780
+
781
+ events = read_events(tmp_path)
782
+ ids = [event["tool_call_id"] for event in only(events, "tool_use")]
783
+ assert ids == ["call_0", "call_0#1"]
784
+ assert [e["tool_call_id"] for e in only(events, "tool_result")] == ids
785
+ assert only(events, "tool_result")[0]["fw_closed_by"] == "human_wait"
786
+
787
+
788
+ # ---------------------------------------------------------------------------
789
+ # Structure
790
+ # ---------------------------------------------------------------------------
791
+
792
+ def test_a_nested_workflow_becomes_a_nested_agent(instrumented, tmp_path):
793
+ from llama_index.core.workflow import Event, StartEvent, StopEvent, Workflow, step
794
+
795
+ class Mid(Event):
796
+ payload: str
797
+
798
+ class Inner(Workflow):
799
+ @step
800
+ async def go(self, ev: StartEvent) -> StopEvent:
801
+ return StopEvent(result="inner-done")
802
+
803
+ class Outer(Workflow):
804
+ @step
805
+ async def first(self, ev: StartEvent) -> Mid:
806
+ return Mid(payload="hi")
807
+
808
+ @step
809
+ async def second(self, ev: Mid) -> StopEvent:
810
+ return StopEvent(result=str(await Inner(timeout=5).run()))
811
+
812
+ assert str(run_workflow(Outer(timeout=5))) == "inner-done"
813
+
814
+ events = read_events(tmp_path)
815
+ starts = only(events, "agent_start")
816
+ assert [start["agent_id"] for start in starts] == ["Outer", "Inner"]
817
+ assert "parent_id" not in starts[0]
818
+ assert starts[1]["parent_id"] == "Outer"
819
+ assert len({event["session_id"] for event in events}) == 1
820
+ assert_rendering_invariants(events)
821
+
822
+
823
+ # ---------------------------------------------------------------------------
824
+ # AgentWorkflow handoffs
825
+ #
826
+ # `AgentWorkflow` does NOT run its agents as nested workflows: there is one
827
+ # `AgentWorkflow.run` span and the agents are steps inside it. Read off the span
828
+ # tree alone a two-agent crew is one flat `agent_id="AgentWorkflow"` and the
829
+ # handoff is invisible, so the adapter keys nested agents off the
830
+ # `current_agent_name` the runtime puts on every AgentInput/AgentSetup/
831
+ # AgentOutput instead.
832
+ # ---------------------------------------------------------------------------
833
+
834
+ HANDOFF_TO_ANALYST = ("handoff", {"to_agent": "analyst", "reason": "over to you"})
835
+ HANDOFF_TO_RESEARCHER = ("handoff", {"to_agent": "researcher", "reason": "back to you"})
836
+
837
+
838
+ def crew(researcher_llm, analyst_llm, *, handoff_back=False):
839
+ """A real two-agent `AgentWorkflow` — the API LlamaIndex documents."""
840
+ from llama_index.core.agent.workflow import AgentWorkflow, FunctionAgent
841
+
842
+ researcher = FunctionAgent(
843
+ name="researcher",
844
+ description="looks numbers up",
845
+ tools=[add],
846
+ llm=researcher_llm,
847
+ streaming=False,
848
+ can_handoff_to=["analyst"],
849
+ )
850
+ analyst = FunctionAgent(
851
+ name="analyst",
852
+ description="does the maths",
853
+ tools=[add],
854
+ llm=analyst_llm,
855
+ streaming=False,
856
+ # `None` here would mean "may hand off to anyone", which loops.
857
+ can_handoff_to=["researcher"] if handoff_back else [],
858
+ )
859
+ return AgentWorkflow(agents=[researcher, analyst], root_agent="researcher")
860
+
861
+
862
+ def test_an_agent_workflow_handoff_is_two_nested_agents_not_one_flat_one(
863
+ instrumented, tmp_path
864
+ ):
865
+ """The names a customer facets by are `researcher` and `analyst`.
866
+
867
+ Flattened, every event in the session carries `agent_id="AgentWorkflow"` and
868
+ the two agents are distinguishable only by a payload extra, which
869
+ `agent_id`-keyed surfaces cannot group by at all.
870
+ """
871
+ workflow = crew(
872
+ StubLLM(script=[("add", {"a": 1, "b": 1}), HANDOFF_TO_ANALYST]),
873
+ StubLLM(script=[("add", {"a": 2, "b": 3})], final="5"),
874
+ )
875
+ assert str(run_workflow(workflow, user_msg="add things")) == "5"
876
+
877
+ events = read_events(tmp_path)
878
+ starts = only(events, "agent_start")
879
+ assert [start["agent_id"] for start in starts] == [
880
+ "AgentWorkflow",
881
+ "researcher",
882
+ "analyst",
883
+ ]
884
+ assert "parent_id" not in starts[0]
885
+ assert starts[1]["parent_id"] == "AgentWorkflow"
886
+ assert starts[2]["parent_id"] == "AgentWorkflow"
887
+ assert len({event["session_id"] for event in events}) == 1
888
+
889
+ # Sticky, and this is the subtle half: `ToolCall` carries no
890
+ # `current_agent_name`, so a `call_tool` step has to keep whichever agent
891
+ # asked for the tool.
892
+ assert [(e["agent_id"], e["tool_name"]) for e in only(events, "tool_use")] == [
893
+ ("researcher", "add"),
894
+ ("researcher", "handoff"),
895
+ ("analyst", "add"),
896
+ ]
897
+ assert {e["agent_id"] for e in only(events, "model_request")} == {
898
+ "researcher",
899
+ "analyst",
900
+ }
901
+ assert_rendering_invariants(events)
902
+
903
+
904
+ def test_a_standalone_function_agent_does_not_nest_inside_itself(instrumented, tmp_path):
905
+ """The guard on the rule above, and the reason it is `name != agent_id`.
906
+
907
+ A standalone `FunctionAgent.run` drives those same `AgentWorkflow` steps
908
+ with its OWN name in `current_agent_name`. Without the guard every
909
+ single-agent run would open a `calc` nested inside a `calc` — doubling the
910
+ agent count on every LlamaIndex session in the product.
911
+ """
912
+ llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5")
913
+ run_agent(calculator(llm), "2+3?")
914
+
915
+ events = read_events(tmp_path)
916
+ assert [start["agent_id"] for start in only(events, "agent_start")] == ["calc"]
917
+ assert types_of(events) == ONE_TOOL_SEQUENCE
918
+ assert_rendering_invariants(events)
919
+
920
+
921
+ def test_a_handoff_back_opens_the_first_agent_again_as_a_second_turn(
922
+ instrumented, tmp_path
923
+ ):
924
+ """A -> B -> A: two turns for `researcher`, each opened and closed on its own.
925
+
926
+ The nested agent is keyed per turn rather than per name. Reusing the key of
927
+ the `researcher` we already ended would collide in the tracker, and one of
928
+ the two `agent_start`s would never be closed.
929
+ """
930
+ workflow = crew(
931
+ StubLLM(script=[HANDOFF_TO_ANALYST], final="done"),
932
+ StubLLM(script=[HANDOFF_TO_RESEARCHER]),
933
+ handoff_back=True,
934
+ )
935
+ assert str(run_workflow(workflow, user_msg="round trip")) == "done"
936
+
937
+ events = read_events(tmp_path)
938
+ assert [start["agent_id"] for start in only(events, "agent_start")] == [
939
+ "AgentWorkflow",
940
+ "researcher",
941
+ "analyst",
942
+ "researcher",
943
+ ]
944
+ # Inner-first, and every open closed: the invariant check below fails on an
945
+ # agent left open, which is what a key collision would produce.
946
+ assert [end["agent_id"] for end in only(events, "agent_end")] == [
947
+ "researcher",
948
+ "analyst",
949
+ "researcher",
950
+ "AgentWorkflow",
951
+ ]
952
+ assert_rendering_invariants(events)
953
+
954
+
955
+ def test_sub_agents_are_still_attributed_with_the_step_hooks_off(
956
+ instrumented_without_steps, tmp_path
957
+ ):
958
+ """`steps=False` drops the hook pairs, not the agents.
959
+
960
+ The sub-agent is resolved when the step span OPENS, which is a different
961
+ code path from the `hook_triggered` the option suppresses — so it is worth
962
+ proving rather than assuming.
963
+ """
964
+ workflow = crew(
965
+ StubLLM(script=[HANDOFF_TO_ANALYST]),
966
+ StubLLM(script=[("add", {"a": 2, "b": 3})], final="5"),
967
+ )
968
+ assert str(run_workflow(workflow, user_msg="add things")) == "5"
969
+
970
+ events = read_events(tmp_path)
971
+ assert only(events, "hook_triggered") == []
972
+ assert only(events, "hook_completed") == []
973
+ assert [start["agent_id"] for start in only(events, "agent_start")] == [
974
+ "AgentWorkflow",
975
+ "researcher",
976
+ "analyst",
977
+ ]
978
+ assert [(e["agent_id"], e["tool_name"]) for e in only(events, "tool_use")] == [
979
+ ("researcher", "handoff"),
980
+ ("analyst", "add"),
981
+ ]
982
+ assert_rendering_invariants(events)
983
+
984
+
985
+ def test_two_concurrent_runs_do_not_mix(instrumented, tmp_path):
986
+ """Two overlapping runs, one process, one dispatcher, no contextvars.
987
+
988
+ The whole reason `RunTracker` passes `session_id=` explicitly instead of
989
+ reading a contextvar: a start and its end are separate dispatcher calls,
990
+ and interleaved runs would otherwise attribute events to whichever run
991
+ happened to touch the variable last.
992
+ """
993
+
994
+ async def both():
995
+ left = calculator(StubLLM(script=[("add", {"a": 1, "b": 1})], final="2"))
996
+ right = calculator(StubLLM(script=[("add", {"a": 3, "b": 4})], final="7"))
997
+ return await asyncio.gather(left.run("1+1?"), right.run("3+4?"))
998
+
999
+ answers = {str(answer) for answer in asyncio.run(both())}
1000
+ assert answers == {"2", "7"}
1001
+
1002
+ events = read_events(tmp_path)
1003
+ sessions = {event["session_id"] for event in events}
1004
+ assert len(sessions) == 2
1005
+
1006
+ for session in sessions:
1007
+ rows = [event for event in events if event["session_id"] == session]
1008
+ assert types_of(rows) == ONE_TOOL_SEQUENCE
1009
+ assert_rendering_invariants(rows)
1010
+ # Each session saw exactly one of the two tool calls, whole.
1011
+ inputs = [event["input"] for event in rows if event["type"] == "tool_use"]
1012
+ assert inputs in ([{"a": 1, "b": 1}], [{"a": 3, "b": 4}])
1013
+
1014
+
1015
+ def test_a_retrieval_becomes_a_tool_with_a_summarized_output(instrumented, tmp_path):
1016
+ from llama_index.core.base.base_retriever import BaseRetriever
1017
+ from llama_index.core.schema import NodeWithScore, QueryBundle, TextNode
1018
+ from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step
1019
+
1020
+ class Fake(BaseRetriever):
1021
+ def _retrieve(self, query_bundle: QueryBundle):
1022
+ return [
1023
+ NodeWithScore(node=TextNode(text="x" * 5000, id_=f"n{i}"), score=1.0)
1024
+ for i in range(12)
1025
+ ]
1026
+
1027
+ class Rag(Workflow):
1028
+ @step
1029
+ async def go(self, ev: StartEvent) -> StopEvent:
1030
+ return StopEvent(result=f"{len(Fake().retrieve('what?'))} nodes")
1031
+
1032
+ assert str(run_workflow(Rag(timeout=5))) == "12 nodes"
1033
+
1034
+ events = read_events(tmp_path)
1035
+ use = only(events, "tool_use")[0]
1036
+ result = only(events, "tool_result")[0]
1037
+ assert use["tool_name"] == "Fake"
1038
+ assert use["input"] == {"query": "what?"}
1039
+ assert use["fw_kind"] == "retrieval"
1040
+ # Retrieved documents are the largest strings in the process and the
1041
+ # payload is not a promoted column: summarize, never ship them whole.
1042
+ assert result["output"]["num_nodes"] == 12
1043
+ assert len(result["output"]["top"]) == 5
1044
+ assert len(result["output"]["top"][0]["text"]) <= 200
1045
+ assert_rendering_invariants(events)
1046
+
1047
+
1048
+ def test_embeddings_are_off_by_default(instrumented, tmp_path):
1049
+ assert instrumented.state.embeddings is False
1050
+
1051
+
1052
+ def test_no_event_carries_an_extra_that_shadows_a_declared_field(instrumented, tmp_path):
1053
+ """`_schema._build()` merges extras LAST, at the top level.
1054
+
1055
+ So an extra called `tool_name`, `model`, `outcome` or `input_tokens`
1056
+ silently overwrites the declared field — changing the promoted the events store
1057
+ column and the server's computed summary while every test still passes.
1058
+ """
1059
+ llm = StubLLM(script=[("add", {"a": 2, "b": 3})], final="5")
1060
+ run_agent(calculator(llm), "2+3?")
1061
+
1062
+ by_type = {}
1063
+ for name, obj in vars(_schema).items():
1064
+ if name.endswith("Event") and hasattr(obj, "__dataclass_fields__"):
1065
+ kind = "".join("_" + c.lower() if c.isupper() else c for c in name[:-5]).lstrip("_")
1066
+ by_type[kind] = {f.name for f in dataclass_fields(obj)} - {"extra_fields"}
1067
+
1068
+ for event in read_events(tmp_path):
1069
+ declared = by_type[event["type"]] | {"type", "environment"}
1070
+ extras = set(event) - declared
1071
+ assert not (extras & _core.FORBIDDEN_EXTRAS), f"{event['type']} shadows {extras}"
1072
+ for key in extras:
1073
+ assert key.startswith("fw_") or key in _core.ALLOWED_TOP_LEVEL, (
1074
+ f"{event['type']} carries un-namespaced extra {key!r}"
1075
+ )
1076
+
1077
+
1078
+ # ---------------------------------------------------------------------------
1079
+ # Teardown
1080
+ # ---------------------------------------------------------------------------
1081
+
1082
+ def test_uninstrument_detaches_only_our_handlers(tmp_path):
1083
+ from llama_index.core.instrumentation import get_dispatcher
1084
+ from llama_index_instrumentation.event_handlers.base import BaseEventHandler
1085
+
1086
+ class Foreign(BaseEventHandler):
1087
+ def handle(self, event, **kwargs):
1088
+ return None
1089
+
1090
+ dispatcher = get_dispatcher()
1091
+ foreign = Foreign()
1092
+ dispatcher.add_event_handler(foreign)
1093
+ before_events = list(dispatcher.event_handlers)
1094
+ before_spans = list(dispatcher.span_handlers)
1095
+ try:
1096
+ failproofai_sdk.instrument("llama_index")
1097
+ assert len(dispatcher.event_handlers) == len(before_events) + 1
1098
+ assert len(dispatcher.span_handlers) == len(before_spans) + 1
1099
+ failproofai_sdk.uninstrument("llama_index")
1100
+ # In-place slice assignment: `add_span_handler` does `+= [h]`, so a
1101
+ # plain `=` rebinds the pydantic field and can drop someone else's.
1102
+ assert list(dispatcher.event_handlers) == before_events
1103
+ assert list(dispatcher.span_handlers) == before_spans
1104
+ assert foreign in dispatcher.event_handlers
1105
+ finally:
1106
+ dispatcher.event_handlers[:] = [h for h in dispatcher.event_handlers if h is not foreign]
1107
+
1108
+
1109
+ ROOT_SPAN = "Empty.run-11111111-1111-4111-8111-111111111111"
1110
+ TOOL_SPAN = "FunctionTool.acall-22222222-2222-4222-8222-222222222222"
1111
+ LLM_SPAN = "StubLLM.astream_chat-44444444-4444-4444-8444-444444444444"
1112
+
1113
+
1114
+ def _bound_args():
1115
+ """A real `inspect.BoundArguments`, which is what the dispatcher passes."""
1116
+
1117
+ def target(a=None, b=None):
1118
+ return None
1119
+
1120
+ return inspect.signature(target).bind()
1121
+
1122
+
1123
+ def _empty_workflow():
1124
+ from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step
1125
+
1126
+ class Empty(Workflow):
1127
+ @step
1128
+ async def go(self, ev: StartEvent) -> StopEvent:
1129
+ return StopEvent(result="ok")
1130
+
1131
+ return Empty()
1132
+
1133
+
1134
+ def _fake_tool():
1135
+ class _Metadata:
1136
+ name = "adder"
1137
+
1138
+ class _Tool(BaseTool):
1139
+ metadata = _Metadata()
1140
+
1141
+ def __call__(self, *args, **kwargs):
1142
+ return None
1143
+
1144
+ return _Tool()
1145
+
1146
+
1147
+ def _open_a_run_holding_a_tool(state):
1148
+ """Drive the real span handler to a run with one leaf still open.
1149
+
1150
+ Synthetic span ids, real handler, real base class: this is the state a
1151
+ process is in when a run dies mid-tool, which no cooperative workflow will
1152
+ produce on demand.
1153
+ """
1154
+ _, span_cls = adapter_module.handler_classes()
1155
+ handler = span_cls(state=state)
1156
+ bound = _bound_args()
1157
+ handler.span_enter(id_=ROOT_SPAN, bound_args=bound, instance=_empty_workflow(), parent_id=None)
1158
+ handler.span_enter(id_=TOOL_SPAN, bound_args=bound, instance=_fake_tool(), parent_id=ROOT_SPAN)
1159
+ assert state._runs[ROOT_SPAN].open_leaves, "the tool leaf should be open"
1160
+ return handler, bound
1161
+
1162
+
1163
+ def test_a_run_that_ends_holding_an_open_tool_closes_it(instrumented, tmp_path):
1164
+ """Invariant 4, at the run boundary.
1165
+
1166
+ `agent_end` force-closes open *pauses* but not tools or models, so a run
1167
+ that finishes while a leaf is open leaves the session `ongoing` forever —
1168
+ and nothing else in the suite notices, because a cooperative workflow
1169
+ always closes its own leaves.
1170
+ """
1171
+ state = instrumented.state
1172
+ handler, bound = _open_a_run_holding_a_tool(state)
1173
+
1174
+ handler.span_exit(id_=ROOT_SPAN, bound_args=bound, instance=None, result=None)
1175
+
1176
+ events = read_events(tmp_path)
1177
+ assert types_of(events) == ["agent_start", "tool_use", "tool_result", "agent_end"]
1178
+ assert only(events, "tool_result")[0]["fw_closed_by"] == "run_ended"
1179
+ assert only(events, "agent_end")[0]["outcome"] == "success"
1180
+ assert_rendering_invariants(events)
1181
+
1182
+
1183
+ def test_shutdown_closes_every_leaf_a_dead_run_left_open(instrumented, tmp_path):
1184
+ """Same invariant at the uninstrument boundary: nothing is left dangling."""
1185
+ state = instrumented.state
1186
+ _open_a_run_holding_a_tool(state)
1187
+
1188
+ failproofai_sdk.uninstrument("llama_index")
1189
+
1190
+ events = read_events(tmp_path)
1191
+ assert types_of(events) == ["agent_start", "tool_use", "tool_result", "agent_end"]
1192
+ assert only(events, "tool_result")[0]["fw_closed_by"] == "uninstrument"
1193
+ assert only(events, "agent_end")[0]["outcome"] == "cancelled"
1194
+ assert_rendering_invariants(events)
1195
+
1196
+
1197
+ def test_the_reaper_closes_a_stale_parked_stream(instrumented, tmp_path):
1198
+ """A streaming response nobody consumes never gets an LLMChatEndEvent.
1199
+
1200
+ Its span has already exited, so nothing else will ever close it: without
1201
+ the sweep the model_request stays open and the session reads `ongoing`
1202
+ forever.
1203
+ """
1204
+ state = instrumented.state
1205
+ event_cls, span_cls = adapter_module.handler_classes()
1206
+ spans = span_cls(state=state)
1207
+ events_handler = event_cls(state=state)
1208
+
1209
+ from llama_index.core.instrumentation.events.llm import LLMChatStartEvent
1210
+
1211
+ bound = _bound_args()
1212
+ spans.span_enter(id_=ROOT_SPAN, bound_args=bound, instance=_empty_workflow(), parent_id=None)
1213
+ spans.span_enter(id_=LLM_SPAN, bound_args=bound, instance=StubLLM(), parent_id=ROOT_SPAN)
1214
+ events_handler.handle(
1215
+ LLMChatStartEvent(span_id=LLM_SPAN, messages=[], additional_kwargs={}, model_dict={})
1216
+ )
1217
+ # The span exits the instant the generator is created; nobody consumes it.
1218
+ spans.span_exit(id_=LLM_SPAN, bound_args=bound, instance=None, result=iter([]))
1219
+
1220
+ assert state._leaf_run.get(LLM_SPAN) == ROOT_SPAN
1221
+ state.stale_after = 0.0
1222
+ assert state.sweep() == 1
1223
+ assert state.sweep() == 0
1224
+
1225
+ response = only(read_events(tmp_path), "model_response")[0]
1226
+ assert response["fw_closed_by"] == "stale"
1227
+ assert isinstance(response["duration_ms"], int)
1228
+
1229
+
1230
+ # ---------------------------------------------------------------------------
1231
+ # Cancellation
1232
+ #
1233
+ # `handler.cancel_run()` does NOT drop the run span. The runtime catches its own
1234
+ # `WorkflowCancelledByUser` and exits the span cleanly, with `result=None` and
1235
+ # no error — "so it shows as OK rather than ERROR in traces". Read off the span
1236
+ # alone, a user pressing stop is indistinguishable from a completed run, which
1237
+ # is why the adapter listens for `SpanCancelledEvent`.
1238
+ # ---------------------------------------------------------------------------
1239
+
1240
+ def _cancel_a_run_mid_step() -> None:
1241
+ """Cancel a real run while a step is in flight, the way a stop button does."""
1242
+ from llama_index.core.workflow import StartEvent, StopEvent, Workflow, step
1243
+
1244
+ started = asyncio.Event()
1245
+
1246
+ class Slow(Workflow):
1247
+ @step
1248
+ async def go(self, ev: StartEvent) -> StopEvent:
1249
+ started.set()
1250
+ await asyncio.sleep(30)
1251
+ return StopEvent(result="never")
1252
+
1253
+ async def _main() -> None:
1254
+ handler = Slow(timeout=30).run()
1255
+ await asyncio.wait_for(started.wait(), timeout=5)
1256
+ await handler.cancel_run()
1257
+ with pytest.raises(BaseException):
1258
+ await handler
1259
+
1260
+ asyncio.run(_main())
1261
+
1262
+
1263
+ def test_a_cancelled_run_is_not_reported_as_a_success(instrumented, tmp_path):
1264
+ """Reporting a cancellation as success inflates the completion rate.
1265
+
1266
+ `cancelled` is deliberately not `failed` either: the server counts only
1267
+ `error|failed|timeout|rejected` as a failure, and a stop button is neither.
1268
+ """
1269
+ _cancel_a_run_mid_step()
1270
+
1271
+ events = read_events(tmp_path)
1272
+ assert only(events, "agent_end")[0]["outcome"] == "cancelled"
1273
+ # A cancellation is not an error, so nothing may report one.
1274
+ assert only(events, "error") == []
1275
+ assert_rendering_invariants(events)
1276
+
1277
+
1278
+ def test_a_step_cancelled_mid_flight_is_not_reported_as_a_success(
1279
+ instrumented, tmp_path
1280
+ ):
1281
+ """Same signal one level down: the step exits with `result=None`, no error."""
1282
+ _cancel_a_run_mid_step()
1283
+
1284
+ completed = only(read_events(tmp_path), "hook_completed")
1285
+ assert completed, "the in-flight step still has to close"
1286
+ assert [hook["outcome"] for hook in completed] == ["cancelled"]
1287
+
1288
+
1289
+ # ---------------------------------------------------------------------------
1290
+ # Structural anti-drift — the one that catches a silently dead adapter
1291
+ # ---------------------------------------------------------------------------
1292
+
1293
+ def _our_overrides(cls, base) -> set[str]:
1294
+ return {
1295
+ name
1296
+ for name in vars(cls)
1297
+ if not name.startswith("__")
1298
+ and inspect.isroutine(getattr(cls, name, None))
1299
+ and hasattr(base, name)
1300
+ }
1301
+
1302
+
1303
+ def _named_params(func) -> set[str]:
1304
+ return {
1305
+ name
1306
+ for name, parameter in inspect.signature(func).parameters.items()
1307
+ if name not in {"self", "cls"}
1308
+ and parameter.kind
1309
+ not in (inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL)
1310
+ }
1311
+
1312
+
1313
+ def test_our_overrides_still_exist_on_the_framework_base_classes():
1314
+ """If upstream renames a callback, our override becomes DEAD CODE.
1315
+
1316
+ It is never called, nothing raises, and every other test in this file still
1317
+ passes because they exercise the adapter through our own objects. This is
1318
+ the only test that notices.
1319
+ """
1320
+ from llama_index_instrumentation.event_handlers.base import BaseEventHandler
1321
+ from llama_index_instrumentation.span_handlers.base import BaseSpanHandler
1322
+
1323
+ event_cls, span_cls = adapter_module.handler_classes()
1324
+
1325
+ for cls, base, expected in (
1326
+ (event_cls, BaseEventHandler, {"handle"}),
1327
+ (span_cls, BaseSpanHandler, {"new_span", "prepare_to_exit_span", "prepare_to_drop_span"}),
1328
+ ):
1329
+ overrides = _our_overrides(cls, base)
1330
+ assert expected <= overrides, f"{cls.__name__} no longer overrides {expected - overrides}"
1331
+ for name in overrides:
1332
+ ours = getattr(cls, name)
1333
+ theirs = getattr(base, name, None)
1334
+ assert inspect.isroutine(theirs), f"{base.__name__}.{name} is gone"
1335
+ missing = _named_params(ours) - _named_params(theirs)
1336
+ assert not missing, (
1337
+ f"{cls.__name__}.{name} declares {sorted(missing)}, which "
1338
+ f"{base.__name__}.{name} no longer accepts by name"
1339
+ )
1340
+
1341
+
1342
+ def test_the_span_enter_to_new_span_kwarg_rename_still_holds():
1343
+ """`span_enter(parent_id=...)` calls `new_span(parent_span_id=...)`.
1344
+
1345
+ Declaring the wrong one is not an error — it lands in `**kwargs`, every
1346
+ parent comes through as None, and the whole trace renders flat.
1347
+ """
1348
+ from llama_index_instrumentation.span_handlers.base import BaseSpanHandler
1349
+
1350
+ assert "parent_id" in _named_params(BaseSpanHandler.span_enter)
1351
+ assert "parent_span_id" in _named_params(BaseSpanHandler.new_span)
1352
+ assert "parent_span_id" not in _named_params(BaseSpanHandler.span_enter)
1353
+
1354
+ _, span_cls = adapter_module.handler_classes()
1355
+ assert "parent_span_id" in _named_params(span_cls.new_span)
1356
+
1357
+
1358
+ def test_every_dispatcher_event_class_we_dispatch_on_still_exists():
1359
+ """We dispatch on the event's class NAME, so these names are the API.
1360
+
1361
+ A rename upstream leaves the table below looking perfectly healthy while
1362
+ the adapter records nothing at all.
1363
+ """
1364
+ import importlib
1365
+
1366
+ modules = [
1367
+ importlib.import_module(f"llama_index.core.instrumentation.events.{name}")
1368
+ for name in ("llm", "chat_engine", "retrieval", "embedding", "exception")
1369
+ ]
1370
+ for name in adapter_module._HANDLED_EVENTS:
1371
+ assert any(hasattr(module, name) for module in modules), (
1372
+ f"{name} no longer exists in llama_index.core.instrumentation.events"
1373
+ )
1374
+
1375
+
1376
+ def test_the_cancel_event_we_dispatch_on_still_exists_where_we_expect_it():
1377
+ """`CANCEL_EVENTS` has no other guard in this file, by construction.
1378
+
1379
+ Every other name we dispatch on lives under
1380
+ `llama_index.core.instrumentation.events.*`, which the test above walks.
1381
+ This one is dispatched by the workflows RUNTIME, so that test cannot see it
1382
+ — and if it is renamed or moved, nothing raises: cancelled runs quietly go
1383
+ back to being reported as successes.
1384
+ """
1385
+ import importlib
1386
+
1387
+ from llama_index.core.instrumentation.events.base import BaseEvent
1388
+
1389
+ module = importlib.import_module("workflows.runtime.types.step_function")
1390
+ for name in adapter_module.CANCEL_EVENTS:
1391
+ cls = getattr(module, name, None)
1392
+ assert cls is not None, f"{name} is gone from {module.__name__}"
1393
+ assert issubclass(cls, BaseEvent), f"{name} is no longer a dispatcher event"
1394
+ # We match on the class name and pair the mark with the span_exit behind
1395
+ # it using `span_id`, which the dispatcher stamps from the active span.
1396
+ assert cls.class_name() == name
1397
+ assert "span_id" in cls.model_fields
1398
+
1399
+ # And the exception that path exists to serve, which `cancel_run()` raises.
1400
+ from workflows.errors import WorkflowCancelledByUser
1401
+
1402
+ assert issubclass(WorkflowCancelledByUser, BaseException)
1403
+
1404
+
1405
+ @pytest.mark.parametrize(
1406
+ ("module", "event", "attributes"),
1407
+ [
1408
+ ("llm", "LLMChatStartEvent", ("messages", "model_dict")),
1409
+ ("llm", "LLMChatEndEvent", ("messages", "response")),
1410
+ ("retrieval", "RetrievalStartEvent", ("str_or_query_bundle",)),
1411
+ ("retrieval", "RetrievalEndEvent", ("nodes",)),
1412
+ ("exception", "ExceptionEvent", ("exception",)),
1413
+ ],
1414
+ )
1415
+ def test_the_event_attributes_we_read_still_exist(module, event, attributes):
1416
+ import importlib
1417
+
1418
+ cls = getattr(importlib.import_module(f"llama_index.core.instrumentation.events.{module}"), event)
1419
+ for attribute in attributes:
1420
+ assert attribute in cls.model_fields, f"{event}.{attribute} is gone"
1421
+
1422
+
1423
+ def test_the_waiting_for_event_signal_still_looks_like_we_think_it_does():
1424
+ """HITL hinges on a name match: `WaitingForEvent` is a pause, not an error.
1425
+
1426
+ It is not exported from `workflows.errors` and has moved before, so we
1427
+ match on the class name. If it is renamed, every human-in-the-loop pause
1428
+ becomes a red error event and a failed run.
1429
+ """
1430
+ from workflows.runtime.types.results import AddWaiter, WaitingForEvent
1431
+
1432
+ assert WaitingForEvent.__name__ == "WaitingForEvent"
1433
+ assert issubclass(WaitingForEvent, Exception)
1434
+ assert "waiter_id" in AddWaiter.model_fields
1435
+ assert "waiter_event" in AddWaiter.model_fields
1436
+ assert adapter_module._is_waiting(WaitingForEvent.__new__(WaitingForEvent)) is True
1437
+ assert adapter_module._is_waiting(RuntimeError("nope")) is False
1438
+
1439
+
1440
+ def test_the_dispatcher_surface_we_register_on_still_exists():
1441
+ from llama_index.core.instrumentation import get_dispatcher
1442
+
1443
+ dispatcher = get_dispatcher()
1444
+ assert dispatcher.name == "root", "get_dispatcher() must return the ROOT dispatcher"
1445
+ assert callable(dispatcher.add_event_handler)
1446
+ assert callable(dispatcher.add_span_handler)
1447
+ # Child dispatchers propagate upward; that is why one registration is enough.
1448
+ assert get_dispatcher("llama_index.core.something").propagate is True
1449
+
1450
+
1451
+ # ---------------------------------------------------------------------------
1452
+ # Registry wiring
1453
+ # ---------------------------------------------------------------------------
1454
+
1455
+ def test_the_registry_reaches_this_adapter_by_all_its_spellings():
1456
+ from failproofai_sdk.integrations import _canonical
1457
+
1458
+ for spelling in ("llama_index", "llamaindex", "llama-index", "LlamaIndex"):
1459
+ assert _canonical(spelling) == "llama_index"
1460
+
1461
+
1462
+ def test_instrumenting_twice_is_a_no_op(tmp_path):
1463
+ from failproofai_sdk.integrations import active
1464
+
1465
+ try:
1466
+ assert failproofai_sdk.instrument("llama_index") == ("llama_index",)
1467
+ assert failproofai_sdk.instrument("llama_index") == ()
1468
+ assert "llama_index" in active()
1469
+ finally:
1470
+ failproofai_sdk.uninstrument("llama_index")
1471
+
1472
+
1473
+ def test_uninstrumenting_something_that_was_never_installed_is_a_no_op():
1474
+ assert failproofai_sdk.uninstrument("llama_index") == ()
1475
+
1476
+
1477
+ def test_the_adapter_joins_a_hand_written_agent_scope(instrumented, tmp_path):
1478
+ """The interop story: adapter events land in the ambient session.
1479
+
1480
+ `RunTracker.identity()` falls back to `failproofai_sdk.current()`, so mixing the
1481
+ manual API with an adapter produces one tree rather than two.
1482
+ """
1483
+ llm = StubLLM(script=[], final="5")
1484
+ with failproofai_sdk.agent("planner", goal="do maths") as identity:
1485
+ run_agent(calculator(llm), "2+3?")
1486
+ session_id = identity.session_id
1487
+
1488
+ events = read_events(tmp_path)
1489
+ assert {event["session_id"] for event in events} == {session_id}
1490
+ starts = only(events, "agent_start")
1491
+ assert [start["agent_id"] for start in starts] == ["planner", "calc"]
1492
+ assert starts[1]["parent_id"] == "planner"
1493
+
1494
+
1495
+ @pytest.fixture
1496
+ def instrumented_without_content(tmp_path):
1497
+ """`instrumented`, with the documented regulated-data switch turned off."""
1498
+ _core.set_strict(False)
1499
+ _compat.set_strict_integrations(False)
1500
+ _core.reset_failures()
1501
+ _runtime.writer.set_flush_interval(3600)
1502
+ assert failproofai_sdk.instrument("llama_index", capture_messages=False) == (
1503
+ "llama_index",
1504
+ )
1505
+ try:
1506
+ yield adapter_module.adapter
1507
+ finally:
1508
+ failproofai_sdk.uninstrument("llama_index")
1509
+ _core.set_strict(None)
1510
+ _compat.set_strict_integrations(None)
1511
+ _core.reset_failures()
1512
+
1513
+
1514
+ def test_capture_messages_off_records_no_payload_anywhere(
1515
+ instrumented_without_content, tmp_path
1516
+ ):
1517
+ """The switch was consulted in ONE place and documented as covering all of them.
1518
+
1519
+ `capture_messages` gated only the messages and system prompt on
1520
+ `model_start`. The model's completion, every tool call's arguments, every
1521
+ tool's return value, every workflow-step input and output, the retrieval
1522
+ query and the agent's goal were all still written to the spool and shipped —
1523
+ so the setting looked like it had worked (prompts did stop) while the
1524
+ answers, the arguments and the outputs did not.
1525
+
1526
+ `docs/start/integrations/llamaindex.mdx` presents this as the control for
1527
+ regulated data, and `collector.redact` explicitly does not apply to SDK
1528
+ events, so there was no second line of defence behind it.
1529
+ """
1530
+ llm = StubLLM(script=[("add", {"a": 987654321, "b": 123456789})], final="SECRET-COMPLETION")
1531
+ run_agent(calculator(llm), "SECRET-PROMPT: add them")
1532
+
1533
+ events = read_events(tmp_path)
1534
+ assert events, "nothing was recorded at all; the test proves nothing"
1535
+
1536
+ blob = json.dumps(events)
1537
+ # Distinctive on purpose: short values collide with the hex in a random
1538
+ # span/session id and make this flaky rather than wrong.
1539
+ for secret in ("SECRET-COMPLETION", "SECRET-PROMPT", "987654321", "123456789"):
1540
+ assert secret not in blob, f"{secret!r} reached the spool with capture_messages=False"
1541
+
1542
+ # And on the fields themselves, so this still fails if a payload is recorded
1543
+ # in some form the substrings above happen not to match.
1544
+ for row in events:
1545
+ for field in ("input", "output", "content", "messages", "system", "goal", "prompt", "response"):
1546
+ assert row.get(field) is None, (
1547
+ f"{row['type']}.{field} = {row[field]!r} with capture_messages=False"
1548
+ )
1549
+
1550
+ # Structure, timings and outcomes are still recorded — that is the whole
1551
+ # bargain the option offers, and an adapter that recorded nothing would
1552
+ # pass the assertions above for the wrong reason.
1553
+ kinds = types_of(events)
1554
+ assert "agent_start" in kinds and "tool_use" in kinds and "model_request" in kinds
1555
+
1556
+
1557
+ def test_captured_values_are_not_capped_at_a_quarter_of_the_core_limit():
1558
+ """512 was a quarter of the limit the LangChain adapter had just rejected.
1559
+
1560
+ `_summarize` renders every user-visible payload here, and this is the
1561
+ RAG-first framework: a real retrieved context clears 512 characters many
1562
+ times over, so the adapter documented as the way to see what your agent said
1563
+ showed the first two sentences of it — with no option to raise it.
1564
+ """
1565
+ assert adapter_module._SUMMARY_LIMIT == _core.FIELD_LIMIT
1566
+
1567
+ state = adapter_module._State(capture_limit=32768)
1568
+ assert len(state.capture("z" * 30000)) == 30000
1569
+ assert state.tracker._field_limit == 32768
1570
+
1571
+ # A bad value must not take the adapter down: `instrument()` with no name
1572
+ # installs every detected adapter with the same options, and `inf` is the
1573
+ # obvious spelling of "capture everything".
1574
+ assert adapter_module._capture_limit(float("inf")) == _core.FIELD_LIMIT
1575
+ assert adapter_module._capture_limit("nope") == _core.FIELD_LIMIT
1576
+ assert adapter_module._capture_limit(0) == _core.FIELD_LIMIT