qwen-sci 0.2.2__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 (549) hide show
  1. qwen_sci-0.2.2.dist-info/METADATA +492 -0
  2. qwen_sci-0.2.2.dist-info/RECORD +549 -0
  3. qwen_sci-0.2.2.dist-info/WHEEL +4 -0
  4. qwen_sci-0.2.2.dist-info/entry_points.txt +6 -0
  5. qwen_sci-0.2.2.dist-info/licenses/LICENSE +21 -0
  6. src/1.txt +1 -0
  7. src/__init__.py +1 -0
  8. src/__main__.py +5 -0
  9. src/agents/__init__.py +1 -0
  10. src/agents/experiment_design_agent/__init__.py +268 -0
  11. src/agents/experiment_design_agent/artifacts.py +747 -0
  12. src/agents/experiment_design_agent/cache.py +319 -0
  13. src/agents/experiment_design_agent/completeness.py +130 -0
  14. src/agents/experiment_design_agent/contracts.py +771 -0
  15. src/agents/experiment_design_agent/counterexample_analyzer.py +187 -0
  16. src/agents/experiment_design_agent/design_evidence_paper_screener.py +470 -0
  17. src/agents/experiment_design_agent/discipline_catalog.py +178 -0
  18. src/agents/experiment_design_agent/evidence_cards.py +338 -0
  19. src/agents/experiment_design_agent/evidence_planner.py +452 -0
  20. src/agents/experiment_design_agent/formal_reasoning_planner.py +884 -0
  21. src/agents/experiment_design_agent/fulltext_acquisition.py +863 -0
  22. src/agents/experiment_design_agent/idea_adapter.py +100 -0
  23. src/agents/experiment_design_agent/idea_intake.py +87 -0
  24. src/agents/experiment_design_agent/llm_json.py +328 -0
  25. src/agents/experiment_design_agent/orchestrator.py +1220 -0
  26. src/agents/experiment_design_agent/reasoning_context.py +183 -0
  27. src/agents/experiment_design_agent/reasoning_validation.py +546 -0
  28. src/agents/experiment_design_agent/run.py +481 -0
  29. src/agents/experiment_design_agent/run_logging.py +292 -0
  30. src/agents/experiment_design_agent/scope_gate.py +198 -0
  31. src/agents/experiment_design_agent/study_type_composer.py +1360 -0
  32. src/agents/experiment_design_agent/survey_evidence.py +1381 -0
  33. src/agents/experiment_design_agent/template_router.py +195 -0
  34. src/agents/experiment_design_agent/variable_claim_extractor.py +93 -0
  35. src/agents/idea_agent/__init__.py +1 -0
  36. src/agents/idea_agent/agent/__init__.py +6 -0
  37. src/agents/idea_agent/agent/artifacts.py +308 -0
  38. src/agents/idea_agent/agent/base.py +122 -0
  39. src/agents/idea_agent/agent/ligagent.py +488 -0
  40. src/agents/idea_agent/agent/mcts.py +2060 -0
  41. src/agents/idea_agent/agent/prompts/__init__.py +48 -0
  42. src/agents/idea_agent/agent/prompts/advanced_analysis.py +235 -0
  43. src/agents/idea_agent/agent/prompts/algorithm_alignment.py +55 -0
  44. src/agents/idea_agent/agent/prompts/algorithm_structuring.py +48 -0
  45. src/agents/idea_agent/agent/prompts/component_extraction.py +62 -0
  46. src/agents/idea_agent/agent/prompts/component_novelty_evaluation.py +54 -0
  47. src/agents/idea_agent/agent/prompts/experiment_findings_extraction.py +39 -0
  48. src/agents/idea_agent/agent/prompts/idea_fusion.py +295 -0
  49. src/agents/idea_agent/agent/prompts/idea_introduction.py +30 -0
  50. src/agents/idea_agent/agent/prompts/idea_result_alignment.py +39 -0
  51. src/agents/idea_agent/agent/prompts/input_interpreter.py +52 -0
  52. src/agents/idea_agent/agent/prompts/keynote_ops.py +78 -0
  53. src/agents/idea_agent/agent/prompts/mcts_evaluation.py +235 -0
  54. src/agents/idea_agent/agent/prompts/mcts_generation.py +140 -0
  55. src/agents/idea_agent/agent/prompts/mechanism_commit_query.py +99 -0
  56. src/agents/idea_agent/agent/prompts/memory_selection.py +0 -0
  57. src/agents/idea_agent/agent/prompts/prompt_modes.py +16 -0
  58. src/agents/idea_agent/agent/prompts/rag_query.py +34 -0
  59. src/agents/idea_agent/agent/prompts/re_analysis_replan.py +48 -0
  60. src/agents/idea_agent/agent/prompts/reference_grounding.py +34 -0
  61. src/agents/idea_agent/agent/prompts/root_domain_classification.py +32 -0
  62. src/agents/idea_agent/agent/prompts/scientific_debate.py +88 -0
  63. src/agents/idea_agent/agent/prompts/scientific_materialization.py +45 -0
  64. src/agents/idea_agent/agent/prompts/skill_instantiation.py +193 -0
  65. src/agents/idea_agent/agent/prompts/theory_transfer_query.py +93 -0
  66. src/agents/idea_agent/agent/prompts/topic_background.py +15 -0
  67. src/agents/idea_agent/agent/skills/DEFAULT_SKILL_TEMPLATES.json +263 -0
  68. src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/SKILL.md +40 -0
  69. src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/references/contrast_patterns.md +22 -0
  70. src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/SKILL.md +40 -0
  71. src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/references/mechanism_cards.md +24 -0
  72. src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/SKILL.md +39 -0
  73. src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/references/hierarchy_patterns.md +22 -0
  74. src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/SKILL.md +39 -0
  75. src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/references/mechanism_commit_patterns.md +24 -0
  76. src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/SKILL.md +39 -0
  77. src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/references/coordination_patterns.md +22 -0
  78. src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/SKILL.md +41 -0
  79. src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/references/speculation_patterns.md +22 -0
  80. src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/SKILL.md +39 -0
  81. src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/references/modularity_patterns.md +22 -0
  82. src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/SKILL.md +40 -0
  83. src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/references/transfer_patterns.md +24 -0
  84. src/agents/idea_agent/example/idea_result.json +900 -0
  85. src/agents/idea_agent/run.py +276 -0
  86. src/agents/idea_agent/tests/test_advanced_analysis_contract.py +90 -0
  87. src/agents/idea_agent/tests/test_gap_hypothesis_bridge.py +166 -0
  88. src/agents/idea_agent/tests/test_idea_debate.py +480 -0
  89. src/agents/idea_agent/tests/test_idea_result_v5.py +99 -0
  90. src/agents/idea_agent/tests/test_idea_synthesis.py +92 -0
  91. src/agents/idea_agent/tests/test_mature_idea_sources.py +27 -0
  92. src/agents/idea_agent/tests/test_multimodal_idea_integration.py +386 -0
  93. src/agents/idea_agent/tests/test_profile_materialization_and_scoring.py +219 -0
  94. src/agents/idea_agent/tests/test_profile_native_skill_rendering.py +122 -0
  95. src/agents/idea_agent/tests/test_result_persistence.py +183 -0
  96. src/agents/idea_agent/tests/test_scientific_intervention_profiles.py +241 -0
  97. src/agents/idea_agent/tests/test_scientific_rubric_and_prompts.py +559 -0
  98. src/agents/idea_agent/tests/test_strict_json_object_responses.py +48 -0
  99. src/agents/idea_agent/utils/core/__init__.py +1 -0
  100. src/agents/idea_agent/utils/core/ablation_inputs.py +60 -0
  101. src/agents/idea_agent/utils/core/chat_errors.py +95 -0
  102. src/agents/idea_agent/utils/core/chat_router.py +130 -0
  103. src/agents/idea_agent/utils/core/chat_transport.py +188 -0
  104. src/agents/idea_agent/utils/core/config_loader.py +166 -0
  105. src/agents/idea_agent/utils/core/json_utils.py +28 -0
  106. src/agents/idea_agent/utils/core/logger.py +287 -0
  107. src/agents/idea_agent/utils/core/progress.py +58 -0
  108. src/agents/idea_agent/utils/core/response_parsing.py +57 -0
  109. src/agents/idea_agent/utils/core/run_inputs.py +198 -0
  110. src/agents/idea_agent/utils/mcts/__init__.py +1 -0
  111. src/agents/idea_agent/utils/mcts/component_novelty.py +342 -0
  112. src/agents/idea_agent/utils/mcts/defect_registry.py +302 -0
  113. src/agents/idea_agent/utils/mcts/idea_routes.py +61 -0
  114. src/agents/idea_agent/utils/mcts/idea_taste_presets.py +312 -0
  115. src/agents/idea_agent/utils/mcts/mcts_helpers.py +738 -0
  116. src/agents/idea_agent/utils/mcts/mcts_runtime.py +3000 -0
  117. src/agents/idea_agent/utils/mcts/scientific_intervention_ontology.py +1561 -0
  118. src/agents/idea_agent/utils/mcts/scientific_rubric.py +211 -0
  119. src/agents/idea_agent/utils/mcts/skill_parsing.py +92 -0
  120. src/agents/idea_agent/utils/papers/__init__.py +1 -0
  121. src/agents/idea_agent/utils/papers/paper_graph_vector_store.py +499 -0
  122. src/agents/idea_agent/utils/papers/paper_repository.py +372 -0
  123. src/agents/idea_agent/utils/prompting/__init__.py +1 -0
  124. src/agents/idea_agent/utils/prompting/prompt_views.py +637 -0
  125. src/agents/idea_agent/utils/workflow/__init__.py +1 -0
  126. src/agents/idea_agent/utils/workflow/advanced_analysis_contract.py +244 -0
  127. src/agents/idea_agent/utils/workflow/gap_hypothesis_bridge.py +461 -0
  128. src/agents/idea_agent/utils/workflow/idea_contract.py +426 -0
  129. src/agents/idea_agent/utils/workflow/idea_debate.py +1238 -0
  130. src/agents/idea_agent/utils/workflow/idea_diversity.py +163 -0
  131. src/agents/idea_agent/utils/workflow/idea_fusion.py +889 -0
  132. src/agents/idea_agent/utils/workflow/idea_helpers.py +478 -0
  133. src/agents/idea_agent/utils/workflow/idea_portfolio.py +422 -0
  134. src/agents/idea_agent/utils/workflow/idea_synthesis.py +482 -0
  135. src/agents/idea_agent/utils/workflow/ligagent_flow.py +581 -0
  136. src/agents/idea_agent/utils/workflow/ligagent_handlers.py +1836 -0
  137. src/agents/idea_agent/utils/workflow/ligagent_helpers.py +1331 -0
  138. src/agents/idea_agent/utils/workflow/ligagent_utils.py +322 -0
  139. src/agents/idea_agent/utils/workflow/mature_idea_sources.py +648 -0
  140. src/agents/idea_agent/utils/workflow/multimodal_data_anchoring.py +578 -0
  141. src/agents/idea_agent/utils/workflow/result_persistence.py +774 -0
  142. src/agents/idea_agent/utils/workflow/stage_contract.py +76 -0
  143. src/agents/idea_agent/utils/workflow/workflow_runtime.py +288 -0
  144. src/agents/research_plan_author/__init__.py +50 -0
  145. src/agents/research_plan_author/artifacts.py +270 -0
  146. src/agents/research_plan_author/authoring_blueprint.py +1246 -0
  147. src/agents/research_plan_author/bibtex_renderer.py +252 -0
  148. src/agents/research_plan_author/contract_repair.py +131 -0
  149. src/agents/research_plan_author/contracts.py +745 -0
  150. src/agents/research_plan_author/cross_section_editor.py +232 -0
  151. src/agents/research_plan_author/document_quality.py +987 -0
  152. src/agents/research_plan_author/idea_evolution.py +223 -0
  153. src/agents/research_plan_author/input_loader.py +76 -0
  154. src/agents/research_plan_author/latex_compiler.py +260 -0
  155. src/agents/research_plan_author/latex_safety.py +159 -0
  156. src/agents/research_plan_author/llm_json.py +74 -0
  157. src/agents/research_plan_author/markdown_renderer.py +275 -0
  158. src/agents/research_plan_author/pdf_validator.py +149 -0
  159. src/agents/research_plan_author/render.py +166 -0
  160. src/agents/research_plan_author/render_artifacts.py +187 -0
  161. src/agents/research_plan_author/run.py +765 -0
  162. src/agents/research_plan_author/run_logging.py +209 -0
  163. src/agents/research_plan_author/section_cache.py +184 -0
  164. src/agents/research_plan_author/section_composer.py +1764 -0
  165. src/agents/research_plan_author/section_router.py +209 -0
  166. src/agents/research_plan_author/semantic_validator.py +254 -0
  167. src/agents/research_plan_author/source_bundle.py +46 -0
  168. src/agents/research_plan_author/source_registry.py +208 -0
  169. src/agents/research_plan_author/survey_source_loader.py +137 -0
  170. src/agents/research_plan_author/template_adapter.py +173 -0
  171. src/agents/research_plan_author/template_profile.py +204 -0
  172. src/agents/research_plan_author/tex_renderer.py +496 -0
  173. src/agents/research_plan_author/theory_presentation.py +134 -0
  174. src/agents/research_plan_author/theory_spine.py +703 -0
  175. src/agents/survey_agent/Dockerfile +54 -0
  176. src/agents/survey_agent/README.md +350 -0
  177. src/agents/survey_agent/config/deep_survey.yaml +217 -0
  178. src/agents/survey_agent/config/deep_survey_batch.yaml +176 -0
  179. src/agents/survey_agent/config/deep_survey_fast.yaml +183 -0
  180. src/agents/survey_agent/config/evaluate_baseline.yaml +197 -0
  181. src/agents/survey_agent/config/outcomeRAG.yaml +159 -0
  182. src/agents/survey_agent/env_report.md +348 -0
  183. src/agents/survey_agent/modules/__init__.py +0 -0
  184. src/agents/survey_agent/modules/code_collector.py +2194 -0
  185. src/agents/survey_agent/modules/code_report_generator.py +1282 -0
  186. src/agents/survey_agent/modules/data_manager.py +1713 -0
  187. src/agents/survey_agent/modules/database.py +167 -0
  188. src/agents/survey_agent/modules/fulltext_download_cache.py +462 -0
  189. src/agents/survey_agent/modules/fulltext_http.py +134 -0
  190. src/agents/survey_agent/modules/fulltext_resolution.py +749 -0
  191. src/agents/survey_agent/modules/judge.py +434 -0
  192. src/agents/survey_agent/modules/outcome_RAG.py +236 -0
  193. src/agents/survey_agent/modules/paper_graph_retriever.py +1075 -0
  194. src/agents/survey_agent/modules/pe.py +1729 -0
  195. src/agents/survey_agent/modules/pseudo_creator.py +782 -0
  196. src/agents/survey_agent/modules/pseudo_reviser.py +537 -0
  197. src/agents/survey_agent/modules/refine_agent.py +1952 -0
  198. src/agents/survey_agent/modules/survey_generator.py +5691 -0
  199. src/agents/survey_agent/modules/survey_visualizer.py +1817 -0
  200. src/agents/survey_agent/modules/visual_prompts.py +327 -0
  201. src/agents/survey_agent/modules/work_analyzer.py +2750 -0
  202. src/agents/survey_agent/modules/work_collector.py +5649 -0
  203. src/agents/survey_agent/scripts/baseline_evaluation.py +411 -0
  204. src/agents/survey_agent/scripts/run_deep_survey.py +224 -0
  205. src/agents/survey_agent/scripts/run_deep_survey_adapter.py +407 -0
  206. src/agents/survey_agent/scripts/run_deep_survey_adapter_with_input.py +409 -0
  207. src/agents/survey_agent/scripts/run_deep_survey_batch.py +228 -0
  208. src/agents/survey_agent/scripts/run_deep_survey_batch_arg.py +243 -0
  209. src/agents/survey_agent/scripts/run_deep_survey_batch_settings.py +218 -0
  210. src/agents/survey_agent/scripts/run_keynotes_gen.py +128 -0
  211. src/agents/survey_agent/scripts/run_work_clusters.py +140 -0
  212. src/agents/survey_agent/utils/__init__.py +0 -0
  213. src/agents/survey_agent/utils/ablation_utils.py +317 -0
  214. src/agents/survey_agent/utils/api_call.py +2518 -0
  215. src/agents/survey_agent/utils/chat_utils.py +82 -0
  216. src/agents/survey_agent/utils/config_utils.py +70 -0
  217. src/agents/survey_agent/utils/convert_to_md.py +29 -0
  218. src/agents/survey_agent/utils/err_info.py +24 -0
  219. src/agents/survey_agent/utils/file_utils.py +176 -0
  220. src/agents/survey_agent/utils/gpu_utils.py +145 -0
  221. src/agents/survey_agent/utils/graph_visualizer.py +279 -0
  222. src/agents/survey_agent/utils/html_utils.py +181 -0
  223. src/agents/survey_agent/utils/latex_generator_intra_cluster.py +529 -0
  224. src/agents/survey_agent/utils/latex_generator_relation_table.py +193 -0
  225. src/agents/survey_agent/utils/mineru_section_packer.py +334 -0
  226. src/agents/survey_agent/utils/mineru_utils.py +396 -0
  227. src/agents/survey_agent/utils/paper.py +144 -0
  228. src/agents/survey_agent/utils/read_yaml.py +15 -0
  229. src/agents/survey_agent/utils/repo_utils.py +167 -0
  230. src/agents/survey_agent/utils/rich_logger.py +77 -0
  231. src/agents/survey_agent/utils/step2v2.py +874 -0
  232. src/agents/survey_agent/utils/step2v2_extractor.py +856 -0
  233. src/agents/survey_agent/utils/topic_survey_storage.py +309 -0
  234. src/agents/survey_agent/utils/utils.py +118 -0
  235. src/agents/survey_agent/utils/work.py +2 -0
  236. src/cli.py +1611 -0
  237. src/config/__init__.py +213 -0
  238. src/config/default.yaml +1122 -0
  239. src/harness/LICENSE +21 -0
  240. src/harness/VENDORED.md +14 -0
  241. src/harness/src/__init__.py +0 -0
  242. src/harness/src/openharness/__init__.py +0 -0
  243. src/harness/src/openharness/__main__.py +6 -0
  244. src/harness/src/openharness/api/__init__.py +21 -0
  245. src/harness/src/openharness/api/client.py +281 -0
  246. src/harness/src/openharness/api/codex_client.py +407 -0
  247. src/harness/src/openharness/api/copilot_auth.py +240 -0
  248. src/harness/src/openharness/api/copilot_client.py +134 -0
  249. src/harness/src/openharness/api/errors.py +19 -0
  250. src/harness/src/openharness/api/openai_client.py +577 -0
  251. src/harness/src/openharness/api/provider.py +186 -0
  252. src/harness/src/openharness/api/registry.py +437 -0
  253. src/harness/src/openharness/api/usage.py +17 -0
  254. src/harness/src/openharness/auth/__init__.py +29 -0
  255. src/harness/src/openharness/auth/external.py +610 -0
  256. src/harness/src/openharness/auth/flows.py +189 -0
  257. src/harness/src/openharness/auth/manager.py +482 -0
  258. src/harness/src/openharness/auth/storage.py +269 -0
  259. src/harness/src/openharness/autopilot/__init__.py +23 -0
  260. src/harness/src/openharness/autopilot/service.py +2239 -0
  261. src/harness/src/openharness/autopilot/types.py +91 -0
  262. src/harness/src/openharness/bridge/__init__.py +20 -0
  263. src/harness/src/openharness/bridge/manager.py +106 -0
  264. src/harness/src/openharness/bridge/session_runner.py +46 -0
  265. src/harness/src/openharness/bridge/types.py +37 -0
  266. src/harness/src/openharness/bridge/work_secret.py +41 -0
  267. src/harness/src/openharness/channels/UPSTREAM +6 -0
  268. src/harness/src/openharness/channels/__init__.py +22 -0
  269. src/harness/src/openharness/channels/adapter.py +131 -0
  270. src/harness/src/openharness/channels/bus/__init__.py +6 -0
  271. src/harness/src/openharness/channels/bus/events.py +38 -0
  272. src/harness/src/openharness/channels/bus/queue.py +44 -0
  273. src/harness/src/openharness/channels/impl/__init__.py +6 -0
  274. src/harness/src/openharness/channels/impl/base.py +141 -0
  275. src/harness/src/openharness/channels/impl/dingtalk.py +445 -0
  276. src/harness/src/openharness/channels/impl/discord.py +311 -0
  277. src/harness/src/openharness/channels/impl/email.py +410 -0
  278. src/harness/src/openharness/channels/impl/feishu.py +1342 -0
  279. src/harness/src/openharness/channels/impl/manager.py +257 -0
  280. src/harness/src/openharness/channels/impl/matrix.py +700 -0
  281. src/harness/src/openharness/channels/impl/mochat.py +897 -0
  282. src/harness/src/openharness/channels/impl/qq.py +141 -0
  283. src/harness/src/openharness/channels/impl/slack.py +284 -0
  284. src/harness/src/openharness/channels/impl/telegram.py +525 -0
  285. src/harness/src/openharness/channels/impl/whatsapp.py +159 -0
  286. src/harness/src/openharness/cli.py +2551 -0
  287. src/harness/src/openharness/commands/__init__.py +21 -0
  288. src/harness/src/openharness/commands/registry.py +2783 -0
  289. src/harness/src/openharness/config/__init__.py +34 -0
  290. src/harness/src/openharness/config/paths.py +160 -0
  291. src/harness/src/openharness/config/schema.py +119 -0
  292. src/harness/src/openharness/config/settings.py +1104 -0
  293. src/harness/src/openharness/coordinator/__init__.py +12 -0
  294. src/harness/src/openharness/coordinator/agent_definitions.py +975 -0
  295. src/harness/src/openharness/coordinator/coordinator_mode.py +520 -0
  296. src/harness/src/openharness/engine/__init__.py +80 -0
  297. src/harness/src/openharness/engine/cost_tracker.py +24 -0
  298. src/harness/src/openharness/engine/messages.py +222 -0
  299. src/harness/src/openharness/engine/query.py +1188 -0
  300. src/harness/src/openharness/engine/query_engine.py +313 -0
  301. src/harness/src/openharness/engine/stream_events.py +90 -0
  302. src/harness/src/openharness/hooks/__init__.py +50 -0
  303. src/harness/src/openharness/hooks/events.py +20 -0
  304. src/harness/src/openharness/hooks/executor.py +242 -0
  305. src/harness/src/openharness/hooks/hot_reload.py +31 -0
  306. src/harness/src/openharness/hooks/loader.py +68 -0
  307. src/harness/src/openharness/hooks/schemas.py +66 -0
  308. src/harness/src/openharness/hooks/types.py +38 -0
  309. src/harness/src/openharness/keybindings/__init__.py +14 -0
  310. src/harness/src/openharness/keybindings/default_bindings.py +11 -0
  311. src/harness/src/openharness/keybindings/loader.py +22 -0
  312. src/harness/src/openharness/keybindings/parser.py +18 -0
  313. src/harness/src/openharness/keybindings/resolver.py +13 -0
  314. src/harness/src/openharness/mcp/__init__.py +79 -0
  315. src/harness/src/openharness/mcp/client.py +298 -0
  316. src/harness/src/openharness/mcp/config.py +16 -0
  317. src/harness/src/openharness/mcp/types.py +76 -0
  318. src/harness/src/openharness/memory/__init__.py +25 -0
  319. src/harness/src/openharness/memory/agent.py +91 -0
  320. src/harness/src/openharness/memory/manager.py +183 -0
  321. src/harness/src/openharness/memory/memdir.py +52 -0
  322. src/harness/src/openharness/memory/migrate.py +137 -0
  323. src/harness/src/openharness/memory/paths.py +22 -0
  324. src/harness/src/openharness/memory/relevance.py +145 -0
  325. src/harness/src/openharness/memory/scan.py +116 -0
  326. src/harness/src/openharness/memory/schema.py +443 -0
  327. src/harness/src/openharness/memory/search.py +71 -0
  328. src/harness/src/openharness/memory/team.py +90 -0
  329. src/harness/src/openharness/memory/types.py +33 -0
  330. src/harness/src/openharness/memory/usage.py +153 -0
  331. src/harness/src/openharness/output_styles/__init__.py +5 -0
  332. src/harness/src/openharness/output_styles/loader.py +42 -0
  333. src/harness/src/openharness/permissions/__init__.py +26 -0
  334. src/harness/src/openharness/permissions/checker.py +200 -0
  335. src/harness/src/openharness/permissions/modes.py +13 -0
  336. src/harness/src/openharness/personalization/__init__.py +6 -0
  337. src/harness/src/openharness/personalization/extractor.py +138 -0
  338. src/harness/src/openharness/personalization/rules.py +64 -0
  339. src/harness/src/openharness/personalization/session_hook.py +65 -0
  340. src/harness/src/openharness/platforms.py +86 -0
  341. src/harness/src/openharness/plugins/__init__.py +53 -0
  342. src/harness/src/openharness/plugins/bundled/__init__.py +0 -0
  343. src/harness/src/openharness/plugins/installer.py +39 -0
  344. src/harness/src/openharness/plugins/loader.py +730 -0
  345. src/harness/src/openharness/plugins/schemas.py +24 -0
  346. src/harness/src/openharness/plugins/types.py +59 -0
  347. src/harness/src/openharness/prompts/__init__.py +14 -0
  348. src/harness/src/openharness/prompts/claudemd.py +48 -0
  349. src/harness/src/openharness/prompts/context.py +187 -0
  350. src/harness/src/openharness/prompts/environment.py +135 -0
  351. src/harness/src/openharness/prompts/system_prompt.py +109 -0
  352. src/harness/src/openharness/sandbox/Dockerfile +6 -0
  353. src/harness/src/openharness/sandbox/__init__.py +33 -0
  354. src/harness/src/openharness/sandbox/adapter.py +148 -0
  355. src/harness/src/openharness/sandbox/docker_backend.py +232 -0
  356. src/harness/src/openharness/sandbox/docker_image.py +103 -0
  357. src/harness/src/openharness/sandbox/path_validator.py +37 -0
  358. src/harness/src/openharness/sandbox/session.py +63 -0
  359. src/harness/src/openharness/services/__init__.py +30 -0
  360. src/harness/src/openharness/services/autodream/__init__.py +34 -0
  361. src/harness/src/openharness/services/autodream/backup.py +104 -0
  362. src/harness/src/openharness/services/autodream/lock.py +138 -0
  363. src/harness/src/openharness/services/autodream/prompt.py +128 -0
  364. src/harness/src/openharness/services/autodream/service.py +313 -0
  365. src/harness/src/openharness/services/compact/__init__.py +1725 -0
  366. src/harness/src/openharness/services/cron.py +137 -0
  367. src/harness/src/openharness/services/cron_scheduler.py +595 -0
  368. src/harness/src/openharness/services/lsp/__init__.py +216 -0
  369. src/harness/src/openharness/services/memory_extract/__init__.py +261 -0
  370. src/harness/src/openharness/services/oauth/__init__.py +0 -0
  371. src/harness/src/openharness/services/session_backend.py +97 -0
  372. src/harness/src/openharness/services/session_memory/__init__.py +139 -0
  373. src/harness/src/openharness/services/session_storage.py +230 -0
  374. src/harness/src/openharness/services/token_estimation.py +15 -0
  375. src/harness/src/openharness/services/tool_outputs.py +55 -0
  376. src/harness/src/openharness/skills/__init__.py +44 -0
  377. src/harness/src/openharness/skills/_frontmatter.py +97 -0
  378. src/harness/src/openharness/skills/bundled/__init__.py +75 -0
  379. src/harness/src/openharness/skills/bundled/content/commit.md +25 -0
  380. src/harness/src/openharness/skills/bundled/content/debug.md +25 -0
  381. src/harness/src/openharness/skills/bundled/content/diagnose.md +35 -0
  382. src/harness/src/openharness/skills/bundled/content/plan.md +39 -0
  383. src/harness/src/openharness/skills/bundled/content/review.md +33 -0
  384. src/harness/src/openharness/skills/bundled/content/simplify.md +26 -0
  385. src/harness/src/openharness/skills/bundled/content/skill-creator.md +175 -0
  386. src/harness/src/openharness/skills/bundled/content/test.md +38 -0
  387. src/harness/src/openharness/skills/loader.py +229 -0
  388. src/harness/src/openharness/skills/registry.py +29 -0
  389. src/harness/src/openharness/skills/types.py +24 -0
  390. src/harness/src/openharness/state/__init__.py +6 -0
  391. src/harness/src/openharness/state/app_state.py +30 -0
  392. src/harness/src/openharness/state/store.py +40 -0
  393. src/harness/src/openharness/swarm/__init__.py +70 -0
  394. src/harness/src/openharness/swarm/in_process.py +693 -0
  395. src/harness/src/openharness/swarm/lockfile.py +24 -0
  396. src/harness/src/openharness/swarm/mailbox.py +522 -0
  397. src/harness/src/openharness/swarm/permission_sync.py +1168 -0
  398. src/harness/src/openharness/swarm/registry.py +410 -0
  399. src/harness/src/openharness/swarm/spawn_utils.py +229 -0
  400. src/harness/src/openharness/swarm/subprocess_backend.py +171 -0
  401. src/harness/src/openharness/swarm/team_lifecycle.py +910 -0
  402. src/harness/src/openharness/swarm/types.py +398 -0
  403. src/harness/src/openharness/swarm/worktree.py +315 -0
  404. src/harness/src/openharness/tasks/__init__.py +18 -0
  405. src/harness/src/openharness/tasks/local_agent_task.py +28 -0
  406. src/harness/src/openharness/tasks/local_shell_task.py +17 -0
  407. src/harness/src/openharness/tasks/manager.py +475 -0
  408. src/harness/src/openharness/tasks/stop_task.py +11 -0
  409. src/harness/src/openharness/tasks/types.py +32 -0
  410. src/harness/src/openharness/themes/__init__.py +21 -0
  411. src/harness/src/openharness/themes/builtin.py +89 -0
  412. src/harness/src/openharness/themes/loader.py +55 -0
  413. src/harness/src/openharness/themes/schema.py +54 -0
  414. src/harness/src/openharness/tools/__init__.py +107 -0
  415. src/harness/src/openharness/tools/agent_tool.py +141 -0
  416. src/harness/src/openharness/tools/ask_user_question_tool.py +46 -0
  417. src/harness/src/openharness/tools/base.py +80 -0
  418. src/harness/src/openharness/tools/bash_tool.py +259 -0
  419. src/harness/src/openharness/tools/brief_tool.py +33 -0
  420. src/harness/src/openharness/tools/config_tool.py +54 -0
  421. src/harness/src/openharness/tools/cron_create_tool.py +105 -0
  422. src/harness/src/openharness/tools/cron_delete_tool.py +32 -0
  423. src/harness/src/openharness/tools/cron_list_tool.py +69 -0
  424. src/harness/src/openharness/tools/cron_toggle_tool.py +37 -0
  425. src/harness/src/openharness/tools/enter_plan_mode_tool.py +28 -0
  426. src/harness/src/openharness/tools/enter_worktree_tool.py +80 -0
  427. src/harness/src/openharness/tools/exit_plan_mode_tool.py +28 -0
  428. src/harness/src/openharness/tools/exit_worktree_tool.py +42 -0
  429. src/harness/src/openharness/tools/file_edit_tool.py +95 -0
  430. src/harness/src/openharness/tools/file_read_tool.py +72 -0
  431. src/harness/src/openharness/tools/file_write_tool.py +87 -0
  432. src/harness/src/openharness/tools/glob_tool.py +175 -0
  433. src/harness/src/openharness/tools/grep_tool.py +374 -0
  434. src/harness/src/openharness/tools/image_generation_tool.py +447 -0
  435. src/harness/src/openharness/tools/image_to_text_tool.py +237 -0
  436. src/harness/src/openharness/tools/list_mcp_resources_tool.py +36 -0
  437. src/harness/src/openharness/tools/lsp_tool.py +154 -0
  438. src/harness/src/openharness/tools/mcp_auth_tool.py +71 -0
  439. src/harness/src/openharness/tools/mcp_tool.py +72 -0
  440. src/harness/src/openharness/tools/notebook_edit_tool.py +97 -0
  441. src/harness/src/openharness/tools/read_mcp_resource_tool.py +38 -0
  442. src/harness/src/openharness/tools/remote_trigger_tool.py +74 -0
  443. src/harness/src/openharness/tools/send_message_tool.py +58 -0
  444. src/harness/src/openharness/tools/skill_tool.py +43 -0
  445. src/harness/src/openharness/tools/sleep_tool.py +32 -0
  446. src/harness/src/openharness/tools/task_create_tool.py +56 -0
  447. src/harness/src/openharness/tools/task_get_tool.py +33 -0
  448. src/harness/src/openharness/tools/task_list_tool.py +35 -0
  449. src/harness/src/openharness/tools/task_output_tool.py +35 -0
  450. src/harness/src/openharness/tools/task_stop_tool.py +30 -0
  451. src/harness/src/openharness/tools/task_update_tool.py +50 -0
  452. src/harness/src/openharness/tools/team_create_tool.py +31 -0
  453. src/harness/src/openharness/tools/team_delete_tool.py +30 -0
  454. src/harness/src/openharness/tools/todo_write_tool.py +46 -0
  455. src/harness/src/openharness/tools/tool_search_tool.py +38 -0
  456. src/harness/src/openharness/tools/web_fetch_tool.py +117 -0
  457. src/harness/src/openharness/tools/web_search_tool.py +119 -0
  458. src/harness/src/openharness/ui/__init__.py +5 -0
  459. src/harness/src/openharness/ui/app.py +320 -0
  460. src/harness/src/openharness/ui/backend_host.py +941 -0
  461. src/harness/src/openharness/ui/coordinator_drain.py +198 -0
  462. src/harness/src/openharness/ui/input.py +32 -0
  463. src/harness/src/openharness/ui/output.py +265 -0
  464. src/harness/src/openharness/ui/permission_dialog.py +14 -0
  465. src/harness/src/openharness/ui/protocol.py +247 -0
  466. src/harness/src/openharness/ui/react_launcher.py +179 -0
  467. src/harness/src/openharness/ui/runtime.py +799 -0
  468. src/harness/src/openharness/ui/textual_app.py +495 -0
  469. src/harness/src/openharness/utils/__init__.py +0 -0
  470. src/harness/src/openharness/utils/file_lock.py +86 -0
  471. src/harness/src/openharness/utils/fs.py +98 -0
  472. src/harness/src/openharness/utils/helpers.py +77 -0
  473. src/harness/src/openharness/utils/network_guard.py +340 -0
  474. src/harness/src/openharness/utils/shell.py +147 -0
  475. src/harness/src/openharness/vim/__init__.py +5 -0
  476. src/harness/src/openharness/vim/transitions.py +8 -0
  477. src/harness/src/openharness/voice/__init__.py +7 -0
  478. src/harness/src/openharness/voice/keyterms.py +10 -0
  479. src/harness/src/openharness/voice/stream_stt.py +8 -0
  480. src/harness/src/openharness/voice/voice_mode.py +44 -0
  481. src/llm/__init__.py +29 -0
  482. src/llm/image_generation.py +402 -0
  483. src/llm/provider_registry.py +419 -0
  484. src/llm/runtime_env.py +64 -0
  485. src/llm/structured_output.py +131 -0
  486. src/llm/vision.py +198 -0
  487. src/memory/__init__.py +2 -0
  488. src/memory/api/__init__.py +1 -0
  489. src/memory/api/base_memory_system_api.py +98 -0
  490. src/memory/api/base_symbolic_memory_system_api.py +190 -0
  491. src/memory/api/base_vector_memory_system_api.py +116 -0
  492. src/memory/api/faiss_memory_system_api.py +228 -0
  493. src/memory/api/slot_process_api.py +827 -0
  494. src/memory/api/symbolic_memory_system_api.py +572 -0
  495. src/memory/memory_system/__init__.py +48 -0
  496. src/memory/memory_system/component_taxonomy.py +430 -0
  497. src/memory/memory_system/llm.py +437 -0
  498. src/memory/memory_system/models.py +168 -0
  499. src/memory/memory_system/user_prompt.py +418 -0
  500. src/memory/memory_system/utils.py +282 -0
  501. src/memory/memory_system/vectorstore.py +311 -0
  502. src/memory/memory_system/working_slot.py +99 -0
  503. src/pipeline/__init__.py +129 -0
  504. src/pipeline/contracts.py +531 -0
  505. src/pipeline/discipline_taxonomy.py +916 -0
  506. src/pipeline/evidence_coverage_ledger.py +1150 -0
  507. src/pipeline/evidence_refinement.py +620 -0
  508. src/pipeline/experiment_to_symbolic.py +137 -0
  509. src/pipeline/multimodal_evidence/__init__.py +28 -0
  510. src/pipeline/multimodal_evidence/capabilities.py +107 -0
  511. src/pipeline/multimodal_evidence/claims.py +135 -0
  512. src/pipeline/multimodal_evidence/contract.py +291 -0
  513. src/pipeline/multimodal_evidence/data_sh_compiler.py +505 -0
  514. src/pipeline/multimodal_evidence/handoff_binding.py +135 -0
  515. src/pipeline/multimodal_evidence/manifest.py +254 -0
  516. src/pipeline/multimodal_evidence/native_analysis.py +479 -0
  517. src/pipeline/multimodal_evidence/perception.py +277 -0
  518. src/pipeline/multimodal_evidence/query_binding.py +65 -0
  519. src/pipeline/multimodal_evidence/rendering.py +120 -0
  520. src/pipeline/multimodal_evidence/safety.py +42 -0
  521. src/pipeline/multimodal_evidence/sampling.py +69 -0
  522. src/pipeline/multimodal_evidence/service.py +148 -0
  523. src/pipeline/multimodal_evidence/survey_integration.py +460 -0
  524. src/pipeline/paper_identity.py +52 -0
  525. src/pipeline/research_design_inventory.py +338 -0
  526. src/pipeline/research_domain_resolution.py +669 -0
  527. src/pipeline/research_identity.py +548 -0
  528. src/pipeline/research_question_contract.py +646 -0
  529. src/pipeline/retrieval_lanes.py +889 -0
  530. src/pipeline/run_loop.py +862 -0
  531. src/pipeline/science_events.py +30 -0
  532. src/pipeline/science_manifests.py +602 -0
  533. src/pipeline/science_run.py +918 -0
  534. src/pipeline/science_stages.py +709 -0
  535. src/pipeline/science_workflow.py +807 -0
  536. src/pipeline/sh_cluster_projection.py +306 -0
  537. src/pipeline/sh_graph_provenance.py +648 -0
  538. src/pipeline/subhypothesis_decomposition.py +392 -0
  539. src/pipeline/subhypothesis_evidence.py +856 -0
  540. src/pipeline/survey_evidence_plan.py +858 -0
  541. src/pipeline/survey_gap_adjudication.py +647 -0
  542. src/pipeline/survey_gap_candidates.py +840 -0
  543. src/pipeline/survey_gap_ledger.py +683 -0
  544. src/pipeline/survey_gap_triage.py +401 -0
  545. src/pipeline/survey_handoff_persistence.py +481 -0
  546. src/pipeline/survey_handoff_projection.py +457 -0
  547. src/pipeline/survey_idea_handoff.py +1112 -0
  548. src/pipeline/survey_idea_loader.py +289 -0
  549. src/research_run_ids.py +11 -0
