alysis-code 0.13.0__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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,3218 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import dataclasses
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
import warnings
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from collections.abc import Callable, Collection, Iterable
|
|
13
|
+
from dataclasses import dataclass, field, replace
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from .. import __version__
|
|
19
|
+
from ..agent import _patchable
|
|
20
|
+
from ..agentbox_integration import AgentBoxTelemetry
|
|
21
|
+
from ..background_runner import (
|
|
22
|
+
DisabledBackgroundRunner,
|
|
23
|
+
LazyBackgroundShellRunner,
|
|
24
|
+
build_background_shell_runner_from_settings,
|
|
25
|
+
)
|
|
26
|
+
from ..branding import env_get
|
|
27
|
+
from ..budget_policy import (
|
|
28
|
+
BUDGET_STOP_EXIT_CODE,
|
|
29
|
+
STOP_REASON_RUN_BUDGET_EXHAUSTED,
|
|
30
|
+
is_budget_cancellation,
|
|
31
|
+
resolve_budget_grace_seconds,
|
|
32
|
+
)
|
|
33
|
+
from ..build_identity import load_build_info
|
|
34
|
+
from ..cancellation import CooperativeCancellationError, EventCancellationToken
|
|
35
|
+
from ..compaction.conversation_compactor import ConversationCompactor
|
|
36
|
+
from ..compaction.settings import resolve_compaction_settings
|
|
37
|
+
from ..compaction.tool_output_offload import ToolOutputOffloader
|
|
38
|
+
from ..config import (
|
|
39
|
+
AppConfig,
|
|
40
|
+
ConfigError,
|
|
41
|
+
get_api_key,
|
|
42
|
+
resolve_api_key,
|
|
43
|
+
resolve_crash_diagnostic_log_path,
|
|
44
|
+
resolve_llm_enable_thinking,
|
|
45
|
+
resolve_llm_reasoning_effort,
|
|
46
|
+
resolve_llm_timeout_s,
|
|
47
|
+
resolve_prompt_cache_key,
|
|
48
|
+
resolve_prompt_cache_retention,
|
|
49
|
+
resolve_role_temperature,
|
|
50
|
+
)
|
|
51
|
+
from ..crash_diagnostics import (
|
|
52
|
+
CrashDiagnosticLogger,
|
|
53
|
+
build_crash_diagnostic_logger,
|
|
54
|
+
build_error_event_fields,
|
|
55
|
+
)
|
|
56
|
+
from ..custom_tools import CustomToolSessionState, build_custom_tool_session_state
|
|
57
|
+
from ..durable_service_manager import DurableServiceManager
|
|
58
|
+
from ..edit_discipline import EditDisciplineState
|
|
59
|
+
from ..error_text import sanitize_error_text_for_output
|
|
60
|
+
from ..execution_deadline import (
|
|
61
|
+
MINIMUM_FORCED_SUMMARY_SECONDS,
|
|
62
|
+
DeadlineExhausted,
|
|
63
|
+
ExecutionDeadline,
|
|
64
|
+
temporarily_clamp_client_timeout,
|
|
65
|
+
)
|
|
66
|
+
from ..extensions.activation import (
|
|
67
|
+
ActivationDecision,
|
|
68
|
+
WorkspaceTrustPromptFn,
|
|
69
|
+
WorkspaceTrustPromptRequest,
|
|
70
|
+
)
|
|
71
|
+
from ..hooks import (
|
|
72
|
+
HOOK_AUDIT_ARTIFACT_PARTS,
|
|
73
|
+
HookDispatcher,
|
|
74
|
+
HookDispatchResult,
|
|
75
|
+
ResolvedHookConfig,
|
|
76
|
+
load_resolved_hooks_config,
|
|
77
|
+
)
|
|
78
|
+
from ..host_actions import HostActionHandler
|
|
79
|
+
from ..internal_artifacts import (
|
|
80
|
+
ArtifactVisibility,
|
|
81
|
+
mark_message_internal,
|
|
82
|
+
summary_input_messages,
|
|
83
|
+
)
|
|
84
|
+
from ..llm.base import (
|
|
85
|
+
ChatClient,
|
|
86
|
+
count_input_tokens_if_supported,
|
|
87
|
+
effective_tools_for_client,
|
|
88
|
+
)
|
|
89
|
+
from ..llm.cache_policy import build_prompt_cache_namespace, derive_prompt_cache_stream_key
|
|
90
|
+
from ..llm.factory import _resolve_base_url, make_llm_client
|
|
91
|
+
from ..llm.metadata import (
|
|
92
|
+
PROVIDER_METADATA_KEY,
|
|
93
|
+
assistant_message_from_response,
|
|
94
|
+
endpoint_descriptor,
|
|
95
|
+
)
|
|
96
|
+
from ..llm.openai_compat import OpenAICompatClient as _OpenAICompatClient
|
|
97
|
+
from ..llm.protocols import OPENAI_COMPAT_PROTOCOL, get_provider_protocol_capabilities
|
|
98
|
+
from ..llm.types import UsageConfidence, UsageSource
|
|
99
|
+
from ..mcp.config import load_resolved_mcp_config
|
|
100
|
+
from ..mcp.manager import ForgeTaskScopedMcpManager, McpManager, create_mcp_manager
|
|
101
|
+
from ..model_metadata_policy import ActiveModelRef, evaluate_active_model_metadata_policy
|
|
102
|
+
from ..model_registry import ModelRegistry, resolve_model_provider_key
|
|
103
|
+
from ..model_router import ROLE_CODING, ROLE_COMPACTOR, resolve_model_for_role
|
|
104
|
+
from ..personas import (
|
|
105
|
+
PersonaSwitchState,
|
|
106
|
+
load_custom_personas,
|
|
107
|
+
normalize_persona,
|
|
108
|
+
persona_modes_enabled,
|
|
109
|
+
)
|
|
110
|
+
from ..process_reaping import (
|
|
111
|
+
ProcessGroupRegistry,
|
|
112
|
+
ReapAction,
|
|
113
|
+
ReapEvent,
|
|
114
|
+
_process_reaping_enabled,
|
|
115
|
+
reap_tracked_groups,
|
|
116
|
+
resolve_reap_decision,
|
|
117
|
+
survivor_payloads,
|
|
118
|
+
)
|
|
119
|
+
from ..profile_presets import find_preset_for_profile
|
|
120
|
+
from ..profiles import get_active_profile, resolve_effective_base_url
|
|
121
|
+
from ..provider_telemetry import (
|
|
122
|
+
ProviderCallTelemetryRecorder,
|
|
123
|
+
provider_telemetry_operation,
|
|
124
|
+
provider_telemetry_record_count,
|
|
125
|
+
set_provider_telemetry_sink,
|
|
126
|
+
)
|
|
127
|
+
from ..repo_scan import scan_workspace as scan_workspace
|
|
128
|
+
from ..request_estimation import (
|
|
129
|
+
estimate_request_token_breakdown,
|
|
130
|
+
estimate_request_tokens,
|
|
131
|
+
request_contains_media,
|
|
132
|
+
request_message_signatures,
|
|
133
|
+
tool_schema_signature,
|
|
134
|
+
)
|
|
135
|
+
from ..run_provenance import (
|
|
136
|
+
CONFIG_SNAPSHOT_EVENT,
|
|
137
|
+
config_snapshot_payload,
|
|
138
|
+
resolve_sampling_settings,
|
|
139
|
+
set_active_sampling_settings,
|
|
140
|
+
)
|
|
141
|
+
from ..runtime_context_features import resolve_runtime_context_features
|
|
142
|
+
from ..runtime_kind import RuntimeKind, resolve_session_runtime_kind
|
|
143
|
+
from ..sandbox_runner import (
|
|
144
|
+
DisabledShellRunner,
|
|
145
|
+
LazyShellRunner,
|
|
146
|
+
build_shell_runner,
|
|
147
|
+
build_shell_runner_from_settings,
|
|
148
|
+
with_closed_stdin,
|
|
149
|
+
)
|
|
150
|
+
from ..sandbox_settings import resolve_shell_sandbox_settings
|
|
151
|
+
from ..service_persistence import PersistentServiceRegistry
|
|
152
|
+
from ..session_store import SessionStore, make_session_id, resolve_sessions_dir
|
|
153
|
+
from ..skills import ConventionDocument, SkillBundle, SkillCatalogEntry
|
|
154
|
+
from ..step_budget import StepBudgetRuntime, normalize_step_budget_policy
|
|
155
|
+
from ..subagents import SubagentDefinition, unavailable_builtin_subagents
|
|
156
|
+
from ..surface import ApprovalRequest, NoopSurface, StatusEvent
|
|
157
|
+
from ..surface.base import Surface
|
|
158
|
+
from ..terminal_manager import TerminalManager
|
|
159
|
+
from ..tools.registry import iter_builtin_tool_metadata
|
|
160
|
+
from ..usage_tracker import (
|
|
161
|
+
ContextLeft,
|
|
162
|
+
RequestContextMeasurement,
|
|
163
|
+
UsageSummary,
|
|
164
|
+
build_usage_record,
|
|
165
|
+
compute_context_left,
|
|
166
|
+
usage_context_from_client_response,
|
|
167
|
+
)
|
|
168
|
+
from ..verify_gate import (
|
|
169
|
+
ResolvedVerifyCommands,
|
|
170
|
+
is_authoritative_verify_command_selection,
|
|
171
|
+
verification_selection_payload,
|
|
172
|
+
)
|
|
173
|
+
from ..workspace_binding import WorkspaceBinding
|
|
174
|
+
from ..workspace_provisioning import (
|
|
175
|
+
ProvisioningAction,
|
|
176
|
+
ShellCommandRunner,
|
|
177
|
+
ShellProbeResult,
|
|
178
|
+
_workspace_provisioning_enabled,
|
|
179
|
+
detect_declared_test_runner,
|
|
180
|
+
probe_runner_importable,
|
|
181
|
+
provision_test_runner,
|
|
182
|
+
provisioning_already_attempted,
|
|
183
|
+
resolve_provisioning_decision,
|
|
184
|
+
)
|
|
185
|
+
from .cache_keepalive import (
|
|
186
|
+
CacheKeepaliveRequest,
|
|
187
|
+
ParentCacheKeepalive,
|
|
188
|
+
cache_keepalive_unsupported_reason,
|
|
189
|
+
)
|
|
190
|
+
from .empty_response_stall import EmptyResponseStallTracker
|
|
191
|
+
from .errors import SessionWorkdirError
|
|
192
|
+
from .llm_calls import _main_agent_chat, _rewrite_final_summary_for_language
|
|
193
|
+
from .prompt_context import (
|
|
194
|
+
_build_plugin_activation_index,
|
|
195
|
+
_component_plugin_allowed,
|
|
196
|
+
_merge_dropped_counts,
|
|
197
|
+
_normalize_workspace_relpath,
|
|
198
|
+
_PluginActivationIndex,
|
|
199
|
+
_repo_summary_data,
|
|
200
|
+
_resolve_requested_workdir_within_workspace,
|
|
201
|
+
_session_verify_command_selection,
|
|
202
|
+
_subagent_context_message,
|
|
203
|
+
_workspace_binding_context_message,
|
|
204
|
+
_workspace_relpath_for_path,
|
|
205
|
+
_WorkspaceGroundingDescriptor,
|
|
206
|
+
prepare_session_prompt_context,
|
|
207
|
+
resolve_session_active_workdir_path,
|
|
208
|
+
resolve_session_active_workdir_relpath,
|
|
209
|
+
resolve_workdir_relpath_within_workspace,
|
|
210
|
+
set_session_active_workdir,
|
|
211
|
+
)
|
|
212
|
+
from .read_ledger import SessionReadLedger
|
|
213
|
+
from .tools_assembly import (
|
|
214
|
+
ToolDef,
|
|
215
|
+
ToolDispatchGuard,
|
|
216
|
+
_custom_tools_write_scope_restricted,
|
|
217
|
+
_filter_custom_tool_session_state_for_plugins,
|
|
218
|
+
_filter_mcp_config_for_plugins,
|
|
219
|
+
build_tools,
|
|
220
|
+
)
|
|
221
|
+
from .turn import (
|
|
222
|
+
_FORCED_FINAL_SUMMARY_SYSTEM_PROMPT_TEMPLATE,
|
|
223
|
+
_looks_like_unexecuted_tool_call_markup,
|
|
224
|
+
)
|
|
225
|
+
from .turn import run_turn as _run_turn
|
|
226
|
+
from .turn.events import (
|
|
227
|
+
_emit_assistant_message_events,
|
|
228
|
+
_legacy_message_tool_events_required,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if TYPE_CHECKING:
|
|
232
|
+
from ..ide.managed_browser import ManagedBrowserService
|
|
233
|
+
from .steering import SteerInbox
|
|
234
|
+
from .subagent_execution import ChildScheduler
|
|
235
|
+
|
|
236
|
+
OpenAICompatClient = _OpenAICompatClient
|
|
237
|
+
_DEFAULT_CREATE_MCP_MANAGER = create_mcp_manager
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _build_workspace_trust_prompt(
|
|
241
|
+
*,
|
|
242
|
+
surface: Surface,
|
|
243
|
+
non_interactive: bool,
|
|
244
|
+
) -> WorkspaceTrustPromptFn | None:
|
|
245
|
+
if non_interactive or env_get("ALYSIS_CI") == "1":
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
def prompt(request: WorkspaceTrustPromptRequest) -> bool:
|
|
249
|
+
preview = (
|
|
250
|
+
f"Workspace: {request.repo_root}\n"
|
|
251
|
+
f"Overrides SHA-256: {request.overrides_sha256}\n"
|
|
252
|
+
f"Project enables: {', '.join(request.plugins_added) or '-'}\n"
|
|
253
|
+
f"Project disables: {', '.join(request.plugins_removed) or '-'}"
|
|
254
|
+
)
|
|
255
|
+
decision = surface.request_approval(
|
|
256
|
+
ApprovalRequest(
|
|
257
|
+
kind="workspace_trust",
|
|
258
|
+
reason="Trust this workspace's plugin enable/disable overrides?",
|
|
259
|
+
preview=preview,
|
|
260
|
+
files=[request.repo_root],
|
|
261
|
+
metadata=request.model_dump(mode="json"),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
return bool(decision.allow)
|
|
265
|
+
|
|
266
|
+
return prompt
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _hook_plugin_id(hook_id: str | None, index: _PluginActivationIndex) -> str | None:
|
|
270
|
+
raw = str(hook_id or "").strip()
|
|
271
|
+
if "." not in raw:
|
|
272
|
+
return None
|
|
273
|
+
return index.slug_to_plugin_id.get(raw.split(".", 1)[0])
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _filter_hooks_config_for_plugins(
|
|
277
|
+
*,
|
|
278
|
+
config: ResolvedHookConfig,
|
|
279
|
+
activation_decision: ActivationDecision,
|
|
280
|
+
index: _PluginActivationIndex,
|
|
281
|
+
) -> tuple[ResolvedHookConfig, Counter[str]]:
|
|
282
|
+
dropped_counts: Counter[str] = Counter()
|
|
283
|
+
groups_by_event: dict[str, tuple[Any, ...]] = {}
|
|
284
|
+
for event_name, groups in config.groups_by_event.items():
|
|
285
|
+
kept_groups = []
|
|
286
|
+
for group in groups:
|
|
287
|
+
kept_hooks = tuple(
|
|
288
|
+
hook
|
|
289
|
+
for hook in group.hooks
|
|
290
|
+
if _component_plugin_allowed(
|
|
291
|
+
_hook_plugin_id(hook.id, index),
|
|
292
|
+
activation_decision,
|
|
293
|
+
dropped_counts,
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
if kept_hooks:
|
|
297
|
+
kept_groups.append(replace(group, hooks=kept_hooks))
|
|
298
|
+
groups_by_event[event_name] = tuple(kept_groups)
|
|
299
|
+
return (
|
|
300
|
+
ResolvedHookConfig(
|
|
301
|
+
groups_by_event=groups_by_event,
|
|
302
|
+
loaded_paths=config.loaded_paths,
|
|
303
|
+
untrusted_project_paths=config.untrusted_project_paths,
|
|
304
|
+
),
|
|
305
|
+
dropped_counts,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _make_session_llm_client(
|
|
310
|
+
*,
|
|
311
|
+
cfg: AppConfig,
|
|
312
|
+
api_key: str,
|
|
313
|
+
model: str,
|
|
314
|
+
timeout_s: float | None,
|
|
315
|
+
temperature: float,
|
|
316
|
+
prompt_cache_key: str | None,
|
|
317
|
+
prompt_cache_retention: str | None,
|
|
318
|
+
prompt_cache_namespace: str | None,
|
|
319
|
+
enable_thinking: bool | None,
|
|
320
|
+
reasoning_effort: str | None,
|
|
321
|
+
session_id: str | None,
|
|
322
|
+
) -> ChatClient:
|
|
323
|
+
openai_client_cls = _patchable("OpenAICompatClient", OpenAICompatClient)
|
|
324
|
+
if openai_client_cls is _OpenAICompatClient:
|
|
325
|
+
return make_llm_client(
|
|
326
|
+
cfg=cfg,
|
|
327
|
+
api_key=api_key,
|
|
328
|
+
model=model,
|
|
329
|
+
timeout_s=timeout_s,
|
|
330
|
+
temperature=temperature,
|
|
331
|
+
prompt_cache_key=prompt_cache_key,
|
|
332
|
+
prompt_cache_retention=prompt_cache_retention,
|
|
333
|
+
prompt_cache_namespace=prompt_cache_namespace,
|
|
334
|
+
enable_thinking=enable_thinking,
|
|
335
|
+
reasoning_effort=reasoning_effort,
|
|
336
|
+
session_id=session_id,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
profile = get_active_profile(cfg)
|
|
340
|
+
return openai_client_cls(
|
|
341
|
+
base_url=_resolve_base_url(cfg=cfg, profile=profile),
|
|
342
|
+
api_key=api_key,
|
|
343
|
+
model=model,
|
|
344
|
+
timeout_s=60.0 if timeout_s is None else timeout_s,
|
|
345
|
+
temperature=temperature,
|
|
346
|
+
prompt_cache_key=prompt_cache_key,
|
|
347
|
+
prompt_cache_retention=prompt_cache_retention,
|
|
348
|
+
enable_thinking=enable_thinking,
|
|
349
|
+
reasoning_effort=reasoning_effort,
|
|
350
|
+
extra_headers=profile.extra_headers,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _git_branch(root: Path) -> str:
|
|
355
|
+
try:
|
|
356
|
+
proc = subprocess.run(
|
|
357
|
+
["git", "-C", os.fspath(root), "rev-parse", "--abbrev-ref", "HEAD"],
|
|
358
|
+
check=False,
|
|
359
|
+
capture_output=True,
|
|
360
|
+
text=True,
|
|
361
|
+
)
|
|
362
|
+
except OSError:
|
|
363
|
+
return "-"
|
|
364
|
+
if proc.returncode != 0:
|
|
365
|
+
return "-"
|
|
366
|
+
branch = proc.stdout.strip()
|
|
367
|
+
return branch or "-"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _git_is_dirty(root: Path) -> bool:
|
|
371
|
+
try:
|
|
372
|
+
proc = subprocess.run(
|
|
373
|
+
["git", "-C", os.fspath(root), "status", "--porcelain"],
|
|
374
|
+
check=False,
|
|
375
|
+
capture_output=True,
|
|
376
|
+
text=True,
|
|
377
|
+
)
|
|
378
|
+
except OSError:
|
|
379
|
+
return False
|
|
380
|
+
if proc.returncode != 0:
|
|
381
|
+
return False
|
|
382
|
+
return bool(proc.stdout.strip())
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _surface_needs_startup_git_status(surface: Surface) -> bool:
|
|
386
|
+
if isinstance(surface, NoopSurface):
|
|
387
|
+
return False
|
|
388
|
+
# RichSurface stores this flag internally; when hidden, skip expensive startup git probes.
|
|
389
|
+
show_status_line = getattr(surface, "_show_status_line", None)
|
|
390
|
+
if isinstance(show_status_line, bool):
|
|
391
|
+
return show_status_line
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _meaningful_surface_warning_handler(surface: Surface | object) -> Callable[[str], None] | None:
|
|
396
|
+
surface_cls = getattr(surface, "__class__", None)
|
|
397
|
+
handler = getattr(surface, "emit_warning", None)
|
|
398
|
+
if callable(handler):
|
|
399
|
+
cls_handler = getattr(surface_cls, "emit_warning", None)
|
|
400
|
+
if cls_handler is not getattr(NoopSurface, "emit_warning", None):
|
|
401
|
+
return handler
|
|
402
|
+
return _meaningful_surface_legacy_warning_handler(surface)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _meaningful_surface_legacy_warning_handler(
|
|
406
|
+
surface: Surface | object,
|
|
407
|
+
) -> Callable[[str], None] | None:
|
|
408
|
+
surface_cls = getattr(surface, "__class__", None)
|
|
409
|
+
handler = getattr(surface, "on_warning", None)
|
|
410
|
+
if not callable(handler):
|
|
411
|
+
return None
|
|
412
|
+
cls_handler = getattr(surface_cls, "on_warning", None)
|
|
413
|
+
if cls_handler is getattr(NoopSurface, "on_warning", None):
|
|
414
|
+
return None
|
|
415
|
+
return handler
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _repo_summary(root: Path) -> str:
|
|
419
|
+
return _repo_summary_data(root).text
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _disable_unsupported_native_streaming(
|
|
423
|
+
*,
|
|
424
|
+
cfg: AppConfig,
|
|
425
|
+
) -> tuple[AppConfig, str | None]:
|
|
426
|
+
if not bool(getattr(cfg, "stream", False)):
|
|
427
|
+
return cfg, None
|
|
428
|
+
profile = get_active_profile(cfg)
|
|
429
|
+
protocol = str(profile.protocol or OPENAI_COMPAT_PROTOCOL).strip()
|
|
430
|
+
if protocol == OPENAI_COMPAT_PROTOCOL:
|
|
431
|
+
return cfg, None
|
|
432
|
+
base_url = _resolve_base_url(cfg=cfg, profile=profile)
|
|
433
|
+
provider_key = resolve_model_provider_key(
|
|
434
|
+
cfg=cfg,
|
|
435
|
+
model_name=cfg.model,
|
|
436
|
+
base_url=base_url,
|
|
437
|
+
profile_name=profile.name,
|
|
438
|
+
)
|
|
439
|
+
capabilities = get_provider_protocol_capabilities(
|
|
440
|
+
provider_key=provider_key,
|
|
441
|
+
protocol=protocol,
|
|
442
|
+
)
|
|
443
|
+
# Unknown provider capabilities must not disable streaming: assume it works
|
|
444
|
+
# and rely on the per-step stream-unsupported fallback in the turn loop to
|
|
445
|
+
# downgrade at runtime if the provider rejects it.
|
|
446
|
+
streaming_supported = capabilities.supports_streaming if capabilities is not None else True
|
|
447
|
+
if streaming_supported:
|
|
448
|
+
return cfg, None
|
|
449
|
+
warning = (
|
|
450
|
+
f"Streaming requested but profile {profile.name!r} uses protocol={protocol!r}, "
|
|
451
|
+
"which does not support streaming yet in Alysis Code; streaming is disabled for this run."
|
|
452
|
+
)
|
|
453
|
+
return cfg.model_copy(update={"stream": False}, deep=True), warning
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class ForcedFinalSummaryTerminationKind(StrEnum):
|
|
457
|
+
STEP_BUDGET_EXHAUSTED = "step_budget_exhausted"
|
|
458
|
+
COMPLETION_GATE_STAGNATION = "completion_gate_stagnation"
|
|
459
|
+
EXECUTION_GUARD_STAGNATION = "execution_guard_stagnation"
|
|
460
|
+
DEADLINE_EXHAUSTED = "deadline_exhausted"
|
|
461
|
+
OTHER = "other"
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _normalize_forced_summary_termination_kind(
|
|
465
|
+
value: str | ForcedFinalSummaryTerminationKind,
|
|
466
|
+
) -> ForcedFinalSummaryTerminationKind:
|
|
467
|
+
if isinstance(value, ForcedFinalSummaryTerminationKind):
|
|
468
|
+
return value
|
|
469
|
+
normalized = str(value or "").strip().lower()
|
|
470
|
+
for item in ForcedFinalSummaryTerminationKind:
|
|
471
|
+
if normalized == item.value:
|
|
472
|
+
return item
|
|
473
|
+
return ForcedFinalSummaryTerminationKind.OTHER
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _add_event_diagnostics(
|
|
477
|
+
payload: dict[str, Any],
|
|
478
|
+
diagnostics: dict[str, Any] | None,
|
|
479
|
+
) -> dict[str, Any]:
|
|
480
|
+
if not diagnostics:
|
|
481
|
+
return payload
|
|
482
|
+
for key, value in diagnostics.items():
|
|
483
|
+
payload.setdefault(key, value)
|
|
484
|
+
return payload
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@dataclass
|
|
488
|
+
class AgentSession:
|
|
489
|
+
cfg: AppConfig
|
|
490
|
+
root: Path
|
|
491
|
+
mode: str
|
|
492
|
+
yes: bool
|
|
493
|
+
stream: bool
|
|
494
|
+
routing_mode: str
|
|
495
|
+
max_steps: int | None
|
|
496
|
+
console: Any | None
|
|
497
|
+
surface: Surface
|
|
498
|
+
store: SessionStore
|
|
499
|
+
client: ChatClient
|
|
500
|
+
model_registry: ModelRegistry
|
|
501
|
+
usage_summary: UsageSummary
|
|
502
|
+
usage_role: str
|
|
503
|
+
tool_output_offloader: ToolOutputOffloader | None
|
|
504
|
+
conversation_compactor: ConversationCompactor | None
|
|
505
|
+
tool_output_offload_enabled: bool
|
|
506
|
+
conversation_summarization_enabled: bool
|
|
507
|
+
compaction_profile: str
|
|
508
|
+
tools: dict[str, ToolDef]
|
|
509
|
+
tool_list: list[dict[str, Any]]
|
|
510
|
+
messages: list[dict[str, Any]]
|
|
511
|
+
_cache_efficiency_summary_recorded: bool = field(
|
|
512
|
+
default=False,
|
|
513
|
+
init=False,
|
|
514
|
+
repr=False,
|
|
515
|
+
)
|
|
516
|
+
# Machine-readable reason this run stopped, set by whichever path
|
|
517
|
+
# terminates the turn. Read by close() so the run_finished crash event
|
|
518
|
+
# carries it, which is how a harness tells a budget stop from a crash.
|
|
519
|
+
stop_reason: str | None = field(default=None, init=False, repr=False)
|
|
520
|
+
startup_messages: list[dict[str, Any]] = field(default_factory=list)
|
|
521
|
+
runtime_kind: RuntimeKind = RuntimeKind.INTERACTIVE_CHAT
|
|
522
|
+
prompt_cache_stream_key: str | None = None
|
|
523
|
+
mcp_manager: McpManager | ForgeTaskScopedMcpManager | None = None
|
|
524
|
+
terminal_manager: TerminalManager | None = None
|
|
525
|
+
durable_service_manager: DurableServiceManager | None = None
|
|
526
|
+
persistent_service_registry: PersistentServiceRegistry | None = None
|
|
527
|
+
edit_discipline: EditDisciplineState | None = None
|
|
528
|
+
managed_browser_service: ManagedBrowserService | None = None
|
|
529
|
+
managed_browser_owner_id: str | None = None
|
|
530
|
+
managed_browser_cancel_check: Callable[[], bool] | None = None
|
|
531
|
+
router_client: Any | None = None
|
|
532
|
+
_semantic_router_bound_client: Any | None = None
|
|
533
|
+
_provisioned_router_client: Any | None = None
|
|
534
|
+
api_key: str = ""
|
|
535
|
+
api_key_source: str = "missing"
|
|
536
|
+
shell_runner: Any | None = None
|
|
537
|
+
no_log: bool = False
|
|
538
|
+
non_interactive: bool = False
|
|
539
|
+
one_shot_execution: bool = False
|
|
540
|
+
# Persona mode (code|architect|ask|debug). A convention layered on the
|
|
541
|
+
# execution-mode gate, never an enforcement layer; "code" is the no-op
|
|
542
|
+
# persona. See docs/persona_modes_design.md.
|
|
543
|
+
persona: str = "code"
|
|
544
|
+
# The user's chosen execution mode remembered while a narrowing persona
|
|
545
|
+
# (architect/ask) is active, so switching back to code/debug restores it.
|
|
546
|
+
# None when the active persona narrows nothing. An explicit /mode <exec>
|
|
547
|
+
# always wins and clears the restore point (and restores the write scope).
|
|
548
|
+
persona_restore_mode: str | None = None
|
|
549
|
+
# The user's base allow_write_globs snapshotted alongside
|
|
550
|
+
# persona_restore_mode (may legitimately be None = unrestricted; the
|
|
551
|
+
# restore-active signal is persona_restore_mode itself).
|
|
552
|
+
persona_restore_write_globs: list[str] | None = None
|
|
553
|
+
# Active persona scope, enforced independently from the user's base
|
|
554
|
+
# allow_write_globs. A path must satisfy both scopes when both exist.
|
|
555
|
+
persona_allow_write_globs: list[str] | None = None
|
|
556
|
+
# Coordination cell for the switch_mode tool (interactive chat only; None
|
|
557
|
+
# everywhere else, which also keeps the tool unregistered).
|
|
558
|
+
persona_switch_state: PersonaSwitchState | None = None
|
|
559
|
+
# Persona clients are keyed by the resolved client configuration that can
|
|
560
|
+
# vary by role. Model-only caching would reuse the wrong temperature when
|
|
561
|
+
# two roles intentionally share a model.
|
|
562
|
+
persona_client_cache: dict[tuple[str, float], Any] | None = None
|
|
563
|
+
persona_client_key: tuple[str, float] | None = None
|
|
564
|
+
# Custom personas loaded from .alysis_personas / <user-config>/personas
|
|
565
|
+
# (interactive chat only; builtins always win on name collisions).
|
|
566
|
+
persona_registry: dict[str, Any] | None = None
|
|
567
|
+
persona_registry_warnings: tuple[str, ...] = ()
|
|
568
|
+
enable_chat_turn_step_budget: bool = False
|
|
569
|
+
chat_turn_fixed_override: int | None = None
|
|
570
|
+
verification_enabled: bool = True
|
|
571
|
+
effective_verification_commands: list[str] = field(default_factory=list)
|
|
572
|
+
authoritative_verification_commands: list[str] | None = None
|
|
573
|
+
verification_selection_source: str = ""
|
|
574
|
+
verification_selection_reason: str = ""
|
|
575
|
+
verification_contract_type: str = ""
|
|
576
|
+
verification_authoritative: bool = False
|
|
577
|
+
verification_best_effort: bool = False
|
|
578
|
+
deny_write_prefixes: list[str] | None = None
|
|
579
|
+
allow_write_globs: list[str] | None = None
|
|
580
|
+
session_log_dir_override: Path | None = None
|
|
581
|
+
skills_enabled: bool = True
|
|
582
|
+
skills_auto_invoke: bool = True
|
|
583
|
+
skill_registry: dict[str, SkillBundle] | None = None
|
|
584
|
+
skills_ordered: tuple[SkillBundle, ...] = ()
|
|
585
|
+
skill_discovery_issues: tuple[Any, ...] = ()
|
|
586
|
+
skill_catalog_entries: tuple[SkillCatalogEntry, ...] = ()
|
|
587
|
+
repo_conventions: tuple[ConventionDocument, ...] = ()
|
|
588
|
+
subagents_enabled: bool = False
|
|
589
|
+
enforce_explicit_subagent_requests: bool = True
|
|
590
|
+
subagent_depth: int = 0
|
|
591
|
+
subagent_registry: dict[str, SubagentDefinition] | None = None
|
|
592
|
+
child_scheduler: ChildScheduler | None = None
|
|
593
|
+
steer_inbox: SteerInbox = field(default_factory=SteerInbox)
|
|
594
|
+
child_repetition_signal: Callable[[dict[str, Any]], bool] | None = None
|
|
595
|
+
read_ledger: SessionReadLedger | None = None
|
|
596
|
+
step_system_message_provider: Callable[[], list[str]] | None = None
|
|
597
|
+
step_system_message_delivery_observer: Callable[[int], None] | None = None
|
|
598
|
+
step_budget_runtime: StepBudgetRuntime | None = None
|
|
599
|
+
planner_workspace_context: dict[str, Any] | None = None
|
|
600
|
+
workspace_grounding: _WorkspaceGroundingDescriptor | None = None
|
|
601
|
+
focus_dir: Path | None = None
|
|
602
|
+
focus_relpath: str = "."
|
|
603
|
+
workspace_kind: str = "plain_dir"
|
|
604
|
+
binding_requested_path: str | None = None
|
|
605
|
+
binding_source: str | None = None
|
|
606
|
+
binding_risk_level: str | None = None
|
|
607
|
+
binding_created_path: bool | None = None
|
|
608
|
+
active_workdir_relpath: str = "."
|
|
609
|
+
session_source: str = "startup"
|
|
610
|
+
session_source_metadata: dict[str, Any] = field(default_factory=dict)
|
|
611
|
+
pinned_prefix_len: int = 0
|
|
612
|
+
startup_context_baseline_tokens: int = 0
|
|
613
|
+
request_context_measurement: RequestContextMeasurement | None = None
|
|
614
|
+
workspace_touched_paths: set[str] = field(default_factory=set)
|
|
615
|
+
workspace_writes_allowed: bool | None = None
|
|
616
|
+
custom_tool_session_state: CustomToolSessionState | None = None
|
|
617
|
+
hook_dispatcher: HookDispatcher | None = None
|
|
618
|
+
execution_deadline: ExecutionDeadline | None = None
|
|
619
|
+
cache_keepalive: ParentCacheKeepalive | None = None
|
|
620
|
+
crash_diagnostics: CrashDiagnosticLogger | None = None
|
|
621
|
+
crash_diagnostic_log_path: str | None = None
|
|
622
|
+
agentbox_telemetry: AgentBoxTelemetry | None = None
|
|
623
|
+
process_group_registry: ProcessGroupRegistry | None = None
|
|
624
|
+
# Empty-response handling is budgeted per session, not per turn: two failed
|
|
625
|
+
# recovery cycles mean the endpoint is not answering, and re-spending the
|
|
626
|
+
# budget every turn would reintroduce the unbounded retrying this bounds.
|
|
627
|
+
empty_response_stall_tracker: EmptyResponseStallTracker | None = None
|
|
628
|
+
|
|
629
|
+
def workspace_write_contract_allows_writes(self) -> bool:
|
|
630
|
+
"""Return the effective write capability after role/tool filtering."""
|
|
631
|
+
writes_allowed = self.workspace_writes_allowed
|
|
632
|
+
if writes_allowed is None:
|
|
633
|
+
writes_allowed = str(self.mode or "").strip().lower() != "readonly"
|
|
634
|
+
return writes_allowed
|
|
635
|
+
|
|
636
|
+
def initial_outstanding_action(self) -> str:
|
|
637
|
+
"""Describe the first unmet outcome allowed by this session's contract."""
|
|
638
|
+
if self.workspace_write_contract_allows_writes():
|
|
639
|
+
return "edit a relevant path"
|
|
640
|
+
return "deliver the requested analysis or report"
|
|
641
|
+
|
|
642
|
+
def __post_init__(self) -> None:
|
|
643
|
+
self._bind_provider_retry_observer(self.client)
|
|
644
|
+
if self.child_scheduler is None:
|
|
645
|
+
subagent_tool = self.tools.get("subagent_run")
|
|
646
|
+
launcher = getattr(getattr(subagent_tool, "run", None), "__self__", None)
|
|
647
|
+
scheduler = getattr(launcher, "child_scheduler", None)
|
|
648
|
+
if scheduler is not None:
|
|
649
|
+
self.child_scheduler = scheduler
|
|
650
|
+
if self.child_scheduler is not None:
|
|
651
|
+
self.child_scheduler.set_parent_steer_inbox(self.steer_inbox)
|
|
652
|
+
if self.cache_keepalive is None and self.subagent_depth == 0:
|
|
653
|
+
cache_config = self.cfg.cache
|
|
654
|
+
unsupported_reason = cache_keepalive_unsupported_reason(self.client)
|
|
655
|
+
self.cache_keepalive = ParentCacheKeepalive(
|
|
656
|
+
enabled=cache_config.keepalive_enabled,
|
|
657
|
+
idle_threshold_s=cache_config.keepalive_idle_threshold_s,
|
|
658
|
+
send_ping=self._send_cache_keepalive,
|
|
659
|
+
on_disabled=self._on_cache_keepalive_disabled,
|
|
660
|
+
on_unsupported=self._on_cache_keepalive_unsupported,
|
|
661
|
+
unsupported_reason=unsupported_reason,
|
|
662
|
+
deadline=self.execution_deadline,
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
def _bind_provider_retry_observer(self, client: Any) -> None:
|
|
666
|
+
if client is None:
|
|
667
|
+
return
|
|
668
|
+
|
|
669
|
+
def _record_retry(payload: dict[str, object]) -> None:
|
|
670
|
+
safe_payload = {
|
|
671
|
+
"provider": str(payload.get("provider") or "unknown"),
|
|
672
|
+
"attempt": max(1, int(payload.get("attempt") or 1)),
|
|
673
|
+
"reason": str(payload.get("reason") or "provider_retry"),
|
|
674
|
+
"elapsed_ms": max(0, int(payload.get("elapsed_ms") or 0)),
|
|
675
|
+
}
|
|
676
|
+
self.store.append("llm_call_retry", safe_payload)
|
|
677
|
+
set_activity = getattr(self.surface, "set_model_retry_activity", None)
|
|
678
|
+
if callable(set_activity):
|
|
679
|
+
set_activity(safe_payload["attempt"])
|
|
680
|
+
|
|
681
|
+
try:
|
|
682
|
+
client._provider_retry_event_observer = _record_retry
|
|
683
|
+
except Exception: # noqa: BLE001 - diagnostics must not break a provider.
|
|
684
|
+
pass
|
|
685
|
+
|
|
686
|
+
def _reap_tracked_process_groups(self, *, event: ReapEvent) -> None:
|
|
687
|
+
"""Terminate or report the process groups this session's runner started.
|
|
688
|
+
|
|
689
|
+
Runs on every turn exit path (success, honest-unverified, error,
|
|
690
|
+
cancellation) and again at session close. Only groups the runner itself
|
|
691
|
+
created and recorded are ever signalled; nothing is discovered by name
|
|
692
|
+
or by scanning the process table.
|
|
693
|
+
"""
|
|
694
|
+
registry = self.process_group_registry
|
|
695
|
+
if registry is None:
|
|
696
|
+
return
|
|
697
|
+
try:
|
|
698
|
+
decision = resolve_reap_decision(
|
|
699
|
+
runtime_kind=self.runtime_kind,
|
|
700
|
+
event=event,
|
|
701
|
+
enabled=_process_reaping_enabled(self.cfg),
|
|
702
|
+
)
|
|
703
|
+
if decision.action is ReapAction.SKIP:
|
|
704
|
+
return
|
|
705
|
+
if decision.action is ReapAction.REPORT:
|
|
706
|
+
survivors = survivor_payloads(registry)
|
|
707
|
+
if survivors:
|
|
708
|
+
self.store.append(
|
|
709
|
+
"process_survivors",
|
|
710
|
+
{
|
|
711
|
+
"event": str(event),
|
|
712
|
+
"runtime_kind": self.runtime_kind.value,
|
|
713
|
+
"reason": decision.reason,
|
|
714
|
+
"count": len(survivors),
|
|
715
|
+
"groups": list(survivors),
|
|
716
|
+
},
|
|
717
|
+
)
|
|
718
|
+
return
|
|
719
|
+
for outcome in reap_tracked_groups(registry):
|
|
720
|
+
self.store.append(
|
|
721
|
+
"process_reaped",
|
|
722
|
+
{
|
|
723
|
+
"event": str(event),
|
|
724
|
+
"runtime_kind": self.runtime_kind.value,
|
|
725
|
+
"reason": decision.reason,
|
|
726
|
+
**outcome.payload(),
|
|
727
|
+
},
|
|
728
|
+
)
|
|
729
|
+
except Exception as exc: # noqa: BLE001 - hygiene must never break the turn
|
|
730
|
+
try:
|
|
731
|
+
self.store.append(
|
|
732
|
+
"warning",
|
|
733
|
+
{"warning": "process_reaping_failed", "error": str(exc)},
|
|
734
|
+
)
|
|
735
|
+
except Exception: # noqa: BLE001 - best-effort diagnostic
|
|
736
|
+
pass
|
|
737
|
+
|
|
738
|
+
def close(self, *, reason: str = "session_close") -> None:
|
|
739
|
+
self._reap_tracked_process_groups(event=ReapEvent.SESSION_CLOSE)
|
|
740
|
+
if self.cache_keepalive is not None:
|
|
741
|
+
self.cache_keepalive.close()
|
|
742
|
+
if self.child_scheduler is not None:
|
|
743
|
+
try:
|
|
744
|
+
self.child_scheduler.shutdown(cancel_pending=True)
|
|
745
|
+
except Exception as exc: # noqa: BLE001 - session teardown must continue
|
|
746
|
+
self._hook_warning(
|
|
747
|
+
f"Child scheduler shutdown failed: {exc}",
|
|
748
|
+
code="child_scheduler_shutdown_failed",
|
|
749
|
+
)
|
|
750
|
+
if self.subagent_depth == 0:
|
|
751
|
+
# Release the process-wide telemetry sink registered for this top-level run.
|
|
752
|
+
set_provider_telemetry_sink(None)
|
|
753
|
+
if self.terminal_manager is not None:
|
|
754
|
+
try:
|
|
755
|
+
self.terminal_manager.shutdown_all()
|
|
756
|
+
except Exception as exc: # noqa: BLE001
|
|
757
|
+
# Session teardown must continue even if terminal shutdown hits an unexpected bug.
|
|
758
|
+
self._hook_warning(
|
|
759
|
+
f"Terminal manager shutdown failed: {exc}",
|
|
760
|
+
code="terminal_shutdown_failed",
|
|
761
|
+
)
|
|
762
|
+
if self.durable_service_manager is not None:
|
|
763
|
+
try:
|
|
764
|
+
active_services = self.durable_service_manager.list_active()
|
|
765
|
+
except Exception as exc: # noqa: BLE001
|
|
766
|
+
self._hook_warning(
|
|
767
|
+
f"Durable service status check failed during close: {exc}",
|
|
768
|
+
code="durable_service_status_failed",
|
|
769
|
+
)
|
|
770
|
+
else:
|
|
771
|
+
if active_services:
|
|
772
|
+
self.store.append(
|
|
773
|
+
"durable_services_left_active",
|
|
774
|
+
{
|
|
775
|
+
"count": len(active_services),
|
|
776
|
+
"services": active_services,
|
|
777
|
+
},
|
|
778
|
+
)
|
|
779
|
+
if not self._cache_efficiency_summary_recorded:
|
|
780
|
+
cache_summary = self.usage_summary.cache_efficiency_summary()
|
|
781
|
+
if int(cache_summary.get("reported_calls") or 0) > 0:
|
|
782
|
+
self.store.append("cache_efficiency_summary", cache_summary)
|
|
783
|
+
self._cache_efficiency_summary_recorded = True
|
|
784
|
+
try:
|
|
785
|
+
if self.crash_diagnostics is not None:
|
|
786
|
+
run_finished_payload: dict[str, Any] = {
|
|
787
|
+
"status": reason,
|
|
788
|
+
"runtime_kind": self.runtime_kind.value,
|
|
789
|
+
"deadline": (
|
|
790
|
+
self.execution_deadline.telemetry_snapshot()
|
|
791
|
+
if self.execution_deadline is not None
|
|
792
|
+
else None
|
|
793
|
+
),
|
|
794
|
+
}
|
|
795
|
+
if self.stop_reason:
|
|
796
|
+
# Only present when a path actually claimed a reason, so an
|
|
797
|
+
# ordinary run's event is byte-identical to before.
|
|
798
|
+
run_finished_payload["stop_reason"] = self.stop_reason
|
|
799
|
+
self.crash_diagnostics.event(
|
|
800
|
+
"run_finished",
|
|
801
|
+
run_finished_payload,
|
|
802
|
+
durable=True,
|
|
803
|
+
)
|
|
804
|
+
cwd, active_workdir_relpath = self._hook_runtime_context()
|
|
805
|
+
self._safe_dispatch_hooks(
|
|
806
|
+
lambda: self.hook_dispatcher.fire_session_end(
|
|
807
|
+
cwd=cwd,
|
|
808
|
+
active_workdir_relpath=active_workdir_relpath,
|
|
809
|
+
payload={
|
|
810
|
+
"reason": reason,
|
|
811
|
+
"mode": self.mode,
|
|
812
|
+
"runtime_kind": self.runtime_kind.value,
|
|
813
|
+
"session_source": self.session_source,
|
|
814
|
+
"session_source_metadata": copy.deepcopy(self.session_source_metadata),
|
|
815
|
+
"workspace_root": os.fspath(self.root),
|
|
816
|
+
"focus_dir": os.fspath(self.focus_dir or self.root),
|
|
817
|
+
"focus_relpath": self.focus_relpath,
|
|
818
|
+
"active_workdir": os.fspath(cwd),
|
|
819
|
+
"active_workdir_relpath": active_workdir_relpath,
|
|
820
|
+
"workspace_kind": self.workspace_kind,
|
|
821
|
+
"usage_role": self.usage_role,
|
|
822
|
+
"message_count": len(self.messages),
|
|
823
|
+
"pinned_prefix_len": self.pinned_prefix_len,
|
|
824
|
+
"subagent_depth": self.subagent_depth,
|
|
825
|
+
"skills_enabled": self.skills_enabled,
|
|
826
|
+
"subagents_enabled": self.subagents_enabled,
|
|
827
|
+
},
|
|
828
|
+
)
|
|
829
|
+
)
|
|
830
|
+
if self.mcp_manager is not None:
|
|
831
|
+
self.mcp_manager.close()
|
|
832
|
+
finally:
|
|
833
|
+
if self.agentbox_telemetry is not None:
|
|
834
|
+
self.agentbox_telemetry.close(error=reason not in {"session_close", "completed"})
|
|
835
|
+
self.store.close()
|
|
836
|
+
|
|
837
|
+
def _hook_warning(self, message: str, *, code: str = "hook_warning") -> None:
|
|
838
|
+
clean = str(message or "").strip()
|
|
839
|
+
if not clean:
|
|
840
|
+
return
|
|
841
|
+
self.store.append("warning", {"warning": code, "message": clean})
|
|
842
|
+
surface_on_warning = _meaningful_surface_warning_handler(self.surface)
|
|
843
|
+
if callable(surface_on_warning):
|
|
844
|
+
surface_on_warning(clean)
|
|
845
|
+
else:
|
|
846
|
+
warnings.warn(clean, stacklevel=2)
|
|
847
|
+
|
|
848
|
+
def _safe_dispatch_hooks(
|
|
849
|
+
self,
|
|
850
|
+
dispatcher_call: Callable[[], HookDispatchResult],
|
|
851
|
+
) -> HookDispatchResult:
|
|
852
|
+
if self.hook_dispatcher is None:
|
|
853
|
+
return HookDispatchResult()
|
|
854
|
+
try:
|
|
855
|
+
result = dispatcher_call()
|
|
856
|
+
except Exception as exc: # noqa: BLE001
|
|
857
|
+
self._hook_warning(
|
|
858
|
+
f"Lifecycle hook dispatch failed: {exc}",
|
|
859
|
+
code="hook_dispatch_failed",
|
|
860
|
+
)
|
|
861
|
+
return HookDispatchResult()
|
|
862
|
+
for notice in result.system_notices:
|
|
863
|
+
self._hook_notice(notice)
|
|
864
|
+
return result
|
|
865
|
+
|
|
866
|
+
def _hook_notice(self, message: str) -> None:
|
|
867
|
+
clean = str(message or "").strip()
|
|
868
|
+
if not clean:
|
|
869
|
+
return
|
|
870
|
+
self.store.append("hook_notice", {"message": clean})
|
|
871
|
+
handler = getattr(self.surface, "on_notice", None)
|
|
872
|
+
if callable(handler):
|
|
873
|
+
handler(clean)
|
|
874
|
+
return
|
|
875
|
+
fallback = _meaningful_surface_legacy_warning_handler(self.surface)
|
|
876
|
+
if callable(fallback):
|
|
877
|
+
fallback(clean)
|
|
878
|
+
|
|
879
|
+
def _hook_runtime_context(self) -> tuple[Path, str]:
|
|
880
|
+
return (
|
|
881
|
+
resolve_session_active_workdir_path(self),
|
|
882
|
+
resolve_session_active_workdir_relpath(self),
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
def _append_hook_messages(
|
|
886
|
+
self,
|
|
887
|
+
*,
|
|
888
|
+
event_name: str,
|
|
889
|
+
system_messages: Iterable[str] = (),
|
|
890
|
+
user_messages: Iterable[str] = (),
|
|
891
|
+
pinned: bool = False,
|
|
892
|
+
) -> int:
|
|
893
|
+
appended_count = 0
|
|
894
|
+
for role, messages in (("system", system_messages), ("user", user_messages)):
|
|
895
|
+
for raw_message in messages:
|
|
896
|
+
text = str(raw_message or "").strip()
|
|
897
|
+
if not text:
|
|
898
|
+
continue
|
|
899
|
+
self.messages.append({"role": role, "content": text})
|
|
900
|
+
appended_count += 1
|
|
901
|
+
self.store.append(
|
|
902
|
+
"hook_message_added",
|
|
903
|
+
{
|
|
904
|
+
"event_name": event_name,
|
|
905
|
+
"role": role,
|
|
906
|
+
"chars": len(text),
|
|
907
|
+
"pinned": pinned,
|
|
908
|
+
},
|
|
909
|
+
)
|
|
910
|
+
if pinned and appended_count > 0:
|
|
911
|
+
self.pinned_prefix_len += appended_count
|
|
912
|
+
return appended_count
|
|
913
|
+
|
|
914
|
+
def context_left(self) -> ContextLeft:
|
|
915
|
+
compaction_settings = resolve_compaction_settings(self.cfg)
|
|
916
|
+
effective_tool_list = effective_tools_for_client(self.client, self.tool_list)
|
|
917
|
+
startup_baseline_tokens = self.startup_context_baseline_tokens
|
|
918
|
+
if self.startup_messages:
|
|
919
|
+
# Tool support can change after the provider rejects a tool-bearing
|
|
920
|
+
# request. Keep the dynamic HUD baseline aligned with what the
|
|
921
|
+
# client's current transport state will actually send.
|
|
922
|
+
startup_baseline_tokens = estimate_request_token_breakdown(
|
|
923
|
+
messages=self.startup_messages,
|
|
924
|
+
tool_list=effective_tool_list,
|
|
925
|
+
pinned_prefix_len=self.pinned_prefix_len,
|
|
926
|
+
).total_tokens
|
|
927
|
+
usage_context = usage_context_from_client_response(
|
|
928
|
+
client=self.client,
|
|
929
|
+
response=None,
|
|
930
|
+
operation="main_llm",
|
|
931
|
+
)
|
|
932
|
+
request_measurement = self.request_context_measurement
|
|
933
|
+
if request_measurement is not None and not request_measurement.matches_route(
|
|
934
|
+
requested_model=self.client.model,
|
|
935
|
+
provider_key=usage_context.get("provider_key"),
|
|
936
|
+
protocol=usage_context.get("protocol"),
|
|
937
|
+
base_url_host=usage_context.get("base_url_host"),
|
|
938
|
+
):
|
|
939
|
+
request_measurement = None
|
|
940
|
+
calibration = self.usage_summary.recent_calibration_snapshot(
|
|
941
|
+
requested_model=self.client.model,
|
|
942
|
+
provider_key=usage_context.get("provider_key"),
|
|
943
|
+
protocol=usage_context.get("protocol"),
|
|
944
|
+
base_url_host=usage_context.get("base_url_host"),
|
|
945
|
+
operation="main_llm",
|
|
946
|
+
request_mode=(request_measurement.request_mode if request_measurement else None),
|
|
947
|
+
cache_strategy=(request_measurement.cache_strategy if request_measurement else None),
|
|
948
|
+
limit=20,
|
|
949
|
+
)
|
|
950
|
+
estimate_multiplier = calibration.get("prompt_estimate_error_ratio_p90")
|
|
951
|
+
return compute_context_left(
|
|
952
|
+
messages=self.messages,
|
|
953
|
+
model_name=self.client.model,
|
|
954
|
+
registry=self.model_registry,
|
|
955
|
+
tool_list=effective_tool_list,
|
|
956
|
+
pinned_prefix_len=self.pinned_prefix_len,
|
|
957
|
+
safety_margin_tokens=compaction_settings.safety_margin_tokens,
|
|
958
|
+
startup_baseline_tokens=startup_baseline_tokens,
|
|
959
|
+
prompt_estimate_multiplier=(
|
|
960
|
+
float(estimate_multiplier) if isinstance(estimate_multiplier, int | float) else None
|
|
961
|
+
),
|
|
962
|
+
request_measurement=request_measurement,
|
|
963
|
+
)
|
|
964
|
+
|
|
965
|
+
def refresh_compactor_calibration_filters(self) -> None:
|
|
966
|
+
compactor = self.conversation_compactor
|
|
967
|
+
updater = getattr(compactor, "update_calibration_filters", None)
|
|
968
|
+
if not callable(updater):
|
|
969
|
+
return
|
|
970
|
+
updater(
|
|
971
|
+
usage_context_from_client_response(
|
|
972
|
+
client=self.client,
|
|
973
|
+
response=None,
|
|
974
|
+
operation="main_llm",
|
|
975
|
+
)
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
def invalidate_request_context(self, *, reason: str) -> None:
|
|
979
|
+
if self.request_context_measurement is None:
|
|
980
|
+
return
|
|
981
|
+
self.request_context_measurement = None
|
|
982
|
+
self.store.append(
|
|
983
|
+
"request_context_invalidated",
|
|
984
|
+
{"reason": str(reason or "request_shape_changed")},
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
@staticmethod
|
|
988
|
+
def _normalize_visible_assistant_text(text: str) -> str:
|
|
989
|
+
return str(text or "").strip()
|
|
990
|
+
|
|
991
|
+
def _emit_assistant_message_if_changed(
|
|
992
|
+
self,
|
|
993
|
+
*,
|
|
994
|
+
text: str,
|
|
995
|
+
prior_visible_text: str = "",
|
|
996
|
+
extra_payload: dict[str, Any] | None = None,
|
|
997
|
+
streamed_text_emitted: bool = False,
|
|
998
|
+
) -> str:
|
|
999
|
+
normalized_text = self._normalize_visible_assistant_text(text)
|
|
1000
|
+
if not normalized_text:
|
|
1001
|
+
if extra_payload:
|
|
1002
|
+
payload = {"content": text}
|
|
1003
|
+
payload.update(extra_payload)
|
|
1004
|
+
self.store.append("assistant_message", payload)
|
|
1005
|
+
return self._normalize_visible_assistant_text(prior_visible_text)
|
|
1006
|
+
if normalized_text == self._normalize_visible_assistant_text(prior_visible_text):
|
|
1007
|
+
if extra_payload:
|
|
1008
|
+
payload = {"content": text}
|
|
1009
|
+
payload.update(extra_payload)
|
|
1010
|
+
self.store.append("assistant_message", payload)
|
|
1011
|
+
return normalized_text
|
|
1012
|
+
payload = {"content": text}
|
|
1013
|
+
if extra_payload:
|
|
1014
|
+
payload.update(extra_payload)
|
|
1015
|
+
self.store.append("assistant_message", payload)
|
|
1016
|
+
_emit_assistant_message_events(
|
|
1017
|
+
self.surface,
|
|
1018
|
+
text,
|
|
1019
|
+
streamed_text_emitted=streamed_text_emitted,
|
|
1020
|
+
)
|
|
1021
|
+
if _legacy_message_tool_events_required(self.surface):
|
|
1022
|
+
self.surface.on_assistant_message_done(text)
|
|
1023
|
+
return normalized_text
|
|
1024
|
+
|
|
1025
|
+
def _record_llm_usage(
|
|
1026
|
+
self,
|
|
1027
|
+
*,
|
|
1028
|
+
client: Any,
|
|
1029
|
+
response: Any,
|
|
1030
|
+
messages: list[dict[str, Any]],
|
|
1031
|
+
tool_list: list[dict[str, Any]] | None,
|
|
1032
|
+
operation: str,
|
|
1033
|
+
role_override: str | None = None,
|
|
1034
|
+
) -> Any | None:
|
|
1035
|
+
"""Record one provider call without allowing telemetry to break the turn."""
|
|
1036
|
+
if response is None:
|
|
1037
|
+
return None
|
|
1038
|
+
try:
|
|
1039
|
+
tool_list = effective_tools_for_client(client, tool_list)
|
|
1040
|
+
usage = getattr(response, "usage", None)
|
|
1041
|
+
prompt_tokens = getattr(usage, "prompt_tokens", None) if usage else None
|
|
1042
|
+
usage_context = usage_context_from_client_response(
|
|
1043
|
+
client=client,
|
|
1044
|
+
response=response,
|
|
1045
|
+
operation=operation,
|
|
1046
|
+
)
|
|
1047
|
+
prompt_token_source = str(
|
|
1048
|
+
usage_context.get("api_usage_source_detail") or UsageSource.PROVIDER_RESPONSE.value
|
|
1049
|
+
)
|
|
1050
|
+
prompt_token_confidence = str(
|
|
1051
|
+
usage_context.get("api_usage_confidence") or UsageConfidence.REPORTED.value
|
|
1052
|
+
)
|
|
1053
|
+
if prompt_tokens is None:
|
|
1054
|
+
try:
|
|
1055
|
+
counted_input = count_input_tokens_if_supported(
|
|
1056
|
+
client=client,
|
|
1057
|
+
messages=list(messages or []),
|
|
1058
|
+
tools=tool_list,
|
|
1059
|
+
)
|
|
1060
|
+
except Exception as exc: # noqa: BLE001 -- fallback must not break a turn
|
|
1061
|
+
self.store.append(
|
|
1062
|
+
"warning",
|
|
1063
|
+
{
|
|
1064
|
+
"warning": "provider_input_token_count_failed",
|
|
1065
|
+
"operation": operation,
|
|
1066
|
+
"error": str(exc),
|
|
1067
|
+
},
|
|
1068
|
+
)
|
|
1069
|
+
else:
|
|
1070
|
+
if counted_input is not None:
|
|
1071
|
+
prompt_tokens = counted_input.input_tokens
|
|
1072
|
+
usage_context["api_usage_source_detail"] = counted_input.source.value
|
|
1073
|
+
usage_context["api_usage_confidence"] = counted_input.confidence.value
|
|
1074
|
+
usage_context["api_prompt_tokens_authoritative"] = (
|
|
1075
|
+
counted_input.confidence.value == "authoritative"
|
|
1076
|
+
)
|
|
1077
|
+
prompt_token_source = counted_input.source.value
|
|
1078
|
+
prompt_token_confidence = counted_input.confidence.value
|
|
1079
|
+
response_tool_calls = [
|
|
1080
|
+
{
|
|
1081
|
+
"id": getattr(tool_call, "id", ""),
|
|
1082
|
+
"name": getattr(tool_call, "name", ""),
|
|
1083
|
+
"arguments": getattr(tool_call, "arguments", {}),
|
|
1084
|
+
}
|
|
1085
|
+
for tool_call in (getattr(response, "tool_calls", None) or [])
|
|
1086
|
+
]
|
|
1087
|
+
usage_record = build_usage_record(
|
|
1088
|
+
role=role_override or self.usage_role,
|
|
1089
|
+
requested_model=getattr(client, "model", None) or self.client.model,
|
|
1090
|
+
response_model=getattr(response, "response_model", None),
|
|
1091
|
+
messages=list(messages or []),
|
|
1092
|
+
response_content=str(getattr(response, "content", "") or ""),
|
|
1093
|
+
response_tool_calls=response_tool_calls,
|
|
1094
|
+
api_prompt_tokens=prompt_tokens,
|
|
1095
|
+
api_completion_tokens=(
|
|
1096
|
+
getattr(usage, "completion_tokens", None) if usage else None
|
|
1097
|
+
),
|
|
1098
|
+
api_total_tokens=(getattr(usage, "total_tokens", None) if usage else None),
|
|
1099
|
+
api_usage=usage,
|
|
1100
|
+
api_cached_prompt_tokens=(
|
|
1101
|
+
getattr(usage, "cached_prompt_tokens", None) if usage else None
|
|
1102
|
+
),
|
|
1103
|
+
tool_list=tool_list,
|
|
1104
|
+
pinned_prefix_len=self.pinned_prefix_len,
|
|
1105
|
+
registry=self.model_registry,
|
|
1106
|
+
**usage_context,
|
|
1107
|
+
)
|
|
1108
|
+
self.usage_summary.add_record(usage_record)
|
|
1109
|
+
if operation in {"main_llm", "context_overflow_retry"}:
|
|
1110
|
+
if prompt_tokens is None or usage_record.prompt_tokens != prompt_tokens:
|
|
1111
|
+
prompt_token_source = UsageSource.LOCAL_ESTIMATE.value
|
|
1112
|
+
prompt_token_confidence = UsageConfidence.ESTIMATED.value
|
|
1113
|
+
measurement_tools = tool_list
|
|
1114
|
+
request_plan = usage_record.request_plan or {}
|
|
1115
|
+
try:
|
|
1116
|
+
planned_tool_count = int(request_plan.get("tool_count"))
|
|
1117
|
+
except (TypeError, ValueError):
|
|
1118
|
+
planned_tool_count = -1
|
|
1119
|
+
if planned_tool_count == 0:
|
|
1120
|
+
measurement_tools = None
|
|
1121
|
+
self.request_context_measurement = RequestContextMeasurement(
|
|
1122
|
+
input_tokens=max(0, usage_record.prompt_tokens),
|
|
1123
|
+
anchor_estimate_tokens=estimate_request_tokens(
|
|
1124
|
+
list(messages or []),
|
|
1125
|
+
measurement_tools,
|
|
1126
|
+
),
|
|
1127
|
+
persistent_anchor_estimate_tokens=estimate_request_tokens(
|
|
1128
|
+
self.messages,
|
|
1129
|
+
measurement_tools,
|
|
1130
|
+
),
|
|
1131
|
+
source=prompt_token_source,
|
|
1132
|
+
confidence=prompt_token_confidence,
|
|
1133
|
+
requested_model=usage_record.requested_model,
|
|
1134
|
+
provider_key=usage_record.provider_key,
|
|
1135
|
+
protocol=usage_record.protocol,
|
|
1136
|
+
base_url_host=usage_record.base_url_host,
|
|
1137
|
+
operation=usage_record.operation,
|
|
1138
|
+
request_mode=usage_record.request_mode,
|
|
1139
|
+
cache_strategy=usage_record.cache_strategy,
|
|
1140
|
+
request_message_signatures=request_message_signatures(list(messages or [])),
|
|
1141
|
+
persistent_message_signatures=request_message_signatures(self.messages),
|
|
1142
|
+
tool_schema_signature=tool_schema_signature(measurement_tools),
|
|
1143
|
+
request_has_media=request_contains_media(list(messages or [])),
|
|
1144
|
+
persistent_has_media=request_contains_media(self.messages),
|
|
1145
|
+
)
|
|
1146
|
+
self.store.append("llm_usage", usage_record.to_payload())
|
|
1147
|
+
if self.agentbox_telemetry is not None:
|
|
1148
|
+
self.agentbox_telemetry.record_usage(usage_record)
|
|
1149
|
+
return usage_record
|
|
1150
|
+
except Exception as exc: # noqa: BLE001 -- accounting cannot break the agent turn
|
|
1151
|
+
self.store.append(
|
|
1152
|
+
"warning",
|
|
1153
|
+
{
|
|
1154
|
+
"warning": "llm_usage_record_failed",
|
|
1155
|
+
"operation": operation,
|
|
1156
|
+
"error": str(exc),
|
|
1157
|
+
},
|
|
1158
|
+
)
|
|
1159
|
+
return None
|
|
1160
|
+
|
|
1161
|
+
def _send_cache_keepalive(
|
|
1162
|
+
self,
|
|
1163
|
+
request: CacheKeepaliveRequest,
|
|
1164
|
+
stop_event: Any,
|
|
1165
|
+
) -> bool:
|
|
1166
|
+
"""Replay the exact parent prefix and discard the bounded response."""
|
|
1167
|
+
tools = effective_tools_for_client(self.client, request.tools)
|
|
1168
|
+
tool_choice = (
|
|
1169
|
+
copy.deepcopy(request.tool_choice)
|
|
1170
|
+
if tools is not None and request.tool_choice is not None
|
|
1171
|
+
else None
|
|
1172
|
+
)
|
|
1173
|
+
telemetry_count_before = provider_telemetry_record_count()
|
|
1174
|
+
try:
|
|
1175
|
+
with provider_telemetry_operation("cache_keepalive"):
|
|
1176
|
+
response = _main_agent_chat(
|
|
1177
|
+
client=self.client,
|
|
1178
|
+
messages=copy.deepcopy(request.messages),
|
|
1179
|
+
tools=copy.deepcopy(tools),
|
|
1180
|
+
stream=False,
|
|
1181
|
+
on_text_delta=None,
|
|
1182
|
+
temperature=0.0,
|
|
1183
|
+
max_tokens=16,
|
|
1184
|
+
cancellation_token=EventCancellationToken(stop_event),
|
|
1185
|
+
tool_choice=tool_choice,
|
|
1186
|
+
)
|
|
1187
|
+
except Exception as exc: # noqa: BLE001 - affinity is best-effort only
|
|
1188
|
+
if provider_telemetry_record_count() == telemetry_count_before:
|
|
1189
|
+
ProviderCallTelemetryRecorder(
|
|
1190
|
+
provider_key=getattr(self.client, "provider_key", None),
|
|
1191
|
+
protocol=str(getattr(self.client, "protocol", "unknown") or "unknown"),
|
|
1192
|
+
model=str(getattr(self.client, "model", "") or "unknown"),
|
|
1193
|
+
base_url=str(getattr(self.client, "base_url", "") or ""),
|
|
1194
|
+
stream=False,
|
|
1195
|
+
tools=tools,
|
|
1196
|
+
operation="cache_keepalive",
|
|
1197
|
+
).record_error(exc)
|
|
1198
|
+
self.store.append(
|
|
1199
|
+
"cache_keepalive",
|
|
1200
|
+
{
|
|
1201
|
+
"status": "failed",
|
|
1202
|
+
"error_class": type(exc).__name__,
|
|
1203
|
+
"error": sanitize_error_text_for_output(exc),
|
|
1204
|
+
"request": {
|
|
1205
|
+
"message_count": len(request.messages),
|
|
1206
|
+
"tool_count": len(tools or []),
|
|
1207
|
+
"stream": False,
|
|
1208
|
+
"temperature": 0.0,
|
|
1209
|
+
"max_tokens": 16,
|
|
1210
|
+
"tool_choice_present": tool_choice is not None,
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
)
|
|
1214
|
+
return False
|
|
1215
|
+
usage_record = self._record_llm_usage(
|
|
1216
|
+
client=self.client,
|
|
1217
|
+
response=response,
|
|
1218
|
+
messages=request.messages,
|
|
1219
|
+
tool_list=tools,
|
|
1220
|
+
operation="cache_keepalive",
|
|
1221
|
+
role_override=f"{self.usage_role}:cache_keepalive",
|
|
1222
|
+
)
|
|
1223
|
+
self.store.append(
|
|
1224
|
+
"cache_keepalive",
|
|
1225
|
+
{
|
|
1226
|
+
"status": "completed",
|
|
1227
|
+
"request": {
|
|
1228
|
+
"message_count": len(request.messages),
|
|
1229
|
+
"tool_count": len(tools or []),
|
|
1230
|
+
"stream": False,
|
|
1231
|
+
"temperature": 0.0,
|
|
1232
|
+
"max_tokens": 16,
|
|
1233
|
+
"tool_choice_present": tool_choice is not None,
|
|
1234
|
+
},
|
|
1235
|
+
"usage": usage_record.to_payload() if usage_record is not None else None,
|
|
1236
|
+
},
|
|
1237
|
+
)
|
|
1238
|
+
return True
|
|
1239
|
+
|
|
1240
|
+
def _on_cache_keepalive_disabled(self, consecutive_failures: int) -> None:
|
|
1241
|
+
self.store.append(
|
|
1242
|
+
"warning",
|
|
1243
|
+
{
|
|
1244
|
+
"warning": "cache_keepalive_disabled_after_failures",
|
|
1245
|
+
"consecutive_failures": consecutive_failures,
|
|
1246
|
+
"message": (
|
|
1247
|
+
"Cache keepalive disabled for this session after repeated failures; "
|
|
1248
|
+
"child execution continues unchanged."
|
|
1249
|
+
),
|
|
1250
|
+
},
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
def _on_cache_keepalive_unsupported(self, reason: str) -> None:
|
|
1254
|
+
self.store.append(
|
|
1255
|
+
"warning",
|
|
1256
|
+
{
|
|
1257
|
+
"warning": "keepalive_unsupported_transport",
|
|
1258
|
+
"reason": reason,
|
|
1259
|
+
"message": (
|
|
1260
|
+
"Cache keepalive is disabled because this transport does not replay the "
|
|
1261
|
+
"parent's active prompt-cache stream."
|
|
1262
|
+
),
|
|
1263
|
+
},
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
def _emit_final_assistant_text(
|
|
1267
|
+
self,
|
|
1268
|
+
*,
|
|
1269
|
+
final_text: str,
|
|
1270
|
+
assistant_response: Any | None = None,
|
|
1271
|
+
language: str = "",
|
|
1272
|
+
script: str = "",
|
|
1273
|
+
explicit_language_override: bool = False,
|
|
1274
|
+
prior_visible_text: str = "",
|
|
1275
|
+
streamed_text_emitted: bool = False,
|
|
1276
|
+
final_event_payload: dict[str, Any] | None = None,
|
|
1277
|
+
internal_fallback: bool = False,
|
|
1278
|
+
internal_fallback_kind: str = "",
|
|
1279
|
+
) -> str:
|
|
1280
|
+
emitted_text = str(final_text or "").strip()
|
|
1281
|
+
emitted_text, rewrite_payload = _rewrite_final_summary_for_language(
|
|
1282
|
+
client=self.client,
|
|
1283
|
+
final_text=emitted_text,
|
|
1284
|
+
language=language,
|
|
1285
|
+
script=script,
|
|
1286
|
+
explicit_language_override=explicit_language_override,
|
|
1287
|
+
record_usage=lambda **kw: self._record_llm_usage(client=self.client, **kw),
|
|
1288
|
+
)
|
|
1289
|
+
if rewrite_payload is not None:
|
|
1290
|
+
self.store.append("final_summary_rewrite", rewrite_payload)
|
|
1291
|
+
assistant_message = None
|
|
1292
|
+
if assistant_response is not None:
|
|
1293
|
+
candidate_message = assistant_message_from_response(
|
|
1294
|
+
assistant_response,
|
|
1295
|
+
content=emitted_text,
|
|
1296
|
+
)
|
|
1297
|
+
if PROVIDER_METADATA_KEY in candidate_message:
|
|
1298
|
+
assistant_message = candidate_message
|
|
1299
|
+
extra_payload = {"message": assistant_message} if assistant_message is not None else None
|
|
1300
|
+
if internal_fallback and self.subagent_depth > 0:
|
|
1301
|
+
# A nested run's locally generated stop report is internal state. The
|
|
1302
|
+
# nested surface forwards assistant messages up to the parent's panel,
|
|
1303
|
+
# so emitting it here would render the dump to the user even though the
|
|
1304
|
+
# tool result never carries it. Record it; do not show it.
|
|
1305
|
+
self.store.append(
|
|
1306
|
+
"assistant_message",
|
|
1307
|
+
{"content": emitted_text, "internal_fallback": True},
|
|
1308
|
+
)
|
|
1309
|
+
else:
|
|
1310
|
+
self._emit_assistant_message_if_changed(
|
|
1311
|
+
text=emitted_text,
|
|
1312
|
+
prior_visible_text=prior_visible_text,
|
|
1313
|
+
extra_payload=extra_payload,
|
|
1314
|
+
streamed_text_emitted=streamed_text_emitted,
|
|
1315
|
+
)
|
|
1316
|
+
if assistant_message is not None:
|
|
1317
|
+
if internal_fallback:
|
|
1318
|
+
# Today a fallback always arrives with assistant_response=None, so
|
|
1319
|
+
# nothing is appended and this does not fire. It is here because the
|
|
1320
|
+
# invariant is "an internal artifact that enters the transcript is
|
|
1321
|
+
# marked", and that has to hold at the point of entry, not by anyone
|
|
1322
|
+
# remembering to re-check later.
|
|
1323
|
+
mark_message_internal(
|
|
1324
|
+
assistant_message,
|
|
1325
|
+
kind=internal_fallback_kind or "forced_final_summary_fallback",
|
|
1326
|
+
)
|
|
1327
|
+
self.messages.append(assistant_message)
|
|
1328
|
+
final_payload: dict[str, Any] = {"content": emitted_text}
|
|
1329
|
+
if internal_fallback:
|
|
1330
|
+
# A locally generated stop report, not a model answer. Recorded as a
|
|
1331
|
+
# fact here so the subagent boundary can refuse to hand it to a parent
|
|
1332
|
+
# as a deliverable, without inspecting the text.
|
|
1333
|
+
final_payload["internal_fallback"] = True
|
|
1334
|
+
final_payload["artifact_visibility"] = ArtifactVisibility.INTERNAL.value
|
|
1335
|
+
if internal_fallback_kind:
|
|
1336
|
+
final_payload["internal_fallback_kind"] = internal_fallback_kind
|
|
1337
|
+
self.store.append(
|
|
1338
|
+
"final",
|
|
1339
|
+
_add_event_diagnostics(final_payload, final_event_payload),
|
|
1340
|
+
)
|
|
1341
|
+
return emitted_text
|
|
1342
|
+
|
|
1343
|
+
def _forced_final_summary_activity_snapshot(self) -> dict[str, Any]:
|
|
1344
|
+
tool_calls_by_id: dict[str, tuple[str, dict[str, Any]]] = {}
|
|
1345
|
+
read_paths: list[str] = []
|
|
1346
|
+
listed_paths: list[str] = []
|
|
1347
|
+
edited_paths: list[str] = []
|
|
1348
|
+
verification_commands: list[str] = []
|
|
1349
|
+
shell_commands: list[str] = []
|
|
1350
|
+
other_actions: list[str] = []
|
|
1351
|
+
failed_actions: list[str] = []
|
|
1352
|
+
|
|
1353
|
+
def _append_unique(items: list[str], value: str) -> None:
|
|
1354
|
+
clean = str(value or "").strip()
|
|
1355
|
+
if clean and clean not in items:
|
|
1356
|
+
items.append(clean)
|
|
1357
|
+
|
|
1358
|
+
def _path_arg(args: dict[str, Any]) -> str:
|
|
1359
|
+
for key in ("path", "file", "target", "target_path"):
|
|
1360
|
+
value = args.get(key)
|
|
1361
|
+
if isinstance(value, str) and value.strip():
|
|
1362
|
+
return value.strip()
|
|
1363
|
+
return ""
|
|
1364
|
+
|
|
1365
|
+
def _command_arg(args: dict[str, Any]) -> str:
|
|
1366
|
+
for key in ("command", "cmd"):
|
|
1367
|
+
value = args.get(key)
|
|
1368
|
+
if isinstance(value, str) and value.strip():
|
|
1369
|
+
return value.strip()
|
|
1370
|
+
return ""
|
|
1371
|
+
|
|
1372
|
+
for message in self.messages:
|
|
1373
|
+
role = str(message.get("role") or "")
|
|
1374
|
+
if role == "assistant":
|
|
1375
|
+
for raw_call in message.get("tool_calls") or []:
|
|
1376
|
+
if not isinstance(raw_call, dict):
|
|
1377
|
+
continue
|
|
1378
|
+
call_id = str(raw_call.get("id") or "").strip()
|
|
1379
|
+
function = raw_call.get("function")
|
|
1380
|
+
if not call_id or not isinstance(function, dict):
|
|
1381
|
+
continue
|
|
1382
|
+
name = str(function.get("name") or "").strip()
|
|
1383
|
+
raw_args = function.get("arguments")
|
|
1384
|
+
args: dict[str, Any] = {}
|
|
1385
|
+
if isinstance(raw_args, str) and raw_args.strip():
|
|
1386
|
+
try:
|
|
1387
|
+
parsed_args = json.loads(raw_args)
|
|
1388
|
+
except Exception: # noqa: BLE001
|
|
1389
|
+
parsed_args = None
|
|
1390
|
+
if isinstance(parsed_args, dict):
|
|
1391
|
+
args = parsed_args
|
|
1392
|
+
if name:
|
|
1393
|
+
tool_calls_by_id[call_id] = (name, args)
|
|
1394
|
+
continue
|
|
1395
|
+
if role != "tool":
|
|
1396
|
+
continue
|
|
1397
|
+
call_id = str(message.get("tool_call_id") or "").strip()
|
|
1398
|
+
name, args = tool_calls_by_id.get(call_id, ("", {}))
|
|
1399
|
+
if not name:
|
|
1400
|
+
continue
|
|
1401
|
+
content = message.get("content")
|
|
1402
|
+
result: Any = None
|
|
1403
|
+
if isinstance(content, str) and content.strip():
|
|
1404
|
+
try:
|
|
1405
|
+
result = json.loads(content)
|
|
1406
|
+
except Exception: # noqa: BLE001
|
|
1407
|
+
result = None
|
|
1408
|
+
failed = isinstance(result, dict) and "error" in result
|
|
1409
|
+
path = _path_arg(args)
|
|
1410
|
+
command = _command_arg(args)
|
|
1411
|
+
if failed:
|
|
1412
|
+
label = f"{name} {path}".strip() if path else name
|
|
1413
|
+
_append_unique(failed_actions, label)
|
|
1414
|
+
continue
|
|
1415
|
+
if name == "fs_read" and path:
|
|
1416
|
+
_append_unique(read_paths, path)
|
|
1417
|
+
elif name == "fs_list" and path:
|
|
1418
|
+
_append_unique(listed_paths, path)
|
|
1419
|
+
elif name in {"fs_write", "fs_edit", "apply_patch"} and path:
|
|
1420
|
+
_append_unique(edited_paths, path)
|
|
1421
|
+
elif name in {"shell", "shell_command", "shell_run", "verify_run"} and command:
|
|
1422
|
+
if name == "verify_run":
|
|
1423
|
+
_append_unique(verification_commands, command)
|
|
1424
|
+
else:
|
|
1425
|
+
_append_unique(shell_commands, command)
|
|
1426
|
+
else:
|
|
1427
|
+
label = f"{name} {path}".strip() if path else name
|
|
1428
|
+
_append_unique(other_actions, label)
|
|
1429
|
+
|
|
1430
|
+
return {
|
|
1431
|
+
"read_paths": read_paths,
|
|
1432
|
+
"listed_paths": listed_paths,
|
|
1433
|
+
"edited_paths": edited_paths,
|
|
1434
|
+
"verification_commands": verification_commands,
|
|
1435
|
+
"shell_commands": shell_commands,
|
|
1436
|
+
"other_actions": other_actions,
|
|
1437
|
+
"failed_actions": failed_actions,
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
def _forced_final_summary_fallback_text(
|
|
1441
|
+
self,
|
|
1442
|
+
*,
|
|
1443
|
+
termination_cause: str,
|
|
1444
|
+
termination_kind: str = "step_budget_exhausted",
|
|
1445
|
+
max_steps: int | None,
|
|
1446
|
+
fallback_reason: str,
|
|
1447
|
+
latest_assistant_text: str = "",
|
|
1448
|
+
) -> str:
|
|
1449
|
+
snapshot = self._forced_final_summary_activity_snapshot()
|
|
1450
|
+
|
|
1451
|
+
def _join_limited(items: list[str], *, limit: int = 8) -> str:
|
|
1452
|
+
visible = items[:limit]
|
|
1453
|
+
text = ", ".join(visible)
|
|
1454
|
+
remaining = len(items) - len(visible)
|
|
1455
|
+
if remaining > 0:
|
|
1456
|
+
text += f", and {remaining} more"
|
|
1457
|
+
return text
|
|
1458
|
+
|
|
1459
|
+
completed: list[str] = []
|
|
1460
|
+
read_paths = snapshot["read_paths"]
|
|
1461
|
+
listed_paths = snapshot["listed_paths"]
|
|
1462
|
+
edited_paths = snapshot["edited_paths"]
|
|
1463
|
+
verification_commands = snapshot["verification_commands"]
|
|
1464
|
+
shell_commands = snapshot["shell_commands"]
|
|
1465
|
+
other_actions = snapshot["other_actions"]
|
|
1466
|
+
failed_actions = snapshot["failed_actions"]
|
|
1467
|
+
|
|
1468
|
+
if read_paths:
|
|
1469
|
+
completed.append(f"- Read files: {_join_limited(read_paths)}.")
|
|
1470
|
+
if listed_paths:
|
|
1471
|
+
completed.append(f"- Listed directories: {_join_limited(listed_paths)}.")
|
|
1472
|
+
if edited_paths:
|
|
1473
|
+
completed.append(f"- Edited files: {_join_limited(edited_paths)}.")
|
|
1474
|
+
if verification_commands:
|
|
1475
|
+
completed.append(
|
|
1476
|
+
f"- Ran verification: {_join_limited(verification_commands, limit=4)}."
|
|
1477
|
+
)
|
|
1478
|
+
if shell_commands:
|
|
1479
|
+
completed.append(f"- Ran shell commands: {_join_limited(shell_commands, limit=4)}.")
|
|
1480
|
+
if other_actions:
|
|
1481
|
+
completed.append(f"- Ran tools: {_join_limited(other_actions)}.")
|
|
1482
|
+
latest = str(latest_assistant_text or "").strip()
|
|
1483
|
+
if latest:
|
|
1484
|
+
latest = " ".join(latest.split())
|
|
1485
|
+
if len(latest) > 180:
|
|
1486
|
+
latest = latest[:177].rstrip() + "..."
|
|
1487
|
+
completed.append(f"- Last assistant progress note: {latest}")
|
|
1488
|
+
if not completed:
|
|
1489
|
+
completed.append(
|
|
1490
|
+
"- No durable repository change was completed before the turn stopped."
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
remaining = [
|
|
1494
|
+
"- Continue from the recorded tool results instead of restarting from scratch.",
|
|
1495
|
+
]
|
|
1496
|
+
writes_allowed = self.workspace_write_contract_allows_writes()
|
|
1497
|
+
if not edited_paths and writes_allowed:
|
|
1498
|
+
remaining.append(
|
|
1499
|
+
"- Implementation has not started yet; identify the smallest safe fix first."
|
|
1500
|
+
)
|
|
1501
|
+
elif not edited_paths:
|
|
1502
|
+
remaining.append("- Deliver the requested analysis or report.")
|
|
1503
|
+
if edited_paths and not verification_commands:
|
|
1504
|
+
remaining.append("- Run focused verification for the edited files before finalizing.")
|
|
1505
|
+
if failed_actions:
|
|
1506
|
+
remaining.append(
|
|
1507
|
+
f"- Resolve failed tool calls: {_join_limited(failed_actions, limit=5)}."
|
|
1508
|
+
)
|
|
1509
|
+
if writes_allowed:
|
|
1510
|
+
remaining.append("- Finish the requested implementation or report a concrete blocker.")
|
|
1511
|
+
else:
|
|
1512
|
+
remaining.append(
|
|
1513
|
+
"- Report a concrete blocker if the requested result cannot be delivered."
|
|
1514
|
+
)
|
|
1515
|
+
|
|
1516
|
+
kind = _normalize_forced_summary_termination_kind(termination_kind)
|
|
1517
|
+
if kind == ForcedFinalSummaryTerminationKind.STEP_BUDGET_EXHAUSTED:
|
|
1518
|
+
if max_steps is None:
|
|
1519
|
+
stop_risk = "- The turn stopped before completion."
|
|
1520
|
+
else:
|
|
1521
|
+
stop_risk = f"- The turn exhausted its {max_steps}-step limit before completion."
|
|
1522
|
+
elif kind == ForcedFinalSummaryTerminationKind.COMPLETION_GATE_STAGNATION:
|
|
1523
|
+
stop_risk = (
|
|
1524
|
+
"- Execution stopped after repeated invalid finalization attempts without "
|
|
1525
|
+
"new implementation or verification progress."
|
|
1526
|
+
)
|
|
1527
|
+
elif kind == ForcedFinalSummaryTerminationKind.EXECUTION_GUARD_STAGNATION:
|
|
1528
|
+
stop_risk = (
|
|
1529
|
+
"- Execution stopped after a runtime guard observed repeated no-progress behavior."
|
|
1530
|
+
)
|
|
1531
|
+
elif kind == ForcedFinalSummaryTerminationKind.DEADLINE_EXHAUSTED:
|
|
1532
|
+
stop_risk = "- The run deadline was exhausted before the turn could finish."
|
|
1533
|
+
else:
|
|
1534
|
+
stop_risk = "- The turn stopped before completion for the reported reason."
|
|
1535
|
+
risks = [
|
|
1536
|
+
stop_risk,
|
|
1537
|
+
"- This fallback was generated from runtime state before the turn terminated.",
|
|
1538
|
+
]
|
|
1539
|
+
if not verification_commands:
|
|
1540
|
+
risks.append("- No verification result was recorded in this turn.")
|
|
1541
|
+
|
|
1542
|
+
return (
|
|
1543
|
+
f"The turn stopped before it could finish ({termination_cause}).\n\n"
|
|
1544
|
+
"Completed work:\n" + "\n".join(completed) + "\n\n"
|
|
1545
|
+
"Remaining work:\n" + "\n".join(remaining) + "\n\n"
|
|
1546
|
+
"Known issues or risks:\n" + "\n".join(risks)
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
def _emit_forced_final_summary_before_termination(
|
|
1550
|
+
self,
|
|
1551
|
+
*,
|
|
1552
|
+
reason: str,
|
|
1553
|
+
termination_cause: str,
|
|
1554
|
+
termination_kind: str = "step_budget_exhausted",
|
|
1555
|
+
max_steps: int | None,
|
|
1556
|
+
language: str = "",
|
|
1557
|
+
script: str = "",
|
|
1558
|
+
explicit_language_override: bool = False,
|
|
1559
|
+
latest_assistant_text: str = "",
|
|
1560
|
+
allow_llm_summary: bool = True,
|
|
1561
|
+
local_summary_override: str = "",
|
|
1562
|
+
final_event_payload: dict[str, Any] | None = None,
|
|
1563
|
+
) -> str:
|
|
1564
|
+
normalized_termination_kind = _normalize_forced_summary_termination_kind(
|
|
1565
|
+
termination_kind
|
|
1566
|
+
).value
|
|
1567
|
+
# Internal artifacts (a previous turn's locally generated stop report) are
|
|
1568
|
+
# excluded here by marker, so a summary can never be built by re-narrating
|
|
1569
|
+
# one. Marker-based, never a scan of the text.
|
|
1570
|
+
request_messages = summary_input_messages(self.messages)
|
|
1571
|
+
latest_assistant_text = str(latest_assistant_text or "").strip()
|
|
1572
|
+
if latest_assistant_text:
|
|
1573
|
+
request_messages.append({"role": "assistant", "content": latest_assistant_text})
|
|
1574
|
+
request_messages.append(
|
|
1575
|
+
{
|
|
1576
|
+
"role": "system",
|
|
1577
|
+
"content": _FORCED_FINAL_SUMMARY_SYSTEM_PROMPT_TEMPLATE.format(
|
|
1578
|
+
termination_cause=termination_cause
|
|
1579
|
+
),
|
|
1580
|
+
}
|
|
1581
|
+
)
|
|
1582
|
+
self.store.append(
|
|
1583
|
+
"forced_final_summary_requested",
|
|
1584
|
+
{
|
|
1585
|
+
"reason": reason,
|
|
1586
|
+
"termination_cause": termination_cause,
|
|
1587
|
+
"termination_kind": normalized_termination_kind,
|
|
1588
|
+
"max_steps": max_steps,
|
|
1589
|
+
},
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
final_text = ""
|
|
1593
|
+
fallback_reason: str | None = None
|
|
1594
|
+
fallback_error: str | None = None
|
|
1595
|
+
resp = None
|
|
1596
|
+
deadline = self.execution_deadline
|
|
1597
|
+
if not allow_llm_summary or (
|
|
1598
|
+
deadline is not None and not deadline.can_start(MINIMUM_FORCED_SUMMARY_SECONDS)
|
|
1599
|
+
):
|
|
1600
|
+
fallback_reason = "local_summary_due_to_deadline"
|
|
1601
|
+
else:
|
|
1602
|
+
try:
|
|
1603
|
+
with temporarily_clamp_client_timeout(
|
|
1604
|
+
self.client,
|
|
1605
|
+
deadline,
|
|
1606
|
+
operation="forced_final_summary_llm",
|
|
1607
|
+
):
|
|
1608
|
+
resp = _main_agent_chat(
|
|
1609
|
+
client=self.client,
|
|
1610
|
+
messages=request_messages,
|
|
1611
|
+
tools=None,
|
|
1612
|
+
stream=False,
|
|
1613
|
+
on_text_delta=None,
|
|
1614
|
+
)
|
|
1615
|
+
except DeadlineExhausted:
|
|
1616
|
+
fallback_reason = "local_summary_due_to_deadline"
|
|
1617
|
+
except Exception as exc: # noqa: BLE001
|
|
1618
|
+
fallback_reason = "finalization_error"
|
|
1619
|
+
fallback_error = str(exc)
|
|
1620
|
+
else:
|
|
1621
|
+
self._record_llm_usage(
|
|
1622
|
+
client=self.client,
|
|
1623
|
+
response=resp,
|
|
1624
|
+
messages=request_messages,
|
|
1625
|
+
tool_list=None,
|
|
1626
|
+
operation="forced_final_summary_llm",
|
|
1627
|
+
)
|
|
1628
|
+
final_text = str(resp.content or "").strip()
|
|
1629
|
+
if resp.tool_calls:
|
|
1630
|
+
fallback_reason = "tool_call_response"
|
|
1631
|
+
elif not final_text:
|
|
1632
|
+
fallback_reason = "blank_response"
|
|
1633
|
+
elif _looks_like_unexecuted_tool_call_markup(final_text):
|
|
1634
|
+
fallback_reason = "tool_call_markup_response"
|
|
1635
|
+
|
|
1636
|
+
if fallback_reason is not None:
|
|
1637
|
+
# A caller that already built a factual account of what it salvaged
|
|
1638
|
+
# supplies it here, so the local summary reports real state instead
|
|
1639
|
+
# of the generic termination notice.
|
|
1640
|
+
final_text = str(local_summary_override or "").strip()
|
|
1641
|
+
if not final_text:
|
|
1642
|
+
final_text = self._forced_final_summary_fallback_text(
|
|
1643
|
+
termination_cause=termination_cause,
|
|
1644
|
+
termination_kind=normalized_termination_kind,
|
|
1645
|
+
max_steps=max_steps,
|
|
1646
|
+
fallback_reason=fallback_reason,
|
|
1647
|
+
latest_assistant_text=latest_assistant_text,
|
|
1648
|
+
)
|
|
1649
|
+
fallback_payload: dict[str, Any] = {
|
|
1650
|
+
"reason": reason,
|
|
1651
|
+
"termination_cause": termination_cause,
|
|
1652
|
+
"termination_kind": normalized_termination_kind,
|
|
1653
|
+
"max_steps": max_steps,
|
|
1654
|
+
"fallback_reason": fallback_reason,
|
|
1655
|
+
}
|
|
1656
|
+
if fallback_error:
|
|
1657
|
+
fallback_payload["error"] = fallback_error
|
|
1658
|
+
_add_event_diagnostics(fallback_payload, final_event_payload)
|
|
1659
|
+
self.store.append("forced_final_summary_fallback", fallback_payload)
|
|
1660
|
+
|
|
1661
|
+
emitted_text = self._emit_final_assistant_text(
|
|
1662
|
+
final_text=final_text,
|
|
1663
|
+
assistant_response=resp if fallback_reason is None else None,
|
|
1664
|
+
internal_fallback=fallback_reason is not None,
|
|
1665
|
+
internal_fallback_kind=normalized_termination_kind,
|
|
1666
|
+
language=language,
|
|
1667
|
+
script=script,
|
|
1668
|
+
explicit_language_override=explicit_language_override,
|
|
1669
|
+
# Thread the turn's last-shown answer so the change-dedup can suppress a
|
|
1670
|
+
# forced summary that merely repeats it (otherwise prior_visible_text
|
|
1671
|
+
# defaults to "" and on_assistant_message_done always re-fires).
|
|
1672
|
+
prior_visible_text=latest_assistant_text,
|
|
1673
|
+
final_event_payload=final_event_payload,
|
|
1674
|
+
)
|
|
1675
|
+
if fallback_reason is None:
|
|
1676
|
+
completed_payload = {
|
|
1677
|
+
"reason": reason,
|
|
1678
|
+
"termination_cause": termination_cause,
|
|
1679
|
+
"termination_kind": normalized_termination_kind,
|
|
1680
|
+
"max_steps": max_steps,
|
|
1681
|
+
"content_length": len(emitted_text),
|
|
1682
|
+
}
|
|
1683
|
+
_add_event_diagnostics(completed_payload, final_event_payload)
|
|
1684
|
+
self.store.append("forced_final_summary_completed", completed_payload)
|
|
1685
|
+
return emitted_text
|
|
1686
|
+
|
|
1687
|
+
def run_turn(
|
|
1688
|
+
self,
|
|
1689
|
+
instruction: str,
|
|
1690
|
+
*,
|
|
1691
|
+
image_paths: list[str] | None = None,
|
|
1692
|
+
routing_mode_override: str | None = None,
|
|
1693
|
+
ephemeral_system_messages: list[str] | tuple[str, ...] | None = None,
|
|
1694
|
+
ephemeral_user_messages: list[str] | tuple[str, ...] | None = None,
|
|
1695
|
+
cancellation_token: Any | None = None,
|
|
1696
|
+
chat_only: bool = False,
|
|
1697
|
+
) -> int:
|
|
1698
|
+
turn_thread_id = threading.get_ident()
|
|
1699
|
+
self._turn_owner_thread_id = turn_thread_id
|
|
1700
|
+
self._bind_provider_retry_observer(self.client)
|
|
1701
|
+
try:
|
|
1702
|
+
if self.agentbox_telemetry is None:
|
|
1703
|
+
return _run_turn(
|
|
1704
|
+
self,
|
|
1705
|
+
instruction,
|
|
1706
|
+
image_paths=image_paths,
|
|
1707
|
+
routing_mode_override=routing_mode_override,
|
|
1708
|
+
ephemeral_system_messages=ephemeral_system_messages,
|
|
1709
|
+
ephemeral_user_messages=ephemeral_user_messages,
|
|
1710
|
+
cancellation_token=cancellation_token,
|
|
1711
|
+
chat_only=chat_only,
|
|
1712
|
+
)
|
|
1713
|
+
self.agentbox_telemetry.task(instruction)
|
|
1714
|
+
with self.agentbox_telemetry.turn():
|
|
1715
|
+
return _run_turn(
|
|
1716
|
+
self,
|
|
1717
|
+
instruction,
|
|
1718
|
+
image_paths=image_paths,
|
|
1719
|
+
routing_mode_override=routing_mode_override,
|
|
1720
|
+
ephemeral_system_messages=ephemeral_system_messages,
|
|
1721
|
+
ephemeral_user_messages=ephemeral_user_messages,
|
|
1722
|
+
cancellation_token=cancellation_token,
|
|
1723
|
+
chat_only=chat_only,
|
|
1724
|
+
)
|
|
1725
|
+
except CooperativeCancellationError as exc:
|
|
1726
|
+
# The budget watchdog cancels through the same cooperative channel a
|
|
1727
|
+
# user does, so the two are told apart by reason. A budget stop is a
|
|
1728
|
+
# normal outcome and is finalized cleanly; a user cancellation and
|
|
1729
|
+
# every other cooperative stop fall through to the failure boundary
|
|
1730
|
+
# below, unchanged.
|
|
1731
|
+
if not is_budget_cancellation(exc):
|
|
1732
|
+
self._emit_terminal_error(exc)
|
|
1733
|
+
raise
|
|
1734
|
+
return self._finalize_run_budget_stop()
|
|
1735
|
+
except Exception as exc:
|
|
1736
|
+
# Single authoritative terminal-failure boundary: every caller (one-shot
|
|
1737
|
+
# run, interactive chat, Forge workers, subagents) routes turns through
|
|
1738
|
+
# here, so one durable, redacted record makes a crashed build
|
|
1739
|
+
# reconstructable from artifacts alone. Re-raise unchanged afterwards.
|
|
1740
|
+
self._emit_terminal_error(exc)
|
|
1741
|
+
raise
|
|
1742
|
+
finally:
|
|
1743
|
+
# Same reasoning as the boundary above: this is the only per-turn point
|
|
1744
|
+
# that sees normal returns, exceptions, and cancellation, so turn-level
|
|
1745
|
+
# process hygiene belongs here rather than in the loop's success path.
|
|
1746
|
+
try:
|
|
1747
|
+
if (
|
|
1748
|
+
cancellation_token is not None
|
|
1749
|
+
and bool(getattr(cancellation_token, "is_cancelled", False))
|
|
1750
|
+
and self.child_scheduler is not None
|
|
1751
|
+
):
|
|
1752
|
+
pending_run_ids = self.child_scheduler.pending_run_ids()
|
|
1753
|
+
if pending_run_ids:
|
|
1754
|
+
self.child_scheduler.cancel(
|
|
1755
|
+
run_id=pending_run_ids,
|
|
1756
|
+
wait_for_running=True,
|
|
1757
|
+
# Unbounded while there is budget left, as before.
|
|
1758
|
+
# Once it is gone this join has nothing to wait
|
|
1759
|
+
# with, and a child that ignores its cancellation
|
|
1760
|
+
# would otherwise pin the run open indefinitely --
|
|
1761
|
+
# on a path that only became reachable now that a
|
|
1762
|
+
# budget stop cancels cooperatively.
|
|
1763
|
+
wait_timeout_s=(
|
|
1764
|
+
resolve_budget_grace_seconds()
|
|
1765
|
+
if (
|
|
1766
|
+
self.execution_deadline is not None
|
|
1767
|
+
and self.execution_deadline.is_exhausted()
|
|
1768
|
+
)
|
|
1769
|
+
else None
|
|
1770
|
+
),
|
|
1771
|
+
)
|
|
1772
|
+
self.store.append(
|
|
1773
|
+
"subagent_turn_end_enforcement",
|
|
1774
|
+
{
|
|
1775
|
+
"policy": str(self.cfg.subagent_orchestration.turn_end_policy),
|
|
1776
|
+
"action": "parent_cancel",
|
|
1777
|
+
"run_ids": pending_run_ids,
|
|
1778
|
+
},
|
|
1779
|
+
)
|
|
1780
|
+
finally:
|
|
1781
|
+
try:
|
|
1782
|
+
self._reap_tracked_process_groups(event=ReapEvent.TURN_FINALIZATION)
|
|
1783
|
+
finally:
|
|
1784
|
+
if getattr(self, "_turn_owner_thread_id", None) == turn_thread_id:
|
|
1785
|
+
self._turn_owner_thread_id = None
|
|
1786
|
+
|
|
1787
|
+
def _finalize_run_budget_stop(self) -> int:
|
|
1788
|
+
"""Finalize a watchdog-cancelled run as a clean budget stop.
|
|
1789
|
+
|
|
1790
|
+
This is the backstop, not the common path. The turn engine's
|
|
1791
|
+
``_deadline_exhausted_result`` handles a budget stop the run noticed
|
|
1792
|
+
itself and is richer, because it can still salvage workspace state.
|
|
1793
|
+
This runs only when the run was blocked somewhere no cooperative
|
|
1794
|
+
checkpoint could reach and had to be cancelled from outside: record the
|
|
1795
|
+
same markers, emit the same local (never model-generated) summary, and
|
|
1796
|
+
exit zero, because running out of time is an outcome and not a crash.
|
|
1797
|
+
"""
|
|
1798
|
+
self.stop_reason = STOP_REASON_RUN_BUDGET_EXHAUSTED
|
|
1799
|
+
payload: dict[str, Any] = {
|
|
1800
|
+
"operation": "budget_watchdog",
|
|
1801
|
+
"stop_reason": STOP_REASON_RUN_BUDGET_EXHAUSTED,
|
|
1802
|
+
"deadline_exhausted": True,
|
|
1803
|
+
"deadline": (
|
|
1804
|
+
self.execution_deadline.telemetry_snapshot()
|
|
1805
|
+
if self.execution_deadline is not None
|
|
1806
|
+
else None
|
|
1807
|
+
),
|
|
1808
|
+
}
|
|
1809
|
+
self.store.append("deadline_exhausted", payload)
|
|
1810
|
+
if self.crash_diagnostics is not None:
|
|
1811
|
+
self.crash_diagnostics.event("deadline_exhausted", payload, durable=True)
|
|
1812
|
+
try:
|
|
1813
|
+
self._emit_forced_final_summary_before_termination(
|
|
1814
|
+
reason="deadline_exhausted",
|
|
1815
|
+
termination_cause="the run deadline is exhausted",
|
|
1816
|
+
termination_kind="deadline_exhausted",
|
|
1817
|
+
max_steps=None,
|
|
1818
|
+
# No LLM call: the budget is already gone, and this PR adds no
|
|
1819
|
+
# model-visible traffic on the stop path.
|
|
1820
|
+
allow_llm_summary=False,
|
|
1821
|
+
final_event_payload={
|
|
1822
|
+
"degraded": False,
|
|
1823
|
+
"degraded_reason": STOP_REASON_RUN_BUDGET_EXHAUSTED,
|
|
1824
|
+
"stop_reason": STOP_REASON_RUN_BUDGET_EXHAUSTED,
|
|
1825
|
+
},
|
|
1826
|
+
)
|
|
1827
|
+
except Exception as exc: # noqa: BLE001 - a clean stop must stay clean
|
|
1828
|
+
self.store.append(
|
|
1829
|
+
"warning",
|
|
1830
|
+
{"warning": "budget_stop_summary_failed", "error": str(exc)},
|
|
1831
|
+
)
|
|
1832
|
+
return BUDGET_STOP_EXIT_CODE
|
|
1833
|
+
|
|
1834
|
+
def _emit_terminal_error(
|
|
1835
|
+
self,
|
|
1836
|
+
error: BaseException,
|
|
1837
|
+
*,
|
|
1838
|
+
operation: str = "run_turn",
|
|
1839
|
+
) -> None:
|
|
1840
|
+
"""Record one redacted, joinable terminal-failure record for ``error``.
|
|
1841
|
+
|
|
1842
|
+
Written to the per-run store (default-on; suppressed only by ``--no-log``) and,
|
|
1843
|
+
when the opt-in crash-diagnostic log is enabled, as the durable ``terminal_error``
|
|
1844
|
+
event. Never raises — a diagnostic failure here must not mask the user's error.
|
|
1845
|
+
"""
|
|
1846
|
+
try:
|
|
1847
|
+
fields = build_error_event_fields(error, operation=operation)
|
|
1848
|
+
except Exception: # noqa: BLE001 - diagnostics must never mask the real failure
|
|
1849
|
+
fields = {"error_type": type(error).__name__, "operation": operation}
|
|
1850
|
+
try:
|
|
1851
|
+
self.store.append("terminal_error", dict(fields))
|
|
1852
|
+
except Exception: # noqa: BLE001 - best-effort durable record
|
|
1853
|
+
pass
|
|
1854
|
+
if self.crash_diagnostics is not None:
|
|
1855
|
+
try:
|
|
1856
|
+
self.crash_diagnostics.event("terminal_error", dict(fields), durable=True)
|
|
1857
|
+
except Exception: # noqa: BLE001 - best-effort diagnostic event
|
|
1858
|
+
pass
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
def _shell_command_probe(*, shell_runner: Any, root: Path) -> ShellCommandRunner:
|
|
1862
|
+
"""Adapt the session's shell runner to the provisioning probe interface.
|
|
1863
|
+
|
|
1864
|
+
Everything provisioning does goes through here, so it inherits the session's
|
|
1865
|
+
sandbox, PATH, and environment -- the same ones the agent's own test command
|
|
1866
|
+
will resolve. A disabled or unbuildable runner yields "could not run", which
|
|
1867
|
+
the decision reads as unknown rather than as a missing package.
|
|
1868
|
+
"""
|
|
1869
|
+
|
|
1870
|
+
def _run(command: str, timeout_s: float) -> ShellProbeResult:
|
|
1871
|
+
if shell_runner is None:
|
|
1872
|
+
return ShellProbeResult(exit_code=None, stderr="shell execution is unavailable")
|
|
1873
|
+
try:
|
|
1874
|
+
completed = shell_runner.run(
|
|
1875
|
+
root=root,
|
|
1876
|
+
cwd=root,
|
|
1877
|
+
cmd=command,
|
|
1878
|
+
timeout_s=int(timeout_s),
|
|
1879
|
+
)
|
|
1880
|
+
except Exception as exc: # noqa: BLE001 - readonly mode, sandbox failure, timeout
|
|
1881
|
+
return ShellProbeResult(exit_code=None, stderr=str(exc))
|
|
1882
|
+
return ShellProbeResult(
|
|
1883
|
+
exit_code=int(getattr(completed, "returncode", 1) or 0),
|
|
1884
|
+
stderr=str(getattr(completed, "stderr", "") or ""),
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
return _run
|
|
1888
|
+
|
|
1889
|
+
|
|
1890
|
+
def _pre_provision_declared_test_runner(
|
|
1891
|
+
*,
|
|
1892
|
+
store: SessionStore,
|
|
1893
|
+
root: Path,
|
|
1894
|
+
cfg: AppConfig,
|
|
1895
|
+
runtime_kind: RuntimeKind,
|
|
1896
|
+
subagent_depth: int,
|
|
1897
|
+
shell_runner: Any,
|
|
1898
|
+
) -> None:
|
|
1899
|
+
"""Close a declared-but-missing test runner gap once, during warmup.
|
|
1900
|
+
|
|
1901
|
+
A top-level autonomous run installs the runner the repo's own config
|
|
1902
|
+
declares; an interactive run only records that the gap exists so the agent
|
|
1903
|
+
or the user can decide. Never raises: startup must survive a broken package
|
|
1904
|
+
index, a disabled shell, or an unreachable sandbox.
|
|
1905
|
+
"""
|
|
1906
|
+
try:
|
|
1907
|
+
enabled = _workspace_provisioning_enabled(cfg)
|
|
1908
|
+
declared = detect_declared_test_runner(root) if enabled else None
|
|
1909
|
+
importable: bool | None = None
|
|
1910
|
+
if declared is not None:
|
|
1911
|
+
importable = probe_runner_importable(
|
|
1912
|
+
declared.package,
|
|
1913
|
+
run_command=_shell_command_probe(shell_runner=shell_runner, root=root),
|
|
1914
|
+
)
|
|
1915
|
+
decision = resolve_provisioning_decision(
|
|
1916
|
+
declared=declared,
|
|
1917
|
+
importable=importable,
|
|
1918
|
+
runtime_kind=runtime_kind,
|
|
1919
|
+
enabled=enabled,
|
|
1920
|
+
subagent_depth=subagent_depth,
|
|
1921
|
+
already_attempted=(
|
|
1922
|
+
provisioning_already_attempted(declared.package) if declared is not None else False
|
|
1923
|
+
),
|
|
1924
|
+
)
|
|
1925
|
+
if decision.action is ProvisioningAction.SKIP:
|
|
1926
|
+
return
|
|
1927
|
+
if decision.action is ProvisioningAction.REPORT_GAP:
|
|
1928
|
+
store.append(
|
|
1929
|
+
"env_gap_detected",
|
|
1930
|
+
{
|
|
1931
|
+
"package": decision.package,
|
|
1932
|
+
"trigger_config_file": decision.trigger_config_file,
|
|
1933
|
+
"runtime_kind": runtime_kind.value,
|
|
1934
|
+
"reason": decision.reason,
|
|
1935
|
+
},
|
|
1936
|
+
)
|
|
1937
|
+
return
|
|
1938
|
+
outcome = provision_test_runner(
|
|
1939
|
+
decision,
|
|
1940
|
+
run_command=_shell_command_probe(shell_runner=shell_runner, root=root),
|
|
1941
|
+
)
|
|
1942
|
+
if outcome is None:
|
|
1943
|
+
# Another session in this process claimed the one attempt; nothing
|
|
1944
|
+
# happened here, so nothing is reported here.
|
|
1945
|
+
return
|
|
1946
|
+
store.append(
|
|
1947
|
+
"env_provisioned",
|
|
1948
|
+
{**outcome.payload(), "runtime_kind": runtime_kind.value},
|
|
1949
|
+
)
|
|
1950
|
+
except Exception as exc: # noqa: BLE001 - provisioning must never break startup
|
|
1951
|
+
try:
|
|
1952
|
+
store.append(
|
|
1953
|
+
"warning",
|
|
1954
|
+
{"warning": "workspace_provisioning_failed", "error": str(exc)},
|
|
1955
|
+
)
|
|
1956
|
+
except Exception: # noqa: BLE001 - best-effort diagnostic
|
|
1957
|
+
pass
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
def create_session(
|
|
1961
|
+
*,
|
|
1962
|
+
cfg: AppConfig,
|
|
1963
|
+
root: Path,
|
|
1964
|
+
mode: str,
|
|
1965
|
+
yes: bool,
|
|
1966
|
+
max_steps: int | None,
|
|
1967
|
+
no_log: bool,
|
|
1968
|
+
api_key_override: str | None = None,
|
|
1969
|
+
console: Any | None = None,
|
|
1970
|
+
deny_write_prefixes: list[str] | None = None,
|
|
1971
|
+
allow_write_globs: list[str] | None = None,
|
|
1972
|
+
persona_allow_write_globs: list[str] | None = None,
|
|
1973
|
+
non_interactive: bool = False,
|
|
1974
|
+
one_shot_execution: bool = False,
|
|
1975
|
+
enable_chat_turn_step_budget: bool = False,
|
|
1976
|
+
chat_turn_fixed_override: int | None = None,
|
|
1977
|
+
session_log_dir_override: Path | None = None,
|
|
1978
|
+
session_id_override: str | None = None,
|
|
1979
|
+
prompt_cache_parent_session_id: str | None = None,
|
|
1980
|
+
surface: Surface | None = None,
|
|
1981
|
+
usage_role: str = "main",
|
|
1982
|
+
trusted_system_prompt_override: str | None = None,
|
|
1983
|
+
trusted_system_prompt_append: str | None = None,
|
|
1984
|
+
untrusted_prompt_prelude: str | None = None,
|
|
1985
|
+
enable_compaction: bool = True,
|
|
1986
|
+
enable_tool_output_offload: bool | None = None,
|
|
1987
|
+
enable_conversation_summarization: bool | None = None,
|
|
1988
|
+
compaction_profile: str = "chat",
|
|
1989
|
+
verification_enabled: bool = True,
|
|
1990
|
+
authoritative_verification_commands: list[str] | None = None,
|
|
1991
|
+
verify_cmd: list[str] | None = None,
|
|
1992
|
+
subagents_enabled: bool | None = None,
|
|
1993
|
+
helper_subagents_enabled: bool = False,
|
|
1994
|
+
enforce_explicit_subagent_requests: bool = True,
|
|
1995
|
+
subagent_depth: int = 0,
|
|
1996
|
+
subagent_registry: dict[str, SubagentDefinition] | None = None,
|
|
1997
|
+
workspace_binding: WorkspaceBinding | None = None,
|
|
1998
|
+
active_workdir_relpath_override: str | None = None,
|
|
1999
|
+
runtime_kind: RuntimeKind | str | None = None,
|
|
2000
|
+
mcp_manager: McpManager | ForgeTaskScopedMcpManager | None = None,
|
|
2001
|
+
session_source: str = "startup",
|
|
2002
|
+
session_source_metadata: dict[str, Any] | None = None,
|
|
2003
|
+
execution_deadline: ExecutionDeadline | None = None,
|
|
2004
|
+
crash_diagnostic_log_path: str | Path | None = None,
|
|
2005
|
+
crash_diagnostic_logger: CrashDiagnosticLogger | None = None,
|
|
2006
|
+
tool_dispatch_guard: ToolDispatchGuard | None = None,
|
|
2007
|
+
managed_browser_service: ManagedBrowserService | None = None,
|
|
2008
|
+
managed_browser_owner_id: str | None = None,
|
|
2009
|
+
managed_browser_cancel_check: Callable[[], bool] | None = None,
|
|
2010
|
+
host_action_handler: HostActionHandler | None = None,
|
|
2011
|
+
host_action_capabilities: Collection[str] | None = None,
|
|
2012
|
+
readonly_child_web_tool_names: Collection[str] | None = None,
|
|
2013
|
+
child_managed_browser_tool_names: Collection[str] | None = None,
|
|
2014
|
+
) -> AgentSession:
|
|
2015
|
+
surface = surface or NoopSurface()
|
|
2016
|
+
resolved_runtime_kind = resolve_session_runtime_kind(
|
|
2017
|
+
runtime_kind=runtime_kind,
|
|
2018
|
+
one_shot_execution=one_shot_execution,
|
|
2019
|
+
subagent_depth=subagent_depth,
|
|
2020
|
+
)
|
|
2021
|
+
parent_steer_inbox = SteerInbox()
|
|
2022
|
+
workspace_trust_prompt = _build_workspace_trust_prompt(
|
|
2023
|
+
surface=surface,
|
|
2024
|
+
non_interactive=non_interactive,
|
|
2025
|
+
)
|
|
2026
|
+
prompt_context = prepare_session_prompt_context(
|
|
2027
|
+
cfg=cfg,
|
|
2028
|
+
root=root,
|
|
2029
|
+
mode=mode,
|
|
2030
|
+
yes=yes,
|
|
2031
|
+
deny_write_prefixes=deny_write_prefixes,
|
|
2032
|
+
allow_write_globs=allow_write_globs,
|
|
2033
|
+
persona_allow_write_globs=persona_allow_write_globs,
|
|
2034
|
+
non_interactive=non_interactive,
|
|
2035
|
+
one_shot_execution=one_shot_execution,
|
|
2036
|
+
verification_enabled=verification_enabled,
|
|
2037
|
+
authoritative_verification_commands=authoritative_verification_commands,
|
|
2038
|
+
verify_cmd=verify_cmd,
|
|
2039
|
+
trusted_system_prompt_override=trusted_system_prompt_override,
|
|
2040
|
+
trusted_system_prompt_append=trusted_system_prompt_append,
|
|
2041
|
+
untrusted_prompt_prelude=untrusted_prompt_prelude,
|
|
2042
|
+
subagents_enabled=subagents_enabled,
|
|
2043
|
+
subagent_depth=subagent_depth,
|
|
2044
|
+
subagent_registry=subagent_registry,
|
|
2045
|
+
workspace_binding=workspace_binding,
|
|
2046
|
+
workspace_trust_prompt=workspace_trust_prompt,
|
|
2047
|
+
)
|
|
2048
|
+
root = prompt_context.root
|
|
2049
|
+
workspace_context = prompt_context.workspace_context
|
|
2050
|
+
binding_requested_path = prompt_context.binding_requested_path
|
|
2051
|
+
binding_source = prompt_context.binding_source
|
|
2052
|
+
binding_risk_level = prompt_context.binding_risk_level
|
|
2053
|
+
binding_created_path = prompt_context.binding_created_path
|
|
2054
|
+
authoritative_verify_commands = prompt_context.authoritative_verify_commands
|
|
2055
|
+
session_cfg = prompt_context.session_cfg
|
|
2056
|
+
session_cfg.step_budget_policy = normalize_step_budget_policy(session_cfg.step_budget_policy)
|
|
2057
|
+
session_cfg, native_streaming_warning_message = _disable_unsupported_native_streaming(
|
|
2058
|
+
cfg=session_cfg
|
|
2059
|
+
)
|
|
2060
|
+
normalized_session_source = str(session_source or "startup").strip().lower() or "startup"
|
|
2061
|
+
if normalized_session_source not in {"startup", "resume", "fork"}:
|
|
2062
|
+
raise ConfigError("session_source must be one of: startup, resume, fork.")
|
|
2063
|
+
if session_source_metadata is not None and not isinstance(session_source_metadata, dict):
|
|
2064
|
+
raise ConfigError("session_source_metadata must be an object.")
|
|
2065
|
+
normalized_session_source_metadata = (
|
|
2066
|
+
copy.deepcopy(session_source_metadata) if session_source_metadata is not None else {}
|
|
2067
|
+
)
|
|
2068
|
+
if active_workdir_relpath_override is None:
|
|
2069
|
+
initial_active_workdir_relpath = _normalize_workspace_relpath(
|
|
2070
|
+
workspace_context.focus_relpath
|
|
2071
|
+
)
|
|
2072
|
+
else:
|
|
2073
|
+
initial_active_workdir_relpath = _normalize_workspace_relpath(
|
|
2074
|
+
active_workdir_relpath_override
|
|
2075
|
+
)
|
|
2076
|
+
initial_active_workdir = resolve_workdir_relpath_within_workspace(
|
|
2077
|
+
workspace_root=workspace_context.workspace_root,
|
|
2078
|
+
relpath=initial_active_workdir_relpath,
|
|
2079
|
+
)
|
|
2080
|
+
if not initial_active_workdir.exists():
|
|
2081
|
+
raise SessionWorkdirError(f"Directory does not exist: {initial_active_workdir}")
|
|
2082
|
+
if not initial_active_workdir.is_dir():
|
|
2083
|
+
raise SessionWorkdirError(f"Path is not a directory: {initial_active_workdir}")
|
|
2084
|
+
|
|
2085
|
+
active_profile = get_active_profile(session_cfg)
|
|
2086
|
+
if api_key_override is None:
|
|
2087
|
+
if active_profile.auth_provider:
|
|
2088
|
+
api_key = ""
|
|
2089
|
+
api_key_source = f"provider-auth:{active_profile.auth_provider}"
|
|
2090
|
+
else:
|
|
2091
|
+
api_key_resolution = resolve_api_key(session_cfg)
|
|
2092
|
+
if api_key_resolution.key is None:
|
|
2093
|
+
api_key = get_api_key(session_cfg)
|
|
2094
|
+
api_key_source = "missing"
|
|
2095
|
+
else:
|
|
2096
|
+
api_key = api_key_resolution.key
|
|
2097
|
+
api_key_source = api_key_resolution.source
|
|
2098
|
+
else:
|
|
2099
|
+
api_key = api_key_override.strip()
|
|
2100
|
+
if not api_key:
|
|
2101
|
+
raise ConfigError("API key is empty.")
|
|
2102
|
+
api_key_source = "override"
|
|
2103
|
+
coding_temperature = resolve_role_temperature(session_cfg, role="coding")
|
|
2104
|
+
review_temperature = resolve_role_temperature(session_cfg, role="review")
|
|
2105
|
+
planner_temperature = resolve_role_temperature(session_cfg, role="planner")
|
|
2106
|
+
conflict_review_temperature = resolve_role_temperature(session_cfg, role="conflict_review")
|
|
2107
|
+
compactor_temperature = resolve_role_temperature(session_cfg, role="compactor")
|
|
2108
|
+
chat_temperature = resolve_role_temperature(session_cfg, role="chat")
|
|
2109
|
+
llm_timeout_s = resolve_llm_timeout_s(session_cfg)
|
|
2110
|
+
llm_enable_thinking = resolve_llm_enable_thinking(session_cfg)
|
|
2111
|
+
llm_reasoning_effort = resolve_llm_reasoning_effort(session_cfg)
|
|
2112
|
+
active_profile_name = active_profile.name
|
|
2113
|
+
active_profile_base_url = resolve_effective_base_url(
|
|
2114
|
+
cfg=session_cfg,
|
|
2115
|
+
profile=active_profile,
|
|
2116
|
+
)
|
|
2117
|
+
cache_affinity_preset = find_preset_for_profile(active_profile)
|
|
2118
|
+
session_scoped_cache_affinity = (
|
|
2119
|
+
cache_affinity_preset is not None
|
|
2120
|
+
and str(cache_affinity_preset.provider_key or "").strip().lower() == "moonshot"
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
def _prompt_cache_namespace(role: str) -> str | None:
|
|
2124
|
+
return build_prompt_cache_namespace(
|
|
2125
|
+
workspace_root=workspace_context.workspace_root,
|
|
2126
|
+
role=role,
|
|
2127
|
+
profile_name=active_profile_name,
|
|
2128
|
+
session_id=session_id if session_scoped_cache_affinity else None,
|
|
2129
|
+
)
|
|
2130
|
+
|
|
2131
|
+
registry = ModelRegistry(cfg=session_cfg, api_key=api_key)
|
|
2132
|
+
# Deprecated no-op: recorded for observability and the (ignored)
|
|
2133
|
+
# AgentSession.routing_mode field; nothing consults it.
|
|
2134
|
+
routing_mode = str(getattr(session_cfg, "routing_mode", "auto") or "auto")
|
|
2135
|
+
resolved_subagents_enabled = prompt_context.resolved_subagents_enabled
|
|
2136
|
+
resolved_skills_enabled = prompt_context.resolved_skills_enabled
|
|
2137
|
+
skills_auto_invoke = prompt_context.skills_auto_invoke
|
|
2138
|
+
activation_decision = prompt_context.activation_decision
|
|
2139
|
+
plugin_activation_index = _build_plugin_activation_index(root)
|
|
2140
|
+
plugin_activation_dropped_counts: Counter[str] = Counter(
|
|
2141
|
+
prompt_context.plugin_activation_dropped_counts
|
|
2142
|
+
)
|
|
2143
|
+
discovered_skills = prompt_context.discovered_skills
|
|
2144
|
+
repo_conventions = prompt_context.repo_conventions
|
|
2145
|
+
effective_one_shot_execution = prompt_context.effective_one_shot_execution
|
|
2146
|
+
resolved_subagent_registry = prompt_context.resolved_subagent_registry
|
|
2147
|
+
step_budget_runtime = StepBudgetRuntime()
|
|
2148
|
+
|
|
2149
|
+
session_id = session_id_override.strip() if session_id_override else make_session_id()
|
|
2150
|
+
if not session_id:
|
|
2151
|
+
session_id = make_session_id()
|
|
2152
|
+
prompt_cache_stream_key = resolve_prompt_cache_key(session_cfg)
|
|
2153
|
+
if prompt_cache_stream_key is None and session_cfg.cache.prompt_cache_key_enabled:
|
|
2154
|
+
prompt_cache_stream_key = derive_prompt_cache_stream_key(
|
|
2155
|
+
session_id=session_id,
|
|
2156
|
+
parent_session_id=prompt_cache_parent_session_id,
|
|
2157
|
+
)
|
|
2158
|
+
if mcp_manager is None:
|
|
2159
|
+
create_mcp_manager_fn = _patchable("create_mcp_manager", create_mcp_manager)
|
|
2160
|
+
if create_mcp_manager_fn is not _DEFAULT_CREATE_MCP_MANAGER:
|
|
2161
|
+
mcp_manager = create_mcp_manager_fn(
|
|
2162
|
+
workspace_root=workspace_context.workspace_root,
|
|
2163
|
+
runtime_kind=resolved_runtime_kind,
|
|
2164
|
+
session_id=session_id,
|
|
2165
|
+
)
|
|
2166
|
+
else:
|
|
2167
|
+
resolved_mcp_config = load_resolved_mcp_config(
|
|
2168
|
+
workspace_root=workspace_context.workspace_root
|
|
2169
|
+
)
|
|
2170
|
+
resolved_mcp_config, mcp_dropped_counts = _filter_mcp_config_for_plugins(
|
|
2171
|
+
config=resolved_mcp_config,
|
|
2172
|
+
activation_decision=activation_decision,
|
|
2173
|
+
)
|
|
2174
|
+
plugin_activation_dropped_counts.update(mcp_dropped_counts)
|
|
2175
|
+
mcp_manager = McpManager(
|
|
2176
|
+
resolved_config=resolved_mcp_config,
|
|
2177
|
+
workspace_root=workspace_context.workspace_root,
|
|
2178
|
+
runtime_kind=resolved_runtime_kind,
|
|
2179
|
+
session_id=session_id,
|
|
2180
|
+
)
|
|
2181
|
+
compaction_settings = resolve_compaction_settings(session_cfg)
|
|
2182
|
+
runtime_context_features = resolve_runtime_context_features(
|
|
2183
|
+
settings=compaction_settings,
|
|
2184
|
+
enable_compaction=enable_compaction,
|
|
2185
|
+
enable_tool_output_offload=enable_tool_output_offload,
|
|
2186
|
+
enable_conversation_summarization=enable_conversation_summarization,
|
|
2187
|
+
logging_enabled=not no_log,
|
|
2188
|
+
explicit_session_artifact_root=session_log_dir_override is not None,
|
|
2189
|
+
)
|
|
2190
|
+
compaction_enabled = runtime_context_features.any_enabled
|
|
2191
|
+
tool_output_offload_enabled = runtime_context_features.tool_output_offload_enabled
|
|
2192
|
+
conversation_summarization_enabled = runtime_context_features.conversation_summarization_enabled
|
|
2193
|
+
compactor_model_name: str | None = None
|
|
2194
|
+
if conversation_summarization_enabled:
|
|
2195
|
+
compactor_model_name = resolve_model_for_role(
|
|
2196
|
+
cfg=session_cfg,
|
|
2197
|
+
role=ROLE_COMPACTOR,
|
|
2198
|
+
plan=None,
|
|
2199
|
+
)
|
|
2200
|
+
active_model_refs = [ActiveModelRef(role=ROLE_CODING, model_name=session_cfg.model)]
|
|
2201
|
+
if compactor_model_name:
|
|
2202
|
+
active_model_refs.append(
|
|
2203
|
+
ActiveModelRef(role=ROLE_COMPACTOR, model_name=compactor_model_name)
|
|
2204
|
+
)
|
|
2205
|
+
model_metadata_policy_result = evaluate_active_model_metadata_policy(
|
|
2206
|
+
cfg=session_cfg,
|
|
2207
|
+
registry=registry,
|
|
2208
|
+
active_models=active_model_refs,
|
|
2209
|
+
)
|
|
2210
|
+
client = _make_session_llm_client(
|
|
2211
|
+
cfg=session_cfg,
|
|
2212
|
+
api_key=api_key,
|
|
2213
|
+
model=session_cfg.model,
|
|
2214
|
+
timeout_s=llm_timeout_s,
|
|
2215
|
+
temperature=coding_temperature,
|
|
2216
|
+
prompt_cache_key=prompt_cache_stream_key,
|
|
2217
|
+
prompt_cache_retention=resolve_prompt_cache_retention(session_cfg),
|
|
2218
|
+
prompt_cache_namespace=_prompt_cache_namespace(ROLE_CODING),
|
|
2219
|
+
enable_thinking=llm_enable_thinking,
|
|
2220
|
+
reasoning_effort=llm_reasoning_effort,
|
|
2221
|
+
session_id=session_id,
|
|
2222
|
+
)
|
|
2223
|
+
sessions_dir = (
|
|
2224
|
+
session_log_dir_override
|
|
2225
|
+
if session_log_dir_override is not None
|
|
2226
|
+
else resolve_sessions_dir(session_cfg)
|
|
2227
|
+
)
|
|
2228
|
+
store = SessionStore(
|
|
2229
|
+
enabled=not no_log,
|
|
2230
|
+
artifact_persistence_enabled=(not no_log) or session_log_dir_override is not None,
|
|
2231
|
+
sessions_dir=sessions_dir,
|
|
2232
|
+
session_id=session_id,
|
|
2233
|
+
cwd=str(initial_active_workdir),
|
|
2234
|
+
repo_root=str(root),
|
|
2235
|
+
workspace_root=str(workspace_context.workspace_root),
|
|
2236
|
+
focus_dir=str(workspace_context.focus_path),
|
|
2237
|
+
git_root=(
|
|
2238
|
+
str(workspace_context.git_root) if workspace_context.git_root is not None else None
|
|
2239
|
+
),
|
|
2240
|
+
workspace_kind=workspace_context.workspace_kind,
|
|
2241
|
+
binding_source=binding_source,
|
|
2242
|
+
binding_requested_path=binding_requested_path,
|
|
2243
|
+
binding_risk_level=binding_risk_level,
|
|
2244
|
+
binding_created_path=binding_created_path,
|
|
2245
|
+
runtime_kind=resolved_runtime_kind.value,
|
|
2246
|
+
active_workdir=str(initial_active_workdir),
|
|
2247
|
+
active_workdir_relpath=initial_active_workdir_relpath,
|
|
2248
|
+
)
|
|
2249
|
+
resolved_crash_diagnostic_log_path = resolve_crash_diagnostic_log_path(
|
|
2250
|
+
session_cfg,
|
|
2251
|
+
cli_diagnostic_log_path=crash_diagnostic_log_path,
|
|
2252
|
+
)
|
|
2253
|
+
crash_diagnostics = crash_diagnostic_logger or build_crash_diagnostic_logger(
|
|
2254
|
+
path=resolved_crash_diagnostic_log_path,
|
|
2255
|
+
run_id=session_id,
|
|
2256
|
+
session_id=session_id,
|
|
2257
|
+
runtime_kind=resolved_runtime_kind.value,
|
|
2258
|
+
)
|
|
2259
|
+
crash_diagnostics.event(
|
|
2260
|
+
"run_started",
|
|
2261
|
+
{
|
|
2262
|
+
"runtime_kind": resolved_runtime_kind.value,
|
|
2263
|
+
"model": session_cfg.model,
|
|
2264
|
+
"max_steps": max_steps,
|
|
2265
|
+
"session_source": normalized_session_source,
|
|
2266
|
+
"deadline": (
|
|
2267
|
+
execution_deadline.telemetry_snapshot() if execution_deadline is not None else None
|
|
2268
|
+
),
|
|
2269
|
+
},
|
|
2270
|
+
durable=True,
|
|
2271
|
+
)
|
|
2272
|
+
surface_on_warning = _meaningful_surface_warning_handler(surface)
|
|
2273
|
+
|
|
2274
|
+
def _emit_hook_warning(message: str) -> None:
|
|
2275
|
+
clean = str(message or "").strip()
|
|
2276
|
+
if not clean:
|
|
2277
|
+
return
|
|
2278
|
+
store.append("warning", {"warning": "hook_warning", "message": clean})
|
|
2279
|
+
if callable(surface_on_warning):
|
|
2280
|
+
surface_on_warning(clean)
|
|
2281
|
+
else:
|
|
2282
|
+
warnings.warn(clean, stacklevel=2)
|
|
2283
|
+
|
|
2284
|
+
tool_output_offloader: ToolOutputOffloader | None = None
|
|
2285
|
+
if tool_output_offload_enabled:
|
|
2286
|
+
workspace_artifacts_enabled = resolved_runtime_kind != RuntimeKind.SWARM_WORKER
|
|
2287
|
+
tool_output_offloader = ToolOutputOffloader(
|
|
2288
|
+
artifact_layout=store.session_artifact_layout,
|
|
2289
|
+
workspace_root=root,
|
|
2290
|
+
threshold_chars=compaction_settings.tool_output_offload_threshold_chars,
|
|
2291
|
+
preview_chars=compaction_settings.tool_output_preview_chars,
|
|
2292
|
+
workspace_artifacts_enabled=workspace_artifacts_enabled,
|
|
2293
|
+
)
|
|
2294
|
+
system_prompt = prompt_context.system_prompt
|
|
2295
|
+
system_prompt_sha256 = hashlib.sha256(system_prompt.encode("utf-8")).hexdigest()
|
|
2296
|
+
effective_deny_write_prefixes = prompt_context.effective_deny_write_prefixes
|
|
2297
|
+
effective_allow_write_globs = prompt_context.effective_allow_write_globs
|
|
2298
|
+
effective_verification_selection = prompt_context.effective_verification_selection
|
|
2299
|
+
effective_verification_commands = prompt_context.effective_verification_commands
|
|
2300
|
+
recommended_verification_commands = prompt_context.recommended_verification_commands
|
|
2301
|
+
verification_selection_metadata = verification_selection_payload(
|
|
2302
|
+
effective_verification_selection,
|
|
2303
|
+
authoritative=is_authoritative_verify_command_selection(effective_verification_selection),
|
|
2304
|
+
)
|
|
2305
|
+
|
|
2306
|
+
usage_summary = UsageSummary()
|
|
2307
|
+
custom_tool_session_state = build_custom_tool_session_state(
|
|
2308
|
+
workspace_root=root,
|
|
2309
|
+
custom_tools_enabled=bool(getattr(session_cfg, "custom_tools_enabled", True)),
|
|
2310
|
+
mode=mode,
|
|
2311
|
+
runtime_kind=resolved_runtime_kind,
|
|
2312
|
+
built_in_tool_names={spec.name.casefold() for spec in iter_builtin_tool_metadata()},
|
|
2313
|
+
write_scope_restricted=_custom_tools_write_scope_restricted(
|
|
2314
|
+
mode=mode,
|
|
2315
|
+
deny_write_prefixes=deny_write_prefixes,
|
|
2316
|
+
allow_write_globs=allow_write_globs,
|
|
2317
|
+
persona_allow_write_globs=persona_allow_write_globs,
|
|
2318
|
+
),
|
|
2319
|
+
)
|
|
2320
|
+
(
|
|
2321
|
+
custom_tool_session_state,
|
|
2322
|
+
custom_tool_dropped_counts,
|
|
2323
|
+
) = _filter_custom_tool_session_state_for_plugins(
|
|
2324
|
+
state=custom_tool_session_state,
|
|
2325
|
+
activation_decision=activation_decision,
|
|
2326
|
+
index=plugin_activation_index,
|
|
2327
|
+
)
|
|
2328
|
+
plugin_activation_dropped_counts.update(custom_tool_dropped_counts)
|
|
2329
|
+
if activation_decision.untrusted_project_plugin_ids:
|
|
2330
|
+
ids = ", ".join(sorted(activation_decision.untrusted_project_plugin_ids))
|
|
2331
|
+
warning_message = f"Ignoring untrusted project plugin overrides: {ids}"
|
|
2332
|
+
store.append(
|
|
2333
|
+
"workspace_trust_untrusted_overrides",
|
|
2334
|
+
{"plugin_ids": sorted(activation_decision.untrusted_project_plugin_ids)},
|
|
2335
|
+
)
|
|
2336
|
+
if callable(surface_on_warning):
|
|
2337
|
+
surface_on_warning(warning_message)
|
|
2338
|
+
else:
|
|
2339
|
+
warnings.warn(warning_message, stacklevel=2)
|
|
2340
|
+
|
|
2341
|
+
if native_streaming_warning_message:
|
|
2342
|
+
store.append(
|
|
2343
|
+
"warning",
|
|
2344
|
+
{
|
|
2345
|
+
"warning": "native_streaming_disabled",
|
|
2346
|
+
"message": native_streaming_warning_message,
|
|
2347
|
+
},
|
|
2348
|
+
)
|
|
2349
|
+
if callable(surface_on_warning):
|
|
2350
|
+
surface_on_warning(native_streaming_warning_message)
|
|
2351
|
+
else:
|
|
2352
|
+
warnings.warn(native_streaming_warning_message, stacklevel=2)
|
|
2353
|
+
|
|
2354
|
+
for verification_warning_message in prompt_context.verification_selection_warnings:
|
|
2355
|
+
store.append(
|
|
2356
|
+
"warning",
|
|
2357
|
+
{
|
|
2358
|
+
"warning": "verification_selection_degraded",
|
|
2359
|
+
"message": verification_warning_message,
|
|
2360
|
+
**verification_selection_metadata,
|
|
2361
|
+
},
|
|
2362
|
+
)
|
|
2363
|
+
if callable(surface_on_warning):
|
|
2364
|
+
surface_on_warning(verification_warning_message)
|
|
2365
|
+
else:
|
|
2366
|
+
warnings.warn(verification_warning_message, stacklevel=2)
|
|
2367
|
+
|
|
2368
|
+
# Determinism controls and the effective-configuration snapshot, both
|
|
2369
|
+
# resolved before the first provider call so nothing is recorded against a
|
|
2370
|
+
# configuration that was not yet in force.
|
|
2371
|
+
#
|
|
2372
|
+
# Only the top-level session does this. A subagent shares the process, so
|
|
2373
|
+
# it shares the installed sampling settings; re-resolving per subagent
|
|
2374
|
+
# would just rewrite the same values, and re-snapshotting would bury the
|
|
2375
|
+
# run's one authoritative config record under near-duplicates.
|
|
2376
|
+
if subagent_depth == 0:
|
|
2377
|
+
sampling_settings = resolve_sampling_settings(
|
|
2378
|
+
config_values=cfg.model_dump(),
|
|
2379
|
+
)
|
|
2380
|
+
set_active_sampling_settings(sampling_settings)
|
|
2381
|
+
for sampling_warning in sampling_settings.warnings:
|
|
2382
|
+
# An invalid sampling value is ignored rather than fatal (a typo in
|
|
2383
|
+
# a benchmark harness must not abort a campaign), so it has to be
|
|
2384
|
+
# visible in the log or it is indistinguishable from never having
|
|
2385
|
+
# been set at all.
|
|
2386
|
+
store.append(
|
|
2387
|
+
"warning",
|
|
2388
|
+
{
|
|
2389
|
+
"warning": "sampling_setting_ignored",
|
|
2390
|
+
"message": sampling_warning.message(),
|
|
2391
|
+
**sampling_warning.payload(),
|
|
2392
|
+
},
|
|
2393
|
+
)
|
|
2394
|
+
store.append(
|
|
2395
|
+
CONFIG_SNAPSHOT_EVENT,
|
|
2396
|
+
config_snapshot_payload(
|
|
2397
|
+
config_values=cfg.model_dump(),
|
|
2398
|
+
version=__version__,
|
|
2399
|
+
build_info=load_build_info().telemetry_payload(),
|
|
2400
|
+
sampling=sampling_settings,
|
|
2401
|
+
),
|
|
2402
|
+
)
|
|
2403
|
+
|
|
2404
|
+
store.append(
|
|
2405
|
+
"session_start",
|
|
2406
|
+
{
|
|
2407
|
+
"session_source": normalized_session_source,
|
|
2408
|
+
"session_source_metadata": normalized_session_source_metadata,
|
|
2409
|
+
"mode": mode,
|
|
2410
|
+
"runtime_kind": resolved_runtime_kind.value,
|
|
2411
|
+
"max_steps": max_steps,
|
|
2412
|
+
"step_budget_policy": session_cfg.step_budget_policy,
|
|
2413
|
+
"task_max_steps": session_cfg.task_max_steps,
|
|
2414
|
+
"subagent_max_steps": session_cfg.subagent_max_steps,
|
|
2415
|
+
"model": session_cfg.model,
|
|
2416
|
+
"router_model": "",
|
|
2417
|
+
"base_url_descriptor": endpoint_descriptor(session_cfg.base_url),
|
|
2418
|
+
"profile_name": active_profile.name,
|
|
2419
|
+
"protocol": active_profile.protocol,
|
|
2420
|
+
"provider_base_url_descriptor": endpoint_descriptor(active_profile_base_url),
|
|
2421
|
+
"auth_provider": active_profile.auth_provider,
|
|
2422
|
+
"reasoning_trace_adapter": active_profile.reasoning_trace_adapter,
|
|
2423
|
+
"api_key_source": api_key_source,
|
|
2424
|
+
"temperature": session_cfg.temperature,
|
|
2425
|
+
"coding_temperature": coding_temperature,
|
|
2426
|
+
"review_temperature": review_temperature,
|
|
2427
|
+
"planner_temperature": planner_temperature,
|
|
2428
|
+
"conflict_review_temperature": conflict_review_temperature,
|
|
2429
|
+
"compactor_temperature": compactor_temperature,
|
|
2430
|
+
"chat_temperature": chat_temperature,
|
|
2431
|
+
"llm_enable_thinking": llm_enable_thinking,
|
|
2432
|
+
"llm_reasoning_effort": llm_reasoning_effort,
|
|
2433
|
+
"stream": session_cfg.stream,
|
|
2434
|
+
"routing_mode": routing_mode,
|
|
2435
|
+
"subagents_enabled": resolved_subagents_enabled,
|
|
2436
|
+
"skills_enabled": resolved_skills_enabled,
|
|
2437
|
+
"skills_auto_invoke": skills_auto_invoke,
|
|
2438
|
+
"custom_tools_enabled": bool(getattr(session_cfg, "custom_tools_enabled", True)),
|
|
2439
|
+
"custom_tool_count": len(custom_tool_session_state.discovery.effective_tools),
|
|
2440
|
+
"custom_tool_issue_count": len(custom_tool_session_state.discovery.issues),
|
|
2441
|
+
"discovered_skill_count": len(discovered_skills.ordered),
|
|
2442
|
+
"repo_convention_count": len(repo_conventions),
|
|
2443
|
+
"skill_discovery_issues": [
|
|
2444
|
+
{
|
|
2445
|
+
"source_path": issue.source_path.as_posix(),
|
|
2446
|
+
"message": issue.message,
|
|
2447
|
+
}
|
|
2448
|
+
for issue in discovered_skills.issues
|
|
2449
|
+
],
|
|
2450
|
+
"subagent_depth": subagent_depth,
|
|
2451
|
+
"subagent_count": len(resolved_subagent_registry),
|
|
2452
|
+
"root": str(root),
|
|
2453
|
+
"workspace_root": str(workspace_context.workspace_root),
|
|
2454
|
+
"focus_dir": str(workspace_context.focus_path),
|
|
2455
|
+
"focus_relpath": workspace_context.focus_relpath,
|
|
2456
|
+
"active_workdir": str(initial_active_workdir),
|
|
2457
|
+
"active_workdir_relpath": initial_active_workdir_relpath,
|
|
2458
|
+
"workspace_kind": workspace_context.workspace_kind,
|
|
2459
|
+
"git_root": (
|
|
2460
|
+
str(workspace_context.git_root) if workspace_context.git_root is not None else None
|
|
2461
|
+
),
|
|
2462
|
+
"has_head_commit": workspace_context.has_head_commit,
|
|
2463
|
+
"current_branch": workspace_context.current_branch,
|
|
2464
|
+
"binding_requested_path": binding_requested_path,
|
|
2465
|
+
"binding_source": binding_source,
|
|
2466
|
+
"binding_risk_level": binding_risk_level,
|
|
2467
|
+
"binding_created_path": binding_created_path,
|
|
2468
|
+
"usage_role": usage_role,
|
|
2469
|
+
"yes": yes,
|
|
2470
|
+
"non_interactive": non_interactive,
|
|
2471
|
+
"one_shot_execution": effective_one_shot_execution,
|
|
2472
|
+
"enable_chat_turn_step_budget": enable_chat_turn_step_budget,
|
|
2473
|
+
"workspace_grounding": prompt_context.workspace_grounding.to_payload(),
|
|
2474
|
+
"chat_turn_fixed_override": chat_turn_fixed_override,
|
|
2475
|
+
"verification_enabled": verification_enabled,
|
|
2476
|
+
"effective_verification_commands": effective_verification_commands,
|
|
2477
|
+
**verification_selection_metadata,
|
|
2478
|
+
"model_metadata_policy": model_metadata_policy_result.policy,
|
|
2479
|
+
"model_metadata_diagnostics": [
|
|
2480
|
+
diagnostic.as_payload() for diagnostic in model_metadata_policy_result.diagnostics
|
|
2481
|
+
],
|
|
2482
|
+
"deny_write_prefixes": effective_deny_write_prefixes,
|
|
2483
|
+
"allow_write_globs": effective_allow_write_globs,
|
|
2484
|
+
"recommended_verification_commands": recommended_verification_commands,
|
|
2485
|
+
"authoritative_verification_commands": authoritative_verify_commands,
|
|
2486
|
+
"system_prompt_sha256": system_prompt_sha256,
|
|
2487
|
+
"requested_enable_compaction": runtime_context_features.requested_enable_compaction,
|
|
2488
|
+
"requested_tool_output_offload": (
|
|
2489
|
+
runtime_context_features.requested_tool_output_offload
|
|
2490
|
+
),
|
|
2491
|
+
"requested_conversation_summarization": (
|
|
2492
|
+
runtime_context_features.requested_conversation_summarization
|
|
2493
|
+
),
|
|
2494
|
+
"logging_enabled": runtime_context_features.logging_enabled,
|
|
2495
|
+
"explicit_session_artifact_root": (
|
|
2496
|
+
runtime_context_features.explicit_session_artifact_root
|
|
2497
|
+
),
|
|
2498
|
+
"tool_output_offload_artifact_persistence_available": (
|
|
2499
|
+
runtime_context_features.tool_output_offload_artifact_persistence_available
|
|
2500
|
+
),
|
|
2501
|
+
"compaction_enabled": compaction_enabled,
|
|
2502
|
+
"compaction_settings_enabled": runtime_context_features.settings_enabled,
|
|
2503
|
+
"tool_output_offload_enabled": tool_output_offload_enabled,
|
|
2504
|
+
"compaction_settings_offload_tool_outputs": (
|
|
2505
|
+
runtime_context_features.settings_offload_tool_outputs
|
|
2506
|
+
),
|
|
2507
|
+
"tool_output_offload_threshold_chars": (
|
|
2508
|
+
compaction_settings.tool_output_offload_threshold_chars
|
|
2509
|
+
),
|
|
2510
|
+
"tool_output_preview_chars": compaction_settings.tool_output_preview_chars,
|
|
2511
|
+
"conversation_summarization_enabled": conversation_summarization_enabled,
|
|
2512
|
+
"compaction_profile": compaction_profile,
|
|
2513
|
+
"compaction_settings_summarize_conversation": (
|
|
2514
|
+
runtime_context_features.settings_summarize_conversation
|
|
2515
|
+
),
|
|
2516
|
+
"compaction_recent_user_turns_to_keep": (compaction_settings.recent_user_turns_to_keep),
|
|
2517
|
+
"compaction_trigger_ratio": compaction_settings.trigger_ratio,
|
|
2518
|
+
"compaction_target_ratio": compaction_settings.target_ratio,
|
|
2519
|
+
"compaction_max_chunk_messages": compaction_settings.max_chunk_messages,
|
|
2520
|
+
"compaction_safety_margin_tokens": compaction_settings.safety_margin_tokens,
|
|
2521
|
+
"compactor_model": compactor_model_name,
|
|
2522
|
+
"mcp": mcp_manager.startup_metadata(),
|
|
2523
|
+
},
|
|
2524
|
+
)
|
|
2525
|
+
|
|
2526
|
+
if callable(surface_on_warning):
|
|
2527
|
+
for warning_message in model_metadata_policy_result.warning_messages:
|
|
2528
|
+
surface_on_warning(warning_message)
|
|
2529
|
+
else:
|
|
2530
|
+
for warning_message in model_metadata_policy_result.warning_messages:
|
|
2531
|
+
warnings.warn(warning_message, stacklevel=2)
|
|
2532
|
+
|
|
2533
|
+
if conversation_summarization_enabled and compactor_model_name == session_cfg.model:
|
|
2534
|
+
store.append(
|
|
2535
|
+
"warning",
|
|
2536
|
+
{
|
|
2537
|
+
"warning": "compactor_model_equals_main_model",
|
|
2538
|
+
"model": session_cfg.model,
|
|
2539
|
+
},
|
|
2540
|
+
)
|
|
2541
|
+
for issue in discovered_skills.issues:
|
|
2542
|
+
store.append(
|
|
2543
|
+
"warning",
|
|
2544
|
+
{
|
|
2545
|
+
"warning": "skill_discovery_issue",
|
|
2546
|
+
"source_path": issue.source_path.as_posix(),
|
|
2547
|
+
"message": issue.message,
|
|
2548
|
+
},
|
|
2549
|
+
)
|
|
2550
|
+
for issue in custom_tool_session_state.discovery.issues:
|
|
2551
|
+
store.append(
|
|
2552
|
+
"warning",
|
|
2553
|
+
{
|
|
2554
|
+
"warning": "custom_tool_discovery_issue",
|
|
2555
|
+
"source_scope": issue.source_scope,
|
|
2556
|
+
"source_path": issue.source_path.as_posix(),
|
|
2557
|
+
"tool_name": issue.tool_name,
|
|
2558
|
+
"code": issue.code,
|
|
2559
|
+
"message": issue.message,
|
|
2560
|
+
},
|
|
2561
|
+
)
|
|
2562
|
+
|
|
2563
|
+
resolved_sandbox_settings = resolve_shell_sandbox_settings(session_cfg)
|
|
2564
|
+
process_group_registry = ProcessGroupRegistry()
|
|
2565
|
+
|
|
2566
|
+
def _sandbox_warning_callback(message: str) -> None:
|
|
2567
|
+
store.append("sandbox_warning", {"message": message})
|
|
2568
|
+
|
|
2569
|
+
def _with_process_group_registry(built_runner: Any) -> Any:
|
|
2570
|
+
# Attach the session's registry to whichever runner the (possibly patched)
|
|
2571
|
+
# builder produced. Backends that cannot be reaped by process group -- the
|
|
2572
|
+
# Docker runner tears its container down through the docker CLI instead --
|
|
2573
|
+
# simply do not declare the field and are left untouched.
|
|
2574
|
+
if not dataclasses.is_dataclass(built_runner):
|
|
2575
|
+
return built_runner
|
|
2576
|
+
field_names = {f.name for f in dataclasses.fields(built_runner)}
|
|
2577
|
+
if "process_group_registry" not in field_names:
|
|
2578
|
+
return built_runner
|
|
2579
|
+
return dataclasses.replace(built_runner, process_group_registry=process_group_registry)
|
|
2580
|
+
|
|
2581
|
+
def _load_shell_runner_from_resolved_settings() -> Any:
|
|
2582
|
+
# stdin is closed for every foreground command, not just verification:
|
|
2583
|
+
# nobody is watching the terminal, so a tool that stops to ask a
|
|
2584
|
+
# question (ssh-keygen's "Overwrite (y/n)?", apt's config prompts) would
|
|
2585
|
+
# otherwise hold the pipe open until the hard kill.
|
|
2586
|
+
patched_build_shell_runner = _patchable("build_shell_runner", build_shell_runner)
|
|
2587
|
+
if patched_build_shell_runner is not build_shell_runner:
|
|
2588
|
+
return with_closed_stdin(
|
|
2589
|
+
_with_process_group_registry(
|
|
2590
|
+
patched_build_shell_runner(
|
|
2591
|
+
cfg=session_cfg,
|
|
2592
|
+
root=root,
|
|
2593
|
+
warning_callback=_sandbox_warning_callback,
|
|
2594
|
+
)
|
|
2595
|
+
)
|
|
2596
|
+
)
|
|
2597
|
+
return with_closed_stdin(
|
|
2598
|
+
_with_process_group_registry(
|
|
2599
|
+
_patchable(
|
|
2600
|
+
"build_shell_runner_from_settings",
|
|
2601
|
+
build_shell_runner_from_settings,
|
|
2602
|
+
)(
|
|
2603
|
+
resolved_sandbox_settings,
|
|
2604
|
+
root,
|
|
2605
|
+
warning_callback=_sandbox_warning_callback,
|
|
2606
|
+
)
|
|
2607
|
+
)
|
|
2608
|
+
)
|
|
2609
|
+
|
|
2610
|
+
if mode == "readonly":
|
|
2611
|
+
runner = DisabledShellRunner(reason="shell_run is disabled in readonly mode.")
|
|
2612
|
+
bg_runner = DisabledBackgroundRunner(
|
|
2613
|
+
reason="Background shell tools are disabled in readonly mode."
|
|
2614
|
+
)
|
|
2615
|
+
else:
|
|
2616
|
+
runner = LazyShellRunner(_load_shell_runner_from_resolved_settings)
|
|
2617
|
+
bg_runner = LazyBackgroundShellRunner(
|
|
2618
|
+
lambda: _patchable(
|
|
2619
|
+
"build_background_shell_runner_from_settings",
|
|
2620
|
+
build_background_shell_runner_from_settings,
|
|
2621
|
+
)(
|
|
2622
|
+
resolved_sandbox_settings,
|
|
2623
|
+
root,
|
|
2624
|
+
warning_callback=_sandbox_warning_callback,
|
|
2625
|
+
)
|
|
2626
|
+
)
|
|
2627
|
+
|
|
2628
|
+
# Warmup: close a declared-but-missing test-runner gap before the model
|
|
2629
|
+
# discovers it by failing a test run. Placed here because it must go through
|
|
2630
|
+
# the session's own shell runner -- the environment the agent's commands
|
|
2631
|
+
# resolve, and the one the operator's sandbox policy governs.
|
|
2632
|
+
_pre_provision_declared_test_runner(
|
|
2633
|
+
store=store,
|
|
2634
|
+
root=workspace_context.workspace_root,
|
|
2635
|
+
cfg=session_cfg,
|
|
2636
|
+
runtime_kind=resolved_runtime_kind,
|
|
2637
|
+
subagent_depth=subagent_depth,
|
|
2638
|
+
shell_runner=runner,
|
|
2639
|
+
)
|
|
2640
|
+
|
|
2641
|
+
terminal_manager = TerminalManager(
|
|
2642
|
+
runner=bg_runner,
|
|
2643
|
+
settings=resolved_sandbox_settings,
|
|
2644
|
+
)
|
|
2645
|
+
# Session-scoped: only the persist starts this run made, so the
|
|
2646
|
+
# finalization re-check cannot report a neighbouring session's service.
|
|
2647
|
+
persistent_service_registry = PersistentServiceRegistry()
|
|
2648
|
+
# Session-scoped for the same reason, and shared between the tool layer
|
|
2649
|
+
# (which scores overwrites) and the turn controller (which counts repeated
|
|
2650
|
+
# failures and reports scratch files) so both halves see one run's history.
|
|
2651
|
+
edit_discipline_state = EditDisciplineState()
|
|
2652
|
+
durable_service_manager = DurableServiceManager(
|
|
2653
|
+
root=root,
|
|
2654
|
+
state_dir=store.sessions_dir / "durable_services",
|
|
2655
|
+
settings=resolved_sandbox_settings,
|
|
2656
|
+
)
|
|
2657
|
+
|
|
2658
|
+
try:
|
|
2659
|
+
active_workdir_state: dict[str, Any] = {
|
|
2660
|
+
"relpath": initial_active_workdir_relpath,
|
|
2661
|
+
"session": None,
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
def _get_active_workdir_relpath() -> str:
|
|
2665
|
+
session_obj = active_workdir_state.get("session")
|
|
2666
|
+
if session_obj is not None:
|
|
2667
|
+
return resolve_session_active_workdir_relpath(session_obj)
|
|
2668
|
+
return _normalize_workspace_relpath(active_workdir_state.get("relpath"))
|
|
2669
|
+
|
|
2670
|
+
def _set_active_workdir(raw_path: str, source: str) -> dict[str, Any]:
|
|
2671
|
+
session_obj = active_workdir_state.get("session")
|
|
2672
|
+
if session_obj is None:
|
|
2673
|
+
workspace_root = workspace_context.workspace_root.resolve()
|
|
2674
|
+
current_relpath = _normalize_workspace_relpath(active_workdir_state.get("relpath"))
|
|
2675
|
+
current_path = resolve_workdir_relpath_within_workspace(
|
|
2676
|
+
workspace_root=workspace_root,
|
|
2677
|
+
relpath=current_relpath,
|
|
2678
|
+
)
|
|
2679
|
+
next_path = _resolve_requested_workdir_within_workspace(
|
|
2680
|
+
workspace_root=workspace_root,
|
|
2681
|
+
current_workdir=current_path,
|
|
2682
|
+
requested_path=raw_path,
|
|
2683
|
+
)
|
|
2684
|
+
next_relpath = _workspace_relpath_for_path(
|
|
2685
|
+
workspace_root=workspace_root,
|
|
2686
|
+
path=next_path,
|
|
2687
|
+
)
|
|
2688
|
+
changed = next_relpath != current_relpath
|
|
2689
|
+
active_workdir_state["relpath"] = next_relpath
|
|
2690
|
+
store.update_active_workdir(
|
|
2691
|
+
cwd=os.fspath(next_path),
|
|
2692
|
+
active_workdir_relpath=next_relpath,
|
|
2693
|
+
)
|
|
2694
|
+
payload = {
|
|
2695
|
+
"source": source,
|
|
2696
|
+
"workspace_root": os.fspath(workspace_root),
|
|
2697
|
+
"focus_dir": os.fspath(workspace_context.focus_path),
|
|
2698
|
+
"focus_relpath": workspace_context.focus_relpath,
|
|
2699
|
+
"previous_active_workdir": os.fspath(current_path),
|
|
2700
|
+
"previous_active_workdir_relpath": current_relpath,
|
|
2701
|
+
"active_workdir": os.fspath(next_path),
|
|
2702
|
+
"active_workdir_relpath": next_relpath,
|
|
2703
|
+
"changed": changed,
|
|
2704
|
+
}
|
|
2705
|
+
if payload["changed"]:
|
|
2706
|
+
store.append("session_workdir_changed", payload)
|
|
2707
|
+
return payload
|
|
2708
|
+
return set_session_active_workdir(session_obj, raw_path, source=source)
|
|
2709
|
+
|
|
2710
|
+
def _get_verify_command_selection() -> ResolvedVerifyCommands | None:
|
|
2711
|
+
session_obj = active_workdir_state.get("session")
|
|
2712
|
+
if session_obj is not None:
|
|
2713
|
+
return _session_verify_command_selection(session_obj)
|
|
2714
|
+
return prompt_context.effective_verification_selection
|
|
2715
|
+
|
|
2716
|
+
persona_switch_state = (
|
|
2717
|
+
PersonaSwitchState()
|
|
2718
|
+
if (
|
|
2719
|
+
resolved_runtime_kind == RuntimeKind.INTERACTIVE_CHAT
|
|
2720
|
+
and not non_interactive
|
|
2721
|
+
and persona_modes_enabled(session_cfg)
|
|
2722
|
+
)
|
|
2723
|
+
else None
|
|
2724
|
+
)
|
|
2725
|
+
persona_registry: dict[str, Any] | None = None
|
|
2726
|
+
persona_registry_warnings: tuple[str, ...] = ()
|
|
2727
|
+
if persona_switch_state is not None:
|
|
2728
|
+
try:
|
|
2729
|
+
loaded_personas, persona_registry_warnings = load_custom_personas(root)
|
|
2730
|
+
persona_registry = loaded_personas or None
|
|
2731
|
+
except Exception: # noqa: BLE001 - custom personas must not break startup
|
|
2732
|
+
persona_registry = None
|
|
2733
|
+
persona_registry_warnings = ()
|
|
2734
|
+
completion_gate_tools_enabled = bool(
|
|
2735
|
+
subagent_depth == 0
|
|
2736
|
+
and str(mode or "").strip().lower() != "readonly"
|
|
2737
|
+
and (
|
|
2738
|
+
effective_one_shot_execution
|
|
2739
|
+
or (
|
|
2740
|
+
resolved_runtime_kind == RuntimeKind.INTERACTIVE_CHAT
|
|
2741
|
+
and enable_chat_turn_step_budget
|
|
2742
|
+
)
|
|
2743
|
+
)
|
|
2744
|
+
)
|
|
2745
|
+
child_scheduler_holder: dict[str, ChildScheduler] = {}
|
|
2746
|
+
read_ledger_holder: dict[str, SessionReadLedger] = {}
|
|
2747
|
+
|
|
2748
|
+
def _capture_child_scheduler(scheduler: ChildScheduler) -> None:
|
|
2749
|
+
child_scheduler_holder["scheduler"] = scheduler
|
|
2750
|
+
|
|
2751
|
+
def _capture_read_ledger(ledger: SessionReadLedger) -> None:
|
|
2752
|
+
read_ledger_holder["ledger"] = ledger
|
|
2753
|
+
|
|
2754
|
+
tools = build_tools(
|
|
2755
|
+
root=root,
|
|
2756
|
+
console=console,
|
|
2757
|
+
surface=surface,
|
|
2758
|
+
persona_switch_state=persona_switch_state,
|
|
2759
|
+
store=store,
|
|
2760
|
+
process_group_registry=process_group_registry,
|
|
2761
|
+
mode=mode,
|
|
2762
|
+
yes=yes,
|
|
2763
|
+
cfg=session_cfg,
|
|
2764
|
+
api_key=api_key,
|
|
2765
|
+
max_steps=max_steps,
|
|
2766
|
+
no_log=no_log,
|
|
2767
|
+
usage_role=usage_role,
|
|
2768
|
+
usage_summary=usage_summary,
|
|
2769
|
+
model_registry=registry,
|
|
2770
|
+
deny_write_prefixes=deny_write_prefixes,
|
|
2771
|
+
allow_write_globs=allow_write_globs,
|
|
2772
|
+
persona_allow_write_globs=persona_allow_write_globs,
|
|
2773
|
+
non_interactive=non_interactive,
|
|
2774
|
+
shell_runner=runner,
|
|
2775
|
+
terminal_manager=terminal_manager,
|
|
2776
|
+
durable_service_manager=durable_service_manager,
|
|
2777
|
+
persistent_service_registry=persistent_service_registry,
|
|
2778
|
+
edit_discipline=edit_discipline_state,
|
|
2779
|
+
managed_browser_service=managed_browser_service,
|
|
2780
|
+
managed_browser_owner_id=managed_browser_owner_id,
|
|
2781
|
+
managed_browser_cancel_check=managed_browser_cancel_check,
|
|
2782
|
+
verification_enabled=verification_enabled,
|
|
2783
|
+
authoritative_verification_commands=authoritative_verify_commands,
|
|
2784
|
+
effective_verification_commands=effective_verification_commands,
|
|
2785
|
+
verify_command_selection=prompt_context.effective_verification_selection,
|
|
2786
|
+
get_verify_command_selection=_get_verify_command_selection,
|
|
2787
|
+
one_shot_execution=effective_one_shot_execution,
|
|
2788
|
+
completion_gate_tools_enabled=completion_gate_tools_enabled,
|
|
2789
|
+
skills_enabled=resolved_skills_enabled,
|
|
2790
|
+
skill_registry=discovered_skills.skills,
|
|
2791
|
+
subagents_enabled=resolved_subagents_enabled,
|
|
2792
|
+
helper_subagents_enabled=helper_subagents_enabled,
|
|
2793
|
+
subagent_depth=subagent_depth,
|
|
2794
|
+
subagent_registry=resolved_subagent_registry,
|
|
2795
|
+
session_log_dir_override=session_log_dir_override,
|
|
2796
|
+
step_budget_runtime=step_budget_runtime,
|
|
2797
|
+
emit_web_search_runtime_diagnostics=(subagent_depth == 0),
|
|
2798
|
+
runtime_kind=resolved_runtime_kind,
|
|
2799
|
+
mcp_manager=mcp_manager,
|
|
2800
|
+
custom_tool_session_state=custom_tool_session_state,
|
|
2801
|
+
get_active_workdir_relpath=_get_active_workdir_relpath,
|
|
2802
|
+
set_active_workdir_callback=_set_active_workdir,
|
|
2803
|
+
create_session_factory=create_session,
|
|
2804
|
+
prompt_cache_parent_session_id=(prompt_cache_parent_session_id or session_id),
|
|
2805
|
+
execution_deadline=execution_deadline,
|
|
2806
|
+
crash_diagnostic_log_path=resolved_crash_diagnostic_log_path,
|
|
2807
|
+
crash_diagnostics=crash_diagnostics,
|
|
2808
|
+
tool_dispatch_guard=tool_dispatch_guard,
|
|
2809
|
+
host_action_handler=host_action_handler,
|
|
2810
|
+
host_action_capabilities=host_action_capabilities,
|
|
2811
|
+
child_scheduler_sink=_capture_child_scheduler,
|
|
2812
|
+
parent_steer_inbox=parent_steer_inbox,
|
|
2813
|
+
read_ledger_sink=_capture_read_ledger,
|
|
2814
|
+
readonly_child_web_tool_names=readonly_child_web_tool_names,
|
|
2815
|
+
child_managed_browser_tool_names=child_managed_browser_tool_names,
|
|
2816
|
+
)
|
|
2817
|
+
if mcp_manager is not None and mcp_manager.resolved_config.has_any_config:
|
|
2818
|
+
store.append("mcp_catalog_snapshot", mcp_manager.catalog_snapshot_metadata())
|
|
2819
|
+
tool_list = [t.as_openai_tool() for t in tools.values()]
|
|
2820
|
+
messages: list[dict[str, Any]] = list(prompt_context.messages)
|
|
2821
|
+
if active_workdir_relpath_override is not None:
|
|
2822
|
+
binding_context = _workspace_binding_context_message(
|
|
2823
|
+
workspace_root=workspace_context.workspace_root,
|
|
2824
|
+
focus_dir=workspace_context.focus_path,
|
|
2825
|
+
focus_relpath=workspace_context.focus_relpath,
|
|
2826
|
+
workspace_kind=workspace_context.workspace_kind,
|
|
2827
|
+
active_workdir=initial_active_workdir,
|
|
2828
|
+
active_workdir_relpath=initial_active_workdir_relpath,
|
|
2829
|
+
binding_requested_path=binding_requested_path,
|
|
2830
|
+
binding_source=binding_source,
|
|
2831
|
+
binding_risk_level=binding_risk_level,
|
|
2832
|
+
binding_created_path=binding_created_path,
|
|
2833
|
+
)
|
|
2834
|
+
for index, message in enumerate(messages):
|
|
2835
|
+
if str(message.get("role") or "") != "user":
|
|
2836
|
+
continue
|
|
2837
|
+
if (
|
|
2838
|
+
not str(message.get("content") or "")
|
|
2839
|
+
.lstrip()
|
|
2840
|
+
.startswith("<workspace_binding_context>")
|
|
2841
|
+
):
|
|
2842
|
+
continue
|
|
2843
|
+
messages[index] = {**message, "content": binding_context}
|
|
2844
|
+
break
|
|
2845
|
+
pinned_prefix_len = prompt_context.pinned_prefix_len
|
|
2846
|
+
if resolved_subagents_enabled and subagent_depth == 0:
|
|
2847
|
+
effective_subagent_context = _subagent_context_message(
|
|
2848
|
+
subagent_registry=resolved_subagent_registry,
|
|
2849
|
+
unavailable_subagents=unavailable_builtin_subagents(
|
|
2850
|
+
registry=resolved_subagent_registry,
|
|
2851
|
+
cfg=session_cfg,
|
|
2852
|
+
available_tool_names=set(tools),
|
|
2853
|
+
),
|
|
2854
|
+
max_background_children=(
|
|
2855
|
+
session_cfg.subagent_orchestration.max_background_children
|
|
2856
|
+
),
|
|
2857
|
+
)
|
|
2858
|
+
replaced_subagent_context = False
|
|
2859
|
+
for message_index, message in enumerate(messages):
|
|
2860
|
+
if str(message.get("role") or "") == "user" and "<subagent_context>" in str(
|
|
2861
|
+
message.get("content") or ""
|
|
2862
|
+
):
|
|
2863
|
+
if effective_subagent_context is not None:
|
|
2864
|
+
messages[message_index] = {
|
|
2865
|
+
**message,
|
|
2866
|
+
"content": effective_subagent_context,
|
|
2867
|
+
}
|
|
2868
|
+
replaced_subagent_context = True
|
|
2869
|
+
break
|
|
2870
|
+
if effective_subagent_context is not None and not replaced_subagent_context:
|
|
2871
|
+
insert_at = next(
|
|
2872
|
+
(
|
|
2873
|
+
index
|
|
2874
|
+
for index, message in enumerate(messages)
|
|
2875
|
+
if "<environment_context>" in str(message.get("content") or "")
|
|
2876
|
+
),
|
|
2877
|
+
len(messages),
|
|
2878
|
+
)
|
|
2879
|
+
messages.insert(
|
|
2880
|
+
insert_at,
|
|
2881
|
+
{"role": "user", "content": effective_subagent_context},
|
|
2882
|
+
)
|
|
2883
|
+
pinned_prefix_len += 1
|
|
2884
|
+
hooks_config = load_resolved_hooks_config(workspace_context.workspace_root)
|
|
2885
|
+
hooks_config, hook_dropped_counts = _filter_hooks_config_for_plugins(
|
|
2886
|
+
config=hooks_config,
|
|
2887
|
+
activation_decision=activation_decision,
|
|
2888
|
+
index=plugin_activation_index,
|
|
2889
|
+
)
|
|
2890
|
+
plugin_activation_dropped_counts.update(hook_dropped_counts)
|
|
2891
|
+
dropped_counts_payload = _merge_dropped_counts(plugin_activation_dropped_counts)
|
|
2892
|
+
if dropped_counts_payload:
|
|
2893
|
+
store.append(
|
|
2894
|
+
"plugin_activation_filter",
|
|
2895
|
+
{
|
|
2896
|
+
"enabled_plugin_ids": sorted(activation_decision.enabled_plugin_ids),
|
|
2897
|
+
"dropped_component_counts": dropped_counts_payload,
|
|
2898
|
+
},
|
|
2899
|
+
)
|
|
2900
|
+
hook_dispatcher: HookDispatcher | None = None
|
|
2901
|
+
if hooks_config.untrusted_project_paths:
|
|
2902
|
+
untrusted_paths = [os.fspath(path) for path in hooks_config.untrusted_project_paths]
|
|
2903
|
+
store.append("hook_config_untrusted", {"paths": untrusted_paths})
|
|
2904
|
+
for path_text in untrusted_paths:
|
|
2905
|
+
_emit_hook_warning(
|
|
2906
|
+
"Ignoring untrusted project hooks config: "
|
|
2907
|
+
f"{path_text}. Run `alysis hooks trust --path "
|
|
2908
|
+
f"{os.fspath(workspace_context.workspace_root)}` to allow it."
|
|
2909
|
+
)
|
|
2910
|
+
if hooks_config.has_any_hooks:
|
|
2911
|
+
hook_audit_artifact = (
|
|
2912
|
+
store.runtime_artifact_path(*HOOK_AUDIT_ARTIFACT_PARTS) if store.enabled else None
|
|
2913
|
+
)
|
|
2914
|
+
hook_dispatcher = HookDispatcher(
|
|
2915
|
+
config=hooks_config,
|
|
2916
|
+
workspace_root=workspace_context.workspace_root,
|
|
2917
|
+
repo_root=root,
|
|
2918
|
+
session_id=session_id,
|
|
2919
|
+
mode=mode,
|
|
2920
|
+
runtime_kind=resolved_runtime_kind.value,
|
|
2921
|
+
warning_callback=_emit_hook_warning,
|
|
2922
|
+
log_callback=store.append,
|
|
2923
|
+
audit_callback=(
|
|
2924
|
+
(
|
|
2925
|
+
lambda payload: store.append_artifact_jsonl(
|
|
2926
|
+
*HOOK_AUDIT_ARTIFACT_PARTS, payload=payload
|
|
2927
|
+
)
|
|
2928
|
+
)
|
|
2929
|
+
if store.enabled
|
|
2930
|
+
else None
|
|
2931
|
+
),
|
|
2932
|
+
)
|
|
2933
|
+
store.append(
|
|
2934
|
+
"hook_config_loaded",
|
|
2935
|
+
{
|
|
2936
|
+
"loaded_paths": [os.fspath(path) for path in hooks_config.loaded_paths],
|
|
2937
|
+
"events": {
|
|
2938
|
+
event_name: len(groups)
|
|
2939
|
+
for event_name, groups in hooks_config.groups_by_event.items()
|
|
2940
|
+
},
|
|
2941
|
+
"hook_audit_artifact": os.fspath(hook_audit_artifact)
|
|
2942
|
+
if hook_audit_artifact is not None
|
|
2943
|
+
else None,
|
|
2944
|
+
},
|
|
2945
|
+
)
|
|
2946
|
+
try:
|
|
2947
|
+
session_start_hook_result = hook_dispatcher.fire_session_start(
|
|
2948
|
+
cwd=initial_active_workdir,
|
|
2949
|
+
active_workdir_relpath=initial_active_workdir_relpath,
|
|
2950
|
+
session_source=normalized_session_source,
|
|
2951
|
+
payload={
|
|
2952
|
+
"session_source": normalized_session_source,
|
|
2953
|
+
"session_source_metadata": copy.deepcopy(
|
|
2954
|
+
normalized_session_source_metadata
|
|
2955
|
+
),
|
|
2956
|
+
"mode": mode,
|
|
2957
|
+
"runtime_kind": resolved_runtime_kind.value,
|
|
2958
|
+
"workspace_root": os.fspath(workspace_context.workspace_root),
|
|
2959
|
+
"focus_dir": os.fspath(workspace_context.focus_path),
|
|
2960
|
+
"focus_relpath": workspace_context.focus_relpath,
|
|
2961
|
+
"active_workdir": os.fspath(initial_active_workdir),
|
|
2962
|
+
"active_workdir_relpath": initial_active_workdir_relpath,
|
|
2963
|
+
"workspace_kind": workspace_context.workspace_kind,
|
|
2964
|
+
"current_branch": workspace_context.current_branch,
|
|
2965
|
+
"max_steps": max_steps,
|
|
2966
|
+
"non_interactive": non_interactive,
|
|
2967
|
+
"one_shot_execution": effective_one_shot_execution,
|
|
2968
|
+
},
|
|
2969
|
+
)
|
|
2970
|
+
except Exception as exc: # noqa: BLE001
|
|
2971
|
+
_emit_hook_warning(f"Lifecycle hook dispatch failed: {exc}")
|
|
2972
|
+
session_start_hook_result = HookDispatchResult()
|
|
2973
|
+
for notice in session_start_hook_result.system_notices:
|
|
2974
|
+
_emit_hook_warning(notice)
|
|
2975
|
+
if session_start_hook_result.blocked:
|
|
2976
|
+
blocked_reason = session_start_hook_result.reason or "session start blocked by hook"
|
|
2977
|
+
raise ConfigError(f"Session blocked by hook: {blocked_reason}")
|
|
2978
|
+
for hook_message in session_start_hook_result.additional_system_messages:
|
|
2979
|
+
messages.append({"role": "system", "content": hook_message})
|
|
2980
|
+
pinned_prefix_len += 1
|
|
2981
|
+
store.append(
|
|
2982
|
+
"hook_message_added",
|
|
2983
|
+
{
|
|
2984
|
+
"event_name": "SessionStart",
|
|
2985
|
+
"role": "system",
|
|
2986
|
+
"chars": len(hook_message),
|
|
2987
|
+
"pinned": True,
|
|
2988
|
+
},
|
|
2989
|
+
)
|
|
2990
|
+
for hook_message in session_start_hook_result.additional_user_messages:
|
|
2991
|
+
messages.append({"role": "user", "content": hook_message})
|
|
2992
|
+
pinned_prefix_len += 1
|
|
2993
|
+
store.append(
|
|
2994
|
+
"hook_message_added",
|
|
2995
|
+
{
|
|
2996
|
+
"event_name": "SessionStart",
|
|
2997
|
+
"role": "user",
|
|
2998
|
+
"chars": len(hook_message),
|
|
2999
|
+
"pinned": True,
|
|
3000
|
+
},
|
|
3001
|
+
)
|
|
3002
|
+
|
|
3003
|
+
startup_messages = copy.deepcopy(messages)
|
|
3004
|
+
|
|
3005
|
+
conversation_compactor: ConversationCompactor | None = None
|
|
3006
|
+
if conversation_summarization_enabled and compactor_model_name:
|
|
3007
|
+
compactor_client = _make_session_llm_client(
|
|
3008
|
+
cfg=session_cfg,
|
|
3009
|
+
api_key=api_key,
|
|
3010
|
+
model=compactor_model_name,
|
|
3011
|
+
timeout_s=llm_timeout_s,
|
|
3012
|
+
temperature=compactor_temperature,
|
|
3013
|
+
prompt_cache_key=prompt_cache_stream_key,
|
|
3014
|
+
prompt_cache_retention=resolve_prompt_cache_retention(session_cfg),
|
|
3015
|
+
prompt_cache_namespace=_prompt_cache_namespace(ROLE_COMPACTOR),
|
|
3016
|
+
enable_thinking=llm_enable_thinking,
|
|
3017
|
+
reasoning_effort=llm_reasoning_effort,
|
|
3018
|
+
session_id=session_id,
|
|
3019
|
+
)
|
|
3020
|
+
conversation_compactor = ConversationCompactor(
|
|
3021
|
+
root=root,
|
|
3022
|
+
artifact_layout=store.session_artifact_layout,
|
|
3023
|
+
store=store,
|
|
3024
|
+
settings=compaction_settings,
|
|
3025
|
+
compactor_client=compactor_client,
|
|
3026
|
+
model_registry=registry,
|
|
3027
|
+
usage_summary=usage_summary,
|
|
3028
|
+
usage_role=usage_role,
|
|
3029
|
+
pinned_prefix_len=pinned_prefix_len,
|
|
3030
|
+
profile=("execution" if compaction_profile == "execution" else "chat"),
|
|
3031
|
+
input_token_counter=(
|
|
3032
|
+
lambda count_messages, count_tools: count_input_tokens_if_supported(
|
|
3033
|
+
client=client,
|
|
3034
|
+
messages=count_messages,
|
|
3035
|
+
tools=effective_tools_for_client(client, count_tools),
|
|
3036
|
+
)
|
|
3037
|
+
),
|
|
3038
|
+
calibration_filters=usage_context_from_client_response(
|
|
3039
|
+
client=client,
|
|
3040
|
+
response=None,
|
|
3041
|
+
operation="main_llm",
|
|
3042
|
+
),
|
|
3043
|
+
)
|
|
3044
|
+
|
|
3045
|
+
if _surface_needs_startup_git_status(surface):
|
|
3046
|
+
git_branch = _patchable("_git_branch", _git_branch)
|
|
3047
|
+
git_is_dirty = _patchable("_git_is_dirty", _git_is_dirty)
|
|
3048
|
+
startup_branch = git_branch(root)
|
|
3049
|
+
startup_dirty = git_is_dirty(root)
|
|
3050
|
+
else:
|
|
3051
|
+
startup_branch = "-"
|
|
3052
|
+
startup_dirty = False
|
|
3053
|
+
|
|
3054
|
+
startup_context_baseline_tokens = estimate_request_token_breakdown(
|
|
3055
|
+
messages=messages,
|
|
3056
|
+
tool_list=effective_tools_for_client(client, tool_list),
|
|
3057
|
+
pinned_prefix_len=pinned_prefix_len,
|
|
3058
|
+
).total_tokens
|
|
3059
|
+
|
|
3060
|
+
surface.on_status_update(
|
|
3061
|
+
StatusEvent(
|
|
3062
|
+
mode=mode,
|
|
3063
|
+
model=session_cfg.model,
|
|
3064
|
+
workspace=os.fspath(root),
|
|
3065
|
+
session_id=session_id,
|
|
3066
|
+
branch=startup_branch,
|
|
3067
|
+
dirty=startup_dirty,
|
|
3068
|
+
stream=session_cfg.stream,
|
|
3069
|
+
task="-",
|
|
3070
|
+
)
|
|
3071
|
+
)
|
|
3072
|
+
|
|
3073
|
+
session = AgentSession(
|
|
3074
|
+
cfg=session_cfg,
|
|
3075
|
+
root=root,
|
|
3076
|
+
mode=mode,
|
|
3077
|
+
persona=(
|
|
3078
|
+
normalize_persona(getattr(session_cfg, "default_persona", "code"), persona_registry)
|
|
3079
|
+
if persona_modes_enabled(session_cfg)
|
|
3080
|
+
else "code"
|
|
3081
|
+
),
|
|
3082
|
+
persona_switch_state=persona_switch_state,
|
|
3083
|
+
persona_registry=persona_registry,
|
|
3084
|
+
persona_registry_warnings=persona_registry_warnings,
|
|
3085
|
+
yes=yes,
|
|
3086
|
+
stream=session_cfg.stream,
|
|
3087
|
+
routing_mode=routing_mode,
|
|
3088
|
+
max_steps=max_steps,
|
|
3089
|
+
api_key=api_key,
|
|
3090
|
+
api_key_source=api_key_source,
|
|
3091
|
+
no_log=no_log,
|
|
3092
|
+
non_interactive=non_interactive,
|
|
3093
|
+
one_shot_execution=effective_one_shot_execution,
|
|
3094
|
+
enable_chat_turn_step_budget=enable_chat_turn_step_budget,
|
|
3095
|
+
chat_turn_fixed_override=chat_turn_fixed_override,
|
|
3096
|
+
verification_enabled=verification_enabled,
|
|
3097
|
+
effective_verification_commands=list(effective_verification_commands),
|
|
3098
|
+
authoritative_verification_commands=(
|
|
3099
|
+
list(authoritative_verify_commands)
|
|
3100
|
+
if authoritative_verify_commands is not None
|
|
3101
|
+
else None
|
|
3102
|
+
),
|
|
3103
|
+
verification_selection_source=str(
|
|
3104
|
+
verification_selection_metadata.get("verification_selection_source") or ""
|
|
3105
|
+
),
|
|
3106
|
+
verification_selection_reason=str(
|
|
3107
|
+
verification_selection_metadata.get("verification_selection_reason") or ""
|
|
3108
|
+
),
|
|
3109
|
+
verification_contract_type=str(
|
|
3110
|
+
verification_selection_metadata.get("verification_contract_type") or ""
|
|
3111
|
+
),
|
|
3112
|
+
verification_authoritative=bool(
|
|
3113
|
+
verification_selection_metadata.get("verification_authoritative", False)
|
|
3114
|
+
),
|
|
3115
|
+
verification_best_effort=bool(
|
|
3116
|
+
verification_selection_metadata.get("verification_best_effort", False)
|
|
3117
|
+
),
|
|
3118
|
+
deny_write_prefixes=(
|
|
3119
|
+
list(deny_write_prefixes) if deny_write_prefixes is not None else None
|
|
3120
|
+
),
|
|
3121
|
+
allow_write_globs=(list(allow_write_globs) if allow_write_globs is not None else None),
|
|
3122
|
+
persona_allow_write_globs=(
|
|
3123
|
+
list(persona_allow_write_globs) if persona_allow_write_globs is not None else None
|
|
3124
|
+
),
|
|
3125
|
+
session_log_dir_override=session_log_dir_override,
|
|
3126
|
+
skills_enabled=resolved_skills_enabled,
|
|
3127
|
+
skills_auto_invoke=skills_auto_invoke,
|
|
3128
|
+
skill_registry=dict(discovered_skills.skills),
|
|
3129
|
+
skills_ordered=tuple(discovered_skills.ordered),
|
|
3130
|
+
skill_discovery_issues=tuple(discovered_skills.issues),
|
|
3131
|
+
skill_catalog_entries=tuple(prompt_context.skill_catalog_entries),
|
|
3132
|
+
repo_conventions=tuple(repo_conventions),
|
|
3133
|
+
console=console,
|
|
3134
|
+
surface=surface,
|
|
3135
|
+
store=store,
|
|
3136
|
+
client=client,
|
|
3137
|
+
persona_client_cache={(session_cfg.model, coding_temperature): client},
|
|
3138
|
+
persona_client_key=(session_cfg.model, coding_temperature),
|
|
3139
|
+
model_registry=registry,
|
|
3140
|
+
usage_summary=usage_summary,
|
|
3141
|
+
usage_role=usage_role,
|
|
3142
|
+
tool_output_offloader=tool_output_offloader,
|
|
3143
|
+
conversation_compactor=conversation_compactor,
|
|
3144
|
+
tool_output_offload_enabled=tool_output_offload_enabled,
|
|
3145
|
+
conversation_summarization_enabled=conversation_summarization_enabled,
|
|
3146
|
+
compaction_profile=compaction_profile,
|
|
3147
|
+
tools=tools,
|
|
3148
|
+
tool_list=tool_list,
|
|
3149
|
+
messages=messages,
|
|
3150
|
+
startup_messages=startup_messages,
|
|
3151
|
+
runtime_kind=resolved_runtime_kind,
|
|
3152
|
+
prompt_cache_stream_key=prompt_cache_stream_key,
|
|
3153
|
+
mcp_manager=mcp_manager,
|
|
3154
|
+
terminal_manager=terminal_manager,
|
|
3155
|
+
durable_service_manager=durable_service_manager,
|
|
3156
|
+
persistent_service_registry=persistent_service_registry,
|
|
3157
|
+
edit_discipline=edit_discipline_state,
|
|
3158
|
+
subagents_enabled=resolved_subagents_enabled,
|
|
3159
|
+
enforce_explicit_subagent_requests=bool(enforce_explicit_subagent_requests),
|
|
3160
|
+
subagent_depth=subagent_depth,
|
|
3161
|
+
subagent_registry=resolved_subagent_registry,
|
|
3162
|
+
child_scheduler=child_scheduler_holder.get("scheduler"),
|
|
3163
|
+
steer_inbox=parent_steer_inbox,
|
|
3164
|
+
read_ledger=read_ledger_holder.get("ledger"),
|
|
3165
|
+
shell_runner=runner,
|
|
3166
|
+
step_budget_runtime=step_budget_runtime,
|
|
3167
|
+
planner_workspace_context=prompt_context.planner_workspace_context,
|
|
3168
|
+
workspace_grounding=prompt_context.workspace_grounding,
|
|
3169
|
+
focus_dir=workspace_context.focus_path,
|
|
3170
|
+
focus_relpath=workspace_context.focus_relpath,
|
|
3171
|
+
workspace_kind=workspace_context.workspace_kind,
|
|
3172
|
+
binding_requested_path=binding_requested_path,
|
|
3173
|
+
binding_source=binding_source,
|
|
3174
|
+
binding_risk_level=binding_risk_level,
|
|
3175
|
+
binding_created_path=binding_created_path,
|
|
3176
|
+
active_workdir_relpath=initial_active_workdir_relpath,
|
|
3177
|
+
session_source=normalized_session_source,
|
|
3178
|
+
session_source_metadata=copy.deepcopy(normalized_session_source_metadata),
|
|
3179
|
+
pinned_prefix_len=pinned_prefix_len,
|
|
3180
|
+
startup_context_baseline_tokens=startup_context_baseline_tokens,
|
|
3181
|
+
custom_tool_session_state=custom_tool_session_state,
|
|
3182
|
+
hook_dispatcher=hook_dispatcher,
|
|
3183
|
+
execution_deadline=execution_deadline,
|
|
3184
|
+
crash_diagnostics=crash_diagnostics,
|
|
3185
|
+
crash_diagnostic_log_path=resolved_crash_diagnostic_log_path,
|
|
3186
|
+
agentbox_telemetry=AgentBoxTelemetry.from_env(
|
|
3187
|
+
root=root,
|
|
3188
|
+
runtime_version=f"alysis-{__version__}",
|
|
3189
|
+
),
|
|
3190
|
+
process_group_registry=process_group_registry,
|
|
3191
|
+
)
|
|
3192
|
+
if subagent_depth == 0 and store.enabled:
|
|
3193
|
+
# Persist the process's provider/web-search telemetry to the run's artifact
|
|
3194
|
+
# dir so retry/throttle/latency history survives exit. Only the top-level
|
|
3195
|
+
# session registers the sink; nested subagent/candidate calls in the same
|
|
3196
|
+
# process flow into it. Released on close().
|
|
3197
|
+
set_provider_telemetry_sink(
|
|
3198
|
+
store.runtime_artifact_path("diagnostics", "provider_telemetry.jsonl")
|
|
3199
|
+
)
|
|
3200
|
+
active_workdir_state["session"] = session
|
|
3201
|
+
return session
|
|
3202
|
+
except Exception:
|
|
3203
|
+
try:
|
|
3204
|
+
terminal_manager.shutdown_all()
|
|
3205
|
+
except Exception as exc: # noqa: BLE001
|
|
3206
|
+
# Startup failure cleanup is best-effort; still close MCP/store below.
|
|
3207
|
+
store.append(
|
|
3208
|
+
"warning",
|
|
3209
|
+
{
|
|
3210
|
+
"warning": "terminal_shutdown_failed",
|
|
3211
|
+
"message": f"Terminal manager shutdown failed: {exc}",
|
|
3212
|
+
},
|
|
3213
|
+
)
|
|
3214
|
+
try:
|
|
3215
|
+
mcp_manager.close()
|
|
3216
|
+
finally:
|
|
3217
|
+
store.close()
|
|
3218
|
+
raise
|