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.
- qwen_sci-0.2.2.dist-info/METADATA +492 -0
- qwen_sci-0.2.2.dist-info/RECORD +549 -0
- qwen_sci-0.2.2.dist-info/WHEEL +4 -0
- qwen_sci-0.2.2.dist-info/entry_points.txt +6 -0
- qwen_sci-0.2.2.dist-info/licenses/LICENSE +21 -0
- src/1.txt +1 -0
- src/__init__.py +1 -0
- src/__main__.py +5 -0
- src/agents/__init__.py +1 -0
- src/agents/experiment_design_agent/__init__.py +268 -0
- src/agents/experiment_design_agent/artifacts.py +747 -0
- src/agents/experiment_design_agent/cache.py +319 -0
- src/agents/experiment_design_agent/completeness.py +130 -0
- src/agents/experiment_design_agent/contracts.py +771 -0
- src/agents/experiment_design_agent/counterexample_analyzer.py +187 -0
- src/agents/experiment_design_agent/design_evidence_paper_screener.py +470 -0
- src/agents/experiment_design_agent/discipline_catalog.py +178 -0
- src/agents/experiment_design_agent/evidence_cards.py +338 -0
- src/agents/experiment_design_agent/evidence_planner.py +452 -0
- src/agents/experiment_design_agent/formal_reasoning_planner.py +884 -0
- src/agents/experiment_design_agent/fulltext_acquisition.py +863 -0
- src/agents/experiment_design_agent/idea_adapter.py +100 -0
- src/agents/experiment_design_agent/idea_intake.py +87 -0
- src/agents/experiment_design_agent/llm_json.py +328 -0
- src/agents/experiment_design_agent/orchestrator.py +1220 -0
- src/agents/experiment_design_agent/reasoning_context.py +183 -0
- src/agents/experiment_design_agent/reasoning_validation.py +546 -0
- src/agents/experiment_design_agent/run.py +481 -0
- src/agents/experiment_design_agent/run_logging.py +292 -0
- src/agents/experiment_design_agent/scope_gate.py +198 -0
- src/agents/experiment_design_agent/study_type_composer.py +1360 -0
- src/agents/experiment_design_agent/survey_evidence.py +1381 -0
- src/agents/experiment_design_agent/template_router.py +195 -0
- src/agents/experiment_design_agent/variable_claim_extractor.py +93 -0
- src/agents/idea_agent/__init__.py +1 -0
- src/agents/idea_agent/agent/__init__.py +6 -0
- src/agents/idea_agent/agent/artifacts.py +308 -0
- src/agents/idea_agent/agent/base.py +122 -0
- src/agents/idea_agent/agent/ligagent.py +488 -0
- src/agents/idea_agent/agent/mcts.py +2060 -0
- src/agents/idea_agent/agent/prompts/__init__.py +48 -0
- src/agents/idea_agent/agent/prompts/advanced_analysis.py +235 -0
- src/agents/idea_agent/agent/prompts/algorithm_alignment.py +55 -0
- src/agents/idea_agent/agent/prompts/algorithm_structuring.py +48 -0
- src/agents/idea_agent/agent/prompts/component_extraction.py +62 -0
- src/agents/idea_agent/agent/prompts/component_novelty_evaluation.py +54 -0
- src/agents/idea_agent/agent/prompts/experiment_findings_extraction.py +39 -0
- src/agents/idea_agent/agent/prompts/idea_fusion.py +295 -0
- src/agents/idea_agent/agent/prompts/idea_introduction.py +30 -0
- src/agents/idea_agent/agent/prompts/idea_result_alignment.py +39 -0
- src/agents/idea_agent/agent/prompts/input_interpreter.py +52 -0
- src/agents/idea_agent/agent/prompts/keynote_ops.py +78 -0
- src/agents/idea_agent/agent/prompts/mcts_evaluation.py +235 -0
- src/agents/idea_agent/agent/prompts/mcts_generation.py +140 -0
- src/agents/idea_agent/agent/prompts/mechanism_commit_query.py +99 -0
- src/agents/idea_agent/agent/prompts/memory_selection.py +0 -0
- src/agents/idea_agent/agent/prompts/prompt_modes.py +16 -0
- src/agents/idea_agent/agent/prompts/rag_query.py +34 -0
- src/agents/idea_agent/agent/prompts/re_analysis_replan.py +48 -0
- src/agents/idea_agent/agent/prompts/reference_grounding.py +34 -0
- src/agents/idea_agent/agent/prompts/root_domain_classification.py +32 -0
- src/agents/idea_agent/agent/prompts/scientific_debate.py +88 -0
- src/agents/idea_agent/agent/prompts/scientific_materialization.py +45 -0
- src/agents/idea_agent/agent/prompts/skill_instantiation.py +193 -0
- src/agents/idea_agent/agent/prompts/theory_transfer_query.py +93 -0
- src/agents/idea_agent/agent/prompts/topic_background.py +15 -0
- src/agents/idea_agent/agent/skills/DEFAULT_SKILL_TEMPLATES.json +263 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/SKILL.md +40 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/references/contrast_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/SKILL.md +40 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/references/mechanism_cards.md +24 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/SKILL.md +39 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/references/hierarchy_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/SKILL.md +39 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/references/mechanism_commit_patterns.md +24 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/SKILL.md +39 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/references/coordination_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/SKILL.md +41 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/references/speculation_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/SKILL.md +39 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/references/modularity_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/SKILL.md +40 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/references/transfer_patterns.md +24 -0
- src/agents/idea_agent/example/idea_result.json +900 -0
- src/agents/idea_agent/run.py +276 -0
- src/agents/idea_agent/tests/test_advanced_analysis_contract.py +90 -0
- src/agents/idea_agent/tests/test_gap_hypothesis_bridge.py +166 -0
- src/agents/idea_agent/tests/test_idea_debate.py +480 -0
- src/agents/idea_agent/tests/test_idea_result_v5.py +99 -0
- src/agents/idea_agent/tests/test_idea_synthesis.py +92 -0
- src/agents/idea_agent/tests/test_mature_idea_sources.py +27 -0
- src/agents/idea_agent/tests/test_multimodal_idea_integration.py +386 -0
- src/agents/idea_agent/tests/test_profile_materialization_and_scoring.py +219 -0
- src/agents/idea_agent/tests/test_profile_native_skill_rendering.py +122 -0
- src/agents/idea_agent/tests/test_result_persistence.py +183 -0
- src/agents/idea_agent/tests/test_scientific_intervention_profiles.py +241 -0
- src/agents/idea_agent/tests/test_scientific_rubric_and_prompts.py +559 -0
- src/agents/idea_agent/tests/test_strict_json_object_responses.py +48 -0
- src/agents/idea_agent/utils/core/__init__.py +1 -0
- src/agents/idea_agent/utils/core/ablation_inputs.py +60 -0
- src/agents/idea_agent/utils/core/chat_errors.py +95 -0
- src/agents/idea_agent/utils/core/chat_router.py +130 -0
- src/agents/idea_agent/utils/core/chat_transport.py +188 -0
- src/agents/idea_agent/utils/core/config_loader.py +166 -0
- src/agents/idea_agent/utils/core/json_utils.py +28 -0
- src/agents/idea_agent/utils/core/logger.py +287 -0
- src/agents/idea_agent/utils/core/progress.py +58 -0
- src/agents/idea_agent/utils/core/response_parsing.py +57 -0
- src/agents/idea_agent/utils/core/run_inputs.py +198 -0
- src/agents/idea_agent/utils/mcts/__init__.py +1 -0
- src/agents/idea_agent/utils/mcts/component_novelty.py +342 -0
- src/agents/idea_agent/utils/mcts/defect_registry.py +302 -0
- src/agents/idea_agent/utils/mcts/idea_routes.py +61 -0
- src/agents/idea_agent/utils/mcts/idea_taste_presets.py +312 -0
- src/agents/idea_agent/utils/mcts/mcts_helpers.py +738 -0
- src/agents/idea_agent/utils/mcts/mcts_runtime.py +3000 -0
- src/agents/idea_agent/utils/mcts/scientific_intervention_ontology.py +1561 -0
- src/agents/idea_agent/utils/mcts/scientific_rubric.py +211 -0
- src/agents/idea_agent/utils/mcts/skill_parsing.py +92 -0
- src/agents/idea_agent/utils/papers/__init__.py +1 -0
- src/agents/idea_agent/utils/papers/paper_graph_vector_store.py +499 -0
- src/agents/idea_agent/utils/papers/paper_repository.py +372 -0
- src/agents/idea_agent/utils/prompting/__init__.py +1 -0
- src/agents/idea_agent/utils/prompting/prompt_views.py +637 -0
- src/agents/idea_agent/utils/workflow/__init__.py +1 -0
- src/agents/idea_agent/utils/workflow/advanced_analysis_contract.py +244 -0
- src/agents/idea_agent/utils/workflow/gap_hypothesis_bridge.py +461 -0
- src/agents/idea_agent/utils/workflow/idea_contract.py +426 -0
- src/agents/idea_agent/utils/workflow/idea_debate.py +1238 -0
- src/agents/idea_agent/utils/workflow/idea_diversity.py +163 -0
- src/agents/idea_agent/utils/workflow/idea_fusion.py +889 -0
- src/agents/idea_agent/utils/workflow/idea_helpers.py +478 -0
- src/agents/idea_agent/utils/workflow/idea_portfolio.py +422 -0
- src/agents/idea_agent/utils/workflow/idea_synthesis.py +482 -0
- src/agents/idea_agent/utils/workflow/ligagent_flow.py +581 -0
- src/agents/idea_agent/utils/workflow/ligagent_handlers.py +1836 -0
- src/agents/idea_agent/utils/workflow/ligagent_helpers.py +1331 -0
- src/agents/idea_agent/utils/workflow/ligagent_utils.py +322 -0
- src/agents/idea_agent/utils/workflow/mature_idea_sources.py +648 -0
- src/agents/idea_agent/utils/workflow/multimodal_data_anchoring.py +578 -0
- src/agents/idea_agent/utils/workflow/result_persistence.py +774 -0
- src/agents/idea_agent/utils/workflow/stage_contract.py +76 -0
- src/agents/idea_agent/utils/workflow/workflow_runtime.py +288 -0
- src/agents/research_plan_author/__init__.py +50 -0
- src/agents/research_plan_author/artifacts.py +270 -0
- src/agents/research_plan_author/authoring_blueprint.py +1246 -0
- src/agents/research_plan_author/bibtex_renderer.py +252 -0
- src/agents/research_plan_author/contract_repair.py +131 -0
- src/agents/research_plan_author/contracts.py +745 -0
- src/agents/research_plan_author/cross_section_editor.py +232 -0
- src/agents/research_plan_author/document_quality.py +987 -0
- src/agents/research_plan_author/idea_evolution.py +223 -0
- src/agents/research_plan_author/input_loader.py +76 -0
- src/agents/research_plan_author/latex_compiler.py +260 -0
- src/agents/research_plan_author/latex_safety.py +159 -0
- src/agents/research_plan_author/llm_json.py +74 -0
- src/agents/research_plan_author/markdown_renderer.py +275 -0
- src/agents/research_plan_author/pdf_validator.py +149 -0
- src/agents/research_plan_author/render.py +166 -0
- src/agents/research_plan_author/render_artifacts.py +187 -0
- src/agents/research_plan_author/run.py +765 -0
- src/agents/research_plan_author/run_logging.py +209 -0
- src/agents/research_plan_author/section_cache.py +184 -0
- src/agents/research_plan_author/section_composer.py +1764 -0
- src/agents/research_plan_author/section_router.py +209 -0
- src/agents/research_plan_author/semantic_validator.py +254 -0
- src/agents/research_plan_author/source_bundle.py +46 -0
- src/agents/research_plan_author/source_registry.py +208 -0
- src/agents/research_plan_author/survey_source_loader.py +137 -0
- src/agents/research_plan_author/template_adapter.py +173 -0
- src/agents/research_plan_author/template_profile.py +204 -0
- src/agents/research_plan_author/tex_renderer.py +496 -0
- src/agents/research_plan_author/theory_presentation.py +134 -0
- src/agents/research_plan_author/theory_spine.py +703 -0
- src/agents/survey_agent/Dockerfile +54 -0
- src/agents/survey_agent/README.md +350 -0
- src/agents/survey_agent/config/deep_survey.yaml +217 -0
- src/agents/survey_agent/config/deep_survey_batch.yaml +176 -0
- src/agents/survey_agent/config/deep_survey_fast.yaml +183 -0
- src/agents/survey_agent/config/evaluate_baseline.yaml +197 -0
- src/agents/survey_agent/config/outcomeRAG.yaml +159 -0
- src/agents/survey_agent/env_report.md +348 -0
- src/agents/survey_agent/modules/__init__.py +0 -0
- src/agents/survey_agent/modules/code_collector.py +2194 -0
- src/agents/survey_agent/modules/code_report_generator.py +1282 -0
- src/agents/survey_agent/modules/data_manager.py +1713 -0
- src/agents/survey_agent/modules/database.py +167 -0
- src/agents/survey_agent/modules/fulltext_download_cache.py +462 -0
- src/agents/survey_agent/modules/fulltext_http.py +134 -0
- src/agents/survey_agent/modules/fulltext_resolution.py +749 -0
- src/agents/survey_agent/modules/judge.py +434 -0
- src/agents/survey_agent/modules/outcome_RAG.py +236 -0
- src/agents/survey_agent/modules/paper_graph_retriever.py +1075 -0
- src/agents/survey_agent/modules/pe.py +1729 -0
- src/agents/survey_agent/modules/pseudo_creator.py +782 -0
- src/agents/survey_agent/modules/pseudo_reviser.py +537 -0
- src/agents/survey_agent/modules/refine_agent.py +1952 -0
- src/agents/survey_agent/modules/survey_generator.py +5691 -0
- src/agents/survey_agent/modules/survey_visualizer.py +1817 -0
- src/agents/survey_agent/modules/visual_prompts.py +327 -0
- src/agents/survey_agent/modules/work_analyzer.py +2750 -0
- src/agents/survey_agent/modules/work_collector.py +5649 -0
- src/agents/survey_agent/scripts/baseline_evaluation.py +411 -0
- src/agents/survey_agent/scripts/run_deep_survey.py +224 -0
- src/agents/survey_agent/scripts/run_deep_survey_adapter.py +407 -0
- src/agents/survey_agent/scripts/run_deep_survey_adapter_with_input.py +409 -0
- src/agents/survey_agent/scripts/run_deep_survey_batch.py +228 -0
- src/agents/survey_agent/scripts/run_deep_survey_batch_arg.py +243 -0
- src/agents/survey_agent/scripts/run_deep_survey_batch_settings.py +218 -0
- src/agents/survey_agent/scripts/run_keynotes_gen.py +128 -0
- src/agents/survey_agent/scripts/run_work_clusters.py +140 -0
- src/agents/survey_agent/utils/__init__.py +0 -0
- src/agents/survey_agent/utils/ablation_utils.py +317 -0
- src/agents/survey_agent/utils/api_call.py +2518 -0
- src/agents/survey_agent/utils/chat_utils.py +82 -0
- src/agents/survey_agent/utils/config_utils.py +70 -0
- src/agents/survey_agent/utils/convert_to_md.py +29 -0
- src/agents/survey_agent/utils/err_info.py +24 -0
- src/agents/survey_agent/utils/file_utils.py +176 -0
- src/agents/survey_agent/utils/gpu_utils.py +145 -0
- src/agents/survey_agent/utils/graph_visualizer.py +279 -0
- src/agents/survey_agent/utils/html_utils.py +181 -0
- src/agents/survey_agent/utils/latex_generator_intra_cluster.py +529 -0
- src/agents/survey_agent/utils/latex_generator_relation_table.py +193 -0
- src/agents/survey_agent/utils/mineru_section_packer.py +334 -0
- src/agents/survey_agent/utils/mineru_utils.py +396 -0
- src/agents/survey_agent/utils/paper.py +144 -0
- src/agents/survey_agent/utils/read_yaml.py +15 -0
- src/agents/survey_agent/utils/repo_utils.py +167 -0
- src/agents/survey_agent/utils/rich_logger.py +77 -0
- src/agents/survey_agent/utils/step2v2.py +874 -0
- src/agents/survey_agent/utils/step2v2_extractor.py +856 -0
- src/agents/survey_agent/utils/topic_survey_storage.py +309 -0
- src/agents/survey_agent/utils/utils.py +118 -0
- src/agents/survey_agent/utils/work.py +2 -0
- src/cli.py +1611 -0
- src/config/__init__.py +213 -0
- src/config/default.yaml +1122 -0
- src/harness/LICENSE +21 -0
- src/harness/VENDORED.md +14 -0
- src/harness/src/__init__.py +0 -0
- src/harness/src/openharness/__init__.py +0 -0
- src/harness/src/openharness/__main__.py +6 -0
- src/harness/src/openharness/api/__init__.py +21 -0
- src/harness/src/openharness/api/client.py +281 -0
- src/harness/src/openharness/api/codex_client.py +407 -0
- src/harness/src/openharness/api/copilot_auth.py +240 -0
- src/harness/src/openharness/api/copilot_client.py +134 -0
- src/harness/src/openharness/api/errors.py +19 -0
- src/harness/src/openharness/api/openai_client.py +577 -0
- src/harness/src/openharness/api/provider.py +186 -0
- src/harness/src/openharness/api/registry.py +437 -0
- src/harness/src/openharness/api/usage.py +17 -0
- src/harness/src/openharness/auth/__init__.py +29 -0
- src/harness/src/openharness/auth/external.py +610 -0
- src/harness/src/openharness/auth/flows.py +189 -0
- src/harness/src/openharness/auth/manager.py +482 -0
- src/harness/src/openharness/auth/storage.py +269 -0
- src/harness/src/openharness/autopilot/__init__.py +23 -0
- src/harness/src/openharness/autopilot/service.py +2239 -0
- src/harness/src/openharness/autopilot/types.py +91 -0
- src/harness/src/openharness/bridge/__init__.py +20 -0
- src/harness/src/openharness/bridge/manager.py +106 -0
- src/harness/src/openharness/bridge/session_runner.py +46 -0
- src/harness/src/openharness/bridge/types.py +37 -0
- src/harness/src/openharness/bridge/work_secret.py +41 -0
- src/harness/src/openharness/channels/UPSTREAM +6 -0
- src/harness/src/openharness/channels/__init__.py +22 -0
- src/harness/src/openharness/channels/adapter.py +131 -0
- src/harness/src/openharness/channels/bus/__init__.py +6 -0
- src/harness/src/openharness/channels/bus/events.py +38 -0
- src/harness/src/openharness/channels/bus/queue.py +44 -0
- src/harness/src/openharness/channels/impl/__init__.py +6 -0
- src/harness/src/openharness/channels/impl/base.py +141 -0
- src/harness/src/openharness/channels/impl/dingtalk.py +445 -0
- src/harness/src/openharness/channels/impl/discord.py +311 -0
- src/harness/src/openharness/channels/impl/email.py +410 -0
- src/harness/src/openharness/channels/impl/feishu.py +1342 -0
- src/harness/src/openharness/channels/impl/manager.py +257 -0
- src/harness/src/openharness/channels/impl/matrix.py +700 -0
- src/harness/src/openharness/channels/impl/mochat.py +897 -0
- src/harness/src/openharness/channels/impl/qq.py +141 -0
- src/harness/src/openharness/channels/impl/slack.py +284 -0
- src/harness/src/openharness/channels/impl/telegram.py +525 -0
- src/harness/src/openharness/channels/impl/whatsapp.py +159 -0
- src/harness/src/openharness/cli.py +2551 -0
- src/harness/src/openharness/commands/__init__.py +21 -0
- src/harness/src/openharness/commands/registry.py +2783 -0
- src/harness/src/openharness/config/__init__.py +34 -0
- src/harness/src/openharness/config/paths.py +160 -0
- src/harness/src/openharness/config/schema.py +119 -0
- src/harness/src/openharness/config/settings.py +1104 -0
- src/harness/src/openharness/coordinator/__init__.py +12 -0
- src/harness/src/openharness/coordinator/agent_definitions.py +975 -0
- src/harness/src/openharness/coordinator/coordinator_mode.py +520 -0
- src/harness/src/openharness/engine/__init__.py +80 -0
- src/harness/src/openharness/engine/cost_tracker.py +24 -0
- src/harness/src/openharness/engine/messages.py +222 -0
- src/harness/src/openharness/engine/query.py +1188 -0
- src/harness/src/openharness/engine/query_engine.py +313 -0
- src/harness/src/openharness/engine/stream_events.py +90 -0
- src/harness/src/openharness/hooks/__init__.py +50 -0
- src/harness/src/openharness/hooks/events.py +20 -0
- src/harness/src/openharness/hooks/executor.py +242 -0
- src/harness/src/openharness/hooks/hot_reload.py +31 -0
- src/harness/src/openharness/hooks/loader.py +68 -0
- src/harness/src/openharness/hooks/schemas.py +66 -0
- src/harness/src/openharness/hooks/types.py +38 -0
- src/harness/src/openharness/keybindings/__init__.py +14 -0
- src/harness/src/openharness/keybindings/default_bindings.py +11 -0
- src/harness/src/openharness/keybindings/loader.py +22 -0
- src/harness/src/openharness/keybindings/parser.py +18 -0
- src/harness/src/openharness/keybindings/resolver.py +13 -0
- src/harness/src/openharness/mcp/__init__.py +79 -0
- src/harness/src/openharness/mcp/client.py +298 -0
- src/harness/src/openharness/mcp/config.py +16 -0
- src/harness/src/openharness/mcp/types.py +76 -0
- src/harness/src/openharness/memory/__init__.py +25 -0
- src/harness/src/openharness/memory/agent.py +91 -0
- src/harness/src/openharness/memory/manager.py +183 -0
- src/harness/src/openharness/memory/memdir.py +52 -0
- src/harness/src/openharness/memory/migrate.py +137 -0
- src/harness/src/openharness/memory/paths.py +22 -0
- src/harness/src/openharness/memory/relevance.py +145 -0
- src/harness/src/openharness/memory/scan.py +116 -0
- src/harness/src/openharness/memory/schema.py +443 -0
- src/harness/src/openharness/memory/search.py +71 -0
- src/harness/src/openharness/memory/team.py +90 -0
- src/harness/src/openharness/memory/types.py +33 -0
- src/harness/src/openharness/memory/usage.py +153 -0
- src/harness/src/openharness/output_styles/__init__.py +5 -0
- src/harness/src/openharness/output_styles/loader.py +42 -0
- src/harness/src/openharness/permissions/__init__.py +26 -0
- src/harness/src/openharness/permissions/checker.py +200 -0
- src/harness/src/openharness/permissions/modes.py +13 -0
- src/harness/src/openharness/personalization/__init__.py +6 -0
- src/harness/src/openharness/personalization/extractor.py +138 -0
- src/harness/src/openharness/personalization/rules.py +64 -0
- src/harness/src/openharness/personalization/session_hook.py +65 -0
- src/harness/src/openharness/platforms.py +86 -0
- src/harness/src/openharness/plugins/__init__.py +53 -0
- src/harness/src/openharness/plugins/bundled/__init__.py +0 -0
- src/harness/src/openharness/plugins/installer.py +39 -0
- src/harness/src/openharness/plugins/loader.py +730 -0
- src/harness/src/openharness/plugins/schemas.py +24 -0
- src/harness/src/openharness/plugins/types.py +59 -0
- src/harness/src/openharness/prompts/__init__.py +14 -0
- src/harness/src/openharness/prompts/claudemd.py +48 -0
- src/harness/src/openharness/prompts/context.py +187 -0
- src/harness/src/openharness/prompts/environment.py +135 -0
- src/harness/src/openharness/prompts/system_prompt.py +109 -0
- src/harness/src/openharness/sandbox/Dockerfile +6 -0
- src/harness/src/openharness/sandbox/__init__.py +33 -0
- src/harness/src/openharness/sandbox/adapter.py +148 -0
- src/harness/src/openharness/sandbox/docker_backend.py +232 -0
- src/harness/src/openharness/sandbox/docker_image.py +103 -0
- src/harness/src/openharness/sandbox/path_validator.py +37 -0
- src/harness/src/openharness/sandbox/session.py +63 -0
- src/harness/src/openharness/services/__init__.py +30 -0
- src/harness/src/openharness/services/autodream/__init__.py +34 -0
- src/harness/src/openharness/services/autodream/backup.py +104 -0
- src/harness/src/openharness/services/autodream/lock.py +138 -0
- src/harness/src/openharness/services/autodream/prompt.py +128 -0
- src/harness/src/openharness/services/autodream/service.py +313 -0
- src/harness/src/openharness/services/compact/__init__.py +1725 -0
- src/harness/src/openharness/services/cron.py +137 -0
- src/harness/src/openharness/services/cron_scheduler.py +595 -0
- src/harness/src/openharness/services/lsp/__init__.py +216 -0
- src/harness/src/openharness/services/memory_extract/__init__.py +261 -0
- src/harness/src/openharness/services/oauth/__init__.py +0 -0
- src/harness/src/openharness/services/session_backend.py +97 -0
- src/harness/src/openharness/services/session_memory/__init__.py +139 -0
- src/harness/src/openharness/services/session_storage.py +230 -0
- src/harness/src/openharness/services/token_estimation.py +15 -0
- src/harness/src/openharness/services/tool_outputs.py +55 -0
- src/harness/src/openharness/skills/__init__.py +44 -0
- src/harness/src/openharness/skills/_frontmatter.py +97 -0
- src/harness/src/openharness/skills/bundled/__init__.py +75 -0
- src/harness/src/openharness/skills/bundled/content/commit.md +25 -0
- src/harness/src/openharness/skills/bundled/content/debug.md +25 -0
- src/harness/src/openharness/skills/bundled/content/diagnose.md +35 -0
- src/harness/src/openharness/skills/bundled/content/plan.md +39 -0
- src/harness/src/openharness/skills/bundled/content/review.md +33 -0
- src/harness/src/openharness/skills/bundled/content/simplify.md +26 -0
- src/harness/src/openharness/skills/bundled/content/skill-creator.md +175 -0
- src/harness/src/openharness/skills/bundled/content/test.md +38 -0
- src/harness/src/openharness/skills/loader.py +229 -0
- src/harness/src/openharness/skills/registry.py +29 -0
- src/harness/src/openharness/skills/types.py +24 -0
- src/harness/src/openharness/state/__init__.py +6 -0
- src/harness/src/openharness/state/app_state.py +30 -0
- src/harness/src/openharness/state/store.py +40 -0
- src/harness/src/openharness/swarm/__init__.py +70 -0
- src/harness/src/openharness/swarm/in_process.py +693 -0
- src/harness/src/openharness/swarm/lockfile.py +24 -0
- src/harness/src/openharness/swarm/mailbox.py +522 -0
- src/harness/src/openharness/swarm/permission_sync.py +1168 -0
- src/harness/src/openharness/swarm/registry.py +410 -0
- src/harness/src/openharness/swarm/spawn_utils.py +229 -0
- src/harness/src/openharness/swarm/subprocess_backend.py +171 -0
- src/harness/src/openharness/swarm/team_lifecycle.py +910 -0
- src/harness/src/openharness/swarm/types.py +398 -0
- src/harness/src/openharness/swarm/worktree.py +315 -0
- src/harness/src/openharness/tasks/__init__.py +18 -0
- src/harness/src/openharness/tasks/local_agent_task.py +28 -0
- src/harness/src/openharness/tasks/local_shell_task.py +17 -0
- src/harness/src/openharness/tasks/manager.py +475 -0
- src/harness/src/openharness/tasks/stop_task.py +11 -0
- src/harness/src/openharness/tasks/types.py +32 -0
- src/harness/src/openharness/themes/__init__.py +21 -0
- src/harness/src/openharness/themes/builtin.py +89 -0
- src/harness/src/openharness/themes/loader.py +55 -0
- src/harness/src/openharness/themes/schema.py +54 -0
- src/harness/src/openharness/tools/__init__.py +107 -0
- src/harness/src/openharness/tools/agent_tool.py +141 -0
- src/harness/src/openharness/tools/ask_user_question_tool.py +46 -0
- src/harness/src/openharness/tools/base.py +80 -0
- src/harness/src/openharness/tools/bash_tool.py +259 -0
- src/harness/src/openharness/tools/brief_tool.py +33 -0
- src/harness/src/openharness/tools/config_tool.py +54 -0
- src/harness/src/openharness/tools/cron_create_tool.py +105 -0
- src/harness/src/openharness/tools/cron_delete_tool.py +32 -0
- src/harness/src/openharness/tools/cron_list_tool.py +69 -0
- src/harness/src/openharness/tools/cron_toggle_tool.py +37 -0
- src/harness/src/openharness/tools/enter_plan_mode_tool.py +28 -0
- src/harness/src/openharness/tools/enter_worktree_tool.py +80 -0
- src/harness/src/openharness/tools/exit_plan_mode_tool.py +28 -0
- src/harness/src/openharness/tools/exit_worktree_tool.py +42 -0
- src/harness/src/openharness/tools/file_edit_tool.py +95 -0
- src/harness/src/openharness/tools/file_read_tool.py +72 -0
- src/harness/src/openharness/tools/file_write_tool.py +87 -0
- src/harness/src/openharness/tools/glob_tool.py +175 -0
- src/harness/src/openharness/tools/grep_tool.py +374 -0
- src/harness/src/openharness/tools/image_generation_tool.py +447 -0
- src/harness/src/openharness/tools/image_to_text_tool.py +237 -0
- src/harness/src/openharness/tools/list_mcp_resources_tool.py +36 -0
- src/harness/src/openharness/tools/lsp_tool.py +154 -0
- src/harness/src/openharness/tools/mcp_auth_tool.py +71 -0
- src/harness/src/openharness/tools/mcp_tool.py +72 -0
- src/harness/src/openharness/tools/notebook_edit_tool.py +97 -0
- src/harness/src/openharness/tools/read_mcp_resource_tool.py +38 -0
- src/harness/src/openharness/tools/remote_trigger_tool.py +74 -0
- src/harness/src/openharness/tools/send_message_tool.py +58 -0
- src/harness/src/openharness/tools/skill_tool.py +43 -0
- src/harness/src/openharness/tools/sleep_tool.py +32 -0
- src/harness/src/openharness/tools/task_create_tool.py +56 -0
- src/harness/src/openharness/tools/task_get_tool.py +33 -0
- src/harness/src/openharness/tools/task_list_tool.py +35 -0
- src/harness/src/openharness/tools/task_output_tool.py +35 -0
- src/harness/src/openharness/tools/task_stop_tool.py +30 -0
- src/harness/src/openharness/tools/task_update_tool.py +50 -0
- src/harness/src/openharness/tools/team_create_tool.py +31 -0
- src/harness/src/openharness/tools/team_delete_tool.py +30 -0
- src/harness/src/openharness/tools/todo_write_tool.py +46 -0
- src/harness/src/openharness/tools/tool_search_tool.py +38 -0
- src/harness/src/openharness/tools/web_fetch_tool.py +117 -0
- src/harness/src/openharness/tools/web_search_tool.py +119 -0
- src/harness/src/openharness/ui/__init__.py +5 -0
- src/harness/src/openharness/ui/app.py +320 -0
- src/harness/src/openharness/ui/backend_host.py +941 -0
- src/harness/src/openharness/ui/coordinator_drain.py +198 -0
- src/harness/src/openharness/ui/input.py +32 -0
- src/harness/src/openharness/ui/output.py +265 -0
- src/harness/src/openharness/ui/permission_dialog.py +14 -0
- src/harness/src/openharness/ui/protocol.py +247 -0
- src/harness/src/openharness/ui/react_launcher.py +179 -0
- src/harness/src/openharness/ui/runtime.py +799 -0
- src/harness/src/openharness/ui/textual_app.py +495 -0
- src/harness/src/openharness/utils/__init__.py +0 -0
- src/harness/src/openharness/utils/file_lock.py +86 -0
- src/harness/src/openharness/utils/fs.py +98 -0
- src/harness/src/openharness/utils/helpers.py +77 -0
- src/harness/src/openharness/utils/network_guard.py +340 -0
- src/harness/src/openharness/utils/shell.py +147 -0
- src/harness/src/openharness/vim/__init__.py +5 -0
- src/harness/src/openharness/vim/transitions.py +8 -0
- src/harness/src/openharness/voice/__init__.py +7 -0
- src/harness/src/openharness/voice/keyterms.py +10 -0
- src/harness/src/openharness/voice/stream_stt.py +8 -0
- src/harness/src/openharness/voice/voice_mode.py +44 -0
- src/llm/__init__.py +29 -0
- src/llm/image_generation.py +402 -0
- src/llm/provider_registry.py +419 -0
- src/llm/runtime_env.py +64 -0
- src/llm/structured_output.py +131 -0
- src/llm/vision.py +198 -0
- src/memory/__init__.py +2 -0
- src/memory/api/__init__.py +1 -0
- src/memory/api/base_memory_system_api.py +98 -0
- src/memory/api/base_symbolic_memory_system_api.py +190 -0
- src/memory/api/base_vector_memory_system_api.py +116 -0
- src/memory/api/faiss_memory_system_api.py +228 -0
- src/memory/api/slot_process_api.py +827 -0
- src/memory/api/symbolic_memory_system_api.py +572 -0
- src/memory/memory_system/__init__.py +48 -0
- src/memory/memory_system/component_taxonomy.py +430 -0
- src/memory/memory_system/llm.py +437 -0
- src/memory/memory_system/models.py +168 -0
- src/memory/memory_system/user_prompt.py +418 -0
- src/memory/memory_system/utils.py +282 -0
- src/memory/memory_system/vectorstore.py +311 -0
- src/memory/memory_system/working_slot.py +99 -0
- src/pipeline/__init__.py +129 -0
- src/pipeline/contracts.py +531 -0
- src/pipeline/discipline_taxonomy.py +916 -0
- src/pipeline/evidence_coverage_ledger.py +1150 -0
- src/pipeline/evidence_refinement.py +620 -0
- src/pipeline/experiment_to_symbolic.py +137 -0
- src/pipeline/multimodal_evidence/__init__.py +28 -0
- src/pipeline/multimodal_evidence/capabilities.py +107 -0
- src/pipeline/multimodal_evidence/claims.py +135 -0
- src/pipeline/multimodal_evidence/contract.py +291 -0
- src/pipeline/multimodal_evidence/data_sh_compiler.py +505 -0
- src/pipeline/multimodal_evidence/handoff_binding.py +135 -0
- src/pipeline/multimodal_evidence/manifest.py +254 -0
- src/pipeline/multimodal_evidence/native_analysis.py +479 -0
- src/pipeline/multimodal_evidence/perception.py +277 -0
- src/pipeline/multimodal_evidence/query_binding.py +65 -0
- src/pipeline/multimodal_evidence/rendering.py +120 -0
- src/pipeline/multimodal_evidence/safety.py +42 -0
- src/pipeline/multimodal_evidence/sampling.py +69 -0
- src/pipeline/multimodal_evidence/service.py +148 -0
- src/pipeline/multimodal_evidence/survey_integration.py +460 -0
- src/pipeline/paper_identity.py +52 -0
- src/pipeline/research_design_inventory.py +338 -0
- src/pipeline/research_domain_resolution.py +669 -0
- src/pipeline/research_identity.py +548 -0
- src/pipeline/research_question_contract.py +646 -0
- src/pipeline/retrieval_lanes.py +889 -0
- src/pipeline/run_loop.py +862 -0
- src/pipeline/science_events.py +30 -0
- src/pipeline/science_manifests.py +602 -0
- src/pipeline/science_run.py +918 -0
- src/pipeline/science_stages.py +709 -0
- src/pipeline/science_workflow.py +807 -0
- src/pipeline/sh_cluster_projection.py +306 -0
- src/pipeline/sh_graph_provenance.py +648 -0
- src/pipeline/subhypothesis_decomposition.py +392 -0
- src/pipeline/subhypothesis_evidence.py +856 -0
- src/pipeline/survey_evidence_plan.py +858 -0
- src/pipeline/survey_gap_adjudication.py +647 -0
- src/pipeline/survey_gap_candidates.py +840 -0
- src/pipeline/survey_gap_ledger.py +683 -0
- src/pipeline/survey_gap_triage.py +401 -0
- src/pipeline/survey_handoff_persistence.py +481 -0
- src/pipeline/survey_handoff_projection.py +457 -0
- src/pipeline/survey_idea_handoff.py +1112 -0
- src/pipeline/survey_idea_loader.py +289 -0
- src/research_run_ids.py +11 -0
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
"""Core tool-aware query loop."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, AsyncIterator, Awaitable, Callable
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from openharness.api.client import (
|
|
15
|
+
ApiMessageCompleteEvent,
|
|
16
|
+
ApiMessageRequest,
|
|
17
|
+
ApiRetryEvent,
|
|
18
|
+
ApiTextDeltaEvent,
|
|
19
|
+
SupportsStreamingMessages,
|
|
20
|
+
)
|
|
21
|
+
from openharness.api.provider import is_model_multimodal
|
|
22
|
+
from openharness.api.usage import UsageSnapshot
|
|
23
|
+
from openharness.config.paths import get_data_dir
|
|
24
|
+
from openharness.engine.messages import (
|
|
25
|
+
ConversationMessage,
|
|
26
|
+
ImageBlock,
|
|
27
|
+
TextBlock,
|
|
28
|
+
ToolResultBlock,
|
|
29
|
+
)
|
|
30
|
+
from openharness.engine.stream_events import (
|
|
31
|
+
AssistantTextDelta,
|
|
32
|
+
AssistantTurnComplete,
|
|
33
|
+
CompactProgressEvent,
|
|
34
|
+
ErrorEvent,
|
|
35
|
+
StatusEvent,
|
|
36
|
+
StreamEvent,
|
|
37
|
+
ToolExecutionCompleted,
|
|
38
|
+
ToolExecutionStarted,
|
|
39
|
+
)
|
|
40
|
+
from openharness.hooks import HookEvent, HookExecutor
|
|
41
|
+
from openharness.permissions.checker import PermissionChecker
|
|
42
|
+
from openharness.services.tool_outputs import tool_output_inline_chars, tool_output_preview_chars
|
|
43
|
+
from openharness.tools.base import ToolExecutionContext
|
|
44
|
+
from openharness.tools.base import ToolRegistry
|
|
45
|
+
|
|
46
|
+
AUTO_COMPACT_STATUS_MESSAGE = "Auto-compacting conversation memory to keep things fast and focused."
|
|
47
|
+
REACTIVE_COMPACT_STATUS_MESSAGE = "Prompt too long; compacting conversation memory and retrying."
|
|
48
|
+
MAX_SAFE_COMPLETION_TOKENS = 128_000
|
|
49
|
+
|
|
50
|
+
log = logging.getLogger(__name__)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
PermissionPrompt = Callable[[str, str], Awaitable[bool]]
|
|
54
|
+
AskUserPrompt = Callable[[str], Awaitable[str]]
|
|
55
|
+
|
|
56
|
+
MAX_TRACKED_READ_FILES = 6
|
|
57
|
+
MAX_TRACKED_SKILLS = 8
|
|
58
|
+
MAX_TRACKED_ASYNC_AGENT_EVENTS = 8
|
|
59
|
+
MAX_TRACKED_ASYNC_AGENT_TASKS = 12
|
|
60
|
+
MAX_TRACKED_WORK_LOG = 10
|
|
61
|
+
MAX_TRACKED_USER_GOALS = 5
|
|
62
|
+
MAX_TRACKED_ACTIVE_ARTIFACTS = 8
|
|
63
|
+
MAX_TRACKED_VERIFIED_WORK = 10
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_prompt_too_long_error(exc: Exception) -> bool:
|
|
67
|
+
text = str(exc).lower()
|
|
68
|
+
return any(
|
|
69
|
+
needle in text
|
|
70
|
+
for needle in (
|
|
71
|
+
"prompt too long",
|
|
72
|
+
"context_length_exceeded",
|
|
73
|
+
"context length",
|
|
74
|
+
"maximum context",
|
|
75
|
+
"context window",
|
|
76
|
+
"input tokens exceed",
|
|
77
|
+
"messages resulted in",
|
|
78
|
+
"reduce the length of the messages",
|
|
79
|
+
"configured limit",
|
|
80
|
+
"too many tokens",
|
|
81
|
+
"too large for the model",
|
|
82
|
+
"maximum context length",
|
|
83
|
+
"exceed_context",
|
|
84
|
+
"exceeds the available context size",
|
|
85
|
+
"available context size",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _bounded_completion_tokens(max_tokens: int, context_window_tokens: int | None = None) -> int:
|
|
91
|
+
"""Return a conservative per-request output token cap.
|
|
92
|
+
|
|
93
|
+
Some OpenAI-compatible providers reject very large ``max_tokens`` before
|
|
94
|
+
the request reaches model-side context management. Keep oversized user
|
|
95
|
+
config from making every turn fail while preserving normal defaults.
|
|
96
|
+
"""
|
|
97
|
+
limit = MAX_SAFE_COMPLETION_TOKENS
|
|
98
|
+
if context_window_tokens is not None and context_window_tokens > 0:
|
|
99
|
+
limit = min(limit, int(context_window_tokens))
|
|
100
|
+
return max(1, min(int(max_tokens), limit))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _extract_completion_token_limit(exc: Exception) -> int | None:
|
|
104
|
+
"""Parse provider errors such as "supports at most 128000 completion tokens"."""
|
|
105
|
+
text = str(exc).lower().replace(",", "")
|
|
106
|
+
patterns = (
|
|
107
|
+
r"supports at most\s+(\d+)\s+completion tokens",
|
|
108
|
+
r"at most\s+(\d+)\s+completion tokens",
|
|
109
|
+
r"max(?:imum)?(?:_completion)?[_\s-]tokens.*?(?:<=|less than or equal to|at most)\s+(\d+)",
|
|
110
|
+
)
|
|
111
|
+
for pattern in patterns:
|
|
112
|
+
match = re.search(pattern, text)
|
|
113
|
+
if match:
|
|
114
|
+
try:
|
|
115
|
+
return max(1, int(match.group(1)))
|
|
116
|
+
except ValueError:
|
|
117
|
+
return None
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _is_completion_token_limit_error(exc: Exception) -> bool:
|
|
122
|
+
text = str(exc).lower()
|
|
123
|
+
return (
|
|
124
|
+
("max_tokens" in text or "max_completion_tokens" in text)
|
|
125
|
+
and ("too large" in text or "at most" in text or "completion tokens" in text)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class MaxTurnsExceeded(RuntimeError):
|
|
130
|
+
"""Raised when the agent exceeds the configured max_turns for one user prompt."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, max_turns: int) -> None:
|
|
133
|
+
super().__init__(f"Exceeded maximum turn limit ({max_turns})")
|
|
134
|
+
self.max_turns = max_turns
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class QueryContext:
|
|
139
|
+
"""Context shared across a query run."""
|
|
140
|
+
|
|
141
|
+
api_client: SupportsStreamingMessages
|
|
142
|
+
tool_registry: ToolRegistry
|
|
143
|
+
permission_checker: PermissionChecker
|
|
144
|
+
cwd: Path
|
|
145
|
+
model: str
|
|
146
|
+
system_prompt: str
|
|
147
|
+
max_tokens: int
|
|
148
|
+
effort: str | None = None
|
|
149
|
+
context_window_tokens: int | None = None
|
|
150
|
+
auto_compact_threshold_tokens: int | None = None
|
|
151
|
+
permission_prompt: PermissionPrompt | None = None
|
|
152
|
+
ask_user_prompt: AskUserPrompt | None = None
|
|
153
|
+
max_turns: int | None = 200
|
|
154
|
+
hook_executor: HookExecutor | None = None
|
|
155
|
+
tool_metadata: dict[str, object] | None = None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _append_capped_unique(bucket: list[Any], value: Any, *, limit: int) -> None:
|
|
159
|
+
if value in bucket:
|
|
160
|
+
bucket.remove(value)
|
|
161
|
+
bucket.append(value)
|
|
162
|
+
if len(bucket) > limit:
|
|
163
|
+
del bucket[:-limit]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _task_focus_state(tool_metadata: dict[str, object] | None) -> dict[str, object]:
|
|
167
|
+
if tool_metadata is None:
|
|
168
|
+
return {}
|
|
169
|
+
value = tool_metadata.setdefault(
|
|
170
|
+
"task_focus_state",
|
|
171
|
+
{
|
|
172
|
+
"goal": "",
|
|
173
|
+
"recent_goals": [],
|
|
174
|
+
"active_artifacts": [],
|
|
175
|
+
"verified_state": [],
|
|
176
|
+
"next_step": "",
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
if isinstance(value, dict):
|
|
180
|
+
value.setdefault("goal", "")
|
|
181
|
+
value.setdefault("recent_goals", [])
|
|
182
|
+
value.setdefault("active_artifacts", [])
|
|
183
|
+
value.setdefault("verified_state", [])
|
|
184
|
+
value.setdefault("next_step", "")
|
|
185
|
+
return value
|
|
186
|
+
replacement = {
|
|
187
|
+
"goal": "",
|
|
188
|
+
"recent_goals": [],
|
|
189
|
+
"active_artifacts": [],
|
|
190
|
+
"verified_state": [],
|
|
191
|
+
"next_step": "",
|
|
192
|
+
}
|
|
193
|
+
tool_metadata["task_focus_state"] = replacement
|
|
194
|
+
return replacement
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _summarize_focus_text(text: str) -> str:
|
|
198
|
+
normalized = " ".join(text.split())
|
|
199
|
+
if not normalized:
|
|
200
|
+
return ""
|
|
201
|
+
return normalized[:240]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def remember_user_goal(
|
|
205
|
+
tool_metadata: dict[str, object] | None,
|
|
206
|
+
prompt: str,
|
|
207
|
+
) -> None:
|
|
208
|
+
state = _task_focus_state(tool_metadata)
|
|
209
|
+
summary = _summarize_focus_text(prompt)
|
|
210
|
+
if not summary:
|
|
211
|
+
return
|
|
212
|
+
recent_goals = state.setdefault("recent_goals", [])
|
|
213
|
+
if isinstance(recent_goals, list):
|
|
214
|
+
_append_capped_unique(recent_goals, summary, limit=MAX_TRACKED_USER_GOALS)
|
|
215
|
+
state["goal"] = summary
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _remember_active_artifact(
|
|
219
|
+
tool_metadata: dict[str, object] | None,
|
|
220
|
+
artifact: str,
|
|
221
|
+
) -> None:
|
|
222
|
+
normalized = artifact.strip()
|
|
223
|
+
if not normalized:
|
|
224
|
+
return
|
|
225
|
+
state = _task_focus_state(tool_metadata)
|
|
226
|
+
artifacts = state.setdefault("active_artifacts", [])
|
|
227
|
+
if isinstance(artifacts, list):
|
|
228
|
+
_append_capped_unique(artifacts, normalized[:240], limit=MAX_TRACKED_ACTIVE_ARTIFACTS)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _remember_verified_work(
|
|
232
|
+
tool_metadata: dict[str, object] | None,
|
|
233
|
+
entry: str,
|
|
234
|
+
) -> None:
|
|
235
|
+
normalized = entry.strip()
|
|
236
|
+
if not normalized:
|
|
237
|
+
return
|
|
238
|
+
bucket = _tool_metadata_bucket(tool_metadata, "recent_verified_work")
|
|
239
|
+
_append_capped_unique(bucket, normalized[:320], limit=MAX_TRACKED_VERIFIED_WORK)
|
|
240
|
+
state = _task_focus_state(tool_metadata)
|
|
241
|
+
verified_state = state.setdefault("verified_state", [])
|
|
242
|
+
if isinstance(verified_state, list):
|
|
243
|
+
_append_capped_unique(verified_state, normalized[:320], limit=MAX_TRACKED_VERIFIED_WORK)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _tool_metadata_bucket(
|
|
247
|
+
tool_metadata: dict[str, object] | None,
|
|
248
|
+
key: str,
|
|
249
|
+
) -> list[Any]:
|
|
250
|
+
if tool_metadata is None:
|
|
251
|
+
return []
|
|
252
|
+
value = tool_metadata.setdefault(key, [])
|
|
253
|
+
if isinstance(value, list):
|
|
254
|
+
return value
|
|
255
|
+
replacement: list[Any] = []
|
|
256
|
+
tool_metadata[key] = replacement
|
|
257
|
+
return replacement
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _remember_read_file(
|
|
261
|
+
tool_metadata: dict[str, object] | None,
|
|
262
|
+
*,
|
|
263
|
+
path: str,
|
|
264
|
+
offset: int,
|
|
265
|
+
limit: int,
|
|
266
|
+
output: str,
|
|
267
|
+
) -> None:
|
|
268
|
+
bucket = _tool_metadata_bucket(tool_metadata, "read_file_state")
|
|
269
|
+
preview_lines = [line.strip() for line in output.splitlines()[:6] if line.strip()]
|
|
270
|
+
entry = {
|
|
271
|
+
"path": path,
|
|
272
|
+
"span": f"lines {offset + 1}-{offset + limit}",
|
|
273
|
+
"preview": " | ".join(preview_lines)[:320],
|
|
274
|
+
"timestamp": time.time(),
|
|
275
|
+
}
|
|
276
|
+
if isinstance(bucket, list):
|
|
277
|
+
bucket[:] = [
|
|
278
|
+
existing
|
|
279
|
+
for existing in bucket
|
|
280
|
+
if not isinstance(existing, dict) or str(existing.get("path") or "") != path
|
|
281
|
+
]
|
|
282
|
+
bucket.append(entry)
|
|
283
|
+
if len(bucket) > MAX_TRACKED_READ_FILES:
|
|
284
|
+
del bucket[:-MAX_TRACKED_READ_FILES]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _remember_skill_invocation(
|
|
288
|
+
tool_metadata: dict[str, object] | None,
|
|
289
|
+
*,
|
|
290
|
+
skill_name: str,
|
|
291
|
+
) -> None:
|
|
292
|
+
bucket = _tool_metadata_bucket(tool_metadata, "invoked_skills")
|
|
293
|
+
normalized = skill_name.strip()
|
|
294
|
+
if not normalized:
|
|
295
|
+
return
|
|
296
|
+
if normalized in bucket:
|
|
297
|
+
bucket.remove(normalized)
|
|
298
|
+
bucket.append(normalized)
|
|
299
|
+
if len(bucket) > MAX_TRACKED_SKILLS:
|
|
300
|
+
del bucket[:-MAX_TRACKED_SKILLS]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _remember_async_agent_activity(
|
|
304
|
+
tool_metadata: dict[str, object] | None,
|
|
305
|
+
*,
|
|
306
|
+
tool_name: str,
|
|
307
|
+
tool_input: dict[str, object],
|
|
308
|
+
output: str,
|
|
309
|
+
) -> None:
|
|
310
|
+
bucket = _tool_metadata_bucket(tool_metadata, "async_agent_state")
|
|
311
|
+
if tool_name == "agent":
|
|
312
|
+
description = str(tool_input.get("description") or tool_input.get("prompt") or "").strip()
|
|
313
|
+
summary = f"Spawned async agent. {description}".strip()
|
|
314
|
+
if output.strip():
|
|
315
|
+
summary = f"{summary} [{output.strip()[:180]}]".strip()
|
|
316
|
+
elif tool_name == "send_message":
|
|
317
|
+
target = str(tool_input.get("task_id") or "").strip()
|
|
318
|
+
summary = f"Sent follow-up message to async agent {target}".strip()
|
|
319
|
+
else:
|
|
320
|
+
summary = output.strip()[:220] or f"Async agent activity via {tool_name}"
|
|
321
|
+
bucket.append(summary)
|
|
322
|
+
if len(bucket) > MAX_TRACKED_ASYNC_AGENT_EVENTS:
|
|
323
|
+
del bucket[:-MAX_TRACKED_ASYNC_AGENT_EVENTS]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _parse_spawned_agent_identity(
|
|
327
|
+
output: str,
|
|
328
|
+
metadata: dict[str, object] | None = None,
|
|
329
|
+
) -> tuple[str, str] | None:
|
|
330
|
+
if isinstance(metadata, dict):
|
|
331
|
+
agent_id = str(metadata.get("agent_id") or "").strip()
|
|
332
|
+
task_id = str(metadata.get("task_id") or "").strip()
|
|
333
|
+
if agent_id and task_id:
|
|
334
|
+
return agent_id, task_id
|
|
335
|
+
match = re.search(r"Spawned agent (.+?) \(task_id=(\S+?)(?:[,)]|$)", output.strip())
|
|
336
|
+
if match is None:
|
|
337
|
+
return None
|
|
338
|
+
return match.group(1).strip(), match.group(2).strip()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _remember_async_agent_task(
|
|
342
|
+
tool_metadata: dict[str, object] | None,
|
|
343
|
+
*,
|
|
344
|
+
tool_name: str,
|
|
345
|
+
tool_input: dict[str, object],
|
|
346
|
+
output: str,
|
|
347
|
+
result_metadata: dict[str, object] | None = None,
|
|
348
|
+
) -> None:
|
|
349
|
+
if tool_name != "agent":
|
|
350
|
+
return
|
|
351
|
+
identity = _parse_spawned_agent_identity(output, result_metadata)
|
|
352
|
+
if identity is None:
|
|
353
|
+
return
|
|
354
|
+
agent_id, task_id = identity
|
|
355
|
+
bucket = _tool_metadata_bucket(tool_metadata, "async_agent_tasks")
|
|
356
|
+
description = str(tool_input.get("description") or tool_input.get("prompt") or "").strip()
|
|
357
|
+
entry = {
|
|
358
|
+
"agent_id": agent_id,
|
|
359
|
+
"task_id": task_id,
|
|
360
|
+
"description": description[:240],
|
|
361
|
+
"status": "spawned",
|
|
362
|
+
"notification_sent": False,
|
|
363
|
+
"spawned_at": time.time(),
|
|
364
|
+
}
|
|
365
|
+
bucket[:] = [
|
|
366
|
+
existing
|
|
367
|
+
for existing in bucket
|
|
368
|
+
if not isinstance(existing, dict) or str(existing.get("task_id") or "") != task_id
|
|
369
|
+
]
|
|
370
|
+
bucket.append(entry)
|
|
371
|
+
if len(bucket) > MAX_TRACKED_ASYNC_AGENT_TASKS:
|
|
372
|
+
del bucket[:-MAX_TRACKED_ASYNC_AGENT_TASKS]
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _remember_work_log(
|
|
376
|
+
tool_metadata: dict[str, object] | None,
|
|
377
|
+
*,
|
|
378
|
+
entry: str,
|
|
379
|
+
) -> None:
|
|
380
|
+
bucket = _tool_metadata_bucket(tool_metadata, "recent_work_log")
|
|
381
|
+
normalized = entry.strip()
|
|
382
|
+
if not normalized:
|
|
383
|
+
return
|
|
384
|
+
bucket.append(normalized[:320])
|
|
385
|
+
if len(bucket) > MAX_TRACKED_WORK_LOG:
|
|
386
|
+
del bucket[:-MAX_TRACKED_WORK_LOG]
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _update_plan_mode(tool_metadata: dict[str, object] | None, mode: str) -> None:
|
|
390
|
+
if tool_metadata is None:
|
|
391
|
+
return
|
|
392
|
+
tool_metadata["permission_mode"] = mode
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _record_tool_carryover(
|
|
396
|
+
context: QueryContext,
|
|
397
|
+
*,
|
|
398
|
+
tool_name: str,
|
|
399
|
+
tool_input: dict[str, object],
|
|
400
|
+
tool_output: str,
|
|
401
|
+
tool_result_metadata: dict[str, object] | None,
|
|
402
|
+
is_error: bool,
|
|
403
|
+
resolved_file_path: str | None,
|
|
404
|
+
) -> None:
|
|
405
|
+
if is_error:
|
|
406
|
+
return
|
|
407
|
+
if resolved_file_path is not None:
|
|
408
|
+
_remember_active_artifact(context.tool_metadata, resolved_file_path)
|
|
409
|
+
if tool_name == "read_file" and resolved_file_path is not None:
|
|
410
|
+
offset = int(tool_input.get("offset") or 0)
|
|
411
|
+
limit = int(tool_input.get("limit") or 200)
|
|
412
|
+
_remember_read_file(
|
|
413
|
+
context.tool_metadata,
|
|
414
|
+
path=resolved_file_path,
|
|
415
|
+
offset=offset,
|
|
416
|
+
limit=limit,
|
|
417
|
+
output=tool_output,
|
|
418
|
+
)
|
|
419
|
+
_remember_verified_work(
|
|
420
|
+
context.tool_metadata,
|
|
421
|
+
f"Inspected file {resolved_file_path} (lines {offset + 1}-{offset + limit})",
|
|
422
|
+
)
|
|
423
|
+
elif tool_name == "skill":
|
|
424
|
+
_remember_skill_invocation(
|
|
425
|
+
context.tool_metadata,
|
|
426
|
+
skill_name=str(tool_input.get("name") or ""),
|
|
427
|
+
)
|
|
428
|
+
skill_name = str(tool_input.get("name") or "").strip()
|
|
429
|
+
if skill_name:
|
|
430
|
+
_remember_active_artifact(context.tool_metadata, f"skill:{skill_name}")
|
|
431
|
+
_remember_verified_work(context.tool_metadata, f"Loaded skill {skill_name}")
|
|
432
|
+
elif tool_name in {"agent", "send_message"}:
|
|
433
|
+
_remember_async_agent_activity(
|
|
434
|
+
context.tool_metadata,
|
|
435
|
+
tool_name=tool_name,
|
|
436
|
+
tool_input=tool_input,
|
|
437
|
+
output=tool_output,
|
|
438
|
+
)
|
|
439
|
+
_remember_async_agent_task(
|
|
440
|
+
context.tool_metadata,
|
|
441
|
+
tool_name=tool_name,
|
|
442
|
+
tool_input=tool_input,
|
|
443
|
+
output=tool_output,
|
|
444
|
+
result_metadata=tool_result_metadata,
|
|
445
|
+
)
|
|
446
|
+
description = str(tool_input.get("description") or tool_input.get("prompt") or tool_name).strip()
|
|
447
|
+
_remember_verified_work(
|
|
448
|
+
context.tool_metadata,
|
|
449
|
+
f"Confirmed async-agent activity via {tool_name}: {description[:180]}",
|
|
450
|
+
)
|
|
451
|
+
elif tool_name == "enter_plan_mode":
|
|
452
|
+
_update_plan_mode(context.tool_metadata, "plan")
|
|
453
|
+
elif tool_name == "exit_plan_mode":
|
|
454
|
+
_update_plan_mode(context.tool_metadata, "default")
|
|
455
|
+
elif tool_name == "web_fetch":
|
|
456
|
+
url = str(tool_input.get("url") or "").strip()
|
|
457
|
+
if url:
|
|
458
|
+
_remember_active_artifact(context.tool_metadata, url)
|
|
459
|
+
_remember_verified_work(context.tool_metadata, f"Fetched remote content from {url}")
|
|
460
|
+
elif tool_name == "web_search":
|
|
461
|
+
query = str(tool_input.get("query") or "").strip()
|
|
462
|
+
if query:
|
|
463
|
+
_remember_verified_work(context.tool_metadata, f"Ran web search for {query[:180]}")
|
|
464
|
+
elif tool_name == "glob":
|
|
465
|
+
pattern = str(tool_input.get("pattern") or "").strip()
|
|
466
|
+
if pattern:
|
|
467
|
+
_remember_verified_work(context.tool_metadata, f"Expanded glob pattern {pattern[:180]}")
|
|
468
|
+
elif tool_name == "grep":
|
|
469
|
+
pattern = str(tool_input.get("pattern") or "").strip()
|
|
470
|
+
if pattern:
|
|
471
|
+
_remember_verified_work(context.tool_metadata, f"Checked repository matches for grep pattern {pattern[:180]}")
|
|
472
|
+
elif tool_name == "bash":
|
|
473
|
+
command = str(tool_input.get("command") or "").strip()
|
|
474
|
+
summary = tool_output.splitlines()[0].strip() if tool_output.strip() else "no output"
|
|
475
|
+
_remember_verified_work(
|
|
476
|
+
context.tool_metadata,
|
|
477
|
+
f"Ran bash command {command[:160]} [{summary[:120]}]",
|
|
478
|
+
)
|
|
479
|
+
if tool_name == "read_file" and resolved_file_path is not None:
|
|
480
|
+
_remember_work_log(
|
|
481
|
+
context.tool_metadata,
|
|
482
|
+
entry=f"Read file {resolved_file_path}",
|
|
483
|
+
)
|
|
484
|
+
elif tool_name == "bash":
|
|
485
|
+
command = str(tool_input.get("command") or "").strip()
|
|
486
|
+
summary = tool_output.splitlines()[0].strip() if tool_output.strip() else "no output"
|
|
487
|
+
_remember_work_log(
|
|
488
|
+
context.tool_metadata,
|
|
489
|
+
entry=f"Ran bash: {command[:160]} [{summary[:120]}]",
|
|
490
|
+
)
|
|
491
|
+
elif tool_name == "grep":
|
|
492
|
+
pattern = str(tool_input.get("pattern") or "").strip()
|
|
493
|
+
_remember_work_log(
|
|
494
|
+
context.tool_metadata,
|
|
495
|
+
entry=f"Searched with grep pattern={pattern[:160]}",
|
|
496
|
+
)
|
|
497
|
+
elif tool_name == "skill":
|
|
498
|
+
_remember_work_log(
|
|
499
|
+
context.tool_metadata,
|
|
500
|
+
entry=f"Loaded skill {str(tool_input.get('name') or '').strip()}",
|
|
501
|
+
)
|
|
502
|
+
elif tool_name in {"agent", "send_message"}:
|
|
503
|
+
_remember_work_log(
|
|
504
|
+
context.tool_metadata,
|
|
505
|
+
entry=f"Async agent action via {tool_name}",
|
|
506
|
+
)
|
|
507
|
+
elif tool_name == "enter_plan_mode":
|
|
508
|
+
_remember_work_log(context.tool_metadata, entry="Entered plan mode")
|
|
509
|
+
elif tool_name == "exit_plan_mode":
|
|
510
|
+
_remember_work_log(context.tool_metadata, entry="Exited plan mode")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _tool_artifact_dir() -> Path:
|
|
514
|
+
artifact_dir = get_data_dir() / "tool_artifacts"
|
|
515
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
516
|
+
return artifact_dir
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _safe_tool_artifact_name(tool_name: str) -> str:
|
|
520
|
+
normalized = re.sub(r"[^A-Za-z0-9_.-]+", "_", tool_name.strip())
|
|
521
|
+
return (normalized or "tool")[:80]
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _offload_tool_output_if_needed(
|
|
525
|
+
*,
|
|
526
|
+
tool_name: str,
|
|
527
|
+
tool_use_id: str,
|
|
528
|
+
output: str,
|
|
529
|
+
) -> tuple[str, Path | None]:
|
|
530
|
+
inline_limit = tool_output_inline_chars()
|
|
531
|
+
if len(output) <= inline_limit:
|
|
532
|
+
return output, None
|
|
533
|
+
|
|
534
|
+
artifact_path = (
|
|
535
|
+
_tool_artifact_dir()
|
|
536
|
+
/ f"{time.strftime('%Y%m%d-%H%M%S')}-{_safe_tool_artifact_name(tool_name)}-{uuid4().hex[:12]}.txt"
|
|
537
|
+
)
|
|
538
|
+
artifact_path.write_text(output, encoding="utf-8", errors="replace")
|
|
539
|
+
preview = output[:tool_output_preview_chars()]
|
|
540
|
+
omitted = max(0, len(output) - len(preview))
|
|
541
|
+
inline = (
|
|
542
|
+
"[Tool output truncated]\n"
|
|
543
|
+
f"Tool: {tool_name}\n"
|
|
544
|
+
f"Tool use id: {tool_use_id}\n"
|
|
545
|
+
f"Original size: {len(output)} chars\n"
|
|
546
|
+
f"Full output saved to: {artifact_path}\n"
|
|
547
|
+
f"Inline preview: first {len(preview)} chars"
|
|
548
|
+
)
|
|
549
|
+
if omitted:
|
|
550
|
+
inline += f" ({omitted} chars omitted)"
|
|
551
|
+
if preview:
|
|
552
|
+
inline += f"\n\nPreview:\n{preview}"
|
|
553
|
+
return inline, artifact_path
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# ---------------------------------------------------------------------------
|
|
557
|
+
# Image preprocessing — convert ImageBlocks to text for non-multimodal models
|
|
558
|
+
# ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
_IMAGE_PREPROCESS_STATUS = "Converting image to text description via vision model…"
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
async def _preprocess_images_in_messages(
|
|
564
|
+
messages: list[ConversationMessage],
|
|
565
|
+
context: QueryContext,
|
|
566
|
+
) -> AsyncIterator[StreamEvent]:
|
|
567
|
+
"""Scan messages for ImageBlocks and convert them to text if the active
|
|
568
|
+
model does not support multimodal input.
|
|
569
|
+
|
|
570
|
+
Yields status events during conversion so the UI stays responsive.
|
|
571
|
+
"""
|
|
572
|
+
if is_model_multimodal(context.model):
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
vision_config = context.tool_metadata.get("vision_model_config")
|
|
576
|
+
if not vision_config:
|
|
577
|
+
# No vision model configured — skip preprocessing.
|
|
578
|
+
return
|
|
579
|
+
|
|
580
|
+
# Collect all ImageBlocks with their parent message index and block index
|
|
581
|
+
pending: list[tuple[int, int, ImageBlock]] = []
|
|
582
|
+
for msg_idx, msg in enumerate(messages):
|
|
583
|
+
if msg.role != "user":
|
|
584
|
+
continue
|
|
585
|
+
for blk_idx, block in enumerate(msg.content):
|
|
586
|
+
if isinstance(block, ImageBlock):
|
|
587
|
+
pending.append((msg_idx, blk_idx, block))
|
|
588
|
+
|
|
589
|
+
if not pending:
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
yield StatusEvent(message=_IMAGE_PREPROCESS_STATUS)
|
|
593
|
+
|
|
594
|
+
# Process images in parallel
|
|
595
|
+
async def _describe(msg_idx: int, blk_idx: int, block: ImageBlock) -> tuple[int, int, str]:
|
|
596
|
+
tool = context.tool_registry.get("image_to_text")
|
|
597
|
+
if tool is None:
|
|
598
|
+
return msg_idx, blk_idx, "[Image: could not describe — image_to_text tool not available]"
|
|
599
|
+
|
|
600
|
+
# Build tool input
|
|
601
|
+
tool_input_data: dict[str, object] = {
|
|
602
|
+
"image_data": block.data,
|
|
603
|
+
"media_type": block.media_type,
|
|
604
|
+
"prompt": "Describe this image in detail, including any text, "
|
|
605
|
+
"UI elements, code, diagrams, or visual information present.",
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
try:
|
|
609
|
+
parsed = tool.input_model.model_validate(tool_input_data)
|
|
610
|
+
except Exception:
|
|
611
|
+
return msg_idx, blk_idx, "[Image: could not parse image data]"
|
|
612
|
+
|
|
613
|
+
exec_context = ToolExecutionContext(
|
|
614
|
+
cwd=context.cwd,
|
|
615
|
+
metadata={
|
|
616
|
+
"vision_model_config": vision_config,
|
|
617
|
+
**(context.tool_metadata or {}),
|
|
618
|
+
},
|
|
619
|
+
)
|
|
620
|
+
result = await tool.execute(parsed, exec_context)
|
|
621
|
+
if result.is_error:
|
|
622
|
+
return msg_idx, blk_idx, f"[Image description failed: {result.output}]"
|
|
623
|
+
return msg_idx, blk_idx, result.output
|
|
624
|
+
|
|
625
|
+
results = await asyncio.gather(*[_describe(mi, bi, blk) for mi, bi, blk in pending])
|
|
626
|
+
|
|
627
|
+
# Replace ImageBlocks with TextBlocks in-place
|
|
628
|
+
for msg_idx, blk_idx, description in results:
|
|
629
|
+
msg = messages[msg_idx]
|
|
630
|
+
msg.content[blk_idx] = TextBlock(text=description)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
async def run_query(
|
|
634
|
+
context: QueryContext,
|
|
635
|
+
messages: list[ConversationMessage],
|
|
636
|
+
) -> AsyncIterator[tuple[StreamEvent, UsageSnapshot | None]]:
|
|
637
|
+
"""Run the conversation loop until the model stops requesting tools.
|
|
638
|
+
|
|
639
|
+
Auto-compaction is checked at the start of each turn. When the
|
|
640
|
+
estimated token count exceeds the model's auto-compact threshold,
|
|
641
|
+
the engine first tries a cheap microcompact (clearing old tool result
|
|
642
|
+
content) and, if that is not enough, performs a full LLM-based
|
|
643
|
+
summarization of older messages.
|
|
644
|
+
"""
|
|
645
|
+
from openharness.services.compact import (
|
|
646
|
+
AutoCompactState,
|
|
647
|
+
auto_compact_if_needed,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
compact_state = AutoCompactState()
|
|
651
|
+
reactive_compact_attempted = False
|
|
652
|
+
last_compaction_result: tuple[list[ConversationMessage], bool] = (messages, False)
|
|
653
|
+
effective_max_tokens = _bounded_completion_tokens(
|
|
654
|
+
context.max_tokens,
|
|
655
|
+
context.context_window_tokens,
|
|
656
|
+
)
|
|
657
|
+
reported_token_clamp = False
|
|
658
|
+
|
|
659
|
+
async def _stream_compaction(
|
|
660
|
+
*,
|
|
661
|
+
trigger: str,
|
|
662
|
+
force: bool = False,
|
|
663
|
+
) -> AsyncIterator[tuple[StreamEvent, UsageSnapshot | None]]:
|
|
664
|
+
nonlocal last_compaction_result
|
|
665
|
+
progress_queue: asyncio.Queue[CompactProgressEvent] = asyncio.Queue()
|
|
666
|
+
|
|
667
|
+
async def _progress(event: CompactProgressEvent) -> None:
|
|
668
|
+
await progress_queue.put(event)
|
|
669
|
+
|
|
670
|
+
task = asyncio.create_task(
|
|
671
|
+
auto_compact_if_needed(
|
|
672
|
+
messages,
|
|
673
|
+
api_client=context.api_client,
|
|
674
|
+
model=context.model,
|
|
675
|
+
system_prompt=context.system_prompt,
|
|
676
|
+
state=compact_state,
|
|
677
|
+
progress_callback=_progress,
|
|
678
|
+
force=force,
|
|
679
|
+
trigger=trigger,
|
|
680
|
+
hook_executor=context.hook_executor,
|
|
681
|
+
carryover_metadata=context.tool_metadata,
|
|
682
|
+
context_window_tokens=context.context_window_tokens,
|
|
683
|
+
auto_compact_threshold_tokens=context.auto_compact_threshold_tokens,
|
|
684
|
+
)
|
|
685
|
+
)
|
|
686
|
+
while True:
|
|
687
|
+
try:
|
|
688
|
+
event = await asyncio.wait_for(progress_queue.get(), timeout=0.05)
|
|
689
|
+
yield event, None
|
|
690
|
+
except asyncio.TimeoutError:
|
|
691
|
+
if task.done():
|
|
692
|
+
break
|
|
693
|
+
continue
|
|
694
|
+
while not progress_queue.empty():
|
|
695
|
+
yield progress_queue.get_nowait(), None
|
|
696
|
+
last_compaction_result = await task
|
|
697
|
+
return
|
|
698
|
+
|
|
699
|
+
turn_count = 0
|
|
700
|
+
while context.max_turns is None or turn_count < context.max_turns:
|
|
701
|
+
turn_count += 1
|
|
702
|
+
if effective_max_tokens != context.max_tokens and not reported_token_clamp:
|
|
703
|
+
reported_token_clamp = True
|
|
704
|
+
yield StatusEvent(
|
|
705
|
+
message=(
|
|
706
|
+
"Requested max_tokens="
|
|
707
|
+
f"{context.max_tokens} exceeds the safe per-request output cap; "
|
|
708
|
+
f"using {effective_max_tokens}."
|
|
709
|
+
)
|
|
710
|
+
), None
|
|
711
|
+
# --- auto-compact check before calling the model ---------------
|
|
712
|
+
async for event, usage in _stream_compaction(trigger="auto"):
|
|
713
|
+
yield event, usage
|
|
714
|
+
compacted_messages, was_compacted = last_compaction_result
|
|
715
|
+
if compacted_messages is not messages:
|
|
716
|
+
messages[:] = compacted_messages
|
|
717
|
+
# ---------------------------------------------------------------
|
|
718
|
+
|
|
719
|
+
# --- image preprocessing: convert ImageBlocks to text for non-vision models ---
|
|
720
|
+
async for event in _preprocess_images_in_messages(messages, context):
|
|
721
|
+
yield event, None
|
|
722
|
+
# -----------------------------------------------------------------------------
|
|
723
|
+
|
|
724
|
+
final_message: ConversationMessage | None = None
|
|
725
|
+
usage = UsageSnapshot()
|
|
726
|
+
|
|
727
|
+
try:
|
|
728
|
+
async for event in context.api_client.stream_message(
|
|
729
|
+
ApiMessageRequest(
|
|
730
|
+
model=context.model,
|
|
731
|
+
messages=messages,
|
|
732
|
+
system_prompt=context.system_prompt,
|
|
733
|
+
max_tokens=effective_max_tokens,
|
|
734
|
+
tools=context.tool_registry.to_api_schema(),
|
|
735
|
+
effort=context.effort,
|
|
736
|
+
)
|
|
737
|
+
):
|
|
738
|
+
if isinstance(event, ApiTextDeltaEvent):
|
|
739
|
+
yield AssistantTextDelta(text=event.text), None
|
|
740
|
+
continue
|
|
741
|
+
if isinstance(event, ApiRetryEvent):
|
|
742
|
+
yield StatusEvent(
|
|
743
|
+
message=(
|
|
744
|
+
f"Request failed; retrying in {event.delay_seconds:.1f}s "
|
|
745
|
+
f"(attempt {event.attempt + 1} of {event.max_attempts}): {event.message}"
|
|
746
|
+
)
|
|
747
|
+
), None
|
|
748
|
+
continue
|
|
749
|
+
|
|
750
|
+
if isinstance(event, ApiMessageCompleteEvent):
|
|
751
|
+
final_message = event.message
|
|
752
|
+
usage = event.usage
|
|
753
|
+
except Exception as exc:
|
|
754
|
+
error_msg = str(exc)
|
|
755
|
+
if _is_completion_token_limit_error(exc):
|
|
756
|
+
supported_limit = _extract_completion_token_limit(exc)
|
|
757
|
+
if supported_limit is not None and effective_max_tokens > supported_limit:
|
|
758
|
+
previous_max_tokens = effective_max_tokens
|
|
759
|
+
effective_max_tokens = supported_limit
|
|
760
|
+
yield StatusEvent(
|
|
761
|
+
message=(
|
|
762
|
+
f"Model rejected max_tokens={previous_max_tokens}; "
|
|
763
|
+
f"retrying with provider limit {effective_max_tokens}."
|
|
764
|
+
)
|
|
765
|
+
), None
|
|
766
|
+
turn_count = max(0, turn_count - 1)
|
|
767
|
+
continue
|
|
768
|
+
if not reactive_compact_attempted and _is_prompt_too_long_error(exc):
|
|
769
|
+
reactive_compact_attempted = True
|
|
770
|
+
yield StatusEvent(message=REACTIVE_COMPACT_STATUS_MESSAGE), None
|
|
771
|
+
async for event, usage in _stream_compaction(trigger="reactive", force=True):
|
|
772
|
+
yield event, usage
|
|
773
|
+
compacted_messages, was_compacted = last_compaction_result
|
|
774
|
+
if compacted_messages is not messages:
|
|
775
|
+
messages[:] = compacted_messages
|
|
776
|
+
if was_compacted:
|
|
777
|
+
continue
|
|
778
|
+
if "connect" in error_msg.lower() or "timeout" in error_msg.lower() or "network" in error_msg.lower():
|
|
779
|
+
yield ErrorEvent(message=f"Network error: {error_msg}. Check your internet connection and try again."), None
|
|
780
|
+
else:
|
|
781
|
+
yield ErrorEvent(message=f"API error: {error_msg}"), None
|
|
782
|
+
return
|
|
783
|
+
|
|
784
|
+
if final_message is None:
|
|
785
|
+
raise RuntimeError("Model stream finished without a final message")
|
|
786
|
+
|
|
787
|
+
coordinator_context_message: ConversationMessage | None = None
|
|
788
|
+
if context.system_prompt.startswith("You are a **coordinator**."):
|
|
789
|
+
if messages and messages[-1].role == "user" and messages[-1].text.startswith("# Coordinator User Context"):
|
|
790
|
+
coordinator_context_message = messages.pop()
|
|
791
|
+
|
|
792
|
+
if final_message.role == "assistant" and final_message.is_effectively_empty():
|
|
793
|
+
log.warning("dropping empty assistant message from provider response")
|
|
794
|
+
yield ErrorEvent(
|
|
795
|
+
message=(
|
|
796
|
+
"Model returned an empty assistant message. "
|
|
797
|
+
"The turn was ignored to keep the session healthy."
|
|
798
|
+
)
|
|
799
|
+
), usage
|
|
800
|
+
return
|
|
801
|
+
|
|
802
|
+
if not final_message.tool_uses:
|
|
803
|
+
messages.append(final_message)
|
|
804
|
+
if context.hook_executor is not None:
|
|
805
|
+
stop_hooks = await context.hook_executor.execute(
|
|
806
|
+
HookEvent.STOP,
|
|
807
|
+
{
|
|
808
|
+
"event": HookEvent.STOP.value,
|
|
809
|
+
"stop_reason": "tool_uses_empty",
|
|
810
|
+
"assistant_text": final_message.text,
|
|
811
|
+
"assistant_message": final_message.model_dump(mode="json"),
|
|
812
|
+
},
|
|
813
|
+
)
|
|
814
|
+
if stop_hooks.blocked:
|
|
815
|
+
reason = stop_hooks.reason or "STOP hook blocked completion."
|
|
816
|
+
yield StatusEvent(message=reason), None
|
|
817
|
+
if coordinator_context_message is not None:
|
|
818
|
+
messages.append(coordinator_context_message)
|
|
819
|
+
coordinator_context_message = None
|
|
820
|
+
messages.append(
|
|
821
|
+
ConversationMessage.from_user_text(
|
|
822
|
+
"# Hook Feedback: Completion Blocked\n\n"
|
|
823
|
+
f"{reason}\n\n"
|
|
824
|
+
"Fix the reported issues with the available tools, then finish again."
|
|
825
|
+
)
|
|
826
|
+
)
|
|
827
|
+
continue
|
|
828
|
+
yield AssistantTurnComplete(message=final_message, usage=usage), usage
|
|
829
|
+
if coordinator_context_message is not None:
|
|
830
|
+
messages.append(coordinator_context_message)
|
|
831
|
+
return
|
|
832
|
+
|
|
833
|
+
messages.append(final_message)
|
|
834
|
+
yield AssistantTurnComplete(message=final_message, usage=usage), usage
|
|
835
|
+
|
|
836
|
+
if coordinator_context_message is not None:
|
|
837
|
+
messages.append(coordinator_context_message)
|
|
838
|
+
|
|
839
|
+
tool_calls = final_message.tool_uses
|
|
840
|
+
|
|
841
|
+
if len(tool_calls) == 1:
|
|
842
|
+
# Single tool: sequential (stream events immediately)
|
|
843
|
+
tc = tool_calls[0]
|
|
844
|
+
yield ToolExecutionStarted(tool_name=tc.name, tool_input=tc.input), None
|
|
845
|
+
try:
|
|
846
|
+
result = await _execute_tool_call(context, tc.name, tc.id, tc.input)
|
|
847
|
+
except Exception as exc:
|
|
848
|
+
log.exception("tool execution raised: name=%s id=%s", tc.name, tc.id)
|
|
849
|
+
result = ToolResultBlock(
|
|
850
|
+
tool_use_id=tc.id,
|
|
851
|
+
content=f"Tool {tc.name} failed: {type(exc).__name__}: {exc}",
|
|
852
|
+
is_error=True,
|
|
853
|
+
)
|
|
854
|
+
yield ToolExecutionCompleted(
|
|
855
|
+
tool_name=tc.name,
|
|
856
|
+
output=result.content,
|
|
857
|
+
is_error=result.is_error,
|
|
858
|
+
metadata=result.result_metadata,
|
|
859
|
+
), None
|
|
860
|
+
tool_results = [result]
|
|
861
|
+
else:
|
|
862
|
+
# Multiple tools: execute concurrently, emit events after
|
|
863
|
+
for tc in tool_calls:
|
|
864
|
+
yield ToolExecutionStarted(tool_name=tc.name, tool_input=tc.input), None
|
|
865
|
+
|
|
866
|
+
async def _run(tc):
|
|
867
|
+
return await _execute_tool_call(context, tc.name, tc.id, tc.input)
|
|
868
|
+
|
|
869
|
+
# Use return_exceptions=True so a single failing tool does not abandon
|
|
870
|
+
# its siblings as cancelled coroutines and leave the conversation with
|
|
871
|
+
# un-replied tool_use blocks (Anthropic's API rejects the next request
|
|
872
|
+
# on the session if any tool_use is missing a matching tool_result).
|
|
873
|
+
raw_results = await asyncio.gather(
|
|
874
|
+
*[_run(tc) for tc in tool_calls], return_exceptions=True
|
|
875
|
+
)
|
|
876
|
+
tool_results = []
|
|
877
|
+
for tc, result in zip(tool_calls, raw_results):
|
|
878
|
+
if isinstance(result, BaseException):
|
|
879
|
+
log.exception(
|
|
880
|
+
"tool execution raised: name=%s id=%s",
|
|
881
|
+
tc.name,
|
|
882
|
+
tc.id,
|
|
883
|
+
exc_info=result,
|
|
884
|
+
)
|
|
885
|
+
result = ToolResultBlock(
|
|
886
|
+
tool_use_id=tc.id,
|
|
887
|
+
content=f"Tool {tc.name} failed: {type(result).__name__}: {result}",
|
|
888
|
+
is_error=True,
|
|
889
|
+
)
|
|
890
|
+
tool_results.append(result)
|
|
891
|
+
|
|
892
|
+
for tc, result in zip(tool_calls, tool_results):
|
|
893
|
+
yield ToolExecutionCompleted(
|
|
894
|
+
tool_name=tc.name,
|
|
895
|
+
output=result.content,
|
|
896
|
+
is_error=result.is_error,
|
|
897
|
+
metadata=result.result_metadata,
|
|
898
|
+
), None
|
|
899
|
+
|
|
900
|
+
messages.append(ConversationMessage(role="user", content=tool_results))
|
|
901
|
+
|
|
902
|
+
if context.max_turns is not None:
|
|
903
|
+
raise MaxTurnsExceeded(context.max_turns)
|
|
904
|
+
raise RuntimeError("Query loop exited without a max_turns limit or final response")
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
async def _execute_tool_call(
|
|
908
|
+
context: QueryContext,
|
|
909
|
+
tool_name: str,
|
|
910
|
+
tool_use_id: str,
|
|
911
|
+
tool_input: dict[str, object],
|
|
912
|
+
) -> ToolResultBlock:
|
|
913
|
+
if context.tool_metadata is None:
|
|
914
|
+
context.tool_metadata = {}
|
|
915
|
+
cache = _tool_call_result_cache(context.tool_metadata)
|
|
916
|
+
cached = cache.get(tool_use_id) if tool_use_id else None
|
|
917
|
+
if isinstance(cached, ToolResultBlock):
|
|
918
|
+
return cached.model_copy(deep=True)
|
|
919
|
+
|
|
920
|
+
inflight = _tool_call_inflight_cache(context.tool_metadata)
|
|
921
|
+
if tool_use_id:
|
|
922
|
+
existing = inflight.get(tool_use_id)
|
|
923
|
+
if isinstance(existing, asyncio.Future):
|
|
924
|
+
return (await asyncio.shield(existing)).model_copy(deep=True)
|
|
925
|
+
future = asyncio.get_running_loop().create_future()
|
|
926
|
+
inflight[tool_use_id] = future
|
|
927
|
+
else:
|
|
928
|
+
future = None
|
|
929
|
+
|
|
930
|
+
try:
|
|
931
|
+
result = await _execute_tool_call_uncached(
|
|
932
|
+
context,
|
|
933
|
+
tool_name,
|
|
934
|
+
tool_use_id,
|
|
935
|
+
tool_input,
|
|
936
|
+
)
|
|
937
|
+
if future is not None and not future.done():
|
|
938
|
+
future.set_result(result.model_copy(deep=True))
|
|
939
|
+
return result
|
|
940
|
+
except BaseException as exc:
|
|
941
|
+
if future is not None and not future.done():
|
|
942
|
+
future.set_exception(exc)
|
|
943
|
+
raise
|
|
944
|
+
finally:
|
|
945
|
+
if tool_use_id and inflight.get(tool_use_id) is future:
|
|
946
|
+
inflight.pop(tool_use_id, None)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
async def _execute_tool_call_uncached(
|
|
950
|
+
context: QueryContext,
|
|
951
|
+
tool_name: str,
|
|
952
|
+
tool_use_id: str,
|
|
953
|
+
tool_input: dict[str, object],
|
|
954
|
+
) -> ToolResultBlock:
|
|
955
|
+
cache = _tool_call_result_cache(context.tool_metadata)
|
|
956
|
+
cached = cache.get(tool_use_id) if tool_use_id else None
|
|
957
|
+
if isinstance(cached, ToolResultBlock):
|
|
958
|
+
return cached.model_copy(deep=True)
|
|
959
|
+
|
|
960
|
+
def _finish(result: ToolResultBlock) -> ToolResultBlock:
|
|
961
|
+
if tool_use_id:
|
|
962
|
+
cache[tool_use_id] = result.model_copy(deep=True)
|
|
963
|
+
return result
|
|
964
|
+
|
|
965
|
+
if context.hook_executor is not None:
|
|
966
|
+
pre_hooks = await context.hook_executor.execute(
|
|
967
|
+
HookEvent.PRE_TOOL_USE,
|
|
968
|
+
{"tool_name": tool_name, "tool_input": tool_input, "event": HookEvent.PRE_TOOL_USE.value},
|
|
969
|
+
)
|
|
970
|
+
if pre_hooks.blocked:
|
|
971
|
+
return _finish(ToolResultBlock(
|
|
972
|
+
tool_use_id=tool_use_id,
|
|
973
|
+
content=pre_hooks.reason or f"pre_tool_use hook blocked {tool_name}",
|
|
974
|
+
is_error=True,
|
|
975
|
+
))
|
|
976
|
+
|
|
977
|
+
log.debug("tool_call start: %s id=%s", tool_name, tool_use_id)
|
|
978
|
+
|
|
979
|
+
argument_error = tool_input.get("__openharness_tool_arguments_error")
|
|
980
|
+
if argument_error:
|
|
981
|
+
return _finish(ToolResultBlock(
|
|
982
|
+
tool_use_id=tool_use_id,
|
|
983
|
+
content=str(argument_error),
|
|
984
|
+
is_error=True,
|
|
985
|
+
))
|
|
986
|
+
|
|
987
|
+
tool = context.tool_registry.get(tool_name)
|
|
988
|
+
if tool is None:
|
|
989
|
+
log.warning("unknown tool: %s", tool_name)
|
|
990
|
+
return _finish(ToolResultBlock(
|
|
991
|
+
tool_use_id=tool_use_id,
|
|
992
|
+
content=f"Unknown tool: {tool_name}",
|
|
993
|
+
is_error=True,
|
|
994
|
+
))
|
|
995
|
+
|
|
996
|
+
try:
|
|
997
|
+
parsed_input = tool.input_model.model_validate(tool_input)
|
|
998
|
+
except Exception as exc:
|
|
999
|
+
log.warning("invalid input for %s: %s", tool_name, exc)
|
|
1000
|
+
return _finish(ToolResultBlock(
|
|
1001
|
+
tool_use_id=tool_use_id,
|
|
1002
|
+
content=f"Invalid input for {tool_name}: {exc}",
|
|
1003
|
+
is_error=True,
|
|
1004
|
+
))
|
|
1005
|
+
|
|
1006
|
+
# Normalize common tool inputs before permission checks so path rules apply
|
|
1007
|
+
# consistently across built-in tools that use `file_path`, `path`, or
|
|
1008
|
+
# directory-scoped roots such as `glob`/`grep`.
|
|
1009
|
+
_file_path = _resolve_permission_file_path(context.cwd, tool_input, parsed_input)
|
|
1010
|
+
_command = _extract_permission_command(tool_input, parsed_input)
|
|
1011
|
+
log.debug("permission check: %s read_only=%s path=%s cmd=%s",
|
|
1012
|
+
tool_name, tool.is_read_only(parsed_input), _file_path, _command and _command[:80])
|
|
1013
|
+
decision = context.permission_checker.evaluate(
|
|
1014
|
+
tool_name,
|
|
1015
|
+
is_read_only=tool.is_read_only(parsed_input),
|
|
1016
|
+
file_path=_file_path,
|
|
1017
|
+
command=_command,
|
|
1018
|
+
)
|
|
1019
|
+
if not decision.allowed:
|
|
1020
|
+
if decision.requires_confirmation and context.permission_prompt is not None:
|
|
1021
|
+
log.debug("permission prompt for %s: %s", tool_name, decision.reason)
|
|
1022
|
+
if context.hook_executor is not None:
|
|
1023
|
+
await context.hook_executor.execute(
|
|
1024
|
+
HookEvent.NOTIFICATION,
|
|
1025
|
+
{
|
|
1026
|
+
"event": HookEvent.NOTIFICATION.value,
|
|
1027
|
+
"notification_type": "permission_prompt",
|
|
1028
|
+
"tool_name": tool_name,
|
|
1029
|
+
"reason": decision.reason,
|
|
1030
|
+
},
|
|
1031
|
+
)
|
|
1032
|
+
confirmed = await context.permission_prompt(tool_name, decision.reason)
|
|
1033
|
+
if not confirmed:
|
|
1034
|
+
log.debug("permission denied by user for %s", tool_name)
|
|
1035
|
+
return _finish(ToolResultBlock(
|
|
1036
|
+
tool_use_id=tool_use_id,
|
|
1037
|
+
content=decision.reason or f"Permission denied for {tool_name}",
|
|
1038
|
+
is_error=True,
|
|
1039
|
+
))
|
|
1040
|
+
else:
|
|
1041
|
+
log.debug("permission blocked for %s: %s", tool_name, decision.reason)
|
|
1042
|
+
return _finish(ToolResultBlock(
|
|
1043
|
+
tool_use_id=tool_use_id,
|
|
1044
|
+
content=decision.reason or f"Permission denied for {tool_name}",
|
|
1045
|
+
is_error=True,
|
|
1046
|
+
))
|
|
1047
|
+
|
|
1048
|
+
log.debug("executing %s ...", tool_name)
|
|
1049
|
+
t0 = time.monotonic()
|
|
1050
|
+
try:
|
|
1051
|
+
result = await tool.execute(
|
|
1052
|
+
parsed_input,
|
|
1053
|
+
ToolExecutionContext(
|
|
1054
|
+
cwd=context.cwd,
|
|
1055
|
+
metadata={
|
|
1056
|
+
"tool_registry": context.tool_registry,
|
|
1057
|
+
"ask_user_prompt": context.ask_user_prompt,
|
|
1058
|
+
**(context.tool_metadata or {}),
|
|
1059
|
+
},
|
|
1060
|
+
hook_executor=context.hook_executor,
|
|
1061
|
+
),
|
|
1062
|
+
)
|
|
1063
|
+
except Exception as exc:
|
|
1064
|
+
return _finish(ToolResultBlock(
|
|
1065
|
+
tool_use_id=tool_use_id,
|
|
1066
|
+
content=f"Tool {tool_name} failed: {type(exc).__name__}: {exc}",
|
|
1067
|
+
is_error=True,
|
|
1068
|
+
))
|
|
1069
|
+
elapsed = time.monotonic() - t0
|
|
1070
|
+
log.debug("executed %s in %.2fs err=%s output_len=%d",
|
|
1071
|
+
tool_name, elapsed, result.is_error, len(result.output or ""))
|
|
1072
|
+
inline_output, artifact_path = _offload_tool_output_if_needed(
|
|
1073
|
+
tool_name=tool_name,
|
|
1074
|
+
tool_use_id=tool_use_id,
|
|
1075
|
+
output=result.output,
|
|
1076
|
+
)
|
|
1077
|
+
if artifact_path is not None:
|
|
1078
|
+
_remember_active_artifact(context.tool_metadata, str(artifact_path))
|
|
1079
|
+
tool_result = ToolResultBlock(
|
|
1080
|
+
tool_use_id=tool_use_id,
|
|
1081
|
+
content=inline_output,
|
|
1082
|
+
is_error=result.is_error,
|
|
1083
|
+
result_metadata=dict(result.metadata or {}),
|
|
1084
|
+
)
|
|
1085
|
+
if context.hook_executor is not None:
|
|
1086
|
+
try:
|
|
1087
|
+
post_hooks = await context.hook_executor.execute(
|
|
1088
|
+
HookEvent.POST_TOOL_USE,
|
|
1089
|
+
{
|
|
1090
|
+
"tool_name": tool_name,
|
|
1091
|
+
"tool_input": tool_input,
|
|
1092
|
+
"tool_output": tool_result.content,
|
|
1093
|
+
"tool_is_error": tool_result.is_error,
|
|
1094
|
+
"event": HookEvent.POST_TOOL_USE.value,
|
|
1095
|
+
},
|
|
1096
|
+
)
|
|
1097
|
+
except Exception as exc:
|
|
1098
|
+
post_hooks = None
|
|
1099
|
+
tool_result = ToolResultBlock(
|
|
1100
|
+
tool_use_id=tool_use_id,
|
|
1101
|
+
content=f"post_tool_use hook failed for {tool_name}: {type(exc).__name__}: {exc}",
|
|
1102
|
+
is_error=True,
|
|
1103
|
+
result_metadata=dict(result.metadata or {}),
|
|
1104
|
+
)
|
|
1105
|
+
if post_hooks is not None and post_hooks.blocked:
|
|
1106
|
+
tool_result = ToolResultBlock(
|
|
1107
|
+
tool_use_id=tool_use_id,
|
|
1108
|
+
content=post_hooks.reason or f"post_tool_use hook blocked {tool_name}",
|
|
1109
|
+
is_error=True,
|
|
1110
|
+
result_metadata=dict(result.metadata or {}),
|
|
1111
|
+
)
|
|
1112
|
+
finished_result = _finish(tool_result)
|
|
1113
|
+
try:
|
|
1114
|
+
_record_tool_carryover(
|
|
1115
|
+
context,
|
|
1116
|
+
tool_name=tool_name,
|
|
1117
|
+
tool_input=tool_input,
|
|
1118
|
+
tool_output=finished_result.content,
|
|
1119
|
+
tool_result_metadata=finished_result.result_metadata,
|
|
1120
|
+
is_error=finished_result.is_error,
|
|
1121
|
+
resolved_file_path=_file_path,
|
|
1122
|
+
)
|
|
1123
|
+
except Exception:
|
|
1124
|
+
log.exception("tool carryover recording failed: name=%s id=%s", tool_name, tool_use_id)
|
|
1125
|
+
return finished_result
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
def _tool_call_result_cache(
|
|
1129
|
+
tool_metadata: dict[str, object] | None,
|
|
1130
|
+
) -> dict[str, ToolResultBlock]:
|
|
1131
|
+
if tool_metadata is None:
|
|
1132
|
+
return {}
|
|
1133
|
+
value = tool_metadata.setdefault("tool_call_result_cache", {})
|
|
1134
|
+
if isinstance(value, dict):
|
|
1135
|
+
return value # type: ignore[return-value]
|
|
1136
|
+
replacement: dict[str, ToolResultBlock] = {}
|
|
1137
|
+
tool_metadata["tool_call_result_cache"] = replacement
|
|
1138
|
+
return replacement
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _tool_call_inflight_cache(
|
|
1142
|
+
tool_metadata: dict[str, object],
|
|
1143
|
+
) -> dict[str, asyncio.Future]:
|
|
1144
|
+
value = tool_metadata.setdefault("tool_call_inflight_cache", {})
|
|
1145
|
+
if isinstance(value, dict):
|
|
1146
|
+
return value # type: ignore[return-value]
|
|
1147
|
+
replacement: dict[str, asyncio.Future] = {}
|
|
1148
|
+
tool_metadata["tool_call_inflight_cache"] = replacement
|
|
1149
|
+
return replacement
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def _resolve_permission_file_path(
|
|
1153
|
+
cwd: Path,
|
|
1154
|
+
raw_input: dict[str, object],
|
|
1155
|
+
parsed_input: object,
|
|
1156
|
+
) -> str | None:
|
|
1157
|
+
for key in ("file_path", "path", "root"):
|
|
1158
|
+
value = raw_input.get(key)
|
|
1159
|
+
if isinstance(value, str) and value.strip():
|
|
1160
|
+
path = Path(value).expanduser()
|
|
1161
|
+
if not path.is_absolute():
|
|
1162
|
+
path = cwd / path
|
|
1163
|
+
return str(path.resolve())
|
|
1164
|
+
|
|
1165
|
+
for attr in ("file_path", "path", "root"):
|
|
1166
|
+
value = getattr(parsed_input, attr, None)
|
|
1167
|
+
if isinstance(value, str) and value.strip():
|
|
1168
|
+
path = Path(value).expanduser()
|
|
1169
|
+
if not path.is_absolute():
|
|
1170
|
+
path = cwd / path
|
|
1171
|
+
return str(path.resolve())
|
|
1172
|
+
|
|
1173
|
+
return None
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def _extract_permission_command(
|
|
1177
|
+
raw_input: dict[str, object],
|
|
1178
|
+
parsed_input: object,
|
|
1179
|
+
) -> str | None:
|
|
1180
|
+
value = raw_input.get("command")
|
|
1181
|
+
if isinstance(value, str) and value.strip():
|
|
1182
|
+
return value
|
|
1183
|
+
|
|
1184
|
+
value = getattr(parsed_input, "command", None)
|
|
1185
|
+
if isinstance(value, str) and value.strip():
|
|
1186
|
+
return value
|
|
1187
|
+
|
|
1188
|
+
return None
|