claude-smart 0.2.45 → 0.2.47

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 (353) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +47 -10
  3. package/bin/claude-smart.js +535 -23
  4. package/package.json +22 -3
  5. package/plugin/.claude-plugin/plugin.json +4 -2
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +27 -6
  8. package/plugin/commands/clear-all.md +1 -0
  9. package/plugin/commands/dashboard.md +1 -0
  10. package/plugin/commands/learn.md +1 -0
  11. package/plugin/commands/restart.md +1 -0
  12. package/plugin/commands/show.md +1 -0
  13. package/plugin/dashboard/app/configure/env/page.tsx +40 -14
  14. package/plugin/dashboard/app/configure/server/page.tsx +51 -1
  15. package/plugin/dashboard/app/layout.tsx +20 -0
  16. package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
  17. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  18. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  19. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  20. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  21. package/plugin/dashboard/lib/status.ts +10 -3
  22. package/plugin/dashboard/lib/types.ts +18 -6
  23. package/plugin/hooks/codex-hooks.json +0 -1
  24. package/plugin/opencode/assistant-buffer.ts +108 -0
  25. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  26. package/plugin/opencode/dist/internal.js +12 -0
  27. package/plugin/opencode/dist/payload.js +85 -0
  28. package/plugin/opencode/dist/server.mjs +232 -0
  29. package/plugin/opencode/internal.ts +18 -0
  30. package/plugin/opencode/package.json +3 -0
  31. package/plugin/opencode/payload.ts +90 -0
  32. package/plugin/opencode/server.mts +251 -0
  33. package/plugin/opencode/tsconfig.json +13 -0
  34. package/plugin/pyproject.toml +25 -4
  35. package/plugin/scripts/_lib.sh +5 -1
  36. package/plugin/scripts/backend-service.sh +22 -2
  37. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  38. package/plugin/scripts/hook_entry.sh +1 -1
  39. package/plugin/scripts/opencode-claude-compat +4 -0
  40. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  41. package/plugin/scripts/opencode-claude-compat.js +225 -0
  42. package/plugin/scripts/smart-install.sh +17 -9
  43. package/plugin/src/README.md +1 -1
  44. package/plugin/src/claude_smart/cli.py +498 -12
  45. package/plugin/src/claude_smart/context_format.py +9 -9
  46. package/plugin/src/claude_smart/cs_cite.py +66 -19
  47. package/plugin/src/claude_smart/env_config.py +12 -4
  48. package/plugin/src/claude_smart/events/session_start.py +26 -7
  49. package/plugin/src/claude_smart/events/stop.py +1 -1
  50. package/plugin/src/claude_smart/ids.py +1 -1
  51. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  52. package/plugin/src/claude_smart/runtime.py +7 -1
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +54 -30
  55. package/plugin/vendor/reflexio/README.md +17 -11
  56. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  57. package/plugin/vendor/reflexio/reflexio/README.md +4 -1
  58. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  59. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  60. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  63. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  64. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +212 -52
  65. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  66. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  67. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  68. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  69. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  70. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  71. package/plugin/vendor/reflexio/reflexio/client/client.py +154 -13
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  145. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +246 -10
  146. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  147. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  148. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +17 -5
  149. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  150. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +224 -21
  151. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  152. package/plugin/vendor/reflexio/reflexio/lib/_search.py +52 -25
  153. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +11 -1
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  157. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  158. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  159. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  160. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  161. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +97 -3
  162. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  163. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  164. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +104 -7
  165. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  166. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  167. package/plugin/vendor/reflexio/reflexio/server/README.md +58 -33
  168. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  169. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  170. package/plugin/vendor/reflexio/reflexio/server/api.py +899 -103
  171. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  172. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +4 -2
  173. package/plugin/vendor/reflexio/reflexio/server/auth.py +90 -0
  174. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  175. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  176. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  177. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  178. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  179. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  180. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  181. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  182. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  183. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
  184. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  185. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  186. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  187. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  188. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  189. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  197. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  198. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +278 -0
  199. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  200. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  201. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  202. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  203. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  204. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  205. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  206. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  207. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  208. package/plugin/vendor/reflexio/reflexio/server/services/README.md +28 -11
  209. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  211. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  212. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  214. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  215. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  216. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  217. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  218. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  219. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  220. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  221. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  222. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  223. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  225. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  226. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  227. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  228. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  229. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  230. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  231. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +47 -16
  232. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  233. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  234. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  235. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  237. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +219 -0
  238. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  239. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +18 -11
  241. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  242. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  243. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +334 -178
  244. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  245. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  246. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  247. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  248. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  249. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  250. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  251. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  252. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  253. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  254. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  255. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +31 -22
  256. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  257. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  258. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  259. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  260. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  261. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  262. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  263. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +99 -6
  264. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  265. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  266. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +31 -0
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +16 -2
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +644 -197
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +106 -119
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +630 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  277. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +31 -1369
  278. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +756 -210
  279. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +148 -34
  280. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  281. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  282. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  283. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +150 -23
  289. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  290. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +40 -57
  292. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +4 -677
  296. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +134 -1
  297. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +62 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +372 -34
  311. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  312. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  313. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  314. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  315. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  316. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  317. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  318. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  319. package/scripts/setup-claude-smart.sh +8 -3
  320. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  321. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  322. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  323. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  324. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  325. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  326. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  327. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  328. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  329. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  330. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  331. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  332. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  333. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  334. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  335. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  336. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  337. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  338. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  339. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  340. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  341. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  342. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  343. package/plugin/vendor/reflexio/reflexio/server/_auth.py +0 -25
  344. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  345. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  346. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  347. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  348. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  349. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  350. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  351. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  352. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  353. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -2,11 +2,16 @@ import asyncio
2
2
  import inspect
3
3
  import logging
4
4
  import os
5
+ import time
5
6
  from collections.abc import Callable
6
7
  from concurrent.futures import ThreadPoolExecutor
7
8
  from datetime import UTC, datetime
8
- from typing import Any
9
+ from typing import TYPE_CHECKING, Any
9
10
 