@@ -0,0 +1,2060 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import itertools
5
+ import math
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple
9
+
10
+ try:
11
+ from omegaconf import OmegaConf
12
+ except Exception: # pragma: no cover - optional runtime fallback
13
+ OmegaConf = None
14
+
15
+ from src.memory.api.faiss_memory_system_api import FAISSMemorySystem
16
+ from src.memory.api.slot_process_api import SlotProcess
17
+ from src.memory.api.symbolic_memory_system_api import SymbolicMemorySystem
18
+ from src.memory.memory_system import FaissVectorStore
19
+ from src.memory.memory_system.models import EpisodicRecord, ProceduralRecord, SemanticRecord
20
+ from src.memory.memory_system.utils import _multi_thread_run, _safe_dump_str
21
+ from src.agents.idea_agent.utils.core.json_utils import pretty_json
22
+ from src.agents.idea_agent.utils.core.progress import iter_with_progress
23
+ from src.agents.idea_agent.utils.core.response_parsing import parse_json_response
24
+ from src.agents.idea_agent.utils.core.logger import LoguruCompatLogger, get_logger
25
+ from src.agents.idea_agent.utils.mcts.component_novelty import ComponentNoveltyScorer
26
+ from src.agents.idea_agent.utils.mcts.mcts_helpers import (
27
+ _dedupe_keep_order_strings,
28
+ _format_root_domains_for_prompt,
29
+ _normalize_root_domains,
30
+ _safe_float_default,
31
+ apply_normalized_score_weights,
32
+ clip_metric_score,
33
+ clip_text,
34
+ coerce_integer_metric_score,
35
+ parent_centers_control_surface,
36
+ format_mechanism_commit_references,
37
+ format_theory_transfer_references,
38
+ component_inventory_payload,
39
+ format_analysis_blob,
40
+ normalize_mechanism_commit_query_payload,
41
+ normalize_theory_transfer_query_payload,
42
+ normalize_component_explanations,
43
+ text_uses_forbidden_query_terms,
44
+ )
45
+ from src.pipeline.discipline_taxonomy import (
46
+ canonicalize_discipline_key,
47
+ format_taxonomy_catalog_for_prompt,
48
+ resolve_discipline_taxonomy,
49
+ )
50
+ from src.agents.idea_agent.utils.prompting.prompt_views import (
51
+ format_idea_prompt_view,
52
+ )
53
+ from src.agents.idea_agent.utils.papers.paper_graph_vector_store import (
54
+ PaperGraphComponentVectorStore,
55
+ )
56
+ from src.agents.idea_agent.agent.prompts.root_domain_classification import (
57
+ ROOT_DOMAIN_CLASSIFICATION_PROMPT,
58
+ )
59
+ from src.agents.idea_agent.agent.prompts.skill_instantiation import (
60
+ get_skill_instantiation_prompt,
61
+ )
62
+ from src.agents.idea_agent.agent.prompts.mechanism_commit_query import (
63
+ get_mechanism_commit_query_prompt,
64
+ )
65
+ from src.agents.idea_agent.agent.prompts.theory_transfer_query import (
66
+ get_theory_transfer_query_prompt,
67
+ )
68
+ from src.agents.idea_agent.agent.prompts.component_extraction import (
69
+ render_component_extraction_prompt,
70
+ )
71
+ from src.agents.idea_agent.utils.core.config_loader import (
72
+ load_idea_agent_config,
73
+ resolve_workspace_path,
74
+ )
75
+ from src.agents.idea_agent.utils.mcts.idea_taste_presets import (
76
+ IdeaTastePreset,
77
+ SCORE_WEIGHT_FIELDS,
78
+ get_idea_taste_preset,
79
+ )
80
+ from src.agents.idea_agent.utils.mcts.scientific_intervention_ontology import (
81
+ build_scientific_intervention_payload,
82
+ normalize_project_context_discipline_resolution,
83
+ resolve_scientific_intervention_profile,
84
+ )
85
+ from src.agents.idea_agent.utils.mcts.scientific_rubric import (
86
+ PROFILE_NOVELTY_AXES,
87
+ profile_score_weights,
88
+ )
89
+ from src.agents.idea_agent.utils.workflow.gap_hypothesis_bridge import (
90
+ build_gap_hypothesis_seeds,
91
+ build_gap_seed_context,
92
+ build_gap_seed_status,
93
+ )
94
+ from src.agents.idea_agent.utils.workflow.idea_contract import normalize_mature_idea
95
+ from src.agents.idea_agent.utils.mcts.mcts_runtime import (
96
+ EditPlan,
97
+ MemoryBundle,
98
+ MemorySnippet,
99
+ SkillCatalog,
100
+ SkillUsagePrior,
101
+ best_candidate,
102
+ build_symbolic_eval_hints,
103
+ build_root_state,
104
+ compute_protocol_score_from_plan,
105
+ extract_mature_idea_components_via_llm,
106
+ instantiate_skill_plan_for_node,
107
+ log_message,
108
+ materialize_child_state,
109
+ memory_bundle_log_payload,
110
+ new_node,
111
+ pareto_candidates,
112
+ path_summary,
113
+ backpropagate_rollout,
114
+ expand_node_with_skills,
115
+ reset_search_state,
116
+ select_leaf_for_rollout,
117
+ simulate_log_payload,
118
+ simulate_node_value,
119
+ update_skill_prior_from_evaluation,
120
+ )
121
+
122
+
123
+ UNIFORM_CLIP_TEXT_LIMIT = 10000
124
+ MAX_LIST_ENTRIES = 16
125
+ MIN_COMPONENTS = 1
126
+ MAX_COMPONENTS = 5
127
+ HYPOTHESIS_PAYLOAD_FIELDS = (
128
+ "direction_mode",
129
+ "direction_summary",
130
+ "central_hypothesis",
131
+ "scientific_object",
132
+ "mechanism_or_relation",
133
+ "intervention_or_transformation",
134
+ "expected_mechanism",
135
+ "discriminating_observation",
136
+ "boundary_or_failure_condition",
137
+ "claim_scope",
138
+ "assumptions",
139
+ "target_gap_ids",
140
+ "gap_alignment",
141
+ "evidence_requirement",
142
+ "evidence_basis",
143
+ )
144
+ INTEGER_EVALUATION_FIELDS = (
145
+ "novelty",
146
+ "surprise",
147
+ "feasibility",
148
+ "clarity",
149
+ "impact",
150
+ "risk",
151
+ "conciseness",
152
+ "alignment_score",
153
+ "complexity_penalty",
154
+ "protocol_score",
155
+ "explanatory_power",
156
+ "identifiability",
157
+ "boundary_calibration",
158
+ "claim_overreach_penalty",
159
+ )
160
+
161
+ module_logger = get_logger()
162
+
163
+
164
+ def _load_mcts_defaults() -> Dict[str, Any]:
165
+ if OmegaConf is None:
166
+ return {}
167
+ config = load_idea_agent_config()
168
+ mcts_config = config.get("mcts") if hasattr(config, "get") else None
169
+ if mcts_config is None:
170
+ return {}
171
+ try:
172
+ return OmegaConf.to_container(mcts_config, resolve=True) or {}
173
+ except Exception:
174
+ return dict(mcts_config) if isinstance(mcts_config, dict) else {}
175
+
176
+
177
+ _MCTS_DEFAULTS = _load_mcts_defaults()
178
+
179
+
180
+ def _mcts_default(key: str, fallback: Any) -> Any:
181
+ value = _MCTS_DEFAULTS.get(key, fallback)
182
+ if key in {"generation_model", "evaluation_model", "component_novelty_eval_model"}:
183
+ if not str(value or "").strip():
184
+ role = {
185
+ "generation_model": "idea_generation",
186
+ "evaluation_model": "idea_evaluation",
187
+ "component_novelty_eval_model": "idea_evaluation",
188
+ }[key]
189
+ try:
190
+ from src.llm.provider_registry import resolve_role_model
191
+
192
+ return resolve_role_model(None, role).name
193
+ except Exception:
194
+ return fallback
195
+ return fallback if value is None else value
196
+
197
+
198
+ @dataclass
199
+ class IdeaState:
200
+ title: str
201
+ abstract: str
202
+ core_contribution: str
203
+ method: str
204
+ risks: str
205
+ tags: List[str]
206
+ operator: str
207
+ target_defects: List[str]
208
+ rationale: str
209
+ memory_refs: List[str] = field(default_factory=list)
210
+ components: List[str] = field(default_factory=list)
211
+ component_explanations: Dict[str, str] = field(default_factory=dict)
212
+ root_domains: List[str] = field(default_factory=list)
213
+ discipline_resolution: Dict[str, Any] = field(default_factory=dict)
214
+ scientific_intervention: Dict[str, Any] = field(default_factory=dict)
215
+ paper_graph_context: str = ""
216
+ edit_plan: Optional[Dict[str, Any]] = None
217
+ skill_metrics: Dict[str, Any] = field(default_factory=dict)
218
+ signature: str = field(init=False)
219
+
220
+ def __post_init__(self) -> None:
221
+ self.title = clip_text(self.title, UNIFORM_CLIP_TEXT_LIMIT)
222
+ self.abstract = clip_text(self.abstract, UNIFORM_CLIP_TEXT_LIMIT)
223
+ self.core_contribution = clip_text(self.core_contribution, UNIFORM_CLIP_TEXT_LIMIT)
224
+ self.method = clip_text(self.method, UNIFORM_CLIP_TEXT_LIMIT)
225
+ self.risks = clip_text(self.risks, UNIFORM_CLIP_TEXT_LIMIT)
226
+ self.rationale = clip_text(self.rationale, UNIFORM_CLIP_TEXT_LIMIT)
227
+ self.paper_graph_context = clip_text(self.paper_graph_context, UNIFORM_CLIP_TEXT_LIMIT)
228
+
229
+ self.tags = _dedupe_keep_order_strings(
230
+ [clip_text(tag, UNIFORM_CLIP_TEXT_LIMIT) for tag in self.tags[:MAX_LIST_ENTRIES]]
231
+ )
232
+ self.target_defects = _dedupe_keep_order_strings(
233
+ [clip_text(tag, UNIFORM_CLIP_TEXT_LIMIT) for tag in self.target_defects[:MAX_LIST_ENTRIES]]
234
+ )
235
+ self.memory_refs = _dedupe_keep_order_strings(
236
+ [clip_text(ref, UNIFORM_CLIP_TEXT_LIMIT) for ref in self.memory_refs[:MAX_LIST_ENTRIES]]
237
+ )
238
+ self.components = _dedupe_keep_order_strings(
239
+ [clip_text(comp, UNIFORM_CLIP_TEXT_LIMIT) for comp in self.components[: 2 * MAX_LIST_ENTRIES]]
240
+ )
241
+ self.component_explanations = normalize_component_explanations(
242
+ self.components,
243
+ self.component_explanations,
244
+ )
245
+ self.root_domains = _normalize_root_domains(self.root_domains)
246
+ self.discipline_resolution = (
247
+ dict(self.discipline_resolution)
248
+ if isinstance(self.discipline_resolution, dict)
249
+ else {}
250
+ )
251
+ self.scientific_intervention = (
252
+ dict(self.scientific_intervention)
253
+ if isinstance(self.scientific_intervention, dict)
254
+ else {}
255
+ )
256
+
257
+ canonical = "|".join(
258
+ [
259
+ self.title.lower(),
260
+ self.core_contribution.lower(),
261
+ self.method.lower(),
262
+ ",".join(sorted(self.tags)),
263
+ ",".join(self.root_domains),
264
+ str(self.scientific_intervention.get("profile_id") or ""),
265
+ str(self.scientific_intervention.get("contribution_mode") or ""),
266
+ ",".join(sorted(self.components)),
267
+ "|".join(
268
+ f"{component}:{self.component_explanations.get(component, '').lower()}"
269
+ for component in self.components
270
+ ),
271
+ ]
272
+ )
273
+ self.signature = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
274
+
275
+ def describe(self) -> str:
276
+ return self.to_prompt_view(heading="Parent Idea")
277
+
278
+ def to_prompt_view(self, heading: str = "Idea Snapshot") -> str:
279
+ return format_idea_prompt_view(self, heading=heading)
280
+
281
+ def to_payload(self) -> Dict[str, Any]:
282
+ payload = {
283
+ "title": self.title,
284
+ "abstract": self.abstract,
285
+ "core_contribution": self.core_contribution,
286
+ "method": self.method,
287
+ "risks": self.risks,
288
+ "tags": self.tags,
289
+ "operator": self.operator,
290
+ "target_defects": self.target_defects,
291
+ "memory_refs": self.memory_refs,
292
+ "rationale": self.rationale,
293
+ "components": self.components,
294
+ "component_explanations": self.component_explanations,
295
+ "root_domains": self.root_domains,
296
+ "discipline_resolution": self.discipline_resolution,
297
+ "scientific_intervention": self.scientific_intervention,
298
+ "components_with_explanations": self.component_inventory(),
299
+ "paper_graph_context": self.paper_graph_context,
300
+ }
301
+ hypothesis_contract = self.scientific_intervention.get("hypothesis_contract")
302
+ if isinstance(hypothesis_contract, dict):
303
+ for field_name in HYPOTHESIS_PAYLOAD_FIELDS:
304
+ value = hypothesis_contract.get(field_name)
305
+ if value not in (None, "", [], {}):
306
+ payload[field_name] = value
307
+ direction_mode = self.scientific_intervention.get("direction_mode")
308
+ if direction_mode and not payload.get("direction_mode"):
309
+ payload["direction_mode"] = direction_mode
310
+ direction_summary = self.scientific_intervention.get("direction_summary")
311
+ if direction_summary and not payload.get("direction_summary"):
312
+ payload["direction_summary"] = direction_summary
313
+ if self.edit_plan:
314
+ payload["edit_plan"] = self.edit_plan
315
+ if self.skill_metrics:
316
+ payload["skill_metrics"] = self.skill_metrics
317
+ return payload
318
+
319
+ def component_inventory(self) -> List[Dict[str, str]]:
320
+ return component_inventory_payload(self.components, self.component_explanations)
321
+
322
+
323
+ @dataclass
324
+ class IdeaEvaluation:
325
+ novelty: float
326
+ surprise: float
327
+ feasibility: float
328
+ clarity: float
329
+ impact: float
330
+ risk: float
331
+ conciseness: float
332
+ alignment_score: float
333
+ complexity_penalty: float
334
+ protocol_score: float
335
+ confidence: float
336
+ failure_modes: List[str]
337
+ fairness_protocol: str
338
+ feedback: str
339
+ defect_fix_summary: str
340
+ detected_defects: List[str]
341
+ explanatory_power: float = 0.0
342
+ identifiability: float = 0.0
343
+ boundary_calibration: float = 0.0
344
+ claim_overreach_penalty: float = 0.0
345
+ novelty_axes: Dict[str, float] = field(default_factory=dict)
346
+ profile_id: str = ""
347
+ novelty_weight: float = 0.20
348
+ surprise_weight: float = 0.10
349
+ impact_weight: float = 0.18
350
+ feasibility_weight: float = 0.11
351
+ clarity_weight: float = 0.06
352
+ conciseness_weight: float = 0.03
353
+ risk_weight: float = 0.09
354
+ alignment_weight: float = 0.15
355
+ complexity_weight: float = 0.05
356
+ protocol_weight: float = 0.03
357
+ explanatory_power_weight: float = 0.06
358
+ identifiability_weight: float = 0.06
359
+ boundary_calibration_weight: float = 0.04
360
+ claim_overreach_weight: float = 0.10
361
+
362
+ def __post_init__(self) -> None:
363
+ self.failure_modes = [
364
+ clip_text(mode, UNIFORM_CLIP_TEXT_LIMIT)
365
+ for mode in (self.failure_modes or [])[:MAX_LIST_ENTRIES]
366
+ ]
367
+ self.fairness_protocol = clip_text(self.fairness_protocol, UNIFORM_CLIP_TEXT_LIMIT)
368
+ self.feedback = clip_text(self.feedback, UNIFORM_CLIP_TEXT_LIMIT)
369
+ self.defect_fix_summary = clip_text(self.defect_fix_summary, UNIFORM_CLIP_TEXT_LIMIT)
370
+ self.detected_defects = [
371
+ clip_text(tag, UNIFORM_CLIP_TEXT_LIMIT)
372
+ for tag in (self.detected_defects or [])[:MAX_LIST_ENTRIES]
373
+ ]
374
+ self.profile_id = str(self.profile_id or "").strip().lower()
375
+ allowed_axes = None
376
+ if self.profile_id:
377
+ allowed_axes = set(
378
+ PROFILE_NOVELTY_AXES.get(
379
+ self.profile_id,
380
+ PROFILE_NOVELTY_AXES["generic_scientific"],
381
+ )
382
+ )
383
+ allowed_axes.add("scientific_novelty")
384
+ normalized_axes: Dict[str, float] = {}
385
+ if isinstance(self.novelty_axes, dict):
386
+ for key, value in self.novelty_axes.items():
387
+ normalized_key = str(key).strip()
388
+ if not normalized_key:
389
+ continue
390
+ if allowed_axes is not None and normalized_key not in allowed_axes:
391
+ continue
392
+ try:
393
+ normalized_axes[normalized_key] = max(0.0, min(5.0, float(value)))
394
+ except (TypeError, ValueError):
395
+ continue
396
+ if not normalized_axes:
397
+ normalized_axes = {"scientific_novelty": float(self.novelty)}
398
+ normalized_axes.setdefault("scientific_novelty", float(self.novelty))
399
+ self.novelty_axes = normalized_axes
400
+ for field_name in INTEGER_EVALUATION_FIELDS:
401
+ setattr(
402
+ self,
403
+ field_name,
404
+ coerce_integer_metric_score(getattr(self, field_name, 0)),
405
+ )
406
+ self.confidence = max(0.0, min(1.0, _safe_float_default(self.confidence, 0.0)))
407
+ apply_normalized_score_weights(self, SCORE_WEIGHT_FIELDS)
408
+
409
+ @classmethod
410
+ def from_payload(
411
+ cls,
412
+ payload: Dict[str, Any],
413
+ weights: Optional[Dict[str, float]] = None,
414
+ profile_id: str = "",
415
+ ) -> "IdeaEvaluation":
416
+ def _num(key: str, default: float = 0.0) -> float:
417
+ return _safe_float_default(payload.get(key, default), default)
418
+
419
+ def _list(key: str) -> List[str]:
420
+ raw = payload.get(key, [])
421
+ if isinstance(raw, list):
422
+ return [str(x) for x in raw]
423
+ if isinstance(raw, str):
424
+ return [raw]
425
+ return []
426
+
427
+ if weights is not None:
428
+ w = dict(weights)
429
+ elif profile_id:
430
+ w = profile_score_weights(
431
+ profile_id,
432
+ {
433
+ "novelty_weight": 0.20,
434
+ "surprise_weight": 0.10,
435
+ "impact_weight": 0.18,
436
+ "feasibility_weight": 0.11,
437
+ "clarity_weight": 0.06,
438
+ "conciseness_weight": 0.03,
439
+ "risk_weight": 0.09,
440
+ "alignment_weight": 0.15,
441
+ "complexity_weight": 0.05,
442
+ "protocol_weight": 0.03,
443
+ "explanatory_power_weight": 0.06,
444
+ "identifiability_weight": 0.06,
445
+ "boundary_calibration_weight": 0.04,
446
+ "claim_overreach_weight": 0.10,
447
+ },
448
+ )
449
+ else:
450
+ w = {}
451
+ return cls(
452
+ novelty=_num("novelty"),
453
+ surprise=_num("surprise"),
454
+ feasibility=_num("feasibility"),
455
+ clarity=_num("clarity"),
456
+ impact=_num("impact"),
457
+ risk=_num("risk"),
458
+ conciseness=_num("conciseness"),
459
+ alignment_score=_num("alignment_score"),
460
+ complexity_penalty=_num("complexity_penalty"),
461
+ protocol_score=_num("protocol_score"),
462
+ explanatory_power=_num("explanatory_power"),
463
+ identifiability=_num("identifiability"),
464
+ boundary_calibration=_num("boundary_calibration"),
465
+ claim_overreach_penalty=_num("claim_overreach_penalty"),
466
+ novelty_axes=(
467
+ payload.get("novelty_axes")
468
+ if isinstance(payload.get("novelty_axes"), dict)
469
+ else {}
470
+ ),
471
+ profile_id=profile_id or str(payload.get("profile_id") or ""),
472
+ confidence=_num("confidence", 0.0),
473
+ failure_modes=_list("failure_modes"),
474
+ fairness_protocol=str(payload.get("fairness_protocol", "")),
475
+ feedback=str(payload.get("feedback", "")),
476
+ defect_fix_summary=str(payload.get("defect_fix_summary", "")),
477
+ detected_defects=_list("detected_defects"),
478
+ novelty_weight=w.get("novelty_weight", 0.20),
479
+ surprise_weight=w.get("surprise_weight", 0.10),
480
+ impact_weight=w.get("impact_weight", 0.18),
481
+ feasibility_weight=w.get("feasibility_weight", 0.11),
482
+ clarity_weight=w.get("clarity_weight", 0.06),
483
+ conciseness_weight=w.get("conciseness_weight", 0.03),
484
+ risk_weight=w.get("risk_weight", 0.09),
485
+ alignment_weight=w.get("alignment_weight", 0.15),
486
+ complexity_weight=w.get("complexity_weight", 0.05),
487
+ protocol_weight=w.get("protocol_weight", 0.03),
488
+ explanatory_power_weight=w.get("explanatory_power_weight", 0.06),
489
+ identifiability_weight=w.get("identifiability_weight", 0.06),
490
+ boundary_calibration_weight=w.get("boundary_calibration_weight", 0.04),
491
+ claim_overreach_weight=w.get("claim_overreach_weight", 0.10),
492
+ )
493
+
494
+ def to_dict(self) -> Dict[str, Any]:
495
+ return {
496
+ "novelty": self.novelty,
497
+ "surprise": self.surprise,
498
+ "feasibility": self.feasibility,
499
+ "clarity": self.clarity,
500
+ "impact": self.impact,
501
+ "risk": self.risk,
502
+ "conciseness": self.conciseness,
503
+ "alignment_score": self.alignment_score,
504
+ "complexity_penalty": self.complexity_penalty,
505
+ "protocol_score": self.protocol_score,
506
+ "explanatory_power": self.explanatory_power,
507
+ "identifiability": self.identifiability,
508
+ "boundary_calibration": self.boundary_calibration,
509
+ "claim_overreach_penalty": self.claim_overreach_penalty,
510
+ "novelty_axes": self.novelty_axes,
511
+ "profile_id": self.profile_id,
512
+ "confidence": self.confidence,
513
+ "failure_modes": self.failure_modes,
514
+ "fairness_protocol": self.fairness_protocol,
515
+ "feedback": self.feedback,
516
+ "defect_fix_summary": self.defect_fix_summary,
517
+ "detected_defects": self.detected_defects,
518
+ }
519
+
520
+ @property
521
+ def composite(self) -> float:
522
+ allowed_axes = None
523
+ if self.profile_id:
524
+ allowed_axes = set(
525
+ PROFILE_NOVELTY_AXES.get(
526
+ self.profile_id,
527
+ PROFILE_NOVELTY_AXES["generic_scientific"],
528
+ )
529
+ )
530
+ allowed_axes.add("scientific_novelty")
531
+ novelty_values = [
532
+ score for key, score in self.novelty_axes.items()
533
+ if key != "retrieval_similarity"
534
+ and (allowed_axes is None or key in allowed_axes)
535
+ ]
536
+ scientific_novelty = (
537
+ sum(novelty_values) / len(novelty_values)
538
+ if novelty_values
539
+ else self.novelty
540
+ )
541
+ weighted_scores = {
542
+ "novelty_weight": clip_metric_score(scientific_novelty),
543
+ "surprise_weight": clip_metric_score(self.surprise),
544
+ "impact_weight": clip_metric_score(self.impact),
545
+ "feasibility_weight": clip_metric_score(self.feasibility),
546
+ "clarity_weight": clip_metric_score(self.clarity),
547
+ "conciseness_weight": clip_metric_score(self.conciseness),
548
+ "alignment_weight": clip_metric_score(self.alignment_score),
549
+ "protocol_weight": clip_metric_score(self.protocol_score),
550
+ "risk_weight": 5.0 - clip_metric_score(self.risk),
551
+ "complexity_weight": 5.0 - clip_metric_score(self.complexity_penalty),
552
+ "explanatory_power_weight": clip_metric_score(self.explanatory_power),
553
+ "identifiability_weight": clip_metric_score(self.identifiability),
554
+ "boundary_calibration_weight": clip_metric_score(self.boundary_calibration),
555
+ "claim_overreach_weight": 5.0 - clip_metric_score(self.claim_overreach_penalty),
556
+ }
557
+ return sum(
558
+ getattr(self, field_name) * score
559
+ for field_name, score in weighted_scores.items()
560
+ )
561
+
562
+
563
+ @dataclass
564
+ class OperatorApplication:
565
+ operator: str
566
+ defects: List[str]
567
+ rationale: str
568
+ memory_refs: List[str]
569
+
570
+ def __post_init__(self) -> None:
571
+ self.operator = clip_text(self.operator, UNIFORM_CLIP_TEXT_LIMIT)
572
+ self.defects = [clip_text(defect, UNIFORM_CLIP_TEXT_LIMIT) for defect in self.defects[:MAX_LIST_ENTRIES]]
573
+ self.rationale = clip_text(self.rationale, UNIFORM_CLIP_TEXT_LIMIT)
574
+ self.memory_refs = [clip_text(ref, UNIFORM_CLIP_TEXT_LIMIT) for ref in self.memory_refs[:MAX_LIST_ENTRIES]]
575
+
576
+
577
+ @dataclass
578
+ class IdeaNode:
579
+ node_id: int
580
+ state: IdeaState
581
+ depth: int
582
+ parent: Optional["IdeaNode"]
583
+ transformation: OperatorApplication
584
+ children: List["IdeaNode"] = field(default_factory=list)
585
+ visits: int = 0
586
+ value_sum: float = 0.0
587
+ expanded: bool = False
588
+ evaluation: Optional[IdeaEvaluation] = None
589
+ latest_path_summary: str = ""
590
+
591
+ def uct_value(self, parent_visits: int, exploration_constant: float) -> float:
592
+ if self.visits == 0:
593
+ return float("inf")
594
+ exploit = self.value_sum / self.visits
595
+ explore = exploration_constant * math.sqrt(math.log(max(1, parent_visits)) / self.visits)
596
+ return exploit + explore
597
+
598
+ def path(self) -> List["IdeaNode"]:
599
+ chain: List[IdeaNode] = []
600
+ node: Optional[IdeaNode] = self
601
+ while node:
602
+ chain.append(node)
603
+ node = node.parent
604
+ return list(reversed(chain))
605
+
606
+ def path_summary(self) -> str:
607
+ if self.latest_path_summary:
608
+ return self.latest_path_summary
609
+ steps: List[str] = []
610
+ for hop in self.path():
611
+ steps.append(
612
+ f"{hop.state.title} [{hop.transformation.operator}] -> defects {hop.transformation.defects}"
613
+ )
614
+ return " | ".join(steps)
615
+
616
+
617
+ @dataclass
618
+ class MCTSConfig:
619
+ max_iterations: int = _mcts_default("exploration_budget", _mcts_default("max_iterations", 128))
620
+ max_depth: int = _mcts_default("max_depth", 4)
621
+ branching_factor: int = _mcts_default("branching_factor", 3)
622
+ exploration_constant: float = _mcts_default("exploration_constant", 1.15)
623
+ idea_taste_mode: Optional[str] = _mcts_default("idea_taste_mode", None)
624
+ seed_id: Optional[str] = None
625
+ route_id: Optional[str] = None
626
+ prompt_mode: str = _mcts_default("prompt_mode", "default")
627
+ generation_model: str = _mcts_default("generation_model", "gpt-5-mini")
628
+ evaluation_model: str = _mcts_default("evaluation_model", "gpt-5.2")
629
+ generation_temperature: float = _mcts_default("generation_temperature", 0.2)
630
+ evaluation_temperature: float = _mcts_default("evaluation_temperature", 0.01)
631
+ generation_max_tokens: int = _mcts_default("generation_max_tokens", 8192)
632
+ evaluation_max_tokens: int = _mcts_default("evaluation_max_tokens", 8192)
633
+ component_novelty_model_path: str = _mcts_default(
634
+ "component_novelty_model_path", "models/bge-m3"
635
+ )
636
+ component_novelty_index_dir: Optional[str] = _mcts_default(
637
+ "component_novelty_index_dir", None
638
+ )
639
+ component_novelty_retrieval_top_k: int = _mcts_default(
640
+ "component_novelty_retrieval_top_k", 50
641
+ )
642
+ component_novelty_evidence_top_k: int = _mcts_default(
643
+ "component_novelty_evidence_top_k", 5
644
+ )
645
+ component_novelty_eval_model: Optional[str] = _mcts_default(
646
+ "component_novelty_eval_model", None
647
+ )
648
+ component_novelty_eval_temperature: float = _mcts_default(
649
+ "component_novelty_eval_temperature",
650
+ _mcts_default("evaluation_temperature", 0.01),
651
+ )
652
+ component_novelty_eval_max_tokens: int = _mcts_default(
653
+ "component_novelty_eval_max_tokens", 4096
654
+ )
655
+ mechanism_commit_retrieval_top_k: int = _mcts_default(
656
+ "mechanism_commit_retrieval_top_k", 3
657
+ )
658
+ mechanism_commit_similarity_threshold: float = _mcts_default(
659
+ "mechanism_commit_similarity_threshold", 0.6
660
+ )
661
+ theory_transfer_retrieval_top_k: int = _mcts_default(
662
+ "theory_transfer_retrieval_top_k", 3
663
+ )
664
+ theory_transfer_similarity_threshold: float = _mcts_default(
665
+ "theory_transfer_similarity_threshold", 0.4
666
+ )
667
+ enable_vector_memory: bool = _mcts_default("enable_vector_memory", True)
668
+ enable_symbolic_memory: bool = _mcts_default("enable_symbolic_memory", True)
669
+ min_confidence_for_memory: float = _mcts_default("min_confidence_for_memory", 0.6)
670
+ pareto_top_k: int = _mcts_default("pareto_top_k", 5)
671
+ alignment_weight: float = _mcts_default("alignment_weight", 0.15)
672
+ complexity_weight: float = _mcts_default("complexity_weight", 0.05)
673
+ novelty_weight: float = _mcts_default("novelty_weight", 0.20)
674
+ surprise_weight: float = _mcts_default("surprise_weight", 0.10)
675
+ impact_weight: float = _mcts_default("impact_weight", 0.18)
676
+ feasibility_weight: float = _mcts_default("feasibility_weight", 0.11)
677
+ clarity_weight: float = _mcts_default("clarity_weight", 0.06)
678
+ conciseness_weight: float = _mcts_default("conciseness_weight", 0.03)
679
+ risk_weight: float = _mcts_default("risk_weight", 0.09)
680
+ protocol_weight: float = _mcts_default("protocol_weight", 0.03)
681
+ explanatory_power_weight: float = _mcts_default("explanatory_power_weight", 0.06)
682
+ identifiability_weight: float = _mcts_default("identifiability_weight", 0.06)
683
+ boundary_calibration_weight: float = _mcts_default("boundary_calibration_weight", 0.04)
684
+ claim_overreach_weight: float = _mcts_default("claim_overreach_weight", 0.10)
685
+
686
+ symbolic_memory_path: str = _mcts_default(
687
+ "symbolic_memory_path",
688
+ _mcts_default("skill_prior_memory_path", "output/idea_skill_priors"),
689
+ )
690
+ skill_prior_success_threshold: float = _mcts_default(
691
+ "skill_prior_success_threshold", 0.6
692
+ )
693
+
694
+ def apply_idea_taste_preset(config: MCTSConfig) -> Optional[IdeaTastePreset]:
695
+ raw_mode = getattr(config, "idea_taste_mode", None)
696
+ preset = get_idea_taste_preset(raw_mode)
697
+ if preset is None:
698
+ apply_normalized_score_weights(config, SCORE_WEIGHT_FIELDS)
699
+ config.idea_taste_mode = None
700
+ return None
701
+
702
+ for field_name in SCORE_WEIGHT_FIELDS:
703
+ setattr(config, field_name, float(preset.weights[field_name]))
704
+ apply_normalized_score_weights(config, SCORE_WEIGHT_FIELDS)
705
+ config.idea_taste_mode = preset.mode
706
+ return preset
707
+
708
+
709
+ @dataclass
710
+ class SearchCandidate:
711
+ node: IdeaNode
712
+ evaluation: IdeaEvaluation
713
+
714
+ def to_dict(self) -> Dict[str, Any]:
715
+ return {
716
+ "idea": self.node.state.to_payload(),
717
+ "evaluation": self.evaluation.to_dict(),
718
+ "score": self.evaluation.composite,
719
+ "path": self.node.path_summary(),
720
+ }
721
+
722
+
723
+ @dataclass
724
+ class SearchResult:
725
+ best: Optional[SearchCandidate]
726
+ pareto: Dict[str, Optional[SearchCandidate]]
727
+ trace: List[Dict[str, Any]]
728
+ cache_size: int
729
+ experiences: List[Dict[str, Any]]
730
+ retrieved_core_titles: List[str]
731
+
732
+
733
+ class VectorMemoryAccessor:
734
+ def __init__(
735
+ self,
736
+ semantic_cfg: Optional[Dict[str, Any]] = None,
737
+ episodic_cfg: Optional[Dict[str, Any]] = None,
738
+ procedural_cfg: Optional[Dict[str, Any]] = None,
739
+ llm_name: str = "gpt-5-mini",
740
+ llm_backend: str = "openai",
741
+ llm_provider: Optional[str] = None,
742
+ logger: Optional[LoguruCompatLogger] = None,
743
+ ) -> None:
744
+ self.semantic_cfg = semantic_cfg or {}
745
+ self.episodic_cfg = episodic_cfg or {}
746
+ self.procedural_cfg = procedural_cfg or {}
747
+ self.llm_name = str(llm_name or "gpt-5-mini")
748
+ self.llm_backend = str(llm_backend or "openai")
749
+ self.llm_provider = str(llm_provider or "").strip() or None
750
+ self.logger = logger or module_logger
751
+ self._stores: Dict[str, Optional[FAISSMemorySystem]] = {
752
+ "semantic": None,
753
+ "episodic": None,
754
+ "procedural": None,
755
+ }
756
+ self._embedding_models: Dict[str, Any] = {}
757
+
758
+ def _get_embedding_model(self, cfg: Dict[str, Any]) -> Any:
759
+ model_path = str((cfg or {}).get("model_path") or ".cache/all-MiniLM-L6-v2").strip()
760
+ resolved_model_path = resolve_workspace_path(model_path)
761
+ cached_model = self._embedding_models.get(resolved_model_path)
762
+ if cached_model is not None:
763
+ return cached_model
764
+
765
+ from sentence_transformers import SentenceTransformer
766
+
767
+ embedding_model = SentenceTransformer(resolved_model_path)
768
+ self._embedding_models[resolved_model_path] = embedding_model
769
+ return embedding_model
770
+
771
+ def _get_store(self, memory_type: str) -> Optional[FAISSMemorySystem]:
772
+ if memory_type not in self._stores:
773
+ return None
774
+ if self._stores[memory_type] is None:
775
+ cfg = {
776
+ "semantic": self.semantic_cfg,
777
+ "episodic": self.episodic_cfg,
778
+ "procedural": self.procedural_cfg,
779
+ }.get(memory_type, {})
780
+ try:
781
+ embedding_model = self._get_embedding_model(cfg)
782
+ store_cfg = dict(cfg)
783
+ if self.llm_provider and "llm_provider" not in store_cfg:
784
+ store_cfg["llm_provider"] = self.llm_provider
785
+ self._stores[memory_type] = FAISSMemorySystem(
786
+ embedding_model=embedding_model,
787
+ memory_type=memory_type,
788
+ llm_name=self.llm_name,
789
+ llm_backend=self.llm_backend,
790
+ **store_cfg,
791
+ )
792
+ except Exception as exc:
793
+ log_message(
794
+ self.logger,
795
+ None,
796
+ "warning",
797
+ "⚠️ Unable to initialize %s memory store: %s",
798
+ memory_type,
799
+ exc,
800
+ )
801
+ self._stores[memory_type] = None
802
+ return self._stores[memory_type]
803
+
804
+ def _query_store(
805
+ self,
806
+ store: FAISSMemorySystem,
807
+ query: str,
808
+ limit: int,
809
+ prefix: str,
810
+ ) -> List[MemorySnippet]:
811
+ snippets: List[MemorySnippet] = []
812
+ try:
813
+ records_with_scores = store.query(
814
+ query_text=query,
815
+ method="embedding",
816
+ limit=limit,
817
+ agent_id="idea_agent",
818
+ threshold=0.4,
819
+ )
820
+ except Exception as exc:
821
+ log_message(
822
+ self.logger,
823
+ None,
824
+ "warning",
825
+ "⚠️ Memory query failed (%s): %s",
826
+ prefix,
827
+ exc,
828
+ )
829
+ return snippets
830
+
831
+ for idx, (_, record) in enumerate(records_with_scores, start=1):
832
+ if not record:
833
+ continue
834
+ identifier = f"{prefix}#{idx}"
835
+
836
+ if isinstance(record, SemanticRecord):
837
+ title = record.summary
838
+ detail = record.detail
839
+ elif isinstance(record, EpisodicRecord):
840
+ title = record.summary
841
+ detail = _safe_dump_str(record.detail)
842
+ else:
843
+ title = record.name
844
+ detail = record.description
845
+
846
+ snippets.append(
847
+ MemorySnippet(
848
+ identifier=identifier,
849
+ title=(title or ""),
850
+ detail=str(detail),
851
+ tags=list(getattr(record, "tags", []) or []),
852
+ )
853
+ )
854
+ return snippets
855
+
856
+ def retrieve_bundle(self, query: str, limit: int = 3) -> MemoryBundle:
857
+ bundle = MemoryBundle()
858
+ semantic = self._get_store("semantic")
859
+ episodic = self._get_store("episodic")
860
+ procedural = self._get_store("procedural")
861
+
862
+ if semantic:
863
+ bundle.field_knowledge = self._query_store(semantic, query, limit, prefix="Field")
864
+ if episodic:
865
+ bundle.anti_patterns = self._query_store(episodic, query, limit, prefix="Pattern")
866
+ if procedural:
867
+ bundle.fix_recipes = self._query_store(procedural, query, limit, prefix="Recipe")
868
+
869
+ return bundle
870
+
871
+ def persist_experience(self, experience: Dict[str, Any], max_workers: int = 20) -> None:
872
+ if not experience:
873
+ return
874
+
875
+ semantic = self._get_store("semantic")
876
+ episodic = self._get_store("episodic")
877
+ procedural = self._get_store("procedural")
878
+ if not semantic and not episodic and not procedural:
879
+ log_message(
880
+ self.logger,
881
+ None,
882
+ "info",
883
+ "ℹ️ Skipping persistence because semantic store is unavailable.",
884
+ )
885
+ return
886
+
887
+ slot_process = SlotProcess(
888
+ llm_name=self.llm_name,
889
+ llm_backend=self.llm_backend,
890
+ llm_provider=self.llm_provider,
891
+ )
892
+ try:
893
+ working_slots = slot_process.transfer_idea_agent_context_to_working_slots(experience)
894
+ log_message(
895
+ self.logger,
896
+ None,
897
+ "info",
898
+ "[MCTS] Transferred experience to working slots (count=%d)",
899
+ len(working_slots),
900
+ )
901
+ _multi_thread_run(
902
+ slot_process._multi_thread_filter_and_route_slot,
903
+ working_slots,
904
+ max_workers,
905
+ show_progress=False,
906
+ )
907
+ _multi_thread_run(
908
+ slot_process._multi_thread_transfer_slot_to_memory,
909
+ slot_process.routed_slot_container,
910
+ max_workers,
911
+ show_progress=False,
912
+ )
913
+ except Exception as exc:
914
+ log_message(
915
+ self.logger,
916
+ None,
917
+ "warning",
918
+ "⚠️ Failed to persist experience: %s",
919
+ exc,
920
+ )
921
+ return
922
+
923
+ semantic_records: List[SemanticRecord] = []
924
+ episodic_records: List[EpisodicRecord] = []
925
+ procedural_records: List[ProceduralRecord] = []
926
+ for memory in slot_process.memory_dict:
927
+ if memory["memory_type"] == "semantic" and semantic:
928
+ semantic_records.append(semantic.instantiate_sem_record(**memory["input"]))
929
+ elif memory["memory_type"] == "episodic" and episodic:
930
+ episodic_records.append(episodic.instantiate_epi_record(**memory["input"]))
931
+ elif memory["memory_type"] == "procedural" and procedural:
932
+ procedural_records.append(procedural.instantiate_proc_record(**memory["input"]))
933
+
934
+ if semantic and semantic_records:
935
+ try:
936
+ semantic.add(semantic_records, agent_id="idea_agent")
937
+ except Exception as exc:
938
+ log_message(self.logger, None, "warning", "⚠️ Failed to persist semantic records: %s", exc)
939
+ if episodic and episodic_records:
940
+ try:
941
+ episodic.add(episodic_records, agent_id="idea_agent")
942
+ except Exception as exc:
943
+ log_message(self.logger, None, "warning", "⚠️ Failed to persist episodic records: %s", exc)
944
+ if procedural and procedural_records:
945
+ try:
946
+ procedural.add(procedural_records, agent_id="idea_agent")
947
+ except Exception as exc:
948
+ log_message(self.logger, None, "warning", "⚠️ Failed to persist procedural records: %s", exc)
949
+
950
+
951
+ class MemoryGuidedMCTS:
952
+ def __init__(
953
+ self,
954
+ chat_fn: Callable[..., str],
955
+ evaluation_prompt: str,
956
+ config: Optional[MCTSConfig] = None,
957
+ memory_accessor: Optional[VectorMemoryAccessor] = None,
958
+ paper_graph_vector_store: Optional[PaperGraphComponentVectorStore] = None,
959
+ logger: Optional[LoguruCompatLogger] = None,
960
+ log_sink: Optional[Callable[[str, str], None]] = None,
961
+ ) -> None:
962
+ self.chat_fn = chat_fn
963
+ self.evaluation_prompt = evaluation_prompt
964
+
965
+ self.config = config or MCTSConfig()
966
+ self.logger = logger or module_logger
967
+ self.log_sink = log_sink
968
+ self.enable_vector_memory = bool(self.config.enable_vector_memory)
969
+ self.enable_symbolic_memory = bool(self.config.enable_symbolic_memory)
970
+ self.memory_accessor = memory_accessor or VectorMemoryAccessor(logger=self.logger)
971
+ component_model_path = resolve_workspace_path(
972
+ self.config.component_novelty_model_path
973
+ )
974
+ self.paper_graph_vector_store = paper_graph_vector_store or PaperGraphComponentVectorStore(
975
+ model_name_or_path=component_model_path,
976
+ index_dir=self.config.component_novelty_index_dir or None,
977
+ )
978
+ self.component_novelty_scorer = ComponentNoveltyScorer(
979
+ model_name_or_path=component_model_path,
980
+ index_dir=self.config.component_novelty_index_dir or None,
981
+ retrieval_top_k=self.config.component_novelty_retrieval_top_k,
982
+ evidence_node_top_k=self.config.component_novelty_evidence_top_k,
983
+ evaluation_model=(
984
+ self.config.component_novelty_eval_model or self.config.evaluation_model
985
+ ),
986
+ evaluation_temperature=self.config.component_novelty_eval_temperature,
987
+ evaluation_max_tokens=self.config.component_novelty_eval_max_tokens,
988
+ chat_fn=self.chat_fn,
989
+ logger=self.logger,
990
+ log_sink=self.log_sink,
991
+ vector_store=self.paper_graph_vector_store,
992
+ )
993
+
994
+ self.skill_catalog = SkillCatalog()
995
+ self.symbolic_memory = SymbolicMemorySystem()
996
+ self.symbolic_memory_path = Path(self.config.symbolic_memory_path)
997
+
998
+ self._id_iter = itertools.count()
999
+ self.signature_nodes: Dict[str, IdeaNode] = {}
1000
+ self.evaluation_cache: Dict[str, Dict[str, IdeaEvaluation]] = {}
1001
+ self.experience_cache: Set[str] = set()
1002
+ self.trace: List[Dict[str, Any]] = []
1003
+ self.retrieved_core_titles: List[str] = []
1004
+
1005
+ self.topic: str = ""
1006
+ self.seed_id: str = str(getattr(self.config, "seed_id", "") or "").strip()
1007
+ self.route_id: str = str(getattr(self.config, "route_id", "") or "").strip()
1008
+ self.route_policy: Dict[str, Any] = {}
1009
+ self.mature_idea_record: Dict[str, Any] = {}
1010
+ self.analysis_blob: str = ""
1011
+ self.paper_context: str = ""
1012
+ self.mature_idea: str = ""
1013
+ self.refinement_scope: str = ""
1014
+ self.scientific_intervention_profile: Dict[str, Any] = {}
1015
+ self.gap_hypothesis_seeds: List[Dict[str, Any]] = []
1016
+ self.gap_seed_status: Dict[str, Any] = {}
1017
+ self.gap_seed_context: str = ""
1018
+ self.multimodal_evidence_projection: Dict[str, Any] = {}
1019
+ self.idea_taste_preset: Optional[IdeaTastePreset] = get_idea_taste_preset(
1020
+ getattr(self.config, "idea_taste_mode", None)
1021
+ )
1022
+ self._mature_idea_components: List[str] = []
1023
+ self._mature_idea_component_explanations: Dict[str, str] = {}
1024
+ self._component_novelty_fallback_logged = False
1025
+ self._mechanism_commit_retrieval_unavailable = False
1026
+ self.persist_symbolic_memory = True
1027
+
1028
+ if self.enable_symbolic_memory:
1029
+ self.reload_symbolic_memory()
1030
+
1031
+ def reload_symbolic_memory(self) -> None:
1032
+ """Reload the symbolic memory store from disk.
1033
+
1034
+ The store is populated externally (e.g. from experiment ablation
1035
+ results or paper-graph conclusions) — not from within MCTS. Reloading
1036
+ clears the current in-memory store first so the active process always
1037
+ reflects the latest on-disk state.
1038
+ """
1039
+ if not self.enable_symbolic_memory:
1040
+ return
1041
+ self.symbolic_memory = SymbolicMemorySystem()
1042
+ try:
1043
+ if not self.symbolic_memory_path.exists():
1044
+ return
1045
+ self.symbolic_memory.load(str(self.symbolic_memory_path))
1046
+ except Exception as exc:
1047
+ log_message(
1048
+ self.logger,
1049
+ self.log_sink,
1050
+ "warning",
1051
+ "⚠️ Unable to load symbolic memory store: %s",
1052
+ exc,
1053
+ )
1054
+
1055
+ def _skill_prior_for_prompt(self, skill_name: str) -> Dict[str, Any]:
1056
+ prior = self.skill_catalog.priors.get(skill_name, SkillUsagePrior())
1057
+ return prior.to_dict()
1058
+
1059
+ def _compute_protocol_score(self, plan: Optional[Dict[str, Any]]) -> float:
1060
+ return compute_protocol_score_from_plan(plan)
1061
+
1062
+ def _memory_bundle_log_payload(self, bundle: MemoryBundle) -> Dict[str, Any]:
1063
+ return memory_bundle_log_payload(bundle)
1064
+
1065
+ def _simulate_log_payload(self, evaluation: IdeaEvaluation) -> Dict[str, Any]:
1066
+ return simulate_log_payload(evaluation)
1067
+
1068
+ def _score_component_novelty(self, state: IdeaState) -> Optional[float]:
1069
+ try:
1070
+ return self.component_novelty_scorer.score(state=state, topic=self.topic)
1071
+ except Exception as exc:
1072
+ if not self._component_novelty_fallback_logged:
1073
+ log_message(
1074
+ self.logger,
1075
+ self.log_sink,
1076
+ "warning",
1077
+ "⚠️ Component novelty scorer unavailable (%s); falling back to LLM novelty.",
1078
+ exc,
1079
+ )
1080
+ self._component_novelty_fallback_logged = True
1081
+ return None
1082
+
1083
+ def _classify_root_discipline_resolution(
1084
+ self,
1085
+ topic: str,
1086
+ root_state: IdeaState,
1087
+ ) -> Dict[str, Any]:
1088
+ root_text = "\n".join(
1089
+ [
1090
+ root_state.title,
1091
+ root_state.abstract,
1092
+ root_state.core_contribution,
1093
+ root_state.method,
1094
+ ]
1095
+ )
1096
+ fallback = resolve_discipline_taxonomy(topic, query=root_text)
1097
+ if fallback.get("status") == "out_of_scope":
1098
+ return fallback
1099
+ prompt = ROOT_DOMAIN_CLASSIFICATION_PROMPT.format(
1100
+ topic=topic or "Unknown topic",
1101
+ root_idea=root_state.to_prompt_view(heading="Root Idea"),
1102
+ discipline_catalog=format_taxonomy_catalog_for_prompt(),
1103
+ )
1104
+ try:
1105
+ response = self.chat_fn(
1106
+ prompt,
1107
+ model=self.config.generation_model,
1108
+ temperature=0.01,
1109
+ max_output_tokens=65536,
1110
+ )
1111
+ payload = parse_json_response(response)
1112
+ if isinstance(payload, list):
1113
+ payload = payload[0] if payload else {}
1114
+ if not isinstance(payload, dict):
1115
+ return fallback
1116
+ raw_domains = [payload.get("primary_discipline")]
1117
+ adjacent = payload.get("adjacent_disciplines")
1118
+ if isinstance(adjacent, list):
1119
+ raw_domains.extend(adjacent)
1120
+ legacy_domains = payload.get("domains")
1121
+ if isinstance(legacy_domains, list):
1122
+ raw_domains.extend(legacy_domains)
1123
+ normalized = _normalize_root_domains(raw_domains)
1124
+ allowed = set(fallback.get("discipline_ids") or [])
1125
+ allowed.update(fallback.get("adjacent_disciplines") or [])
1126
+ selected = [domain for domain in normalized if domain in allowed]
1127
+ if not selected:
1128
+ return fallback
1129
+ resolution = resolve_discipline_taxonomy(selected[0], query=root_text)
1130
+ if len(selected) > 1 and str(payload.get("status") or "").strip() == "ambiguous":
1131
+ resolution["status"] = "ambiguous"
1132
+ resolution["discipline_ids"] = selected[:2]
1133
+ resolution["adjacent_disciplines"] = [
1134
+ domain for domain in selected[1:] if domain != selected[0]
1135
+ ]
1136
+ resolution["llm_selection"] = {
1137
+ "status": str(payload.get("status") or "").strip(),
1138
+ "confidence": payload.get("confidence"),
1139
+ "matched_terms": payload.get("matched_terms")
1140
+ if isinstance(payload.get("matched_terms"), list)
1141
+ else [],
1142
+ }
1143
+ return resolution
1144
+ except Exception as exc:
1145
+ log_message(
1146
+ self.logger,
1147
+ self.log_sink,
1148
+ "warning",
1149
+ "⚠️ Root-domain classification failed; using heuristic fallback: %s",
1150
+ exc,
1151
+ )
1152
+ return fallback
1153
+
1154
+ def _classify_root_domains(self, topic: str, root_state: IdeaState) -> List[str]:
1155
+ resolution = self._classify_root_discipline_resolution(topic, root_state)
1156
+ return _normalize_root_domains(resolution.get("discipline_ids") or [])
1157
+
1158
+ def _build_theory_transfer_query(
1159
+ self,
1160
+ plan: EditPlan,
1161
+ parent_state: IdeaState,
1162
+ ) -> Optional[Dict[str, str]]:
1163
+ prompt = get_theory_transfer_query_prompt(self.config.prompt_mode).format(
1164
+ topic=self.topic or "Unknown topic",
1165
+ root_domains=_format_root_domains_for_prompt(parent_state.root_domains),
1166
+ refinement_scope=self.refinement_scope or "None",
1167
+ idea=parent_state.to_prompt_view(heading="Current Idea"),
1168
+ edit_plan=pretty_json(plan.to_dict()),
1169
+ )
1170
+ empty_payload = {"query": "", "needed_content": "", "expected_role": ""}
1171
+ try:
1172
+ response = self.chat_fn(
1173
+ prompt,
1174
+ model=self.config.generation_model,
1175
+ temperature=0.2,
1176
+ max_output_tokens=65536,
1177
+ )
1178
+ payload = parse_json_response(response)
1179
+ if isinstance(payload, list):
1180
+ payload = payload[0] if payload else {}
1181
+ normalized = normalize_theory_transfer_query_payload(
1182
+ payload,
1183
+ fallback=empty_payload,
1184
+ clip_limit=UNIFORM_CLIP_TEXT_LIMIT,
1185
+ )
1186
+ if not str(normalized.get("query") or "").strip():
1187
+ return None
1188
+ if self._query_payload_uses_forbidden_terms(normalized):
1189
+ return None
1190
+ return normalized
1191
+ except Exception as exc:
1192
+ log_message(
1193
+ self.logger,
1194
+ self.log_sink,
1195
+ "warning",
1196
+ "⚠️ Theory-transfer query generation failed; skipping paper-graph retrieval: %s",
1197
+ exc,
1198
+ )
1199
+ return None
1200
+
1201
+ def _build_mechanism_commit_query(
1202
+ self,
1203
+ plan: EditPlan,
1204
+ parent_state: IdeaState,
1205
+ ) -> Optional[Dict[str, str]]:
1206
+ prompt = get_mechanism_commit_query_prompt(self.config.prompt_mode).format(
1207
+ topic=self.topic or "Unknown topic",
1208
+ root_domains=_format_root_domains_for_prompt(parent_state.root_domains),
1209
+ refinement_scope=self.refinement_scope or "None",
1210
+ idea=parent_state.to_prompt_view(heading="Current Idea"),
1211
+ edit_plan=pretty_json(plan.to_dict()),
1212
+ )
1213
+ empty_payload = {"query": "", "mechanism_gap": "", "expected_role": ""}
1214
+ try:
1215
+ response = self.chat_fn(
1216
+ prompt,
1217
+ model=self.config.generation_model,
1218
+ temperature=0.2,
1219
+ max_output_tokens=65536,
1220
+ )
1221
+ payload = parse_json_response(response)
1222
+ if isinstance(payload, list):
1223
+ payload = payload[0] if payload else {}
1224
+ normalized = normalize_mechanism_commit_query_payload(
1225
+ payload,
1226
+ fallback=empty_payload,
1227
+ clip_limit=UNIFORM_CLIP_TEXT_LIMIT,
1228
+ )
1229
+ if not str(normalized.get("query") or "").strip():
1230
+ return None
1231
+ if self._query_payload_uses_forbidden_terms(normalized):
1232
+ return None
1233
+ return normalized
1234
+ except Exception as exc:
1235
+ log_message(
1236
+ self.logger,
1237
+ self.log_sink,
1238
+ "warning",
1239
+ "⚠️ Mechanism-commit query generation failed; skipping paper-graph retrieval: %s",
1240
+ exc,
1241
+ )
1242
+ return None
1243
+
1244
+ def _query_payload_uses_forbidden_terms(self, payload: Dict[str, str]) -> bool:
1245
+ return any(
1246
+ text_uses_forbidden_query_terms(str(payload.get(field_name) or ""))
1247
+ for field_name in ("query", "mechanism_gap", "needed_content", "expected_role")
1248
+ )
1249
+
1250
+ def _record_retrieved_core_titles(self, hits: List[Dict[str, Any]]) -> None:
1251
+ seen = {str(title).strip().lower() for title in self.retrieved_core_titles if str(title).strip()}
1252
+ for hit in hits:
1253
+ core_node = hit.get("core_node") if isinstance(hit.get("core_node"), dict) else {}
1254
+ title = str(
1255
+ core_node.get("paper_title")
1256
+ or core_node.get("title")
1257
+ or core_node.get("full_name")
1258
+ or core_node.get("label")
1259
+ or hit.get("node_id")
1260
+ or ""
1261
+ ).strip()
1262
+ if not title:
1263
+ continue
1264
+ key = title.lower()
1265
+ if key in seen:
1266
+ continue
1267
+ seen.add(key)
1268
+ self.retrieved_core_titles.append(title)
1269
+
1270
+ def _retrieve_theory_transfer_references(
1271
+ self,
1272
+ query_payload: Dict[str, str],
1273
+ root_domains: List[str],
1274
+ ) -> List[Dict[str, Any]]:
1275
+ query = str(query_payload.get("query") or "").strip()
1276
+ if not query:
1277
+ return []
1278
+ try:
1279
+ search_pool = max(int(self.config.theory_transfer_retrieval_top_k) * 6, 12)
1280
+ hits = self.paper_graph_vector_store.search(
1281
+ query=query,
1282
+ top_k=search_pool,
1283
+ component_hits_per_core=2,
1284
+ )
1285
+ except Exception as exc:
1286
+ log_message(
1287
+ self.logger,
1288
+ self.log_sink,
1289
+ "warning",
1290
+ "⚠️ Theory-transfer paper-graph retrieval unavailable: %s",
1291
+ exc,
1292
+ )
1293
+ return []
1294
+
1295
+ threshold = float(self.config.theory_transfer_similarity_threshold)
1296
+ blocked_domains = set(_normalize_root_domains(root_domains))
1297
+ filtered: List[Dict[str, Any]] = []
1298
+ for hit in hits:
1299
+ core_node = hit.get("core_node") if isinstance(hit.get("core_node"), dict) else {}
1300
+ paper_domain = str(core_node.get("paper_domain") or "").strip()
1301
+ paper_domain_key = canonicalize_discipline_key(paper_domain)
1302
+ if not paper_domain or (
1303
+ paper_domain_key and paper_domain_key in blocked_domains
1304
+ ):
1305
+ continue
1306
+ if float(hit.get("score") or 0.0) < threshold:
1307
+ continue
1308
+ filtered.append(hit)
1309
+ if len(filtered) >= int(self.config.theory_transfer_retrieval_top_k):
1310
+ break
1311
+ if filtered:
1312
+ rendered_hits: List[str] = []
1313
+ for idx, hit in enumerate(filtered, start=1):
1314
+ core_node = hit.get("core_node") if isinstance(hit.get("core_node"), dict) else {}
1315
+ paper_domain = str(core_node.get("paper_domain") or "").strip() or "unknown"
1316
+ label = (
1317
+ str(core_node.get("full_name") or "").strip()
1318
+ or str(core_node.get("label") or "").strip()
1319
+ or str(core_node.get("node_id") or "").strip()
1320
+ or f"core_{idx}"
1321
+ )
1322
+ paper_title = clip_text(str(core_node.get("paper_title") or "").strip(), 400)
1323
+ summary = clip_text(str(core_node.get("summary") or "").strip(), 1000)
1324
+ insight = clip_text(str(core_node.get("insight") or "").strip(), 1000)
1325
+ rendered_hits.append(
1326
+ f"{idx}. {label} | domain={paper_domain} | score={float(hit.get('score') or 0.0):.3f}"
1327
+ )
1328
+ if paper_title:
1329
+ rendered_hits.append(f" title: {paper_title}")
1330
+ if summary:
1331
+ rendered_hits.append(f" summary: {summary}")
1332
+ if insight:
1333
+ rendered_hits.append(f" insight: {insight}")
1334
+ log_message(
1335
+ self.logger,
1336
+ self.log_sink,
1337
+ "info",
1338
+ "[MCTS] Theory-transfer eligible core nodes for query=%s\n%s",
1339
+ clip_text(query, 1000),
1340
+ "\n".join(rendered_hits),
1341
+ )
1342
+ self._record_retrieved_core_titles(filtered)
1343
+ return filtered
1344
+
1345
+ def _retrieve_mechanism_commit_references(
1346
+ self,
1347
+ query_payload: Dict[str, str],
1348
+ ) -> List[Dict[str, Any]]:
1349
+ query = str(query_payload.get("query") or "").strip()
1350
+ if not query:
1351
+ return []
1352
+ if getattr(self, "_mechanism_commit_retrieval_unavailable", False):
1353
+ return []
1354
+ try:
1355
+ search_pool = max(int(self.config.mechanism_commit_retrieval_top_k) * 6, 12)
1356
+ hits = self.paper_graph_vector_store.search(
1357
+ query=query,
1358
+ top_k=search_pool,
1359
+ component_hits_per_core=2,
1360
+ )
1361
+ except Exception as exc:
1362
+ log_message(
1363
+ self.logger,
1364
+ self.log_sink,
1365
+ "warning",
1366
+ "⚠️ Mechanism-commit paper-graph retrieval unavailable (%s); "
1367
+ "continuing without paper-graph grounding.",
1368
+ exc,
1369
+ )
1370
+ self._mechanism_commit_retrieval_unavailable = True
1371
+ return []
1372
+ threshold = float(self.config.mechanism_commit_similarity_threshold)
1373
+ filtered: List[Dict[str, Any]] = []
1374
+ for hit in hits:
1375
+ if float(hit.get("score") or 0.0) < threshold:
1376
+ continue
1377
+ filtered.append(hit)
1378
+ if len(filtered) >= int(self.config.mechanism_commit_retrieval_top_k):
1379
+ break
1380
+ if filtered:
1381
+ rendered_hits: List[str] = []
1382
+ for idx, hit in enumerate(filtered, start=1):
1383
+ core_node = hit.get("core_node") if isinstance(hit.get("core_node"), dict) else {}
1384
+ paper_domain = str(core_node.get("paper_domain") or "").strip() or "unknown"
1385
+ label = (
1386
+ str(core_node.get("full_name") or "").strip()
1387
+ or str(core_node.get("label") or "").strip()
1388
+ or str(core_node.get("node_id") or "").strip()
1389
+ or f"core_{idx}"
1390
+ )
1391
+ paper_title = clip_text(str(core_node.get("paper_title") or "").strip(), 400)
1392
+ summary = clip_text(str(core_node.get("summary") or "").strip(), 1000)
1393
+ insight = clip_text(str(core_node.get("insight") or "").strip(), 1000)
1394
+ rendered_hits.append(
1395
+ f"{idx}. {label} | domain={paper_domain} | score={float(hit.get('score') or 0.0):.3f}"
1396
+ )
1397
+ if paper_title:
1398
+ rendered_hits.append(f" title: {paper_title}")
1399
+ if summary:
1400
+ rendered_hits.append(f" summary: {summary}")
1401
+ if insight:
1402
+ rendered_hits.append(f" insight: {insight}")
1403
+ log_message(
1404
+ self.logger,
1405
+ self.log_sink,
1406
+ "info",
1407
+ "[MCTS] Mechanism-commit grounding core nodes for query=%s\n%s",
1408
+ clip_text(query, 1000),
1409
+ "\n".join(rendered_hits),
1410
+ )
1411
+ self._record_retrieved_core_titles(filtered)
1412
+ return filtered
1413
+
1414
+ def _materialize_child_state(
1415
+ self,
1416
+ parent_state: IdeaState,
1417
+ plan: EditPlan,
1418
+ instantiated: Optional[Dict[str, Any]] = None,
1419
+ selection_metadata: Optional[Dict[str, Any]] = None,
1420
+ ) -> IdeaState:
1421
+ return materialize_child_state(
1422
+ self,
1423
+ parent_state,
1424
+ plan,
1425
+ instantiated,
1426
+ selection_metadata=selection_metadata,
1427
+ idea_state_cls=IdeaState,
1428
+ )
1429
+
1430
+ def _select(self, node: IdeaNode) -> Tuple[IdeaNode, List[IdeaNode]]:
1431
+ return select_leaf_for_rollout(self, node)
1432
+
1433
+ def _instantiate_skill_plan(
1434
+ self,
1435
+ plan: EditPlan,
1436
+ parent_state: IdeaState,
1437
+ bundle: MemoryBundle,
1438
+ ) -> Optional[Dict[str, Any]]:
1439
+ root_domains_text = _format_root_domains_for_prompt(parent_state.root_domains)
1440
+ additional_retrieval_context = ""
1441
+ skip_payload: Optional[Dict[str, Any]] = None
1442
+
1443
+ if plan.skill_name == "mechanism-commit-innovation":
1444
+ query_payload = self._build_mechanism_commit_query(plan, parent_state)
1445
+ if query_payload is not None:
1446
+ hits = self._retrieve_mechanism_commit_references(query_payload)
1447
+ if hits:
1448
+ additional_retrieval_context = "\n".join(
1449
+ [
1450
+ "Mechanism-grounding retrieval query:",
1451
+ query_payload.get("query") or "None",
1452
+ format_mechanism_commit_references(
1453
+ query_payload,
1454
+ hits,
1455
+ clip_limit=UNIFORM_CLIP_TEXT_LIMIT,
1456
+ ),
1457
+ ]
1458
+ )
1459
+ elif plan.skill_name == "theory-transfer-injection":
1460
+ query_payload = self._build_theory_transfer_query(plan, parent_state)
1461
+ if query_payload is not None:
1462
+ hits = self._retrieve_theory_transfer_references(query_payload, parent_state.root_domains)
1463
+ if not hits:
1464
+ skip_payload = {
1465
+ "_skip_child_creation": True,
1466
+ "_skip_reason": (
1467
+ "theory-transfer-injection retrieved no eligible cross-domain core nodes "
1468
+ f"(threshold={self.config.theory_transfer_similarity_threshold}, "
1469
+ f"excluded_domains={parent_state.root_domains})"
1470
+ ),
1471
+ }
1472
+ else:
1473
+ additional_retrieval_context = "\n".join(
1474
+ [
1475
+ "Cross-domain transfer query:",
1476
+ query_payload.get("query") or "None",
1477
+ format_theory_transfer_references(
1478
+ query_payload,
1479
+ hits,
1480
+ clip_limit=UNIFORM_CLIP_TEXT_LIMIT,
1481
+ ),
1482
+ ]
1483
+ )
1484
+
1485
+ if skip_payload is not None:
1486
+ return skip_payload
1487
+
1488
+ payload = instantiate_skill_plan_for_node(
1489
+ self,
1490
+ plan,
1491
+ parent_state,
1492
+ bundle,
1493
+ prompt_template=get_skill_instantiation_prompt(
1494
+ self.config.prompt_mode,
1495
+ profile_id=str(
1496
+ (getattr(parent_state, "scientific_intervention", {}) or {}).get("profile_id")
1497
+ or "generic_scientific"
1498
+ ),
1499
+ ),
1500
+ root_domains_text=root_domains_text,
1501
+ additional_retrieval_context=additional_retrieval_context,
1502
+ )
1503
+ if plan.skill_name == "theory-transfer-injection":
1504
+ if not isinstance(payload, dict):
1505
+ return {
1506
+ "_skip_child_creation": True,
1507
+ "_skip_reason": "theory-transfer-injection instantiation returned no structured payload",
1508
+ }
1509
+ payload.pop("_paper_graph_context", None)
1510
+ payload["_paper_graph_context"] = additional_retrieval_context
1511
+ if (
1512
+ plan.skill_name == "mechanism-commit-innovation"
1513
+ and isinstance(payload, dict)
1514
+ and not parent_centers_control_surface(parent_state)
1515
+ ):
1516
+ rendered = "\n".join(
1517
+ str(payload.get(field_name) or "")
1518
+ for field_name in ("title", "abstract", "core_contribution", "method", "rationale")
1519
+ )
1520
+ if text_uses_forbidden_query_terms(rendered):
1521
+ return {
1522
+ "_skip_child_creation": True,
1523
+ "_skip_reason": (
1524
+ "mechanism-commit-innovation instantiated a threshold/gating/suppression/quota-style "
1525
+ "patch for a parent idea that is not control-centered"
1526
+ ),
1527
+ }
1528
+ if plan.skill_name == "mechanism-commit-innovation" and isinstance(payload, dict):
1529
+ payload.pop("_paper_graph_context", None)
1530
+ payload["_paper_graph_context"] = additional_retrieval_context
1531
+ return payload
1532
+
1533
+ def _expand(self, node: IdeaNode, path: List[IdeaNode]) -> Tuple[Optional[IdeaNode], List[IdeaNode]]:
1534
+ return expand_node_with_skills(
1535
+ self,
1536
+ node,
1537
+ path,
1538
+ min_components=MIN_COMPONENTS,
1539
+ max_components=MAX_COMPONENTS,
1540
+ )
1541
+
1542
+
1543
+ def _build_symbolic_eval_hints(self, node: IdeaNode) -> str:
1544
+ return build_symbolic_eval_hints(self, node)
1545
+
1546
+ def _simulate(
1547
+ self,
1548
+ node: IdeaNode,
1549
+ path: List[IdeaNode],
1550
+ experiences: List[Dict[str, Any]],
1551
+ ) -> Optional[IdeaEvaluation]:
1552
+ return simulate_node_value(
1553
+ self,
1554
+ node,
1555
+ path,
1556
+ experiences,
1557
+ idea_evaluation_cls=IdeaEvaluation,
1558
+ )
1559
+
1560
+ def _update_skill_prior(self, node: IdeaNode, evaluation: IdeaEvaluation) -> None:
1561
+ update_skill_prior_from_evaluation(self, node, evaluation)
1562
+
1563
+ def _backpropagate(self, path: List[IdeaNode], evaluation: IdeaEvaluation) -> None:
1564
+ backpropagate_rollout(path, evaluation)
1565
+
1566
+ def _extract_mature_idea_components(
1567
+ self,
1568
+ mature_idea: str,
1569
+ topic: str,
1570
+ *,
1571
+ prior_components: Optional[List[str]] = None,
1572
+ prior_component_explanations: Optional[Dict[str, str]] = None,
1573
+ component_decisions: Optional[List[Dict[str, Any]]] = None,
1574
+ ) -> Tuple[List[str], Dict[str, str]]:
1575
+ return extract_mature_idea_components_via_llm(
1576
+ self,
1577
+ mature_idea,
1578
+ topic,
1579
+ prompt_template=render_component_extraction_prompt(
1580
+ str(self.scientific_intervention_profile.get("profile_id") or "")
1581
+ ),
1582
+ max_components=MAX_COMPONENTS,
1583
+ prior_components=prior_components,
1584
+ prior_component_explanations=prior_component_explanations,
1585
+ component_decisions=component_decisions,
1586
+ )
1587
+
1588
+ def _resolve_prior_component_context(
1589
+ self,
1590
+ prepared: Dict[str, Any],
1591
+ ) -> Tuple[List[str], Dict[str, str], List[Dict[str, Any]]]:
1592
+ prior_components = (
1593
+ [str(component).strip() for component in prepared.get("prior_components", []) if str(component).strip()]
1594
+ if isinstance(prepared.get("prior_components"), list)
1595
+ else []
1596
+ )
1597
+ prior_component_explanations = (
1598
+ prepared.get("prior_component_explanations")
1599
+ if isinstance(prepared.get("prior_component_explanations"), (dict, list))
1600
+ else {}
1601
+ )
1602
+ component_decisions = (
1603
+ [decision for decision in prepared.get("component_decisions", []) if isinstance(decision, dict)]
1604
+ if isinstance(prepared.get("component_decisions"), list)
1605
+ else []
1606
+ )
1607
+ if not prior_components:
1608
+ for entry_key in ("latest_candidate", "root_idea"):
1609
+ entry = prepared.get(entry_key)
1610
+ if not isinstance(entry, dict):
1611
+ continue
1612
+ raw_components = entry.get("components")
1613
+ if not isinstance(raw_components, list):
1614
+ continue
1615
+ prior_components = [
1616
+ str(component).strip()
1617
+ for component in raw_components
1618
+ if str(component).strip()
1619
+ ]
1620
+ if prior_components:
1621
+ if not prior_component_explanations:
1622
+ raw_explanations = entry.get("component_explanations")
1623
+ if isinstance(raw_explanations, (dict, list)):
1624
+ prior_component_explanations = raw_explanations
1625
+ break
1626
+ normalized_prior_explanations = normalize_component_explanations(
1627
+ prior_components,
1628
+ prior_component_explanations,
1629
+ )
1630
+ return prior_components, normalized_prior_explanations, component_decisions
1631
+
1632
+ def prepare_root_context(self, topic: str, context: Dict[str, Any]) -> Dict[str, Any]:
1633
+ prepared = dict(context or {})
1634
+ self.multimodal_evidence_projection = (
1635
+ dict(prepared.get("multimodal_evidence_projection"))
1636
+ if isinstance(prepared.get("multimodal_evidence_projection"), dict)
1637
+ else {}
1638
+ )
1639
+ survey_handoff = prepared.get("survey_idea_handoff")
1640
+ self.survey_idea_handoff = (
1641
+ dict(survey_handoff) if isinstance(survey_handoff, dict) else {}
1642
+ )
1643
+ gap_triage = (
1644
+ dict(self.survey_idea_handoff.get("gap_triage") or {})
1645
+ if isinstance(self.survey_idea_handoff.get("gap_triage"), dict)
1646
+ else {}
1647
+ )
1648
+ triage_rows = [
1649
+ row for row in (gap_triage.get("gaps", []) if gap_triage else [])
1650
+ if isinstance(row, dict) and str(row.get("gap_id") or "").strip()
1651
+ ]
1652
+ triage_by_id = {str(row["gap_id"]).strip(): row for row in triage_rows}
1653
+ active_gap_ids = {
1654
+ gap_id for gap_id, row in triage_by_id.items()
1655
+ if str(row.get("eligibility_route") or "") in {
1656
+ "core_hypothesis", "provisional_hypothesis", "exploratory_frontier"
1657
+ }
1658
+ }
1659
+ prepared["gap_triage"] = gap_triage
1660
+ prepared["active_gaps"] = [
1661
+ row for row in triage_rows if str(row.get("gap_id")) in active_gap_ids
1662
+ ]
1663
+ prepared["provisional_gaps"] = [
1664
+ row for row in triage_rows
1665
+ if str(row.get("eligibility_route") or "") in {"provisional_hypothesis", "exploratory_frontier"}
1666
+ ]
1667
+ prepared["supporting_constraints"] = [
1668
+ row for row in triage_rows if str(row.get("eligibility_route") or "") == "supporting_constraint"
1669
+ ]
1670
+ prepared["verification_only_gaps"] = [
1671
+ row for row in triage_rows if str(row.get("eligibility_route") or "") == "verification_only"
1672
+ ]
1673
+ prepared["future_work_seeds"] = [
1674
+ row for row in triage_rows if str(row.get("eligibility_route") or "") == "future_work_seed"
1675
+ ]
1676
+ prepared["excluded_gaps"] = [
1677
+ row for row in triage_rows if str(row.get("eligibility_route") or "") == "exclude"
1678
+ ]
1679
+ handoff_gap_by_id = {
1680
+ str(gap.get("gap_id") or "").strip(): gap
1681
+ for gap in (self.survey_idea_handoff.get("gaps", []) if self.survey_idea_handoff else [])
1682
+ if isinstance(gap, dict) and str(gap.get("gap_id") or "").strip()
1683
+ }
1684
+ routing_labels = (
1685
+ ("active_gaps", "Core or active hypothesis gaps"),
1686
+ ("provisional_gaps", "Provisional exploratory gaps"),
1687
+ ("supporting_constraints", "Supporting scientific constraints"),
1688
+ ("verification_only_gaps", "Verification-only gaps"),
1689
+ ("future_work_seeds", "Future-work hypothesis seeds"),
1690
+ )
1691
+ routing_lines = ["== Survey Gap Routing (authoritative) =="]
1692
+ for key, label in routing_labels:
1693
+ rows = prepared.get(key, [])
1694
+ statements = [
1695
+ str(handoff_gap_by_id.get(str(row.get("gap_id") or ""), {}).get("statement") or "").strip()
1696
+ for row in rows[:5]
1697
+ if isinstance(row, dict)
1698
+ ]
1699
+ if statements:
1700
+ routing_lines.append(f"{label}: " + " | ".join(statement for statement in statements if statement))
1701
+ routing_lines.append("Do not turn verification-only or supporting constraints into primary novelty. Future-work items are exploratory seeds, not established evidence.")
1702
+ prepared["gap_routing_context"] = "\n".join(routing_lines)
1703
+ handoff_profile_resolution = (
1704
+ dict(self.survey_idea_handoff.get("profile_resolution") or {})
1705
+ if self.survey_idea_handoff
1706
+ and isinstance(self.survey_idea_handoff.get("profile_resolution"), dict)
1707
+ else {}
1708
+ )
1709
+ profile_hint = str(handoff_profile_resolution.get("profile_id_hint") or "").strip()
1710
+ if profile_hint:
1711
+ handoff_profile_resolution["profile_id"] = profile_hint
1712
+ handoff_defect_tags = []
1713
+ raw_handoff_gaps = (
1714
+ self.survey_idea_handoff.get("gaps", [])
1715
+ if self.survey_idea_handoff
1716
+ else []
1717
+ )
1718
+ for raw_gap in raw_handoff_gaps:
1719
+ if not isinstance(raw_gap, dict):
1720
+ continue
1721
+ gap_id = str(raw_gap.get("gap_id") or "").strip()
1722
+ route = str(triage_by_id.get(gap_id, {}).get("eligibility_route") or "")
1723
+ if route in {"supporting_constraint", "verification_only", "exclude"}:
1724
+ continue
1725
+ raw_tags = raw_gap.get("candidate_defect_tags", [])
1726
+ values = raw_tags if isinstance(raw_tags, list) else [raw_tags]
1727
+ handoff_defect_tags.extend(str(tag).strip() for tag in values if str(tag).strip())
1728
+ if handoff_defect_tags:
1729
+ existing_tags = prepared.get("defect_tags", [])
1730
+ existing_values = existing_tags if isinstance(existing_tags, list) else [existing_tags]
1731
+ prepared["defect_tags"] = _dedupe_keep_order_strings(
1732
+ [*existing_values, *handoff_defect_tags]
1733
+ )
1734
+ elif raw_handoff_gaps and not gap_triage and not prepared.get("active_gaps"):
1735
+ # Fail-open floor: a deterministic open gap must remain available
1736
+ # even when triage cannot establish a stronger route.
1737
+ fallback_gap = next(
1738
+ (gap for gap in raw_handoff_gaps if isinstance(gap, dict)), None
1739
+ )
1740
+ if fallback_gap:
1741
+ prepared["defect_tags"] = _dedupe_keep_order_strings(
1742
+ [
1743
+ *([str(value) for value in (prepared.get("defect_tags") or [])] if isinstance(prepared.get("defect_tags"), list) else []),
1744
+ *([str(value) for value in (fallback_gap.get("candidate_defect_tags") or [])] if isinstance(fallback_gap.get("candidate_defect_tags"), list) else []),
1745
+ str(fallback_gap.get("gap_kind") or "unexplored_gap"),
1746
+ ]
1747
+ )
1748
+ base_paper_context = prepared.get("paper_context") or "No curated papers available yet."
1749
+ self.paper_context = str(base_paper_context) + "\n\n" + prepared["gap_routing_context"]
1750
+ prepared["paper_context"] = self.paper_context
1751
+ prepared["refinement_scope"] = str(prepared.get("refinement_scope") or "").strip()
1752
+ project_context = (
1753
+ prepared.get("project_context")
1754
+ if isinstance(prepared.get("project_context"), dict)
1755
+ else None
1756
+ )
1757
+ project_resolution = normalize_project_context_discipline_resolution(project_context)
1758
+ if project_resolution:
1759
+ prepared["project_context_discipline_resolution"] = project_resolution
1760
+ early_root_domains = _normalize_root_domains(prepared.get("root_domains") or [])
1761
+ explicit_handoff_fields = handoff_profile_resolution.get("openalex_field_ids") or handoff_profile_resolution.get(
1762
+ "paperseek_field_ids"
1763
+ )
1764
+ early_resolution = (
1765
+ handoff_profile_resolution
1766
+ if handoff_profile_resolution and explicit_handoff_fields
1767
+ else prepared.get("discipline_resolution")
1768
+ )
1769
+ early_profile = resolve_scientific_intervention_profile(
1770
+ early_resolution if isinstance(early_resolution, dict) else {},
1771
+ root_domains=early_root_domains,
1772
+ project_context=project_context,
1773
+ )
1774
+ if early_profile is not None:
1775
+ self.scientific_intervention_profile = early_profile.to_payload()
1776
+ mature_record = prepared.get("mature_idea_record")
1777
+ if isinstance(mature_record, dict):
1778
+ self.mature_idea_record = normalize_mature_idea(mature_record)
1779
+ prepared["mature_idea_record"] = dict(self.mature_idea_record)
1780
+ else:
1781
+ self.mature_idea_record = {}
1782
+ mature_idea_value = prepared.get("mature_idea")
1783
+ if isinstance(mature_idea_value, dict):
1784
+ self.mature_idea_record = normalize_mature_idea(mature_idea_value)
1785
+ prepared["mature_idea_record"] = dict(self.mature_idea_record)
1786
+ mature_idea = str(
1787
+ self.mature_idea_record.get("hypothesis")
1788
+ or self.mature_idea_record.get("abstract")
1789
+ or self.mature_idea_record.get("title")
1790
+ or ""
1791
+ ).strip()
1792
+ else:
1793
+ mature_idea = str(mature_idea_value or "").strip()
1794
+ if mature_idea:
1795
+ prepared["mature_idea"] = mature_idea
1796
+ components = prepared.get("components")
1797
+ prior_components, prior_component_explanations, component_decisions = (
1798
+ self._resolve_prior_component_context(prepared)
1799
+ )
1800
+ if mature_idea and not isinstance(components, list):
1801
+ components, explanations = self._extract_mature_idea_components(
1802
+ mature_idea,
1803
+ topic,
1804
+ prior_components=prior_components,
1805
+ prior_component_explanations=prior_component_explanations,
1806
+ component_decisions=component_decisions,
1807
+ )
1808
+ if components:
1809
+ prepared["components"] = components
1810
+ prepared["component_explanations"] = explanations
1811
+
1812
+ handoff_root_domains = _normalize_root_domains(
1813
+ handoff_profile_resolution.get("discipline_ids") or []
1814
+ )
1815
+ project_root_domains = _normalize_root_domains(
1816
+ project_resolution.get("discipline_ids") or []
1817
+ )
1818
+ handoff_has_explicit_scope = bool(explicit_handoff_fields or handoff_root_domains)
1819
+ project_has_explicit_scope = bool(
1820
+ project_resolution.get("paperseek_openalex_field_ids")
1821
+ or project_root_domains
1822
+ )
1823
+ root_domains = handoff_root_domains or project_root_domains or _normalize_root_domains(
1824
+ prepared.get("root_domains") or []
1825
+ )
1826
+ if handoff_has_explicit_scope:
1827
+ resolution = dict(handoff_profile_resolution)
1828
+ resolution["discipline_ids"] = root_domains
1829
+ if root_domains:
1830
+ resolution["primary_discipline"] = root_domains[0]
1831
+ elif project_has_explicit_scope:
1832
+ resolution = dict(project_resolution)
1833
+ resolution["discipline_ids"] = project_root_domains
1834
+ if project_root_domains:
1835
+ resolution["primary_discipline"] = project_root_domains[0]
1836
+ elif not root_domains:
1837
+ draft_root_state = build_root_state(topic, prepared, IdeaState)
1838
+ resolution = self._classify_root_discipline_resolution(topic, draft_root_state)
1839
+ root_domains = _normalize_root_domains(resolution.get("discipline_ids") or [])
1840
+ else:
1841
+ resolution = resolve_discipline_taxonomy(
1842
+ root_domains[0],
1843
+ query="\n".join(
1844
+ [
1845
+ topic,
1846
+ str(prepared.get("mature_idea") or ""),
1847
+ str(prepared.get("paper_context") or ""),
1848
+ ]
1849
+ ),
1850
+ )
1851
+ resolution["discipline_ids"] = root_domains
1852
+ prepared["root_domains"] = root_domains
1853
+ prepared["discipline_resolution"] = resolution
1854
+ profile_resolution = dict(resolution)
1855
+ existing_intervention = prepared.get("scientific_intervention")
1856
+ if isinstance(existing_intervention, dict) and existing_intervention.get("profile_id"):
1857
+ profile_resolution.setdefault("profile_id", existing_intervention.get("profile_id"))
1858
+ profile = resolve_scientific_intervention_profile(
1859
+ profile_resolution,
1860
+ root_domains=root_domains,
1861
+ project_context=project_context,
1862
+ )
1863
+ prepared["scientific_intervention_profile"] = (
1864
+ profile.to_payload()
1865
+ if profile is not None
1866
+ else {"schema_version": "scientific_intervention_profile_v1", "status": "out_of_scope"}
1867
+ )
1868
+ self.scientific_intervention_profile = dict(prepared["scientific_intervention_profile"])
1869
+ gap_hypothesis_seeds = build_gap_hypothesis_seeds(
1870
+ self.survey_idea_handoff,
1871
+ gap_triage,
1872
+ profile_resolution=prepared["scientific_intervention_profile"],
1873
+ scope=prepared.get("scope") if isinstance(prepared.get("scope"), dict) else None,
1874
+ topic=topic,
1875
+ multimodal_evidence_projection=self.multimodal_evidence_projection,
1876
+ )
1877
+ gap_seed_status = build_gap_seed_status(gap_hypothesis_seeds)
1878
+ gap_seed_context = build_gap_seed_context(gap_hypothesis_seeds)
1879
+ prepared["gap_hypothesis_seeds"] = gap_hypothesis_seeds
1880
+ prepared["gap_seed_status"] = gap_seed_status
1881
+ prepared["gap_seed_context"] = gap_seed_context
1882
+ self.gap_hypothesis_seeds = gap_hypothesis_seeds
1883
+ self.gap_seed_status = gap_seed_status
1884
+ self.gap_seed_context = gap_seed_context
1885
+ return prepared
1886
+
1887
+ def search(self, topic: str, context: Dict[str, Any]) -> SearchResult:
1888
+ reset_search_state(self)
1889
+ context = self.prepare_root_context(topic, context)
1890
+ self.topic = topic
1891
+ self.analysis_blob = format_analysis_blob(context.get("analysis", []))
1892
+ self.paper_context = context.get("paper_context") or "No curated papers available yet."
1893
+ mature_value = context.get("mature_idea")
1894
+ if isinstance(mature_value, dict):
1895
+ self.mature_idea_record = normalize_mature_idea(mature_value)
1896
+ self.mature_idea = str(
1897
+ self.mature_idea_record.get("hypothesis")
1898
+ or self.mature_idea_record.get("abstract")
1899
+ or self.mature_idea_record.get("title")
1900
+ or ""
1901
+ ).strip()
1902
+ else:
1903
+ self.mature_idea = str(mature_value or "").strip()
1904
+ self.seed_id = str(context.get("seed_id") or context.get("idea_id") or self.seed_id or "").strip()
1905
+ self.route_id = str(context.get("route_id") or self.route_id or "").strip()
1906
+ self.route_policy = (
1907
+ dict(context.get("route_policy"))
1908
+ if isinstance(context.get("route_policy"), dict)
1909
+ else {}
1910
+ )
1911
+ self.refinement_scope = (context.get("refinement_scope") or "").strip()
1912
+ self._mature_idea_components = list(context.get("components") or [])
1913
+ self._mature_idea_component_explanations = dict(context.get("component_explanations") or {})
1914
+ raw_iteration_budget = context.get("mcts_iteration_budget")
1915
+ try:
1916
+ effective_iterations = int(raw_iteration_budget)
1917
+ except (TypeError, ValueError):
1918
+ effective_iterations = self.config.max_iterations
1919
+ effective_iterations = max(0, min(self.config.max_iterations, effective_iterations))
1920
+ try:
1921
+ depth_multiplier = float(context.get("data_anchored_mcts_depth_multiplier", 1.0))
1922
+ except (TypeError, ValueError):
1923
+ depth_multiplier = 1.0
1924
+ effective_max_depth = max(
1925
+ 1,
1926
+ int(math.ceil(self.config.max_depth * max(1.0, depth_multiplier))),
1927
+ )
1928
+ root_domains = context.get("root_domains") or []
1929
+ log_message(
1930
+ self.logger,
1931
+ self.log_sink,
1932
+ "info",
1933
+ "[MCTS] Root-domain classification: %s",
1934
+ _format_root_domains_for_prompt(root_domains),
1935
+ )
1936
+
1937
+ root_state = build_root_state(topic, context, IdeaState)
1938
+ root = new_node(
1939
+ root_state,
1940
+ depth=0,
1941
+ parent=None,
1942
+ signature_nodes=self.signature_nodes,
1943
+ id_iter=self._id_iter,
1944
+ idea_node_cls=IdeaNode,
1945
+ operator_application_cls=OperatorApplication,
1946
+ )
1947
+ experiences: List[Dict[str, Any]] = []
1948
+
1949
+ # Prime root defects via one evaluator pass before the first expand so
1950
+ # initial skill selection is not forced to rely on the placeholder
1951
+ # "unexplored_gap" defect tag.
1952
+ root_eval = self._simulate(root, [root], experiences)
1953
+ if root_eval and root_eval.detected_defects:
1954
+ inferred_defects = _dedupe_keep_order_strings(
1955
+ [str(tag) for tag in root_eval.detected_defects if str(tag).strip()]
1956
+ )
1957
+ if inferred_defects:
1958
+ previous_defects = list(root.state.target_defects)
1959
+ root.state.target_defects = _dedupe_keep_order_strings(
1960
+ [*previous_defects, *inferred_defects]
1961
+ )
1962
+ root.transformation.defects = list(root.state.target_defects)
1963
+ root.latest_path_summary = ""
1964
+
1965
+ def _record_rollout(iteration: int, target: IdeaNode, rollout_path: List[IdeaNode], evaluation: IdeaEvaluation) -> None:
1966
+ self._backpropagate(rollout_path, evaluation)
1967
+ self._update_skill_prior(target, evaluation)
1968
+
1969
+ rollout_summary = path_summary(rollout_path)
1970
+ transformation = target.transformation
1971
+ defects = list(transformation.defects) if transformation and transformation.defects else []
1972
+ operator = transformation.operator if transformation else "unknown"
1973
+ action_summary = f"{operator} -> {', '.join(defects) if defects else 'unspecified_defect'}"
1974
+
1975
+ self.trace.append(
1976
+ {
1977
+ "iteration": iteration,
1978
+ "node_id": target.node_id,
1979
+ "depth": len(rollout_path) - 1,
1980
+ "title": target.state.title,
1981
+ "operator": operator,
1982
+ "defects": defects,
1983
+ "memory_refs": list(transformation.memory_refs) if transformation else [],
1984
+ "rationale": transformation.rationale if transformation else "",
1985
+ "score": evaluation.composite,
1986
+ "visits": target.visits,
1987
+ "path": rollout_summary,
1988
+ "action_summary": action_summary,
1989
+ "evaluation": {**evaluation.to_dict(), "composite": evaluation.composite},
1990
+ "signature": target.state.signature,
1991
+ "edit_plan": target.state.edit_plan,
1992
+ "skill_metrics": target.state.skill_metrics,
1993
+ "data_anchored_coverage_branch": (
1994
+ dict(
1995
+ target.state.scientific_intervention.get(
1996
+ "data_anchored_coverage_branch"
1997
+ )
1998
+ or {}
1999
+ )
2000
+ if isinstance(target.state.scientific_intervention, dict)
2001
+ else {}
2002
+ ),
2003
+ }
2004
+ )
2005
+
2006
+ for iteration in iter_with_progress(
2007
+ range(effective_iterations),
2008
+ description="MCTS search",
2009
+ total=effective_iterations,
2010
+ ):
2011
+ leaf, path = self._select(root)
2012
+ depth = len(path) - 1
2013
+ rollout_targets: List[Tuple[IdeaNode, List[IdeaNode]]] = []
2014
+
2015
+ if depth >= effective_max_depth:
2016
+ rollout_targets = [(leaf, path)]
2017
+ else:
2018
+ target, rollout_path = self._expand(leaf, path)
2019
+ if depth <= 1:
2020
+ rollout_targets = [
2021
+ (child, path + [child])
2022
+ for child in leaf.children
2023
+ if child.visits == 0
2024
+ ]
2025
+ elif target is not None:
2026
+ rollout_targets = [(target, rollout_path)]
2027
+
2028
+ for target, rollout_path in rollout_targets:
2029
+ evaluation = self._simulate(target, rollout_path, experiences)
2030
+ if evaluation is None:
2031
+ continue
2032
+ _record_rollout(iteration, target, rollout_path, evaluation)
2033
+
2034
+ best = best_candidate(root, SearchCandidate)
2035
+ pareto = pareto_candidates(root, SearchCandidate)
2036
+ cache_entries = sum(len(entries) for entries in self.evaluation_cache.values())
2037
+
2038
+ # Persist injected symbolic priors to disk so they survive across runs
2039
+ if self.enable_symbolic_memory and self.persist_symbolic_memory:
2040
+ try:
2041
+ self.symbolic_memory_path.parent.mkdir(parents=True, exist_ok=True)
2042
+ self.symbolic_memory.save(str(self.symbolic_memory_path))
2043
+
2044
+ except Exception as exc:
2045
+ log_message(
2046
+ self.logger,
2047
+ self.log_sink,
2048
+ "warning",
2049
+ "⚠️ Failed to save symbolic memory: %s",
2050
+ exc,
2051
+ )
2052
+
2053
+ return SearchResult(
2054
+ best=best,
2055
+ pareto=pareto,
2056
+ trace=self.trace,
2057
+ cache_size=cache_entries,
2058
+ experiences=experiences,
2059
+ retrieved_core_titles=list(self.retrieved_core_titles),
2060
+ )