code-muse 0.0.1__py3-none-any.whl

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 (394) hide show
  1. code_muse/__init__.py +26 -0
  2. code_muse/__main__.py +10 -0
  3. code_muse/agents/__init__.py +31 -0
  4. code_muse/agents/_builder.py +214 -0
  5. code_muse/agents/_compaction.py +506 -0
  6. code_muse/agents/_diagnostics.py +171 -0
  7. code_muse/agents/_history.py +382 -0
  8. code_muse/agents/_key_listeners.py +148 -0
  9. code_muse/agents/_non_streaming_render.py +148 -0
  10. code_muse/agents/_runtime.py +596 -0
  11. code_muse/agents/agent_creator_agent.py +603 -0
  12. code_muse/agents/agent_helios.py +47 -0
  13. code_muse/agents/agent_manager.py +740 -0
  14. code_muse/agents/agent_muse.py +78 -0
  15. code_muse/agents/agent_planning.py +44 -0
  16. code_muse/agents/agent_qa_melpomene.py +207 -0
  17. code_muse/agents/base_agent.py +194 -0
  18. code_muse/agents/event_stream_handler.py +361 -0
  19. code_muse/agents/json_agent.py +201 -0
  20. code_muse/agents/prompt_v3.py +521 -0
  21. code_muse/agents/subagent_stream_handler.py +273 -0
  22. code_muse/callbacks.py +941 -0
  23. code_muse/chatgpt_codex_client.py +333 -0
  24. code_muse/claude_cache_client.py +853 -0
  25. code_muse/cli_runner/__init__.py +319 -0
  26. code_muse/cli_runner/args.py +63 -0
  27. code_muse/cli_runner/loop.py +510 -0
  28. code_muse/cli_runner/resume.py +72 -0
  29. code_muse/cli_runner/runner.py +161 -0
  30. code_muse/command_line/__init__.py +1 -0
  31. code_muse/command_line/add_model_menu.py +1331 -0
  32. code_muse/command_line/agent_menu.py +674 -0
  33. code_muse/command_line/attachments.py +397 -0
  34. code_muse/command_line/autosave_menu.py +709 -0
  35. code_muse/command_line/clipboard.py +528 -0
  36. code_muse/command_line/colors_menu.py +530 -0
  37. code_muse/command_line/command_handler.py +262 -0
  38. code_muse/command_line/command_registry.py +150 -0
  39. code_muse/command_line/config_commands.py +711 -0
  40. code_muse/command_line/core_commands.py +740 -0
  41. code_muse/command_line/diff_menu.py +865 -0
  42. code_muse/command_line/file_path_completion.py +73 -0
  43. code_muse/command_line/load_context_completion.py +57 -0
  44. code_muse/command_line/model_picker_completion.py +512 -0
  45. code_muse/command_line/model_settings_menu.py +983 -0
  46. code_muse/command_line/onboarding_slides.py +162 -0
  47. code_muse/command_line/onboarding_wizard.py +337 -0
  48. code_muse/command_line/pagination.py +41 -0
  49. code_muse/command_line/pin_command_completion.py +329 -0
  50. code_muse/command_line/prompt_toolkit_completion.py +886 -0
  51. code_muse/command_line/session_commands.py +304 -0
  52. code_muse/command_line/shell_passthrough.py +145 -0
  53. code_muse/command_line/skills_completion.py +158 -0
  54. code_muse/command_line/types.py +18 -0
  55. code_muse/command_line/uc_menu.py +908 -0
  56. code_muse/command_line/utils.py +105 -0
  57. code_muse/command_line/wiggum_state.py +77 -0
  58. code_muse/config.py +1138 -0
  59. code_muse/config_agent.py +168 -0
  60. code_muse/config_appearance.py +241 -0
  61. code_muse/config_model.py +357 -0
  62. code_muse/config_security.py +73 -0
  63. code_muse/error_logging.py +132 -0
  64. code_muse/evals/__init__.py +35 -0
  65. code_muse/evals/eval_helpers.py +81 -0
  66. code_muse/evals/eval_runner.py +299 -0
  67. code_muse/evals/sample_evals/__init__.py +1 -0
  68. code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
  69. code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
  70. code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
  71. code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
  72. code_muse/fs_scan_cache/__init__.py +31 -0
  73. code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
  74. code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
  75. code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
  76. code_muse/fs_scan_cache/tool_integration.py +309 -0
  77. code_muse/fs_scan_cache/ttl_policy.py +44 -0
  78. code_muse/gemini_code_assist.py +383 -0
  79. code_muse/gemini_model.py +838 -0
  80. code_muse/hook_engine/README.md +105 -0
  81. code_muse/hook_engine/__init__.py +21 -0
  82. code_muse/hook_engine/aliases.py +153 -0
  83. code_muse/hook_engine/engine.py +221 -0
  84. code_muse/hook_engine/executor.py +347 -0
  85. code_muse/hook_engine/matcher.py +154 -0
  86. code_muse/hook_engine/models.py +245 -0
  87. code_muse/hook_engine/registry.py +114 -0
  88. code_muse/hook_engine/trust.py +268 -0
  89. code_muse/hook_engine/validator.py +144 -0
  90. code_muse/http_utils.py +360 -0
  91. code_muse/keymap.py +128 -0
  92. code_muse/list_filtering.py +26 -0
  93. code_muse/main.py +10 -0
  94. code_muse/messaging/__init__.py +259 -0
  95. code_muse/messaging/bus.py +621 -0
  96. code_muse/messaging/commands.py +166 -0
  97. code_muse/messaging/markdown_patches.py +57 -0
  98. code_muse/messaging/message_queue.py +397 -0
  99. code_muse/messaging/messages.py +591 -0
  100. code_muse/messaging/queue_console.py +269 -0
  101. code_muse/messaging/renderers.py +308 -0
  102. code_muse/messaging/rich_renderer.py +1158 -0
  103. code_muse/messaging/shimmer.py +154 -0
  104. code_muse/messaging/spinner/__init__.py +87 -0
  105. code_muse/messaging/spinner/console_spinner.py +250 -0
  106. code_muse/messaging/spinner/spinner_base.py +82 -0
  107. code_muse/messaging/subagent_console.py +458 -0
  108. code_muse/model_factory.py +1203 -0
  109. code_muse/model_switching.py +59 -0
  110. code_muse/model_utils.py +156 -0
  111. code_muse/models.json +66 -0
  112. code_muse/models_cache/__init__.py +26 -0
  113. code_muse/models_cache/blocking_lru_cache.py +98 -0
  114. code_muse/models_cache/cache_writer.py +86 -0
  115. code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
  116. code_muse/models_cache/sha256_hash.pyx +34 -0
  117. code_muse/models_cache/startup_integration.py +75 -0
  118. code_muse/models_dev_api.json +1 -0
  119. code_muse/models_dev_parser.py +590 -0
  120. code_muse/motion.py +126 -0
  121. code_muse/plugins/__init__.py +471 -0
  122. code_muse/plugins/agent_skills/__init__.py +32 -0
  123. code_muse/plugins/agent_skills/config.py +176 -0
  124. code_muse/plugins/agent_skills/discovery.py +309 -0
  125. code_muse/plugins/agent_skills/downloader.py +389 -0
  126. code_muse/plugins/agent_skills/installer.py +19 -0
  127. code_muse/plugins/agent_skills/metadata.py +293 -0
  128. code_muse/plugins/agent_skills/prompt_builder.py +66 -0
  129. code_muse/plugins/agent_skills/register_callbacks.py +298 -0
  130. code_muse/plugins/agent_skills/remote_catalog.py +320 -0
  131. code_muse/plugins/agent_skills/skill_catalog.py +254 -0
  132. code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
  133. code_muse/plugins/agent_skills/skills_menu.py +791 -0
  134. code_muse/plugins/autonomous_memory/__init__.py +39 -0
  135. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
  136. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
  137. code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
  138. code_muse/plugins/autonomous_memory/consolidation.py +82 -0
  139. code_muse/plugins/autonomous_memory/extraction.py +382 -0
  140. code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
  141. code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
  142. code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
  143. code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
  144. code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
  145. code_muse/plugins/aws_bedrock/__init__.py +14 -0
  146. code_muse/plugins/aws_bedrock/config.py +99 -0
  147. code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
  148. code_muse/plugins/aws_bedrock/utils.py +153 -0
  149. code_muse/plugins/azure_foundry/README.md +238 -0
  150. code_muse/plugins/azure_foundry/__init__.py +15 -0
  151. code_muse/plugins/azure_foundry/config.py +125 -0
  152. code_muse/plugins/azure_foundry/discovery.py +187 -0
  153. code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
  154. code_muse/plugins/azure_foundry/token.py +180 -0
  155. code_muse/plugins/azure_foundry/utils.py +345 -0
  156. code_muse/plugins/build_filter/__init__.py +1 -0
  157. code_muse/plugins/build_filter/register_callbacks.py +201 -0
  158. code_muse/plugins/build_filter/strategies/__init__.py +1 -0
  159. code_muse/plugins/build_filter/strategies/build.py +397 -0
  160. code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
  161. code_muse/plugins/chatgpt_oauth/config.py +52 -0
  162. code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
  163. code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
  164. code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
  165. code_muse/plugins/chatgpt_oauth/utils.py +538 -0
  166. code_muse/plugins/checkpointing/__init__.py +29 -0
  167. code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
  168. code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
  169. code_muse/plugins/checkpointing/register_callbacks.py +51 -0
  170. code_muse/plugins/checkpointing/restore_command.py +263 -0
  171. code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
  172. code_muse/plugins/checkpointing/shadow_git.py +90 -0
  173. code_muse/plugins/claude_code_hooks/__init__.py +1 -0
  174. code_muse/plugins/claude_code_hooks/config.py +188 -0
  175. code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
  176. code_muse/plugins/claude_code_oauth/README.md +167 -0
  177. code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
  178. code_muse/plugins/claude_code_oauth/__init__.py +25 -0
  179. code_muse/plugins/claude_code_oauth/config.py +52 -0
  180. code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
  181. code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
  182. code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
  183. code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
  184. code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
  185. code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
  186. code_muse/plugins/claude_code_oauth/utils.py +664 -0
  187. code_muse/plugins/copilot_auth/__init__.py +11 -0
  188. code_muse/plugins/copilot_auth/config.py +91 -0
  189. code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
  190. code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
  191. code_muse/plugins/copilot_auth/utils.py +584 -0
  192. code_muse/plugins/custom_commands/__init__.py +14 -0
  193. code_muse/plugins/custom_commands/args_injection.py +82 -0
  194. code_muse/plugins/custom_commands/command_discovery.py +89 -0
  195. code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
  196. code_muse/plugins/custom_commands/register_callbacks.py +176 -0
  197. code_muse/plugins/customizable_commands/__init__.py +0 -0
  198. code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
  199. code_muse/plugins/destructive_command_guard/__init__.py +14 -0
  200. code_muse/plugins/destructive_command_guard/detector.py +375 -0
  201. code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
  202. code_muse/plugins/example_custom_command/README.md +280 -0
  203. code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
  204. code_muse/plugins/file_permission_handler/__init__.py +4 -0
  205. code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
  206. code_muse/plugins/filter_engine/__init__.py +30 -0
  207. code_muse/plugins/filter_engine/classifier.py +153 -0
  208. code_muse/plugins/filter_engine/content_detector.py +184 -0
  209. code_muse/plugins/filter_engine/dispatcher.py +244 -0
  210. code_muse/plugins/filter_engine/register_callbacks.py +188 -0
  211. code_muse/plugins/filter_engine/registry.py +279 -0
  212. code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
  213. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
  214. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
  215. code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
  216. code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
  217. code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
  218. code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
  219. code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
  220. code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
  221. code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
  222. code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
  223. code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
  224. code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
  225. code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
  226. code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
  227. code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
  228. code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
  229. code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
  230. code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
  231. code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
  232. code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
  233. code_muse/plugins/filter_engine/verbosity.py +63 -0
  234. code_muse/plugins/force_push_guard/__init__.py +5 -0
  235. code_muse/plugins/force_push_guard/detector.py +96 -0
  236. code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
  237. code_muse/plugins/force_push_guard/test_detector.py +143 -0
  238. code_muse/plugins/frontend_emitter/__init__.py +25 -0
  239. code_muse/plugins/frontend_emitter/emitter.py +121 -0
  240. code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
  241. code_muse/plugins/gac/__init__.py +4 -0
  242. code_muse/plugins/gac/git_ops.py +136 -0
  243. code_muse/plugins/gac/prompt.py +191 -0
  244. code_muse/plugins/gac/register_callbacks.py +82 -0
  245. code_muse/plugins/hook_creator/__init__.py +1 -0
  246. code_muse/plugins/hook_creator/register_callbacks.py +34 -0
  247. code_muse/plugins/hook_manager/__init__.py +1 -0
  248. code_muse/plugins/hook_manager/config.py +289 -0
  249. code_muse/plugins/hook_manager/hooks_menu.py +563 -0
  250. code_muse/plugins/hook_manager/register_callbacks.py +227 -0
  251. code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
  252. code_muse/plugins/mindpack/__init__.py +0 -0
  253. code_muse/plugins/mindpack/factory.py +930 -0
  254. code_muse/plugins/mindpack/judge.py +573 -0
  255. code_muse/plugins/mindpack/memory.py +100 -0
  256. code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
  257. code_muse/plugins/mindpack/orchestration.py +605 -0
  258. code_muse/plugins/mindpack/register_callbacks.py +175 -0
  259. code_muse/plugins/mindpack/schemas.py +358 -0
  260. code_muse/plugins/mindpack/tools.py +387 -0
  261. code_muse/plugins/oauth_muse_html.py +226 -0
  262. code_muse/plugins/ollama_setup/__init__.py +5 -0
  263. code_muse/plugins/ollama_setup/completer.py +36 -0
  264. code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
  265. code_muse/plugins/plan_command/__init__.py +0 -0
  266. code_muse/plugins/plan_command/register_callbacks.py +206 -0
  267. code_muse/plugins/plan_mode/__init__.py +37 -0
  268. code_muse/plugins/plan_mode/mode_cycling.py +40 -0
  269. code_muse/plugins/plan_mode/plan_generation.py +68 -0
  270. code_muse/plugins/plan_mode/plan_hooks.py +74 -0
  271. code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
  272. code_muse/plugins/plan_mode/register_callbacks.py +121 -0
  273. code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
  274. code_muse/plugins/policy_engine/__init__.py +46 -0
  275. code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
  276. code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
  277. code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
  278. code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
  279. code_muse/plugins/policy_engine/register_callbacks.py +112 -0
  280. code_muse/plugins/pop_command/__init__.py +1 -0
  281. code_muse/plugins/pop_command/register_callbacks.py +189 -0
  282. code_muse/plugins/prompt_newline/__init__.py +13 -0
  283. code_muse/plugins/prompt_newline/config.py +19 -0
  284. code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
  285. code_muse/plugins/safety_status/__init__.py +0 -0
  286. code_muse/plugins/safety_status/register_callbacks.py +113 -0
  287. code_muse/plugins/semantic_compression/__init__.py +6 -0
  288. code_muse/plugins/semantic_compression/compressor.py +295 -0
  289. code_muse/plugins/semantic_compression/config.py +123 -0
  290. code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
  291. code_muse/plugins/shell_minimizer/__init__.py +50 -0
  292. code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
  293. code_muse/plugins/shell_minimizer/pipeline.py +556 -0
  294. code_muse/plugins/shell_minimizer/primitives.py +482 -0
  295. code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
  296. code_muse/plugins/shell_safety/__init__.py +6 -0
  297. code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
  298. code_muse/plugins/shell_safety/command_cache.py +149 -0
  299. code_muse/plugins/shell_safety/register_callbacks.py +202 -0
  300. code_muse/plugins/synthetic_status/__init__.py +1 -0
  301. code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
  302. code_muse/plugins/synthetic_status/status_api.py +145 -0
  303. code_muse/plugins/token_caching/__init__.py +21 -0
  304. code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
  305. code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
  306. code_muse/plugins/token_caching/register_callbacks.py +54 -0
  307. code_muse/plugins/token_caching/stats_display.py +35 -0
  308. code_muse/plugins/token_tracking/__init__.py +26 -0
  309. code_muse/plugins/token_tracking/database.py +381 -0
  310. code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
  311. code_muse/plugins/token_tracking/record.py +55 -0
  312. code_muse/plugins/token_tracking/register_callbacks.py +277 -0
  313. code_muse/plugins/token_tracking/reports.py +329 -0
  314. code_muse/plugins/universal_constructor/__init__.py +13 -0
  315. code_muse/plugins/universal_constructor/models.py +136 -0
  316. code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
  317. code_muse/plugins/universal_constructor/registry.py +390 -0
  318. code_muse/plugins/universal_constructor/runner.py +474 -0
  319. code_muse/plugins/universal_constructor/safety.py +440 -0
  320. code_muse/plugins/universal_constructor/sandbox.py +584 -0
  321. code_muse/provider_identity.py +105 -0
  322. code_muse/pydantic_patches.py +410 -0
  323. code_muse/reopenable_async_client.py +233 -0
  324. code_muse/round_robin_model.py +151 -0
  325. code_muse/secret_storage.py +74 -0
  326. code_muse/security/__init__.py +1 -0
  327. code_muse/security/redaction.cpython-314-darwin.so +0 -0
  328. code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
  329. code_muse/security/redaction.pyx +135 -0
  330. code_muse/session_storage.py +565 -0
  331. code_muse/status_display.py +261 -0
  332. code_muse/stream_parser/__init__.py +76 -0
  333. code_muse/stream_parser/assistant_text_parser.py +90 -0
  334. code_muse/stream_parser/citation_parser.py +76 -0
  335. code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
  336. code_muse/stream_parser/proposed_plan_parser.py +158 -0
  337. code_muse/stream_parser/stream_text_chunk.py +23 -0
  338. code_muse/stream_parser/stream_text_parser.py +27 -0
  339. code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
  340. code_muse/stream_parser/tagged_line_parser.pyx +251 -0
  341. code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
  342. code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
  343. code_muse/summarization_agent.py +308 -0
  344. code_muse/terminal_utils.cpython-314-darwin.so +0 -0
  345. code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
  346. code_muse/terminal_utils.pyx +483 -0
  347. code_muse/tools/__init__.py +459 -0
  348. code_muse/tools/agent_tools.py +613 -0
  349. code_muse/tools/ask_user_question/__init__.py +26 -0
  350. code_muse/tools/ask_user_question/constants.py +73 -0
  351. code_muse/tools/ask_user_question/demo_tui.py +55 -0
  352. code_muse/tools/ask_user_question/handler.py +232 -0
  353. code_muse/tools/ask_user_question/models.py +302 -0
  354. code_muse/tools/ask_user_question/registration.py +37 -0
  355. code_muse/tools/ask_user_question/renderers.py +336 -0
  356. code_muse/tools/ask_user_question/terminal_ui.py +327 -0
  357. code_muse/tools/ask_user_question/theme.py +156 -0
  358. code_muse/tools/ask_user_question/tui_loop.py +422 -0
  359. code_muse/tools/background_jobs.py +99 -0
  360. code_muse/tools/browser/__init__.py +37 -0
  361. code_muse/tools/browser/browser_control.py +289 -0
  362. code_muse/tools/browser/browser_interactions.py +545 -0
  363. code_muse/tools/browser/browser_locators.py +640 -0
  364. code_muse/tools/browser/browser_manager.py +376 -0
  365. code_muse/tools/browser/browser_navigation.py +251 -0
  366. code_muse/tools/browser/browser_screenshot.py +180 -0
  367. code_muse/tools/browser/browser_scripts.py +462 -0
  368. code_muse/tools/browser/browser_workflows.py +222 -0
  369. code_muse/tools/chrome_cdp/__init__.py +1070 -0
  370. code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
  371. code_muse/tools/command_runner.py +1401 -0
  372. code_muse/tools/common.py +1407 -0
  373. code_muse/tools/display.py +87 -0
  374. code_muse/tools/file_modifications.py +1099 -0
  375. code_muse/tools/file_operations.py +860 -0
  376. code_muse/tools/image_tools.py +185 -0
  377. code_muse/tools/meetin_proxy/__init__.py +243 -0
  378. code_muse/tools/meetin_proxy/capture_addon.py +82 -0
  379. code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
  380. code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
  381. code_muse/tools/path_policy.py +219 -0
  382. code_muse/tools/skills_tools.py +586 -0
  383. code_muse/tools/subagent_context.py +158 -0
  384. code_muse/tools/tools_content.py +50 -0
  385. code_muse/tools/universal_constructor.py +965 -0
  386. code_muse/uvx_detection.py +241 -0
  387. code_muse/version_checker.py +86 -0
  388. code_muse-0.0.1.data/data/code_muse/models.json +66 -0
  389. code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
  390. code_muse-0.0.1.dist-info/METADATA +845 -0
  391. code_muse-0.0.1.dist-info/RECORD +394 -0
  392. code_muse-0.0.1.dist-info/WHEEL +4 -0
  393. code_muse-0.0.1.dist-info/entry_points.txt +2 -0
  394. code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,930 @@
