claude-smart 0.2.42 → 0.2.44

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 (402) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/README.md +1 -1
  3. package/bin/claude-smart.js +2 -2
  4. package/package.json +9 -3
  5. package/plugin/.claude-plugin/plugin.json +9 -3
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +23 -3
  8. package/plugin/pyproject.toml +3 -3
  9. package/plugin/scripts/_lib.sh +91 -0
  10. package/plugin/scripts/backend-service.sh +51 -4
  11. package/plugin/scripts/cli.sh +3 -1
  12. package/plugin/scripts/codex-hook.js +72 -4
  13. package/plugin/scripts/dashboard-build.sh +1 -0
  14. package/plugin/scripts/dashboard-service.sh +1 -0
  15. package/plugin/scripts/ensure-plugin-root.sh +1 -0
  16. package/plugin/scripts/hook_entry.sh +6 -3
  17. package/plugin/scripts/smart-install.sh +3 -2
  18. package/plugin/src/README.md +57 -0
  19. package/plugin/src/claude_smart/context_format.py +11 -12
  20. package/plugin/src/claude_smart/cs_cite.py +26 -12
  21. package/plugin/src/claude_smart/ids.py +13 -5
  22. package/plugin/uv.lock +126 -5
  23. package/plugin/vendor/reflexio/.env.example +62 -0
  24. package/plugin/vendor/reflexio/LICENSE +201 -0
  25. package/plugin/vendor/reflexio/README.md +338 -0
  26. package/plugin/vendor/reflexio/pyproject.toml +274 -0
  27. package/plugin/vendor/reflexio/reflexio/README.md +184 -0
  28. package/plugin/vendor/reflexio/reflexio/__init__.py +166 -0
  29. package/plugin/vendor/reflexio/reflexio/benchmarks/__init__.py +1 -0
  30. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/README.md +109 -0
  31. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/__init__.py +1 -0
  32. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/backends.py +175 -0
  33. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/bench.py +642 -0
  34. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/embed_cache.py +330 -0
  35. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/report.py +317 -0
  36. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/report.md +43 -0
  37. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/results/results.json +4478 -0
  38. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/scenarios.py +134 -0
  39. package/plugin/vendor/reflexio/reflexio/benchmarks/retrieval_latency/seed.py +255 -0
  40. package/plugin/vendor/reflexio/reflexio/cli/README.md +287 -0
  41. package/plugin/vendor/reflexio/reflexio/cli/__init__.py +0 -0
  42. package/plugin/vendor/reflexio/reflexio/cli/__main__.py +56 -0
  43. package/plugin/vendor/reflexio/reflexio/cli/_client.py +86 -0
  44. package/plugin/vendor/reflexio/reflexio/cli/app.py +127 -0
  45. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +265 -0
  46. package/plugin/vendor/reflexio/reflexio/cli/codex_auth.py +503 -0
  47. package/plugin/vendor/reflexio/reflexio/cli/commands/__init__.py +0 -0
  48. package/plugin/vendor/reflexio/reflexio/cli/commands/admin_cmd.py +65 -0
  49. package/plugin/vendor/reflexio/reflexio/cli/commands/agent_playbooks.py +503 -0
  50. package/plugin/vendor/reflexio/reflexio/cli/commands/api.py +114 -0
  51. package/plugin/vendor/reflexio/reflexio/cli/commands/auth.py +109 -0
  52. package/plugin/vendor/reflexio/reflexio/cli/commands/config_cmd.py +511 -0
  53. package/plugin/vendor/reflexio/reflexio/cli/commands/doctor.py +127 -0
  54. package/plugin/vendor/reflexio/reflexio/cli/commands/embeddings.py +53 -0
  55. package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +478 -0
  56. package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +303 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +289 -0
  58. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +964 -0
  59. package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +285 -0
  60. package/plugin/vendor/reflexio/reflexio/cli/commands/status_cmd.py +143 -0
  61. package/plugin/vendor/reflexio/reflexio/cli/commands/user_playbooks.py +373 -0
  62. package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +284 -0
  63. package/plugin/vendor/reflexio/reflexio/cli/errors.py +217 -0
  64. package/plugin/vendor/reflexio/reflexio/cli/log_format.py +247 -0
  65. package/plugin/vendor/reflexio/reflexio/cli/output.py +867 -0
  66. package/plugin/vendor/reflexio/reflexio/cli/paths.py +41 -0
  67. package/plugin/vendor/reflexio/reflexio/cli/run_services.py +391 -0
  68. package/plugin/vendor/reflexio/reflexio/cli/state.py +204 -0
  69. package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +96 -0
  70. package/plugin/vendor/reflexio/reflexio/cli/utils.py +329 -0
  71. package/plugin/vendor/reflexio/reflexio/client/__init__.py +3 -0
  72. package/plugin/vendor/reflexio/reflexio/client/cache.py +150 -0
  73. package/plugin/vendor/reflexio/reflexio/client/client.py +2613 -0
  74. package/plugin/vendor/reflexio/reflexio/defaults.py +23 -0
  75. package/plugin/vendor/reflexio/reflexio/integrations/__init__.py +0 -0
  76. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/.clawhubignore +7 -0
  77. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +274 -0
  78. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +517 -0
  79. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +473 -0
  80. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +2156 -0
  81. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +18 -0
  82. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +241 -0
  83. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +140 -0
  84. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +130 -0
  85. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +113 -0
  86. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +52 -0
  87. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +103 -0
  88. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +156 -0
  89. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +134 -0
  90. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +41 -0
  91. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +17 -0
  92. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +24 -0
  93. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +48 -0
  94. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +278 -0
  95. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +164 -0
  96. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +36 -0
  97. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +35 -0
  98. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +27 -0
  99. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +31 -0
  100. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +42 -0
  101. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +49 -0
  102. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +91 -0
  103. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +50 -0
  104. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tsconfig.json +16 -0
  105. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/types/openclaw.d.ts +230 -0
  106. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +13 -0
  107. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/README.md +120 -0
  108. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/TESTING.md +168 -0
  109. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package-lock.json +1657 -0
  110. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/package.json +16 -0
  111. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/HEARTBEAT.md +6 -0
  112. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/README.md +84 -0
  113. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/SKILL.md +194 -0
  114. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/_meta.json +6 -0
  115. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/agents/reflexio-extractor.md +45 -0
  116. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/handler.ts +214 -0
  117. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/hook/setup.ts +55 -0
  118. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/index.ts +327 -0
  119. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/consolidate.ts +233 -0
  120. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/dedup.ts +80 -0
  121. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/io.ts +155 -0
  122. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/openclaw-cli.ts +67 -0
  123. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/search.ts +33 -0
  124. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-playbook.ts +76 -0
  125. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/lib/write-profile.ts +79 -0
  126. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/openclaw.plugin.json +46 -0
  127. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/package.json +18 -0
  128. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/README.md +36 -0
  129. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/full_consolidation.md +56 -0
  130. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/playbook_extraction.md +217 -0
  131. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/prompts/profile_extraction.md +132 -0
  132. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-consolidate/SKILL.md +33 -0
  133. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/plugin/skills/reflexio-embedded/SKILL.md +194 -0
  134. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/HOOK.md +18 -0
  135. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/architecture.md +49 -0
  136. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/comparison.md +31 -0
  137. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/future-work.md +47 -0
  138. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/references/porting-notes.md +52 -0
  139. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/install.sh +52 -0
  140. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/scripts/uninstall.sh +36 -0
  141. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/consolidate.test.ts +135 -0
  142. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/dedup.test.ts +104 -0
  143. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/io.test.ts +175 -0
  144. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/search.test.ts +66 -0
  145. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/smoke-test.ts +140 -0
  146. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-playbook.test.ts +93 -0
  147. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tests/write-profile.test.ts +174 -0
  148. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/tsconfig.json +16 -0
  149. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/types/openclaw.d.ts +230 -0
  150. package/plugin/vendor/reflexio/reflexio/integrations/openclaw-embedded/vitest.config.ts +7 -0
  151. package/plugin/vendor/reflexio/reflexio/lib/__init__.py +23 -0
  152. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +310 -0
  153. package/plugin/vendor/reflexio/reflexio/lib/_base.py +225 -0
  154. package/plugin/vendor/reflexio/reflexio/lib/_config.py +83 -0
  155. package/plugin/vendor/reflexio/reflexio/lib/_dashboard.py +266 -0
  156. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +176 -0
  157. package/plugin/vendor/reflexio/reflexio/lib/_interactions.py +334 -0
  158. package/plugin/vendor/reflexio/reflexio/lib/_operations.py +153 -0
  159. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +545 -0
  160. package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +52 -0
  161. package/plugin/vendor/reflexio/reflexio/lib/_search.py +167 -0
  162. package/plugin/vendor/reflexio/reflexio/lib/_storage_labels.py +103 -0
  163. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +288 -0
  164. package/plugin/vendor/reflexio/reflexio/lib/reflexio_lib.py +27 -0
  165. package/plugin/vendor/reflexio/reflexio/models/__init__.py +0 -0
  166. package/plugin/vendor/reflexio/reflexio/models/api_schema/__init__.py +0 -0
  167. package/plugin/vendor/reflexio/reflexio/models/api_schema/braintrust_schema.py +141 -0
  168. package/plugin/vendor/reflexio/reflexio/models/api_schema/common.py +41 -0
  169. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +3 -0
  170. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +1112 -0
  171. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +63 -0
  172. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +487 -0
  173. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +28 -0
  174. package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +83 -0
  175. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +768 -0
  176. package/plugin/vendor/reflexio/reflexio/models/api_schema/service_schemas.py +9 -0
  177. package/plugin/vendor/reflexio/reflexio/models/api_schema/stall_state_schema.py +32 -0
  178. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/__init__.py +3 -0
  179. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +177 -0
  180. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +129 -0
  181. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/enums.py +25 -0
  182. package/plugin/vendor/reflexio/reflexio/models/api_schema/validators.py +333 -0
  183. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +908 -0
  184. package/plugin/vendor/reflexio/reflexio/models/py.typed +0 -0
  185. package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +90 -0
  186. package/plugin/vendor/reflexio/reflexio/server/README.md +622 -0
  187. package/plugin/vendor/reflexio/reflexio/server/__init__.py +210 -0
  188. package/plugin/vendor/reflexio/reflexio/server/__main__.py +132 -0
  189. package/plugin/vendor/reflexio/reflexio/server/_auth.py +25 -0
  190. package/plugin/vendor/reflexio/reflexio/server/api.py +2868 -0
  191. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/README.md +34 -0
  192. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/account_api.py +143 -0
  193. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/health_api.py +91 -0
  194. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/pending_tool_call_api.py +572 -0
  195. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +66 -0
  196. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/publisher_api.py +562 -0
  197. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +50 -0
  198. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/stall_state_api.py +100 -0
  199. package/plugin/vendor/reflexio/reflexio/server/cache/__init__.py +15 -0
  200. package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +208 -0
  201. package/plugin/vendor/reflexio/reflexio/server/correlation.py +46 -0
  202. package/plugin/vendor/reflexio/reflexio/server/llm/__init__.py +30 -0
  203. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +359 -0
  204. package/plugin/vendor/reflexio/reflexio/server/llm/image_utils.py +55 -0
  205. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1871 -0
  206. package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +140 -0
  207. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +479 -0
  208. package/plugin/vendor/reflexio/reflexio/server/llm/providers/__init__.py +1 -0
  209. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1122 -0
  210. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_stream_parser.py +197 -0
  211. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +338 -0
  212. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +213 -0
  213. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +288 -0
  214. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/__init__.py +6 -0
  215. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +187 -0
  216. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/llm_reranker.py +148 -0
  217. package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +716 -0
  218. package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +179 -0
  219. package/plugin/vendor/reflexio/reflexio/server/prompt/__init__.py +0 -0
  220. package/plugin/vendor/reflexio/reflexio/server/prompt/_dispatchers.py +54 -0
  221. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/README.md +121 -0
  222. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation/v1.0.0.prompt.md +58 -0
  223. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/agent_success_evaluation_with_comparison/v1.0.0.prompt.md +76 -0
  224. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/answer_synthesis/v1.5.2.prompt.md +88 -0
  225. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/compress_session_for_query/v1.3.0.prompt.md +31 -0
  226. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +20 -0
  227. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.0.0.prompt.md +53 -0
  228. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.1.0.prompt.md +57 -0
  229. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.2.0.prompt.md +68 -0
  230. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.3.0.prompt.md +70 -0
  231. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.4.0.prompt.md +77 -0
  232. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.5.0.prompt.md +82 -0
  233. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +83 -0
  234. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.1.0.prompt.md +193 -0
  235. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +206 -0
  236. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0-deprecated.prompt.md +66 -0
  237. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.0.0.prompt.md +43 -0
  238. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v1.1.0.prompt.md +46 -0
  239. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0-deprecated.prompt.md +64 -0
  240. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.0.0.prompt.md +39 -0
  241. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.1.0.prompt.md +39 -0
  242. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.2.0.prompt.md +47 -0
  243. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md +58 -0
  244. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md +69 -0
  245. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +71 -0
  246. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.0.2.prompt.md +254 -0
  247. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.1.0.prompt.md +274 -0
  248. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md +283 -0
  249. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md +234 -0
  250. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +244 -0
  251. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v1.0.0.prompt.md +73 -0
  252. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v2.0.0.prompt.md +86 -0
  253. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.0.0.prompt.md +97 -0
  254. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.1.0.prompt.md +119 -0
  255. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.2.0.prompt.md +123 -0
  256. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +137 -0
  257. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.0.0.prompt.md +14 -0
  258. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.1.0.prompt.md +24 -0
  259. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +29 -0
  260. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.0.0.prompt.md +11 -0
  261. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.1.0.prompt.md +21 -0
  262. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main_expert/v1.2.0.prompt.md +25 -0
  263. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.0.0.prompt.md +37 -0
  264. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.1.0.prompt.md +40 -0
  265. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_optimizer_judge/v1.2.0.prompt.md +36 -0
  266. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v1.0.0.prompt.md +45 -0
  267. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v2.0.0.prompt.md +81 -0
  268. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate/v3.0.0.prompt.md +80 -0
  269. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_should_generate_expert/v1.0.0.prompt.md +34 -0
  270. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_deduplication/v1.0.0.prompt.md +116 -0
  271. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate/v1.0.0.prompt.md +33 -0
  272. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_should_generate_override/v1.0.0.prompt.md +16 -0
  273. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +140 -0
  274. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +160 -0
  275. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_main/v1.0.0.prompt.md +14 -0
  276. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +19 -0
  277. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/rerank_relevance/v1.1.0.prompt.md +44 -0
  278. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +43 -0
  279. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_content_evaluation/v1.0.0.prompt.md +33 -0
  280. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/feedback_extraction_main_v1.jsonl +10 -0
  281. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_evaluation/prompt_evaluation_dataset/profile_update_main_v1.jsonl +10 -0
  282. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +280 -0
  283. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_schema.py +11 -0
  284. package/plugin/vendor/reflexio/reflexio/server/services/README.md +58 -0
  285. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +131 -0
  286. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +60 -0
  287. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_service.py +228 -0
  288. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +87 -0
  289. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluator.py +372 -0
  290. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/delayed_group_evaluator.py +156 -0
  291. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/group_evaluation_runner.py +336 -0
  292. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +471 -0
  293. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +1668 -0
  294. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/__init__.py +0 -0
  295. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_cron.py +196 -0
  296. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/_encryption.py +101 -0
  297. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/client.py +167 -0
  298. package/plugin/vendor/reflexio/reflexio/server/services/braintrust/service.py +281 -0
  299. package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +179 -0
  300. package/plugin/vendor/reflexio/reflexio/server/services/configurator/config_storage.py +62 -0
  301. package/plugin/vendor/reflexio/reflexio/server/services/configurator/configurator.py +87 -0
  302. package/plugin/vendor/reflexio/reflexio/server/services/configurator/local_file_config_storage.py +187 -0
  303. package/plugin/vendor/reflexio/reflexio/server/services/configurator/test_config_storage.py +162 -0
  304. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +112 -0
  305. package/plugin/vendor/reflexio/reflexio/server/services/embedding_text.py +62 -0
  306. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/__init__.py +0 -0
  307. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/distribution.py +33 -0
  308. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +126 -0
  309. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +192 -0
  310. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/hero_state.py +75 -0
  311. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/rule_attribution.py +97 -0
  312. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +515 -0
  313. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/shadow_aggregation.py +90 -0
  314. package/plugin/vendor/reflexio/reflexio/server/services/extraction/__init__.py +0 -0
  315. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +91 -0
  316. package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +303 -0
  317. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +25 -0
  318. package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +358 -0
  319. package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +138 -0
  320. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +217 -0
  321. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +535 -0
  322. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +171 -0
  323. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +779 -0
  324. package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +1125 -0
  325. package/plugin/vendor/reflexio/reflexio/server/services/extractor_config_utils.py +94 -0
  326. package/plugin/vendor/reflexio/reflexio/server/services/extractor_interaction_utils.py +251 -0
  327. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +702 -0
  328. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +835 -0
  329. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +89 -0
  330. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_aggregator.py +1388 -0
  331. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_consolidator.py +1045 -0
  332. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_extractor.py +436 -0
  333. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_generation_service.py +808 -0
  334. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_constants.py +28 -0
  335. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +362 -0
  336. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/__init__.py +24 -0
  337. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/assistant_webhook.py +246 -0
  338. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/gepa_adapter.py +291 -0
  339. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/judge.py +97 -0
  340. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +96 -0
  341. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +645 -0
  342. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/rollout.py +35 -0
  343. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scenario_resolver.py +93 -0
  344. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +174 -0
  345. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/__init__.py +26 -0
  346. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_document_expander.py +179 -0
  347. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +297 -0
  348. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_deduplicator.py +772 -0
  349. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_extractor.py +462 -0
  350. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service.py +737 -0
  351. package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +290 -0
  352. package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +17 -0
  353. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_extractor.py +247 -0
  354. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service.py +803 -0
  355. package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +146 -0
  356. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/__init__.py +0 -0
  357. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +80 -0
  358. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
  359. package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +756 -0
  360. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/__init__.py +1 -0
  361. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +184 -0
  362. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +81 -0
  363. package/plugin/vendor/reflexio/reflexio/server/services/storage/constants.py +2 -0
  364. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +11 -0
  365. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +154 -0
  366. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +155 -0
  367. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +59 -0
  368. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +1298 -0
  369. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +1945 -0
  370. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +600 -0
  371. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +346 -0
  372. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1378 -0
  373. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +747 -0
  374. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +263 -0
  375. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +193 -0
  376. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_share_links.py +166 -0
  377. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +217 -0
  378. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +153 -0
  379. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +384 -0
  380. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +71 -0
  381. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +235 -0
  382. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +170 -0
  383. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +677 -0
  384. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +250 -0
  385. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +154 -0
  386. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +130 -0
  387. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_share_links.py +93 -0
  388. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_stall_state.py +76 -0
  389. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +572 -0
  390. package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +77 -0
  391. package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +116 -0
  392. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_manager.py +263 -0
  393. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +13 -0
  394. package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/llm_model_setting.json +7 -0
  395. package/plugin/vendor/reflexio/reflexio/server/tracing.py +158 -0
  396. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +113 -0
  397. package/plugin/vendor/reflexio/reflexio/server/uvicorn_logging.py +76 -0
  398. package/plugin/vendor/reflexio/reflexio/test_support/__init__.py +1 -0
  399. package/plugin/vendor/reflexio/reflexio/test_support/llm_fixtures.py +62 -0
  400. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +242 -0
  401. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +129 -0
  402. package/plugin/vendor/reflexio/reflexio/test_support/skip_decorators.py +43 -0