11
+ if TYPE_CHECKING:
12
+ from reflexio.server.extensions import AppContext, CapabilityRegistry
13
+
14
+ from anyio.to_thread import current_default_thread_limiter
10
15
  from fastapi import (
11
16
  APIRouter,
12
17
  BackgroundTasks,
@@ -52,6 +57,7 @@ from reflexio.models.api_schema.retriever_schema import (
52
57
  GetEvaluationResultsViewResponse,
53
58
  GetInteractionsRequest,
54
59
  GetInteractionsViewResponse,
60
+ GetLearningProvenanceRequest,
55
61
  GetPlaybookApplicationStatsRequest,
56
62
  GetPlaybookApplicationStatsResponse,
57
63
  GetProfileStatisticsResponse,
@@ -61,6 +67,7 @@ from reflexio.models.api_schema.retriever_schema import (
61
67
  GetUserPlaybooksRequest,
62
68
  GetUserPlaybooksViewResponse,
63
69
  GetUserProfilesRequest,
70
+ LearningProvenanceViewResponse,
64
71
  ProfileChangeLogViewResponse,
65
72
  RequestDataView,
66
73
  RerankUserProfilesRequest,
@@ -74,6 +81,7 @@ from reflexio.models.api_schema.retriever_schema import (
74
81
  SearchUserProfileRequest,
75
82
  SessionView,
76
83
  SetConfigResponse,
84
+ SourceUserPlaybookProvenanceView,
77
85
  StorageStatsRequest,
78
86
  StorageStatsResponse,
79
87
  UnifiedSearchRequest,
@@ -153,10 +161,10 @@ from reflexio.models.api_schema.ui.converters import (
153
161
  to_user_playbook_view,
154
162
  )
155
163
  from reflexio.models.config_schema import (
164
+ DEFAULT_WINDOW_SIZE,
156
165
  SINGLETON_AGENT_SUCCESS_EVALUATION_NAME,
157
166
  Config,
158
167
  )
159
- from reflexio.server._auth import DEFAULT_ORG_ID, default_get_org_id
160
168
  from reflexio.server.api_endpoints import (
161
169
  account_api,
162
170
  health_api,
@@ -164,6 +172,12 @@ from reflexio.server.api_endpoints import (
164
172
  publisher_api,
165
173
  stall_state_api,
166
174
  )
175
+ from reflexio.server.auth import (
176
+ DEFAULT_ORG_ID,
177
+ default_billing_gate,
178
+ default_get_caller_type,
179
+ default_get_org_id,
180
+ )
167
181
  from reflexio.server.cache.reflexio_cache import (
168
182
  get_reflexio,
169
183
  invalidate_reflexio_cache,
@@ -172,22 +186,34 @@ from reflexio.server.correlation import correlation_id_var, generate_correlation
172
186
  from reflexio.server.operation_limiter import (
173
187
  OperationName,
174
188
  limiter_http_exception,
189
+ log_publish_hardware_capacity,
175
190
  run_with_operation_limit,
176
191
  )
177
- from reflexio.server.services.agent_success_evaluation.group_evaluation_runner import (
178
- run_group_evaluation,
179
- )
180
192
  from reflexio.server.services.agent_success_evaluation.regen_jobs import (
181
193
  REGEN_JOBS,
182
194
  run_regen,
183
195
  )
196
+ from reflexio.server.services.agent_success_evaluation.runner import (
197
+ run_group_evaluation,
198
+ )
199
+ from reflexio.server.services.extractor_interaction_utils import (
200
+ get_effective_source_filter,
201
+ get_extractor_window_params,
202
+ )
203
+ from reflexio.server.tracing import profile_step
184
204
 
185
205
  logger = logging.getLogger(__name__)
186
206
 
187
207
  # Re-exported for backwards compatibility — callers that did
188
208
  # ``from reflexio.server.api import default_get_org_id`` or ``DEFAULT_ORG_ID``
189
209
  # continue to work.
190
- __all__ = ["DEFAULT_ORG_ID", "create_app", "default_get_org_id"]
210
+ __all__ = [
211
+ "DEFAULT_ORG_ID",
212
+ "create_app",
213
+ "default_billing_gate",
214
+ "default_get_caller_type",
215
+ "default_get_org_id",
216
+ ]
191
217
 
192
218
  # Bot protection configuration
193
219
  REQUEST_TIMEOUT_SECONDS = 60
@@ -255,8 +281,41 @@ def get_rate_limit_key(request: Request) -> str:
255
281
  return get_remote_address(request)
256
282
 
257
283
 
284
+ def _storage_backend_name(limiter_obj: Limiter) -> str:
285
+ storage = getattr(limiter_obj, "_storage", None)
286
+ if storage is None:
287
+ return "unknown"
288
+ return storage.__class__.__name__
289
+
290
+
291
+ def _trace_external_rate_limit_backend(limiter_obj: Limiter) -> None:
292
+ """Trace rate-limit storage hits when the backend is an external service."""
293
+ backend = _storage_backend_name(limiter_obj)
294
+ if backend == "MemoryStorage":
295
+ return
296
+
297
+ strategy = getattr(limiter_obj, "limiter", None)
298
+ if strategy is None or getattr(strategy, "_reflexio_traced", False):
299
+ return
300
+
301
+ original_hit = strategy.hit
302
+
303
+ def traced_hit(item: Any, *identifiers: str, cost: int = 1) -> bool:
304
+ with profile_step(
305
+ "rate_limit.backend_hit",
306
+ storage_backend=backend,
307
+ strategy=strategy.__class__.__name__,
308
+ cost=cost,
309
+ ):
310
+ return original_hit(item, *identifiers, cost=cost)
311
+
312
+ strategy.hit = traced_hit
313
+ strategy._reflexio_traced = True
314
+
315
+
258
316
  # Initialize rate limiter
259
317
  limiter = Limiter(key_func=get_rate_limit_key)
318
+ _trace_external_rate_limit_backend(limiter)
260
319
 
261
320
 
262
321
  def _run_limited_api[T](
@@ -448,6 +507,16 @@ class CorrelationIdMiddleware(BaseHTTPMiddleware):
448
507
  ) -> Response:
449
508
  cid = generate_correlation_id()
450
509
  correlation_id_var.set(cid)
510
+ try:
511
+ stats = current_default_thread_limiter().statistics()
512
+ request.state.tp_borrowed = stats.borrowed_tokens
513
+ request.state.tp_total = stats.total_tokens
514
+ request.state.tp_waiting = stats.tasks_waiting
515
+ except Exception as exc: # noqa: BLE001
516
+ logger.debug("Failed to snapshot threadpool limiter stats: %s", exc)
517
+ request.state.tp_borrowed = None
518
+ request.state.tp_total = None
519
+ request.state.tp_waiting = None
451
520
  response = await call_next(request)
452
521
  response.headers["X-Correlation-ID"] = cid
453
522
  return response
@@ -456,6 +525,86 @@ class CorrelationIdMiddleware(BaseHTTPMiddleware):
456
525
  core_router = APIRouter()
457
526
 
458
527
 
528
+ def _stamp_search_dependencies_done(request: Request) -> None:
529
+ """Stamp when dependency resolution reaches its final search dependency."""
530
+ request.state.search_deps_done_monotonic = time.monotonic()
531
+
532
+
533
+ def _meter_applied_learnings(
534
+ *,
535
+ org_id: str,
536
+ caller_type: str,
537
+ surfaced_count: int,
538
+ request_id: str | None = None,
539
+ session_id: str | None = None,
540
+ ) -> None:
541
+ """Emit the ③ Application event via the OSS emission helper.
542
+
543
+ No-op unless a production-agent caller surfaced >= 1 result. The cheap
544
+ caller-type and count guard runs first so the get_reflexio / config lookup
545
+ is skipped on the free paths (dashboard / empty result).
546
+
547
+ Args:
548
+ org_id: Organization ID for the requesting caller.
549
+ caller_type: Resolved caller classification (e.g. ``"production_agent"``).
550
+ surfaced_count: Total number of learnings returned to the caller.
551
+ request_id: Optional request correlation ID from the payload.
552
+ session_id: Optional session ID from the payload.
553
+ """
554
+ if caller_type != "production_agent" or surfaced_count <= 0:
555
+ return
556
+ try:
557
+ from reflexio.server.billing_meter import record_applied_learnings
558
+ from reflexio.server.billing_signals import platform_llm_from_config
559
+
560
+ config = get_reflexio(org_id=org_id).request_context.configurator.get_config()
561
+ record_applied_learnings(
562
+ org_id=org_id,
563
+ surfaced_count=surfaced_count,
564
+ caller_type=caller_type,
565
+ platform_llm=platform_llm_from_config(config),
566
+ platform_storage=None, # resolved enterprise-side at rollup (Phase 1)
567
+ request_id=request_id,
568
+ session_id=session_id,
569
+ )
570
+ except Exception:
571
+ logger.warning(
572
+ "applied-learnings metering failed for org %s", org_id, exc_info=True
573
+ )
574
+
575
+
576
+ def _meter_search_request(
577
+ *,
578
+ org_id: str,
579
+ caller_type: str,
580
+ request_id: str | None = None,
581
+ session_id: str | None = None,
582
+ ) -> None:
583
+ """Emit one production-agent search request metric.
584
+
585
+ Args:
586
+ org_id: Organization ID for the requesting caller.
587
+ caller_type: Resolved caller classification.
588
+ request_id: Optional request correlation ID from the payload.
589
+ session_id: Optional session ID from the payload.
590
+ """
591
+ if caller_type != "production_agent":
592
+ return
593
+ try:
594
+ from reflexio.server.billing_meter import record_search_request
595
+
596
+ record_search_request(
597
+ org_id=org_id,
598
+ caller_type=caller_type,
599
+ request_id=request_id,
600
+ session_id=session_id,
601
+ )
602
+ except Exception:
603
+ logger.warning(
604
+ "search-request metering failed for org %s", org_id, exc_info=True
605
+ )
606
+
607
+
459
608
  @core_router.get("/")
460
609
  def root() -> dict[str, str]:
461
610
  return {
@@ -466,7 +615,7 @@ def root() -> dict[str, str]:
466
615
 
467
616
 
468
617
  @core_router.get("/health")
469
- def health_check() -> dict[str, str]:
618
+ async def health_check() -> dict[str, str]:
470
619
  """Health check endpoint for ECS/container orchestration."""
471
620
  return {"status": "healthy"}
472
621
 
@@ -534,6 +683,7 @@ def publish_user_interaction(
534
683
  background_tasks: BackgroundTasks,
535
684
  org_id: str = Depends(default_get_org_id),
536
685
  wait_for_response: bool = False,
686
+ _gate: None = Depends(default_billing_gate("learnings_generated")), # noqa: B008
537
687
  ) -> PublishUserInteractionResponse:
538
688
  if wait_for_response:
539
689
  # Process synchronously so the caller gets the real result
@@ -551,6 +701,7 @@ def publish_user_interaction(
551
701
  fn=lambda: publisher_api.add_user_interaction(
552
702
  org_id=org_id, request=payload
553
703
  ),
704
+ wait_forever=False,
554
705
  )
555
706
  except TimeoutError:
556
707
  logger.warning(
@@ -647,17 +798,33 @@ def search_user_profiles(
647
798
  request: Request,
648
799
  payload: SearchUserProfileRequest,
649
800
  org_id: str = Depends(default_get_org_id),
801
+ caller_type: str = Depends(default_get_caller_type),
802
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
650
803
  ) -> SearchProfilesViewResponse:
651
804
  response = _run_limited_api(
652
805
  org_id,
653
806
  "search",
654
807
  lambda: get_reflexio(org_id=org_id).search_user_profiles(payload),
655
808
  )
656
- return SearchProfilesViewResponse(
809
+ resp = SearchProfilesViewResponse(
657
810
  success=response.success,
658
811
  user_profiles=[to_profile_view(p) for p in response.user_profiles],
659
812
  msg=response.msg,
660
813
  )
814
+ _meter_search_request(
815
+ org_id=org_id,
816
+ caller_type=caller_type,
817
+ request_id=getattr(payload, "request_id", None),
818
+ session_id=getattr(payload, "session_id", None),
819
+ )
820
+ _meter_applied_learnings(
821
+ org_id=org_id,
822
+ caller_type=caller_type,
823
+ surfaced_count=len(resp.user_profiles),
824
+ request_id=getattr(payload, "request_id", None),
825
+ session_id=getattr(payload, "session_id", None),
826
+ )
827
+ return resp
661
828
 
662
829
 
663
830
  @core_router.post(
@@ -757,6 +924,8 @@ def search_user_playbooks_endpoint(
757
924
  request: Request,
758
925
  payload: SearchUserPlaybookRequest,
759
926
  org_id: str = Depends(default_get_org_id),
927
+ caller_type: str = Depends(default_get_caller_type),
928
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
760
929
  ) -> SearchUserPlaybooksViewResponse:
761
930
  """Search user playbooks with semantic search and advanced filtering.
762
931
 
@@ -767,6 +936,7 @@ def search_user_playbooks_endpoint(
767
936
  request (Request): The HTTP request object (for rate limiting)
768
937
  payload (SearchUserPlaybookRequest): The search request
769
938
  org_id (str): Organization ID
939
+ caller_type (str): Billing caller classification (injected via dependency).
770
940
 
771
941
  Returns:
772
942
  SearchUserPlaybooksViewResponse: Response containing matching user playbooks
@@ -776,11 +946,25 @@ def search_user_playbooks_endpoint(
776
946
  "search",
777
947
  lambda: get_reflexio(org_id=org_id).search_user_playbooks(payload),
778
948
  )
779
- return SearchUserPlaybooksViewResponse(
949
+ resp = SearchUserPlaybooksViewResponse(
780
950
  success=response.success,
781
951
  user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
782
952
  msg=response.msg,
783
953
  )
954
+ _meter_search_request(
955
+ org_id=org_id,
956
+ caller_type=caller_type,
957
+ request_id=getattr(payload, "request_id", None),
958
+ session_id=getattr(payload, "session_id", None),
959
+ )
960
+ _meter_applied_learnings(
961
+ org_id=org_id,
962
+ caller_type=caller_type,
963
+ surfaced_count=len(resp.user_playbooks),
964
+ request_id=getattr(payload, "request_id", None),
965
+ session_id=getattr(payload, "session_id", None),
966
+ )
967
+ return resp
784
968
 
785
969
 
786
970
  @core_router.post(
@@ -793,6 +977,8 @@ def search_agent_playbooks_endpoint(
793
977
  request: Request,
794
978
  payload: SearchAgentPlaybookRequest,
795
979
  org_id: str = Depends(default_get_org_id),
980
+ caller_type: str = Depends(default_get_caller_type),
981
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
796
982
  ) -> SearchAgentPlaybooksViewResponse:
797
983
  """Search agent playbooks with semantic search and advanced filtering.
798
984
 
@@ -803,6 +989,7 @@ def search_agent_playbooks_endpoint(
803
989
  request (Request): The HTTP request object (for rate limiting)
804
990
  payload (SearchAgentPlaybookRequest): The search request
805
991
  org_id (str): Organization ID
992
+ caller_type (str): Billing caller classification (injected via dependency).
806
993
 
807
994
  Returns:
808
995
  SearchAgentPlaybooksViewResponse: Response containing matching agent playbooks
@@ -812,11 +999,25 @@ def search_agent_playbooks_endpoint(
812
999
  "search",
813
1000
  lambda: get_reflexio(org_id=org_id).search_agent_playbooks(payload),
814
1001
  )
815
- return SearchAgentPlaybooksViewResponse(
1002
+ resp = SearchAgentPlaybooksViewResponse(
816
1003
  success=response.success,
817
1004
  agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
818
1005
  msg=response.msg,
819
1006
  )
1007
+ _meter_search_request(
1008
+ org_id=org_id,
1009
+ caller_type=caller_type,
1010
+ request_id=getattr(payload, "request_id", None),
1011
+ session_id=getattr(payload, "session_id", None),
1012
+ )
1013
+ _meter_applied_learnings(
1014
+ org_id=org_id,
1015
+ caller_type=caller_type,
1016
+ surfaced_count=len(resp.agent_playbooks),
1017
+ request_id=getattr(payload, "request_id", None),
1018
+ session_id=getattr(payload, "session_id", None),
1019
+ )
1020
+ return resp
820
1021
 
821
1022
 
822
1023
  @core_router.post(
@@ -828,7 +1029,11 @@ def search_agent_playbooks_endpoint(
828
1029
  def unified_search_endpoint(
829
1030
  request: Request,
830
1031
  payload: UnifiedSearchRequest,
1032
+ background_tasks: BackgroundTasks,
831
1033
  org_id: str = Depends(default_get_org_id),
1034
+ caller_type: str = Depends(default_get_caller_type),
1035
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
1036
+ _deps_done: None = Depends(_stamp_search_dependencies_done),
832
1037
  ) -> UnifiedSearchViewResponse:
833
1038
  """Search across all entity types (profiles, agent playbooks, user playbooks).
834
1039
 
@@ -840,31 +1045,76 @@ def unified_search_endpoint(
840
1045
  request (Request): The HTTP request object (for rate limiting)
841
1046
  payload (UnifiedSearchRequest): The unified search request
842
1047
  org_id (str): Organization ID
1048
+ caller_type (str): Billing caller classification (injected via dependency).
843
1049
 
844
1050
  Returns:
845
1051
  UnifiedSearchViewResponse: Combined search results
846
1052
  """
847
- response = _run_limited_api(
848
- org_id,
849
- "search",
850
- lambda: get_reflexio(org_id=org_id).unified_search(payload, org_id=org_id),
851
- )
852
- return UnifiedSearchViewResponse(
853
- success=response.success,
854
- profiles=[to_profile_view(p) for p in response.profiles],
855
- agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
856
- user_playbooks=[to_user_playbook_view(rf) for rf in response.user_playbooks],
857
- reformulated_query=response.reformulated_query,
858
- msg=response.msg,
859
- agent_trace=response.agent_trace,
860
- rehydrated_text=response.rehydrated_text,
1053
+ deps_done = getattr(request.state, "search_deps_done_monotonic", None)
1054
+ deps_to_body_ms = (
1055
+ int((time.monotonic() - deps_done) * 1000) if deps_done is not None else None
861
1056
  )
1057
+ with profile_step(
1058
+ "search.endpoint",
1059
+ enabled=bool(payload.enable_reformulation),
1060
+ has_conversation_history=bool(payload.conversation_history),
1061
+ search_mode=payload.search_mode,
1062
+ ) as endpoint_span:
1063
+ endpoint_span.set_data("deps_to_body_ms", deps_to_body_ms)
1064
+ endpoint_span.set_data(
1065
+ "tp_borrowed", getattr(request.state, "tp_borrowed", None)
1066
+ )
1067
+ endpoint_span.set_data("tp_total", getattr(request.state, "tp_total", None))
1068
+ endpoint_span.set_data("tp_waiting", getattr(request.state, "tp_waiting", None))
1069
+
1070
+ def run_search() -> Any:
1071
+ with profile_step("search.reflexio_cache"):
1072
+ reflexio = get_reflexio(org_id=org_id)
1073
+ return reflexio.unified_search(payload, org_id=org_id)
1074
+
1075
+ response = _run_limited_api(org_id, "search", run_search)
1076
+ with profile_step("search.response_view"):
1077
+ resp = UnifiedSearchViewResponse(
1078
+ success=response.success,
1079
+ profiles=[to_profile_view(p) for p in response.profiles],
1080
+ agent_playbooks=[
1081
+ to_agent_playbook_view(fb) for fb in response.agent_playbooks
1082
+ ],
1083
+ user_playbooks=[
1084
+ to_user_playbook_view(rf) for rf in response.user_playbooks
1085
+ ],
1086
+ reformulated_query=response.reformulated_query,
1087
+ msg=response.msg,
1088
+ agent_trace=response.agent_trace,
1089
+ rehydrated_text=response.rehydrated_text,
1090
+ )
1091
+ background_tasks.add_task(
1092
+ _meter_search_request,
1093
+ org_id=org_id,
1094
+ caller_type=caller_type,
1095
+ request_id=getattr(payload, "request_id", None),
1096
+ session_id=getattr(payload, "session_id", None),
1097
+ )
1098
+ background_tasks.add_task(
1099
+ _meter_applied_learnings,
1100
+ org_id=org_id,
1101
+ caller_type=caller_type,
1102
+ surfaced_count=len(resp.profiles)
1103
+ + len(resp.agent_playbooks)
1104
+ + len(resp.user_playbooks),
1105
+ request_id=getattr(payload, "request_id", None),
1106
+ session_id=getattr(payload, "session_id", None),
1107
+ )
1108
+ return resp
862
1109
 
863
1110
 
864
1111
  @core_router.get("/api/profile_change_log", response_model=ProfileChangeLogViewResponse)
865
1112
  def get_profile_change_log(
866
1113
  org_id: str = Depends(default_get_org_id),
867
1114
  ) -> ProfileChangeLogViewResponse:
1115
+ # Serves the reconstructed profile change log (rebuilt from lineage events). The
1116
+ # legacy `profile_change_logs` table is no longer written; see
1117
+ # reconstruct_profile_change_log in lib/_profiles.py.
868
1118
  response = get_reflexio(org_id=org_id).get_profile_change_logs()
869
1119
  return ProfileChangeLogViewResponse(
870
1120
  success=response.success,
@@ -1269,6 +1519,307 @@ def get_requests_endpoint(
1269
1519
  )
1270
1520
 
1271
1521
 
1522
+ def _learning_provenance_error(
1523
+ payload: GetLearningProvenanceRequest,
1524
+ msg: str,
1525
+ ) -> LearningProvenanceViewResponse:
1526
+ return LearningProvenanceViewResponse(
1527
+ success=False,
1528
+ target_kind=payload.kind,
1529
+ target_id=payload.id,
1530
+ provenance_status="unavailable",
1531
+ msg=msg,
1532
+ )
1533
+
1534
+
1535
+ def _parse_learning_target_int(
1536
+ payload: GetLearningProvenanceRequest,
1537
+ ) -> int | LearningProvenanceViewResponse:
1538
+ try:
1539
+ return int(payload.id)
1540
+ except ValueError:
1541
+ return _learning_provenance_error(
1542
+ payload,
1543
+ f"{payload.kind} id must be an integer",
1544
+ )
1545
+
1546
+
1547
+ def _sort_interactions_by_time(interactions: list[Any]) -> list[Any]:
1548
+ return sorted(
1549
+ interactions,
1550
+ key=lambda i: (
1551
+ getattr(i, "created_at", 0) or 0,
1552
+ getattr(i, "interaction_id", 0),
1553
+ ),
1554
+ )
1555
+
1556
+
1557
+ def _interaction_views(interactions: list[Any]) -> list[Any]:
1558
+ return [to_interaction_view(i) for i in _sort_interactions_by_time(interactions)]
1559
+
1560
+
1561
+ def _effective_profile_provenance_window(
1562
+ reflexio: Any, trigger_source: str | None
1563
+ ) -> tuple[int, list[str] | None]:
1564
+ config = reflexio.request_context.configurator.get_config()
1565
+ profile_config = getattr(config, "profile_extractor_config", None)
1566
+ if profile_config is None:
1567
+ return getattr(config, "window_size", DEFAULT_WINDOW_SIZE), None
1568
+
1569
+ window_size, _ = get_extractor_window_params(
1570
+ profile_config,
1571
+ getattr(config, "window_size", None),
1572
+ getattr(config, "stride_size", None),
1573
+ )
1574
+ should_skip, source_filter = get_effective_source_filter(
1575
+ profile_config,
1576
+ trigger_source,
1577
+ )
1578
+ if should_skip:
1579
+ return window_size, None
1580
+ return window_size, source_filter
1581
+
1582
+
1583
+ def _get_profile_learning_provenance(
1584
+ payload: GetLearningProvenanceRequest,
1585
+ reflexio: Any,
1586
+ ) -> LearningProvenanceViewResponse:
1587
+ storage = reflexio.request_context.storage
1588
+ profile = storage.get_profile_by_id(payload.id)
1589
+ if profile is None:
1590
+ return _learning_provenance_error(payload, "Profile not found")
1591
+
1592
+ if profile.source_interaction_ids:
1593
+ interactions = storage.get_interactions_by_ids(profile.source_interaction_ids)
1594
+ return LearningProvenanceViewResponse(
1595
+ success=True,
1596
+ target_kind=payload.kind,
1597
+ target_id=payload.id,
1598
+ provenance_status="exact",
1599
+ trigger_request_id=profile.generated_from_request_id,
1600
+ interactions=_interaction_views(interactions),
1601
+ )
1602
+
1603
+ if not profile.generated_from_request_id:
1604
+ return _learning_provenance_error(
1605
+ payload,
1606
+ "Profile has no generation request for provenance reconstruction",
1607
+ )
1608
+
1609
+ trigger_request = storage.get_request(profile.generated_from_request_id)
1610
+ if trigger_request is None:
1611
+ return _learning_provenance_error(
1612
+ payload,
1613
+ "Profile generation request was not found",
1614
+ )
1615
+
1616
+ trigger_interactions = storage.get_interactions_by_request_ids(
1617
+ [profile.generated_from_request_id]
1618
+ )
1619
+ anchor_time = (
1620
+ max(i.created_at for i in trigger_interactions)
1621
+ if trigger_interactions
1622
+ else trigger_request.created_at
1623
+ )
1624
+ window_size, source_filter = _effective_profile_provenance_window(
1625
+ reflexio,
1626
+ trigger_request.source,
1627
+ )
1628
+ _, interactions = storage.get_last_k_interactions_grouped(
1629
+ user_id=profile.user_id,
1630
+ k=window_size,
1631
+ sources=source_filter,
1632
+ end_time=anchor_time,
1633
+ )
1634
+ return LearningProvenanceViewResponse(
1635
+ success=True,
1636
+ target_kind=payload.kind,
1637
+ target_id=payload.id,
1638
+ provenance_status="best_effort" if interactions else "unavailable",
1639
+ trigger_request_id=profile.generated_from_request_id,
1640
+ interactions=_interaction_views(interactions),
1641
+ msg=None if interactions else "No interactions found for reconstructed window",
1642
+ )
1643
+
1644
+
1645
+ def _get_user_playbook_learning_provenance(
1646
+ payload: GetLearningProvenanceRequest,
1647
+ reflexio: Any,
1648
+ ) -> LearningProvenanceViewResponse:
1649
+ parsed_id = _parse_learning_target_int(payload)
1650
+ if isinstance(parsed_id, LearningProvenanceViewResponse):
1651
+ return parsed_id
1652
+
1653
+ storage = reflexio.request_context.storage
1654
+ playbook = storage.get_user_playbook_by_id(parsed_id)
1655
+ if playbook is None:
1656
+ return _learning_provenance_error(payload, "User playbook not found")
1657
+
1658
+ status = "exact"
1659
+ if playbook.source_interaction_ids:
1660
+ interactions = storage.get_interactions_by_ids(playbook.source_interaction_ids)
1661
+ elif playbook.request_id:
1662
+ status = "best_effort"
1663
+ interactions = storage.get_interactions_by_request_ids([playbook.request_id])
1664
+ else:
1665
+ status = "unavailable"
1666
+ interactions = []
1667
+
1668
+ return LearningProvenanceViewResponse(
1669
+ success=True,
1670
+ target_kind=payload.kind,
1671
+ target_id=payload.id,
1672
+ provenance_status=status if interactions else "unavailable",
1673
+ trigger_request_id=playbook.request_id,
1674
+ interactions=_interaction_views(interactions),
1675
+ msg=None if interactions else "No interactions found for this user playbook",
1676
+ )
1677
+
1678
+
1679
+ def _get_agent_playbook_learning_provenance(
1680
+ payload: GetLearningProvenanceRequest,
1681
+ reflexio: Any,
1682
+ ) -> LearningProvenanceViewResponse:
1683
+ parsed_id = _parse_learning_target_int(payload)
1684
+ if isinstance(parsed_id, LearningProvenanceViewResponse):
1685
+ return parsed_id
1686
+
1687
+ storage = reflexio.request_context.storage
1688
+ agent_playbook = storage.get_agent_playbook_by_id(parsed_id)
1689
+ if agent_playbook is None:
1690
+ return _learning_provenance_error(payload, "Agent playbook not found")
1691
+
1692
+ windows = storage.get_source_windows_for_agent_playbook(parsed_id)
1693
+ if not windows:
1694
+ return LearningProvenanceViewResponse(
1695
+ success=True,
1696
+ target_kind=payload.kind,
1697
+ target_id=payload.id,
1698
+ provenance_status="unavailable",
1699
+ msg="No source user playbooks are recorded for this agent playbook",
1700
+ )
1701
+
1702
+ user_playbook_ids = [window.user_playbook_id for window in windows]
1703
+ user_playbooks = storage.get_user_playbooks_by_ids_any_user(
1704
+ user_playbook_ids,
1705
+ status_filter=None,
1706
+ )
1707
+ user_playbooks_by_id = {
1708
+ playbook.user_playbook_id: playbook for playbook in user_playbooks
1709
+ }
1710
+
1711
+ interaction_ids_to_fetch: set[int] = set()
1712
+ request_ids_to_fetch: set[str] = set()
1713
+ group_sources: dict[int, tuple[list[int], str | None, bool]] = {}
1714
+ for window in windows:
1715
+ source_user_playbook = user_playbooks_by_id.get(window.user_playbook_id)
1716
+ if source_user_playbook is None:
1717
+ continue
1718
+
1719
+ source_ids = list(window.source_interaction_ids)
1720
+ request_id: str | None = None
1721
+ uses_fallback = False
1722
+ if not source_ids and source_user_playbook.source_interaction_ids:
1723
+ source_ids = list(source_user_playbook.source_interaction_ids)
1724
+ elif not source_ids and source_user_playbook.request_id:
1725
+ request_id = source_user_playbook.request_id
1726
+ uses_fallback = True
1727
+ elif not source_ids:
1728
+ uses_fallback = True
1729
+
1730
+ interaction_ids_to_fetch.update(source_ids)
1731
+ if request_id:
1732
+ request_ids_to_fetch.add(request_id)
1733
+ group_sources[window.user_playbook_id] = (source_ids, request_id, uses_fallback)
1734
+
1735
+ interactions_by_id = (
1736
+ {
1737
+ interaction.interaction_id: interaction
1738
+ for interaction in storage.get_interactions_by_ids(
1739
+ sorted(interaction_ids_to_fetch)
1740
+ )
1741
+ }
1742
+ if interaction_ids_to_fetch
1743
+ else {}
1744
+ )
1745
+ request_interactions: dict[str, list[Any]] = {}
1746
+ if request_ids_to_fetch:
1747
+ for interaction in storage.get_interactions_by_request_ids(
1748
+ sorted(request_ids_to_fetch)
1749
+ ):
1750
+ request_interactions.setdefault(interaction.request_id, []).append(
1751
+ interaction
1752
+ )
1753
+
1754
+ groups: list[SourceUserPlaybookProvenanceView] = []
1755
+ used_fallback = False
1756
+ found_interactions = False
1757
+ for window in windows:
1758
+ source_user_playbook = user_playbooks_by_id.get(window.user_playbook_id)
1759
+ if source_user_playbook is None:
1760
+ used_fallback = True
1761
+ continue
1762
+
1763
+ source_ids, request_id, uses_fallback = group_sources.get(
1764
+ window.user_playbook_id,
1765
+ ([], None, True),
1766
+ )
1767
+ used_fallback = used_fallback or uses_fallback
1768
+ interactions = (
1769
+ [interactions_by_id[i] for i in source_ids if i in interactions_by_id]
1770
+ if source_ids
1771
+ else request_interactions.get(request_id or "", [])
1772
+ )
1773
+
1774
+ found_interactions = found_interactions or bool(interactions)
1775
+ groups.append(
1776
+ SourceUserPlaybookProvenanceView(
1777
+ user_playbook=to_user_playbook_view(source_user_playbook),
1778
+ interactions=_interaction_views(interactions),
1779
+ source_interaction_ids=source_ids,
1780
+ )
1781
+ )
1782
+
1783
+ if not groups:
1784
+ return _learning_provenance_error(
1785
+ payload,
1786
+ "Recorded source user playbooks were not found",
1787
+ )
1788
+
1789
+ return LearningProvenanceViewResponse(
1790
+ success=True,
1791
+ target_kind=payload.kind,
1792
+ target_id=payload.id,
1793
+ provenance_status=(
1794
+ "exact"
1795
+ if found_interactions and not used_fallback
1796
+ else "best_effort"
1797
+ if found_interactions
1798
+ else "unavailable"
1799
+ ),
1800
+ source_user_playbooks=groups,
1801
+ msg=None if found_interactions else "No source interactions were found",
1802
+ )
1803
+
1804
+
1805
+ @core_router.post(
1806
+ "/api/get_learning_provenance",
1807
+ response_model=LearningProvenanceViewResponse,
1808
+ response_model_exclude_none=True,
1809
+ )
1810
+ def get_learning_provenance(
1811
+ payload: GetLearningProvenanceRequest,
1812
+ org_id: str = Depends(default_get_org_id),
1813
+ ) -> LearningProvenanceViewResponse:
1814
+ """Return read-only learning provenance for a generated learning row."""
1815
+ reflexio = get_reflexio(org_id=org_id)
1816
+ if payload.kind == "profile":
1817
+ return _get_profile_learning_provenance(payload, reflexio)
1818
+ if payload.kind == "user_playbook":
1819
+ return _get_user_playbook_learning_provenance(payload, reflexio)
1820
+ return _get_agent_playbook_learning_provenance(payload, reflexio)
1821
+
1822
+
1272
1823
  @core_router.post(
1273
1824
  "/api/get_profiles",
1274
1825
  response_model=GetProfilesViewResponse,
@@ -1296,6 +1847,14 @@ def get_all_profiles(
1296
1847
  request: Request,
1297
1848
  limit: int = 100,
1298
1849
  status_filter: str | None = None,
1850
+ profile_id: str | None = None,
1851
+ user_id: str | None = None,
1852
+ query: str | None = None,
1853
+ source: str | None = None,
1854
+ profile_time_to_live: str | None = None,
1855
+ start_time: int | None = None,
1856
+ end_time: int | None = None,
1857
+ include_tombstones: bool = False,
1299
1858
  org_id: str = Depends(default_get_org_id),
1300
1859
  ) -> GetProfilesViewResponse:
1301
1860
  """Get all user profiles across all users.
@@ -1303,6 +1862,15 @@ def get_all_profiles(
1303
1862
  Args:
1304
1863
  limit (int, optional): Maximum number of profiles to return. Defaults to 100.
1305
1864
  status_filter (str, optional): Filter by profile status. Can be "current", "pending", or "archived".
1865
+ profile_id (str, optional): Exact profile ID to retrieve.
1866
+ user_id (str, optional): Exact user ID to filter by.
1867
+ query (str, optional): Case-insensitive text filter across visible fields.
1868
+ source (str, optional): Exact profile source to filter by.
1869
+ profile_time_to_live (str, optional): Exact TTL value to filter by.
1870
+ start_time (int, optional): Minimum last-modified epoch seconds.
1871
+ end_time (int, optional): Maximum last-modified epoch seconds.
1872
+ include_tombstones (bool, optional): Include merged/superseded rows when
1873
+ looking up a specific profile_id.
1306
1874
  org_id (str): Organization ID
1307
1875
 
1308
1876
  Returns:
@@ -1310,6 +1878,24 @@ def get_all_profiles(
1310
1878
  """
1311
1879
  reflexio = get_reflexio(org_id=org_id)
1312
1880
 
1881
+ if profile_id and include_tombstones:
1882
+ storage = reflexio.request_context.storage
1883
+ if storage is None:
1884
+ return GetProfilesViewResponse(
1885
+ success=True,
1886
+ user_profiles=[],
1887
+ msg="Storage is not configured",
1888
+ )
1889
+ profile = storage.get_profile_by_id(
1890
+ profile_id, include_tombstones=include_tombstones
1891
+ )
1892
+ profiles = [profile] if profile else []
1893
+ return GetProfilesViewResponse(
1894
+ success=True,
1895
+ user_profiles=[to_profile_view(p) for p in profiles],
1896
+ msg=f"Found {len(profiles)} profile(s)",
1897
+ )
1898
+
1313
1899
  # Map status_filter string to Status list
1314
1900
  status_filter_list = None
1315
1901
  if status_filter == "current":
@@ -1319,7 +1905,17 @@ def get_all_profiles(
1319
1905
  elif status_filter == "archived":
1320
1906
  status_filter_list = [Status.ARCHIVED]
1321
1907
 
1322
- response = reflexio.get_all_profiles(limit=limit, status_filter=status_filter_list) # type: ignore[reportArgumentType]
1908
+ response = reflexio.get_all_profiles(
1909
+ limit=limit,
1910
+ status_filter=status_filter_list, # type: ignore[reportArgumentType]
1911
+ user_id=user_id,
1912
+ profile_id=profile_id,
1913
+ query=query,
1914
+ source=source,
1915
+ profile_time_to_live=profile_time_to_live,
1916
+ start_time=start_time,
1917
+ end_time=end_time,
1918
+ )
1323
1919
  return GetProfilesViewResponse(
1324
1920
  success=response.success,
1325
1921
  user_profiles=[to_profile_view(p) for p in response.user_profiles],
@@ -1378,17 +1974,30 @@ def set_config(
1378
1974
  """Set configuration for the organization.
1379
1975
 
1380
1976
  Args:
1381
- config (Config): The configuration to set
1977
+ config (dict[str, Any]): The configuration payload to set
1382
1978
  org_id (str): Organization ID
1383
1979
 
1384
1980
  Returns:
1385
1981
  dict: Response containing success status and message
1386
1982
  """
1983
+ from pydantic import ValidationError
1984
+
1387
1985
  # Create Reflexio instance to access the configurator through request_context
1388
1986
  reflexio = get_reflexio(org_id=org_id)
1987
+ configurator = reflexio.request_context.configurator
1988
+ normalized_config = configurator.normalize_config_payload(config)
1989
+ if not isinstance(normalized_config, dict):
1990
+ normalized_config = config
1991
+ try:
1992
+ Config.model_validate(normalized_config)
1993
+ except ValidationError as exc:
1994
+ raise HTTPException(
1995
+ status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
1996
+ detail=exc.errors(),
1997
+ ) from exc
1389
1998
 
1390
1999
  # Set the config using Reflexio's set_config method
1391
- response = reflexio.set_config(config)
2000
+ response = reflexio.set_config(normalized_config)
1392
2001
 
1393
2002
  # Invalidate cache on successful config change to ensure fresh instance next request
1394
2003
  if response.success:
@@ -1568,26 +2177,40 @@ def get_user_playbooks(
1568
2177
  response_model=GetAgentPlaybooksViewResponse,
1569
2178
  response_model_exclude_none=True,
1570
2179
  )
2180
+ @limiter.limit("120/minute") # Rate limit for read operations
1571
2181
  def get_agent_playbooks(
1572
- request: GetAgentPlaybooksRequest,
2182
+ request: Request,
2183
+ payload: GetAgentPlaybooksRequest,
1573
2184
  org_id: str = Depends(default_get_org_id),
2185
+ caller_type: str = Depends(default_get_caller_type),
2186
+ _gate: None = Depends(default_billing_gate("application")), # noqa: B008
1574
2187
  ) -> GetAgentPlaybooksViewResponse:
1575
2188
  """Get agent playbooks with internal fields filtered out.
1576
2189
 
1577
2190
  Args:
1578
- request (GetAgentPlaybooksRequest): The get request
2191
+ request (Request): The HTTP request object (for rate limiting)
2192
+ payload (GetAgentPlaybooksRequest): The get request
1579
2193
  org_id (str): Organization ID
2194
+ caller_type (str): Billing caller classification (injected via dependency).
1580
2195
 
1581
2196
  Returns:
1582
2197
  GetAgentPlaybooksViewResponse: Response containing agent playbooks without internal fields
1583
2198
  """
1584
2199
  reflexio = get_reflexio(org_id=org_id)
1585
- response = reflexio.get_agent_playbooks(request)
1586
- return GetAgentPlaybooksViewResponse(
2200
+ response = reflexio.get_agent_playbooks(payload)
2201
+ resp = GetAgentPlaybooksViewResponse(
1587
2202
  success=response.success,
1588
2203
  agent_playbooks=[to_agent_playbook_view(fb) for fb in response.agent_playbooks],
1589
2204
  msg=response.msg,
1590
2205
  )
2206
+ _meter_applied_learnings(
2207
+ org_id=org_id,
2208
+ caller_type=caller_type,
2209
+ surfaced_count=len(resp.agent_playbooks),
2210
+ request_id=getattr(payload, "request_id", None),
2211
+ session_id=getattr(payload, "session_id", None),
2212
+ )
2213
+ return resp
1591
2214
 
1592
2215
 
1593
2216
  @core_router.post(
@@ -2094,10 +2717,8 @@ def _read_grade_on_demand_cache(
2094
2717
  def _resolve_session_user_id(storage: Any, session_id: str) -> str | None:
2095
2718
  """Look up the user_id that owns a session_id without requiring it as input.
2096
2719
 
2097
- Uses ``get_sessions(session_id=...)`` because it's the only storage
2098
- method on ``BaseStorage`` that accepts session_id alone every other
2099
- request-fetcher requires a user_id paired with it. Returns None when
2100
- the session has no requests, signalling NO_REQUESTS to the caller.
2720
+ Uses the first-request bulk helper even for this single-session path so the
2721
+ lookup can use the same indexed query shape as evaluation overview.
2101
2722
 
2102
2723
  Args:
2103
2724
  storage: The request's storage backend.
@@ -2107,17 +2728,17 @@ def _resolve_session_user_id(storage: Any, session_id: str) -> str | None:
2107
2728
  str | None: The user_id of the earliest request in the session,
2108
2729
  or None when no requests exist.
2109
2730
  """
2110
- sessions = storage.get_sessions(session_id=session_id, top_k=100)
2111
- rows = sessions.get(session_id) or []
2112
- if not rows:
2731
+ first_requests = storage.get_first_requests_by_session_ids([session_id])
2732
+ first = first_requests.get(session_id)
2733
+ if first is None:
2113
2734
  return None
2114
- earliest = min(rows, key=lambda r: r.request.created_at)
2115
- return earliest.request.user_id
2735
+ return first.user_id
2116
2736
 
2117
2737
 
2118
2738
  def _find_fresh_result_id(
2119
2739
  storage: Any,
2120
2740
  *,
2741
+ user_id: str,
2121
2742
  session_id: str,
2122
2743
  agent_version: str,
2123
2744
  evaluation_name: str,
@@ -2125,12 +2746,12 @@ def _find_fresh_result_id(
2125
2746
  ) -> int | None:
2126
2747
  """Locate the result_id written by the most-recent grade for this session.
2127
2748
 
2128
- The runner writes rows but doesn't return the id. Reading back through
2129
- ``get_agent_success_evaluation_results`` matches the pattern used by
2130
- the regen worker's prior-row capture (group_evaluation_runner step 5).
2749
+ The runner writes rows but doesn't return the id. Use the targeted result-id
2750
+ lookup so this path does not scan broad evaluation windows.
2131
2751
 
2132
2752
  Args:
2133
2753
  storage: The request's storage backend.
2754
+ user_id (str): The user whose session slice was graded.
2134
2755
  session_id (str): The graded session.
2135
2756
  agent_version (str): The version dimension.
2136
2757
  evaluation_name (str): Evaluator/result namespace to isolate readback.
@@ -2140,20 +2761,16 @@ def _find_fresh_result_id(
2140
2761
  int | None: result_id of the latest matching row, or None if the
2141
2762
  runner wrote nothing.
2142
2763
  """
2143
- rows = storage.get_agent_success_evaluation_results(
2144
- limit=1000, agent_version=agent_version
2145
- )
2146
- matched = [
2147
- r
2148
- for r in rows
2149
- if r.session_id == session_id
2150
- and r.evaluation_name == evaluation_name
2151
- and r.result_id not in previous_result_ids
2152
- ]
2153
- if not matched:
2764
+ result_ids = storage.get_agent_success_evaluation_result_ids(
2765
+ user_id=user_id,
2766
+ session_id=session_id,
2767
+ evaluation_name=evaluation_name,
2768
+ agent_version=agent_version,
2769
+ )
2770
+ fresh_result_ids = [rid for rid in result_ids if rid not in previous_result_ids]
2771
+ if not fresh_result_ids:
2154
2772
  return None
2155
- latest = max(matched, key=lambda r: r.created_at)
2156
- return latest.result_id
2773
+ return max(fresh_result_ids)
2157
2774
 
2158
2775
 
2159
2776
  @core_router.post(
@@ -2224,13 +2841,14 @@ def grade_on_demand(
2224
2841
  skipped_reason="NO_REQUESTS",
2225
2842
  )
2226
2843
 
2227
- previous_result_ids = {
2228
- r.result_id
2229
- for r in storage.get_agent_success_evaluation_results(
2230
- limit=1000, agent_version=payload.agent_version
2844
+ previous_result_ids = set(
2845
+ storage.get_agent_success_evaluation_result_ids(
2846
+ user_id=user_id,
2847
+ session_id=payload.session_id,
2848
+ evaluation_name=evaluation_name,
2849
+ agent_version=payload.agent_version,
2231
2850
  )
2232
- if r.session_id == payload.session_id and r.evaluation_name == evaluation_name
2233
- }
2851
+ )
2234
2852
 
2235
2853
  # Two operation_state rows are intentionally written for this session:
2236
2854
  # 1) `grade_on_demand::{org_id}::{session_id}::{agent_version}::{evaluation_name}`
@@ -2255,6 +2873,7 @@ def grade_on_demand(
2255
2873
 
2256
2874
  result_id = _find_fresh_result_id(
2257
2875
  storage,
2876
+ user_id=user_id,
2258
2877
  session_id=payload.session_id,
2259
2878
  agent_version=payload.agent_version,
2260
2879
  evaluation_name=evaluation_name,
@@ -2307,8 +2926,8 @@ def get_recent_shadow_comparisons(
2307
2926
  Filters to the org's currently pinned
2308
2927
  ``Config.shadow_comparison_judge_prompt_version`` so verdicts produced
2309
2928
  under an older rubric do not mix into the drawer or the Top 10
2310
- disagreements widget. Storage returns verdicts in ascending ``created_at``
2311
- order; we reverse to "newest first" and cap at ``limit``.
2929
+ disagreements widget. Storage returns verdicts newest-first and caps the
2930
+ read at ``limit``.
2312
2931
 
2313
2932
  Args:
2314
2933
  limit (int): Max verdicts to return. Clamped to ``[1, 100]``.
@@ -2340,10 +2959,11 @@ def get_recent_shadow_comparisons(
2340
2959
 
2341
2960
  now = int(datetime.now(UTC).timestamp())
2342
2961
  try:
2343
- verdicts = storage.get_shadow_comparison_verdicts(
2962
+ verdicts = storage.get_recent_shadow_comparison_verdicts(
2344
2963
  from_ts=now - _RECENT_SHADOW_COMPARISONS_LOOKBACK_SECONDS,
2345
2964
  to_ts=now,
2346
2965
  judge_prompt_version=pinned_version,
2966
+ limit=clamped_limit,
2347
2967
  )
2348
2968
  except NotImplementedError:
2349
2969
  # Backends that don't support shadow verdicts (e.g., Disk) should
@@ -2351,9 +2971,7 @@ def get_recent_shadow_comparisons(
2351
2971
  # "no data yet" in the UI.
2352
2972
  return GetRecentShadowComparisonsResponse(verdicts=[])
2353
2973
 
2354
- # Storage contract returns ascending — flip to "newest first" and cap.
2355
- newest_first = list(reversed(verdicts))[:clamped_limit]
2356
- return GetRecentShadowComparisonsResponse(verdicts=newest_first)
2974
+ return GetRecentShadowComparisonsResponse(verdicts=verdicts)
2357
2975
 
2358
2976
 
2359
2977
  @core_router.post(
@@ -2709,11 +3327,107 @@ def _add_openapi_security(app: FastAPI) -> None:
2709
3327
  app.openapi = custom_openapi # type: ignore[method-assign]
2710
3328
 
2711
3329
 
3330
+ def _resolve_lifespan_org_id(get_org_id: Callable[..., str] | None) -> str:
3331
+ """Resolve the bootstrap org ID for lifespan schedulers without a request context.
3332
+
3333
+ Args:
3334
+ get_org_id (Callable[..., str] | None): Custom org-ID dependency, or None for
3335
+ the default single-tenant mode.
3336
+
3337
+ Returns:
3338
+ str: The resolved org ID string.
3339
+ """
3340
+ from reflexio.server.auth import default_get_org_id
3341
+
3342
+ if get_org_id is None:
3343
+ return default_get_org_id()
3344
+ try:
3345
+ signature = inspect.signature(get_org_id)
3346
+ except (TypeError, ValueError):
3347
+ return default_get_org_id()
3348
+ if signature.parameters:
3349
+ return default_get_org_id()
3350
+ try:
3351
+ return str(get_org_id())
3352
+ except Exception:
3353
+ logger.exception("Failed to resolve lifespan org_id; using default org")
3354
+ return default_get_org_id()
3355
+
3356
+
3357
+ def _wire_capabilities(
3358
+ app: FastAPI,
3359
+ capabilities: "CapabilityRegistry | None",
3360
+ mount_data_plane: bool,
3361
+ additional_routers: list[APIRouter] | None = None,
3362
+ ) -> None:
3363
+ """Wire capability routers, services, and hooks into the app at construction time.
3364
+
3365
+ No-op when ``capabilities`` is None.
3366
+
3367
+ The deployment role passed to each capability's ``routers(role)`` is
3368
+ sourced from ``capabilities.role`` when set (threaded in by the enterprise
3369
+ composition root via ``build_registry(deployment_role())``). When
3370
+ ``capabilities.role`` is ``None`` the role falls back to the
3371
+ ``mount_data_plane`` derivation (``"all"`` when True, ``"control-plane"``
3372
+ when False), preserving the existing OSS-only behaviour.
3373
+
3374
+ Args:
3375
+ app (FastAPI): The application instance to wire into.
3376
+ capabilities (CapabilityRegistry | None): Capabilities to install, or None.
3377
+ mount_data_plane (bool): Whether the data-plane role is active.
3378
+ additional_routers (list[APIRouter] | None): Routers already mounted via
3379
+ ``additional_routers``; used to detect and reject double-mounts at boot.
3380
+
3381
+ Raises:
3382
+ ValueError: If a capability's router object is also present in
3383
+ ``additional_routers`` — each router must be mounted exactly once.
3384
+ """
3385
+ if capabilities is None:
3386
+ return
3387
+ from reflexio.server.auth import default_billing_gate
3388
+ from reflexio.server.extensions import HookRegistry
3389
+
3390
+ if capabilities.role is not None:
3391
+ role = capabilities.role
3392
+ else:
3393
+ role = "all" if mount_data_plane else "control-plane"
3394
+ if capabilities.configurator_class is not None:
3395
+ from reflexio.server.services.configurator.configurator import (
3396
+ set_configurator_class,
3397
+ )
3398
+
3399
+ set_configurator_class(capabilities.configurator_class)
3400
+ if capabilities.billing_gate is not None:
3401
+ for line in ("application", "learnings_generated"):
3402
+ app.dependency_overrides[default_billing_gate(line)] = (
3403
+ capabilities.billing_gate(line)
3404
+ )
3405
+ # HookRegistry is a stateless facade: its methods configure process-global
3406
+ # tracer/usage-recorder/retrieval-capture singletons; the object needn't be stored.
3407
+ hooks = HookRegistry()
3408
+ additional = additional_routers or []
3409
+ for cap in capabilities.capabilities:
3410
+ cap.install_services()
3411
+ cap.install_hooks(hooks)
3412
+ for r in cap.routers(role):
3413
+ if any(r is a for a in additional):
3414
+ raise ValueError(
3415
+ f"router for capability {cap.name!r} is mounted both via the "
3416
+ f"registry and additional_routers; mount it exactly once"
3417
+ )
3418
+ app.include_router(r)
3419
+
3420
+
2712
3421
  def create_app(
2713
3422
  get_org_id: Callable[..., str] | None = None,
2714
3423
  additional_routers: list[APIRouter] | None = None,
2715
3424
  middleware_config: dict | None = None,
2716
3425
  require_auth: bool = False,
3426
+ get_caller_type: Callable[..., str] | None = None,
3427
+ get_billing_gate: Callable[[str], Callable[..., None]] | None = None,
3428
+ mount_data_plane: bool = True,
3429
+ capabilities: "CapabilityRegistry | None" = None,
3430
+ app_context_factory: "Callable[[], AppContext] | None" = None,
2717
3431
  ) -> FastAPI:
2718
3432
  """Factory to create a FastAPI app.
2719
3433
 
@@ -2724,54 +3438,114 @@ def create_app(
2724
3438
  middleware_config: Optional middleware overrides (not used yet, reserved for future).
2725
3439
  require_auth: When True, declares a Bearer security scheme in the OpenAPI spec
2726
3440
  so Swagger UI shows lock icons and the Authorize button works.
3441
+ get_caller_type: Custom dependency for classifying the caller (e.g., production
3442
+ agent vs dashboard). When provided, overrides the default_get_caller_type
3443
+ dependency globally, exactly mirroring the get_org_id override.
3444
+ get_billing_gate: Optional factory ``(line: str) -> FastAPI dependency`` that
3445
+ replaces the default no-op gate for each billable billing line. When
3446
+ provided, for every line used in the app (``"application"`` and
3447
+ ``"learnings_generated"``) the returned dependency overrides the
3448
+ ``default_billing_gate(line)`` sentinel in ``dependency_overrides``,
3449
+ exactly mirroring the ``get_caller_type`` override pattern.
3450
+ mount_data_plane: When True (default), include the data-plane routers
3451
+ (core, stall-state, pending-tool-call) and run the data-plane
3452
+ lifespan work (LLM availability check, cross-encoder prewarm,
3453
+ resume scheduler). When False, skip both so a control-plane host
3454
+ can build an app without requiring LLM/storage or starting the
3455
+ scheduler, while keeping all other scaffolding (middleware, CORS,
3456
+ auth overrides, OpenAPI security, health, ``/meta/version``,
3457
+ ``additional_routers``).
3458
+ capabilities: Optional registry of enterprise capabilities. When provided,
3459
+ each capability's routers, services, hooks, and lifecycle methods are
3460
+ wired into the app at construction time. Behavior is unchanged when
3461
+ ``None``.
3462
+ app_context_factory: Optional callable returning the AppContext passed to
3463
+ each capability's on_startup. When None, an empty AppContext() is used
3464
+ (local OSS / tests). Enterprise binds this to supply self_host_org_id /
3465
+ activated computed during its own startup.
2727
3466
 
2728
3467
  Returns:
2729
3468
  Configured FastAPI application.
2730
- """
3469
+
3470
+ Raises:
3471
+ ValueError: If both ``get_billing_gate`` and ``capabilities.billing_gate``
3472
+ are provided — pass billing_gate through exactly one path.
3473
+ """
3474
+ if (
3475
+ get_billing_gate is not None
3476
+ and capabilities is not None
3477
+ and capabilities.billing_gate is not None
3478
+ ):
3479
+ raise ValueError(
3480
+ "pass billing_gate via either get_billing_gate or capabilities, not both"
3481
+ )
2731
3482
  from collections.abc import AsyncIterator
2732
3483
  from contextlib import asynccontextmanager
2733
3484
 
2734
- from reflexio.server._auth import default_get_org_id
2735
3485
  from reflexio.server.api_endpoints.request_context import RequestContext
3486
+ from reflexio.server.auth import (
3487
+ default_billing_gate,
3488
+ default_get_caller_type,
3489
+ default_get_org_id,
3490
+ )
3491
+ from reflexio.server.extensions import AppContext
2736
3492
  from reflexio.server.llm.model_defaults import validate_llm_availability
2737
3493
  from reflexio.server.services.extraction.resume_scheduler import (
2738
3494
  maybe_start_resume_scheduler,
2739
3495
  )
2740
-
2741
- def _lifespan_org_id() -> str:
2742
- if get_org_id is None:
2743
- return default_get_org_id()
2744
- try:
2745
- signature = inspect.signature(get_org_id)
2746
- except (TypeError, ValueError):
2747
- return default_get_org_id()
2748
- if signature.parameters:
2749
- return default_get_org_id()
2750
- try:
2751
- return str(get_org_id())
2752
- except Exception:
2753
- logger.exception("Failed to resolve lifespan org_id; using default org")
2754
- return default_get_org_id()
3496
+ from reflexio.server.services.lineage.gc_scheduler import (
3497
+ maybe_start_lineage_gc,
3498
+ )
2755
3499
 
2756
3500
  @asynccontextmanager
2757
3501
  async def lifespan(app: FastAPI) -> AsyncIterator[None]: # noqa: ARG001
2758
- validate_llm_availability()
2759
- from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
2760
-
2761
- _prewarm_cross_encoder()
2762
- # The scheduler discovers every org with resumable work each tick and
2763
- # drives a per-org worker with org-scoped claims, so it is not limited
2764
- # to the bootstrap org. The bootstrap org is only used to read config
2765
- # and to seed cross-org discovery.
2766
- scheduler = maybe_start_resume_scheduler(
2767
- lambda org_id: RequestContext(org_id=org_id),
2768
- bootstrap_org_id=_lifespan_org_id(),
2769
- )
3502
+ scheduler = None
3503
+ gc_scheduler = None
3504
+ started_caps: list = []
3505
+ if mount_data_plane:
3506
+ log_publish_hardware_capacity()
3507
+ validate_llm_availability()
3508
+ from reflexio.server.llm.rerank import prewarm as _prewarm_cross_encoder
3509
+
3510
+ _prewarm_cross_encoder()
3511
+ # The scheduler discovers every org with resumable work each tick and
3512
+ # drives a per-org worker with org-scoped claims, so it is not limited
3513
+ # to the bootstrap org. The bootstrap org is only used to read config
3514
+ # and to seed cross-org discovery.
3515
+ bootstrap_org_id = _resolve_lifespan_org_id(get_org_id)
3516
+ scheduler = maybe_start_resume_scheduler(
3517
+ lambda org_id: RequestContext(org_id=org_id),
3518
+ bootstrap_org_id=bootstrap_org_id,
3519
+ )
3520
+ gc_scheduler = maybe_start_lineage_gc(
3521
+ lambda org_id: RequestContext(org_id=org_id),
3522
+ bootstrap_org_id=bootstrap_org_id,
3523
+ )
2770
3524
  try:
3525
+ if capabilities is not None:
3526
+ ctx = (
3527
+ app_context_factory()
3528
+ if app_context_factory is not None
3529
+ else AppContext()
3530
+ )
3531
+ for cap in capabilities.capabilities:
3532
+ await cap.on_startup(ctx)
3533
+ started_caps.append(cap)
2771
3534
  yield
2772
3535
  finally:
3536
+ for cap in reversed(started_caps):
3537
+ try:
3538
+ await cap.on_shutdown()
3539
+ except Exception:
3540
+ logger.warning(
3541
+ "capability %r on_shutdown raised; continuing cleanup",
3542
+ cap,
3543
+ exc_info=True,
3544
+ )
2773
3545
  if scheduler is not None:
2774
3546
  scheduler.stop()
3547
+ if gc_scheduler is not None:
3548
+ gc_scheduler.stop()
2775
3549
 
2776
3550
  app = FastAPI(docs_url="/docs", lifespan=lifespan)
2777
3551
 
@@ -2837,6 +3611,21 @@ def create_app(
2837
3611
  if get_org_id is not None:
2838
3612
  app.dependency_overrides[default_get_org_id] = get_org_id
2839
3613
 
3614
+ # Override get_caller_type dependency if custom one provided
3615
+ if get_caller_type is not None:
3616
+ app.dependency_overrides[default_get_caller_type] = get_caller_type
3617
+
3618
+ # Override billing gate dependencies if a custom gate factory is provided.
3619
+ # Each billing line needs its own override because dependency_overrides is
3620
+ # keyed by callable identity. ``default_billing_gate`` uses lru_cache so
3621
+ # the same sentinel object is returned for the same line — which is why
3622
+ # the overrides reliably fire at request time.
3623
+ if get_billing_gate is not None:
3624
+ for line in ("application", "learnings_generated"):
3625
+ app.dependency_overrides[default_billing_gate(line)] = get_billing_gate(
3626
+ line
3627
+ )
3628
+
2840
3629
  # When a custom get_org_id is provided together with require_auth,
2841
3630
  # auth is enforced on every route — mark this app instance so the
2842
3631
  # token-gated my_config endpoint becomes reachable. Using
@@ -2845,19 +3634,26 @@ def create_app(
2845
3634
  # multi-tenant embeddings) can coexist without leaking state.
2846
3635
  app.state.my_config_enabled = bool(get_org_id is not None and require_auth)
2847
3636
 
2848
- # Include core routes
2849
- app.include_router(core_router)
3637
+ # Include data-plane routes (core, stall-state, pending-tool-call). A
3638
+ # control-plane host sets mount_data_plane=False to skip these while
3639
+ # keeping every other piece of scaffolding below.
3640
+ if mount_data_plane:
3641
+ # Include core routes
3642
+ app.include_router(core_router)
2850
3643
 
2851
- # Include stall_state routes
2852
- app.include_router(stall_state_api.router)
3644
+ # Include stall_state routes
3645
+ app.include_router(stall_state_api.router)
2853
3646
 
2854
- # Include pending tool call routes
2855
- app.include_router(pending_tool_call_api.router)
3647
+ # Include pending tool call routes
3648
+ app.include_router(pending_tool_call_api.router)
2856
3649
 
2857
3650
  # Include additional routers
2858
3651
  for router in additional_routers or []:
2859
3652
  app.include_router(router)
2860
3653
 
3654
+ # Wire capability routers, services, and hooks (composition-root only)
3655
+ _wire_capabilities(app, capabilities, mount_data_plane, additional_routers)
3656
+
2861
3657
  # Health/observability endpoint (per-worker metrics for recycling)
2862
3658
  health_api.install(app)
2863
3659