1
+ """Expert agent factory — creates read-only sub-agents from ExpertDescriptors.
2
+
3
+ Follows the same sub-agent construction pattern as
4
+ ``code_muse.tools.agent_tools.register_invoke_agent`` but with two key
5
+ constraints:
6
+
7
+ 1. **Read-only tool set** — experts can inspect code but never mutate it.
8
+ 2. **Structured output** — experts return ``ExpertReport`` models, not free
9
+ text, so the judge merger receives machine-parseable data.
10
+
11
+ The factory is deliberately stateless: it builds an agent per call and
12
+ tears it down afterwards. No session history is persisted to disk because
13
+ expert consultations are ephemeral — they exist only for the duration of a
14
+ single ``MindPackOrchestrator.consult()`` call.
15
+ """
16
+
17
+ import asyncio
18
+ import logging
19
+ import uuid
20
+ from contextlib import AsyncExitStack
21
+ from functools import partial
22
+ from typing import Any, get_args
23
+
24
+ from pydantic_ai import Agent as PydanticAgent
25
+ from pydantic_ai import UsageLimits
26
+
27
+ from code_muse.plugins.mindpack.schemas import (
28
+ AskMindPackInput,
29
+ ExpertDescriptor,
30
+ ExpertSpawnMode,
31
+ MindPackExpertPoolConfig,
32
+ )
33
+ from code_muse.plugins.mindpack.schemas import MindPackExpertReport as ExpertReport
34
+ from code_muse.tools.subagent_context import subagent_context
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # INI config helpers
40
+ # ---------------------------------------------------------------------------
41
+
42
+
43
+ def _get_config_str(key: str, default: str) -> str:
44
+ from code_muse.config import get_value
45
+
46
+ return get_value(key) or default
47
+
48
+
49
+ def _get_config_int(key: str, default: int) -> int:
50
+ from code_muse.config import get_value
51
+
52
+ val = get_value(key)
53
+ if val is None:
54
+ return default
55
+ try:
56
+ return int(val)
57
+ except ValueError, TypeError:
58
+ logger.warning(
59
+ "Invalid integer config '%s=%s'; using default %s", key, val, default
60
+ )
61
+ return default
62
+
63
+
64
+ def _get_config_bool(key: str, default: bool) -> bool:
65
+ from code_muse.config import get_value
66
+
67
+ val = get_value(key)
68
+ if val is None:
69
+ return default
70
+ return str(val).lower() in ("1", "true", "yes", "on")
71
+
72
+
73
+ def load_pool_config_from_ini(
74
+ overrides: MindPackExpertPoolConfig | None = None,
75
+ ) -> MindPackExpertPoolConfig:
76
+ """Build a ``MindPackExpertPoolConfig`` from INI settings.
77
+
78
+ Reads ``packmind_expert_spawn_mode``, ``packmind_expert_count``,
79
+ ``packmind_min_experts``, ``packmind_max_experts``, and
80
+ ``packmind_model_strategy`` from the Muse INI config.
81
+
82
+ Any caller-supplied ``overrides`` take precedence over INI values.
83
+ """
84
+ spawn_mode_raw = _get_config_str("packmind_expert_spawn_mode", "fixed")
85
+ valid_modes = set(get_args(ExpertSpawnMode))
86
+ if spawn_mode_raw not in valid_modes:
87
+ logger.warning(
88
+ "Invalid packmind_expert_spawn_mode '%s'; falling back to 'fixed'",
89
+ spawn_mode_raw,
90
+ )
91
+ spawn_mode_raw = "fixed"
92
+
93
+ config = MindPackExpertPoolConfig(
94
+ spawn_mode=spawn_mode_raw, # type: ignore[arg-type]
95
+ default_expert_count=_get_config_int("packmind_expert_count", 5),
96
+ min_experts=_get_config_int("packmind_min_experts", 3),
97
+ max_experts=_get_config_int("packmind_max_experts", 7),
98
+ model_strategy=_get_config_str("packmind_model_strategy", "same_model"), # type: ignore[arg-type]
99
+ )
100
+
101
+ if overrides is not None:
102
+ # Apply caller overrides field-by-field
103
+ for field_name in MindPackExpertPoolConfig.model_fields:
104
+ override_val = getattr(overrides, field_name, None)
105
+ if override_val is not None:
106
+ setattr(config, field_name, override_val)
107
+
108
+ return config
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Read-only tool allow-list
113
+ # ---------------------------------------------------------------------------
114
+
115
+ READ_ONLY_TOOLS: list[str] = [
116
+ "list_files",
117
+ "read_file",
118
+ "grep",
119
+ "load_image_for_analysis",
120
+ "list_or_search_skills",
121
+ ]
122
+ """Tools that an expert agent may use. All write-capable tools (create_file,
123
+ replace_in_file, delete_snippet, delete_file, agent_run_shell_command,
124
+ ask_user_question, invoke_agent, list_agents, browser_*, activate_skill,
125
+ universal_constructor) are deliberately excluded."""
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Expert system prompt template
129
+ # ---------------------------------------------------------------------------
130
+
131
+ _EXPERT_SYSTEM_PROMPT_TEMPLATE = """\
132
+ You are {expert_name}, a specialist in {speciality}.
133
+
134
+ {system_prompt_fragment}
135
+
136
+ ## CRITICAL CONSTRAINTS
137
+
138
+ You are operating in **read-only advisory mode**. You must NEVER:
139
+
140
+ - Create, modify, or delete any files
141
+ - Run shell commands
142
+ - Invoke other agents or sub-agents
143
+ - Ask the user questions
144
+ - Navigate websites or use browser tools
145
+
146
+ You MAY:
147
+ - List and read files
148
+ - Search code with grep
149
+ - Load images for analysis
150
+
151
+ Your task is to analyse the problem, explore the relevant code, and produce
152
+ a structured report with your findings, recommendations, identified risks,
153
+ and confidence level.
154
+
155
+ ## OUTPUT FORMAT
156
+
157
+ You MUST produce your response as a structured report containing:
158
+ - **summary**: Your detailed analysis of the problem
159
+ - **findings**: Key findings and observations
160
+ - **proposed_plan**: A list of specific, actionable recommendations
161
+ - **risks**: A list of risks, edge cases, or failure modes you identified
162
+ - **files_to_inspect**: Files you recommend the executor inspect or change
163
+ - **tests_to_run**: Tests you recommend running
164
+ - **confidence**: Your confidence in your analysis (0.0 to 1.0)
165
+ - **assumptions**: Any assumptions you made
166
+ """
167
+
168
+ # ---------------------------------------------------------------------------
169
+ # Prompt builder
170
+ # ---------------------------------------------------------------------------
171
+
172
+
173
+ def build_expert_prompt(
174
+ expert: ExpertDescriptor,
175
+ request: AskMindPackInput,
176
+ ) -> str:
177
+ """Compose the user-facing prompt for an expert consultation.
178
+
179
+ The prompt carries the full problem context so the expert can work
180
+ autonomously with only read-only tools.
181
+ """
182
+ parts: list[str] = []
183
+
184
+ parts.append(f"## Problem Statement\n{request.problem_statement}")
185
+
186
+ parts.append(f"## Current Goal\n{request.current_goal}")
187
+
188
+ if request.current_plan:
189
+ parts.append(f"## Current Plan\n{request.current_plan}")
190
+
191
+ if request.what_has_been_tried:
192
+ parts.append(
193
+ "## What Has Been Tried\n"
194
+ + "\n".join(f"- {item}" for item in request.what_has_been_tried)
195
+ )
196
+
197
+ if request.relevant_files:
198
+ parts.append(
199
+ "## Relevant Files\n" + "\n".join(f"- {f}" for f in request.relevant_files)
200
+ )
201
+
202
+ if request.observed_errors:
203
+ parts.append(
204
+ "## Observed Errors\n"
205
+ + "\n".join(f"- {e}" for e in request.observed_errors)
206
+ )
207
+
208
+ if request.uncertainty:
209
+ parts.append(f"## Uncertainty\n{request.uncertainty}")
210
+
211
+ parts.append(f"## Desired Output Type\n{request.desired_output}")
212
+
213
+ return "\n\n".join(parts)
214
+
215
+
216
+ # ---------------------------------------------------------------------------
217
+ # ExpertAgentFactory
218
+ # ---------------------------------------------------------------------------
219
+
220
+
221
+ class ExpertAgentFactory:
222
+ """Creates and runs read-only expert sub-agents.
223
+
224
+ Usage::
225
+
226
+ factory = ExpertAgentFactory()
227
+ report = await factory.invoke_expert(descriptor, request, session_id)
228
+ """
229
+
230
+ def __init__(self) -> None:
231
+ self._concurrency_limit = _get_config_int("packmind_concurrency_limit", 8)
232
+ self._semaphore = asyncio.Semaphore(self._concurrency_limit)
233
+ logger.debug(
234
+ "ExpertAgentFactory: concurrency limit set to %d",
235
+ self._concurrency_limit,
236
+ )
237
+
238
+ def create_expert_agent(
239
+ self,
240
+ expert: ExpertDescriptor,
241
+ *,
242
+ session_id: str,
243
+ message_group: str | None = None,
244
+ model_override: str | None = None,
245
+ ) -> PydanticAgent:
246
+ """Build a pydantic-ai agent for the given expert descriptor.
247
+
248
+ The agent is configured with:
249
+ - The expert's system prompt (identity + fragment + constraints)
250
+ - Read-only tools only
251
+ - ``ExpertReport`` as the structured output type
252
+
253
+ This is a **temporary agent** — it is not registered in the agent
254
+ manager and does not persist across calls.
255
+ """
256
+ from code_muse.agents._builder import load_muse_rules
257
+ from code_muse.model_factory import ModelFactory, make_model_settings
258
+ from code_muse.model_utils import prepare_prompt_for_model
259
+
260
+ # Resolve model
261
+ model_name = model_override or self._resolve_model_name(expert)
262
+ models_config = ModelFactory.load_config()
263
+
264
+ if model_name not in models_config:
265
+ raise ValueError(
266
+ f"Model '{model_name}' not found in configuration — "
267
+ "cannot create expert agent"
268
+ )
269
+
270
+ model = ModelFactory.get_model(model_name, models_config)
271
+
272
+ # Build instructions
273
+ instructions = _EXPERT_SYSTEM_PROMPT_TEMPLATE.format(
274
+ expert_name=expert.name,
275
+ speciality=expert.speciality,
276
+ system_prompt_fragment=expert.system_prompt_fragment,
277
+ )
278
+
279
+ # Append AGENTS.md rules if available
280
+ agent_rules = load_muse_rules()
281
+ if agent_rules:
282
+ instructions += f"\n\n{agent_rules}"
283
+
284
+ # Append plugin prompt additions
285
+ from code_muse import callbacks
286
+
287
+ prompt_additions = callbacks.on_load_prompt()
288
+ if prompt_additions:
289
+ instructions += "\n" + "\n".join(prompt_additions)
290
+
291
+ # Prepare for model (handles claude-code prepending etc.)
292
+ prepared = prepare_prompt_for_model(
293
+ model_name,
294
+ instructions,
295
+ "", # user prompt is empty at build time
296
+ prepend_system_to_user=False,
297
+ )
298
+ instructions = prepared.instructions
299
+
300
+ model_settings = make_model_settings(model_name)
301
+
302
+ # Build the pydantic-ai agent with ExpertReport output type
303
+ temp_agent = PydanticAgent(
304
+ model=model,
305
+ instructions=instructions,
306
+ output_type=ExpertReport,
307
+ retries=2,
308
+ # Explicitly restrict tools via registration later,
309
+ # but ensure this agent has NO access to agent/browser control.
310
+ toolsets=[],
311
+ history_processors=[],
312
+ model_settings=model_settings,
313
+ )
314
+
315
+ # Register read-only tools
316
+ from code_muse.tools import register_tools_for_agent
317
+
318
+ tools = list(READ_ONLY_TOOLS)
319
+ register_tools_for_agent(temp_agent, tools, model_name=model_name)
320
+
321
+ logger.debug(
322
+ "ExpertAgentFactory: created agent for '%s' with tools=%s session=%s",
323
+ expert.name,
324
+ tools,
325
+ session_id,
326
+ )
327
+ return temp_agent
328
+
329
+ async def build_expert_pool(
330
+ self,
331
+ experts: list[ExpertDescriptor],
332
+ request: AskMindPackInput,
333
+ pool_config: MindPackExpertPoolConfig,
334
+ session_id: str,
335
+ ) -> list[tuple[ExpertDescriptor, PydanticAgent]]:
336
+ """Spawns an asynchronous expert pool based on the configured mode."""
337
+ spawn_mode = pool_config.spawn_mode
338
+
339
+ if spawn_mode == "fixed":
340
+ return await self._build_pool_fixed(
341
+ experts,
342
+ request,
343
+ pool_config,
344
+ session_id,
345
+ count=pool_config.default_expert_count,
346
+ )
347
+
348
+ if spawn_mode == "adaptive":
349
+ return await self._build_pool_fixed(
350
+ experts,
351
+ request,
352
+ pool_config,
353
+ session_id,
354
+ count=pool_config.min_experts,
355
+ )
356
+
357
+ if spawn_mode == "same_agent_replicas":
358
+ return await self._build_pool_same_agent_replicas(
359
+ experts,
360
+ request,
361
+ pool_config,
362
+ session_id,
363
+ )
364
+
365
+ if spawn_mode == "multi_model_replicas":
366
+ return await self._build_pool_multi_model_replicas(
367
+ experts,
368
+ request,
369
+ pool_config,
370
+ session_id,
371
+ )
372
+
373
+ if spawn_mode == "hybrid":
374
+ return await self._build_pool_hybrid(
375
+ experts,
376
+ request,
377
+ pool_config,
378
+ session_id,
379
+ )
380
+
381
+ if spawn_mode == "multi_agent":
382
+ return await self._build_pool_multi_agent(
383
+ experts,
384
+ request,
385
+ pool_config,
386
+ session_id,
387
+ )
388
+
389
+ logger.warning(
390
+ "Unknown spawn mode '%s'; falling back to fixed pool",
391
+ spawn_mode,
392
+ )
393
+ return await self._build_pool_fixed(
394
+ experts,
395
+ request,
396
+ pool_config,
397
+ session_id,
398
+ count=pool_config.default_expert_count,
399
+ )
400
+
401
+ async def _build_pool_fixed(
402
+ self,
403
+ experts: list[ExpertDescriptor],
404
+ request: AskMindPackInput,
405
+ pool_config: MindPackExpertPoolConfig,
406
+ session_id: str,
407
+ count: int | None = None,
408
+ ) -> list[tuple[ExpertDescriptor, PydanticAgent]]:
409
+ """Fixed-mode pool builder: spawn up to *count* agents from *experts*."""
410
+ target_count = count if count is not None else pool_config.default_expert_count
411
+ selected = experts[:target_count]
412
+ if len(selected) < target_count:
413
+ logger.warning(
414
+ "Requested %d experts but registry only has %d; using all available",
415
+ target_count,
416
+ len(selected),
417
+ )
418
+
419
+ pool: list[tuple[ExpertDescriptor, PydanticAgent]] = []
420
+ global_model = self._resolve_model_name(selected[0]) if selected else None
421
+
422
+ for expert in selected:
423
+ model_to_use = global_model
424
+ if pool_config.model_strategy == "per_expert" and expert.model:
425
+ model_to_use = expert.model
426
+ elif pool_config.model_strategy == "model_pool":
427
+ # Round-robin model rotation across experts
428
+ if not hasattr(self, "_model_pool_index"):
429
+ self._model_pool_index = 0
430
+ from code_muse.model_factory import ModelFactory
431
+
432
+ models_config = ModelFactory.load_config()
433
+ model_names = list(models_config.keys())
434
+ if model_names:
435
+ model_to_use = model_names[
436
+ self._model_pool_index % len(model_names)
437
+ ]
438
+ self._model_pool_index += 1
439
+ else:
440
+ model_to_use = global_model
441
+
442
+ agent = self.create_expert_agent(
443
+ expert,
444
+ session_id=session_id,
445
+ model_override=model_to_use,
446
+ )
447
+ pool.append((expert, agent))
448
+
449
+ return pool
450
+
451
+ async def _build_pool_same_agent_replicas(
452
+ self,
453
+ experts: list[ExpertDescriptor],
454
+ request: AskMindPackInput,
455
+ pool_config: MindPackExpertPoolConfig,
456
+ session_id: str,
457
+ ) -> list[tuple[ExpertDescriptor, PydanticAgent]]:
458
+ """Spawn N copies of the first expert with different role lenses.
459
+
460
+ Each replica gets a distinct lens (scout, architect, watchdog,
461
+ test_planner, challenger) injected into its system prompt, providing
462
+ diverse perspectives from a single base expert configuration.
463
+ """
464
+ if not experts:
465
+ return []
466
+
467
+ base = experts[0]
468
+ count = min(pool_config.default_expert_count, pool_config.max_experts)
469
+ lenses = ["scout", "architect", "watchdog", "test_planner", "challenger"]
470
+
471
+ pool: list[tuple[ExpertDescriptor, PydanticAgent]] = []
472
+ for i in range(count):
473
+ lens = lenses[i % len(lenses)]
474
+ variant = ExpertDescriptor(
475
+ name=f"{base.name}-{lens}",
476
+ speciality=f"{base.speciality} [{lens} lens]",
477
+ system_prompt_fragment=(
478
+ f"LENS: {lens} perspective\n\n{base.system_prompt_fragment}"
479
+ ),
480
+ model=base.model,
481
+ max_experts_override=base.max_experts_override,
482
+ )
483
+ model_name = self._resolve_model_name(base)
484
+ agent = self.create_expert_agent(
485
+ variant,
486
+ session_id=session_id,
487
+ model_override=model_name,
488
+ )
489
+ pool.append((variant, agent))
490
+
491
+ logger.info(
492
+ "same_agent_replicas: spawned %d replicas of '%s'",
493
+ count,
494
+ base.name,
495
+ )
496
+ return pool
497
+
498
+ async def _build_pool_multi_model_replicas(
499
+ self,
500
+ experts: list[ExpertDescriptor],
501
+ request: AskMindPackInput,
502
+ pool_config: MindPackExpertPoolConfig,
503
+ session_id: str,
504
+ ) -> list[tuple[ExpertDescriptor, PydanticAgent]]:
505
+ """Spawn experts across multiple models from the available model pool.
506
+
507
+ Rotates through available models, assigning each expert a different
508
+ model to get diverse LLM perspectives on the same problem.
509
+ Falls back to fixed pool if no models are available.
510
+ """
511
+ if not experts:
512
+ return []
513
+
514
+ from code_muse.model_factory import ModelFactory
515
+
516
+ models_config = ModelFactory.load_config()
517
+ model_names = list(models_config.keys())
518
+
519
+ if not model_names:
520
+ logger.warning(
521
+ "multi_model_replicas: no models available; falling back to fixed"
522
+ )
523
+ return await self._build_pool_fixed(
524
+ experts,
525
+ request,
526
+ pool_config,
527
+ session_id,
528
+ count=pool_config.default_expert_count,
529
+ )
530
+
531
+ count = min(
532
+ pool_config.default_expert_count,
533
+ len(model_names),
534
+ pool_config.max_experts,
535
+ )
536
+
537
+ pool: list[tuple[ExpertDescriptor, PydanticAgent]] = []
538
+ for i in range(count):
539
+ expert = experts[i % len(experts)]
540
+ model_name = model_names[i % len(model_names)]
541
+
542
+ variant = ExpertDescriptor(
543
+ name=f"{expert.name}",
544
+ speciality=expert.speciality,
545
+ system_prompt_fragment=expert.system_prompt_fragment,
546
+ model=model_name,
547
+ max_experts_override=expert.max_experts_override,
548
+ )
549
+ agent = self.create_expert_agent(
550
+ variant,
551
+ session_id=session_id,
552
+ model_override=model_name,
553
+ )
554
+ pool.append((variant, agent))
555
+
556
+ logger.info(
557
+ "multi_model_replicas: spawned %d experts across models: %s",
558
+ count,
559
+ model_names[:count],
560
+ )
561
+ return pool
562
+
563
+ async def _build_pool_hybrid(
564
+ self,
565
+ experts: list[ExpertDescriptor],
566
+ request: AskMindPackInput,
567
+ pool_config: MindPackExpertPoolConfig,
568
+ session_id: str,
569
+ ) -> list[tuple[ExpertDescriptor, PydanticAgent]]:
570
+ """Hybrid mode: fixed base + adaptive extras.
571
+
572
+ Starts with a fixed base of min_experts, then adds adaptive bonus
573
+ experts scaled by problem complexity (statement length heuristic).
574
+ Caps at max_experts.
575
+ """
576
+ if not experts:
577
+ return []
578
+
579
+ fixed_base = min(pool_config.min_experts, len(experts))
580
+ # Heuristic: +1 extra expert per 500 chars of problem statement, max +2
581
+ problem_length = len(request.problem_statement)
582
+ adaptive_bonus = min(2, problem_length // 500)
583
+ total = min(fixed_base + adaptive_bonus, pool_config.max_experts, len(experts))
584
+
585
+ logger.info(
586
+ "hybrid: fixed_base=%d + adaptive_bonus=%d → total=%d experts",
587
+ fixed_base,
588
+ adaptive_bonus,
589
+ total,
590
+ )
591
+ return await self._build_pool_fixed(
592
+ experts,
593
+ request,
594
+ pool_config,
595
+ session_id,
596
+ count=total,
597
+ )
598
+
599
+ async def _build_pool_multi_agent(
600
+ self,
601
+ experts: list[ExpertDescriptor],
602
+ request: AskMindPackInput,
603
+ pool_config: MindPackExpertPoolConfig,
604
+ session_id: str,
605
+ ) -> list[tuple[ExpertDescriptor, PydanticAgent]]:
606
+ """Multi-agent mode: attempts to load named agent configs per expert.
607
+
608
+ For each expert, tries to load a matching agent from the agent
609
+ manager. If the expert's name matches a registered agent, that
610
+ agent's config/model are used. Otherwise falls back to the
611
+ standard expert agent builder.
612
+
613
+ This enables mixing MindPack experts with full Muse agents.
614
+ """
615
+ if not experts:
616
+ return []
617
+
618
+ from code_muse.agents.agent_manager import load_agent
619
+
620
+ count = min(
621
+ pool_config.default_expert_count, len(experts), pool_config.max_experts
622
+ )
623
+ selected = experts[:count]
624
+
625
+ pool: list[tuple[ExpertDescriptor, PydanticAgent]] = []
626
+ for expert in selected:
627
+ # Try to load a matching named agent
628
+ agent_config = None
629
+ try:
630
+ agent_config = load_agent(expert.name)
631
+ logger.debug(
632
+ "multi_agent: loaded agent '%s' for expert '%s'",
633
+ expert.name,
634
+ expert.name,
635
+ )
636
+ except ValueError:
637
+ logger.debug(
638
+ "multi_agent: no agent named '%s'; using standard expert agent",
639
+ expert.name,
640
+ )
641
+
642
+ # Use the agent's model if available, otherwise fall back
643
+ model_name = expert.model or self._resolve_model_name(expert)
644
+ if agent_config is not None:
645
+ model_name = getattr(agent_config, "model", None) or model_name
646
+
647
+ agent = self.create_expert_agent(
648
+ expert,
649
+ session_id=session_id,
650
+ model_override=model_name,
651
+ )
652
+ pool.append((expert, agent))
653
+
654
+ logger.info(
655
+ "multi_agent: spawned %d experts (agent-mapped where available)",
656
+ len(pool),
657
+ )
658
+ return pool
659
+
660
+ async def invoke_expert(
661
+ self,
662
+ expert: ExpertDescriptor,
663
+ request: AskMindPackInput,
664
+ session_id: str,
665
+ ) -> ExpertReport | None:
666
+ """Run an expert agent and return its structured report.
667
+
668
+ Returns ``None`` if the agent fails to produce a valid report.
669
+ """
670
+ group_id = f"mindpack-{expert.name}-{uuid.uuid4().hex[:6]}"
671
+
672
+ try:
673
+ temp_agent = self.create_expert_agent(
674
+ expert, session_id=session_id, message_group=group_id
675
+ )
676
+ except ValueError as exc:
677
+ logger.error(
678
+ "ExpertAgentFactory: failed to create agent for '%s': %s",
679
+ expert.name,
680
+ exc,
681
+ )
682
+ return None
683
+
684
+ user_prompt = build_expert_prompt(expert, request)
685
+
686
+ return await self._run_expert(
687
+ temp_agent=temp_agent,
688
+ expert=expert,
689
+ user_prompt=user_prompt,
690
+ session_id=session_id,
691
+ group_id=group_id,
692
+ )
693
+
694
+ # -- internal -----------------------------------------------------------
695
+
696
+ @staticmethod
697
+ def _resolve_model_name(expert: ExpertDescriptor) -> str:
698
+ """Resolve the model name to use for expert agents.
699
+
700
+ If the expert descriptor specifies a per-expert model override,
701
+ use that. Otherwise fall back to the global model name.
702
+ """
703
+ if expert.model:
704
+ return expert.model
705
+
706
+ from code_muse.config import get_global_model_name
707
+
708
+ name = get_global_model_name()
709
+ if not name:
710
+ raise ValueError("No global model configured — cannot create expert agent")
711
+ return name
712
+
713
+ async def _run_expert(
714
+ self,
715
+ temp_agent: PydanticAgent,
716
+ expert: ExpertDescriptor,
717
+ user_prompt: str,
718
+ session_id: str,
719
+ group_id: str,
720
+ ) -> ExpertReport | None:
721
+ """Execute the expert agent in a subagent context.
722
+
723
+ Handles streaming, cancellation, and error recovery. Falls back
724
+ to a text-parsed report if structured output fails.
725
+ """
726
+ from code_muse.agents.subagent_stream_handler import (
727
+ subagent_stream_handler,
728
+ )
729
+ from code_muse.callbacks import on_agent_run_cancel, on_agent_run_context
730
+ from code_muse.config import get_message_limit
731
+ from code_muse.messaging import (
732
+ SubAgentInvocationMessage,
733
+ SubAgentResponseMessage,
734
+ emit_success,
735
+ get_message_bus,
736
+ )
737
+
738
+ bus = get_message_bus()
739
+
740
+ # Emit invocation message for the console
741
+ bus.emit(
742
+ SubAgentInvocationMessage(
743
+ agent_name=f"mindpack-{expert.name}",
744
+ session_id=session_id,
745
+ prompt=user_prompt[:200],
746
+ is_new_session=True,
747
+ message_count=0,
748
+ )
749
+ )
750
+
751
+ stream_handler = partial(subagent_stream_handler, session_id=session_id)
752
+
753
+ async with self._semaphore:
754
+ with subagent_context(f"mindpack-{expert.name}"):
755
+ run_ctxs = on_agent_run_context(
756
+ # Provide a minimal agent-like object for the hook
757
+ _MinimalAgentProxy(expert.name),
758
+ temp_agent,
759
+ group_id,
760
+ )
761
+
762
+ task = None
763
+ try:
764
+ async with AsyncExitStack() as stack:
765
+ for cm in run_ctxs:
766
+ await stack.enter_async_context(cm)
767
+
768
+ task = asyncio.create_task(
769
+ temp_agent.run(
770
+ user_prompt,
771
+ message_history=[],
772
+ usage_limits=UsageLimits(
773
+ request_limit=get_message_limit()
774
+ ),
775
+ event_stream_handler=stream_handler,
776
+ )
777
+ )
778
+
779
+ result = await task
780
+
781
+ except asyncio.CancelledError:
782
+ if task and not task.done():
783
+ task.cancel()
784
+ await on_agent_run_cancel(group_id)
785
+ logger.warning(
786
+ "ExpertAgentFactory: expert '%s' cancelled", expert.name
787
+ )
788
+ return None
789
+
790
+ except Exception as exc:
791
+ logger.error(
792
+ "ExpertAgentFactory: expert '%s' failed: %s",
793
+ expert.name,
794
+ exc,
795
+ exc_info=True,
796
+ )
797
+ return self._fallback_report(expert, session_id, str(exc))
798
+
799
+ # Extract structured output
800
+ report = self._extract_report(result, expert, session_id)
801
+
802
+ # Emit completion message
803
+ bus.emit(
804
+ SubAgentResponseMessage(
805
+ agent_name=f"mindpack-{expert.name}",
806
+ session_id=session_id,
807
+ response=report.summary[:200] if report else "",
808
+ message_count=0,
809
+ )
810
+ )
811
+
812
+ emit_success(
813
+ f"✓ mindpack-{expert.name} completed",
814
+ message_group=group_id,
815
+ )
816
+
817
+ return report
818
+
819
+ @staticmethod
820
+ def _extract_report(
821
+ result: Any,
822
+ expert: ExpertDescriptor,
823
+ session_id: str,
824
+ ) -> ExpertReport | None:
825
+ """Extract an ExpertReport from the pydantic-ai result.
826
+
827
+ Tries structured output first (``result.output``), then falls
828
+ back to best-effort text parsing.
829
+ """
830
+ # Structured output path
831
+ if result is not None and hasattr(result, "output"):
832
+ output = result.output
833
+ if isinstance(output, ExpertReport):
834
+ # Ensure run_id is set correctly
835
+ if output.run_id != session_id:
836
+ output = output.model_copy(update={"run_id": session_id})
837
+ if output.expert_id != expert.name:
838
+ output = output.model_copy(update={"expert_id": expert.name})
839
+ return output
840
+
841
+ # If output is a dict, try to build ExpertReport from it
842
+ if isinstance(output, dict):
843
+ try:
844
+ report = ExpertReport(
845
+ expert_id=expert.name,
846
+ run_id=session_id,
847
+ **{
848
+ k: v
849
+ for k, v in output.items()
850
+ if k in ExpertReport.model_fields
851
+ },
852
+ )
853
+ return report
854
+ except Exception:
855
+ pass
856
+
857
+ # Text fallback — try to parse the raw output as text
858
+ raw_text = ""
859
+ if result is not None:
860
+ if hasattr(result, "output"):
861
+ raw_text = str(result.output)
862
+ elif hasattr(result, "data"):
863
+ raw_text = str(result.data)
864
+ else:
865
+ raw_text = str(result)
866
+
867
+ if raw_text:
868
+ return ExpertReport(
869
+ expert_id=expert.name,
870
+ run_id=session_id,
871
+ lens="unknown",
872
+ prompt_variant="fallback",
873
+ summary=raw_text,
874
+ findings=[],
875
+ proposed_plan=[],
876
+ risks=["Fallback: structured output not produced"],
877
+ files_to_inspect=[],
878
+ confidence=0.3,
879
+ status="partial",
880
+ )
881
+
882
+ return None
883
+
884
+ @staticmethod
885
+ def _fallback_report(
886
+ expert: ExpertDescriptor,
887
+ session_id: str,
888
+ error_msg: str,
889
+ ) -> ExpertReport:
890
+ """Build a minimal error report when the expert fails entirely."""
891
+ return ExpertReport(
892
+ expert_id=expert.name,
893
+ run_id=session_id,
894
+ lens="error",
895
+ prompt_variant="fallback",
896
+ summary=f"[Error] Expert '{expert.name}' failed: {error_msg}",
897
+ findings=[],
898
+ proposed_plan=[],
899
+ risks=[f"Expert invocation failed: {error_msg}"],
900
+ files_to_inspect=[],
901
+ confidence=0.0,
902
+ status="failed",
903
+ )
904
+
905
+
906
+ # ---------------------------------------------------------------------------
907
+ # Minimal agent-like proxy for callback hooks
908
+ # ---------------------------------------------------------------------------
909
+
910
+
911
+ class _MinimalAgentProxy:
912
+ """Lightweight object that satisfies the attribute requirements of
913
+ ``on_agent_run_context`` and related hooks without needing a full
914
+ ``BaseAgent`` instance.
915
+
916
+ Expert agents are ephemeral — they don't have a real BaseAgent backing
917
+ them, but some callback hooks expect an object with ``name`` and
918
+ ``get_model_name()``.
919
+ """
920
+
921
+ def __init__(self, expert_name: str) -> None:
922
+ self.name = f"mindpack-{expert_name}"
923
+ self._model_name: str | None = None
924
+
925
+ def get_model_name(self) -> str:
926
+ if self._model_name is None:
927
+ from code_muse.config import get_global_model_name
928
+
929
+ self._model_name = get_global_model_name() or "unknown"
930
+ return self._model_name