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,1238 @@
1
+ """Fail-open, post-synthesis scientific debate for Idea Agent directions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from copy import deepcopy
7
+ from itertools import combinations
8
+ import json
9
+ from typing import Any, Dict, Iterable, List, Mapping, Optional
10
+
11
+ from src.agents.idea_agent.agent.prompts.scientific_debate import SCIENTIFIC_DEBATE_PROMPT
12
+ from src.agents.idea_agent.utils.core.json_utils import pretty_json
13
+ from src.agents.idea_agent.utils.mcts.scientific_intervention_ontology import (
14
+ detect_profile_drift,
15
+ format_scientific_intervention_profile_for_prompt,
16
+ get_scientific_intervention_profile,
17
+ )
18
+ from src.agents.idea_agent.utils.workflow.idea_contract import normalize_idea_contract
19
+ from src.agents.idea_agent.utils.workflow.idea_diversity import compare_mature_ideas
20
+
21
+
22
+ DEBATE_ROUNDS = (1, 2)
23
+ DEBATE_PROMPT_CHAR_LIMIT = 120_000
24
+ INTERNAL_DEBATE_PROMPT_LIMIT = 80_000
25
+ CROSS_SEED_DEBATE_PROMPT_LIMIT = 60_000
26
+ IMMUTABLE_IDENTITY_FIELDS = (
27
+ "idea_id",
28
+ "seed_id",
29
+ "target_gap_ids",
30
+ "route_id",
31
+ "route_signature",
32
+ "lineage",
33
+ )
34
+ _DEBATE_CANDIDATE_FIELDS = (
35
+ "direction_mode",
36
+ "idea_taste_mode",
37
+ "direction_summary",
38
+ "title",
39
+ "abstract",
40
+ "core_contribution",
41
+ "central_hypothesis",
42
+ "mechanism_or_relation",
43
+ "expected_mechanism",
44
+ "discriminating_observation",
45
+ "boundary_or_failure_condition",
46
+ "claim_scope",
47
+ "assumptions",
48
+ "alternative_explanations",
49
+ "gap_alignment",
50
+ "evidence_requirement",
51
+ "evidence_basis",
52
+ "falsifier",
53
+ "risks",
54
+ "target_gap_ids",
55
+ "scientific_object",
56
+ "intervention_or_transformation",
57
+ "independence_status",
58
+ "refinement_scope",
59
+ "scientific_intervention",
60
+ )
61
+ _DEBATE_INTERVENTION_FIELDS = (
62
+ "profile_id",
63
+ "profile_label",
64
+ "contribution_mode",
65
+ "scientific_object_schema",
66
+ "object_roles",
67
+ "allowed_operations",
68
+ "evidence_obligations",
69
+ "boundary_obligations",
70
+ "measurement_or_observation_roles",
71
+ "route_contract_incomplete_fields",
72
+ "route_contract_noop_fields",
73
+ "route_contract_parent_values",
74
+ )
75
+ ALLOWED_REVISION_FIELDS = (
76
+ "abstract",
77
+ "core_contribution",
78
+ "central_hypothesis",
79
+ "scientific_object",
80
+ "intervention_or_transformation",
81
+ "mechanism_or_relation",
82
+ "expected_mechanism",
83
+ "discriminating_observation",
84
+ "boundary_or_failure_condition",
85
+ "claim_scope",
86
+ "assumptions",
87
+ "alternative_explanations",
88
+ "gap_alignment",
89
+ "evidence_requirement",
90
+ "evidence_basis",
91
+ "risks",
92
+ "falsifier",
93
+ )
94
+ DEBATE_STATUSES = {
95
+ "SCIENTIFICALLY_QUALIFIED",
96
+ "SCIENTIFICALLY_QUALIFIED_WITH_UNCERTAINTY",
97
+ "NEEDS_SCOPE_REDUCTION",
98
+ "PROFILE_DRIFT",
99
+ "REQUIRES_REVIEW",
100
+ "LOWER_CONFIDENCE",
101
+ }
102
+ EXPERIMENT_FIELDS = (
103
+ "experiments",
104
+ "experiment_design",
105
+ "predicted_results",
106
+ "sample_size",
107
+ "statistical_test",
108
+ "instrument_configuration",
109
+ "ablation_plan",
110
+ "failure_repair_plan",
111
+ )
112
+ _ROUTE_CONTRACT_FIELDS = {
113
+ "premise_inversion": ("central_hypothesis",),
114
+ "object_substitution": ("scientific_object",),
115
+ "mechanism_replacement": ("mechanism_or_relation", "expected_mechanism"),
116
+ "representation_shift": ("intervention_or_transformation",),
117
+ "verification_reversal": ("discriminating_observation",),
118
+ }
119
+
120
+
121
+ def _text(value: Any) -> str:
122
+ return str(value or "").strip()
123
+
124
+
125
+ def _list(value: Any) -> List[str]:
126
+ if value is None:
127
+ return []
128
+ values = value if isinstance(value, (list, tuple)) else [value]
129
+ return [item for item in (_text(item) for item in values) if item]
130
+
131
+
132
+ def _profile_id(direction: Mapping[str, Any], profile_id: str = "") -> str:
133
+ if profile_id:
134
+ return _text(profile_id).lower()
135
+ intervention = direction.get("scientific_intervention")
136
+ if isinstance(intervention, Mapping):
137
+ return _text(intervention.get("profile_id") or "generic_scientific").lower()
138
+ return "generic_scientific"
139
+
140
+
141
+ def _gap_ids(direction: Mapping[str, Any]) -> List[str]:
142
+ raw = direction.get("target_gap_ids")
143
+ return _list(raw)
144
+
145
+
146
+ def _status(value: Any, default: str) -> str:
147
+ normalized = _text(value).upper()
148
+ return normalized if normalized in DEBATE_STATUSES else default
149
+
150
+
151
+ def _severity(value: Any) -> str:
152
+ normalized = _text(value).lower()
153
+ return normalized if normalized in {"none", "minor", "moderate", "major"} else "none"
154
+
155
+
156
+ def _sanitize_candidate(candidate: Mapping[str, Any]) -> Dict[str, Any]:
157
+ sanitized = deepcopy(dict(candidate))
158
+ for field_name in EXPERIMENT_FIELDS:
159
+ sanitized.pop(field_name, None)
160
+ return sanitized
161
+
162
+
163
+ def _compact_prompt_value(value: Any, *, max_text: int = 4_000, max_items: int = 8) -> Any:
164
+ """Bound values embedded in Debate prompts without changing the candidate."""
165
+
166
+ if isinstance(value, str):
167
+ text = value.strip()
168
+ return text if len(text) <= max_text else text[: max_text - 3].rstrip() + "..."
169
+ if isinstance(value, Mapping):
170
+ return {
171
+ str(key): _compact_prompt_value(item, max_text=max_text, max_items=max_items)
172
+ for key, item in list(value.items())[:max_items]
173
+ }
174
+ if isinstance(value, (list, tuple)):
175
+ return [
176
+ _compact_prompt_value(item, max_text=max_text, max_items=max_items)
177
+ for item in list(value)[:max_items]
178
+ ]
179
+ return value
180
+
181
+
182
+ def _is_populated(value: Any) -> bool:
183
+ return value not in (None, "", [], {})
184
+
185
+
186
+ def _contract_value_signature(value: Any) -> str:
187
+ if isinstance(value, (Mapping, list, tuple, set, frozenset)):
188
+ try:
189
+ return json.dumps(value, sort_keys=True, ensure_ascii=False, default=str)
190
+ except (TypeError, ValueError):
191
+ pass
192
+ return " ".join(_text(value).casefold().split())
193
+
194
+
195
+ def _has_route_material_change(
196
+ route_id: str,
197
+ candidate: Mapping[str, Any],
198
+ parent_values: Mapping[str, Any],
199
+ ) -> bool:
200
+ route_fields = _ROUTE_CONTRACT_FIELDS.get(route_id, ())
201
+ return any(
202
+ _is_populated(candidate.get(field_name))
203
+ and (
204
+ not _is_populated(parent_values.get(field_name))
205
+ or _contract_value_signature(candidate.get(field_name))
206
+ != _contract_value_signature(parent_values.get(field_name))
207
+ )
208
+ for field_name in route_fields
209
+ )
210
+
211
+
212
+ def effective_scientific_contract(direction: Mapping[str, Any]) -> Dict[str, Any]:
213
+ """Resolve the scientific fields Debate should evaluate without losing lineage.
214
+
215
+ MCTS payloads may expose a field at the top level, within the state-level
216
+ hypothesis contract, or only on the mature seed. Debate should read the
217
+ same effective view across those representations, while revisions continue
218
+ to obey the immutable identity constraints below.
219
+ """
220
+
221
+ effective: Dict[str, Any] = {}
222
+ intervention = direction.get("scientific_intervention")
223
+ intervention = intervention if isinstance(intervention, Mapping) else {}
224
+ seed_records: List[Mapping[str, Any]] = []
225
+ for field_name in ("mature_idea_record", "mature_idea", "seed", "root_idea"):
226
+ source = direction.get(field_name)
227
+ if isinstance(source, Mapping):
228
+ seed_records.append(source)
229
+ embedded_seed = intervention.get("mature_idea_record")
230
+ if isinstance(embedded_seed, Mapping):
231
+ seed_records.append(embedded_seed)
232
+
233
+ seed_aliases = {
234
+ "central_hypothesis": "hypothesis",
235
+ "mechanism_or_relation": "mechanism",
236
+ "claim_scope": "refinement_scope",
237
+ }
238
+ for seed in seed_records:
239
+ for field_name in _DEBATE_CANDIDATE_FIELDS:
240
+ value = seed.get(field_name)
241
+ if not _is_populated(value):
242
+ alias = seed_aliases.get(field_name)
243
+ value = seed.get(alias) if alias else None
244
+ if _is_populated(value):
245
+ effective.setdefault(field_name, deepcopy(value))
246
+
247
+ nested_contract = intervention.get("hypothesis_contract")
248
+ if isinstance(nested_contract, Mapping):
249
+ for field_name, value in nested_contract.items():
250
+ if _is_populated(value):
251
+ effective[field_name] = deepcopy(value)
252
+
253
+ for field_name in _DEBATE_CANDIDATE_FIELDS:
254
+ value = direction.get(field_name)
255
+ if _is_populated(value):
256
+ effective[field_name] = deepcopy(value)
257
+ effective["scientific_intervention"] = dict(intervention)
258
+ return effective
259
+
260
+
261
+ def _debate_candidate_view(direction: Mapping[str, Any]) -> Dict[str, Any]:
262
+ effective = effective_scientific_contract(direction)
263
+ view: Dict[str, Any] = {}
264
+ for field_name in _DEBATE_CANDIDATE_FIELDS:
265
+ if field_name not in effective:
266
+ continue
267
+ value = effective.get(field_name)
268
+ if field_name == "scientific_intervention" and isinstance(value, Mapping):
269
+ value = {
270
+ key: value.get(key)
271
+ for key in _DEBATE_INTERVENTION_FIELDS
272
+ if key in value
273
+ }
274
+ view[field_name] = _compact_prompt_value(value)
275
+ return view
276
+
277
+
278
+ def _immutable_identity_snapshot(candidate: Mapping[str, Any]) -> Dict[str, Any]:
279
+ return {field: deepcopy(candidate.get(field)) for field in IMMUTABLE_IDENTITY_FIELDS if field in candidate}
280
+
281
+
282
+ def _identity_violations(
283
+ original: Mapping[str, Any],
284
+ attempted: Any,
285
+ revised: Mapping[str, Any],
286
+ ) -> List[Dict[str, Any]]:
287
+ if not isinstance(attempted, Mapping):
288
+ return []
289
+ violations: List[Dict[str, Any]] = []
290
+ for field in IMMUTABLE_IDENTITY_FIELDS:
291
+ if field not in attempted:
292
+ continue
293
+ before = deepcopy(original.get(field))
294
+ proposed = deepcopy(attempted.get(field))
295
+ if proposed == before:
296
+ continue
297
+ violations.append(
298
+ {
299
+ "violation_type": "gap_mapping_violation" if field == "target_gap_ids" else "identity_violation",
300
+ "field": field,
301
+ "attempted_value": proposed,
302
+ "restored_value": deepcopy(revised.get(field, before)),
303
+ }
304
+ )
305
+ return violations
306
+
307
+
308
+ def _debate_handoff_view(
309
+ survey_handoff: Any,
310
+ target_gap_ids: Iterable[str],
311
+ ) -> Dict[str, Any]:
312
+ if not isinstance(survey_handoff, Mapping):
313
+ return {}
314
+ target_ids = set(_list(target_gap_ids))
315
+ view: Dict[str, Any] = {}
316
+ for field_name in ("survey_run_id", "project_id", "topic", "profile_resolution", "scope", "constraints"):
317
+ if field_name in survey_handoff:
318
+ view[field_name] = _compact_prompt_value(survey_handoff.get(field_name))
319
+ gaps = []
320
+ for gap in survey_handoff.get("gaps", []):
321
+ if not isinstance(gap, Mapping):
322
+ continue
323
+ gap_id = _text(gap.get("gap_id"))
324
+ if target_ids and gap_id not in target_ids:
325
+ continue
326
+ gaps.append(
327
+ _compact_prompt_value(
328
+ {
329
+ key: gap.get(key)
330
+ for key in (
331
+ "gap_id",
332
+ "subhypothesis_id",
333
+ "gap_kind",
334
+ "target_slot",
335
+ "statement",
336
+ "status",
337
+ "priority",
338
+ "support_level",
339
+ "gap_audit",
340
+ )
341
+ if key in gap
342
+ }
343
+ )
344
+ )
345
+ if len(gaps) >= 8:
346
+ break
347
+ if gaps:
348
+ view["gaps"] = gaps
349
+ triage = survey_handoff.get("gap_triage")
350
+ if isinstance(triage, Mapping):
351
+ rows = [
352
+ row for row in triage.get("gaps", [])
353
+ if isinstance(row, Mapping)
354
+ and (not target_ids or _text(row.get("gap_id")) in target_ids)
355
+ ]
356
+ if rows:
357
+ view["gap_triage"] = _compact_prompt_value({"gaps": rows[:8]})
358
+ return view
359
+
360
+
361
+ def _baseline_concerns(
362
+ direction: Mapping[str, Any],
363
+ *,
364
+ profile_drift: Mapping[str, Any],
365
+ round_number: int,
366
+ ) -> Dict[str, Any]:
367
+ candidate = effective_scientific_contract(direction)
368
+ gap_ids = _gap_ids(candidate)
369
+ concerns: List[str] = []
370
+ alternatives: List[str] = []
371
+ changed_field: List[str] = []
372
+ actual_missing_fields: List[str] = []
373
+ profile_drift_fields: List[str] = []
374
+ severity = "none"
375
+ required_revision = ""
376
+ if round_number == 1:
377
+ question_type = "scientific_consistency"
378
+ if not gap_ids:
379
+ concerns.append("The candidate has no explicit target_gap_ids.")
380
+ changed_field.append("target_gap_ids")
381
+ actual_missing_fields.append("target_gap_ids")
382
+ severity = "major"
383
+ required_revision = "Keep the candidate but mark its scope as needing Gap alignment review."
384
+ if not _text(candidate.get("central_hypothesis")):
385
+ concerns.append("The central hypothesis cannot be read as a standalone claim.")
386
+ changed_field.append("central_hypothesis")
387
+ actual_missing_fields.append("central_hypothesis")
388
+ severity = "major"
389
+ required_revision = "State one direction-specific, uncertainty-calibrated hypothesis."
390
+ if not _text(candidate.get("mechanism_or_relation") or candidate.get("expected_mechanism")):
391
+ concerns.append("The mechanism or relation is underspecified.")
392
+ changed_field.append("mechanism_or_relation")
393
+ actual_missing_fields.append("mechanism_or_relation")
394
+ severity = "major"
395
+ required_revision = "Name the profile-native mechanism or relation that links intervention and observation."
396
+ if not _list(candidate.get("assumptions")):
397
+ concerns.append("The candidate does not expose its operating assumptions.")
398
+ changed_field.append("assumptions")
399
+ actual_missing_fields.append("assumptions")
400
+ severity = max(severity, "moderate", key=("none", "minor", "moderate", "major").index)
401
+ required_revision = "State the minimum assumptions needed for the mechanism."
402
+ if not _text(candidate.get("refinement_scope") or candidate.get("claim_scope")):
403
+ concerns.append("The refinement scope is not explicit.")
404
+ changed_field.append("claim_scope")
405
+ actual_missing_fields.append("claim_scope")
406
+ severity = max(severity, "moderate", key=("none", "minor", "moderate", "major").index)
407
+ required_revision = "Bound the claim to the smallest defensible refinement scope."
408
+ if _text(candidate.get("independence_status")).casefold() in {"collapsed_duplicate", "duplicate", "rejected"}:
409
+ concerns.append("The candidate was marked non-independent by mature-idea adjudication.")
410
+ changed_field.append("independence_status")
411
+ severity = "major"
412
+ required_revision = "Preserve the candidate identity and explain its substantive route difference."
413
+ if profile_drift.get("drift_severity") == "material":
414
+ concerns.append("A CS/ML concept appears to be the primary contribution for a non-computational profile.")
415
+ changed_field.append("mechanism_or_relation")
416
+ profile_drift_fields.append("mechanism_or_relation")
417
+ severity = "major"
418
+ required_revision = "Rewrite the primary contribution using the selected profile's native mechanism or process."
419
+ elif profile_drift.get("drift_severity") == "soft":
420
+ concerns.append("CS/ML terms appear as secondary analysis tools.")
421
+ severity = "moderate"
422
+ required_revision = "Keep the tool secondary and state the domain-native scientific claim first."
423
+ intervention = candidate.get("scientific_intervention")
424
+ incomplete_route_fields = (
425
+ _list(intervention.get("route_contract_incomplete_fields"))
426
+ if isinstance(intervention, Mapping)
427
+ else []
428
+ )
429
+ if incomplete_route_fields:
430
+ concerns.append(
431
+ "The route-defining fields were inherited from the parent instead of being generated by this route: "
432
+ + ", ".join(incomplete_route_fields)
433
+ + "."
434
+ )
435
+ changed_field.extend(incomplete_route_fields)
436
+ severity = "major"
437
+ required_revision = "Supply a route-specific scientific field rather than relying on the parent contract."
438
+ noop_route_fields = (
439
+ _list(intervention.get("route_contract_noop_fields"))
440
+ if isinstance(intervention, Mapping)
441
+ else []
442
+ )
443
+ if noop_route_fields:
444
+ concerns.append(
445
+ "The route-defining fields only restate the parent contract instead of making a substantive change: "
446
+ + ", ".join(noop_route_fields)
447
+ + "."
448
+ )
449
+ changed_field.extend(noop_route_fields)
450
+ severity = "major"
451
+ required_revision = "Replace the inherited route field with a substantively different profile-native value."
452
+ else:
453
+ question_type = "scope_and_alternatives"
454
+ claim_scope = _text(candidate.get("claim_scope"))
455
+ if not claim_scope:
456
+ concerns.append("The candidate does not state a boundary for its claim.")
457
+ changed_field.append("claim_scope")
458
+ severity = "moderate"
459
+ required_revision = "Add a bounded validity or transfer scope."
460
+ if not _text(candidate.get("boundary_or_failure_condition")):
461
+ concerns.append("No explicit boundary or failure condition is stated.")
462
+ changed_field.append("boundary_or_failure_condition")
463
+ severity = max(severity, "moderate", key=("none", "minor", "moderate", "major").index)
464
+ required_revision = "Add the smallest meaningful failure or validity boundary."
465
+ alternatives = _list(candidate.get("alternative_explanations"))
466
+ if not alternatives:
467
+ alternatives = ["A competing mechanism or confounder could explain the same observation."]
468
+ return {
469
+ "question_type": question_type,
470
+ "scientific_concern": " ".join(concerns),
471
+ "opponent_concern": alternatives[0] if alternatives else "",
472
+ "severity": severity,
473
+ "required_revision": required_revision,
474
+ "changed_field": list(dict.fromkeys(changed_field)),
475
+ "actual_missing_fields": list(dict.fromkeys(actual_missing_fields)),
476
+ "profile_drift_fields": list(dict.fromkeys(profile_drift_fields)),
477
+ "alternative_explanations": alternatives,
478
+ }
479
+
480
+
481
+ def _apply_revision(
482
+ original: Mapping[str, Any],
483
+ raw_revision: Any,
484
+ *,
485
+ direction_mode: str,
486
+ original_gap_ids: List[str],
487
+ ) -> tuple[Dict[str, Any], List[str], bool, bool]:
488
+ revised = _sanitize_candidate(original)
489
+ if isinstance(raw_revision, Mapping):
490
+ for field_name in ALLOWED_REVISION_FIELDS:
491
+ if field_name in raw_revision and raw_revision[field_name] not in (None, ""):
492
+ value = deepcopy(raw_revision[field_name])
493
+ if field_name == "scientific_object" and not isinstance(value, Mapping):
494
+ description = _text(value)
495
+ if not description:
496
+ continue
497
+ value = {"description": description}
498
+ revised[field_name] = value
499
+ changed_fields = [
500
+ field_name
501
+ for field_name in ALLOWED_REVISION_FIELDS
502
+ if revised.get(field_name) != original.get(field_name)
503
+ ]
504
+ gap_violation = False
505
+ revised["direction_mode"] = direction_mode
506
+ revised["target_gap_ids"] = list(original_gap_ids)
507
+ if isinstance(raw_revision, Mapping) and "target_gap_ids" in raw_revision:
508
+ proposed_gap_ids = _list(raw_revision.get("target_gap_ids"))
509
+ if proposed_gap_ids != original_gap_ids:
510
+ gap_violation = True
511
+ changed_fields.append("target_gap_ids")
512
+ intervention = revised.get("scientific_intervention")
513
+ if isinstance(intervention, Mapping):
514
+ intervention = dict(intervention)
515
+ incomplete_route_fields = _list(intervention.get("route_contract_incomplete_fields"))
516
+ noop_route_fields = _list(intervention.get("route_contract_noop_fields"))
517
+ parent_values = intervention.get("route_contract_parent_values")
518
+ parent_values = parent_values if isinstance(parent_values, Mapping) else {}
519
+ route_id = _text(original.get("route_id") or intervention.get("route_id"))
520
+ if _has_route_material_change(route_id, revised, parent_values):
521
+ incomplete_route_fields = []
522
+ noop_route_fields = []
523
+ if incomplete_route_fields:
524
+ intervention["route_contract_incomplete_fields"] = incomplete_route_fields
525
+ else:
526
+ intervention.pop("route_contract_incomplete_fields", None)
527
+ if noop_route_fields:
528
+ intervention["route_contract_noop_fields"] = noop_route_fields
529
+ else:
530
+ intervention.pop("route_contract_noop_fields", None)
531
+ if incomplete_route_fields or noop_route_fields:
532
+ intervention["route_contract_parent_values"] = dict(parent_values)
533
+ else:
534
+ intervention.pop("route_contract_parent_values", None)
535
+ revised["scientific_intervention"] = intervention
536
+ try:
537
+ normalized = normalize_idea_contract(revised, keep_extra=True)
538
+ except (TypeError, ValueError):
539
+ normalized = revised
540
+ normalized["direction_mode"] = direction_mode
541
+ normalized["target_gap_ids"] = list(original_gap_ids)
542
+ for field_name in EXPERIMENT_FIELDS:
543
+ normalized.pop(field_name, None)
544
+ return normalized, list(dict.fromkeys(changed_fields)), bool(changed_fields), gap_violation
545
+
546
+
547
+ def _status_from_baseline(baseline: Mapping[str, Any]) -> str:
548
+ if baseline.get("severity") == "major":
549
+ if baseline.get("profile_drift_fields") and not baseline.get("actual_missing_fields"):
550
+ return "PROFILE_DRIFT"
551
+ return "NEEDS_SCOPE_REDUCTION"
552
+ if baseline.get("severity") in {"minor", "moderate"}:
553
+ return "SCIENTIFICALLY_QUALIFIED_WITH_UNCERTAINTY"
554
+ return "SCIENTIFICALLY_QUALIFIED"
555
+
556
+
557
+ def _event_payload(
558
+ payload: Mapping[str, Any],
559
+ *,
560
+ round_number: int,
561
+ direction_mode: str,
562
+ candidate: Mapping[str, Any],
563
+ baseline: Mapping[str, Any],
564
+ final_scope: str,
565
+ final_status: str,
566
+ revision_applied: bool,
567
+ changed_fields: List[str],
568
+ ) -> Dict[str, Any]:
569
+ return {
570
+ "round": round_number,
571
+ "direction_mode": direction_mode,
572
+ "question_type": _text(payload.get("question_type") or baseline.get("question_type")),
573
+ "target_claim": _text(payload.get("target_claim") or candidate.get("central_hypothesis")),
574
+ "scientific_concern": _text(payload.get("scientific_concern") or baseline.get("scientific_concern")),
575
+ "opponent_concern": _text(payload.get("opponent_concern") or baseline.get("opponent_concern")),
576
+ "severity": _severity(payload.get("severity") or baseline.get("severity")),
577
+ "required_revision": _text(payload.get("required_revision") or baseline.get("required_revision")),
578
+ "revision_applied": bool(revision_applied or payload.get("revision_applied", False)),
579
+ "changed_field": _list(payload.get("changed_field") or changed_fields),
580
+ "actual_missing_fields": _list(baseline.get("actual_missing_fields")),
581
+ "profile_drift_fields": _list(baseline.get("profile_drift_fields")),
582
+ "alternative_explanations": _list(
583
+ payload.get("alternative_explanations") or baseline.get("alternative_explanations")
584
+ ),
585
+ "final_scope": _text(payload.get("final_scope") or final_scope),
586
+ "final_status": final_status,
587
+ }
588
+
589
+
590
+ def _fallback_event(
591
+ *,
592
+ round_number: int,
593
+ direction_mode: str,
594
+ candidate: Mapping[str, Any],
595
+ baseline: Mapping[str, Any],
596
+ ) -> Dict[str, Any]:
597
+ default_status = _status_from_baseline(baseline)
598
+ return _event_payload(
599
+ baseline,
600
+ round_number=round_number,
601
+ direction_mode=direction_mode,
602
+ candidate=candidate,
603
+ baseline=baseline,
604
+ final_scope=_text(candidate.get("claim_scope")),
605
+ final_status=default_status,
606
+ revision_applied=False,
607
+ changed_fields=[],
608
+ )
609
+
610
+
611
+ def _debate_prompt(
612
+ *,
613
+ topic: str,
614
+ round_number: int,
615
+ direction: Mapping[str, Any],
616
+ profile_id: str,
617
+ profile_context: str,
618
+ survey_handoff: Any,
619
+ profile_drift: Mapping[str, Any],
620
+ baseline: Mapping[str, Any],
621
+ prompt_limit: int = DEBATE_PROMPT_CHAR_LIMIT,
622
+ ) -> str:
623
+ prompt = SCIENTIFIC_DEBATE_PROMPT.format(
624
+ topic=topic or "unspecified topic",
625
+ round=round_number,
626
+ question_type=baseline.get("question_type") or "scientific_consistency",
627
+ direction_mode=_text(direction.get("direction_mode") or direction.get("idea_taste_mode")),
628
+ direction_summary=_text(direction.get("direction_summary")),
629
+ profile_id=profile_id,
630
+ profile_context=profile_context or "Use the profile-native object schema already attached to the candidate.",
631
+ survey_handoff=pretty_json(_debate_handoff_view(survey_handoff, _gap_ids(direction))),
632
+ profile_drift=pretty_json(profile_drift),
633
+ candidate=pretty_json(_debate_candidate_view(direction)),
634
+ round_checks=pretty_json(dict(baseline)),
635
+ )
636
+ limit = max(2_000, int(prompt_limit or DEBATE_PROMPT_CHAR_LIMIT))
637
+ if len(prompt) > limit:
638
+ prompt = prompt[: limit - 80].rstrip() + "\n[Debate context truncated to the configured input budget.]"
639
+ return prompt
640
+
641
+
642
+ def _cross_seed_prompt(
643
+ *,
644
+ topic: str,
645
+ left: Mapping[str, Any],
646
+ right: Mapping[str, Any],
647
+ current_disputes: Any = None,
648
+ prompt_limit: int,
649
+ ) -> str:
650
+ left_view = _debate_candidate_view(left)
651
+ right_view = _debate_candidate_view(right)
652
+ prompt = (
653
+ "You are an adversarial scientific referee comparing two mature-idea candidates.\n"
654
+ f"Topic: {topic or 'unspecified topic'}\n\n"
655
+ "Compare only the bounded candidate views below. Do not merge their identity, "
656
+ "seed, route, lineage, or target Gap mapping. Explain whether they are genuinely "
657
+ "different mechanisms, objects, assumptions, or validation paths, which evidence "
658
+ "would discriminate them, and whether both should be retained. Return JSON only.\n"
659
+ "Candidate A:\n" + pretty_json(left_view) + "\nCandidate B:\n" + pretty_json(right_view) + "\n"
660
+ "Current internal disputes:\n" + pretty_json(current_disputes or []) + "\n"
661
+ "Schema: {\"same_mechanism\": false, \"conflicting_assumptions\": [], "
662
+ "\"problem_redefinition\": \"\", \"local_repair_assessment\": \"\", "
663
+ "\"discriminating_evidence\": [], \"retain_both\": true, \"rationale\": \"\"}"
664
+ )
665
+ limit = max(2_000, int(prompt_limit or CROSS_SEED_DEBATE_PROMPT_LIMIT))
666
+ return prompt if len(prompt) <= limit else prompt[: limit - 80].rstrip() + "\n[Cross-seed context truncated.]"
667
+
668
+
669
+ def cross_seed_debate(
670
+ directions: Iterable[Mapping[str, Any]],
671
+ *,
672
+ topic: str = "",
673
+ runtime: Any = None,
674
+ session: Any = None,
675
+ workflow_name: str = "",
676
+ model: str = "",
677
+ logger: Any = None,
678
+ max_rounds: int = 1,
679
+ max_parallel_cross_seed: int = 1,
680
+ prompt_limit: int = CROSS_SEED_DEBATE_PROMPT_LIMIT,
681
+ ) -> Dict[str, Any]:
682
+ """Run bounded pairwise comparisons across distinct mature-idea seeds."""
683
+
684
+ candidates = [dict(item) for item in directions or [] if isinstance(item, Mapping)]
685
+ representatives: Dict[str, Dict[str, Any]] = {}
686
+ for item in candidates:
687
+ seed_id = _text(item.get("seed_id") or item.get("idea_id") or "legacy-primary")
688
+ current_score = float(item.get("search_score") or 0.0)
689
+ previous = representatives.get(seed_id)
690
+ if previous is None or current_score > float(previous.get("search_score") or 0.0):
691
+ representatives[seed_id] = item
692
+ pair_specs = list(combinations(representatives.values(), 2))
693
+
694
+ def _log_pair_progress(
695
+ event: str,
696
+ pair_number: int,
697
+ comparison: Mapping[str, Any],
698
+ *,
699
+ failure_reason: str = "",
700
+ ) -> None:
701
+ if logger is None:
702
+ return
703
+ try:
704
+ if failure_reason:
705
+ logger.warning(
706
+ "⚠️ Cross-seed debate pair %d/%d %s (left_seed_id=%s, right_seed_id=%s): %s",
707
+ pair_number,
708
+ len(pair_specs),
709
+ event,
710
+ comparison.get("left_seed_id"),
711
+ comparison.get("right_seed_id"),
712
+ failure_reason,
713
+ )
714
+ else:
715
+ logger.info(
716
+ "⚖️ Cross-seed debate pair %d/%d %s (left_seed_id=%s, right_seed_id=%s).",
717
+ pair_number,
718
+ len(pair_specs),
719
+ event,
720
+ comparison.get("left_seed_id"),
721
+ comparison.get("right_seed_id"),
722
+ )
723
+ except Exception:
724
+ pass
725
+
726
+ def _compare_pair(
727
+ pair_number: int,
728
+ left: Mapping[str, Any],
729
+ right: Mapping[str, Any],
730
+ ) -> tuple[Dict[str, Any], Optional[Dict[str, Any]]]:
731
+ comparison = {
732
+ "left_idea_id": _text(left.get("idea_id")),
733
+ "left_seed_id": _text(left.get("seed_id")),
734
+ "right_idea_id": _text(right.get("idea_id")),
735
+ "right_seed_id": _text(right.get("seed_id")),
736
+ "same_mechanism": False,
737
+ "conflicting_assumptions": [],
738
+ "problem_redefinition": "",
739
+ "local_repair_assessment": "",
740
+ "discriminating_evidence": [],
741
+ "retain_both": True,
742
+ "rationale": "Pair retained for cross-seed comparison.",
743
+ }
744
+ _log_pair_progress("started", pair_number, comparison)
745
+ structural = compare_mature_ideas(left, right)
746
+ mechanism_similarity = max(
747
+ structural.get("field_similarity", {}).get(field, 0.0)
748
+ for field in ("mechanism", "mechanism_or_relation", "intervention_or_transformation")
749
+ ) if structural.get("field_similarity") else 0.0
750
+ comparison["same_mechanism"] = mechanism_similarity >= 0.72
751
+ comparison["structural_comparison"] = structural
752
+ disputes = {
753
+ "left": list(left.get("debate_trace") or [])[-2:],
754
+ "right": list(right.get("debate_trace") or [])[-2:],
755
+ }
756
+ comparison["current_internal_disputes"] = disputes
757
+ if runtime is not None and callable(getattr(runtime, "llm_json", None)):
758
+ try:
759
+ payload = runtime.llm_json(
760
+ session=session,
761
+ stage="cross_seed_debate",
762
+ workflow_name=workflow_name or None,
763
+ op_name="cross_seed_debate_pair",
764
+ prompt=_cross_seed_prompt(topic=topic, left=left, right=right, current_disputes=disputes, prompt_limit=prompt_limit),
765
+ model=model or None,
766
+ temperature=0.1,
767
+ max_output_tokens=2048,
768
+ )
769
+ if isinstance(payload, list):
770
+ payload = payload[0] if payload else {}
771
+ if isinstance(payload, Mapping):
772
+ for key in comparison:
773
+ if key in payload and key not in {"left_idea_id", "left_seed_id", "right_idea_id", "right_seed_id"}:
774
+ comparison[key] = deepcopy(payload[key])
775
+ except Exception as exc:
776
+ failure = {"pair": [comparison["left_seed_id"], comparison["right_seed_id"]], "reason": str(exc)}
777
+ comparison["rationale"] = "Cross-seed debate failed; retain both candidates for review."
778
+ _log_pair_progress("failed", pair_number, comparison, failure_reason=str(exc))
779
+ return comparison, failure
780
+ _log_pair_progress("completed", pair_number, comparison)
781
+ return comparison, None
782
+
783
+ worker_count = min(max(1, int(max_parallel_cross_seed or 1)), len(pair_specs)) if pair_specs else 1
784
+ pair_results: List[tuple[Dict[str, Any], Optional[Dict[str, Any]]] | None] = [None] * len(pair_specs)
785
+ if worker_count == 1:
786
+ for index, (left, right) in enumerate(pair_specs):
787
+ pair_results[index] = _compare_pair(index + 1, left, right)
788
+ else:
789
+ if logger is not None:
790
+ try:
791
+ logger.info(
792
+ "⚖️ Cross-seed debate scheduling %d pair(s) with up to %d parallel worker(s).",
793
+ len(pair_specs),
794
+ worker_count,
795
+ )
796
+ except Exception:
797
+ pass
798
+ with ThreadPoolExecutor(max_workers=worker_count) as executor:
799
+ futures = {
800
+ executor.submit(_compare_pair, index + 1, left, right): index
801
+ for index, (left, right) in enumerate(pair_specs)
802
+ }
803
+ for future in as_completed(futures):
804
+ pair_results[futures[future]] = future.result()
805
+
806
+ pairs: List[Dict[str, Any]] = []
807
+ failures: List[Dict[str, Any]] = []
808
+ for result in pair_results:
809
+ if result is None:
810
+ continue
811
+ comparison, failure = result
812
+ pairs.append(comparison)
813
+ if failure is not None:
814
+ failures.append(failure)
815
+ return {
816
+ "mode": "cross_seed_pairwise",
817
+ "round_count": max(0, int(max_rounds)) if pairs else 0,
818
+ "representative_count": len(representatives),
819
+ "representatives": [_debate_candidate_view(item) for item in representatives.values()],
820
+ "pairs": pairs,
821
+ "failures": failures,
822
+ }
823
+
824
+
825
+ def debate_seed_portfolio(*args: Any, **kwargs: Any) -> Dict[str, Any]:
826
+ """Compatibility alias for callers naming the cross-seed adjudication."""
827
+
828
+ return cross_seed_debate(*args, **kwargs)
829
+
830
+
831
+ def _debate_single_direction(
832
+ direction: Mapping[str, Any],
833
+ *,
834
+ topic: str,
835
+ survey_handoff: Any,
836
+ profile_id: str,
837
+ profile_context: str,
838
+ runtime: Any,
839
+ session: Any,
840
+ workflow_name: str,
841
+ model: str,
842
+ logger: Any,
843
+ round_numbers: List[int],
844
+ internal_prompt_limit: int,
845
+ ) -> tuple[Dict[str, Any], List[Dict[str, Any]], List[Dict[str, str]]]:
846
+ """Run sequential debate rounds for one isolated candidate."""
847
+
848
+ current = _sanitize_candidate(direction)
849
+ effective_initial = effective_scientific_contract(current)
850
+ for field_name, value in effective_initial.items():
851
+ if not _is_populated(current.get(field_name)) and _is_populated(value):
852
+ current[field_name] = deepcopy(value)
853
+ direction_mode = _text(current.get("direction_mode") or current.get("idea_taste_mode") or "default")
854
+ current["direction_mode"] = direction_mode
855
+ original_gap_ids = _gap_ids(current)
856
+ resolved_profile_id = _profile_id(current, profile_id)
857
+ direction_events: List[Dict[str, Any]] = []
858
+ failures: List[Dict[str, str]] = []
859
+ failure_reason = ""
860
+ identity_snapshot = _immutable_identity_snapshot(current)
861
+ for round_number in round_numbers:
862
+ preflight_drift = detect_profile_drift(
863
+ resolved_profile_id,
864
+ candidate=effective_scientific_contract(current),
865
+ survey_handoff=survey_handoff if isinstance(survey_handoff, Mapping) else None,
866
+ research_object=current.get("scientific_object"),
867
+ )
868
+ preflight = _baseline_concerns(
869
+ current,
870
+ profile_drift=preflight_drift,
871
+ round_number=round_number,
872
+ )
873
+ payload: Dict[str, Any] = {}
874
+ if runtime is not None and callable(getattr(runtime, "llm_json", None)):
875
+ try:
876
+ debate_prompt = _debate_prompt(
877
+ topic=topic,
878
+ round_number=round_number,
879
+ direction=current,
880
+ profile_id=resolved_profile_id,
881
+ profile_context=profile_context,
882
+ survey_handoff=survey_handoff,
883
+ profile_drift=preflight_drift,
884
+ baseline=preflight,
885
+ prompt_limit=internal_prompt_limit,
886
+ )
887
+ if logger is not None:
888
+ try:
889
+ logger.info(
890
+ "Scientific debate prompt bounded: direction=%s round=%s chars=%d",
891
+ direction_mode,
892
+ round_number,
893
+ len(debate_prompt),
894
+ )
895
+ except Exception:
896
+ pass
897
+ payload = runtime.llm_json(
898
+ session=session,
899
+ stage="scientific_debate",
900
+ workflow_name=workflow_name or None,
901
+ op_name=f"scientific_debate_round_{round_number}",
902
+ prompt=debate_prompt,
903
+ model=model or None,
904
+ temperature=0.1,
905
+ max_output_tokens=4096,
906
+ )
907
+ if isinstance(payload, list):
908
+ payload = payload[0] if payload else {}
909
+ if not isinstance(payload, Mapping):
910
+ payload = {}
911
+ except Exception as exc:
912
+ failure_reason = str(exc)
913
+ failures.append({"direction_mode": direction_mode, "round": str(round_number), "reason": failure_reason})
914
+ direction_events.append(
915
+ {
916
+ **_fallback_event(
917
+ round_number=round_number,
918
+ direction_mode=direction_mode,
919
+ candidate=current,
920
+ baseline=preflight,
921
+ ),
922
+ "final_status": "REQUIRES_REVIEW",
923
+ "required_revision": "Debate call failed; retain the candidate and review its scientific scope.",
924
+ "preflight_missing": list(preflight.get("actual_missing_fields") or []),
925
+ "postflight_missing": list(preflight.get("actual_missing_fields") or []),
926
+ "preflight_changed_fields": list(preflight.get("changed_field") or []),
927
+ "postflight_changed_fields": list(preflight.get("changed_field") or []),
928
+ "preflight_profile_drift_fields": list(preflight.get("profile_drift_fields") or []),
929
+ "postflight_profile_drift_fields": list(preflight.get("profile_drift_fields") or []),
930
+ "preflight_severity": preflight.get("severity"),
931
+ "postflight_severity": preflight.get("severity"),
932
+ "termination_reason": "runtime_failure",
933
+ }
934
+ )
935
+ break
936
+
937
+ revision_payload = payload.get("revised_candidate") if isinstance(payload, Mapping) else None
938
+ if not isinstance(revision_payload, Mapping) and isinstance(payload, Mapping):
939
+ revision_payload = payload.get("candidate")
940
+ if not isinstance(revision_payload, Mapping) and isinstance(payload, Mapping):
941
+ revision_payload = payload
942
+ revised, changed_fields, revision_applied, gap_violation = _apply_revision(
943
+ current,
944
+ revision_payload,
945
+ direction_mode=direction_mode,
946
+ original_gap_ids=original_gap_ids,
947
+ )
948
+ identity_violations = _identity_violations(current, revision_payload, revised)
949
+ immutable_violation = bool(identity_violations)
950
+ if identity_violations:
951
+ gap_violation = gap_violation or any(item.get("field") == "target_gap_ids" for item in identity_violations)
952
+ changed_fields.extend(item.get("field") for item in identity_violations if item.get("field"))
953
+ revised.update(deepcopy(identity_snapshot))
954
+ revised["target_gap_ids"] = list(original_gap_ids)
955
+ current = revised
956
+ postflight_drift = detect_profile_drift(
957
+ resolved_profile_id,
958
+ candidate=effective_scientific_contract(current),
959
+ survey_handoff=survey_handoff if isinstance(survey_handoff, Mapping) else None,
960
+ research_object=current.get("scientific_object"),
961
+ )
962
+ postflight = _baseline_concerns(
963
+ current,
964
+ profile_drift=postflight_drift,
965
+ round_number=round_number,
966
+ )
967
+ termination_reason = ""
968
+ if immutable_violation:
969
+ violated_fields = ", ".join(item.get("field", "") for item in identity_violations)
970
+ postflight["scientific_concern"] = (
971
+ _text(postflight.get("scientific_concern"))
972
+ + f" Debate attempted to change immutable identity fields: {violated_fields}."
973
+ ).strip()
974
+ postflight["severity"] = "major"
975
+ postflight["required_revision"] = "Preserve the original idea, seed, route, lineage, and Gap mapping."
976
+ termination_reason = "immutable_gap_mapping_violation" if gap_violation else "immutable_identity_violation"
977
+ elif postflight.get("severity") == "major":
978
+ termination_reason = "post_revision_major_contract_violation"
979
+
980
+ default_status = _status_from_baseline(postflight)
981
+ proposed_status = _status(
982
+ payload.get("final_status") if isinstance(payload, Mapping) else None,
983
+ default_status,
984
+ )
985
+ event_status = default_status
986
+ if proposed_status == "LOWER_CONFIDENCE" and default_status not in {"NEEDS_SCOPE_REDUCTION", "PROFILE_DRIFT"}:
987
+ event_status = "LOWER_CONFIDENCE"
988
+ if immutable_violation:
989
+ event_status = "NEEDS_SCOPE_REDUCTION"
990
+ if current.get("scientificity_status") == "LOWER_CONFIDENCE" and event_status == "SCIENTIFICALLY_QUALIFIED":
991
+ event_status = "LOWER_CONFIDENCE"
992
+ event = _event_payload(
993
+ payload if isinstance(payload, Mapping) else {},
994
+ round_number=round_number,
995
+ direction_mode=direction_mode,
996
+ candidate=current,
997
+ baseline=postflight,
998
+ final_scope=_text(current.get("claim_scope")),
999
+ final_status=event_status,
1000
+ revision_applied=revision_applied,
1001
+ changed_fields=changed_fields,
1002
+ )
1003
+ if identity_violations:
1004
+ event["identity_violations"] = identity_violations
1005
+ event["gap_mapping_violation"] = any(
1006
+ item.get("violation_type") == "gap_mapping_violation" for item in identity_violations
1007
+ )
1008
+ event["preflight_missing"] = list(preflight.get("actual_missing_fields") or [])
1009
+ event["postflight_missing"] = list(postflight.get("actual_missing_fields") or [])
1010
+ event["preflight_changed_fields"] = list(preflight.get("changed_field") or [])
1011
+ event["postflight_changed_fields"] = list(postflight.get("changed_field") or [])
1012
+ event["preflight_profile_drift_fields"] = list(preflight.get("profile_drift_fields") or [])
1013
+ event["postflight_profile_drift_fields"] = list(postflight.get("profile_drift_fields") or [])
1014
+ event["preflight_severity"] = preflight.get("severity")
1015
+ event["postflight_severity"] = postflight.get("severity")
1016
+ event["termination_reason"] = termination_reason
1017
+ event["next_round"] = bool(
1018
+ not termination_reason and round_number < max(round_numbers)
1019
+ )
1020
+ direction_events.append(event)
1021
+ if termination_reason:
1022
+ break
1023
+
1024
+ if logger is not None and direction_events:
1025
+ final_event = direction_events[-1]
1026
+ try:
1027
+ logger.info(
1028
+ "⚖️ Scientific debate completed: seed_id=%s route_id=%s rounds=%d "
1029
+ "preflight_missing=%s preflight_profile_drift=%s revision_applied=%s "
1030
+ "postflight_missing=%s postflight_profile_drift=%s status=%s "
1031
+ "next_round=%s termination_reason=%s.",
1032
+ _text(current.get("seed_id") or current.get("idea_id") or "legacy-primary"),
1033
+ _text(current.get("route_id") or direction_mode),
1034
+ len(direction_events),
1035
+ final_event.get("preflight_missing", []),
1036
+ final_event.get("preflight_profile_drift_fields", []),
1037
+ final_event.get("revision_applied", False),
1038
+ final_event.get("postflight_missing", []),
1039
+ final_event.get("postflight_profile_drift_fields", []),
1040
+ final_event.get("final_status"),
1041
+ final_event.get("next_round", False),
1042
+ final_event.get("termination_reason", ""),
1043
+ )
1044
+ except Exception:
1045
+ pass
1046
+
1047
+ if not direction_events:
1048
+ direction_events.append(
1049
+ _fallback_event(
1050
+ round_number=1,
1051
+ direction_mode=direction_mode,
1052
+ candidate=current,
1053
+ baseline={
1054
+ "question_type": "scientific_consistency",
1055
+ "severity": "none",
1056
+ "alternative_explanations": [],
1057
+ },
1058
+ )
1059
+ )
1060
+ final_status = direction_events[-1]["final_status"]
1061
+ if failure_reason:
1062
+ final_status = "REQUIRES_REVIEW"
1063
+ current["direction_mode"] = direction_mode
1064
+ current["target_gap_ids"] = original_gap_ids
1065
+ current["debate_trace"] = direction_events
1066
+ current["debate_status"] = final_status
1067
+ current["scientificity_status"] = final_status
1068
+ if failure_reason:
1069
+ current["debate_failure_reason"] = failure_reason
1070
+ return current, direction_events, failures
1071
+
1072
+
1073
+ def debate_direction_set(
1074
+ directions: Iterable[Mapping[str, Any]],
1075
+ *,
1076
+ topic: str = "",
1077
+ survey_handoff: Any = None,
1078
+ profile_id: str = "",
1079
+ profile_context: str = "",
1080
+ runtime: Any = None,
1081
+ session: Any = None,
1082
+ workflow_name: str = "",
1083
+ model: str = "",
1084
+ logger: Any = None,
1085
+ max_rounds: int = 2,
1086
+ max_parallel_internal: int = 1,
1087
+ max_parallel_cross_seed: int = 1,
1088
+ internal_prompt_limit: int = INTERNAL_DEBATE_PROMPT_LIMIT,
1089
+ cross_seed_prompt_limit: int = CROSS_SEED_DEBATE_PROMPT_LIMIT,
1090
+ cross_seed_max_rounds: int = 1,
1091
+ run_cross_seed: bool = True,
1092
+ ) -> Dict[str, Any]:
1093
+ """Debate each direction independently and preserve candidates on failure."""
1094
+
1095
+ round_numbers = [round_number for round_number in DEBATE_ROUNDS if round_number <= max(1, min(2, int(max_rounds)))]
1096
+ source_directions = [direction for direction in directions or [] if isinstance(direction, Mapping)]
1097
+ worker_count = min(max(1, int(max_parallel_internal or 1)), len(source_directions)) if source_directions else 1
1098
+
1099
+ def _run_direction(direction: Mapping[str, Any]) -> tuple[Dict[str, Any], List[Dict[str, Any]], List[Dict[str, str]]]:
1100
+ return _debate_single_direction(
1101
+ direction,
1102
+ topic=topic,
1103
+ survey_handoff=survey_handoff,
1104
+ profile_id=profile_id,
1105
+ profile_context=profile_context,
1106
+ runtime=runtime,
1107
+ session=session,
1108
+ workflow_name=workflow_name,
1109
+ model=model,
1110
+ logger=logger,
1111
+ round_numbers=round_numbers,
1112
+ internal_prompt_limit=internal_prompt_limit,
1113
+ )
1114
+
1115
+ direction_results: List[tuple[Dict[str, Any], List[Dict[str, Any]], List[Dict[str, str]]] | None] = [None] * len(source_directions)
1116
+ if worker_count == 1:
1117
+ for index, direction in enumerate(source_directions):
1118
+ direction_results[index] = _run_direction(direction)
1119
+ else:
1120
+ if logger is not None:
1121
+ try:
1122
+ logger.info(
1123
+ "⚖️ Scientific debate scheduling %d directions with up to %d parallel worker(s).",
1124
+ len(source_directions),
1125
+ worker_count,
1126
+ )
1127
+ except Exception:
1128
+ pass
1129
+ with ThreadPoolExecutor(max_workers=worker_count) as executor:
1130
+ futures = {
1131
+ executor.submit(_run_direction, direction): index
1132
+ for index, direction in enumerate(source_directions)
1133
+ }
1134
+ for future in as_completed(futures):
1135
+ direction_results[futures[future]] = future.result()
1136
+
1137
+ revised_directions: List[Dict[str, Any]] = []
1138
+ debate_trace: List[Dict[str, Any]] = []
1139
+ failures: List[Dict[str, str]] = []
1140
+ for result in direction_results:
1141
+ if result is None:
1142
+ continue
1143
+ current, direction_events, direction_failures = result
1144
+ revised_directions.append(current)
1145
+ debate_trace.extend(direction_events)
1146
+ failures.extend(direction_failures)
1147
+
1148
+ cross_seed_result = cross_seed_debate(
1149
+ revised_directions,
1150
+ topic=topic,
1151
+ runtime=runtime,
1152
+ session=session,
1153
+ workflow_name=workflow_name,
1154
+ model=model,
1155
+ logger=logger,
1156
+ max_rounds=cross_seed_max_rounds,
1157
+ max_parallel_cross_seed=max_parallel_cross_seed,
1158
+ prompt_limit=cross_seed_prompt_limit,
1159
+ ) if run_cross_seed else {"mode": "disabled", "pairs": [], "failures": []}
1160
+ internal_events = [
1161
+ event
1162
+ for direction in revised_directions
1163
+ for event in direction.get("debate_trace", [])
1164
+ if isinstance(event, Mapping)
1165
+ ]
1166
+ first_round_events = [event for event in internal_events if event.get("round") == 1]
1167
+ second_round_events = [event for event in internal_events if event.get("round") == 2]
1168
+ debate_statistics = {
1169
+ "preflight_major_count": sum(
1170
+ event.get("preflight_severity") == "major" for event in first_round_events
1171
+ ),
1172
+ "repaired_after_round_1_count": sum(
1173
+ event.get("preflight_severity") == "major"
1174
+ and event.get("postflight_severity") != "major"
1175
+ and not event.get("termination_reason")
1176
+ for event in first_round_events
1177
+ ),
1178
+ "postflight_major_count": sum(
1179
+ event.get("postflight_severity") == "major" for event in internal_events
1180
+ ),
1181
+ "immutable_violation_count": sum(
1182
+ bool(event.get("termination_reason", "").startswith("immutable_"))
1183
+ for event in internal_events
1184
+ ),
1185
+ "qualified_after_round_2_count": sum(
1186
+ event.get("final_status")
1187
+ in {"SCIENTIFICALLY_QUALIFIED", "SCIENTIFICALLY_QUALIFIED_WITH_UNCERTAINTY", "LOWER_CONFIDENCE"}
1188
+ for event in second_round_events
1189
+ ),
1190
+ }
1191
+ result = {
1192
+ "debate_mode": "two_round_fail_open",
1193
+ "debate_layers": "internal_plus_cross_seed",
1194
+ "internal_debate": [
1195
+ {
1196
+ "idea_id": item.get("idea_id"),
1197
+ "seed_id": item.get("seed_id"),
1198
+ "status": item.get("debate_status"),
1199
+ "trace": item.get("debate_trace", []),
1200
+ }
1201
+ for item in revised_directions
1202
+ ],
1203
+ "cross_seed_debate": cross_seed_result,
1204
+ "directions": revised_directions,
1205
+ "debate_trace": debate_trace,
1206
+ "failures": failures,
1207
+ "statistics": debate_statistics,
1208
+ "round_count": len(round_numbers),
1209
+ "direction_count": len(revised_directions),
1210
+ }
1211
+ if logger is not None:
1212
+ try:
1213
+ logger.info(
1214
+ "⚖️ Scientific debate processed %d directions in %d configured round(s); failures=%d; "
1215
+ "cross-seed pairs=%d; preflight_major=%d; repaired_after_round_1=%d; "
1216
+ "postflight_major=%d; immutable_violations=%d; qualified_after_round_2=%d.",
1217
+ len(revised_directions),
1218
+ len(round_numbers),
1219
+ len(failures),
1220
+ len(cross_seed_result.get("pairs", [])),
1221
+ debate_statistics["preflight_major_count"],
1222
+ debate_statistics["repaired_after_round_1_count"],
1223
+ debate_statistics["postflight_major_count"],
1224
+ debate_statistics["immutable_violation_count"],
1225
+ debate_statistics["qualified_after_round_2_count"],
1226
+ )
1227
+ except Exception:
1228
+ pass
1229
+ return result
1230
+
1231
+
1232
+ __all__ = [
1233
+ "debate_direction_set",
1234
+ "cross_seed_debate",
1235
+ "debate_seed_portfolio",
1236
+ "DEBATE_ROUNDS",
1237
+ "DEBATE_STATUSES",
1238
+ ]