@@ -0,0 +1,716 @@
1
+ """Tool-calling primitives shared by agentic extraction and search pipelines."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import time
8
+ from collections.abc import Callable
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING, Any, Literal
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
15
+
16
+ from reflexio.server.llm.llm_utils import make_strict_json_schema
17
+ from reflexio.server.llm.model_defaults import ModelRole, resolve_model_name
18
+
19
+ if TYPE_CHECKING:
20
+ from reflexio.server.llm.litellm_client import LiteLLMClient
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class AsyncRequestSpec:
25
+ """Pre-persistence request produced by an asynchronous information tool."""
26
+
27
+ tool_name: str
28
+ dedup_key: str
29
+ scope: dict[str, Any]
30
+ question_text: str
31
+ answer_format: str | None = None
32
+ args: dict[str, Any] = field(default_factory=dict)
33
+ tags: list[str] = field(default_factory=list)
34
+ cache_until_seconds: int = 300
35
+ valid_until_seconds: int = 2_592_000
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class Completed:
40
+ """Synchronous tool result."""
41
+
42
+ result: dict[str, Any]
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class AsyncAccepted:
47
+ """Accepted asynchronous tool request returned as a normal tool result."""
48
+
49
+ pending_tool_call_id: str
50
+ result: dict[str, Any]
51
+
52
+
53
+ ToolOutcome = Completed | AsyncAccepted
54
+ ToolHandlerResult = dict[str, Any] | ToolOutcome
55
+
56
+
57
+ class Tool(BaseModel):
58
+ """A single LLM-callable tool.
59
+
60
+ Arguments are defined by a Pydantic model (its schema goes to the LLM,
61
+ its docstring becomes the tool description). The handler takes a
62
+ validated args instance plus a caller-supplied context object and
63
+ returns a JSON-serialisable dict that is fed back as the tool result.
64
+ """
65
+
66
+ model_config = ConfigDict(arbitrary_types_allowed=True)
67
+
68
+ name: str
69
+ args_model: type[BaseModel]
70
+ handler: Callable[[BaseModel, Any], ToolHandlerResult]
71
+ strict: bool = True
72
+
73
+ def openai_spec(self) -> dict:
74
+ parameters = self.args_model.model_json_schema()
75
+ if self.strict:
76
+ parameters = make_strict_json_schema(parameters)
77
+ return {
78
+ "type": "function",
79
+ "function": {
80
+ "name": self.name,
81
+ "description": (self.args_model.__doc__ or "").strip(),
82
+ "parameters": parameters,
83
+ "strict": self.strict,
84
+ },
85
+ }
86
+
87
+
88
+ class AsyncInfoTool(Tool):
89
+ """Marker type for tools that register async work and continue the loop."""
90
+
91
+
92
+ def _coerce_tool_outcome(value: ToolHandlerResult) -> ToolOutcome:
93
+ if isinstance(value, Completed | AsyncAccepted):
94
+ return value
95
+ return Completed(result=value)
96
+
97
+
98
+ def _tool_result_from_outcome(
99
+ outcome: ToolOutcome,
100
+ pending_tool_call_ids: list[str] | None = None,
101
+ ) -> dict[str, Any]:
102
+ if isinstance(outcome, AsyncAccepted):
103
+ if pending_tool_call_ids is not None:
104
+ pending_tool_call_ids.append(outcome.pending_tool_call_id)
105
+ return outcome.result
106
+ return outcome.result
107
+
108
+
109
+ class ToolRegistry:
110
+ def __init__(self, tools: list[Tool] | None = None) -> None:
111
+ self._tools: dict[str, Tool] = {}
112
+ for t in tools or []:
113
+ self.register(t)
114
+
115
+ def register(self, tool: Tool) -> None:
116
+ self._tools[tool.name] = tool
117
+
118
+ def openai_specs(self) -> list[dict]:
119
+ return [t.openai_spec() for t in self._tools.values()]
120
+
121
+ def handle_outcome(self, name: str, args_json: str, ctx: Any) -> ToolOutcome:
122
+ tool = self._tools.get(name)
123
+ if tool is None:
124
+ return Completed(result={"error": f"unknown tool: {name}"})
125
+ try:
126
+ raw = json.loads(args_json or "{}")
127
+ args = tool.args_model.model_validate(raw)
128
+ except (ValidationError, json.JSONDecodeError) as e:
129
+ return Completed(result={"error": f"invalid args for {name}: {e}"})
130
+ try:
131
+ return _coerce_tool_outcome(tool.handler(args, ctx))
132
+ except Exception as e: # handler errors are recoverable tool-turn errors
133
+ logger.exception("tool handler %s failed", name)
134
+ return Completed(result={"error": f"handler error: {type(e).__name__}"})
135
+
136
+ def handle(self, name: str, args_json: str, ctx: Any) -> dict:
137
+ return _tool_result_from_outcome(self.handle_outcome(name, args_json, ctx))
138
+
139
+
140
+ class ToolLoopTurn(BaseModel):
141
+ """A single tool call turn in a tool-loop trace."""
142
+
143
+ model_config = ConfigDict(arbitrary_types_allowed=True)
144
+
145
+ tool_name: str
146
+ args: dict[str, Any]
147
+ result: dict[str, Any]
148
+ latency_ms: int
149
+ # Populated from the LLM response's ``usage`` object when available
150
+ # (native tool-call mode). All None in capability-fallback mode and
151
+ # when the provider doesn't report usage.
152
+ model: str | None = None
153
+ prompt_tokens: int | None = None
154
+ completion_tokens: int | None = None
155
+ total_tokens: int | None = None
156
+ cost_usd: float | None = None
157
+
158
+
159
+ class ToolLoopTrace(BaseModel):
160
+ """Full trace of a tool-loop execution."""
161
+
162
+ turns: list[ToolLoopTurn] = []
163
+ finished: bool = False
164
+
165
+
166
+ class ToolLoopResult(BaseModel):
167
+ """Outcome of ``run_tool_loop``: final ``ctx``, trace, and terminator reason."""
168
+
169
+ model_config = ConfigDict(arbitrary_types_allowed=True)
170
+
171
+ ctx: Any
172
+ trace: ToolLoopTrace
173
+ finished_reason: Literal["finish_tool", "no_tool_call", "max_steps", "error"]
174
+ messages: list[dict[str, Any]] = Field(default_factory=list)
175
+ pending_tool_call_ids: list[str] = Field(default_factory=list)
176
+ max_steps_remaining: int = 0
177
+
178
+
179
+ # Models we know support function calling per vendor docs but that litellm's
180
+ # model_cost registry hasn't catalogued yet. When litellm returns False
181
+ # (without raising) for a model whose name starts with one of these prefixes,
182
+ # treat that as a registry gap rather than an actual capability gap.
183
+ #
184
+ # Each entry must be justified by (a) the vendor docs and (b) a confirmed
185
+ # round-trip tool call against the live API. Update this list when litellm
186
+ # upstreams the registration so the override becomes redundant.
187
+ _TOOL_CALLING_OVERRIDES: tuple[str, ...] = (
188
+ # https://platform.minimax.io/docs/guides/text-m2-function-call says
189
+ # MiniMax-M2.7 supports tool use + interleaved thinking via OpenAI-compatible
190
+ # tools format. Verified by a live `litellm.completion(model='minimax/MiniMax-M2.7',
191
+ # tools=[...])` round-trip that returned a proper tool_call message.
192
+ # litellm 1.80.x has 'minimax/MiniMax-M2' in model_cost but not 'MiniMax-M2.7'.
193
+ "minimax/MiniMax-M2",
194
+ # MiniMax-M3 supports OpenAI-compatible tools the same way the M2 family
195
+ # does, but litellm 1.80.x has no 'minimax/MiniMax-M3' model_cost entry so
196
+ # supports_function_calling returns False. Verified by a live
197
+ # `litellm.completion(model='minimax/MiniMax-M3', tools=[...])` round-trip
198
+ # that returned a proper tool_call message (finish_reason='tool_calls').
199
+ "minimax/MiniMax-M3",
200
+ # claude-code/* models route through our local CLI provider
201
+ # (see providers/claude_code_provider.py). litellm has no registry
202
+ # entry for them, so it returns False. The provider handles tool
203
+ # calling explicitly by rendering tool specs into the system prompt
204
+ # and parsing the model's JSON output back into ChatCompletionMessageToolCall
205
+ # blocks. Verified end-to-end against the resumable extraction tool loop.
206
+ "claude-code/",
207
+ )
208
+
209
+
210
+ def supports_tool_calling(model: str) -> bool:
211
+ """Return True when litellm reports native function-calling support.
212
+
213
+ Wrapped so tests can monkeypatch the probe without touching litellm.
214
+ On any internal error we optimistically assume support — cheaper to
215
+ attempt a real call than to wrongly fall back. When litellm returns
216
+ False (without raising) for a model in :data:`_TOOL_CALLING_OVERRIDES`,
217
+ we override to True — see the constant for the rationale.
218
+
219
+ Args:
220
+ model (str): Fully-qualified model name.
221
+
222
+ Returns:
223
+ bool: True if litellm advertises function-calling for ``model``,
224
+ or the model name matches a known-good override prefix.
225
+ """
226
+ try:
227
+ import litellm
228
+
229
+ if bool(litellm.supports_function_calling(model=model)):
230
+ return True
231
+ if any(model.startswith(prefix) for prefix in _TOOL_CALLING_OVERRIDES):
232
+ logger.debug(
233
+ "litellm.supports_function_calling returned False for %s; "
234
+ "applying override (see _TOOL_CALLING_OVERRIDES)",
235
+ model,
236
+ )
237
+ return True
238
+ return False
239
+ except Exception as e:
240
+ logger.warning(
241
+ "supports_function_calling probe failed for %s: %s: %s — assuming True",
242
+ model,
243
+ type(e).__name__,
244
+ e,
245
+ )
246
+ return True
247
+
248
+
249
+ # Cap on tool-result payload size injected back into the message history
250
+ # in multi-stage mode. Without this, a single fat search response could
251
+ # blow the model's context window in two or three turns.
252
+ _MULTI_STAGE_RESULT_CHAR_CAP = 4000
253
+
254
+
255
+ def _serialize_tool_result_for_history(result: dict[str, Any]) -> str:
256
+ """Render a tool result dict as a JSON string capped at a fixed size.
257
+
258
+ Args:
259
+ result (dict[str, Any]): The tool handler's return value.
260
+
261
+ Returns:
262
+ str: A JSON string truncated to ``_MULTI_STAGE_RESULT_CHAR_CAP``
263
+ characters with a ``... [truncated]`` marker on overflow.
264
+ """
265
+ payload = json.dumps(result, default=str)
266
+ if len(payload) <= _MULTI_STAGE_RESULT_CHAR_CAP:
267
+ return payload
268
+ return f"{payload[:_MULTI_STAGE_RESULT_CHAR_CAP]}... [truncated]"
269
+
270
+
271
+ def _run_multi_stage_fallback(
272
+ *,
273
+ client: LiteLLMClient,
274
+ messages: list[dict[str, Any]],
275
+ registry: ToolRegistry,
276
+ model_role: ModelRole,
277
+ max_steps: int,
278
+ ctx: Any,
279
+ finish_tool_name: str,
280
+ multi_stage_schema: type[BaseModel],
281
+ log_label: str | None,
282
+ trace: ToolLoopTrace,
283
+ pending_tool_call_ids: list[str],
284
+ ) -> ToolLoopResult:
285
+ """Drive a multi-turn tool loop using one structured-output call per turn.
286
+
287
+ Used when the configured model lacks native tool-calling but the
288
+ caller wants observe-decide-act semantics (e.g. the search agent on
289
+ ``minimax/MiniMax-M2.7``). Each turn:
290
+
291
+ 1. Asks the model for a ``multi_stage_schema`` instance whose
292
+ ``next_call`` field carries a discriminator literal naming the
293
+ desired tool.
294
+ 2. Dispatches that call against the registry.
295
+ 3. Appends the agent's plan as an assistant message and the tool
296
+ result as a user message, so the next turn's model call sees both.
297
+
298
+ Loop terminates when ``next_call.tool == finish_tool_name`` or
299
+ ``max_steps`` is exhausted.
300
+
301
+ Args:
302
+ client (LiteLLMClient): Configured client.
303
+ messages (list[dict]): Seed message list; extended in place.
304
+ registry (ToolRegistry): Tools exposed to the LLM.
305
+ model_role (ModelRole): Role used to resolve the target model.
306
+ max_steps (int): Cap on tool-calling turns.
307
+ ctx (Any): Per-run context passed to each tool handler.
308
+ finish_tool_name (str): Sentinel literal that ends the loop.
309
+ multi_stage_schema (type[BaseModel]): Schema with a ``next_call``
310
+ discriminated-union field.
311
+ log_label (str | None): Optional llm_io.log label.
312
+ trace (ToolLoopTrace): Trace to extend with per-turn entries.
313
+
314
+ Returns:
315
+ ToolLoopResult: ``ctx``, trace, and the terminator reason.
316
+ """
317
+ if log_label:
318
+ from reflexio.server.services.service_utils import (
319
+ log_llm_messages,
320
+ log_model_response,
321
+ )
322
+
323
+ for turn_idx in range(max_steps):
324
+ turn_label = f"(multi-stage turn {turn_idx + 1})"
325
+ if log_label:
326
+ log_llm_messages(logger, f"{log_label} {turn_label}", messages)
327
+ tool_t0 = time.monotonic()
328
+ parsed = client.generate_chat_response(
329
+ messages=messages,
330
+ response_format=multi_stage_schema,
331
+ model_role=model_role,
332
+ )
333
+ if log_label:
334
+ log_model_response(logger, f"{log_label} {turn_label}", parsed)
335
+ if not isinstance(parsed, BaseModel):
336
+ raise RuntimeError(
337
+ f"Multi-stage structured call returned unexpected type {type(parsed)}"
338
+ )
339
+
340
+ next_call = getattr(parsed, "next_call", None)
341
+ if next_call is None:
342
+ raise RuntimeError(
343
+ "Multi-stage schema must expose a 'next_call' field; "
344
+ f"got {type(parsed).__name__}"
345
+ )
346
+ tool_name = getattr(next_call, "tool", None)
347
+ if not isinstance(tool_name, str):
348
+ raise RuntimeError(
349
+ "Multi-stage next_call must carry a 'tool' discriminator literal; "
350
+ f"got {type(next_call).__name__}"
351
+ )
352
+
353
+ reasoning = getattr(parsed, "reasoning", "") or ""
354
+ args_dict = next_call.model_dump(exclude={"tool"})
355
+ args_json = next_call.model_dump_json(exclude={"tool"})
356
+
357
+ # Echo the agent's plan back into history so subsequent turns can
358
+ # reason about what was tried already.
359
+ messages.append(
360
+ {
361
+ "role": "assistant",
362
+ "content": (
363
+ f"Reasoning: {reasoning}\nNext call: {tool_name}({args_json})"
364
+ ),
365
+ }
366
+ )
367
+
368
+ if tool_name == finish_tool_name:
369
+ # Dispatch finish through the registry so any ctx-side
370
+ # bookkeeping (e.g. stashing the answer) still runs.
371
+ outcome = registry.handle_outcome(tool_name, args_json, ctx)
372
+ result = _tool_result_from_outcome(outcome, pending_tool_call_ids)
373
+ trace.turns.append(
374
+ ToolLoopTurn(
375
+ tool_name=tool_name,
376
+ args=args_dict,
377
+ result=result,
378
+ latency_ms=int((time.monotonic() - tool_t0) * 1000),
379
+ )
380
+ )
381
+ trace.finished = True
382
+ return ToolLoopResult(
383
+ ctx=ctx,
384
+ trace=trace,
385
+ finished_reason="finish_tool",
386
+ messages=messages,
387
+ pending_tool_call_ids=pending_tool_call_ids,
388
+ max_steps_remaining=max_steps - turn_idx - 1,
389
+ )
390
+
391
+ outcome = registry.handle_outcome(tool_name, args_json, ctx)
392
+ result = _tool_result_from_outcome(outcome, pending_tool_call_ids)
393
+ trace.turns.append(
394
+ ToolLoopTurn(
395
+ tool_name=tool_name,
396
+ args=args_dict,
397
+ result=result,
398
+ latency_ms=int((time.monotonic() - tool_t0) * 1000),
399
+ )
400
+ )
401
+ messages.append(
402
+ {
403
+ "role": "user",
404
+ "content": (
405
+ f"Tool {tool_name} returned: "
406
+ f"{_serialize_tool_result_for_history(result)}"
407
+ ),
408
+ }
409
+ )
410
+
411
+ trace.finished = False
412
+ return ToolLoopResult(
413
+ ctx=ctx,
414
+ trace=trace,
415
+ finished_reason="max_steps",
416
+ messages=messages,
417
+ pending_tool_call_ids=pending_tool_call_ids,
418
+ max_steps_remaining=0,
419
+ )
420
+
421
+
422
+ def run_tool_loop(
423
+ client: LiteLLMClient,
424
+ messages: list[dict[str, Any]],
425
+ registry: ToolRegistry,
426
+ model_role: ModelRole,
427
+ *,
428
+ max_steps: int = 8,
429
+ ctx: Any = None,
430
+ finish_tool_name: str = "finish",
431
+ fallback_schema: type[BaseModel] | None = None,
432
+ fallback_tool_name: str | None = None,
433
+ multi_stage_schema: type[BaseModel] | None = None,
434
+ tool_choice: str | dict[str, Any] = "auto",
435
+ log_label: str | None = None,
436
+ ) -> ToolLoopResult:
437
+ """Drive an LLM through a tool-calling loop until ``finish_tool_name`` or ``max_steps``.
438
+
439
+ For providers that lack native tool-calling there are two fallback
440
+ modes (in priority order):
441
+
442
+ 1. **Multi-stage** (``multi_stage_schema`` set): one structured-output
443
+ call per turn whose parsed schema carries a ``next_call``
444
+ discriminated-union. The server dispatches ``next_call`` against
445
+ the registry, appends the result to the message history, and asks
446
+ for the next turn — preserving observe-decide-act semantics.
447
+ 2. **Single-shot** (``fallback_schema`` + ``fallback_tool_name``):
448
+ one structured-output call whose parsed list is converted into
449
+ synthetic tool calls dispatched against ``fallback_tool_name``.
450
+ All calls are planned upfront so the agent never observes any
451
+ tool result.
452
+
453
+ Args:
454
+ client (LiteLLMClient): Configured client — ``generate_chat_response``
455
+ is invoked with ``tools=`` in native mode and with
456
+ ``response_format=`` in either fallback mode.
457
+ messages (list[dict]): Seed message list; extended in place per turn.
458
+ registry (ToolRegistry): Tools exposed to the LLM.
459
+ model_role (ModelRole): Role used to resolve the target model.
460
+ max_steps (int): Cap on tool-calling turns.
461
+ ctx (Any): Caller-supplied context object passed to each tool handler.
462
+ finish_tool_name (str): Name of the sentinel tool that terminates the loop.
463
+ fallback_schema (type[BaseModel] | None): Pydantic schema for the
464
+ single-shot fallback path. Used only if ``multi_stage_schema``
465
+ is None.
466
+ fallback_tool_name (str | None): Name of the tool each single-shot
467
+ fallback item is dispatched against.
468
+ multi_stage_schema (type[BaseModel] | None): Pydantic schema for
469
+ the multi-stage fallback path. The schema must expose a
470
+ ``next_call`` field whose value is a Pydantic model carrying a
471
+ ``tool`` discriminator literal — that literal names the tool
472
+ to dispatch, all other fields become its args. Takes priority
473
+ over ``fallback_schema``.
474
+ tool_choice (str | dict): Forwarded to each native tool-calling turn.
475
+ Defaults to ``"auto"``. Pass an OpenAI tool-choice dict (e.g.
476
+ ``{"type": "function", "function": {"name": "finish"}}``) to force a
477
+ specific tool — used to make a single-tool loop behave like a forced
478
+ structured-output call.
479
+ log_label (str | None): When set, each LLM call in the loop is
480
+ mirrored into ``~/.reflexio/logs/llm_io.log`` using this label
481
+ (suffixed with ``(turn N)``, ``(fallback)``, or
482
+ ``(multi-stage turn N)``). Matches classic per-call logging
483
+ parity. Leave unset (default) to suppress file-level logging
484
+ for tool-loop callers like unit tests.
485
+
486
+ Returns:
487
+ ToolLoopResult: ``ctx``, trace, and the terminator reason.
488
+
489
+ Raises:
490
+ RuntimeError: If the model lacks tool-calling AND no fallback
491
+ (multi-stage or single-shot) is provided.
492
+ """
493
+ model = resolve_model_name(
494
+ role=model_role,
495
+ site_var_value=None,
496
+ config_override=None,
497
+ api_key_config=getattr(client.config, "api_key_config", None),
498
+ )
499
+ trace = ToolLoopTrace()
500
+ pending_tool_call_ids: list[str] = []
501
+
502
+ # Lazily import the llm_io helpers only when logging is requested —
503
+ # matches classic's per-call lazy-import pattern in profile_deduplicator.py.
504
+ if log_label:
505
+ from reflexio.server.services.service_utils import (
506
+ log_llm_messages,
507
+ log_model_response,
508
+ )
509
+
510
+ # ---- Capability fallback ------------------------------------------
511
+ if not supports_tool_calling(model):
512
+ if multi_stage_schema is not None:
513
+ return _run_multi_stage_fallback(
514
+ client=client,
515
+ messages=messages,
516
+ registry=registry,
517
+ model_role=model_role,
518
+ max_steps=max_steps,
519
+ ctx=ctx,
520
+ finish_tool_name=finish_tool_name,
521
+ multi_stage_schema=multi_stage_schema,
522
+ log_label=log_label,
523
+ trace=trace,
524
+ pending_tool_call_ids=pending_tool_call_ids,
525
+ )
526
+ if fallback_schema is None or fallback_tool_name is None:
527
+ raise RuntimeError(
528
+ f"Model {model} lacks tool-calling and no fallback_schema provided"
529
+ )
530
+ if log_label:
531
+ log_llm_messages(logger, f"{log_label} (fallback)", messages)
532
+ parsed = client.generate_chat_response(
533
+ messages=messages,
534
+ response_format=fallback_schema,
535
+ model_role=model_role,
536
+ )
537
+ if log_label:
538
+ log_model_response(logger, f"{log_label} (fallback)", parsed)
539
+ # The fallback path always passes response_format so the client
540
+ # returns a parsed BaseModel instance. Narrow the type so pyright
541
+ # can see model_fields is available.
542
+ if not isinstance(parsed, BaseModel):
543
+ raise RuntimeError(
544
+ f"Fallback structured call returned unexpected type {type(parsed)}"
545
+ )
546
+ # Expect the schema's first field to be a list of items whose
547
+ # ``model_dump_json()`` matches the fallback tool's args model.
548
+ items = getattr(parsed, next(iter(type(parsed).model_fields)))
549
+ # Respect the configured max_steps budget even on the fallback path
550
+ # — otherwise a non-tool-calling provider could blow past the loop
551
+ # cap when the structured response includes more items than expected.
552
+ bounded_items = items[:max_steps]
553
+ for item in bounded_items:
554
+ tool_t0 = time.monotonic()
555
+ outcome = registry.handle_outcome(
556
+ fallback_tool_name,
557
+ item.model_dump_json(),
558
+ ctx,
559
+ )
560
+ res = _tool_result_from_outcome(outcome, pending_tool_call_ids)
561
+ trace.turns.append(
562
+ ToolLoopTurn(
563
+ tool_name=fallback_tool_name,
564
+ args=item.model_dump(),
565
+ result=res,
566
+ latency_ms=int((time.monotonic() - tool_t0) * 1000),
567
+ )
568
+ )
569
+ exceeded = len(items) > max_steps
570
+ trace.finished = not exceeded
571
+ return ToolLoopResult(
572
+ ctx=ctx,
573
+ trace=trace,
574
+ finished_reason="max_steps" if exceeded else "finish_tool",
575
+ messages=messages,
576
+ pending_tool_call_ids=pending_tool_call_ids,
577
+ max_steps_remaining=0 if exceeded else max_steps - len(bounded_items),
578
+ )
579
+
580
+ # ---- Native tool loop ---------------------------------------------
581
+ # Local import keeps litellm_client a type-only dependency of this module.
582
+ from reflexio.server.llm.litellm_client import LiteLLMClientError
583
+
584
+ local_msgs = list(messages)
585
+ try:
586
+ for _step in range(max_steps):
587
+ if log_label:
588
+ log_llm_messages(logger, f"{log_label} (turn {_step + 1})", local_msgs)
589
+ resp = client.generate_chat_response(
590
+ messages=local_msgs,
591
+ tools=registry.openai_specs(),
592
+ tool_choice=tool_choice,
593
+ model_role=model_role,
594
+ )
595
+ if log_label:
596
+ log_model_response(logger, f"{log_label} (turn {_step + 1})", resp)
597
+
598
+ # Extract per-turn usage from the response (populated by LiteLLMClient
599
+ # when the provider reports it; None otherwise).
600
+ turn_usage = getattr(resp, "usage", None)
601
+ turn_prompt_tokens = (
602
+ getattr(turn_usage, "prompt_tokens", None) if turn_usage else None
603
+ )
604
+ turn_completion_tokens = (
605
+ getattr(turn_usage, "completion_tokens", None) if turn_usage else None
606
+ )
607
+ turn_total_tokens = (
608
+ getattr(turn_usage, "total_tokens", None) if turn_usage else None
609
+ )
610
+ turn_cost_usd = getattr(resp, "cost_usd", None)
611
+
612
+ tool_calls = getattr(resp, "tool_calls", None)
613
+ if not tool_calls:
614
+ # The model returned a plain-text turn with no tool calls. For a
615
+ # text-output agent this means "done", but the finish handler did
616
+ # NOT run, so no structured output was committed. Report a
617
+ # distinct reason so callers (and logs) don't conflate this with
618
+ # an actual finish_extraction call. Callers that require output
619
+ # (extraction) already gate success on committed output, so this
620
+ # surfaces accurately as a non-finish termination.
621
+ trace.finished = True
622
+ return ToolLoopResult(
623
+ ctx=ctx,
624
+ trace=trace,
625
+ finished_reason="no_tool_call",
626
+ messages=local_msgs,
627
+ pending_tool_call_ids=pending_tool_call_ids,
628
+ max_steps_remaining=max_steps - _step,
629
+ )
630
+ # Emit ONE assistant message carrying ALL tool_calls from this turn.
631
+ # OpenAI/Anthropic strict mode requires this shape.
632
+ local_msgs.append(
633
+ {"role": "assistant", "content": None, "tool_calls": list(tool_calls)}
634
+ )
635
+ # Process every tool call and append per-call tool result messages.
636
+ # A single response's usage is attached to every turn it produced —
637
+ # the summary helpers dedup by (model, prompt_tokens, completion_tokens).
638
+ for tc in tool_calls:
639
+ # Time each tool individually — using the turn-start clock
640
+ # would inflate later tools' latencies with model time and
641
+ # earlier tools' work, masking the actual per-tool cost.
642
+ tool_t0 = time.monotonic()
643
+ name = tc.function.name
644
+ args_json = tc.function.arguments
645
+ outcome = registry.handle_outcome(name, args_json, ctx)
646
+ result = _tool_result_from_outcome(outcome, pending_tool_call_ids)
647
+ try:
648
+ args_dict = json.loads(args_json or "{}")
649
+ except json.JSONDecodeError:
650
+ args_dict = {}
651
+ trace.turns.append(
652
+ ToolLoopTurn(
653
+ tool_name=name,
654
+ args=args_dict,
655
+ result=result,
656
+ latency_ms=int((time.monotonic() - tool_t0) * 1000),
657
+ model=model,
658
+ prompt_tokens=turn_prompt_tokens,
659
+ completion_tokens=turn_completion_tokens,
660
+ total_tokens=turn_total_tokens,
661
+ cost_usd=turn_cost_usd,
662
+ )
663
+ )
664
+ local_msgs.append(
665
+ {
666
+ "role": "tool",
667
+ "tool_call_id": tc.id,
668
+ "content": json.dumps(result),
669
+ }
670
+ )
671
+ # After processing ALL tool calls, check whether the finish sentinel
672
+ # appeared in this turn (may be alongside sibling calls).
673
+ if any(tc.function.name == finish_tool_name for tc in tool_calls):
674
+ trace.finished = True
675
+ return ToolLoopResult(
676
+ ctx=ctx,
677
+ trace=trace,
678
+ finished_reason="finish_tool",
679
+ messages=local_msgs,
680
+ pending_tool_call_ids=pending_tool_call_ids,
681
+ max_steps_remaining=max_steps - _step - 1,
682
+ )
683
+ except LiteLLMClientError as e:
684
+ # LLM failure after the client exhausted its retries and fallbacks —
685
+ # a known failure mode (timeouts, provider errors), not a bug. Log at
686
+ # warning so it doesn't surface as a Sentry error.
687
+ logger.warning("event=tool_loop_llm_error error=%s", e)
688
+ trace.finished = False
689
+ return ToolLoopResult(
690
+ ctx=ctx,
691
+ trace=trace,
692
+ finished_reason="error",
693
+ messages=local_msgs,
694
+ pending_tool_call_ids=pending_tool_call_ids,
695
+ max_steps_remaining=0,
696
+ )
697
+ except Exception:
698
+ logger.exception("Tool loop raised an unexpected exception")
699
+ trace.finished = False
700
+ return ToolLoopResult(
701
+ ctx=ctx,
702
+ trace=trace,
703
+ finished_reason="error",
704
+ messages=local_msgs,
705
+ pending_tool_call_ids=pending_tool_call_ids,
706
+ max_steps_remaining=0,
707
+ )
708
+
709
+ return ToolLoopResult(
710
+ ctx=ctx,
711
+ trace=trace,
712
+ finished_reason="max_steps",
713
+ messages=local_msgs,
714
+ pending_tool_call_ids=pending_tool_call_ids,
715
+ max_steps_remaining=0,
716
+ )