claude-smart 0.2.45 → 0.2.46

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 (322) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +34 -5
  3. package/bin/claude-smart.js +295 -5
  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 +22 -2
  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/preferences/[id]/page.tsx +8 -8
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
  18. package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
  19. package/plugin/dashboard/lib/reflexio-client.ts +16 -0
  20. package/plugin/dashboard/lib/status.ts +10 -3
  21. package/plugin/dashboard/lib/types.ts +18 -6
  22. package/plugin/hooks/codex-hooks.json +0 -1
  23. package/plugin/opencode/assistant-buffer.ts +108 -0
  24. package/plugin/opencode/dist/assistant-buffer.js +96 -0
  25. package/plugin/opencode/dist/internal.js +12 -0
  26. package/plugin/opencode/dist/payload.js +85 -0
  27. package/plugin/opencode/dist/server.mjs +158 -0
  28. package/plugin/opencode/internal.ts +18 -0
  29. package/plugin/opencode/package.json +3 -0
  30. package/plugin/opencode/payload.ts +90 -0
  31. package/plugin/opencode/server.mts +174 -0
  32. package/plugin/opencode/tsconfig.json +13 -0
  33. package/plugin/pyproject.toml +20 -3
  34. package/plugin/scripts/_lib.sh +5 -1
  35. package/plugin/scripts/backend-service.sh +22 -2
  36. package/plugin/scripts/ensure-plugin-root.sh +4 -4
  37. package/plugin/scripts/hook_entry.sh +1 -1
  38. package/plugin/scripts/opencode-claude-compat +4 -0
  39. package/plugin/scripts/opencode-claude-compat.cmd +3 -0
  40. package/plugin/scripts/opencode-claude-compat.js +225 -0
  41. package/plugin/scripts/smart-install.sh +10 -8
  42. package/plugin/src/README.md +1 -1
  43. package/plugin/src/claude_smart/cli.py +304 -6
  44. package/plugin/src/claude_smart/env_config.py +12 -4
  45. package/plugin/src/claude_smart/events/session_start.py +26 -7
  46. package/plugin/src/claude_smart/events/stop.py +1 -1
  47. package/plugin/src/claude_smart/ids.py +1 -1
  48. package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
  49. package/plugin/src/claude_smart/runtime.py +7 -1
  50. package/plugin/uv.lock +1 -1
  51. package/plugin/vendor/reflexio/.env.example +54 -30
  52. package/plugin/vendor/reflexio/README.md +14 -8
  53. package/plugin/vendor/reflexio/pyproject.toml +13 -1
  54. package/plugin/vendor/reflexio/reflexio/README.md +1 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
  56. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
  57. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +210 -50
  62. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
  63. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
  64. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
  65. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
  66. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
  67. package/plugin/vendor/reflexio/reflexio/client/client.py +57 -13
  68. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
  69. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
  70. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
  71. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
  72. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
  73. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
  74. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
  75. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
  141. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +238 -10
  142. package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
  143. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +16 -5
  144. package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
  145. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +198 -6
  146. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
  147. package/plugin/vendor/reflexio/reflexio/lib/_search.py +29 -20
  148. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +2 -1
  149. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
  150. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
  151. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
  152. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
  153. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
  154. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +52 -0
  155. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
  156. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
  157. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +88 -7
  158. package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
  159. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
  160. package/plugin/vendor/reflexio/reflexio/server/README.md +45 -32
  161. package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
  162. package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
  163. package/plugin/vendor/reflexio/reflexio/server/_auth.py +65 -2
  164. package/plugin/vendor/reflexio/reflexio/server/api.py +734 -85
  165. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
  166. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +3 -1
  167. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
  168. package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
  169. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
  170. package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
  171. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
  172. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
  173. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
  174. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
  175. package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
  176. package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
  177. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
  178. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
  179. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
  180. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
  181. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
  182. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
  183. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
  184. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
  185. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
  186. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
  187. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +266 -0
  188. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
  189. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
  190. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
  191. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
  192. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
  193. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
  194. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
  195. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
  196. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
  197. package/plugin/vendor/reflexio/reflexio/server/services/README.md +26 -11
  198. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
  199. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
  200. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
  201. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
  202. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
  203. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
  204. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
  205. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
  206. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
  207. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
  208. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
  209. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
  210. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
  211. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
  212. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
  213. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
  214. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
  215. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
  216. package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
  217. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
  218. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
  219. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
  220. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +39 -16
  221. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
  222. package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
  223. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +193 -0
  224. package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
  225. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
  226. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +11 -12
  227. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
  228. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +316 -178
  229. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
  230. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
  231. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
  232. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
  233. package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
  234. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +84 -0
  235. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
  236. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
  237. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
  238. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
  239. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
  240. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
  241. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +30 -22
  242. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
  243. package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
  244. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
  245. package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
  246. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
  247. package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
  248. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +83 -6
  249. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
  250. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
  251. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
  252. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +27 -0
  253. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
  254. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  255. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
  256. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +642 -197
  257. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +61 -104
  258. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +628 -0
  259. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
  260. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +978 -184
  261. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +519 -128
  262. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +77 -3
  263. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
  264. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
  265. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +131 -20
  266. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
  267. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
  268. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +36 -53
  269. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
  270. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
  271. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +243 -7
  272. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +121 -1
  273. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +60 -0
  274. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
  275. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
  276. package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
  277. package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
  278. package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
  279. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
  280. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +229 -32
  281. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
  282. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
  283. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
  284. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
  285. package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
  286. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
  287. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
  288. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
  289. package/scripts/setup-claude-smart.sh +8 -3
  290. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
  291. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
  292. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
  293. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
  294. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
  295. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
  296. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
  297. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
  298. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
  299. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
  300. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
  301. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
  302. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
  303. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
  304. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
  305. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
  306. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
  307. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
  308. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
  309. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
  310. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
  311. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
  312. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
  313. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
  317. /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
  318. /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
  319. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
  320. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
  321. /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
  322. /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
