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
@@ -0,0 +1,952 @@
1
+ """Agent playbook CRUD + search methods for SQLite storage."""
2
+
3
+ import json
4
+ import logging
5
+ import sqlite3
6
+ import uuid
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from typing import Any
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ from reflexio.models.api_schema.common import BlockingIssue
13
+ from reflexio.models.api_schema.retriever_schema import (
14
+ SearchAgentPlaybookRequest,
15
+ )
16
+ from reflexio.models.api_schema.service_schemas import (
17
+ AgentPlaybook,
18
+ PlaybookStatus,
19
+ Status,
20
+ )
21
+ from reflexio.models.config_schema import SearchMode, SearchOptions
22
+ from reflexio.server.services.storage.storage_base._playbook import (
23
+ AGGREGATE_REASON_PREFIX,
24
+ )
25
+
26
+ from .._base import (
27
+ _TOMBSTONE_STATUS_VALUES,
28
+ SQLiteStorageBase,
29
+ _build_status_sql,
30
+ _effective_search_mode,
31
+ _epoch_now,
32
+ _epoch_to_iso,
33
+ _json_dumps,
34
+ _row_to_agent_playbook,
35
+ _sanitize_fts_query,
36
+ _true_rrf_merge,
37
+ _vector_rank_rows,
38
+ )
39
+ from .._lineage import _append_event_stmt
40
+ from .._playbook import _build_tags_sql, _emit_hard_delete_playbook
41
+
42
+ _AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES = (
43
+ *_TOMBSTONE_STATUS_VALUES,
44
+ Status.ARCHIVE_IN_PROGRESS.value,
45
+ )
46
+
47
+
48
+ def _emit_supersede_playbook(
49
+ conn: sqlite3.Connection,
50
+ *,
51
+ org_id: str,
52
+ entity_id: str,
53
+ old_status: str | None,
54
+ request_id: str,
55
+ ) -> None:
56
+ """Emit a single status_change->superseded lineage event for an agent playbook."""
57
+ _append_event_stmt(
58
+ conn,
59
+ org_id=org_id,
60
+ entity_type="agent_playbook",
61
+ entity_id=entity_id,
62
+ op="status_change",
63
+ prov="wasInvalidatedBy",
64
+ source_ids=[],
65
+ actor="aggregator",
66
+ request_id=request_id,
67
+ reason=f"{old_status or 'None'}->superseded",
68
+ from_status=old_status,
69
+ to_status=Status.SUPERSEDED.value,
70
+ status_namespace="lifecycle_status",
71
+ )
72
+
73
+
74
+ class AgentPlaybookStoreMixin:
75
+ """Mixin providing agent playbook CRUD + search for SQLite storage."""
76
+
77
+ # Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
78
+ _lock: Any
79
+ conn: sqlite3.Connection
80
+ org_id: str
81
+ _execute: Any
82
+ _fetchone: Any
83
+ _fetchall: Any
84
+ _get_embedding: Any
85
+ _should_expand_documents: Any
86
+ _expand_document: Any
87
+ _fts_upsert: Any
88
+ _vec_upsert: Any
89
+ _delete_playbook_search_rows: Any
90
+
91
+ def _index_agent_playbook_fts_vec(self, ap: AgentPlaybook) -> None:
92
+ """Update the FTS and vector indexes for a single agent playbook row.
93
+
94
+ Must be called AFTER the row's transaction has been committed. The FTS
95
+ and vec helpers self-commit, so they must never be interleaved inside a
96
+ transaction that still has pending mutations.
97
+
98
+ Args:
99
+ ap (AgentPlaybook): The already-saved playbook (``agent_playbook_id``
100
+ must be set).
101
+ """
102
+ fts_parts = [ap.trigger or "", ap.content or ""]
103
+ if ap.expanded_terms:
104
+ fts_parts.append(ap.expanded_terms)
105
+ self._fts_upsert(
106
+ "agent_playbooks_fts",
107
+ ap.agent_playbook_id,
108
+ search_text=" ".join(p for p in fts_parts if p) or "",
109
+ )
110
+ if ap.embedding:
111
+ self._vec_upsert("agent_playbooks_vec", ap.agent_playbook_id, ap.embedding)
112
+
113
+ def _insert_agent_playbook_row(
114
+ self, conn: "sqlite3.Connection", ap: AgentPlaybook, created_at_iso: str
115
+ ) -> "sqlite3.Cursor":
116
+ """Execute the agent_playbooks INSERT and populate ``ap.agent_playbook_id``.
117
+
118
+ Runs the INSERT inside the caller's connection context; does NOT commit.
119
+ The caller is responsible for committing (or rolling back) the transaction.
120
+
121
+ Args:
122
+ conn: The open SQLite connection to execute against.
123
+ ap: The playbook to insert; ``agent_playbook_id`` is set on return.
124
+ created_at_iso: ISO-8601 timestamp string for ``created_at``.
125
+
126
+ Returns:
127
+ sqlite3.Cursor: The cursor from the INSERT (``lastrowid`` is the new PK).
128
+ """
129
+ cur = conn.execute(
130
+ """INSERT INTO agent_playbooks
131
+ (playbook_name, created_at, agent_version, content,
132
+ trigger, rationale, blocking_issue,
133
+ playbook_status, playbook_metadata, embedding,
134
+ expanded_terms, tags, status,
135
+ merged_into, superseded_by)
136
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
137
+ (
138
+ ap.playbook_name,
139
+ created_at_iso,
140
+ ap.agent_version,
141
+ ap.content,
142
+ ap.trigger,
143
+ ap.rationale,
144
+ json.dumps(ap.blocking_issue.model_dump())
145
+ if ap.blocking_issue
146
+ else None,
147
+ ap.playbook_status.value
148
+ if isinstance(ap.playbook_status, PlaybookStatus)
149
+ else ap.playbook_status,
150
+ ap.playbook_metadata,
151
+ _json_dumps(ap.embedding),
152
+ ap.expanded_terms,
153
+ _json_dumps(ap.tags),
154
+ ap.status.value if ap.status else None,
155
+ ap.merged_into,
156
+ ap.superseded_by,
157
+ ),
158
+ )
159
+ ap.agent_playbook_id = cur.lastrowid or 0
160
+ return cur
161
+
162
+ @SQLiteStorageBase.handle_exceptions
163
+ def save_agent_playbooks(
164
+ self, agent_playbooks: list[AgentPlaybook]
165
+ ) -> list[AgentPlaybook]:
166
+ saved: list[AgentPlaybook] = []
167
+ for ap in agent_playbooks:
168
+ embedding_text = ap.trigger or ap.content
169
+ if self._should_expand_documents():
170
+ with ThreadPoolExecutor(max_workers=2) as executor:
171
+ emb_future = executor.submit(self._get_embedding, embedding_text)
172
+ exp_future = executor.submit(self._expand_document, embedding_text)
173
+ ap.embedding = emb_future.result(timeout=15)
174
+ ap.expanded_terms = exp_future.result(timeout=15)
175
+ else:
176
+ ap.embedding = self._get_embedding(embedding_text)
177
+
178
+ created_at_iso = _epoch_to_iso(ap.created_at)
179
+ with self._lock:
180
+ self._insert_agent_playbook_row(self.conn, ap, created_at_iso)
181
+ self.conn.commit()
182
+
183
+ self._index_agent_playbook_fts_vec(ap)
184
+ saved.append(ap)
185
+ return saved
186
+
187
+ @SQLiteStorageBase.handle_exceptions
188
+ def save_agent_playbook_with_aggregate_event(
189
+ self,
190
+ agent_playbook: AgentPlaybook,
191
+ *,
192
+ source_ids: list[str],
193
+ request_id: str,
194
+ run_mode: str,
195
+ ) -> AgentPlaybook:
196
+ """Persist an agent playbook AND its ``op=aggregate`` lineage event atomically.
197
+
198
+ The INSERT and the event are committed in a single transaction — if either
199
+ fails, both roll back. The event is the sole record of the run->playbook
200
+ membership for reconstruction, so atomicity is critical.
201
+
202
+ Args:
203
+ agent_playbook (AgentPlaybook): The playbook to persist.
204
+ source_ids (list[str]): IDs of the source entities that produced this playbook.
205
+ request_id (str): The aggregation run ID.
206
+ run_mode (str): Aggregation run mode (e.g. ``full_archive`` or ``incremental``).
207
+
208
+ Returns:
209
+ AgentPlaybook: The saved playbook with ``agent_playbook_id`` populated.
210
+
211
+ Raises:
212
+ ValueError: If ``request_id`` is empty (would produce an unreconstructable event).
213
+ """
214
+ if not request_id or not request_id.strip():
215
+ raise ValueError(
216
+ "save_agent_playbook_with_aggregate_event requires a non-empty request_id"
217
+ )
218
+ ap = agent_playbook
219
+ embedding_text = ap.trigger or ap.content
220
+ if self._should_expand_documents():
221
+ with ThreadPoolExecutor(max_workers=2) as executor:
222
+ emb_future = executor.submit(self._get_embedding, embedding_text)
223
+ exp_future = executor.submit(self._expand_document, embedding_text)
224
+ ap.embedding = emb_future.result(timeout=15)
225
+ ap.expanded_terms = exp_future.result(timeout=15)
226
+ else:
227
+ ap.embedding = self._get_embedding(embedding_text)
228
+
229
+ created_at_iso = _epoch_to_iso(ap.created_at)
230
+ with self._lock:
231
+ try:
232
+ self._insert_agent_playbook_row(self.conn, ap, created_at_iso)
233
+ _append_event_stmt(
234
+ self.conn,
235
+ org_id=self.org_id,
236
+ entity_type="agent_playbook",
237
+ entity_id=str(ap.agent_playbook_id),
238
+ op="aggregate",
239
+ prov="wasDerivedFrom",
240
+ source_ids=source_ids,
241
+ actor="aggregator",
242
+ request_id=request_id,
243
+ reason=f"{AGGREGATE_REASON_PREFIX}{run_mode}",
244
+ )
245
+ self.conn.commit()
246
+ except Exception:
247
+ self.conn.rollback()
248
+ raise
249
+
250
+ # FTS/vec indexing AFTER commit — these helpers self-commit and must
251
+ # not be interleaved inside the atomic transaction above.
252
+ # Index failure does NOT invalidate the committed row+event; the index
253
+ # is reconstructable from the authoritative row.
254
+ try:
255
+ self._index_agent_playbook_fts_vec(ap)
256
+ except Exception:
257
+ logger.exception(
258
+ "FTS/vec indexing failed for agent_playbook %s (row committed, index skipped)",
259
+ ap.agent_playbook_id,
260
+ )
261
+ return ap
262
+
263
+ @SQLiteStorageBase.handle_exceptions
264
+ def get_agent_playbooks(
265
+ self,
266
+ limit: int = 100,
267
+ playbook_name: str | None = None,
268
+ agent_version: str | None = None,
269
+ status_filter: list[Status | None] | None = None,
270
+ playbook_status_filter: list[PlaybookStatus] | None = None,
271
+ tags: list[str] | None = None,
272
+ agent_playbook_id: int | None = None,
273
+ query: str | None = None,
274
+ start_time: int | None = None,
275
+ end_time: int | None = None,
276
+ ) -> list[AgentPlaybook]:
277
+ sql = "SELECT * FROM agent_playbooks WHERE 1=1"
278
+ params: list[Any] = []
279
+
280
+ if agent_playbook_id is not None:
281
+ sql += " AND agent_playbook_id = ?"
282
+ params.append(agent_playbook_id)
283
+ if query:
284
+ like = f"%{query.lower()}%"
285
+ sql += (
286
+ " AND (LOWER(content) LIKE ? OR LOWER(trigger) LIKE ? "
287
+ "OR LOWER(rationale) LIKE ? OR LOWER(playbook_name) LIKE ? "
288
+ "OR LOWER(playbook_metadata) LIKE ?)"
289
+ )
290
+ params.extend([like, like, like, like, like])
291
+ if playbook_name:
292
+ sql += " AND playbook_name = ?"
293
+ params.append(playbook_name)
294
+
295
+ if agent_version is not None:
296
+ sql += " AND agent_version = ?"
297
+ params.append(agent_version)
298
+ if start_time is not None:
299
+ sql += " AND created_at >= ?"
300
+ params.append(_epoch_to_iso(start_time))
301
+ if end_time is not None:
302
+ sql += " AND created_at <= ?"
303
+ params.append(_epoch_to_iso(end_time))
304
+
305
+ if status_filter is not None:
306
+ frag, sparams = _build_status_sql(status_filter)
307
+ sql += f" AND {frag}"
308
+ params.extend(sparams)
309
+ else:
310
+ sql += " AND status IS NULL"
311
+
312
+ if playbook_status_filter:
313
+ ph = ",".join("?" for _ in playbook_status_filter)
314
+ sql += f" AND playbook_status IN ({ph})"
315
+ params.extend(ps.value for ps in playbook_status_filter)
316
+ tag_frag, tag_params = _build_tags_sql("agent_playbooks", tags)
317
+ if tag_frag:
318
+ sql += f" AND {tag_frag}"
319
+ params.extend(tag_params)
320
+
321
+ sql += " ORDER BY created_at DESC LIMIT ?"
322
+ params.append(limit)
323
+ rows = self._fetchall(sql, params)
324
+ return [_row_to_agent_playbook(r) for r in rows]
325
+
326
+ @SQLiteStorageBase.handle_exceptions
327
+ def get_agent_playbook_by_id(
328
+ self, agent_playbook_id: int, *, include_tombstones: bool = False
329
+ ) -> AgentPlaybook | None:
330
+ sql = "SELECT * FROM agent_playbooks WHERE agent_playbook_id = ?"
331
+ if not include_tombstones:
332
+ sql += " AND (status IS NULL OR status NOT IN (?, ?, ?))"
333
+ row = self._fetchone(
334
+ sql,
335
+ (agent_playbook_id, *_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES),
336
+ )
337
+ else:
338
+ row = self._fetchone(sql, (agent_playbook_id,))
339
+ return _row_to_agent_playbook(row) if row else None
340
+
341
+ @SQLiteStorageBase.handle_exceptions
342
+ def delete_all_agent_playbooks(self) -> None:
343
+ batch_request_id = uuid.uuid4().hex
344
+ with self._lock:
345
+ ids = [
346
+ r["agent_playbook_id"]
347
+ for r in self.conn.execute(
348
+ "SELECT agent_playbook_id FROM agent_playbooks"
349
+ ).fetchall()
350
+ ]
351
+ self.conn.execute("DELETE FROM agent_playbooks")
352
+ for apid in ids:
353
+ _emit_hard_delete_playbook(
354
+ self.conn,
355
+ org_id=self.org_id,
356
+ entity_type="agent_playbook",
357
+ entity_id=str(apid),
358
+ request_id=batch_request_id,
359
+ )
360
+ self.conn.commit()
361
+ self._delete_playbook_search_rows("agent", ids)
362
+
363
+ @SQLiteStorageBase.handle_exceptions
364
+ def delete_agent_playbook(self, agent_playbook_id: int) -> None:
365
+ with self._lock:
366
+ cur = self.conn.execute(
367
+ "DELETE FROM agent_playbooks WHERE agent_playbook_id = ?",
368
+ (agent_playbook_id,),
369
+ )
370
+ if cur.rowcount > 0:
371
+ _emit_hard_delete_playbook(
372
+ self.conn,
373
+ org_id=self.org_id,
374
+ entity_type="agent_playbook",
375
+ entity_id=str(agent_playbook_id),
376
+ request_id=uuid.uuid4().hex,
377
+ )
378
+ self._delete_playbook_search_rows(
379
+ "agent", [agent_playbook_id], commit=False
380
+ )
381
+ self.conn.commit()
382
+
383
+ @SQLiteStorageBase.handle_exceptions
384
+ def delete_all_agent_playbooks_by_playbook_name(
385
+ self, playbook_name: str, agent_version: str | None = None
386
+ ) -> None:
387
+ sql = "SELECT agent_playbook_id FROM agent_playbooks WHERE playbook_name = ?"
388
+ params: list[Any] = [playbook_name]
389
+ if agent_version is not None:
390
+ sql += " AND agent_version = ?"
391
+ params.append(agent_version)
392
+ batch_request_id = uuid.uuid4().hex
393
+ with self._lock:
394
+ ids = [
395
+ r["agent_playbook_id"]
396
+ for r in self.conn.execute(sql, params).fetchall()
397
+ ]
398
+ if not ids:
399
+ return
400
+ ph = ",".join("?" for _ in ids)
401
+ self.conn.execute(
402
+ f"DELETE FROM agent_playbooks WHERE agent_playbook_id IN ({ph})", ids
403
+ )
404
+ for apid in ids:
405
+ _emit_hard_delete_playbook(
406
+ self.conn,
407
+ org_id=self.org_id,
408
+ entity_type="agent_playbook",
409
+ entity_id=str(apid),
410
+ request_id=batch_request_id,
411
+ )
412
+ self.conn.commit()
413
+ self._delete_playbook_search_rows("agent", ids)
414
+
415
+ @SQLiteStorageBase.handle_exceptions
416
+ def delete_agent_playbooks_by_ids(
417
+ self, agent_playbook_ids: list[int], *, emit_hard_delete: bool = True
418
+ ) -> None:
419
+ if not agent_playbook_ids:
420
+ return
421
+ ph = ",".join("?" for _ in agent_playbook_ids)
422
+ batch_request_id = uuid.uuid4().hex
423
+ with self._lock:
424
+ existing = [
425
+ r["agent_playbook_id"]
426
+ for r in self.conn.execute(
427
+ f"SELECT agent_playbook_id FROM agent_playbooks WHERE agent_playbook_id IN ({ph})",
428
+ agent_playbook_ids,
429
+ ).fetchall()
430
+ ]
431
+ self.conn.execute(
432
+ f"DELETE FROM agent_playbooks WHERE agent_playbook_id IN ({ph})",
433
+ agent_playbook_ids,
434
+ )
435
+ if emit_hard_delete:
436
+ for apid in existing:
437
+ _emit_hard_delete_playbook(
438
+ self.conn,
439
+ org_id=self.org_id,
440
+ entity_type="agent_playbook",
441
+ entity_id=str(apid),
442
+ request_id=batch_request_id,
443
+ actor="system",
444
+ )
445
+ self.conn.commit()
446
+ self._delete_playbook_search_rows("agent", agent_playbook_ids)
447
+
448
+ @SQLiteStorageBase.handle_exceptions
449
+ def update_agent_playbook_status(
450
+ self, agent_playbook_id: int, playbook_status: PlaybookStatus
451
+ ) -> None:
452
+ """Update an agent playbook's status and emit a status_change lineage event.
453
+
454
+ Each call generates a fresh request_id so every status change is recorded as
455
+ a distinct audit event (not collapsed by the idempotency key).
456
+ """
457
+ with self._lock:
458
+ row = self.conn.execute(
459
+ "SELECT playbook_status FROM agent_playbooks WHERE agent_playbook_id = ?",
460
+ (agent_playbook_id,),
461
+ ).fetchone()
462
+ if not row:
463
+ raise ValueError(
464
+ f"Agent playbook with ID {agent_playbook_id} not found"
465
+ )
466
+ prior_playbook_status = row["playbook_status"]
467
+ old_status = prior_playbook_status or "None"
468
+ cur = self.conn.execute(
469
+ "UPDATE agent_playbooks SET playbook_status = ? WHERE agent_playbook_id = ?",
470
+ (playbook_status.value, agent_playbook_id),
471
+ )
472
+ if cur.rowcount > 0:
473
+ _append_event_stmt(
474
+ self.conn,
475
+ org_id=self.org_id,
476
+ entity_type="agent_playbook",
477
+ entity_id=str(agent_playbook_id),
478
+ op="status_change",
479
+ prov="wasInvalidatedBy",
480
+ source_ids=[],
481
+ actor="api",
482
+ request_id=uuid.uuid4().hex,
483
+ reason=f"{old_status}->{playbook_status.value}",
484
+ from_status=prior_playbook_status,
485
+ to_status=playbook_status.value,
486
+ status_namespace="playbook_status",
487
+ )
488
+ self.conn.commit()
489
+
490
+ @SQLiteStorageBase.handle_exceptions
491
+ def update_agent_playbook(
492
+ self,
493
+ agent_playbook_id: int,
494
+ playbook_name: str | None = None,
495
+ content: str | None = None,
496
+ trigger: str | None = None,
497
+ rationale: str | None = None,
498
+ blocking_issue: BlockingIssue | None = None,
499
+ playbook_status: PlaybookStatus | None = None,
500
+ tags: list[str] | None = None,
501
+ ) -> None:
502
+ updates: list[str] = []
503
+ params: list[Any] = []
504
+ if playbook_name is not None:
505
+ updates.append("playbook_name = ?")
506
+ params.append(playbook_name)
507
+ if content is not None:
508
+ updates.append("content = ?")
509
+ params.append(content)
510
+ if trigger is not None:
511
+ updates.append("trigger = ?")
512
+ params.append(trigger)
513
+ if rationale is not None:
514
+ updates.append("rationale = ?")
515
+ params.append(rationale)
516
+ if blocking_issue is not None:
517
+ updates.append("blocking_issue = ?")
518
+ params.append(json.dumps(blocking_issue.model_dump()))
519
+ if playbook_status is not None:
520
+ updates.append("playbook_status = ?")
521
+ params.append(playbook_status.value)
522
+ if tags is not None:
523
+ updates.append("tags = ?")
524
+ params.append(_json_dumps(tags))
525
+ if updates:
526
+ params.append(agent_playbook_id)
527
+ op = "revise" if content is not None else "status_change"
528
+ prov = "wasRevisionOf" if op == "revise" else "wasInvalidatedBy"
529
+ with self._lock:
530
+ prior_row = self.conn.execute(
531
+ "SELECT playbook_status FROM agent_playbooks WHERE agent_playbook_id = ?",
532
+ (agent_playbook_id,),
533
+ ).fetchone()
534
+ if not prior_row:
535
+ raise ValueError(
536
+ f"Agent playbook with ID {agent_playbook_id} not found"
537
+ )
538
+ prior_playbook_status = prior_row["playbook_status"]
539
+ cur = self.conn.execute(
540
+ f"UPDATE agent_playbooks SET {', '.join(updates)} WHERE agent_playbook_id = ?",
541
+ tuple(params),
542
+ )
543
+ if cur.rowcount > 0:
544
+ # Populate structured status fields only when playbook_status is
545
+ # among the updated fields and the op is status_change (not revise).
546
+ if op == "status_change" and playbook_status is not None:
547
+ from_status = prior_playbook_status
548
+ to_status = playbook_status.value
549
+ status_namespace: str | None = "playbook_status"
550
+ else:
551
+ from_status = None
552
+ to_status = None
553
+ status_namespace = None
554
+ _append_event_stmt(
555
+ self.conn,
556
+ org_id=self.org_id,
557
+ entity_type="agent_playbook",
558
+ entity_id=str(agent_playbook_id),
559
+ op=op,
560
+ prov=prov,
561
+ source_ids=[],
562
+ actor="api",
563
+ request_id=uuid.uuid4().hex,
564
+ reason="in-place update",
565
+ from_status=from_status,
566
+ to_status=to_status,
567
+ status_namespace=status_namespace,
568
+ )
569
+ self.conn.commit()
570
+
571
+ @SQLiteStorageBase.handle_exceptions
572
+ def archive_agent_playbooks_by_playbook_name(
573
+ self, playbook_name: str, agent_version: str | None = None
574
+ ) -> None:
575
+ where = (
576
+ "playbook_name = ? AND playbook_status != ?"
577
+ " AND (status IS NULL OR status != 'archived')"
578
+ )
579
+ params: list[Any] = [playbook_name, PlaybookStatus.APPROVED.value]
580
+ if agent_version is not None:
581
+ where += " AND agent_version = ?"
582
+ params.append(agent_version)
583
+ now_ts = _epoch_now()
584
+ batch_request_id = uuid.uuid4().hex
585
+ with self._lock:
586
+ affected = self.conn.execute(
587
+ f"SELECT agent_playbook_id, status FROM agent_playbooks WHERE {where}",
588
+ params,
589
+ ).fetchall()
590
+ self.conn.execute(
591
+ f"UPDATE agent_playbooks SET status = 'archived', retired_at = ? WHERE {where}",
592
+ [now_ts, *params],
593
+ )
594
+ for row in affected:
595
+ prior = row["status"] or "None"
596
+ _append_event_stmt(
597
+ self.conn,
598
+ org_id=self.org_id,
599
+ entity_type="agent_playbook",
600
+ entity_id=str(row["agent_playbook_id"]),
601
+ op="status_change",
602
+ prov="wasInvalidatedBy",
603
+ source_ids=[],
604
+ actor="api",
605
+ request_id=batch_request_id,
606
+ reason=f"{prior}->archived",
607
+ from_status=row["status"],
608
+ to_status="archived",
609
+ status_namespace="lifecycle_status",
610
+ )
611
+ self.conn.commit()
612
+
613
+ @SQLiteStorageBase.handle_exceptions
614
+ def archive_agent_playbooks_by_ids(self, agent_playbook_ids: list[int]) -> None:
615
+ if not agent_playbook_ids:
616
+ return
617
+ ph = ",".join("?" for _ in agent_playbook_ids)
618
+ now_ts = _epoch_now()
619
+ batch_request_id = uuid.uuid4().hex
620
+ with self._lock:
621
+ affected = self.conn.execute(
622
+ f"SELECT agent_playbook_id, status FROM agent_playbooks"
623
+ f" WHERE agent_playbook_id IN ({ph}) AND playbook_status != ?"
624
+ f" AND (status IS NULL OR status != 'archived')",
625
+ [*agent_playbook_ids, PlaybookStatus.APPROVED.value],
626
+ ).fetchall()
627
+ self.conn.execute(
628
+ f"UPDATE agent_playbooks SET status = 'archived', retired_at = ?"
629
+ f" WHERE agent_playbook_id IN ({ph}) AND playbook_status != ?"
630
+ f" AND (status IS NULL OR status != 'archived')",
631
+ [now_ts, *agent_playbook_ids, PlaybookStatus.APPROVED.value],
632
+ )
633
+ for row in affected:
634
+ prior = row["status"] or "None"
635
+ _append_event_stmt(
636
+ self.conn,
637
+ org_id=self.org_id,
638
+ entity_type="agent_playbook",
639
+ entity_id=str(row["agent_playbook_id"]),
640
+ op="status_change",
641
+ prov="wasInvalidatedBy",
642
+ source_ids=[],
643
+ actor="api",
644
+ request_id=batch_request_id,
645
+ reason=f"{prior}->archived",
646
+ from_status=row["status"],
647
+ to_status="archived",
648
+ status_namespace="lifecycle_status",
649
+ )
650
+ self.conn.commit()
651
+
652
+ @SQLiteStorageBase.handle_exceptions
653
+ def supersede_agent_playbooks_by_ids(
654
+ self, agent_playbook_ids: list[int], request_id: str
655
+ ) -> int:
656
+ """Soft-delete agent playbooks by setting status to SUPERSEDED, emitting set-based lineage.
657
+
658
+ For each eligible id (not APPROVED, not already tombstoned), updates status to
659
+ SUPERSEDED and emits one ``status_change`` event under the shared ``request_id``.
660
+ Atomic: one ``conn.commit()`` at the end, guarded on rowcount per id.
661
+ FTS/vec rows are NOT removed — reads exclude tombstones by status filter.
662
+
663
+ Args:
664
+ agent_playbook_ids (list[int]): Agent playbook ids to supersede.
665
+ request_id (str): Shared request id for all emitted lineage events.
666
+
667
+ Returns:
668
+ int: Number of agent playbooks actually updated.
669
+ """
670
+ if not agent_playbook_ids:
671
+ return 0
672
+ if not request_id:
673
+ raise ValueError("request_id must be non-empty for supersede")
674
+ now_ts = _epoch_now()
675
+ updated = 0
676
+ with self._lock:
677
+ for apid in agent_playbook_ids:
678
+ row = self.conn.execute(
679
+ "SELECT status FROM agent_playbooks WHERE agent_playbook_id = ?",
680
+ (apid,),
681
+ ).fetchone()
682
+ if row is None:
683
+ continue
684
+ old_status = row["status"]
685
+ # NOTE (M3): The model supersede_profiles_by_ids adds an eligible-check
686
+ # `if old_status_val not in eligible: continue` before the UPDATE. For
687
+ # agent_playbooks the ineligible condition spans two columns (status in
688
+ # _TOMBSTONE_STATUS_VALUES OR playbook_status == APPROVED), so aligning
689
+ # would require adding playbook_status to the SELECT. The UPDATE WHERE
690
+ # clause already excludes those rows atomically; the extra continue here
691
+ # would be a no-op and not worth the added complexity.
692
+ cur = self.conn.execute(
693
+ "UPDATE agent_playbooks SET status = ?, retired_at = ?"
694
+ " WHERE agent_playbook_id = ? AND playbook_status != ?"
695
+ " AND (status IS NULL OR status NOT IN (?, ?))",
696
+ (
697
+ Status.SUPERSEDED.value,
698
+ now_ts,
699
+ apid,
700
+ PlaybookStatus.APPROVED.value,
701
+ *_TOMBSTONE_STATUS_VALUES,
702
+ ),
703
+ )
704
+ if cur.rowcount > 0:
705
+ _emit_supersede_playbook(
706
+ self.conn,
707
+ org_id=self.org_id,
708
+ entity_id=str(apid),
709
+ old_status=old_status,
710
+ request_id=request_id,
711
+ )
712
+ updated += 1
713
+ self.conn.commit()
714
+ return updated
715
+
716
+ @SQLiteStorageBase.handle_exceptions
717
+ def supersede_agent_playbooks_by_playbook_name(
718
+ self, playbook_name: str, agent_version: str | None, request_id: str
719
+ ) -> int:
720
+ """Soft-delete archived agent playbooks by name/version via SUPERSEDED status.
721
+
722
+ Mirrors ``delete_archived_agent_playbooks_by_playbook_name`` but converts
723
+ the hard-delete to a soft-supersede with status_change lineage events.
724
+ Atomic: one ``conn.commit()`` at the end.
725
+ FTS/vec rows are NOT removed.
726
+
727
+ Args:
728
+ playbook_name (str): Playbook name to supersede.
729
+ agent_version (str | None): Agent version filter. None matches all versions.
730
+ request_id (str): Shared request id for all emitted lineage events.
731
+
732
+ Returns:
733
+ int: Number of agent playbooks actually updated.
734
+ """
735
+ if not request_id:
736
+ raise ValueError("request_id must be non-empty for supersede")
737
+ sql = (
738
+ "SELECT agent_playbook_id, status FROM agent_playbooks"
739
+ " WHERE playbook_name = ? AND status = 'archived'"
740
+ )
741
+ params: list[Any] = [playbook_name]
742
+ if agent_version is not None:
743
+ sql += " AND agent_version = ?"
744
+ params.append(agent_version)
745
+ updated = 0
746
+ with self._lock:
747
+ rows = self.conn.execute(sql, params).fetchall()
748
+ if not rows:
749
+ return 0
750
+ now_ts = _epoch_now()
751
+ for row in rows:
752
+ apid = row["agent_playbook_id"]
753
+ old_status = row["status"]
754
+ cur = self.conn.execute(
755
+ "UPDATE agent_playbooks SET status = ?, retired_at = ?"
756
+ " WHERE agent_playbook_id = ? AND playbook_status != ?"
757
+ " AND status = 'archived'",
758
+ (
759
+ Status.SUPERSEDED.value,
760
+ now_ts,
761
+ apid,
762
+ PlaybookStatus.APPROVED.value,
763
+ ),
764
+ )
765
+ if cur.rowcount > 0:
766
+ _emit_supersede_playbook(
767
+ self.conn,
768
+ org_id=self.org_id,
769
+ entity_id=str(apid),
770
+ old_status=old_status,
771
+ request_id=request_id,
772
+ )
773
+ updated += 1
774
+ self.conn.commit()
775
+ return updated
776
+
777
+ @SQLiteStorageBase.handle_exceptions
778
+ def restore_archived_agent_playbooks_by_playbook_name(
779
+ self, playbook_name: str, agent_version: str | None = None
780
+ ) -> None:
781
+ sql = "UPDATE agent_playbooks SET status = NULL, retired_at = NULL WHERE playbook_name = ? AND status = 'archived'"
782
+ params: list[Any] = [playbook_name]
783
+ if agent_version is not None:
784
+ sql += " AND agent_version = ?"
785
+ params.append(agent_version)
786
+ self._execute(sql, params)
787
+
788
+ @SQLiteStorageBase.handle_exceptions
789
+ def restore_archived_agent_playbooks_by_ids(
790
+ self, agent_playbook_ids: list[int]
791
+ ) -> None:
792
+ if not agent_playbook_ids:
793
+ return
794
+ ph = ",".join("?" for _ in agent_playbook_ids)
795
+ self._execute(
796
+ f"UPDATE agent_playbooks SET status = NULL, retired_at = NULL WHERE agent_playbook_id IN ({ph}) AND status = 'archived'",
797
+ agent_playbook_ids,
798
+ )
799
+
800
+ @SQLiteStorageBase.handle_exceptions
801
+ def delete_archived_agent_playbooks_by_playbook_name(
802
+ self, playbook_name: str, agent_version: str | None = None
803
+ ) -> None:
804
+ sql = "SELECT agent_playbook_id FROM agent_playbooks WHERE playbook_name = ? AND status = 'archived'"
805
+ params: list[Any] = [playbook_name]
806
+ if agent_version is not None:
807
+ sql += " AND agent_version = ?"
808
+ params.append(agent_version)
809
+ batch_request_id = uuid.uuid4().hex
810
+ with self._lock:
811
+ ids = [
812
+ r["agent_playbook_id"]
813
+ for r in self.conn.execute(sql, params).fetchall()
814
+ ]
815
+ if not ids:
816
+ return
817
+ ph = ",".join("?" for _ in ids)
818
+ self.conn.execute(
819
+ f"DELETE FROM agent_playbooks WHERE agent_playbook_id IN ({ph})", ids
820
+ )
821
+ for apid in ids:
822
+ _emit_hard_delete_playbook(
823
+ self.conn,
824
+ org_id=self.org_id,
825
+ entity_type="agent_playbook",
826
+ entity_id=str(apid),
827
+ request_id=batch_request_id,
828
+ )
829
+ self.conn.commit()
830
+ self._delete_playbook_search_rows("agent", ids)
831
+
832
+ @SQLiteStorageBase.handle_exceptions
833
+ def search_agent_playbooks( # noqa: C901
834
+ self,
835
+ request: SearchAgentPlaybookRequest,
836
+ options: SearchOptions | None = None,
837
+ ) -> list[AgentPlaybook]:
838
+ query = request.query
839
+ agent_version = request.agent_version
840
+ playbook_name = request.playbook_name
841
+ start_time = int(request.start_time.timestamp()) if request.start_time else None
842
+ end_time = int(request.end_time.timestamp()) if request.end_time else None
843
+ status_filter = request.status_filter
844
+ playbook_status_filter = request.playbook_status_filter
845
+ match_count = request.top_k or 10
846
+ query_embedding = options.query_embedding if options else None
847
+ mode = _effective_search_mode(
848
+ request.search_mode, query_embedding, request.query
849
+ )
850
+ rrf_k = options.rrf_k if options else 60
851
+ vector_weight = options.vector_weight if options else 1.0
852
+ fts_weight = options.fts_weight if options else 1.0
853
+
854
+ conditions: list[str] = []
855
+ params: list[Any] = []
856
+
857
+ if agent_version:
858
+ conditions.append("ap.agent_version = ?")
859
+ params.append(agent_version)
860
+ if playbook_name:
861
+ conditions.append("ap.playbook_name = ?")
862
+ params.append(playbook_name)
863
+ if start_time:
864
+ conditions.append("ap.created_at >= ?")
865
+ params.append(_epoch_to_iso(start_time))
866
+ if end_time:
867
+ conditions.append("ap.created_at <= ?")
868
+ params.append(_epoch_to_iso(end_time))
869
+ if playbook_status_filter is not None:
870
+ if isinstance(playbook_status_filter, list):
871
+ if not playbook_status_filter:
872
+ conditions.append("1=0")
873
+ else:
874
+ placeholders = ",".join("?" for _ in playbook_status_filter)
875
+ conditions.append(f"ap.playbook_status IN ({placeholders})")
876
+ params.extend(s.value for s in playbook_status_filter)
877
+ else:
878
+ conditions.append("ap.playbook_status = ?")
879
+ params.append(playbook_status_filter.value)
880
+ if status_filter is not None:
881
+ frag, sparams = _build_status_sql(status_filter)
882
+ conditions.append(frag)
883
+ params.extend(sparams)
884
+ else:
885
+ conditions.append("(ap.status IS NULL OR ap.status NOT IN (?, ?, ?))")
886
+ params.extend(_AGENT_PLAYBOOK_DEFAULT_EXCLUDED_STATUSES)
887
+ tag_frag, tag_params = _build_tags_sql("ap", request.tags)
888
+ if tag_frag:
889
+ conditions.append(tag_frag)
890
+ params.extend(tag_params)
891
+
892
+ where_extra = (" AND " + " AND ".join(conditions)) if conditions else ""
893
+ overfetch = match_count * 5 if mode != SearchMode.FTS else match_count
894
+
895
+ # Pure vector search: fetch all candidates, rank by cosine similarity
896
+ if mode == SearchMode.VECTOR and query_embedding:
897
+ base_where = "WHERE " + " AND ".join(conditions) if conditions else ""
898
+ sql = f"""SELECT * FROM agent_playbooks ap
899
+ {base_where}
900
+ ORDER BY ap.created_at DESC"""
901
+ rows = self._fetchall(sql, params)
902
+ rows = _vector_rank_rows(rows, query_embedding, match_count)
903
+ return [_row_to_agent_playbook(r) for r in rows]
904
+
905
+ if query:
906
+ fts_query = _sanitize_fts_query(query)
907
+ sql = f"""SELECT ap.* FROM agent_playbooks ap
908
+ JOIN agent_playbooks_fts f ON ap.agent_playbook_id = f.rowid
909
+ WHERE agent_playbooks_fts MATCH ?{where_extra}
910
+ ORDER BY bm25(agent_playbooks_fts, 1.0)
911
+ LIMIT ?"""
912
+ fts_rows = self._fetchall(sql, [fts_query, *params, overfetch])
913
+
914
+ if mode == SearchMode.HYBRID and query_embedding:
915
+ base_where = "WHERE " + " AND ".join(conditions) if conditions else ""
916
+ vec_limit = match_count * 10
917
+ vec_sql = f"""SELECT * FROM agent_playbooks ap
918
+ {base_where}
919
+ ORDER BY ap.created_at DESC
920
+ LIMIT ?"""
921
+ vec_candidates = self._fetchall(vec_sql, [*params, vec_limit])
922
+ vec_rows = _vector_rank_rows(vec_candidates, query_embedding, overfetch)
923
+ rows = _true_rrf_merge(
924
+ fts_rows,
925
+ vec_rows,
926
+ "agent_playbook_id",
927
+ match_count,
928
+ rrf_k,
929
+ vector_weight,
930
+ fts_weight,
931
+ )
932
+ return [_row_to_agent_playbook(r) for r in rows]
933
+ return [_row_to_agent_playbook(r) for r in fts_rows[:match_count]]
934
+
935
+ # HYBRID without query text: rank by embedding only
936
+ if query_embedding:
937
+ base_where = "WHERE " + " AND ".join(conditions) if conditions else ""
938
+ sql = f"""SELECT * FROM agent_playbooks ap
939
+ {base_where}
940
+ ORDER BY ap.created_at DESC"""
941
+ rows = self._fetchall(sql, params)
942
+ rows = _vector_rank_rows(rows, query_embedding, match_count)
943
+ return [_row_to_agent_playbook(r) for r in rows]
944
+
945
+ # No query text, no embedding -- recency fallback
946
+ base_where = "WHERE " + " AND ".join(conditions) if conditions else "WHERE 1=1"
947
+ sql = f"""SELECT * FROM agent_playbooks ap
948
+ {base_where}
949
+ ORDER BY ap.created_at DESC LIMIT ?"""
950
+ params.append(match_count)
951
+ rows = self._fetchall(sql, params)
952
+ return [_row_to_agent_playbook(r) for r in rows]