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,1836 @@
1
+ from __future__ import annotations
2
+
3
+ from concurrent.futures import ThreadPoolExecutor, as_completed
4
+ from copy import deepcopy
5
+ from typing import Any, Dict, List
6
+
7
+ from src.agents.idea_agent.agent.artifacts import artifact_get
8
+ from src.agents.idea_agent.agent.prompts import PROMPTS
9
+ from src.agents.idea_agent.agent.prompts.advanced_analysis import render_advanced_analysis_prompt
10
+ from src.agents.idea_agent.utils.core.config_loader import get_config_value
11
+ from src.agents.idea_agent.utils.core.json_utils import pretty_json
12
+ from src.agents.idea_agent.utils.core.logger import suspend_console_handlers
13
+ from src.agents.idea_agent.utils.core.progress import iter_with_progress
14
+ from src.agents.idea_agent.utils.core.response_parsing import JsonObjectResponseError
15
+ from src.agents.idea_agent.utils.mcts.idea_taste_presets import IDEA_TASTE_PRESETS
16
+ from src.agents.idea_agent.utils.mcts.idea_routes import IDEA_ROUTE_POLICIES
17
+ from src.agents.idea_agent.utils.mcts.scientific_intervention_ontology import (
18
+ format_scientific_intervention_profile_for_prompt,
19
+ get_scientific_intervention_profile,
20
+ resolve_scientific_intervention_profile,
21
+ )
22
+ from src.agents.idea_agent.utils.prompting.prompt_views import format_paper_capsules_prompt_view
23
+ from src.agents.idea_agent.utils.workflow.idea_contract import (
24
+ mature_idea_legacy_text,
25
+ normalize_idea_contract,
26
+ normalize_mature_ideas,
27
+ )
28
+ from src.agents.idea_agent.utils.workflow.advanced_analysis_contract import (
29
+ build_advanced_analysis_repair_prompt,
30
+ validate_advanced_analysis_response,
31
+ )
32
+ from src.agents.idea_agent.utils.workflow.idea_diversity import filter_independent_mature_ideas
33
+ from src.agents.idea_agent.utils.workflow.mature_idea_sources import (
34
+ build_mature_idea_evidence_context,
35
+ collect_mature_idea_sources,
36
+ )
37
+ from src.agents.idea_agent.utils.workflow.multimodal_data_anchoring import (
38
+ DATA_ANCHORED_PRIORITY,
39
+ apply_data_anchored_idea_constraints,
40
+ build_data_anchored_coverage_schedule,
41
+ is_data_anchored,
42
+ )
43
+ from src.agents.idea_agent.utils.workflow.idea_debate import debate_direction_set
44
+ from src.agents.idea_agent.utils.workflow.idea_helpers import build_direction_result_document
45
+ from src.agents.idea_agent.utils.workflow.idea_synthesis import synthesize_direction_set
46
+ from src.agents.idea_agent.utils.workflow.idea_portfolio import build_idea_portfolio
47
+ from src.agents.idea_agent.utils.workflow.ligagent_flow import (
48
+ make_stage_context,
49
+ persist_final_idea,
50
+ save_candidate_payload,
51
+ save_idea_result_payload,
52
+ )
53
+ from src.agents.idea_agent.utils.workflow.result_persistence import compact_candidate_entry
54
+ from src.agents.idea_agent.utils.workflow.ligagent_helpers import (
55
+ build_replanned_idea_entry,
56
+ collect_rag_citation_references,
57
+ collect_analysis_background_lines,
58
+ collect_rag_citations,
59
+ collect_rag_contents,
60
+ extract_experiment_findings_from_raw_ablation,
61
+ extract_root_idea_from_analysis,
62
+ generate_rag_query,
63
+ merge_title_lists,
64
+ normalize_analysis_entry,
65
+ paper_context_with_rag,
66
+ prior_component_seed,
67
+ result_to_best_entry,
68
+ retrieve_outcome_rag,
69
+ root_idea_to_mature_idea_text,
70
+ root_idea_to_refinement_scope_text,
71
+ should_preserve_current_mature_idea,
72
+ preserve_mature_idea_as_root,
73
+ )
74
+ from src.agents.idea_agent.utils.workflow.ligagent_utils import (
75
+ collect_paper_context_entries,
76
+ )
77
+ from src.agents.idea_agent.utils.workflow.stage_contract import (
78
+ ArtifactPatch,
79
+ StageContext,
80
+ StageResult,
81
+ )
82
+
83
+
84
+ def _logger(agent: Any, ctx: StageContext) -> Any:
85
+ return ctx.logger or getattr(agent, "logger", None)
86
+
87
+
88
+ def _runtime(agent: Any, ctx: StageContext) -> Any:
89
+ return ctx.runtime or getattr(agent, "runtime", None)
90
+
91
+
92
+ def _session(agent: Any, ctx: StageContext) -> Any:
93
+ return ctx.session or getattr(agent, "session", None)
94
+
95
+
96
+ def _chat(agent: Any, ctx: StageContext, op_name: str):
97
+ runtime = _runtime(agent, ctx)
98
+ session = _session(agent, ctx)
99
+ default_stage = ctx.stage_name or ctx.workflow_name
100
+ workflow_name = ctx.workflow_name
101
+
102
+ def _invoke(prompt: str, **kwargs: Any) -> str:
103
+ stage = str(kwargs.pop("stage", "") or "").strip() or default_stage
104
+ return runtime.llm_text(
105
+ session=session,
106
+ stage=stage,
107
+ workflow_name=workflow_name,
108
+ op_name=op_name,
109
+ prompt=prompt,
110
+ **kwargs,
111
+ )
112
+
113
+ return _invoke
114
+
115
+
116
+ def _score_keynote_reference(
117
+ agent: Any,
118
+ ctx: StageContext,
119
+ topic: str,
120
+ rag_query: str,
121
+ reference: Dict[str, Any],
122
+ ) -> Dict[str, Any]:
123
+ runtime = _runtime(agent, ctx)
124
+ session = _session(agent, ctx)
125
+ prompt = PROMPTS["keynote_scoring"].format(
126
+ topic=topic,
127
+ rag_query=rag_query,
128
+ title=str(reference.get("title") or reference.get("paper_title") or "").strip(),
129
+ keynote=str(reference.get("keynote") or "").strip(),
130
+ )
131
+ payload = runtime.llm_json(
132
+ session=session,
133
+ stage="ka_keynote_scoring",
134
+ workflow_name=ctx.workflow_name,
135
+ op_name="keynote_scoring",
136
+ prompt=prompt,
137
+ model=agent.model,
138
+ temperature=0.0,
139
+ max_output_tokens=512,
140
+ )
141
+ scored = dict(reference)
142
+ scored["score"] = int(payload["score"])
143
+ return scored
144
+
145
+
146
+ def _compress_single_keynote_reference(
147
+ agent: Any,
148
+ ctx: StageContext,
149
+ topic: str,
150
+ rag_query: str,
151
+ reference: Dict[str, Any],
152
+ ) -> Dict[str, Any]:
153
+ runtime = _runtime(agent, ctx)
154
+ session = _session(agent, ctx)
155
+ prompt = PROMPTS["keynote_single_compression"].format(
156
+ topic=topic,
157
+ rag_query=rag_query,
158
+ title=str(reference.get("title") or reference.get("paper_title") or "").strip(),
159
+ keynote=str(reference.get("keynote") or "").strip(),
160
+ )
161
+ payload = runtime.llm_json(
162
+ session=session,
163
+ stage="ka_keynote_single_compression",
164
+ workflow_name=ctx.workflow_name,
165
+ op_name="keynote_single_compression",
166
+ prompt=prompt,
167
+ model=agent.model,
168
+ temperature=0.1,
169
+ max_output_tokens=2048,
170
+ )
171
+ return {
172
+ "paper_id": reference.get("paper_id"),
173
+ "node_id": reference.get("node_id"),
174
+ "title": reference.get("title"),
175
+ "paper_title": reference.get("paper_title"),
176
+ "summary": str(payload["summary"]).strip(),
177
+ "insight": str(payload["insight"]).strip(),
178
+ "authors": reference.get("authors") or [],
179
+ "source": reference.get("source") or "survey_keynote",
180
+ "source_keywords": reference.get("source_keywords") or rag_query,
181
+ "paper_domain": reference.get("paper_domain"),
182
+ "venue": reference.get("venue"),
183
+ "year": reference.get("year"),
184
+ "score": int(reference["score"]),
185
+ "reference_mode": "paper_summary",
186
+ }
187
+
188
+
189
+ def _summarize_remaining_keynotes(
190
+ agent: Any,
191
+ ctx: StageContext,
192
+ topic: str,
193
+ rag_query: str,
194
+ references: List[Dict[str, Any]],
195
+ ) -> Dict[str, Any]:
196
+ runtime = _runtime(agent, ctx)
197
+ session = _session(agent, ctx)
198
+ prompt = PROMPTS["keynote_group_summary"].format(
199
+ topic=topic,
200
+ rag_query=rag_query,
201
+ papers=pretty_json(
202
+ [
203
+ {
204
+ "title": reference.get("title"),
205
+ "score": reference.get("score"),
206
+ "keynote": reference.get("keynote"),
207
+ }
208
+ for reference in references
209
+ ]
210
+ ),
211
+ )
212
+ payload = runtime.llm_json(
213
+ session=session,
214
+ stage="ka_keynote_group_summary",
215
+ workflow_name=ctx.workflow_name,
216
+ op_name="keynote_group_summary",
217
+ prompt=prompt,
218
+ model=agent.model,
219
+ temperature=0.1,
220
+ max_output_tokens=4096,
221
+ )
222
+ return {
223
+ "paper_id": "remaining_survey_cited_papers",
224
+ "node_id": "remaining_survey_cited_papers",
225
+ "title": f"Remaining survey-cited papers ({len(references)})",
226
+ "paper_title": f"Remaining survey-cited papers ({len(references)})",
227
+ "summary": str(payload["summary"]).strip(),
228
+ "insight": "",
229
+ "authors": [],
230
+ "source": "survey_keynote_rollup",
231
+ "source_keywords": rag_query,
232
+ "paper_domain": "",
233
+ "venue": "",
234
+ "year": "",
235
+ "score": int(references[0]["score"]),
236
+ "reference_mode": "group_summary",
237
+ }
238
+
239
+
240
+ def execute_knowledge_acquisition_stage(agent: Any, ctx: StageContext) -> StageResult:
241
+ spec = agent._build_knowledge_acquisition_workflow()
242
+ nested = agent.workflow_executor.run(
243
+ spec,
244
+ make_stage_context(agent, workflow_name=spec.name),
245
+ )
246
+ summary = nested.state.get("summary") or (
247
+ "\nIn this knowledge_aquisition action, no explicit retrieval outcome was recorded."
248
+ )
249
+ return StageResult(
250
+ status=nested.status,
251
+ step_summary=summary,
252
+ metrics={
253
+ "mode": nested.state.get("mode"),
254
+ "rag_hits": len(nested.state.get("rag_hits", [])),
255
+ "references": len(nested.state.get("curated_references", [])),
256
+ },
257
+ )
258
+
259
+
260
+ def execute_advanced_analysis_stage(agent: Any, ctx: StageContext) -> StageResult:
261
+ artifact = agent.artifact
262
+ logger = _logger(agent, ctx)
263
+ session = _session(agent, ctx)
264
+ runtime = _runtime(agent, ctx)
265
+
266
+ topic_history = artifact_get(artifact, "topic", [])
267
+ topic = topic_history[-1] if topic_history else "unspecified topic"
268
+ reference_batches = artifact_get(artifact, "references", [])
269
+ references = reference_batches[-1] if reference_batches else []
270
+ mature_idea = artifact_get(artifact, "mature_idea", "")
271
+ mature_idea_source = artifact_get(artifact, "mature_idea_source", "")
272
+ mature_idea_records = filter_independent_mature_ideas(
273
+ artifact_get(artifact, "mature_ideas", [])
274
+ )
275
+ if not mature_idea_records and mature_idea:
276
+ mature_idea_records = normalize_mature_ideas(
277
+ mature_idea,
278
+ default_source=str(mature_idea_source or "legacy") or "legacy",
279
+ )
280
+ refinement_scope = artifact_get(artifact, "refinement_scope", "")
281
+ refinement_scope_source = artifact_get(artifact, "refinement_scope_source", "")
282
+ ablation_results = artifact_get(artifact, "ablation_results", [])
283
+ rag_contents = artifact_get(artifact, "rag_contents", [])
284
+ latest_rag_contents = rag_contents[-1] if rag_contents else []
285
+ project_context: Dict[str, Any] = {}
286
+ paper_repository = getattr(agent, "paper_repository", None)
287
+ load_project_context = getattr(paper_repository, "load_project_context", None)
288
+ if callable(load_project_context):
289
+ loaded_project_context = load_project_context()
290
+ if isinstance(loaded_project_context, dict):
291
+ project_context = loaded_project_context
292
+ resolved_profile = resolve_scientific_intervention_profile(
293
+ {},
294
+ project_context=project_context or None,
295
+ ) or get_scientific_intervention_profile("generic_scientific")
296
+ profile_id = resolved_profile.profile_id if resolved_profile is not None else "generic_scientific"
297
+ raw_ablation = (ctx.inputs or {}).get("raw_ablation")
298
+ if raw_ablation is None:
299
+ artifact_raw_ablation = artifact_get(artifact, "ablation_results_raw", {})
300
+ raw_ablation = artifact_raw_ablation if isinstance(artifact_raw_ablation, dict) else None
301
+
302
+ experiment_findings = None
303
+ if isinstance(raw_ablation, dict):
304
+ extractor_cfg = get_config_value(
305
+ agent.config,
306
+ "agent.experiment_findings_extraction",
307
+ {},
308
+ )
309
+
310
+ def _extractor_chat(prompt: str, **kwargs: Any) -> str:
311
+ extractor_stage = str(kwargs.pop("stage", "") or "experiment_findings_extraction").strip()
312
+ return runtime.llm_text(
313
+ session=session,
314
+ stage=extractor_stage,
315
+ workflow_name=ctx.workflow_name,
316
+ op_name="experiment_findings_extraction",
317
+ prompt=prompt,
318
+ **kwargs,
319
+ )
320
+
321
+ experiment_findings = extract_experiment_findings_from_raw_ablation(
322
+ raw_ablation,
323
+ chat_fn=_extractor_chat,
324
+ logger=logger,
325
+ extractor_config=extractor_cfg,
326
+ )
327
+
328
+ prompt = PROMPTS["advanced_analysis"].format(
329
+ topic=topic,
330
+ mature_idea=(mature_idea or "").strip(),
331
+ mature_idea_source=(mature_idea_source or "empty").strip(),
332
+ refinement_scope=(refinement_scope or "").strip(),
333
+ refinement_scope_source=(refinement_scope_source or "empty").strip(),
334
+ survey_contents="\n".join(latest_rag_contents) if isinstance(latest_rag_contents, list) else "",
335
+ papers=format_paper_capsules_prompt_view(references),
336
+ experiment_findings=(
337
+ pretty_json(experiment_findings)
338
+ if isinstance(experiment_findings, dict) and experiment_findings
339
+ else "None"
340
+ ),
341
+ )
342
+ survey_context = artifact_get(artifact, "survey_idea_context", {})
343
+ if isinstance(survey_context, dict) and survey_context.get("handoff"):
344
+ prompt = (
345
+ "== Verified Survey -> Idea handoff (authoritative gap boundary) ==\n"
346
+ + pretty_json(survey_context["handoff"])
347
+ + "\n\n"
348
+ + prompt
349
+ )
350
+ multimodal_projection = (
351
+ survey_context.get("multimodal_evidence_projection")
352
+ if isinstance(survey_context, dict)
353
+ else None
354
+ )
355
+ if isinstance(multimodal_projection, dict) and multimodal_projection:
356
+ prompt = (
357
+ "== Bounded supplied-data projection (not literature evidence) ==\n"
358
+ + pretty_json(multimodal_projection)
359
+ + "\nUse this only as dataset-local observation context. Do not describe it as proving "
360
+ "a mechanism, a universal result, or a first discovery. Preserve candidate mechanisms, "
361
+ "alternative explanations, measurement artifacts, and the stated claim limits.\n\n"
362
+ + prompt
363
+ )
364
+ prompt = render_advanced_analysis_prompt(prompt, profile_id)
365
+ prompt = (
366
+ "== Scientific intervention profile (fixed before analysis) ==\n"
367
+ + format_scientific_intervention_profile_for_prompt(
368
+ resolved_profile.to_payload() if resolved_profile is not None else {"profile_id": profile_id}
369
+ )
370
+ + "\n\n"
371
+ + prompt
372
+ )
373
+ require_grounded_fields = (
374
+ not bool(str(mature_idea or "").strip())
375
+ or str(mature_idea_source or "").strip().casefold() in {"empty", "input_inferred"}
376
+ )
377
+ require_gap_ids = bool(isinstance(survey_context, dict) and survey_context.get("handoff"))
378
+
379
+ def _request_advanced_analysis(op_name: str, request_prompt: str) -> tuple[Any, List[str]]:
380
+ try:
381
+ candidate = runtime.llm_json(
382
+ session=session,
383
+ stage=ctx.stage_name,
384
+ workflow_name=ctx.workflow_name,
385
+ op_name=op_name,
386
+ prompt=request_prompt,
387
+ model=agent.model,
388
+ max_output_tokens=65536,
389
+ response_format={"type": "json_object"},
390
+ require_json_object=True,
391
+ )
392
+ except JsonObjectResponseError as exc:
393
+ return None, [str(exc)]
394
+ return candidate, validate_advanced_analysis_response(
395
+ candidate,
396
+ require_grounded_fields=require_grounded_fields,
397
+ require_gap_ids=require_gap_ids,
398
+ )
399
+
400
+ raw_response, contract_errors = _request_advanced_analysis("advanced_analysis", prompt)
401
+ try:
402
+ contract_retries = max(
403
+ 0,
404
+ int(get_config_value(agent.config, "agent.advanced_analysis_contract_retries", 2) or 0),
405
+ )
406
+ except (TypeError, ValueError):
407
+ contract_retries = 2
408
+ for attempt in range(1, contract_retries + 1):
409
+ if not contract_errors:
410
+ break
411
+ logger.warning(
412
+ "⚠️ Advanced analysis contract violation; requesting repair %s/%s: %s",
413
+ attempt,
414
+ contract_retries,
415
+ "; ".join(contract_errors[:8]),
416
+ )
417
+ raw_response, contract_errors = _request_advanced_analysis(
418
+ f"advanced_analysis_contract_repair_{attempt}",
419
+ build_advanced_analysis_repair_prompt(prompt, raw_response, contract_errors),
420
+ )
421
+ if contract_errors:
422
+ raise ValueError(
423
+ "Advanced analysis output failed the required contract after "
424
+ f"{contract_retries} repair attempt(s): {'; '.join(contract_errors[:12])}"
425
+ )
426
+ response = normalize_analysis_entry(raw_response)
427
+ if isinstance(response, (dict, list)):
428
+ logger.info(
429
+ "📝 Advanced Analysis Result:\n%s",
430
+ pretty_json(response),
431
+ )
432
+ else:
433
+ logger.info("📝 Advanced Analysis Result:\n%s", response)
434
+
435
+ if experiment_findings is not None:
436
+ response["experiment_findings"] = experiment_findings
437
+ root_idea = extract_root_idea_from_analysis(response, topic=topic)
438
+ preserved_mature_idea = False
439
+ if isinstance(mature_idea, str) and mature_idea.strip():
440
+ preserve_original, preserve_reason = should_preserve_current_mature_idea(
441
+ response,
442
+ root_idea,
443
+ mature_idea,
444
+ )
445
+ if preserve_original:
446
+ preserved_mature_idea = True
447
+ response["preserve_current_idea"] = {
448
+ "keep_original": True,
449
+ "reason": preserve_reason,
450
+ }
451
+ root_idea = preserve_mature_idea_as_root(
452
+ topic,
453
+ mature_idea,
454
+ target_defects=list(root_idea.get("target_defects") or ["unclear_mechanism"]),
455
+ reason=preserve_reason,
456
+ )
457
+ response["tldr"] = preserve_reason
458
+ response["root_idea"] = root_idea
459
+ existing_background = set(artifact_get(artifact, "background_knowledge", []))
460
+ background_lines = [
461
+ line
462
+ for line in collect_analysis_background_lines(response)
463
+ if line not in existing_background
464
+ ]
465
+ if session is not None:
466
+ session.set_slot("analysis.latest", response)
467
+ replace_patch: Dict[str, Any] = {"root_idea": root_idea}
468
+ grounded_mature_idea = str(response.get("grounded_mature_idea") or "").strip()
469
+ survey_context = artifact_get(artifact, "survey_idea_context", {})
470
+ survey_handoff = survey_context.get("handoff") if isinstance(survey_context, dict) else {}
471
+ response_seed_records = normalize_mature_ideas(
472
+ response.get("mature_ideas") if isinstance(response, dict) else [],
473
+ default_source="problem_reframing",
474
+ )
475
+ source_inputs = [*mature_idea_records, *response_seed_records]
476
+ if grounded_mature_idea and not source_inputs:
477
+ source_inputs.append(
478
+ normalize_mature_ideas(
479
+ grounded_mature_idea,
480
+ default_source="problem_reframing",
481
+ )[0]
482
+ )
483
+ analyzed_mature_ideas = collect_mature_idea_sources(
484
+ existing=source_inputs,
485
+ survey_handoff=survey_handoff,
486
+ prior_candidate=artifact_get(artifact, "latest_candidate", {}),
487
+ experiment_results=ablation_results,
488
+ analysis=response,
489
+ max_ideas=int(get_config_value(agent.config, "run.max_mature_ideas", 12) or 12),
490
+ allow_problem_reframing=bool(get_config_value(agent.config, "run.allow_problem_reframing", True)),
491
+ allow_unanchored_seed=bool(get_config_value(agent.config, "run.allow_unanchored_seed", True)),
492
+ allow_high_risk_seed=bool(get_config_value(agent.config, "run.allow_high_risk_seed", True)),
493
+ )
494
+ if analyzed_mature_ideas:
495
+ replace_patch["mature_ideas"] = analyzed_mature_ideas
496
+ replace_patch["mature_idea"] = mature_idea_legacy_text(analyzed_mature_ideas)
497
+ replace_patch["mature_idea_source"] = "analysis_grounded_collection"
498
+ grounded_refinement_scope = str(response.get("grounded_refinement_scope") or "").strip()
499
+ current_mature_source = str(mature_idea_source or ("empty" if not str(mature_idea or "").strip() else "unknown"))
500
+ current_scope_source = str(
501
+ refinement_scope_source or ("empty" if not str(refinement_scope or "").strip() else "unknown")
502
+ )
503
+ if current_mature_source in {"empty", "input_inferred"}:
504
+ promoted = grounded_mature_idea or root_idea_to_mature_idea_text(root_idea)
505
+ if promoted:
506
+ replace_patch["mature_idea"] = promoted
507
+ replace_patch["mature_idea_source"] = "analysis_grounded"
508
+ if session is not None:
509
+ session.set_slot("mature_idea.latest", promoted)
510
+ if current_scope_source in {"empty", "input_inferred"} and grounded_refinement_scope:
511
+ replace_patch["refinement_scope"] = grounded_refinement_scope
512
+ replace_patch["refinement_scope_source"] = "analysis_grounded"
513
+ elif current_scope_source in {"empty", "input_inferred"}:
514
+ fallback_scope = root_idea_to_refinement_scope_text(root_idea)
515
+ if fallback_scope:
516
+ replace_patch["refinement_scope"] = fallback_scope
517
+ replace_patch["refinement_scope_source"] = "analysis_grounded"
518
+ promoted_root_to_mature = False
519
+ if (
520
+ isinstance(mature_idea, str)
521
+ and mature_idea.strip()
522
+ and current_mature_source not in {"empty", "input_inferred"}
523
+ and not ablation_results
524
+ and not preserved_mature_idea
525
+ ):
526
+ promoted_mature_idea = root_idea_to_mature_idea_text(root_idea)
527
+ if promoted_mature_idea:
528
+ replace_patch["mature_idea"] = promoted_mature_idea
529
+ promoted_root_to_mature = True
530
+ if session is not None:
531
+ session.set_slot("mature_idea.latest", promoted_mature_idea)
532
+ logger.info(
533
+ "🪴 Advanced analysis promoted calibrated root_idea to mature_idea for no-ablation continuation."
534
+ )
535
+ step = (
536
+ "\nIn this advanced_analysis action, I analyzed the retrieved references and summarized my findings: "
537
+ f"{response.get('tldr', 'No TL;DR provided.')}"
538
+ )
539
+ return StageResult(
540
+ artifact_patch=ArtifactPatch(
541
+ replace=replace_patch,
542
+ append={
543
+ "analysis": [response],
544
+ "background_knowledge": background_lines,
545
+ }
546
+ ),
547
+ step_summary=step,
548
+ metrics={
549
+ "reference_count": len(references),
550
+ "background_lines": len(background_lines),
551
+ "root_idea_title": root_idea.get("title"),
552
+ "experiment_findings_used": bool(experiment_findings),
553
+ "promoted_root_to_mature_idea": promoted_root_to_mature,
554
+ "preserved_mature_idea": preserved_mature_idea,
555
+ },
556
+ )
557
+
558
+
559
+ def _build_mode_result_document(
560
+ topic: str,
561
+ direction: Dict[str, Any],
562
+ source_entry: Dict[str, Any],
563
+ artifact: Dict[str, Any],
564
+ survey_binding: Optional[Dict[str, Any]],
565
+ ) -> Dict[str, Any]:
566
+ """Build a mode document from the raw candidate, then compact its legacy view."""
567
+ mode_payload = build_direction_result_document(
568
+ topic,
569
+ source_entry,
570
+ artifact,
571
+ survey_binding,
572
+ )
573
+ mode = str(direction.get("direction_mode") or direction.get("idea_taste_mode") or "default").strip() or "default"
574
+ mode_payload["primary_direction"] = mode
575
+ mode_payload["legacy_best_entry"] = compact_candidate_entry(source_entry)
576
+ return mode_payload
577
+
578
+
579
+ def execute_idea_generation_stage(agent: Any, ctx: StageContext) -> StageResult:
580
+ artifact = agent.artifact
581
+ logger = _logger(agent, ctx)
582
+ session = _session(agent, ctx)
583
+
584
+ topic_history = artifact_get(artifact, "topic", [])
585
+ topic = topic_history[-1]
586
+ reference_batches = artifact_get(artifact, "references", [])
587
+ latest_batch = reference_batches[-1] if reference_batches else []
588
+ batch_list = [latest_batch] if latest_batch else reference_batches
589
+ paper_entries = collect_paper_context_entries(artifact, batch_list)
590
+ latest_candidate = artifact_get(artifact, "latest_candidate", {})
591
+ latest_candidate_payload = (
592
+ normalize_idea_contract(latest_candidate, allow_legacy=True, keep_extra=True)
593
+ if isinstance(latest_candidate, dict) and latest_candidate
594
+ else None
595
+ )
596
+ root_idea = artifact_get(artifact, "root_idea", {})
597
+ root_idea_payload = (
598
+ normalize_idea_contract(root_idea, allow_legacy=True, keep_extra=True)
599
+ if isinstance(root_idea, dict) and root_idea
600
+ else None
601
+ )
602
+ mature_idea = artifact_get(artifact, "mature_idea", "")
603
+ mature_ideas = normalize_mature_ideas(
604
+ artifact_get(artifact, "mature_ideas", []),
605
+ legacy_value=mature_idea,
606
+ default_source=str(artifact_get(artifact, "mature_idea_source", "user_input") or "user_input"),
607
+ )
608
+ refinement_scope = artifact_get(artifact, "refinement_scope", "")
609
+ component_decisions = artifact_get(artifact, "component_decisions", [])
610
+ prior_components, prior_component_explanations = prior_component_seed(
611
+ latest_candidate_payload,
612
+ root_idea_payload,
613
+ )
614
+ context = {
615
+ "analysis": artifact_get(artifact, "analysis", []),
616
+ "latest_candidate": latest_candidate_payload,
617
+ "root_idea": root_idea_payload,
618
+ "background_knowledge": artifact_get(artifact, "background_knowledge", []),
619
+ "paper_context": paper_context_with_rag(paper_entries, artifact),
620
+ "component_decisions": (
621
+ [decision for decision in component_decisions if isinstance(decision, dict)]
622
+ if isinstance(component_decisions, list)
623
+ else []
624
+ ),
625
+ }
626
+ survey_context = artifact_get(artifact, "survey_idea_context", {})
627
+ if isinstance(survey_context, dict) and survey_context:
628
+ context["survey_idea_handoff"] = dict(survey_context.get("handoff") or {})
629
+ context["survey_gap_ledger"] = dict(survey_context.get("gap_ledger") or {})
630
+ context["defect_tags"] = list(survey_context.get("defect_tags") or [])
631
+ context["project_context"] = dict(survey_context.get("project_context") or {})
632
+ if isinstance(survey_context.get("multimodal_evidence_projection"), dict):
633
+ context["multimodal_evidence_projection"] = deepcopy(
634
+ survey_context["multimodal_evidence_projection"]
635
+ )
636
+ context["paper_context"] = (
637
+ "Verified Survey handoff:\n"
638
+ + pretty_json(context["survey_idea_handoff"])
639
+ + "\n\n"
640
+ + str(context.get("paper_context") or "")
641
+ )
642
+ paper_repository = getattr(agent, "paper_repository", None)
643
+ load_project_context = getattr(paper_repository, "load_project_context", None)
644
+ if callable(load_project_context):
645
+ project_context = load_project_context()
646
+ if isinstance(project_context, dict) and project_context:
647
+ context["project_context"] = project_context
648
+ handoff_for_public_facts = context.get("survey_idea_handoff")
649
+ if isinstance(handoff_for_public_facts, dict):
650
+ context["public_facts"] = {
651
+ "topic": topic,
652
+ "verified_facts": deepcopy(
653
+ handoff_for_public_facts.get("verified_facts")
654
+ or handoff_for_public_facts.get("field_consensus")
655
+ or []
656
+ ),
657
+ "hard_constraints": deepcopy(
658
+ handoff_for_public_facts.get("hard_constraints")
659
+ or handoff_for_public_facts.get("constraints")
660
+ or []
661
+ ),
662
+ "profile_resolution": deepcopy(handoff_for_public_facts.get("profile_resolution") or {}),
663
+ }
664
+ analysis_history = artifact_get(artifact, "analysis", [])
665
+ latest_analysis = analysis_history[-1] if analysis_history else {}
666
+ mature_ideas = collect_mature_idea_sources(
667
+ existing=mature_ideas,
668
+ survey_handoff=context.get("survey_idea_handoff", {}),
669
+ prior_candidate=latest_candidate_payload,
670
+ experiment_results=artifact_get(artifact, "ablation_results", []),
671
+ analysis=latest_analysis,
672
+ max_ideas=int(get_config_value(agent.config, "run.max_mature_ideas", 12) or 12),
673
+ allow_problem_reframing=bool(get_config_value(agent.config, "run.allow_problem_reframing", True)),
674
+ allow_unanchored_seed=bool(get_config_value(agent.config, "run.allow_unanchored_seed", True)),
675
+ allow_high_risk_seed=bool(get_config_value(agent.config, "run.allow_high_risk_seed", True)),
676
+ multimodal_evidence_projection=context.get("multimodal_evidence_projection"),
677
+ )
678
+ mature_idea_contexts = [
679
+ build_mature_idea_evidence_context(
680
+ idea,
681
+ topic=topic,
682
+ survey_handoff=context.get("survey_idea_handoff", {}),
683
+ references=paper_entries,
684
+ ablation_results=artifact_get(artifact, "ablation_results", []),
685
+ public_facts=context.get("public_facts", {}),
686
+ multimodal_evidence_projection=context.get("multimodal_evidence_projection"),
687
+ )
688
+ for idea in mature_ideas
689
+ ]
690
+ context["mature_idea_contexts"] = mature_idea_contexts
691
+ if isinstance(mature_idea, str) and mature_idea.strip():
692
+ context["mature_idea"] = mature_idea.strip()
693
+ if mature_ideas:
694
+ context["mature_ideas"] = mature_ideas
695
+ context["mature_idea"] = mature_idea_legacy_text(mature_ideas) or context.get("mature_idea", "")
696
+ if isinstance(refinement_scope, str) and refinement_scope.strip():
697
+ context["refinement_scope"] = refinement_scope.strip()
698
+ if prior_components:
699
+ context["prior_components"] = prior_components
700
+ context["prior_component_explanations"] = prior_component_explanations
701
+
702
+ agent.mcts.reload_symbolic_memory()
703
+ ligagent_pro = bool(get_config_value(agent.config, "run.LigAgent-Pro", False))
704
+
705
+ materialization_model = str(
706
+ get_config_value(agent.config, "fusion.model", "") or agent.model
707
+ ).strip()
708
+ materialize_chat = _chat(agent, ctx, "idea_materialization")
709
+ completed_modes: List[Dict[str, Any]] = []
710
+ mode_results: Dict[str, Any] = {}
711
+
712
+ def _complete_mode(
713
+ mode: str,
714
+ result: Any,
715
+ seed: Dict[str, Any] | None = None,
716
+ route: Any = None,
717
+ ) -> None:
718
+ entry = result_to_best_entry(result, mode)
719
+ if seed:
720
+ seed_id = str(seed.get("idea_id") or seed.get("seed_id") or "legacy-primary")
721
+ entry["idea_id"] = seed_id
722
+ entry["seed_id"] = seed_id
723
+ if not entry.get("target_gap_ids"):
724
+ entry["target_gap_ids"] = list(seed.get("target_gap_ids") or [])
725
+ entry.setdefault("gap_alignment", deepcopy(seed.get("gap_alignment") or ""))
726
+ entry["mature_idea"] = deepcopy(seed)
727
+ for field_name in (
728
+ "maturity",
729
+ "maturity_is_not_rank",
730
+ "maturity_status",
731
+ "lineage",
732
+ "source_lineage",
733
+ "idea_source",
734
+ "independence_rationale",
735
+ "anchor_policy",
736
+ "anti_anchor",
737
+ "anti_anchor_reason",
738
+ ):
739
+ if seed.get(field_name) not in (None, "", [], {}):
740
+ entry[field_name] = deepcopy(seed[field_name])
741
+ if route is not None:
742
+ entry["route_id"] = route.route_id
743
+ entry["route_signature"] = {
744
+ "mature_idea": deepcopy(seed.get("route_signature") if seed else {}),
745
+ "route_id": route.route_id,
746
+ "required_structural_change": route.required_structural_change,
747
+ }
748
+ entry["route_policy"] = route.to_payload()
749
+ entry["legacy_direction_mode"] = route.legacy_mode
750
+ entry["direction_mode"] = route.route_id
751
+ entry["idea_taste_mode"] = route.legacy_mode
752
+ else:
753
+ entry.setdefault("seed_id", str(seed.get("idea_id") if seed else "legacy-primary"))
754
+ entry.setdefault("route_id", f"legacy:{mode}")
755
+ entry.setdefault("route_signature", {"route_id": f"legacy:{mode}", "direction_mode": mode})
756
+ if seed is not None and is_data_anchored(seed):
757
+ entry = apply_data_anchored_idea_constraints(entry, seed=seed)
758
+ completed_modes.append({
759
+ "mode": mode,
760
+ "result": result,
761
+ "entry": entry,
762
+ "seed": seed,
763
+ "route": route,
764
+ })
765
+
766
+ route_matrix_enabled = bool(get_config_value(agent.config, "run.route_matrix_enabled", True))
767
+ active_mature_ideas = [
768
+ idea for idea in mature_ideas
769
+ if str(idea.get("maturity_status") or "").casefold() != "rejected"
770
+ and str(idea.get("independence_status") or "").casefold() != "collapsed_duplicate"
771
+ ]
772
+ seed_specs: List[Dict[str, Any] | None] = [
773
+ idea for idea in active_mature_ideas if not is_data_anchored(idea)
774
+ ]
775
+ if not seed_specs:
776
+ seed_specs = [None]
777
+ if route_matrix_enabled and (mature_ideas or ligagent_pro):
778
+ max_route_expansions = int(get_config_value(agent.config, "portfolio.max_route_expansions_per_seed", len(IDEA_ROUTE_POLICIES)) or len(IDEA_ROUTE_POLICIES))
779
+ route_specs = list(IDEA_ROUTE_POLICIES)[: max(1, max_route_expansions)]
780
+ else:
781
+ route_specs = [None]
782
+ tasks = [(seed, route) for seed in seed_specs for route in route_specs]
783
+ iterations_per_search = int(
784
+ get_config_value(
785
+ agent.config,
786
+ "mcts.max_iterations",
787
+ getattr(agent.mcts.config, "max_iterations", 0),
788
+ )
789
+ or getattr(agent.mcts.config, "max_iterations", 0)
790
+ )
791
+ raw_data_budget_cap = get_config_value(
792
+ agent.config,
793
+ "survey.multimodal_evidence.data_sh_mcts_budget_cap",
794
+ 0.50,
795
+ )
796
+ try:
797
+ data_budget_cap = float(raw_data_budget_cap)
798
+ except (TypeError, ValueError):
799
+ data_budget_cap = 0.50
800
+ shared_context = agent.mcts.prepare_root_context(topic, context)
801
+ data_schedule = build_data_anchored_coverage_schedule(
802
+ shared_context.get("gap_hypothesis_seeds", []) if isinstance(shared_context, dict) else [],
803
+ ordinary_task_count=len(tasks),
804
+ iterations_per_search=iterations_per_search,
805
+ budget_cap=data_budget_cap,
806
+ )
807
+ data_seed_by_subhypothesis = {
808
+ str(seed.get("subhypothesis_id") or "").strip(): dict(seed)
809
+ for seed in (shared_context.get("gap_hypothesis_seeds", []) if isinstance(shared_context, dict) else [])
810
+ if isinstance(seed, dict) and is_data_anchored(seed)
811
+ }
812
+ coverage_tasks: list[tuple[dict[str, Any], dict[str, Any]]] = []
813
+ for assignment in data_schedule.get("assignments", []):
814
+ if not isinstance(assignment, dict):
815
+ continue
816
+ source_seed = data_seed_by_subhypothesis.get(
817
+ str(assignment.get("subhypothesis_id") or "").strip()
818
+ )
819
+ if source_seed is None:
820
+ continue
821
+ coverage_seed = deepcopy(source_seed)
822
+ coverage_seed.update(
823
+ {
824
+ "idea_id": "data-coverage-" + str(
825
+ assignment.get("subhypothesis_id") or coverage_seed.get("seed_id")
826
+ ),
827
+ "title": "Data-anchored coverage: " + str(
828
+ assignment.get("subhypothesis_id") or "supplied observation"
829
+ ),
830
+ "abstract": str(coverage_seed.get("gap_statement") or "").strip(),
831
+ "hypothesis": str(coverage_seed.get("gap_statement") or "").strip(),
832
+ "target_gap_ids": [coverage_seed.get("gap_id")],
833
+ "maturity_status": "provisional",
834
+ "idea_source": "survey_gap",
835
+ "data_anchored_coverage_assignment": deepcopy(assignment),
836
+ }
837
+ )
838
+ coverage_tasks.append((coverage_seed, assignment))
839
+ total_mcts_searches = len(coverage_tasks) + len(tasks)
840
+ logger.info(
841
+ "🧭 MCTS schedule: %s search(es), including %s data-SH coverage pass(es) before %s ordinary task(s); %s iteration(s) per ordinary search.",
842
+ total_mcts_searches,
843
+ len(coverage_tasks),
844
+ len(tasks),
845
+ iterations_per_search,
846
+ )
847
+
848
+ def _mcts_task_identity(seed: Dict[str, Any] | None, route: Any) -> tuple[str, str]:
849
+ seed_id = str(seed.get("idea_id") if seed else "legacy-primary")
850
+ route_id = str(route.route_id if route is not None else "default")
851
+ return seed_id, route_id
852
+
853
+ def _route_context(seed: Dict[str, Any] | None, route: Any) -> Dict[str, Any]:
854
+ isolated = deepcopy(context)
855
+ if seed is not None:
856
+ seed_id = str(seed.get("idea_id") or "").strip()
857
+ cached_contexts = context.get("mature_idea_contexts") or []
858
+ evidence_context = next(
859
+ (
860
+ deepcopy(item)
861
+ for item in cached_contexts
862
+ if isinstance(item, dict) and str(item.get("idea_id") or "").strip() == seed_id
863
+ ),
864
+ None,
865
+ )
866
+ if evidence_context is None:
867
+ evidence_context = build_mature_idea_evidence_context(
868
+ seed,
869
+ topic=topic,
870
+ survey_handoff=context.get("survey_idea_handoff", {}),
871
+ references=paper_entries,
872
+ ablation_results=artifact_get(artifact, "ablation_results", []),
873
+ public_facts=context.get("public_facts", {}),
874
+ multimodal_evidence_projection=context.get("multimodal_evidence_projection"),
875
+ )
876
+ isolated["mature_idea_evidence_context"] = evidence_context
877
+ isolated["public_facts"] = deepcopy(evidence_context.get("public_facts") or {})
878
+ isolated["survey_gap_ledger"] = {
879
+ "gaps": deepcopy(evidence_context.get("gap_explanation") or [])
880
+ }
881
+ isolated["defect_tags"] = [
882
+ str(gap.get("gap_kind") or "").strip()
883
+ for gap in evidence_context.get("gap_explanation") or []
884
+ if isinstance(gap, dict) and str(gap.get("gap_kind") or "").strip()
885
+ ]
886
+ scoped_analysis = {
887
+ "mature_idea": deepcopy(seed),
888
+ "idea_source": evidence_context.get("idea_source"),
889
+ "lineage": deepcopy(seed.get("lineage")),
890
+ "gap_explanation": deepcopy(evidence_context.get("gap_explanation") or []),
891
+ "evidence_subset": deepcopy(evidence_context.get("evidence_subset") or []),
892
+ "mechanism_chain": deepcopy(evidence_context.get("mechanism_chain") or []),
893
+ "validation_targets": deepcopy(evidence_context.get("validation_targets") or []),
894
+ }
895
+ isolated["analysis"] = [scoped_analysis]
896
+ isolated["background_knowledge"] = deepcopy(evidence_context.get("evidence_subset") or [])
897
+ isolated["retrieval_queries"] = list(evidence_context.get("retrieval_queries") or [])
898
+ isolated["counterexamples"] = deepcopy(evidence_context.get("counterexamples") or [])
899
+ isolated["validation_targets"] = list(evidence_context.get("validation_targets") or [])
900
+ isolated["mechanism_chain"] = list(evidence_context.get("mechanism_chain") or [])
901
+ isolated["survey_idea_handoff"] = deepcopy(evidence_context.get("survey_handoff") or {})
902
+ isolated["paper_context"] = (
903
+ "== Public scientific facts and hard constraints ==\n"
904
+ + pretty_json(evidence_context.get("public_facts") or {})
905
+ + "\n\n== Idea-specific evidence subset ==\n"
906
+ + pretty_json(
907
+ {
908
+ "gap_explanation": evidence_context.get("gap_explanation") or [],
909
+ "evidence_subset": evidence_context.get("evidence_subset") or [],
910
+ "counterexamples": evidence_context.get("counterexamples") or [],
911
+ "mechanism_chain": evidence_context.get("mechanism_chain") or [],
912
+ "validation_targets": evidence_context.get("validation_targets") or [],
913
+ "retrieval_queries": evidence_context.get("retrieval_queries") or [],
914
+ "multimodal_evidence_context": evidence_context.get("multimodal_evidence_context") or {},
915
+ "anchor_policy": evidence_context.get("anchor_policy"),
916
+ }
917
+ )
918
+ )
919
+ seed_text = str(
920
+ seed.get("hypothesis")
921
+ or seed.get("central_hypothesis")
922
+ or seed.get("abstract")
923
+ or seed.get("title")
924
+ or ""
925
+ ).strip()
926
+ isolated.update({
927
+ "mature_idea": seed_text,
928
+ "mature_idea_record": deepcopy(seed),
929
+ "mature_ideas": [deepcopy(seed)],
930
+ "idea_id": seed.get("idea_id"),
931
+ "seed_id": seed.get("idea_id"),
932
+ "target_gap_ids": list(seed.get("target_gap_ids") or []),
933
+ "gap_alignment": deepcopy(seed.get("gap_alignment") or ""),
934
+ "refinement_scope": seed.get("refinement_scope") or isolated.get("refinement_scope", ""),
935
+ })
936
+ if is_data_anchored(seed):
937
+ isolated["data_anchored_context"] = deepcopy(seed)
938
+ isolated["data_anchored_mcts_depth_multiplier"] = float(
939
+ seed.get("mcts_depth_multiplier") or 1.75
940
+ )
941
+ if route is not None:
942
+ isolated["route_id"] = route.route_id
943
+ isolated["route_policy"] = route.to_payload()
944
+ isolated["route_signature"] = {
945
+ "mature_idea": deepcopy(seed.get("route_signature") if seed else {}),
946
+ "route_id": route.route_id,
947
+ }
948
+ isolated["paper_context"] = (
949
+ str(isolated.get("paper_context") or "")
950
+ + "\n\n== Independent route policy ==\n"
951
+ + pretty_json(route.to_payload())
952
+ )
953
+ return isolated
954
+
955
+ for coverage_number, (coverage_seed, assignment) in enumerate(coverage_tasks, start=1):
956
+ coverage_context = _route_context(coverage_seed, None)
957
+ coverage_context["mcts_iteration_budget"] = int(
958
+ assignment.get("iteration_budget") or 0
959
+ )
960
+ coverage_context["data_anchored_mcts_depth_multiplier"] = float(
961
+ assignment.get("mcts_depth_multiplier") or 1.75
962
+ )
963
+ coverage_context["data_anchored_coverage_assignment"] = deepcopy(assignment)
964
+ coverage_mode = "evidence_first"
965
+ coverage_route_id = "data_coverage:" + str(
966
+ assignment.get("subhypothesis_id") or coverage_number
967
+ )
968
+ logger.info(
969
+ "🚀 Data-SH coverage %s/%s started (subhypothesis_id=%s; budget=%s).",
970
+ coverage_number,
971
+ len(coverage_tasks),
972
+ assignment.get("subhypothesis_id"),
973
+ assignment.get("iteration_budget"),
974
+ )
975
+ try:
976
+ result = agent.build_mcts_for_mode(
977
+ coverage_mode,
978
+ seed_id=str(coverage_seed.get("idea_id") or "data-coverage"),
979
+ route_id=coverage_route_id,
980
+ ).search(topic, coverage_context)
981
+ except Exception as exc:
982
+ assignment["materialized_branch_ids"] = []
983
+ assignment["coverage_status"] = "failed"
984
+ logger.warning(
985
+ "⚠️ Data-SH coverage failed (subhypothesis_id=%s): %s",
986
+ assignment.get("subhypothesis_id"),
987
+ exc,
988
+ )
989
+ mode_results[coverage_route_id] = None
990
+ continue
991
+ required_branch_ids = {
992
+ str(branch.get("branch_id") or "").strip()
993
+ for branch in assignment.get("coverage_branches", [])
994
+ if isinstance(branch, dict) and str(branch.get("branch_id") or "").strip()
995
+ }
996
+ materialized_branch_ids = {
997
+ str((item.get("data_anchored_coverage_branch") or {}).get("branch_id") or "").strip()
998
+ for item in (getattr(result, "trace", []) or [])
999
+ if isinstance(item, dict)
1000
+ and isinstance(item.get("data_anchored_coverage_branch"), dict)
1001
+ }
1002
+ materialized_branch_ids.discard("")
1003
+ assignment["materialized_branch_ids"] = sorted(materialized_branch_ids)
1004
+ assignment["coverage_status"] = (
1005
+ "complete"
1006
+ if required_branch_ids.issubset(materialized_branch_ids)
1007
+ else "incomplete_missing_required_branches"
1008
+ )
1009
+ mode_results[coverage_route_id] = result
1010
+ if result.best and assignment["coverage_status"] == "complete":
1011
+ _complete_mode(coverage_route_id, result, seed=coverage_seed)
1012
+ elif assignment["coverage_status"] != "complete":
1013
+ logger.warning(
1014
+ "⚠️ Data-SH coverage is incomplete (subhypothesis_id=%s; missing branches=%s); "
1015
+ "its candidate will not enter the portfolio.",
1016
+ assignment.get("subhypothesis_id"),
1017
+ sorted(required_branch_ids - materialized_branch_ids),
1018
+ )
1019
+
1020
+ if len(tasks) == 1 and tasks[0][0] is None and tasks[0][1] is None:
1021
+ logger.info("🚀 MCTS 1/1 started (seed_id=legacy-primary, route_id=default).")
1022
+ with suspend_console_handlers(logger):
1023
+ result = agent.mcts.search(topic=topic, context=context)
1024
+ mode_label = getattr(getattr(agent.mcts, "idea_taste_preset", None), "mode", None) or "default"
1025
+ mode_results[mode_label] = result
1026
+ logger.info(
1027
+ "✅ MCTS 1/1 completed (seed_id=legacy-primary, route_id=default; candidate=%s).",
1028
+ "yes" if result.best else "no",
1029
+ )
1030
+ if result.best:
1031
+ _complete_mode(mode_label, result)
1032
+ elif tasks:
1033
+ agent.mcts.symbolic_memory_path.parent.mkdir(parents=True, exist_ok=True)
1034
+ agent.mcts.symbolic_memory.save(str(agent.mcts.symbolic_memory_path))
1035
+ max_workers = min(
1036
+ max(1, int(get_config_value(agent.config, "run.idea_route_matrix_max_workers", 8) or 8)),
1037
+ max(1, int(get_config_value(agent.config, "mcts.max_parallel_seeds", 4) or 4))
1038
+ * max(1, int(get_config_value(agent.config, "mcts.max_parallel_routes", 5) or 5)),
1039
+ len(tasks),
1040
+ )
1041
+
1042
+ logger.info("🧵 MCTS schedule will use up to %s parallel worker(s).", max_workers)
1043
+
1044
+ def _run_task(task_number: int, seed: Dict[str, Any] | None, route: Any) -> Any:
1045
+ seed_id, route_id = _mcts_task_identity(seed, route)
1046
+ logger.info(
1047
+ "🚀 MCTS %s/%s started (seed_id=%s, route_id=%s).",
1048
+ task_number,
1049
+ total_mcts_searches,
1050
+ seed_id,
1051
+ route_id,
1052
+ )
1053
+ if route is None:
1054
+ return agent.mcts.search(topic=topic, context=_route_context(seed, route))
1055
+ return agent.build_mcts_for_mode(
1056
+ route.legacy_mode,
1057
+ seed_id=str(seed.get("idea_id") if seed else "legacy-primary"),
1058
+ route_id=route.route_id,
1059
+ ).search(topic, _route_context(seed, route))
1060
+
1061
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
1062
+ futures = {
1063
+ executor.submit(_run_task, task_number, seed, route): (task_number, seed, route)
1064
+ for task_number, (seed, route) in enumerate(tasks, start=1)
1065
+ }
1066
+ for future in as_completed(futures):
1067
+ task_number, seed, route = futures[future]
1068
+ mode = route.route_id if route is not None else "default"
1069
+ seed_id, route_id = _mcts_task_identity(seed, route)
1070
+ key = f"{seed_id}:{mode}"
1071
+ try:
1072
+ result = future.result()
1073
+ except Exception as exc:
1074
+ logger.warning(
1075
+ "⚠️ MCTS %s/%s failed (seed_id=%s, route_id=%s); synthesis will use fallback: %s",
1076
+ task_number,
1077
+ total_mcts_searches,
1078
+ seed_id,
1079
+ route_id,
1080
+ exc,
1081
+ )
1082
+ mode_results[key] = None
1083
+ continue
1084
+ mode_results[key] = result
1085
+ logger.info(
1086
+ "✅ MCTS %s/%s completed (seed_id=%s, route_id=%s; candidate=%s).",
1087
+ task_number,
1088
+ total_mcts_searches,
1089
+ seed_id,
1090
+ route_id,
1091
+ "yes" if result.best else "no",
1092
+ )
1093
+ if result.best:
1094
+ _complete_mode(mode, result, seed=seed, route=route)
1095
+
1096
+ if not completed_modes:
1097
+ logger.warning("⚠️ MCTS search returned no candidate; keeping latest candidate unchanged.")
1098
+ replace_patch = (
1099
+ {"latest_candidate": latest_candidate_payload}
1100
+ if latest_candidate_payload is not None
1101
+ else {}
1102
+ )
1103
+ return StageResult(
1104
+ status="degraded",
1105
+ artifact_patch=ArtifactPatch(replace=replace_patch),
1106
+ step_summary=(
1107
+ "\nIn this idea_generation action, MCTS returned no candidate "
1108
+ "and no fallback legacy path was used."
1109
+ ),
1110
+ metrics={"experience_count": 0},
1111
+ )
1112
+
1113
+ mode_order = {
1114
+ route.route_id: idx for idx, route in enumerate(IDEA_ROUTE_POLICIES)
1115
+ }
1116
+ mode_order.update({mode: idx for idx, mode in enumerate(IDEA_TASTE_PRESETS.keys())})
1117
+ completed_modes.sort(
1118
+ key=lambda item: (
1119
+ mode_order.get(item["mode"], len(mode_order)),
1120
+ str((item.get("seed") or {}).get("idea_id") or ""),
1121
+ )
1122
+ )
1123
+ raw_mode_entries = [item["entry"] for item in completed_modes]
1124
+ matrix_mode = any(route is not None for route in route_specs)
1125
+ if ligagent_pro and not matrix_mode:
1126
+ expected_modes = (
1127
+ [route.route_id for route in route_specs if route is not None]
1128
+ if any(route is not None for route in route_specs)
1129
+ else list(IDEA_TASTE_PRESETS.keys())
1130
+ )
1131
+ synthesis_result = synthesize_direction_set(
1132
+ raw_mode_entries,
1133
+ expected_modes=expected_modes,
1134
+ shared_candidates=raw_mode_entries,
1135
+ mode_results=mode_results,
1136
+ topic=topic,
1137
+ logger=logger,
1138
+ )
1139
+ mode_entries = list(synthesis_result.get("directions") or raw_mode_entries)
1140
+ else:
1141
+ synthesis_result = {
1142
+ "synthesis_mode": "mature_idea_route_matrix" if matrix_mode else "single_direction_compatibility",
1143
+ "directions": list(raw_mode_entries),
1144
+ "cross_direction_notes": [],
1145
+ "fallbacks": [],
1146
+ "direction_count": len(raw_mode_entries),
1147
+ "expected_direction_modes": [item.get("idea_taste_mode", "default") for item in raw_mode_entries],
1148
+ "matrix_cells": [
1149
+ {
1150
+ "idea_id": item.get("idea_id") or item.get("seed_id"),
1151
+ "route_id": item.get("route_id"),
1152
+ }
1153
+ for item in raw_mode_entries
1154
+ if item.get("route_id")
1155
+ ],
1156
+ }
1157
+ mode_entries = raw_mode_entries
1158
+ if ligagent_pro:
1159
+ profile_payload = (
1160
+ shared_context.get("scientific_intervention_profile")
1161
+ if isinstance(shared_context, dict)
1162
+ else None
1163
+ )
1164
+ profile_payload = profile_payload if isinstance(profile_payload, dict) else {}
1165
+ debate_result = debate_direction_set(
1166
+ mode_entries,
1167
+ topic=topic,
1168
+ survey_handoff=(shared_context.get("survey_idea_handoff") if isinstance(shared_context, dict) else None),
1169
+ profile_id=str(profile_payload.get("profile_id") or "generic_scientific"),
1170
+ profile_context=format_scientific_intervention_profile_for_prompt(profile_payload),
1171
+ runtime=_runtime(agent, ctx),
1172
+ session=session,
1173
+ workflow_name=ctx.workflow_name,
1174
+ model=materialization_model,
1175
+ logger=logger,
1176
+ max_rounds=int(get_config_value(agent.config, "debate.internal_max_rounds", 2) or 2),
1177
+ max_parallel_internal=int(get_config_value(agent.config, "debate.max_parallel_internal", 1) or 1),
1178
+ internal_prompt_limit=int(get_config_value(agent.config, "debate.internal_debate_prompt_limit", get_config_value(agent.config, "debate.prompt_char_limit", 80000)) or 80000),
1179
+ cross_seed_prompt_limit=int(get_config_value(agent.config, "debate.cross_seed_debate_prompt_limit", get_config_value(agent.config, "debate.prompt_char_limit", 60000)) or 60000),
1180
+ cross_seed_max_rounds=int(get_config_value(agent.config, "debate.cross_seed_max_rounds", 1) or 1),
1181
+ max_parallel_cross_seed=int(get_config_value(agent.config, "debate.max_parallel_cross_seed", 1) or 1),
1182
+ run_cross_seed=bool(get_config_value(agent.config, "debate.enabled", True)),
1183
+ )
1184
+ mode_entries = list(debate_result.get("directions") or mode_entries)
1185
+ else:
1186
+ debate_result = {
1187
+ "debate_mode": "single_direction_compatibility",
1188
+ "directions": list(mode_entries),
1189
+ "debate_trace": [],
1190
+ "failures": [],
1191
+ "round_count": 0,
1192
+ "direction_count": len(mode_entries),
1193
+ }
1194
+ idea_portfolio = build_idea_portfolio(
1195
+ mode_entries,
1196
+ mature_ideas,
1197
+ topic=topic,
1198
+ max_candidates_per_seed=int(get_config_value(agent.config, "portfolio.max_candidates_per_seed", 5) or 5),
1199
+ max_same_route_ratio=float(get_config_value(agent.config, "diversity.max_same_route_ratio", 0.60) or 0.60),
1200
+ debate_result=debate_result,
1201
+ has_survey_handoff=bool(isinstance(shared_context, dict) and shared_context.get("survey_idea_handoff")),
1202
+ min_independent_ideas=int(get_config_value(agent.config, "portfolio.min_independent_ideas", 2) or 2),
1203
+ primary_selection_policy=str(get_config_value(agent.config, "portfolio.primary_selection_policy", "scientific_maturity_diversity_validation") or "scientific_maturity_diversity_validation"),
1204
+ diversity_enabled=bool(get_config_value(agent.config, "diversity.enabled", True)),
1205
+ min_route_distance=float(get_config_value(agent.config, "diversity.min_route_distance", 0.35) or 0.35),
1206
+ regenerate_collapsed_routes=bool(get_config_value(agent.config, "diversity.regenerate_collapsed_routes", False)),
1207
+ preserve_high_risk_unique_candidates=bool(get_config_value(agent.config, "diversity.preserve_high_risk_unique_candidates", True)),
1208
+ )
1209
+ primary_entry = idea_portfolio.get("selected_primary_idea") or {}
1210
+ if isinstance(shared_context, dict) and shared_context.get("survey_idea_handoff"):
1211
+ portfolio_fallbacks = list(idea_portfolio.get("competitive_ideas", [])) + list(idea_portfolio.get("high_risk_ideas", []))
1212
+ fallback_pool = [
1213
+ item for item in portfolio_fallbacks
1214
+ if item.get("target_gap_ids") and item.get("invariant_status") != "violated"
1215
+ ]
1216
+ else:
1217
+ fallback_pool = list(mode_entries)
1218
+ if not primary_entry and isinstance(shared_context, dict) and shared_context.get("survey_idea_handoff") and not fallback_pool:
1219
+ logger.error("⚠️ Portfolio primary selection blocked: all candidates violate Survey invariants.")
1220
+ return StageResult(
1221
+ status="degraded",
1222
+ artifact_patch=ArtifactPatch(replace={
1223
+ "idea_portfolio": idea_portfolio,
1224
+ "data_anchored_mcts_schedule": data_schedule,
1225
+ "route_clusters": idea_portfolio.get("route_clusters", []),
1226
+ "diversity_report": idea_portfolio.get("diversity_report", {}),
1227
+ }),
1228
+ step_summary="\nIdea generation degraded because no Survey-aligned candidate satisfied the structural invariants.",
1229
+ metrics={"invariant_failure": True, "mode_count": len(mode_entries)},
1230
+ )
1231
+ best_entry = primary_entry or max(
1232
+ fallback_pool or mode_entries,
1233
+ key=lambda item: float(item.get("search_score") or 0.0),
1234
+ )
1235
+ best_entry.setdefault("search_score", 0.0)
1236
+ synthesis_result["primary_direction"] = best_entry.get("direction_mode") or best_entry.get("idea_taste_mode")
1237
+ best_entry["retrieved_core_titles"] = merge_title_lists(
1238
+ best_entry.get("retrieved_core_titles") or [],
1239
+ *[entry.get("retrieved_core_titles") or [] for entry in mode_entries],
1240
+ )
1241
+ save_candidate_payload(
1242
+ best_entry,
1243
+ agent.run_dir / "idea_candidate.json",
1244
+ logger,
1245
+ )
1246
+ final_payload = persist_final_idea(
1247
+ best_entry=best_entry,
1248
+ paper_entries=paper_entries,
1249
+ artifact=artifact,
1250
+ idea_result_path=agent.idea_result_path,
1251
+ chat_fn=materialize_chat,
1252
+ model=materialization_model,
1253
+ logger=logger,
1254
+ prompts=PROMPTS,
1255
+ persist_to_artifact=False,
1256
+ direction_synthesis=synthesis_result,
1257
+ scientific_debate=debate_result,
1258
+ mature_idea_contexts=mature_idea_contexts,
1259
+ mature_ideas=mature_ideas,
1260
+ idea_portfolio=idea_portfolio,
1261
+ introduction_max_output_tokens=int(
1262
+ get_config_value(agent.config, "agent.introduction_max_output_tokens", 25600) or 25600
1263
+ ),
1264
+ introduction_json_repair_attempts=int(
1265
+ get_config_value(agent.config, "agent.introduction_json_repair_attempts", 2) or 0
1266
+ ),
1267
+ )
1268
+ for source_entry in mode_entries:
1269
+ if not isinstance(source_entry, dict):
1270
+ continue
1271
+ mode = str(source_entry.get("direction_mode") or source_entry.get("idea_taste_mode") or "default").strip() or "default"
1272
+ direction = source_entry
1273
+ mode_payload = _build_mode_result_document(
1274
+ topic,
1275
+ direction,
1276
+ source_entry,
1277
+ artifact,
1278
+ final_payload.get("survey_binding"),
1279
+ )
1280
+ save_idea_result_payload(
1281
+ mode_payload,
1282
+ agent.run_dir
1283
+ / "mode_idea_results"
1284
+ / (
1285
+ f"{str(direction.get('idea_id') or direction.get('seed_id') or 'legacy-primary')}_"
1286
+ f"{mode}.json"
1287
+ ),
1288
+ logger,
1289
+ )
1290
+
1291
+ pareto_lines = []
1292
+ pareto_count = 0
1293
+ for item in completed_modes:
1294
+ mode = item["mode"]
1295
+ result = item["result"]
1296
+ for label, cand in result.pareto.items():
1297
+ if cand:
1298
+ pareto_count += 1
1299
+ pareto_lines.append(
1300
+ f"{mode}/{label}: {cand.node.state.title} (score={cand.evaluation.composite:.2f})"
1301
+ )
1302
+ pareto_summary = "; ".join(pareto_lines) if pareto_lines else "no Pareto picks"
1303
+ all_experiences: List[Any] = []
1304
+ all_evaluations: List[Any] = []
1305
+ for item in completed_modes:
1306
+ result = item["result"]
1307
+ all_experiences.extend(result.experiences)
1308
+ if result.best is not None:
1309
+ all_evaluations.append(result.best.to_dict()["evaluation"])
1310
+ if session is not None:
1311
+ session.set_slot("idea.latest", best_entry)
1312
+ session.set_slot("idea_result.latest", final_payload)
1313
+ step = (
1314
+ f"\nIn this idea_generation action, I ran memory-guided MCTS over '{topic}'. "
1315
+ f"{'The mature-idea × route matrix used isolated roots and contexts. ' if matrix_mode else ''}"
1316
+ f"{'All legacy idea taste modes were retained. ' if ligagent_pro and not matrix_mode else ''}"
1317
+ f"{'A direction-preserving synthesis pass retained all expected modes. ' if ligagent_pro else ''}"
1318
+ f"{'A fail-open scientific debate pass calibrated claims and scope. ' if ligagent_pro else ''}"
1319
+ f"Best idea: {best_entry['title']} (score={best_entry['search_score']:.2f}). "
1320
+ f"Pareto set -> {pareto_summary}. Persisted {len(all_experiences)} defect->fix lifts to long-term memory."
1321
+ )
1322
+ return StageResult(
1323
+ artifact_patch=ArtifactPatch(
1324
+ replace={
1325
+ "latest_candidate": best_entry,
1326
+ "mature_idea_contexts": mature_idea_contexts,
1327
+ "idea_result": final_payload,
1328
+ "ligagent_pro_candidates": mode_entries if (ligagent_pro or matrix_mode) else [],
1329
+ "fusion_result": {},
1330
+ "synthesis_result": synthesis_result,
1331
+ "debate_result": debate_result,
1332
+ "idea_portfolio": idea_portfolio,
1333
+ "route_clusters": idea_portfolio.get("route_clusters", []),
1334
+ "diversity_report": idea_portfolio.get("diversity_report", {}),
1335
+ "cross_seed_debate_result": debate_result.get("cross_seed_debate", {}),
1336
+ "idea_direction_results": final_payload.get("directions", []),
1337
+ "idea_hypotheses": [
1338
+ item.get("hypothesis", {})
1339
+ for item in final_payload.get("directions", [])
1340
+ if isinstance(item, dict)
1341
+ ],
1342
+ },
1343
+ append={
1344
+ "evaluations": all_evaluations,
1345
+ "ltm_experiences": all_experiences,
1346
+ },
1347
+ ),
1348
+ step_summary=step,
1349
+ metrics={
1350
+ "experience_count": len(all_experiences),
1351
+ "pareto_count": pareto_count,
1352
+ "search_score": best_entry["search_score"],
1353
+ "mode_count": len(mode_entries),
1354
+ "mature_idea_count": len(mature_ideas),
1355
+ "route_count": len([route for route in route_specs if route is not None]),
1356
+ "fusion_used": False,
1357
+ "synthesis_used": bool(ligagent_pro),
1358
+ },
1359
+ )
1360
+
1361
+
1362
+ def execute_reanalysis_replan_stage(agent: Any, ctx: StageContext) -> StageResult:
1363
+ artifact = agent.artifact
1364
+ logger = _logger(agent, ctx)
1365
+ runtime = _runtime(agent, ctx)
1366
+ session = _session(agent, ctx)
1367
+
1368
+ analysis_entries = artifact_get(artifact, "analysis", [])
1369
+ analysis = analysis_entries[-1] if analysis_entries else {}
1370
+ ablation_results = artifact_get(artifact, "ablation_results", [])
1371
+ mature_idea = artifact_get(artifact, "mature_idea", "")
1372
+ mature_idea_records = filter_independent_mature_ideas(
1373
+ [
1374
+ idea for idea in normalize_mature_ideas(artifact_get(artifact, "mature_ideas", []))
1375
+ if str(idea.get("maturity_status") or "").casefold() != "rejected"
1376
+ and str(idea.get("independence_status") or "").casefold() != "collapsed_duplicate"
1377
+ ],
1378
+ )
1379
+ if not mature_idea_records and mature_idea:
1380
+ mature_idea_records = normalize_mature_ideas(mature_idea)
1381
+ refinement_scope = artifact_get(artifact, "refinement_scope", "")
1382
+ root_idea = artifact_get(artifact, "root_idea", {})
1383
+ latest_candidate = artifact_get(artifact, "latest_candidate", {})
1384
+ topic = artifact_get(artifact, "topic", [])[-1]
1385
+
1386
+ seeds = mature_idea_records or [{"idea_id": "legacy-primary", "title": mature_idea, "hypothesis": mature_idea}]
1387
+ responses: List[Dict[str, Any]] = []
1388
+ for index, seed in enumerate(seeds):
1389
+ seed_text = mature_idea_legacy_text([seed]) or mature_idea or "(no mature idea yet)"
1390
+ prompt = PROMPTS["re_analysis_replan"].format(
1391
+ topic=topic,
1392
+ mature_idea=seed_text,
1393
+ refinement_scope=(seed.get("refinement_scope") or refinement_scope or "").strip(),
1394
+ analysis=pretty_json({**analysis, "mature_idea": seed}) if isinstance(analysis, dict) else str(analysis),
1395
+ ablation_results=pretty_json(ablation_results) if ablation_results else "[]",
1396
+ )
1397
+ response_i = runtime.llm_json(
1398
+ session=session,
1399
+ stage=ctx.stage_name,
1400
+ workflow_name=ctx.workflow_name,
1401
+ op_name=f"re_analysis_replan_{seed.get('idea_id') or index}",
1402
+ prompt=prompt,
1403
+ model=agent.model,
1404
+ )
1405
+ responses.append(response_i if isinstance(response_i, dict) else {})
1406
+ response = responses[0] if responses else {}
1407
+
1408
+ component_decisions = [
1409
+ decision
1410
+ for item in responses
1411
+ for decision in (item.get("component_decisions", []) if isinstance(item.get("component_decisions", []), list) else [])
1412
+ ]
1413
+ search_keywords = [str(item.get("search_keywords") or "").strip() for item in responses if str(item.get("search_keywords") or "").strip()]
1414
+ search_kw = search_keywords[0] if search_keywords else ""
1415
+
1416
+ replace_patch: Dict[str, Any] = {}
1417
+ updated_records: List[Dict[str, Any]] = []
1418
+ for seed, seed_response in zip(seeds, responses):
1419
+ updated = deepcopy(seed)
1420
+ if seed_response.get("mature_idea"):
1421
+ updated["hypothesis"] = seed_response["mature_idea"]
1422
+ updated["central_hypothesis"] = seed_response["mature_idea"]
1423
+ updated["abstract"] = seed_response["mature_idea"]
1424
+ updated.setdefault("lineage", [])
1425
+ lineage = updated["lineage"] if isinstance(updated["lineage"], list) else [updated["lineage"]]
1426
+ lineage.append({"event": "ablation_replan", "replan_id": f"replan:{updated.get('idea_id', 'legacy-primary')}"})
1427
+ updated["lineage"] = lineage
1428
+ updated["replan_id"] = f"replan:{updated.get('idea_id', 'legacy-primary')}"
1429
+ updated_records.append(updated)
1430
+ if updated_records:
1431
+ replace_patch["mature_ideas"] = updated_records
1432
+ replace_patch["mature_idea"] = mature_idea_legacy_text(updated_records)
1433
+
1434
+ append_patch: Dict[str, List[Any]] = {}
1435
+ if component_decisions:
1436
+ append_patch["component_decisions"] = list(component_decisions)
1437
+ if search_keywords:
1438
+ append_patch["retrieval_keywords"] = search_keywords
1439
+
1440
+ updated_mature_idea = str(response.get("mature_idea") or mature_idea or "").strip()
1441
+ replanned_entry = build_replanned_idea_entry(
1442
+ latest_candidate=latest_candidate,
1443
+ root_idea=root_idea,
1444
+ mature_idea=updated_mature_idea,
1445
+ component_decisions=component_decisions if isinstance(component_decisions, list) else [],
1446
+ )
1447
+ reference_batches = artifact_get(artifact, "references", [])
1448
+ latest_batch = reference_batches[-1] if reference_batches else []
1449
+ batch_list = [latest_batch] if latest_batch else reference_batches
1450
+ paper_entries = collect_paper_context_entries(artifact, batch_list)
1451
+ materialization_model = str(
1452
+ get_config_value(agent.config, "fusion.model", "") or agent.model
1453
+ ).strip()
1454
+ materialize_chat = _chat(agent, ctx, "idea_materialization")
1455
+ persist_final_idea(
1456
+ best_entry=replanned_entry,
1457
+ paper_entries=paper_entries,
1458
+ artifact=artifact,
1459
+ idea_result_path=agent.run_dir / "replanned_idea_result.json",
1460
+ chat_fn=materialize_chat,
1461
+ model=materialization_model,
1462
+ logger=logger,
1463
+ prompts=PROMPTS,
1464
+ persist_to_artifact=False,
1465
+ mature_idea_override=updated_mature_idea,
1466
+ refinement_scope_override=(refinement_scope or "").strip(),
1467
+ mature_ideas=updated_records,
1468
+ introduction_max_output_tokens=int(
1469
+ get_config_value(agent.config, "agent.introduction_max_output_tokens", 25600) or 25600
1470
+ ),
1471
+ introduction_json_repair_attempts=int(
1472
+ get_config_value(agent.config, "agent.introduction_json_repair_attempts", 2) or 0
1473
+ ),
1474
+ )
1475
+
1476
+ if session is not None and updated_records:
1477
+ session.set_slot("mature_idea.latest", updated_records)
1478
+ n_decisions = len(component_decisions)
1479
+ decision_summary = "; ".join(
1480
+ f"{d['component']}->{d['decision']}" for d in component_decisions if isinstance(d, dict)
1481
+ ) or "no component decisions"
1482
+ step = (
1483
+ f"\nIn this re_analysis_replan action, I made {n_decisions} component-level "
1484
+ f"modification(s) based on ablation evidence: [{decision_summary}]. "
1485
+ f"Updated mature idea for MCTS root node."
1486
+ )
1487
+ return StageResult(
1488
+ artifact_patch=ArtifactPatch(
1489
+ replace=replace_patch,
1490
+ append=append_patch,
1491
+ ),
1492
+ step_summary=step,
1493
+ metrics={
1494
+ "component_decisions": n_decisions,
1495
+ "updated_mature_idea": bool(updated_records),
1496
+ "updated_search_keywords": bool(search_keywords),
1497
+ "mature_idea_count": len(updated_records),
1498
+ },
1499
+ )
1500
+
1501
+
1502
+ def _workflow_phase_stage(agent: Any, ctx: StageContext, phase: str) -> StageResult:
1503
+ """Record an explicit compatibility phase without duplicating network work."""
1504
+ return StageResult(
1505
+ state_patch={"idea_workflow_phase": phase},
1506
+ step_summary=f"\nIdea workflow phase: {phase}.",
1507
+ metrics={"phase": phase},
1508
+ )
1509
+
1510
+
1511
+ def execute_mature_idea_portfolio_generation_stage(agent: Any, ctx: StageContext) -> StageResult:
1512
+ artifact = agent.artifact
1513
+ topic_history = artifact_get(artifact, "topic", [])
1514
+ topic = topic_history[-1] if topic_history else ""
1515
+ existing = normalize_mature_ideas(
1516
+ artifact_get(artifact, "mature_ideas", []),
1517
+ legacy_value=artifact_get(artifact, "mature_idea", ""),
1518
+ )
1519
+ analysis_entries = artifact_get(artifact, "analysis", [])
1520
+ survey_context = artifact_get(artifact, "survey_idea_context", {})
1521
+ survey_handoff = (
1522
+ dict(survey_context.get("handoff") or {})
1523
+ if isinstance(survey_context, dict)
1524
+ else {}
1525
+ )
1526
+ multimodal_projection = (
1527
+ survey_context.get("multimodal_evidence_projection")
1528
+ if isinstance(survey_context, dict)
1529
+ else None
1530
+ )
1531
+ mature_ideas = collect_mature_idea_sources(
1532
+ existing=existing,
1533
+ survey_handoff=survey_handoff,
1534
+ prior_candidate=artifact_get(artifact, "latest_candidate", {}),
1535
+ experiment_results=artifact_get(artifact, "ablation_results", []),
1536
+ analysis=analysis_entries[-1] if analysis_entries else {},
1537
+ max_ideas=int(get_config_value(agent.config, "run.max_mature_ideas", 12) or 12),
1538
+ allow_problem_reframing=bool(get_config_value(agent.config, "run.allow_problem_reframing", True)),
1539
+ allow_unanchored_seed=bool(get_config_value(agent.config, "run.allow_unanchored_seed", True)),
1540
+ allow_high_risk_seed=bool(get_config_value(agent.config, "run.allow_high_risk_seed", True)),
1541
+ multimodal_evidence_projection=multimodal_projection,
1542
+ )
1543
+ return StageResult(
1544
+ artifact_patch=ArtifactPatch(replace={
1545
+ "mature_ideas": mature_ideas,
1546
+ "mature_idea": mature_idea_legacy_text(mature_ideas),
1547
+ }),
1548
+ state_patch={"idea_workflow_phase": "mature_idea_portfolio_generation", "topic": topic},
1549
+ step_summary="\nGenerated the multi-source mature-idea portfolio.",
1550
+ metrics={"mature_idea_count": len(mature_ideas)},
1551
+ )
1552
+
1553
+
1554
+ def execute_mature_idea_adjudication_stage(agent: Any, ctx: StageContext) -> StageResult:
1555
+ artifact = agent.artifact
1556
+ adjudicated = filter_independent_mature_ideas(
1557
+ artifact_get(artifact, "mature_ideas", []),
1558
+ return_rejected=True,
1559
+ )
1560
+ accepted = adjudicated.get("accepted", [])
1561
+ rejected = adjudicated.get("rejected", [])
1562
+ return StageResult(
1563
+ artifact_patch=ArtifactPatch(replace={
1564
+ "mature_ideas": accepted + rejected,
1565
+ "mature_idea": mature_idea_legacy_text(accepted),
1566
+ }),
1567
+ state_patch={"idea_workflow_phase": "mature_idea_adjudication"},
1568
+ step_summary="\nAdjudicated mature-idea independence while retaining rejected records for the portfolio.",
1569
+ metrics={"independent_mature_ideas": len(accepted), "collapsed_mature_ideas": len(rejected)},
1570
+ )
1571
+
1572
+
1573
+ def execute_route_context_preparation_stage(agent: Any, ctx: StageContext) -> StageResult:
1574
+ artifact = agent.artifact
1575
+ topic_history = artifact_get(artifact, "topic", [])
1576
+ topic = topic_history[-1] if topic_history else ""
1577
+ references = collect_paper_context_entries(artifact, artifact_get(artifact, "references", []))
1578
+ survey_context = artifact_get(artifact, "survey_idea_context", {})
1579
+ handoff = (
1580
+ dict(survey_context.get("handoff") or {})
1581
+ if isinstance(survey_context, dict)
1582
+ else {}
1583
+ )
1584
+ multimodal_projection = (
1585
+ survey_context.get("multimodal_evidence_projection")
1586
+ if isinstance(survey_context, dict)
1587
+ else None
1588
+ )
1589
+ mature_ideas = artifact_get(artifact, "mature_ideas", [])
1590
+ contexts = [
1591
+ build_mature_idea_evidence_context(
1592
+ idea,
1593
+ topic=topic,
1594
+ survey_handoff=handoff if isinstance(handoff, dict) else {},
1595
+ references=references,
1596
+ ablation_results=artifact_get(artifact, "ablation_results", []),
1597
+ multimodal_evidence_projection=multimodal_projection,
1598
+ )
1599
+ for idea in mature_ideas
1600
+ if isinstance(idea, dict)
1601
+ and str(idea.get("maturity_status") or "").casefold() != "rejected"
1602
+ and str(idea.get("independence_status") or "").casefold() != "collapsed_duplicate"
1603
+ ]
1604
+ return StageResult(
1605
+ artifact_patch=ArtifactPatch(replace={"mature_idea_contexts": contexts}),
1606
+ state_patch={"idea_workflow_phase": "route_context_preparation"},
1607
+ step_summary="\nPrepared isolated evidence contexts for mature-idea route expansion.",
1608
+ metrics={"route_context_count": len(contexts)},
1609
+ )
1610
+
1611
+
1612
+ def execute_diversity_adjudication_stage(agent: Any, ctx: StageContext) -> StageResult:
1613
+ artifact = agent.artifact
1614
+ portfolio = artifact_get(artifact, "idea_portfolio", {})
1615
+ return StageResult(
1616
+ artifact_patch=ArtifactPatch(replace={
1617
+ "route_clusters": list(portfolio.get("route_clusters", [])) if isinstance(portfolio, dict) else [],
1618
+ "diversity_report": dict(portfolio.get("diversity_report", {})) if isinstance(portfolio, dict) else {},
1619
+ }),
1620
+ state_patch={"idea_workflow_phase": "diversity_adjudication"},
1621
+ step_summary="\nMaterialized route clusters and the portfolio diversity adjudication.",
1622
+ metrics={"diversity_failure": bool(portfolio.get("diversity_report", {}).get("diversity_failure")) if isinstance(portfolio, dict) else False},
1623
+ )
1624
+
1625
+
1626
+ def execute_cross_seed_debate_stage(agent: Any, ctx: StageContext) -> StageResult:
1627
+ debate_result = artifact_get(agent.artifact, "debate_result", {})
1628
+ cross_seed = debate_result.get("cross_seed_debate", {}) if isinstance(debate_result, dict) else {}
1629
+ return StageResult(
1630
+ artifact_patch=ArtifactPatch(replace={"cross_seed_debate_result": cross_seed}),
1631
+ state_patch={"idea_workflow_phase": "cross_seed_debate"},
1632
+ step_summary="\nExposed bounded cross-seed Debate comparisons for portfolio synthesis.",
1633
+ metrics={"cross_seed_pair_count": len(cross_seed.get("pairs", [])) if isinstance(cross_seed, dict) else 0},
1634
+ )
1635
+
1636
+
1637
+ def execute_portfolio_synthesis_stage(agent: Any, ctx: StageContext) -> StageResult:
1638
+ portfolio = artifact_get(agent.artifact, "idea_portfolio", {})
1639
+ return StageResult(
1640
+ state_patch={"idea_workflow_phase": "portfolio_synthesis"},
1641
+ step_summary="\nSynthesized primary, competitive, high-risk, and rejected portfolio views.",
1642
+ metrics={"portfolio_candidate_count": len(portfolio.get("competitive_ideas", [])) + 1 if isinstance(portfolio, dict) and portfolio.get("selected_primary_idea") else 0},
1643
+ )
1644
+
1645
+
1646
+ def execute_primary_idea_materialization_stage(agent: Any, ctx: StageContext) -> StageResult:
1647
+ portfolio = artifact_get(agent.artifact, "idea_portfolio", {})
1648
+ primary = portfolio.get("selected_primary_idea", {}) if isinstance(portfolio, dict) else {}
1649
+ return StageResult(
1650
+ artifact_patch=ArtifactPatch(replace={"latest_candidate": primary}) if primary else ArtifactPatch(),
1651
+ state_patch={"idea_workflow_phase": "primary_idea_materialization"},
1652
+ step_summary="\nMaterialized the portfolio primary idea through the legacy Experiment-Agent handoff.",
1653
+ metrics={"primary_materialized": bool(primary)},
1654
+ )
1655
+
1656
+
1657
+ def ka_route_stage(agent: Any, ctx: StageContext) -> StageResult:
1658
+ artifact = agent.artifact
1659
+ retrieval_keywords = artifact_get(artifact, "retrieval_keywords", [])
1660
+ search_keywords = retrieval_keywords[-1]
1661
+ topic_history = artifact_get(artifact, "topic", [])
1662
+ topic = topic_history[-1] if topic_history else search_keywords
1663
+ mature_idea = (artifact_get(artifact, "mature_idea", "") or "").strip()
1664
+ refinement_scope = (artifact_get(artifact, "refinement_scope", "") or "").strip()
1665
+ mode = "mature_idea" if mature_idea else "standard"
1666
+ return StageResult(
1667
+ state_patch={
1668
+ "mode": mode,
1669
+ "topic": topic,
1670
+ "search_keywords": search_keywords,
1671
+ "mature_idea": mature_idea,
1672
+ "refinement_scope": refinement_scope,
1673
+ "rag_query": "",
1674
+ "rag_hits": [],
1675
+ "survey_contents": [],
1676
+ "citation_titles": [],
1677
+ "citation_references": [],
1678
+ "ranked_keynotes": [],
1679
+ "curated_references": [],
1680
+ "retrieval_source": "",
1681
+ "summary": "",
1682
+ },
1683
+ next_stage="ka_query_generation",
1684
+ metrics={"mode": mode},
1685
+ )
1686
+
1687
+
1688
+ def ka_query_generation_stage(agent: Any, ctx: StageContext) -> StageResult:
1689
+ logger = _logger(agent, ctx)
1690
+
1691
+ mode = ctx.state["mode"]
1692
+ mature_idea = ctx.state.get("mature_idea", "")
1693
+ refinement_scope = ctx.state.get("refinement_scope", "")
1694
+ topic = ctx.state["topic"]
1695
+ search_keywords = ctx.state["search_keywords"]
1696
+ query_topic = search_keywords or topic
1697
+ rag_query = generate_rag_query(
1698
+ query_topic,
1699
+ PROMPTS,
1700
+ _chat(agent, ctx, "rag_query_generation"),
1701
+ agent.model,
1702
+ logger,
1703
+ mature_idea=mature_idea if mature_idea else None,
1704
+ refinement_scope=refinement_scope if refinement_scope else None,
1705
+ )
1706
+ logger.info(
1707
+ "🔎 Generated RAG Query%s: %s",
1708
+ " (mature idea)" if mode == "mature_idea" else "",
1709
+ rag_query,
1710
+ )
1711
+ return StageResult(
1712
+ state_patch={
1713
+ "rag_query": rag_query,
1714
+ },
1715
+ metrics={"rag_query_length": len(rag_query)},
1716
+ )
1717
+
1718
+
1719
+ def ka_outcome_rag_stage(agent: Any, ctx: StageContext) -> StageResult:
1720
+ session = _session(agent, ctx)
1721
+ rag_query = ctx.state["rag_query"]
1722
+ rag_hits = retrieve_outcome_rag(
1723
+ query=rag_query,
1724
+ top_k=5,
1725
+ paper_repository=agent.paper_repository,
1726
+ logger=_logger(agent, ctx),
1727
+ )
1728
+ survey_contents = collect_rag_contents(rag_hits)
1729
+ citation_references = collect_rag_citation_references(rag_hits)
1730
+ citation_titles = collect_rag_citations(rag_hits)
1731
+ if session is not None:
1732
+ session.set_slot("rag.latest", {"query": rag_query, "hits": rag_hits})
1733
+ return StageResult(
1734
+ artifact_patch=ArtifactPatch(
1735
+ append={
1736
+ "rag_query": [rag_query],
1737
+ "rag_hits": [{"query": rag_query, "hits": rag_hits}],
1738
+ "rag_contents": [survey_contents],
1739
+ }
1740
+ ),
1741
+ state_patch={
1742
+ "rag_hits": rag_hits,
1743
+ "survey_contents": survey_contents,
1744
+ "citation_titles": citation_titles,
1745
+ "citation_references": citation_references,
1746
+ },
1747
+ metrics={
1748
+ "rag_hits": len(rag_hits),
1749
+ "citation_titles": len(citation_titles),
1750
+ },
1751
+ )
1752
+
1753
+
1754
+ def ka_keynote_ranking_stage(agent: Any, ctx: StageContext) -> StageResult:
1755
+ logger = _logger(agent, ctx)
1756
+ topic = ctx.state["topic"]
1757
+ rag_query = ctx.state["rag_query"]
1758
+ citation_references = ctx.state.get("citation_references", [])
1759
+ keynote_references = agent.paper_repository.retrieve_keynotes_by_paper_ids(citation_references)
1760
+ scored_keynotes = []
1761
+ for reference in iter_with_progress(
1762
+ keynote_references,
1763
+ description="Scoring keynotes",
1764
+ total=len(keynote_references),
1765
+ ):
1766
+ scored_keynotes.append(
1767
+ _score_keynote_reference(agent, ctx, topic, rag_query, {**reference, "source_keywords": rag_query})
1768
+ )
1769
+ scored_keynotes.sort(key=lambda item: int(item["score"]), reverse=True)
1770
+ retrieval_source = "survey_keynotes"
1771
+ if logger is not None:
1772
+ for idx, reference in enumerate(scored_keynotes, 1):
1773
+ title = str(reference.get("title") or reference.get("paper_title") or "").strip()
1774
+ logger.info("📚 Survey Paper %d Title: %s", idx, title)
1775
+ logger.info("📚 Survey Paper %d Score: %s", idx, int(reference["score"]))
1776
+ return StageResult(
1777
+ state_patch={
1778
+ "ranked_keynotes": scored_keynotes,
1779
+ "retrieval_source": retrieval_source,
1780
+ },
1781
+ metrics={
1782
+ "keynote_references": len(keynote_references),
1783
+ "scored_keynotes": len(scored_keynotes),
1784
+ "retrieval_source": retrieval_source,
1785
+ },
1786
+ )
1787
+
1788
+
1789
+ def ka_reference_selection_stage(agent: Any, ctx: StageContext) -> StageResult:
1790
+ session = _session(agent, ctx)
1791
+ topic = ctx.state["topic"]
1792
+ rag_query = ctx.state["rag_query"]
1793
+ rag_hits = ctx.state.get("rag_hits", [])
1794
+ ranked_keynotes = list(ctx.state.get("ranked_keynotes", []) or [])
1795
+ keynote_keep_top_k = min(1, int(get_config_value(agent.config, "agent.paper_keynote_keep_top_k", 5)))
1796
+ top_keynotes = ranked_keynotes[:keynote_keep_top_k]
1797
+ remaining_keynotes = ranked_keynotes[keynote_keep_top_k:]
1798
+ curated_references = [
1799
+ _compress_single_keynote_reference(agent, ctx, topic, rag_query, reference)
1800
+ for reference in top_keynotes
1801
+ ]
1802
+ if remaining_keynotes:
1803
+ curated_references.append(
1804
+ _summarize_remaining_keynotes(agent, ctx, topic, rag_query, remaining_keynotes)
1805
+ )
1806
+ mode = ctx.state["mode"]
1807
+ retrieval_source = ctx.state.get("retrieval_source", "")
1808
+ top_kept = len(top_keynotes)
1809
+ rolled_up = len(remaining_keynotes)
1810
+ if mode == "mature_idea":
1811
+ summary = (
1812
+ f"\nIn this knowledge_aquisition action, I used the mature idea to generate "
1813
+ f"a focused query '{rag_query}', retrieved {len(rag_hits)} survey hits, "
1814
+ f"and materialized {len(curated_references)} survey-cited paper capsules "
1815
+ f"via {retrieval_source or 'survey keynotes'} "
1816
+ f"({top_kept} individual paper summaries, {rolled_up} papers rolled into one summary)."
1817
+ )
1818
+ else:
1819
+ summary = (
1820
+ f"\nIn this knowledge_aquisition action, I generated a focused query '{rag_query}', "
1821
+ f"retrieved {len(rag_hits)} survey hits, and materialized {len(curated_references)} "
1822
+ f"survey-cited paper capsules via {retrieval_source or 'survey keynotes'} "
1823
+ f"({top_kept} individual paper summaries, {rolled_up} papers rolled into one summary)."
1824
+ )
1825
+ if session is not None:
1826
+ session.set_slot("references.latest", curated_references)
1827
+ return StageResult(
1828
+ artifact_patch=ArtifactPatch(
1829
+ append={"references": [curated_references]},
1830
+ ),
1831
+ state_patch={
1832
+ "curated_references": curated_references,
1833
+ "summary": summary,
1834
+ },
1835
+ metrics={"curated_references": len(curated_references)},
1836
+ )