@@ -11,11 +11,14 @@ from __future__ import annotations
11
11
  import json
12
12
  import logging
13
13
  import os
14
+ import threading
14
15
  import time
15
16
  from typing import Any, Literal
16
17
 
17
18
  import httpx
18
19
 
20
+ from reflexio.server.tracing import profile_step
21
+
19
22
  _LOGGER = logging.getLogger(__name__)
20
23
 
21
24
  EmbeddingProviderMode = Literal[
@@ -41,10 +44,22 @@ _DEFAULT_LOCAL_SERVICE_PROBE_TIMEOUT_MS = 200
41
44
  # daemon. Cap texts per request and concatenate; encode batching is a separate
42
45
  # server-side knob (REFLEXIO_EMBED_BATCH_SIZE).
43
46
  _DEFAULT_MAX_TEXTS_PER_REQUEST = 32
44
- _LOCAL_SERVICE_PROBE_CACHE_SECONDS = 5.0
47
+ # A reachable daemon is stable, so positive probe results are cached long
48
+ # enough that steady-state requests skip the sequential /health round trip.
49
+ # Failures stay short-lived so a restarted daemon is re-adopted quickly and
50
+ # the inprocess-fallback window stays small.
51
+ _LOCAL_SERVICE_PROBE_CACHE_SECONDS = 60.0
52
+ _LOCAL_SERVICE_PROBE_FAILURE_CACHE_SECONDS = 5.0
53
+ # Keep-alive must expire below the ALB's default 60s idle timeout so the pool
54
+ # never hands out a connection the load balancer already closed.
55
+ _HTTP_KEEPALIVE_EXPIRY_SECONDS = 50.0
56
+ _EMBEDDING_RETRY_BACKOFF_SECONDS = 0.1
45
57
  _SERVICE_MODES = {"local_service", "internal_service"}
46
58
  _VALID_MODES = {"cloud", *_SERVICE_MODES, "inprocess", "off"}
47
59
  _local_service_probe_cache: tuple[float, bool, str | None] | None = None
60
+ _http_client_lock = threading.Lock()
61
+ _http_client_instance: httpx.Client | None = None
62
+ _http_client_pid: int | None = None
48
63
 
49
64
 
50
65
  class EmbeddingUnavailableError(RuntimeError):
@@ -113,17 +128,46 @@ def _is_local_model(model: str | None) -> bool:
113
128
  return bool(model and model.startswith("local/"))
114
129
 
115
130
 
131
+ def _http_client() -> httpx.Client:
132
+ """Shared keep-alive client for health probes and embedding POSTs.
133
+
134
+ A per-request client pays DNS resolution plus a TCP handshake on every
135
+ embedding call. The client is recreated after fork (PID check) so worker
136
+ children never reuse sockets inherited from the parent process.
137
+ """
138
+ global _http_client_instance, _http_client_pid
139
+ pid = os.getpid()
140
+ with _http_client_lock:
141
+ if _http_client_instance is None or _http_client_pid != pid:
142
+ if _http_client_instance is not None and _http_client_pid != pid:
143
+ try:
144
+ _http_client_instance.close()
145
+ except Exception:
146
+ _LOGGER.debug(
147
+ "Failed to close stale embedding HTTP client", exc_info=True
148
+ )
149
+ _http_client_instance = httpx.Client(
150
+ limits=httpx.Limits(keepalive_expiry=_HTTP_KEEPALIVE_EXPIRY_SECONDS)
151
+ )
152
+ _http_client_pid = pid
153
+ return _http_client_instance
154
+
155
+
116
156
  def _local_service_status() -> tuple[bool, str | None]:
117
157
  global _local_service_probe_cache
118
158
  now = time.monotonic()
119
- if (
120
- _local_service_probe_cache is not None
121
- and now - _local_service_probe_cache[0] < _LOCAL_SERVICE_PROBE_CACHE_SECONDS
122
- ):
123
- return _local_service_probe_cache[1], _local_service_probe_cache[2]
159
+ if _local_service_probe_cache is not None:
160
+ cached_at, cached_reachable, cached_model = _local_service_probe_cache
161
+ ttl = (
162
+ _LOCAL_SERVICE_PROBE_CACHE_SECONDS
163
+ if cached_reachable
164
+ else _LOCAL_SERVICE_PROBE_FAILURE_CACHE_SECONDS
165
+ )
166
+ if now - cached_at < ttl:
167
+ return cached_reachable, cached_model
124
168
 
125
169
  try:
126
- response = httpx.get(
170
+ response = _http_client().get(
127
171
  f"{_local_service_url()}/health",
128
172
  timeout=_local_service_probe_timeout_seconds(),
129
173
  )
@@ -192,9 +236,9 @@ def embedding_provider_mode(model: str | None = None) -> EmbeddingProviderMode:
192
236
  """Resolve the embedding provider mode for a model.
193
237
 
194
238
  Explicit legacy env modes keep their historical behavior. With no explicit
195
- env mode, routing is model-driven: ``local/*`` uses the local daemon when it
196
- is reachable and otherwise falls back to the in-process embedder; cloud
197
- models use their provider directly.
239
+ env mode, routing is model-driven: ``local/*`` uses a configured daemon host
240
+ authoritatively, otherwise it uses the local daemon when reachable and falls
241
+ back to the in-process embedder. Cloud models use their provider directly.
198
242
  """
199
243
  configured = os.environ.get(_ENV_PROVIDER)
200
244
  if configured:
@@ -212,6 +256,9 @@ def embedding_provider_mode(model: str | None = None) -> EmbeddingProviderMode:
212
256
  if os.environ.get(_ENV_SERVICE_URL):
213
257
  return "internal_service"
214
258
 
259
+ if _is_local_model(model) and os.environ.get(_ENV_DAEMON_HOST, "").strip():
260
+ return "local_service"
261
+
215
262
  if _is_local_model(model):
216
263
  return "local_service" if _local_service_supports_model(model) else "inprocess"
217
264
  return "cloud"
@@ -286,17 +333,29 @@ def _post_embedding_batch(
286
333
  last_error: Exception | None = None
287
334
  for attempt in range(2):
288
335
  try:
289
- with httpx.Client(timeout=timeout) as client:
290
- response = client.post(url, json=payload)
336
+ response = _http_client().post(url, json=payload, timeout=timeout)
291
337
  response.raise_for_status()
292
338
  body = response.json()
293
339
  return _ordered_embeddings_from_response(body.get("data"), len(texts))
294
- except (httpx.ConnectError, httpx.ConnectTimeout) as exc:
295
- # Connection never established — the request never reached the
296
- # server, so a single retry is safe and cannot duplicate work.
340
+ except (
341
+ httpx.ConnectError,
342
+ httpx.ConnectTimeout,
343
+ httpx.RemoteProtocolError,
344
+ ) as exc:
345
+ # Connection never established, or the server closed a pooled
346
+ # keep-alive connection before sending a response (stale-reuse
347
+ # race) — the request was not processed, so a single retry is
348
+ # safe and cannot duplicate work.
297
349
  last_error = exc
298
350
  if attempt == 0:
299
- time.sleep(0.1)
351
+ with profile_step(
352
+ "search.embedding.api.retry_backoff",
353
+ retry_reason=type(exc).__name__,
354
+ retry_backoff_ms=int(_EMBEDDING_RETRY_BACKOFF_SECONDS * 1000),
355
+ attempt=attempt + 1,
356
+ max_attempts=2,
357
+ ):
358
+ time.sleep(_EMBEDDING_RETRY_BACKOFF_SECONDS)
300
359
  except (httpx.HTTPError, json.JSONDecodeError, ValueError) as exc:
301
360
  # The server already received the request: a read timeout means it
302
361
  # is still encoding, and retrying would queue a second identical
@@ -0,0 +1,280 @@
1
+ """openClaw CLI as a LiteLLM custom provider.
2
+
3
+ Routes ``litellm.completion(model="openclaw/...", ...)`` through the
4
+ user's locally-installed ``openclaw`` CLI by spawning
5
+ ``openclaw infer model run --prompt <p> --json [--model <m>]``.
6
+
7
+ Activation is opt-in via ``OPENCLAW_SMART_USE_LOCAL_CLI=1``. Without it,
8
+ the provider does not register and reflexio falls back to its normal
9
+ provider priority. Every spawn sets ``OPENCLAW_SMART_INTERNAL=1`` so the
10
+ openclaw-smart plugin's hooks short-circuit when this provider invokes
11
+ the CLI (recursion guard).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import os
19
+ import shutil
20
+ import subprocess # noqa: S404 — subprocess is the integration point.
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ import litellm
25
+ from litellm.llms.custom_llm import CustomLLM
26
+ from litellm.types.utils import Choices, Message, ModelResponse, Usage
27
+
28
+ _LOGGER = logging.getLogger(__name__)
29
+
30
+ PROVIDER_KEY = "openclaw"
31
+ ENV_ENABLE = "OPENCLAW_SMART_USE_LOCAL_CLI"
32
+ ENV_CLI_PATH = "OPENCLAW_BIN"
33
+ ENV_DEFAULT_MODEL = "OPENCLAW_DEFAULT_MODEL"
34
+ ENV_TIMEOUT = "OPENCLAW_CLI_TIMEOUT"
35
+ _DEFAULT_TIMEOUT_SECONDS = 180
36
+
37
+ _TRUTHY = {"1", "true", "yes"}
38
+
39
+ # Module-level state reset by tests via the _reset_module_state fixture.
40
+ _REGISTERED: bool = False
41
+ _HANDLER: OpenClawLLM | None = None
42
+
43
+
44
+ class OpenClawCLIError(RuntimeError):
45
+ """Raised when the openclaw CLI subprocess fails."""
46
+
47
+
48
+ def _env_enabled() -> bool:
49
+ """Return True when OPENCLAW_SMART_USE_LOCAL_CLI is truthy.
50
+
51
+ Returns:
52
+ bool: True if the opt-in env var is set to a truthy value, else False.
53
+ """
54
+ return os.environ.get(ENV_ENABLE, "").lower() in _TRUTHY
55
+
56
+
57
+ def _resolve_cli_path() -> str | None:
58
+ """Resolve the openclaw CLI absolute path.
59
+
60
+ Honors ``OPENCLAW_BIN`` env override, otherwise falls back to
61
+ ``shutil.which("openclaw")``.
62
+
63
+ Returns:
64
+ str | None: Absolute path to the openclaw binary, or None if not found.
65
+ """
66
+ override = os.environ.get(ENV_CLI_PATH)
67
+ if override:
68
+ path = Path(override)
69
+ if path.is_file() and os.access(override, os.X_OK):
70
+ return override
71
+ return shutil.which("openclaw")
72
+
73
+
74
+ def is_openclaw_available() -> bool:
75
+ """Return True when the openclaw provider is usable right now.
76
+
77
+ Both the opt-in env var *and* a resolvable CLI path are required.
78
+
79
+ Returns:
80
+ bool: True iff ``OPENCLAW_SMART_USE_LOCAL_CLI`` is truthy AND a
81
+ CLI binary is resolvable.
82
+ """
83
+ return _env_enabled() and _resolve_cli_path() is not None
84
+
85
+
86
+ def _extract_text(payload: Any) -> str:
87
+ """Extract the model's text reply from openclaw's JSON output.
88
+
89
+ The exact field name in ``openclaw infer model run --json`` output
90
+ couldn't be probed at design time (no model providers were
91
+ auth-configured). Scan common keys in priority order: ``text`` →
92
+ ``content`` → ``output`` → ``message.content``. The first end-to-end
93
+ test will surface the real key; we can pin it then if needed.
94
+
95
+ Args:
96
+ payload (Any): Parsed JSON response from openclaw.
97
+
98
+ Returns:
99
+ str: Completion text, or empty string if no known key matches.
100
+ """
101
+ if not isinstance(payload, dict):
102
+ return ""
103
+ for key in ("text", "content", "output"):
104
+ value = payload.get(key)
105
+ if isinstance(value, str):
106
+ return value
107
+ msg = payload.get("message")
108
+ if isinstance(msg, dict) and isinstance(msg.get("content"), str):
109
+ return msg["content"]
110
+ return ""
111
+
112
+
113
+ def _call_cli(*, prompt: str, model: str | None, timeout_s: int) -> str:
114
+ """Invoke openclaw CLI for a one-shot completion.
115
+
116
+ Args:
117
+ prompt (str): User prompt text passed via ``--prompt``.
118
+ model (str | None): Optional model id passed via ``--model``.
119
+ timeout_s (int): Subprocess timeout in seconds.
120
+
121
+ Returns:
122
+ str: Completion text extracted from the CLI's JSON stdout.
123
+
124
+ Raises:
125
+ OpenClawCLIError: If the CLI is not found, exits non-zero, returns
126
+ non-JSON stdout, or returns JSON with no recognized text field.
127
+ """
128
+ cli = _resolve_cli_path()
129
+ if not cli:
130
+ raise OpenClawCLIError("openclaw CLI not found; set OPENCLAW_BIN")
131
+
132
+ argv = [cli, "infer", "model", "run", "--prompt", prompt, "--json"]
133
+ if model:
134
+ argv += ["--model", model]
135
+
136
+ env = os.environ.copy()
137
+ env["OPENCLAW_SMART_INTERNAL"] = "1"
138
+
139
+ try:
140
+ proc = subprocess.run( # noqa: S603 — argv is fully constructed.
141
+ argv,
142
+ env=env,
143
+ capture_output=True,
144
+ text=True,
145
+ timeout=timeout_s,
146
+ check=False,
147
+ )
148
+ except subprocess.TimeoutExpired as exc:
149
+ raise OpenClawCLIError(f"openclaw timed out after {timeout_s}s") from exc
150
+
151
+ if proc.returncode != 0:
152
+ raise OpenClawCLIError(
153
+ proc.stderr.strip() or f"openclaw exit {proc.returncode}"
154
+ )
155
+
156
+ try:
157
+ payload = json.loads(proc.stdout)
158
+ except json.JSONDecodeError as exc:
159
+ raise OpenClawCLIError(
160
+ f"openclaw returned non-JSON: {proc.stdout[:200]}"
161
+ ) from exc
162
+
163
+ text = _extract_text(payload)
164
+ if not text:
165
+ raise OpenClawCLIError(
166
+ f"openclaw JSON had no completion text: {list(payload)[:5]}"
167
+ )
168
+ return text
169
+
170
+
171
+ class OpenClawLLM(CustomLLM):
172
+ """LiteLLM CustomLLM that routes completions through the openclaw CLI."""
173
+
174
+ def completion(self, *args: Any, **kwargs: Any) -> ModelResponse: # noqa: ARG002 — LiteLLM CustomLLM signature
175
+ """Synchronous completion via ``openclaw infer model run``.
176
+
177
+ The ``openclaw/`` prefix is stripped from the requested model id.
178
+ If no model is provided, ``OPENCLAW_DEFAULT_MODEL`` is used; if
179
+ that's also unset, the CLI's own default is used.
180
+
181
+ Args:
182
+ *args: Unused (LiteLLM signature).
183
+ **kwargs: LiteLLM completion kwargs (``messages``, ``model``).
184
+
185
+ Returns:
186
+ ModelResponse: Reply text in ``choices[0].message.content``.
187
+ """
188
+ # ``kwargs.get("model")`` can be ``None`` — coercing via ``str(...)``
189
+ # would yield the literal "None" and skip the default-model fallback.
190
+ raw_model = kwargs.get("model")
191
+ model_kwarg = "" if raw_model is None else str(raw_model)
192
+ if "/" in model_kwarg:
193
+ model = model_kwarg.split("/", 1)[1] or None
194
+ else:
195
+ model = model_kwarg or None
196
+ if not model:
197
+ model = os.environ.get(ENV_DEFAULT_MODEL) or None
198
+
199
+ messages = kwargs.get("messages", [])
200
+ prompt = _messages_to_prompt(messages)
201
+ # An invalid OPENCLAW_CLI_TIMEOUT must not crash the completion path —
202
+ # fall back to the default with a warning so misconfiguration degrades
203
+ # to working-but-slow rather than working-then-erroring.
204
+ raw_timeout = os.environ.get(ENV_TIMEOUT)
205
+ try:
206
+ timeout_s = (
207
+ int(raw_timeout)
208
+ if raw_timeout is not None
209
+ else _DEFAULT_TIMEOUT_SECONDS
210
+ )
211
+ except ValueError:
212
+ _LOGGER.warning(
213
+ "Invalid %s=%r; using default %d",
214
+ ENV_TIMEOUT,
215
+ raw_timeout,
216
+ _DEFAULT_TIMEOUT_SECONDS,
217
+ )
218
+ timeout_s = _DEFAULT_TIMEOUT_SECONDS
219
+
220
+ text = _call_cli(prompt=prompt, model=model, timeout_s=timeout_s)
221
+
222
+ return ModelResponse(
223
+ choices=[Choices(message=Message(content=text, role="assistant"))],
224
+ usage=Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
225
+ model=model_kwarg or "openclaw",
226
+ )
227
+
228
+
229
+ def _messages_to_prompt(messages: list[dict[str, Any]]) -> str:
230
+ """Flatten chat messages into a single prompt string.
231
+
232
+ ``openclaw infer model run`` takes a single ``--prompt``; we serialize
233
+ the chat as ``role: content`` lines separated by blank lines. Content
234
+ blocks (lists) are flattened to text.
235
+
236
+ Args:
237
+ messages (list[dict[str, Any]]): LiteLLM-style chat messages.
238
+
239
+ Returns:
240
+ str: Newline-joined role:content lines.
241
+ """
242
+ parts: list[str] = []
243
+ for m in messages:
244
+ role = m.get("role", "user")
245
+ content = m.get("content", "")
246
+ if isinstance(content, list):
247
+ content = "".join(c.get("text", "") for c in content if isinstance(c, dict))
248
+ parts.append(f"{role}: {content}")
249
+ return "\n\n".join(parts)
250
+
251
+
252
+ def register_if_enabled() -> bool:
253
+ """Register the openclaw provider with LiteLLM if the opt-in env is set.
254
+
255
+ Idempotent: safe to call multiple times.
256
+
257
+ Returns:
258
+ bool: True if the provider is now registered, False if env is off or
259
+ the CLI is missing.
260
+ """
261
+ global _REGISTERED, _HANDLER
262
+ if _REGISTERED:
263
+ return True
264
+ if not _env_enabled():
265
+ return False
266
+ if not _resolve_cli_path():
267
+ _LOGGER.warning(
268
+ "%s=1 but openclaw CLI not found; set %s",
269
+ ENV_ENABLE,
270
+ ENV_CLI_PATH,
271
+ )
272
+ return False
273
+ _HANDLER = OpenClawLLM()
274
+ litellm.custom_provider_map = [
275
+ *getattr(litellm, "custom_provider_map", []),
276
+ {"provider": PROVIDER_KEY, "custom_handler": _HANDLER},
277
+ ]
278
+ _REGISTERED = True
279
+ _LOGGER.info("Registered openclaw LiteLLM provider")
280
+ return True
@@ -0,0 +1,48 @@
1
+ """Plain, dependency-free per-run token accounting (OSS-safe).
2
+
3
+ Folds the per-turn token counts already present on a ToolLoopTrace into a single
4
+ run total. The enterprise billing layer converts this into a TokenUsage; OSS never
5
+ imports reflexio_ext.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class RunTokenTotals:
16
+ """Accumulated token counts for a single extraction agent run."""
17
+
18
+ prompt_tokens: int = 0
19
+ completion_tokens: int = 0
20
+
21
+ def add(self, *, prompt_tokens: int | None, completion_tokens: int | None) -> None:
22
+ """Accumulate token counts, treating None as 0.
23
+
24
+ Args:
25
+ prompt_tokens: Prompt token count for one turn, or None if unavailable.
26
+ completion_tokens: Completion token count for one turn, or None if unavailable.
27
+ """
28
+ self.prompt_tokens += int(prompt_tokens or 0)
29
+ self.completion_tokens += int(completion_tokens or 0)
30
+
31
+
32
+ def sum_trace_tokens(trace: Any) -> RunTokenTotals:
33
+ """Fold a ToolLoopTrace's per-turn token counts into one RunTokenTotals.
34
+
35
+ Args:
36
+ trace: A ToolLoopTrace (or duck-typed equivalent) with a ``turns`` attribute.
37
+ Each turn may have ``prompt_tokens`` and ``completion_tokens`` attributes.
38
+
39
+ Returns:
40
+ RunTokenTotals with the summed prompt and completion tokens across all turns.
41
+ """
42
+ totals = RunTokenTotals()
43
+ for turn in getattr(trace, "turns", []) or []:
44
+ totals.add(
45
+ prompt_tokens=getattr(turn, "prompt_tokens", None),
46
+ completion_tokens=getattr(turn, "completion_tokens", None),
47
+ )
48
+ return totals