From d76cce4f1161bdac895f7e373ce41cbd04e7c2f2 Mon Sep 17 00:00:00 2001 From: Maciej Lisiewski Date: Thu, 8 Oct 2026 17:22:54 +0200 Subject: [PATCH] ci: apply all automatic linter fixes available --- acp_adapter/entry.py | 6 +- acp_adapter/events.py | 20 +- acp_adapter/model_catalog.py | 2 +- acp_adapter/provenance.py | 8 +- acp_adapter/server.py | 6 +- acp_adapter/session.py | 18 +- acp_adapter/tools.py | 28 +- agent/__init__.py | 2 +- agent/agent_init.py | 68 +-- agent/agent_init_fallback.py | 2 +- agent/agent_runtime_helpers.py | 162 +++---- agent/anthropic_adapter.py | 28 +- agent/anthropic_credentials.py | 46 +- agent/anthropic_message_convert.py | 98 ++--- agent/api_error_summary.py | 2 +- agent/api_request_hooks.py | 10 +- agent/auxiliary_async_rebuild.py | 2 +- agent/auxiliary_client.py | 405 +++++++++--------- agent/auxiliary_client_registry.py | 2 +- agent/auxiliary_codex_response.py | 2 +- agent/auxiliary_fallback_recovery.py | 6 +- agent/auxiliary_hooks.py | 20 +- agent/auxiliary_local_runtime.py | 2 +- agent/auxiliary_reasoning_floor.py | 6 +- agent/auxiliary_structured_output.py | 8 +- agent/auxiliary_task_config.py | 4 +- agent/azure_identity_adapter.py | 20 +- agent/background_review.py | 88 ++-- agent/battery.py | 2 +- agent/bedrock_adapter.py | 136 +++--- agent/billing_view.py | 6 +- agent/bounded_response.py | 6 +- agent/browser_provider.py | 2 +- agent/chat_completion_helpers.py | 8 +- agent/client_lifecycle.py | 2 +- agent/codex_headers.py | 8 +- agent/codex_responses_adapter.py | 112 ++--- agent/codex_runtime.py | 24 +- agent/codex_runtime_history_seed.py | 4 +- agent/compaction_display.py | 2 +- agent/context_breakdown.py | 42 +- agent/context_compressor.py | 280 ++++++------ agent/context_compressor_prellm.py | 4 +- agent/context_compressor_summary.py | 6 +- agent/context_engine.py | 32 +- agent/context_file_sources.py | 12 +- agent/context_references.py | 2 +- agent/conversation_compression.py | 51 ++- agent/conversation_compression_archive.py | 16 +- agent/conversation_compression_manual.py | 14 +- agent/conversation_loop.py | 76 ++-- agent/credential_persistence.py | 2 +- agent/credential_pool.py | 145 ++++--- agent/credential_pool_admin.py | 2 +- agent/credential_pool_model_cooldowns.py | 10 +- agent/credential_pool_plugin.py | 2 +- agent/credential_sources.py | 6 +- agent/curator.py | 108 ++--- agent/curator_backup.py | 50 +-- agent/display.py | 4 +- agent/empty_response_guard.py | 14 +- agent/error_classifier.py | 14 +- agent/file_safety.py | 2 +- agent/gemini_native_adapter.py | 108 ++--- agent/gemini_schema.py | 18 +- agent/image_gen_provider.py | 18 +- agent/image_routing.py | 48 +-- agent/image_token_cost.py | 12 +- agent/initiate_setup_facts.py | 4 +- agent/inline_tool_executors.py | 10 +- agent/insights.py | 48 +-- agent/jiter_preload.py | 2 +- agent/kanban_turn_recovery.py | 3 +- agent/learning_graph_render.py | 4 +- agent/legacy_cli.py | 8 +- agent/lmstudio_reasoning.py | 2 +- agent/lsp/__init__.py | 6 +- agent/lsp/cli.py | 2 +- agent/lsp/client.py | 50 +-- agent/lsp/eventlog.py | 6 +- agent/lsp/install.py | 22 +- agent/lsp/manager.py | 66 +-- agent/lsp/protocol.py | 2 +- agent/lsp/range_shift.py | 4 +- agent/lsp/reporter.py | 4 +- agent/lsp/servers.py | 42 +- agent/lsp/workspace.py | 6 +- agent/markdown_tables.py | 22 +- agent/memory_manager.py | 58 +-- agent/memory_provider.py | 24 +- agent/message_metadata.py | 6 +- agent/micro_compaction.py | 28 +- agent/model_metadata.py | 138 +++--- agent/models_dev.py | 110 ++--- agent/monitoring/emitter.py | 4 +- agent/monitoring/events.py | 2 +- agent/monitoring/gateway_health.py | 10 +- agent/monitoring/gateway_health_export.py | 28 +- agent/monitoring/otlp_exporter.py | 48 +-- agent/monitoring/policy.py | 2 +- agent/moonshot_schema.py | 14 +- agent/native_compaction.py | 20 +- agent/nous_wire.py | 2 +- agent/oneshot.py | 12 +- agent/oneshot_footprint.py | 2 +- agent/outbound_webhooks.py | 24 +- agent/periodic_scheduler.py | 2 +- agent/pet/generate/atlas.py | 6 +- agent/pet/generate/imagegen.py | 8 +- agent/pet/generate/orchestrate.py | 8 +- agent/pet/manifest.py | 4 +- agent/pet/render.py | 4 +- agent/pet/store.py | 10 +- agent/plugin_llm.py | 54 +-- agent/portal_tags.py | 2 +- agent/process_bootstrap.py | 2 +- agent/prompt_builder.py | 6 +- agent/prompt_caching.py | 26 +- agent/provider_base.py | 4 +- agent/provider_media.py | 4 +- agent/provider_registry.py | 20 +- agent/proxy_sources/iron_proxy.py | 82 ++-- agent/redact.py | 2 +- agent/relay_runtime.py | 4 +- agent/repetition_guard.py | 2 +- agent/replay_cleanup.py | 24 +- agent/review_engine.py | 18 +- agent/review_idle_queue.py | 20 +- agent/route_binding.py | 6 +- agent/secret_scope.py | 10 +- agent/secret_sources/__init__.py | 2 +- agent/secret_sources/_cache.py | 4 +- agent/secret_sources/base.py | 22 +- agent/secret_sources/bitwarden.py | 18 +- agent/secret_sources/command.py | 6 +- agent/secret_sources/onepassword.py | 26 +- agent/secret_sources/registry.py | 50 +-- agent/session_persistence.py | 42 +- agent/shell_hooks.py | 60 +-- agent/side_question.py | 18 +- agent/skill_bundles.py | 26 +- agent/skill_commands.py | 32 +- agent/skill_utils.py | 100 ++--- agent/ssl_verify.py | 2 +- agent/stream_delivery.py | 16 +- agent/stream_diag.py | 14 +- agent/subagent_lifecycle.py | 2 +- agent/subdirectory_hints.py | 14 +- agent/surface_switch.py | 2 +- agent/system_prompt.py | 42 +- agent/terminal_env_provider.py | 10 +- agent/terminal_env_registry.py | 2 +- agent/think_scrubber.py | 20 +- agent/tool_dispatch_helpers.py | 22 +- agent/tool_executor.py | 2 +- agent/trace_upload.py | 26 +- agent/trajectory.py | 2 +- agent/transcript_repair.py | 16 +- agent/transcription_provider.py | 4 +- agent/transports/__init__.py | 2 +- agent/transports/anthropic.py | 12 +- agent/transports/base.py | 14 +- agent/transports/bedrock.py | 12 +- agent/transports/chat_completions.py | 2 +- agent/transports/codex.py | 4 +- agent/transports/codex_app_server.py | 2 +- agent/transports/hermes_tools_mcp_server.py | 2 +- agent/tts_provider.py | 2 +- agent/turn_api_call.py | 5 +- agent/turn_api_error.py | 8 +- agent/turn_author.py | 10 +- agent/turn_context.py | 46 +- agent/turn_context_compaction.py | 12 +- agent/turn_empty_response.py | 6 +- agent/turn_explainers.py | 10 +- agent/turn_facade.py | 12 +- agent/turn_facade_lease.py | 16 +- agent/turn_failure_copy.py | 22 +- agent/turn_final_response.py | 4 +- agent/turn_finalizer.py | 12 +- agent/turn_iteration_prep.py | 4 +- agent/turn_liveness.py | 6 +- agent/turn_loop_errors.py | 4 +- agent/turn_overflow.py | 12 +- agent/turn_preflight.py | 10 +- agent/turn_recovery.py | 60 +-- agent/turn_recovery_autorecover.py | 2 +- agent/turn_response_check.py | 8 +- agent/turn_response_intake.py | 4 +- agent/turn_scripted_prelude.py | 4 +- agent/turn_stop_gates.py | 2 +- agent/turn_tool_round.py | 6 +- agent/turn_tool_validation.py | 8 +- agent/turn_truncation.py | 38 +- agent/turn_usage.py | 2 +- agent/usage_anchor.py | 16 +- agent/usage_pricing.py | 16 +- agent/vault_backends/base.py | 18 +- agent/vault_backends/bitwarden.py | 10 +- agent/vault_backends/local.py | 4 +- agent/vault_backends/onepassword.py | 14 +- agent/vault_backends/unlock.py | 8 +- agent/vault_login_classifier.py | 22 +- agent/vault_store.py | 16 +- agent/vertex_adapter.py | 12 +- agent/video_gen_provider.py | 36 +- agent/vision_message_prep.py | 6 +- agent/voice_turn_route.py | 18 +- agent/web_search_provider.py | 8 +- agent/web_search_registry.py | 8 +- apps/desktop/e2e/update/seed.py | 4 +- .../electron/fixtures/source-backend.py | 2 +- .../scripts/perf/gateway_attach_bench.py | 5 +- batch_runner.py | 66 +-- cli.py | 34 +- cron/__init__.py | 4 +- cron/blueprint_catalog.py | 32 +- cron/blueprint_plugins.py | 4 +- cron/executions.py | 30 +- cron/incidents.py | 4 +- cron/job_definition.py | 6 +- cron/jobs.py | 218 +++++----- cron/lifecycle_guard.py | 9 +- cron/notepad.py | 4 +- cron/quota_hold.py | 12 +- cron/scheduler.py | 24 +- cron/scheduler_delivery.py | 14 +- cron/scheduler_failure_copy.py | 2 +- cron/scheduler_ownership.py | 2 +- cron/scheduler_preflight.py | 4 +- cron/scheduler_prompt.py | 6 +- cron/scheduler_provider.py | 10 +- cron/scheduler_script.py | 2 +- cron/scripts/classify_items.py | 8 +- cron/suggestion_catalog.py | 12 +- cron/suggestions.py | 18 +- cron/unreachable_retry.py | 14 +- cron/worker_bootstrap.py | 2 +- evals/approval_deny_dispatch.py | 2 +- evals/browser_use/orchestrate_cloud.py | 4 +- evals/browser_use/single_run.py | 8 +- evals/compaction/fixtures.py | 12 +- evals/compaction/jev_arm.py | 58 +-- evals/compaction/policies.py | 4 +- evals/compaction/runner.py | 6 +- evals/compaction/scripts/codex_arm.py | 4 +- evals/compaction/scripts/jev_cycles.py | 4 +- evals/compaction/scripts/replay_lineage.py | 2 +- evals/compaction/test_region_scoping.py | 2 +- evals/core_tool_deferral/worker.py | 16 +- .../refresh_singleflight_live_e2e.py | 10 +- .../desktop_mcp_oauth/backend_http_fixture.py | 8 +- .../gateway_completion/delivery_lifecycle.py | 1 - evals/heartbeat_idle_wire.py | 12 +- evals/mcp_device_flow.py | 4 +- .../ab_checkpoint_preflight.py | 2 +- evals/openrouter_pkce_ab/harness.py | 4 +- evals/plugin_isolation/audit_catalog.py | 4 +- evals/postmortem/forensics/common.py | 34 +- evals/postmortem/forensics/delegation.py | 14 +- evals/postmortem/forensics/goal_loop.py | 6 +- evals/postmortem/forensics/logcalls.py | 8 +- evals/postmortem/forensics/tokens.py | 6 +- evals/postmortem/forensics/tools.py | 10 +- evals/postmortem/live_ab/auth_stampede.py | 2 +- .../credential_identity_probe.py | 2 +- evals/readtool/runner.py | 8 +- evals/session_search_schema/runner.py | 8 +- .../stress_handoff_live.py | 14 +- .../ab_image_cost_calibration.py | 2 +- evals/token_accounting/replay_gates.py | 2 +- evals/tool_search/tool_search_livetest.py | 14 +- evals/tool_search/tool_search_livetest2.py | 10 +- evals/tool_search/tool_search_livetest_ue.py | 10 +- .../tool_search_livetest_ue_disc.py | 12 +- .../tool_search_livetest_ue_hard.py | 12 +- evals/toolperf_abeval/ab_eval.py | 2 +- evals/vault_fill_live_e2e.py | 2 +- gateway/agent_cache_pressure.py | 6 +- gateway/authz_mixin.py | 6 +- gateway/bot_loop_guard.py | 6 +- gateway/browser_control_artifacts.py | 2 +- gateway/browser_control_broker.py | 8 +- gateway/channel_directory.py | 62 +-- gateway/config.py | 34 +- gateway/config_env.py | 4 +- gateway/dead_targets.py | 2 +- gateway/delivery.py | 16 +- gateway/delivery_ledger.py | 16 +- gateway/disk_status.py | 4 +- gateway/hooks.py | 22 +- gateway/host_rendezvous.py | 2 +- gateway/hosted_rooms.py | 2 +- gateway/lifecycle_ledger.py | 28 +- gateway/media_policy.py | 8 +- gateway/media_repair.py | 14 +- gateway/memory_status.py | 4 +- gateway/message_timestamps.py | 2 +- gateway/platforms/api_server.py | 214 ++++----- .../platforms/api_server_memory_sessions.py | 10 +- gateway/platforms/api_server_openai_routes.py | 80 ++-- .../platforms/api_server_request_options.py | 2 +- .../platforms/api_server_run_idempotency.py | 6 +- gateway/platforms/api_server_runs.py | 42 +- gateway/platforms/api_server_turn_boundary.py | 4 +- gateway/platforms/base.py | 172 ++++---- gateway/platforms/bluebubbles.py | 42 +- gateway/platforms/event.py | 10 +- gateway/platforms/msgraph_webhook.py | 16 +- gateway/platforms/qqbot/__init__.py | 12 +- gateway/platforms/qqbot/adapter.py | 76 ++-- gateway/platforms/qqbot/chunked_upload.py | 18 +- gateway/platforms/qqbot/keyboards.py | 6 +- gateway/platforms/qqbot/onboard.py | 4 +- gateway/platforms/qqbot/utils.py | 4 +- gateway/platforms/signal.py | 44 +- gateway/platforms/webhook.py | 22 +- gateway/platforms/webhook_coalesce.py | 8 +- gateway/platforms/weixin.py | 118 ++--- gateway/platforms/whatsapp_cloud.py | 54 +-- gateway/platforms/whatsapp_common.py | 16 +- gateway/platforms/yuanbao.py | 114 ++--- gateway/platforms/yuanbao_media.py | 2 +- gateway/profile_routing.py | 6 +- gateway/relay/__init__.py | 20 +- gateway/relay/adapter.py | 220 +++++----- gateway/relay/auth.py | 2 +- gateway/relay/command_manifest.py | 10 +- gateway/relay/egress.py | 32 +- gateway/relay/transport.py | 10 +- gateway/relay/ws_transport.py | 74 ++-- gateway/restart_loop_guard.py | 10 +- gateway/run.py | 112 ++--- gateway/run_adapters.py | 16 +- gateway/run_agent_cache.py | 16 +- gateway/run_busy.py | 44 +- gateway/run_config_loaders.py | 6 +- gateway/run_goals.py | 4 +- gateway/run_inbound.py | 62 +-- gateway/run_inbound_consumer.py | 2 +- gateway/run_notifications.py | 12 +- gateway/run_plugin_rewire.py | 2 +- gateway/run_profile_reconcile.py | 14 +- gateway/run_shutdown.py | 26 +- gateway/run_shutdown_session_end.py | 6 +- gateway/run_startup.py | 34 +- gateway/run_topics.py | 10 +- gateway/run_turn.py | 54 +-- gateway/run_turn_runner.py | 14 +- gateway/run_voice.py | 4 +- gateway/run_watchers.py | 4 +- gateway/scale_to_zero.py | 2 +- gateway/session.py | 54 +-- gateway/session_persistence.py | 10 +- gateway/session_prompt_pin.py | 6 +- gateway/session_recovery.py | 14 +- gateway/session_state.py | 28 +- gateway/session_transcript.py | 22 +- gateway/shutdown_flush.py | 18 +- gateway/shutdown_forensics.py | 20 +- gateway/shutdown_watchdog.py | 8 +- gateway/slash_access.py | 12 +- gateway/slash_commands.py | 2 +- gateway/status_inline_source.py | 12 +- gateway/stream_events.py | 4 +- gateway/streaming_tts_consumer.py | 6 +- gateway/turn_context.py | 2 +- gateway/turn_executor.py | 2 +- gateway/turn_lease.py | 6 +- gateway/whatsapp_identity.py | 4 +- hermes_cli/_early_recovery.py | 4 +- hermes_cli/_launchers.py | 6 +- hermes_cli/_scan_venv_blockers.py | 2 +- hermes_cli/agent_import.py | 66 +-- hermes_cli/agent_import_sync.py | 12 +- hermes_cli/agent_plugins.py | 28 +- hermes_cli/anon_auth.py | 64 +-- hermes_cli/anon_challenge.py | 18 +- hermes_cli/anon_sign_in.py | 8 +- hermes_cli/approvals_test.py | 2 +- hermes_cli/auth.py | 172 ++++---- hermes_cli/auth_codex.py | 56 +-- hermes_cli/auth_codex_browser.py | 6 +- hermes_cli/auth_commands.py | 2 +- hermes_cli/auth_constants.py | 4 +- hermes_cli/auth_device_flow.py | 24 +- hermes_cli/auth_error_copy.py | 2 +- hermes_cli/auth_minimax.py | 20 +- hermes_cli/auth_model_picker.py | 14 +- hermes_cli/auth_nous.py | 102 ++--- hermes_cli/auth_oauth_grants.py | 54 +-- hermes_cli/auth_oauth_pkce_plugin.py | 16 +- hermes_cli/auth_openrouter.py | 4 +- hermes_cli/auth_plugin_providers.py | 2 +- hermes_cli/auth_qwen.py | 14 +- hermes_cli/auth_spotify.py | 26 +- hermes_cli/auth_xai.py | 32 +- hermes_cli/auth_zai_kimi.py | 10 +- hermes_cli/backup.py | 66 +-- hermes_cli/backup_restore.py | 6 +- hermes_cli/banner.py | 32 +- hermes_cli/blueprint_cmd.py | 16 +- hermes_cli/boot_bootstrap.py | 2 +- hermes_cli/bundles.py | 2 +- hermes_cli/cli_chat_turn_mixin.py | 3 +- hermes_cli/cli_commands_mixin.py | 4 +- hermes_cli/cli_commands_session_tools.py | 2 +- hermes_cli/cli_config_load.py | 8 +- hermes_cli/cli_info_mixin.py | 2 +- hermes_cli/cli_init_mixin.py | 4 +- hermes_cli/cli_modal_mixin.py | 6 +- hermes_cli/cli_single_query.py | 2 +- hermes_cli/cli_status_bar_mixin.py | 4 +- hermes_cli/cli_stream_mixin.py | 2 +- hermes_cli/cli_terminal_input.py | 4 +- hermes_cli/cli_tui_mixin.py | 1 - hermes_cli/cli_tui_runtime_mixin.py | 4 +- hermes_cli/codex_models.py | 24 +- hermes_cli/codex_runtime_plugin_migration.py | 2 +- hermes_cli/commands_completion.py | 6 +- hermes_cli/config.py | 190 ++++---- hermes_cli/config_effective.py | 12 +- hermes_cli/config_migrations.py | 62 +-- hermes_cli/config_providers.py | 82 ++-- hermes_cli/config_section_guard.py | 2 +- hermes_cli/config_version_stamp.py | 4 +- hermes_cli/console_engine.py | 2 +- hermes_cli/context_switch_guard.py | 4 +- hermes_cli/copilot_auth.py | 2 +- hermes_cli/credential_lifecycle.py | 22 +- hermes_cli/cron.py | 26 +- hermes_cli/curses_ui.py | 26 +- hermes_cli/dashboard_auth/cookies.py | 2 +- hermes_cli/dashboard_auth/native_flow.py | 4 +- hermes_cli/dashboard_auth/prefix.py | 2 +- hermes_cli/dashboard_auth/registry.py | 6 +- hermes_cli/dashboard_auth/routes.py | 4 +- hermes_cli/dashboard_auth/token_auth.py | 4 +- hermes_cli/dashboard_auth/ws_tickets.py | 8 +- hermes_cli/dingtalk_auth.py | 6 +- hermes_cli/doctor_config.py | 2 +- hermes_cli/doctor_connectivity.py | 2 +- hermes_cli/doctor_live.py | 10 +- hermes_cli/doctor_platform.py | 2 +- hermes_cli/doctor_state.py | 2 +- hermes_cli/doctor_tools.py | 4 +- hermes_cli/env_loader.py | 24 +- hermes_cli/fallback_cmd.py | 20 +- hermes_cli/foreign_sessions.py | 30 +- hermes_cli/free_tier_bootstrap.py | 10 +- hermes_cli/free_tier_offer.py | 8 +- hermes_cli/gateway.py | 26 +- hermes_cli/gateway_migrate.py | 2 +- hermes_cli/gateway_windows_legacy.py | 2 +- hermes_cli/git_pack_tidy.py | 14 +- hermes_cli/gitlock.py | 14 +- hermes_cli/goals.py | 64 +-- hermes_cli/gui_uninstall.py | 2 +- hermes_cli/hooks.py | 4 +- hermes_cli/journey.py | 2 +- hermes_cli/kanban.py | 2 +- hermes_cli/kanban_db.py | 10 +- hermes_cli/kanban_db_boards.py | 2 +- hermes_cli/kanban_db_connect.py | 6 +- hermes_cli/kanban_db_dispatch.py | 8 +- hermes_cli/kanban_db_notify.py | 2 +- hermes_cli/kanban_db_workspace.py | 2 +- hermes_cli/lifecycle.py | 12 +- hermes_cli/local_runtime/binaries.py | 2 +- hermes_cli/local_runtime/bootstrap.py | 16 +- hermes_cli/local_runtime/catalog.py | 2 +- hermes_cli/local_runtime/endpoint.py | 2 +- hermes_cli/local_runtime/gguf.py | 2 +- hermes_cli/local_runtime/growth.py | 2 +- hermes_cli/local_runtime/load_progress.py | 4 +- hermes_cli/local_runtime/presets.py | 2 +- hermes_cli/local_runtime/supervisor.py | 14 +- hermes_cli/loops.py | 24 +- hermes_cli/main.py | 18 +- hermes_cli/main_desktop.py | 16 +- hermes_cli/main_install_repair.py | 2 +- hermes_cli/managed_scope.py | 16 +- hermes_cli/mcp_catalog.py | 56 +-- hermes_cli/mcp_config.py | 46 +- hermes_cli/mcp_picker.py | 6 +- hermes_cli/mcp_startup.py | 4 +- hermes_cli/middleware.py | 18 +- hermes_cli/model_cost_guard.py | 4 +- hermes_cli/model_data_policy_guard.py | 2 +- hermes_cli/model_selection_guards.py | 6 +- hermes_cli/model_switch_providers.py | 8 +- hermes_cli/nous_subscription.py | 46 +- hermes_cli/oneshot.py | 2 +- hermes_cli/partial_compress.py | 20 +- hermes_cli/personality.py | 14 +- hermes_cli/pets.py | 2 +- hermes_cli/platform_actions.py | 16 +- hermes_cli/plugin_capabilities.py | 12 +- hermes_cli/plugin_catalog.py | 48 +-- hermes_cli/plugin_catalog_presence.py | 8 +- hermes_cli/plugin_compat.py | 4 +- hermes_cli/plugin_host.py | 44 +- hermes_cli/plugin_host_child.py | 85 ++-- hermes_cli/plugin_host_profiles.py | 10 +- hermes_cli/plugin_host_wire.py | 20 +- hermes_cli/plugin_isolation.py | 12 +- hermes_cli/plugin_isolation_audit.py | 30 +- hermes_cli/plugin_packs.py | 24 +- hermes_cli/plugin_validate.py | 26 +- hermes_cli/plugin_validate_core_override.py | 42 +- hermes_cli/plugin_validate_desktop.py | 14 +- hermes_cli/plugin_validate_locales.py | 4 +- hermes_cli/plugins.py | 188 ++++---- hermes_cli/plugins_activation.py | 28 +- hermes_cli/plugins_activation_live.py | 10 +- hermes_cli/plugins_aux_tasks.py | 4 +- hermes_cli/plugins_cmd.py | 18 +- hermes_cli/plugins_cmd_catalog.py | 32 +- hermes_cli/plugins_discovery.py | 22 +- hermes_cli/plugins_dispatch.py | 42 +- hermes_cli/plugins_ledger.py | 16 +- hermes_cli/plugins_loader.py | 16 +- hermes_cli/plugins_manifest.py | 56 +-- hermes_cli/plugins_settings.py | 14 +- hermes_cli/plugins_state.py | 2 +- hermes_cli/post_update.py | 2 +- hermes_cli/process_identity.py | 8 +- hermes_cli/profile_channels.py | 70 +-- hermes_cli/profile_distribution.py | 38 +- hermes_cli/profiles.py | 52 +-- hermes_cli/projects_db.py | 6 +- hermes_cli/prompt_size.py | 22 +- hermes_cli/prompt_stash.py | 14 +- hermes_cli/providers.py | 24 +- hermes_cli/proxy/adapters/__init__.py | 2 +- hermes_cli/proxy/adapters/base.py | 2 +- hermes_cli/proxy/adapters/nous_portal.py | 8 +- hermes_cli/proxy/adapters/xai.py | 4 +- hermes_cli/proxy_cli.py | 20 +- hermes_cli/pty_session.py | 4 +- hermes_cli/runtime_paths.py | 2 +- hermes_cli/runtime_provider.py | 86 ++-- hermes_cli/runtime_provider_backends.py | 12 +- hermes_cli/runtime_provider_custom.py | 48 +-- hermes_cli/secrets_cli.py | 10 +- hermes_cli/session_export.py | 34 +- hermes_cli/session_export_html.py | 12 +- hermes_cli/session_filters.py | 6 +- hermes_cli/session_lost_and_found.py | 2 +- hermes_cli/session_recap.py | 18 +- hermes_cli/session_reset_retirement.py | 2 +- hermes_cli/sessions_cmd.py | 6 +- hermes_cli/sessions_cmd_repair_profiles.py | 6 +- hermes_cli/sessions_repair_profiles.py | 62 +-- hermes_cli/setup.py | 12 +- hermes_cli/setup_summary.py | 2 +- hermes_cli/setup_terminal.py | 2 +- hermes_cli/setup_tts.py | 2 +- hermes_cli/skills_config.py | 12 +- hermes_cli/skills_hub.py | 14 +- hermes_cli/skin_engine.py | 38 +- hermes_cli/source_completion.py | 2 +- hermes_cli/status_auth.py | 4 +- hermes_cli/steward.py | 2 +- hermes_cli/subcommands/codex_runtime.py | 2 +- hermes_cli/subcommands/computer_use.py | 2 +- hermes_cli/subcommands/egress.py | 2 +- hermes_cli/subcommands/secrets.py | 2 +- hermes_cli/suggestions_cmd.py | 4 +- hermes_cli/tools_config.py | 80 ++-- hermes_cli/tools_config_mcp.py | 14 +- hermes_cli/tools_config_post_setup.py | 4 +- hermes_cli/toolset_scope.py | 2 +- hermes_cli/toolset_validation.py | 6 +- hermes_cli/update_channel.py | 2 +- hermes_cli/update_cmd.py | 28 +- hermes_cli/update_cmd_fleet.py | 4 +- hermes_cli/update_cmd_maint.py | 4 +- hermes_cli/update_cmd_windows.py | 6 +- hermes_cli/update_completion.py | 2 +- hermes_cli/update_finish.py | 4 +- hermes_cli/update_receipt.py | 2 +- hermes_cli/vault.py | 2 +- hermes_cli/web_deps.py | 6 +- hermes_cli/web_models.py | 54 +-- hermes_cli/web_routers/_common.py | 2 +- hermes_cli/web_routers/actions.py | 18 +- hermes_cli/web_routers/analytics.py | 20 +- hermes_cli/web_routers/audio.py | 6 +- hermes_cli/web_routers/chat_ws.py | 4 +- hermes_cli/web_routers/config_env.py | 38 +- hermes_cli/web_routers/cron.py | 40 +- hermes_cli/web_routers/files.py | 2 +- hermes_cli/web_routers/local_models.py | 60 +-- hermes_cli/web_routers/mcp.py | 4 +- hermes_cli/web_routers/memory_providers.py | 50 +-- hermes_cli/web_routers/oauth.py | 40 +- hermes_cli/web_routers/ops.py | 16 +- hermes_cli/web_routers/profiles.py | 62 +-- hermes_cli/web_routers/sessions.py | 2 +- hermes_cli/web_routers/status.py | 14 +- hermes_cli/web_routers/tools.py | 8 +- hermes_cli/web_server.py | 40 +- hermes_cli/web_server_chat.py | 4 +- hermes_cli/web_server_config.py | 44 +- hermes_cli/web_server_cron.py | 16 +- hermes_cli/web_server_dashboard.py | 38 +- hermes_cli/web_server_files.py | 4 +- hermes_cli/web_server_gateway.py | 50 +-- hermes_cli/web_server_mcp.py | 10 +- hermes_cli/web_server_memory.py | 42 +- hermes_cli/web_server_oauth.py | 24 +- hermes_cli/web_server_profiles.py | 38 +- hermes_cli/web_server_sessions.py | 2 +- hermes_cli/webhook.py | 12 +- hermes_cli/windows_appinstaller_update.py | 4 +- hermes_cli/windows_store_update.py | 4 +- hermes_cli/worktree_gc.py | 34 +- hermes_cli/worktree_ops.py | 14 +- hermes_cli/write_approval_commands.py | 14 +- hermes_cli/xai_retirement.py | 14 +- hermes_logging.py | 8 +- hermes_platform/declaration.py | 2 +- hermes_startup_watchdog.py | 2 +- hermes_state.py | 26 +- hermes_state_compression.py | 20 +- hermes_state_coverage.py | 40 +- hermes_state_dbfile.py | 48 +-- hermes_state_gateway.py | 52 +-- hermes_state_health.py | 2 +- hermes_state_holders.py | 30 +- hermes_state_identity.py | 8 +- hermes_state_lockguard.py | 8 +- hermes_state_lockowners.py | 10 +- hermes_state_maintenance.py | 22 +- hermes_state_messages.py | 147 ++++--- hermes_state_portability.py | 60 +-- hermes_state_profile_repair.py | 26 +- hermes_state_registry.py | 34 +- hermes_state_repair.py | 24 +- hermes_state_rewind.py | 10 +- hermes_state_schema.py | 12 +- hermes_state_search.py | 86 ++-- hermes_state_sessions.py | 112 ++--- hermes_state_telegram.py | 8 +- hermes_state_titles.py | 2 +- hermes_state_tool_retries.py | 8 +- hermes_state_usage.py | 12 +- hermes_state_wal.py | 6 +- hermes_time.py | 6 +- mcp_serve.py | 12 +- mini_swe_runner.py | 18 +- model_tools.py | 92 ++-- .../blockchain/evm/scripts/evm_client.py | 34 +- .../hyperliquid/scripts/hyperliquid_client.py | 143 +++---- .../solana/scripts/solana_client.py | 10 +- .../creative/comfyui/scripts/auto_fix_deps.py | 6 +- .../creative/comfyui/scripts/check_deps.py | 2 +- .../comfyui/scripts/extract_schema.py | 2 +- .../creative/comfyui/scripts/fetch_logs.py | 2 +- .../creative/comfyui/scripts/health_check.py | 2 +- .../creative/comfyui/scripts/run_batch.py | 6 +- .../creative/comfyui/scripts/run_workflow.py | 2 +- .../creative/comfyui/scripts/ws_monitor.py | 2 +- .../meme-generation/scripts/generate_meme.py | 4 +- .../devops/watchers/scripts/_watermark.py | 16 +- .../finance/dcf-model/scripts/validate_dcf.py | 6 +- .../scripts/openclaw_to_hermes.py | 160 +++---- .../scripts/youtube_quiz.py | 2 +- .../telephony/scripts/telephony.py | 2 +- .../scripts/show_snapshot.py | 4 +- .../drug-discovery/scripts/chembl_target.py | 2 +- .../osint-investigation/scripts/_http.py | 2 +- .../scripts/entity_resolution.py | 2 +- .../scripts/fetch_courtlistener.py | 4 +- .../scripts/fetch_gdelt.py | 4 +- .../scripts/fetch_icij_offshore.py | 2 +- .../scripts/fetch_nyc_acris.py | 4 +- .../scripts/fetch_ofac_sdn.py | 2 +- .../scripts/fetch_opencorporates.py | 4 +- .../scripts/fetch_sec_edgar.py | 2 +- .../scripts/fetch_senate_ld.py | 4 +- .../scripts/fetch_usaspending.py | 2 +- .../scripts/fetch_wayback.py | 4 +- .../scripts/fetch_wikipedia.py | 10 +- .../research/rss-feeds/scripts/feed.py | 8 +- .../security/godmode/scripts/godmode_race.py | 72 ++-- .../oss-forensics/scripts/evidence-store.py | 2 +- .../security/unbroker/scripts/badbool.py | 8 +- .../security/unbroker/scripts/cdp.py | 2 +- .../security/unbroker/scripts/emailer.py | 4 +- .../security/unbroker/scripts/pdd.py | 32 +- .../security/unbroker/scripts/registry.py | 8 +- .../security/unbroker/scripts/scan.py | 2 +- .../reddit-reading/scripts/reddit.py | 2 +- .../ast-grep/scripts/ast_grep_helper.py | 2 +- .../scripts/har_to_client.py | 4 +- plugins/browser/_common.py | 20 +- plugins/browser/browser_use/provider.py | 14 +- plugins/browser/browserbase/provider.py | 12 +- plugins/browser/firecrawl/provider.py | 8 +- plugins/computer_use/__init__.py | 4 +- plugins/context_engine/__init__.py | 8 +- plugins/cron_providers/__init__.py | 10 +- plugins/cron_providers/chronos/__init__.py | 14 +- plugins/cron_providers/chronos/_nas_client.py | 10 +- plugins/cron_providers/chronos/verify.py | 8 +- plugins/dashboard_auth/_shared.py | 14 +- plugins/dashboard_auth/drain/__init__.py | 2 +- plugins/dashboard_auth/nous/__init__.py | 8 +- .../dashboard_auth/self_hosted/__init__.py | 28 +- plugins/disk-cleanup/__init__.py | 36 +- plugins/disk-cleanup/disk_cleanup.py | 30 +- plugins/google_meet/node/__init__.py | 2 +- plugins/google_meet/node/cli.py | 2 +- plugins/google_meet/node/client.py | 16 +- plugins/google_meet/node/protocol.py | 12 +- plugins/google_meet/node/registry.py | 10 +- plugins/google_meet/node/server.py | 8 +- plugins/google_meet/process_manager.py | 16 +- plugins/google_meet/realtime/__init__.py | 2 +- plugins/google_meet/tools.py | 18 +- .../dashboard/plugin_api.py | 96 ++--- plugins/image_gen/_common.py | 60 +-- plugins/image_gen/deepinfra/__init__.py | 14 +- plugins/image_gen/fal/__init__.py | 18 +- plugins/image_gen/krea/__init__.py | 74 ++-- plugins/image_gen/meta-ai/__init__.py | 12 +- plugins/image_gen/openai-codex/__init__.py | 30 +- plugins/image_gen/openai/__init__.py | 20 +- plugins/image_gen/openrouter/__init__.py | 122 +++--- plugins/image_gen/xai/__init__.py | 46 +- plugins/memory/__init__.py | 12 +- plugins/memory/byterover/__init__.py | 12 +- plugins/memory/holographic/__init__.py | 6 +- plugins/memory/holographic/holographic.py | 4 +- plugins/memory/retaindb/__init__.py | 2 +- .../model-providers/openrouter/__init__.py | 2 +- plugins/model-providers/router/__init__.py | 2 +- plugins/model-providers/solstice/auth.py | 4 +- plugins/model-providers/solstice/transport.py | 2 +- plugins/observability/langfuse/__init__.py | 30 +- plugins/platforms/a2a/adapter.py | 20 +- plugins/platforms/a2a/tools.py | 4 +- plugins/platforms/buzz/adapter.py | 122 +++--- plugins/platforms/dingtalk/adapter.py | 34 +- plugins/platforms/dingtalk/inbound.py | 10 +- plugins/platforms/discord/adapter.py | 118 ++--- plugins/platforms/discord/adapter_media.py | 24 +- .../platforms/discord/adapter_slash_auth.py | 2 +- .../platforms/discord/adapter_voice_info.py | 4 +- plugins/platforms/discord/voice_mixer.py | 8 +- plugins/platforms/email/adapter.py | 56 +-- plugins/platforms/feishu/adapter.py | 252 +++++------ plugins/platforms/feishu/feishu_comment.py | 54 +-- .../platforms/feishu/feishu_comment_rules.py | 6 +- .../platforms/feishu/feishu_meeting_invite.py | 6 +- plugins/platforms/google_chat/adapter.py | 106 ++--- plugins/platforms/google_chat/cards.py | 40 +- plugins/platforms/google_chat/oauth.py | 8 +- plugins/platforms/google_chat/setup_files.py | 2 +- plugins/platforms/irc/adapter.py | 22 +- plugins/platforms/line/adapter.py | 86 ++-- plugins/platforms/matrix/adapter.py | 110 ++--- plugins/platforms/matrix/voice_mention.py | 10 +- plugins/platforms/mattermost/adapter.py | 46 +- plugins/platforms/ntfy/adapter.py | 20 +- plugins/platforms/photon/adapter.py | 116 ++--- plugins/platforms/photon/auth.py | 66 +-- plugins/platforms/photon/cli.py | 2 +- plugins/platforms/raft/adapter.py | 22 +- plugins/platforms/simplex/adapter.py | 24 +- plugins/platforms/slack/adapter.py | 370 ++++++++-------- plugins/platforms/slack/block_kit.py | 74 ++-- plugins/platforms/sms/adapter.py | 4 +- plugins/platforms/teams/adapter.py | 18 +- plugins/platforms/telegram/adapter.py | 256 +++++------ plugins/platforms/telegram/inline_picker.py | 16 +- .../platforms/telegram/update_admission.py | 2 +- plugins/platforms/wecom/adapter.py | 60 +-- plugins/platforms/wecom/callback_adapter.py | 28 +- plugins/platforms/wecom/media.py | 34 +- plugins/platforms/wecom/send_queue.py | 2 +- plugins/platforms/wecom/streaming.py | 18 +- plugins/platforms/whatsapp/adapter.py | 34 +- plugins/plugin_loader.py | 12 +- plugins/plugin_utils.py | 2 +- plugins/security-guidance/__init__.py | 18 +- plugins/security-guidance/patterns.py | 6 +- plugins/spotify/client.py | 10 +- plugins/spotify/tools.py | 22 +- plugins/teams_pipeline/models.py | 8 +- plugins/teams_pipeline/store.py | 14 +- plugins/video_gen/deepinfra/__init__.py | 8 +- plugins/video_gen/fal/__init__.py | 42 +- plugins/video_gen/openrouter/__init__.py | 62 +-- plugins/video_gen/xai/__init__.py | 44 +- plugins/web/_common.py | 48 +-- plugins/web/brave_free/provider.py | 4 +- plugins/web/ddgs/_search_worker.py | 4 +- plugins/web/ddgs/provider.py | 14 +- plugins/web/exa/provider.py | 10 +- plugins/web/firecrawl/provider.py | 30 +- plugins/web/keenable/provider.py | 16 +- plugins/web/keyless_mcp.py | 66 +-- plugins/web/openai_native/provider.py | 6 +- plugins/web/parallel/provider.py | 12 +- plugins/web/perplexity/provider.py | 24 +- plugins/web/searxng/provider.py | 4 +- plugins/web/tavily/provider.py | 20 +- plugins/web/xai/provider.py | 36 +- pm/shell.py | 2 +- pm/workspace.py | 2 +- run_agent.py | 46 +- scripts/add_contributor.py | 2 +- scripts/audit-old-updater-imports.py | 2 +- scripts/build_model_catalog.py | 2 +- scripts/bundles/release_artifacts.py | 2 +- scripts/check_profile_scope_patterns.py | 2 +- scripts/ci/check_lazy_deps_imports.py | 8 +- scripts/ci/check_public_surface.py | 2 +- scripts/code_health/py_rules.py | 4 +- scripts/code_health/py_structure.py | 4 +- scripts/contributor_audit.py | 2 +- scripts/desktop-update/serve-ui.py | 2 +- scripts/gen-bootstrap-pins.py | 2 +- scripts/gen_gateway_contracts.py | 16 +- scripts/generate_conformance_vectors.py | 20 +- scripts/generate_icons.py | 10 +- scripts/keystroke_diagnostic.py | 4 +- .../observability/otel_capture_collector.py | 2 +- scripts/profile-tui.py | 2 +- scripts/release.py | 4 +- scripts/releases/channel_build.py | 2 +- scripts/releases/channel_disposable.py | 2 +- scripts/releases/r2.py | 8 +- scripts/releases/stable.py | 3 +- scripts/render-builds-table.py | 2 +- scripts/run_tests_parallel.py | 88 ++-- scripts/sample_and_compress.py | 16 +- scripts/smoke_nemo_relay_shared_metrics.py | 6 +- scripts/termux/stage_runtime_libs.py | 10 +- scripts/write_install_stamp.py | 4 +- .../docx/scripts/docx_comments.py | 2 +- .../docx/scripts/docx_validate.py | 2 +- skills/productivity/pdf/scripts/_raster.py | 2 +- .../productivity/pdf/tests/test_pdf_skill.py | 2 +- .../productivity/xlsx/scripts/csv_to_xlsx.py | 2 +- .../productivity/xlsx/scripts/xlsx_create.py | 2 +- skills/productivity/xlsx/scripts/xlsx_edit.py | 2 +- skills/productivity/xlsx/scripts/xlsx_read.py | 2 +- .../productivity/xlsx/scripts/xlsx_recalc.py | 2 +- .../xlsx/scripts/xlsx_restructure.py | 2 +- .../productivity/xlsx/scripts/xlsx_to_csv.py | 2 +- .../grounded-citations/scripts/sources.py | 2 +- tests/acp_adapter/conftest.py | 2 +- tests/acp_adapter/test_acp_mcp_discovery.py | 2 +- tests/acp_adapter/test_approval_isolation.py | 6 +- tests/acp_adapter/test_failed_turn_closure.py | 6 +- tests/acp_adapter/test_permissions.py | 4 +- tests/acp_adapter/test_ping_suppression.py | 16 +- .../test_session_construction_off_loop.py | 1 - tests/agent/lsp/test_manager_locking.py | 2 +- tests/agent/repro_image_shrink_brick.py | 4 +- tests/agent/test_account_usage.py | 4 +- tests/agent/test_acp_openai_bridge.py | 2 +- tests/agent/test_anthropic_oauth_pkce.py | 12 +- .../test_anthropic_token_scope_isolation.py | 8 +- tests/agent/test_api_content_sidecar.py | 2 +- .../test_auxiliary_auth_rung_fallthrough.py | 1 - tests/agent/test_auxiliary_client.py | 6 +- .../test_auxiliary_client_azure_foundry.py | 2 +- .../test_auxiliary_client_resolve_dedup.py | 2 +- .../agent/test_auxiliary_client_ssl_verify.py | 2 +- tests/agent/test_auxiliary_concurrency.py | 6 +- .../test_auxiliary_explicit_cancellation.py | 2 +- tests/agent/test_azure_identity_adapter.py | 8 +- .../test_background_review_memory_scope.py | 2 +- .../test_background_review_tool_call_guard.py | 2 +- tests/agent/test_bedrock_adapter.py | 2 +- tests/agent/test_billing_view.py | 12 +- tests/agent/test_bitwarden_secrets.py | 2 +- tests/agent/test_bounded_response.py | 4 +- .../test_codex_aux_no_progress_timeout.py | 2 +- .../test_codex_gpt55_autoraise_notice.py | 6 +- tests/agent/test_codex_no_tools_nonetype.py | 4 +- .../test_codex_runtime_prompt_handoff.py | 2 +- .../agent/test_codex_sdk_transform_bypass.py | 2 +- tests/agent/test_codex_xai_oauth_recovery.py | 2 +- tests/agent/test_command_secret_source.py | 6 +- .../test_compression_attempt_ownership.py | 4 +- tests/agent/test_compression_boundary_hook.py | 1 - .../test_compression_commit_fence_race.py | 6 +- .../agent/test_compression_concurrent_fork.py | 40 +- tests/agent/test_compression_persistence.py | 2 +- tests/agent/test_compression_review.py | 2 +- .../agent/test_compression_rotation_state.py | 8 +- ...est_compressor_tail_cut_tool_pair_floor.py | 2 +- ...est_content_policy_refusal_stop_details.py | 2 +- tests/agent/test_context_breakdown.py | 2 +- tests/agent/test_context_engine.py | 8 +- ...t_context_engine_on_turn_complete_usage.py | 8 +- .../test_context_engine_select_context.py | 7 +- tests/agent/test_context_token_tracking.py | 2 +- tests/agent/test_copilot_initiator.py | 2 +- ..._credential_pool_anthropic_refresh_race.py | 2 +- ...t_credential_pool_nous_refresh_stampede.py | 2 +- ...test_credential_pool_oauth_writethrough.py | 2 +- ...test_credential_pool_profile_oauth_fork.py | 2 +- ...t_credential_pool_refresh_sweep_spacing.py | 2 +- tests/agent/test_credits_cold_start.py | 4 +- tests/agent/test_credits_tracker.py | 2 +- .../test_cron_inflight_prompt_reappend.py | 18 +- tests/agent/test_curator.py | 6 +- tests/agent/test_curator_activity.py | 4 +- tests/agent/test_curator_backup.py | 2 +- tests/agent/test_curator_run_hygiene.py | 4 +- tests/agent/test_custom_provider_ca_probes.py | 2 +- tests/agent/test_deadline.py | 14 +- .../test_direct_contexts_stream_inline.py | 2 +- tests/agent/test_empty_response_guard.py | 4 +- .../test_empty_tool_name_loop_dampening.py | 2 +- tests/agent/test_env_loader_reload_restore.py | 2 +- tests/agent/test_env_loader_secret_sources.py | 2 +- tests/agent/test_error_classifier.py | 1 - .../test_external_process_provider_init.py | 2 +- .../agent/test_fallback_dynamic_credential.py | 2 +- tests/agent/test_file_safety_credentials.py | 4 +- .../agent/test_free_tier_rate_limit_class.py | 2 +- tests/agent/test_image_routing.py | 6 +- tests/agent/test_in_place_compaction.py | 4 +- ...est_injected_param_strip_retry_registry.py | 2 +- tests/agent/test_inline_edit_persistence.py | 2 +- tests/agent/test_interrupt_compat.py | 2 +- tests/agent/test_memory_boundary_commit.py | 8 +- tests/agent/test_moa_cold_start_cache.py | 2 +- tests/agent/test_moa_loop_mode.py | 22 +- .../agent/test_moa_trace_streamed_capture.py | 2 +- .../test_models_dev_provider_capabilities.py | 4 +- tests/agent/test_multiplex_base_url_scope.py | 2 +- ...test_multiplex_cloud_credential_clients.py | 2 +- tests/agent/test_native_compaction.py | 4 +- .../agent/test_nous_portal_anthropic_wire.py | 2 +- tests/agent/test_onepassword_secrets.py | 2 +- tests/agent/test_openai_client_lifecycle.py | 2 +- tests/agent/test_outbound_webhooks.py | 10 +- tests/agent/test_plugin_llm_task_routing.py | 30 +- tests/agent/test_post_compression_trim.py | 2 +- .../test_proactive_prune_rearm_threshold.py | 10 +- tests/agent/test_probe_cache_followups.py | 2 +- tests/agent/test_prompt_builder.py | 6 +- tests/agent/test_prompt_cache_boundary.py | 8 +- tests/agent/test_provider_parity.py | 2 +- tests/agent/test_provider_projection.py | 2 +- tests/agent/test_redact.py | 2 +- tests/agent/test_relay_llm.py | 8 +- tests/agent/test_remote_fetch_ssrf_guard.py | 2 +- tests/agent/test_retry_utils.py | 2 +- tests/agent/test_run_agent.py | 8 +- tests/agent/test_run_agent_codex_responses.py | 26 +- tests/agent/test_runtime_self_protection.py | 2 +- tests/agent/test_save_url_image.py | 4 +- tests/agent/test_sequential_tool_interrupt.py | 2 +- tests/agent/test_sequential_tool_timeout.py | 4 +- tests/agent/test_shared_http_transport.py | 2 +- .../test_skill_invocation_description.py | 6 +- .../test_skills_guidance_content_filter.py | 2 +- tests/agent/test_skip_memory_store.py | 2 +- tests/agent/test_steer.py | 2 +- tests/agent/test_stream_serving_provider.py | 4 +- tests/agent/test_strict_api_validation.py | 2 +- tests/agent/test_subscription_view.py | 8 +- tests/agent/test_system_prompt.py | 10 +- tests/agent/test_tool_arg_coercion.py | 2 +- .../agent/test_tool_call_guardrail_runtime.py | 7 +- tests/agent/test_tool_guardrails.py | 4 +- tests/agent/test_tool_result_memory_trim.py | 2 +- tests/agent/test_turn_context.py | 2 +- tests/agent/test_turn_recovery_autorecover.py | 2 +- .../test_uncompressed_context_guardrail.py | 2 +- tests/agent/test_usage_pricing.py | 10 +- tests/agent/test_verification_stop_caching.py | 2 +- tests/agent/test_vertex_adapter.py | 2 +- tests/agent/test_welcome_error_identity.py | 2 +- .../transports/test_bedrock_transport.py | 2 +- .../agent/transports/test_chat_completions.py | 2 +- .../test_chat_completions_empty_tool_calls.py | 2 +- ..._chat_completions_xai_tool_search_alias.py | 2 +- .../agent/transports/test_codex_transport.py | 4 +- .../test_codex_transport_github_reasoning.py | 2 +- .../agent/transports/test_meta_codex_cache.py | 2 +- .../test_profile_hook_keyword_compat.py | 2 +- .../test_reasoning_effort_sibling_sites.py | 4 +- .../transports/test_router_codex_efforts.py | 2 +- tests/agent/transports/test_transport.py | 2 +- tests/ci/_gha_expr.py | 4 +- tests/ci/test_channel_build_publication.py | 2 +- tests/ci/test_check_os_marker_fakes.py | 2 +- tests/ci/test_classify_changes.py | 2 +- tests/ci/test_commit_build_staging.py | 2 +- tests/ci/test_desktop_bundle_smoke.py | 2 +- tests/ci/test_protected_canary_publication.py | 2 +- tests/ci/test_tag_builds_summary.py | 2 +- tests/ci/test_update_ci_routing.py | 10 +- tests/ci/test_windows_update_crash_oracles.py | 2 +- tests/ci/test_workflow_job_graph.py | 6 +- tests/compat/old_updater_dependencies.py | 2 +- tests/conformance/test_vector_generator.py | 2 +- tests/conftest.py | 22 +- tests/cron/test_blueprint_catalog.py | 2 +- tests/cron/test_bot_chat_cli_home.py | 2 - tests/cron/test_claim_job_for_fire.py | 18 +- tests/cron/test_cleanup_timeout.py | 1 - tests/cron/test_codex_execution_paths.py | 2 +- .../test_cron_direct_api_call_watchdog.py | 2 +- tests/cron/test_cron_empty_payload.py | 10 +- tests/cron/test_cron_failure_deliver.py | 2 +- tests/cron/test_cron_failure_notice_copy.py | 4 +- ...test_cron_failure_summarizer_inactivity.py | 2 +- .../cron/test_cron_import_error_skew_hint.py | 2 +- tests/cron/test_cron_incidents.py | 3 +- tests/cron/test_cron_incidents_resolve.py | 2 +- tests/cron/test_cron_live_bot_delivery.py | 2 +- .../test_cron_live_delivery_confirmation.py | 2 +- tests/cron/test_cron_manage_profile_scope.py | 4 +- ...est_cron_multiplex_desktop_ticker_scope.py | 8 +- tests/cron/test_cron_no_agent.py | 2 +- tests/cron/test_cron_profile_isolation.py | 2 +- tests/cron/test_cron_provider_pin.py | 2 +- tests/cron/test_due_scan_save_failure.py | 2 +- tests/cron/test_execution_ledger.py | 10 +- .../cron/test_external_worker_config_hooks.py | 10 +- .../test_external_worker_secret_sources.py | 4 +- .../test_fire_claim_lost_after_delivery.py | 1 - tests/cron/test_jobs.py | 20 +- tests/cron/test_jobs_changed_notify.py | 2 +- tests/cron/test_jobs_file_ownership.py | 2 +- tests/cron/test_jobs_shrink_merge.py | 6 +- tests/cron/test_lifecycle_guard_budget.py | 2 +- .../cron/test_lifecycle_guard_heredoc_walk.py | 5 +- tests/cron/test_lifecycle_guard_live_db.py | 2 +- tests/cron/test_media_send_timeout.py | 4 +- .../test_misfire_backstop_oneshot_grace.py | 1 - tests/cron/test_preflight_config.py | 2 - tests/cron/test_relay_fronted_delivery.py | 2 +- tests/cron/test_restart_safe_worker.py | 89 ++-- ...test_restart_safe_worker_death_evidence.py | 8 +- tests/cron/test_run_one_job.py | 4 +- tests/cron/test_scheduler.py | 12 +- .../test_scheduler_completion_verification.py | 2 +- .../test_scheduler_cron_session_isolation.py | 2 +- tests/cron/test_scheduler_provider.py | 16 +- tests/cron/test_script_claim_heartbeat.py | 38 +- tests/cron/test_shutdown_interrupt.py | 4 +- tests/docker/conftest.py | 2 +- tests/e2e/conftest.py | 10 +- tests/e2e/core/_pending_fixes.py | 6 +- .../e2e/core/chaos/_gateway_fake_platform.py | 8 +- tests/e2e/core/chaos/_tui_rpc.py | 2 +- .../core/cli_tmux/test_resize_scrollback.py | 2 +- tests/e2e/core/dashboard/_helpers.py | 2 +- tests/e2e/core/dashboard/_issue_helpers.py | 4 +- tests/e2e/core/dashboard/_pty_helpers.py | 4 +- .../dashboard/test_dashboard_mcp_install.py | 6 +- tests/e2e/core/delivery/_cron_clock.py | 36 +- tests/e2e/core/delivery/_fake_platform.py | 48 +-- tests/e2e/core/delivery/_pending_fixes.py | 4 +- .../delivery/test_cron_virtual_clock_soak.py | 26 +- .../delivery/test_messaging_exactly_once.py | 14 +- tests/e2e/core/history/_helpers.py | 2 +- tests/e2e/core/live/_helpers.py | 4 +- tests/e2e/core/mcp_plugins/_helpers.py | 2 +- .../core/mcp_plugins/mcp_fixture_server.py | 3 +- .../mcp_plugins/test_mcp_streamable_http.py | 2 +- tests/e2e/core/parity/_drive_cron.py | 2 +- tests/e2e/core/parity/_drive_gateway.py | 2 +- tests/e2e/core/parity/_gateway_child.py | 4 +- tests/e2e/core/parity/_helpers.py | 2 +- .../e2e/core/parity/test_entrypoint_parity.py | 5 +- tests/e2e/core/platforms/_contract.py | 24 +- tests/e2e/core/platforms/_drv_discord.py | 28 +- tests/e2e/core/platforms/_drv_slack.py | 36 +- tests/e2e/core/platforms/_drv_telegram.py | 28 +- tests/e2e/core/platforms/_helpers.py | 20 +- tests/e2e/core/platforms/_suite.py | 10 +- tests/e2e/core/providers/_oauth_helpers.py | 2 +- tests/e2e/core/providers/_openai_tui.py | 2 +- .../core/providers/test_anthropic_oracle.py | 2 +- .../core/providers/test_fallback_providers.py | 2 +- .../test_native_bedrock_converse_faults.py | 6 +- .../test_native_bedrock_converse_turns.py | 6 +- .../providers/test_native_vertex_errors.py | 6 +- .../providers/test_native_vertex_recovery.py | 4 +- .../providers/test_native_vertex_tools.py | 6 +- .../core/providers/test_openai_codex_pool.py | 4 +- .../e2e/core/providers/test_openai_pricing.py | 4 +- .../core/security/test_approval_boundary.py | 22 +- tests/e2e/core/sqlite/_helpers.py | 6 +- tests/e2e/core/tenancy/_helpers.py | 4 +- tests/e2e/core/tenancy/_routing_helpers.py | 4 +- .../core/tenancy/test_two_tenant_gateway.py | 2 +- tests/e2e/core/terminal/_gateway_client.py | 6 +- tests/e2e/core/terminal/_pty.py | 2 +- tests/e2e/core/tui_pty/_helpers.py | 2 +- tests/e2e/core/upgrade/git/_git_world.py | 2 +- .../core/upgrade/git/test_hostile_pause.py | 4 +- .../e2e/core/upgrade/git/test_hostile_post.py | 4 +- tests/e2e/core/upgrade/handoff/_ns_agent.py | 2 +- tests/e2e/core/upgrade/handoff/_nshost.py | 2 +- tests/e2e/core/upgrade/handoff/_scenario.py | 4 +- tests/e2e/core/upgrade/hosts/_hosts.py | 2 +- tests/e2e/core/upgrade/network/_netedge.py | 2 +- tests/e2e/core/upgrade/network/_seed.py | 2 +- .../test_config_roundtrip_properties.py | 8 +- .../upgrade/test_fresh_process_entrypoints.py | 2 +- tests/e2e/core/upgrade/test_upgrade_path.py | 2 +- tests/e2e/core/windows/test_unicode_input.py | 8 +- .../core/windows_update/test_crash_cells.py | 4 +- .../test_gateway_across_update.py | 4 +- .../core/windows_update/test_interpreter.py | 2 +- tests/fakes/fake_llm_provider.py | 4 +- tests/fakes/platforms/_standin.py | 18 +- tests/fakes/platforms/discord_standin.py | 146 +++---- tests/fakes/platforms/slack_standin.py | 120 +++--- tests/fakes/platforms/telegram_standin.py | 76 ++-- tests/fakes/providers/anthropic_messages.py | 6 +- tests/fakes/providers/bedrock_converse.py | 2 +- tests/fakes/providers/catalog_fake.py | 6 +- tests/fakes/providers/catalog_oauth.py | 4 +- tests/fakes/providers/chat_variants.py | 8 +- tests/fakes/providers/copilot_acp.py | 2 +- tests/fakes/providers/gemini_native.py | 10 +- tests/fakes/providers/oauth_token_server.py | 4 +- tests/fakes/providers/openai_responses.py | 8 +- tests/fakes/providers/vertex.py | 10 +- tests/gateway/conftest.py | 2 +- .../platforms/test_yuanbao_state_cleanup.py | 6 +- tests/gateway/relay/stub_connector.py | 36 +- tests/gateway/relay/test_explicit_disable.py | 2 +- .../relay/test_identity_token_resolver.py | 4 +- .../relay/test_provision_secret_optional.py | 6 +- tests/gateway/relay/test_relay_adapter.py | 8 +- .../relay/test_relay_egress_declines.py | 20 +- tests/gateway/relay/test_relay_interactive.py | 2 +- tests/gateway/relay/test_relay_media.py | 4 +- .../gateway/relay/test_relay_multiplatform.py | 4 +- .../relay/test_relay_per_platform_caps.py | 2 +- tests/gateway/relay/test_relay_policy_send.py | 4 +- tests/gateway/relay/test_relay_roundtrip.py | 1 - .../relay/test_relay_roundtrip_telegram.py | 1 - .../gateway/relay/test_relay_slack_unfurl.py | 2 +- tests/gateway/relay/test_self_provision.py | 6 +- tests/gateway/test_access_policy_mixin.py | 2 +- tests/gateway/test_active_turn_recovery.py | 28 +- .../test_adapter_connect_classification.py | 8 +- tests/gateway/test_agent_cache.py | 4 +- tests/gateway/test_api_server.py | 2 +- .../test_api_server_active_work_drain.py | 4 +- .../test_api_server_media_data_urls.py | 2 +- .../test_api_server_reasoning_stream.py | 2 +- tests/gateway/test_api_server_runs.py | 2 +- .../test_api_server_runs_import_isolation.py | 2 +- .../gateway/test_api_server_status_stream.py | 2 +- tests/gateway/test_approval_timeout_notice.py | 1 - .../test_async_delegation_session_binding.py | 16 +- tests/gateway/test_async_media_cache.py | 2 +- tests/gateway/test_async_session_db.py | 1 - tests/gateway/test_auto_skill_title_input.py | 4 +- ...ackground_process_served_apiserver_wake.py | 2 +- tests/gateway/test_base_topic_sessions.py | 1 - tests/gateway/test_branch_routing_columns.py | 2 +- tests/gateway/test_browser_control_api.py | 1 - .../test_busy_followup_guard_release.py | 1 - tests/gateway/test_busy_session_ack.py | 6 +- tests/gateway/test_choice_picker.py | 4 +- tests/gateway/test_clarify_progress_leak.py | 2 +- tests/gateway/test_compress_plugin_engine.py | 6 +- .../test_compression_failure_session_sync.py | 4 +- .../test_compression_interrupt_demotion.py | 2 +- .../test_compression_progress_notices.py | 2 +- tests/gateway/test_control_socket.py | 4 +- tests/gateway/test_control_socket_pause.py | 2 +- .../test_control_socket_windows_live.py | 2 +- tests/gateway/test_cron_active_work_drain.py | 2 +- .../test_cron_delivery_housekeeping.py | 2 +- .../test_custom_provider_request_overrides.py | 8 +- .../test_decline_fallback_suppression.py | 8 +- ...test_dingtalk_reconnect_circuit_breaker.py | 2 +- .../gateway/test_discord_allowed_mentions.py | 2 +- .../test_discord_attachment_download.py | 4 +- .../gateway/test_discord_channel_controls.py | 4 +- tests/gateway/test_discord_clarify_buttons.py | 6 +- tests/gateway/test_discord_component_auth.py | 4 +- tests/gateway/test_discord_connect.py | 4 +- .../gateway/test_discord_document_handling.py | 4 +- tests/gateway/test_discord_double_dispatch.py | 4 +- .../test_discord_edit_message_overflow.py | 2 +- tests/gateway/test_discord_event_silence.py | 4 +- tests/gateway/test_discord_free_response.py | 4 +- tests/gateway/test_discord_liveness.py | 8 +- .../test_discord_missed_message_backfill.py | 4 +- ...est_discord_pending_text_batch_shutdown.py | 2 +- tests/gateway/test_discord_platform_events.py | 8 +- tests/gateway/test_discord_plugin_setup.py | 2 +- .../test_discord_prompt_timeout_config.py | 2 +- tests/gateway/test_discord_reactions.py | 2 +- tests/gateway/test_discord_reply_mode.py | 2 +- tests/gateway/test_discord_send.py | 4 +- tests/gateway/test_discord_slash_auth.py | 6 +- tests/gateway/test_discord_slash_commands.py | 4 +- tests/gateway/test_discord_split_cap.py | 2 +- .../gateway/test_discord_view_base_parity.py | 2 +- tests/gateway/test_discord_voice_mixer.py | 2 +- tests/gateway/test_disk_status.py | 2 +- tests/gateway/test_dm_topics.py | 6 +- tests/gateway/test_email.py | 1 - tests/gateway/test_email_charset_fallback.py | 6 +- tests/gateway/test_exec_approval_template.py | 2 +- .../test_exec_approval_timeout_notice.py | 8 +- tests/gateway/test_fast_command.py | 10 +- .../gateway/test_finalize_session_off_loop.py | 4 +- tests/gateway/test_free_tier_gateway_boot.py | 2 +- .../test_gateway_platform_event_hook.py | 10 +- tests/gateway/test_gateway_shutdown.py | 4 +- tests/gateway/test_google_chat.py | 6 +- .../gateway/test_handoff_watcher_async_db.py | 2 +- tests/gateway/test_heartbeat_acceptance.py | 1 - tests/gateway/test_heartbeat_poller.py | 3 - tests/gateway/test_heartbeat_watch_restore.py | 2 +- .../test_history_media_current_turn.py | 8 +- .../test_hosted_room_execution_policy.py | 2 +- .../test_housekeeping_profile_scope.py | 2 +- .../test_identity_canonical_ingress.py | 2 - ...nal_event_never_interrupts_busy_session.py | 2 +- tests/gateway/test_irc_adapter.py | 2 +- .../test_kanban_changes_requested_notifier.py | 2 +- ..._kanban_multiplex_served_profile_parity.py | 2 +- tests/gateway/test_kanban_notifier.py | 2 +- .../test_kanban_notifier_apiserver_wake.py | 2 +- ...t_kanban_notifier_served_apiserver_wake.py | 4 +- ...test_kanban_notifier_wake_only_ordering.py | 2 +- .../test_kanban_notifier_zero_sub_gate.py | 2 +- tests/gateway/test_kanban_wake_scope.py | 2 +- .../test_launchd_exit_timeout_drain_cap.py | 4 +- tests/gateway/test_matrix.py | 10 +- ...st_matrix_approval_reaction_fail_closed.py | 2 +- tests/gateway/test_media_download_retry.py | 8 +- tests/gateway/test_media_resend_dedup.py | 1 - tests/gateway/test_media_tag_separator.py | 2 +- .../gateway/test_memory_trim_housekeeping.py | 2 +- .../test_model_command_async_offload.py | 2 +- .../test_model_command_context_offload.py | 2 +- .../test_model_command_expensive_confirm.py | 3 +- .../test_model_command_flat_string_config.py | 4 +- .../test_model_command_profile_config.py | 2 +- .../test_model_command_request_overrides.py | 2 +- tests/gateway/test_model_picker_persist.py | 2 +- .../gateway/test_model_switch_persistence.py | 2 +- .../test_multiplex_adapter_registry.py | 4 +- tests/gateway/test_multiplex_hot_serve.py | 3 +- .../test_multiplex_interactive_auth.py | 2 +- tests/gateway/test_multiplex_lifecycle.py | 4 +- tests/gateway/test_multiplex_mcp_discovery.py | 2 +- ...multiplex_notice_egress_profile_adapter.py | 2 +- .../gateway/test_multiplex_shared_ingress.py | 6 +- ...st_multiplex_toolsets_profile_isolation.py | 8 +- .../test_multiplex_unserved_shared_ingress.py | 4 +- .../test_native_producer_owner_matrix.py | 2 +- tests/gateway/test_platform_base.py | 4 +- .../gateway/test_platform_plugin_handlers.py | 6 +- tests/gateway/test_platform_reconnect.py | 1 - tests/gateway/test_platform_registry.py | 1 - .../gateway/test_plugin_message_injection.py | 1 - tests/gateway/test_pre_gateway_dispatch.py | 2 +- tests/gateway/test_prompt_tail_freeze.py | 28 +- tests/gateway/test_proxy_mode.py | 4 +- .../test_relay_source_platform_metrics.py | 2 +- tests/gateway/test_remote_media_fetch.py | 2 +- tests/gateway/test_restart_drain.py | 12 +- tests/gateway/test_restart_notice_replay.py | 2 +- tests/gateway/test_restart_resume_pending.py | 1 - tests/gateway/test_run_cleanup_progress.py | 2 +- tests/gateway/test_run_progress_topics.py | 6 +- tests/gateway/test_scale_to_zero.py | 4 +- .../test_scale_to_zero_dashboard_client.py | 2 +- tests/gateway/test_send_image_file.py | 8 +- tests/gateway/test_send_multiple_images.py | 10 +- tests/gateway/test_send_retry_hooks.py | 2 +- tests/gateway/test_session_api.py | 2 +- .../test_session_model_override_routing.py | 2 +- .../gateway/test_session_title_rename_lane.py | 1 - .../gateway/test_shutdown_executor_quiesce.py | 1 - tests/gateway/test_signal.py | 1 - tests/gateway/test_simplex_plugin.py | 3 +- tests/gateway/test_skill_slash_offloop.py | 1 - tests/gateway/test_slack.py | 8 +- tests/gateway/test_slack_api_human_senders.py | 6 +- tests/gateway/test_slack_approval_buttons.py | 1 - tests/gateway/test_slack_block_kit_adapter.py | 4 +- .../test_slack_cron_continuable_surface.py | 4 +- tests/gateway/test_slack_dedup_ttl.py | 6 +- .../gateway/test_slack_download_redirects.py | 4 +- tests/gateway/test_slack_download_ssrf.py | 6 +- .../test_slack_ignore_other_user_mentions.py | 4 +- tests/gateway/test_slack_log_noise.py | 6 +- tests/gateway/test_slack_mention.py | 8 +- .../test_slack_mention_humanization.py | 4 +- tests/gateway/test_slack_model_picker.py | 4 +- tests/gateway/test_slack_peer_agent_smoke.py | 2 +- .../test_slack_plugin_action_handlers.py | 8 +- tests/gateway/test_slack_reply_expected.py | 2 +- .../test_slack_require_mention_channels.py | 4 +- tests/gateway/test_slack_sdk_response.py | 6 +- .../test_slack_socket_reconnect_heal.py | 6 +- tests/gateway/test_slack_status_update.py | 6 +- .../test_slack_wake_external_bot_messages.py | 6 +- tests/gateway/test_sse_frame.py | 2 +- tests/gateway/test_stale_base_url_api_mode.py | 2 +- .../gateway/test_stale_confirmation_expiry.py | 2 +- .../test_stale_platform_lock_retryable.py | 2 +- tests/gateway/test_status.py | 4 - tests/gateway/test_stream_consumer.py | 2 +- tests/gateway/test_streaming_tts_consumer.py | 4 +- .../test_streaming_tts_gateway_regression.py | 2 +- tests/gateway/test_subagent_protection.py | 2 +- .../test_suppression_contract_matrix.py | 10 +- tests/gateway/test_teams.py | 2 +- tests/gateway/test_teams_dotenv_isolation.py | 2 +- .../gateway/test_telegram_closewait_limits.py | 6 +- .../test_telegram_closewait_windows_live.py | 4 +- tests/gateway/test_telegram_conflict.py | 6 +- tests/gateway/test_telegram_connect.py | 4 +- tests/gateway/test_telegram_documents.py | 2 +- tests/gateway/test_telegram_format.py | 2 +- .../test_telegram_image_precompress.py | 6 +- tests/gateway/test_telegram_init_deadline.py | 4 +- .../test_telegram_lazy_install_typehandler.py | 2 +- tests/gateway/test_telegram_max_doc_bytes.py | 2 +- .../test_telegram_media_read_timeout.py | 8 +- .../test_telegram_network_reconnect.py | 8 +- .../test_telegram_pending_update_probe.py | 2 +- ...st_telegram_polling_health_confirmation.py | 4 +- .../gateway/test_telegram_polling_progress.py | 4 +- tests/gateway/test_telegram_reply_mode.py | 2 +- tests/gateway/test_telegram_reply_quote.py | 2 +- tests/gateway/test_telegram_rich_messages.py | 6 +- .../test_telegram_send_draft_format.py | 4 +- .../gateway/test_telegram_send_path_health.py | 2 +- .../test_telegram_send_reconnect_wait.py | 2 +- .../gateway/test_telegram_split_send_flood.py | 2 +- .../test_telegram_start_polling_timeout.py | 4 +- .../gateway/test_telegram_status_indicator.py | 2 +- tests/gateway/test_telegram_text_batching.py | 1 - .../gateway/test_telegram_thread_fallback.py | 2 +- tests/gateway/test_telegram_typing_backoff.py | 1 - .../test_telegram_voice_caption_markdown.py | 4 +- tests/gateway/test_telegram_voice_duration.py | 4 +- .../test_text_batch_flush_invariants.py | 2 +- tests/gateway/test_tts_media_routing.py | 4 +- tests/gateway/test_turn_request_overrides.py | 2 +- tests/gateway/test_update_command.py | 1 - .../test_warning_delivery_real_producers.py | 2 +- tests/hermes_cli/test_actual_provider.py | 2 +- tests/hermes_cli/test_anon_auth_core.py | 2 +- tests/hermes_cli/test_anon_challenge.py | 4 +- tests/hermes_cli/test_anon_failure_modes.py | 2 +- tests/hermes_cli/test_anon_sign_in_flow.py | 2 +- tests/hermes_cli/test_anon_surfaces.py | 4 +- .../test_anthropic_models_pagination.py | 4 +- tests/hermes_cli/test_api_key_providers.py | 15 +- tests/hermes_cli/test_approval_transport.py | 2 +- tests/hermes_cli/test_approvals_test.py | 2 +- .../test_argparse_flag_propagation.py | 6 +- tests/hermes_cli/test_audio_playback_guard.py | 2 +- tests/hermes_cli/test_audio_router_scope.py | 6 +- .../test_auth_codex_browser_login.py | 2 +- .../hermes_cli/test_auth_codex_quota_probe.py | 2 +- tests/hermes_cli/test_auth_codex_self_heal.py | 4 +- .../test_auth_lock_timeout_diagnosis.py | 10 +- tests/hermes_cli/test_auth_nous_provider.py | 13 +- .../hermes_cli/test_auth_profile_fallback.py | 2 +- .../test_auth_store_read_failure.py | 2 +- .../test_auth_store_windows_encoding.py | 2 +- ..._authenticated_providers_exhausted_pool.py | 2 +- tests/hermes_cli/test_backup.py | 2 +- tests/hermes_cli/test_backup_all_profiles.py | 2 +- tests/hermes_cli/test_banner.py | 4 +- tests/hermes_cli/test_banner_skills.py | 2 +- tests/hermes_cli/test_banner_skills_width.py | 2 +- .../hermes_cli/test_bare_custom_picker_row.py | 6 +- tests/hermes_cli/test_billing_scope_stepup.py | 6 +- .../test_canonical_custom_identity.py | 6 +- .../test_chat_turn_streaming_tts_arming.py | 6 +- tests/hermes_cli/test_cli_billing_i18n.py | 2 +- .../hermes_cli/test_cli_model_switch_i18n.py | 2 +- .../test_cli_session_store_shared_registry.py | 2 +- tests/hermes_cli/test_cmd_uninstall.py | 2 +- tests/hermes_cli/test_cmd_update.py | 2 +- tests/hermes_cli/test_code_skew.py | 2 +- .../test_codex_credential_host_binding.py | 4 +- .../test_codex_runtime_plugin_migration.py | 2 +- tests/hermes_cli/test_commands.py | 2 +- tests/hermes_cli/test_config.py | 4 +- tests/hermes_cli/test_config_edit_seed.py | 4 +- tests/hermes_cli/test_config_rmw_lock.py | 8 +- .../hermes_cli/test_copilot_in_model_list.py | 8 +- tests/hermes_cli/test_cprint_bg_thread.py | 1 - tests/hermes_cli/test_cron.py | 8 +- .../test_cron_dashboard_off_loop.py | 1 - tests/hermes_cli/test_cron_fire_dashboard.py | 6 +- .../test_cron_satellite_diagnostics.py | 6 +- .../hermes_cli/test_curator_archive_prune.py | 2 +- .../hermes_cli/test_curator_pin_unmanaged.py | 4 +- .../hermes_cli/test_curator_pin_visibility.py | 2 +- tests/hermes_cli/test_curator_usage.py | 6 +- .../test_dashboard_admin_endpoints.py | 10 +- .../test_dashboard_auth_ws_tickets.py | 2 +- .../hermes_cli/test_dashboard_param_clamps.py | 2 +- ...st_dashboard_plugin_routes_secret_scope.py | 2 +- .../test_dashboard_web_dist_validation.py | 2 +- tests/hermes_cli/test_data_uninstall.py | 2 +- .../hermes_cli/test_data_uninstall_holders.py | 2 +- tests/hermes_cli/test_desktop_build_lock.py | 1 - ...t_desktop_cron_ticker_gateway_standdown.py | 14 +- tests/hermes_cli/test_desktop_source_build.py | 2 +- tests/hermes_cli/test_dingtalk_auth.py | 2 +- tests/hermes_cli/test_doctor.py | 86 ++-- tests/hermes_cli/test_doctor_exit_status.py | 2 +- .../test_doctor_github_token_probe.py | 4 +- .../hermes_cli/test_doctor_legacy_checkout.py | 2 +- tests/hermes_cli/test_doctor_live.py | 2 +- .../test_doctor_wal_holder_guard.py | 4 +- tests/hermes_cli/test_ensure_utf8_locale.py | 2 +- tests/hermes_cli/test_env_export_prefix.py | 2 +- tests/hermes_cli/test_env_loader.py | 8 +- .../test_env_loader_op_bootstrap.py | 4 +- tests/hermes_cli/test_env_write_race.py | 2 +- .../test_exit_summary_resume_hint.py | 2 +- tests/hermes_cli/test_fireworks_provider.py | 4 +- ...est_fleet_config_migration_windows_live.py | 2 +- .../test_fleet_matrix_self_restart_pending.py | 4 +- tests/hermes_cli/test_gateway.py | 13 +- .../test_gateway_external_supervisor.py | 2 +- .../test_gateway_job_teardown_live.py | 4 +- tests/hermes_cli/test_gateway_linger.py | 2 +- ...test_gateway_migrate_compensator_resume.py | 2 +- .../test_gateway_migrate_converged_host.py | 2 +- .../test_gateway_migrate_multiplex.py | 2 +- .../hermes_cli/test_gateway_multiplex_mode.py | 2 +- .../test_gateway_multiplex_served_record.py | 4 +- .../test_gateway_multiplex_status.py | 2 +- .../test_gateway_no_new_standalone_profile.py | 2 +- tests/hermes_cli/test_gateway_restart_loop.py | 10 +- .../test_gateway_scan_home_identity.py | 2 +- .../test_gateway_scm_feature_removed.py | 2 +- tests/hermes_cli/test_gateway_service.py | 12 +- .../test_gateway_start_attestation.py | 2 +- tests/hermes_cli/test_gateway_unit_paths.py | 2 +- tests/hermes_cli/test_gateway_windows.py | 10 +- tests/hermes_cli/test_gateway_wsl.py | 2 +- tests/hermes_cli/test_gitlock.py | 10 +- tests/hermes_cli/test_gmi_provider.py | 4 +- .../test_goal_judge_timeout_config.py | 2 +- .../test_goals_db_bootstrap_off_loop.py | 2 +- tests/hermes_cli/test_gui_command.py | 3 +- tests/hermes_cli/test_gui_uninstall.py | 4 +- .../test_hatch_prompt_thread_safety.py | 1 - .../test_imagegen_managed_gateway.py | 2 +- tests/hermes_cli/test_install_identity.py | 2 +- tests/hermes_cli/test_install_lock_timeout.py | 2 +- tests/hermes_cli/test_jobs_json_utf8_bom.py | 6 +- tests/hermes_cli/test_kanban_db.py | 4 +- tests/hermes_cli/test_kanban_memory_guard.py | 4 +- .../test_kanban_review_lifecycle.py | 1 - .../hermes_cli/test_kanban_review_surfaces.py | 4 +- .../test_kanban_worker_exit_decode.py | 2 +- .../test_kanban_worker_pid_fingerprint.py | 4 +- .../test_kanban_write_txn_busy_retry.py | 1 - tests/hermes_cli/test_launchd_plist_guard.py | 1 - tests/hermes_cli/test_left_core_migration.py | 4 +- .../test_legacy_launchers_windows_live.py | 2 +- .../hermes_cli/test_list_picker_providers.py | 2 +- tests/hermes_cli/test_local_component_jobs.py | 4 +- tests/hermes_cli/test_local_download_jobs.py | 2 +- tests/hermes_cli/test_local_job_registry.py | 2 +- tests/hermes_cli/test_local_launch_budget.py | 2 +- tests/hermes_cli/test_local_models_routes.py | 2 +- tests/hermes_cli/test_local_runtime.py | 6 +- .../test_local_runtime_package_import.py | 2 +- .../test_mcp_catalog_env_boundary.py | 10 +- tests/hermes_cli/test_mcp_config.py | 2 +- tests/hermes_cli/test_mcp_discovery_timing.py | 6 +- tests/hermes_cli/test_mcp_probe_redaction.py | 4 +- tests/hermes_cli/test_mcp_security.py | 2 +- tests/hermes_cli/test_mcp_startup.py | 8 +- tests/hermes_cli/test_mem_trim.py | 2 +- tests/hermes_cli/test_memory_setup.py | 6 +- .../hermes_cli/test_memory_status_env_hint.py | 2 +- tests/hermes_cli/test_minimax_oauth.py | 2 +- .../test_model_alias_credentials.py | 2 +- tests/hermes_cli/test_model_catalog.py | 3 +- tests/hermes_cli/test_model_cost_guard.py | 4 +- tests/hermes_cli/test_model_flow_local.py | 2 +- .../test_model_picker_excluded_providers.py | 7 +- tests/hermes_cli/test_model_prefix_routing.py | 4 +- .../test_model_switch_custom_providers.py | 82 ++-- .../test_model_switch_reasoning_flag.py | 2 +- ...odel_switch_user_provider_slug_verbatim.py | 2 +- tests/hermes_cli/test_models.py | 22 +- .../test_models_dev_preferred_merge.py | 1 - .../hermes_cli/test_models_relay_base_url.py | 2 +- .../test_multiplex_cli_cache_scope.py | 14 +- .../test_multiplex_host_topology_reporting.py | 2 +- tests/hermes_cli/test_nous_billing_request.py | 2 +- .../test_nous_inference_url_validation.py | 6 +- tests/hermes_cli/test_nous_policy_surfaces.py | 3 +- .../test_nous_portal_staging_allowlist.py | 6 +- .../hermes_cli/test_nous_session_validity.py | 4 +- .../test_oauth_status_pool_observation.py | 12 +- .../test_observational_sessiondb_modes.py | 2 +- tests/hermes_cli/test_old_updater_shims.py | 2 +- tests/hermes_cli/test_old_updater_takeover.py | 2 +- .../hermes_cli/test_oneshot_exit_contract.py | 2 +- .../test_orphan_desktop_serve_reap.py | 27 +- tests/hermes_cli/test_peer_cmd.py | 8 +- tests/hermes_cli/test_picker_prewarm.py | 2 +- .../test_plugin_dependency_consent.py | 2 +- tests/hermes_cli/test_plugin_event_bus.py | 2 +- tests/hermes_cli/test_plugin_events.py | 6 +- .../test_plugin_install_manifest_version.py | 2 +- .../test_plugin_provider_picker_residue.py | 2 +- .../test_plugin_scanner_recursion.py | 2 +- .../test_plugin_uninstall_windows_live.py | 2 +- .../test_plugin_update_transaction.py | 3 +- tests/hermes_cli/test_plugin_validate.py | 2 +- tests/hermes_cli/test_plugins.py | 6 +- .../hermes_cli/test_plugins_check_updates.py | 4 +- tests/hermes_cli/test_plugins_cmd.py | 2 +- tests/hermes_cli/test_plugins_cmd_catalog.py | 2 +- .../test_plugins_cmd_enable_disable_nested.py | 2 +- tests/hermes_cli/test_plugins_cmd_list.py | 2 +- .../test_plugins_hub_live_catalog.py | 6 +- .../hermes_cli/test_plugins_hub_perf_guard.py | 6 +- .../test_plugins_picker_effective_state.py | 2 +- ...test_plugins_transcription_registration.py | 2 +- .../test_plugins_tts_registration.py | 2 +- tests/hermes_cli/test_plugins_update_sync.py | 4 +- ..._pooled_served_profile_backend_unscoped.py | 2 +- tests/hermes_cli/test_post_update.py | 2 +- ...est_process_identity_canonical_matchers.py | 6 +- .../hermes_cli/test_profile_listing_cache.py | 2 +- .../hermes_cli/test_profile_pm_portability.py | 2 +- tests/hermes_cli/test_provider_precedence.py | 2 +- .../test_provider_section3_grouping.py | 2 +- tests/hermes_cli/test_proxy.py | 14 +- tests/hermes_cli/test_proxy_off_loop.py | 24 +- tests/hermes_cli/test_real_home_tripwire.py | 4 +- .../test_relay_shared_metrics_runtime.py | 4 +- tests/hermes_cli/test_resolve_token_memo.py | 2 +- tests/hermes_cli/test_resource_limits.py | 4 +- .../test_restore_source_integrity.py | 2 +- .../hermes_cli/test_runtime_machine_scope.py | 10 +- .../test_runtime_provider_resolution.py | 32 +- tests/hermes_cli/test_safe_mode.py | 2 +- .../test_save_conversation_location.py | 2 +- .../test_secret_source_bootstrap.py | 2 +- .../test_secrets_bitwarden_non_tty.py | 2 +- .../hermes_cli/test_security_audit_startup.py | 2 +- tests/hermes_cli/test_send_cmd.py | 2 +- tests/hermes_cli/test_serve_host_attach.py | 4 +- .../test_serve_mcp_discovery_after_bind.py | 8 +- .../test_serve_runtime_inventory.py | 4 +- .../test_serve_skill_maintenance.py | 2 +- .../test_serve_windows_loop_factory.py | 1 - .../test_served_profile_mirror_platforms.py | 6 +- .../hermes_cli/test_session_skill_previews.py | 4 +- .../hermes_cli/test_sessions_export_md_cli.py | 2 +- .../test_sessions_held_store_gate.py | 2 +- tests/hermes_cli/test_setup_model_provider.py | 2 +- .../hermes_cli/test_setup_profile_answers.py | 2 +- .../hermes_cli/test_setup_provider_catalog.py | 2 +- .../test_setup_summary_provider_warning.py | 2 +- tests/hermes_cli/test_setup_tts_xai_oauth.py | 2 +- .../test_shallow_boundary_repair.py | 2 +- tests/hermes_cli/test_shallow_graft_prune.py | 2 +- .../hermes_cli/test_shared_metrics_desktop.py | 4 +- .../test_shared_metrics_efficiency.py | 6 +- .../test_shared_metrics_engagement.py | 4 +- .../hermes_cli/test_shared_metrics_harness.py | 6 +- .../test_shared_metrics_install_failures.py | 2 +- .../test_shared_metrics_install_run.py | 2 +- .../test_shared_metrics_install_v4.py | 2 +- tests/hermes_cli/test_shared_metrics_loop.py | 4 +- tests/hermes_cli/test_shared_metrics_model.py | 4 +- .../test_shared_metrics_model_attribution.py | 4 +- .../test_shared_metrics_reliability.py | 2 +- .../test_shared_metrics_sender_e2e.py | 4 +- .../hermes_cli/test_shared_metrics_signals.py | 4 +- ...test_shared_metrics_update_stop_reasons.py | 2 +- .../test_sibling_config_migration.py | 4 +- .../test_skill_collision_visibility.py | 4 +- tests/hermes_cli/test_skills_config.py | 2 +- tests/hermes_cli/test_skills_hub.py | 4 +- tests/hermes_cli/test_skin_engine.py | 7 - tests/hermes_cli/test_source_build.py | 2 +- .../test_spawn_gateway_restart_cooldown.py | 4 +- .../test_spawn_gateway_restart_reap.py | 2 +- tests/hermes_cli/test_spotify_auth.py | 2 +- tests/hermes_cli/test_sse_done.py | 2 +- tests/hermes_cli/test_stale_pid_guard.py | 1 - tests/hermes_cli/test_status.py | 32 +- .../hermes_cli/test_status_model_provider.py | 6 +- .../hermes_cli/test_status_report_contract.py | 2 +- tests/hermes_cli/test_stepfun_picker_merge.py | 1 - tests/hermes_cli/test_stt_picker.py | 2 +- tests/hermes_cli/test_subagent_monitor.py | 2 +- tests/hermes_cli/test_subscription_cli.py | 18 +- .../test_taskkill_identity_windows_live.py | 1 - tests/hermes_cli/test_telegram_managed_bot.py | 1 - tests/hermes_cli/test_terminal_breadcrumbs.py | 2 +- .../test_terminal_bridge_profile_scope.py | 2 +- tests/hermes_cli/test_tools_config.py | 10 +- tests/hermes_cli/test_tui_npm_install.py | 2 +- tests/hermes_cli/test_unified_pool_quirk.py | 4 +- tests/hermes_cli/test_uninstall_dry_run.py | 2 +- .../test_uninstall_full_wipe_sweep.py | 6 +- .../test_uninstall_gateway_service.py | 4 +- .../hermes_cli/test_uninstall_gui_userdata.py | 6 +- .../hermes_cli/test_uninstall_install_kind.py | 4 +- tests/hermes_cli/test_uninstall_leftovers.py | 2 +- .../test_uninstall_node_symlinks.py | 2 +- tests/hermes_cli/test_update_autostash.py | 6 +- .../test_update_behind_count_recovery.py | 2 +- .../hermes_cli/test_update_channel_stable.py | 4 +- ...test_update_cold_start_gateway_liveness.py | 2 +- .../test_update_completion_routing.py | 2 +- .../test_update_concurrent_quarantine.py | 8 +- ...test_update_config_migration_on_current.py | 2 +- .../test_update_existing_branch_baseline.py | 2 +- tests/hermes_cli/test_update_fetch_timeout.py | 2 +- tests/hermes_cli/test_update_finish_stamp.py | 4 +- .../test_update_fleet_checkout_build_stamp.py | 2 +- .../test_update_fleet_restart_pending.py | 8 +- .../test_update_fleet_snapshot_settle.py | 2 +- .../test_update_gateway_launcher_refresh.py | 2 +- .../test_update_gateway_restart_fallback.py | 2 +- .../hermes_cli/test_update_head_moved_gate.py | 4 +- .../hermes_cli/test_update_host_obligation.py | 2 +- .../test_update_launchd_fleet_restart.py | 2 +- ...est_update_launchd_restart_verification.py | 2 +- .../test_update_list_venv_holders.py | 2 +- .../test_update_missing_configured_deps.py | 4 +- .../hermes_cli/test_update_modified_notice.py | 2 +- .../test_update_narrow_clone_refspec.py | 2 +- .../test_update_no_gateway_restart.py | 2 +- .../test_update_parked_branch_guard.py | 8 +- tests/hermes_cli/test_update_posix_pause.py | 6 +- .../test_update_post_commit_followups.py | 6 +- .../test_update_receipt_endpoint.py | 2 +- .../test_update_stale_gateway_yield.py | 2 +- .../test_update_venv_holder_retirement.py | 6 +- ...st_update_windows_cross_install_gateway.py | 2 +- .../test_update_zip_fallback_guards.py | 2 +- tests/hermes_cli/test_urllib_security.py | 8 +- .../test_user_providers_model_switch.py | 26 +- .../test_ux_messages_chat_sessions.py | 2 +- tests/hermes_cli/test_venv_sync_currency.py | 2 +- tests/hermes_cli/test_video_gen_picker.py | 4 +- tests/hermes_cli/test_voice_wrapper.py | 22 +- tests/hermes_cli/test_web_git_gh_stderr.py | 2 +- .../test_web_memory_provider_setup_install.py | 2 +- .../test_web_multi_profile_scope.py | 4 +- .../test_web_profile_delete_settlement.py | 2 +- .../test_web_profile_soul_writes.py | 2 +- .../test_web_profiles_model_scope.py | 4 +- .../hermes_cli/test_web_profiles_off_loop.py | 2 +- .../test_web_recommended_default_profile.py | 4 +- ...t_web_routers_destructive_profile_scope.py | 6 +- .../test_web_routers_status_health.py | 2 +- ...est_web_routers_tools_install_on_enable.py | 28 +- tests/hermes_cli/test_web_server.py | 31 +- .../hermes_cli/test_web_server_asset_mime.py | 2 +- .../test_web_server_gateway_topology.py | 2 +- tests/hermes_cli/test_web_server_idle_exit.py | 2 +- .../hermes_cli/test_web_server_idle_proof.py | 2 +- .../test_web_server_messaging_profiles.py | 2 +- .../test_web_server_profile_unification.py | 25 +- tests/hermes_cli/test_web_server_skew_exit.py | 2 +- .../test_web_server_skills_profiles.py | 4 +- .../test_web_server_speak_stream.py | 2 +- tests/hermes_cli/test_web_server_ws_ping.py | 1 - tests/hermes_cli/test_web_ui_build.py | 2 +- .../test_whatsapp_missing_runtime.py | 2 +- ...ws_gateway_cold_start_desktop_lifecycle.py | 6 +- .../test_windows_gateway_job_teardown.py | 2 +- ...t_windows_update_restart_reconciliation.py | 2 +- tests/hermes_cli/test_worktree_pushed_tier.py | 4 +- tests/hermes_cli/test_worktree_security.py | 2 +- tests/hermes_cli/test_worktree_selfheal.py | 2 +- .../hermes_cli/test_xai_oauth_profile_auth.py | 2 +- tests/hermes_platform/test_resolver_app.py | 4 +- .../test_fts_rebuild_admission.py | 8 +- tests/hermes_state/test_hermes_state.py | 2 +- .../test_no_locked_readers_gate.py | 4 +- .../test_session_db_read_conn_pool.py | 4 +- .../test_state_db_repair_non_destructive.py | 1 - .../test_writer_conn_thread_safety.py | 4 +- tests/home_io_guard.py | 2 +- tests/manual/cron_inchannel_dm_e2e.py | 8 +- tests/manual/cron_inchannel_e2e.py | 8 +- tests/monitoring/test_emitter.py | 4 +- .../dashboard_auth/test_basic_provider.py | 6 +- .../dashboard_auth/test_drain_provider.py | 6 +- .../test_jwt_clock_skew_leeway.py | 8 +- .../dashboard_auth/test_nous_provider.py | 8 +- .../test_opaque_bearer_not_unreachable.py | 2 +- .../test_self_hosted_provider.py | 10 +- .../image_gen/test_deepinfra_provider.py | 2 +- .../test_openrouter_compat_provider.py | 2 +- tests/plugins/memory/test_config_schema.py | 6 +- .../test_holographic_profile_db_path.py | 2 +- .../plugins/memory/test_holographic_store.py | 2 +- .../test_multiplex_memory_identity_scope.py | 2 +- .../test_provider_threads_inherit_profile.py | 4 +- .../plugins/memory/test_retaindb_provider.py | 4 +- .../test_commandcode_profile.py | 4 +- .../model_providers/test_copilot_profile.py | 2 +- .../model_providers/test_custom_profile.py | 2 +- .../model_providers/test_deepinfra_profile.py | 2 +- .../model_providers/test_deepseek_profile.py | 2 +- .../model_providers/test_fireworks_profile.py | 2 +- .../model_providers/test_gemini_profile.py | 2 +- .../model_providers/test_kimi_profile.py | 4 +- .../model_providers/test_minimax_profile.py | 10 +- .../model_providers/test_nous_profile.py | 2 +- .../test_ollama_cloud_profile.py | 2 +- .../test_opencode_go_profile.py | 4 +- .../model_providers/test_upstage_profile.py | 2 +- .../model_providers/test_zai_profile.py | 2 +- .../test_buzz_unscoped_requirement_gate.py | 22 +- tests/plugins/platforms/photon/test_auth.py | 12 +- .../photon/test_fatal_notify_self_cancel.py | 2 +- .../plugins/platforms/photon/test_inbound.py | 20 +- .../plugins/platforms/photon/test_markdown.py | 12 +- .../platforms/photon/test_mention_gating.py | 6 +- .../platforms/photon/test_outbound_media.py | 12 +- .../photon/test_overflow_recovery.py | 14 +- .../platforms/photon/test_poll_clarify.py | 16 +- .../platforms/photon/test_reactions.py | 14 +- .../platforms/photon/test_rich_links.py | 26 +- .../platforms/photon/test_runtime_record.py | 4 +- .../photon/test_sidecar_lifecycle.py | 14 +- .../platforms/photon/test_sidecar_paths.py | 2 +- .../platforms/photon/test_threaded_replies.py | 18 +- .../platforms/photon/test_url_send_path.py | 6 +- .../photon/test_zombie_stream_watchdog.py | 6 +- tests/plugins/test_a2a_plugin.py | 4 +- tests/plugins/test_achievements_plugin.py | 8 +- tests/plugins/test_chronos_cron.py | 2 +- tests/plugins/test_google_meet_realtime.py | 4 +- tests/plugins/test_kanban_dashboard_plugin.py | 2 +- tests/plugins/test_langfuse_plugin.py | 12 +- .../test_multiplex_plugin_cache_scope.py | 2 +- .../test_plugins_manage_profile_scope.py | 4 +- .../test_secret_scope_plugin_families.py | 8 +- .../plugins/test_security_guidance_plugin.py | 2 +- .../video_gen/test_xai_plugin_integration.py | 8 +- tests/pm/_range_server.py | 4 +- tests/pm/test_activation_setup.py | 2 +- tests/pm/test_artifact_mirror.py | 2 +- tests/pm/test_download_mirrors.py | 2 +- tests/pm/test_download_progress.py | 2 +- tests/pm/test_download_retry_limits.py | 2 +- tests/pm/test_download_serial_fallback.py | 2 +- tests/pm/test_downloader_integrity.py | 2 +- tests/pm/test_environment_build.py | 4 +- tests/pm/test_extras.py | 4 +- tests/pm/test_feature_inventory.py | 4 +- tests/pm/test_features.py | 2 +- tests/pm/test_generation_durability.py | 2 +- tests/pm/test_held_entry_windows_live.py | 8 +- tests/pm/test_install_default_closure.py | 1 - tests/pm/test_install_download_control.py | 2 +- tests/pm/test_install_extra.py | 4 +- tests/pm/test_installed_package.py | 2 +- tests/pm/test_network_retries.py | 4 +- tests/pm/test_npm_dist_tags.py | 4 +- tests/pm/test_plugin_survival_contract.py | 2 +- tests/pm/test_pm_authority.py | 6 +- tests/pm/test_pm_core.py | 8 +- tests/pm/test_receipt.py | 4 +- tests/pm/test_recovery.py | 6 +- tests/pm/test_runtime_bootstrap_tls.py | 2 +- tests/pm/test_runtime_entrypoints.py | 2 +- .../test_runtime_journal_concurrent_edit.py | 2 +- tests/pm/test_setup_lock_format.py | 2 +- tests/pm/test_source_update_launch.py | 4 +- tests/pm/test_startup_recovery.py | 2 +- tests/pm/test_store_extensions.py | 4 +- tests/pm/test_store_resume.py | 2 +- tests/pm/test_termux_libs.py | 2 +- tests/pm/test_update_artifact_refresh.py | 2 +- tests/pm/test_update_request_reuse.py | 2 +- tests/pm/test_uv_cache.py | 2 +- tests/pm/test_uv_python.py | 14 +- tests/pm/test_worker.py | 4 +- tests/pm/test_worker_publication.py | 2 +- tests/pm/test_worker_registry.py | 4 +- tests/pm/test_workspace.py | 2 +- tests/providers/fake_pkce_idp.py | 6 +- ...t_desktop_update_posix_handoff_protocol.py | 4 +- .../desktop_update/windows_handoff_support.py | 2 +- .../test_install_desktop_product_present.py | 2 +- tests/scripts/test_appinstaller.py | 2 +- tests/scripts/test_archive_inputs.py | 2 +- tests/scripts/test_baked_bytecode.py | 2 +- tests/scripts/test_bundle_native.py | 2 +- tests/scripts/test_contributor_map.py | 4 +- tests/scripts/test_desktop_cli_wrapper.py | 2 +- tests/scripts/test_release_artifacts.py | 4 +- tests/scripts/test_release_canary_git.py | 2 +- tests/scripts/test_release_darwin.py | 2 +- tests/scripts/test_release_docker_cli.py | 2 +- tests/scripts/test_release_handoff.py | 2 +- .../scripts/test_render_builds_publication.py | 2 +- .../scripts/test_run_tests_parallel_stdio.py | 2 +- tests/scripts/test_setup_toolchain.py | 2 +- tests/scripts/test_source_build_env.py | 2 +- tests/scripts/test_stable_release.py | 2 +- tests/scripts/test_stage_apt_repo.py | 2 +- tests/scripts/test_termux_build_wheels.py | 4 +- tests/scripts/test_termux_retag_wheel.py | 2 +- tests/scripts/test_termux_runtime_libs.py | 2 +- .../test_secret_source_registry.py | 8 +- tests/skills/test_authoring_standards.py | 4 +- tests/skills/test_reddit_reading_skill.py | 2 +- tests/skills/test_rss_feeds_skill.py | 2 +- tests/skills/test_skill_document_contracts.py | 4 +- tests/skills/test_unbroker_skill.py | 34 +- tests/test_agent_close_lifecycle.py | 2 +- tests/test_hermes_logging.py | 2 +- tests/test_managed_runtime_resolution.py | 2 +- tests/test_mcp_serve.py | 4 +- tests/test_packaging_build_guard.py | 4 +- tests/test_utils_no_proxy_sanitize.py | 2 +- .../test_local_path_completion.py | 6 +- tests/tools/test_approval_deny_rules.py | 2 +- tests/tools/test_approval_outcome_parity.py | 4 +- tests/tools/test_async_delegation.py | 4 +- tests/tools/test_audio_container.py | 2 +- tests/tools/test_base_environment.py | 2 +- tests/tools/test_bot_desktop_browser.py | 6 +- tests/tools/test_bot_desktop_browser_fence.py | 4 +- tests/tools/test_bot_desktop_lease.py | 1 - tests/tools/test_bot_desktop_placement.py | 2 +- tests/tools/test_bot_desktop_resources.py | 4 +- tests/tools/test_bot_desktop_runtime.py | 4 +- tests/tools/test_bot_mode_dm.py | 2 - tests/tools/test_bot_relay_windows_paths.py | 4 +- tests/tools/test_browser_cdp_override.py | 8 +- tests/tools/test_browser_cdp_tool.py | 6 +- tests/tools/test_browser_cloud_fallback.py | 2 +- .../test_browser_cloud_provider_cache.py | 6 +- .../test_browser_env_loopback_no_proxy.py | 2 +- .../test_browser_extension_router_wiring.py | 4 +- tests/tools/test_browser_hybrid_routing.py | 2 +- tests/tools/test_browser_lightpanda_serve.py | 4 +- tests/tools/test_browser_open_timeout.py | 2 +- tests/tools/test_browser_real_profile.py | 14 +- tests/tools/test_browser_real_profile_pin.py | 6 +- .../tools/test_browser_supervisor_capture.py | 2 +- .../test_browser_supervisor_healthcheck.py | 2 +- tests/tools/test_browser_suspect_recycle.py | 2 +- tests/tools/test_browser_use_cli.py | 16 +- .../tools/test_browser_use_session_expiry.py | 2 +- tests/tools/test_browser_vault.py | 8 +- tests/tools/test_build_subprocess_env.py | 2 +- tests/tools/test_code_execution.py | 6 +- tests/tools/test_code_execution_file_rpc.py | 6 +- tests/tools/test_code_execution_modes.py | 2 +- tests/tools/test_computer_use.py | 16 +- .../tools/test_computer_use_ax_walk_bound.py | 4 +- .../test_computer_use_cua_0_10_permissions.py | 14 +- tests/tools/test_computer_use_cua_0_9.py | 8 +- .../test_computer_use_cua_macos_identity.py | 2 +- .../test_computer_use_delivery_ladder.py | 8 +- ..._computer_use_empty_discovery_diagnosis.py | 2 +- .../test_computer_use_fullscreen_capture.py | 8 +- .../test_computer_use_native_platform.py | 2 +- .../test_computer_use_placeholder_ids.py | 2 +- tests/tools/test_connector_bridge_wiring.py | 2 +- tests/tools/test_connector_dispatch_policy.py | 2 +- tests/tools/test_connector_local_batches.py | 1 - tests/tools/test_connectors_managed.py | 4 - tests/tools/test_connectors_mcp.py | 13 +- tests/tools/test_cronjob_tools.py | 2 +- tests/tools/test_daemon_pool.py | 2 +- tests/tools/test_daytona_environment.py | 2 +- tests/tools/test_delegate_child_cache_ttl.py | 4 +- tests/tools/test_delegate_kanban_isolation.py | 8 +- tests/tools/test_delegate_native_vision.py | 2 +- tests/tools/test_denial_circuit_breaker.py | 4 +- tests/tools/test_docker_environment.py | 20 +- tests/tools/test_docker_network_config.py | 6 +- .../test_docker_orphan_reaper_integration.py | 2 +- tests/tools/test_docker_session_isolation.py | 2 +- tests/tools/test_docker_teardown_drain.py | 2 +- tests/tools/test_env_probe.py | 2 +- .../test_execute_code_approval_cluster.py | 4 +- ...t_execute_code_lifecycle_guard_deadline.py | 4 +- tests/tools/test_file_operations.py | 4 +- tests/tools/test_file_ops_single_roundtrip.py | 12 +- tests/tools/test_file_read_guards.py | 2 +- tests/tools/test_file_sync_sigint.py | 2 +- tests/tools/test_file_tools.py | 2 +- .../tools/test_file_tools_container_config.py | 2 +- tests/tools/test_file_tools_cwd_resolution.py | 2 +- .../test_file_tools_plugin_container_cwd.py | 2 +- tests/tools/test_file_tools_tilde_profile.py | 2 +- .../tools/test_finite_delegation_outcomes.py | 2 +- tests/tools/test_fuzzy_match.py | 4 +- tests/tools/test_hub_lock_non_utf8.py | 2 +- .../test_image_generation_image_to_image.py | 4 +- tests/tools/test_interrupt.py | 2 +- tests/tools/test_kanban_persisted_identity.py | 2 +- tests/tools/test_kanban_tools.py | 2 +- tests/tools/test_local_env_blocklist.py | 22 +- tests/tools/test_local_interrupt_cleanup.py | 4 +- tests/tools/test_local_temp_dir.py | 2 +- tests/tools/test_macos_protected_search.py | 2 +- .../test_managed_browserbase_and_modal.py | 2 +- tests/tools/test_managed_media_gateways.py | 2 +- tests/tools/test_mcp_cimd.py | 2 +- tests/tools/test_mcp_circuit_breaker.py | 3 +- tests/tools/test_mcp_config_change_rebind.py | 2 +- tests/tools/test_mcp_connect_secret_scope.py | 2 +- tests/tools/test_mcp_dashboard_oauth.py | 2 +- tests/tools/test_mcp_death_supervisor.py | 2 +- tests/tools/test_mcp_elicitation.py | 4 +- tests/tools/test_mcp_google_offline_access.py | 2 +- tests/tools/test_mcp_liveness.py | 18 +- tests/tools/test_mcp_liveness_catalog.py | 2 +- .../test_mcp_multiplex_connection_keys.py | 6 +- tests/tools/test_mcp_oauth_bidirectional.py | 2 +- tests/tools/test_mcp_oauth_callback_latch.py | 2 +- tests/tools/test_mcp_oauth_issuer_binding.py | 6 +- tests/tools/test_mcp_oauth_issuer_origin.py | 2 +- tests/tools/test_mcp_oauth_manager.py | 6 +- tests/tools/test_mcp_oauth_metadata.py | 3 +- tests/tools/test_mcp_oauth_missing_iss.py | 8 +- tests/tools/test_mcp_oauth_user_agent.py | 8 +- .../tools/test_mcp_preflight_content_type.py | 6 +- tests/tools/test_mcp_protocol_negotiation.py | 2 +- tests/tools/test_mcp_reconnect_retry_reset.py | 1 - tests/tools/test_mcp_resource_content.py | 4 +- .../test_mcp_stdio_fastfail_reconnect.py | 2 +- tests/tools/test_mcp_timeout_resolution.py | 2 +- tests/tools/test_mcp_tool.py | 13 +- tests/tools/test_mcp_tool_401_handling.py | 2 +- tests/tools/test_mcp_tool_issue_948.py | 1 - tests/tools/test_mcp_windows_orphan_fix.py | 2 +- tests/tools/test_memory_tool.py | 2 +- tests/tools/test_modal_sandbox_fixes.py | 2 +- tests/tools/test_modal_snapshot_isolation.py | 2 +- tests/tools/test_multiplex_tool_memo_scope.py | 2 +- tests/tools/test_parse_env_var.py | 2 +- .../test_permanent_allowlist_reconcile.py | 2 +- tests/tools/test_read_shell_line_clamp.py | 3 +- tests/tools/test_refresh_agent_mcp_tools.py | 2 +- tests/tools/test_remote_binary_write_guard.py | 4 +- tests/tools/test_request_tool_approval.py | 4 +- .../test_safe_getcwd_permission_error.py | 2 +- tests/tools/test_schema_sanitizer.py | 2 +- tests/tools/test_search_budget_truncation.py | 2 +- .../test_search_files_engine_selection.py | 2 +- .../test_search_zero_match_and_multipath.py | 2 +- .../test_send_message_relay_target_authz.py | 7 +- tests/tools/test_skill_create_dir.py | 2 +- tests/tools/test_skill_ledger_delta.py | 2 +- tests/tools/test_skillevaluator_scan.py | 2 +- tests/tools/test_skills_guard.py | 2 +- tests/tools/test_skills_hub.py | 8 +- .../tools/test_skills_tool_discovery_cache.py | 2 +- tests/tools/test_skills_tool_profile_scope.py | 2 +- tests/tools/test_spawn_site_child_env.py | 2 +- tests/tools/test_ssh_environment.py | 4 +- tests/tools/test_ssh_remote_cwd.py | 4 +- tests/tools/test_ssh_session_cwd.py | 2 +- .../tools/test_startup_latency_regressions.py | 6 +- tests/tools/test_subagent_worktree.py | 2 +- ...est_terminal_backend_unavailable_reason.py | 2 +- tests/tools/test_terminal_degraded_mode.py | 8 +- tests/tools/test_terminal_env_bridge.py | 2 +- tests/tools/test_terminal_error_redaction.py | 4 +- tests/tools/test_terminal_scope_multiplex.py | 2 +- tests/tools/test_terminal_self_repo_guard.py | 2 +- tests/tools/test_terminal_task_cwd.py | 4 +- tests/tools/test_terminal_tool.py | 4 +- .../test_terminal_tool_exception_redaction.py | 2 +- tests/tools/test_tool_result_storage.py | 4 +- tests/tools/test_tool_search.py | 4 +- tests/tools/test_tool_search_multiquery.py | 4 +- tests/tools/test_transcription_tools.py | 4 +- .../test_truncation_footer_sandbox_paths.py | 6 +- tests/tools/test_tts_deepinfra.py | 2 +- tests/tools/test_tts_lifecycle_leases.py | 2 +- tests/tools/test_tts_macos_output.py | 2 +- .../test_tts_output_dir_profile_scope.py | 2 +- tests/tools/test_tts_path_traversal.py | 4 +- tests/tools/test_tts_provider_authority.py | 2 +- tests/tools/test_url_safety.py | 2 +- .../tools/test_vercel_sandbox_environment.py | 2 +- tests/tools/test_video_generation_dispatch.py | 8 +- .../test_video_generation_dynamic_schema.py | 8 +- ...st_video_generation_tool_surface_matrix.py | 4 +- tests/tools/test_vision_scale_disclosure.py | 6 +- tests/tools/test_voice_cli_integration.py | 1 - .../test_voice_credential_pool_resolution.py | 2 +- tests/tools/test_voice_mode.py | 2 +- tests/tools/test_voice_wsl_pipewire.py | 2 +- tests/tools/test_wake_word.py | 4 +- tests/tools/test_wake_word_stream_recovery.py | 2 - tests/tools/test_web_extract_robustness.py | 2 +- tests/tools/test_web_keyless_fallback.py | 6 +- tests/tools/test_web_keyless_rescue.py | 2 +- tests/tools/test_web_picker_active_rows.py | 4 +- tests/tools/test_web_providers.py | 4 +- tests/tools/test_web_providers_brave_free.py | 2 +- tests/tools/test_web_providers_ddgs.py | 2 +- tests/tools/test_web_providers_searxng.py | 6 +- tests/tools/test_web_providers_xai.py | 2 +- tests/tools/test_web_result_cache.py | 2 +- .../test_windows_agent_loop_papercuts.py | 1 - tests/tools/test_xai_http_credentials.py | 2 +- tests/tools/test_zombie_process_cleanup.py | 2 +- tests/tui_gateway/conftest.py | 2 +- tests/tui_gateway/contracts/test_generated.py | 10 +- tests/tui_gateway/test_auto_continue.py | 4 +- .../test_background_thread_profile_scope.py | 2 +- tests/tui_gateway/test_billing_rpc.py | 8 +- tests/tui_gateway/test_change_watcher.py | 2 +- .../test_change_watcher_sessions.py | 10 +- .../test_compression_config_hot_reload.py | 2 +- .../test_compute_host_borrowed_lease.py | 2 +- .../tui_gateway/test_config_profile_scope.py | 2 +- .../test_config_set_session_profile_scope.py | 2 +- .../test_cross_process_orphan_ownership.py | 2 +- ...est_custom_provider_session_persistence.py | 16 +- tests/tui_gateway/test_display_methods.py | 18 +- tests/tui_gateway/test_display_watch.py | 16 +- tests/tui_gateway/test_empty_fast_status.py | 2 +- .../tui_gateway/test_entry_picker_prewarm.py | 1 - .../test_external_process_picker.py | 4 +- tests/tui_gateway/test_fast_session_scope.py | 2 +- tests/tui_gateway/test_free_tier_task_done.py | 2 +- tests/tui_gateway/test_goal_command.py | 2 +- .../tui_gateway/test_gui_surface_toolsets.py | 2 +- .../tui_gateway/test_history_inline_images.py | 2 +- .../test_hosted_room_prompt_fence.py | 2 +- tests/tui_gateway/test_i18n_methods.py | 4 +- .../tui_gateway/test_kanban_notify_poller.py | 2 +- tests/tui_gateway/test_kanban_resume_guard.py | 4 +- .../test_launch_profile_scope_plugin_hooks.py | 3 +- .../test_local_model_session_identity.py | 4 +- .../test_mcp_late_refresh_thread_owner.py | 2 +- .../test_mcp_oauth_client_callback.py | 2 +- tests/tui_gateway/test_mcp_profile_rpcs.py | 10 +- tests/tui_gateway/test_mcp_reload_rev.py | 2 +- .../test_model_options_profile_scope.py | 2 +- .../tui_gateway/test_model_rpc_skew_guard.py | 8 +- .../test_model_switch_reasoning_flag.py | 2 +- .../test_multi_profile_hosting_fail_closed.py | 4 +- .../test_multi_profile_hosting_transitions.py | 2 +- .../tui_gateway/test_none_agent_turn_guard.py | 2 +- ...test_personality_clobbers_system_prompt.py | 2 +- tests/tui_gateway/test_plugin_skill_slash.py | 2 +- .../test_plugins_manage_install.py | 8 +- .../test_preview_action_bridge_fail_fast.py | 2 +- tests/tui_gateway/test_profile_db_readonly.py | 2 +- tests/tui_gateway/test_profiles_configure.py | 2 +- .../test_profiles_create_env_refs.py | 2 +- .../test_profiles_describe_secret_scope.py | 2 +- tests/tui_gateway/test_project_tree.py | 1 - tests/tui_gateway/test_projects_rpc.py | 8 +- .../tui_gateway/test_prompt_accept_logging.py | 4 +- tests/tui_gateway/test_protocol.py | 14 +- .../test_reasoning_session_scope.py | 4 +- .../tui_gateway/test_resume_readonly_mount.py | 2 +- .../test_secondary_profile_model_and_key.py | 2 +- .../test_seeded_branch_title_routing.py | 2 +- .../test_served_profile_child_env.py | 4 +- tests/tui_gateway/test_session_agent_cwd.py | 2 +- tests/tui_gateway/test_session_archive_rpc.py | 2 +- ...est_session_create_model_provider_guard.py | 2 +- tests/tui_gateway/test_session_cwd_follow.py | 4 +- .../test_session_cwd_profile_key.py | 4 +- .../test_session_db_ownership_teardown.py | 4 +- .../test_session_finalize_reaps_kernels.py | 2 +- .../test_session_git_metadata_generation.py | 2 +- tests/tui_gateway/test_session_hidden_rpc.py | 2 +- .../test_session_history_codex_sidecar.py | 6 +- .../tui_gateway/test_session_id_injection.py | 2 +- .../test_session_list_lineage_rpc.py | 2 +- .../test_shared_metrics_consent_rpc.py | 2 +- .../test_shared_metrics_slash_command_rpc.py | 2 +- .../test_shared_metrics_update_run_rpc.py | 2 +- tests/tui_gateway/test_slash_fuzzy.py | 2 +- .../tui_gateway/test_slash_worker_provider.py | 2 +- .../test_ssh_session_cwd_persistence.py | 2 +- .../test_stale_provider_resume_live.py | 2 +- .../tui_gateway/test_startup_orphan_sweep.py | 2 +- .../test_submit_row_session_target.py | 2 +- tests/tui_gateway/test_todo_state_events.py | 2 +- .../tui_gateway/test_tools_readback_scope.py | 2 +- .../tui_gateway/test_tour_bridge_fail_fast.py | 2 +- tests/tui_gateway/test_tui_gateway_server.py | 69 ++- tests/tui_gateway/test_tui_gateway_ws.py | 2 +- .../test_wait_for_mcp_discovery.py | 2 +- tests/tui_gateway/test_ws_orphan_races.py | 8 +- .../test_ledger_and_nudge_integration.py | 2 +- tests/website/test_catalog_rules_mirror.py | 2 +- tools/approval_gateway_wait.py | 2 +- tools/approval_human_wait.py | 2 +- tools/arg_coercion.py | 2 +- tools/async_delegation.py | 102 ++--- tools/async_delegation_recovery_hints.py | 2 +- tools/blueprints.py | 18 +- tools/bot_desktop/browser.py | 2 +- tools/bot_desktop/install.py | 2 +- tools/bot_desktop/lease.py | 10 +- tools/bot_desktop/runtime.py | 16 +- tools/bot_desktop/sandbox_host.py | 24 +- tools/bot_mode_dm.py | 2 +- tools/bot_mode_probe.py | 2 +- tools/browser_camofox.py | 48 +-- tools/browser_camofox_state.py | 2 +- tools/browser_cdp_tool.py | 32 +- tools/browser_dialog_tool.py | 2 +- tools/browser_extension_router.py | 6 +- tools/browser_lightpanda.py | 4 +- tools/browser_supervisor.py | 46 +- tools/browser_supervisor_capture.py | 8 +- tools/browser_supervisor_dialogs.py | 18 +- tools/browser_supervisor_frames.py | 20 +- tools/browser_tool.py | 68 +-- tools/browser_tool_cdp.py | 2 +- tools/browser_tool_eval_policy.py | 22 +- tools/browser_tool_lifecycle.py | 8 +- tools/browser_tool_lightpanda_fallback.py | 16 +- tools/browser_tool_real_profile.py | 4 +- tools/browser_tool_session.py | 72 ++-- tools/browser_tool_snapshot.py | 2 +- tools/browser_tool_vision.py | 6 +- tools/browser_use_cli.py | 6 +- tools/browser_vault_tool.py | 18 +- tools/budget_config.py | 4 +- tools/checkpoint_maintenance.py | 20 +- tools/checkpoint_manager.py | 76 ++-- tools/checkpoint_manager_profile_rename.py | 6 +- tools/clarify_gateway.py | 16 +- tools/clarify_tool.py | 6 +- tools/code_execution_env.py | 8 +- tools/code_execution_tool.py | 26 +- tools/code_kernel.py | 44 +- tools/code_kernel_remote.py | 22 +- tools/computer_use/backend.py | 24 +- tools/computer_use/cua_backend.py | 30 +- tools/computer_use/cua_backend_capture.py | 46 +- tools/computer_use/cua_backend_daemon.py | 16 +- tools/computer_use/cua_backend_driver.py | 18 +- tools/computer_use/cua_backend_input.py | 20 +- tools/computer_use/cua_backend_parse.py | 50 +-- tools/computer_use/cua_backend_session.py | 20 +- tools/computer_use/doctor.py | 24 +- tools/computer_use/permissions.py | 10 +- tools/computer_use/schema.py | 6 +- tools/computer_use/tool.py | 60 +-- tools/connectors/catalog.py | 44 +- tools/connectors/catalog_tool.py | 18 +- tools/connectors/contract.py | 4 +- tools/connectors/live.py | 6 +- tools/connectors/managed.py | 16 +- tools/connectors/mcp.py | 74 ++-- tools/connectors/mcp_oauth.py | 12 +- tools/connectors/operation.py | 32 +- tools/connectors/run.py | 4 +- tools/connectors/search.py | 22 +- tools/connectors/targets.py | 12 +- tools/connectors/tool.py | 4 +- tools/credential_files.py | 34 +- tools/cronjob_job_args.py | 30 +- tools/cronjob_tools.py | 62 +-- tools/debug_helpers.py | 4 +- tools/delegate_tool.py | 44 +- tools/delegate_tool_child_run.py | 60 +-- tools/delegate_tool_config.py | 10 +- tools/delegate_tool_dispatch.py | 24 +- tools/delegate_tool_progress.py | 14 +- tools/delegate_tool_registry.py | 16 +- tools/delegate_tool_results.py | 24 +- tools/delegate_tool_tasks.py | 22 +- tools/delegate_tool_toolsets.py | 8 +- tools/delegation_live_log.py | 22 +- tools/delegation_output_schema.py | 8 +- tools/desktop_ui.py | 2 +- tools/discord_tool.py | 48 +-- tools/environments/base.py | 1 - tools/environments/file_sync.py | 2 +- tools/environments/local.py | 4 +- tools/environments/local_env_policy.py | 4 +- tools/environments/managed_modal.py | 8 +- tools/environments/remote_common.py | 2 +- tools/fal_common.py | 12 +- tools/feishu_doc_tool.py | 2 +- tools/feishu_drive_tool.py | 2 +- tools/file_operations.py | 6 +- tools/file_operations_common.py | 28 +- tools/file_operations_lint.py | 24 +- tools/file_operations_search.py | 54 +-- tools/file_state.py | 20 +- tools/file_tools.py | 6 +- tools/file_tools_paths.py | 2 +- tools/hook_output_spill.py | 6 +- tools/image_generation_catalog.py | 8 +- tools/image_generation_managed.py | 6 +- tools/image_generation_tool.py | 54 +-- tools/image_source.py | 6 +- tools/mcp_oauth.py | 8 +- tools/mcp_oauth_manager.py | 2 +- tools/mcp_schema_cache.py | 14 +- tools/mcp_tool.py | 85 ++-- tools/mcp_tool_agent.py | 8 +- tools/mcp_tool_config.py | 18 +- tools/mcp_tool_content.py | 12 +- tools/mcp_tool_discovery.py | 44 +- tools/mcp_tool_errors.py | 11 +- tools/mcp_tool_handlers.py | 14 +- tools/mcp_tool_lifecycle.py | 18 +- tools/mcp_tool_node_abi.py | 2 +- tools/mcp_tool_registration.py | 38 +- tools/mcp_tool_sampling.py | 8 +- tools/mcp_tool_schema.py | 6 +- tools/mcp_tool_scope.py | 2 +- tools/mcp_tool_transport.py | 10 +- tools/memory_tool.py | 36 +- tools/memory_tool_store.py | 66 +-- tools/open_preview_tool.py | 4 +- tools/osv_check.py | 8 +- tools/patch_parser.py | 42 +- tools/plugin_guard.py | 18 +- tools/process_registry.py | 32 +- tools/process_registry_checkpoint.py | 4 +- tools/process_registry_termination.py | 8 +- tools/read_extract.py | 2 +- tools/registry.py | 92 ++-- tools/send_message_tool.py | 12 +- tools/session_search_tool.py | 38 +- tools/skill_ledger.py | 50 +-- tools/skill_linter.py | 16 +- tools/skill_manager_batch.py | 6 +- tools/skill_manager_guards.py | 10 +- tools/skill_manager_tool.py | 32 +- tools/skill_usage.py | 72 ++-- tools/skillevaluator_scan.py | 16 +- tools/skills_ast_audit.py | 14 +- tools/skills_guard.py | 26 +- tools/skills_hub.py | 18 +- tools/skills_hub_clawhub.py | 44 +- tools/skills_hub_github.py | 42 +- tools/skills_hub_install.py | 12 +- tools/skills_hub_models.py | 22 +- tools/skills_hub_official.py | 34 +- tools/skills_hub_search.py | 32 +- tools/skills_hub_skillssh.py | 20 +- tools/skills_hub_sources.py | 22 +- tools/skills_sync.py | 32 +- tools/skills_sync_bundled_ops.py | 12 +- tools/skills_sync_optional.py | 30 +- tools/skills_tool.py | 22 +- tools/skills_tool_dedup.py | 2 +- tools/skills_tool_plugin.py | 10 +- tools/skills_tool_setup.py | 14 +- tools/slash_confirm.py | 6 +- tools/stt_lease.py | 12 +- tools/subagent_worktree.py | 14 +- tools/terminal_hints.py | 2 +- tools/terminal_scope.py | 18 +- tools/terminal_tool.py | 36 +- tools/terminal_tool_backends.py | 26 +- tools/terminal_tool_background.py | 2 +- tools/terminal_tool_guards.py | 2 +- tools/terminal_tool_lifecycle.py | 4 +- tools/threat_patterns.py | 10 +- tools/todo_tool.py | 32 +- tools/tool_backend_helpers.py | 6 +- tools/tool_labels.py | 6 +- tools/tool_output_limits.py | 4 +- tools/tool_result_storage.py | 2 +- tools/tool_search.py | 64 +-- tools/tool_search_catalog.py | 42 +- tools/tool_search_validation.py | 8 +- tools/transcription_audio.py | 18 +- tools/transcription_chunking.py | 22 +- tools/transcription_cloud.py | 34 +- tools/transcription_command.py | 26 +- tools/transcription_common.py | 6 +- tools/transcription_local.py | 12 +- tools/transcription_streaming.py | 42 +- tools/transcription_tools.py | 36 +- tools/tts_command_provider.py | 30 +- tools/tts_streaming.py | 24 +- tools/tts_tool.py | 36 +- tools/tts_tool_delivery.py | 42 +- tools/tts_tool_lifecycle.py | 20 +- tools/tts_tool_local.py | 18 +- tools/tts_tool_openai.py | 10 +- tools/tts_tool_plugins.py | 12 +- tools/tts_tool_providers.py | 38 +- tools/tts_tool_speaker.py | 8 +- tools/video_generation_tool.py | 16 +- tools/vision_tools.py | 12 +- tools/voice_client_config.py | 22 +- tools/voice_live.py | 18 +- tools/voice_mode.py | 36 +- tools/voice_mode_transcript.py | 2 +- tools/wake_word.py | 50 +-- tools/wake_word_engines.py | 16 +- tools/web_result_cache.py | 22 +- tools/web_tools.py | 20 +- tools/web_tools_extract.py | 16 +- tools/web_tools_rescue.py | 6 +- tools/web_tools_truncate.py | 6 +- tools/website_policy.py | 16 +- tools/working_diff.py | 10 +- tools/write_approval.py | 20 +- tools/x_search_tool.py | 24 +- tools/xai_http.py | 10 +- tools/xai_video_tools.py | 12 +- tools/yuanbao_tools.py | 2 +- toolset_distributions.py | 10 +- toolsets.py | 32 +- trajectory_compressor.py | 49 ++- tui_gateway/change_watcher.py | 20 +- tui_gateway/compute_host.py | 4 +- tui_gateway/contracts/__init__.py | 2 +- .../contracts/billing_delegation_pets.py | 34 +- tui_gateway/contracts/events.py | 6 +- tui_gateway/contracts/tools_mcp_plugins.py | 2 +- tui_gateway/event_publisher.py | 2 +- tui_gateway/hosted_room_peer_http.py | 2 +- tui_gateway/launch_profile_policy.py | 10 +- tui_gateway/mcp_oauth_sessions.py | 18 +- tui_gateway/mcp_rpc_helpers.py | 6 +- tui_gateway/methods_display_watch.py | 2 +- tui_gateway/methods_prompt.py | 2 +- tui_gateway/methods_session.py | 12 +- tui_gateway/methods_tools.py | 4 +- tui_gateway/methods_voice.py | 2 +- tui_gateway/model_switch.py | 2 +- tui_gateway/prompt_attachments.py | 2 +- tui_gateway/server.py | 34 +- tui_gateway/server_requests.py | 24 +- tui_gateway/session_notifications.py | 2 +- tui_gateway/transport.py | 2 +- tui_gateway/ws.py | 4 +- website/scripts/fetch-plugin-stars.py | 2 +- 2366 files changed, 14579 insertions(+), 14723 deletions(-) diff --git a/acp_adapter/entry.py b/acp_adapter/entry.py index c8c14485e7..436bef7222 100644 --- a/acp_adapter/entry.py +++ b/acp_adapter/entry.py @@ -11,7 +11,7 @@ Usage:: # IMPORTANT: hermes_bootstrap must be the very first import — UTF-8 stdio # on Windows. No-op on POSIX. See hermes_bootstrap.py for full rationale. try: - import hermes_bootstrap # noqa: F401 + import hermes_bootstrap except ModuleNotFoundError as exc: # Partial ``hermes update`` (git-reset landed, ``uv pip install -e .`` did not). if exc.name != "hermes_bootstrap": @@ -111,8 +111,8 @@ def _print_version() -> None: def _run_check() -> None: - import acp # noqa: F401 - from acp_adapter.server import HermesACPAgent # noqa: F401 + import acp + from acp_adapter.server import HermesACPAgent print("Hermes ACP check OK") diff --git a/acp_adapter/events.py b/acp_adapter/events.py index 7bcc1d0598..47b5c57872 100644 --- a/acp_adapter/events.py +++ b/acp_adapter/events.py @@ -67,7 +67,7 @@ def _send_update(conn: acp.Client, session_id: str, loop: asyncio.AbstractEventL logger.debug("Failed to send ACP update", exc_info=True) -def _upgrade_queue(tool_call_ids: Dict[str, Deque[str]], name: str) -> Deque[str] | None: +def _upgrade_queue(tool_call_ids: dict[str, deque[str]], name: str) -> deque[str] | None: """Fetch the per-tool FIFO of pending call IDs, upgrading a legacy bare-string entry in place.""" queue = tool_call_ids.get(name) if isinstance(queue, str): @@ -76,8 +76,8 @@ def _upgrade_queue(tool_call_ids: Dict[str, Deque[str]], name: str) -> Deque[str def close_tool_call( - conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: Dict[str, Deque[str]], - tool_call_meta: Dict[str, Dict[str, Any]], name: str, result: Any = None, is_error: bool = False, + conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: dict[str, deque[str]], + tool_call_meta: dict[str, dict[str, Any]], name: str, result: Any = None, is_error: bool = False, ) -> str | None: """Close the oldest open ACP tool call for ``name``; returns its id, or None when none is open.""" queue = _upgrade_queue(tool_call_ids, name) @@ -95,8 +95,8 @@ def close_tool_call( def flush_open_tool_calls( - conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: Dict[str, Deque[str]], - tool_call_meta: Dict[str, Dict[str, Any]], + conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: dict[str, deque[str]], + tool_call_meta: dict[str, dict[str, Any]], ) -> int: """Close every tool call still open at the end of a turn, and report how many there were. @@ -117,10 +117,10 @@ def flush_open_tool_calls( def make_tool_progress_cb( - conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: Dict[str, Deque[str]], - tool_call_meta: Dict[str, Dict[str, Any]], + conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: dict[str, deque[str]], + tool_call_meta: dict[str, dict[str, Any]], edit_approval_policy_getter: Callable[[], tuple[str, str | None]] | None = None, - turn_state: Dict[str, Any] | None = None, + turn_state: dict[str, Any] | None = None, ) -> Callable: """Create a ``tool_progress_callback`` for AIAgent. @@ -253,8 +253,8 @@ def make_message_cb( def make_step_cb( - conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: Dict[str, Deque[str]], - tool_call_meta: Dict[str, Dict[str, Any]], turn_state: Dict[str, Any] | None = None, + conn: acp.Client, session_id: str, loop: asyncio.AbstractEventLoop, tool_call_ids: dict[str, deque[str]], + tool_call_meta: dict[str, dict[str, Any]], turn_state: dict[str, Any] | None = None, ) -> Callable: """Create a ``step_callback(api_call_count: int, prev_tools: list)`` for AIAgent.""" diff --git a/acp_adapter/model_catalog.py b/acp_adapter/model_catalog.py index b2b036f51f..b014391795 100644 --- a/acp_adapter/model_catalog.py +++ b/acp_adapter/model_catalog.py @@ -50,7 +50,7 @@ def _named_custom_provider_catalogs() -> list[tuple[str, str, list[tuple[str, st } def _entry_catalog(entry: dict) -> tuple[str, str, list[tuple[str, str]]] | None: - field = lambda key: str(entry.get(key) or "").strip() # noqa: E731 + field = lambda key: str(entry.get(key) or "").strip() provider_key, name, base_url = field("provider_key"), field("name"), field("base_url") if provider_key.lower() in disabled_keys or not name or not base_url: return None diff --git a/acp_adapter/provenance.py b/acp_adapter/provenance.py index 56db28fb43..c8f3685d4c 100644 --- a/acp_adapter/provenance.py +++ b/acp_adapter/provenance.py @@ -18,7 +18,7 @@ from typing import Any, Dict, Optional _MAX_WALK = 100 -def _get_row(db: Any, session_id: str) -> Optional[Dict[str, Any]]: +def _get_row(db: Any, session_id: str) -> Optional[dict[str, Any]]: try: return db.get_session(session_id) except Exception: @@ -31,7 +31,7 @@ def _is_compression_end(row: Any) -> bool: def build_session_provenance( db: Any, acp_session_id: str, current_hermes_session_id: str, *, previous_hermes_session_id: Optional[str] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """Build ``_meta.hermes.sessionProvenance`` for an ACP session. ``db`` must expose ``get_session``. ``current_hermes_session_id`` is the live @@ -62,7 +62,7 @@ def build_session_provenance( # A continuation is a session whose immediate parent ended with end_reason='compression'. is_continuation = bool(parent_id) and _is_compression_end(_get_row(db, parent_id)) - provenance: Dict[str, Any] = { + provenance: dict[str, Any] = { "acpSessionId": acp_session_id, "currentHermesSessionId": current_hermes_session_id, "rootHermesSessionId": root_id, "parentHermesSessionId": parent_id, "sessionKind": "continuation" if is_continuation else "root", "compressionDepth": compression_depth, @@ -79,7 +79,7 @@ def build_session_provenance( def session_provenance_meta( db: Any, acp_session_id: str, current_hermes_session_id: str, *, previous_hermes_session_id: Optional[str] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """Return a ready ``_meta`` payload: ``{"hermes": {"sessionProvenance": ...}}``.""" prov = build_session_provenance(db, acp_session_id, current_hermes_session_id, previous_hermes_session_id=previous_hermes_session_id) diff --git a/acp_adapter/server.py b/acp_adapter/server.py index 2ba7d0e8ff..6032714a99 100644 --- a/acp_adapter/server.py +++ b/acp_adapter/server.py @@ -899,13 +899,13 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent): """Install the ACP streaming callbacks on the session agent for one turn.""" cbs = _TurnCallbacks() if conn: - tool_call_ids: dict[str, Deque[str]] = defaultdict(deque) + tool_call_ids: dict[str, deque[str]] = defaultdict(deque) tool_call_meta: dict[str, dict[str, Any]] = {} cbs.tool_call_ids, cbs.tool_call_meta = tool_call_ids, tool_call_meta # Shared with the step callback so a runtime that projects # ``tool.completed`` closes each call once, not twice. turn_state: dict[str, Any] = {} - policy_getter = lambda: self._edit_approval_policy_for_state(state) # noqa: E731 + policy_getter = lambda: self._edit_approval_policy_for_state(state) cbs.tool_progress_cb = make_tool_progress_cb( conn, session_id, loop, tool_call_ids, tool_call_meta, edit_approval_policy_getter=policy_getter, turn_state=turn_state, @@ -925,7 +925,7 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent): cbs.stream_delta_cb = stream_delta_cb # Closes the synthetic permission-request bubble once the user has answered. - send_update = lambda update: _send_update(conn, session_id, loop, update) # noqa: E731 + send_update = lambda update: _send_update(conn, session_id, loop, update) cbs.approval_cb = make_approval_callback(conn.request_permission, loop, session_id, send_update=send_update) try: from acp_adapter.edit_approval import make_acp_edit_approval_requester diff --git a/acp_adapter/session.py b/acp_adapter/session.py index e5a868b0cc..14fb532746 100644 --- a/acp_adapter/session.py +++ b/acp_adapter/session.py @@ -100,8 +100,8 @@ def _register_task_cwd(task_id: str, cwd: str) -> None: logger.debug("Failed to register ACP task cwd override", exc_info=True) -def _expand_acp_enabled_toolsets(toolsets: List[str] | None = None, - mcp_server_names: List[str] | None = None) -> List[str]: +def _expand_acp_enabled_toolsets(toolsets: list[str] | None = None, + mcp_server_names: list[str] | None = None) -> list[str]: """Return ACP toolsets plus explicit MCP server toolsets for this session.""" names = [n for n in (["hermes-acp"] if toolsets is None else toolsets) if n] names += [f"mcp-{s}" for s in (mcp_server_names or []) if s] @@ -118,12 +118,12 @@ def _parse_model_config(mc: Any) -> dict: def _session_info(sid: str, cwd: str, model: Any, history_len: int, title: Any, preview: Any, - updated_at: Any) -> Dict[str, Any]: + updated_at: Any) -> dict[str, Any]: return {"session_id": sid, "cwd": cwd, "model": model, "history_len": history_len, "title": _build_session_title(title, preview, cwd), "updated_at": _format_updated_at(updated_at)} -def _first_user_preview(history: List[Dict[str, Any]], default: str) -> str: +def _first_user_preview(history: list[dict[str, Any]], default: str) -> str: return next((str(m.get("content") or "").strip() for m in history if m.get("role") == "user" and str(m.get("content") or "").strip()), default) @@ -136,7 +136,7 @@ class SessionState: agent: Any # AIAgent instance cwd: str = "." model: str = "" - history: List[Dict[str, Any]] = field(default_factory=list) + history: list[dict[str, Any]] = field(default_factory=list) cancel_event: Any = None # threading.Event is_running: bool = False # A state-mutating slash command (/reset, /compress, /model) is in flight. Turn claims @@ -144,7 +144,7 @@ class SessionState: # a bare is_running check in the slash thread would leave a check-then-act window where # a prompt claims the turn mid-mutation. command_op: bool = False - queued_prompts: List[str] = field(default_factory=list) + queued_prompts: list[str] = field(default_factory=list) runtime_lock: Any = field(default_factory=threading.Lock) current_prompt_text: str = "" interrupted_prompt_text: str = "" @@ -162,7 +162,7 @@ class SessionManager: def __init__(self, agent_factory=None, db=None): """``agent_factory``: AIAgent-like factory (tests); default builds a real AIAgent from the runtime provider config. ``db``: SessionDB; default lazily opens ``~/.hermes/state.db``.""" - self._sessions: Dict[str, SessionState] = {} + self._sessions: dict[str, SessionState] = {} self._lock = threading.Lock() # Serializes DB restores: session construction runs off the event loop, so two # overlapping session/load for one id must share a single agent build. @@ -207,7 +207,7 @@ class SessionManager: logger.info("Forked ACP session %s -> %s", session_id, new_id) return state - def list_sessions(self, cwd: str | None = None) -> List[Dict[str, Any]]: + def list_sessions(self, cwd: str | None = None) -> list[dict[str, Any]]: """Return lightweight info dicts for all sessions (memory + database).""" normalized_cwd = _normalize_cwd_for_compare(cwd) if cwd else None db = self._get_db() @@ -304,7 +304,7 @@ class SessionManager: # ---- persistence via SessionDB ------------------------------------------ def _install_state(self, session_id: str, agent: Any, cwd: str, model: str, - history: List[Dict[str, Any]], *, persist: bool = True) -> SessionState: + history: list[dict[str, Any]], *, persist: bool = True) -> SessionState: """Build a SessionState, register it in memory, bind its cwd for tools, optionally persist.""" state = SessionState(session_id=session_id, agent=agent, cwd=cwd, model=model, history=history, cancel_event=threading.Event()) diff --git a/acp_adapter/tools.py b/acp_adapter/tools.py index f06b65fe1d..794eedc2cf 100644 --- a/acp_adapter/tools.py +++ b/acp_adapter/tools.py @@ -15,7 +15,7 @@ from agent.display import build_tool_preview logger = logging.getLogger(__name__) # Hermes tool name -> ACP ToolKind (anything unlisted is "other"). -TOOL_KIND_MAP: Dict[str, ToolKind] = { +TOOL_KIND_MAP: dict[str, ToolKind] = { name: kind for kind, names in { "read": ("read_file", "skill_view", "skills_list", "browser_snapshot", "browser_vision", @@ -55,7 +55,7 @@ _POLISHED_TOOLS = { } _EMPTYISH = (None, "", [], {}) -Args = Dict[str, Any] +Args = dict[str, Any] _Formatter = Callable[[str, Optional[str], Optional[Args]], Optional[str]] @@ -521,7 +521,7 @@ def _format_media_or_cron_result(tool_name: str, data: Args, args: Args) -> Opti return "\n".join([f"✅ {tool_name} completed", *(f"- **{k}:** {data.get(k)}" for k in keys if data.get(k))]) -def _format_structured_value(key: str, value: Any, *, indent: int = 0, max_depth: int = 3, max_items: int = 8) -> List[str]: +def _format_structured_value(key: str, value: Any, *, indent: int = 0, max_depth: int = 3, max_items: int = 8) -> list[str]: """Render nested JSON-ish values as compact Markdown bullets, not inline blobs.""" pad = " " * indent bullet = f"{pad}- " @@ -530,7 +530,7 @@ def _format_structured_value(key: str, value: Any, *, indent: int = 0, max_depth def _line(text: str) -> str: return f"{bullet}{label} {text}" if label else f"{bullet}{text}" - def _child(child_key: str, child_value: Any, extra_indent: int) -> List[str]: + def _child(child_key: str, child_value: Any, extra_indent: int) -> list[str]: return _format_structured_value(child_key, child_value, indent=indent + extra_indent, max_depth=max_depth - 1, max_items=max_items) @@ -617,7 +617,7 @@ def _format_generic_structured_result(tool_name: str, result: Optional[str], *, return _truncate_text("\n".join(lines), limit=7000) -_COMPLETION_FORMATTERS: Dict[str, _Formatter] = { +_COMPLETION_FORMATTERS: dict[str, _Formatter] = { "todo": _format_todo_result, "read_file": _format_read_file_result, "write_file": _format_edit_result, @@ -636,10 +636,10 @@ _COMPLETION_FORMATTERS: Dict[str, _Formatter] = { } -def _parse_unified_diff_content(diff_text: str) -> List[Any]: +def _parse_unified_diff_content(diff_text: str) -> list[Any]: """Convert unified diff text into ACP diff content blocks (one per ``---``/``+++`` pair).""" - content: List[Any] = [] - state: Dict[str, Any] = {"old": None, "new": None, "old_lines": [], "new_lines": []} + content: list[Any] = [] + state: dict[str, Any] = {"old": None, "new": None, "old_lines": [], "new_lines": []} def _flush() -> None: old_path, new_path = state["old"], state["new"] @@ -674,7 +674,7 @@ def _parse_unified_diff_content(diff_text: str) -> List[Any]: def _build_tool_complete_content( tool_name: str, result: Optional[str], *, function_args: Optional[Args] = None, snapshot: Any = None -) -> List[Any]: +) -> list[Any]: """Build structured ACP completion content, falling back to plain text.""" if tool_name == "skill_manage": try: @@ -697,7 +697,7 @@ def _build_tool_complete_content( # --- ToolCallStart / ToolCallProgress events --------------------------------- -def _more(items: list, shown: int, unit: str = "") -> List[str]: +def _more(items: list, shown: int, unit: str = "") -> list[str]: """``["... N more"]`` trailer when ``items`` overflowed the ``shown`` cap, else ``[]``.""" return [f"... {len(items) - shown} more{unit}"] if len(items) > shown else [] @@ -724,7 +724,7 @@ def _start_skill_manage(args: Args) -> Any: target = str(args.get("file_path") or "file") return acp.tool_diff_content(path=f"skills/{name}/{target}", new_text=str(args.get("file_content") or "")) if action in {"delete", "remove_file"}: - return f"Removing {str(args.get('file_path') or file_path)} from skill '{name}'" + return f"Removing {args.get('file_path') or file_path!s} from skill '{name}'" return f"Running skill_manage action '{action}' on skill '{name}' ({file_path})" @@ -751,7 +751,7 @@ def _preview(label: str, value: str, limit: int) -> str: # Per-tool start-content builders returning text or one ACP content block. ``None`` means the # title/location already identify the target (read_file, web_extract): a synthetic content block # would make Zed render an unhelpful Output section before completion. -_START_CONTENT_BUILDERS: Dict[str, Optional[Callable[[Args], Any]]] = { +_START_CONTENT_BUILDERS: dict[str, Optional[Callable[[Args], Any]]] = { "patch": lambda a: ( f"Preparing {a.get('mode', 'replace')} edit for {a.get('path') or 'patch input'}. Approval prompt shows the diff." ), @@ -787,7 +787,7 @@ def build_tool_start(tool_call_id: str, tool_name: str, arguments: Args, *, edit (mirrors ``get_cute_tool_message`` in ``agent/display.py``).""" try: return _build_tool_start(tool_call_id, tool_name, arguments, edit_diff=edit_diff) - except Exception as exc: # noqa: BLE001 — a tool-call render must never abort the turn + except Exception as exc: logger.debug("ACP tool-start render failed for %r: %s", tool_name, exc) safe_name = tool_name if isinstance(tool_name, str) and tool_name else "tool" return acp.start_tool_call(tool_call_id, safe_name, kind=get_tool_kind(safe_name), content=None, locations=[]) @@ -846,7 +846,7 @@ def build_tool_abandoned(tool_call_id: str, tool_name: str) -> ToolCallProgress: ) -def extract_locations(arguments: Args) -> List[ToolCallLocation]: +def extract_locations(arguments: Args) -> list[ToolCallLocation]: """Extract file-system locations from tool arguments.""" if not (path := arguments.get("path")): return [] diff --git a/agent/__init__.py b/agent/__init__.py index 77f1cad846..9555e82101 100644 --- a/agent/__init__.py +++ b/agent/__init__.py @@ -1,3 +1,3 @@ """Agent internals extracted from run_agent.py so it stays focused on AIAgent.""" -from . import jiter_preload as _jiter_preload # noqa: F401 +from . import jiter_preload as _jiter_preload diff --git a/agent/agent_init.py b/agent/agent_init.py index 35b7b086d2..2e05039002 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -170,7 +170,7 @@ def _custom_provider_runtime_ids(value: Any) -> set[str]: def _build_codex_gpt5_autoraise_notice( - autoraise: Dict[str, Any], context_length: Optional[int] = None + autoraise: dict[str, Any], context_length: Optional[int] = None ) -> str: """One-time notice when Codex gpt-5.x raises compaction (``autoraise``: model/from/to). @@ -197,7 +197,7 @@ def _build_codex_gpt5_autoraise_notice( def _resolve_compression_threshold( global_threshold: float, model_cthresh: Optional[float], *, model: Optional[str] = None, is_codex_autoraise: bool, -) -> tuple[float, Optional[Dict[str, Any]]]: +) -> tuple[float, Optional[dict[str, Any]]]: """Global compaction threshold merged with a per-model override. Returns ``(threshold, autoraise_notice)``; the notice is set only when a Codex autoraise @@ -218,7 +218,7 @@ def _codex_gpt55_autoraise_notice_marker(): return get_hermes_home() / ".codex_gpt55_autoraise_notice" -def _codex_gpt55_autoraise_notice_state(autoraise: Dict[str, Any]) -> str: +def _codex_gpt55_autoraise_notice_state(autoraise: dict[str, Any]) -> str: """Notice identity keyed on what it displays (model + from→to percentages). An unchanged threshold stays silent across restarts; a changed global threshold or a @@ -230,7 +230,7 @@ def _codex_gpt55_autoraise_notice_state(autoraise: Dict[str, Any]) -> str: return f"{model}:{from_pct}:{to_pct}" -def _codex_gpt55_autoraise_notice_seen(autoraise: Dict[str, Any]) -> bool: +def _codex_gpt55_autoraise_notice_seen(autoraise: dict[str, Any]) -> bool: """True if this exact notice was already shown for this profile (unreadable = unseen).""" try: current = _codex_gpt55_autoraise_notice_state(autoraise) @@ -241,7 +241,7 @@ def _codex_gpt55_autoraise_notice_seen(autoraise: Dict[str, Any]) -> bool: return False -def _record_codex_gpt55_autoraise_notice(autoraise: Dict[str, Any]) -> None: +def _record_codex_gpt55_autoraise_notice(autoraise: dict[str, Any]) -> None: """Persist that the notice was shown. Best-effort: a failure only re-shows it later.""" with suppress(OSError, KeyError, TypeError, ValueError): marker = _codex_gpt55_autoraise_notice_marker() @@ -255,7 +255,7 @@ def _normalized_custom_base_url(value: Any) -> str: return value.strip().rstrip("/") -def _custom_provider_model_matches(agent_model: str, entry: Dict[str, Any]) -> bool: +def _custom_provider_model_matches(agent_model: str, entry: dict[str, Any]) -> bool: agent_model_norm = str(agent_model or "").strip().lower() # Multi-model entries (`providers..models` mapping / legacy `models:` list): # matching ANY catalog entry counts, else a provider whose `model` differs from the @@ -269,8 +269,8 @@ def _custom_provider_model_matches(agent_model: str, entry: Dict[str, Any]) -> b def _custom_provider_extra_body_for_agent( - *, provider: str, model: str, base_url: str, custom_providers: List[Dict[str, Any]] -) -> Optional[Dict[str, Any]]: + *, provider: str, model: str, base_url: str, custom_providers: list[dict[str, Any]] +) -> Optional[dict[str, Any]]: provider_norm = (provider or "").strip().lower() if provider_norm != "custom" and not provider_norm.startswith("custom:"): return None @@ -279,7 +279,7 @@ def _custom_provider_extra_body_for_agent( if not target_url: return None - fallback: Optional[Dict[str, Any]] = None + fallback: Optional[dict[str, Any]] = None for entry in custom_providers or []: if not isinstance(entry, dict): continue @@ -302,7 +302,7 @@ def _custom_provider_extra_body_for_agent( return fallback -def _merge_custom_provider_extra_body(agent, custom_providers: List[Dict[str, Any]]) -> None: +def _merge_custom_provider_extra_body(agent, custom_providers: list[dict[str, Any]]) -> None: extra_body = _custom_provider_extra_body_for_agent( provider=agent.provider, model=agent.model, base_url=agent.base_url, custom_providers=custom_providers, @@ -361,12 +361,12 @@ def _parse_config_int(raw: Any, default: int) -> int: return default -def _cfg_flag(cfg: Dict[str, Any], key: str, default: bool) -> bool: +def _cfg_flag(cfg: dict[str, Any], key: str, default: bool) -> bool: """Legacy string-set truthiness used by the ``compression`` section.""" return str(cfg.get(key, default)).lower() in {"true", "1", "yes"} -def _cfg_dict(cfg: Dict[str, Any], key: str) -> Dict[str, Any]: +def _cfg_dict(cfg: dict[str, Any], key: str) -> dict[str, Any]: """``cfg[key]`` if it is a mapping, else ``{}`` (malformed sections are ignored).""" section = cfg.get(key, {}) return section if isinstance(section, dict) else {} @@ -519,14 +519,14 @@ def _finalize_routing(agent, api_mode, credential_pool): ).start() -def _set_defaults(agent, table: Dict[str, Any]) -> None: +def _set_defaults(agent, table: dict[str, Any]) -> None: """Assign each ``name -> value`` on ``agent``; callables are factories (fresh per agent).""" for name, value in table.items(): setattr(agent, name, value() if callable(value) else value) # Control-flow state (interrupts / steer / redirect / delegation / background review). -_CONTROL_STATE: Dict[str, Any] = { +_CONTROL_STATE: dict[str, Any] = { "_executing_tools": False, # lets _vprint print while tools run with stream consumers on "_trim_after_tool_batch": False, # a >=1 MB tool result was committed; trim once the batch unwinds "_tool_guardrails": ToolCallGuardrailController, @@ -565,7 +565,7 @@ _CONTROL_STATE: Dict[str, Any] = { } # Per-turn bookkeeping: budgets, activity tracking, rate-limit/credits telemetry. -_TURN_STATE: Dict[str, Any] = { +_TURN_STATE: dict[str, Any] = { # Intermediate pressure warnings made models give up early; ordinary conversations # remain opt-in. Dispatcher workers receive a bounded completion checkpoint. "_iteration_budget_warning_injected": False, @@ -595,7 +595,7 @@ _TURN_STATE: Dict[str, Any] = { } # Session persistence state. -_SESSION_STATE: Dict[str, Any] = { +_SESSION_STATE: dict[str, Any] = { "_session_messages": list, # Responses encrypted-reasoning replay. The first ``invalid_encrypted_content`` rejection only # strips the stale blobs (a rotated sealing key); a second one means the route cannot round-trip @@ -636,7 +636,7 @@ _SESSION_STATE: Dict[str, Any] = { } # Streaming delivery state. -_STREAM_STATE: Dict[str, Any] = { +_STREAM_STATE: dict[str, Any] = { "_stream_callback": None, # streaming TTS; set early so _vprint can reference it "_stream_needs_break": False, # one "\n\n" before the next real text delta after tools # Stateful scrubbers: / thinking spans split across deltas defeat @@ -765,7 +765,7 @@ def _init_anthropic_client(agent, api_key, base_url, _provider_timeout): try: from hermes_cli.auth import build_minimax_oauth_token_provider effective_key = build_minimax_oauth_token_provider() - except Exception as _mm_exc: # noqa: BLE001 — never block startup on this + except Exception as _mm_exc: logging.getLogger(__name__).warning( "MiniMax OAuth: failed to install per-request token provider " "(%s); falling back to static bearer that will expire ~15min in.", @@ -813,7 +813,7 @@ def _init_bedrock_client(agent, base_url): print(f"🤖 AI Agent initialized with model: {agent.model} (AWS Bedrock, {agent._bedrock_region}{_gr_label})") -def _explicit_client_kwargs(agent, api_key, base_url, _provider_timeout) -> Dict[str, Any]: +def _explicit_client_kwargs(agent, api_key, base_url, _provider_timeout) -> dict[str, Any]: """OpenAI-client kwargs from explicit CLI/gateway credentials (auth already resolved).""" _parsed_url = urlparse(base_url) client_kwargs = {"api_key": api_key, "base_url": base_url} @@ -844,7 +844,7 @@ def _explicit_client_kwargs(agent, api_key, base_url, _provider_timeout) -> Dict return client_kwargs -def _routed_client_kwargs(agent, fallback_model, _provider_timeout) -> Optional[Dict[str, Any]]: +def _routed_client_kwargs(agent, fallback_model, _provider_timeout) -> Optional[dict[str, Any]]: """OpenAI-client kwargs via the centralized provider router (no explicit creds). Falls through to the init-time fallback chain, then raises with the missing-key / @@ -953,7 +953,7 @@ def _routed_client_kwargs(agent, fallback_model, _provider_timeout) -> Optional[ ) -def _apply_openai_header_policy(agent, client_kwargs: Dict[str, Any]) -> None: +def _apply_openai_header_policy(agent, client_kwargs: dict[str, Any]) -> None: """Mutate ``client_kwargs`` (== ``agent._client_kwargs``) with header/TLS policy, in order: OpenRouter Claude beta header → model.default_headers → custom-provider TLS/extra_headers.""" # Fine-grained tool streaming for Claude on OpenRouter: without the beta header @@ -1054,7 +1054,7 @@ def _lazy_headers(module: str, name: str, pass_key: bool = False, pass_base: boo # Host → default_headers factory for explicit base_url client construction. Ordered: first # host match wins; no match falls back to the provider profile's declared headers. -_HOST_DEFAULT_HEADERS: List[tuple[str, Callable[[Any, str], Dict[str, str]]]] = [ +_HOST_DEFAULT_HEADERS: list[tuple[str, Callable[[Any, str], dict[str, str]]]] = [ ("openrouter.ai", _lazy_headers("agent.auxiliary_client", "build_or_headers")), ("integrate.api.nvidia.com", _lazy_headers("agent.auxiliary_client", "build_nvidia_nim_headers", pass_base=True)), @@ -1074,7 +1074,7 @@ def _host_default_headers_factory(base_url: str): return None -def _client_kwargs_from_routed(client, timeout) -> Dict[str, Any]: +def _client_kwargs_from_routed(client, timeout) -> dict[str, Any]: """OpenAI-client kwargs mirroring a router-resolved client, keeping its provider headers (SDK stores them in ``_custom_headers``; older/mocked clients expose ``default_headers``).""" kwargs = {"api_key": client.api_key, "base_url": str(client.base_url)} @@ -1255,7 +1255,7 @@ def _apply_display_config(agent, _agent_cfg, platform): _ra().logger.warning("Tool loop guardrail config ignored: %s", _tlg_err) -def _memory_provider_init_kwargs(agent, platform) -> Dict[str, Any]: +def _memory_provider_init_kwargs(agent, platform) -> dict[str, Any]: """Scoping kwargs for ``MemoryManager.initialize_all`` (status_callback is CLI-only: gateway status travels a different path and the indicator no-ops without it).""" kwargs = { @@ -1456,7 +1456,7 @@ def _positive_int(raw: Any, *, reject: tuple = ()) -> Optional[int]: return parsed if parsed > 0 else None -def _compression_threshold(agent, cfg: Dict[str, Any]) -> tuple[float, bool]: +def _compression_threshold(agent, cfg: dict[str, Any]) -> tuple[float, bool]: """Global threshold merged with the per-model override; stashes the autoraise notice. Codex gpt-5.4/5.5 raise to 85% (272K cap → 50% would compact at ~136K); the opt-out flag restores the global value, and the notice has its own display gate.""" @@ -1487,7 +1487,7 @@ def _compression_threshold(agent, cfg: Dict[str, Any]) -> tuple[float, bool]: return threshold, notice_enabled -def _compression_codex_settings(cfg: Dict[str, Any]) -> tuple[str, bool, Optional[int]]: +def _compression_codex_settings(cfg: dict[str, Any]) -> tuple[str, bool, Optional[int]]: """``codex_app_server_auto`` / ``codex_responses_native`` / ``codex_responses_compact_threshold``.""" app_server_auto = str(cfg.get("codex_app_server_auto", "native") or "native").lower() if app_server_auto not in {"native", "hermes", "off"}: @@ -2300,7 +2300,7 @@ def _init_usage_state(agent): # Per-session usage accounting. -_USAGE_STATE: Dict[str, Any] = { +_USAGE_STATE: dict[str, Any] = { "_user_turn_count": 0, "_is_user_initiated_turn": False, # Copilot x-initiator: first call of a user turn = "user" # Usage anchors (agent/usage_anchor.py): last response's exact usage + transcript @@ -2362,11 +2362,11 @@ def init_agent( agent, base_url: str = None, api_key: str = None, provider: str = None, api_mode: str = None, acp_command: str = None, acp_args: list[str] | None = None, command: str = None, args: list[str] | None = None, model: str = "", max_iterations: int = sys.maxsize, - enabled_toolsets: List[str] = None, disabled_toolsets: List[str] = None, + enabled_toolsets: list[str] = None, disabled_toolsets: list[str] = None, save_trajectories: bool = False, verbose_logging: bool = False, quiet_mode: bool = False, tool_progress_mode: str = "all", ephemeral_system_prompt: str = None, - log_prefix_chars: int = 100, log_prefix: str = "", providers_allowed: List[str] = None, - providers_ignored: List[str] = None, providers_order: List[str] = None, + log_prefix_chars: int = 100, log_prefix: str = "", providers_allowed: list[str] = None, + providers_ignored: list[str] = None, providers_order: list[str] = None, provider_sort: str = None, provider_require_parameters: bool = False, provider_data_collection: str = None, openrouter_min_coding_score: Optional[float] = None, session_id: str = None, tool_progress_callback: callable = None, @@ -2381,18 +2381,18 @@ def init_agent( notice_callback: callable = None, notice_clear_callback: callable = None, event_callback: Optional[Callable[[str, dict], None]] = None, reaction_callback: Optional[Callable[[str], None]] = None, max_tokens: int = None, - reasoning_config: Dict[str, Any] = None, service_tier: str = None, - request_overrides: Dict[str, Any] = None, prefill_messages: List[Dict[str, Any]] = None, + reasoning_config: dict[str, Any] = None, service_tier: str = None, + request_overrides: dict[str, Any] = None, prefill_messages: list[dict[str, Any]] = None, platform: str = None, user_id: str = None, user_id_alt: str = None, user_name: str = None, chat_id: str = None, chat_name: str = None, chat_type: str = None, thread_id: str = None, gateway_session_key: str = None, skip_context_files: bool = False, load_soul_identity: bool = False, skip_memory: bool = False, skip_background_review: bool = False, session_db=None, parent_session_id: str = None, iteration_budget: "IterationBudget" = None, run_budget_seconds: Optional[float] = None, - fallback_model: Dict[str, Any] = None, credential_pool=None, checkpoints_enabled: bool = False, + fallback_model: dict[str, Any] = None, credential_pool=None, checkpoints_enabled: bool = False, checkpoint_max_snapshots: int = 20, checkpoint_max_total_size_mb: int = 500, checkpoint_max_file_size_mb: int = 10, pass_session_id: bool = False, - requested_provider: str = None, capabilities: Optional[Dict[str, bool]] = None, cwd: Optional[str] = None, + requested_provider: str = None, capabilities: Optional[dict[str, bool]] = None, cwd: Optional[str] = None, side_agent: bool = False, memory_manager=None, tool_result_metadata_callback: Optional[Callable[..., dict]] = None, ): diff --git a/agent/agent_init_fallback.py b/agent/agent_init_fallback.py index 157138225c..ee4a124f54 100644 --- a/agent/agent_init_fallback.py +++ b/agent/agent_init_fallback.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List logger = logging.getLogger("run_agent") -def _fallback_entries(fallback_model) -> List[Dict[str, Any]]: +def _fallback_entries(fallback_model) -> list[dict[str, Any]]: """Normalize legacy single-dict ``fallback_model`` / list ``fallback_providers``.""" if isinstance(fallback_model, dict): fallback_model = [fallback_model] diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index db35f62ee3..4a67918ab5 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -107,7 +107,7 @@ _TRAJECTORY_SYSTEM_PROMPT = ( ) -def _trajectory_gpt_prefix(msg: Dict[str, Any]) -> str: +def _trajectory_gpt_prefix(msg: dict[str, Any]) -> str: """Leading ```` block from native reasoning tokens, if any.""" if msg.get("reasoning") and msg["reasoning"].strip(): return f"\n{msg['reasoning']}\n\n" @@ -119,7 +119,7 @@ def _with_think_block(content: str) -> str: return content if "" in content else "\n\n" + content -def _trajectory_tool_call_turn(msg: Dict[str, Any]) -> str: +def _trajectory_tool_call_turn(msg: dict[str, Any]) -> str: content = _trajectory_gpt_prefix(msg) if msg.get("content") and msg["content"].strip(): # -> (model reasons via XML when native thinking is off) @@ -139,7 +139,7 @@ def _trajectory_tool_call_turn(msg: Dict[str, Any]) -> str: return _with_think_block(content).rstrip() -def _trajectory_tool_responses(msg: Dict[str, Any], messages: List[Dict[str, Any]], start: int) -> Tuple[List[str], int]: +def _trajectory_tool_responses(msg: dict[str, Any], messages: list[dict[str, Any]], start: int) -> tuple[list[str], int]: """Collect the ```` blocks for the tool run starting at ``start``; returns ``(blocks, next_index)``.""" tool_responses = [] j = start @@ -166,7 +166,7 @@ def _trajectory_tool_responses(msg: Dict[str, Any], messages: List[Dict[str, Any return tool_responses, j -def convert_to_trajectory_format(agent, messages: List[Dict[str, Any]], user_query: str, completed: bool) -> List[Dict[str, Any]]: +def convert_to_trajectory_format(agent, messages: list[dict[str, Any]], user_query: str, completed: bool) -> list[dict[str, Any]]: """Convert internal message history to trajectory format for saving.""" # Trajectories are text-only: swap image-bearing tool messages for their text_summary so ~1MB # base64 blobs are not embedded. @@ -313,7 +313,7 @@ def sanitize_tool_call_arguments( # Session-scoped in-flight registry for note_turn_start: the gateway caches agents per routing key # while the transcript is keyed by session_id, so two agent objects can run concurrent turns on one # session unseen by per-agent state. -_INFLIGHT_TURNS_BY_SESSION: Dict[str, Tuple[str, float]] = {} +_INFLIGHT_TURNS_BY_SESSION: dict[str, tuple[str, float]] = {} _INFLIGHT_TURNS_LOCK = threading.Lock() @@ -374,7 +374,7 @@ def note_turn_persisted(agent): agent._inflight_turn_session_id = None -def _is_codex_interim(m: Dict) -> bool: +def _is_codex_interim(m: dict) -> bool: """Codex Responses interim turn: carries its own continuation state, replayed verbatim.""" return bool( m.get("codex_reasoning_items") @@ -383,7 +383,7 @@ def _is_codex_interim(m: Dict) -> bool: ) -def _merge_assistant_into(prev: Dict, msg: Dict) -> bool: +def _merge_assistant_into(prev: dict, msg: dict) -> bool: """Fold consecutive assistant *msg* into *prev* (union tool_calls, concat text). Returns whether *msg*'s text survives: multimodal (list) content is never joined.""" from agent.context_compressor import _DB_PERSISTED_MARKER @@ -463,7 +463,7 @@ def _merge_assistant_into(prev: Dict, msg: Dict) -> bool: return text_kept -def _remember_absorbed_row(survivor: Dict[str, Any], dropped: Dict[str, Any], *, folded: bool) -> None: +def _remember_absorbed_row(survivor: dict[str, Any], dropped: dict[str, Any], *, folded: bool) -> None: """Retire *dropped*'s row ids onto *survivor*; record its uid as a merge witness only when *folded* (its text survives). An empty incoming turn still merges; stamping an empty list would change a message that absorbed nothing. A dropped id that equals the survivor's own live id (the display-marker merge adopts @@ -496,7 +496,7 @@ def _remember_absorbed_row(survivor: Dict[str, Any], dropped: Dict[str, Any], *, record_absorbed_message(survivor, dropped) -def _count_unnamed_row(survivor: Dict[str, Any], retired: Dict[str, Any]) -> None: +def _count_unnamed_row(survivor: dict[str, Any], retired: dict[str, Any]) -> None: """On a reload without row ids nothing names *retired*'s durable row once the repair takes the dict out of the list, so *survivor* counts it. A dict that counts rows was loaded too: a merge may have popped its marker since. Call before the merge rewrites the survivor: *retired*'s own fields are @@ -505,7 +505,7 @@ def _count_unnamed_row(survivor: Dict[str, Any], retired: Dict[str, Any]) -> Non from agent.conversation_compression_archive import ( OWN_ROW, RETIRED_DURABLE_ROWS, UNNAMED_DURABLE_ROWS, retired_row_payload) - def loaded(message: Dict[str, Any]) -> bool: + def loaded(message: dict[str, Any]) -> bool: return bool(message.get(_DB_PERSISTED_MARKER) or message.get(UNNAMED_DURABLE_ROWS)) if loaded(survivor) and loaded(retired) and not isinstance(retired.get("_row_id"), int): @@ -516,7 +516,7 @@ def _count_unnamed_row(survivor: Dict[str, Any], retired: Dict[str, Any]) -> Non survivor.setdefault(RETIRED_DURABLE_ROWS, []).append(retired_row_payload(retired)) -def _remember_own_row(survivor: Dict[str, Any]) -> None: +def _remember_own_row(survivor: dict[str, Any]) -> None: """An assistant fold rewrites *survivor*'s text, so on a reload without row ids its own row no longer matches it by content. Record its loaded fields first, once.""" from agent.context_compressor import _DB_PERSISTED_MARKER @@ -528,7 +528,7 @@ def _remember_own_row(survivor: Dict[str, Any]) -> None: survivor.setdefault(RETIRED_DURABLE_ROWS, []).append({**retired_row_payload(survivor), OWN_ROW: True}) -def _retire_dropped_row(kept: List[Dict], dropped: Dict[str, Any], leading: List[Dict]) -> None: +def _retire_dropped_row(kept: list[dict], dropped: dict[str, Any], leading: list[dict]) -> None: """A durable row the repair drops was still handed to the caller, so the survivor before it stands for it. Left unnamed, an in-place compaction takes it for a row another surface appended and re-sequences it behind the running turn. A drop with no survivor before it waits in *leading*.""" @@ -539,17 +539,17 @@ def _retire_dropped_row(kept: List[Dict], dropped: Dict[str, Any], leading: List _remember_absorbed_row(kept[-1], dropped, folded=False) -def _retire_leading_drops(kept: List[Dict], leading: List[Dict]) -> None: +def _retire_leading_drops(kept: list[dict], leading: list[dict]) -> None: """Rows dropped ahead of the first survivor sit right before its own row, so it stands for them.""" for dropped in leading if kept and isinstance(kept[0], dict) else (): _count_unnamed_row(kept[0], dropped) _remember_absorbed_row(kept[0], dropped, folded=False) -def _merge_consecutive_assistants(messages: List[Dict]) -> Tuple[List[Dict], int]: +def _merge_consecutive_assistants(messages: list[dict]) -> tuple[list[dict], int]: """Pass 0: merge consecutive assistant turns (codex interims exempt).""" repairs = 0 - collapsed: List[Dict] = [] + collapsed: list[dict] = [] for msg in messages: prev = collapsed[-1] if collapsed and isinstance(collapsed[-1], dict) else None if ( @@ -572,12 +572,12 @@ def _merge_consecutive_assistants(messages: List[Dict]) -> Tuple[List[Dict], int return collapsed, repairs -def _drop_stray_tool_results(messages: List[Dict]) -> Tuple[List[Dict], int]: +def _drop_stray_tool_results(messages: list[dict]) -> tuple[list[dict], int]: """Pass 1: drop tool results not following a known assistant tool call. Consumes the whole alias group (call_id/id/response_item_id/composite) so a duplicate keyed on a sibling alias is not replayed to strict providers.""" repairs = 0 - known_tool_ids: Dict[str, int] = {} # alias -> group id; reset by assistant/user turns + known_tool_ids: dict[str, int] = {} # alias -> group id; reset by assistant/user turns # Pass 1: drop stray tool messages that don't follow a known assistant tool call. A Responses call can # have several equivalent spellings (call_id, id, response_item_id, or a composite ``call|item`` id), so # consume the whole alias group when one spelling is matched. Alias expansion lives in @@ -585,8 +585,8 @@ def _drop_stray_tool_results(messages: List[Dict]) -> Tuple[List[Dict], int]: # owner) — which also handles SDK tool_call objects, preserving the #91768 dict-or-object tolerance. matched_tool_groups: set = set() next_tool_group = 0 - filtered: List[Dict] = [] - leading: List[Dict] = [] + filtered: list[dict] = [] + leading: list[dict] = [] for msg in messages: role = msg.get("role") if isinstance(msg, dict) else None if role in ("assistant", "user"): @@ -617,15 +617,15 @@ def _drop_stray_tool_results(messages: List[Dict]) -> Tuple[List[Dict], int]: return filtered, repairs -def _prune_unanswered_tool_calls(messages: List[Dict]) -> Tuple[List[Dict], int]: +def _prune_unanswered_tool_calls(messages: list[dict]) -> tuple[list[dict], int]: """Pass 2: prune tool_calls not answered in the IMMEDIATELY following tool run (a displaced result masks the per-call stub pass and strict providers 400). Payload-empty turns are dropped; codex interims exempt.""" from agent.context_compressor import _DB_PERSISTED_MARKER repairs = 0 - pruned: List[Dict] = [] - leading: List[Dict] = [] + pruned: list[dict] = [] + leading: list[dict] = [] for i, msg in enumerate(messages): if not ( isinstance(msg, dict) and msg.get("role") == "assistant" and msg.get("tool_calls") @@ -659,7 +659,7 @@ def _prune_unanswered_tool_calls(messages: List[Dict]) -> Tuple[List[Dict], int] return pruned, repairs -def _merge_consecutive_users(messages: List[Dict]) -> Tuple[List[Dict], int]: +def _merge_consecutive_users(messages: list[dict]) -> tuple[list[dict], int]: """Pass 3: merge consecutive plain-text user messages (no user input lost).""" from agent.context_compressor import _DB_PERSISTED_MARKER, split_user_originated_turn from agent.conversation_compression_archive import MERGED_DURABLE_ROWS @@ -670,7 +670,7 @@ def _merge_consecutive_users(messages: List[Dict]) -> Tuple[List[Dict], int]: return isinstance(content, str) and not content.startswith(SessionDB._CONTENT_JSON_PREFIX) repairs = 0 - merged: List[Dict] = [] + merged: list[dict] = [] for msg in messages: prev = merged[-1] if merged and isinstance(merged[-1], dict) else None if ( @@ -744,7 +744,7 @@ _SEQUENCE_REPAIR_PASSES = ( _merge_consecutive_users, ) -def _normalize_sentinel_encoded_content(messages: List[Dict]) -> None: +def _normalize_sentinel_encoded_content(messages: list[dict]) -> None: """Decode any sentinel-encoded row content in place before the alternation passes run, so an image-bearing turn is never merged as text (#125299). A multimodal turn can re-enter the working set as its ``\\x00json:[…]`` string (e.g. after a proactive prune re-inserts history, #124102). A body @@ -762,7 +762,7 @@ def _normalize_sentinel_encoded_content(messages: List[Dict]) -> None: # it makes the append-only flush re-append this user turn as a duplicate (#125331). -def repair_message_sequence(agent, messages: List[Dict]) -> int: +def repair_message_sequence(agent, messages: list[dict]) -> int: """Collapse malformed role-alternation left in the live history; returns repair count. Providers require strict alternation after the system message (violations: silent empty responses or 400s); this is the pre-call belt for host-fed, resumed or replayed histories. @@ -785,7 +785,7 @@ def repair_message_sequence(agent, messages: List[Dict]) -> int: return repairs -def repair_message_sequence_with_cursor(agent, messages: List[Dict]) -> int: +def repair_message_sequence_with_cursor(agent, messages: list[dict]) -> int: """Run :func:`repair_message_sequence` and keep ``_last_flushed_db_idx`` consistent. Repair shrinks the list in place; counting identity-preserved survivors of the flushed prefix gives the exact new cursor, whereas a ``min()`` clamp would skip unflushed rows (used only without a snapshot).""" @@ -876,7 +876,7 @@ _USAGE_LIMIT_REASON_TOKENS = ("usage_limit_reached", "gousagelimit") _USAGE_LIMIT_MESSAGE_TOKENS = ("usage limit reached", "usage limit has been reached") -def _failed_credential_identity(agent, pool) -> Tuple[Optional[str], Optional[str]]: +def _failed_credential_identity(agent, pool) -> tuple[Optional[str], Optional[str]]: """``(api_key_hint, credential_id)`` of the key actually dispatched, not ``pool.current()``: the shared pointer often points at a different healthy entry, and marking it exhausted can take the whole pool offline from one 429.""" @@ -1006,7 +1006,7 @@ def _recover_rate_limit(pool, *, has_retried_429, error_context, api_key_hint, c def recover_with_credential_pool( agent, *, status_code: Optional[int], has_retried_429: bool, classified_reason: Optional[FailoverReason] = None, - error_context: Optional[Dict[str, Any]] = None, billing_unverified: bool = False, + error_context: Optional[dict[str, Any]] = None, billing_unverified: bool = False, ) -> tuple[bool, bool]: """Attempt credential recovery via pool rotation; returns (recovered, has_retried_429). Rate limits: retry once, then rotate. Billing: rotate immediately. Auth: refresh before @@ -1133,7 +1133,7 @@ def recover_with_credential_pool( return False, has_retried_429 -def _apply_primary_runtime_fields(agent, rt: Dict[str, Any]) -> None: +def _apply_primary_runtime_fields(agent, rt: dict[str, Any]) -> None: """Copy the identity/transport fields of a ``_primary_runtime`` snapshot onto ``agent`` (shared by transport recovery and turn-start restore; the caller rebuilds the client).""" agent.model = rt["model"] @@ -1150,7 +1150,7 @@ def _apply_primary_runtime_fields(agent, rt: Dict[str, Any]) -> None: agent._client_kwargs = dict(rt["client_kwargs"]) -def _build_anthropic_client_from_runtime(agent, rt: Dict[str, Any]) -> None: +def _build_anthropic_client_from_runtime(agent, rt: dict[str, Any]) -> None: """Rebuild the native Anthropic client from a ``_primary_runtime`` snapshot.""" from agent.anthropic_adapter import build_anthropic_client agent._anthropic_api_key = rt["anthropic_api_key"] @@ -1163,7 +1163,7 @@ def _build_anthropic_client_from_runtime(agent, rt: Dict[str, Any]) -> None: agent.client = None -def _rebuild_primary_client(agent, rt: Dict[str, Any], *, reason: str) -> None: +def _rebuild_primary_client(agent, rt: dict[str, Any], *, reason: str) -> None: """Rebuild the primary client from a ``_primary_runtime`` snapshot (MoA facade / native Anthropic / OpenAI wire).""" if (agent.provider or "").strip().lower() == "moa": # MoA has empty client_kwargs; rebuild via the shared facade factory so the @@ -1243,9 +1243,9 @@ _UNMERGEABLE = object() def drop_thinking_only_and_merge_users( - messages: List[Dict[str, Any]], *, drop_codex_reasoning_items: bool = True, + messages: list[dict[str, Any]], *, drop_codex_reasoning_items: bool = True, drop_nudge_marker: Optional[str] = None, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: """Drop thinking-only assistant turns and merge adjacent user messages left behind, on the per-call ``api_messages`` copy only (``agent.messages`` is never mutated). Drop-and-merge (not stub text) keeps history honest and preserves role alternation. @@ -1262,7 +1262,7 @@ def drop_thinking_only_and_merge_users( and not _ra().AIAgent._is_thinking_only_assistant(m, drop_codex_reasoning_items=drop_codex_reasoning_items) ] dropped = len(messages) - len(kept) - merged: List[Dict[str, Any]] = [] + merged: list[dict[str, Any]] = [] merges = 0 for m in kept: prev = merged[-1] if merged else None @@ -1312,7 +1312,7 @@ def _primary_reset_gate_blocks(agent, rt, primary_provider, primary_runtime_base return False, prefetched_pool, prefetched -def _restore_runtime_capabilities(agent, rt: Dict[str, Any]) -> None: +def _restore_runtime_capabilities(agent, rt: dict[str, Any]) -> None: # ``capabilities`` is the legacy key from the initial capability propagation patch. raw = rt["runtime_capabilities"] if "runtime_capabilities" in rt else rt.get("capabilities") if isinstance(raw, dict): @@ -1508,7 +1508,7 @@ def extract_reasoning(agent, assistant_message) -> Optional[str]: (OpenRouter unified), else inline thinking blocks in the content; None when absent.""" from agent.message_content import flatten_message_text - parts: List[str] = [] + parts: list[str] = [] def _add(text) -> None: text = flatten_message_text(text, sep="") @@ -1539,8 +1539,8 @@ def extract_reasoning(agent, assistant_message) -> Optional[str]: return "\n\n".join(parts) if parts else None -def _api_error_debug_info(error: Exception) -> Dict[str, Any]: - info: Dict[str, Any] = {"type": type(error).__name__, "message": str(error)} +def _api_error_debug_info(error: Exception) -> dict[str, Any]: + info: dict[str, Any] = {"type": type(error).__name__, "message": str(error)} info.update({ k: v for k in ("status_code", "request_id", "code", "param", "type", "body") if (v := getattr(error, k, None)) is not None @@ -1556,7 +1556,7 @@ def _api_error_debug_info(error: Exception) -> Dict[str, Any]: def dump_api_request_debug( - agent, api_kwargs: Dict[str, Any], *, reason: str, error: Optional[Exception] = None + agent, api_kwargs: dict[str, Any], *, reason: str, error: Optional[Exception] = None ) -> Optional[Path]: """Dump the request body from api_kwargs (minus transport keys) for debugging provider 4xx failures.""" try: @@ -1573,7 +1573,7 @@ def dump_api_request_debug( endpoint = {"codex_responses": "/responses", "anthropic_messages": "/messages"}.get( agent.api_mode, "/chat/completions" ) - dump_payload: Dict[str, Any] = { + dump_payload: dict[str, Any] = { "timestamp": datetime.now().isoformat(), "session_id": agent.session_id, "reason": reason, "request": { "method": "POST", "url": f"{agent.base_url.rstrip('/')}{endpoint}", @@ -1669,7 +1669,7 @@ def plan_cache_sections_for_destination( messages: list, tools: Optional[list], *, provider: str, base_url: str, api_mode: str, model: str, cache_disabled: Optional[bool] = None, cache_ttl: Optional[str] = None, static_system_prefix: Optional[str] = None, -) -> Tuple[list, list]: +) -> tuple[list, list]: """Plan request-local cache sections for one resolved destination (MoA / auxiliary senders): stripped copies (non-caching route) or a ``build_prompt_cache_plan`` layout; never mutates inputs. ``cache_disabled``/``cache_ttl`` default to live config so the operator's disable and @@ -2132,7 +2132,7 @@ _SWITCH_SNAPSHOT_FIELDS = ( _MISSING = object() -def _snapshot_switch_state(agent) -> Dict[str, Any]: +def _snapshot_switch_state(agent) -> dict[str, Any]: """Snapshot every field the swap+rebuild mutates so a failed rebuild rolls back atomically (else a new model name + OLD client 400s next turn). The sentinel distinguishes unset from None: tests build bare agents via ``__new__`` without all fields.""" @@ -2142,7 +2142,7 @@ def _snapshot_switch_state(agent) -> Dict[str, Any]: return snapshot -def _restore_switch_snapshot(agent, snapshot: Dict[str, Any]) -> None: +def _restore_switch_snapshot(agent, snapshot: dict[str, Any]) -> None: for name, value in snapshot.items(): if value is _MISSING: continue # attribute did not exist before the swap; don't fabricate it @@ -2221,7 +2221,7 @@ def _build_switched_client(agent, new_provider, api_key, base_url, api_mode, new try: from hermes_cli.auth import build_minimax_oauth_token_provider effective_key = build_minimax_oauth_token_provider() - except Exception as _mm_exc: # noqa: BLE001 + except Exception as _mm_exc: logger.warning( "MiniMax OAuth: failed to install per-request token provider " "on switch (%s); using static bearer.", _mm_exc, @@ -2299,7 +2299,7 @@ def _swap_switch_runtime(agent, new_model, new_provider, api_key, base_url, api_ try: from agent.credential_pool import load_pool agent._credential_pool = load_pool(new_provider) - except Exception as _pool_exc: # noqa: BLE001 + except Exception as _pool_exc: logger.warning( "switch_model: credential pool reload failed for %s (%s); " "continuing without pool rotation this turn", new_provider, _pool_exc, @@ -2384,7 +2384,7 @@ def _update_switch_compressor(agent, custom_providers, effective_context_length, revalidate_compression_feasibility(agent) -def _build_primary_runtime_snapshot(agent, api_mode) -> Dict[str, Any]: +def _build_primary_runtime_snapshot(agent, api_mode) -> dict[str, Any]: """The ``_primary_runtime`` record that persists a switch across turns.""" cc = getattr(agent, "context_compressor", None) or None rt = { @@ -2548,7 +2548,7 @@ def invoke_tool(agent, function_name: str, function_args: dict, effective_task_i tool_call_id: Optional[str] = None, messages: list = None, pre_tool_block_checked: bool = False, skip_tool_request_middleware: bool = False, - tool_request_middleware_trace: Optional[List[Dict[str, Any]]] = None, + tool_request_middleware_trace: Optional[list[dict[str, Any]]] = None, skip_tool_execution_middleware: bool = False) -> str: """Invoke a single tool (agent-level or registry-dispatched) and return the result string; no display logic. Used by the concurrent path; the sequential path keeps its own inline @@ -2651,8 +2651,8 @@ def repair_tool_call(agent, tool_name: str) -> str | None: tool_name = tool_name[:_idx] if not tool_name: return None - _norm = lambda s: s.lower().replace("-", "_").replace(" ", "_") # noqa: E731 - _camel_snake = lambda s: re.sub(r"(? str | None: lc = s.lower() @@ -2687,7 +2687,7 @@ def repair_tool_call(agent, tool_name: str) -> str | None: # still fire per window). _EMPTY_HEAL_ESCALATE_AFTER = 3 _EMPTY_HEAL_WINDOW_S = 600.0 -_empty_heal_log_state: Dict[str, Dict[str, Any]] = {} +_empty_heal_log_state: dict[str, dict[str, Any]] = {} _empty_heal_log_lock = threading.Lock() # Sessions already told ONCE (out-of-band, never in conversation context); kept apart from the # windowed log state so a new window never re-arms the notice. @@ -2697,7 +2697,7 @@ _empty_heal_log_lock = threading.Lock() _empty_heal_user_notified: set = set() # One-shot pending notices keyed by session, drained via ``consume_pending_sanitizer_heal_notice`` # and delivered via the status/warning callback. -_empty_heal_pending_notice: Dict[str, str] = {} +_empty_heal_pending_notice: dict[str, str] = {} def _content_has_payload(content: Any) -> bool: @@ -2713,7 +2713,7 @@ def _content_has_payload(content: Any) -> bool: ) -def _msg_has_payload(msg: Dict[str, Any]) -> bool: +def _msg_has_payload(msg: dict[str, Any]) -> bool: """True if ``msg`` carries anything the API treats as non-empty content (text, multimodal blocks, tool_calls, reasoning). Role-agnostic counterpart of ``AIAgent._is_thinking_only_assistant``. Codex Responses item carriers persist with content:"" by design (text lives in codex_*_items @@ -2728,7 +2728,7 @@ def _msg_has_payload(msg: Dict[str, Any]) -> bool: ) -def fill_empty_non_final_wire_payload(msg: Dict[str, Any], *, is_final: bool) -> bool: +def fill_empty_non_final_wire_payload(msg: dict[str, Any], *, is_final: bool) -> bool: """Write the interrupted placeholder onto an empty non-final wire copy; True when filled. Pass the per-call copy only; durable history must not be mutated.""" if is_final or not isinstance(msg, dict) or msg.get("role") not in ("user", "assistant"): @@ -2765,7 +2765,7 @@ def consume_pending_sanitizer_heal_notice() -> Optional[str]: return _empty_heal_pending_notice.pop(key, None) -def get_sanitizer_heal_stats() -> Dict[str, Dict[str, Any]]: +def get_sanitizer_heal_stats() -> dict[str, dict[str, Any]]: """Read-only per-session sanitiser heal counters (``heal_events``, ``messages_healed``, ``escalated``) for diagnostics.""" with _empty_heal_log_lock: return { @@ -2830,14 +2830,14 @@ def _log_empty_non_final_heal(healed: int) -> None: ) -def repair_empty_non_final_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def repair_empty_non_final_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Substitute a placeholder for empty-content non-final messages on the per-call copy. Anthropic/litellm/Bedrock 400 on any empty non-final message and a persisted stub poisons every later turn; repairing the wire copy heals the session in memory. Substitution (not deletion) keeps role alternation and tool-call pairing intact. The final message is untouched.""" if not messages or len(messages) < 2: return messages - repaired: List[Dict[str, Any]] = [] + repaired: list[dict[str, Any]] = [] healed = 0 last_idx = len(messages) - 1 for idx, msg in enumerate(messages): @@ -2854,7 +2854,7 @@ def repair_empty_non_final_messages(messages: List[Dict[str, Any]]) -> List[Dict return messages -def _classify_tool_call_orphans(messages: List[Dict[str, Any]]): +def _classify_tool_call_orphans(messages: list[dict[str, Any]]): """Classify orphaned tool-call / tool-result pairs; single source of truth for GLOBAL orphan detection. Returns ``(surviving_call_ids, result_call_ids, orphaned_results, missing_tool_calls)``; every id variant of a tool_call is registered so a result matching any alias survives, and @@ -2880,7 +2880,7 @@ def _classify_tool_call_orphans(messages: List[Dict[str, Any]]): return surviving_call_ids, result_call_ids, orphaned_results, missing_tool_calls -def _drop_invalid_roles(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def _drop_invalid_roles(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Drop messages whose role the API won't accept.""" valid = _ra().AIAgent._VALID_API_ROLES for msg in messages: @@ -2889,7 +2889,7 @@ def _drop_invalid_roles(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: return [m for m in messages if m.get("role") in valid] -def _drop_empty_tool_calls_arrays(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def _drop_empty_tool_calls_arrays(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Strict providers 400 on ``tool_calls: []``; normalize on shallow copies so history stays byte-stable.""" # --- Drop empty / malformed tool_calls arrays on assistant messages --- An assistant message carrying # ``tool_calls: []`` (an empty array) — or a non-list value under the key — is semantically identical to @@ -2902,7 +2902,7 @@ def _drop_empty_tool_calls_arrays(messages: List[Dict[str, Any]]) -> List[Dict[s # per-call copy rather than in ``repair_message_sequence``, which would destructively rewrite the # persisted trajectory. Shallow-copy the message before dropping the key so stored history (and prompt # caching) stays byte-stable. - normalized: List[Dict[str, Any]] = [] + normalized: list[dict[str, Any]] = [] dropped = 0 for msg in messages: if ( @@ -2930,7 +2930,7 @@ def _drop_empty_tool_calls_arrays(messages: List[Dict[str, Any]]) -> List[Dict[s return normalized -def _repair_invalid_tool_call_names(messages: List[Dict[str, Any]]) -> None: +def _repair_invalid_tool_call_names(messages: list[dict[str, Any]]) -> None: """Coerce every ``function.name`` to the provider-safe ``^[A-Za-z0-9_-]{1,64}$``. An empty/missing name becomes the ``invalid_tool_call`` sentinel (dropping would unpair the anti-priming result the dispatch loop keeps for it); an invalid one (``multi_tool_use.parallel``, a shell command a weak @@ -2970,7 +2970,7 @@ def _repair_invalid_tool_call_names(messages: List[Dict[str, Any]]) -> None: } -def _drop_results_without_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def _drop_results_without_ids(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Drop tool results with a missing/empty tool_call_id. Kept explicit (not left to the positional walk) for its own log line and so the final-chokepoint guarantee holds for callers skipping ``repair_message_sequence``.""" @@ -2986,7 +2986,7 @@ def _drop_results_without_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, return kept -def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def _pair_tool_calls_positionally(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Positional tool_call <-> tool_result pairing: strict providers (DeepSeek v4, Kimi) require results IMMEDIATELY after their call. Drops positional orphans, stubs unanswered declared ids; matching is alias-aware.""" @@ -3006,8 +3006,8 @@ def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[s # (``tool_call_id_variants`` / ``tool_result_id_variants``): a result keyed on ANY alias spelling # (``id`` / ``call_id`` / ``response_item_id`` / composite bridge) answers the call, preserving the # unified alias policy from #55626/#63000/#93251. - paired: List[Dict[str, Any]] = [] - declared_calls: Dict[str, tuple] = {} + paired: list[dict[str, Any]] = [] + declared_calls: dict[str, tuple] = {} dropped = 0 stubs = 0 @@ -3060,12 +3060,12 @@ def _pair_tool_calls_positionally(messages: List[Dict[str, Any]]) -> List[Dict[s return paired if (dropped or stubs) else messages -def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def _dedupe_tool_call_ids(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Deduplicate tool_call_ids (strict providers 400 on duplicates): collapse duplicates within an assistant message, drop results answering no OUTSTANDING call. Tracks outstanding calls (not ids ever seen) because llama.cpp reuses one constant id, and whole variant groups so alias-keyed results are not deleted.""" - outstanding: Dict[str, int] = {} # every alias of an unanswered call -> its group id + outstanding: dict[str, int] = {} # every alias of an unanswered call -> its group id # 3. Deduplicate tool_call_ids. Strict providers (DeepSeek) reject a payload where the same tool_call_id # appears more than once with HTTP 400 "Duplicate value for 'tool_call_id'" (#58327). Duplicates can # arise from retries, crash/resume glitches, or a compression window that re-emits a tool result. This @@ -3084,9 +3084,9 @@ def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any] # (#55626/#58168/#63000); tracking only the coalesced id here made a result keyed on any OTHER variant # look like it answered no outstanding call, so this pass deleted the very result step 2's # variant-aware matching had just preserved (issue #93251 — whole parallel batches vanished). - outstanding_groups: Dict[int, frozenset] = {} + outstanding_groups: dict[int, frozenset] = {} next_group_id = 0 - deduped: List[Dict[str, Any]] = [] + deduped: list[dict[str, Any]] = [] removed = 0 for msg in messages: role = msg.get("role") @@ -3132,7 +3132,7 @@ def _dedupe_tool_call_ids(messages: List[Dict[str, Any]]) -> List[Dict[str, Any] return deduped -def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def _realign_tool_result_names(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Align each tool result's wire ``name`` with its call's function name (per-call copy only): Google 400s on a mismatch, routine when tool_search bridges via ``tool_call``.""" # 4. Google matches functionResponse.name against functionCall.name and rejects a mismatch with HTTP 400 @@ -3151,7 +3151,7 @@ def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str, # changes. A no-op for the native Gemini path, which already resolves the same name. A result whose # assistant call frame is missing entirely never reaches here — pass 1 above drops it as an orphan — # so the only results this pass sees are ones whose call name is knowable. - call_names: Dict[str, str] = {} + call_names: dict[str, str] = {} for msg in messages: if msg.get("role") == "assistant": for tc in msg.get("tool_calls") or []: @@ -3160,8 +3160,8 @@ def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str, nm = _ra().AIAgent._get_tool_call_name_static(tc) if cid and nm: call_names[cid] = nm - realigned: List[Tuple[str, str]] = [] - aligned: List[Dict[str, Any]] = [] + realigned: list[tuple[str, str]] = [] + aligned: list[dict[str, Any]] = [] for msg in messages: if msg.get("role") == "tool": expected = call_names.get((msg.get("tool_call_id") or "").strip()) @@ -3181,7 +3181,7 @@ def _realign_tool_result_names(messages: List[Dict[str, Any]]) -> List[Dict[str, return aligned -def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def sanitize_api_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Fix orphaned tool_call / tool_result pairs before every LLM call; runs unconditionally (not gated on the compressor). Order matters: empty non-final messages are healed first so the substituted turn participates in the pairing and dedup passes.""" @@ -3207,7 +3207,7 @@ _ACK_WORKSPACE_MARKERS = ( def looks_like_codex_intermediate_ack( - agent, user_message: Any, assistant_content: str, messages: List[Dict[str, Any]], + agent, user_message: Any, assistant_content: str, messages: list[dict[str, Any]], require_workspace: bool = True, ) -> bool: """Detect a planning/ack message that should continue instead of ending the turn. @@ -3398,7 +3398,7 @@ def looks_like_degenerate_final(text: str, user_message: Any = None) -> bool: return not any(ch.isalpha() and not ch.isascii() for ch in user_text) -def tool_results_this_turn(messages: List[Dict[str, Any]]) -> int: +def tool_results_this_turn(messages: list[dict[str, Any]]) -> int: """Tool-result rows after the most recent user row — whether the turn did real tool work. ANY user row ends the window, the continuation nudges included: that is what bounds the @@ -3651,7 +3651,7 @@ def _iter_pool_sockets(client: Any): yield sock -def _set_reset_from_retry_after(context: Dict[str, Any], retry_after: Any) -> None: +def _set_reset_from_retry_after(context: dict[str, Any], retry_after: Any) -> None: if "reset_at" in context: return seconds = parse_retry_after_seconds(retry_after) @@ -3685,7 +3685,7 @@ def _duration_string_seconds(text: str) -> Optional[float]: return sum(float(n) * _DURATION_UNIT_SECONDS[u] for n, u in parts) -def _set_reset_from_vendor_headers(context: Dict[str, Any], headers: Any) -> None: +def _set_reset_from_vendor_headers(context: dict[str, Any], headers: Any) -> None: for name in _VENDOR_RESET_HEADERS: value = headers.get(name) if not isinstance(value, str) or not value.strip(): @@ -3699,9 +3699,9 @@ def _set_reset_from_vendor_headers(context: Dict[str, Any], headers: Any) -> Non return -def extract_api_error_context(error: Exception) -> Dict[str, Any]: +def extract_api_error_context(error: Exception) -> dict[str, Any]: """Extract structured rate-limit details from provider errors.""" - context: Dict[str, Any] = {} + context: dict[str, Any] = {} body = getattr(error, "body", None) payload = (body.get("error") if isinstance(body.get("error"), dict) else body) if isinstance(body, dict) else None if isinstance(payload, dict): diff --git a/agent/anthropic_adapter.py b/agent/anthropic_adapter.py index a6148e0e7f..468312afea 100644 --- a/agent/anthropic_adapter.py +++ b/agent/anthropic_adapter.py @@ -248,14 +248,14 @@ _CLAUDE_CODE_PREFIXES = ( _CLAUDE_CODE_NAMES = ("claude", "claude-code") -def _claude_code_candidates() -> List[str]: +def _claude_code_candidates() -> list[str]: """Executable paths to try, deduped and filtered to files that exist. Two passes: every PATH hit first (what the user's shell would run), then the well-known install prefixes. A single nested loop would probe a stale prefix ``claude`` before a current PATH ``claude-code``. """ - seen: Dict[str, None] = {} + seen: dict[str, None] = {} for name in _CLAUDE_CODE_NAMES: hit = shutil.which(name) if hit: @@ -351,12 +351,12 @@ def _common_betas_for_base_url(base_url: str | None, *, drop_context_1m_beta: bo return betas -def _beta_header(betas: list) -> Dict[str, str]: +def _beta_header(betas: list) -> dict[str, str]: """``{"anthropic-beta": ...}`` when there are betas, else ``{}``.""" return {"anthropic-beta": ",".join(betas)} if betas else {} -def _attribution_headers() -> Dict[str, str]: +def _attribution_headers() -> dict[str, str]: """Same client-attribution set sent to OpenRouter / Vercel AI Gateway / Fireworks.""" return { "HTTP-Referer": "https://hermes-agent.nousresearch.com", "X-Title": "Hermes Agent", @@ -371,13 +371,13 @@ def _client_timeout(timeout): return Timeout(timeout=float(read), connect=10.0) -def _base_client_kwargs(base_url, timeout) -> tuple[str, Dict[str, Any]]: +def _base_client_kwargs(base_url, timeout) -> tuple[str, dict[str, Any]]: """Shared SDK constructor kwargs -> ``(normalized_base_url, kwargs)``. Retry is delegated to hermes's outer loop (``max_retries=0``): the SDK default of 2 uses its own backoff that ignores Retry-After and double-retries inside our loop. Any trailing ``/v1`` is stripped because the SDK appends ``/v1/messages``. Azure's ``api-version`` goes through ``default_query`` so the base_url is not corrupted into ``/anthropic?api-version=.../v1/messages``.""" - kwargs: Dict[str, Any] = {"timeout": _client_timeout(timeout), "max_retries": 0} + kwargs: dict[str, Any] = {"timeout": _client_timeout(timeout), "max_retries": 0} normalized = re.sub(r"/v1/?$", "", _normalize_base_url_text(base_url).rstrip("/")) if normalized: kwargs["base_url"] = normalized @@ -414,7 +414,7 @@ def _build_anthropic_client_with_bearer_hook( return _new_sdk_client(sdk, kwargs, headers, route=base_url) -def _new_sdk_client(sdk, kwargs: Dict[str, Any], headers: Dict[str, str], route: str = None): +def _new_sdk_client(sdk, kwargs: dict[str, Any], headers: dict[str, str], route: str = None): """``sdk.Anthropic(**kwargs)`` with ``headers`` attached, sending exactly ONE credential. ``route`` is the caller's un-normalized base_url (the ``/v1`` form ``custom_providers`` entries are keyed by; ``kwargs["base_url"]`` has it stripped) for the per-provider ``extra_headers`` lookup. @@ -439,7 +439,7 @@ def _new_sdk_client(sdk, kwargs: Dict[str, Any], headers: Dict[str, str], route: return sdk.Anthropic(**kwargs) -def _custom_provider_extra_headers(base_url) -> Dict[str, str]: +def _custom_provider_extra_headers(base_url) -> dict[str, str]: """``extra_headers`` of the ``custom_providers`` entry routed at *base_url*, else ``{}``. SECURITY: values routinely carry credentials (Cloudflare Access tokens) — never log them.""" if not base_url: @@ -537,7 +537,7 @@ def _normalize_to_mcp_wire(name: str) -> str: return _MCP_TOOL_PREFIX + name.removeprefix("mcp_") -def _oauth_wire_namer(anthropic_tools: List[Dict[str, Any]]): +def _oauth_wire_namer(anthropic_tools: list[dict[str, Any]]): """Return ``name -> OAuth wire name`` for this request's tool set. An alias must never collide with a wire name owned by a non-alias tool: two identical tool names in one request is a hard 400, strictly worse than the bug being fixed. Mirrors normalize_response's "registered tool @@ -596,7 +596,7 @@ def _apply_claude_code_identity(system, anthropic_tools, anthropic_messages, to_ return system -def _thinking_kwargs(reasoning_config: Dict[str, Any], model: str, effective_max_tokens: int) -> Dict[str, Any]: +def _thinking_kwargs(reasoning_config: dict[str, Any], model: str, effective_max_tokens: int) -> dict[str, Any]: """Map ``reasoning_config`` to Anthropic thinking kwargs. Adaptive models (Claude 4.6+, Kimi/Moonshot) get ``thinking.type=adaptive`` + ``output_config.effort``; older models and manual-only compat endpoints (MiniMax) get budget_tokens. Haiku has no extended thinking. On @@ -630,11 +630,11 @@ _TOOL_CHOICE_MAP = {None: {"type": "auto"}, "auto": {"type": "auto"}, "required" def build_anthropic_kwargs( - model: str, messages: List[Dict], tools: Optional[List[Dict]], max_tokens: Optional[int], - reasoning_config: Optional[Dict[str, Any]], tool_choice: Optional[str] = None, + model: str, messages: list[dict], tools: Optional[list[dict]], max_tokens: Optional[int], + reasoning_config: Optional[dict[str, Any]], tool_choice: Optional[str] = None, is_oauth: bool = False, preserve_dots: bool = False, context_length: Optional[int] = None, base_url: str | None = None, fast_mode: bool = False, drop_context_1m_beta: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Build kwargs for anthropic.messages.create(). ``max_tokens`` is the OUTPUT cap for one response; ``context_length`` is the TOTAL window (input + output). ``max_tokens=None`` uses the model's native output ceiling; if that exceeds ``context_length`` (small local endpoints) it is @@ -656,7 +656,7 @@ def build_anthropic_kwargs( to_wire = _oauth_wire_namer(anthropic_tools) if is_oauth else None if to_wire: system = _apply_claude_code_identity(system, anthropic_tools, anthropic_messages, to_wire) - kwargs: Dict[str, Any] = {"model": model, "messages": anthropic_messages, "max_tokens": effective_max_tokens} + kwargs: dict[str, Any] = {"model": model, "messages": anthropic_messages, "max_tokens": effective_max_tokens} if system: kwargs["system"] = system if anthropic_tools: diff --git a/agent/anthropic_credentials.py b/agent/anthropic_credentials.py index 27742c5b6c..4f8d1477b4 100644 --- a/agent/anthropic_credentials.py +++ b/agent/anthropic_credentials.py @@ -85,7 +85,7 @@ def anthropic_route_is_oauth(base_url: Any, credential: Any, *, provider: Option if callable(credential) and not isinstance(credential, str): try: credential = credential() - except Exception: # noqa: BLE001 — classification must never raise + except Exception: return False return isinstance(credential, str) and _is_oauth_token(credential) @@ -223,7 +223,7 @@ _SINGLETON_SOURCE_PATHS = { } -def _claude_oauth_record(data: Any, source: str) -> Optional[Dict[str, Any]]: +def _claude_oauth_record(data: Any, source: str) -> Optional[dict[str, Any]]: """Normalise a ``{"claudeAiOauth": {...}}`` payload into our credential dict.""" oauth_data = data.get("claudeAiOauth") access_token = oauth_data.get("accessToken", "") if isinstance(oauth_data, dict) else "" @@ -252,7 +252,7 @@ def _decode_keychain_attr(match: Optional["re.Match[str]"]) -> str: return match.group("text") or "" -def _find_claude_code_keychain_item() -> Optional[tuple[str, Dict[str, Any]]]: +def _find_claude_code_keychain_item() -> Optional[tuple[str, dict[str, Any]]]: """``(account, payload)`` of the ``Claude Code-credentials`` login Keychain item, or None. One ``find-generic-password -g`` call: attributes on stdout, ``password: …`` on stderr. The @@ -270,8 +270,8 @@ def _find_claude_code_keychain_item() -> Optional[tuple[str, Dict[str, Any]]]: return None if result.returncode != 0: return None - account = _decode_keychain_attr(re.search(r'^\s*"acct"=' + _KEYCHAIN_ATTR + r"\s*$", result.stdout, re.M)) - raw = _decode_keychain_attr(re.search(r"^password: " + _KEYCHAIN_ATTR + r"\s*$", result.stderr, re.M)) + account = _decode_keychain_attr(re.search(r'^\s*"acct"=' + _KEYCHAIN_ATTR + r"\s*$", result.stdout, re.MULTILINE)) + raw = _decode_keychain_attr(re.search(r"^password: " + _KEYCHAIN_ATTR + r"\s*$", result.stderr, re.MULTILINE)) if not account or not raw: return None try: @@ -281,7 +281,7 @@ def _find_claude_code_keychain_item() -> Optional[tuple[str, Dict[str, Any]]]: return (account, payload) if isinstance(payload, dict) else None -def _read_claude_code_keychain_payload() -> Optional[Dict[str, Any]]: +def _read_claude_code_keychain_payload() -> Optional[dict[str, Any]]: """Raw ``{"claudeAiOauth": {...}, ...}`` payload from the macOS Keychain, or None. Returns the full entry (not the normalised credential record) so a refresh @@ -312,7 +312,7 @@ def _read_claude_code_keychain_payload() -> Optional[Dict[str, Any]]: return payload if isinstance(payload, dict) else None -def _keychain_mirror_command(account: str, payload: Dict[str, Any]) -> tuple[list[str], str]: +def _keychain_mirror_command(account: str, payload: dict[str, Any]) -> tuple[list[str], str]: """``(argv, stdin)`` that updates the Claude Code Keychain item with ``payload``. The command line goes to ``security -i`` on stdin, with the secret hex-encoded (``-X``): @@ -328,7 +328,7 @@ def _keychain_mirror_command(account: str, payload: Dict[str, Any]) -> tuple[lis return ["security", "-i"], line -def _read_claude_code_credentials_from_keychain() -> Optional[Dict[str, Any]]: +def _read_claude_code_credentials_from_keychain() -> Optional[dict[str, Any]]: """Read the "Claude Code-credentials" macOS Keychain entry (Claude Code >=2.1.114).""" payload = _read_claude_code_keychain_payload() return _claude_oauth_record(payload, "macos_keychain") if payload else None @@ -343,12 +343,12 @@ def claude_code_credentials_path() -> Path: return root / ".credentials.json" -def _read_claude_code_credentials_from_file() -> Optional[Dict[str, Any]]: +def _read_claude_code_credentials_from_file() -> Optional[dict[str, Any]]: data = _load_json_if_exists(claude_code_credentials_path(), "~/.claude/.credentials.json") return _claude_oauth_record(data, "claude_code_credentials_file") if data is not None else None -def read_claude_code_credentials() -> Optional[Dict[str, Any]]: +def read_claude_code_credentials() -> Optional[dict[str, Any]]: """Read refreshable Claude Code OAuth credentials (Keychain and/or file). When both exist: prefer the only non-expired one (Claude Code 2.1.x refreshes one source but not the other), else the later ``expiresAt`` so a refresh uses the freshest refreshToken. ~/.claude.json primaryApiKey is deliberately excluded. @@ -368,7 +368,7 @@ def read_claude_code_credentials() -> Optional[Dict[str, Any]]: return kc_creds if (kc_creds.get("expiresAt", 0) or 0) >= (file_creds.get("expiresAt", 0) or 0) else file_creds -def is_claude_code_token_valid(creds: Dict[str, Any]) -> bool: +def is_claude_code_token_valid(creds: dict[str, Any]) -> bool: """Non-expired access token (60s buffer); no expiresAt means managed key → valid if present.""" expires_at = creds.get("expiresAt", 0) return int(time.time() * 1000) < (expires_at - 60_000) if expires_at else bool(creds.get("accessToken")) @@ -415,7 +415,7 @@ def _oauth_http_error(exc: Any, *, what: str) -> AnthropicOAuthError: def _post_oauth_token( data: bytes, *, content_type: str, timeout: int, what: str, user_agent: str = _OAUTH_TOKEN_USER_AGENT -) -> Dict[str, Any]: +) -> dict[str, Any]: """POST to the token endpoints in order; raise the last error if all fail.""" import urllib.error import urllib.request @@ -438,7 +438,7 @@ def _post_oauth_token( raise last_error or ValueError(f"Anthropic token {what} failed") -def _oauth_token_state(result: Dict[str, Any], *, fallback_refresh_token: str = "") -> Dict[str, Any]: +def _oauth_token_state(result: dict[str, Any], *, fallback_refresh_token: str = "") -> dict[str, Any]: """Token-endpoint JSON -> ``{access_token, refresh_token, expires_at_ms}`` (expires_in defaults to 3600s).""" return { "access_token": result.get("access_token", ""), @@ -447,7 +447,7 @@ def _oauth_token_state(result: Dict[str, Any], *, fallback_refresh_token: str = } -def refresh_anthropic_oauth_pure(refresh_token: str, *, use_json: bool = False) -> Dict[str, Any]: +def refresh_anthropic_oauth_pure(refresh_token: str, *, use_json: bool = False) -> dict[str, Any]: """Refresh an Anthropic OAuth token without mutating local credential files.""" import urllib.parse if not refresh_token: @@ -462,7 +462,7 @@ def refresh_anthropic_oauth_pure(refresh_token: str, *, use_json: bool = False) return _oauth_token_state(result, fallback_refresh_token=refresh_token) -def _refresh_oauth_token(creds: Dict[str, Any]) -> Optional[str]: +def _refresh_oauth_token(creds: dict[str, Any]) -> Optional[str]: """Refresh an expired Claude Code OAuth token, returning the new access token. Refresh tokens are single-use and Claude Code refreshes on its own schedule, so we first re-read the live sources and adopt an already-rotated token instead of racing it into ``invalid_grant``. Read, decision, POST and write-back share the pool's @@ -545,7 +545,7 @@ def _write_claude_code_credentials( except (OSError, ValueError) as e: logger.error("Failed to write refreshed credentials to %s: %s", cred_path, e) raise CredentialPersistError(cred_path, e) from e - oauth_data: Dict[str, Any] = {"accessToken": access_token, "refreshToken": refresh_token, "expiresAt": expires_at_ms} + oauth_data: dict[str, Any] = {"accessToken": access_token, "refreshToken": refresh_token, "expiresAt": expires_at_ms} if scopes is not None: oauth_data["scopes"] = scopes elif "claudeAiOauth" in existing and "scopes" in existing["claudeAiOauth"]: @@ -557,8 +557,8 @@ def _write_claude_code_credentials( def _merge_keychain_credential_payload( - existing_payload: Dict[str, Any], access_token: str, refresh_token: str, expires_at_ms: int -) -> Dict[str, Any]: + existing_payload: dict[str, Any], access_token: str, refresh_token: str, expires_at_ms: int +) -> dict[str, Any]: """Rotate the ``claudeAiOauth`` token triple over the existing Keychain payload, preserving its metadata (``subscriptionType`` / ``rateLimitTier`` / ``scopes``). @@ -608,7 +608,7 @@ def _mirror_claude_code_credentials_to_keychain( # ── Resolution ── -def _resolve_claude_code_token_from_credentials(creds: Optional[Dict[str, Any]] = None) -> Optional[str]: +def _resolve_claude_code_token_from_credentials(creds: Optional[dict[str, Any]] = None) -> Optional[str]: """Resolve a token from Claude Code credential files, refreshing if needed.""" creds = creds or read_claude_code_credentials() if not creds: @@ -627,7 +627,7 @@ def _resolve_claude_code_token_from_credentials(creds: Optional[Dict[str, Any]] return refreshed or None -def _prefer_refreshable_claude_code_token(env_token: str, creds: Optional[Dict[str, Any]]) -> Optional[str]: +def _prefer_refreshable_claude_code_token(env_token: str, creds: Optional[dict[str, Any]]) -> Optional[str]: """Prefer refreshable Claude Code creds over a static env OAuth token: Hermes historically persisted setup tokens into ANTHROPIC_TOKEN, and that static token would otherwise win before the refreshable file is inspected.""" if not (env_token and _is_oauth_token(env_token) and isinstance(creds, dict) and creds.get("refreshToken")): @@ -751,7 +751,7 @@ def _generate_pkce() -> tuple: return verifier, challenge -def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]: +def run_hermes_oauth_login_pure() -> Optional[dict[str, Any]]: """Run Hermes-native OAuth PKCE flow and return credential state.""" import webbrowser from urllib.parse import urlencode @@ -773,7 +773,7 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]: try: from hermes_cli.auth import _can_open_graphical_browser as _can_open_gui except Exception: - _can_open_gui = lambda: True # noqa: E731 — degrade to prior behavior + _can_open_gui = lambda: True if _can_open_gui(): with contextlib.suppress(Exception): webbrowser.open(auth_url) @@ -806,7 +806,7 @@ def run_hermes_oauth_login_pure() -> Optional[Dict[str, Any]]: return _oauth_token_state(result) -def read_hermes_oauth_credentials() -> Optional[Dict[str, Any]]: +def read_hermes_oauth_credentials() -> Optional[dict[str, Any]]: """Read Hermes-managed OAuth credentials from ~/.hermes/.anthropic_oauth.json.""" data = _load_json_if_exists(_get_hermes_oauth_file(), "Hermes OAuth credentials") return data if data is not None and data.get("accessToken") else None diff --git a/agent/anthropic_message_convert.py b/agent/anthropic_message_convert.py index 4094829db6..7e6d6c66dd 100644 --- a/agent/anthropic_message_convert.py +++ b/agent/anthropic_message_convert.py @@ -27,7 +27,7 @@ def _block_type(b: Any) -> Any: return b.get("type") if isinstance(b, dict) else None -def _has_block_type(blocks: List[Any], types) -> bool: +def _has_block_type(blocks: list[Any], types) -> bool: return any(_block_type(b) in types for b in blocks) @@ -37,18 +37,18 @@ def _is_blank_text_block(b: Any) -> bool: return _block_type(b) == "text" and not (isinstance(b.get("text"), str) and b["text"].strip()) -def _cache_control_of(b: Any) -> Optional[Dict[str, Any]]: +def _cache_control_of(b: Any) -> Optional[dict[str, Any]]: cc = b.get("cache_control") if isinstance(b, dict) else None return cc if isinstance(cc, dict) else None -def _text_block(text: str) -> Dict[str, str]: +def _text_block(text: str) -> dict[str, str]: return {"type": "text", "text": text} -def _text_block_with_citations(text: Any, cits: Any) -> Dict[str, Any]: +def _text_block_with_citations(text: Any, cits: Any) -> dict[str, Any]: """Text block carrying ``citations`` only when it is a non-empty list (the only input-valid shape).""" - block: Dict[str, Any] = _text_block(text) + block: dict[str, Any] = _text_block(text) if isinstance(cits, list) and cits: block["citations"] = cits return block @@ -62,20 +62,20 @@ def _parse_tool_args(raw: Any) -> Any: return {} -def _strip_thinking(blocks: List[Any]) -> List[Any]: +def _strip_thinking(blocks: list[Any]) -> list[Any]: return [b for b in blocks if _block_type(b) not in _THINKING_TYPES] -def _block_ids(blocks: List[Any], btype: str, key: str) -> set: +def _block_ids(blocks: list[Any], btype: str, key: str) -> set: return {b.get(key) for b in blocks if _block_type(b) == btype} -def _assistant_block_lists(result: List[Dict[str, Any]]): +def _assistant_block_lists(result: list[dict[str, Any]]): """``(index, message)`` for every assistant message whose content is a block list.""" return ((i, m) for i, m in enumerate(result) if m.get("role") == "assistant" and isinstance(m.get("content"), list)) -def _carry_cache_control(out: Dict[str, Any], b: Any, *, copy: bool = False) -> Dict[str, Any]: +def _carry_cache_control(out: dict[str, Any], b: Any, *, copy: bool = False) -> dict[str, Any]: """Carry a dict-valued ``cache_control`` marker from ``b`` onto ``out`` (returned); ``copy`` shallow-copies it so the caller's dict is never shared with the wire payload.""" cc = _cache_control_of(b) @@ -84,7 +84,7 @@ def _carry_cache_control(out: Dict[str, Any], b: Any, *, copy: bool = False) -> return out -def _split_blank_text_blocks(blocks: List[Any]) -> Tuple[List[Any], Any, List[int]]: +def _split_blank_text_blocks(blocks: list[Any]) -> tuple[list[Any], Any, list[int]]: """``(kept, relocated_cache_control, dropped_indexes)``: drop blank text blocks, remembering the cache_control of the last one dropped so the caller can relocate the breakpoint.""" dropped = [i for i, blk in enumerate(blocks) if _is_blank_text_block(blk)] @@ -117,11 +117,11 @@ def _sanitize_tool_id(tool_id: str) -> str: return (re.sub(r"[^a-zA-Z0-9_-]", "_", tool_id) if tool_id else "") or "tool_0" -def _tool_use_block(tool_id: Any, name: Any, tool_input: Any) -> Dict[str, Any]: +def _tool_use_block(tool_id: Any, name: Any, tool_input: Any) -> dict[str, Any]: return {"type": "tool_use", "id": _sanitize_tool_id(tool_id), "name": name, "input": tool_input} -def _normalize_tool_input_schema(schema: Any) -> Dict[str, Any]: +def _normalize_tool_input_schema(schema: Any) -> dict[str, Any]: """Normalize a tool schema for Anthropic's validator: collapse nullable unions (``anyOf: [{type: string}, {type: null}]`` from Pydantic/MCP optional fields) to the non-null branch — optionality is already expressed by ``required``; ``keep_nullable_hint=False`` because the @@ -141,7 +141,7 @@ def _normalize_tool_input_schema(schema: Any) -> Dict[str, Any]: return normalized -def convert_tools_to_anthropic(tools: List[Dict]) -> List[Dict]: +def convert_tools_to_anthropic(tools: list[dict]) -> list[dict]: """Convert OpenAI tool definitions to Anthropic format. Duplicate names are dropped with a warning (Anthropic hard-400s on them); ``cache_control`` on the OpenAI tool dict is forwarded.""" result = [] @@ -156,7 +156,7 @@ def convert_tools_to_anthropic(tools: List[Dict]) -> List[Dict]: continue if name: seen_names.add(name) - anthropic_tool: Dict[str, Any] = { + anthropic_tool: dict[str, Any] = { "name": name, "description": fn.get("description", ""), "input_schema": _normalize_tool_input_schema(fn.get("parameters") or {}), } @@ -164,7 +164,7 @@ def convert_tools_to_anthropic(tools: List[Dict]) -> List[Dict]: return result -def _image_block_from_openai_url(url: str) -> Dict[str, Any]: +def _image_block_from_openai_url(url: str) -> dict[str, Any]: """OpenAI image URL / data URL -> Anthropic ``image`` block. An inline subtype the API rejects (svg+xml, bmp, tiff) 400s every replay once in history: an SVG is rasterized to PNG when a rasterizer is installed, anything else unsupported becomes a text placeholder.""" @@ -184,7 +184,7 @@ def _image_block_from_openai_url(url: str) -> Dict[str, Any]: return {"type": "image", "source": {"type": "base64", "media_type": media_type, "data": data}} -def _convert_content_part_to_anthropic(part: Any) -> Optional[Dict[str, Any]]: +def _convert_content_part_to_anthropic(part: Any) -> Optional[dict[str, Any]]: """Convert one OpenAI-style content part to an Anthropic block (None -> dropped).""" if part is None: return None @@ -239,7 +239,7 @@ def _to_plain_data(value: Any, *, _depth: int = 0, _path: Optional[set] = None) return result -def _extract_preserved_thinking_blocks(message: Dict[str, Any]) -> List[Dict[str, Any]]: +def _extract_preserved_thinking_blocks(message: dict[str, Any]) -> list[dict[str, Any]]: """Deep-copied thinking/redacted_thinking blocks from ``reasoning_details``.""" raw_details = message.get("reasoning_details") if not isinstance(raw_details, list): @@ -258,10 +258,10 @@ def _convert_content_to_anthropic(content: Any) -> Any: return [b for b in map(_convert_content_part_to_anthropic, content) if b is not None] -def _content_parts_to_anthropic_blocks(parts: Any) -> List[Dict[str, Any]]: +def _content_parts_to_anthropic_blocks(parts: Any) -> list[dict[str, Any]]: """Tool-message content parts -> tool_result inner blocks (text + image only, the types Anthropic accepts there). Used for multimodal tool results.""" - out: List[Dict[str, Any]] = [] + out: list[dict[str, Any]] = [] for block in map(_convert_content_part_to_anthropic, parts if isinstance(parts, list) else []): if not block: continue @@ -281,7 +281,7 @@ def _safe_text(text: Any) -> str: return text if text.strip() else _EMPTY_TEXT_PLACEHOLDER -def _replay_text(b: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _replay_text(b: dict[str, Any]) -> Optional[dict[str, Any]]: # Drop blank blocks rather than coerce in place: the caller relocates any cache_control and # falls back to a placeholder only when nothing survives, so "(empty)" never sits as # model-visible noise next to real blocks. @@ -290,20 +290,20 @@ def _replay_text(b: Dict[str, Any]) -> Optional[Dict[str, Any]]: return _carry_cache_control(_text_block_with_citations(b["text"], b.get("citations")), b) -def _replay_thinking(b: Dict[str, Any]) -> Dict[str, Any]: +def _replay_thinking(b: dict[str, Any]) -> dict[str, Any]: out = {"type": "thinking", "thinking": b.get("thinking", "")} return {**out, "signature": b["signature"]} if b.get("signature") else out -def _replay_redacted_thinking(b: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _replay_redacted_thinking(b: dict[str, Any]) -> Optional[dict[str, Any]]: return {"type": "redacted_thinking", "data": b["data"]} if b.get("data") else None -def _replay_tool_use(b: Dict[str, Any]) -> Dict[str, Any]: +def _replay_tool_use(b: dict[str, Any]) -> dict[str, Any]: return _carry_cache_control(_tool_use_block(b.get("id", ""), b.get("name", ""), b.get("input", {})), b) -def _replay_image(b: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _replay_image(b: dict[str, Any]) -> Optional[dict[str, Any]]: src = b.get("source") return {"type": "image", "source": src} if isinstance(src, dict) else None @@ -314,7 +314,7 @@ _REPLAY_SANITIZERS = { } -def _sanitize_replay_block(b: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _sanitize_replay_block(b: dict[str, Any]) -> Optional[dict[str, Any]]: """Whitelist a stored Anthropic block so it is valid as REQUEST input. SDK response blocks carry output-only fields the INPUT schema forbids ("Extra inputs are not permitted": ``parsed_output``, ``caller``, ``citations=None``), and ``_to_plain_data`` captured them verbatim. Whitelist per @@ -326,7 +326,7 @@ def _sanitize_replay_block(b: Dict[str, Any]) -> Optional[Dict[str, Any]]: return sanitizer(b) if sanitizer else None -def _apply_assistant_cache_control_to_last_cacheable_block(blocks: List[Dict[str, Any]], cache_control: Any) -> None: +def _apply_assistant_cache_control_to_last_cacheable_block(blocks: list[dict[str, Any]], cache_control: Any) -> None: if not isinstance(cache_control, dict): return for block in reversed(blocks): @@ -335,7 +335,7 @@ def _apply_assistant_cache_control_to_last_cacheable_block(blocks: List[Dict[str break -def _replay_ordered_blocks(m: Dict[str, Any], ordered_blocks: List[Any]) -> Optional[List[Dict[str, Any]]]: +def _replay_ordered_blocks(m: dict[str, Any], ordered_blocks: list[Any]) -> Optional[list[dict[str, Any]]]: """Interleaved-thinking replay: rebuild the assistant turn from the verbatim block list normalize_response stored (only for turns interleaving SIGNED thinking with tool_use). Preserves block ORDER; returns None if nothing survives. tool_use ``input`` is re-sourced from @@ -346,7 +346,7 @@ def _replay_ordered_blocks(m: Dict[str, Any], ordered_blocks: List[Any]) -> Opti for tc in m.get("tool_calls", []) or [] if isinstance(tc, dict) } - replayed: List[Dict[str, Any]] = [] + replayed: list[dict[str, Any]] = [] relocated_cc = None dropped_blank_text = False for b in ordered_blocks: @@ -377,7 +377,7 @@ def _replay_ordered_blocks(m: Dict[str, Any], ordered_blocks: List[Any]) -> Opti return replayed -def assistant_replay_carrier(m: Dict[str, Any]) -> Tuple[bool, List[Dict[str, Any]]]: +def assistant_replay_carrier(m: dict[str, Any]) -> tuple[bool, list[dict[str, Any]]]: """``(ordered, blocks)``: the sanitized ordered sidecar when any block survives it (the whole turn then replays from it), else the ``reasoning_details`` thinking blocks. Single owner of carrier precedence for conversion, accounting and rejected-signature mirrors.""" @@ -389,7 +389,7 @@ def assistant_replay_carrier(m: Dict[str, Any]) -> Tuple[bool, List[Dict[str, An return False, _extract_preserved_thinking_blocks(m) -def _convert_assistant_message(m: Dict[str, Any]) -> Dict[str, Any]: +def _convert_assistant_message(m: dict[str, Any]) -> dict[str, Any]: """Assistant message -> Anthropic content blocks (thinking, text, tool_use, Kimi/DeepSeek reasoning_content injection).""" # apply_anthropic_cache_control marks an assistant turn with non-empty text by writing cache_control @@ -434,10 +434,10 @@ def _convert_assistant_message(m: Dict[str, Any]) -> Dict[str, Any]: return {"role": "assistant", "content": effective} -def _tool_result_content(m: Dict[str, Any]) -> Any: +def _tool_result_content(m: dict[str, Any]) -> Any: """Resolve a tool message's content into tool_result content (blocks or string).""" content = m.get("content", "") - multimodal_blocks: Optional[List[Dict[str, Any]]] = None + multimodal_blocks: Optional[list[dict[str, Any]]] = None if isinstance(content, dict) and content.get("_multimodal"): multimodal_blocks = _content_parts_to_anthropic_blocks(content.get("content") or []) if not multimodal_blocks and content.get("text_summary"): @@ -456,7 +456,7 @@ def _tool_result_content(m: Dict[str, Any]) -> Any: return (content if isinstance(content, str) else json.dumps(content) if content else "") or "(no output)" -def _convert_tool_message_to_result(result: List[Dict[str, Any]], m: Dict[str, Any]) -> None: +def _convert_tool_message_to_result(result: list[dict[str, Any]], m: dict[str, Any]) -> None: """Append a tool_result to ``result``, merging into a trailing tool_result user message when there is one. Mutates ``result`` in place.""" tool_result = { @@ -472,7 +472,7 @@ def _convert_tool_message_to_result(result: List[Dict[str, Any]], m: Dict[str, A result.append({"role": "user", "content": [tool_result]}) -def _convert_user_message(content: Any) -> Dict[str, Any]: +def _convert_user_message(content: Any) -> dict[str, Any]: """Validate and convert a user message to Anthropic format.""" if isinstance(content, list): content = _fix_blank_text_blocks_in_list( @@ -484,7 +484,7 @@ def _convert_user_message(content: Any) -> Dict[str, Any]: return {"role": "user", "content": content} -def _strip_orphaned_tool_blocks(result: List[Dict[str, Any]]) -> None: +def _strip_orphaned_tool_blocks(result: list[dict[str, Any]]) -> None: """Strip tool_use blocks with no matching tool_result, and vice versa. Compression/truncation can remove either side of a pair or insert messages between them. Anthropic requires the tool_result in the IMMEDIATELY FOLLOWING user message — a global id match is not enough. @@ -523,19 +523,19 @@ def _strip_orphaned_tool_blocks(result: List[Dict[str, Any]]) -> None: m["content"] = new_content if new_content else [_text_block("(tool result removed)")] -def _concat_content(prev: Any, curr: Any) -> List[Any]: +def _concat_content(prev: Any, curr: Any) -> list[Any]: """Merge two message contents into one block list, each side's blocks kept intact (a string becomes its own text block). Strings are never joined: the first turn's bytes must equal what a later request replays as a standalone turn, or the prompt-cache prefix diverges at that block (MoA appends per-turn guidance after ``user(task)`` on iteration 1 and replays ``user(task)`` alone on iteration 2 — #112358).""" - as_blocks = lambda c: [_text_block(c)] if isinstance(c, str) else list(c) # noqa: E731 + as_blocks = lambda c: [_text_block(c)] if isinstance(c, str) else list(c) return as_blocks(prev) + as_blocks(curr) -def _merge_consecutive_roles(result: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def _merge_consecutive_roles(result: list[dict[str, Any]]) -> list[dict[str, Any]]: """Merge consecutive same-role messages to enforce alternation. Returns a new list.""" - fixed: List[Dict[str, Any]] = [] + fixed: list[dict[str, Any]] = [] for m in result: if not (fixed and fixed[-1]["role"] == m["role"]): fixed.append(m) @@ -552,7 +552,7 @@ def _merge_consecutive_roles(result: List[Dict[str, Any]]) -> List[Dict[str, Any return fixed -def _keep_valid_thinking(content: List[Any], signature_dead: bool) -> List[Any]: +def _keep_valid_thinking(content: list[Any], signature_dead: bool) -> list[Any]: """Keep valid signed Anthropic thinking, demoting readable unsigned/invalidated thinking. Preserved-thinking models apply this to historical turns too; older models apply it only to the latest assistant turn.""" @@ -570,7 +570,7 @@ def _keep_valid_thinking(content: List[Any], signature_dead: bool) -> List[Any]: return new_content -def _manage_thinking_signatures(result: List[Dict[str, Any]], base_url: str | None, model: str | None) -> None: +def _manage_thinking_signatures(result: list[dict[str, Any]], base_url: str | None, model: str | None) -> None: """Strip or preserve thinking blocks per endpoint. Mutates ``result`` in place. Anthropic signs thinking blocks against the full turn; any upstream mutation invalidates them @@ -606,7 +606,7 @@ def _manage_thinking_signatures(result: List[Dict[str, Any]], base_url: str | No m.pop("_thinking_signature_invalidated", None) # internal flag, never on the wire -def _evict_old_screenshots(result: List[Dict[str, Any]]) -> None: +def _evict_old_screenshots(result: list[dict[str, Any]]) -> None: """Retire screenshot payloads once the request would cross the API's per-request image limit. Mutates ``result`` in place. This wire pass has no byte sizes, so it enforces the block @@ -639,7 +639,7 @@ def _evict_old_screenshots(result: List[Dict[str, Any]]) -> None: ] -def _ensure_leading_user_turn(result: List[Dict[str, Any]]) -> None: +def _ensure_leading_user_turn(result: list[dict[str, Any]]) -> None: """Anthropic requires messages[0].role == user; prepend a placeholder turn otherwise. A second auto-compaction can leave a role=assistant summary first, which the API rejects (often masked as a misleading tool_use/tool_result 400). The filler must be non-whitespace text or it trades @@ -655,8 +655,8 @@ def _ensure_leading_user_turn(result: List[Dict[str, Any]]) -> None: def _fix_blank_text_blocks_in_list( - blocks: List[Any], *, placeholder_text: str, msg_index: int, role: Any, location: str -) -> List[Any]: + blocks: list[Any], *, placeholder_text: str, msg_index: int, role: Any, location: str +) -> list[Any]: """Drop blank text blocks; relocate any cache_control they carried onto the last surviving cacheable block; if nothing survives, substitute one placeholder block (carrying the relocated marker). Non-text blocks and order are untouched. Returns a new list; logs structure only.""" @@ -672,7 +672,7 @@ def _fix_blank_text_blocks_in_list( return kept -def _scrub_blank_text_blocks(result: List[Dict[str, Any]]) -> None: +def _scrub_blank_text_blocks(result: list[dict[str, Any]]) -> None: """Final boundary guard against blank text blocks (HTTP 400 "text content blocks must contain non-whitespace text"), including inside tool_result content. Runs LAST so a blank block from any current or future producer never reaches the wire. Mutates ``result`` in place.""" @@ -712,15 +712,15 @@ def _convert_system_content(content: Any) -> Any: def convert_messages_to_anthropic( - messages: List[Dict], base_url: str | None = None, model: str | None = None -) -> Tuple[Optional[Any], List[Dict]]: + messages: list[dict], base_url: str | None = None, model: str | None = None +) -> tuple[Optional[Any], list[dict]]: """Convert OpenAI-format messages to Anthropic format -> ``(system, messages)``. System is extracted into its own param (a string, or a block list when cache_control is present). ``base_url``/``model`` drive thinking-signature policy — third-party endpoints strip signatures (proprietary, they 400 on them); Kimi-family endpoints/models keep unsigned reasoning_content-derived blocks, which Kimi requires even when empty.""" system = None - result: List[Dict[str, Any]] = [] + result: list[dict[str, Any]] = [] for m in messages: role = m.get("role", "user") if role == "system": diff --git a/agent/api_error_summary.py b/agent/api_error_summary.py index 304adaba99..d4eaa61712 100644 --- a/agent/api_error_summary.py +++ b/agent/api_error_summary.py @@ -78,7 +78,7 @@ class ApiErrorSummaryMixin: @staticmethod def _is_entitlement_failure( - error_context: Optional[Dict[str, Any]], status_code: Optional[int] + error_context: Optional[dict[str, Any]], status_code: Optional[int] ) -> bool: """Detect subscription/entitlement 401/403s that masquerade as auth failures. diff --git a/agent/api_request_hooks.py b/agent/api_request_hooks.py index c52920465e..5ec2926679 100644 --- a/agent/api_request_hooks.py +++ b/agent/api_request_hooks.py @@ -32,7 +32,7 @@ def _model_dump(value: Any) -> Any: class ApiRequestHooksMixin: """Hook payload sanitising + ``api_request_error`` dispatch (see module docstring).""" - def _usage_summary_for_api_request_hook(self, response: Any) -> Optional[Dict[str, Any]]: + def _usage_summary_for_api_request_hook(self, response: Any) -> Optional[dict[str, Any]]: """Token buckets for ``post_api_request`` plugins (no raw ``response`` object).""" if response is None: return None @@ -86,7 +86,7 @@ class ApiRequestHooksMixin: ) if isinstance(value, dict): - out: Dict[str, Any] = {} + out: dict[str, Any] = {} for idx, (key, item) in enumerate(value.items()): if idx >= max_sequence: out["_truncated_items"] = len(value) - max_sequence @@ -131,7 +131,7 @@ class ApiRequestHooksMixin: "_truncated": True, "original_type": type(value).__name__, "preview": encoded[:limit] } - def _api_request_payload_for_hook(self, api_kwargs: Optional[Dict[str, Any]]) -> Dict[str, Any]: + def _api_request_payload_for_hook(self, api_kwargs: Optional[dict[str, Any]]) -> dict[str, Any]: body = { key: value for key, value in (api_kwargs or {}).items() @@ -141,7 +141,7 @@ class ApiRequestHooksMixin: def _api_response_payload_for_hook( self, response: Any, assistant_message: Any, *, finish_reason: Optional[str] - ) -> Dict[str, Any]: + ) -> dict[str, Any]: # Raw provider SDK tool_call objects are handed to the sanitizer on purpose; `_hook_jsonable` must # keep normalising them (model_dump / __dict__ / dataclass) or subscribers get str() blobs. tool_calls = getattr(assistant_message, "tool_calls", None) or [] @@ -162,7 +162,7 @@ class ApiRequestHooksMixin: def _invoke_api_request_error_hook( self, *, task_id: str, turn_id: str, api_request_id: str, api_call_count: int, - api_start_time: float, api_kwargs: Optional[Dict[str, Any]], error_type: str, + api_start_time: float, api_kwargs: Optional[dict[str, Any]], error_type: str, error_message: str, status_code: Optional[int] = None, retry_count: Optional[int] = None, max_retries: Optional[int] = None, retryable: Optional[bool] = None, reason: Optional[str] = None, diff --git a/agent/auxiliary_async_rebuild.py b/agent/auxiliary_async_rebuild.py index a9dec33ebb..514520b541 100644 --- a/agent/auxiliary_async_rebuild.py +++ b/agent/auxiliary_async_rebuild.py @@ -33,7 +33,7 @@ def async_api_key(sync_client: Any) -> Any: return _provide -def configured_default_headers(sync_client: Any) -> Dict[str, str]: +def configured_default_headers(sync_client: Any) -> dict[str, str]: """The ``default_headers`` mapping the sync client was constructed with (SDK ``_custom_headers``). SECURITY: values may carry credentials — never log them. diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 7404bccb1d..7b5d3c4e07 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -46,7 +46,7 @@ from agent.sdk_transform_bypass import bypass_chat_sdk_request_transform # so in-module calls, `auxiliary_client.OpenAI` reads and # `patch("agent.auxiliary_client.OpenAI")` all keep working. if TYPE_CHECKING: - from openai import OpenAI # noqa: F401 — type hints only + from openai import OpenAI _OPENAI_CLS_CACHE: Optional[type] = None @@ -157,7 +157,7 @@ def _resolve_aux_verify(base_url: Optional[str]) -> Any: _WARNED_KEEPALIVE_IMPORT_SKEW = False -def _openai_http_client_kwargs(base_url: Optional[str], *, async_mode: bool = False) -> Dict[str, Any]: +def _openai_http_client_kwargs(base_url: Optional[str], *, async_mode: bool = False) -> dict[str, Any]: """Inject keepalive httpx client with env-only proxy (not macOS system proxy).""" try: from agent.process_bootstrap import build_keepalive_http_client @@ -536,10 +536,10 @@ _LOCAL_SERVER_ALIASES = { "llama.cpp": "custom", "llama-cpp": "custom", } -_ALIAS_TABLE: Optional[Dict[str, str]] = None +_ALIAS_TABLE: Optional[dict[str, str]] = None -def _provider_alias_table() -> Dict[str, str]: +def _provider_alias_table() -> dict[str, str]: """The same alias table the main provider path resolves against (hermes_cli.auth). A hand-copied mirror here rots silently every time auth grows a family — the local @@ -549,7 +549,7 @@ def _provider_alias_table() -> Dict[str, str]: """ global _ALIAS_TABLE if _ALIAS_TABLE is None: - merged: Dict[str, str] = dict(_LOCAL_SERVER_ALIASES) + merged: dict[str, str] = dict(_LOCAL_SERVER_ALIASES) with contextlib.suppress(Exception): from hermes_cli.auth import _PROVIDER_ALIASES as _auth_table merged.update(_auth_table) @@ -647,7 +647,7 @@ _TEMPERATURE_REJECTED_ROUTES: set = set() def remember_temperature_rejection( - provider: Optional[str], base_url: Optional[str], rejected_kwargs: Dict[str, Any], error: BaseException, + provider: Optional[str], base_url: Optional[str], rejected_kwargs: dict[str, Any], error: BaseException, ) -> None: from agent.auxiliary_structured_output import _route_key _TEMPERATURE_REJECTED_ROUTES.add((_route_key(provider, base_url), _bare_model(rejected_kwargs.get("model")))) @@ -816,7 +816,7 @@ def _get_aux_model_for_provider(provider_id: str, *, prefer_fast: bool = False) # Fallback for providers without ProviderProfile.default_aux_model (plus some pinned here). # New providers should set default_aux_model instead. -_API_KEY_PROVIDER_AUX_MODELS_FALLBACK: Dict[str, str] = { +_API_KEY_PROVIDER_AUX_MODELS_FALLBACK: dict[str, str] = { "gemini": "gemini-3.6-flash", "zai": "glm-4.5-flash", "kimi-coding": "kimi-k2-turbo-preview", "stepfun": "step-3.5-flash", "kimi-coding-cn": "kimi-k2-turbo-preview", "gmi": "google/gemini-3.1-flash-lite-preview", "anthropic": "claude-haiku-4-5-20251001", @@ -827,7 +827,7 @@ _API_KEY_PROVIDER_AUX_MODELS_FALLBACK: Dict[str, str] = { } # Legacy alias for callers not yet using _get_aux_model_for_provider(). -_API_KEY_PROVIDER_AUX_MODELS: Dict[str, str] = _API_KEY_PROVIDER_AUX_MODELS_FALLBACK +_API_KEY_PROVIDER_AUX_MODELS: dict[str, str] = _API_KEY_PROVIDER_AUX_MODELS_FALLBACK # Tasks that may opt into ``auxiliary..prefer_fast_model``. _FAST_MODEL_TASKS: frozenset = frozenset({"title_generation"}) @@ -844,7 +844,7 @@ def _task_prefers_fast_model(task: Optional[str]) -> bool: # global and CN); the former glm-5v-turbo pin 404s / 1211 "Unknown Model" on the coding endpoints # (#111429). ZaiProfile has no default_vision_model(), so dropping the pin would route vision to # the user's text-only chat model and skip Z.AI entirely. -_PROVIDER_VISION_MODELS: Dict[str, str] = {"xiaomi": "mimo-v2.5", "zai": "glm-5.3-flash"} +_PROVIDER_VISION_MODELS: dict[str, str] = {"xiaomi": "mimo-v2.5", "zai": "glm-5.3-flash"} def _resolve_provider_vision_default(provider: str) -> Optional[str]: @@ -1028,7 +1028,7 @@ def _load_pool_with_credentials(provider: str, note: str = "") -> Optional[Any]: return pool if pool and pool.has_credentials() else None -def _select_pool_entry(provider: str) -> Tuple[bool, Optional[Any]]: +def _select_pool_entry(provider: str) -> tuple[bool, Optional[Any]]: """Return (pool_exists_for_provider, selected_entry).""" pool = _load_pool_with_credentials(provider) if pool is None: @@ -1390,7 +1390,7 @@ class _CodexCompletionsAdapter: self._client = real_client self._model = model - def _build_responses_kwargs(self, kwargs: Dict[str, Any]) -> Tuple[Dict[str, Any], str, Any]: + def _build_responses_kwargs(self, kwargs: dict[str, Any]) -> tuple[dict[str, Any], str, Any]: """chat.completions kwargs → Responses API kwargs, ``(resp_kwargs, model, timeout)``; mirrors codex.py::build_kwargs.""" # Separate system/instructions from replayable conversation messages, then route the rest through # the SINGLE shared chat->Responses converter used by the main agent transport @@ -1449,7 +1449,7 @@ class _CodexCompletionsAdapter: # converter (a private loop here once let role="tool" leak into input[]; the shared one # encodes tool history as function_call/function_call_output). instructions = "You are a helpful assistant." - replay_messages: List[Dict[str, Any]] = [] + replay_messages: list[dict[str, Any]] = [] for msg in kwargs.get("messages", []): content = msg.get("content") or "" if msg.get("role", "user") == "system": @@ -1476,7 +1476,7 @@ class _CodexCompletionsAdapter: current_issuer_kind=issuer_kind, current_issuer_model=wire_model, native_compaction_eligible=False, ) - resp_kwargs: Dict[str, Any] = { + resp_kwargs: dict[str, Any] = { # Codex only knows the base slug; strip the Hermes ``-900k`` picker suffix. "model": wire_model, "instructions": instructions, "input": input_items or [_role_message_item("user", "")], "store": False, @@ -1674,7 +1674,7 @@ class AsyncCodexAuxiliaryClient(_AsyncAuxiliaryClientBase): pass -def _translate_anthropic_response_format(anthropic_kwargs: Dict[str, Any], response_format: Any) -> None: +def _translate_anthropic_response_format(anthropic_kwargs: dict[str, Any], response_format: Any) -> None: """Merge an OpenAI response format into Anthropic ``output_config``.""" if not isinstance(response_format, dict): return @@ -2014,7 +2014,7 @@ def _resolve_nous_pool_runtime_api(*, force_refresh: bool = False) -> Optional[t if entry is None: return None - def _entry_state(e: Any) -> Dict[str, Any]: + def _entry_state(e: Any) -> dict[str, Any]: return {k: getattr(e, k, None) for k in ( "agent_key", "agent_key_expires_at", "access_token", "expires_at", "scope")} @@ -2059,7 +2059,7 @@ def _resolve_nous_runtime_api( return _creds_pair(creds) -def _creds_pair(creds: Dict[str, Any]) -> Optional[Tuple[str, str]]: +def _creds_pair(creds: dict[str, Any]) -> Optional[tuple[str, str]]: """``(api_key, base_url)`` from a runtime-credentials dict, or None when either is missing.""" api_key = str(creds.get("api_key") or "").strip() base_url = str(creds.get("base_url") or "").strip().rstrip("/") @@ -2068,7 +2068,7 @@ def _creds_pair(creds: Dict[str, Any]) -> Optional[Tuple[str, str]]: return api_key, base_url -def _resolve_xai_oauth_for_aux() -> Optional[Tuple[str, str]]: +def _resolve_xai_oauth_for_aux() -> Optional[tuple[str, str]]: """Fresh xAI OAuth (api_key, base_url) for aux clients, or None. Pool first (some xAI OAuth logins exist only as pool entries), then the singleton auth-store resolver. @@ -2082,7 +2082,7 @@ def _resolve_xai_oauth_for_aux() -> Optional[Tuple[str, str]]: api_key = str( getattr(entry, "runtime_api_key", None) or getattr(entry, "access_token", "") or "" ).strip() - _url = lambda v: str(v or "").strip().rstrip("/") # noqa: E731 + _url = lambda v: str(v or "").strip().rstrip("/") base_url = _xai_validate_inference_base_url( _url(_scoped_key_env("HERMES_XAI_BASE_URL")) or _url(_scoped_key_env("XAI_BASE_URL")) @@ -2103,7 +2103,7 @@ def _resolve_xai_oauth_for_aux() -> Optional[Tuple[str, str]]: return _creds_pair(creds) -def _resolve_codex_credential_and_base() -> Tuple[Optional[str], str]: +def _resolve_codex_credential_and_base() -> tuple[Optional[str], str]: """``(token, base_url)`` taken from ONE authority, so a Codex key is only ever sent to the host it belongs to (#121486): the profile-scoped ``HERMES_CODEX_BASE_URL`` wins; otherwise a pooled key goes where that pool entry routes (row URL / ``model.base_url``) and the auth.json OAuth @@ -2146,7 +2146,7 @@ def _read_codex_singleton_token() -> Optional[str]: return None -def _resolve_api_key_provider() -> Tuple[Optional[OpenAI], Optional[str]]: +def _resolve_api_key_provider() -> tuple[Optional[OpenAI], Optional[str]]: """Try each API-key provider in PROVIDER_REGISTRY order; (client, model) or (None, None).""" try: from hermes_cli.auth import PROVIDER_REGISTRY, resolve_api_key_provider_credentials @@ -2271,7 +2271,7 @@ def _is_free_model(model: Optional[str]) -> bool: return normalized.endswith(":free") or normalized.startswith("stealth/") -def _aux_openrouter_settings() -> Tuple[bool, str]: +def _aux_openrouter_settings() -> tuple[bool, str]: """Read (free_only, openrouter_model) from config; (False, _OPENROUTER_MODEL) on failure.""" try: from hermes_cli.config import cfg_get, load_config_readonly @@ -2297,7 +2297,7 @@ def _warn_paid_lane_once(model: str) -> None: def _try_openrouter(explicit_api_key: Optional[Union[str, Callable[[], str]]] = None, model: str = None, - explicit_base_url: Optional[str] = None) -> Tuple[Optional[OpenAI], Optional[str]]: + explicit_base_url: Optional[str] = None) -> tuple[Optional[OpenAI], Optional[str]]: free_only, cfg_model = _aux_openrouter_settings() or_model = model or cfg_model if free_only and not _is_free_model(or_model): @@ -2355,7 +2355,7 @@ def _describe_openrouter_unavailable(model: str = None) -> str: return "no usable OpenRouter credentials found" -def _try_nous(vision: bool = False) -> Tuple[Optional[OpenAI], Optional[str]]: +def _try_nous(vision: bool = False) -> tuple[Optional[OpenAI], Optional[str]]: nous = _read_nous_auth() runtime = _resolve_nous_runtime_api(force_refresh=False) if runtime is None and not nous: @@ -2475,7 +2475,7 @@ _read_main_api_key = functools.partial(_read_main_field, "api_key", readonly=Fal _read_main_base_url = functools.partial(_read_main_field, "base_url", readonly=False) -def _resolve_moa_aggregator(preset_name: Optional[str]) -> Tuple[Optional[str], Optional[str]]: +def _resolve_moa_aggregator(preset_name: Optional[str]) -> tuple[Optional[str], Optional[str]]: """MoA preset → aggregator (provider, model); (None, None) if unresolvable. None/"" = default preset. "moa" is virtual — aux tasks skip the fan-out and use the aggregator slot; shared so lookup can't drift. @@ -2535,11 +2535,11 @@ _RUNTIME_MAIN_BASE_URL: str = "" _RUNTIME_MAIN_API_KEY: Any = "" _RUNTIME_MAIN_API_MODE: str = "" _RUNTIME_MAIN_AUTH_MODE: str = "" -_RUNTIME_MAIN_CONTEXT: contextvars.ContextVar[Optional[Dict[str, Any]]] = ( +_RUNTIME_MAIN_CONTEXT: contextvars.ContextVar[Optional[dict[str, Any]]] = ( contextvars.ContextVar("auxiliary_runtime_main", default=None) ) -_RELAY_AUX_CALL_CONTEXT: contextvars.ContextVar[Optional[Dict[str, Any]]] = ( +_RELAY_AUX_CALL_CONTEXT: contextvars.ContextVar[Optional[dict[str, Any]]] = ( contextvars.ContextVar("auxiliary_relay_call", default=None) ) @@ -2595,7 +2595,7 @@ def _set_relay_auxiliary_route(provider: str | None, model: str | None, api_mode def _record_route_info( - route_info: Optional[Dict[str, str]], provider: Optional[str], model: Optional[str] + route_info: Optional[dict[str, str]], provider: Optional[str], model: Optional[str] ) -> None: """Expose the concrete route selected for one auxiliary call.""" if route_info is not None: @@ -2699,7 +2699,7 @@ def _relay_sync_stream( kwargs = prepare_chat_messages(client, kwargs) # The bypass runs inside the provider callback, AFTER Relay has seen (and possibly # rewritten) the real conversation; applying it to `kwargs` would hand Relay an empty one. - create = lambda request: client.chat.completions.create(**bypass_chat_sdk_request_transform(request, client)) # noqa: E731 + create = lambda request: client.chat.completions.create(**bypass_chat_sdk_request_transform(request, client)) route = _relay_auxiliary_metadata(provider=provider, api_mode=api_mode) if route is None: return create(kwargs) @@ -2717,11 +2717,11 @@ def _relay_sync_stream( ) -_RUNTIME_MAIN_COMPAT_SNAPSHOT: Tuple[Any, ...] = ("", "", "", "", "", "") +_RUNTIME_MAIN_COMPAT_SNAPSHOT: tuple[Any, ...] = ("", "", "", "", "", "") _RUNTIME_MAIN_COMPAT_LOCK = threading.Lock() -def _publish_runtime_main_mirrors(values: Tuple[Any, ...]) -> None: +def _publish_runtime_main_mirrors(values: tuple[Any, ...]) -> None: """Write the legacy globals + compat snapshot (``_MAIN_RUNTIME_FIELDS`` order) under the lock.""" global _RUNTIME_MAIN_PROVIDER, _RUNTIME_MAIN_MODEL, _RUNTIME_MAIN_BASE_URL, _RUNTIME_MAIN_API_KEY global _RUNTIME_MAIN_API_MODE, _RUNTIME_MAIN_AUTH_MODE, _RUNTIME_MAIN_COMPAT_SNAPSHOT @@ -2731,7 +2731,7 @@ def _publish_runtime_main_mirrors(values: Tuple[Any, ...]) -> None: _RUNTIME_MAIN_COMPAT_SNAPSHOT = tuple(values) -def _compat_runtime_main() -> Optional[Dict[str, Any]]: +def _compat_runtime_main() -> Optional[dict[str, Any]]: """Expose deliberately patched legacy globals as a main context. Mirrors must never become runtime inputs: a direct patch counts only when it differs from @@ -2797,7 +2797,7 @@ def reset_runtime_main(token: contextvars.Token) -> None: @contextlib.contextmanager -def scoped_runtime_main(main_runtime: Optional[Dict[str, Any]]): +def scoped_runtime_main(main_runtime: Optional[dict[str, Any]]): """Temporarily bind an explicit runtime without touching legacy mirrors.""" runtime = _normalize_main_runtime(main_runtime) token = _RUNTIME_MAIN_CONTEXT.set(runtime or None) @@ -2813,7 +2813,7 @@ def clear_runtime_main() -> None: _publish_runtime_main_mirrors(("", "", "", "", "", "")) -def _resolve_custom_runtime() -> Tuple[Optional[str], Optional[str], Optional[str]]: +def _resolve_custom_runtime() -> tuple[Optional[str], Optional[str], Optional[str]]: """Resolve the active custom/main endpoint like the main CLI (env OPENAI_BASE_URL or config-saved).""" try: from hermes_cli.auth import AuthError @@ -2893,7 +2893,7 @@ def _validate_base_url(base_url: str) -> None: ) from exc -def _try_custom_endpoint() -> Tuple[Optional[Any], Optional[str]]: +def _try_custom_endpoint() -> tuple[Optional[Any], Optional[str]]: runtime = _resolve_custom_runtime() custom_base, custom_key, custom_mode = (*runtime, None) if len(runtime) == 2 else runtime if not custom_base or not custom_key: @@ -2931,7 +2931,7 @@ def _try_custom_endpoint() -> Tuple[Optional[Any], Optional[str]]: return _maybe_wrap_anthropic(_fallback_client, model, custom_key, custom_base, custom_mode), model -def _build_xai_oauth_aux_client(model: str) -> Tuple[Optional[Any], Optional[str]]: +def _build_xai_oauth_aux_client(model: str) -> tuple[Optional[Any], Optional[str]]: """CodexAuxiliaryClient for xAI Grok OAuth (Responses API); (None, None) if not authed. Caller must pass an explicit model — a pinned Grok default would rot as xAI's allowlist drifts. @@ -2960,7 +2960,7 @@ def _codex_base_url_override() -> str: return _scoped_key_env("HERMES_CODEX_BASE_URL").rstrip("/") -def _build_codex_client(model: str) -> Tuple[Optional[Any], Optional[str]]: +def _build_codex_client(model: str) -> tuple[Optional[Any], Optional[str]]: """CodexAuxiliaryClient for an explicit model; (None, None) without a Codex OAuth token. No auto-selected default: the Codex model allow-list is undocumented and drifts. @@ -2985,7 +2985,7 @@ def _build_codex_client(model: str) -> Tuple[Optional[Any], Optional[str]]: def _try_azure_foundry( *, model: Optional[str] = None, explicit_api_key: Optional[str] = None, explicit_base_url: Optional[str] = None, api_mode: Optional[str] = None, -) -> Tuple[Optional[Any], Optional[str]]: +) -> tuple[Optional[Any], Optional[str]]: """Azure Foundry aux client via the main agent's ``_resolve_azure_foundry_runtime`` (api_key vs Entra callable bearer, per-model api_mode, base_url overrides). Returns ``(client, model)`` or ``(None, None)``.""" try: @@ -3029,7 +3029,7 @@ def _try_azure_foundry( return None, None # The SDK drops api-version query params from the base URL; pass via default_query. _clean_base, _dq = _extract_url_query_params(base_url) - extra: Dict[str, Any] = {"default_query": _dq} if _dq else {} + extra: dict[str, Any] = {"default_query": _dq} if _dq else {} client = _create_openai_client(api_key=api_key, base_url=_clean_base, **extra) if runtime_api_mode == "codex_responses": return CodexAuxiliaryClient(client, final_model), final_model @@ -3040,7 +3040,7 @@ def _try_azure_foundry( def _try_anthropic(explicit_api_key: Optional[Union[str, Callable[[], str]]] = None, - explicit_base_url: Optional[str] = None) -> Tuple[Optional[Any], Optional[str]]: + explicit_base_url: Optional[str] = None) -> tuple[Optional[Any], Optional[str]]: try: from agent.anthropic_adapter import build_anthropic_client from agent.anthropic_credentials import resolve_anthropic_token @@ -3103,7 +3103,7 @@ _MAIN_RUNTIME_CONTEXT_FIELDS = _MAIN_RUNTIME_FIELDS + ( ) -def _normalize_main_runtime(main_runtime: Optional[Dict[str, Any]]) -> Dict[str, Any]: +def _normalize_main_runtime(main_runtime: Optional[dict[str, Any]]) -> dict[str, Any]: """Return a sanitized copy of a live main-runtime override. ``api_key`` may be a zero-arg callable (Entra ID token provider, accepted by the OpenAI SDK) @@ -3116,7 +3116,7 @@ def _normalize_main_runtime(main_runtime: Optional[Dict[str, Any]]) -> Dict[str, main_runtime = _compat_runtime_main() if not isinstance(main_runtime, dict): return {} - normalized: Dict[str, Any] = {} + normalized: dict[str, Any] = {} for field in _MAIN_RUNTIME_CONTEXT_FIELDS: value = main_runtime.get(field) if field == "api_key" and callable(value) and not isinstance(value, str): @@ -3130,7 +3130,7 @@ def _normalize_main_runtime(main_runtime: Optional[Dict[str, Any]]) -> Dict[str, return normalized -def _get_provider_chain() -> List[tuple]: +def _get_provider_chain() -> list[tuple]: """Ordered provider detection chain, built at call time so ``_try_*`` patches are picked up. ``openai-codex`` is deliberately absent (shifting allow-list breaks guessed-model fallback). @@ -3144,9 +3144,9 @@ def _get_provider_chain() -> List[tuple]: # "Recently 402'd" unhealthy-provider cache: a depleted provider stays so for hours, so hiding it # for a TTL saves an RTT per aux call. In-process only (profiles may use different keys). _AUX_UNHEALTHY_TTL_SECONDS = 600 # 10 minutes -_aux_unhealthy_until: Dict[Any, float] = {} -_aux_unhealthy_logged_at: Dict[Any, float] = {} -_aux_unhealthy_reason: Dict[Any, str] = {} +_aux_unhealthy_until: dict[Any, float] = {} +_aux_unhealthy_logged_at: dict[Any, float] = {} +_aux_unhealthy_reason: dict[Any, str] = {} # resolved_provider / explicit-config names → chain labels. _AUX_UNHEALTHY_LABEL_ALIASES = { "openrouter": "openrouter", "nous": "nous", "custom": "local/custom", @@ -3227,7 +3227,7 @@ def _reset_aux_unhealthy_cache() -> None: _aux_unhealthy_reason.clear() -def _contains_any(text: str, needles: Tuple[str, ...]) -> bool: +def _contains_any(text: str, needles: tuple[str, ...]) -> bool: """True when any needle is a substring of ``text``.""" return any(kw in text for kw in needles) @@ -3624,7 +3624,7 @@ def _pool_credential_digest(pool: Any, entry: Any = None) -> str: return digest.hexdigest() -def _pool_cache_hint(provider: str, *, main_runtime: Optional[Dict[str, Any]] = None) -> str: +def _pool_cache_hint(provider: str, *, main_runtime: Optional[dict[str, Any]] = None) -> str: """Return a cache discriminator that follows the pooled credential, not just its entry id. ``::`` when ``peek()`` names an entry, @@ -3665,7 +3665,7 @@ _AUTH_REFRESH_PROVIDER_BY_HOST = ( ) -def _provider_for_host(base_url: str, table: Tuple[Tuple[str, str], ...]) -> Optional[str]: +def _provider_for_host(base_url: str, table: tuple[tuple[str, str], ...]) -> Optional[str]: """First provider in ``table`` whose host matches ``base_url``, else None.""" for host, provider in table: if base_url_host_matches(base_url, host): @@ -3674,7 +3674,7 @@ def _provider_for_host(base_url: str, table: Tuple[Tuple[str, str], ...]) -> Opt def _recoverable_pool_provider( - resolved_provider: str, client: Any, main_runtime: Optional[Dict[str, Any]] = None + resolved_provider: str, client: Any, main_runtime: Optional[dict[str, Any]] = None ) -> Optional[str]: """Infer which provider pool can recover the current auxiliary client. None when the client targets a different host than the session's configured endpoint for that @@ -3737,7 +3737,7 @@ def _recover_provider_pool(provider: str, exc: Exception, *, failed_api_key: str status_code = getattr(exc, "status_code", None) def _rotate(fallback_status: int) -> bool: - error_context: Dict[str, Any] = {"message": str(exc)} + error_context: dict[str, Any] = {"message": str(exc)} if status_code is not None: error_context["status_code"] = status_code next_entry = pool.mark_exhausted_and_rotate( @@ -3764,12 +3764,12 @@ def _recover_provider_pool(provider: str, exc: Exception, *, failed_api_key: str def _prepare_same_provider_retry( *, task: Optional[str], resolved_provider: str, resolved_model: Optional[str], resolved_base_url: Optional[str], resolved_api_key: Optional[str], - resolved_api_mode: Optional[str], main_runtime: Optional[Dict[str, Any]], + resolved_api_mode: Optional[str], main_runtime: Optional[dict[str, Any]], final_model: Optional[str], messages: list, temperature: Optional[float], max_tokens: Optional[int], tools: Optional[list], effective_timeout: float, effective_extra_body: dict, reasoning_config: Optional[dict], async_mode: bool, - extra_headers: Optional[Dict[str, str]] = None, -) -> Tuple[Any, Dict[str, Any]]: + extra_headers: Optional[dict[str, str]] = None, +) -> tuple[Any, dict[str, Any]]: """Rebuild (client, request kwargs) for a same-provider retry after credential recovery.""" if task == "vision": effective_provider, retry_client, retry_model = resolve_vision_provider_client( @@ -3824,7 +3824,7 @@ async def _retry_same_provider_async(*, resolved_provider: str, resolved_api_mod ) -def _creds_have_api_key(creds: Dict[str, Any]) -> bool: +def _creds_have_api_key(creds: dict[str, Any]) -> bool: return bool(str(creds.get("api_key", "") or "").strip()) @@ -3886,7 +3886,7 @@ def _refresh_vertex_credentials() -> bool: # Each refresher returns True when a usable credential exists; the caller then evicts cached clients. -_CREDENTIAL_REFRESHERS: Dict[str, Callable[..., bool]] = { +_CREDENTIAL_REFRESHERS: dict[str, Callable[..., bool]] = { "copilot": _refresh_copilot_credentials, "openai-codex": _refresh_codex_credentials, "nous": _refresh_nous_credentials, "anthropic": _refresh_anthropic_credentials, "xai-oauth": _refresh_xai_oauth_credentials, "vertex": _refresh_vertex_credentials, @@ -3925,7 +3925,7 @@ def _auth_refresh_provider_for_route( return host_provider or normalized -def _fallback_chain_entry(task: Optional[str], fb_label: str) -> Optional[Dict[str, Any]]: +def _fallback_chain_entry(task: Optional[str], fb_label: str) -> Optional[dict[str, Any]]: """Resolve the ``fallback_chain`` entry a ``fallback_chain[]()`` label points at, or None when the label is not a configured-chain candidate or the index no longer resolves.""" if not task or not fb_label: @@ -3991,7 +3991,7 @@ def _complete_fallback_destination( def _fallback_destination_from_entry( - entry: Dict[str, Any], fb_client: Any, fb_model: Optional[str] + entry: dict[str, Any], fb_client: Any, fb_model: Optional[str] ) -> _FallbackDestination: provider = str(entry.get("provider") or "").strip() base_url = str(entry.get("base_url") or getattr(fb_client, "base_url", "") or "").strip() @@ -4033,7 +4033,7 @@ def _fallback_request_kwargs( tools: Optional[list], temperature: Optional[float], max_tokens: Optional[int], effective_timeout: float, effective_extra_body: dict, reasoning_config: Optional[dict], fallback_entry: dict, task_config: dict, apply_fast_lane: bool, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Build request kwargs for one fallback destination (cache-section replan + fast-lane cap).""" fallback_max_tokens, fallback_extra_body = max_tokens, effective_extra_body if apply_fast_lane: @@ -4054,7 +4054,7 @@ def _fallback_request_kwargs( def _plan_fallback_candidate( fb_client: Any, fb_model: Optional[str], fb_label: str, *, task: Optional[str], effective_timeout: float, apply_fast_lane: bool, **request, -) -> Tuple[_FallbackDestination, Dict[str, Any], Callable[[str, Any, Optional[str]], Dict[str, Any]]]: +) -> tuple[_FallbackDestination, dict[str, Any], Callable[[str, Any, Optional[str]], dict[str, Any]]]: """Resolve the destination + first-attempt kwargs for a fallback candidate. Returns ``(destination, kwargs, rebuild)`` where ``rebuild(provider, client, model)`` produces @@ -4077,7 +4077,7 @@ def _plan_fallback_candidate( task_config=task_config, apply_fast_lane=apply_fast_lane, **request, ) - def _rebuild(provider: str, client: Any, model: Optional[str]) -> Tuple[_FallbackDestination, Dict[str, Any]]: + def _rebuild(provider: str, client: Any, model: Optional[str]) -> tuple[_FallbackDestination, dict[str, Any]]: retry_destination = _FallbackDestination( provider, destination.base_url or str(getattr(client, "base_url", "") or ""), destination.api_mode, model or destination.model, @@ -4104,10 +4104,10 @@ def _quarantine_fallback_candidate( def _plan_fallback_auth_retry( destination: _FallbackDestination, - rebuild: Callable[[str, Any, Optional[str]], Tuple[_FallbackDestination, Dict[str, Any]]], *, + rebuild: Callable[[str, Any, Optional[str]], tuple[_FallbackDestination, dict[str, Any]]], *, async_mode: bool, failed_api_key: str = "", -) -> Tuple[str, Optional[Tuple[Any, Dict[str, Any], _FallbackDestination]]]: +) -> tuple[str, Optional[tuple[Any, dict[str, Any], _FallbackDestination]]]: """After an auth error on a fallback candidate: refresh credentials and rebuild the request. Returns ``(refresh_provider, retry)``; ``retry`` = ``(client, kwargs, destination)`` or None.""" fb_provider = _auth_refresh_provider_for_route(destination.provider, destination.base_url) @@ -4145,7 +4145,7 @@ def _call_fallback_candidate_sync( reasoning_config=reasoning_config, ) - def _send(client: Any, request_kwargs: Dict[str, Any], dest: _FallbackDestination) -> Any: + def _send(client: Any, request_kwargs: dict[str, Any], dest: _FallbackDestination) -> Any: return _validate_llm_response( _relay_sync_completion( client, request_kwargs, provider=dest.provider, api_mode=dest.api_mode, @@ -4158,7 +4158,7 @@ def _call_fallback_candidate_sync( ) from agent.auxiliary_fallback_recovery import send_with_parameter_rungs - def _send_recovering(client: Any, request_kwargs: Dict[str, Any], dest: _FallbackDestination) -> Any: + def _send_recovering(client: Any, request_kwargs: dict[str, Any], dest: _FallbackDestination) -> Any: return send_with_parameter_rungs( lambda c, kw: _send(c, kw, dest), client, request_kwargs, task=task) try: @@ -4201,14 +4201,14 @@ async def _call_fallback_candidate_async( reasoning_config=reasoning_config, ) - async def _send(client: Any, request_kwargs: Dict[str, Any], dest: _FallbackDestination) -> Any: + async def _send(client: Any, request_kwargs: dict[str, Any], dest: _FallbackDestination) -> Any: return _validate_llm_response( await _relay_async_completion(client, request_kwargs, provider=dest.provider, api_mode=dest.api_mode), task, ) from agent.auxiliary_fallback_recovery import send_with_parameter_rungs_async - async def _send_recovering(client: Any, request_kwargs: Dict[str, Any], dest: _FallbackDestination) -> Any: + async def _send_recovering(client: Any, request_kwargs: dict[str, Any], dest: _FallbackDestination) -> Any: return await send_with_parameter_rungs_async( lambda c, kw: _send(c, kw, dest), client, request_kwargs, task=task) try: @@ -4241,8 +4241,8 @@ async def _call_fallback_candidate_async( def _try_payment_fallback( failed_provider: str, task: str = None, reason: str = "payment error", *, - failed_base_url: str = "", failure_scope: Any = None, main_runtime: Optional[Dict[str, Any]] = None, -) -> Tuple[Optional[Any], Optional[str], str]: + failed_base_url: str = "", failure_scope: Any = None, main_runtime: Optional[dict[str, Any]] = None, +) -> tuple[Optional[Any], Optional[str], str]: """Try the auto-detection chain after a payment/credit or connection error, skipping the failed provider (and the main-provider path when it maps to the same backend). Returns (client, model, label) or (None, None, "").""" skip = failed_provider.lower().strip() @@ -4301,7 +4301,7 @@ def _failed_backend_skip( def _try_main_agent_model_fallback( failed_provider: str, task: str = None, reason: str = "error", failed_model: Optional[str] = None, failed_base_url: str = "", failure_scope: Any = None, -) -> Tuple[Optional[Any], Optional[str], str]: +) -> tuple[Optional[Any], Optional[str], str]: """Last-resort fallback to the main agent provider + model after the configured chain is exhausted. ``failed_model`` scoping per ``_failed_backend_skip``; same-URL custom endpoints serve many models, so a hung aux model says nothing about the main model's health. Returns (client, model, label) or (None, None, "").""" @@ -4375,7 +4375,7 @@ def _candidate_context_window(provider: str, model: str, base_url: str = "", api def _context_too_small( - entry: Dict[str, Any], provider: str, model: str, min_ctx: Optional[int], *, + entry: dict[str, Any], provider: str, model: str, min_ctx: Optional[int], *, task: Optional[str], label: str, name_model: bool = False, ) -> Optional[str]: """Screen one fallback candidate by context window; returns the ``tried`` note when it is too small.""" @@ -4397,7 +4397,7 @@ def _context_too_small( def _try_configured_fallback_chain( task: str, failed_provider: str, reason: str = "error", failed_model: Optional[str] = None, *, failed_base_url: str = "", failure_scope: Any = None, -) -> Tuple[Optional[Any], Optional[str], str]: +) -> tuple[Optional[Any], Optional[str], str]: """Try auxiliary..fallback_chain entries in order (each needs ``provider``; model/base_url/api_key optional). ``failed_model`` scoping per ``_failed_backend_skip`` (sibling models on the same provider still run after a model-scoped failure). Returns (client, model, provider_label) or (None, None, "").""" @@ -4448,7 +4448,7 @@ def _try_configured_fallback_chain( def _try_configured_fallback_for_unavailable_client( task: Optional[str], failed_provider: str -) -> Tuple[Optional[Any], Optional[str], str]: +) -> tuple[Optional[Any], Optional[str], str]: """Task fallback_chain when an explicit aux provider cannot build a client (no key/OAuth/pool creds); stops at the per-task chain — the main-agent model stays the runtime last resort.""" explicit = (failed_provider or "").strip().lower() @@ -4457,13 +4457,13 @@ def _try_configured_fallback_for_unavailable_client( return _try_configured_fallback_chain(task, explicit, reason="provider unavailable") -def _fallback_entry_api_key(entry: Dict[str, Any]) -> Optional[str]: +def _fallback_entry_api_key(entry: dict[str, Any]) -> Optional[str]: """Resolve inline or env-backed API key via the secret-scope-aware resolver (no raw os.getenv under multiplexing).""" from hermes_cli.fallback_config import resolve_entry_api_key return resolve_entry_api_key(entry) -def _resolve_fallback_entry(entry: Dict[str, Any]) -> Tuple[Optional[Any], Optional[str]]: +def _resolve_fallback_entry(entry: dict[str, Any]) -> tuple[Optional[Any], Optional[str]]: """Resolve one fallback entry through the central provider router.""" provider = str(entry.get("provider") or "").strip() model = str(entry.get("model") or "").strip() or None @@ -4483,7 +4483,7 @@ def _resolve_fallback_entry(entry: Dict[str, Any]) -> Tuple[Optional[Any], Optio def _try_main_fallback_chain( task: Optional[str], failed_provider: str = "", reason: str = "error", *, failed_model: Optional[str] = None, failed_base_url: str = "", failure_scope: Any = None, -) -> Tuple[Optional[Any], Optional[str], str]: +) -> tuple[Optional[Any], Optional[str], str]: """Top-level main-agent fallback chain for a ``provider: auto`` auxiliary call: auto tasks honour the user's main fallback policy before the built-in discovery chain; read via ``get_fallback_chain`` so ``fallback_providers`` and legacy ``fallback_model`` keep the main agent's order.""" @@ -4498,7 +4498,7 @@ def _try_main_fallback_chain( return None, None, "" skip = _failed_backend_skip( failed_provider, failed_model, failed_base_url=failed_base_url, failure_scope=failure_scope) - tried: List[str] = [] + tried: list[str] = [] min_ctx = _task_minimum_context_length(task) for i, entry in enumerate(chain): if not isinstance(entry, dict): @@ -4557,7 +4557,7 @@ def _warn_stale_openai_base_url(runtime_provider: str) -> None: _stale_base_url_warned = True -def _main_route_target(runtime: Dict[str, Any], task: Optional[str]) -> Tuple[str, str, str, Any, str]: +def _main_route_target(runtime: dict[str, Any], task: Optional[str]) -> tuple[str, str, str, Any, str]: """Step-1 target: (provider, model, base_url, api_key, api_mode) of the main runtime, after the fast-model opt-in and the MoA aggregator substitution.""" main_provider = str(runtime.get("provider", "") or _read_main_provider() or "") @@ -4585,7 +4585,7 @@ def _main_route_target(runtime: Dict[str, Any], task: Optional[str]) -> Tuple[st def _try_main_provider_route( main_provider: str, main_model: str, runtime_base_url: str, runtime_api_key: Any, runtime_api_mode: str, -) -> Optional[Tuple[Any, str, str]]: +) -> Optional[tuple[Any, str, str]]: """Step 1: route aux onto the main provider + main model; None if unusable.""" if not (main_provider and main_model and main_provider not in {"auto", ""}): return None @@ -4646,7 +4646,7 @@ def _discovery_chain_allowed(main_provider: str, task: Optional[str] = None) -> return False -def _try_discovery_chain() -> Tuple[Optional[OpenAI], Optional[str], str]: +def _try_discovery_chain() -> tuple[Optional[OpenAI], Optional[str], str]: """Step 3: hardcoded aggregator/fallback chain, skipping unhealthy providers.""" tried = [] for label, try_fn in _get_provider_chain(): @@ -4671,8 +4671,8 @@ def _try_discovery_chain() -> Tuple[Optional[OpenAI], Optional[str], str]: def _resolve_auto_route( - main_runtime: Optional[Dict[str, Any]] = None, task: Optional[str] = None -) -> Tuple[Optional[OpenAI], Optional[str], str]: + main_runtime: Optional[dict[str, Any]] = None, task: Optional[str] = None +) -> tuple[Optional[OpenAI], Optional[str], str]: """Full auto-detection chain, including the selected provider identity. Priority: (1) main provider + main model, regardless of provider type ("auto" means "my main model for side tasks too"; explicit per-task overrides still win); (2) configured fallback policy — task chain, then the main agent's @@ -4771,7 +4771,7 @@ def _normalize_resolved_model(model_name: Optional[str], provider: str) -> Optio return model_name -def _named_custom_api_key(custom_entry: Dict[str, Any], provider: str, custom_base: str) -> Any: +def _named_custom_api_key(custom_entry: dict[str, Any], provider: str, custom_base: str) -> Any: """Credential for a named custom provider: inline api_key → key_env → key_cmd → credential pool → placeholder. Aux resolves named custom providers here, not via _resolve_named_custom_runtime, so key_cmd must be honoured at the same precedence or every aux call 401s.""" @@ -4804,7 +4804,7 @@ def _named_custom_api_key(custom_entry: Dict[str, Any], provider: str, custom_ba return custom_key or "no-key-required" -def _build_bedrock_client(provider: str, model: Optional[str], *, raw_codex: bool) -> Tuple[Optional[Any], Optional[str]]: +def _build_bedrock_client(provider: str, model: Optional[str], *, raw_codex: bool) -> tuple[Optional[Any], Optional[str]]: """AWS Bedrock: Claude → Anthropic Bedrock SDK (prompt caching, thinking); bare in-Region OpenAI IDs → Mantle Responses; everything else, incl. OpenAI ``us.``/``global.`` profiles, → Converse API.""" try: @@ -4828,7 +4828,7 @@ def _build_bedrock_client(provider: str, model: Optional[str], *, raw_codex: boo final_model = _normalize_resolved_model(model or default_model, provider) or default_model if bedrock_openai_uses_mantle(final_model): # Module-level lazy ``OpenAI`` proxy on purpose so tests can patch("agent.auxiliary_client.OpenAI"). - client_kwargs: Dict[str, Any] = { + client_kwargs: dict[str, Any] = { "api_key": resolve_bedrock_bearer_token() or "aws-sdk", "base_url": bedrock_openai_base_url(region), } @@ -4851,7 +4851,7 @@ def _build_bedrock_client(provider: str, model: Optional[str], *, raw_codex: boo return client, final_model -def _build_vertex_client(provider: str, model: Optional[str]) -> Tuple[Optional[Any], Optional[str]]: +def _build_vertex_client(provider: str, model: Optional[str]) -> tuple[Optional[Any], Optional[str]]: """Google Vertex AI: Gemini via the OpenAI-compatible endpoint with an OAuth2 bearer (standard OpenAI client).""" try: from agent.vertex_adapter import get_vertex_config, has_vertex_credentials @@ -4887,12 +4887,12 @@ class _ResolveRequest(NamedTuple): explicit_base_url: Optional[str] explicit_api_key: Optional[Union[str, Callable[[], str]]] api_mode: Optional[str] - main_runtime: Optional[Dict[str, Any]] + main_runtime: Optional[dict[str, Any]] is_vision: bool task: Optional[str] -_ResolveResult = Tuple[Optional[Any], Optional[str]] +_ResolveResult = tuple[Optional[Any], Optional[str]] def _normalize_api_key(raw: Any) -> Union[str, Callable[[], str]]: @@ -5005,7 +5005,7 @@ def _resolve_auto_branch(req: _ResolveRequest) -> _ResolveResult: routed_client, routed_model = _route_client(req, client, model or resolved) if routed_client is not None and effective_provider: try: - setattr(routed_client, "_hermes_aux_effective_provider", effective_provider) + routed_client._hermes_aux_effective_provider = effective_provider except (AttributeError, TypeError): logger.debug("Auxiliary client %s cannot retain effective provider %s", type(routed_client).__name__, effective_provider) @@ -5146,7 +5146,7 @@ def _resolve_custom_branch(req: _ResolveRequest) -> _ResolveResult: return None, None -def _named_custom_openai_wire_client(custom_base: str, custom_key: Any, extra_headers: Optional[Dict[str, str]] = None): +def _named_custom_openai_wire_client(custom_base: str, custom_key: Any, extra_headers: Optional[dict[str, str]] = None): """Plain OpenAI client on the /v1 equivalent of a named custom entry's base URL. ``extra_headers`` is the entry's own header block (gateway routing tags, proxy auth): the main @@ -5336,7 +5336,7 @@ def _resolve_api_key_branch(req: _ResolveRequest, pconfig: Any, resolve_creds: C return _route_client(req, client, final_model) -def _resolve_external_process_branch(req: _ResolveRequest, creds: Dict[str, Any]) -> _ResolveResult: +def _resolve_external_process_branch(req: _ResolveRequest, creds: dict[str, Any]) -> _ResolveResult: """PROVIDER_REGISTRY ``external_process`` providers, served via their registered profile.""" provider = req.provider final_model = _normalize_resolved_model( @@ -5410,7 +5410,7 @@ def _resolve_registry_branch(req: _ResolveRequest) -> _ResolveResult: # Explicit providers with a dedicated branch; anything else falls through to named custom # providers → azure-foundry → PROVIDER_REGISTRY (order preserved from the original if-chain). -_EXPLICIT_PROVIDER_BRANCHES: Dict[str, Callable[[_ResolveRequest], _ResolveResult]] = { +_EXPLICIT_PROVIDER_BRANCHES: dict[str, Callable[[_ResolveRequest], _ResolveResult]] = { "auto": _resolve_auto_branch, "openrouter": _resolve_openrouter_branch, "nous": _resolve_nous_branch, @@ -5423,9 +5423,9 @@ _EXPLICIT_PROVIDER_BRANCHES: Dict[str, Callable[[_ResolveRequest], _ResolveResul def resolve_provider_client( provider: str, model: str = None, async_mode: bool = False, raw_codex: bool = False, explicit_base_url: str = None, explicit_api_key: Optional[Union[str, Callable[[], str]]] = None, - api_mode: str = None, main_runtime: Optional[Dict[str, Any]] = None, is_vision: bool = False, + api_mode: str = None, main_runtime: Optional[dict[str, Any]] = None, is_vision: bool = False, task: Optional[str] = None, -) -> Tuple[Optional[Any], Optional[str]]: +) -> tuple[Optional[Any], Optional[str]]: """Central router: return a configured client (auth, base URL, API format) for a provider + optional model. The client always exposes ``.chat.completions.create()``; Codex/Responses providers get an adapter. ``provider``: built-in name, ``custom:``, "custom" (OPENAI_BASE_URL + OPENAI_API_KEY) or "auto" @@ -5511,7 +5511,7 @@ def resolve_provider_client( # ── Public API ────────────────────────────────────────────────────────────── -def get_text_auxiliary_client(task: str = "", *, main_runtime: Optional[Dict[str, Any]] = None) -> Tuple[Optional[OpenAI], Optional[str]]: +def get_text_auxiliary_client(task: str = "", *, main_runtime: Optional[dict[str, Any]] = None) -> tuple[Optional[OpenAI], Optional[str]]: """Return (client, default_model_slug) for text-only aux tasks; ``task`` selects auxiliary. overrides.""" provider, model, base_url, api_key, api_mode = _resolve_task_provider_model(task or None) return resolve_provider_client( @@ -5541,7 +5541,7 @@ def _normalize_vision_provider(provider: Optional[str]) -> str: return _normalize_aux_provider(provider) -def _deepinfra_strict_vision_backend(model: Optional[str]) -> Tuple[Optional[Any], Optional[str]]: +def _deepinfra_strict_vision_backend(model: Optional[str]) -> tuple[Optional[Any], Optional[str]]: """DeepInfra vision: default model is discovered live via default_vision_model() so no hardcoded id can rot.""" vision_model = model or _resolve_provider_vision_default("deepinfra") if not vision_model: @@ -5553,7 +5553,7 @@ def _deepinfra_strict_vision_backend(model: Optional[str]) -> Tuple[Optional[Any # Strict (explicitly requested) vision backends by normalized provider name. nous MUST go # through resolve_provider_client so anthropic/* picks wrap onto /v1/messages (a bare _try_nous # client 404s). openai-codex has no safe default model; callers set auxiliary..model. -_STRICT_VISION_BACKENDS: Dict[str, Callable[[Optional[str]], Tuple[Optional[Any], Optional[str]]]] = { +_STRICT_VISION_BACKENDS: dict[str, Callable[[Optional[str]], tuple[Optional[Any], Optional[str]]]] = { "copilot": lambda model: resolve_provider_client("copilot", model, is_vision=True), "openrouter": lambda model: _try_openrouter(model=model), "nous": lambda model: resolve_provider_client("nous", model, is_vision=True), @@ -5564,17 +5564,17 @@ _STRICT_VISION_BACKENDS: Dict[str, Callable[[Optional[str]], Tuple[Optional[Any] } -def _resolve_strict_vision_backend(provider: str, model: Optional[str] = None) -> Tuple[Optional[Any], Optional[str]]: +def _resolve_strict_vision_backend(provider: str, model: Optional[str] = None) -> tuple[Optional[Any], Optional[str]]: backend = _STRICT_VISION_BACKENDS.get(_normalize_vision_provider(provider)) return backend(model) if backend is not None else (None, None) -def get_available_vision_backends() -> List[str]: +def get_available_vision_backends() -> list[str]: """Available vision backends in auto-selection order (active provider → OpenRouter → Nous → DeepInfra). Single source of truth for setup, tool gating, and runtime auto-routing. """ - available: List[str] = [] + available: list[str] = [] main_provider = _read_main_provider() if main_provider and main_provider not in {"auto", ""}: if main_provider in _VISION_AUTO_PROVIDER_ORDER: @@ -5592,7 +5592,7 @@ def get_available_vision_backends() -> List[str]: def _finalize_vision_client( resolved_provider: str, sync_client: Any, default_model: Optional[str], resolved_model: Optional[str], async_mode: bool, -) -> Tuple[Optional[str], Optional[Any], Optional[str]]: +) -> tuple[Optional[str], Optional[Any], Optional[str]]: """Apply the explicit model override (and async wrapping) to a resolved vision client.""" if sync_client is None: return resolved_provider, None, None @@ -5604,9 +5604,9 @@ def _finalize_vision_client( def _vision_main_provider_client( - main_provider: str, main_model: str, runtime: Dict[str, Any], resolved_model: Optional[str], + main_provider: str, main_model: str, runtime: dict[str, Any], resolved_model: Optional[str], resolved_api_mode: Optional[str], -) -> Tuple[Optional[Any], Optional[str]]: +) -> tuple[Optional[Any], Optional[str]]: """Auto-detect step 1: try the main provider; (None, None) falls through to the aggregator chain.""" # A provider vision default (static override or catalog discovery) is a *known* multimodal # model; the pinned chat model usually isn't, so only fall back to it when no default exists. @@ -5652,9 +5652,9 @@ def _vision_main_provider_client( def _vision_auto_route( - runtime: Dict[str, Any], resolved_model: Optional[str], resolved_api_mode: Optional[str], + runtime: dict[str, Any], resolved_model: Optional[str], resolved_api_mode: Optional[str], async_mode: bool, -) -> Tuple[Optional[str], Optional[Any], Optional[str]]: +) -> tuple[Optional[str], Optional[Any], Optional[str]]: """Auto-detect order: 1. main provider + model, 2. OpenRouter, 3. Nous Portal, 4. DeepInfra, 5. stop.""" main_provider = str(runtime.get("provider") or _read_main_provider()) main_model = str(runtime.get("model") or _read_main_model()) @@ -5688,8 +5688,8 @@ _ZAI_OPENAI_VISION_URLS = ("https://open.bigmodel.cn/api/paas/v4", "https://api. def resolve_vision_provider_client( provider: Optional[str] = None, model: Optional[str] = None, *, base_url: Optional[str] = None, api_key: Optional[str] = None, async_mode: bool = False, - main_runtime: Optional[Dict[str, Any]] = None, -) -> Tuple[Optional[str], Optional[Any], Optional[str]]: + main_runtime: Optional[dict[str, Any]] = None, +) -> tuple[Optional[str], Optional[Any], Optional[str]]: """Resolve the client actually used for vision tasks. Direct endpoint overrides beat provider selection; explicit providers may force @@ -5756,7 +5756,7 @@ def auxiliary_max_tokens_param(value: int, *, model: Optional[str] = None) -> di # bounding growth to one entry per provider config (avoids fd accumulation in gateways). # This bounds cache growth to one entry per unique provider config rather than one per (config × # event-loop), which previously caused unbounded fd accumulation in long-running gateway processes (#10200). -_client_cache: Dict[tuple, tuple] = {} +_client_cache: dict[tuple, tuple] = {} _client_cache_lock = threading.Lock() _CLIENT_CACHE_MAX_SIZE = 64 # safety belt — evict oldest when exceeded @@ -5791,7 +5791,7 @@ def _runtime_cache_discriminator(field: str, value: Any) -> Any: def _client_cache_key( provider: str, *, async_mode: bool, base_url: Optional[str] = None, api_key: Optional[str] = None, api_mode: Optional[str] = None, - main_runtime: Optional[Dict[str, Any]] = None, is_vision: bool = False, + main_runtime: Optional[dict[str, Any]] = None, is_vision: bool = False, task: Optional[str] = None, model: Optional[str] = None, ) -> tuple: runtime = _normalize_main_runtime(main_runtime) @@ -5809,7 +5809,7 @@ def _client_cache_key( _borrowed_main_credential_key(provider, base_url, api_key, runtime)) -def _borrowed_main_credential_key(provider: str, base_url: Optional[str], api_key: Any, runtime: Dict[str, Any]) -> tuple: +def _borrowed_main_credential_key(provider: str, base_url: Optional[str], api_key: Any, runtime: dict[str, Any]) -> tuple: """What a keyless ``custom`` route borrows from the main runtime when its client is built. The client keeps that credential for its lifetime, so it joins the cache key: otherwise a @@ -5849,9 +5849,9 @@ def _store_cached_client(cache_key: tuple, client: Any, default_model: Optional[ def _refresh_nous_auxiliary_client( *, cache_provider: str, model: Optional[str], async_mode: bool, base_url: Optional[str] = None, api_key: Optional[str] = None, api_mode: Optional[str] = None, - main_runtime: Optional[Dict[str, Any]] = None, is_vision: bool = False, + main_runtime: Optional[dict[str, Any]] = None, is_vision: bool = False, lookup_model: Optional[str] = None, lookup_task: Optional[str] = None, -) -> Tuple[Optional[Any], Optional[str]]: +) -> tuple[Optional[Any], Optional[str]]: """Refresh Nous runtime creds, rebuild the client, and replace the cache entry. ``model`` is the resolved wire model stored as the entry's usable model and returned. The @@ -6014,9 +6014,9 @@ def _compat_model(client: Any, model: Optional[str], cached_default: Optional[st def _get_cached_client( provider: str, model: str = None, async_mode: bool = False, base_url: str = None, - api_key: str = None, api_mode: str = None, main_runtime: Optional[Dict[str, Any]] = None, + api_key: str = None, api_mode: str = None, main_runtime: Optional[dict[str, Any]] = None, is_vision: bool = False, task: Optional[str] = None, -) -> Tuple[Optional[Any], Optional[str]]: +) -> tuple[Optional[Any], Optional[str]]: """Get or create a cached client for the given provider. Async clients bind to the loop they were created on, so every async hit validates the cached @@ -6095,7 +6095,7 @@ def _get_cached_client( # nonsensical "MOA_API_KEY environment variable" error for a provider that was never meant to be reached # over the wire. Auxiliary tasks don't need the reference fan-out — resolve to the preset's aggregator slot # instead, exactly like the implicit path does (shared helper: _resolve_moa_aggregator). -def _unwrap_moa_provider(prov: str, mdl: Optional[str]) -> Tuple[str, Optional[str]]: +def _unwrap_moa_provider(prov: str, mdl: Optional[str]) -> tuple[str, Optional[str]]: """Resolve an *explicit* ``provider: moa`` to its preset's aggregator slot (_resolve_auto_route() only unwraps the implicit case; "moa" isn't in PROVIDER_REGISTRY and would dead-end).""" if prov.strip().lower() != "moa": @@ -6174,7 +6174,7 @@ def _named_custom_provider_present(name: str) -> bool: def _resolve_task_provider_model( task: str = None, provider: str = None, model: str = None, base_url: Optional[str] = None, api_key: Optional[str] = None, -) -> Tuple[str, Optional[str], Optional[str], Optional[str], Optional[str]]: +) -> tuple[str, Optional[str], Optional[str], Optional[str], Optional[str]]: """Determine (provider, model, base_url, api_key, api_mode) for a call. Priority: explicit args > config auxiliary.{task}.* > "auto". A bare base_url means custom, @@ -6263,17 +6263,17 @@ _COMPRESSION_TIMEOUT_FLOOR_SECONDS = 300.0 # Read-time resolution of auxiliary. (plugin defaults, inherit_from) lives in its own module; # re-exported here because callers and tests reach it as agent.auxiliary_client._get_auxiliary_task_config. -from agent.auxiliary_task_config import _get_auxiliary_task_config # noqa: E402,F401 +from agent.auxiliary_task_config import _get_auxiliary_task_config class CompressionFastLane(NamedTuple): """Explicit, non-reasoning compression route.""" certified_non_reasoning: bool - reasoning_config: Optional[Dict[str, Any]] + reasoning_config: Optional[dict[str, Any]] -def _fast_lane_config_fields(config: Dict[str, Any]) -> tuple[str, str, bool]: +def _fast_lane_config_fields(config: dict[str, Any]) -> tuple[str, str, bool]: """Only explicit reasoning disablement certifies a non-reasoning route.""" from hermes_constants import parse_reasoning_effort provider = str(config.get("provider") or "").strip().lower() @@ -6285,7 +6285,7 @@ def _fast_lane_config_fields(config: Dict[str, Any]) -> tuple[str, str, bool]: def resolve_compression_fast_lane( actual_provider: str, actual_model: Optional[str], *, requested_provider: Optional[str] = None, - requested_model: Optional[str] = None, route_config: Optional[Dict[str, Any]] = None, + requested_model: Optional[str] = None, route_config: Optional[dict[str, Any]] = None, ) -> CompressionFastLane: """Certify explicit non-reasoning settings only on the matching destination.""" config = route_config if route_config is not None else _get_auxiliary_task_config("compression") @@ -6301,7 +6301,7 @@ def resolve_compression_fast_lane( return CompressionFastLane(False, None) -def _compression_config_claims_fast_lane(config: Dict[str, Any]) -> bool: +def _compression_config_claims_fast_lane(config: dict[str, Any]) -> bool: """Whether task config declares fast-only controls that cannot leak.""" provider, model, non_reasoning = _fast_lane_config_fields(config) return provider not in {"", "auto"} and model.lower() not in {"", "auto"} and non_reasoning @@ -6309,9 +6309,9 @@ def _compression_config_claims_fast_lane(config: Dict[str, Any]) -> bool: def _compression_fast_lane_controls( task: str | None, *, actual_provider: str, actual_model: str | None, - requested_provider: str | None, requested_model: str | None, route_config: Dict[str, Any], - leak_guard_config: Dict[str, Any], max_tokens: int | None, extra_body: Dict[str, Any], -) -> tuple[int | None, Dict[str, Any]]: + requested_provider: str | None, requested_model: str | None, route_config: dict[str, Any], + leak_guard_config: dict[str, Any], max_tokens: int | None, extra_body: dict[str, Any], +) -> tuple[int | None, dict[str, Any]]: """Apply the certified compression controls to one resolved route.""" if task != "compression" or max_tokens is not None: return max_tokens, extra_body @@ -6399,7 +6399,7 @@ def _with_custom_endpoint_extra_body( return {**inherited, **(extra_body or {})} -def _get_task_extra_body(task: str) -> Dict[str, Any]: +def _get_task_extra_body(task: str) -> dict[str, Any]: """Shallow copy of ``auxiliary..extra_body`` with ``reasoning_effort`` folded into ``reasoning`` unless one is configured (more specific wins). MoA tasks are excluded: their reasoning depth is per-slot in the preset.""" @@ -6437,8 +6437,8 @@ def _get_task_extra_body(task: str) -> Dict[str, Any]: # stays bounded. See #23324. # Keyed by profile home as well: the limit is the profile's ``auxiliary..max_concurrency``, and two # multiplexed profiles with different limits would otherwise rebuild (and reset) one shared semaphore. -_aux_sync_semaphores: Dict[Tuple[str, str], Tuple[int, threading.BoundedSemaphore]] = {} -_aux_async_semaphores: Dict[Tuple[str, str, int], Tuple[int, Any]] = {} +_aux_sync_semaphores: dict[tuple[str, str], tuple[int, threading.BoundedSemaphore]] = {} +_aux_async_semaphores: dict[tuple[str, str, int], tuple[int, Any]] = {} _aux_sem_lock = threading.Lock() @@ -6622,9 +6622,9 @@ def _dedupe_tool_names(tools: list, provider: str, model: str) -> list: class _ProfileProjection(NamedTuple): - body: Dict[str, Any] - reasoning_extra: Dict[str, Any] - top_level: Dict[str, Any] + body: dict[str, Any] + reasoning_extra: dict[str, Any] + top_level: dict[str, Any] handles_reasoning: bool messages_wire: bool = False @@ -6647,9 +6647,9 @@ def _project_provider_profile( provider: str, provider_norm: str, model: str, effective_base: str, reasoning_config: Optional[dict], ) -> _ProfileProjection: """Provider profile's extra_body / kwargs projection; partial on failure.""" - body: Dict[str, Any] = {} - reasoning_extra: Dict[str, Any] = {} - top_level: Dict[str, Any] = {} + body: dict[str, Any] = {} + reasoning_extra: dict[str, Any] = {} + top_level: dict[str, Any] = {} handles_reasoning = False messages_wire = False try: @@ -6685,7 +6685,7 @@ def _project_provider_profile( def _merge_aux_extra_body( extra_body: Optional[dict], projection: _ProfileProjection, reasoning_config: Optional[dict], provider_norm: str, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Caller extra_body + profile body/reasoning + generic reasoning fallback + Nous tags.""" merged_extra = dict(extra_body or {}) caller_reasoning_fields = { @@ -6744,7 +6744,7 @@ def _build_call_kwargs( ``no_progress_timeout`` is a Codex-Responses-only extra (consumed by ``_CodexCompletionsAdapter.create``'s ``**kwargs`` catch-all); callers must only pass it when the resolved client is a ``CodexAuxiliaryClient`` — real SDK clients don't accept it.""" - kwargs: Dict[str, Any] = {"model": model, "messages": messages, "timeout": timeout} + kwargs: dict[str, Any] = {"model": model, "messages": messages, "timeout": timeout} if no_progress_timeout is not None: kwargs["no_progress_timeout"] = no_progress_timeout effective_base = base_url or (_current_custom_base_url() if provider == "custom" else "") @@ -6980,7 +6980,7 @@ def _extract_aux_response_text(response: Any) -> str: output = _field(response, "output") if not isinstance(output, list): return "" - parts: List[str] = [] + parts: list[str] = [] for item in output: item_type = _field(item, "type") if item_type and item_type != "message": @@ -7100,7 +7100,7 @@ def _affordable_max_tokens_from_error(exc: Exception) -> Optional[int]: def _create_with_progress( - client: Any, kwargs: Dict[str, Any], task: Optional[str] = None, *, force_stream: bool = False + client: Any, kwargs: dict[str, Any], task: Optional[str] = None, *, force_stream: bool = False ) -> Any: """Credit-aware :func:`_create_with_progress_once`: a 402 naming an affordable budget retries ONCE with that cap (only ever lowering); anything else re-raises.""" @@ -7124,7 +7124,7 @@ def _create_with_progress( return _create_with_progress_once(client, retry_kwargs, task, force_stream=force_stream) -def _stream_request_plan(kwargs: Dict[str, Any]) -> "Tuple[Dict[str, Any], str, float]": +def _stream_request_plan(kwargs: dict[str, Any]) -> "tuple[dict[str, Any], str, float]": """(stream kwargs, model name, total ceiling) for a streamed re-aggregation.""" stream_kwargs = dict(kwargs) stream_kwargs["stream"] = True @@ -7134,7 +7134,7 @@ def _stream_request_plan(kwargs: Dict[str, Any]) -> "Tuple[Dict[str, Any], str, def _create_with_progress_once( - client: Any, kwargs: Dict[str, Any], task: Optional[str] = None, *, force_stream: bool = False + client: Any, kwargs: dict[str, Any], task: Optional[str] = None, *, force_stream: bool = False ) -> Any: """create() that streams (and re-aggregates, ticking the hook per substantive chunk) when a progress hook is active or the provider is stream-only; plain ``create(**kwargs)`` otherwise @@ -7191,7 +7191,7 @@ def _create_with_progress_once( def _aggregate_chat_stream( chunks: Any, *, model: str = "", total_ceiling: Optional[float] = None, - no_progress: "Optional[Tuple[float, Optional[float]]]" = None, + no_progress: "Optional[tuple[float, Optional[float]]]" = None, ) -> Any: """Consume a chunk stream into a complete response; TimeoutError (phrased "timed out" so ``_is_timeout_error`` matches) past *total_ceiling* or the *no_progress* windows (#100501).""" @@ -7222,10 +7222,10 @@ class _ChatStreamAccumulator: # host deadline, and the host deadline is absolute, so it is unaffected by however long dispatch and # TTFT took before this accumulator was constructed. See #99692. self._host_deadline = host_deadline - self.content_parts: List[str] = [] - self.reasoning_parts: List[str] = [] - self.reasoning_details: List[Any] = [] - self.tool_calls_acc: Dict[int, Dict[str, Any]] = {} + self.content_parts: list[str] = [] + self.reasoning_parts: list[str] = [] + self.reasoning_details: list[Any] = [] + self.tool_calls_acc: dict[int, dict[str, Any]] = {} self.finish_reason = self.usage = None self.resp_id = "" self.resp_model = model or "" @@ -7353,7 +7353,7 @@ async def _aggregate_chat_stream_async( return acc.finish() -async def _acreate_with_stream(client: Any, kwargs: Dict[str, Any], task: Optional[str] = None) -> Any: +async def _acreate_with_stream(client: Any, kwargs: dict[str, Any], task: Optional[str] = None) -> Any: """Async create() for stream-only providers: ``stream=True`` + aggregate the async chunks.""" stream_kwargs, model, total_ceiling = _stream_request_plan(kwargs) chunks = await client.chat.completions.create(**stream_kwargs) @@ -7368,7 +7368,7 @@ def _async_client_streams_internally(client: Any) -> bool: async def _acreate_with_progress( - client: Any, kwargs: Dict[str, Any], task: Optional[str] = None, *, force_stream: bool = False + client: Any, kwargs: dict[str, Any], task: Optional[str] = None, *, force_stream: bool = False ) -> Any: """Async :func:`_create_with_progress`: stream + re-aggregate (ticking the hook per substantive chunk) when a progress hook is active or the provider is stream-only; plain create otherwise.""" @@ -7406,16 +7406,18 @@ async def _acreate_with_progress( # only in how a request is awaited, so route resolution and the ordered recovery ladder are # written once. The ladder is a generator yielding ``_LadderStep`` requests and receiving the # response (or thrown exception), so rung ORDER and accept/re-raise contracts match on both wires. -_ResolvedAuxRoute = NamedTuple("_ResolvedAuxRoute", [ - ("client", Any), ("final_model", Optional[str]), ("resolved_provider", str), - ("effective_provider", str)]) +class _ResolvedAuxRoute(NamedTuple): + client: Any + final_model: Optional[str] + resolved_provider: str + effective_provider: str def _resolve_call_client( task: Optional[str], *, provider: Optional[str], model: Optional[str], base_url: Optional[str], api_key: Optional[str], resolved_provider: str, resolved_model: Optional[str], resolved_base_url: Optional[str], resolved_api_key: Optional[str], - resolved_api_mode: Optional[str], main_runtime: Optional[Dict[str, Any]], async_mode: bool, + resolved_api_mode: Optional[str], main_runtime: Optional[dict[str, Any]], async_mode: bool, ) -> _ResolvedAuxRoute: """Resolve the client for one aux call: vision chain, or cached text client with the explicit-provider fallback_chain / auto-chain rescue; RuntimeError when nothing is configured.""" @@ -7471,21 +7473,28 @@ def _resolve_call_client( return _ResolvedAuxRoute(client, final_model, resolved_provider, effective_provider) -_PreparedAuxRequest = NamedTuple("_PreparedAuxRequest", [ - ("client", Any), ("final_model", Optional[str]), ("kwargs", Dict[str, Any]), - ("resolved_provider", str), ("request_provider", str), ("resolved_model", Optional[str]), - ("resolved_base_url", Optional[str]), ("resolved_api_key", Optional[str]), - ("resolved_api_mode", Optional[str]), ("effective_timeout", float), - ("effective_extra_body", Dict[str, Any]), ("base_info", str)]) +class _PreparedAuxRequest(NamedTuple): + client: Any + final_model: Optional[str] + kwargs: dict[str, Any] + resolved_provider: str + request_provider: str + resolved_model: Optional[str] + resolved_base_url: Optional[str] + resolved_api_key: Optional[str] + resolved_api_mode: Optional[str] + effective_timeout: float + effective_extra_body: dict[str, Any] + base_info: str def _prepare_aux_request( task: Optional[str], *, provider: Optional[str], model: Optional[str], base_url: Optional[str], - api_key: Optional[str], main_runtime: Dict[str, Any], messages: list, + api_key: Optional[str], main_runtime: dict[str, Any], messages: list, temperature: Optional[float], max_tokens: Optional[int], tools: Optional[list], timeout: Optional[float], extra_body: Optional[dict], reasoning_config: Optional[dict], - extra_headers: Optional[Dict[str, str]], api_mode: Optional[str], - route_info: Optional[Dict[str, str]], async_mode: bool, + extra_headers: Optional[dict[str, str]], api_mode: Optional[str], + route_info: Optional[dict[str, str]], async_mode: bool, ) -> _PreparedAuxRequest: """Shared head of call_llm/async_call_llm: resolve route + client, publish it, build request kwargs. Sync-only: compression fast lane, per-request ``extra_headers``, and ``base_info`` falling @@ -7556,7 +7565,7 @@ class _LadderStep(NamedTuple): # Ordered (predicate, reason) pairs for the provider-fallback rung: first match # wins, so a payment-flavoured 429 reads as "payment error", not "rate limit". -_FALLBACK_REASONS: Tuple[Tuple[Callable[[Exception], bool], str], ...] = ( +_FALLBACK_REASONS: tuple[tuple[Callable[[Exception], bool], str], ...] = ( (_is_auth_error, "auth error"), (_is_payment_error, "payment error"), (_is_rate_limit_error, "rate limit"), (_is_model_incompatible_error, "model incompatible with route"), (_is_invalid_aux_response_error, "invalid provider response"), @@ -7602,13 +7611,21 @@ def _credential_rung_accepts(exc: Exception) -> bool: # Immutable route context shared by the recovery rungs. -_LadderRoute = NamedTuple("_LadderRoute", [ - ("client", Any), ("task", Optional[str]), ("tag", str), ("async_mode", bool), ("base_info", str), - ("resolved_provider", str), ("resolved_model", Optional[str]), ("resolved_base_url", Optional[str]), - ("resolved_api_key", Optional[str]), ("resolved_api_mode", Optional[str]), - ("final_model", Optional[str]), ("main_runtime", Optional[Dict[str, Any]]), - ("route_info", Optional[Dict[str, str]]), ("timeout", Optional[float]), -]) +class _LadderRoute(NamedTuple): + client: Any + task: Optional[str] + tag: str + async_mode: bool + base_info: str + resolved_provider: str + resolved_model: Optional[str] + resolved_base_url: Optional[str] + resolved_api_key: Optional[str] + resolved_api_mode: Optional[str] + final_model: Optional[str] + main_runtime: Optional[dict[str, Any]] + route_info: Optional[dict[str, str]] + timeout: Optional[float] def _without_temperature(kwargs: dict) -> Optional[dict]: @@ -7661,7 +7678,7 @@ def _parameter_rungs(client: Any, max_tokens: Optional[int]) -> tuple: def _ladder_parameter_rungs( - first_err: Exception, route: _LadderRoute, kwargs: Dict[str, Any], max_tokens: Optional[int], + first_err: Exception, route: _LadderRoute, kwargs: dict[str, Any], max_tokens: Optional[int], ): """Parameter rungs: retry without temperature / structured-output format / reasoning field / max_tokens. Rungs chain in whichever order the provider raises them (reasoning models reject @@ -7693,7 +7710,7 @@ def _ladder_parameter_rungs( return None, first_err, kwargs -def _refreshed_nous_step(route: _LadderRoute, kwargs: Dict[str, Any], message: str) -> Optional[_LadderStep]: +def _refreshed_nous_step(route: _LadderRoute, kwargs: dict[str, Any], message: str) -> Optional[_LadderStep]: """Rebuild the Nous client after a credential event; None when nothing refreshed.""" refreshed_client, refreshed_model = _refresh_nous_auxiliary_client( cache_provider=route.resolved_provider or "nous", model=route.final_model, @@ -7711,7 +7728,7 @@ def _refreshed_nous_step(route: _LadderRoute, kwargs: Dict[str, Any], message: s def _ladder_nous_rungs( - first_err: Exception, route: _LadderRoute, kwargs: Dict[str, Any], client_is_nous: bool, + first_err: Exception, route: _LadderRoute, kwargs: dict[str, Any], client_is_nous: bool, ): """Nous-only rungs: stale-model self-heal, paid-account refresh, 401 refresh. Returns ``(response, None)`` or ``(None, first_err)`` to fall through.""" @@ -7751,7 +7768,7 @@ def _ladder_nous_rungs( def _ladder_credential_rungs( - first_err: Exception, route: _LadderRoute, kwargs: Dict[str, Any], client_is_nous: bool, + first_err: Exception, route: _LadderRoute, kwargs: dict[str, Any], client_is_nous: bool, ): """OAuth credential refresh + same-provider retry, then credential-pool rotation. Returns ``(response, None)`` or ``(None, first_err)`` to fall through.""" @@ -7817,7 +7834,7 @@ def _ladder_credential_rungs( def _next_fallback_after_quarantine( task: Optional[str], resolved_provider: str, is_auto: bool, route: _LadderRoute, failed_model: Optional[str], failure_scope: Any, *, task_chain_only: bool = False, -) -> Tuple[Optional[Any], Optional[str], str]: +) -> tuple[Optional[Any], Optional[str], str]: """Next candidate after a fallback entry was quarantined mid-request (dead credential or a capacity error): remaining configured entries (task chain, then main chain on auto) before the discovery chain. ``task_chain_only`` (explicit-provider auth error) stops at the task chain — @@ -7935,11 +7952,11 @@ def _ladder_provider_fallback(first_err: Exception, route: _LadderRoute): def _aux_recovery_ladder( - first_err: Exception, *, client: Any, kwargs: Dict[str, Any], task: Optional[str], + first_err: Exception, *, client: Any, kwargs: dict[str, Any], task: Optional[str], async_mode: bool, base_info: str, resolved_provider: str, resolved_model: Optional[str], resolved_base_url: Optional[str], resolved_api_key: Optional[str], resolved_api_mode: Optional[str], final_model: Optional[str], max_tokens: Optional[int], - main_runtime: Optional[Dict[str, Any]], route_info: Optional[Dict[str, str]], + main_runtime: Optional[dict[str, Any]], route_info: Optional[dict[str, str]], ): """Ordered recovery rungs after the primary request failed (generator): parameter strips → Nous heal/refresh → credential refresh/pool rotation → provider fallback. @@ -8017,7 +8034,7 @@ def _elapsed_ms(started_at: float, now: Optional[float] = None) -> int: return max(0, int(((time.monotonic() if now is None else now) - started_at) * 1000)) -def _stamp_latency_once(latency_info: Optional[Dict[str, int]], key: str, started_at: float) -> None: +def _stamp_latency_once(latency_info: Optional[dict[str, int]], key: str, started_at: float) -> None: """Record ``key`` in ``latency_info`` the first time it fires.""" if latency_info is not None and key not in latency_info: latency_info[key] = _elapsed_ms(started_at) @@ -8026,12 +8043,12 @@ def _stamp_latency_once(latency_info: Optional[Dict[str, int]], key: str, starte @_relay_auxiliary_call def call_llm( task: str = None, *, provider: str = None, model: str = None, base_url: str = None, - api_key: str = None, main_runtime: Optional[Dict[str, Any]] = None, messages: list, + api_key: str = None, main_runtime: Optional[dict[str, Any]] = None, messages: list, temperature: Optional[float] = None, max_tokens: int = None, tools: list = None, timeout: float = None, extra_body: dict = None, reasoning_config: Optional[dict] = None, - extra_headers: Optional[Dict[str, str]] = None, api_mode: str = None, stream: bool = False, - stream_options: dict = None, route_info: Optional[Dict[str, str]] = None, - latency_info: Optional[Dict[str, int]] = None, + extra_headers: Optional[dict[str, str]] = None, api_mode: str = None, stream: bool = False, + stream_options: dict = None, route_info: Optional[dict[str, str]] = None, + latency_info: Optional[dict[str, int]] = None, ) -> Any: """Run an auxiliary LLM request, applying the configured task limit.""" queue_started_at = time.monotonic() @@ -8089,12 +8106,12 @@ def _release_sync_semaphore_after_stream(stream: Any, semaphore: threading.Bound def _plan_aux_call( task: Optional[str], *, async_mode: bool, provider: Optional[str], model: Optional[str], - base_url: Optional[str], api_key: Optional[str], main_runtime: Optional[Dict[str, Any]], + base_url: Optional[str], api_key: Optional[str], main_runtime: Optional[dict[str, Any]], messages: list, temperature: Optional[float], max_tokens: Optional[int], tools: Optional[list], timeout: Optional[float], extra_body: Optional[dict], reasoning_config: Optional[dict], - extra_headers: Optional[Dict[str, str]], api_mode: Optional[str], - route_info: Optional[Dict[str, str]], -) -> Tuple[_PreparedAuxRequest, Dict[str, Any], Dict[str, Any]]: + extra_headers: Optional[dict[str, str]], api_mode: Optional[str], + route_info: Optional[dict[str, str]], +) -> tuple[_PreparedAuxRequest, dict[str, Any], dict[str, Any]]: """Shared head of both call impls: prepare the request and bundle the kwargs the recovery drivers pass to ``_retry_same_provider_*`` / ``_call_fallback_candidate_*``. One immutable runtime snapshot for keying/resolution/retries/fallbacks, so a concurrent /model switch @@ -8134,8 +8151,8 @@ def _should_retry_same_provider(task: Optional[str], exc: Exception, tag: str) - def _ladder_step_call( - step: _LadderStep, req: _PreparedAuxRequest, retry_kwargs: Dict[str, Any], candidate_kwargs: Dict[str, Any], -) -> Tuple[str, tuple, Dict[str, Any]]: + step: _LadderStep, req: _PreparedAuxRequest, retry_kwargs: dict[str, Any], candidate_kwargs: dict[str, Any], +) -> tuple[str, tuple, dict[str, Any]]: """Resolve a ladder step into ``(kind, args, kwargs)`` for the sync/async performer.""" if step.kind == "call": return "call", step.args, dict(provider=req.resolved_provider, api_mode=req.resolved_api_mode) @@ -8146,8 +8163,8 @@ def _ladder_step_call( def _start_recovery_ladder( - first_err: Exception, req: _PreparedAuxRequest, retry_kwargs: Dict[str, Any], *, - task: Optional[str], async_mode: bool, route_info: Optional[Dict[str, str]], + first_err: Exception, req: _PreparedAuxRequest, retry_kwargs: dict[str, Any], *, + task: Optional[str], async_mode: bool, route_info: Optional[dict[str, str]], ): """Build the recovery-ladder generator for a failed primary request.""" return _aux_recovery_ladder( @@ -8161,11 +8178,11 @@ def _start_recovery_ladder( def _call_llm_impl( task: str = None, *, provider: str = None, model: str = None, base_url: str = None, - api_key: str = None, main_runtime: Optional[Dict[str, Any]] = None, messages: list, + api_key: str = None, main_runtime: Optional[dict[str, Any]] = None, messages: list, temperature: Optional[float] = None, max_tokens: int = None, tools: list = None, timeout: float = None, extra_body: dict = None, reasoning_config: Optional[dict] = None, - extra_headers: Optional[Dict[str, str]] = None, api_mode: str = None, stream: bool = False, - stream_options: dict = None, route_info: Optional[Dict[str, str]] = None, + extra_headers: Optional[dict[str, str]] = None, api_mode: str = None, stream: bool = False, + stream_options: dict = None, route_info: Optional[dict[str, str]] = None, ) -> Any: """Centralized synchronous LLM call: resolve provider/model, auth, kwargs, fallbacks. task: aux task whose provider:model comes from config (ignored if provider set); api_mode @@ -8323,10 +8340,10 @@ def extract_content_or_reasoning(response, *, max_reasoning_chars: int | None = @_relay_auxiliary_call_async async def async_call_llm( task: str = None, *, provider: str = None, model: str = None, base_url: str = None, - api_key: str = None, main_runtime: Optional[Dict[str, Any]] = None, messages: list, + api_key: str = None, main_runtime: Optional[dict[str, Any]] = None, messages: list, temperature: Optional[float] = None, max_tokens: int = None, tools: list = None, timeout: float = None, extra_body: dict = None, reasoning_config: Optional[dict] = None, - route_info: Optional[Dict[str, str]] = None, + route_info: Optional[dict[str, str]] = None, ) -> Any: """Run an asynchronous auxiliary LLM request under the configured limit.""" semaphore = _acquire_async_aux_semaphore(task) @@ -8347,10 +8364,10 @@ async def async_call_llm( async def _async_call_llm_impl( task: str = None, *, provider: str = None, model: str = None, base_url: str = None, - api_key: str = None, main_runtime: Optional[Dict[str, Any]] = None, messages: list, + api_key: str = None, main_runtime: Optional[dict[str, Any]] = None, messages: list, temperature: Optional[float] = None, max_tokens: int = None, tools: list = None, timeout: float = None, extra_body: dict = None, reasoning_config: Optional[dict] = None, - route_info: Optional[Dict[str, str]] = None, + route_info: Optional[dict[str, str]] = None, ) -> Any: """Centralized asynchronous LLM call; see call_llm() for full documentation. No per-request header / api_mode override on the async entry point.""" @@ -8367,7 +8384,7 @@ async def _async_call_llm_impl( # (PR #16587) _force_stream_async = _provider_requires_stream(request_provider, req.base_info or req.resolved_base_url) - async def _acreate(_kwargs: Dict[str, Any]) -> Any: + async def _acreate(_kwargs: dict[str, Any]) -> Any: return await _acreate_with_progress(client, _kwargs, task, force_stream=_force_stream_async) async def _primary(**validate_kw: Any) -> Any: diff --git a/agent/auxiliary_client_registry.py b/agent/auxiliary_client_registry.py index e1bd44469f..9bd43ceaf5 100644 --- a/agent/auxiliary_client_registry.py +++ b/agent/auxiliary_client_registry.py @@ -124,7 +124,7 @@ def _resolve_plugin_oauth_arm(req: _ResolveRequest) -> _ResolveResult: # ``api_key`` / ``external_process`` arms stay on the facade (they share its credential # resolvers); the table maps the auth types whose arms are build-and-route one-liners # or live here as topical arms. -REGISTRY_AUTHTYPE_ARMS: Dict[str, Callable[[_ResolveRequest], _ResolveResult]] = { +REGISTRY_AUTHTYPE_ARMS: dict[str, Callable[[_ResolveRequest], _ResolveResult]] = { "vertex": _resolve_vertex_arm, "aws_sdk": _resolve_bedrock_arm, "oauth_minimax": _resolve_minimax_oauth_arm, diff --git a/agent/auxiliary_codex_response.py b/agent/auxiliary_codex_response.py index 12adf529cf..dc77ad4870 100644 --- a/agent/auxiliary_codex_response.py +++ b/agent/auxiliary_codex_response.py @@ -11,7 +11,7 @@ from typing import Any, List, Optional, Tuple def _parse_codex_final_response( final: Any, *, issuer_kind: Optional[str] = None, issuer_model: Optional[str] = None, -) -> Tuple[List[str], List[Any], Any, str]: +) -> tuple[list[str], list[Any], Any, str]: """Normalize Responses output without losing phase or completion state for aux callers.""" from agent.codex_responses_adapter import _lower_or_none, _normalize_codex_response diff --git a/agent/auxiliary_fallback_recovery.py b/agent/auxiliary_fallback_recovery.py index 5875348a7c..d479845e53 100644 --- a/agent/auxiliary_fallback_recovery.py +++ b/agent/auxiliary_fallback_recovery.py @@ -9,7 +9,7 @@ This module runs those rungs — and only those — around a candidate's request from typing import Any, Awaitable, Callable, Dict, Optional -def _parameter_ladder(first_err: Exception, client: Any, kwargs: Dict[str, Any], *, +def _parameter_ladder(first_err: Exception, client: Any, kwargs: dict[str, Any], *, task: Optional[str], tag: str): from agent.auxiliary_client import _LadderRoute, _ladder_parameter_rungs # Keyword construction: the route tuple grows with every new ladder rung (a positional 13-tuple @@ -25,7 +25,7 @@ def _parameter_ladder(first_err: Exception, client: Any, kwargs: Dict[str, Any], def send_with_parameter_rungs( - send: Callable[[Any, Dict[str, Any]], Any], client: Any, kwargs: Dict[str, Any], *, task: Optional[str], + send: Callable[[Any, dict[str, Any]], Any], client: Any, kwargs: dict[str, Any], *, task: Optional[str], ) -> Any: """``send(client, kwargs)``; on a parameter 400, retry through the parameter rungs. Any other error (auth, payment, connection) propagates unchanged for the caller's own handling.""" @@ -38,7 +38,7 @@ def send_with_parameter_rungs( async def send_with_parameter_rungs_async( - send: Callable[[Any, Dict[str, Any]], Awaitable[Any]], client: Any, kwargs: Dict[str, Any], *, + send: Callable[[Any, dict[str, Any]], Awaitable[Any]], client: Any, kwargs: dict[str, Any], *, task: Optional[str], ) -> Any: """Async twin of :func:`send_with_parameter_rungs`.""" diff --git a/agent/auxiliary_hooks.py b/agent/auxiliary_hooks.py index 613d794ca2..30008bf13d 100644 --- a/agent/auxiliary_hooks.py +++ b/agent/auxiliary_hooks.py @@ -22,7 +22,7 @@ PRE_AUXILIARY_CALL = "pre_auxiliary_call" POST_AUXILIARY_CALL = "post_auxiliary_call" -def _parent_turn_identity() -> Dict[str, str]: +def _parent_turn_identity() -> dict[str, str]: """``session_id`` / ``task_id`` / ``turn_id`` / ``platform`` of the main turn this auxiliary call runs under, or empty strings for turn-less callers (cron, gateway idle work).""" ident = {"session_id": "", "task_id": "", "turn_id": "", "platform": ""} @@ -42,7 +42,7 @@ def _parent_turn_identity() -> Dict[str, str]: return ident -def _system_prompt(messages: Any, kwargs: Dict[str, Any]) -> str: +def _system_prompt(messages: Any, kwargs: dict[str, Any]) -> str: if isinstance(kwargs.get("system"), str): # Anthropic return kwargs["system"] if isinstance(kwargs.get("instructions"), str): # Responses @@ -54,7 +54,7 @@ def _system_prompt(messages: Any, kwargs: Dict[str, Any]) -> str: return "" -def _usage_summary(response: Any, *, provider: str, api_mode: str) -> Optional[Dict[str, Any]]: +def _usage_summary(response: Any, *, provider: str, api_mode: str) -> Optional[dict[str, Any]]: raw_usage = getattr(response, "usage", None) if response is None or not raw_usage: return None @@ -90,7 +90,7 @@ class _AuxCallHooks: """Fires the pre/post pair for one provider attempt; the base payload is built once.""" def __init__( - self, *, aux_task: str, metadata: Dict[str, Any], client: Any, kwargs: Dict[str, Any], + self, *, aux_task: str, metadata: dict[str, Any], client: Any, kwargs: dict[str, Any], provider: str, model: str, api_mode: str, streaming: bool, ) -> None: self.provider = provider @@ -98,7 +98,7 @@ class _AuxCallHooks: self.streaming = streaming self.kwargs = kwargs self.started_at = time.time() - self.base: Dict[str, Any] = dict(_parent_turn_identity()) + self.base: dict[str, Any] = dict(_parent_turn_identity()) self.base.update( aux_task=aux_task, api_request_id=str(metadata.get("api_request_id") or ""), @@ -143,7 +143,7 @@ class _AuxCallHooks: from agent.api_request_hooks import ApiRequestHooksMixin as _Sanitize ended_at = time.time() - payload: Dict[str, Any] = dict( + payload: dict[str, Any] = dict( self.base, ended_at=ended_at, api_duration=max(0.0, ended_at - self.started_at), error=None if error is None else f"{type(error).__name__}: {error}"[:2000], error_type=None if error is None else type(error).__name__, @@ -220,8 +220,8 @@ def _post_safely(hooks: Optional[_AuxCallHooks], response: Any = None, error: An def run_with_aux_hooks( - call: Callable[[], Any], *, aux_task: str, metadata: Dict[str, Any], client: Any, - kwargs: Dict[str, Any], provider: str, model: str, api_mode: str, streaming: bool = False, + call: Callable[[], Any], *, aux_task: str, metadata: dict[str, Any], client: Any, + kwargs: dict[str, Any], provider: str, model: str, api_mode: str, streaming: bool = False, ) -> Any: """Run one synchronous provider attempt between ``pre_auxiliary_call`` and ``post_auxiliary_call``; the exception (if any) is reported in ``post`` and re-raised.""" @@ -237,8 +237,8 @@ def run_with_aux_hooks( async def arun_with_aux_hooks( - call: Callable[[], Awaitable[Any]], *, aux_task: str, metadata: Dict[str, Any], client: Any, - kwargs: Dict[str, Any], provider: str, model: str, api_mode: str, + call: Callable[[], Awaitable[Any]], *, aux_task: str, metadata: dict[str, Any], client: Any, + kwargs: dict[str, Any], provider: str, model: str, api_mode: str, ) -> Any: """Async twin of :func:`run_with_aux_hooks`.""" hooks = _hooks_or_none(aux_task=aux_task, metadata=metadata, client=client, kwargs=kwargs, diff --git a/agent/auxiliary_local_runtime.py b/agent/auxiliary_local_runtime.py index 6c5bf24208..8e879e914f 100644 --- a/agent/auxiliary_local_runtime.py +++ b/agent/auxiliary_local_runtime.py @@ -15,7 +15,7 @@ from hermes_cli.local_runtime.endpoint import LLAMACPP_ALIASES def bare_llamacpp_endpoint(provider: Optional[str], base_url: Optional[str], - api_key: Any) -> Optional[Tuple[str, Any]]: + api_key: Any) -> Optional[tuple[str, Any]]: """``(base_url, api_key)`` of the profile's local llama.cpp server for a bare alias. ``base_url`` is ``""`` when nothing is serving. ``None`` when the request is not a bare alias: diff --git a/agent/auxiliary_reasoning_floor.py b/agent/auxiliary_reasoning_floor.py index 62094a1527..748003e179 100644 --- a/agent/auxiliary_reasoning_floor.py +++ b/agent/auxiliary_reasoning_floor.py @@ -40,14 +40,14 @@ def _is_disabled(reasoning_config: Any) -> bool: return isinstance(reasoning_config, dict) and reasoning_config.get("enabled") is False -def floor_reasoning_config(reasoning_config: Any) -> Dict[str, Any]: +def floor_reasoning_config(reasoning_config: Any) -> dict[str, Any]: """The caller's disabled ``reasoning_config`` lifted to the floor; anything else returned as-is.""" if _is_disabled(reasoning_config): return {"enabled": True, "effort": REASONING_FLOOR_EFFORT} return reasoning_config -def with_reasoning_floor(kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def with_reasoning_floor(kwargs: dict[str, Any]) -> Optional[dict[str, Any]]: """Copy of *kwargs* with every thinking-OFF encoding lifted to ``REASONING_FLOOR_EFFORT``: top-level ``reasoning_effort``, ``extra_body.reasoning`` (OpenRouter shape) and the adapter's private ``_reasoning_config``. ``None`` when nothing was disabled, so the ladder never re-sends an unchanged @@ -72,7 +72,7 @@ def with_reasoning_floor(kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]: def remember_reasoning_floor( - provider: Optional[str], base_url: Optional[str], rejected_kwargs: Dict[str, Any], error: BaseException, + provider: Optional[str], base_url: Optional[str], rejected_kwargs: dict[str, Any], error: BaseException, ) -> None: """Record that this route's ``rejected_kwargs["model"]`` refuses to disable reasoning (the ladder calls this after the stepped-up retry succeeded).""" diff --git a/agent/auxiliary_structured_output.py b/agent/auxiliary_structured_output.py index d67057efb4..4ca3001bfc 100644 --- a/agent/auxiliary_structured_output.py +++ b/agent/auxiliary_structured_output.py @@ -45,7 +45,7 @@ def _route_key(provider: Optional[str], base_url: Optional[str]) -> str: return (urlparse(base_url or "").netloc or "").lower() or str(provider or "").strip().lower() -def _response_format_type(request_kwargs: Dict[str, Any]) -> Optional[str]: +def _response_format_type(request_kwargs: dict[str, Any]) -> Optional[str]: extra_body = request_kwargs.get("extra_body") response_format = (extra_body or {}).get("response_format") if isinstance(extra_body, dict) else None if response_format is None: @@ -78,7 +78,7 @@ def is_capability_rejection(error: Optional[BaseException]) -> bool: def remember_structured_output_rejection( - provider: Optional[str], base_url: Optional[str], rejected_kwargs: Dict[str, Any], error: BaseException, + provider: Optional[str], base_url: Optional[str], rejected_kwargs: dict[str, Any], error: BaseException, ) -> None: """Record that this route's ``rejected_kwargs["model"]`` rejected the ``response_format`` type carried by *rejected_kwargs* — only when *error* names the capability, never for a schema-validation 400.""" @@ -88,9 +88,9 @@ def remember_structured_output_rejection( def without_unsupported_response_format( - extra_body: Dict[str, Any], provider: Optional[str], base_url: Optional[str], model: Optional[str], + extra_body: dict[str, Any], provider: Optional[str], base_url: Optional[str], model: Optional[str], task: Optional[str] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """*extra_body* minus a ``response_format`` whose type this route+model is known to reject; unchanged otherwise.""" response_format = extra_body.get("response_format") diff --git a/agent/auxiliary_task_config.py b/agent/auxiliary_task_config.py index e822a1c8f5..d673053352 100644 --- a/agent/auxiliary_task_config.py +++ b/agent/auxiliary_task_config.py @@ -13,7 +13,7 @@ from typing import Any, Dict logger = logging.getLogger(__name__) -def _get_auxiliary_task_config(task: str, _seen: frozenset = frozenset()) -> Dict[str, Any]: +def _get_auxiliary_task_config(task: str, _seen: frozenset = frozenset()) -> dict[str, Any]: """Config dict for auxiliary., or {} when unavailable. Plugin-registered tasks get their declared defaults layered under user config (user wins); built-in defaults live in DEFAULT_CONFIG. A task registered with ``inherit_from`` is resolved here, at read time, over the base task's @@ -57,7 +57,7 @@ _AUX_ROUTE_KEYS = frozenset({"provider", "model", "base_url", "api_key", "api_mo "api_key_env", "reasoning_effort"}) -def _layer_over_inherited(inherited: Dict[str, Any], user: Dict[str, Any]) -> Dict[str, Any]: +def _layer_over_inherited(inherited: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]: """Merge a task's own ``auxiliary.`` block over its inherited base. The picker, "reset to auto" and the dashboard persist ``provider: auto`` plus ``""`` for diff --git a/agent/azure_identity_adapter.py b/agent/azure_identity_adapter.py index dd89c60cea..eb09e7716d 100644 --- a/agent/azure_identity_adapter.py +++ b/agent/azure_identity_adapter.py @@ -41,7 +41,7 @@ _AUTH_HEADERS = ("Authorization", "authorization", "Api-Key", "api-key", "X-Api- def has_azure_identity_installed() -> bool: """Cheap importability check — does not walk the credential chain.""" try: - import azure.identity # noqa: F401 + import azure.identity return True except Exception: return False @@ -85,11 +85,11 @@ class EntraIdentityConfig: def __post_init__(self) -> None: object.__setattr__(self, "scope", str(self.scope or "").strip() or SCOPE_AI_AZURE_DEFAULT) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: return {"scope": self.scope, "exclude_interactive_browser": self.exclude_interactive_browser} @classmethod - def from_dict(cls, data: Optional[Dict[str, Any]], *, default_scope: Optional[str] = None) -> "EntraIdentityConfig": + def from_dict(cls, data: Optional[dict[str, Any]], *, default_scope: Optional[str] = None) -> "EntraIdentityConfig": data = data or {} return cls( scope=str(data.get("scope") or "").strip() or default_scope or SCOPE_AI_AZURE_DEFAULT, @@ -114,7 +114,7 @@ def _default_chain_credential(config: EntraIdentityConfig) -> Any: # the ambient default chain is refused outright when the profile sets no credential of its own: every source it # could mint from (env SP, CLI/azd/PowerShell caches, host managed identity) is the launch context's identity, # and _inject_bearer would ship it to whatever base_url the served profile configured. -_credentials_by_home: Dict[tuple, Any] = {} +_credentials_by_home: dict[tuple, Any] = {} _AMBIENT_CHAIN_REFUSAL = ( "Entra ID auth is refused for this profile: it sets no AZURE_* credential of its own, and under " @@ -129,7 +129,7 @@ _AMBIENT_CHAIN_REFUSAL = ( def _scoped_credential(ai: Any, config: EntraIdentityConfig) -> Any: from agent.secret_scope import current_secret_scope, is_multiplex_active scope = current_secret_scope() or {} - read = lambda name: (scope.get(name) or "").strip() # noqa: E731 + read = lambda name: (scope.get(name) or "").strip() tenant, client = read("AZURE_TENANT_ID"), read("AZURE_CLIENT_ID") if tenant and client and read("AZURE_CLIENT_SECRET"): return ai.ClientSecretCredential(tenant, client, read("AZURE_CLIENT_SECRET")) @@ -164,7 +164,7 @@ def _resolve_config(config: Optional[EntraIdentityConfig], scope: Optional[str], return EntraIdentityConfig(scope=(scope or "").strip() or SCOPE_AI_AZURE_DEFAULT, **overrides) -def _install_failure(allow_install: bool) -> Optional[Dict[str, Any]]: +def _install_failure(allow_install: bool) -> Optional[dict[str, Any]]: """None when ``azure.identity`` is importable (lazy-installing if allowed), else ``{"error", "hint"}``.""" if has_azure_identity_installed(): return None @@ -187,9 +187,9 @@ def build_token_provider(scope: Optional[str] = None, *, config: Optional[EntraI return ai.get_bearer_token_provider(build_credential(config), config.scope) -def _probe_token(config: EntraIdentityConfig, timeout_seconds: float) -> Optional[Dict[str, Any]]: +def _probe_token(config: EntraIdentityConfig, timeout_seconds: float) -> Optional[dict[str, Any]]: """``get_token`` on a daemon thread under a hard deadline → ``{"token"}`` / ``{"error"}`` / None on timeout.""" - result: Dict[str, Any] = {} + result: dict[str, Any] = {} ctx = contextvars.copy_context() def _probe() -> None: @@ -251,11 +251,11 @@ _ENV_SOURCE_CHECKS = ( def describe_active_credential(config: Optional[EntraIdentityConfig] = None, *, scope: Optional[str] = None, timeout_seconds: float = 10.0, allow_install: bool = True, - **overrides: Any) -> Dict[str, Any]: + **overrides: Any) -> dict[str, Any]: """Doctor / preflight diagnostics. Never raises; ``{"ok": False, "error": ...}`` on failure. azure-identity hides the winning inner credential, so this reports a coarse picture (env sources, token expiry) rather than a class name; ``AZURE_LOG_LEVEL=DEBUG`` shows the chain.""" - info: Dict[str, Any] = {"ok": False} + info: dict[str, Any] = {"ok": False} failure = _install_failure(allow_install) if failure is not None: info["error"], info["hint"] = failure["error"], failure["hint"] diff --git a/agent/background_review.py b/agent/background_review.py index 99fa233583..3ccc84a953 100644 --- a/agent/background_review.py +++ b/agent/background_review.py @@ -182,14 +182,14 @@ _REVIEW_INPUT_CONTEXT_FRACTION = 0.75 _REVIEW_MAX_INPUT_TOKENS_FALLBACK = 120_000 -def _task_block(cfg: Any) -> Dict[str, Any]: +def _task_block(cfg: Any) -> dict[str, Any]: """``cfg["auxiliary"]["background_review"]`` as a dict (``{}`` on any shape mismatch).""" aux = cfg.get("auxiliary", {}) if isinstance(cfg.get("auxiliary"), dict) else {} task = aux.get("background_review", {}) return task if isinstance(task, dict) else {} -def _background_review_task_config(task_cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: +def _background_review_task_config(task_cfg: Optional[dict[str, Any]] = None) -> dict[str, Any]: """``auxiliary.background_review`` (or ``{}`` on any failure); pass a pre-loaded ``task_cfg`` so the spawn / resolve / prompt paths do not re-read config on every turn.""" if task_cfg is not None: @@ -213,7 +213,7 @@ def _context_derived_review_input_budget(review_agent: Any = None) -> int: def _review_input_token_budget( - task_cfg: Optional[Dict[str, Any]] = None, review_agent: Any = None, + task_cfg: Optional[dict[str, Any]] = None, review_agent: Any = None, ) -> Optional[int]: """Aggregate input-token budget for one review fork (None = unlimited; <= 0 disables). Unset or malformed ``max_input_tokens`` → derived from ``review_agent``'s context window.""" @@ -225,7 +225,7 @@ def _review_input_token_budget( return budget if budget > 0 else None -def load_background_review_settings() -> tuple[bool, Dict[str, Any]]: +def load_background_review_settings() -> tuple[bool, dict[str, Any]]: """Single config read -> ``(enabled, task_cfg)``. Fail-open (``enabled=True``) so a broken config never silently disables reviews — but WARN so the cost is visible.""" try: @@ -242,7 +242,7 @@ def load_background_review_settings() -> tuple[bool, Dict[str, Any]]: return True, {} -def _resolve_review_runtime(agent: Any, task_cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: +def _resolve_review_runtime(agent: Any, task_cfg: Optional[dict[str, Any]] = None) -> dict[str, Any]: """Resolve provider/model/credentials for the review fork. Default (auto / unset / same as parent): the parent's live runtime with ``routed=False`` (codex_app_server -> codex_responses downgrade applied). When ``auxiliary.background_review.{provider,model}`` names a different @@ -314,14 +314,14 @@ def _parent_can_emit_tool_calls(agent: Any) -> bool: return True -def _msg_text(m: Dict) -> str: +def _msg_text(m: dict) -> str: c = m.get("content") if isinstance(c, list): c = " ".join(b.get("text", "") for b in c if isinstance(b, dict)) return c.strip() if isinstance(c, str) else "" -def _digest_history(messages_snapshot: List[Dict], tail: int = 24) -> List[Dict]: +def _digest_history(messages_snapshot: list[dict], tail: int = 24) -> list[dict]: """Compact replay for the routed (different-model) path only: keep the recent ``tail`` messages verbatim (extended so the kept run never starts on a tool result) and collapse older turns into one synthetic user-role digest, preserving role alternation.""" @@ -333,7 +333,7 @@ def _digest_history(messages_snapshot: List[Dict], tail: int = 24) -> List[Dict] tail += 1 else: return msgs - lines: List[str] = [] + lines: list[str] = [] for m in msgs[:-len(keep)]: if not isinstance(m, dict): continue @@ -566,19 +566,19 @@ def _preview(text: str, limit: int) -> str: # Memory op -> (glyph, which field carries the preview, preview length). -_MEMORY_OP_FORMATS: Dict[str, Tuple[str, str, int]] = { +_MEMORY_OP_FORMATS: dict[str, tuple[str, str, int]] = { "add": ("➕", "content", 120), "replace": ("✏️", "content", 120), "remove": ("➖", "old_text", 60) } -def _memory_op_line(label: str, action: str, fields: Dict[str, str]) -> Optional[str]: +def _memory_op_line(label: str, action: str, fields: dict[str, str]) -> Optional[str]: """Verbose line for one memory add/replace/remove, or None when no preview text.""" glyph, field_name, limit = _MEMORY_OP_FORMATS.get(action) or (None, "", 0) text = fields.get(field_name) or "" if glyph else "" return t("display.review.memory_op_line", label=label, glyph=glyph, preview=_preview(text, limit)) if text else None -def _verbose_skill_line(data: Dict, detail: Dict, message: str) -> str: +def _verbose_skill_line(data: dict, detail: dict, message: str) -> str: action = detail.get("action", "") skill_name = detail.get("name", "") # ``_change`` is free-form (wrapper MCP backends return lists/scalars). @@ -596,7 +596,7 @@ def _verbose_skill_line(data: Dict, detail: Dict, message: str) -> str: return t("display.review.skill_message_verbose", message=message) if message else t("display.review.skill_action", action=action) -def _verbose_memory_lines(label: str, detail: Dict) -> List[str]: +def _verbose_memory_lines(label: str, detail: dict) -> list[str]: # ``operations`` may be any JSON value; only a list of dicts is usable. ops_raw = detail.get("operations") if isinstance(ops_raw, list) and ops_raw: @@ -612,7 +612,7 @@ _CALL_DETAIL_DEFAULTS = ( ) -def _collect_review_call_details(review_messages: List[Dict]) -> Tuple[set, dict]: +def _collect_review_call_details(review_messages: list[dict]) -> tuple[set, dict]: """Map review-agent tool_call ids -> parsed call arguments for notify tools. Result JSON only says "Entry added"; the call arguments carry action, target and content previews. Restricting to notify tools keeps helper tools from surfacing as memory work just because they succeeded.""" @@ -643,11 +643,11 @@ def _collect_review_call_details(review_messages: List[Dict]) -> Tuple[set, dict return all_tool_call_ids, call_details -def _tool_messages(messages: List[Dict]) -> Iterator[Dict]: +def _tool_messages(messages: list[dict]) -> Iterator[dict]: return (m for m in messages or [] if isinstance(m, dict) and m.get("role") == "tool") -def _prior_tool_keys(prior_snapshot: List[Dict]) -> Tuple[set, set]: +def _prior_tool_keys(prior_snapshot: list[dict]) -> tuple[set, set]: """``(tool_call_ids, contents)`` of tool messages already in the parent snapshot.""" priors = list(_tool_messages(prior_snapshot)) ids = {m["tool_call_id"] for m in priors if m.get("tool_call_id")} @@ -655,7 +655,7 @@ def _prior_tool_keys(prior_snapshot: List[Dict]) -> Tuple[set, set]: return ids, contents -def _action_lines(data: Dict, detail: Dict, verbose: bool) -> List[str]: +def _action_lines(data: dict, detail: dict, verbose: bool) -> list[str]: """Summary line(s) for one successful notify-tool result (``[]`` when nothing to report).""" if data.get("staged"): # The fork's own review summary is never published back, so an unattended-review @@ -697,8 +697,8 @@ def _action_lines(data: Dict, detail: Dict, verbose: bool) -> List[str]: def summarize_background_review_actions( - review_messages: List[Dict], prior_snapshot: List[Dict], notification_mode: str = "on" -) -> List[str]: + review_messages: list[dict], prior_snapshot: list[dict], notification_mode: str = "on" +) -> list[str]: """Human-facing action summary for a background review pass: successful memory / skill-management tool results from the review agent's messages, skipping tool messages already present in ``prior_snapshot`` so inherited results are not re-surfaced as fresh work. @@ -713,7 +713,7 @@ def summarize_background_review_actions( verbose = mode == "verbose" existing_tool_call_ids, existing_tool_contents = _prior_tool_keys(prior_snapshot) all_tool_call_ids, call_details = _collect_review_call_details(review_messages) - actions: List[str] = [] + actions: list[str] = [] for msg in _tool_messages(review_messages): tcid = msg.get("tool_call_id") if tcid: @@ -736,9 +736,9 @@ def summarize_background_review_actions( def build_memory_write_metadata( agent: Any, *, write_origin: Optional[str] = None, execution_context: Optional[str] = None, task_id: Optional[str] = None, tool_call_id: Optional[str] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Build provenance metadata for external memory-provider mirrors.""" - metadata: Dict[str, Any] = { + metadata: dict[str, Any] = { "write_origin": write_origin or getattr(agent, "_memory_write_origin", "assistant_tool"), "execution_context": execution_context or getattr(agent, "_memory_write_context", "foreground"), "session_id": agent.session_id or "", @@ -756,7 +756,7 @@ _USAGE_COUNTERS = ( ) -def _snapshot_review_usage(review_agent: Any) -> Dict[str, Any]: +def _snapshot_review_usage(review_agent: Any) -> dict[str, Any]: """Snapshot in-memory usage counters from a review fork (pre-close).""" return { **{key: getattr(review_agent, key, None) for key in ("model", "provider", "base_url")}, @@ -765,7 +765,7 @@ def _snapshot_review_usage(review_agent: Any) -> Dict[str, Any]: } -def _record_review_usage_to_parent(parent_agent: Any, usage: Dict[str, Any]) -> None: +def _record_review_usage_to_parent(parent_agent: Any, usage: dict[str, Any]) -> None: """Record a fork's usage against the parent session (best-effort, never raises). The fork has ``_session_db = None`` so conversation_loop's DB-gated accounting never sees its calls; route them through the aux-accounting chokepoint, which writes only ``session_model_usage`` — never @@ -786,7 +786,7 @@ def _record_review_usage_to_parent(parent_agent: Any, usage: Dict[str, Any]) -> logger.debug("Background review usage recording failed (non-fatal): %s", e) -def _classify_review_result(actions: List[str]) -> str: +def _classify_review_result(actions: list[str]) -> str: """Map a review action summary to ``none`` / ``skill`` / ``memory`` / ``skill+memory``. Prefix-based on the formats :func:`summarize_background_review_actions` emits (``Skill …``, ``📝 Skill …``, ``Memory …``, ``User profile …``), so a free-text line like ``Skipped: no @@ -801,7 +801,7 @@ def _classify_review_result(actions: List[str]) -> str: return "+".join(kind for kind, hit in (("skill", has_skill), ("memory", has_memory)) if hit) or "none" -def _log_review_completion(usage: Dict[str, Any], result: str) -> None: +def _log_review_completion(usage: dict[str, Any], result: str) -> None: """Emit a per-fork completion line so cost is visible where it is incurred.""" logger.info( "Background review complete: thread=bg-review calls=%d in=%d out=%d " @@ -820,12 +820,12 @@ _PROVIDER_PIN_ATTRS = ( ) -def _same_model_parity_kwargs(agent: Any) -> Dict[str, Any]: +def _same_model_parity_kwargs(agent: Any) -> dict[str, Any]: """AIAgent kwargs that keep a SAME-model fork's request bytes identical to the parent's. Only for the un-routed path: on a different model the cache is cold anyway, and the parent's reasoning-effort vocabulary may be invalid for the routed provider (OpenRouter forwards ``reasoning.effort`` unclamped; codex_responses passes ``max``/``ultra`` through unmapped).""" - kwargs: Dict[str, Any] = { + kwargs: dict[str, Any] = { # Anthropic's cache key is namespaced by ``thinking`` presence; the gateway session context # is appended to the cached system prompt at API-call time (without it the prompt diverges). "reasoning_config": getattr(agent, "reasoning_config", None), @@ -840,7 +840,7 @@ def _same_model_parity_kwargs(agent: Any) -> Dict[str, Any]: return kwargs -def _warn_ignored_reasoning_effort(agent: Any, task_cfg: Optional[Dict[str, Any]] = None) -> None: +def _warn_ignored_reasoning_effort(agent: Any, task_cfg: Optional[dict[str, Any]] = None) -> None: """One-shot user-visible notice: ``auxiliary.background_review.reasoning_effort`` is IGNORED on the same-model path (#104116). The fork inherits the parent's ``reasoning_config`` verbatim so its request bytes keep the parent's prompt-cache prefix (#30532: a diverged ``thinking`` field @@ -890,7 +890,7 @@ def _detach_fork_compression(review_agent: Any) -> None: review_agent._review_defer_compaction_before_first_response = True -def _routed_reasoning_config(task_cfg: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: +def _routed_reasoning_config(task_cfg: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]: """``reasoning_config`` for a ROUTED fork from ``auxiliary.background_review.reasoning_effort`` (#94825). The routed branch never inherits the parent's effort (its vocabulary may be invalid for the routed provider), but an explicit per-task pin is the user's choice for THAT model and must @@ -909,14 +909,14 @@ def _routed_reasoning_config(task_cfg: Optional[Dict[str, Any]]) -> Optional[Dic return parsed -def _fork_init_kwargs(agent: Any, rt: Dict[str, Any], routed: bool, max_iterations: int, - task_cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: +def _fork_init_kwargs(agent: Any, rt: dict[str, Any], routed: bool, max_iterations: int, + task_cfg: Optional[dict[str, Any]] = None) -> dict[str, Any]: """AIAgent constructor kwargs for the review fork. skip_memory=True: an external memory plugin scoped to the parent's session_id would leak the harness prompt into the user's real memory namespace; built-in MEMORY.md/USER.md state is re-bound by the caller. Toolsets match the parent so ``tools[]`` is byte-identical (Anthropic's cache key includes it); the runtime whitelist restricts dispatch.""" - kwargs: Dict[str, Any] = { + kwargs: dict[str, Any] = { "model": rt.get("model") or agent.model, "max_iterations": max_iterations, "quiet_mode": True, "platform": agent.platform, "provider": rt.get("provider") or agent.provider, "api_mode": rt.get("api_mode"), "base_url": rt.get("base_url") or None, @@ -954,9 +954,9 @@ def _inherit_parent_tool_surface(review_agent: Any, agent: Any) -> None: def build_cache_parity_fork( - agent: Any, task_cfg: Optional[Dict[str, Any]] = None, *, max_iterations: int, + agent: Any, task_cfg: Optional[dict[str, Any]] = None, *, max_iterations: int, write_origin: str = "background_review", -) -> Tuple[Any, Dict[str, Any], bool]: +) -> tuple[Any, dict[str, Any], bool]: """Construct a detached AIAgent fork with warm prompt-cache parity (shared with ``/btw``): same runtime/credentials as the parent, byte-identical system prompt / tools[] / reasoning config on the same-model path, shared session_id for prefix warmth, full persistence detachment (no @@ -1077,8 +1077,8 @@ def _track_review_fork(agent: Any, review_agent: Any, *, register: bool) -> None def _review_tool_whitelist( - review_agent: Any, task_cfg: Optional[Dict[str, Any]], review_memory: bool = False, -) -> Tuple[set, set]: + review_agent: Any, task_cfg: Optional[dict[str, Any]], review_memory: bool = False, +) -> tuple[set, set]: """``(whitelist, configured_extra_tools)`` for the review fork — DISPATCH-side only, so the advertised ``tools[]`` stays byte-identical to the parent's (prompt-cache parity).""" from model_tools import get_tool_definitions @@ -1124,8 +1124,8 @@ class _ReviewForkState: """Mutable hand-off between the fork phase and the outer worker's error/cleanup paths.""" review_agent: Any = None - review_messages: List[Dict] = field(default_factory=list) - review_usage: Dict[str, Any] = field(default_factory=dict) + review_messages: list[dict] = field(default_factory=list) + review_usage: dict[str, Any] = field(default_factory=dict) def _release_fork_clients(review_agent: Any) -> None: @@ -1136,7 +1136,7 @@ def _release_fork_clients(review_agent: Any) -> None: def _run_review_fork( - agent: Any, messages_snapshot: List[Dict], prompt: str, task_cfg: Optional[Dict[str, Any]], + agent: Any, messages_snapshot: list[dict], prompt: str, task_cfg: Optional[dict[str, Any]], review_run: Optional[_BackgroundReviewRun], st: _ReviewForkState, review_memory: bool = False, explicit: bool = False, ) -> None: @@ -1199,7 +1199,7 @@ def _run_review_fork( st.review_agent = None -def _publish_review_summary(agent: Any, actions: List[str]) -> None: +def _publish_review_summary(agent: Any, actions: list[str]) -> None: summary = " · ".join(dict.fromkeys(actions)) agent._safe_print(t("display.review.summary_cli", summary=summary)) if agent.background_review_callback: @@ -1208,8 +1208,8 @@ def _publish_review_summary(agent: Any, actions: List[str]) -> None: def _run_review_in_thread( - agent: Any, messages_snapshot: List[Dict], prompt: str, - task_cfg: Optional[Dict[str, Any]] = None, review_run: Optional[_BackgroundReviewRun] = None, + agent: Any, messages_snapshot: list[dict], prompt: str, + task_cfg: Optional[dict[str, Any]] = None, review_run: Optional[_BackgroundReviewRun] = None, review_memory: bool = False, explicit: bool = False, ) -> None: """Daemon-thread worker: build the fork, run the prompt, surface the action summary via @@ -1299,9 +1299,9 @@ _PROMPT_NAME_BY_SCOPE = { def spawn_background_review_thread( - agent: Any, messages_snapshot: List[Dict], review_memory: bool = False, + agent: Any, messages_snapshot: list[dict], review_memory: bool = False, review_skills: bool = False, focus: Optional[str] = None, - task_cfg: Optional[Dict[str, Any]] = None, review_run: Optional[_BackgroundReviewRun] = None, + task_cfg: Optional[dict[str, Any]] = None, review_run: Optional[_BackgroundReviewRun] = None, explicit: bool = False, ): """Return ``(target, prompt)``; the caller builds the ``threading.Thread`` so test patches of diff --git a/agent/battery.py b/agent/battery.py index 043fa1c015..53eb56b06c 100644 --- a/agent/battery.py +++ b/agent/battery.py @@ -46,7 +46,7 @@ def _read_battery_uncached() -> BatteryStatus: import psutil # ``sensors_battery`` is missing on some platforms/builds of psutil. - batt = getattr(psutil, "sensors_battery")() + batt = psutil.sensors_battery() except Exception: return UNAVAILABLE if batt is None: diff --git a/agent/bedrock_adapter.py b/agent/bedrock_adapter.py index da783abbe6..e71243109d 100644 --- a/agent/bedrock_adapter.py +++ b/agent/bedrock_adapter.py @@ -25,20 +25,20 @@ from agent.errors import EmptyStreamError logger = logging.getLogger(__name__) -_bedrock_runtime_client_cache: Dict[str, Any] = {} -_bedrock_control_client_cache: Dict[str, Any] = {} +_bedrock_runtime_client_cache: dict[str, Any] = {} +_bedrock_control_client_cache: dict[str, Any] = {} # Routed multiplex profiles: one client per (profile home, region). boto3 freezes the credential # chain into the client at construction, so a region-only slot would sign profile B's calls with A's keys. -_bedrock_clients_by_home: Dict[Tuple[str, str, str], Any] = {} +_bedrock_clients_by_home: dict[tuple[str, str, str], Any] = {} # botocore session kwarg <- profile .env variable (the explicit sources of the default chain). -_AWS_SCOPED_CREDENTIAL_VARS: Tuple[Tuple[str, str], ...] = ( +_AWS_SCOPED_CREDENTIAL_VARS: tuple[tuple[str, str], ...] = ( ("aws_access_key_id", "AWS_ACCESS_KEY_ID"), ("aws_secret_access_key", "AWS_SECRET_ACCESS_KEY"), ("aws_session_token", "AWS_SESSION_TOKEN"), ("profile_name", "AWS_PROFILE"), ) -def scoped_aws_session_kwargs() -> Dict[str, str]: +def scoped_aws_session_kwargs() -> dict[str, str]: """``boto3.session.Session`` kwargs from the routed profile's secret scope, ``{}`` when unscoped. Under a HERMES_HOME override the process env holds the LAUNCH profile's ``AWS_*`` (or nothing), so @@ -73,14 +73,14 @@ def scoped_aws_session_kwargs() -> Dict[str, str]: return kwargs # Geo/global cross-Region inference-profile prefixes Bedrock prepends to a model ID. -_BEDROCK_PROFILE_PREFIXES: Tuple[str, ...] = ("global", "us", "eu", "apac", "ap", "au", "jp", "ca", "sa", "me", "af") +_BEDROCK_PROFILE_PREFIXES: tuple[str, ...] = ("global", "us", "eu", "apac", "ap", "au", "jp", "ca", "sa", "me", "af") # The GPT-5.6 Terra/Luna cards also document India geo profiles (``in.openai.gpt-5.6-terra``). -_BEDROCK_OPENAI_PROFILE_PREFIXES: Tuple[str, ...] = _BEDROCK_PROFILE_PREFIXES + ("in",) +_BEDROCK_OPENAI_PROFILE_PREFIXES: tuple[str, ...] = _BEDROCK_PROFILE_PREFIXES + ("in",) # Bedrock-hosted OpenAI GPT models (bare in-Region IDs). The bare ID is served by the Bedrock Mantle # OpenAI-compatible endpoint; its geo/global profile form is served by bedrock-runtime, where these # cards list Converse as supported. Narrow allowlist so GPT-OSS models stay on the native path. -BEDROCK_OPENAI_RESPONSES_MODEL_IDS: Tuple[str, ...] = ( +BEDROCK_OPENAI_RESPONSES_MODEL_IDS: tuple[str, ...] = ( "openai.gpt-6-astra", "openai.gpt-6.1-sol", "openai.gpt-6-sol", "openai.gpt-6-luna", "openai.gpt-5.5", "openai.gpt-5.6-sol", "openai.gpt-5.6-terra", "openai.gpt-5.6-luna", ) @@ -123,7 +123,7 @@ def _require_boto3(): return boto3 -def _cached_client(cache: Dict[str, Any], service: str, region: str): +def _cached_client(cache: dict[str, Any], service: str, region: str): """Get or create a per-region boto3 client. Unscoped: the default credential chain, one client per region. Routed profile: one client per (home, service, region), built from that profile's scoped ``AWS_*`` (falling back to the default chain only for what the profile does not set).""" @@ -171,7 +171,7 @@ def invalidate_runtime_client(region: str) -> bool: # --- Bedrock Mantle / OpenAI Responses support --- -def parse_bedrock_openai_model_id(model_id: str) -> Optional[Tuple[str, str]]: +def parse_bedrock_openai_model_id(model_id: str) -> Optional[tuple[str, str]]: """``(bare_id, profile_prefix)`` for a Bedrock-hosted OpenAI model, else None. ``profile_prefix`` is ``""`` for the bare in-Region ID and e.g. ``"us"`` / ``"global"`` for a cross-Region profile.""" normalized = str(model_id or "").strip().lower() @@ -195,7 +195,7 @@ def bedrock_openai_uses_mantle(model_id: str) -> bool: return parsed is not None and not parsed[1] -def merge_bedrock_openai_model_ids(model_ids: List[str]) -> List[str]: +def merge_bedrock_openai_model_ids(model_ids: list[str]) -> list[str]: """Append Mantle-only OpenAI models, which control-plane discovery never lists.""" merged = list(model_ids or []) seen = {str(m).lower() for m in merged} @@ -208,7 +208,7 @@ def bedrock_openai_base_url(region: str) -> str: return f"https://bedrock-mantle.{resolved}.api.aws/openai/v1" -def _mantle_url_parts(base_url: str) -> Tuple[Optional[str], str]: +def _mantle_url_parts(base_url: str) -> tuple[Optional[str], str]: """(region or None if not a Mantle host, normalized path) for a base URL.""" parsed = urlparse(str(base_url or "")) match = _BEDROCK_OPENAI_HOST_RE.match(parsed.hostname or "") @@ -226,7 +226,7 @@ def is_bedrock_openai_base_url(base_url: str) -> bool: return region is not None and path in {"", "/openai", "/openai/v1"} -def resolve_bedrock_bearer_token(env: Optional[Dict[str, str]] = None) -> str: +def resolve_bedrock_bearer_token(env: Optional[dict[str, str]] = None) -> str: """Return AWS_BEARER_TOKEN_BEDROCK when Bedrock API-key auth is configured. Under a HERMES_HOME override the read goes through the profile secret scope so a @@ -270,13 +270,13 @@ class BedrockOpenAISigV4Auth(httpx.Auth): def build_bedrock_openai_http_client(region: str, *, timeout: Optional[float] = None): """Build an httpx client that SigV4-signs Bedrock OpenAI requests.""" - kwargs: Dict[str, Any] = {"auth": BedrockOpenAISigV4Auth(region)} + kwargs: dict[str, Any] = {"auth": BedrockOpenAISigV4Auth(region)} if isinstance(timeout, (int, float)) and not isinstance(timeout, bool) and timeout > 0: kwargs["timeout"] = timeout return httpx.Client(**kwargs) -def configure_bedrock_openai_client_kwargs(client_kwargs: Dict[str, Any], *, timeout: Optional[float] = None) -> Dict[str, Any]: +def configure_bedrock_openai_client_kwargs(client_kwargs: dict[str, Any], *, timeout: Optional[float] = None) -> dict[str, Any]: """Install SigV4 auth on OpenAI SDK kwargs for Bedrock Mantle; a real API key keeps the SDK's bearer auth, the ``aws-sdk``/``no-key-required`` placeholders mean IAM chain auth.""" base_url = str(client_kwargs.get("base_url") or "") @@ -338,7 +338,7 @@ def is_streaming_access_denied_error(exc: BaseException) -> bool: # --- AWS credential detection --- # Priority order; the first group whose vars are ALL set names the auth source. -_AWS_AUTH_ENV_CHAIN: Tuple[Tuple[str, ...], ...] = ( +_AWS_AUTH_ENV_CHAIN: tuple[tuple[str, ...], ...] = ( ("AWS_BEARER_TOKEN_BEDROCK",), # Bedrock bearer token ("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"), # explicit IAM key pair ("AWS_PROFILE",), # named profile (SSO, assume-role) @@ -357,7 +357,7 @@ def _boto3_chain_has_credentials() -> bool: return False -def resolve_aws_auth_env_var(env: Optional[Dict[str, str]] = None) -> Optional[str]: +def resolve_aws_auth_env_var(env: Optional[dict[str, str]] = None) -> Optional[str]: """Name of the active AWS auth source: env vars first (no I/O), then ``"iam-role"`` via boto3's chain, else None.""" env = env if env is not None else os.environ for group in _AWS_AUTH_ENV_CHAIN: @@ -366,7 +366,7 @@ def resolve_aws_auth_env_var(env: Optional[Dict[str, str]] = None) -> Optional[s return "iam-role" if _boto3_chain_has_credentials() else None -def has_aws_credentials(env: Optional[Dict[str, str]] = None) -> bool: +def has_aws_credentials(env: Optional[dict[str, str]] = None) -> bool: """True if any AWS credential source (env vars or boto3 chain) is detected. This two-tier approach mirrors the pattern from OpenClaw PR #62673: cloud environments (EC2, ECS, @@ -376,7 +376,7 @@ def has_aws_credentials(env: Optional[Dict[str, str]] = None) -> bool: return resolve_aws_auth_env_var(env) is not None or _boto3_chain_has_credentials() -def resolve_bedrock_region(env: Optional[Dict[str, str]] = None) -> str: +def resolve_bedrock_region(env: Optional[dict[str, str]] = None) -> str: """AWS_REGION → AWS_DEFAULT_REGION → botocore configured region (~/.aws/config profiles) → us-east-1.""" env = env if env is not None else os.environ explicit = env.get("AWS_REGION", "").strip() or env.get("AWS_DEFAULT_REGION", "").strip() @@ -388,7 +388,7 @@ def resolve_bedrock_region(env: Optional[Dict[str, str]] = None) -> str: return "us-east-1" -def resolve_bedrock_runtime_region(config: Optional[Dict[str, Any]] = None) -> str: +def resolve_bedrock_runtime_region(config: Optional[dict[str, Any]] = None) -> str: """``bedrock.region`` from config.yaml, else :func:`resolve_bedrock_region`. Every non-runtime Bedrock endpoint must use this so auxiliary calls never leave the primary runtime's region. *config* skips disk.""" if config is None: @@ -405,7 +405,7 @@ def bedrock_region_from_runtime_url(base_url: str) -> str: return m.group(1) if m else "us-east-1" -def bedrock_guardrail_config(config: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]: +def bedrock_guardrail_config(config: Optional[dict[str, Any]] = None) -> Optional[dict[str, Any]]: """Converse ``guardrailConfig`` from ``bedrock.guardrail`` in config.yaml (None when unset).""" if config is None: config = {} @@ -422,7 +422,7 @@ def bedrock_guardrail_config(config: Optional[Dict[str, Any]] = None) -> Optiona return out -def bedrock_guardrail_headers(config: Optional[Dict[str, Any]] = None) -> Dict[str, str]: +def bedrock_guardrail_headers(config: Optional[dict[str, Any]] = None) -> dict[str, str]: """InvokeModel/Messages-wire form of the configured guardrail. The AnthropicBedrock SDK speaks InvokeModel, which has no ``guardrailConfig`` body field; Bedrock reads the guardrail from these headers instead (same enforcement, keeps prompt caching / thinking / 1M context).""" @@ -469,7 +469,7 @@ def bind_bedrock_runtime(agent, base_url: str, api_mode: str) -> None: agent._anthropic_client = None -def bedrock_model_ids_or_none() -> Optional[List[str]]: +def bedrock_model_ids_or_none() -> Optional[list[str]]: """Live-discover Bedrock model IDs; None on failure/empty so callers use the static list.""" with suppress(Exception): discovered = discover_bedrock_models(resolve_bedrock_runtime_region()) @@ -509,7 +509,7 @@ def _model_supports_prompt_cache(model_id: str) -> bool: # record the verdict, drop the marker there for the rest of the process, and retry once without it. CACHE_POINT_PLACEMENTS = ("tools", "system", "messages") -_CACHE_POINT_REJECTIONS: Dict[str, set] = {} # model_id (lowercased) → placements Bedrock has rejected this process +_CACHE_POINT_REJECTIONS: dict[str, set] = {} # model_id (lowercased) → placements Bedrock has rejected this process # e.g. "#/toolConfig/tools/18: extraneous key [cachePoint] is not permitted" _CACHE_POINT_PATH_PATTERN = re.compile(r"#/(?P[A-Za-z0-9_./\[\]-]*)", re.IGNORECASE) _CACHE_POINT = {"cachePoint": {"type": "default"}} @@ -553,7 +553,7 @@ def _without_cache_points(blocks: Any) -> Optional[list]: return None if len(cleaned) == len(blocks) else cleaned -def strip_cache_points(kwargs: Dict[str, Any], placement: str) -> Dict[str, Any]: +def strip_cache_points(kwargs: dict[str, Any], placement: str) -> dict[str, Any]: """Copy of Converse kwargs with ``placement``'s cachePoint removed; the SAME object back when nothing was stripped (callers use identity to decide a retry cannot help).""" if placement == "messages": @@ -576,7 +576,7 @@ def strip_cache_points(kwargs: Dict[str, Any], placement: str) -> Dict[str, Any] return kwargs -def recover_from_cache_point_rejection(exc: BaseException, kwargs: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def recover_from_cache_point_rejection(exc: BaseException, kwargs: dict[str, Any]) -> Optional[dict[str, Any]]: """Record Bedrock's cachePoint verdict and return retry kwargs, or None when the error was not a cachePoint rejection / the marker was already absent (caller re-raises).""" placement = cache_point_rejection_placement(exc) @@ -664,7 +664,7 @@ def is_anthropic_bedrock_model(model_id: str) -> bool: # --- Message format conversion: OpenAI → Bedrock Converse --- -def convert_tools_to_converse(tools: List[Dict]) -> List[Dict]: +def convert_tools_to_converse(tools: list[dict]) -> list[dict]: """OpenAI ``{"function": {...}}`` tool defs → Converse ``{"toolSpec": {...}}``.""" return [{"toolSpec": { "name": fn.get("name", ""), "description": fn.get("description", ""), @@ -685,7 +685,7 @@ def _safe_text(text) -> str: return text if text.strip() else _EMPTY_TEXT_PLACEHOLDER -def _image_block_from_data_url(url: str) -> Dict: +def _image_block_from_data_url(url: str) -> dict: """``data:;base64,...`` → Converse image block with RAW bytes (boto3 base64-encodes on the wire; passing the string through double-encodes and Bedrock rejects it).""" header, _, data = url.partition(",") @@ -698,7 +698,7 @@ def _image_block_from_data_url(url: str) -> Dict: return {"image": {"format": media_type.split("/")[-1] if "/" in media_type else "jpeg", "source": {"bytes": raw_bytes}}} -def _convert_content_to_converse(content) -> List[Dict]: +def _convert_content_to_converse(content) -> list[dict]: """OpenAI content → Converse blocks; blank text → placeholder, remote image URLs → text reference.""" if not isinstance(content, list): return [{"text": _safe_text(content)}] @@ -715,14 +715,14 @@ def _convert_content_to_converse(content) -> List[Dict]: return blocks or [dict(_PLACEHOLDER_BLOCK)] -def _system_blocks(content) -> List[Dict]: +def _system_blocks(content) -> list[dict]: """System content → text blocks; blank parts are dropped, not placeholder-filled.""" parts = [content] if isinstance(content, str) else content if isinstance(content, list) else [] texts = [part.get("text", "") if isinstance(part, dict) and part.get("type") == "text" else part for part in parts] return [{"text": text} for text in texts if isinstance(text, str) and text.strip()] -def _tool_use_block(tool_use_id, name, input_dict) -> Dict: +def _tool_use_block(tool_use_id, name, input_dict) -> dict: return {"toolUse": {"toolUseId": tool_use_id, "name": name, "input": input_dict}} @@ -734,10 +734,10 @@ def _decode_redacted(encoded) -> Optional[bytes]: return None -def _replay_ordered_blocks(ordered_blocks: List) -> List[Dict]: +def _replay_ordered_blocks(ordered_blocks: list) -> list[dict]: """Rebuild the exact Bedrock block sequence captured at normalization time; redacted reasoning is stored base64 (JSON-safe sidecar) and undecodable entries are skipped.""" - content_blocks: List[Dict] = [] + content_blocks: list[dict] = [] for block in ordered_blocks: if not isinstance(block, dict): continue @@ -750,7 +750,7 @@ def _replay_ordered_blocks(ordered_blocks: List) -> List[Dict]: # ReasoningContentBlock is a tagged union: reasoningText and redactedContent must go # out as separate blocks (#115865). Undecodable redacted entries are skipped alone. if isinstance(reasoning.get("text"), str): - reasoning_text: Dict[str, str] = {"text": reasoning["text"]} + reasoning_text: dict[str, str] = {"text": reasoning["text"]} if isinstance(reasoning.get("signature"), str) and reasoning["signature"]: reasoning_text["signature"] = reasoning["signature"] # models that sign thinking reject unsigned replay content_blocks.append({"reasoningContent": {"reasoningText": reasoning_text}}) @@ -773,7 +773,7 @@ def _parse_tool_args(args) -> Any: return {} -def _assistant_blocks(msg: Dict, content) -> List[Dict]: +def _assistant_blocks(msg: dict, content) -> list[dict]: """Assistant message → Converse blocks. An ordered ``bedrock_content_blocks`` sidecar is authoritative; otherwise redacted thinking from ``reasoning_details`` (byte-for-byte), then text, then tool calls.""" ordered_blocks = msg.get("bedrock_content_blocks") @@ -783,7 +783,7 @@ def _assistant_blocks(msg: Dict, content) -> List[Dict]: _decode_redacted(d.get("data") or d.get("redactedContentBase64")) for d in (msg.get("reasoning_details") or []) if isinstance(d, dict) and d.get("type") == "redacted_thinking" ] - content_blocks: List[Dict] = [{"reasoningContent": {"redactedContent": r}} for r in redacted if r is not None] + content_blocks: list[dict] = [{"reasoningContent": {"redactedContent": r}} for r in redacted if r is not None] if isinstance(content, str) and content.strip(): content_blocks.append({"text": content}) elif isinstance(content, list): @@ -794,14 +794,14 @@ def _assistant_blocks(msg: Dict, content) -> List[Dict]: return content_blocks -def convert_messages_to_converse(messages: List[Dict]) -> Tuple[Optional[List[Dict]], List[Dict]]: +def convert_messages_to_converse(messages: list[dict]) -> tuple[Optional[list[dict]], list[dict]]: """OpenAI messages → ``(system_blocks_or_None, converse_messages)``; tool results become ``toolResult`` user blocks. Converse needs strict user/assistant alternation with a user turn first and last: same-role neighbours merge, placeholder user turns pad the ends.""" - system_blocks: List[Dict] = [] - converse_msgs: List[Dict] = [] + system_blocks: list[dict] = [] + converse_msgs: list[dict] = [] - def append_turn(role: str, blocks: List[Dict]) -> None: + def append_turn(role: str, blocks: list[dict]) -> None: if converse_msgs and converse_msgs[-1]["role"] == role: converse_msgs[-1]["content"].extend(blocks) else: @@ -853,12 +853,12 @@ class _ResponseParts: """Accumulator shared by the sync and streaming normalizers.""" def __init__(self) -> None: - self.text_parts: List[str] = [] - self.reasoning_parts: List[str] = [] - self.reasoning_details: List[Dict[str, Any]] = [] - self.tool_calls: List[SimpleNamespace] = [] + self.text_parts: list[str] = [] + self.reasoning_parts: list[str] = [] + self.reasoning_details: list[dict[str, Any]] = [] + self.tool_calls: list[SimpleNamespace] = [] - def absorb_reasoning(self, reasoning: Any, block: Dict[str, Any], on_text=None) -> None: + def absorb_reasoning(self, reasoning: Any, block: dict[str, Any], on_text=None) -> None: """Fold a Converse ``reasoningContent`` payload into the accumulators and ``block``. The sync response nests ``reasoningText: {text, signature}``; stream deltas carry ``text`` / ``signature`` flat.""" if not isinstance(reasoning, dict): @@ -879,7 +879,7 @@ class _ResponseParts: self.reasoning_details.append({"type": "redacted_thinking", "data": encoded}) block["redactedContentBase64"] = encoded - def build(self, ordered_blocks: List[Dict[str, Any]], usage_data: Dict[str, int], stop_reason: str, model: str) -> SimpleNamespace: + def build(self, ordered_blocks: list[dict[str, Any]], usage_data: dict[str, int], stop_reason: str, model: str) -> SimpleNamespace: """Assemble the OpenAI-shaped response. Converse's inputTokens EXCLUDES cache read/write tokens (OpenAI's prompt_tokens includes them), so they are added back.""" msg = SimpleNamespace( @@ -904,7 +904,7 @@ class _ResponseParts: ) -def normalize_converse_response(response: Dict) -> SimpleNamespace: +def normalize_converse_response(response: dict) -> SimpleNamespace: """Bedrock Converse response → OpenAI ``ChatCompletion``-shaped SimpleNamespace (``.choices[0].message. {content,tool_calls,reasoning_content,reasoning_details,bedrock_content_blocks}``, ``finish_reason``, ``.usage``).""" parts = _ResponseParts() @@ -914,7 +914,7 @@ def normalize_converse_response(response: Dict) -> SimpleNamespace: parts.text_parts.append(block["text"]) ordered_blocks.append({"text": block["text"]}) elif "reasoningContent" in block: - ordered_reasoning: Dict[str, Any] = {} + ordered_reasoning: dict[str, Any] = {} parts.absorb_reasoning(block["reasoningContent"], ordered_reasoning) if ordered_reasoning: ordered_blocks.append({"reasoningContent": ordered_reasoning}) @@ -946,16 +946,16 @@ def stream_converse_with_callbacks( Blocks are keyed by the ``contentBlockIndex`` Bedrock stamps on every contentBlockStart/Delta/Stop: text blocks get NO contentBlockStart, so a counter keyed on starts shredded them (#108200).""" parts = _ResponseParts() - stream_blocks: Dict[int, Dict[str, Any]] = {} + stream_blocks: dict[int, dict[str, Any]] = {} current_block_index: Optional[int] = None - current_tool: Optional[Dict] = None - current_text_buffer: List[str] = [] + current_tool: Optional[dict] = None + current_text_buffer: list[str] = [] has_tool_use = False stop_reason = None interrupted = False - usage_data: Dict[str, int] = {} + usage_data: dict[str, int] = {} - def block_index(payload: Dict[str, Any], *, new_block: bool = False) -> int: + def block_index(payload: dict[str, Any], *, new_block: bool = False) -> int: """Index of the block a contentBlock* event addresses. Without ``contentBlockIndex`` (test doubles, proxies) a start opens a fresh slot and a delta/stop continues the current one.""" idx = payload.get("contentBlockIndex") @@ -1029,18 +1029,18 @@ def stream_converse_with_callbacks( # --- High-level API: call Bedrock Converse --- def build_converse_kwargs( - model: str, messages: List[Dict], tools: Optional[List[Dict]] = None, max_tokens: Optional[int] = 4096, + model: str, messages: list[dict], tools: Optional[list[dict]] = None, max_tokens: Optional[int] = 4096, temperature: Optional[float] = None, top_p: Optional[float] = None, - stop_sequences: Optional[List[str]] = None, guardrail_config: Optional[Dict] = None, -) -> Dict[str, Any]: + stop_sequences: Optional[list[str]] = None, guardrail_config: Optional[dict] = None, +) -> dict[str, Any]: """Build kwargs for ``bedrock-runtime.converse()`` / ``converse_stream()``. ``max_tokens=None`` omits ``maxTokens`` (model maximum; default stays 4096). cachePoint markers go on system, tools and the second-newest message (survives as the tail grows — mirrors Anthropic system_and_3), each only if the model supports caching and Bedrock has not rejected that placement.""" system_prompt, converse_messages = convert_messages_to_converse(messages) cache_at = {p for p in CACHE_POINT_PLACEMENTS if cache_point_allowed(model, p)} if _model_supports_prompt_cache(model) else set() - inference_config: Dict[str, Any] = {} if max_tokens is None else {"maxTokens": max_tokens} - kwargs: Dict[str, Any] = {"modelId": model, "messages": converse_messages, "inferenceConfig": inference_config} + inference_config: dict[str, Any] = {} if max_tokens is None else {"maxTokens": max_tokens} + kwargs: dict[str, Any] = {"modelId": model, "messages": converse_messages, "inferenceConfig": inference_config} if system_prompt: kwargs["system"] = system_prompt + [dict(_CACHE_POINT)] if "system" in cache_at else system_prompt from agent.anthropic_adapter import _forbids_sampling_params @@ -1069,9 +1069,9 @@ def build_converse_kwargs( def call_converse( - region: str, model: str, messages: List[Dict], tools: Optional[List[Dict]] = None, + region: str, model: str, messages: list[dict], tools: Optional[list[dict]] = None, max_tokens: Optional[int] = 4096, temperature: Optional[float] = None, top_p: Optional[float] = None, - stop_sequences: Optional[List[str]] = None, guardrail_config: Optional[Dict] = None, + stop_sequences: Optional[list[str]] = None, guardrail_config: Optional[dict] = None, ) -> SimpleNamespace: """Non-streaming Converse call → OpenAI-compatible response. Retries once without a rejected cachePoint placement; evicts the cached client on stale-connection errors.""" @@ -1098,7 +1098,7 @@ def call_converse( # --- Model discovery --- -_discovery_cache: Dict[str, Any] = {} +_discovery_cache: dict[str, Any] = {} _DISCOVERY_CACHE_TTL_SECONDS = 3600 @@ -1107,12 +1107,12 @@ def reset_discovery_cache(): _discovery_cache.clear() -def _model_entry(model_id: str, name: Any, provider: str, input_mods: list, output_mods: list) -> Dict[str, Any]: +def _model_entry(model_id: str, name: Any, provider: str, input_mods: list, output_mods: list) -> dict[str, Any]: return {"id": model_id, "name": (name or model_id).strip(), "provider": provider, "input_modalities": input_mods, "output_modalities": output_mods, "streaming": True} -def _list_foundation_models(client, filter_set: set, models: List[Dict[str, Any]]) -> None: +def _list_foundation_models(client, filter_set: set, models: list[dict[str, Any]]) -> None: """Append active, streaming-capable, text-output foundation models (optionally provider-filtered).""" for summary in client.list_foundation_models().get("modelSummaries", []): model_id = (summary.get("modelId") or "").strip() @@ -1131,7 +1131,7 @@ def _list_foundation_models(client, filter_set: set, models: List[Dict[str, Any] )) -def _list_inference_profiles(client, filter_set: set, models: List[Dict[str, Any]]) -> None: +def _list_inference_profiles(client, filter_set: set, models: list[dict[str, Any]]) -> None: """Append active cross-region inference profiles whose IDs are not already present (paginated).""" profiles, next_token = [], None while True: @@ -1152,7 +1152,7 @@ def _list_inference_profiles(client, filter_set: set, models: List[Dict[str, Any seen_ids.add(profile_id.lower()) -def discover_bedrock_models(region: str, provider_filter: Optional[List[str]] = None) -> List[Dict[str, Any]]: +def discover_bedrock_models(region: str, provider_filter: Optional[list[str]] = None) -> list[dict[str, Any]]: """Foundation models + inference profiles (cached 1h per region/filter), ``global.`` profiles first then by name; [] when the client cannot be built.""" # The list is account-scoped (whichever credentials the control client signs with), so a routed @@ -1169,7 +1169,7 @@ def discover_bedrock_models(region: str, provider_filter: Optional[List[str]] = except Exception as e: logger.warning("Failed to create Bedrock client for model discovery: %s", e) return [] - models: List[Dict[str, Any]] = [] + models: list[dict[str, Any]] = [] filter_set = {f.lower() for f in (provider_filter or [])} for step, log, message in ( (_list_foundation_models, logger.warning, "Failed to list Bedrock foundation models: %s"), @@ -1194,7 +1194,7 @@ def _extract_provider_from_arn(arn: str) -> str: # Static fallback when the live probe is unavailable (agent/model_metadata.py). Keys match by longest # substring, so versioned entries win over the generic "anthropic.claude-opus-4". -BEDROCK_CONTEXT_LENGTHS: Dict[str, int] = { +BEDROCK_CONTEXT_LENGTHS: dict[str, int] = { # https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-xai-grok-4-6.html "xai.grok-4.6": 500_000, # Anthropic Claude: 1M GA vs 200K. The 1M entries must match agent/model_metadata.py @@ -1303,7 +1303,7 @@ def get_bedrock_context_length(model_id: str, region: str = "", probe: bool = Tr # production caller (agent/model_metadata.py::_resolve_bedrock_context_length) passes none. _APPLICATION_PROFILE_ARN_RE = re.compile(r":application-inference-profile/") _ARN_REGION_RE = re.compile(r"^arn:[^:]+:bedrock:([a-z0-9-]+):", re.IGNORECASE) -_inference_profile_model_cache: Dict[str, str] = {} +_inference_profile_model_cache: dict[str, str] = {} def _resolve_inference_profile_model_id(profile_arn: str, region: str = "") -> str: diff --git a/agent/billing_view.py b/agent/billing_view.py index ee603cc56e..6317a5ac2a 100644 --- a/agent/billing_view.py +++ b/agent/billing_view.py @@ -316,15 +316,15 @@ def _dev_fixture_billing_state() -> Optional[BillingState]: # Prod portal host (matches subscription_view._DEV_FIXTURE_PORTAL) + the /topup deep-link suffix. common: dict[str, Any] = dict( logged_in=True, org_id="org_acme", org_slug="acme", org_name="Acme Inc", role="OWNER", - balance_usd=Decimal("3.40"), cli_billing_enabled=True, min_usd=Decimal("5"), max_usd=Decimal("500"), - charge_presets=(Decimal("10"), Decimal("25"), Decimal("50")), portal_url="https://portal.nousresearch.com/billing?topup=open", + balance_usd=Decimal("3.40"), cli_billing_enabled=True, min_usd=Decimal(5), max_usd=Decimal(500), + charge_presets=(Decimal(10), Decimal(25), Decimal(50)), portal_url="https://portal.nousresearch.com/billing?topup=open", ) card = CardInfo(brand="Visa", last4="4242") overrides: dict[str, dict[str, Any]] = { "nocard": dict(card=None), "card": dict(card=card), "card-sub": dict(card=CardInfo(brand="Visa", last4="4242", resolved_via="subPin")), - "card-autoreload": dict(card=card, auto_reload=AutoReload(enabled=True, threshold_usd=Decimal("5"), reload_to_usd=Decimal("25"))), + "card-autoreload": dict(card=card, auto_reload=AutoReload(enabled=True, threshold_usd=Decimal(5), reload_to_usd=Decimal(25))), "notadmin": dict(card=card, role="MEMBER"), "billing-off": dict(card=None, cli_billing_enabled=False), } diff --git a/agent/bounded_response.py b/agent/bounded_response.py index d0d5ac8ed8..4203ea8431 100644 --- a/agent/bounded_response.py +++ b/agent/bounded_response.py @@ -37,7 +37,7 @@ def read_streaming_error_body( stalls and oversize bodies yield best-effort partial text (or ""), so a read error can't mask the original failure. """ - chunks: List[bytes] = [] + chunks: list[bytes] = [] state = {"truncated": False} done = threading.Event() @@ -55,7 +55,7 @@ def read_streaming_error_body( break chunks.append(chunk) total += len(chunk) - except Exception as exc: # noqa: BLE001 - error path must not raise + except Exception as exc: logger.debug("bounded error-body read failed: %s", exc) finally: done.set() @@ -69,7 +69,7 @@ def read_streaming_error_body( # Closing cancels any in-flight socket read so the worker unwinds. No join (daemon, may be blocked in C). try: response.close() - except Exception: # noqa: BLE001 + except Exception: pass if state["truncated"]: diff --git a/agent/browser_provider.py b/agent/browser_provider.py index cf9989bcd3..f0f596a327 100644 --- a/agent/browser_provider.py +++ b/agent/browser_provider.py @@ -41,7 +41,7 @@ class BrowserProvider(ProviderBase): ``hermes tools`` paint.""" @abc.abstractmethod - def create_session(self, task_id: str) -> Dict[str, object]: + def create_session(self, task_id: str) -> dict[str, object]: """Create a cloud browser session and return the metadata dict from the module docstring. May raise ``ValueError`` (missing credentials) or ``RuntimeError`` (network / API failure); the dispatcher surfaces these to the user.""" diff --git a/agent/chat_completion_helpers.py b/agent/chat_completion_helpers.py index c68d7bf664..b0ce7b6e20 100644 --- a/agent/chat_completion_helpers.py +++ b/agent/chat_completion_helpers.py @@ -371,7 +371,7 @@ def _provider_stream_error_from_text(text: str, finish_reason: Optional[str], *, _IMAGE_PART_TYPES = frozenset({"image_url", "input_image", "image"}) -def _image_part_chars(part: Dict[str, Any], image_cost: int) -> int: +def _image_part_chars(part: dict[str, Any], image_cost: int) -> int: """Char-equivalent of one image content part: the per-image cost learned from provider usage (x4 chars/token), never the base64 payload length. A single native screenshot priced as text read as ~100K+ tokens and selected the giant-conversation watchdog tiers (#63871, #76411).""" @@ -466,7 +466,7 @@ def _validated_openrouter_provider_sort(raw_sort: Any) -> Optional[str]: return None -def _provider_preferences_for_agent(agent) -> Dict[str, Any]: +def _provider_preferences_for_agent(agent) -> dict[str, Any]: """Build the validated provider-routing object shared by request paths. ``provider_routing.models.`` overlays the flat constructor values for the CURRENT @@ -510,7 +510,7 @@ def _merge_nous_portal_messages_extra_body(agent, anthropic_kwargs: dict) -> dic if nous_profile is not None: anthropic_kwargs.setdefault("extra_body", {}).update( nous_profile.build_extra_body(session_id=getattr(agent, "session_id", None))) - except Exception as exc: # noqa: BLE001 — never block a turn on tagging + except Exception as exc: logger.debug("Nous Portal extra_body merge failed: %s", exc) return anthropic_kwargs @@ -861,7 +861,7 @@ def _managed_local_load_notice(agent, api_kwargs: dict) -> "Optional[str]": return f"⚙ processing prompt — {max(0, min(100, round(processed / total * 100)))}%" # Counter past the estimate (estimator undercounted): no honest denominator, label-only. return "⚙ processing prompt" - except Exception: # noqa: BLE001 — a status nicety must never break a call + except Exception: return None diff --git a/agent/client_lifecycle.py b/agent/client_lifecycle.py index 4ce4e539d0..879ca87398 100644 --- a/agent/client_lifecycle.py +++ b/agent/client_lifecycle.py @@ -678,7 +678,7 @@ class ClientLifecycleMixin: return False try: return self._try_refresh_nous_client_credentials(force=False, require_account=str(account)) - except Exception: # noqa: BLE001 — a failed adoption leaves the key in hand; the 401 path still heals + except Exception: logger.debug("Nous key pre-expiry adoption failed", exc_info=True) return False diff --git a/agent/codex_headers.py b/agent/codex_headers.py index d244522fbe..6fa2e53b08 100644 --- a/agent/codex_headers.py +++ b/agent/codex_headers.py @@ -31,7 +31,7 @@ def is_official_codex_base_url(base_url: str) -> bool: return False -def codex_cloudflare_headers(access_token: str, *, base_url: str = CODEX_AUX_BASE_URL) -> Dict[str, str]: +def codex_cloudflare_headers(access_token: str, *, base_url: str = CODEX_AUX_BASE_URL) -> dict[str, str]: """Identity and account headers for chatgpt.com/backend-api/codex. OpenAI requires third-party harnesses to identify themselves: the official @@ -51,7 +51,7 @@ def codex_cloudflare_headers(access_token: str, *, base_url: str = CODEX_AUX_BAS return headers -def codex_account_headers(access_token: str) -> Dict[str, str]: +def codex_account_headers(access_token: str) -> dict[str, str]: """Workspace headers the Codex backend derives from the OAuth JWT. ``ChatGPT-Account-ID`` (canonical casing, from codex-rs ``auth.rs``) comes from @@ -61,7 +61,7 @@ def codex_account_headers(access_token: str) -> Dict[str, str]: this region". A malformed token drops the headers rather than raising, so it surfaces as a 401 instead of a crash at client construction. """ - headers: Dict[str, str] = {} + headers: dict[str, str] = {} if not isinstance(access_token, str) or not access_token.strip(): return headers try: @@ -81,7 +81,7 @@ def codex_account_headers(access_token: str) -> Dict[str, str]: return headers -def apply_required_codex_headers(client_kwargs: Dict[str, Any], *, access_token: str, base_url: str) -> None: +def apply_required_codex_headers(client_kwargs: dict[str, Any], *, access_token: str, base_url: str) -> None: """Keep required Codex identity after user/provider header overrides.""" if not is_official_codex_base_url(base_url): return diff --git a/agent/codex_responses_adapter.py b/agent/codex_responses_adapter.py index 6296934b74..4063e61780 100644 --- a/agent/codex_responses_adapter.py +++ b/agent/codex_responses_adapter.py @@ -144,7 +144,7 @@ def _field(obj: Any, name: str, default: Any = None) -> Any: return obj.get(name) if isinstance(obj, dict) else getattr(obj, name, default) -def _part_type(part: Dict[str, Any]) -> str: +def _part_type(part: dict[str, Any]) -> str: return str(part.get("type") or "").strip().lower() @@ -215,7 +215,7 @@ def _iter_content_parts(content: list) -> Iterator[tuple[str, Any]]: yield "image", part -def _input_image_part(part: Dict[str, Any], role: str = "user", *, keep_empty_url: bool) -> Optional[Dict[str, Any]]: +def _input_image_part(part: dict[str, Any], role: str = "user", *, keep_empty_url: bool) -> Optional[dict[str, Any]]: """Responses image part from a chat/Responses image part (``image_url`` may be a str or ``{url, detail}``). Assistant → text placeholder (an assistant ``input_image`` 400s every replay); user → ``input_image``, None for an empty url unless ``keep_empty_url``; an inline @@ -240,13 +240,13 @@ def _input_image_part(part: Dict[str, Any], role: str = "user", *, keep_empty_ur url, mime = png_url, None if mime is not None: return {"type": "input_text", "text": f"[image omitted: {mime} is not a supported image format]"} - image_part: Dict[str, Any] = {"type": "input_image", "image_url": url} + image_part: dict[str, Any] = {"type": "input_image", "image_url": url} if _nonblank(detail): image_part["detail"] = detail.strip() return image_part -def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> List[Dict[str, Any]]: +def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> list[dict[str, Any]]: """Chat-style multimodal content → Responses API input parts ([] if not a list). Text is ``input_text`` (user) / ``output_text`` (assistant) — the API rejects the wrong type per role; ``input_image`` is only legal on user messages (see :func:`_input_image_part`). Unsupported @@ -257,7 +257,7 @@ def _chat_content_to_responses_parts(content: Any, *, role: str = "user") -> Lis f"Codex Responses does not support {ptype} input; use a video-capable provider." ) text_type = _text_type_for(role) - converted: List[Dict[str, Any]] = [] + converted: list[dict[str, Any]] = [] for kind, payload in _iter_content_parts(_as_list(content)): if kind == "text": converted.append({"type": text_type, "text": payload}) @@ -341,7 +341,7 @@ def _derive_responses_function_call_id(call_id: str, response_item_id: Optional[ # --- Schema conversion -------------------------------------------------------- -def _responses_tools(tools: Optional[List[Dict[str, Any]]] = None) -> Optional[List[Dict[str, Any]]]: +def _responses_tools(tools: Optional[list[dict[str, Any]]] = None) -> Optional[list[dict[str, Any]]]: """Convert chat-completions tool schemas to Responses function-tool schemas.""" fns = [item.get("function", {}) if isinstance(item, dict) else {} for item in tools or []] converted = [ @@ -365,10 +365,10 @@ def _normalize_responses_message_status(value: Any, *, default: str = "completed def _message_item( - content: List[Dict[str, Any]], *, status: str, item_id: Optional[str] = None, phase: Optional[str] = None, -) -> Dict[str, Any]: + content: list[dict[str, Any]], *, status: str, item_id: Optional[str] = None, phase: Optional[str] = None, +) -> dict[str, Any]: """Assistant ``message`` item; ``id``/``phase`` are added only when non-empty.""" - item: Dict[str, Any] = {"type": "message", "role": "assistant", "status": status, "content": content} + item: dict[str, Any] = {"type": "message", "role": "assistant", "status": status, "content": content} item.update({k: v for k, v in (("id", item_id), ("phase", phase)) if v}) return item @@ -376,7 +376,7 @@ def _message_item( _ROLE_MESSAGE_PHASES = frozenset({"commentary", "final_answer"}) -def _role_message_item(role: str, content: Any, phase: Any = None) -> Dict[str, Any]: +def _role_message_item(role: str, content: Any, phase: Any = None) -> dict[str, Any]: """Plain ``message`` input item for ``role``. ``type`` is required: llama.cpp's ``/v1/responses`` parser rejects a typeless assistant item ("Cannot determine type of 'item'"). Assistant ``phase`` is forwarded only for values the API accepts on input messages; others would 400.""" @@ -387,9 +387,9 @@ def _role_message_item(role: str, content: Any, phase: Any = None) -> Dict[str, def _assistant_message_item( - raw: Dict[str, Any], content: List[Dict[str, Any]], *, is_github_responses: bool, + raw: dict[str, Any], content: list[dict[str, Any]], *, is_github_responses: bool, current_issuer_kind: Optional[str] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Replayable assistant ``message`` item from a stored one. ``id`` is kept only when short enough and never for GitHub Copilot (ids bind to a backend connection; stale → 401); ``phase`` is preserved per OpenAI's cache guidance. The ChatGPT Codex backend additionally rejects ids that do not begin with ``msg`` (foreign Responses issuers @@ -405,16 +405,16 @@ def _assistant_message_item( def _replay_reasoning_items( - msg: Dict[str, Any], *, seen_item_ids: set, current_issuer_kind: Optional[str], + msg: dict[str, Any], *, seen_item_ids: set, current_issuer_kind: Optional[str], current_issuer_model: Optional[str] = None, native_compaction_eligible: bool, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: """Replay persisted encrypted reasoning/compaction items for one assistant turn. Skips duplicate ids, ``compaction`` checkpoints unless THIS request carries ``context_management`` (else a persisted checkpoint erases pre-checkpoint history on a model that cannot decrypt it), and items stamped by another issuer or model (HTTP 400). Items without a model stamp (legacy or unstamped) replay on a matching issuer. ``id`` (store=False lookups 404) and the Hermes provenance fields are stripped.""" global _CROSS_ISSUER_WARN_EMITTED - replayed: List[Dict[str, Any]] = [] + replayed: list[dict[str, Any]] = [] for ri in _as_list(msg.get("codex_reasoning_items")): if not (isinstance(ri, dict) and ri.get("encrypted_content")): continue @@ -446,8 +446,8 @@ def _replay_reasoning_items( def _replay_message_items( - msg: Dict[str, Any], *, is_github_responses: bool, current_issuer_kind: Optional[str] = None, -) -> List[Dict[str, Any]]: + msg: dict[str, Any], *, is_github_responses: bool, current_issuer_kind: Optional[str] = None, +) -> list[dict[str, Any]]: """Replay exact assistant message items (id/phase) for prefix-cache hits. A ``msg_*`` id minted in the same response as a ``reasoning`` item is bound to that item's ``rs_*`` id, @@ -457,7 +457,7 @@ def _replay_message_items( trimmed by the transport (``codex_reasoning_trimmed``) — and the message goes out as content/status/phase only. Reasoning-free turns keep their id. """ - replayed: List[Dict[str, Any]] = [] + replayed: list[dict[str, Any]] = [] linked_to_reasoning = bool(msg.get("codex_reasoning_trimmed")) or any( isinstance(ri, dict) and ri.get("encrypted_content") for ri in _as_list(msg.get("codex_reasoning_items")) ) @@ -490,8 +490,8 @@ class _WireCallIds: """ def __init__(self) -> None: - self._seen: Dict[str, int] = {} - self._queue: Dict[str, List[str]] = {} + self._seen: dict[str, int] = {} + self._queue: dict[str, list[str]] = {} def for_call(self, call_id: str) -> str: base = _clamp_responses_call_id(call_id) @@ -508,10 +508,10 @@ class _WireCallIds: def _replay_tool_call_items( - msg: Dict[str, Any], *, start_index: int, wire_ids: Optional[_WireCallIds] = None, -) -> List[Dict[str, Any]]: + msg: dict[str, Any], *, start_index: int, wire_ids: Optional[_WireCallIds] = None, +) -> list[dict[str, Any]]: """Convert an assistant message's ``tool_calls`` into ``function_call`` items.""" - replayed: List[Dict[str, Any]] = [] + replayed: list[dict[str, Any]] = [] for tc in _as_list(msg.get("tool_calls")): if not isinstance(tc, dict): continue @@ -529,7 +529,7 @@ def _replay_tool_call_items( return replayed -def _tool_output_items(msg: Dict[str, Any], *, wire_ids: Optional[_WireCallIds] = None) -> List[Dict[str, Any]]: +def _tool_output_items(msg: dict[str, Any], *, wire_ids: Optional[_WireCallIds] = None) -> list[dict[str, Any]]: """Convert a tool-role message to ``[function_call_output]`` (``[]`` if unpairable).""" raw_tool_call_id = msg.get("tool_call_id") call_id, tool_response_item_id = _split_responses_tool_id(raw_tool_call_id) @@ -550,10 +550,10 @@ def _tool_output_items(msg: Dict[str, Any], *, wire_ids: Optional[_WireCallIds] def _chat_messages_to_responses_input( - messages: List[Dict[str, Any]], *, is_xai_responses: bool = False, is_github_responses: bool = False, + messages: list[dict[str, Any]], *, is_xai_responses: bool = False, is_github_responses: bool = False, replay_encrypted_reasoning: bool = True, current_issuer_kind: Optional[str] = None, current_issuer_model: Optional[str] = None, native_compaction_eligible: bool = False, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: """Convert internal chat-style messages to Responses input items. ``is_xai_responses``: signature compatibility only (xAI DOES replay encrypted reasoning). @@ -591,13 +591,13 @@ def _chat_messages_to_responses_input( Hermes' local history is never truncated by native compaction, so the full conversation is still on the wire. """ - items: List[Dict[str, Any]] = [] + items: list[dict[str, Any]] = [] # Parallel to ``items``: source chat message per item. Pruning reads a summary # carrier's provenance from the source; the converted item may be a lossy shape. # Pruning needs this to read a canonical summary carrier's up-to-date, provenance-tagged content # directly — the converted `item` can be a lossy shape (stale exact-replay, or a typed # `function_call_output` wrapper) that no longer carries it (#90976). - item_sources: List[Optional[Dict[str, Any]]] = [] + item_sources: list[Optional[dict[str, Any]]] = [] seen_item_ids: set = set() wire_ids = _WireCallIds() # The ChatGPT Codex backend rejects a role message whose ``content`` is a plain string with @@ -605,7 +605,7 @@ def _chat_messages_to_responses_input( # (#51512). It accepts only typed parts, so string text goes out as ``input_text``/``output_text`` # there; other Responses routes keep the string shorthand they have always received. typed_text_only = current_issuer_kind == "codex_backend" - def emit(new_items: List[Dict[str, Any]], msg: Dict[str, Any]) -> None: + def emit(new_items: list[dict[str, Any]], msg: dict[str, Any]) -> None: items.extend(new_items) item_sources.extend([msg] * len(new_items)) for msg in messages: @@ -701,8 +701,8 @@ def classify_responses_route(agent: Any) -> ResponsesRouteFlags: def _native_responses_replay_items( - agent: Any, messages: List[Dict[str, Any]] -) -> Optional[List[Dict[str, Any]]]: + agent: Any, messages: list[dict[str, Any]] +) -> Optional[list[dict[str, Any]]]: """Build the native-compaction-eligible wire items, or ``None`` when ineligible.""" if getattr(agent, "api_mode", None) != "codex_responses" or not isinstance(messages, list): return None @@ -731,7 +731,7 @@ def _native_responses_replay_items( def has_replayable_native_compaction_checkpoint( - agent: Any, messages: List[Dict[str, Any]] + agent: Any, messages: list[dict[str, Any]] ) -> bool: """Whether the current route would replay a persisted native checkpoint.""" items = _native_responses_replay_items(agent, messages) @@ -742,7 +742,7 @@ def has_replayable_native_compaction_checkpoint( def estimate_native_responses_preflight_tokens( - agent: Any, messages: List[Dict[str, Any]], *, system_prompt: str = "", tools: Optional[List[Dict[str, Any]]] = None, + agent: Any, messages: list[dict[str, Any]], *, system_prompt: str = "", tools: Optional[list[dict[str, Any]]] = None, ) -> Optional[int]: """Estimate tokens for the checkpoint-pruned Responses payload (the full transcript overstates a natively compacted session and fires local compression needlessly). None when native compaction is not proven eligible or conversion fails. @@ -760,12 +760,14 @@ def estimate_native_responses_preflight_tokens( # --- Input preflight / validation -------------------------------------------- -_PreflightCtx = NamedTuple("_PreflightCtx", [ - ("sanitize_text", Callable[[str], str]), ("sanitize_harmony_tokens", bool), ("is_github_responses", bool), ("seen_ids", set), -]) +class _PreflightCtx(NamedTuple): + sanitize_text: Callable[[str], str] + sanitize_harmony_tokens: bool + is_github_responses: bool + seen_ids: set -def _preflight_function_call(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Dict[str, Any]: +def _preflight_function_call(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> dict[str, Any]: call_id, name = item.get("call_id"), item.get("name") if not _nonblank(call_id): raise ValueError(f"Codex Responses input[{idx}] function_call is missing call_id.") @@ -777,14 +779,14 @@ def _preflight_function_call(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) } -def _preflight_function_call_output(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Dict[str, Any]: +def _preflight_function_call_output(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> dict[str, Any]: call_id = item.get("call_id") if not _nonblank(call_id): raise ValueError(f"Codex Responses input[{idx}] function_call_output is missing call_id.") output = item.get("output", "") if isinstance(output, list): # Multimodal tool result: keep recognised input_text/input_image parts, drop the rest (4xx otherwise). - cleaned: List[Dict[str, Any]] = [] + cleaned: list[dict[str, Any]] = [] for part in output: ptype = part.get("type") if isinstance(part, dict) else None if ptype == "input_text" and _nonempty_str(part.get("text")): @@ -797,7 +799,7 @@ def _preflight_function_call_output(item: Dict[str, Any], idx: int, ctx: _Prefli return {"type": "function_call_output", "call_id": call_id.strip(), "output": output_value} -def _preflight_encrypted(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Optional[Dict[str, Any]]: +def _preflight_encrypted(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> Optional[dict[str, Any]]: """``reasoning`` / ``compaction`` items: opaque, issuer-sealed; forward only API-defined fields.""" encrypted = item.get("encrypted_content") if not _nonempty_str(encrypted): @@ -817,7 +819,7 @@ def _preflight_encrypted(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> } -def _preflight_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Dict[str, Any]: +def _preflight_message(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> dict[str, Any]: # Only replayed assistant output (a list-content item carrying id/status) takes the strict path # below. Phase alone is no replay marker: the converter's plain role items carry it too, and # preflight must not synthesize a status or reject user image parts for them. @@ -843,7 +845,7 @@ def _preflight_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Di return _assistant_message_item(item, normalized_content, is_github_responses=ctx.is_github_responses) -def _preflight_role_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) -> Dict[str, Any]: +def _preflight_role_message(item: dict[str, Any], idx: int, ctx: _PreflightCtx) -> dict[str, Any]: """``user``/``assistant`` role message, typed or untyped; string content or Responses parts.""" role = item.get("role") if role not in {"user", "assistant"}: @@ -856,7 +858,7 @@ def _preflight_role_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) # Parts are already Responses-shaped; validate and re-type text for the role. # Unlike history conversion, empty text / empty image urls are kept, not dropped. text_type = _text_type_for(role) - validated: List[Dict[str, Any]] = [] + validated: list[dict[str, Any]] = [] for part_idx, part in enumerate(content): if isinstance(part, str): if part: @@ -876,7 +878,7 @@ def _preflight_role_message(item: Dict[str, Any], idx: int, ctx: _PreflightCtx) return _role_message_item(role, validated, item.get("phase")) -_PREFLIGHT_ITEM_HANDLERS: Dict[str, Callable[..., Optional[Dict[str, Any]]]] = { +_PREFLIGHT_ITEM_HANDLERS: dict[str, Callable[..., Optional[dict[str, Any]]]] = { "function_call": _preflight_function_call, "function_call_output": _preflight_function_call_output, "reasoning": _preflight_encrypted, "compaction": _preflight_encrypted, "message": _preflight_message, } @@ -884,12 +886,12 @@ _PREFLIGHT_ITEM_HANDLERS: Dict[str, Callable[..., Optional[Dict[str, Any]]]] = { def _preflight_codex_input_items( raw_items: Any, *, is_github_responses: bool = False, sanitize_harmony_tokens: bool = False, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: if not isinstance(raw_items, list): raise ValueError("Codex Responses input must be a list of input items.") sanitize_text = _neutralize_harmony_tokens if sanitize_harmony_tokens else (lambda text: text) ctx = _PreflightCtx(sanitize_text, sanitize_harmony_tokens, is_github_responses, set()) - normalized: List[Dict[str, Any]] = [] + normalized: list[dict[str, Any]] = [] for idx, item in enumerate(raw_items): if not isinstance(item, dict): raise ValueError(f"Codex Responses input[{idx}] must be an object.") @@ -901,7 +903,7 @@ def _preflight_codex_input_items( return normalized -def _preflight_tool(tool: Any, idx: int) -> Dict[str, Any]: +def _preflight_tool(tool: Any, idx: int) -> dict[str, Any]: if not isinstance(tool, dict): raise ValueError(f"Codex Responses tools[{idx}] must be an object.") tool_type = tool.get("type") @@ -945,7 +947,7 @@ _PREFLIGHT_ALLOWED_KEYS = { } -def _optional_dict(api_kwargs: Dict[str, Any], key: str) -> Optional[Dict[str, Any]]: +def _optional_dict(api_kwargs: dict[str, Any], key: str) -> Optional[dict[str, Any]]: value = api_kwargs.get(key) if value is not None and not isinstance(value, dict): raise ValueError(f"Codex Responses request '{key}' must be an object.") @@ -955,7 +957,7 @@ def _optional_dict(api_kwargs: Dict[str, Any], key: str) -> Optional[Dict[str, A def _preflight_codex_api_kwargs( api_kwargs: Any, *, allow_stream: bool = False, is_github_responses: bool = False, sanitize_harmony_tokens: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: if not isinstance(api_kwargs, dict): raise ValueError("Codex Responses request must be a dict.") if missing := sorted(key for key in ("model", "instructions", "input") if key not in api_kwargs): @@ -969,7 +971,7 @@ def _preflight_codex_api_kwargs( input_items = _preflight_codex_input_items( api_kwargs.get("input"), is_github_responses=is_github_responses, sanitize_harmony_tokens=sanitize_harmony_tokens, ) - normalized: Dict[str, Any] = { + normalized: dict[str, Any] = { "model": model.strip(), "instructions": instructions, "input": input_items, "store": False, } tools = api_kwargs.get("tools") @@ -1019,7 +1021,7 @@ def _preflight_codex_api_kwargs( # --- Response extraction helpers ---------------------------------------------- -def _text_chunks(parts: Any, types: Optional[set] = None) -> List[str]: +def _text_chunks(parts: Any, types: Optional[set] = None) -> list[str]: """Non-empty ``.text`` of each part (optionally filtered by ``.type``); [] if not a list.""" selected = [part for part in _as_list(parts) if types is None or getattr(part, "type", None) in types] return [text for text in (getattr(part, "text", None) for part in selected) if _nonempty_str(text)] @@ -1076,13 +1078,13 @@ def _response_tool_call(item: Any, item_type: str, index: int) -> SimpleNamespac def _capture_encrypted_item( item: Any, item_type: str, issuer_kind: Optional[str], issuer_model: Optional[str] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """``{type, encrypted_content[, _issuer_kind, _issuer_model]}`` for replay, or None without a blob. Reasoning items also carry ``id`` + ``summary`` (required by the API on replay); transient ``rs_tmp_`` skip.""" encrypted = getattr(item, "encrypted_content", None) if not _nonempty_str(encrypted): return None - raw_item: Dict[str, Any] = {"type": item_type, "encrypted_content": encrypted} + raw_item: dict[str, Any] = {"type": item_type, "encrypted_content": encrypted} if issuer_kind: raw_item["_issuer_kind"] = issuer_kind if issuer_model: @@ -1112,7 +1114,7 @@ class _OutputScan: self.saw_streaming_or_item_incomplete = response_status in {"queued", "in_progress"} self.saw_commentary_phase = self.saw_final_answer_phase = self.saw_reasoning_item = False - def scan(self, output: List[Any], issuer_kind: Optional[str], issuer_model: Optional[str] = None) -> None: + def scan(self, output: list[Any], issuer_kind: Optional[str], issuer_model: Optional[str] = None) -> None: for item in output: item_type = getattr(item, "type", None) item_status = _lower_or_none(getattr(item, "status", None)) @@ -1176,7 +1178,7 @@ def _normalize_codex_response( if out_text: msg = "Codex response has empty output but output_text is present (%d chars); synthesizing output item." logger.debug(msg, len(out_text)) - content: List[Any] = [SimpleNamespace(type="output_text", text=out_text)] + content: list[Any] = [SimpleNamespace(type="output_text", text=out_text)] elif response_incomplete_content_filter: # Provider safety block, not a partial answer: finish content_filter, not incomplete. content = [] diff --git a/agent/codex_runtime.py b/agent/codex_runtime.py index eb55492ab7..0022529650 100644 --- a/agent/codex_runtime.py +++ b/agent/codex_runtime.py @@ -592,7 +592,7 @@ def _codex_turn_service_tier(agent) -> str | None: return CODEX_TIER_WORDS.get(effective_request_overrides(agent).get("service_tier")) -def _ensure_codex_session(agent, messages: List[Dict[str, Any]] | None = None) -> None: +def _ensure_codex_session(agent, messages: list[dict[str, Any]] | None = None) -> None: """Lazily spawn one CodexAppServerSession per AIAgent (reused across turns, closed by the _cleanup hook). A live session whose thread was started with a different prompt composition (TUI/Desktop ``/personality`` or a prompt mirror mutate the agent in place) is retired first so the new thread carries the current one. @@ -659,7 +659,7 @@ def _ensure_codex_session(agent, messages: List[Dict[str, Any]] | None = None) - ) -def _persist_projected_messages(agent, turn, messages: List[Dict[str, Any]]) -> bool: +def _persist_projected_messages(agent, turn, messages: list[dict[str, Any]]) -> bool: """Splice the projected messages into ``messages`` and flush them to the session DB; True when the rows are durable in the session DB (the codex thread binding may then be published). @@ -693,7 +693,7 @@ def _persist_projected_messages(agent, turn, messages: List[Dict[str, Any]]) -> return flush_ok is True -def _finish_codex_turn(agent, turn, messages: List[Dict[str, Any]], *, original_user_message: Any, +def _finish_codex_turn(agent, turn, messages: list[dict[str, Any]], *, original_user_message: Any, should_review_memory: bool) -> dict[str, Any]: """Post-turn bookkeeping mirroring the chat_completions loop; returns usage fields.""" # run_conversation() already bumped _turns_since_memory / _user_turn_count; only _iters_since_skill is ours. @@ -718,8 +718,8 @@ def _finish_codex_turn(agent, turn, messages: List[Dict[str, Any]], *, original_ return usage_result -def run_codex_app_server_turn(agent, *, user_message: str, original_user_message: Any, messages: List[Dict[str, Any]], - effective_task_id: str, should_review_memory: bool = False) -> Dict[str, Any]: +def run_codex_app_server_turn(agent, *, user_message: str, original_user_message: Any, messages: list[dict[str, Any]], + effective_task_id: str, should_review_memory: bool = False) -> dict[str, Any]: """Hand the turn to a ``codex app-server`` subprocess and project its events into ``messages``. Returns the chat_completions result shape. The user message is ALREADY in ``messages`` — never append it again.""" # Defense in depth for compression.checkpoint_required: agent init refuses the combination, but @@ -764,8 +764,8 @@ def run_codex_app_server_turn(agent, *, user_message: str, original_user_message ) -def _turn_result(interrupt: tuple[bool, Any], messages: List[Dict[str, Any]], *, api_calls: int, completed: bool, - error: Any, final_response: Any, **extra: Any) -> Dict[str, Any]: +def _turn_result(interrupt: tuple[bool, Any], messages: list[dict[str, Any]], *, api_calls: int, completed: bool, + error: Any, final_response: Any, **extra: Any) -> dict[str, Any]: """Result shape shared with the chat_completions path (``partial`` == ``not completed``).""" user_interrupted, interrupt_message = interrupt return { @@ -862,15 +862,15 @@ class _CodexResponseAssembler: def __init__(self, *, model, on_text_delta, on_reasoning_delta, on_commentary_message, on_first_delta): self.model, self.on_text_delta, self.on_reasoning_delta = model, on_text_delta, on_reasoning_delta self.on_commentary_message, self.on_first_delta = on_commentary_message, on_first_delta - self.output_items: List[Any] = [] + self.output_items: list[Any] = [] # output_index / first-observed sequence per output item, in lockstep, so settled pending calls merge # back in stream order. self.output_indexes, self.output_sequences = [], [] self.text_deltas, self.commentary_text_deltas = [], [] # pending_function_calls: announced-but-unconfirmed function calls keyed by item id. announced_output_order: # first-observed (sequence, output_index) per announced item id so a later .done keeps its announced position. - self.pending_function_calls: Dict[str, Dict[str, Any]] = {} - self.announced_output_order: Dict[str, tuple] = {} + self.pending_function_calls: dict[str, dict[str, Any]] = {} + self.announced_output_order: dict[str, tuple] = {} def _safe(self, cb: Callable | None, label: str, *args: Any) -> None: _call_guarded(cb, f"Codex stream {label} raised", args=args) @@ -1009,7 +1009,7 @@ class _CodexResponseAssembler: handler = self._EXACT_HANDLERS.get(event_type) or next((h for m, h in self._FUZZY_HANDLERS if m(event_type)), None) return bool(handler(self, event, event_type)) if handler is not None else False - def _settled_output(self) -> List[Any]: + def _settled_output(self) -> list[Any]: """Merge .done items with settled pending calls, keeping stream order.""" indexed = list(zip(self.output_indexes, self.output_sequences, self.output_items)) for pending in self.pending_function_calls.values(): @@ -1031,7 +1031,7 @@ class _CodexResponseAssembler: def result(self) -> SimpleNamespace: # With only plain text deltas (no tool calls), synthesize one message item. - output: List[Any] = list(self.output_items) + output: list[Any] = list(self.output_items) if not output and self.text_deltas and not self.has_tool_calls: content = [SimpleNamespace(type="output_text", text="".join(self.text_deltas))] output = [SimpleNamespace(type="message", role="assistant", status="completed", content=content)] diff --git a/agent/codex_runtime_history_seed.py b/agent/codex_runtime_history_seed.py index 54a817bb49..7001ac91f0 100644 --- a/agent/codex_runtime_history_seed.py +++ b/agent/codex_runtime_history_seed.py @@ -32,7 +32,7 @@ def _text_of(content: Any) -> str: return "" -def _render_row(msg: Dict[str, Any]) -> str: +def _render_row(msg: dict[str, Any]) -> str: role = msg.get("role") text = _text_of(msg.get("content")).strip() if role == "user": @@ -50,7 +50,7 @@ def _render_row(msg: Dict[str, Any]) -> str: return "" # system rows are the prompt composition, already sent as developerInstructions -def render_history_seed(messages: List[Dict[str, Any]] | None) -> str: +def render_history_seed(messages: list[dict[str, Any]] | None) -> str: """Prior turns as one text block, newest last; empty when there is nothing before the current user message. The trailing user row is the turn being submitted and is never included.""" rows = list(messages or []) diff --git a/agent/compaction_display.py b/agent/compaction_display.py index 53d42ea0dd..3df2e6d6cb 100644 --- a/agent/compaction_display.py +++ b/agent/compaction_display.py @@ -30,7 +30,7 @@ _COMPACTION_INTERNAL_FIELDS = ( ) -def project_compaction_message_for_display(message: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def project_compaction_message_for_display(message: dict[str, Any]) -> Optional[dict[str, Any]]: """Return authentic transcript content, or ``None`` for a pure handoff. Model-facing recovery history retains the complete carrier. Display diff --git a/agent/context_breakdown.py b/agent/context_breakdown.py index 5f001d22cf..6d9fedec6c 100644 --- a/agent/context_breakdown.py +++ b/agent/context_breakdown.py @@ -76,10 +76,10 @@ def _skills_block(stable: str) -> str: return m.group(0) if m else "" -def _split_tools(tools: Sequence[dict]) -> Tuple[List[dict], List[dict], List[dict]]: - builtin: List[dict] = [] - mcp: List[dict] = [] - subagent: List[dict] = [] +def _split_tools(tools: Sequence[dict]) -> tuple[list[dict], list[dict], list[dict]]: + builtin: list[dict] = [] + mcp: list[dict] = [] + subagent: list[dict] = [] for tool in tools: fn = tool.get("function") if isinstance(tool, dict) else None name = str((fn if isinstance(fn, dict) else tool).get("name") or "") @@ -88,7 +88,7 @@ def _split_tools(tools: Sequence[dict]) -> Tuple[List[dict], List[dict], List[di return builtin, mcp, subagent -def _memory_blocks(agent: Any) -> Tuple[str, str]: +def _memory_blocks(agent: Any) -> tuple[str, str]: memory_block = user_block = "" store = getattr(agent, "_memory_store", None) try: @@ -112,7 +112,7 @@ def _join(*parts: str) -> str: return "\n\n".join(part for part in parts if part).strip() -def _glyph(cat: Dict[str, Any]) -> str: +def _glyph(cat: dict[str, Any]) -> str: return _CATEGORIES.get(str(cat.get("id") or ""), (None, "▪"))[1] @@ -127,7 +127,7 @@ def context_display_source(compressor: Any) -> str: return "local_estimate" if isinstance(real, (int, float)) and shown > 0 and shown != real else "provider_usage" -def context_usage_fields(compressor: Any) -> Dict[str, Any]: +def context_usage_fields(compressor: Any) -> dict[str, Any]: """Current occupancy only; lifetime throughput is never a context fallback.""" used = max(0, getattr(compressor, "last_prompt_tokens", 0) or 0) maximum = getattr(compressor, "context_length", 0) or 0 @@ -140,7 +140,7 @@ def context_usage_fields(compressor: Any) -> Dict[str, Any]: "context_source": source, "context_estimated": source != "provider_usage"} -def compute_session_context_breakdown(agent: Any, messages: Optional[List[dict]] = None) -> Dict[str, Any]: +def compute_session_context_breakdown(agent: Any, messages: Optional[list[dict]] = None) -> dict[str, Any]: """Return a Cursor-style context usage breakdown for one live agent.""" from agent.model_metadata import estimate_messages_tokens_rough from agent.usage_anchor import anchored_context_tokens @@ -209,7 +209,7 @@ def compute_session_context_breakdown(agent: Any, messages: Optional[List[dict]] } -def compute_context_details(agent: Any) -> Dict[str, Any]: +def compute_context_details(agent: Any) -> dict[str, Any]: """Expanded per-skill / per-toolset cost listing for ``/context all``. Reuses the ``hermes prompt-size`` attribution (index-line bytes from the @@ -245,12 +245,12 @@ def compute_context_details(agent: Any) -> Dict[str, Any]: # (monospace is not guaranteed on messaging platforms). -def render_context_grid(payload: Dict[str, Any]) -> List[str]: +def render_context_grid(payload: dict[str, Any]) -> list[str]: """Glyph grid: 100 cells, one per percent of the context window; categories fill in declaration order, the remainder is free space.""" context_max = int(payload.get("context_max") or 0) total_cells = _GRID_COLUMNS * _GRID_ROWS - cells: List[str] = [] + cells: list[str] = [] if context_max > 0: for cat in payload.get("categories") or []: tokens = int(cat.get("tokens") or 0) @@ -262,7 +262,7 @@ def render_context_grid(payload: Dict[str, Any]) -> List[str]: return [" ".join(cells[row * _GRID_COLUMNS:(row + 1) * _GRID_COLUMNS]) for row in range(_GRID_ROWS)] -def render_context_category_lines(payload: Dict[str, Any]) -> List[str]: +def render_context_category_lines(payload: dict[str, Any]) -> list[str]: """Render the 'Estimated usage by category' table as plain-text lines.""" categories = payload.get("categories") or [] context_max = int(payload.get("context_max") or 0) @@ -285,12 +285,12 @@ def render_context_category_lines(payload: Dict[str, Any]) -> List[str]: return lines -def _toolset_row(group: Dict[str, Any]) -> str: +def _toolset_row(group: dict[str, Any]) -> str: return t("gateway.context.toolset_row", toolset=f"{group['toolset']:<24}", count=f"{group['tool_count']:>3}", tokens=f"{group['schema_tokens']:>8,}") -def _skill_row(entry: Dict[str, Any]) -> str: +def _skill_row(entry: dict[str, Any]) -> str: name = str(entry.get("name") or "") if len(name) > 28: name = name[:27] + "…" @@ -299,7 +299,7 @@ def _skill_row(entry: Dict[str, Any]) -> str: return t("gateway.context.skill_row", name=f"{name:<28}", index_tokens=f"{entry['index_tokens']:>6,}", md_tokens=md_str) -def _table(lines: List[str], title: str, rows: List[Dict[str, Any]], fmt) -> None: +def _table(lines: list[str], title: str, rows: list[dict[str, Any]], fmt) -> None: """Append a titled, display-capped table (blank-separated from a preceding one).""" if not rows: return @@ -311,23 +311,23 @@ def _table(lines: List[str], title: str, rows: List[Dict[str, Any]], fmt) -> Non lines.append(t("gateway.context.and_more", count=len(rows) - _DETAILS_TABLE_LIMIT)) -def render_context_details_lines(details: Dict[str, Any]) -> List[str]: +def render_context_details_lines(details: dict[str, Any]) -> list[str]: """Render the expanded ``/context all`` per-skill / per-toolset tables.""" - lines: List[str] = [] + lines: list[str] = [] _table(lines, t("gateway.context.toolsets_title"), details.get("toolsets") or [], _toolset_row) _table(lines, t("gateway.context.skills_title"), details.get("skills") or [], _skill_row) return lines def render_context_breakdown_lines( - payload: Dict[str, Any], + payload: dict[str, Any], *, - details: Optional[Dict[str, Any]] = None, + details: Optional[dict[str, Any]] = None, grid: bool = True, -) -> List[str]: +) -> list[str]: """Full /context view. ``grid`` prepends the glyph grid (CLI; the gateway keeps its own gauge); ``details`` appends the expanded listings.""" - lines: List[str] = [*render_context_grid(payload), ""] if grid else [] + lines: list[str] = [*render_context_grid(payload), ""] if grid else [] lines.extend(render_context_category_lines(payload)) context_max = int(payload.get("context_max") or 0) diff --git a/agent/context_compressor.py b/agent/context_compressor.py index fe04105af1..8863e0190c 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -73,7 +73,7 @@ def _safe_int(value: Any) -> int | None: # compression run (its only non-recursive call site is the compress path; the two recursive calls are the # deliberate main-model retry that must NOT re-issue the pin). The summary call is the ONLY auxiliary LLM # call a lean compaction attempt makes (#96603) — there are no sibling digest calls. -_SUMMARY_ROUTE_PIN: contextvars.ContextVar[Optional[Dict[str, Any]]] = ( +_SUMMARY_ROUTE_PIN: contextvars.ContextVar[Optional[dict[str, Any]]] = ( contextvars.ContextVar("hermes_summary_route_pin", default=None) ) @@ -82,7 +82,7 @@ _PINNED_ROUTE_FIELDS: tuple[str, ...] = ("provider", "model", "base_url", "api_k @contextlib.contextmanager -def pin_summary_route(route: Optional[Dict[str, Any]]): +def pin_summary_route(route: Optional[dict[str, Any]]): """Pin the next summary LLM call to an explicit route; ``None`` is a no-op. Re-entrant: restores the prior pin.""" token = _SUMMARY_ROUTE_PIN.set(route if isinstance(route, dict) else None) try: @@ -91,7 +91,7 @@ def pin_summary_route(route: Optional[Dict[str, Any]]): _SUMMARY_ROUTE_PIN.reset(token) -def take_pinned_summary_route() -> Optional[Dict[str, Any]]: +def take_pinned_summary_route() -> Optional[dict[str, Any]]: """Read and consume the pinned summary route (single use: the main-model retry must not re-issue it).""" route = _SUMMARY_ROUTE_PIN.get() if route is not None: @@ -103,7 +103,7 @@ def take_pinned_summary_route() -> Optional[Dict[str, Any]]: # fallback summary instead (``abort_on_summary_failure`` still aborts). The host pins it when the summary # route stalls again after a stall-class backoff already burned one idle window (#112420), so a provably # unhealthy route degrades once instead of re-entering the same silent stream every turn. -DETERMINISTIC_SUMMARY_ROUTE: Dict[str, Any] = {"label": "deterministic fallback summary", "deterministic": True} +DETERMINISTIC_SUMMARY_ROUTE: dict[str, Any] = {"label": "deterministic fallback summary", "deterministic": True} def take_deterministic_summary_pin() -> bool: @@ -115,7 +115,7 @@ def take_deterministic_summary_pin() -> bool: return True -def _pinned_summary_call_kwargs() -> Dict[str, Any]: +def _pinned_summary_call_kwargs() -> dict[str, Any]: """Consume the pinned route as explicit ``call_llm`` keyword arguments.""" route = take_pinned_summary_route() or {} return {field: route[field] for field in _PINNED_ROUTE_FIELDS if route.get(field) not in (None, "")} @@ -347,7 +347,7 @@ MAX_ITERATIONS_SUMMARY_REQUEST = ( _BACKGROUND_PROCESS_NOTIFICATION_PREFIX = "[IMPORTANT: Background process " -def _fresh_compaction_message_copy(msg: Dict[str, Any]) -> Dict[str, Any]: +def _fresh_compaction_message_copy(msg: dict[str, Any]) -> dict[str, Any]: """Copy a message for compaction assembly without persistence markers (``_strip_persistence_markers`` is authoritative).""" fresh = msg.copy() fresh.pop(_DB_PERSISTED_MARKER, None) @@ -364,7 +364,7 @@ def _template_visible_role(message: Any) -> Optional[str]: return None if role == "tool" or (role == "assistant" and message.get("tool_calls")) else role -def _last_template_visible_role(messages: List[Dict[str, Any]]) -> Optional[str]: +def _last_template_visible_role(messages: list[dict[str, Any]]) -> Optional[str]: """Last role a strict alternation template would count in *messages*. ``None`` when every row is template-exempt (tool flow only). @@ -379,7 +379,7 @@ def _last_template_visible_role(messages: List[Dict[str, Any]]) -> Optional[str] ) -def _strip_persistence_markers(messages: List[Dict[str, Any]]) -> None: +def _strip_persistence_markers(messages: list[dict[str, Any]]) -> None: """Enforce the invariant: no assembled message carries a persistence marker. A leaked ``_db_persisted`` makes the child-session rotation flush skip the row, losing it from state.db. Per-copy-site strips are positional and re-leak when a copy site is added; this terminal sweep makes the @@ -399,7 +399,7 @@ class StaleHeldHistory(RuntimeError): """ -def _archive_watermark_for(session_db: Any, session_id: str, held: List[Dict[str, Any]], +def _archive_watermark_for(session_db: Any, session_id: str, held: list[dict[str, Any]], start_watermark: Optional[int] = None) -> Optional[int]: """The archive watermark for a commit that rewrites the history this process holds. @@ -426,7 +426,7 @@ def _archive_watermark_for(session_db: Any, session_id: str, held: List[Dict[str return held_archive_watermark(session_db, session_id, start_watermark, held, stale_raises=True) -def stamp_db_persisted_markers(messages: List[Dict[str, Any]]) -> None: +def stamp_db_persisted_markers(messages: list[dict[str, Any]]) -> None: """Fulfil the post-commit contract of ``SessionDB.archive_and_compact()``. Single stamp site for all callers. Call ONLY after the commit succeeded, on the dict instances the caller keeps live. Needed because compress() output is marker-swept for the ROTATION flush; an @@ -441,7 +441,7 @@ def _is_checkpoint_item(item: Any) -> bool: return isinstance(item, dict) and item.get("type") == "compaction" -def _newest_checkpoint_carrier(messages: List[Dict[str, Any]], key: str) -> int: +def _newest_checkpoint_carrier(messages: list[dict[str, Any]], key: str) -> int: """Index of the last assistant message carrying a ``type: "compaction"`` item under *key*, or -1. Transcript-side mirror of ``native_compaction.prune_pre_checkpoint_items``' newest-run-wins rule: the wire builder drops every checkpoint before the last one, so this is the only carrier whose @@ -456,7 +456,7 @@ def _newest_checkpoint_carrier(messages: List[Dict[str, Any]], key: str) -> int: return -1 -def _set_sidecar(msg: Dict[str, Any], key: str, kept: List[Any]) -> None: +def _set_sidecar(msg: dict[str, Any], key: str, kept: list[Any]) -> None: """Filter items, never leave an empty sidecar behind.""" if kept: msg[key] = kept @@ -465,8 +465,8 @@ def _set_sidecar(msg: Dict[str, Any], key: str, kept: List[Any]) -> None: def drop_shadowed_checkpoints( - messages: List[Dict[str, Any]], key: str = "codex_reasoning_items", *, before: Optional[int] = None, -) -> List[int]: + messages: list[dict[str, Any]], key: str = "codex_reasoning_items", *, before: Optional[int] = None, +) -> list[int]: """Drop ``type: "compaction"`` items from every assistant row older than the newest carrier (rows at index >= *before* are left alone). A checkpoint a newer carrier shadows has no reader on any wire: ``prune_pre_checkpoint_items`` rebuilds each request around the newest checkpoint run and the replay @@ -474,7 +474,7 @@ def drop_shadowed_checkpoints( In place; returns the indices rewritten.""" newest = _newest_checkpoint_carrier(messages, key) stop = newest if before is None else min(newest, before) - rewritten: List[int] = [] + rewritten: list[int] = [] for i in range(max(stop, 0)): msg = messages[i] if not isinstance(msg, dict) or msg.get("role") != "assistant": @@ -487,7 +487,7 @@ def drop_shadowed_checkpoints( return rewritten -def _prune_stale_reasoning_replay(messages: List[Dict[str, Any]]) -> int: +def _prune_stale_reasoning_replay(messages: list[dict[str, Any]]) -> int: """Strip stale ``codex_reasoning_items`` from assistant turns older than the active one. Boundary is the last USER message (a turn spans several assistant rows): the Responses API replays a turn's bridging reasoning items together, so cutting at the last ASSISTANT would strip mid-chain. @@ -546,7 +546,7 @@ _SALVAGE_SUMMARY_MAX_CHARS = 8_000 _SALVAGE_KEEP_RECENT_TOOLS = 2 -def _looks_like_compaction_summary(msg: Dict[str, Any], content: str) -> bool: +def _looks_like_compaction_summary(msg: dict[str, Any], content: str) -> bool: # Only cap standalone handoffs; merged carriers contain live user text. Content heuristics never # authorize mutating a live turn: require the private compressor marker. Tool messages are # handled only by the stub/keep-recent pass. @@ -562,7 +562,7 @@ def _looks_like_compaction_summary(msg: Dict[str, Any], content: str) -> bool: return bool(msg.get(COMPRESSED_SUMMARY_METADATA_KEY)) or "CONTEXT COMPACTION" in head or "Conversation Summary" in head -def _salvage_reduce_todo_snapshot(out: List[Dict[str, Any]]) -> None: +def _salvage_reduce_todo_snapshot(out: list[dict[str, Any]]) -> None: """Last-resort shrink: drop the synthetic todo snapshot, keeping only a pruned-skill reload notice if present.""" from agent.conversation_compression import _PRUNED_SKILL_RELOAD_NOTICE_HEADER for i in range(len(out) - 1, -1, -1): @@ -579,8 +579,8 @@ def _salvage_reduce_todo_snapshot(out: List[Dict[str, Any]]) -> None: def salvage_grown_transcript( - original: List[Dict[str, Any]], candidate: List[Dict[str, Any]], budget: Optional[int] = None, -) -> Optional[List[Dict[str, Any]]]: + original: list[dict[str, Any]], candidate: list[dict[str, Any]], budget: Optional[int] = None, +) -> Optional[list[dict[str, Any]]]: """Mechanically shrink a compression candidate (copies, cheapest loss first); ``None`` unless strictly smaller.""" if not candidate or not original: return None @@ -699,7 +699,7 @@ _RESTART_HANDOFF_PROBE_EXTRA_MESSAGES = 4 class _HandoffScan: """Result of ``ContextCompressor._scan_window_handoffs``.""" - turns_to_summarize: List[Dict[str, Any]] + turns_to_summarize: list[dict[str, Any]] summary_indices: set tail_start: int previous_summary_before: Optional[str] @@ -899,7 +899,7 @@ def _extract_pruned_skill_names(text: str) -> list[str]: return list(dict.fromkeys(m.group(1) for m in _SKILL_PRUNED_MARKER_RE.finditer(text or ""))) -def _collect_ghosted_skill_names(turns: List[Dict[str, Any]]) -> list[str]: +def _collect_ghosted_skill_names(turns: list[dict[str, Any]]) -> list[str]: """Skill names about to be lost in compaction: demoted ``skill_view`` rows and raw, never-demoted bodies.""" call_id_to_skill: dict[str, str] = {} for idx, skill in _skill_view_call_sites(turns): @@ -980,7 +980,7 @@ def _synthetic_user_row(content: str) -> bool: return content.lstrip().startswith(_SYNTHETIC_USER_ROW_PREFIXES) -def _build_verbatim_user_section(turns: List[Dict[str, Any]]) -> str: +def _build_verbatim_user_section(turns: list[dict[str, Any]]) -> str: """Compacted region's REAL user messages verbatim, newest-first under a char budget (straddler truncated); "" if none.""" collected: list[str] = [] used = 0 @@ -1069,7 +1069,7 @@ _ANCHOR_NOISE = frozenset({ }) -def _build_anchor_index(turns: List[Dict[str, Any]]) -> str: +def _build_anchor_index(turns: list[dict[str, Any]]) -> str: """Regex-harvest exact identifiers from the compacted region (LLM-free); per-category caps, most-frequent first.""" text = "\n".join(c for c in (msg.get("content") for msg in turns) if isinstance(c, str) and c) if not text: @@ -1110,7 +1110,7 @@ def _build_anchor_index(turns: List[Dict[str, Any]]) -> str: _SKILL_PRUNE_RECENT_WINDOW = 10 -def _skill_view_call_sites(messages: List[Dict[str, Any]]) -> list[tuple[int, str]]: +def _skill_view_call_sites(messages: list[dict[str, Any]]) -> list[tuple[int, str]]: """Yield ``(message_index, skill_name)`` for every skill_view tool call.""" sites: list[tuple[int, str]] = [] for i, msg in enumerate(messages): @@ -1127,7 +1127,7 @@ def _skill_view_call_sites(messages: List[Dict[str, Any]]) -> list[tuple[int, st return sites -def _collect_protected_skill_names(messages: List[Dict[str, Any]], prune_boundary: int) -> set[str]: +def _collect_protected_skill_names(messages: list[dict[str, Any]], prune_boundary: int) -> set[str]: """Skill names (lower-cased) whose skill_view bodies must survive Phase-1 demotion. Recently loaded, loaded inside the protected tail, or named by a tail user message. Applies to Phase-1/2 only; the Pass-4 pressure demotion ignores it.""" @@ -1216,9 +1216,9 @@ def _extract_tool_call_name_and_args(tool_call: Any) -> tuple[str, str]: return str(_tc_get(fn, "name") or "unknown"), str(_tc_get(fn, "arguments") or "") -def _tool_calls_by_id(messages: List[Dict[str, Any]]) -> Dict[str, tuple]: +def _tool_calls_by_id(messages: list[dict[str, Any]]) -> dict[str, tuple]: """Map ``tool_call_id -> (tool_name, raw_arguments)`` over every assistant tool call.""" - out: Dict[str, tuple] = {} + out: dict[str, tuple] = {} for msg in messages: if msg.get("role") != "assistant": continue @@ -1352,17 +1352,17 @@ def _estimate_msg_budget_tokens(msg: dict, charge_stale_thinking: bool = True) - return tokens -def _last_index_with_role(messages: "List[Dict[str, Any]]", role: str) -> int: +def _last_index_with_role(messages: "list[dict[str, Any]]", role: str) -> int: """Index of the newest dict message with ``role``, or -1.""" return max((i for i, m in enumerate(messages) if isinstance(m, dict) and m.get("role") == role), default=-1) -def _last_assistant_index(messages: "List[Dict[str, Any]]") -> int: +def _last_assistant_index(messages: "list[dict[str, Any]]") -> int: """Newest assistant message index, or -1 (the one turn whose thinking may replay; see ``_NEWEST_TURN_ONLY_BUDGET_KEYS``).""" return _last_index_with_role(messages, "assistant") -def _pending_tool_round(messages: "List[Dict[str, Any]]") -> range: +def _pending_tool_round(messages: "list[dict[str, Any]]") -> range: """Indices of the tool results the transcript ends with — a round the model has not answered yet; empty when the transcript ends in any other row. /steer rows after the round do not answer it: a steer is delivered after the newest tool result before the next API call, and two can land in one iteration (one when the tool @@ -1452,7 +1452,7 @@ def _append_text_to_content(content: Any, text: str, *, prepend: bool = False) - return text + rendered if prepend else rendered + text -def _replace_image_parts(parts: Any, placeholder: str) -> Optional[List[Any]]: +def _replace_image_parts(parts: Any, placeholder: str) -> Optional[list[Any]]: """New parts list with every image part replaced by a text placeholder; None if no images.""" if not isinstance(parts, list) or not any(_is_image_part(p) for p in parts): return None @@ -1469,7 +1469,7 @@ def _tool_content_has_images(content: Any) -> bool: return _content_has_images(_tool_result_parts(content)) -def _strip_images_from_tool_msg(msg: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _strip_images_from_tool_msg(msg: dict[str, Any]) -> Optional[dict[str, Any]]: """Copy of a tool message with image payloads replaced (stale ``api_content`` dropped); ``None`` if nothing to strip.""" content = msg.get("content") if isinstance(content, dict) and content.get("_multimodal"): @@ -1479,7 +1479,7 @@ def _strip_images_from_tool_msg(msg: Dict[str, Any]) -> Optional[Dict[str, Any]] return None if stripped is None else _rewritten(msg, stripped) -def _rewritten(msg: Dict[str, Any], content: Any) -> Dict[str, Any]: +def _rewritten(msg: dict[str, Any], content: Any) -> dict[str, Any]: """Copy of ``msg`` carrying ``content``; drops the stale ``api_content`` sidecar so replay can't resend it.""" new_msg = {**msg, "content": content} drop_stale_api_content(new_msg) @@ -1487,7 +1487,7 @@ def _rewritten(msg: Dict[str, Any], content: Any) -> Dict[str, Any]: def _retire_stale_tool_result_images( - result: List[Dict[str, Any]], keep_newest: int = _MAX_KEEP_TOOL_IMAGES, spared: range = range(0), + result: list[dict[str, Any]], keep_newest: int = _MAX_KEEP_TOOL_IMAGES, spared: range = range(0), ) -> int: """Replace image payloads on older tool results with text placeholders. Keeps the newest ``keep_newest`` image-bearing tool messages and any spared pending round; @@ -1511,7 +1511,7 @@ def _retire_stale_tool_result_images( return pruned -def _image_payload(msg: Dict[str, Any]) -> Tuple[int, int]: +def _image_payload(msg: dict[str, Any]) -> tuple[int, int]: """``(blocks, bytes)`` of image payload in a message. The provider counts BLOCKS: one ``tool_result`` carrying three screenshots is three against @@ -1537,7 +1537,7 @@ def _image_payload(msg: Dict[str, Any]) -> Tuple[int, int]: return blocks, payload -def evict_stale_outbound_tool_images(api_messages: List[Dict[str, Any]]) -> int: +def evict_stale_outbound_tool_images(api_messages: list[dict[str, Any]]) -> int: """Drop stale screenshot/vision payloads from the per-call API copy. Compression's keep-newest pass only runs when prune/compress fires, and the Anthropic @@ -1550,7 +1550,7 @@ def evict_stale_outbound_tool_images(api_messages: List[Dict[str, Any]]) -> int: reserved against the ceiling but never rewritten — policy and rationale in :mod:`agent.image_eviction_policy`. Returns the number of messages rewritten. """ - carriers: List[Tuple[int, Tuple[int, int]]] = [] + carriers: list[tuple[int, tuple[int, int]]] = [] reserved_blocks = reserved_bytes = 0 for i in range(len(api_messages) - 1, -1, -1): msg = api_messages[i] @@ -1598,7 +1598,7 @@ def _strip_images_from_content(content: Any) -> Any: return content if stripped is None else stripped -def _strip_historical_media(messages: List[Dict[str, Any]], spared: range = range(0)) -> List[Dict[str, Any]]: +def _strip_historical_media(messages: list[dict[str, Any]], spared: range = range(0)) -> list[dict[str, Any]]: """Replace image parts in older messages with placeholder text. Rule 1: strip everything before the newest image-bearing user message. Rule 1b: the opening attachment ages out once a newer tool image exists. Rule 2: keep only the newest tool-result image, @@ -1621,7 +1621,7 @@ def _strip_historical_media(messages: List[Dict[str, Any]], spared: range = rang # Nothing to strip under any rule. return messages - def _is_stale(index: int, message: Dict[str, Any]) -> bool: + def _is_stale(index: int, message: dict[str, Any]) -> bool: if index in spared: return False # Rule 1: everything before the newest image-bearing user message. Rule 1b: the opening @@ -1638,7 +1638,7 @@ def _strip_historical_media(messages: List[Dict[str, Any]], spared: range = rang or (message.get("role") == "tool" and index != tool_anchor) ) - def _stripped(i: int, msg: Any) -> Optional[Dict[str, Any]]: + def _stripped(i: int, msg: Any) -> Optional[dict[str, Any]]: if not isinstance(msg, dict) or not _is_stale(i, msg): return None content = msg.get("content") @@ -1664,7 +1664,7 @@ def _summary_part_text(part: Any) -> str: return _image_part_label(part) if ptype in _IMAGE_PART_TYPES else f"[{ptype or 'attachment'}]" -def _image_part_label(part: Dict[str, Any]) -> str: +def _image_part_label(part: dict[str, Any]) -> str: """Short summarizer label for an image part: http(s) URLs kept as a handle, ``data:`` URLs collapse to ``[image]``.""" url = part.get("image_url") if isinstance(url, dict): @@ -1684,7 +1684,7 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) -> """1-line summary of a tool call + result. Never raises: a malformed historical call must not crash-loop compression.""" try: return _summarize_tool_result_unguarded(tool_name, tool_args, tool_content) - except Exception as exc: # noqa: BLE001 — a summary must never crash compression + except Exception as exc: logger.debug("Tool-result summary failed for %s: %s", tool_name, exc) _len = len(tool_content) if isinstance(tool_content, str) else 0 return f"[{tool_name}] ({_len:,} chars result)" @@ -2110,7 +2110,7 @@ def _today_for_prompt() -> str: # Per-section summarizer instructions, keyed by "the transcript has a real user turn". Wording # is deliberately plain: Azure/OpenAI content filters have flagged stronger "injection" / # "do not respond" framing. Prompt text is byte-pinned — restructure code around it only. -_SECTION_INSTRUCTIONS: Dict[bool, Dict[str, str]] = { +_SECTION_INSTRUCTIONS: dict[bool, dict[str, str]] = { True: { "language": ( "Write the summary in the same language the user was using in the " @@ -2199,14 +2199,14 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi def _begin_compression_telemetry( self, *, current_tokens: int | None, attempt_id: str | None = None, session_id: str | None = None, trigger_source: str | None = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Initialize content-free per-attempt compression telemetry.""" seed = getattr(self, "_compression_telemetry_seed", None) seed = seed if isinstance(seed, dict) else {} attempt_id = attempt_id or seed.get("attempt_id") session_id = session_id or seed.get("session_id") trigger_source = trigger_source or seed.get("trigger_source") - telemetry: Dict[str, Any] = { + telemetry: dict[str, Any] = { "event": "compression_attempt", "attempt_id": attempt_id or uuid.uuid4().hex, "session_id": session_id or "", "trigger_source": trigger_source or "unknown", "main_provider": self.provider or "", "main_model": self.model or "", @@ -2228,8 +2228,8 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi return telemetry def _record_compression_regions( - self, *, head_messages: List[Dict[str, Any]], middle_messages: List[Dict[str, Any]], - tail_messages: List[Dict[str, Any]], + self, *, head_messages: list[dict[str, Any]], middle_messages: list[dict[str, Any]], + tail_messages: list[dict[str, Any]], ) -> None: telemetry = getattr(self, "_active_compression_telemetry", None) if isinstance(telemetry, dict): @@ -2238,9 +2238,9 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi telemetry["protected_tail_tokens"] = estimate_messages_tokens_rough(tail_messages) def _record_aux_compression_call( - self, *, prompt_messages: List[Dict[str, Any]], max_tokens: int | None, duration_ms: int, + self, *, prompt_messages: list[dict[str, Any]], max_tokens: int | None, duration_ms: int, aux_provider: str | None = None, aux_model: str | None = None, - effective_aux_context: int | None = None, phase_timings: Dict[str, Any] | None = None, + effective_aux_context: int | None = None, phase_timings: dict[str, Any] | None = None, ) -> None: telemetry = getattr(self, "_active_compression_telemetry", None) if not isinstance(telemetry, dict): @@ -2349,7 +2349,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi def max_summary_tokens(self, value: int) -> None: self._max_summary_tokens = value - def on_session_end(self, session_id: str, messages: List[Dict[str, Any]]) -> None: + def on_session_end(self, session_id: str, messages: list[dict[str, Any]]) -> None: """Clear all per-session compaction state at a real session boundary. Session end (CLI exit, gateway expiry, id rotation) — NOT /new or /reset. Every per-session flag/counter can contaminate the next live session (suppressed compression, stale cooldowns, @@ -2637,7 +2637,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi # Any completed boundary (incl. the degraded fallback) settles the overload budget (#123167). self._reset_consecutive_overload_aborts() - def get_active_compression_failure_cooldown(self, *, refresh: bool = False) -> Optional[Dict[str, Any]]: + def get_active_compression_failure_cooldown(self, *, refresh: bool = False) -> Optional[dict[str, Any]]: """Return the live compression-failure cooldown for the bound session.""" if refresh: # Rollback must distinguish an authoritative empty row from a failed read; the return value can't. @@ -2990,7 +2990,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi # must ABORT and preserve the session regardless of abort_on_summary_failure (see _TERMINAL_SUMMARY_FAILURES). self._clear_terminal_summary_failures() - def update_from_response(self, usage: Dict[str, Any]): + def update_from_response(self, usage: dict[str, Any]): """Update tracked token usage from API response.""" self.last_prompt_tokens = usage.get("prompt_tokens", 0) self.last_completion_tokens = usage.get("completion_tokens", 0) @@ -3209,7 +3209,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi return False def _walk_tail_budget( - self, messages: List[Dict[str, Any]], head_end: int, ceiling: int, min_tail: int, *, cut_at_break: bool, + self, messages: list[dict[str, Any]], head_end: int, ceiling: int, min_tail: int, *, cut_at_break: bool, ) -> tuple[int, int]: """Accumulate message tokens newest-first until ``ceiling`` (once ``min_tail`` rows are kept). Returns ``(cut_idx, accumulated)``; ``cut_idx`` is the first protected index. On the budget @@ -3228,7 +3228,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi cut = i return cut, accumulated - def _tail_row_pricer(self, messages: List[Dict[str, Any]]) -> Callable[[int], int]: + def _tail_row_pricer(self, messages: list[dict[str, Any]]) -> Callable[[int], int]: """Per-row tail price ``index -> tokens``: the walk's accounting, shared by every gate that sizes a tail region.""" newest_asst_idx = _last_assistant_index(messages) charge_all_thinking = self._stale_thinking_on_wire() @@ -3238,7 +3238,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi return lambda i: _estimate_msg_budget_tokens(messages[i], charge_all_thinking or i == newest_asst_idx) def _prune_boundary( - self, result: List[Dict[str, Any]], protect_tail_count: int, protect_tail_tokens: int | None, + self, result: list[dict[str, Any]], protect_tail_count: int, protect_tail_tokens: int | None, ) -> int: """First index of the protected tail; token budget (when given) beats the count floor.""" if protect_tail_tokens is None or protect_tail_tokens <= 0: @@ -3250,7 +3250,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi return min(boundary, len(result) - min_protect) @staticmethod - def _dedupe_tool_results(result: List[Dict[str, Any]]) -> int: + def _dedupe_tool_results(result: list[dict[str, Any]]) -> int: """Pass 1: keep the newest copy of identical tool results, back-reference older ones.""" pruned = 0 content_hashes: set = set() @@ -3269,7 +3269,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi @staticmethod def _demote_tool_result_at( - result: List[Dict[str, Any]], idx: int, call_id_to_tool: Dict[str, tuple[str, str]], + result: list[dict[str, Any]], idx: int, call_id_to_tool: dict[str, tuple[str, str]], min_prune_chars: int, protected_skills: Optional[set[str]] = None, ) -> bool: """Replace the tool result at ``idx`` with a 1-line summary; True if modified. @@ -3309,8 +3309,8 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi return max(ceiling, token_budget) def _pressure_demote_tail( - self, result: List[Dict[str, Any]], prune_boundary: int, protect_tail_tokens: int, - call_id_to_tool: Dict[str, tuple[str, str]], min_prune_chars: int, spared: range, + self, result: list[dict[str, Any]], prune_boundary: int, protect_tail_tokens: int, + call_id_to_tool: dict[str, tuple[str, str]], min_prune_chars: int, spared: range, ) -> int: """Pass 4: demote tool-result bodies inside the protected tail when it alone exceeds the soft budget (#61932). Keeps a short recent floor and the ``spared`` pending tool round verbatim; overrides @@ -3358,7 +3358,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi ) return demoted - def _spared_pending_tool_round(self, messages: List[Dict[str, Any]]) -> range: + def _spared_pending_tool_round(self, messages: list[dict[str, Any]]) -> range: """Return the pending round, owning assistant(tool_calls) row included, only when it fits the input window's hard share. The call's args are part of the unread round: the model reads its result next to them.""" pending = _pending_tool_round(messages) @@ -3370,9 +3370,9 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi return pending if sum(_estimate_msg_budget_tokens(messages[i]) for i in pending) <= hard_share else range(0) def _prune_old_tool_results( - self, messages: List[Dict[str, Any]], protect_tail_count: int, + self, messages: list[dict[str, Any]], protect_tail_count: int, protect_tail_tokens: int | None = None, min_prune_chars: int = _PRUNE_MIN_CHARS, - ) -> tuple[List[Dict[str, Any]], int]: + ) -> tuple[list[dict[str, Any]], int]: """Project old tool-result bodies to bounded summaries without rewriting tool-call arguments. Returns ``(messages, count)``; token budget (when given) takes priority over the message-count floor.""" @@ -3480,8 +3480,8 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi ) def prune_tool_results_only( - self, messages: List[Dict[str, Any]], current_tokens: int | None = None, - ) -> tuple[List[Dict[str, Any]], int]: + self, messages: list[dict[str, Any]], current_tokens: int | None = None, + ) -> tuple[list[dict[str, Any]], int]: """Deterministic, no-LLM tool-result prune gated on ``proactive_prune_tokens``. Protects the tail by message COUNT only. A commit breaks the prompt cache, so it requires ``proactive_prune_min_reclaim_tokens`` and a full regrowth runway; otherwise returns the INPUT @@ -3560,7 +3560,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi self._last_reclaim_block_warn = None return pruned_msgs, pruned_count - def _compute_summary_budget(self, turns_to_summarize: List[Dict[str, Any]]) -> int: + def _compute_summary_budget(self, turns_to_summarize: list[dict[str, Any]]) -> int: """Scale the summary token budget with content size and context window.""" content_tokens = estimate_messages_tokens_rough(turns_to_summarize) budget = int(content_tokens * _SUMMARY_RATIO) @@ -3588,7 +3588,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi args = args[:self._TOOL_ARGS_HEAD] + "..." return f" {fn.get('name', '?')}({args})" - def _serialize_records_for_summary(self, turns: List[Dict[str, Any]]) -> List[str]: + def _serialize_records_for_summary(self, turns: list[dict[str, Any]]) -> list[str]: """Serialize turns into a list of labeled, redacted records for the summarizer.""" # Lazy import: agent_runtime_helpers pulls heavy transitive imports. from agent.agent_runtime_helpers import strip_think_blocks @@ -3613,11 +3613,11 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi parts.append(f"[{role.upper()}]: {content}") return parts - def _serialize_for_summary(self, turns: List[Dict[str, Any]]) -> str: + def _serialize_for_summary(self, turns: list[dict[str, Any]]) -> str: """Serialize turns into labeled, redacted text for the summarizer.""" return "\n\n".join(self._serialize_records_for_summary(turns)) - def _fallback_anchors(self, turns_to_summarize: List[Dict[str, Any]]) -> Dict[str, list[str]]: + def _fallback_anchors(self, turns_to_summarize: list[dict[str, Any]]) -> dict[str, list[str]]: """Locally extractable anchors: user asks, actions, files, blockers, last dropped turns.""" user_asks: list[str] = [] assistant_actions: list[str] = [] @@ -3667,7 +3667,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi elif role == "tool": tool_name, tool_args = call_id_to_tool.get(str(msg.get("tool_call_id") or ""), ("unknown", "")) tool_actions.append(_summarize_tool_result(tool_name, tool_args, text or "")) - if re.search(r"\b(error|failed|exception|traceback|timeout|timed out|fatal)\b", text, re.I): + if re.search(r"\b(error|failed|exception|traceback|timeout|timed out|fatal)\b", text, re.IGNORECASE): blockers.append(text[:500]) return { "user_asks": user_asks, @@ -3678,7 +3678,7 @@ class ContextCompressor(SummaryDispatchMixin, PreLlmSkipMixin, MicroCompactionMi } def _build_static_fallback_summary( - self, turns_to_summarize: List[Dict[str, Any]], reason: str | None = None, + self, turns_to_summarize: list[dict[str, Any]], reason: str | None = None, ) -> str: """Deterministic handoff when the LLM summarizer is unavailable: locally extractable anchors (user asks, actions, files, errors) in the normal summary structure so downstream prompts recover gracefully.""" @@ -3745,7 +3745,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb summary = _reinject_pruned_skill_markers(summary, _pruned_names) return self._augment_summary_lean(summary, turns_to_summarize) - def _demote_stale_tail_tools(self, messages: List[Dict[str, Any]], tail_start: int) -> List[Dict[str, Any]]: + def _demote_stale_tail_tools(self, messages: list[dict[str, Any]], tail_start: int) -> list[dict[str, Any]]: """Lean mode: demote tail tool results older than the newest ``_LEAN_TAIL_KEEP_TOOL_ROUNDS`` rounds to recovery stubs; skill-marker rows untouched. New list (untouched rows shared, demoted copied).""" session_id = getattr(self, "_session_id", "") or "" @@ -3773,7 +3773,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb logger.info("Lean tail: demoted %d stale tool result(s)", demoted) return result - def _augment_summary_lean(self, summary: str, turns_to_summarize: List[Dict[str, Any]]) -> str: + def _augment_summary_lean(self, summary: str, turns_to_summarize: list[dict[str, Any]]) -> str: """Append deterministic lean-mode sections to a summary; no-op in legacy mode.""" if getattr(self, "tail_mode", "lean") != "lean": return summary @@ -3821,7 +3821,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb head_len = (limit - marker_reserve) // 2 return elide_middle(record, head_len, limit - marker_reserve - head_len) - def _record_summary_input_coverage(self, coverage: Dict[str, int]) -> None: + def _record_summary_input_coverage(self, coverage: dict[str, int]) -> None: """Expose lean sampling coverage without including transcript content in telemetry.""" telemetry = getattr(self, "_active_compression_telemetry", None) if not isinstance(telemetry, dict): @@ -3836,7 +3836,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb }) @classmethod - def _sample_summary_records(cls, records: Sequence[str]) -> Tuple[str, Dict[str, int]]: + def _sample_summary_records(cls, records: Sequence[str]) -> tuple[str, dict[str, int]]: """Sample complete serialized records while retaining the character bound. Returns the bounded transcript and record-level coverage counters for compression @@ -3847,7 +3847,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb """ input_chars = sum(len(r) for r in records) - def _coverage(sampled_chars: int, sampled_record_count: int) -> Dict[str, int]: + def _coverage(sampled_chars: int, sampled_record_count: int) -> dict[str, int]: return { "input_chars": input_chars, "sampled_chars": sampled_chars, "omitted_chars": input_chars - sampled_chars, "record_count": len(records), @@ -3987,8 +3987,8 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb Raises RuntimeError for empty content or a length-truncated (PARTIAL) summary so the failure routes through main-model fallback + cooldown instead of wiping the compacted turns.""" # call_llm writes the route it actually selected; never pre-resolve a second, stale pair. - _aux_route: Dict[str, str] = {} - call_kwargs: Dict[str, Any] = { + _aux_route: dict[str, str] = {} + call_kwargs: dict[str, Any] = { "task": "compression", "main_runtime": { "model": self.model, "provider": self.provider, "base_url": self.base_url, "api_key": self.api_key, @@ -4005,7 +4005,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb # static marker, losing the real handoff (#23975). Re-entrant: a main-model retry (_generate_summary # recursion) re-enters harmlessly. _aux_call_start = time.monotonic() - _latency_info: Dict[str, int] = {"prompt_build_ms": max(0, int((_aux_call_start - prompt_started_at) * 1000))} + _latency_info: dict[str, int] = {"prompt_build_ms": max(0, int((_aux_call_start - prompt_started_at) * 1000))} call_kwargs["latency_info"] = _latency_info # Per-attempt observable (#114594): with this line a stalled attempt is distinguishable from a slow # one — silence before it is prompt build, silence after it is the summary provider. @@ -4070,7 +4070,7 @@ Summary generation was unavailable, so this is a best-effort deterministic fallb return content def _generate_summary( - self, turns_to_summarize: List[Dict[str, Any]], focus_topic: Optional[str] = None, + self, turns_to_summarize: list[dict[str, Any]], focus_topic: Optional[str] = None, memory_context: str = "", bypass_cooldown: bool = False, ) -> Optional[str]: """Structured summary of the turns (iterative update when a previous summary exists); None if all attempts fail.""" @@ -4213,7 +4213,7 @@ This compaction should PRIORITISE preserving all information related to the focu return "" @classmethod - def _summary_template_sections(cls, _section: Dict[str, str], summary_budget: int, _session_log_section: str) -> str: + def _summary_template_sections(cls, _section: dict[str, str], summary_budget: int, _session_log_section: str) -> str: """The ``## ...`` section template shared by the fresh and iterative-update prompts.""" _temporal_anchoring_rule = cls._temporal_anchoring_rule() return f"""{HISTORICAL_TASK_HEADING} @@ -4273,7 +4273,7 @@ Target ~{summary_budget + (_LEAN_SESSION_LOG_BUDGET_TOKENS if _session_log_secti Write only the summary body. Do not include any preamble or prefix.""" def _on_summary_failure( - self, e: Exception, turns_to_summarize: List[Dict[str, Any]], focus_topic: Optional[str], memory_context: str, + self, e: Exception, turns_to_summarize: list[dict[str, Any]], focus_topic: Optional[str], memory_context: str, ) -> Optional[str]: """Classify a summary-call failure; retry once on the main model (returning its result) or arm a cooldown (None).""" # A detached stale attempt must not arm a failure cooldown or stamp error state the fallback @@ -4419,7 +4419,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return isinstance(message, dict) and bool(message.get(COMPRESSED_SUMMARY_METADATA_KEY)) @classmethod - def _transcript_has_real_user_turn(cls, messages: List[Dict[str, Any]]) -> bool: + def _transcript_has_real_user_turn(cls, messages: list[dict[str, Any]]) -> bool: """Return whether *messages* contain a user-authored (not synthetic summary) turn.""" return any( isinstance(m, dict) and m.get("role") == "user" and not cls._is_synthetic_compression_user_turn(m) @@ -4508,7 +4508,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return not cls._is_blank_user_turn(message) @classmethod - def _blank_echo_indices_after(cls, messages: List[Dict[str, Any]], user_idx: int) -> set[int]: + def _blank_echo_indices_after(cls, messages: list[dict[str, Any]], user_idx: int) -> set[int]: """Return contiguous blank echoes after a user event; removable only if an assistant follows.""" if user_idx < 0: return set() @@ -4520,7 +4520,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return set(range(user_idx + 1, idx)) @classmethod - def _derive_auto_focus_topic(cls, messages: List[Dict[str, Any]]) -> Optional[str]: + def _derive_auto_focus_topic(cls, messages: list[dict[str, Any]]) -> Optional[str]: """Infer a compact focus hint from the most recent real user turns.""" candidates: list[str] = [] for msg in reversed(messages): @@ -4545,7 +4545,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return focus @classmethod - def _latest_user_task_snapshot(cls, messages: List[Dict[str, Any]]) -> Optional[str]: + def _latest_user_task_snapshot(cls, messages: list[dict[str, Any]]) -> Optional[str]: """Return a deterministic task-snapshot line from the newest real user turn. The summarizer must not invent the active-task anchor from a prompt example or a stale prior summary; this grounds it in the exact compacted turns.""" @@ -4573,7 +4573,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return None @classmethod - def _ground_historical_task_snapshot(cls, summary: str, messages: List[Dict[str, Any]]) -> str: + def _ground_historical_task_snapshot(cls, summary: str, messages: list[dict[str, Any]]) -> str: """Force the task snapshot section to match a real user turn when possible.""" snapshot = cls._latest_user_task_snapshot(messages) if not snapshot: @@ -4599,7 +4599,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return f"{replacement}{body}".strip() @classmethod - def _find_context_summaries(cls, messages: List[Dict[str, Any]], start: int, end: int) -> list[tuple[int, str]]: + def _find_context_summaries(cls, messages: list[dict[str, Any]], start: int, end: int) -> list[tuple[int, str]]: """Find handoff summaries inside a compression window.""" n = len(messages) # Clamp: callers may pass end = len(messages)+1. @@ -4614,14 +4614,14 @@ Write only the summary body. Do not include any preamble or prefix.""" @classmethod def _find_latest_context_summary( - cls, messages: List[Dict[str, Any]], start: int, end: int, + cls, messages: list[dict[str, Any]], start: int, end: int, ) -> tuple[Optional[int], str]: """Find the newest handoff summary inside a compression window.""" summaries = cls._find_context_summaries(messages, start, end) return summaries[-1] if summaries else (None, "") @classmethod - def _strip_context_summary_handoff_message(cls, message: Dict[str, Any]) -> Optional[Dict[str, Any]]: + def _strip_context_summary_handoff_message(cls, message: dict[str, Any]) -> Optional[dict[str, Any]]: """Drop stale handoff data while preserving merged prior-tail content. Returns a copy for non-handoff rows, the unwrapped prior-tail content for merged handoffs (delimiter form, or legacy end-marker form), and ``None`` for standalone ones.""" @@ -4631,7 +4631,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return message.copy() content = message.get("content") - def _unwrapped(new_content: Any) -> Dict[str, Any]: + def _unwrapped(new_content: Any) -> dict[str, Any]: unwrapped = {**message, "content": new_content} unwrapped.pop(COMPRESSED_SUMMARY_METADATA_KEY, None) return unwrapped @@ -4698,7 +4698,7 @@ Write only the summary body. Do not include any preamble or prefix.""" from agent.message_sanitization import tool_call_id_variants return set(tool_call_id_variants(tc)) - def _sanitize_tool_pairs(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _sanitize_tool_pairs(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Remove orphaned tool results and strip orphaned tool_calls after compression. Stubs would be dropped by repair_message_sequence when call_id != id.""" from agent.agent_runtime_helpers import _classify_tool_call_orphans @@ -4738,20 +4738,20 @@ Write only the summary body. Do not include any preamble or prefix.""" logger.info("Compression sanitizer: stripped %d orphaned tool_call(s) from assistant messages", stripped_count) return messages - def _align_boundary_forward(self, messages: List[Dict[str, Any]], idx: int) -> int: + def _align_boundary_forward(self, messages: list[dict[str, Any]], idx: int) -> int: """Push a compress-start boundary forward past any orphan tool results.""" while idx < len(messages) and messages[idx].get("role") == "tool": idx += 1 return idx - def _restart_handoff_probe_bounds(self, messages: List[Dict[str, Any]]) -> tuple[int, int]: + def _restart_handoff_probe_bounds(self, messages: list[dict[str, Any]]) -> tuple[int, int]: """Return the bounded transcript region that can indicate restart decay.""" if not messages or self.protect_first_n <= 0: return 0, 0 first_non_system = 1 if messages[0].get("role") == "system" else 0 return first_non_system, min(len(messages), first_non_system + self.protect_first_n + _RESTART_HANDOFF_PROBE_EXTRA_MESSAGES) - def _effective_protect_first_n(self, messages: Optional[List[Dict[str, Any]]] = None) -> int: + def _effective_protect_first_n(self, messages: Optional[list[dict[str, Any]]] = None) -> int: """``protect_first_n``, decayed to 0 once the session has been compressed so early turns don't fossilize. After a restart the decayed state is inferred from handoff summaries in the resumed head.""" if self.compression_count >= 1 or self._previous_summary: @@ -4763,7 +4763,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return 0 return self.protect_first_n - def _protect_head_size(self, messages: List[Dict[str, Any]]) -> int: + def _protect_head_size(self, messages: list[dict[str, Any]]) -> int: """Head messages to protect: the system prompt (if present) plus the decaying ``protect_first_n`` extra rows. The ``protect_first_n`` portion DECAYS after the first compression (see _effective_protect_first_n) @@ -4772,7 +4772,7 @@ Write only the summary body. Do not include any preamble or prefix.""" head = 1 if messages and messages[0].get("role") == "system" else 0 return head + self._effective_protect_first_n(messages) - def _align_boundary_backward(self, messages: List[Dict[str, Any]], idx: int) -> int: + def _align_boundary_backward(self, messages: list[dict[str, Any]], idx: int) -> int: """Pull a compress-end boundary back so a tool group is not split (orphaned tail results would be dropped).""" if idx <= 0 or idx >= len(messages): return idx @@ -4783,7 +4783,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return idx @classmethod - def _is_real_user_turn(cls, message: Dict[str, Any]) -> bool: + def _is_real_user_turn(cls, message: dict[str, Any]) -> bool: """Actionable user turn that is not synthetic scaffolding — the row test both index scans share. Weaker than ``agent.conversation_compression._is_real_user_message``, which also rejects @@ -4793,14 +4793,14 @@ Write only the summary body. Do not include any preamble or prefix.""" return cls._is_actionable_user_turn(message) and not cls._is_synthetic_compression_user_turn(message) @classmethod - def _real_user_indices_desc(cls, messages: List[Dict[str, Any]], head_end: int) -> list[int]: + def _real_user_indices_desc(cls, messages: list[dict[str, Any]], head_end: int) -> list[int]: """Newest-first indices of actionable, non-synthetic user turns at or after *head_end* (no handoffs/blank echoes).""" return [ i for i in range(len(messages) - 1, head_end - 1, -1) if cls._is_real_user_turn(messages[i]) ] - def _find_last_user_message_idx(self, messages: List[Dict[str, Any]], head_end: int) -> int: + def _find_last_user_message_idx(self, messages: list[dict[str, Any]], head_end: int) -> int: """Return the latest actionable user turn at or after *head_end*, or -1.""" # Early-exit generator: callers want the newest hit only, and collecting every index # (``_real_user_indices_desc``) costs a full backward scan per call. @@ -4809,7 +4809,7 @@ Write only the summary body. Do not include any preamble or prefix.""" -1, ) - def _find_last_assistant_message_idx(self, messages: List[Dict[str, Any]], head_end: int) -> int: + def _find_last_assistant_message_idx(self, messages: list[dict[str, Any]], head_end: int) -> int: """Last text-bearing non-summary assistant reply at/after *head_end* (else last non-summary assistant), or -1.""" last_any = -1 for i in range(len(messages) - 1, head_end - 1, -1): @@ -4828,7 +4828,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return last_any def _ensure_last_assistant_message_in_tail( - self, messages: List[Dict[str, Any]], cut_idx: int, head_end: int, + self, messages: list[dict[str, Any]], cut_idx: int, head_end: int, ) -> int: """Keep the most recent assistant reply in the protected tail, re-aligned back so a tool group is not split.""" last_asst_idx = self._find_last_assistant_message_idx(messages, head_end) @@ -4843,7 +4843,7 @@ Write only the summary body. Do not include any preamble or prefix.""" ) return max(new_cut, head_end + 1) - def _ensure_last_user_message_in_tail(self, messages: List[Dict[str, Any]], cut_idx: int, head_end: int) -> int: + def _ensure_last_user_message_in_tail(self, messages: list[dict[str, Any]], cut_idx: int, head_end: int) -> int: """Guarantee the most recent user message is in the protected tail. Tool-group alignment can pull the cut past the last user message; once summarized, the prefix tells the model to answer only messages AFTER the summary, so the active ask silently vanishes. @@ -4895,8 +4895,8 @@ Write only the summary body. Do not include any preamble or prefix.""" @classmethod def _find_inflight_user_task( - cls, messages: List[Dict[str, Any]] - ) -> Optional[Dict[str, Any]]: + cls, messages: list[dict[str, Any]] + ) -> Optional[dict[str, Any]]: """Return the user turn that is still awaiting completion, or ``None``. Scans the WHOLE transcript, not just the compressible region: a cron @@ -4959,9 +4959,9 @@ Write only the summary body. Do not include any preamble or prefix.""" def _reappend_inflight_user_task( self, - compressed: List[Dict[str, Any]], - inflight: Optional[Dict[str, Any]], - ) -> List[Dict[str, Any]]: + compressed: list[dict[str, Any]], + inflight: Optional[dict[str, Any]], + ) -> list[dict[str, Any]]: """Restate an unfinished user task after the compaction handoff. ``SUMMARY_PREFIX`` instructs the model to act only on a user message @@ -5102,7 +5102,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return compressed def _ensure_last_n_user_messages_in_tail( - self, messages: List[Dict[str, Any]], cut_idx: int, head_end: int, n: int, + self, messages: list[dict[str, Any]], cut_idx: int, head_end: int, n: int, ) -> int: """Keep the last N actionable user messages in the tail; n <= 1 delegates to the single-message method. @@ -5124,7 +5124,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return cut_idx return max(user_indices[min(n, len(user_indices)) - 1], head_end + 1) - def _find_turn_pair_end(self, messages: List[Dict[str, Any]], user_idx: int) -> int: + def _find_turn_pair_end(self, messages: list[dict[str, Any]], user_idx: int) -> int: """Index after the turn-pair (user -> assistant -> tools) at *user_idx*; ``user_idx + 1`` when no reply yet.""" idx = user_idx + 1 if idx >= len(messages) or messages[idx].get("role") != "assistant": @@ -5156,7 +5156,7 @@ Write only the summary body. Do not include any preamble or prefix.""" rejected = session_rejected_thinking(self, getattr(self, "_session_db", None), getattr(self, "_session_id", "")) - def budget(msg: Dict[str, Any]) -> int: + def budget(msg: dict[str, Any]) -> int: request_copy = dict(msg) if rejected: strip_rejected_thinking(request_copy, rejected) @@ -5166,7 +5166,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return budget def _find_tail_cut_by_tokens( - self, messages: List[Dict[str, Any]], head_end: int, token_budget: int | None = None, + self, messages: list[dict[str, Any]], head_end: int, token_budget: int | None = None, *, allow_split_turn: bool = True, ) -> int: """Walk backward accumulating tokens until the budget; return the tail start index. @@ -5289,15 +5289,15 @@ Write only the summary body. Do not include any preamble or prefix.""" # can't split a tool group (backward would give the floor's message back). return min(n, self._align_boundary_forward(messages, max(cut_idx, head_end + 1))) - def has_content_to_compress(self, messages: List[Dict[str, Any]]) -> bool: + def has_content_to_compress(self, messages: list[dict[str, Any]]) -> bool: """True if a non-empty middle region exists (lets the gateway ``/compress`` guard skip the LLM call).""" compress_start = self._align_boundary_forward(messages, self._protect_head_size(messages)) compress_end = self._find_tail_cut_by_tokens(messages, compress_start) return compress_start < compress_end def _scan_window_handoffs( - self, messages: List[Dict[str, Any]], compress_start: int, compress_end: int, - turns_to_summarize: List[Dict[str, Any]], + self, messages: list[dict[str, Any]], compress_start: int, compress_end: int, + turns_to_summarize: list[dict[str, Any]], ) -> "_HandoffScan": """Rehydrate ``_previous_summary`` / user-turn provenance from in-transcript handoffs. Handoff rows are removed from the summarizer window (merged handoffs unwrap to their prior-tail @@ -5339,7 +5339,7 @@ Write only the summary body. Do not include any preamble or prefix.""" # Summary rows are excluded from summarizer input, but a merged handoff carries genuine # prior-tail user content — unwrap it into the window (#47274); standalone ones drop (None). # The newest hit (summary_idx) may itself be a merged handoff — recover its prior tail too. - def _window_row(idx: int, msg: Dict[str, Any]): + def _window_row(idx: int, msg: dict[str, Any]): if idx not in scan.summary_indices: return msg return self._strip_context_summary_handoff_message(_fresh_compaction_message_copy(msg)) @@ -5351,7 +5351,7 @@ Write only the summary body. Do not include any preamble or prefix.""" scan.tail_start = summary_idx + 1 return scan - def _begin_compress_attempt(self, current_tokens: Optional[int], force: bool) -> Dict[str, Any]: + def _begin_compress_attempt(self, current_tokens: Optional[int], force: bool) -> dict[str, Any]: """Reset per-call result state (callers read it after compress()) and open telemetry.""" self._last_summary_dropped_count = 0 self._last_summary_fallback_used = False @@ -5373,18 +5373,18 @@ Write only the summary body. Do not include any preamble or prefix.""" self._structural_no_op_backoff_until = 0.0 return telemetry - def _structural_no_op_result(self, telemetry: Dict[str, Any], failure_class: str, reason: str) -> None: + def _structural_no_op_result(self, telemetry: dict[str, Any], failure_class: str, reason: str) -> None: """Nothing eligible to compress: transient backoff (#93022), never an ineffectiveness strike.""" telemetry["failure_class"] = failure_class self._last_compression_savings_pct = 0.0 self._record_structural_no_op(reason) - def _drop_blank_echoes(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _drop_blank_echoes(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: """Remove blank platform echoes trailing the latest actionable user turn.""" blank = self._blank_echo_indices_after(messages, self._find_last_user_message_idx(messages, 0)) return [m for idx, m in enumerate(messages) if idx not in blank] if blank else messages - def _compress_window(self, messages: List[Dict[str, Any]]) -> tuple[int, int]: + def _compress_window(self, messages: list[dict[str, Any]]) -> tuple[int, int]: """Return ``(compress_start, compress_end)`` for the summarizable middle.""" compress_start = self._align_boundary_forward(messages, self._protect_head_size(messages)) compress_end = self._find_tail_cut_by_tokens(messages, compress_start) @@ -5418,7 +5418,7 @@ Write only the summary body. Do not include any preamble or prefix.""" ) def _abort_on_summary_failure( - self, telemetry: Dict[str, Any], n_skipped: int, previous_summary_before_scan: Optional[str], + self, telemetry: dict[str, Any], n_skipped: int, previous_summary_before_scan: Optional[str], ) -> bool: """Abort (messages unchanged) on a terminal failure or when configured to; True when aborted. Access/quota, network, truncated and empty-content failures ALWAYS abort (#29559); an overload @@ -5452,7 +5452,7 @@ Write only the summary body. Do not include any preamble or prefix.""" _COMPRESSION_NOTE = "[Note: Some earlier conversation turns have been compacted into a handoff summary to preserve context space. The current session state may still reflect earlier work, so build on that summary and state rather than re-doing work. Your persistent memory (MEMORY.md, USER.md) remains fully authoritative regardless of compaction.]" - def _assemble_head(self, messages: List[Dict[str, Any]], compress_start: int) -> List[Dict[str, Any]]: + def _assemble_head(self, messages: list[dict[str, Any]], compress_start: int) -> list[dict[str, Any]]: """Protected head with the compaction note on the system prompt and stale handoffs stripped.""" compressed = [] for i in range(compress_start): @@ -5470,7 +5470,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return compressed def _fallback_summary_for_window( - self, telemetry: Dict[str, Any], turns_to_summarize: List[Dict[str, Any]], + self, telemetry: dict[str, Any], turns_to_summarize: list[dict[str, Any]], n_dropped: int, feasibility_skip: bool, ) -> str: """Deterministic fallback so the model gets recoverable continuity anchors.""" @@ -5498,10 +5498,10 @@ Write only the summary body. Do not include any preamble or prefix.""" return summary def _assemble_tail( - self, messages: List[Dict[str, Any]], compress_end: int, tail_start: int, summary_indices: set, - ) -> List[Dict[str, Any]]: + self, messages: list[dict[str, Any]], compress_end: int, tail_start: int, summary_indices: set, + ) -> list[dict[str, Any]]: """Protected tail with already-folded handoff rows dropped and merged handoffs unwrapped.""" - tail_messages: List[Dict[str, Any]] = [] + tail_messages: list[dict[str, Any]] = [] # Start at tail_start, not compress_end: the rehydration scan may have advanced it (#57835). for i in range(max(compress_end, tail_start), len(messages)): if i in summary_indices and i >= tail_start: @@ -5513,7 +5513,7 @@ Write only the summary body. Do not include any preamble or prefix.""" @staticmethod def _summary_placement( - compressed: List[Dict[str, Any]], tail_messages: List[Dict[str, Any]], compress_start: int, + compressed: list[dict[str, Any]], tail_messages: list[dict[str, Any]], compress_start: int, ) -> tuple[str, bool, bool, Optional[int]]: """Pick the summary row's role so template-visible alternation holds. Returns ``(summary_role, merge_into_tail, force_user_leading, first_tail_visible_idx)``. Roles @@ -5548,7 +5548,7 @@ Write only the summary body. Do not include any preamble or prefix.""" return summary_role, merge_into_tail, force_user_leading, first_tail_visible_idx def _merge_summary_into_tail_row( - self, msg: Dict[str, Any], summary: str, summary_role: str, force_user_leading: bool, + self, msg: dict[str, Any], summary: str, summary_role: str, force_user_leading: bool, ) -> None: """Fold the summary into a carried tail row (in place) when no standalone role alternates.""" old_content = msg.get("content", "") @@ -5569,9 +5569,9 @@ Write only the summary body. Do not include any preamble or prefix.""" drop_stale_api_content(msg) def _finalize_compressed( - self, compressed: List[Dict[str, Any]], messages: List[Dict[str, Any]], n_messages: int, + self, compressed: list[dict[str, Any]], messages: list[dict[str, Any]], n_messages: int, spare_pending_images: bool, - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """Post-assembly cleanup: orphan pairs, media, savings, markers, replay prune, mem trim.""" # Single-prompt cron shape: the only live instruction sits in the protected head, BEFORE the # handoff, and SUMMARY_PREFIX reads that as "nothing to do" — restate it past the boundary @@ -5628,9 +5628,9 @@ Write only the summary body. Do not include any preamble or prefix.""" return compressed def compress( - self, messages: List[Dict[str, Any]], current_tokens: Optional[int] = None, focus_topic: Optional[str] = None, + self, messages: list[dict[str, Any]], current_tokens: Optional[int] = None, focus_topic: Optional[str] = None, force: bool = False, memory_context: str = "", bypass_cooldown: bool = False, - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """Summarize the middle turns: prune tool-result bodies (tool-call args untouched), protect head and a token-budget tail from the pruned copy, summarize, then clean orphaned tool pairs. ``force`` clears the failure cooldown and bypasses the feasibility skip; ``bypass_cooldown`` runs the summary LLM without clearing the cooldown. @@ -5736,8 +5736,8 @@ Write only the summary body. Do not include any preamble or prefix.""" return self._finalize_compressed(compressed, canonical_messages, n_messages, spare_pending_images) def _assemble_compressed( - self, messages: List[Dict[str, Any]], compress_start: int, compress_end: int, scan: "_HandoffScan", summary: str, - ) -> List[Dict[str, Any]]: + self, messages: list[dict[str, Any]], compress_start: int, compress_end: int, scan: "_HandoffScan", summary: str, + ) -> list[dict[str, Any]]: """Head + summary + tail from the pruned copy: its tool-result demotions are what let an oversized head/tail compress at all (#61932); tool-call arguments are never rewritten by pruning.""" compressed = self._assemble_head(messages, compress_start) @@ -5814,13 +5814,13 @@ def _handoff_only_content(content: Any) -> Any: return projected -def split_user_originated_turn(message: Any) -> tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]: +def split_user_originated_turn(message: Any) -> tuple[Optional[dict[str, Any]], Optional[dict[str, Any]]]: """Split a user row into ``(handoff_only, live_view)``; either may be None; fresh dicts.""" if not isinstance(message, dict) or message.get("role") != "user": return None, None is_summary = is_compaction_summary_message(message) - handoff: Optional[Dict[str, Any]] = None + handoff: Optional[dict[str, Any]] = None if is_summary: handoff = { "role": "user", "content": _handoff_only_content(message.get("content")), @@ -5863,14 +5863,14 @@ def split_user_originated_turn(message: Any) -> tuple[Optional[Dict[str, Any]], return handoff, candidate -def user_originated_turn_view(message: Any) -> Optional[Dict[str, Any]]: +def user_originated_turn_view(message: Any) -> Optional[dict[str, Any]]: """Return the live human-authored projection of a user row, if any.""" return split_user_originated_turn(message)[1] def history_before_user_originated_turn( - messages: List[Dict[str, Any]], index: int, -) -> tuple[List[Dict[str, Any]], Dict[str, Any]]: + messages: list[dict[str, Any]], index: int, +) -> tuple[list[dict[str, Any]], dict[str, Any]]: """Rewind prefix and canonical live view for ``index``; a composite carrier keeps its handoff scaffold at the head.""" if index < 0 or index >= len(messages): raise IndexError("user turn index is outside the transcript") @@ -5910,7 +5910,7 @@ def _handoff_carries_live_user_content(message: Any) -> bool: return isinstance(message, dict) and ContextCompressor._strip_context_summary_handoff_message(message) is not None -def reference_handoff_would_drive_next_model_call(messages: Optional[List[Dict[str, Any]]]) -> bool: +def reference_handoff_would_drive_next_model_call(messages: Optional[list[dict[str, Any]]]) -> bool: """True when the next model call would be driven only by a handoff; trailing tool rows mean an in-flight exchange.""" if not messages: return False diff --git a/agent/context_compressor_prellm.py b/agent/context_compressor_prellm.py index 3822971b1c..5aef44c143 100644 --- a/agent/context_compressor_prellm.py +++ b/agent/context_compressor_prellm.py @@ -36,7 +36,7 @@ class PreLlmSkipMixin: float, 0.0, _FALLBACK_PROBE_AT_MODEL_CONFIG_KEY, 0.0, ) - def _fallback_streak_skip(self, telemetry: Dict[str, Any]) -> bool: + def _fallback_streak_skip(self, telemetry: dict[str, Any]) -> bool: """Pre-LLM skip while two summaries in a row fell back: compact deterministically instead of paying for a summary model that keeps failing (#63008). One probe per recovery window; a healthy summary resets the streak, another fallback benches it again.""" @@ -66,7 +66,7 @@ class PreLlmSkipMixin: return True def _feasibility_skip( - self, telemetry: Dict[str, Any], turns_to_summarize: List[Dict[str, Any]], + self, telemetry: dict[str, Any], turns_to_summarize: list[dict[str, Any]], compress_start: int, compress_end: int, ) -> bool: """Pre-LLM skip after a real-usage ineffectiveness strike (reads the counter, never writes).""" diff --git a/agent/context_compressor_summary.py b/agent/context_compressor_summary.py index 0c37ae91a6..decf4aeab0 100644 --- a/agent/context_compressor_summary.py +++ b/agent/context_compressor_summary.py @@ -27,7 +27,7 @@ def _accepts_keyword_argument(callable_obj: Any, name: str) -> bool: class SummaryDispatchMixin: - def _apply_summary_route(self, call_kwargs: dict, pinned: Optional[Dict[str, Any]] = None) -> None: + def _apply_summary_route(self, call_kwargs: dict, pinned: Optional[dict[str, Any]] = None) -> None: """Pin the summary route onto ``call_kwargs`` for both compression entry points. After a fallback, an omitted route is NOT "use the main model": ``call_llm`` re-resolves @@ -58,7 +58,7 @@ class SummaryDispatchMixin: call_kwargs[key] = value def _summarize_window( - self, messages: List[Dict[str, Any]], turns_to_summarize: List[Dict[str, Any]], scan: "_HandoffScan", + self, messages: list[dict[str, Any]], turns_to_summarize: list[dict[str, Any]], scan: "_HandoffScan", focus_topic: Optional[str], memory_context: str, bypass_cooldown: bool, ) -> Optional[str]: """Run the summary LLM; a cancellation rolls back the handoff scan's self-heal mutation first. @@ -80,7 +80,7 @@ class SummaryDispatchMixin: telemetry["failure_class"] = "stall_deterministic_fallback" return None # Focus-topic derivation scans user turns; only pay when a summary is generated. - summary_kwargs: Dict[str, Any] = { + summary_kwargs: dict[str, Any] = { "focus_topic": focus_topic or self._derive_auto_focus_topic(messages), "memory_context": memory_context, } diff --git a/agent/context_engine.py b/agent/context_engine.py index 0b826024a5..b8789e030d 100644 --- a/agent/context_engine.py +++ b/agent/context_engine.py @@ -73,7 +73,7 @@ class ContextEngine(ABC): emit_automatic_compaction_status: bool = True @abstractmethod - def update_from_response(self, usage: Dict[str, Any]) -> None: + def update_from_response(self, usage: dict[str, Any]) -> None: """Update tracked token usage after every LLM call. ``prompt_tokens``/``completion_tokens``/``total_tokens`` are always present; the @@ -96,9 +96,9 @@ class ContextEngine(ABC): @abstractmethod def compress( - self, messages: List[Dict[str, Any]], current_tokens: Optional[int] = None, + self, messages: list[dict[str, Any]], current_tokens: Optional[int] = None, focus_topic: Optional[str] = None, force: bool = False, memory_context: str = "", - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """Compact ``messages`` into a valid OpenAI-format list that fits the budget. ``focus_topic`` comes from manual ``/compress `` (prioritise that topic); @@ -108,8 +108,8 @@ class ContextEngine(ABC): """ def prune_tool_results_only( - self, messages: List[Dict[str, Any]], current_tokens: int | None = None, - ) -> tuple[List[Dict[str, Any]], int]: + self, messages: list[dict[str, Any]], current_tokens: int | None = None, + ) -> tuple[list[dict[str, Any]], int]: """Deterministically trim old tool-result payloads without an LLM call. Runs on a low, cost-oriented trigger independent of ``should_compress`` so @@ -119,9 +119,9 @@ class ContextEngine(ABC): return messages, 0 def select_context( - self, request_messages: List[Dict[str, Any]], *, conversation_messages: List[Dict[str, Any]] = None, - incoming_message: Dict[str, Any] = None, budget_tokens: int = 0, - ) -> List[Dict[str, Any]]: + self, request_messages: list[dict[str, Any]], *, conversation_messages: list[dict[str, Any]] = None, + incoming_message: dict[str, Any] = None, budget_tokens: int = 0, + ) -> list[dict[str, Any]]: """Optionally *select* (replace) the context for THIS request, pre-generation. Runs on every provider request (also retries), independent of @@ -140,7 +140,7 @@ class ContextEngine(ABC): """ return None - def on_turn_complete(self, messages: List[Dict[str, Any]], usage: Dict[str, Any] = None, **kwargs: Any) -> None: + def on_turn_complete(self, messages: list[dict[str, Any]], usage: dict[str, Any] = None, **kwargs: Any) -> None: """Observe a finished turn (complement of ``select_context()``) to index/update routing state for the next request. @@ -151,9 +151,9 @@ class ContextEngine(ABC): ``None`` when no provider response was reached (interrupt). ``kwargs`` may include ``turn_id``, ``task_id``, ``api_call_count``, ``interrupted``, ``failed``, ``turn_exit_reason``. """ - return None + return - def should_compress_preflight(self, messages: List[Dict[str, Any]]) -> bool: + def should_compress_preflight(self, messages: list[dict[str, Any]]) -> bool: """Cheap rough check before the API call (no real token count yet); default skips.""" return False @@ -173,7 +173,7 @@ class ContextEngine(ABC): """ return default_message if self.emit_automatic_compaction_status else None - def has_content_to_compress(self, messages: List[Dict[str, Any]]) -> bool: + def has_content_to_compress(self, messages: list[dict[str, Any]]) -> bool: """Preflight guard for gateway ``/compress``: False reports "nothing to compress yet" without an LLM call (e.g. transcript entirely protected).""" return True @@ -181,7 +181,7 @@ class ContextEngine(ABC): def on_session_start(self, session_id: str, **kwargs) -> None: """Session begins: load persisted state. kwargs may include hermes_home, platform, model.""" - def on_session_end(self, session_id: str, messages: List[Dict[str, Any]]) -> None: + def on_session_end(self, session_id: str, messages: list[dict[str, Any]]) -> None: """Real session boundary (CLI exit, /reset, gateway expiry) — never per-turn.""" def on_session_reset(self) -> None: @@ -201,16 +201,16 @@ class ContextEngine(ABC): self.last_total_tokens = 0 self.compression_count = 0 - def get_tool_schemas(self) -> List[Dict[str, Any]]: + def get_tool_schemas(self) -> list[dict[str, Any]]: """Tool schemas this engine exposes to the agent (default: none).""" return [] - def handle_tool_call(self, name: str, args: Dict[str, Any], **kwargs) -> str: + def handle_tool_call(self, name: str, args: dict[str, Any], **kwargs) -> str: """Handle a call to one of this engine's tools; must return a JSON string. kwargs may include ``messages`` (live in-memory list).""" return json.dumps({"error": f"Unknown context engine tool: {name}"}) - def get_status(self) -> Dict[str, Any]: + def get_status(self) -> dict[str, Any]: """Status dict with the standard fields run_agent.py expects.""" # Clamp the -1 "compression just ran, awaiting real usage" sentinel to 0 so no # reader sees a negative usage_percent on the transitional turn. diff --git a/agent/context_file_sources.py b/agent/context_file_sources.py index 1b784652c6..db0c77e5f5 100644 --- a/agent/context_file_sources.py +++ b/agent/context_file_sources.py @@ -34,7 +34,7 @@ _STATUS_DISPLAY = { } -def _entry(label: str, path: Path, content: str, status: str) -> Dict[str, Any]: +def _entry(label: str, path: Path, content: str, status: str) -> dict[str, Any]: return { "label": label, "path": str(path), "chars": len(content), "est_tokens": estimate_tokens_rough(content), "loaded": status in ("loaded", "truncated", "flagged"), "status": status, @@ -60,7 +60,7 @@ def _loaded_status(content: str, rendered_len: int, max_chars: int, user_authore def list_context_file_sources( cwd: Optional[str] = None, context_length: Optional[int] = None, allow_install_tree_fallback: bool = False, home_override: "Path | None" = None, skip_soul: bool = False, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: """One dict per context file Hermes considered, in the builder's priority order. Same signature semantics as ``build_context_files_prompt`` (``cwd=None`` → launch dir, install-tree guard @@ -70,7 +70,7 @@ def list_context_file_sources( cwd_path = Path(cwd if cwd is not None else os.getcwd()).resolve() max_chars = _pb._get_context_file_max_chars(context_length) suppressed = _pb._project_context_suppressed(cwd, cwd_path, allow_install_tree_fallback) - sources: List[Dict[str, Any]] = [] + sources: list[dict[str, Any]] = [] winner: Optional[str] = None for kind, label, path, content in _pb.discover_context_files(cwd_path): if not content: @@ -96,7 +96,7 @@ def list_context_file_sources( return sources -def context_file_sources_for_agent(agent: Any) -> List[Dict[str, Any]]: +def context_file_sources_for_agent(agent: Any) -> list[dict[str, Any]]: """The manifest for a live agent, resolved exactly like ``agent.system_prompt._context_files_part`` (session cwd, install-tree policy per platform, the agent's own profile home).""" if getattr(agent, "skip_context_files", False): @@ -112,7 +112,7 @@ def context_file_sources_for_agent(agent: Any) -> List[Dict[str, Any]]: ) -def render_context_file_lines(sources: List[Dict[str, Any]]) -> List[str]: +def render_context_file_lines(sources: list[dict[str, Any]]) -> list[str]: """Plain-text ``Context files`` block for ``/context``; [] when nothing was found.""" if not sources: return [] @@ -121,5 +121,5 @@ def render_context_file_lines(sources: List[Dict[str, Any]]) -> List[str]: for src in sources: glyph, note = _STATUS_DISPLAY.get(str(src.get("status") or ""), ("•", "")) suffix = f" ({note})" if note else "" - lines.append(f"{glyph} {str(src.get('label') or ''):<{width}} ~{int(src.get('est_tokens') or 0):>9,} tokens{suffix}") + lines.append(f"{glyph} {src.get('label') or ''!s:<{width}} ~{int(src.get('est_tokens') or 0):>9,} tokens{suffix}") return lines diff --git a/agent/context_references.py b/agent/context_references.py index 3256924d5e..88cacf39f0 100644 --- a/agent/context_references.py +++ b/agent/context_references.py @@ -33,7 +33,7 @@ _context_reference_providers: dict[str, "ContextReferenceProvider"] = {} class ContextCompletionItem: """A single autocomplete result from a context reference provider.""" - __slots__ = ("text", "display", "meta") + __slots__ = ("display", "meta", "text") def __init__(self, text: str, display: str = "", meta: str = "") -> None: self.text = text diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index 3f10382a30..10078eff36 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -761,7 +761,7 @@ def _get_compress_timeout_executor(): return _compress_timeout_executor -def resolve_context_compression_timeouts(compression_cfg: Optional[dict] = None) -> Tuple[float, float]: +def resolve_context_compression_timeouts(compression_cfg: Optional[dict] = None) -> tuple[float, float]: """Return ``(idle_timeout_seconds, total_ceiling_seconds)``. ``idle_timeout_seconds <= 0`` disables the progress-aware wrapper. The ceiling is clamped to at least one idle window when the idle budget is positive.""" @@ -792,8 +792,7 @@ def resolve_context_compression_timeouts(compression_cfg: Optional[dict] = None) from agent.auxiliary_client import _effective_aux_timeout _aux_budget = float(_effective_aux_timeout("compression", None)) if _aux_budget > 0: - if _aux_budget > ceiling: - ceiling = _aux_budget + ceiling = max(ceiling, _aux_budget) idle = max(idle, min(_aux_budget, ceiling)) return idle, ceiling @@ -938,12 +937,12 @@ def request_exceeds_model_window(agent: Any, request_tokens: Any) -> Optional[bo def _retry_compression_on_fallback_chain( - *, worker: Callable[[CompressionCommitFence], Tuple[list, str]], messages: list, + *, worker: Callable[[CompressionCommitFence], tuple[list, str]], messages: list, system_prompt_fallback: Any, idle_timeout_seconds: float, total_ceiling_seconds: float, on_commit_overrun: Optional[Callable[[float, float], None]] = None, on_timeout_cause: Optional[Callable[[bool, bool], None]] = None, telemetry_agent: Any = None, new_fence: Optional[Callable[[], CompressionCommitFence]] = None, escalate_deterministic: bool = False, -) -> Optional[Tuple[list, str]]: +) -> Optional[tuple[list, str]]: """Re-run an aborted compression with the summary route pinned: once on the configured chain entry, then — when ``escalate_deterministic`` (a stall backoff already burned one idle window this session, #112420) — once with the summary LLM skipped so compress() commits its deterministic fallback summary. @@ -978,11 +977,11 @@ def _retry_compression_on_fallback_chain( def _run_pinned_compression_retry( - route: dict, *, worker: Callable[[CompressionCommitFence], Tuple[list, str]], messages: list, + route: dict, *, worker: Callable[[CompressionCommitFence], tuple[list, str]], messages: list, system_prompt_fallback: Any, idle_timeout_seconds: float, total_ceiling_seconds: float, on_commit_overrun: Optional[Callable[[float, float], None]], on_timeout_cause: Optional[Callable[[bool, bool], None]], telemetry_agent: Any, new_fence: Optional[Callable[[], CompressionCommitFence]], -) -> Optional[Tuple[list, str]]: +) -> Optional[tuple[list, str]]: """One bounded re-run of ``worker`` with ``route`` pinned; ``None`` when it produced no compression.""" # The aborted fence refuses all commits; mint a fresh one via the host factory # so a /stop during the retry serializes against THIS attempt's commit boundary. @@ -1053,7 +1052,7 @@ def _run_pinned_compression_retry( def _await_worker_within_budget( future: Any, fence: CompressionCommitFence, *, idle: float, ceiling: float, wait_started: float -) -> Tuple[bool, Any]: +) -> tuple[bool, Any]: """Poll ``future`` under the idle budget + ceiling; ``(True, result)`` when it settled.""" while True: waited = time.monotonic() - wait_started @@ -1162,16 +1161,16 @@ def _release_cancelled_worker( def run_compress_context_with_progress_timeout( - *, worker: Callable[[CompressionCommitFence], Tuple[list, str]], messages: list, + *, worker: Callable[[CompressionCommitFence], tuple[list, str]], messages: list, system_prompt_fallback: Any, idle_timeout_seconds: float, total_ceiling_seconds: float, on_timeout: Optional[Callable[[float, float, float], None]] = None, on_timeout_cause: Optional[Callable[[bool, bool], None]] = None, on_commit_overrun: Optional[Callable[[float, float], None]] = None, fence: Optional[CompressionCommitFence] = None, telemetry_agent: Any = None, stall_fallback: bool = True, new_fence: Optional[Callable[[], CompressionCommitFence]] = None, - fallback_worker: Optional[Callable[[CompressionCommitFence], Tuple[list, str]]] = None, + fallback_worker: Optional[Callable[[CompressionCommitFence], tuple[list, str]]] = None, request_exceeds_window: bool = False, -) -> Tuple[list, str]: +) -> tuple[list, str]: """Run ``worker(fence)`` under a sync progress-aware (idle + ceiling) timeout. Budgets bound the PRE-commit phase only: an admitted commit always completes (overrun logged, surfaced once via ``on_commit_overrun``). A pre-commit cancel returns ``(messages, system_prompt_fallback)`` (lazy @@ -1498,7 +1497,7 @@ def compression_skipped_due_to_lock(agent: Any) -> bool: def _get_context_compression_timeout_state( agent: Any, *, create: bool -) -> Optional[Tuple[Any, Optional[threading.local]]]: +) -> Optional[tuple[Any, Optional[threading.local]]]: """Return the stable lock and thread-local timeout state for an agent.""" try: attributes = vars(agent) @@ -1621,7 +1620,7 @@ def _rebind_session_context(session_id: str) -> None: def _adopt_live_compression_child( agent: Any, session_db: Any, parent_session_id: str -) -> Optional[List[Dict[str, Any]]]: +) -> Optional[list[dict[str, Any]]]: """Move a stale compression contender onto the live continuation tip. Resolve and load first, then mutate the agent, so ambiguous lineage or an unreadable handoff fails closed. Uses the transitive ``get_compression_tip`` walk; a tip is adopted only while its row is still live. @@ -1700,7 +1699,7 @@ def _reopen_orphaned_parent(session_db: Any, session_id: str) -> None: logger.warning("orphaned compression session reopen failed for %s: %s", session_id, exc) -def recover_rotated_compression_session(agent: Any) -> Optional[List[Dict[str, Any]]]: +def recover_rotated_compression_session(agent: Any) -> Optional[list[dict[str, Any]]]: """Recover a stale live agent before a new turn writes to its old parent.""" session_db = getattr(agent, "_session_db", None) session_id = getattr(agent, "session_id", None) or "" @@ -2242,7 +2241,7 @@ _STEER_FALLBACK_OPEN = "[OUT-OF-BAND USER MESSAGE" _STEER_FALLBACK_CLOSE = "[/OUT-OF-BAND USER MESSAGE]" -def _steer_markers() -> Tuple[str, str]: +def _steer_markers() -> tuple[str, str]: """``(open, close)`` steer markers from prompt_builder, or the stable fallback literals.""" try: from agent.prompt_builder import STEER_MARKER_CLOSE, STEER_MARKER_OPEN @@ -2676,7 +2675,7 @@ class _CompressionLease: self.finish_lock_setup() -def _resolve_lock_api(lock_db: Any) -> Tuple[Any, Optional[Exception]]: +def _resolve_lock_api(lock_db: Any) -> tuple[Any, Optional[Exception]]: """Return ``(try_acquire_compression_lock, lookup_error)`` for ``lock_db``. ``(None, None)`` = no db or legacy SessionDB without the lock API (fail open); ``(None, exc)`` = lookup itself failed (caller fails closed).""" @@ -2696,7 +2695,7 @@ def _resolve_lock_api(lock_db: Any) -> Tuple[Any, Optional[Exception]]: def _abort_lease( agent: Any, lifecycle: _CompactionLifecycle, system_message: str, attempt_started_at: float, failure_class: str, prompt: Optional[str] = None, -) -> Tuple[None, str]: +) -> tuple[None, str]: """Sit-out return for lease acquisition: prompt, aborted telemetry, terminal status edge.""" if prompt is None: prompt = _existing_system_prompt(agent, system_message) @@ -2742,7 +2741,7 @@ def _try_acquire_durable_lock(lease: _CompressionLease, try_acquire: Any, commit def _sit_out_lock_contention( agent: Any, lease: _CompressionLease, lifecycle: _CompactionLifecycle, system_message: str, approx_tokens: Optional[int], attempt_started_at: float, -) -> Tuple[None, str]: +) -> tuple[None, str]: """Another path holds the lock: publish the lock-skip signal, warn once, sit out.""" existing = None with contextlib.suppress(Exception): @@ -2773,7 +2772,7 @@ def _sit_out_lock_contention( def _acquire_compression_lease( agent: Any, *, commit_fence: Optional[CompressionCommitFence], lifecycle: _CompactionLifecycle, system_message: str, approx_tokens: Optional[int], attempt_started_at: float, -) -> Tuple[Optional[_CompressionLease], Optional[str]]: +) -> tuple[Optional[_CompressionLease], Optional[str]]: """Take the per-session compression lock; ``(None, prompt)`` means sit out. Two AIAgents sharing a session_id (e.g. background review fork) would both rotate and orphan a child. Keyed on the OLD id (what rivals read from SessionEntry). Loser sits out: messages unchanged, caller sees @@ -2846,7 +2845,7 @@ def _acquire_compression_lease( def _adopt_if_parent_rotated( agent: Any, lease: _CompressionLease, messages: list, system_message: str -) -> Optional[Tuple[list, str]]: +) -> Optional[tuple[list, str]]: """Sit out (or adopt the live child) when the parent was already rotated. A late contender can take the parent lock after the winner released it and rotated; holding the lock does not prove this agent still owns a live parent. Returns the ``compress_context`` result to hand back, or @@ -2970,7 +2969,7 @@ def _pre_compress_memory_context(agent: Any, messages: list, checkpoint_required def _resolve_compress_call( agent: Any, *, approx_tokens: Optional[int], focus_topic: Optional[str], force: bool, memory_context: str, bypass_cooldown: bool, -) -> Tuple[Callable[..., Any], dict[str, Any]]: +) -> tuple[Callable[..., Any], dict[str, Any]]: """Bind ``compress()`` and only the kwargs its signature accepts.""" compress_fn = agent.context_compressor.compress compress_kwargs = _supported_compression_kwargs( @@ -3145,7 +3144,7 @@ def _rebuild_system_prompt_at_boundary(agent: Any, system_message: str) -> str: # object (identity matters to provider-side tool-block caching on some backends). try: _refresh_agent_tool_definitions(agent) - except Exception: # noqa: BLE001 + except Exception: logger.warning( "compaction tool-definition refresh failed; keeping the session's existing tool snapshot", exc_info=True ) @@ -3182,7 +3181,7 @@ def _rebuild_system_prompt_at_boundary(agent: Any, system_message: str) -> str: def _salvage_or_refuse_grown_transcript( agent: Any, messages: list, compressed: list, *, system_message: str, attempt_started_at: float, attempt_snapshot: dict, -) -> Tuple[Optional[list], Optional[str]]: +) -> tuple[Optional[list], Optional[str]]: """Anti-growth guard at the COMMIT SITE (in-place commits before the gateway can inspect). Compares like-for-like rough estimates; on growth tries one mechanical salvage pass, else treats the attempt as a refused no-op. Returns ``(compressed, None)`` to proceed or ``(None, prompt)`` when refused @@ -4017,7 +4016,7 @@ def _begin_compression_attempt( def _route_codex_compaction( agent: Any, messages: list, system_message: str, *, commit_fence: Optional[CompressionCommitFence], attempt: _Attempt, approx_tokens: Optional[int], task_id: str, force: bool, -) -> Tuple[list, str]: +) -> tuple[list, str]: """Codex owns the real thread: run its own compact under the commit fence bracket.""" if commit_fence is not None and not commit_fence.begin_commit(getattr(agent, "_hard_interrupt_requested", None)): attempt.restore_compressor(agent.context_compressor) @@ -4066,7 +4065,7 @@ def compress_context( bypass_cooldown: bool = False, defer_context_engine_notification: bool = False, commit_fence: Optional[CompressionCommitFence] = None, verbatim_tail: Optional[list] = None, trigger: Optional[str] = None, snapshot_is_current: Optional[Callable[[], bool]] = None, -) -> Tuple[list, str]: +) -> tuple[list, str]: """Compress conversation context and split the session in SQLite. ``force`` (manual /compress) clears the summary-failure cooldown; ``bypass_cooldown`` (provider-proven overflow) skips it once, breakers still apply. ``commit_fence`` stops a timed-out worker mutating session @@ -4315,7 +4314,7 @@ def _record_codex_compaction_failure(agent: Any, error: str) -> None: def _compress_context_via_codex_app_server( agent: Any, messages: list, system_message: Optional[str], *, approx_tokens: Optional[int] = None, task_id: str = "default", force: bool = False, -) -> Tuple[list, str]: +) -> tuple[list, str]: """Route compaction to Codex app-server for Codex-owned threads. Rewriting the local transcript would not shrink the Codex thread, so Codex compacts its own thread and Hermes' transcript is left unchanged.""" diff --git a/agent/conversation_compression_archive.py b/agent/conversation_compression_archive.py index 0360baea45..fd4af86d88 100644 --- a/agent/conversation_compression_archive.py +++ b/agent/conversation_compression_archive.py @@ -28,9 +28,9 @@ RETIRED_ROW = "_retired_row" OWN_ROW = "_own_row" -def retired_row_payload(message: Dict[str, Any]) -> Dict[str, Any]: +def retired_row_payload(message: dict[str, Any]) -> dict[str, Any]: """The fields the commit matches a retired id-less row by.""" - payload: Dict[str, Any] = {"role": message.get("role"), "content": message.get("content")} + payload: dict[str, Any] = {"role": message.get("role"), "content": message.get("content")} if message.get("tool_call_id"): payload["tool_call_id"] = message["tool_call_id"] calls = [call.get("id") for call in message.get("tool_calls") or () if isinstance(call, dict)] @@ -47,7 +47,7 @@ def _positive_id(value: Any) -> Optional[int]: def held_archive_coverage( messages: Sequence[Any], verbatim_tail: Optional[Sequence[Any]] = None, -) -> Tuple[List[int], List[Dict[str, Any]]]: +) -> tuple[list[int], list[dict[str, Any]]]: """``(covered ids, unresolved dicts)`` from the history the compressor was handed. A positive ``_row_id`` is covered, including ids a repair merged into that dict. @@ -55,8 +55,8 @@ def held_archive_coverage( marker-less miss is an unpersisted turn rather than a reason to archive the lease watermark. """ - covered: List[int] = [] - unresolved: List[Dict[str, Any]] = [] + covered: list[int] = [] + unresolved: list[dict[str, Any]] = [] for batch in (messages or (), verbatim_tail or ()): for message in batch: if not isinstance(message, dict): @@ -83,7 +83,7 @@ def newest_exact_held_id( """ from agent.context_compressor import _DB_PERSISTED_MARKER - exact: List[int] = [] + exact: list[int] = [] for message in messages or (): if not isinstance(message, dict) or not message.get(_DB_PERSISTED_MARKER): continue @@ -101,7 +101,7 @@ def newest_exact_held_id( def coverage_for_commit( session_db: Any, session_id: str, messages: Sequence[Any], verbatim_tail: Optional[Sequence[Any]] = None, -) -> Tuple[Optional[List[int]], Optional[List[Dict[str, Any]]]]: +) -> tuple[Optional[list[int]], Optional[list[dict[str, Any]]]]: """Coverage to pass into ``archive_and_compact``, or ``(None, None)`` to keep the watermark. ``None`` when the newest exact held row is already inactive (another compaction @@ -120,7 +120,7 @@ def coverage_for_commit( if newest is not None and callable(role_of) and role_of(session_id, newest) is None: return None, None covered, unresolved = held_archive_coverage(messages, verbatim_tail) - retired: List[Dict[str, Any]] = [] + retired: list[dict[str, Any]] = [] for message in unresolved: named = [row for row in message.get(RETIRED_DURABLE_ROWS) or () if isinstance(row, dict)] if int(message.get(UNNAMED_DURABLE_ROWS) or 0) > sum(1 for row in named if not row.get(OWN_ROW)): diff --git a/agent/conversation_compression_manual.py b/agent/conversation_compression_manual.py index 8b8ef36e2a..214a605cb0 100644 --- a/agent/conversation_compression_manual.py +++ b/agent/conversation_compression_manual.py @@ -32,14 +32,14 @@ class CompressRequest: @dataclass class CompressResult: status: str # "preview" | "compressed" | "lock_skipped" | "nothing_to_do" - before_messages: List[Dict[str, Any]] - after_messages: List[Dict[str, Any]] + before_messages: list[dict[str, Any]] + after_messages: list[dict[str, Any]] before_tokens: int after_tokens: int request: CompressRequest - lines: List[str] = field(default_factory=list) # preview report lines (status == "preview") + lines: list[str] = field(default_factory=list) # preview report lines (status == "preview") lock_holder: Any = None - summary: Optional[Dict[str, Any]] = None # ``summarize_manual_compression`` payload when compressed + summary: Optional[dict[str, Any]] = None # ``summarize_manual_compression`` payload when compressed @property def removed(self) -> int: @@ -55,7 +55,7 @@ def parse_compress_args(raw_args: str) -> CompressRequest: focus_topic=focus_topic or None) -def estimate_request_tokens(agent: Any, messages: Sequence[Dict[str, Any]]) -> int: +def estimate_request_tokens(agent: Any, messages: Sequence[dict[str, Any]]) -> int: """Transcript + system prompt + tool schemas: a transcript-only figure understates real request pressure and can even appear to grow after a dense handoff summary replaces many short turns (#6217).""" from agent.model_metadata import estimate_request_tokens_rough @@ -67,7 +67,7 @@ def estimate_request_tokens(agent: Any, messages: Sequence[Dict[str, Any]]) -> i def compress_now( - agent: Any, history: Sequence[Dict[str, Any]], request: CompressRequest, *, + agent: Any, history: Sequence[dict[str, Any]], request: CompressRequest, *, system_message: Any = None, task_id: str = "default", skip_without_window: bool = False, snapshot_is_current: Optional[Callable[[], bool]] = None, ) -> CompressResult: @@ -150,7 +150,7 @@ def compress_now( return CompressResult("compressed", before, list(compressed), before_tokens, after_tokens, request, summary=summary) -def render_compress_result(result: CompressResult, *, prefix: str = "") -> List[str]: +def render_compress_result(result: CompressResult, *, prefix: str = "") -> list[str]: """Surface-neutral text lines for a result (each surface may add its own icon/prefix).""" if result.status == "preview": return [f"{prefix}{line}" for line in result.lines] diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index 77426b92da..6404de28b8 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -81,7 +81,7 @@ RUN_BUDGET_WRAPUP_NOTICE = ( def _midturn_request_pressure_tokens( - agent: Any, api_messages: List[Dict[str, Any]], effective_system: str, approx_tokens: int + agent: Any, api_messages: list[dict[str, Any]], effective_system: str, approx_tokens: int ) -> int: """Token figure the mid-turn pre-API compression guard compares: the pruned native-Responses estimate when native compaction eligibility is proven (the generic @@ -121,7 +121,7 @@ def _review_input_budget_exhausted(agent: Any) -> bool: return isinstance(used, int) and not isinstance(used, bool) and used >= budget -def _maybe_inject_run_budget_wrapup(agent: Any, messages: List[Dict[str, Any]]) -> bool: +def _maybe_inject_run_budget_wrapup(agent: Any, messages: list[dict[str, Any]]) -> bool: """Inject the one-time wall-clock wrap-up notice when past 80% of budget. Appends to the NEWEST ``role:"tool"`` message (cache-safe, like /steer); latches @@ -157,7 +157,7 @@ def _maybe_inject_run_budget_wrapup(agent: Any, messages: List[Dict[str, Any]]) def _restore_user_after_reference_handoff( - messages: List[Dict[str, Any]], user_message: Any + messages: list[dict[str, Any]], user_message: Any ) -> bool: """Re-append this turn's real user ask when compaction left only a handoff (#80622). Returns True when a restore append happened.""" @@ -175,7 +175,7 @@ def _restore_user_after_reference_handoff( def _should_skip_model_call_for_reference_handoff( - messages: List[Dict[str, Any]], user_message: Any + messages: list[dict[str, Any]], user_message: Any ) -> bool: """Guard post-compaction continues against sole-handoff active turns (#80622).""" from agent.context_compressor import reference_handoff_would_drive_next_model_call @@ -291,7 +291,7 @@ def _continuation_overlap_length(previous: str, continuation: str) -> int: return matched if matched >= _MIN_CONTINUATION_OVERLAP else 0 -def _join_truncated_parts(parts: List[tuple[str, bool]]) -> str: +def _join_truncated_parts(parts: list[tuple[str, bool]]) -> str: """Join continuation fragments, deduping only interrupted-stream seams.""" joined = "" previous_was_partial_stub = False @@ -319,7 +319,7 @@ def _moa_reference_metrics_for_hook(agent: Any) -> Any: return None -def _apply_active_turn_redirect(agent: Any, messages: List[Dict[str, Any]], text: str) -> None: +def _apply_active_turn_redirect(agent: Any, messages: list[dict[str, Any]], text: str) -> None: """Append a provider-safe checkpoint and correction to the live turn so role alternation holds and cached messages stay byte-identical. INVARIANTS: raw chain-of-thought never enters replayable content (inlined CoT reads as a prefill jailbreak and bricks the session with @@ -468,7 +468,7 @@ def _maybe_grow_local_window(agent: Any, compressor: Any, getattr(agent, "model", "") or "", base_url=base_url, session_tokens=int(request_tokens), current_window=current_window, ) - except Exception as exc: # noqa: BLE001 — growth must never break a turn + except Exception as exc: logger.debug("local window growth check failed: %s", exc) return None @@ -931,7 +931,7 @@ _LEGACY_LENGTH_CONTINUATION_NETWORK_STUB = ( _LENGTH_CONTINUATION_DROPPED_TOOLS_PREFIX = "[System: Your previous tool call " -def _get_continuation_prompt(is_partial_stub: bool, dropped_tools: Optional[List[str]] = None) -> str: +def _get_continuation_prompt(is_partial_stub: bool, dropped_tools: Optional[list[str]] = None) -> str: if is_partial_stub and dropped_tools: tool_list = ", ".join(dropped_tools[:3]) return ( @@ -988,7 +988,7 @@ _EMPTY_TOOL_RESPONSE_NUDGE = ( # each iteration). Sound because canonicalization is pure; malformed strings raise before # being stored, so the repair fallback is never memoized. The byte budget exists because # argument strings can run 100KB+, so a count bound alone does not bound memory. -_CANON_ARGS_CACHE: Dict[str, str] = {} +_CANON_ARGS_CACHE: dict[str, str] = {} _CANON_ARGS_CACHE_MAX = 4096 _CANON_ARGS_CACHE_MAX_BYTES = 32 * 1024 * 1024 _canon_args_cache_bytes = 0 @@ -1064,8 +1064,8 @@ def _invalid_tool_name_error_content(name: str, valid_tool_names) -> str: def _content_policy_blocked_result( - messages: List[Dict], api_call_count: int, *, final_response: str, error_detail: str -) -> Dict[str, Any]: + messages: list[dict], api_call_count: int, *, final_response: str, error_detail: str +) -> dict[str, Any]: """Terminal turn result for a content-policy block (deterministic for the unchanged prompt, so no retry); shared by the HTTP-200 and exception paths.""" return { @@ -1076,8 +1076,8 @@ def _content_policy_blocked_result( def _partial_turn_result( - final_response: str, messages: List[Dict], api_call_count: int, **flags: Any -) -> Dict[str, Any]: + final_response: str, messages: list[dict], api_call_count: int, **flags: Any +) -> dict[str, Any]: """Incomplete-turn result whose ``error`` mirrors ``final_response``; ``flags`` add the recovery-contract keys (``failed``, ``compression_deferred``, ...).""" return { @@ -1086,7 +1086,7 @@ def _partial_turn_result( } -def _compression_deferred_result(agent, messages: List[Dict], api_call_count: int, reason: str = "lock") -> Dict[str, Any]: +def _compression_deferred_result(agent, messages: list[dict], api_call_count: int, reason: str = "lock") -> dict[str, Any]: """Soft turn result for a transiently-deferred compression. Both reasons must end as ``compression_deferred``, never ``compression_exhausted`` — the gateway wipes the session on exhaustion (#9893/#35809). ``failed`` stays False; the turn persists.""" @@ -1122,9 +1122,9 @@ def _compression_deferred_result(agent, messages: List[Dict], api_call_count: in def _provider_overflow_exhausted_result( - agent, messages: List[Dict], conversation_history, api_call_count: int, + agent, messages: list[dict], conversation_history, api_call_count: int, request_pressure_tokens: int, max_compression_attempts: int, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Fail closed when a rebuilt request is still too large after recovery.""" agent._flush_status_buffer() logger.error( @@ -1201,19 +1201,19 @@ def _ensure_cached_system_prompt_static(agent, system_message=None) -> None: reconstruct_static_prefix(agent, system_message=system_message, log_label="failover redecoration") -def _peel_moa_guidance(messages: List[Dict[str, Any]], guidance: Any) -> List[Dict[str, Any]]: +def _peel_moa_guidance(messages: list[dict[str, Any]], guidance: Any) -> list[dict[str, Any]]: """Remove MoA reference guidance attached by ``_attach_reference_guidance``.""" from agent.moa_loop import peel_reference_guidance return peel_reference_guidance(messages, guidance) def _redecorate_prompt_cache_for_provider( - agent, api_messages: List[Dict[str, Any]], *, system_message=None, - moa_prepared: Optional[Dict[str, Any]] = None, tools_for_api: Optional[List[Dict[str, Any]]] = None, -) -> tuple[List[Dict[str, Any]], Optional[Dict[str, Any]], List[Dict[str, Any]]]: + agent, api_messages: list[dict[str, Any]], *, system_message=None, + moa_prepared: Optional[dict[str, Any]] = None, tools_for_api: Optional[list[dict[str, Any]]] = None, +) -> tuple[list[dict[str, Any]], Optional[dict[str, Any]], list[dict[str, Any]]]: """Strip and re-apply cache_control for the *current* provider policy — failover ``continue`` paths reuse ``api_messages`` (#72626). MoA guidance is peeled and rebased.""" - messages: List[Dict[str, Any]] = [dict(m) if isinstance(m, dict) else m for m in (api_messages or [])] + messages: list[dict[str, Any]] = [dict(m) if isinstance(m, dict) else m for m in (api_messages or [])] prepared = moa_prepared guidance = prepared.get("guidance") if isinstance(prepared, dict) else None if guidance: @@ -1275,9 +1275,9 @@ def _engine_overrides_hook(engine: Any, name: str) -> bool: def _apply_context_engine_selection( - agent: Any, api_messages: List[Dict[str, Any]], conversation_messages: List[Dict[str, Any]], - incoming_message: Optional[Dict[str, Any]], *, logger: Any, -) -> List[Dict[str, Any]]: + agent: Any, api_messages: list[dict[str, Any]], conversation_messages: list[dict[str, Any]], + incoming_message: Optional[dict[str, Any]], *, logger: Any, +) -> list[dict[str, Any]]: """Run the optional per-turn ``ContextEngine.select_context()`` hook, fail-open: any exception or invalid return yields ``api_messages`` unchanged; history is never mutated.""" engine = getattr(agent, "context_compressor", None) @@ -1325,7 +1325,7 @@ def _apply_context_engine_selection( def _notify_context_engine_turn_complete( - agent: Any, messages: List[Dict[str, Any]], *, usage: Optional[Dict[str, Any]] = None, logger: Any, **meta: Any + agent: Any, messages: list[dict[str, Any]], *, usage: Optional[dict[str, Any]] = None, logger: Any, **meta: Any ) -> None: """Notify the active context engine that a user turn has finished (fail-open; the engine gets a copy so it cannot mutate the persisted transcript).""" @@ -1358,7 +1358,7 @@ def _decode_inline_moa_turn(user_message, persist_user_message): return user_message, None, persist_user_message -def _preflight_timeout_result(agent, exc, conversation_history) -> Dict[str, Any]: +def _preflight_timeout_result(agent, exc, conversation_history) -> dict[str, Any]: """Typed recovery result when turn-start preflight compression timed out (#98424): no provider call was sent, and surfaces would otherwise hide the actionable guidance.""" logger.warning( @@ -1420,7 +1420,7 @@ class _LoopState: restart_count: int = 0 _outer_error_count: int = 0 # outer-loop exceptions this turn (#92450), see _MAX_OUTER_LOOP_ERRORS truncated_tool_call_retries: int = 0 - truncated_response_parts: List[tuple[str, bool]] = field(default_factory=list) + truncated_response_parts: list[tuple[str, bool]] = field(default_factory=list) compression_attempts: int = 0 _last_preflight_pressure: Optional[int] = None # A provider overflow outweighs the rough-estimate calibration that defers preflight after @@ -1467,7 +1467,7 @@ _CTX_FIELDS = frozenset({ "active_system_prompt", "current_turn_user_idx", "_preflight_compression_blocked", }) # Keyword names each phase helper takes (minus ``agent``), cached per function object. -_PHASE_PARAMS: Dict[Any, tuple] = {} +_PHASE_PARAMS: dict[Any, tuple] = {} # Verdict fields the loop latches (only ever sets True) instead of copying back: # ``handle_api_error`` reports overflow recovery per call and must not clear an earlier arm. _LATCHED_VERDICT_FIELDS = {"handle_api_error": frozenset({"_provider_overflow_recovery_pending"})} @@ -1494,7 +1494,7 @@ def _run_phase(fn, agent, state: _LoopState, **extra): return verdict -def _run_api_retry_loop(agent, s: _LoopState) -> Optional[Dict[str, Any]]: +def _run_api_retry_loop(agent, s: _LoopState) -> Optional[dict[str, Any]]: """One API call with its retry/recovery loop (guard → build → call → check, error handlers). Returns a turn result dict when a phase ends the turn, else None once the loop is left @@ -1526,7 +1526,7 @@ def _run_api_retry_loop(agent, s: _LoopState) -> Optional[Dict[str, Any]]: return None -def _codex_app_server_turn(agent: Any, s: Any) -> Optional[Dict[str, Any]]: +def _codex_app_server_turn(agent: Any, s: Any) -> Optional[dict[str, Any]]: """The codex app-server's result for this turn, or None when its failure activated a fallback and the generic loop retries the same user turn.""" codex_result = agent._run_codex_app_server_turn( @@ -1548,19 +1548,19 @@ def _run_conversation_turn( agent, user_message: Any, system_message: str = None, - conversation_history: List[Dict[str, Any]] = None, + conversation_history: list[dict[str, Any]] = None, task_id: str = None, stream_callback: Optional[callable] = None, persist_user_message: Optional[Any] = None, persist_user_timestamp: Optional[float] = None, persist_user_display_kind: Optional[str] = None, - persist_user_display_metadata: Optional[Dict[str, Any]] = None, + persist_user_display_metadata: Optional[dict[str, Any]] = None, persist_user_platform_id: Optional[str] = None, - turn_author: Optional[Dict[str, Any]] = None, + turn_author: Optional[dict[str, Any]] = None, moa_config: Optional[dict[str, Any]] = None, title_user_message: Optional[str] = None, prelude: Optional[Prelude] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Run a complete conversation with tool calling until completion; returns the result dict. ``stream_callback``: per-text-delta callback (TTS). ``persist_user_message``: clean text to @@ -1710,19 +1710,19 @@ def run_conversation( agent, user_message: Any, system_message: str = None, - conversation_history: List[Dict[str, Any]] = None, + conversation_history: list[dict[str, Any]] = None, task_id: str = None, stream_callback: Optional[callable] = None, persist_user_message: Optional[Any] = None, persist_user_timestamp: Optional[float] = None, persist_user_display_kind: Optional[str] = None, - persist_user_display_metadata: Optional[Dict[str, Any]] = None, + persist_user_display_metadata: Optional[dict[str, Any]] = None, persist_user_platform_id: Optional[str] = None, moa_config: Optional[dict[str, Any]] = None, - turn_author: Optional[Dict[str, Any]] = None, + turn_author: Optional[dict[str, Any]] = None, title_user_message: Optional[str] = None, prelude: Optional[Prelude] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Run one turn (see ``_run_conversation_turn``) and export the current-turn boundary. Every envelope that leaves the loop — success, partial/error, interrupt, retry-exhausted, diff --git a/agent/credential_persistence.py b/agent/credential_persistence.py index 2c0154e9fb..dfed956fe7 100644 --- a/agent/credential_persistence.py +++ b/agent/credential_persistence.py @@ -99,7 +99,7 @@ def _credential_secret_fingerprint(payload: Mapping[str, Any]) -> str | None: def sanitize_borrowed_credential_payload( payload: Mapping[str, Any], provider_id: Any = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Return a disk-safe credential-pool payload. Owned sources pass through unchanged. Borrowed sources keep labels, diff --git a/agent/credential_pool.py b/agent/credential_pool.py index 9686776beb..e6f11e3965 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -189,7 +189,7 @@ _NOUS_EXTRA_STATE_KEYS = ( ) # ``replace(entry, **_CLEAR_STATUS)`` returns an entry with no error state. -_CLEAR_STATUS: Dict[str, Any] = { +_CLEAR_STATUS: dict[str, Any] = { "last_status": None, "last_status_at": None, "last_error_code": None, @@ -197,7 +197,7 @@ _CLEAR_STATUS: Dict[str, Any] = { "last_error_message": None, "last_error_reset_at": None, } -_MARK_OK: Dict[str, Any] = {**_CLEAR_STATUS, "last_status": STATUS_OK} +_MARK_OK: dict[str, Any] = {**_CLEAR_STATUS, "last_status": STATUS_OK} def _normalize_pool_auth_type(provider: str, token: Any, auth_type: Any) -> str: @@ -237,8 +237,8 @@ class PooledCredential: # A provider may rate-limit one model while the same credential remains # usable for its sibling models. Keep that observation separate from the # credential-wide status used for auth and billing failures. - model_cooldowns: Optional[Dict[str, float]] = None - extra: Dict[str, Any] = None # type: ignore[assignment] + model_cooldowns: Optional[dict[str, float]] = None + extra: dict[str, Any] = None # type: ignore[assignment] def __post_init__(self): if self.extra is None: @@ -251,7 +251,7 @@ class PooledCredential: raise AttributeError(f"'{type(self).__name__}' object has no attribute {name!r}") @classmethod - def from_dict(cls, provider: str, payload: Dict[str, Any]) -> "PooledCredential": + def from_dict(cls, provider: str, payload: dict[str, Any]) -> "PooledCredential": field_names = {f.name for f in fields(cls) if f.name != "provider"} data = {k: payload.get(k) for k in field_names if k in payload} # Rehydrated last_status_at may be an ISO string from to_dict() — normalize to float epoch @@ -273,8 +273,8 @@ class PooledCredential: data.setdefault("access_token", "") return cls(provider=provider, **data) - def to_dict(self) -> Dict[str, Any]: - result: Dict[str, Any] = {} + def to_dict(self) -> dict[str, Any]: + result: dict[str, Any] = {} for field_def in fields(self): if field_def.name in {"provider", "extra"}: continue @@ -327,7 +327,7 @@ def label_from_token(token: str, fallback: str) -> str: return fallback -def _codex_principal_identity(access_token: Any) -> Optional[Tuple[str, str]]: +def _codex_principal_identity(access_token: Any) -> Optional[tuple[str, str]]: """``(chatgpt_account_id, sub)`` of a Codex access token, or None when either claim is missing. Decoded without signature verification: this only decides whether two credentials Hermes @@ -344,7 +344,7 @@ def _codex_principal_identity(access_token: Any) -> Optional[Tuple[str, str]]: return account_id.strip(), subject.strip() -def _codex_entry_tracks_singleton(entry: PooledCredential, singleton_tokens: Dict[str, Any]) -> bool: +def _codex_entry_tracks_singleton(entry: PooledCredential, singleton_tokens: dict[str, Any]) -> bool: """Whether a Codex pool entry may adopt the auth.json singleton's token pair. ``device_code`` IS the singleton. ``manual:device_code`` is ambiguous: a legacy alias of the @@ -361,7 +361,7 @@ def _codex_entry_tracks_singleton(entry: PooledCredential, singleton_tokens: Dic return entry_identity is not None and entry_identity == _codex_principal_identity(singleton_tokens.get("access_token")) -def _next_priority(entries: List[PooledCredential]) -> int: +def _next_priority(entries: list[PooledCredential]) -> int: return max((entry.priority for entry in entries), default=-1) + 1 @@ -442,10 +442,10 @@ def _singleton_predates_entry(state: Any, entry: "PooledCredential") -> bool: return state_ts < entry_ts -def _normalize_error_context(error_context: Optional[Dict[str, Any]]) -> Dict[str, Any]: +def _normalize_error_context(error_context: Optional[dict[str, Any]]) -> dict[str, Any]: if not isinstance(error_context, dict): return {} - normalized: Dict[str, Any] = {} + normalized: dict[str, Any] = {} for key in ("reason", "message"): value = error_context.get(key) if isinstance(value, str) and value.strip(): @@ -510,7 +510,7 @@ def _iter_custom_providers(config: Optional[dict] = None): yield _normalize_custom_pool_name(entry["name"]), entry -def _custom_entry_name_aliases(norm_name: str, entry: Dict[str, Any]) -> set: +def _custom_entry_name_aliases(norm_name: str, entry: dict[str, Any]) -> set: aliases = {norm_name} provider_key = _normalize_custom_pool_name(str(entry.get("provider_key") or "")) if provider_key: @@ -528,9 +528,9 @@ def _requested_custom_name_aliases(provider_name: str) -> set: return aliases -def _pool_keys_for_custom_entry(norm_name: str, entry: Dict[str, Any]) -> List[str]: +def _pool_keys_for_custom_entry(norm_name: str, entry: dict[str, Any]) -> list[str]: """Durable ``providers.`` slug first, then legacy ``custom:``.""" - keys: List[str] = [] + keys: list[str] = [] provider_key = _normalize_custom_pool_name(str(entry.get("provider_key") or "")) for key in (provider_key, f"{CUSTOM_POOL_PREFIX}{norm_name}" if norm_name else ""): normalized = key.strip().lower() @@ -542,7 +542,7 @@ def _pool_keys_for_custom_entry(norm_name: str, entry: Dict[str, Any]) -> List[s def custom_provider_pool_key_candidates( base_url: Optional[str], provider_name: Optional[str] = None, -) -> List[str]: +) -> list[str]: """Return pool keys to try for a custom endpoint. ``hermes auth add `` stores ``providers.`` credentials under the @@ -578,7 +578,7 @@ def get_custom_provider_pool_key(base_url: Optional[str], provider_name: Optiona return candidates[0] if candidates else None -def list_custom_pool_providers() -> List[str]: +def list_custom_pool_providers() -> list[str]: """Return all 'custom:*' pool keys that have entries in auth.json.""" pool_data = read_credential_pool(None) return sorted( @@ -589,7 +589,7 @@ def list_custom_pool_providers() -> List[str]: ) -def _get_custom_provider_config(pool_key: str) -> Optional[Dict[str, Any]]: +def _get_custom_provider_config(pool_key: str) -> Optional[dict[str, Any]]: """Return the custom_providers config entry matching a pool key like 'custom:together.ai'.""" if not pool_key.startswith(CUSTOM_POOL_PREFIX): return None @@ -788,7 +788,7 @@ def _guarded_global_root(global_path: Optional[Path]) -> Optional[Path]: def _write_through_provider_state_to_global_root( - provider_id: str, state: Dict[str, Any] + provider_id: str, state: dict[str, Any] ) -> None: """Persist a rotated OAuth ``state`` into the global-root auth.json. @@ -830,7 +830,7 @@ def _singleton_target_for_entry(pool: "CredentialPool", entry: "PooledCredential return None -def _store_owns_pool_provider(auth_store: Dict[str, Any], provider: str) -> bool: +def _store_owns_pool_provider(auth_store: dict[str, Any], provider: str) -> bool: """True when an already-loaded *auth_store* has its own rows for *provider*.""" pool = auth_store.get("credential_pool") entries = pool.get(provider) if isinstance(pool, dict) else None @@ -867,10 +867,10 @@ def _borrowed_single_use_pool_root() -> Optional[Path]: def _update_root_pool_rows( - provider: str, payloads: List[Dict[str, Any]], global_path: Path, + provider: str, payloads: list[dict[str, Any]], global_path: Path, *, status_cleared_ids: Optional[Iterable[str]] = None, - token_bases: Optional[Dict[str, Tuple[Any, Any]]] = None, -) -> List[Dict[str, Any]]: + token_bases: Optional[dict[str, tuple[Any, Any]]] = None, +) -> list[dict[str, Any]]: """UPDATE-ONLY merge of *payloads* into the root store's rows for *provider*. A borrower may refresh the root's rows (rotation, cooldown state) but @@ -889,7 +889,7 @@ def _update_root_pool_rows( incoming_by_id = auth_mod._entry_ids(payloads) cleared = {cid for cid in (status_cleared_ids or ()) if cid} bases = token_bases or {} - merged: List[Dict[str, Any]] = [] + merged: list[dict[str, Any]] = [] changed = False for disk_entry in existing_list: did = disk_entry.get("id") if isinstance(disk_entry, dict) else None @@ -912,12 +912,12 @@ def _update_root_pool_rows( def persist_pool_entries( provider: str, - payloads: List[Dict[str, Any]], + payloads: list[dict[str, Any]], *, removed_ids: Optional[Iterable[str]] = None, status_cleared_ids: Optional[Iterable[str]] = None, - token_bases: Optional[Dict[str, Tuple[Any, Any]]] = None, -) -> Optional[List[Dict[str, Any]]]: + token_bases: Optional[dict[str, tuple[Any, Any]]] = None, +) -> Optional[list[dict[str, Any]]]: """Persist a provider's pool rows to the store that OWNS them. A named profile that sees a single-use-refresh provider (see @@ -958,7 +958,7 @@ def persist_pool_entries( # (Codex, xAI): log names (sync-message form, " OAuth" form), # ``hermes_cli.auth`` refresh function and terminal-error predicate (looked # up at call time so tests can patch them). -_TOKENS_SINGLETON_PROVIDERS: Dict[str, Tuple[str, str, str, str]] = { +_TOKENS_SINGLETON_PROVIDERS: dict[str, tuple[str, str, str, str]] = { "openai-codex": ("Codex", "Codex", "refresh_codex_oauth_pure", "_is_terminal_codex_oauth_refresh_error"), "xai-oauth": ("xAI OAuth", "xAI", "refresh_xai_oauth_pure", "_is_terminal_xai_oauth_refresh_error"), } @@ -1005,21 +1005,21 @@ class _RefreshDone(Exception): class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin): - def __init__(self, provider: str, entries: List[PooledCredential]): + def __init__(self, provider: str, entries: list[PooledCredential]): self.provider = provider self._entries = sorted(entries, key=lambda entry: entry.priority) self._current_id: Optional[str] = None # Ids of rows read via the global-root fallback (single-use OAuth # providers only); set by load_pool(), consumed by add_entry(). - self._borrowed_root_ids: Set[str] = set() - self._persisted_token_pairs: Dict[str, Tuple[Any, Any]] = {} + self._borrowed_root_ids: set[str] = set() + self._persisted_token_pairs: dict[str, tuple[Any, Any]] = {} self._strategy = get_pool_strategy(provider) # RLock: _replace_entry/_persist self-acquire it so the DEFERRED # single-use-token refresh path (network I/O outside the lock by # design) still serializes its pool mutations; in-lock callers # re-acquire reentrantly. self._lock = threading.RLock() - self._active_leases: Dict[str, int] = {} + self._active_leases: dict[str, int] = {} self._max_concurrent = DEFAULT_MAX_CONCURRENT_PER_CREDENTIAL # Monotonic timestamp of the last "no available entries" log (see # NO_AVAILABLE_ENTRIES_LOG_THROTTLE_SECONDS). Re-armed to None on every @@ -1093,7 +1093,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) ) return min(candidates) if candidates else None - def entries(self) -> List[PooledCredential]: + def entries(self) -> list[PooledCredential]: with self._lock: return list(self._entries) @@ -1146,8 +1146,8 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) def _persist( self, *, - removed_ids: Optional[List[str]] = None, - status_cleared_ids: Optional[List[str]] = None, + removed_ids: Optional[list[str]] = None, + status_cleared_ids: Optional[list[str]] = None, ) -> None: # Self-locking: snapshotting self._entries must not race a rotation. with self._lock: @@ -1186,7 +1186,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) return self._find(lambda e: e.id == updated.id) or updated return updated - def _quarantine_sources(self, entry: PooledCredential, sources: Set[str]) -> None: + def _quarantine_sources(self, entry: PooledCredential, sources: set[str]) -> None: """Drop every entry seeded from *sources* and persist the removal. Atomic read-modify-write of ``self._entries``: this runs on the @@ -1205,7 +1205,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) def _is_terminal_auth_failure( self, status_code: Optional[int], - normalized_error: Dict[str, Any], + normalized_error: dict[str, Any], ) -> bool: """Detect upstream-permanent OAuth failures that won't recover on TTL. @@ -1225,7 +1225,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) self, entry: PooledCredential, status_code: Optional[int], - error_context: Optional[Dict[str, Any]] = None, + error_context: Optional[dict[str, Any]] = None, *, persist: bool = True, failure_reason: Optional[str] = None, @@ -1412,7 +1412,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) "Pool entry %s: syncing %s tokens from auth.json (refreshed by another process)", entry.id, display, ) - field_updates: Dict[str, Any] = { + field_updates: dict[str, Any] = { "access_token": store_access or entry.access_token, "refresh_token": store_refresh or entry.refresh_token, **_CLEAR_STATUS, @@ -1448,7 +1448,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) if not any(v not in (None, "") and getattr(entry, k, None) != v for k, v in comparable.items()): return entry logger.debug("Pool entry %s: syncing Nous state from auth.json", entry.id) - field_updates: Dict[str, Any] = dict(_CLEAR_STATUS) + field_updates: dict[str, Any] = dict(_CLEAR_STATUS) field_updates.update({k: v for k, v in comparable.items() if v}) extra_updates = dict(entry.extra) extra_updates.update( @@ -1502,7 +1502,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) except Exception as exc: logger.debug("Failed to sync %s pool entry back to auth store: %s", self.provider, exc) - def _apply_entry_to_singleton_state(self, entry: PooledCredential, state: Dict[str, Any]) -> bool: + def _apply_entry_to_singleton_state(self, entry: PooledCredential, state: dict[str, Any]) -> bool: """Copy *entry*'s tokens into the provider's auth.json ``state`` in place.""" if self.provider == "nous": state["access_token"] = entry.access_token @@ -1635,7 +1635,6 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) "message": f"rotated credential was not durably written to {store}: {exc}", }, ) - return None def _single_use_refresh_lock_timeout(self) -> float: """Configured refresh POST timeout plus margin, so a slow token endpoint cannot starve the flock.""" @@ -1644,7 +1643,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) return max(float(auth_mod.AUTH_LOCK_TIMEOUT_SECONDS), float(refresh_timeout_seconds) + 5.0) def _commit_anthropic_rotation( - self, entry: PooledCredential, refreshed: Dict[str, Any] + self, entry: PooledCredential, refreshed: dict[str, Any] ) -> None: """Write a rotated Anthropic pair to its authoritative singleton, or fail closed. @@ -2010,11 +2009,11 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) self._unmatched_rotation_streak = 0 return entry - def _select_under_lock(self, *, model: Optional[str] = None) -> Tuple[Optional[PooledCredential], List[PooledCredential]]: + def _select_under_lock(self, *, model: Optional[str] = None) -> tuple[Optional[PooledCredential], list[PooledCredential]]: with self._lock: return self._select_unlocked(model=model) - def _refresh_pending_entries(self, pending: List[PooledCredential]) -> None: + def _refresh_pending_entries(self, pending: list[PooledCredential]) -> None: """Refresh deferred single-use-token entries OUTSIDE the pool lock. Each refresh takes the cross-process ``_auth_store_lock`` (20+ s @@ -2071,7 +2070,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) def _available_entries( self, *, clear_expired: bool = False, refresh: bool = False, model: Optional[str] = None, - ) -> Tuple[List[PooledCredential], List[PooledCredential]]: + ) -> tuple[list[PooledCredential], list[PooledCredential]]: """Return (available, pending_refresh) for entries not in cooldown. *clear_expired* resets elapsed cooldowns to STATUS_OK and persists. @@ -2083,9 +2082,9 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) """ now = time.time() cleared_any = False - entries_to_prune: List[str] = [] - available: List[PooledCredential] = [] - pending_refresh: List[PooledCredential] = [] + entries_to_prune: list[str] = [] + available: list[PooledCredential] = [] + pending_refresh: list[PooledCredential] = [] sole_credential = self._is_sole_credential() for entry in self._entries: # Borrowed credentials persist as metadata-only references and are @@ -2166,7 +2165,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) def _select_unlocked( self, *, refresh: bool = True, count: bool = True, model: Optional[str] = None, - ) -> Tuple[Optional[PooledCredential], List[PooledCredential]]: + ) -> tuple[Optional[PooledCredential], list[PooledCredential]]: """Select the best available entry; returns ``(entry, pending_refresh)``. ``count=False`` skips the ``request_count`` bump for selections that are @@ -2298,7 +2297,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) self, *, status_code: Optional[int], - error_context: Optional[Dict[str, Any]] = None, + error_context: Optional[dict[str, Any]] = None, api_key_hint: Optional[str] = None, credential_id: Optional[str] = None, failure_reason: Optional[str] = None, @@ -2394,7 +2393,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) def _acquire_lease_under_lock( self, credential_id: Optional[str], - ) -> Tuple[Optional[str], List[PooledCredential]]: + ) -> tuple[Optional[str], list[PooledCredential]]: with self._lock: if credential_id: self._active_leases[credential_id] = self._active_leases.get(credential_id, 0) + 1 @@ -2466,7 +2465,7 @@ class CredentialPool(CredentialPoolAdminMixin, CredentialPoolModelCooldownMixin) # --- Seeding -------------------------------------------------------------- -def _upsert_entry(entries: List[PooledCredential], provider: str, source: str, payload: Dict[str, Any]) -> bool: +def _upsert_entry(entries: list[PooledCredential], provider: str, source: str, payload: dict[str, Any]) -> bool: matching_indices = [idx for idx, entry in enumerate(entries) if entry.source == source] existing_idx = matching_indices[0] if matching_indices else None duplicate_indices = set(matching_indices[1:]) @@ -2481,8 +2480,8 @@ def _upsert_entry(entries: List[PooledCredential], provider: str, source: str, p return True existing = entries[existing_idx] - field_updates: Dict[str, Any] = {} - extra_updates: Dict[str, Any] = {} + field_updates: dict[str, Any] = {} + extra_updates: dict[str, Any] = {} _field_names = {f.name for f in fields(existing)} incoming_token = payload.get("access_token") token_changed = incoming_token is not None and incoming_token != existing.access_token @@ -2527,7 +2526,7 @@ _ANTHROPIC_SOURCE_RANK = { } -def _normalize_pool_priorities(provider: str, entries: List[PooledCredential]) -> bool: +def _normalize_pool_priorities(provider: str, entries: list[PooledCredential]) -> bool: if provider != "anthropic": return False manual_entries = sorted( @@ -2551,7 +2550,7 @@ def _normalize_pool_priorities(provider: str, entries: List[PooledCredential]) - return changed -def _retain_sources_not_in(entries: List[PooledCredential], drop: Set[str]) -> bool: +def _retain_sources_not_in(entries: list[PooledCredential], drop: set[str]) -> bool: """Remove entries whose source is in *drop*; True if anything was removed.""" retained = [entry for entry in entries if entry.source not in drop] if len(retained) == len(entries): @@ -2563,14 +2562,14 @@ def _retain_sources_not_in(entries: List[PooledCredential], drop: Set[str]) -> b class _Seeder: """Accumulates ``_upsert_entry`` results for one ``load_pool`` seeding pass.""" - def __init__(self, provider: str, entries: List[PooledCredential]): + def __init__(self, provider: str, entries: list[PooledCredential]): self.provider = provider self.entries = entries self.changed = False - self.active_sources: Set[str] = set() + self.active_sources: set[str] = set() self.is_suppressed = _is_source_suppressed_fn() - def upsert(self, source: str, payload: Dict[str, Any]) -> bool: + def upsert(self, source: str, payload: dict[str, Any]) -> bool: """Upsert unless suppressed (``hermes auth remove`` must stay stable across loads).""" if self.is_suppressed(self.provider, source): return False @@ -2580,7 +2579,7 @@ class _Seeder: return ingested @property - def result(self) -> Tuple[bool, Set[str]]: + def result(self) -> tuple[bool, set[str]]: return self.changed, self.active_sources @@ -2638,7 +2637,7 @@ def _seed_anthropic_singletons(seed: _Seeder) -> None: }) -def _seed_nous_singleton(seed: _Seeder, auth_store: Dict[str, Any]) -> None: +def _seed_nous_singleton(seed: _Seeder, auth_store: dict[str, Any]) -> None: state, source_path = _load_provider_state_with_source(auth_store, "nous") if owned_profile_reads_root_state(auth_store, "nous", source_path): # e.g. an agent_key-only row surviving a fork strip/heal @@ -2676,7 +2675,7 @@ def _seed_nous_singleton(seed: _Seeder, auth_store: Dict[str, Any]) -> None: # Warn once per token per process when Copilot exchange degrades to raw token (#114740). -_COPILOT_RAW_DEGRADATION_WARNED: Set[str] = set() +_COPILOT_RAW_DEGRADATION_WARNED: set[str] = set() def _warn_copilot_raw_degradation_once(token: str) -> None: @@ -2795,7 +2794,7 @@ def _seed_minimax_singleton(seed: _Seeder) -> None: logger.debug("MiniMax OAuth token seed failed: %s", exc) -def _seed_tokens_singleton(seed: _Seeder, auth_store: Dict[str, Any]) -> None: +def _seed_tokens_singleton(seed: _Seeder, auth_store: dict[str, Any]) -> None: """Codex / xAI: surface the auth.json ``providers..tokens`` singleton as ``device_code``. Hermes owns its own Codex auth state and does NOT auto-import @@ -2825,7 +2824,7 @@ def _seed_tokens_singleton(seed: _Seeder, auth_store: Dict[str, Any]) -> None: }) -def _seed_from_singletons(provider: str, entries: List[PooledCredential]) -> Tuple[bool, Set[str]]: +def _seed_from_singletons(provider: str, entries: list[PooledCredential]) -> tuple[bool, set[str]]: seed = _Seeder(provider, entries) auth_store = _load_auth_store() if provider == "anthropic": @@ -2867,7 +2866,7 @@ def get_env_prefer_dotenv(key: str) -> str: # Providers already warned about env-key -> pool ingestion, once per process # (#81952 expected-behavior #3). -_ENV_INGESTION_WARNED: Set[str] = set() +_ENV_INGESTION_WARNED: set[str] = set() def _warn_env_ingestion_once(provider: str, env_var: str) -> None: @@ -2892,8 +2891,8 @@ def _warn_env_ingestion_once(provider: str, env_var: str) -> None: ) -def _env_payload(*, env_var: str, token: str, base_url: str) -> Dict[str, Any]: - payload: Dict[str, Any] = { +def _env_payload(*, env_var: str, token: str, base_url: str) -> dict[str, Any]: + payload: dict[str, Any] = { "auth_type": AUTH_TYPE_API_KEY, "access_token": token, "base_url": base_url, @@ -2917,7 +2916,7 @@ _ENV_BASE_URL_RESOLVERS = { } -def _env_key_var_candidates(env_vars: List[str], entries: List[PooledCredential]) -> List[str]: +def _env_key_var_candidates(env_vars: list[str], entries: list[PooledCredential]) -> list[str]: """*env_vars*, their numbered siblings, and the ``env:VAR`` names already persisted. ``VAR_2``, ``VAR_3``, ... are tried for every declared VAR until the first @@ -2944,7 +2943,7 @@ def _env_key_var_candidates(env_vars: List[str], entries: List[PooledCredential] return names -def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool, Set[str]]: +def _seed_from_env(provider: str, entries: list[PooledCredential]) -> tuple[bool, set[str]]: seed = _Seeder(provider, entries) # Copilot's singleton branch exchanges the raw ghu_ OAuth token for the # api token via `get_copilot_api_token`; the generic loop would re-read @@ -2989,8 +2988,8 @@ def _seed_from_env(provider: str, entries: List[PooledCredential]) -> Tuple[bool def _prune_stale_seeded_entries( - entries: List[PooledCredential], - active_sources: Set[str], + entries: list[PooledCredential], + active_sources: set[str], *, prune_env_sources: bool = True, ) -> bool: @@ -3015,7 +3014,7 @@ def _prune_stale_seeded_entries( return True -def _seed_custom_pool(pool_key: str, entries: List[PooledCredential]) -> Tuple[bool, Set[str]]: +def _seed_custom_pool(pool_key: str, entries: list[PooledCredential]) -> tuple[bool, set[str]]: """Seed a custom endpoint pool from custom_providers config and model config.""" seed = _Seeder(pool_key, entries) diff --git a/agent/credential_pool_admin.py b/agent/credential_pool_admin.py index 2c14c81879..702ef9c575 100644 --- a/agent/credential_pool_admin.py +++ b/agent/credential_pool_admin.py @@ -85,7 +85,7 @@ class CredentialPoolAdminMixin: self._persist() return self._find(lambda e: e.id == credential_id) - def resolve_target(self, target: Any) -> Tuple[Optional[int], Optional[PooledCredential], Optional[str]]: + def resolve_target(self, target: Any) -> tuple[Optional[int], Optional[PooledCredential], Optional[str]]: raw = str(target or "").strip() if not raw: return None, None, "No credential target provided." diff --git a/agent/credential_pool_model_cooldowns.py b/agent/credential_pool_model_cooldowns.py index cc02a88262..941799a810 100644 --- a/agent/credential_pool_model_cooldowns.py +++ b/agent/credential_pool_model_cooldowns.py @@ -32,9 +32,9 @@ def model_cooldown_until(entry: "PooledCredential", model: Optional[str]) -> Opt return max(active) if active else None -def merge_model_cooldowns(*maps: Any) -> Dict[str, float]: +def merge_model_cooldowns(*maps: Any) -> dict[str, float]: """Latest reset per model across snapshots — each writer only observed its own model.""" - merged: Dict[str, float] = {} + merged: dict[str, float] = {} for cooldowns in maps: if not isinstance(cooldowns, dict): continue @@ -75,7 +75,7 @@ class CredentialPoolModelCooldownMixin: ) def _cool_down_model( - self, entry: "PooledCredential", model: str, error_context: Optional[Dict[str, Any]], + self, entry: "PooledCredential", model: str, error_context: Optional[dict[str, Any]], failure_reason: Optional[str] = None, ) -> None: """Record a cooldown for *model* on *entry* and every sibling sharing its key. @@ -103,7 +103,7 @@ class CredentialPoolModelCooldownMixin: self._adopt(scoped, persist=False, model_cooldowns=cooldowns) self._persist() - def limit_state(self, models: Iterable[str]) -> Optional[Dict[str, Any]]: + def limit_state(self, models: Iterable[str]) -> Optional[dict[str, Any]]: """What a picker should say about this pool's rate limits, or ``None`` when nothing is limited. ``{"scope": "account", "resets_at": epoch}`` when every live entry is benched credential-wide @@ -123,7 +123,7 @@ class CredentialPoolModelCooldownMixin: if live and len(benched) == len(live): return {"scope": "account", "resets_at": min(benched.values())} usable = [entry for entry in live if entry.id not in benched] - cooled: Dict[str, float] = {} + cooled: dict[str, float] = {} for model in models: waits = [model_cooldown_until(entry, model) for entry in usable] if waits and all(waits) and min(waits) - now < MODEL_ENTITLEMENT_BENCH_SECONDS / 2: diff --git a/agent/credential_pool_plugin.py b/agent/credential_pool_plugin.py index cb6e93f1c3..e103f45af9 100644 --- a/agent/credential_pool_plugin.py +++ b/agent/credential_pool_plugin.py @@ -79,7 +79,7 @@ def is_terminal_plugin_refresh_error(exc: BaseException) -> bool: def recover_failed_plugin_refresh( pool: "CredentialPool", entry: "PooledCredential", exc: Exception, -) -> Tuple[bool, Optional["PooledCredential"]]: +) -> tuple[bool, Optional["PooledCredential"]]: """Recovery for a plugin hook that raised: adopt a peer's rotation, or quarantine a dead grant. Returns ``(handled, result)``; ``handled=False`` means the caller should bench the row as a diff --git a/agent/credential_sources.py b/agent/credential_sources.py index 7c204a9785..36649a1b79 100644 --- a/agent/credential_sources.py +++ b/agent/credential_sources.py @@ -59,8 +59,8 @@ class RemovalResult: ``load_pool`` skips the source; only ``manual`` entries legitimately use False. """ - cleaned: List[str] = field(default_factory=list) - hints: List[str] = field(default_factory=list) + cleaned: list[str] = field(default_factory=list) + hints: list[str] = field(default_factory=list) suppress: bool = True @@ -224,7 +224,7 @@ def _suppress_only(*hints: str) -> Callable[..., RemovalResult]: # ORDER MATTERS — ``find_removal_step`` returns the first match. Provider- # specific steps precede the generic ``env:*`` step so copilot's ``env:GH_TOKEN`` # takes the copilot path (no .env edits) rather than the generic env-var removal. -_REGISTRY: List[RemovalStep] = [ +_REGISTRY: list[RemovalStep] = [ RemovalStep( provider="copilot", source_id="gh_cli", match_fn=lambda src: src == "gh_cli" or src.startswith("env:"), diff --git a/agent/curator.py b/agent/curator.py index 122cab3b40..c05c148cd4 100644 --- a/agent/curator.py +++ b/agent/curator.py @@ -41,8 +41,8 @@ def _state_file() -> Path: return get_hermes_home() / "skills" / ".curator_state" -def load_state() -> Dict[str, Any]: - base: Dict[str, Any] = { +def load_state() -> dict[str, Any]: + base: dict[str, Any] = { "last_run_at": None, "last_run_duration_seconds": None, "last_run_summary": None, "last_run_summary_shown_at": None, "last_report_path": None, "paused": False, "run_count": 0, } @@ -57,7 +57,7 @@ def load_state() -> Dict[str, Any]: return base -def save_state(data: Dict[str, Any]) -> None: +def save_state(data: dict[str, Any]) -> None: try: atomic_json_write(_state_file(), data, indent=2, sort_keys=True) except Exception as e: @@ -74,14 +74,14 @@ def is_paused() -> bool: # --- Config access --- -def _subdict(node: Any, *keys: str) -> Dict[str, Any]: +def _subdict(node: Any, *keys: str) -> dict[str, Any]: """Walk nested dict keys; {} if any level is missing or not a dict.""" for key in keys: node = node.get(key) if isinstance(node, dict) else None return node if isinstance(node, dict) else {} -def _read_config_section(*path: str, label: str, log: logging.Logger = logger) -> Dict[str, Any]: +def _read_config_section(*path: str, label: str, log: logging.Logger = logger) -> dict[str, Any]: """Read a nested section of ~/.hermes/config.yaml. Tolerates missing file.""" try: from hermes_cli.config import load_config_readonly @@ -92,7 +92,7 @@ def _read_config_section(*path: str, label: str, log: logging.Logger = logger) - return _subdict(cfg, *path) -def _load_config() -> Dict[str, Any]: +def _load_config() -> dict[str, Any]: return _read_config_section("curator", label="curator") @@ -181,7 +181,7 @@ def should_run_now(now: Optional[datetime] = None) -> bool: # --- Automatic state transitions (pure function, no LLM) --- -def _cron_referenced_skills() -> Set[str]: +def _cron_referenced_skills() -> set[str]: """Skill names referenced by any cron job (incl. paused/disabled). Best-effort: a cron import error or corrupt jobs store yields an empty set, never a crash.""" try: from cron.jobs import referenced_skill_names as _refs @@ -206,7 +206,7 @@ def _archive_as_curator(_u, name: str) -> bool: reset_ledger_actor(tok) -def apply_automatic_transitions(now: Optional[datetime] = None) -> Dict[str, int]: +def apply_automatic_transitions(now: Optional[datetime] = None) -> dict[str, int]: """Move every curator-managed skill between active/stale/archived based on its latest real activity; pinned skills are never touched. Built-ins are seeded with a baseline record on first sight so their inactivity clock starts NOW, not at epoch. Returns a counter dict.""" @@ -487,7 +487,7 @@ def _needle_in_path_component(needle: str, path: str) -> bool: return any(part and part.rsplit(".", 1)[0].replace("-", "_") == norm_needle for part in path.replace("\\", "/").split("/")) -def _skill_manage_args(tc: Any, *, raw_fallback: bool) -> Optional[Dict[str, Any]]: +def _skill_manage_args(tc: Any, *, raw_fallback: bool) -> Optional[dict[str, Any]]: """Parsed arguments of a ``skill_manage`` tool call (JSON string or dict), or None to skip. With *raw_fallback*, a malformed string yields ``{"_raw": raw}`` so substring matching still catches the common case.""" if not isinstance(tc, dict) or tc.get("name") != "skill_manage": @@ -502,7 +502,7 @@ def _skill_manage_args(tc: Any, *, raw_fallback: bool) -> Optional[Dict[str, Any return args if isinstance(args, dict) else None -def _find_reference(args: Dict[str, Any], needles: Set[str]) -> Optional[str]: +def _find_reference(args: dict[str, Any], needles: set[str]) -> Optional[str]: """First argument value (file_path, file_content, content, new_string, _raw — in that order) that references one of *needles*. ``file_path`` must match a whole path component; content fields match on word boundaries so "test" does not match "latest".""" for key in ("file_path", "file_content", "content", "new_string", "_raw"): @@ -515,13 +515,13 @@ def _find_reference(args: Dict[str, Any], needles: Set[str]) -> Optional[str]: def _classify_removed_skills( - removed: List[str], added: List[str], after_names: Set[str], tool_calls: List[Dict[str, Any]], -) -> Dict[str, List[Dict[str, Any]]]: + removed: list[str], added: list[str], after_names: set[str], tool_calls: list[dict[str, Any]], +) -> dict[str, list[dict[str, Any]]]: """Split ``removed`` into consolidated vs pruned. Heuristic: a ``skill_manage`` call on a DIFFERENT, surviving-or-new skill whose file_path/content arguments reference the removed name is the "absorbed" signal; earliest match wins. Returns ``{"consolidated": [{name, into, evidence}], "pruned": [{name}]}``.""" - consolidated: List[Dict[str, Any]] = [] - pruned: List[Dict[str, Any]] = [] + consolidated: list[dict[str, Any]] = [] + pruned: list[dict[str, Any]] = [] parsed_calls = [a for a in (_skill_manage_args(tc, raw_fallback=True) for tc in tool_calls or []) if a is not None] destinations = set(after_names) | set(added or []) for name in filter(None, removed): @@ -541,7 +541,7 @@ def _classify_removed_skills( return {"consolidated": consolidated, "pruned": pruned} -def _parse_structured_summary(llm_final: str) -> Dict[str, List[Dict[str, str]]]: +def _parse_structured_summary(llm_final: str) -> dict[str, list[dict[str, str]]]: """Extract the required fenced ```yaml block (``consolidations:`` / ``prunings:`` lists) from the curator's final response. Tolerant: missing block or malformed YAML → empty lists (caller falls back to the tool-call heuristic); a partial block returns what parsed. Returns ``{"consolidations": [{from, into, reason}], "prunings": [{name, reason}]}``.""" @@ -557,7 +557,7 @@ def _parse_structured_summary(llm_final: str) -> Dict[str, List[Dict[str, str]]] if not isinstance(data, dict): return {"consolidations": [], "prunings": []} - def _entries(key: str, *fields: str) -> List[Dict[str, str]]: + def _entries(key: str, *fields: str) -> list[dict[str, str]]: raw = data.get(key) or [] entries = (e for e in raw if isinstance(e, dict)) if isinstance(raw, list) else () cleaned = ({f: (v.strip() if isinstance((v := e.get(f)), str) else "") for f in (*fields, "reason")} for e in entries) @@ -566,11 +566,11 @@ def _parse_structured_summary(llm_final: str) -> Dict[str, List[Dict[str, str]]] return {"consolidations": _entries("consolidations", "from", "into"), "prunings": _entries("prunings", "name")} -def _extract_absorbed_into_declarations(tool_calls: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: +def _extract_absorbed_into_declarations(tool_calls: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: """Model-declared absorption targets from ``skill_manage(action='delete')`` calls — the authoritative classification signal (beats YAML parsing and substring heuristics). Returns ``{name: {"into": umbrella | "", "declared": True}}``; ``into == ""`` is an explicit prune. Deletes omitting ``absorbed_into`` are absent so the caller falls back to heuristic/YAML (older runs).""" - out: Dict[str, Dict[str, Any]] = {} + out: dict[str, dict[str, Any]] = {} for args in (_skill_manage_args(tc, raw_fallback=False) for tc in tool_calls or []): if args is not None and args.get("action") == "delete": name, target = args.get("name"), args.get("absorbed_into") @@ -580,9 +580,9 @@ def _extract_absorbed_into_declarations(tool_calls: List[Dict[str, Any]]) -> Dic def _reconcile_classification( - removed: List[str], heuristic: Dict[str, List[Dict[str, Any]]], model_block: Dict[str, List[Dict[str, str]]], - destinations: Set[str], absorbed_declarations: Optional[Dict[str, Dict[str, Any]]] = None, -) -> Dict[str, List[Dict[str, Any]]]: + removed: list[str], heuristic: dict[str, list[dict[str, Any]]], model_block: dict[str, list[dict[str, str]]], + destinations: set[str], absorbed_declarations: Optional[dict[str, dict[str, Any]]] = None, +) -> dict[str, list[dict[str, Any]]]: """Merge heuristic (tool-call evidence) with the model's structured block. First match wins; every removed skill lands in exactly one bucket: - ``absorbed_into`` declared at delete is authoritative: existing target → consolidated; ``""`` → pruned; missing target → fall through. @@ -594,8 +594,8 @@ def _reconcile_classification( model_cons = {e["from"]: e for e in model_block.get("consolidations", [])} model_pruned = {e["name"]: e for e in model_block.get("prunings", [])} declared = absorbed_declarations or {} - consolidated: List[Dict[str, Any]] = [] - pruned: List[Dict[str, Any]] = [] + consolidated: list[dict[str, Any]] = [] + pruned: list[dict[str, Any]] = [] for name in removed: mc, mp, hc, dec = model_cons.get(name), model_pruned.get(name), heur_cons.get(name), declared.get(name) mc_reason = (mc.get("reason") or "") if mc else "" @@ -626,14 +626,14 @@ def _reconcile_classification( class _RunDiff(NamedTuple): - after_names: Set[str] - removed: List[str] - added: List[str] - consolidated: List[Dict[str, Any]] - pruned: List[Dict[str, Any]] + after_names: set[str] + removed: list[str] + added: list[str] + consolidated: list[dict[str, Any]] + pruned: list[dict[str, Any]] -def _diff_and_classify(before_names: Set[str], after_names: Set[str], tool_calls: List[Dict[str, Any]], model_final: str) -> _RunDiff: +def _diff_and_classify(before_names: set[str], after_names: set[str], tool_calls: list[dict[str, Any]], model_final: str) -> _RunDiff: """Diff the before/after skill sets and classify every removal: the model's YAML block carries intent + rationale, the tool-call heuristic audits for hallucinated umbrellas/omissions, per-delete ``absorbed_into`` beats both.""" removed, added = sorted(before_names - after_names), sorted(after_names - before_names) @@ -646,11 +646,11 @@ def _diff_and_classify(before_names: Set[str], after_names: Set[str], tool_calls return _RunDiff(after_names, removed, added, classification["consolidated"], classification["pruned"]) -def _by_name(report: List[Dict[str, Any]]) -> Dict[Any, Dict[str, Any]]: +def _by_name(report: list[dict[str, Any]]) -> dict[Any, dict[str, Any]]: return {r.get("name"): r for r in report if isinstance(r, dict)} -def _build_rename_summary(*, before_names: Set[str], after_report: List[Dict[str, Any]], tool_calls: List[Dict[str, Any]], model_final: str) -> str: +def _build_rename_summary(*, before_names: set[str], after_report: list[dict[str, Any]], tool_calls: list[dict[str, Any]], model_final: str) -> str: """The "where did my skills go?" lines appended to the user-visible ``final_summary``; "" when nothing was archived. Capped at 10 entries so a big consolidation doesn't flood agent.log (full list is in REPORT.md); the pin hint appears only when a consolidation produced an umbrella.""" @@ -672,7 +672,7 @@ def _build_rename_summary(*, before_names: Set[str], after_report: List[Dict[str return "\n".join(lines) -def _rewrite_cron_refs(consolidated: List[Dict[str, Any]], pruned: List[Dict[str, Any]]) -> Dict[str, Any]: +def _rewrite_cron_refs(consolidated: list[dict[str, Any]], pruned: list[dict[str, Any]]) -> dict[str, Any]: """Point cron jobs at the umbrella when the curator consolidated a skill they list — otherwise the scheduler fails to load it and the job runs without its instructions. Best-effort: a cron-module issue never breaks the curator.""" try: @@ -697,8 +697,8 @@ def _write_file(path: Path, label: str, render: Any) -> None: def _write_run_report( - *, started_at: datetime, elapsed_seconds: float, auto_counts: Dict[str, int], auto_summary: str, - before_report: List[Dict[str, Any]], before_names: Set[str], after_report: List[Dict[str, Any]], llm_meta: Dict[str, Any], + *, started_at: datetime, elapsed_seconds: float, auto_counts: dict[str, int], auto_summary: str, + before_report: list[dict[str, Any]], before_names: set[str], after_report: list[dict[str, Any]], llm_meta: dict[str, Any], ) -> Optional[Path]: """Write run.json + REPORT.md under logs/curator/{YYYYMMDD-HHMMSS}[-N]/ (N disambiguates a crash-rerun in the same second). Returns the report dir, or None if it couldn't be created (reporting is best-effort).""" @@ -717,7 +717,7 @@ def _write_run_report( diff = _diff_and_classify(before_names, set(after_by_name), tool_calls, llm_meta.get("final", "") or "") states = ((n, (before_by_name.get(n) or {}).get("state"), (after_by_name.get(n) or {}).get("state")) for n in sorted(diff.after_names & before_names)) transitions = [{"name": n, "from": b, "to": a} for n, b, a in states if b and a and b != a] - tc_counts: Dict[str, int] = dict(Counter(tc.get("name", "unknown") for tc in tool_calls)) + tc_counts: dict[str, int] = dict(Counter(tc.get("name", "unknown") for tc in tool_calls)) cron_rewrites = _rewrite_cron_refs(diff.consolidated, diff.pruned) jobs_updated = int(cron_rewrites.get("jobs_updated", 0)) payload = { @@ -741,11 +741,11 @@ def _write_run_report( return run_dir -def _reason_suffix(entry: Dict[str, Any]) -> str: +def _reason_suffix(entry: dict[str, Any]) -> str: return f" — {reason}" if (reason := (entry.get("reason") or "").strip()) else "" -def _consolidated_lines(entry: Dict[str, Any]) -> List[str]: +def _consolidated_lines(entry: dict[str, Any]) -> list[str]: line = f"- `{entry.get('name', '?')}` → merged into `{entry.get('into', '?')}`" + _reason_suffix(entry) source = entry.get("source", "") if source and source.startswith("tool-call audit"): @@ -755,12 +755,12 @@ def _consolidated_lines(entry: Dict[str, Any]) -> List[str]: if entry.get("model_claimed_into") else []) -def _pruned_lines(entry: Any) -> List[str]: +def _pruned_lines(entry: Any) -> list[str]: # Reconciler entries are dicts {name, source, reason}; tolerate bare strings (older format). return [f"- `{entry.get('name', '?')}`" + _reason_suffix(entry) if isinstance(entry, dict) else f"- `{entry}`"] -def _cron_rewrite_lines(entry: Dict[str, Any]) -> List[str]: +def _cron_rewrite_lines(entry: dict[str, Any]) -> list[str]: job_name = entry.get("job_name") or entry.get("job_id") or "?" head = f"- `{job_name}`: `{', '.join(entry.get('before') or [])}` → `{', '.join(entry.get('after') or []) or '(none)'}`" return ([head] + [f" - `{old}` → `{new}` (consolidated)" for old, new in (entry.get("mapped") or {}).items()] @@ -787,7 +787,7 @@ _REPORT_SECTIONS = ( ) -def _render_report_markdown(p: Dict[str, Any]) -> str: +def _render_report_markdown(p: dict[str, Any]) -> str: """Render the human-readable REPORT.md.""" mins, secs = divmod(int(p.get("duration_seconds", 0) or 0), 60) dur_label = f"{mins}m {secs}s" if mins else f"{secs}s" @@ -861,7 +861,7 @@ def _render_candidate_list() -> str: ]) -def _llm_meta(summary: str, error: Optional[str] = None) -> Dict[str, Any]: +def _llm_meta(summary: str, error: Optional[str] = None) -> dict[str, Any]: """Structured result of an LLM pass that did not run (skipped or failed).""" return {"final": "", "summary": summary, "model": "", "provider": "", "tool_calls": [], "error": error} @@ -872,13 +872,13 @@ def _notify(on_summary: Optional[Callable[[str], None]], message: str) -> None: on_summary(message) -def _safe_curated_report() -> List[Dict[str, Any]]: +def _safe_curated_report() -> list[dict[str, Any]]: with contextlib.suppress(Exception): return skill_usage.curated_report() return [] -def _consolidation_pass(prefix: str, auto_summary: str, dry_run: bool, before_names: Set[str]) -> tuple: +def _consolidation_pass(prefix: str, auto_summary: str, dry_run: bool, before_names: set[str]) -> tuple: """The LLM half of a run: fork (unless no candidates), then append the rename map (`old-name → umbrella`) so users needn't dig into REPORT.md. Returns ``(final_summary, llm_meta)``; never raises.""" try: @@ -915,7 +915,7 @@ def _consolidation_pass(prefix: str, auto_summary: str, dry_run: bool, before_na def run_curator_review( on_summary: Optional[Callable[[str], None]] = None, synchronous: bool = False, dry_run: bool = False, consolidate: Optional[bool] = None, trigger: str = "manual", -) -> Dict[str, Any]: +) -> dict[str, Any]: """Execute a single curator review pass: (1) automatic state transitions (no LLM); (2) if *consolidate* and there are candidates, fork an AIAgent on the review prompt; (3) update .curator_state; (4) call *on_summary*. *synchronous* runs the LLM review in the calling thread (default: daemon thread). *consolidate* ``None`` reads @@ -1005,7 +1005,7 @@ def run_curator_review( def _record_run_metric(trigger, dry_run, counts, before_report, after_report, llm_meta, hermes_home) -> None: from hermes_cli.observability.shared_metrics_loop import record_curator_run - def what_changed() -> Dict[str, int]: + def what_changed() -> dict[str, int]: before, after = _by_name(before_report), _by_name(after_report) diff = _diff_and_classify(set(before), set(after), llm_meta.get("tool_calls") or [], llm_meta.get("final") or "") patched = sum(int(after[n].get("patch_count") or 0) > int(before[n].get("patch_count") or 0) @@ -1023,10 +1023,10 @@ class _ReviewRuntimeBinding(NamedTuple): model: str explicit_api_key: Optional[str] explicit_base_url: Optional[str] - request_overrides: Dict[str, Any] + request_overrides: dict[str, Any] -def _merge_request_overrides(runtime_overrides: Any, slot_extra_body: Any) -> Dict[str, Any]: +def _merge_request_overrides(runtime_overrides: Any, slot_extra_body: Any) -> dict[str, Any]: """Merge resolver metadata with task-local request body fields.""" merged = dict(runtime_overrides or {}) if isinstance(slot_extra_body, dict) and slot_extra_body: @@ -1034,13 +1034,13 @@ def _merge_request_overrides(runtime_overrides: Any, slot_extra_body: Any) -> Di return merged -def _resolve_review_runtime(cfg: Dict[str, Any]) -> _ReviewRuntimeBinding: +def _resolve_review_runtime(cfg: dict[str, Any]) -> _ReviewRuntimeBinding: """Curator is a regular auxiliary task slot (``auxiliary.curator.*``), so it rides the canonical aux-model plumbing. Precedence: 1. ``auxiliary.curator.{provider,model}`` when both are set non-auto 2. Legacy ``curator.auxiliary.{provider,model}`` (deprecated) when both set 3. Main ``model.{provider,default/model}`` pair ("auto" + "" = main chat model) Non-empty slot ``api_key``/``base_url`` are returned as explicit overrides so ``resolve_runtime_provider`` doesn't reuse the main chat credential chain.""" - def _slot(provider: str, model: str, slot: Dict[str, Any]) -> _ReviewRuntimeBinding: + def _slot(provider: str, model: str, slot: dict[str, Any]) -> _ReviewRuntimeBinding: api_key, base_url = ((str(v).strip() or None) if v is not None else None for v in (slot.get("api_key"), slot.get("base_url"))) return _ReviewRuntimeBinding(provider, model, api_key, base_url, _merge_request_overrides({}, slot.get("extra_body"))) @@ -1061,7 +1061,7 @@ def _resolve_review_provider() -> tuple: """``(runtime_provider, model_name, provider_name, request_overrides)`` resolved the way the CLI does: AIAgent() without explicit provider/model hits an auto-resolution path that fails for OAuth-only providers and pooled credentials (HTTP 400 "No models provided"). Never raises.""" - rp: Dict[str, Any] = {} + rp: dict[str, Any] = {} overrides, provider, model_name, binding = {}, None, "", None try: from hermes_cli.config import load_config_readonly @@ -1082,10 +1082,10 @@ def _resolve_review_provider() -> tuple: return rp, model_name, provider, overrides -def _run_llm_review(prompt: str) -> Dict[str, Any]: +def _run_llm_review(prompt: str) -> dict[str, Any]: """Spawn an AIAgent fork on the review prompt. Returns ``final`` (untruncated response), ``summary`` (240-char cap), ``model``/``provider`` (what ran), ``tool_calls`` ([{name, arguments}], truncated) and ``error``. Never raises.""" - result_meta: Dict[str, Any] = _llm_meta("") + result_meta: dict[str, Any] = _llm_meta("") try: from run_agent import AIAgent except Exception as e: @@ -1095,7 +1095,7 @@ def _run_llm_review(prompt: str) -> Dict[str, Any]: result_meta["model"], result_meta["provider"] = model_name, provider or "" review_agent = None try: - agent_kwargs: Dict[str, Any] = {} + agent_kwargs: dict[str, Any] = {} acp_command = rp.get("command") if isinstance(acp_command, str) and acp_command: agent_kwargs.update(acp_command=acp_command, acp_args=list(rp.get("args") or [])) @@ -1195,7 +1195,7 @@ def _release_run_claim() -> None: _run_claim_path().unlink() -def maybe_run_curator(*, idle_for_seconds: Optional[float] = None, on_summary: Optional[Callable[[str], None]] = None) -> Optional[Dict[str, Any]]: +def maybe_run_curator(*, idle_for_seconds: Optional[float] = None, on_summary: Optional[Callable[[str], None]] = None) -> Optional[dict[str, Any]]: """Best-effort: run a curator pass if all gates pass. Returns the result dict if a pass was started, else None. Never raises.""" try: # Idle gating: only enforce when the caller provided a measurement. diff --git a/agent/curator_backup.py b/agent/curator_backup.py index ed9be7fba0..07cefe3946 100644 --- a/agent/curator_backup.py +++ b/agent/curator_backup.py @@ -74,11 +74,11 @@ def _jobs_list(parsed: Any) -> Optional[list]: return parsed if isinstance(parsed, list) else None -def _backup_cron_jobs_into(dest: Path) -> Dict[str, Any]: +def _backup_cron_jobs_into(dest: Path) -> dict[str, Any]: """Copy the live ``~/.hermes/cron/jobs.json`` into ``dest`` as ``cron-jobs.json``. Never raises: a missing/unreadable file yields ``backed_up=False`` plus a reason, and the snapshot proceeds.""" src = get_hermes_home() / "cron" / "jobs.json" - info: Dict[str, Any] = {"backed_up": False, "jobs_count": 0} + info: dict[str, Any] = {"backed_up": False, "jobs_count": 0} if not src.exists(): return {**info, "reason": "no cron/jobs.json present"} try: @@ -106,7 +106,7 @@ def _utc_id(now: Optional[datetime] = None) -> str: return s.removesuffix("+00:00").replace(":", "-") + "Z" -def _load_config() -> Dict[str, Any]: +def _load_config() -> dict[str, Any]: return _read_config_section("curator", "backup", label="curator backup", log=logger) @@ -130,8 +130,8 @@ def _count_skill_files(base: Path) -> int: return 0 -def _write_manifest(dest: Path, reason: str, archive_path: Path, skills_counted: int, cron_info: Dict[str, Any]) -> None: - cron_jobs: Dict[str, Any] = {"backed_up": bool(cron_info.get("backed_up", False)), "jobs_count": int(cron_info.get("jobs_count", 0))} +def _write_manifest(dest: Path, reason: str, archive_path: Path, skills_counted: int, cron_info: dict[str, Any]) -> None: + cron_jobs: dict[str, Any] = {"backed_up": bool(cron_info.get("backed_up", False)), "jobs_count": int(cron_info.get("jobs_count", 0))} if not cron_info.get("backed_up"): cron_jobs["reason"] = cron_info.get("reason", "not captured") if cron_info.get("parse_warning"): @@ -150,7 +150,7 @@ def _mkdir(path: Path, what: str, *, exist_ok: bool) -> bool: return False -def snapshot_skills(reason: str = "manual", *, protect_ids: Optional[Set[str]] = None) -> Optional[Path]: +def snapshot_skills(reason: str = "manual", *, protect_ids: Optional[set[str]] = None) -> Optional[Path]: """Create a tar.gz snapshot of ``~/.hermes/skills/`` and prune old ones. Returns the snapshot dir, or None when skipped (disabled, skills dir missing, IO error) — logged at debug so the curator never aborts a pass over a backup failure. ``protect_ids`` survive the prune step (rollback protects its target).""" @@ -192,12 +192,12 @@ def snapshot_skills(reason: str = "manual", *, protect_ids: Optional[Set[str]] = return dest -def prune_old_snapshots() -> List[str]: +def prune_old_snapshots() -> list[str]: """Apply ``curator.backup.keep`` without taking a new snapshot (the prune-only curator pass).""" return _prune_old(keep=get_keep()) -def _prune_old(keep: int, protect: Optional[Set[str]] = None) -> List[str]: +def _prune_old(keep: int, protect: Optional[set[str]] = None) -> list[str]: """Delete regular snapshots beyond the newest *keep*; returns deleted ids. Ids in *protect* are never deleted — rollback() uses this so the mandatory pre-rollback safety snapshot cannot evict the snapshot being restored. Stale ``.rollback-staging-*`` dirs (crashed rollback) are cleaned up on every call; ``.rollback-unrestored-*`` is kept on purpose.""" @@ -210,7 +210,7 @@ def _prune_old(keep: int, protect: Optional[Set[str]] = None) -> List[str]: entries = sorted((c for c in dirs if _ID_RE.match(c.name)), key=lambda c: c.name, reverse=True) doomed = [(p, "prune") for p in entries[keep:] if p.name not in protect] doomed += [(p, "clean stale staging dir") for p in dirs if p.name.startswith(_STAGING_PREFIX)] - deleted: List[str] = [] + deleted: list[str] = [] for path, what in doomed: try: shutil.rmtree(path) @@ -222,7 +222,7 @@ def _prune_old(keep: int, protect: Optional[Set[str]] = None) -> List[str]: # --- List + rollback --- -def _read_manifest(snap_dir: Path) -> Dict[str, Any]: +def _read_manifest(snap_dir: Path) -> dict[str, Any]: try: return json.loads((snap_dir / "manifest.json").read_text(encoding="utf-8-sig")) except (OSError, json.JSONDecodeError): @@ -234,15 +234,15 @@ def _is_restorable(child: Path) -> bool: return bool(child.is_dir() and _ID_RE.match(child.name) and (child / _ARCHIVE_NAME).exists()) -def _restorable_snapshots() -> List[Path]: +def _restorable_snapshots() -> list[Path]: """Restorable snapshot dirs, newest first.""" backups = _backups_dir() return [c for c in sorted(backups.iterdir(), reverse=True) if _is_restorable(c)] if backups.exists() else [] -def list_backups() -> List[Dict[str, Any]]: +def list_backups() -> list[dict[str, Any]]: """All restorable snapshots (manifest dicts), newest first.""" - out: List[Dict[str, Any]] = [] + out: list[dict[str, Any]] = [] for child in _restorable_snapshots(): mf = {"id": child.name, "path": str(child), **_read_manifest(child)} try: @@ -261,12 +261,12 @@ def _resolve_backup(backup_id: Optional[str]) -> Optional[Path]: return next(iter(_restorable_snapshots()), None) -def _restore_cron_skill_links(snapshot_dir: Path) -> Dict[str, Any]: +def _restore_cron_skill_links(snapshot_dir: Path) -> dict[str, Any]: """Reconcile backed-up cron skill links into the live ``cron/jobs.json``. Only ``skills``/``skill`` are restored, and only on jobs that still exist live (by ``id``) — everything else is live state. Backup-only jobs are skipped and reported; live-only jobs untouched. Never raises; writes through ``cron.jobs`` under the scheduler's lock so we don't race tick().""" - report: Dict[str, Any] = {"attempted": False, "restored": [], "skipped_missing": [], "unchanged": 0, "error": None} + report: dict[str, Any] = {"attempted": False, "restored": [], "skipped_missing": [], "unchanged": 0, "error": None} backup_file = snapshot_dir / CRON_JOBS_FILENAME if not backup_file.exists(): return {**report, "error": f"snapshot has no {CRON_JOBS_FILENAME}"} @@ -278,7 +278,7 @@ def _restore_cron_skill_links(snapshot_dir: Path) -> Dict[str, Any]: return {**report, "error": "backed-up cron-jobs.json has no jobs list"} # Backed-up skill state keyed by job id (legacy single + modern list field). - backup_by_id: Dict[str, Dict[str, Any]] = { + backup_by_id: dict[str, dict[str, Any]] = { job["id"]: {"skills": job.get("skills"), "skill": job.get("skill"), "name": job.get("name") or job["id"]} for job in backup_jobs if isinstance(job, dict) and isinstance(job.get("id"), str) and job.get("id") } @@ -320,7 +320,7 @@ def _restore_cron_skill_links(snapshot_dir: Path) -> Dict[str, Any]: for jid, b in backup_by_id.items() if jid not in live_ids] if changed: save_jobs(live_jobs) - except Exception as e: # noqa: BLE001 — rollback must not die mid-restore + except Exception as e: logger.debug("Cron skill-link restore failed: %s", e, exc_info=True) report["error"] = f"restore failed mid-flight: {e}" return report @@ -333,7 +333,7 @@ def _remove_entry(entry: Path) -> None: entry.unlink() -def _restore_excluded_subtrees(staged: Path, skills: Path) -> List[str]: +def _restore_excluded_subtrees(staged: Path, skills: Path) -> list[str]: """Move excluded entries (nested ``.git``/``.hub``/...) from *staged* back under *skills* after a successful extract. Snapshots never contain these, so the staged copy of the live tree is the only source. ``.git`` may be a dir or a file (submodule / worktree ``gitdir:`` pointer) — both are moved. Best-effort and conditional: an entry is carried only when @@ -342,7 +342,7 @@ def _restore_excluded_subtrees(staged: Path, skills: Path) -> List[str]: Returns entries whose move failed, so their only surviving copies stay staged for recovery.""" from tools.skill_ledger import TRANSIENT_DIRS - failed: List[str] = [] + failed: list[str] = [] for dirpath, dirnames, filenames in os.walk(staged): carried = [Path(dirpath) / n for n in filenames if n in _EXCLUDE_TOP_LEVEL] carried += [Path(dirpath) / n for n in dirnames if n in _EXCLUDE_TOP_LEVEL or n in TRANSIENT_DIRS] @@ -358,7 +358,7 @@ def _restore_excluded_subtrees(staged: Path, skills: Path) -> List[str]: return failed -def _retain_staging(staged: Path, unrestored: List[str], reason: str) -> Tuple[bool, str, None]: +def _retain_staging(staged: Path, unrestored: list[str], reason: str) -> tuple[bool, str, None]: """Keep a staging dir that still holds the only copy of *unrestored* entries and build rollback()'s failure result. The dir is renamed out of the prunable prefix (unique suffix on a same-second collision). If the rename fails, the original path is reported so recovery never raises out of rollback().""" @@ -375,11 +375,11 @@ def _retain_staging(staged: Path, unrestored: List[str], reason: str) -> Tuple[b return (False, f"{reason} - could not restore {', '.join(sorted(unrestored))}; staged copies kept at {kept}{note}", None) -def _unstage(moved: List[Tuple[Path, Path]]) -> List[str]: +def _unstage(moved: list[tuple[Path, Path]]) -> list[str]: """Move staged entries back to their original paths; returns names that could not be restored. ``shutil.move`` moves *into* an existing destination dir, so partial-extract debris would bury the real skill (``skills/foo/foo/``) — clear each original path first. The staged copy is authoritative.""" - failed: List[str] = [] + failed: list[str] = [] for orig, dest in moved: try: _remove_entry(orig) @@ -389,7 +389,7 @@ def _unstage(moved: List[Tuple[Path, Path]]) -> List[str]: return failed -def _cron_summary(cron_report: Dict[str, Any]) -> Optional[str]: +def _cron_summary(cron_report: dict[str, Any]) -> Optional[str]: if not cron_report.get("attempted"): return None if cron_report.get("error"): @@ -403,7 +403,7 @@ def _cron_summary(cron_report: Dict[str, Any]) -> Optional[str]: return "cron links: " + ", ".join(parts) if parts else None -def rollback(backup_id: Optional[str] = None) -> Tuple[bool, str, Optional[Path]]: +def rollback(backup_id: Optional[str] = None) -> tuple[bool, str, Optional[Path]]: """Restore ``~/.hermes/skills/`` from a snapshot (explicit id or newest): safety-snapshot the CURRENT tree; stage current top-level entries; extract; on failure move staged entries back. Returns ``(ok, message, snapshot_path)``.""" target = _resolve_backup(backup_id) @@ -433,7 +433,7 @@ def rollback(backup_id: Optional[str] = None) -> Tuple[bool, str, Optional[Path] except OSError as e: return (False, f"failed to create staging dir: {e}", None) - moved: List[Tuple[Path, Path]] = [] + moved: list[tuple[Path, Path]] = [] try: for entry in list(skills.iterdir()): if entry.name not in _EXCLUDE_TOP_LEVEL: diff --git a/agent/display.py b/agent/display.py index 2a4574ec2b..c7cdc19ce3 100644 --- a/agent/display.py +++ b/agent/display.py @@ -546,7 +546,7 @@ def tool_labels_for_call(tool_name: str, args: dict | None) -> list: try: from tools.tool_labels import labels_for_call labels = labels_for_call(tool_name, args or {}) - except Exception as exc: # noqa: BLE001 — display must never abort a turn + except Exception as exc: logger.debug("bridge labels failed for %s: %s", tool_name, exc) return [] skin = _get_skin() @@ -1247,7 +1247,7 @@ def get_cute_tool_message(tool_name: str, args: dict, duration: float, result: s """Render a completion label without letting cosmetic failures escape.""" try: return _get_cute_tool_message(tool_name, args, duration, result=result) - except Exception as exc: # noqa: BLE001 — display must never abort a turn + except Exception as exc: logger.debug("Tool completion label failed for %s: %s", tool_name, exc) safe_name = tool_name[:9] if isinstance(tool_name, str) and tool_name else t("display.cute.fallback_tool_name") safe_duration = f"{duration:.1f}s" if isinstance(duration, (int, float)) else t("display.cute.fallback_done") diff --git a/agent/empty_response_guard.py b/agent/empty_response_guard.py index 1d8a563db0..8e9dfd352c 100644 --- a/agent/empty_response_guard.py +++ b/agent/empty_response_guard.py @@ -81,7 +81,7 @@ class EmptyAttempt: return (self.model, self.provider, self.finish_reason) -def resolve_guard_settings(section: Any) -> Tuple[bool, Decimal]: +def resolve_guard_settings(section: Any) -> tuple[bool, Decimal]: """Resolve ``agent.empty_response_guard`` into (enabled, threshold); malformed input → schema defaults.""" if not isinstance(section, dict): return (DEFAULT_GUARD_ENABLED, DEFAULT_COST_THRESHOLD_USD) @@ -99,7 +99,7 @@ def resolve_guard_settings(section: Any) -> Tuple[bool, Decimal]: candidate = Decimal(str(threshold_raw)) if candidate > 0: threshold = candidate - except Exception: # noqa: BLE001 — malformed config must not break init + except Exception: logger.debug("empty-guard: invalid cost_threshold_usd %r, using default", threshold_raw) return (enabled, threshold) @@ -115,7 +115,7 @@ def _cost_threshold_usd(agent: Any) -> Decimal: return value if isinstance(value, Decimal) and value > 0 else DEFAULT_COST_THRESHOLD_USD -def _attempts(agent: Any) -> List[EmptyAttempt]: +def _attempts(agent: Any) -> list[EmptyAttempt]: attempts = getattr(agent, _ATTEMPTS_ATTR, None) if attempts is None: attempts = [] @@ -132,7 +132,7 @@ def _normalized_usage(agent: Any, response: Any, what: str) -> Any: from agent.usage_pricing import normalize_usage return normalize_usage(raw_usage, provider=getattr(agent, "provider", None), api_mode=getattr(agent, "api_mode", None)) - except Exception: # noqa: BLE001 — pricing must never break the loop + except Exception: logger.debug("empty-guard: %s failed", what, exc_info=True) return None @@ -148,7 +148,7 @@ def _estimate_attempt_cost(agent: Any, response: Any) -> Optional[Decimal]: getattr(agent, "model", "") or "", canonical, provider=getattr(agent, "provider", None), base_url=getattr(agent, "base_url", None), api_key=getattr(agent, "api_key", None), ) - except Exception: # noqa: BLE001 — pricing must never break the loop + except Exception: logger.debug("empty-guard: cost estimation failed", exc_info=True) return None return getattr(result, "amount_usd", None) @@ -184,7 +184,7 @@ def record_empty_attempt( attempts = _attempts(agent) if getattr(agent, "_empty_content_retries", 0) == 0: attempts.clear() - setattr(agent, _STREAK_COST_ATTR, Decimal("0")) + setattr(agent, _STREAK_COST_ATTR, Decimal(0)) usage_present, zero_output = _zero_output(agent, response) attempts.append( @@ -200,7 +200,7 @@ def record_empty_attempt( cost = _estimate_attempt_cost(agent, response) if cost is not None and cost > 0: - prior = getattr(agent, _STREAK_COST_ATTR, Decimal("0")) or Decimal("0") + prior = getattr(agent, _STREAK_COST_ATTR, Decimal(0)) or Decimal(0) setattr(agent, _STREAK_COST_ATTR, prior + cost) diff --git a/agent/error_classifier.py b/agent/error_classifier.py index 0c47639e8a..7c44299b6b 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -75,7 +75,7 @@ class ClassifiedError: provider: Optional[str] = None model: Optional[str] = None message: str = "" - error_context: Dict[str, Any] = field(default_factory=dict) + error_context: dict[str, Any] = field(default_factory=dict) # Recovery hints — the retry loop checks these instead of re-classifying. retryable: bool = True @@ -326,7 +326,7 @@ _REQUEST_VALIDATION_PATTERNS = ( # A rejection from any other host means the provider's gateway injected the # field itself: a server-side flake, not our request shape. prompt_cache_retention # is only sent for api.meta.ai / bedrock-mantle (agent/transports/codex.py). -_SERVER_INJECTED_PARAM_SENDERS: Dict[str, tuple] = { +_SERVER_INJECTED_PARAM_SENDERS: dict[str, tuple] = { "prompt_cache_retention": ("meta", "muse", "msl", "model-api", "bedrock", "mantle"), } _PARAM_REJECTION_WORDS = ("not supported", "unsupported", "unknown", "unrecognized") @@ -449,7 +449,7 @@ _UPSTREAM_BLOCKED_PATTERNS = ( # ordered ``(patterns, verdict)`` pairs matched first-hit; ``verdict`` may be # a callable of the error message. -Verdict = Dict[str, Any] +Verdict = dict[str, Any] def _v(reason: FailoverReason, **hints: Any) -> Verdict: @@ -571,7 +571,7 @@ def is_reasoning_field_rejection(error_msg: str) -> bool: def _billing_hints(error_msg: str) -> Verdict: """Billing verdict carrying the #82154 ambiguity marker when applicable.""" - ctx: Dict[str, Any] = {} + ctx: dict[str, Any] = {} if any(p in error_msg for p in _UNVERIFIED_BILLING_PATTERNS): ctx = {"billing_unverified": True, "possible_content_filter": True} return {**_V_BILLING, "error_context": ctx} @@ -636,7 +636,7 @@ _MESSAGE_TAIL_RULES = ( # Structured error code → verdict. The error-code rate_limit verdict rotates # but does not set should_fallback (unlike the message/status paths). -_ERROR_CODE_VERDICTS: Dict[str, Verdict] = { +_ERROR_CODE_VERDICTS: dict[str, Verdict] = { **dict.fromkeys(("resource_exhausted", "throttled", "rate_limit_exceeded"), _v(_R.rate_limit, should_rotate_credential=True)), **dict.fromkeys(_BILLING_ERROR_CODES, _V_BILLING), @@ -653,7 +653,7 @@ _ERROR_CODE_VERDICTS: Dict[str, Verdict] = { # to the family key before lookup. _PROVIDER_CODE_FAMILIES = {"openai-codex": "openai", "google": "gemini", "google-gemini": "gemini", "google-ai-studio": "gemini", "vertex": "gemini", "google-vertex": "gemini"} -_PROVIDER_CODE_VERDICTS: Dict[str, Dict[str, Verdict]] = { +_PROVIDER_CODE_VERDICTS: dict[str, dict[str, Verdict]] = { "openai": {"server_error": _V_SERVER_ERROR}, "gemini": {"unavailable": _V_OVERLOADED, "deadline_exceeded": _V_TIMEOUT, "internal": _V_SERVER_ERROR}, "anthropic": {"api_error": _V_SERVER_ERROR, "rate_limit_error": _V_RATE_LIMIT}, @@ -1222,7 +1222,7 @@ def _classify_image_tool_422(c: _Ctx) -> Verdict: # check, then the client-error abort path (fallback first) is correct. 408 is # retry-safe (RFC 9110 §15.5.9; proxies emit it when generation outruns the # read window). Unlisted 4xx → format_error, 5xx → server_error. -_STATUS_HANDLERS: Dict[int, Callable[[_Ctx], Verdict]] = { +_STATUS_HANDLERS: dict[int, Callable[[_Ctx], Verdict]] = { 400: _classify_400, 401: lambda c: _V_AUTH_ROTATE, 402: lambda c: _classify_402(c.msg, dict), 403: _status_403, 404: _status_404, 408: lambda c: _V_TIMEOUT, 413: lambda c: _V_PAYLOAD_TOO_LARGE, 422: lambda c: _classify_image_tool_422(c), diff --git a/agent/file_safety.py b/agent/file_safety.py index 65f97f23d3..939e049e6b 100644 --- a/agent/file_safety.py +++ b/agent/file_safety.py @@ -423,7 +423,7 @@ def raise_if_read_blocked(path: str) -> None: """ try: blocked = get_read_block_error(path) - except Exception: # noqa: BLE001 - guard must never break local-file loading + except Exception: return if blocked: raise ValueError(blocked) diff --git a/agent/gemini_native_adapter.py b/agent/gemini_native_adapter.py index 913a7a6359..0cbbac4e02 100644 --- a/agent/gemini_native_adapter.py +++ b/agent/gemini_native_adapter.py @@ -253,7 +253,7 @@ class GeminiAPIError(Exception): """Error shape compatible with Hermes retry/error classification.""" def __init__(self, message: str, *, code: str = "gemini_api_error", status_code: Optional[int] = None, - response: Optional[httpx.Response] = None, retry_after: Optional[float] = None, details: Optional[Dict[str, Any]] = None): + response: Optional[httpx.Response] = None, retry_after: Optional[float] = None, details: Optional[dict[str, Any]] = None): super().__init__(message) self.code, self.status_code, self.response = code, status_code, response self.retry_after, self.details = retry_after, details or {} @@ -273,7 +273,7 @@ def _coerce_content_to_text(content: Any) -> str: return "" if content is None else str(content) -def _inline_data_part(item: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _inline_data_part(item: dict[str, Any]) -> Optional[dict[str, Any]]: """``inlineData`` part for an ``image_url`` item carrying a ``data:`` URL; None otherwise.""" url = (item.get("image_url") or {}).get("url") or "" if item.get("type") != "image_url" or not isinstance(url, str) or not url.startswith("data:"): @@ -286,21 +286,21 @@ def _inline_data_part(item: Dict[str, Any]) -> Optional[Dict[str, Any]]: return None -def _multimodal_part(item: Any) -> Optional[Dict[str, Any]]: +def _multimodal_part(item: Any) -> Optional[dict[str, Any]]: text = _text_of(item) if text or isinstance(item, str): return {"text": text} return _inline_data_part(item) if isinstance(item, dict) else None -def _extract_multimodal_parts(content: Any) -> List[Dict[str, Any]]: +def _extract_multimodal_parts(content: Any) -> list[dict[str, Any]]: if isinstance(content, list): return [p for p in map(_multimodal_part, content) if p] text = _coerce_content_to_text(content) return [{"text": text}] if text else [] -def _tool_call_extra_signature(tool_call: Dict[str, Any]) -> Optional[str]: +def _tool_call_extra_signature(tool_call: dict[str, Any]) -> Optional[str]: """Replayed Gemini thoughtSignature from ``extra_content`` (``google.thought_signature`` or flat).""" extra = tool_call.get("extra_content") or {} sig = (extra.get("google") or extra.get("thought_signature")) if isinstance(extra, dict) else None @@ -309,18 +309,18 @@ def _tool_call_extra_signature(tool_call: Dict[str, Any]) -> Optional[str]: return sig if isinstance(sig, str) and sig else None -def _tool_call_id(tool_call: Dict[str, Any]) -> str: +def _tool_call_id(tool_call: dict[str, Any]) -> str: return str(tool_call.get("id") or tool_call.get("call_id") or "") -def _translate_tool_call_to_gemini(tool_call: Dict[str, Any], include_ids: bool = False) -> Dict[str, Any]: +def _translate_tool_call_to_gemini(tool_call: dict[str, Any], include_ids: bool = False) -> dict[str, Any]: fn = tool_call.get("function") or {} args_raw = fn.get("arguments", "") try: args = json.loads(args_raw) if isinstance(args_raw, str) and args_raw else {} except json.JSONDecodeError: args = {"_raw": args_raw} - call: Dict[str, Any] = {"name": str(fn.get("name") or ""), "args": args if isinstance(args, dict) else {"_value": args}} + call: dict[str, Any] = {"name": str(fn.get("name") or ""), "args": args if isinstance(args, dict) else {"_value": args}} if include_ids and (call_id := _tool_call_id(tool_call)): call["id"] = call_id return {"functionCall": call, "thoughtSignature": _tool_call_extra_signature(tool_call) or _SKIP_SIGNATURE} @@ -337,8 +337,8 @@ def _looks_like_json_schema(node: Any) -> bool: def _translate_tool_result_to_gemini( - message: Dict[str, Any], tool_name_by_call_id: Optional[Dict[str, str]] = None, include_ids: bool = False, *, is_gemini3: bool = False, -) -> Dict[str, Any]: + message: dict[str, Any], tool_name_by_call_id: Optional[dict[str, str]] = None, include_ids: bool = False, *, is_gemini3: bool = False, +) -> dict[str, Any]: tool_call_id = str(message.get("tool_call_id") or "") # functionResponse.name must echo the matching functionCall.name, so the call-id # mapping beats the result's own name (may be an unwrapped MCP name via `tool_call`). @@ -354,7 +354,7 @@ def _translate_tool_result_to_gemini( # display_name"; see vercel/ai#14369). A tool result that is itself a JSON Schema (e.g. tool_describe # output for an MCP tool) must therefore be forwarded as opaque text, not as a structured response. structured = isinstance(parsed, dict) and not _looks_like_json_schema(parsed) - function_response: Dict[str, Any] = {"name": name, "response": parsed if structured else {"output": content}} + function_response: dict[str, Any] = {"name": name, "response": parsed if structured else {"output": content}} if include_ids and tool_call_id: function_response["id"] = tool_call_id # Gemini 3.x accepts images inside functionResponse.parts; 2.x rejects the field. @@ -363,18 +363,18 @@ def _translate_tool_result_to_gemini( return {"functionResponse": function_response} -def _has_function_response(content: Dict[str, Any]) -> bool: +def _has_function_response(content: dict[str, Any]) -> bool: return any(isinstance(part, dict) and "functionResponse" in part for part in content.get("parts", [])) -def _merge_alternating(contents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +def _merge_alternating(contents: list[dict[str, Any]]) -> list[dict[str, Any]]: """Alternation contract for generateContent: 1) adjacent same-role contents merge (else HTTP 400 "multiturn requests [must] alternate"); 2) EXCEPT never fuse a human user text turn into a preceding user content that only carries functionResponse parts (or vice versa) — Gemini 3 accepts the fold but reads the text as a continuation of the tool result and returns an empty candidate (parallel functionResponse + functionResponse still merge); 3) the split pair stays API-valid via an interposed placeholder model turn.""" - merged: List[Dict[str, Any]] = [] + merged: list[dict[str, Any]] = [] # Compatibility contract for native Gemini generateContent: 1) Same-role adjacent contents still merge # in general (strict user/model alternation for ordinary text turns and parallel tool-result grouping; # consecutive same-role contents are rejected with HTTP 400 "Please ensure that multiturn requests @@ -400,11 +400,11 @@ def _merge_alternating(contents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def _build_gemini_contents( - messages: List[Dict[str, Any]], include_tool_call_ids: bool = False, *, is_gemini3: bool = False -) -> tuple[List[Dict[str, Any]], Optional[Dict[str, Any]]]: - system_text_parts: List[str] = [] - contents: List[Dict[str, Any]] = [] - tool_name_by_call_id: Dict[str, str] = {} + messages: list[dict[str, Any]], include_tool_call_ids: bool = False, *, is_gemini3: bool = False +) -> tuple[list[dict[str, Any]], Optional[dict[str, Any]]]: + system_text_parts: list[str] = [] + contents: list[dict[str, Any]] = [] + tool_name_by_call_id: dict[str, str] = {} for msg in messages: if not isinstance(msg, dict): continue @@ -429,11 +429,11 @@ def _build_gemini_contents( return _merge_alternating(contents), ({"role": "system", "parts": [{"text": joined_system}]} if joined_system else None) -def _function_declaration(tool: Any, *, json_schema: bool = False) -> Optional[Dict[str, Any]]: +def _function_declaration(tool: Any, *, json_schema: bool = False) -> Optional[dict[str, Any]]: fn = (tool.get("function") or {}) if isinstance(tool, dict) else None if not isinstance(fn, dict) or not (isinstance(fn.get("name"), str) and fn["name"]): return None - decl: Dict[str, Any] = {"name": fn["name"]} + decl: dict[str, Any] = {"name": fn["name"]} if isinstance(fn.get("description"), str) and fn["description"]: decl["description"] = fn["description"] if isinstance(fn.get("parameters"), dict): @@ -446,13 +446,13 @@ def _function_declaration(tool: Any, *, json_schema: bool = False) -> Optional[D return decl -def _translate_tools_to_gemini(tools: Any, *, json_schema: bool = False) -> List[Dict[str, Any]]: +def _translate_tools_to_gemini(tools: Any, *, json_schema: bool = False) -> list[dict[str, Any]]: declarations = [d for d in (_function_declaration(t, json_schema=json_schema) for t in (tools if isinstance(tools, list) else [])) if d] return [{"functionDeclarations": declarations}] if declarations else [] -def _translate_tool_choice_to_gemini(tool_choice: Any) -> Optional[Dict[str, Any]]: +def _translate_tool_choice_to_gemini(tool_choice: Any) -> Optional[dict[str, Any]]: if isinstance(tool_choice, str) and tool_choice in _TOOL_CHOICE_MODES: return {"functionCallingConfig": {"mode": _TOOL_CHOICE_MODES[tool_choice]}} name = (tool_choice.get("function") or {}).get("name") if isinstance(tool_choice, dict) else None @@ -467,7 +467,7 @@ _THINKING_KEYS = ( ) -def _normalize_thinking_config(config: Any) -> Optional[Dict[str, Any]]: +def _normalize_thinking_config(config: Any) -> Optional[dict[str, Any]]: if not isinstance(config, dict): return None values = {key: config.get(key, config.get(alias)) for key, alias, _, _ in _THINKING_KEYS} @@ -499,7 +499,7 @@ def _effective_gemini_max_output_tokens(max_tokens: Optional[int], thinking_conf return requested -def _translate_response_format(response_format: Any, *, json_schema: bool = False) -> Dict[str, Any]: +def _translate_response_format(response_format: Any, *, json_schema: bool = False) -> dict[str, Any]: """OpenAI ``response_format`` → Gemini ``generationConfig`` JSON-output keys. Full-JSON-Schema ``responseJsonSchema`` exists only on the generativelanguage ``v1beta`` @@ -519,17 +519,17 @@ def _translate_response_format(response_format: Any, *, json_schema: bool = Fals def build_gemini_request( - *, messages: List[Dict[str, Any]], tools: Any = None, tool_choice: Any = None, temperature: Optional[float] = None, + *, messages: list[dict[str, Any]], tools: Any = None, tool_choice: Any = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, top_p: Optional[float] = None, stop: Any = None, thinking_config: Any = None, response_format: Any = None, model: str = "", tools_as_json_schema: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: # Gemini 3+ both requires tool-call ids and accepts multimodal functionResponse parts. is_gemini3 = gemini_requires_tool_call_ids(model) contents, system_instruction = _build_gemini_contents(messages, include_tool_call_ids=is_gemini3, is_gemini3=is_gemini3) gemini_tools = _translate_tools_to_gemini(tools, json_schema=tools_as_json_schema) tool_config = _translate_tool_choice_to_gemini(tool_choice) optional = (("systemInstruction", system_instruction), ("tools", gemini_tools), ("toolConfig", tool_config)) - request: Dict[str, Any] = {"contents": contents, **{k: v for k, v in optional if v}} + request: dict[str, Any] = {"contents": contents, **{k: v for k, v in optional if v}} # Key order is part of the wire format (prompt-cache parity): temperature, maxOutputTokens, topP, stop, thinking. generation = ( ("temperature", temperature), ("maxOutputTokens", _effective_gemini_max_output_tokens(max_tokens, thinking_config)), @@ -552,29 +552,29 @@ def build_gemini_request( # ── Gemini → OpenAI response translation ───────────────────────────────────── -def _tool_call_extra_from_part(part: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _tool_call_extra_from_part(part: dict[str, Any]) -> Optional[dict[str, Any]]: sig = part.get("thoughtSignature") return {"google": {"thought_signature": sig}} if isinstance(sig, str) and sig else None -def _provider_call_id(fc: Dict[str, Any]) -> Optional[str]: +def _provider_call_id(fc: dict[str, Any]) -> Optional[str]: fc_id = fc.get("id") return fc_id if isinstance(fc_id, str) and fc_id else None -def _new_call_id(fc: Dict[str, Any]) -> str: +def _new_call_id(fc: dict[str, Any]) -> str: """Echo the functionCall/delta ``id`` when present, else mint an OpenAI-style one.""" return _provider_call_id(fc) or f"call_{uuid.uuid4().hex[:12]}" -def _dump_call_args(fc: Dict[str, Any], **kwargs: Any) -> str: +def _dump_call_args(fc: dict[str, Any], **kwargs: Any) -> str: try: return json.dumps(fc.get("args") or {}, ensure_ascii=False, **kwargs) except (TypeError, ValueError): return "{}" -def _usage_from_metadata(usage_meta: Dict[str, Any]) -> SimpleNamespace: +def _usage_from_metadata(usage_meta: dict[str, Any]) -> SimpleNamespace: """Gemini ``usageMetadata`` → OpenAI-shaped usage. Hidden thinking is reported separately in ``thoughtsTokenCount``: @@ -584,7 +584,7 @@ def _usage_from_metadata(usage_meta: Dict[str, Any]) -> SimpleNamespace: real output and ``prompt + completion != total``) and also surfaced under ``completion_tokens_details.reasoning_tokens``, where ``normalize_usage`` reads them. Absent on non-thinking/older responses, which keeps their numbers as-is.""" - count = lambda key: int(usage_meta.get(key) or 0) # noqa: E731 + count = lambda key: int(usage_meta.get(key) or 0) reasoning_tokens = count("thoughtsTokenCount") return SimpleNamespace( prompt_tokens=count("promptTokenCount"), completion_tokens=count("candidatesTokenCount") + reasoning_tokens, @@ -605,26 +605,26 @@ def _tool_call_ns(name: str, arguments: str, index: int, call_id: str, extra_con return SimpleNamespace(id=call_id, type="function", index=index, function=SimpleNamespace(name=name, arguments=arguments), **extra) -def _part_text(part: Dict[str, Any]) -> tuple[Optional[str], bool]: +def _part_text(part: dict[str, Any]) -> tuple[Optional[str], bool]: """``(text, is_thought)`` for a candidate part; ``(None, False)`` when it carries no text.""" text = part.get("text") return (text, part.get("thought") is True) if isinstance(text, str) else (None, False) -def _part_function_call(part: Dict[str, Any]) -> Optional[Dict[str, Any]]: +def _part_function_call(part: dict[str, Any]) -> Optional[dict[str, Any]]: fc = part.get("functionCall") return fc if isinstance(fc, dict) and fc.get("name") else None -def translate_gemini_response(resp: Dict[str, Any], model: str) -> SimpleNamespace: +def translate_gemini_response(resp: dict[str, Any], model: str) -> SimpleNamespace: candidates = resp.get("candidates") or [] cand = parts = None if isinstance(candidates, list) and candidates: cand = candidates[0] if isinstance(candidates[0], dict) else {} content_obj = cand.get("content") parts = content_obj.get("parts") if isinstance(content_obj, dict) else [] - pieces: Dict[bool, List[str]] = {False: [], True: []} # is_thought → text pieces - tool_calls: List[SimpleNamespace] = [] + pieces: dict[bool, list[str]] = {False: [], True: []} # is_thought → text pieces + tool_calls: list[SimpleNamespace] = [] for index, part in enumerate(parts or []): if not isinstance(part, dict): continue @@ -645,7 +645,7 @@ class _GeminiStreamChunk(SimpleNamespace): ... def _make_stream_chunk( - *, model: str, content: str = "", tool_call_delta: Optional[Dict[str, Any]] = None, finish_reason: Optional[str] = None, reasoning: str = "", + *, model: str, content: str = "", tool_call_delta: Optional[dict[str, Any]] = None, finish_reason: Optional[str] = None, reasoning: str = "", ) -> _GeminiStreamChunk: d = tool_call_delta tool_calls = None if d is None else [ @@ -675,7 +675,7 @@ def _parse_sse_line(line: str) -> Any: return payload if isinstance(payload, dict) else None -def _iter_sse_events(response: httpx.Response) -> Iterator[Dict[str, Any]]: +def _iter_sse_events(response: httpx.Response) -> Iterator[dict[str, Any]]: buffer = "" for chunk in response.iter_text(): buffer += chunk or "" @@ -694,8 +694,8 @@ def _iter_sse_events(response: httpx.Response) -> Iterator[Dict[str, Any]]: yield payload -def _tool_call_slot(fc: Dict[str, Any], part: Dict[str, Any], part_index: int, args_str: str, - tool_call_indices: Dict[str, Dict[str, Any]]) -> tuple[str, Optional[Dict[str, Any]]]: +def _tool_call_slot(fc: dict[str, Any], part: dict[str, Any], part_index: int, args_str: str, + tool_call_indices: dict[str, dict[str, Any]]) -> tuple[str, Optional[dict[str, Any]]]: """``(key, existing slot or None)`` for a streamed functionCall. Gemini 3 ids each tool call, so the id is the slot identity (``part_index`` and the thought @@ -719,13 +719,13 @@ def _tool_call_slot(fc: Dict[str, Any], part: Dict[str, Any], part_index: int, a return f"{key}#{len(tool_call_indices)}", None -def translate_stream_event(event: Dict[str, Any], model: str, tool_call_indices: Dict[str, Dict[str, Any]]) -> List[_GeminiStreamChunk]: +def translate_stream_event(event: dict[str, Any], model: str, tool_call_indices: dict[str, dict[str, Any]]) -> list[_GeminiStreamChunk]: candidates = event.get("candidates") or [] if not candidates: return [] cand = candidates[0] if isinstance(candidates[0], dict) else {} parts = (cand.get("content") or {}).get("parts") or [] - chunks: List[_GeminiStreamChunk] = [] + chunks: list[_GeminiStreamChunk] = [] for part_index, part in enumerate(parts): if not isinstance(part, dict): continue @@ -756,7 +756,7 @@ def translate_stream_event(event: Dict[str, Any], model: str, tool_call_indices: return chunks -def _error_info(err_obj: Dict[str, Any]) -> tuple[str, Dict[str, Any]]: +def _error_info(err_obj: dict[str, Any]) -> tuple[str, dict[str, Any]]: """``(reason, metadata)`` from the first google.rpc.ErrorInfo detail (later ones fill gaps until reason is set).""" reason, metadata = "", {} details = err_obj.get("details") @@ -767,7 +767,7 @@ def _error_info(err_obj: Dict[str, Any]) -> tuple[str, Dict[str, Any]]: return reason, metadata -def _error_object(body_text: str) -> Dict[str, Any]: +def _error_object(body_text: str) -> dict[str, Any]: """The ``error`` object of a Google JSON error body, or ``{}``.""" try: parsed = json.loads(body_text) if body_text else None @@ -820,7 +820,7 @@ class GeminiNativeClient: MISSING_KEY_ERROR = _MISSING_KEY_ERROR def __init__( - self, *, api_key: str, base_url: Optional[str] = None, default_headers: Optional[Dict[str, str]] = None, + self, *, api_key: str, base_url: Optional[str] = None, default_headers: Optional[dict[str, str]] = None, timeout: Any = None, http_client: Optional[httpx.Client] = None, **_: Any, ) -> None: if not (api_key or "").strip(): @@ -843,10 +843,10 @@ class GeminiNativeClient: def __exit__(self, exc_type, exc_val, exc_tb): self.close() - def _auth_headers(self) -> Dict[str, str]: + def _auth_headers(self) -> dict[str, str]: return {"x-goog-api-key": self.api_key} - def _headers(self) -> Dict[str, str]: + def _headers(self) -> dict[str, str]: return {"Content-Type": "application/json", "Accept": "application/json", **self._auth_headers(), "User-Agent": f"{_API_CLIENT} (gemini-native)", "X-Goog-Api-Client": _API_CLIENT, **self._default_headers} @@ -859,9 +859,9 @@ class GeminiNativeClient: return (True, None) if chunk is _END else (False, chunk) def _create_chat_completion( - self, *, model: str = "gemini-3.7-flash", messages: Optional[List[Dict[str, Any]]] = None, stream: bool = False, + self, *, model: str = "gemini-3.7-flash", messages: Optional[list[dict[str, Any]]] = None, stream: bool = False, tools: Any = None, tool_choice: Any = None, temperature: Optional[float] = None, max_tokens: Optional[int] = None, - top_p: Optional[float] = None, stop: Any = None, response_format: Any = None, extra_body: Optional[Dict[str, Any]] = None, + top_p: Optional[float] = None, stop: Any = None, response_format: Any = None, extra_body: Optional[dict[str, Any]] = None, timeout: Any = None, **_: Any, ) -> Any: extra = extra_body if isinstance(extra_body, dict) else {} @@ -886,13 +886,13 @@ class GeminiNativeClient: ) from exc return translate_gemini_response(payload, model=model) - def _stream_completion(self, model: str, url: str, request: Dict[str, Any], timeout: Any) -> Iterator[_GeminiStreamChunk]: + def _stream_completion(self, model: str, url: str, request: dict[str, Any], timeout: Any) -> Iterator[_GeminiStreamChunk]: try: headers = {**self._headers(), "Accept": "text/event-stream"} with self._http.stream("POST", url, json=request, headers=headers, timeout=timeout) as response: if response.status_code != 200: raise self._http_error(response, read_streaming_error_body(response)) - tool_call_indices: Dict[str, Dict[str, Any]] = {} + tool_call_indices: dict[str, dict[str, Any]] = {} for event in _iter_sse_events(response): yield from translate_stream_event(event, model, tool_call_indices) except httpx.HTTPError as exc: diff --git a/agent/gemini_schema.py b/agent/gemini_schema.py index 4355f9280e..b2371d7742 100644 --- a/agent/gemini_schema.py +++ b/agent/gemini_schema.py @@ -41,9 +41,9 @@ def _stringify_enum_value(item: Any) -> Any: return item if isinstance(item, str) else None -def _normalize_gemini_type_array(type_array: list, cleaned: Dict[str, Any]) -> None: +def _normalize_gemini_type_array(type_array: list, cleaned: dict[str, Any]) -> None: """Keep union alternatives and their branch-local structural constraints.""" - derived: Dict[str, Any] = {} + derived: dict[str, Any] = {} _normalize_type_array(type_array, derived) if "anyOf" in derived: constraints = {"anyOf": cleaned["anyOf"]} if "anyOf" in cleaned else {} @@ -65,13 +65,13 @@ def _normalize_gemini_type_array(type_array: list, cleaned: Dict[str, Any]) -> N cleaned["nullable"] = True -def sanitize_gemini_schema(schema: Any) -> Dict[str, Any]: +def sanitize_gemini_schema(schema: Any) -> dict[str, Any]: """Gemini-compatible copy of a tool parameter schema: keeps only the documented subset (drops e.g. ``$schema`` / ``additionalProperties``) and recursively sanitizes nested ``properties`` / ``items`` / ``anyOf``.""" if not isinstance(schema, dict): return {} - cleaned: Dict[str, Any] = {} + cleaned: dict[str, Any] = {} for key, value in schema.items(): if key not in _GEMINI_SCHEMA_ALLOWED_KEYS: continue @@ -119,7 +119,7 @@ def sanitize_gemini_schema(schema: Any) -> Dict[str, Any]: return cleaned -def sanitize_gemini_tool_parameters(parameters: Any) -> Dict[str, Any]: +def sanitize_gemini_tool_parameters(parameters: Any) -> dict[str, Any]: """Normalize tool parameters to a valid Gemini object schema.""" return sanitize_gemini_schema(parameters) or {"type": "object", "properties": {}} @@ -132,13 +132,13 @@ def sanitize_gemini_tool_parameters(parameters: Any) -> Dict[str, Any]: # schema goes as-is; only same-document $refs are inlined (MCP pydantic / zod emit # them and Google rejects reference indirection) and root ``$schema`` is dropped. -_EMPTY_OBJECT_SCHEMA: Dict[str, Any] = {"type": "object", "properties": {}} +_EMPTY_OBJECT_SCHEMA: dict[str, Any] = {"type": "object", "properties": {}} # Real tool schemas hold a handful of refs; the cap stops circular pydantic models # from expanding forever. _MAX_REF_EXPANSIONS = 256 -def _resolve_local_ref(root: Dict[str, Any], ref: str) -> Optional[Dict[str, Any]]: +def _resolve_local_ref(root: dict[str, Any], ref: str) -> Optional[dict[str, Any]]: """Resolve a same-document JSON pointer (``#/$defs/Foo``) against *root*.""" if not isinstance(ref, str) or not ref.startswith("#/"): return None @@ -151,7 +151,7 @@ def _resolve_local_ref(root: Dict[str, Any], ref: str) -> Optional[Dict[str, Any return node if isinstance(node, dict) else None -def _inline_refs(node: Any, root: Dict[str, Any], budget: List[int], stack: tuple = ()) -> Any: +def _inline_refs(node: Any, root: dict[str, Any], budget: list[int], stack: tuple = ()) -> Any: """Recursively inline same-document ``$ref`` nodes; ``ValueError`` on an unresolvable or circular reference or an exhausted budget (the caller then keeps the original).""" if isinstance(node, list): @@ -176,7 +176,7 @@ def _inline_refs(node: Any, root: Dict[str, Any], budget: List[int], stack: tupl return {**inlined, **_inline_refs(siblings, root, budget, stack)} if siblings else inlined -def prepare_gemini_tool_parameters(parameters: Any) -> Dict[str, Any]: +def prepare_gemini_tool_parameters(parameters: Any) -> dict[str, Any]: """Full JSON Schema for ``parametersJsonSchema``: deep-copied, root ``$schema`` dropped, same-document ``$ref`` inlined, object root guaranteed. A schema whose references cannot all be resolved is sent untouched so the provider names the real problem.""" diff --git a/agent/image_gen_provider.py b/agent/image_gen_provider.py index 00046756e4..927586dda4 100644 --- a/agent/image_gen_provider.py +++ b/agent/image_gen_provider.py @@ -23,7 +23,7 @@ from agent.provider_base import CatalogProviderBase logger = logging.getLogger(__name__) -VALID_ASPECT_RATIOS: Tuple[str, ...] = ("landscape", "square", "portrait") +VALID_ASPECT_RATIOS: tuple[str, ...] = ("landscape", "square", "portrait") DEFAULT_ASPECT_RATIO = "landscape" @@ -32,7 +32,7 @@ class ImageGenProvider(CatalogProviderBase): :attr:`name` and :meth:`generate`; ``list_models`` entries may add ``speed`` / ``strengths`` / ``price`` for the picker.""" - def capabilities(self) -> Dict[str, Any]: + def capabilities(self) -> dict[str, Any]: """``modalities`` (``"text"`` and/or ``"image"``) and ``max_reference_images``; optionally ``supports_upscale`` (bool) and ``creative_controls`` (names from the tool's creative-control vocabulary: ``creativity``, ``intensity``, ``complexity``, ``movement``). Surfaced in the @@ -48,9 +48,9 @@ class ImageGenProvider(CatalogProviderBase): aspect_ratio: str = DEFAULT_ASPECT_RATIO, *, image_url: Optional[str] = None, - reference_image_urls: Optional[List[str]] = None, + reference_image_urls: Optional[list[str]] = None, **kwargs: Any, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Generate an image, or edit ``image_url`` (``reference_image_urls`` are extra style/composition refs, clamped to ``max_reference_images``); any source image routes to the edit endpoint. Return :func:`success_response` / :func:`error_response`. @@ -65,7 +65,7 @@ def resolve_aspect_ratio(value: Optional[str]) -> str: return v if v in VALID_ASPECT_RATIOS else DEFAULT_ASPECT_RATIO -def normalize_reference_images(value: Any) -> Optional[List[str]]: +def normalize_reference_images(value: Any) -> Optional[list[str]]: """Coerce a str or list into a clean list of non-blank strings; ``None`` when nothing usable remains so providers treat "no refs" as one sentinel.""" if isinstance(value, str): @@ -109,10 +109,10 @@ def success_response( aspect_ratio: str, provider: str, modality: str = "text", - extra: Optional[Dict[str, Any]] = None, -) -> Dict[str, Any]: + extra: Optional[dict[str, Any]] = None, +) -> dict[str, Any]: """Uniform success dict; ``extra`` keys are added without overriding standard ones.""" - payload: Dict[str, Any] = { + payload: dict[str, Any] = { "success": True, "image": image, "model": model, "prompt": prompt, "aspect_ratio": aspect_ratio, "modality": modality, "provider": provider, } @@ -129,7 +129,7 @@ def error_response( model: str = "", prompt: str = "", aspect_ratio: str = DEFAULT_ASPECT_RATIO, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Build a uniform error response dict.""" return { "success": False, "image": None, "error": error, "error_type": error_type, diff --git a/agent/image_routing.py b/agent/image_routing.py index 34ffafc2ce..3fd83b40d9 100644 --- a/agent/image_routing.py +++ b/agent/image_routing.py @@ -66,7 +66,7 @@ def _existing_file(candidate: str) -> Optional[str]: return None -def extract_image_refs(text: str) -> Tuple[List[str], List[str]]: +def extract_image_refs(text: str) -> tuple[list[str], list[str]]: """Scan free-form text for image references → ``(local_paths, urls)``, each ordered and deduplicated. Local paths must exist as files; URLs are not validated (the provider fetches them). Code spans are skipped so pasted @@ -99,7 +99,7 @@ def _coerce_capability_bool(raw: Any) -> Optional[bool]: return _BOOL_TOKENS.get(raw.strip().lower()) if isinstance(raw, str) else None -def _dict_or_empty(raw: Any) -> Dict[str, Any]: +def _dict_or_empty(raw: Any) -> dict[str, Any]: return raw if isinstance(raw, dict) else {} @@ -124,7 +124,7 @@ def _model_supports_vision_override(models_cfg: Any, model: str) -> Optional[boo return _coerce_capability_bool(per_model.get("supports_vision", per_model.get("vision"))) -def _custom_provider_entries(cfg: Dict[str, Any], names: Iterable[str]) -> Iterable[Dict[str, Any]]: +def _custom_provider_entries(cfg: dict[str, Any], names: Iterable[str]) -> Iterable[dict[str, Any]]: """Yield legacy ``custom_providers`` entries matching ``names`` (case-insensitive); ``names`` is the outer loop so list order cannot let a persisted default shadow the live route.""" entries = _custom_provider_list(cfg) @@ -132,14 +132,14 @@ def _custom_provider_entries(cfg: Dict[str, Any], names: Iterable[str]) -> Itera yield from (e for e in entries if _clean_str(e.get("name")).lower() == wanted) -def _custom_provider_list(cfg: Dict[str, Any]) -> List[Dict[str, Any]]: +def _custom_provider_list(cfg: dict[str, Any]) -> list[dict[str, Any]]: """Dict entries of the legacy ``custom_providers`` list (empty when absent/malformed).""" raw = cfg.get("custom_providers") return [e for e in raw if isinstance(e, dict)] if isinstance(raw, list) else [] def _supports_vision_override( - cfg: Optional[Dict[str, Any]], + cfg: Optional[dict[str, Any]], provider: str, model: str, *, @@ -160,7 +160,7 @@ def _supports_vision_override( if top is not None: return top - candidates: List[str] = [] + candidates: list[str] = [] for candidate in filter(None, (requested_provider, provider, _clean_str(model_cfg.get("provider")))): candidates.append(candidate) if candidate.startswith("custom:") and candidate[len("custom:"):]: @@ -174,7 +174,7 @@ def _supports_vision_override( def _resolve_inference_value( - cfg: Optional[Dict[str, Any]], + cfg: Optional[dict[str, Any]], provider: str, key: str, *, @@ -214,7 +214,7 @@ def _resolve_inference_value( return next((v for v in (_clean_str(e.get(key)) for e in entries) if v), "") -def _resolve_inference_base_url(cfg: Optional[Dict[str, Any]], provider: str) -> str: +def _resolve_inference_base_url(cfg: Optional[dict[str, Any]], provider: str) -> str: """Best-effort base URL for the active inference provider; the runtime value is only trusted when it belongs to the requested provider (or none was requested).""" requested = _clean_str(provider).lower() @@ -224,7 +224,7 @@ def _resolve_inference_base_url(cfg: Optional[Dict[str, Any]], provider: str) -> ) -def _resolve_inference_api_key(cfg: Optional[Dict[str, Any]], provider: str) -> str: +def _resolve_inference_api_key(cfg: Optional[dict[str, Any]], provider: str) -> str: """Best-effort API key, resolved like :func:`_resolve_inference_base_url` so it matches the base URL actually probed; otherwise the local server-type probe hits a keyed remote endpoint without Authorization and sprays 401s on every image turn. @@ -259,7 +259,7 @@ def _coerce_mode(raw: Any) -> str: return mode if mode in _VALID_MODES else "auto" -def _explicit_aux_vision_override(cfg: Optional[Dict[str, Any]]) -> bool: +def _explicit_aux_vision_override(cfg: Optional[dict[str, Any]]) -> bool: """True when the user configured a specific ``auxiliary.vision`` backend — the de-facto image route in ``auto`` mode even when the main model has native vision. ``auto``/empty provider with no model and no base_url is not explicit.""" @@ -271,7 +271,7 @@ def _explicit_aux_vision_override(cfg: Optional[Dict[str, Any]]) -> bool: ) -def _probe_managed_runtime(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> Optional[bool]: +def _probe_managed_runtime(provider: str, model: str, cfg: Optional[dict[str, Any]]) -> Optional[bool]: """Managed local runtime verdict: the server receiving the image is the authority on whether it can see (its /props reports modalities). Cloud catalogs have never heard of a local GGUF, so without this every local model reads as text-only and @@ -282,7 +282,7 @@ def _probe_managed_runtime(provider: str, model: str, cfg: Optional[Dict[str, An return managed_model_supports_vision(model) if managed else None -def _probe_models_dev(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> Optional[bool]: +def _probe_models_dev(provider: str, model: str, cfg: Optional[dict[str, Any]]) -> Optional[bool]: """models.dev catalog verdict. ``allow_network=True`` on purpose: this runs only when an image needs routing, and the text-only-main guard depends on catalog data — a cold cache returning "unknown" would reintroduce attempting the call. @@ -305,7 +305,7 @@ def _probe_models_dev(provider: str, model: str, cfg: Optional[Dict[str, Any]]) return None if caps is None else caps.supports_vision -def _probe_ollama(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> Optional[bool]: +def _probe_ollama(provider: str, model: str, cfg: Optional[dict[str, Any]]) -> Optional[bool]: """Ollama ``/api/show`` verdict for local endpoints (see :func:`_should_probe_ollama_vision`).""" base_url = _resolve_inference_base_url(cfg, provider) if not base_url and _clean_str(provider).lower() == "ollama": @@ -320,7 +320,7 @@ def _probe_ollama(provider: str, model: str, cfg: Optional[Dict[str, Any]]) -> O # Capability probes after the config override, in priority order; each returns # True/False or None (unknown → next probe). Exceptions are logged and treated as None. -_VISION_PROBES: Tuple[Tuple[str, Callable[..., Optional[bool]]], ...] = ( +_VISION_PROBES: tuple[tuple[str, Callable[..., Optional[bool]]], ...] = ( ("managed-runtime caps lookup", _probe_managed_runtime), ("caps lookup", _probe_models_dev), ("ollama vision probe", _probe_ollama), @@ -330,7 +330,7 @@ _VISION_PROBES: Tuple[Tuple[str, Callable[..., Optional[bool]]], ...] = ( def _lookup_supports_vision( provider: str, model: str, - cfg: Optional[Dict[str, Any]] = None, + cfg: Optional[dict[str, Any]] = None, *, requested_provider: str = "", ) -> Optional[bool]: @@ -375,7 +375,7 @@ def _lookup_supports_vision( def decide_image_input_mode( provider: str, model: str, - cfg: Optional[Dict[str, Any]], + cfg: Optional[dict[str, Any]], *, requested_provider: str = "", ) -> str: @@ -404,7 +404,7 @@ _FTYP_BRANDS = { **dict.fromkeys((b"avif", b"avis"), "image/avif"), **dict.fromkeys((b"heic", b"heix", b"hevc", b"hevx", b"mif1", b"msf1", b"heim", b"heis"), "image/heic"), } -_MAGIC: Tuple[Tuple[Tuple[Tuple[int, bytes], ...], str], ...] = ( +_MAGIC: tuple[tuple[tuple[tuple[int, bytes], ...], str], ...] = ( (((0, b"\x89PNG\r\n\x1a\n"),), "image/png"), (((0, b"\xff\xd8\xff"),), "image/jpeg"), (((0, b"GIF87a"),), "image/gif"), (((0, b"GIF89a"),), "image/gif"), @@ -453,7 +453,7 @@ def _transcode_to_png(raw: bytes) -> Optional[bytes]: pillow_heif.register_heif_opener() with suppress(Exception): - import pillow_avif # type: ignore # noqa: F401 -- registers AVIF on import + import pillow_avif # type: ignore try: with Image.open(BytesIO(raw)) as im: # Normalise exotic modes to RGBA so PNG can serialise and transparency survives. @@ -493,7 +493,7 @@ def _accepted_mimes() -> frozenset: if is_managed_provider(str(_runtime_main_value("provider") or ""), str(_runtime_main_value("base_url") or "")): return ACCEPTED_IMAGE_MIMES - except Exception: # noqa: BLE001 — best-effort narrowing only + except Exception: pass return _UNIVERSALLY_SUPPORTED_MIMES @@ -532,9 +532,9 @@ def _file_to_data_url(path: Path) -> Optional[str]: def build_native_content_parts( user_text: str, - image_paths: List[str], - image_urls: Optional[List[str]] = None, -) -> Tuple[List[Dict[str, Any]], List[str]]: + image_paths: list[str], + image_urls: Optional[list[str]] = None, +) -> tuple[list[dict[str, Any]], list[str]]: """Build an OpenAI-style ``content`` list for a user turn. Local paths become base64 ``data:`` URLs; remote URLs pass through verbatim. @@ -544,8 +544,8 @@ def build_native_content_parts( ``Runner._enrich_message_with_vision``. Returns ``(content_parts, skipped)``; ``skipped`` holds unreadable local paths (URLs are never skipped). """ - skipped: List[str] = [] - attached: List[Tuple[str, str]] = [] # (url, hint) + skipped: list[str] = [] + attached: list[tuple[str, str]] = [] # (url, hint) for raw_path in image_paths: p = Path(raw_path) data_url = _file_to_data_url(p) if p.exists() and p.is_file() else None diff --git a/agent/image_token_cost.py b/agent/image_token_cost.py index 78219bf93d..60ccf2a56d 100644 --- a/agent/image_token_cost.py +++ b/agent/image_token_cost.py @@ -26,11 +26,11 @@ _MIN_PLAUSIBLE, _MAX_PLAUSIBLE = 64, 32_768 _EMA_ALPHA = 0.5 _image_cost_var: ContextVar[Optional[int]] = ContextVar("hermes_image_token_cost", default=None) -_LEARNED: Dict[str, int] = {} +_LEARNED: dict[str, int] = {} _LOADED = False # Routed profiles (multiplexed gateway) keep their own table, loaded from THEIR cache file: the # module slot above is the launch profile's and would otherwise be persisted into every home. -_LEARNED_BY_HOME: Dict[str, Dict[str, int]] = {} +_LEARNED_BY_HOME: dict[str, dict[str, int]] = {} def _cache_path(): @@ -45,14 +45,14 @@ def _key(model: Any, base_url: Any) -> str: return f"{model or ''}@{base_url_hostname(base_url or '') or ''}" -def _read_cache() -> Dict[str, int]: +def _read_cache() -> dict[str, int]: from agent.model_metadata import _load_json_dict return {k: v for k, v in _load_json_dict(_cache_path()).items() if isinstance(v, int) and _MIN_PLAUSIBLE <= v <= _MAX_PLAUSIBLE} -def _table() -> Dict[str, int]: +def _table() -> dict[str, int]: """The active profile's learned table, loaded lazily from its cache file.""" global _LOADED from hermes_constants import get_hermes_home_override, hermes_home_key @@ -94,13 +94,13 @@ def bind_image_token_cost(agent: Any) -> None: _image_cost_var.set(learned_image_token_cost(getattr(agent, "model", None), getattr(agent, "base_url", None))) -def count_images(messages: List[Dict[str, Any]]) -> int: +def count_images(messages: list[dict[str, Any]]) -> int: from agent.model_metadata import _count_image_tokens return sum(_count_image_tokens(m, 1) for m in messages if isinstance(m, dict)) -def calibrate_from_usage(agent: Any, messages: List[Dict[str, Any]], prompt_tokens: Any) -> Optional[int]: +def calibrate_from_usage(agent: Any, messages: list[dict[str, Any]], prompt_tokens: Any) -> Optional[int]: """Learn the per-image cost from the response that just priced ``messages``. Requires the PREVIOUS anchor (real count of the prior request) to still match: the residual diff --git a/agent/initiate_setup_facts.py b/agent/initiate_setup_facts.py index 2c76be1b53..6e57adb1ad 100644 --- a/agent/initiate_setup_facts.py +++ b/agent/initiate_setup_facts.py @@ -24,7 +24,7 @@ _NON_NAMES = frozenset({ "admin", "administrator", "default", "guest", "me", "owner", "root", "test", "user", }) _HANDLE_CHARS = re.compile(r"[\d_@/\\]") -_SPARK_MODEL = re.compile(r"\b(dgx|spark|gb10)\b", re.I) +_SPARK_MODEL = re.compile(r"\b(dgx|spark|gb10)\b", re.IGNORECASE) FORK_QUESTION = "Know what you'd like it to make?" # Reading the Blender plugin's app declaration takes the network. @@ -294,7 +294,7 @@ def _handoff(kind: str, description: str) -> dict: """The handoff message's parts and its two plans from ``templates/handoff.md``, the machine plan naming this computer, so the model reads them only when it reaches the handoff.""" text = (skill_dir() / "templates" / "handoff.md").read_text(encoding="utf-8-sig") - sections = dict(re.findall(r"^## (\S+)\n\n(.*?)\n*(?=^## |\Z)", text, re.M | re.S)) + sections = dict(re.findall(r"^## (\S+)\n\n(.*?)\n*(?=^## |\Z)", text, re.MULTILINE | re.DOTALL)) machine = sections["machine"].replace("", kind).replace("", description) return {"message": sections["message"], "build": sections["build"], "machine": machine} diff --git a/agent/inline_tool_executors.py b/agent/inline_tool_executors.py index d203094094..deb0547d34 100644 --- a/agent/inline_tool_executors.py +++ b/agent/inline_tool_executors.py @@ -17,7 +17,7 @@ from typing import Any, Callable, Dict, Optional, Tuple from tools.arg_coercion import coerce_tool_args -def tool_hook_ids(agent, effective_task_id: str, tool_call_id: Optional[str]) -> Dict[str, str]: +def tool_hook_ids(agent, effective_task_id: str, tool_call_id: Optional[str]) -> dict[str, str]: """Identity kwargs every tool hook/middleware call carries (all coerced to ``""``).""" return { "task_id": effective_task_id or "", @@ -97,10 +97,10 @@ class InlineToolContext: InlineToolExecutor = Callable[[Any, dict, InlineToolContext], Any] # ``(kwarg, args_key)`` → ``args.get(key)``; ``(kwarg, args_key, default)`` → ``args.get(key, default)``. -_ArgSpec = Tuple[Any, ...] +_ArgSpec = tuple[Any, ...] -def _call_tool(module: str, func: str, args: dict, arg_specs: Tuple[_ArgSpec, ...], **fixed: Any) -> Any: +def _call_tool(module: str, func: str, args: dict, arg_specs: tuple[_ArgSpec, ...], **fixed: Any) -> Any: """Import ``module.func`` lazily and call it with args mapped per ``arg_specs`` plus ``fixed``.""" fn = getattr(import_module(module), func) return fn(**{spec[0]: args.get(*spec[1:]) for spec in arg_specs}, **fixed) @@ -234,7 +234,7 @@ def _setup_mcp_shim(agent, args: dict, ctx: InlineToolContext) -> Any: # Order is the historical if/elif order of ``execute_tool_calls_sequential``. -_RAW_INLINE_TOOL_EXECUTORS: Dict[str, InlineToolExecutor] = { +_RAW_INLINE_TOOL_EXECUTORS: dict[str, InlineToolExecutor] = { "todo_list": _tool( "tools.todo_tool", "todo_tool", ("todos", "todos"), ("merge", "merge", False), store=lambda agent, ctx: agent._todo_store, @@ -295,7 +295,7 @@ def _coerced(name: str, executor: InlineToolExecutor) -> InlineToolExecutor: return _exec -INLINE_TOOL_EXECUTORS: Dict[str, InlineToolExecutor] = { +INLINE_TOOL_EXECUTORS: dict[str, InlineToolExecutor] = { name: _coerced(name, executor) for name, executor in _RAW_INLINE_TOOL_EXECUTORS.items() } diff --git a/agent/insights.py b/agent/insights.py index 53ac36a7c9..50b7546c96 100644 --- a/agent/insights.py +++ b/agent/insights.py @@ -26,7 +26,7 @@ def _fmt_est_cost(est_cost: float) -> str: return format_cost_label(Decimal(str(est_cost))) -def _estimate_cost(session_or_model: Dict[str, Any] | str, input_tokens: int = 0, output_tokens: int = 0, *, cache_read_tokens: int = 0, +def _estimate_cost(session_or_model: dict[str, Any] | str, input_tokens: int = 0, output_tokens: int = 0, *, cache_read_tokens: int = 0, cache_write_tokens: int = 0, provider: Optional[str] = None, base_url: Optional[str] = None) -> tuple[float, str]: """Estimate the USD cost for a session row or a model/token tuple.""" if isinstance(session_or_model, dict): @@ -41,7 +41,7 @@ def _estimate_cost(session_or_model: Dict[str, Any] | str, input_tokens: int = 0 return float(result.amount_usd or 0.0), result.status -def _bar_chart(values: List[int], max_width: int = 20) -> List[str]: +def _bar_chart(values: list[int], max_width: int = 20) -> list[str]: peak = max(values) if values else 1 return ["" for _ in values] if peak == 0 else ["█" * max(1, int(v / peak * max_width)) if v > 0 else "" for v in values] @@ -170,7 +170,7 @@ class InsightsEngine: sql, params = (getattr(self, base + "_WITH_SOURCE"), (cutoff, source)) if source else (getattr(self, base + "_ALL"), (cutoff,)) return self._conn.execute(sql, params).fetchall() - def generate(self, days: int = 30, source: str = None) -> Dict[str, Any]: + def generate(self, days: int = 30, source: str = None) -> dict[str, Any]: """Generate a complete insights report for the last ``days`` days, optionally filtered by source platform.""" cutoff = time.time() - (days * 86400) # Drain the SessionDB's async accounting queue so counters are exact @@ -197,7 +197,7 @@ class InsightsEngine: "top_sessions": self._compute_top_sessions(sessions), } - def get_usage_breakdown(self, days: int = 30, source: str = None) -> Dict[str, Any]: + def get_usage_breakdown(self, days: int = 30, source: str = None) -> dict[str, Any]: """Analytics-usage payload (tools + skills) without a full generate(); the instr()-prefiltered skill query loads only skill_view/skill_manage messages.""" cutoff = time.time() - (days * 86400) @@ -206,7 +206,7 @@ class InsightsEngine: # ------------------------------------------------------------------ SQL - def _get_sessions(self, cutoff: float, source: str = None) -> List[Dict]: + def _get_sessions(self, cutoff: float, source: str = None) -> list[dict]: # Coerce the two epoch columns once at load: one corrupt/TEXT cell must degrade to "unknown" # for that session, never abort the whole report (#99959). rows = [dict(row) for row in self._query("_GET_SESSIONS", cutoff, source)] @@ -215,7 +215,7 @@ class InsightsEngine: row[col] = coerce_epoch(row.get(col), session_id=row.get("id"), field=col) return rows - def _get_tool_usage(self, cutoff: float, source: str = None) -> List[Dict]: + def _get_tool_usage(self, cutoff: float, source: str = None) -> list[dict]: """Tool call counts from two sources: ``tool_name`` on 'tool' rows (set by the gateway) and ``tool_calls`` JSON on assistant rows (covers CLI, where tool_name is not populated). The two views are reconciled PER @@ -237,9 +237,9 @@ class InsightsEngine: tool_counts[key[1]] += max(by_session_tool.get(key, 0), calls_by_session_tool.get(key, 0)) return [{"tool_name": name, "count": count} for name, count in tool_counts.most_common()] - def _get_skill_usage(self, cutoff: float, source: str = None) -> List[Dict]: + def _get_skill_usage(self, cutoff: float, source: str = None) -> list[dict]: """Extract per-skill usage from assistant tool calls.""" - skill_counts: Dict[str, Dict[str, Any]] = {} + skill_counts: dict[str, dict[str, Any]] = {} for row in self._query("_GET_SKILL_CALLS", cutoff, source): timestamp = row["timestamp"] for func in _iter_functions(row["tool_calls"]): @@ -255,11 +255,11 @@ class InsightsEngine: entry["last_used_at"] = timestamp return list(skill_counts.values()) - def _get_message_stats(self, cutoff: float, source: str = None) -> Dict: + def _get_message_stats(self, cutoff: float, source: str = None) -> dict: rows = self._query("_GET_MESSAGE_STATS", cutoff, source) return dict(rows[0]) if rows else {"total_messages": 0, "user_messages": 0, "assistant_messages": 0, "tool_messages": 0} - def _get_model_usage(self, cutoff: float, source: str = None) -> List[Dict]: + def _get_model_usage(self, cutoff: float, source: str = None) -> list[dict]: """Per-model usage rows; [] when the table is missing (older DB) so the caller falls back to the per-session aggregate.""" try: return [dict(row) for row in self._query("_GET_MODEL_USAGE", cutoff, source)] @@ -268,7 +268,7 @@ class InsightsEngine: # -------------------------------------------------------------- Compute - def _compute_overview(self, sessions: List[Dict], message_stats: Dict, models: Optional[List[Dict]] = None) -> Dict: + def _compute_overview(self, sessions: list[dict], message_stats: dict, models: Optional[list[dict]] = None) -> dict: # Per-model breakdown includes auxiliary usage rows (vision/compression/ # titles) plus reconciled residuals, while session counters carry # main-loop usage only — sum the breakdown when available so overview @@ -315,7 +315,7 @@ class InsightsEngine: "included_cost_sessions": status_counts["included"], } - def _compute_model_breakdown(self, sessions: List[Dict], cutoff: float, source: str = None) -> List[Dict]: + def _compute_model_breakdown(self, sessions: list[dict], cutoff: float, source: str = None) -> list[dict]: """Tokens/cost per model from session_model_usage, so a session that switched models via ``/model`` splits across every model it used. Sessions without per-model rows (pre-table data) fall back to their @@ -325,10 +325,10 @@ class InsightsEngine: model_data = defaultdict(lambda: {"sessions": set(), **dict.fromkeys(_TOKEN_KEYS, 0), "reasoning_tokens": 0, "total_tokens": 0, "api_calls": 0, "tool_calls": 0, "cost": 0.0, "actual_cost": 0.0}) - def _accumulate(model, provider, base_url, session_id, counts: Dict[str, int], *, + def _accumulate(model, provider, base_url, session_id, counts: dict[str, int], *, stored_cost=None, actual_cost=None, cost_status=None): model = model or "unknown" - d: Dict[str, Any] = model_data[_short_model(model)] + d: dict[str, Any] = model_data[_short_model(model)] d["sessions"].add(session_id) for key in _TOKEN_KEYS + ("reasoning_tokens",): d[key] += counts[key] @@ -345,7 +345,7 @@ class InsightsEngine: d["has_pricing"] = has_known_pricing(model, provider or None, base_url) or d.get("has_pricing", False) usage_totals = defaultdict(lambda: dict.fromkeys(count_keys, 0) | {"estimated_cost_usd": 0.0, "actual_cost_usd": 0.0}) for r in self._get_model_usage(cutoff, source): - totals: Dict[str, Any] = usage_totals[r["session_id"]] + totals: dict[str, Any] = usage_totals[r["session_id"]] counts = {key: r[key] or 0 for key in count_keys} for key in count_keys: totals[key] += counts[key] @@ -376,7 +376,7 @@ class InsightsEngine: for model, data in model_data.items()] return sorted(result, key=lambda x: (x["total_tokens"], x["sessions"]), reverse=True) - def _compute_platform_breakdown(self, sessions: List[Dict]) -> List[Dict]: + def _compute_platform_breakdown(self, sessions: list[dict]) -> list[dict]: platform_data = defaultdict(lambda: {"sessions": 0, "messages": 0, **dict.fromkeys(_TOKEN_KEYS, 0), "total_tokens": 0, "tool_calls": 0}) for s in sessions: d = platform_data[s.get("source") or "unknown"] @@ -388,12 +388,12 @@ class InsightsEngine: d["tool_calls"] += s.get("tool_call_count") or 0 return sorted(({"platform": platform, **data} for platform, data in platform_data.items()), key=lambda x: x["sessions"], reverse=True) - def _compute_tool_breakdown(self, tool_usage: List[Dict]) -> List[Dict]: + def _compute_tool_breakdown(self, tool_usage: list[dict]) -> list[dict]: """Ranked tool list with percentages.""" total_calls = sum(t["count"] for t in tool_usage) return [{"tool": t["tool_name"], "count": t["count"], "percentage": (t["count"] / total_calls * 100) if total_calls else 0} for t in tool_usage] - def _compute_skill_breakdown(self, skill_usage: List[Dict]) -> Dict[str, Any]: + def _compute_skill_breakdown(self, skill_usage: list[dict]) -> dict[str, Any]: """Per-skill usage → summary + ranked list.""" total_skill_loads = sum(s["view_count"] for s in skill_usage) total_skill_edits = sum(s["manage_count"] for s in skill_usage) @@ -409,7 +409,7 @@ class InsightsEngine: "top_skills": top_skills, } - def _compute_activity_patterns(self, sessions: List[Dict]) -> Dict: + def _compute_activity_patterns(self, sessions: list[dict]) -> dict: """Activity by day of week, hour, and active-day streak.""" day_counts, hour_counts, daily_counts = Counter(), Counter(), Counter() # weekday (0=Monday), hour, "YYYY-MM-DD" for s in sessions: @@ -439,7 +439,7 @@ class InsightsEngine: ("Most tool calls", lambda s: s.get("tool_call_count") or 0, "{} calls"), ) - def _compute_top_sessions(self, sessions: List[Dict]) -> List[Dict]: + def _compute_top_sessions(self, sessions: list[dict]) -> list[dict]: """Notable sessions (longest, most messages, most tokens, most tool calls).""" top = [] timed = [s for s in sessions if s.get("started_at") and s.get("ended_at")] @@ -457,11 +457,11 @@ class InsightsEngine: # ------------------------------------------------------------- Formatting @staticmethod - def _section(title: str) -> List[str]: + def _section(title: str) -> list[str]: return [f" {title}", " " + "─" * 56] @staticmethod - def _cost_lines(o: Dict, templates: tuple) -> List[str]: + def _cost_lines(o: dict, templates: tuple) -> list[str]: """One formatted line per non-zero cost bucket (estimated, included, unknown).""" # Cost breakdown — surface the three buckets so subscription-included and unknown-cost sessions are # visible instead of silently collapsing to $0. See #77223. @@ -469,7 +469,7 @@ class InsightsEngine: values = (_fmt_est_cost(est_cost) if est_cost > 0 else "", o.get("included_cost_sessions", 0), o.get("unknown_cost_sessions", 0)) return [tpl.format(v) for tpl, v in zip(templates, values) if v] - def format_terminal(self, report: Dict) -> str: + def format_terminal(self, report: dict) -> str: """Format the insights report for terminal display (CLI).""" if report.get("empty"): src = f" (source: {report['source_filter']})" if report.get("source_filter") else "" @@ -548,7 +548,7 @@ class InsightsEngine: lines += [f" {ts['label']:<20} {ts['value']:<18} ({ts['date']}, {ts['session_id']})" for ts in report["top_sessions"]] + [""] return "\n".join(lines) - def format_gateway(self, report: Dict) -> str: + def format_gateway(self, report: dict) -> str: """Format the insights report for gateway/messaging (shorter).""" if report.get("empty"): return f"No sessions found in the last {report.get('days', 30)} days." diff --git a/agent/jiter_preload.py b/agent/jiter_preload.py index 6a69352a78..61b90820b8 100644 --- a/agent/jiter_preload.py +++ b/agent/jiter_preload.py @@ -18,7 +18,7 @@ def preload_jiter_native_extension() -> bool: return True try: importlib.import_module("jiter.jiter") - from jiter import from_json as _from_json # noqa: F401 + from jiter import from_json as _from_json except Exception as exc: _JITER_PRELOAD_ERROR = exc return False diff --git a/agent/kanban_turn_recovery.py b/agent/kanban_turn_recovery.py index e54df56446..93263e875e 100644 --- a/agent/kanban_turn_recovery.py +++ b/agent/kanban_turn_recovery.py @@ -120,8 +120,7 @@ def max_recovery_attempts() -> int: def recovery_delay_seconds(attempt: int) -> float: """Backoff before recovery attempt ``attempt`` (1-based); last entry repeats.""" - if attempt < 1: - attempt = 1 + attempt = max(attempt, 1) index = min(attempt - 1, len(RECOVERY_DELAYS_SECONDS) - 1) return RECOVERY_DELAYS_SECONDS[index] diff --git a/agent/learning_graph_render.py b/agent/learning_graph_render.py index f0be37e5fb..2675e27840 100644 --- a/agent/learning_graph_render.py +++ b/agent/learning_graph_render.py @@ -27,7 +27,7 @@ Row = list # of runs ``[text, style, alpha, hex?]``; a grid is a list of rows def _clamp(v: float, lo: float, hi: float) -> float: - return lo if v < lo else hi if v > hi else v + return lo if v < lo else min(v, hi) def _lerp(a: float, b: float, t: float) -> float: @@ -191,7 +191,7 @@ def _skill_category_counts(nodes: Iterable[dict[str, Any]]) -> Counter: # ── Timeline chart frame ───────────────────────────────────────────────────── class _ChartBucket: - __slots__ = ("label", "ts", "nodes", "rec") + __slots__ = ("label", "nodes", "rec", "ts") def __init__(self, label: str, ts: float): self.label, self.ts, self.rec = label, ts, 1.0 diff --git a/agent/legacy_cli.py b/agent/legacy_cli.py index 7daf9a967f..74d9d6f8ce 100644 --- a/agent/legacy_cli.py +++ b/agent/legacy_cli.py @@ -11,7 +11,7 @@ from __future__ import annotations # hermes_bootstrap first (UTF-8 stdio on Windows; no-op on POSIX), like every other entry point. try: - import hermes_bootstrap # noqa: F401 + import hermes_bootstrap except ModuleNotFoundError: pass # partial `hermes update` — only skips the Windows UTF-8 stdio setup @@ -22,8 +22,8 @@ from hermes_cli import _early_recovery if _early_recovery.restore_interrupted_pull(): _early_recovery.relaunch_after_restore() -import argparse # noqa: E402 -from typing import Callable, List, Optional # noqa: E402 +import argparse +from typing import Callable, List, Optional def _build_parser() -> argparse.ArgumentParser: @@ -57,7 +57,7 @@ def _build_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None, *, run: Optional[Callable[..., object]] = None) -> int: +def main(argv: Optional[list[str]] = None, *, run: Optional[Callable[..., object]] = None) -> int: """Parse ``argv`` (default ``sys.argv[1:]``) and run one query through ``run_agent.main``. Metadata flags and a bare invocation never reach the runner; ``run`` lets diff --git a/agent/lmstudio_reasoning.py b/agent/lmstudio_reasoning.py index 1b6be3842a..77db9f2fae 100644 --- a/agent/lmstudio_reasoning.py +++ b/agent/lmstudio_reasoning.py @@ -20,7 +20,7 @@ _LM_EFFORT_ALIASES = {"off": "none", "on": "medium"} _LM_EFFORT_CLAMP = {"max": "xhigh", "ultra": "xhigh"} -def resolve_lmstudio_effort(reasoning_config: Optional[dict], allowed_options: Optional[List[str]]) -> Optional[str]: +def resolve_lmstudio_effort(reasoning_config: Optional[dict], allowed_options: Optional[list[str]]) -> Optional[str]: """Return the ``reasoning_effort`` to send to LM Studio, or ``None`` = omit the field (the user picked a level the model can't honor, so LM Studio falls back to the model's declared default rather than a silently substituted effort). diff --git a/agent/lsp/__init__.py b/agent/lsp/__init__.py index 7468e131ab..e7f0338ee5 100644 --- a/agent/lsp/__init__.py +++ b/agent/lsp/__init__.py @@ -72,7 +72,7 @@ def release_workspace(path: str) -> int: for svc in services: try: released += svc.release_workspace(path) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("LSP workspace release failed for %s: %s", path, e) return released @@ -88,7 +88,7 @@ def shutdown_service() -> None: if svc is not None: try: svc.shutdown() - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("LSP shutdown error: %s", e) @@ -96,7 +96,7 @@ def _atexit_shutdown() -> None: """atexit wrapper; logs at debug since the user has already seen the final output.""" try: shutdown_service() - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("atexit LSP shutdown failed: %s", e) diff --git a/agent/lsp/cli.py b/agent/lsp/cli.py index 1ce05a47a6..2d1e805e9f 100644 --- a/agent/lsp/cli.py +++ b/agent/lsp/cli.py @@ -62,7 +62,7 @@ def _all_servers() -> list: from hermes_cli.config import load_config_readonly try: lsp_cfg = load_config_readonly().get("lsp") or {} - except Exception: # noqa: BLE001 — a broken config still lists the built-ins + except Exception: lsp_cfg = {} return [*custom_servers(lsp_cfg.get("servers") if isinstance(lsp_cfg, dict) else None), *SERVERS] diff --git a/agent/lsp/client.py b/agent/lsp/client.py index 4a31d12612..3115f31748 100644 --- a/agent/lsp/client.py +++ b/agent/lsp/client.py @@ -55,7 +55,7 @@ MAX_TRACKED_FILES = 64 _WRITE_ERRORS = (BrokenPipeError, ConnectionResetError, OSError) _LIVE_STATES = {"starting", "running"} -_CLIENT_CAPABILITIES: Dict[str, Any] = { +_CLIENT_CAPABILITIES: dict[str, Any] = { "window": {"workDoneProgress": True}, "workspace": {"configuration": True, "workspaceFolders": True, "didChangeWatchedFiles": {"dynamicRegistration": True}, "diagnostics": {"refreshSupport": False}}, @@ -84,7 +84,7 @@ def file_uri(path: str) -> str: return "file://" + quote(abs_path, safe="/:") -def _folder(root: str) -> Dict[str, str]: +def _folder(root: str) -> dict[str, str]: """Build an LSP ``WorkspaceFolder`` for ``root``.""" return {"name": os.path.basename(root.rstrip(os.sep)) or root, "uri": file_uri(root)} @@ -99,7 +99,7 @@ def uri_to_path(uri: str) -> str: return os.path.normpath(unquote(raw)) -def _end_position(text: str) -> Dict[str, int]: +def _end_position(text: str) -> dict[str, int]: """LSP Position at the end of ``text`` (for a whole-document replace range).""" if not text: return {"line": 0, "character": 0} @@ -119,8 +119,8 @@ class _DocState: publishDiagnostics get exact tagging; others are credited with the current version at receipt.""" version: int = 0 text: str = "" - push: List[Dict[str, Any]] = field(default_factory=list) - pull: List[Dict[str, Any]] = field(default_factory=list) + push: list[dict[str, Any]] = field(default_factory=list) + pull: list[dict[str, Any]] = field(default_factory=list) push_version: int = -1 pull_version: int = -1 seed_seen: bool = False @@ -139,15 +139,15 @@ class LSPClient: """One server process + one workspace root. ``start()`` → ``open_file()`` → ``wait_for_diagnostics()`` → ``diagnostics_for()`` → ``shutdown()``.""" - def __init__(self, *, server_id: str, workspace_root: str, command: List[str], - env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None, - initialization_options: Optional[Dict[str, Any]] = None, + def __init__(self, *, server_id: str, workspace_root: str, command: list[str], + env: Optional[dict[str, str]] = None, cwd: Optional[str] = None, + initialization_options: Optional[dict[str, Any]] = None, seed_diagnostics_on_first_push: bool = False) -> None: self.server_id = server_id self.workspace_root = workspace_root # Roots this server serves. Single-root servers only ever hold ``workspace_root``; # multi-root servers (pyright) grow this via ``add_workspace_folder`` instead of a second process. - self.workspace_folders: List[str] = [workspace_root] + self.workspace_folders: list[str] = [workspace_root] self._command = list(command) self._env = env self._cwd = cwd or workspace_root @@ -157,16 +157,16 @@ class LSPClient: self._proc: Optional[asyncio.subprocess.Process] = None self._stderr_task: Optional[asyncio.Task] = None # Ring buffer of the most recent server stderr lines, surfaced when spawn/initialize fails. - self._stderr_tail: List[str] = [] + self._stderr_tail: list[str] = [] self._exit_code: Optional[int] = None self._reader_task: Optional[asyncio.Task] = None self._cleanup_lock = asyncio.Lock() self._next_id: int = 0 - self._pending: Dict[int, asyncio.Future] = {} + self._pending: dict[int, asyncio.Future] = {} # Server → client requests; anything else gets method-not-found. Capability (un)registration # and diagnostic refresh are acknowledged but not acted on: we re-pull on every touch anyway. - self._request_handlers: Dict[str, Callable[[Any], Awaitable[Any]]] = { + self._request_handlers: dict[str, Callable[[Any], Awaitable[Any]]] = { "window/workDoneProgress/create": self._handle_null, "workspace/configuration": self._handle_workspace_configuration, "client/registerCapability": self._handle_null, @@ -175,11 +175,11 @@ class LSPClient: "workspace/diagnostic/refresh": self._handle_null, } # Server → client notifications; others (showMessage, $/progress) are dropped. - self._notification_handlers: Dict[str, Callable[[Any], None]] = { + self._notification_handlers: dict[str, Callable[[Any], None]] = { "textDocument/publishDiagnostics": self._handle_publish_diagnostics, } - self._docs: Dict[str, _DocState] = {} # keyed by absolute path (NOT URI) + self._docs: dict[str, _DocState] = {} # keyed by absolute path (NOT URI) self._state: str = "stopped" self._sync_kind: int = 1 # 1=Full, 2=Incremental self._stopping: bool = False @@ -313,7 +313,7 @@ class LSPClient: def failure_details(self) -> str: """Exit status + last stderr lines for a failed spawn/initialize; empty for a live server.""" - parts: List[str] = [] + parts: list[str] = [] if exit_desc := self._describe_exit(): parts.append(exit_desc) if self._stderr_tail: @@ -353,7 +353,7 @@ class LSPClient: if unexpected_close: await self._cleanup_process() - def _workspace_folders(self) -> List[Dict[str, str]]: + def _workspace_folders(self) -> list[dict[str, str]]: return [_folder(r) for r in self.workspace_folders] async def add_workspace_folder(self, root: str) -> None: @@ -405,7 +405,7 @@ class LSPClient: pass try: await self._send_notification("exit", None) - except Exception: # noqa: BLE001 + except Exception: pass # Signalling right after ``exit`` races the server's own exit: needless SIGTERM # noise for well-behaved servers and, on Darwin, a reaped-and-reused PID target. @@ -517,7 +517,7 @@ class LSPClient: else: try: reply = make_response(req_id, await handler(msg.get("params"))) - except Exception as e: # noqa: BLE001 — protocol must not blow up + except Exception as e: logger.warning("[%s] request handler %s failed: %s", self.server_id, method, e) reply = make_error_response(req_id, -32000, f"handler failed: {e}") await self._send_reply(reply) @@ -528,7 +528,7 @@ class LSPClient: return try: handler(msg.get("params")) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("[%s] notification handler %s failed: %s", self.server_id, method, e) # ---- built-in server-→-client request handlers ---- @@ -611,7 +611,7 @@ class LSPClient: return 0 # pop + reinsert refreshes LRU recency (dicts are insertion-ordered). self._docs[abs_path] = self._docs.pop(abs_path) - change: Dict[str, Any] = {"text": text} + change: dict[str, Any] = {"text": text} if self._sync_kind == 2: change["range"] = {"start": {"line": 0, "character": 0}, "end": _end_position(doc.text)} new_version = doc.version + 1 @@ -741,7 +741,7 @@ class LSPClient: continue await self._await_push(min(remaining, 0.5)) - def diagnostics_for(self, path: str, *, fresh_only: bool = False) -> List[Dict[str, Any]]: + def diagnostics_for(self, path: str, *, fresh_only: bool = False) -> list[dict[str, Any]]: """Merged + deduped push/pull diagnostics for one file. With ``fresh_only=True`` a store only contributes once its version tag has caught up to the document's — report paths must use this so "stale" and "clean" aren't conflated.""" @@ -753,9 +753,9 @@ class LSPClient: return _dedupe(push, pull) -def _dedupe(*lists: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - seen: Set[str] = set() - out: List[Dict[str, Any]] = [] +def _dedupe(*lists: list[dict[str, Any]]) -> list[dict[str, Any]]: + seen: set[str] = set() + out: list[dict[str, Any]] = [] for d in (d for lst in lists for d in lst if isinstance(d, dict)): if (key := _diagnostic_key(d)) not in seen: seen.add(key) @@ -763,7 +763,7 @@ def _dedupe(*lists: List[Dict[str, Any]]) -> List[Dict[str, Any]]: return out -def _diagnostic_key(d: Dict[str, Any]) -> str: +def _diagnostic_key(d: dict[str, Any]) -> str: """Content-equality key: severity + code + source + message + range. Shared with the manager's cross-edit delta filter (``_diag_key``) so both layers agree on identity. Range is included so an identical error at a second site still surfaces as new (the manager line-shifts its baseline first).""" diff --git a/agent/lsp/eventlog.py b/agent/lsp/eventlog.py index 5aea278a4d..c85bdb2724 100644 --- a/agent/lsp/eventlog.py +++ b/agent/lsp/eventlog.py @@ -50,7 +50,7 @@ def _emit(server_id: str, level: int, message: str) -> None: event_log.log(level, "lsp[%s] %s", server_id, message) -def _emit_once(bucket: set, key: Tuple, server_id: str, level: int, first: str, repeat: str) -> None: +def _emit_once(bucket: set, key: tuple, server_id: str, level: int, first: str, repeat: str) -> None: """Log *first* at *level* the first time *key* is seen, *repeat* at DEBUG thereafter.""" with _announce_lock: is_first = key not in bucket @@ -144,7 +144,7 @@ def log_untrusted_skipped(server_id: str, workspace_root: str, file_path: str) - f"skipped: untrusted workspace {workspace_root}") -def log_reaped(keys: List[Tuple[str, str]], idle_timeout: float) -> None: +def log_reaped(keys: list[tuple[str, str]], idle_timeout: float) -> None: """Idle clients were reaped. INFO, one line per sweep. Also forgets the ``log_active`` announcement for those keys so a respawn @@ -156,7 +156,7 @@ def log_reaped(keys: List[Tuple[str, str]], idle_timeout: float) -> None: _emit("reaper", logging.INFO, f"reaped {len(keys)} idle client(s) after {idle_timeout:.0f}s: {summary}") -def log_released(keys: List[Tuple[str, str]], reason: str) -> None: +def log_released(keys: list[tuple[str, str]], reason: str) -> None: """Clients were shut down because their workspace went away (worktree released or root deleted). INFO, one line per event; forgets the ``log_active`` announcement like :func:`log_reaped`.""" with _announce_lock: diff --git a/agent/lsp/install.py b/agent/lsp/install.py index 76f9bccaac..01eece2e76 100644 --- a/agent/lsp/install.py +++ b/agent/lsp/install.py @@ -24,15 +24,15 @@ from hermes_constants import find_node_executable, with_hermes_node_path logger = logging.getLogger("agent.lsp.install") -def _recipe(strategy: str, pkg: str, bin_name: str, **extra: Any) -> Dict[str, Any]: +def _recipe(strategy: str, pkg: str, bin_name: str, **extra: Any) -> dict[str, Any]: return {"strategy": strategy, "pkg": pkg, "bin": bin_name, **extra} -def _npm(pkg: str, bin_name: str, **extra: Any) -> Dict[str, Any]: +def _npm(pkg: str, bin_name: str, **extra: Any) -> dict[str, Any]: return _recipe("npm", pkg, bin_name, **extra) -def _manual(bin_name: str) -> Dict[str, Any]: +def _manual(bin_name: str) -> dict[str, Any]: return _recipe("manual", "", bin_name) @@ -43,7 +43,7 @@ TYPESCRIPT_SDK_PKG = "typescript@6" # Recipe key → {strategy, pkg, bin[, extra_pkgs]}. After install we look for # ``bin`` in ``/lsp/bin/`` first, then on PATH. ``extra_pkgs`` # are sibling npm packages a server needs in the same node_modules tree. -INSTALL_RECIPES: Dict[str, Dict[str, Any]] = { +INSTALL_RECIPES: dict[str, dict[str, Any]] = { "pyright": _npm("pyright", "pyright-langserver"), # tsserver must be importable from the same node_modules tree or # initialize() fails with "Could not find a valid TypeScript installation". @@ -72,8 +72,8 @@ INSTALL_RECIPES: Dict[str, Dict[str, Any]] = { "powershell": _manual("pwsh"), } -_install_locks: Dict[str, threading.Lock] = {} -_install_results: Dict[str, Optional[str]] = {} +_install_locks: dict[str, threading.Lock] = {} +_install_results: dict[str, Optional[str]] = {} _install_lock_meta = threading.Lock() _WINDOWS_WRAPPER_SUFFIXES = (".cmd", ".exe", ".bat") @@ -101,7 +101,7 @@ def _native_binary_candidates(base: Path, *, is_windows: Optional[bool] = None) """ if not (_is_windows() if is_windows is None else is_windows): return [base] - cands: Dict[str, Path] = {} + cands: dict[str, Path] = {} for c in (*(Path(str(base) + s) for s in _WINDOWS_WRAPPER_SUFFIXES), base): cands.setdefault(str(c).lower(), c) return list(cands.values()) @@ -212,7 +212,7 @@ def _link_into_bin(target: Path) -> str: # Node package manager → argv that installs into ``/node_modules`` (``lsp.package_manager``). # Every manager keeps the staging-dir semantics: nothing touches the user's project or global tree. -_NODE_PM_ARGV: Dict[str, Callable[[str], list]] = { +_NODE_PM_ARGV: dict[str, Callable[[str], list]] = { "npm": lambda staging: ["install", "--prefix", staging, "--silent", "--no-fund", "--no-audit"], "pnpm": lambda staging: ["add", "--dir", staging], # Global ``--cwd`` (before the command) is accepted by both Yarn Classic and Yarn Berry; Berry's @@ -226,7 +226,7 @@ def _node_package_manager() -> Optional[str]: try: from hermes_cli.config import load_config_readonly lsp_cfg = load_config_readonly().get("lsp") or {} - except Exception: # noqa: BLE001 — installer must not die on a broken config; npm is the historical default + except Exception: return "npm" pm = str(lsp_cfg.get("package_manager") or "npm").strip().lower() if isinstance(lsp_cfg, dict) else "npm" if pm not in _NODE_PM_ARGV: @@ -250,7 +250,7 @@ def _install_npm(pkg: str, bin_name: str, extra_pkgs: Optional[list] = None) -> pm_store.ensure("npm") pm_bin = find_node_executable("npm") - except Exception as exc: # noqa: BLE001 — a refused/failed PM install skips this server, never the session + except Exception as exc: logger.warning("[install] cannot install %s: managed npm unavailable (%s)", pkg, exc) return None if pm_bin is None: @@ -306,7 +306,7 @@ def _install_pip(pkg: str, bin_name: str) -> Optional[str]: # strategy → installer(recipe, bin_name). ``manual`` is handled before dispatch. -_INSTALLERS: Dict[str, Callable[[Dict[str, Any], str], Optional[str]]] = { +_INSTALLERS: dict[str, Callable[[dict[str, Any], str], Optional[str]]] = { "npm": lambda r, b: _install_npm(r["pkg"], b, extra_pkgs=r.get("extra_pkgs") or []), "go": lambda r, b: _install_go(r["pkg"], b), "pip": lambda r, b: _install_pip(r["pkg"], b), diff --git a/agent/lsp/manager.py b/agent/lsp/manager.py index 56f83e3cc5..7550aeef7f 100644 --- a/agent/lsp/manager.py +++ b/agent/lsp/manager.py @@ -33,8 +33,8 @@ DEFAULT_IDLE_TIMEOUT = 600 # seconds; servers idle for >10min get reaped _DELTA_BASELINE_CAP = 256 # per-file pre-write snapshots; paths never written again would otherwise live forever (#62950) MIN_IDLE_TIMEOUT = 30 # floor for config values; must exceed any per-op wait budget -_Key = Tuple[str, str] -_Diags = List[Dict[str, Any]] +_Key = tuple[str, str] +_Diags = list[dict[str, Any]] def _float_or(value: Any, default: float) -> float: @@ -44,7 +44,7 @@ def _float_or(value: Any, default: float) -> float: return default -def _path_list(value: Any) -> Optional[List[str]]: +def _path_list(value: Any) -> Optional[list[str]]: """A config list of paths, ``~``-expanded; ``None`` when the value is not a list of strings.""" if value is None: return [] @@ -53,7 +53,7 @@ def _path_list(value: Any) -> Optional[List[str]]: return [os.path.expanduser(p) for p in value if p] -def _parse_exclude_roots(value: Any) -> Optional[List[str]]: +def _parse_exclude_roots(value: Any) -> Optional[list[str]]: """``lsp.exclude_roots``; ``None`` (fail closed: every root excluded) for a malformed value.""" if (roots := _path_list(value)) is None: eventlog.event_log.warning( @@ -62,7 +62,7 @@ def _parse_exclude_roots(value: Any) -> Optional[List[str]]: return roots -def parse_trusted_workspaces(value: Any) -> List[str]: +def parse_trusted_workspaces(value: Any) -> list[str]: """``lsp.trusted_workspaces``; a malformed value trusts nothing extra.""" if (roots := _path_list(value)) is None: eventlog.event_log.warning( @@ -103,7 +103,7 @@ class _BackgroundLoop: finally: try: loop.close() - except Exception: # noqa: BLE001 + except Exception: pass def run(self, coro, *, timeout: Optional[float] = None) -> Any: @@ -139,12 +139,12 @@ class LSPService: def __init__( self, *, enabled: bool, wait_mode: str, wait_timeout: float, install_strategy: str, - binary_overrides: Optional[Dict[str, List[str]]] = None, - env_overrides: Optional[Dict[str, Dict[str, str]]] = None, - init_overrides: Optional[Dict[str, Dict[str, Any]]] = None, - disabled_servers: Optional[List[str]] = None, + binary_overrides: Optional[dict[str, list[str]]] = None, + env_overrides: Optional[dict[str, dict[str, str]]] = None, + init_overrides: Optional[dict[str, dict[str, Any]]] = None, + disabled_servers: Optional[list[str]] = None, idle_timeout: float = DEFAULT_IDLE_TIMEOUT, - extra_servers: Optional[List[ServerDef]] = None, + extra_servers: Optional[list[ServerDef]] = None, broken_retry_seconds: float = 0.0, warmup_timeout: float = 0.0, exclude_roots: Any = None, @@ -159,14 +159,14 @@ class LSPService: self._init_overrides = init_overrides or {} self._disabled_servers = set(disabled_servers or []) self._idle_timeout = idle_timeout - self._extra_servers: List[ServerDef] = list(extra_servers or []) + self._extra_servers: list[ServerDef] = list(extra_servers or []) self._broken_retry = max(0.0, broken_retry_seconds) self._warmup_timeout = max(0.0, warmup_timeout) # ``None`` = misconfigured → fail closed (every root excluded) until the user fixes the key: a # bare string here means "exclude that workspace", and excluding nothing would re-pay the stall it # was meant to avoid. - self._exclude_roots: Optional[List[str]] = _parse_exclude_roots(exclude_roots) - self._trusted_workspaces: List[str] = parse_trusted_workspaces(trusted_workspaces) + self._exclude_roots: Optional[list[str]] = _parse_exclude_roots(exclude_roots) + self._trusted_workspaces: list[str] = parse_trusted_workspaces(trusted_workspaces) self._untrusted_skipped: set = set() # (server_id, root) pairs denied by workspace trust self._operator_roots = frozenset(operator_workspace_roots()) # see _note_operator_roots @@ -175,15 +175,15 @@ class LSPService: self._loop.start() # Per-(server_id, workspace_root) state - self._clients: Dict[_Key, LSPClient] = {} + self._clients: dict[_Key, LSPClient] = {} # (server_id, root) → monotonic deadline after which the pair may be retried (inf = lifetime). - self._broken: Dict[_Key, float] = {} - self._spawning: Dict[_Key, asyncio.Future] = {} - self._last_used: Dict[_Key, float] = {} + self._broken: dict[_Key, float] = {} + self._spawning: dict[_Key, asyncio.Future] = {} + self._last_used: dict[_Key, float] = {} self._state_lock = threading.Lock() self._idle_reaper_task: Optional[asyncio.Task] = None # abs file path → diagnostics snapshot taken immediately before a write. - self._delta_baseline: Dict[str, _Diags] = {} + self._delta_baseline: dict[str, _Diags] = {} if self._enabled and self._idle_timeout > 0: self._loop.run(self._start_idle_reaper(), timeout=2.0) @@ -194,7 +194,7 @@ class LSPService: try: from hermes_cli.config import load_config_readonly cfg = load_config_readonly() - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("LSP config load failed: %s", e) return None lsp_cfg = cfg.get("lsp") if isinstance(cfg, dict) else None @@ -255,7 +255,7 @@ class LSPService: return None try: return (srv.server_id, srv.resolve_root(file_path, ws_root) or ws_root) - except Exception: # noqa: BLE001 + except Exception: return (srv.server_id, ws_root) def enabled_for(self, file_path: str) -> bool: @@ -354,7 +354,7 @@ class LSPService: budget = self._wait_budget(file_path) t = max(DIAGNOSTICS_DOCUMENT_WAIT + 3.0, budget + 3.0) diags = self._loop.run(self._snapshot_async(file_path, budget), timeout=t) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("baseline snapshot failed for %s: %s", file_path, e) self._mark_broken_for_file(file_path, e) diags = [] @@ -387,7 +387,7 @@ class LSPService: budget = self._wait_budget(file_path) t = timeout if timeout is not None else budget + 2.0 diags = self._loop.run(self._open_and_wait_async(file_path, budget=budget), timeout=t) - except Exception as e: # noqa: BLE001 + except Exception as e: if isinstance(e, asyncio.TimeoutError): eventlog.log_timeout(server_id, file_path) logger.debug("LSP diagnostics timeout for %s: %s", file_path, e) @@ -424,7 +424,7 @@ class LSPService: # Roll the baseline forward so the next call is a delta against this state. try: fresh = self._loop.run(self._current_diags_async(file_path), timeout=2.0) or [] - except Exception: # noqa: BLE001 + except Exception: fresh = [] if fresh: self._set_delta_baseline(abs_path, fresh) @@ -449,7 +449,7 @@ class LSPService: try: # Fire-and-forget shutdown — we're already on a slow path. self._loop.run(client.shutdown(), timeout=1.0) - except Exception: # noqa: BLE001 + except Exception: pass if not already_broken: eventlog.log_spawn_failed(key[0], key[1], exc) @@ -460,12 +460,12 @@ class LSPService: return try: self._loop.run(self._shutdown_async(), timeout=10.0) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("LSP shutdown error: %s", e) self._loop.stop() clear_cache() - def get_status(self) -> Dict[str, Any]: + def get_status(self) -> dict[str, Any]: """Return a snapshot of the service for ``hermes lsp status``.""" with self._state_lock: clients = [ @@ -512,7 +512,7 @@ class LSPService: file_path, version, mode=self._wait_mode, timeout=budget if budget is not None else self._wait_timeout, ) - except Exception as e: # noqa: BLE001 + except Exception as e: if snapshot: logger.debug("snapshot open/wait failed: %s", e) else: @@ -574,7 +574,7 @@ class LSPService: if not owner: try: client = await spawning - except Exception: # noqa: BLE001 + except Exception: return None return await self._attach_root(srv, client, root) if client is not None else None try: @@ -618,7 +618,7 @@ class LSPService: ) try: await client.start() - except Exception as e: # noqa: BLE001 + except Exception as e: eventlog.log_spawn_failed(srv.server_id, root, e) return None return client @@ -641,7 +641,7 @@ class LSPService: await self._reap_idle_once() except asyncio.CancelledError: raise - except Exception as e: # noqa: BLE001 + except Exception as e: # A transient sweep error must not kill the reaper, or the accumulation leak it fixes comes back. logger.debug("LSP idle reaper sweep error: %s", e) @@ -671,7 +671,7 @@ class LSPService: root = os.path.abspath(workspace_root) try: return self._loop.run(self._release_async(root), timeout=15.0) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("LSP release of %s failed: %s", root, e) return 0 @@ -709,7 +709,7 @@ class LSPService: for folder in folders: try: await client.remove_workspace_folder(folder) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("LSP folder removal for %s failed: %s", folder, e) if clients: eventlog.log_released([(c.server_id, c.workspace_root) for c in clients], reason) diff --git a/agent/lsp/protocol.py b/agent/lsp/protocol.py index 9596e26b82..5e3e52465b 100644 --- a/agent/lsp/protocol.py +++ b/agent/lsp/protocol.py @@ -119,7 +119,7 @@ def make_error_response(req_id: Any, code: int, message: str, data: Any = None) return {"jsonrpc": "2.0", "id": req_id, "error": err} -def classify_message(msg: dict) -> Tuple[str, Any]: +def classify_message(msg: dict) -> tuple[str, Any]: """Return ``(kind, key)``: kind ∈ request/response/notification/invalid; key is the id (request/response), the method (notification) or ``None`` (invalid).""" if not isinstance(msg, dict) or msg.get("jsonrpc") != "2.0": diff --git a/agent/lsp/range_shift.py b/agent/lsp/range_shift.py index cdcd5ccc51..e5cc6ac509 100644 --- a/agent/lsp/range_shift.py +++ b/agent/lsp/range_shift.py @@ -42,7 +42,7 @@ def build_line_shift(pre_text: str, post_text: str) -> _Shift: return shift -def shift_diagnostic_range(diag: Dict[str, Any], shift: _Shift) -> Optional[Dict[str, Any]]: +def shift_diagnostic_range(diag: dict[str, Any], shift: _Shift) -> Optional[dict[str, Any]]: """Copy of ``diag`` with its line range remapped; ``None`` if the start line was deleted. A multi-line diagnostic whose end straddles the deletion collapses to a @@ -67,7 +67,7 @@ def shift_diagnostic_range(diag: Dict[str, Any], shift: _Shift) -> Optional[Dict } -def shift_baseline(baseline: List[Dict[str, Any]], shift: _Shift) -> List[Dict[str, Any]]: +def shift_baseline(baseline: list[dict[str, Any]], shift: _Shift) -> list[dict[str, Any]]: """Apply ``shift`` to every diagnostic in ``baseline``, dropping deleted entries.""" shifted = (shift_diagnostic_range(d, shift) for d in baseline if isinstance(d, dict)) return [s for s in shifted if s is not None] diff --git a/agent/lsp/reporter.py b/agent/lsp/reporter.py index 939e9ae8cc..a05a64e829 100644 --- a/agent/lsp/reporter.py +++ b/agent/lsp/reporter.py @@ -40,7 +40,7 @@ def _sanitize_field(value: Any, *, limit: int) -> str: return html.escape(raw.strip()[:limit], quote=False) -def format_diagnostic(d: Dict[str, Any]) -> str: +def format_diagnostic(d: dict[str, Any]) -> str: """One-line representation of a single diagnostic (fields sanitized).""" sev = SEVERITY_NAMES.get(d.get("severity") or 1, "ERROR") start = (d.get("range") or {}).get("start") or {} @@ -54,7 +54,7 @@ def format_diagnostic(d: Dict[str, Any]) -> str: def report_for_file( file_path: str, - diagnostics: List[Dict[str, Any]], + diagnostics: list[dict[str, Any]], *, severities: frozenset = DEFAULT_SEVERITIES, max_per_file: int = MAX_PER_FILE, diff --git a/agent/lsp/servers.py b/agent/lsp/servers.py index 3a584b8c8b..4836738205 100644 --- a/agent/lsp/servers.py +++ b/agent/lsp/servers.py @@ -20,7 +20,7 @@ logger = logging.getLogger("agent.lsp.servers") # LSP languageId for ``textDocument/didOpen``, as language → extensions. A few # servers (typescript-language-server, vue-language-server) refuse wrong IDs. -_EXTS_BY_LANGUAGE: Dict[str, Sequence[str]] = { +_EXTS_BY_LANGUAGE: dict[str, Sequence[str]] = { "python": (".py", ".pyi"), "typescript": (".ts", ".mts", ".cts"), "typescriptreact": (".tsx",), @@ -45,7 +45,7 @@ _EXTS_BY_LANGUAGE: Dict[str, Sequence[str]] = { "dockerfile": (".dockerfile",), "powershell": (".ps1", ".psm1", ".psd1"), } -LANGUAGE_BY_EXT: Dict[str, str] = {ext: lang for lang, exts in _EXTS_BY_LANGUAGE.items() for ext in exts} +LANGUAGE_BY_EXT: dict[str, str] = {ext: lang for lang, exts in _EXTS_BY_LANGUAGE.items() for ext in exts} _SpawnFn = Callable[[str, "ServerContext"], Optional["SpawnSpec"]] _RootFn = Callable[[str, str], Optional[str]] @@ -54,11 +54,11 @@ _RootFn = Callable[[str, str], Optional[str]] @dataclass class SpawnSpec: """Result of resolving a server for a file (``None`` means skip).""" - command: List[str] + command: list[str] workspace_root: str cwd: str - env: Dict[str, str] = field(default_factory=dict) - initialization_options: Dict[str, Any] = field(default_factory=dict) + env: dict[str, str] = field(default_factory=dict) + initialization_options: dict[str, Any] = field(default_factory=dict) seed_diagnostics_on_first_push: bool = False @@ -67,7 +67,7 @@ class ServerDef: """One language server: ``resolve_root(file, ws)`` → per-server root or ``None`` to skip; ``build_spawn(root, ctx)`` → :class:`SpawnSpec` or ``None`` when the binary can't be found.""" server_id: str - extensions: Tuple[str, ...] + extensions: tuple[str, ...] resolve_root: _RootFn build_spawn: _SpawnFn seed_first_push: bool = False @@ -87,9 +87,9 @@ class ServerContext: """User policy passed into :meth:`ServerDef.build_spawn` (install strategy, overrides).""" workspace_root: str install_strategy: str = "auto" # "auto" | "manual" | "off" - binary_overrides: Dict[str, List[str]] = field(default_factory=dict) - env_overrides: Dict[str, Dict[str, str]] = field(default_factory=dict) - init_overrides: Dict[str, Dict[str, Any]] = field(default_factory=dict) + binary_overrides: dict[str, list[str]] = field(default_factory=dict) + env_overrides: dict[str, dict[str, str]] = field(default_factory=dict) + init_overrides: dict[str, dict[str, Any]] = field(default_factory=dict) # Whether the server may load code the project ships (see workspace.is_trusted_workspace). # Defaults closed: a context built without a trust decision must never run the repo's code. trusted: bool = False @@ -142,8 +142,8 @@ def _find_binary(ctx: ServerContext, server_id: str, which: Sequence[str], insta return bin_path -def _make_spec(root: str, ctx: ServerContext, server_id: str, command: List[str], - base_init: Optional[Dict[str, Any]] = None, seed: bool = False) -> SpawnSpec: +def _make_spec(root: str, ctx: ServerContext, server_id: str, command: list[str], + base_init: Optional[dict[str, Any]] = None, seed: bool = False) -> SpawnSpec: from pm import env_for init = ctx.init_overrides.get(server_id, {}) if base_init is None else {**base_init, **ctx.init_overrides.get(server_id, {})} @@ -154,8 +154,8 @@ def _make_spec(root: str, ctx: ServerContext, server_id: str, command: List[str] def _simple_spawn(server_id: str, which: Sequence[str], args: Sequence[str] = (), - install_pkg: Optional[str] = None, base_init: Optional[Dict[str, Any]] = None, - seed: bool = False, untrusted_init: Optional[Dict[str, Any]] = None) -> _SpawnFn: + install_pkg: Optional[str] = None, base_init: Optional[dict[str, Any]] = None, + seed: bool = False, untrusted_init: Optional[dict[str, Any]] = None) -> _SpawnFn: """Build a spawn function for the common single-binary server shape; ``untrusted_init`` replaces ``base_init`` in an untrusted workspace (the server's own switch for not loading project code).""" def build(root: str, ctx: ServerContext) -> Optional[SpawnSpec]: @@ -235,7 +235,7 @@ _VUE_TSDK_MSG = ( ) -def _node_modules_trees(bin_path: str, root: Optional[str]) -> List[str]: +def _node_modules_trees(bin_path: str, root: Optional[str]) -> list[str]: """``node_modules`` trees that may hold a server and its TypeScript SDK: the launcher's own tree (symlinks resolved), Hermes staging, then the project's (``root`` None: Hermes's trees only, for TypeScript's SDK pin in an untrusted workspace, whose own JavaScript must not load).""" @@ -296,7 +296,7 @@ def _spawn_typescript(root: str, ctx: ServerContext) -> Optional[SpawnSpec]: bin_path = _find_binary(ctx, "typescript", ("typescript-language-server",), "typescript-language-server") if bin_path is None: return None - base: Dict[str, Any] = {} + base: dict[str, Any] = {} if not ctx.trusted: sdk = _typescript_sdk_dir(_node_modules_trees(bin_path, None)) if sdk is None: @@ -396,12 +396,12 @@ _DENO_EXCLUDES = ["deno.json", "deno.jsonc"] _root_typescript = _markers_root(_JS_MARKERS, _DENO_EXCLUDES) -def _server(server_id: str, extensions: Tuple[str, ...], description: str, *, +def _server(server_id: str, extensions: tuple[str, ...], description: str, *, markers: Optional[Sequence[str]] = None, excludes: Sequence[str] = (), resolve_root: Optional[_RootFn] = None, build_spawn: Optional[_SpawnFn] = None, which: Sequence[str] = (), args: Sequence[str] = (), install_pkg: Optional[str] = None, - base_init: Optional[Dict[str, Any]] = None, seed: bool = False, - untrusted_init: Optional[Dict[str, Any]] = None, multi_root: bool = False) -> ServerDef: + base_init: Optional[dict[str, Any]] = None, seed: bool = False, + untrusted_init: Optional[dict[str, Any]] = None, multi_root: bool = False) -> ServerDef: """Registry entry factory: defaults to marker-based root + single-binary spawn.""" return ServerDef( server_id, extensions, @@ -411,7 +411,7 @@ def _server(server_id: str, extensions: Tuple[str, ...], description: str, *, ) -SERVERS: List[ServerDef] = [ +SERVERS: list[ServerDef] = [ _server("pyright", (".py", ".pyi"), "Python — Microsoft pyright", markers=["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"], build_spawn=_spawn_pyright, multi_root=True), @@ -498,14 +498,14 @@ def _custom_spawn(server_id: str, command: Sequence[str]) -> _SpawnFn: return build -def custom_servers(servers_cfg: Any) -> List[ServerDef]: +def custom_servers(servers_cfg: Any) -> list[ServerDef]: """``lsp.servers`` entries that declare ``extensions`` and name no built-in server are user-declared servers (issue #100257). They go AHEAD of the built-ins so a custom entry can claim an extension; malformed entries are logged and skipped so one typo never disables the rest of the subsystem.""" if not isinstance(servers_cfg, dict): return [] builtin = {s.server_id for s in SERVERS} - out: List[ServerDef] = [] + out: list[ServerDef] = [] for server_id, cfg in servers_cfg.items(): if server_id in builtin or not isinstance(cfg, dict) or "extensions" not in cfg: continue diff --git a/agent/lsp/workspace.py b/agent/lsp/workspace.py index fa5560bfe2..c18571c4fd 100644 --- a/agent/lsp/workspace.py +++ b/agent/lsp/workspace.py @@ -134,7 +134,7 @@ def nearest_root( return None -def resolve_workspace_for_file(file_path: str, *, cwd: Optional[str] = None) -> Tuple[Optional[str], bool]: +def resolve_workspace_for_file(file_path: str, *, cwd: Optional[str] = None) -> tuple[Optional[str], bool]: """Return ``(workspace_root, gated_in)`` for a file. The cwd's worktree wins when the file is inside it; otherwise the file's own worktree is the fallback anchor (monorepos / unrelated checkouts). ``(None, False)`` when neither is in a git worktree.""" @@ -154,7 +154,7 @@ def resolve_workspace_for_file(file_path: str, *, cwd: Optional[str] = None) -> return None, False -def operator_workspace_roots() -> Set[str]: +def operator_workspace_roots() -> set[str]: """Git worktrees the operator pointed Hermes at: the launch dir and the surface-set workspace (``resolve_agent_cwd``: the Desktop/TUI session cwd, ``hermes -w``'s worktree, a gateway's ``terminal.cwd``). The agent's ``cd`` moves neither (it only moves the terminal's cwd). A repo at @@ -172,7 +172,7 @@ def operator_workspace_roots() -> Set[str]: if not is_truthy_value(get_session_env("HERMES_CRON_SESSION", "")): anchors.append(resolve_agent_cwd) home = normalize_path("~") - roots: Set[str] = set() + roots: set[str] = set() for anchor in anchors: try: root = find_git_worktree(str(anchor())) diff --git a/agent/markdown_tables.py b/agent/markdown_tables.py index a3158f8cef..471f64d522 100644 --- a/agent/markdown_tables.py +++ b/agent/markdown_tables.py @@ -28,7 +28,7 @@ def _disp_width(s: str) -> int: return max(wcswidth(s), 0) -def split_table_row(row: str) -> List[str]: +def split_table_row(row: str) -> list[str]: """Split ``| a | b | c |`` into ``["a", "b", "c"]`` with trims.""" return [c.strip() for c in row.strip().removeprefix("|").removesuffix("|").split("|")] @@ -51,7 +51,7 @@ def looks_like_table_row(row: str) -> bool: return bool(stripped) and (stripped.startswith("|") or stripped.count("|") >= 2) -def _render_block(rows: List[List[str]], available_width: int | None = None) -> List[str]: +def _render_block(rows: list[list[str]], available_width: int | None = None) -> list[str]: """Render ``rows`` (header + body, divider implied) at uniform widths. When the horizontal table would exceed ``available_width`` fall back to a @@ -65,7 +65,7 @@ def _render_block(rows: List[List[str]], available_width: int | None = None) -> if available_width is not None and sum(widths) + 3 * ncols + 1 > max(available_width, 20): return _render_vertical(rows, ncols, available_width) - def _row(cells: List[str]) -> str: + def _row(cells: list[str]) -> str: return "| " + " | ".join(c + " " * max(0, widths[k] - _disp_width(c)) for k, c in enumerate(cells)) + " |" out = [_row(rows[0]), "|" + "|".join("-" * (w + 2) for w in widths) + "|"] @@ -73,9 +73,9 @@ def _render_block(rows: List[List[str]], available_width: int | None = None) -> return out -def _hard_break(word: str, w: int) -> List[str]: +def _hard_break(word: str, w: int) -> list[str]: """Split a single over-wide word into display-width-``w`` chunks.""" - out: List[str] = [] + out: list[str] = [] buf, bw = "", 0 for ch in word: cw = _disp_width(ch) or 1 @@ -87,7 +87,7 @@ def _hard_break(word: str, w: int) -> List[str]: return out + [buf] if buf else out -def _wrap_to_width(text: str, width: int) -> List[str]: +def _wrap_to_width(text: str, width: int) -> list[str]: """Soft-wrap ``text`` at word boundaries to ``width`` display cells. Words wider than ``width`` are hard-broken. Empty input yields a single @@ -98,7 +98,7 @@ def _wrap_to_width(text: str, width: int) -> List[str]: words = text.split() if not words: return [""] - lines: List[str] = [] + lines: list[str] = [] current, current_w = "", 0 def _start(word: str, ww: int) -> None: @@ -126,7 +126,7 @@ def _wrap_to_width(text: str, width: int) -> List[str]: return lines or [""] -def _render_vertical(rows: List[List[str]], ncols: int, available_width: int) -> List[str]: +def _render_vertical(rows: list[list[str]], ncols: int, available_width: int) -> list[str]: """Render a too-wide table as ``Header: value`` blocks (Claude Code's narrow fallback). Each body row becomes one block with continuation lines indented two spaces, @@ -137,7 +137,7 @@ def _render_vertical(rows: List[List[str]], ncols: int, available_width: int) -> labels = [h or f"Column {i + 1}" for i, h in enumerate(rows[0] + [""] * (ncols - len(rows[0])))] separator = "─" * (max(20, min(40, available_width - 2)) if available_width else 30) cont_budget = max(10, available_width - 2) # continuation lines are indented two spaces - out: List[str] = [] + out: list[str] = [] for ri, row in enumerate(rows[1:]): if ri > 0: out.append(separator) @@ -165,14 +165,14 @@ def realign_markdown_tables(text: str, available_width: int | None = None) -> st if "|" not in text: return text lines = text.split("\n") - out: List[str] = [] + out: list[str] = [] i, n = 0, len(lines) while i < n: line = lines[i] # A table starts with a header row whose next line is a divider. if "|" in line and i + 1 < n and is_table_divider(lines[i + 1]): header = split_table_row(line) - body: List[List[str]] = [] + body: list[list[str]] = [] j = i + 2 while j < n and "|" in lines[j] and lines[j].strip(): if not is_table_divider(lines[j]): diff --git a/agent/memory_manager.py b/agent/memory_manager.py index 5ccbca61cd..793ec2fa44 100644 --- a/agent/memory_manager.py +++ b/agent/memory_manager.py @@ -63,7 +63,7 @@ def _accepts_require_checkpoint(fn: Callable[..., Any]) -> bool: # -- Tool-schema plumbing ----------------------------------------------------- -def normalize_tool_schema(schema: Any) -> Optional[Dict[str, Any]]: +def normalize_tool_schema(schema: Any) -> Optional[dict[str, Any]]: """Return a bare function-tool dict with a resolvable top-level ``name``, else None. Providers should return ``{"name", "description", "parameters"}`` but some return the @@ -78,7 +78,7 @@ def normalize_tool_schema(schema: Any) -> Optional[Dict[str, Any]]: return schema if name and isinstance(name, str) else None -def memory_provider_tools_enabled(enabled_toolsets: Optional[List[str]], disabled_toolsets: Optional[List[str]] = None, +def memory_provider_tools_enabled(enabled_toolsets: Optional[list[str]], disabled_toolsets: Optional[list[str]] = None, *, memory_tool_present: bool = False) -> bool: """Return whether external memory-provider tools should be exposed.""" if disabled_toolsets and "memory" in disabled_toolsets: @@ -226,7 +226,7 @@ def _redact_for_provider(*values: Any) -> Any: return scrubbed[0] if len(scrubbed) == 1 else tuple(scrubbed) -def _redact_messages_for_egress(messages: Optional[List[Any]]) -> Optional[List[Any]]: +def _redact_messages_for_egress(messages: Optional[list[Any]]) -> Optional[list[Any]]: return None if messages is None else list(_redact_for_provider(messages)) @@ -396,16 +396,16 @@ class MemoryManager: """ def __init__(self, *, external_prefetch_timeout: Optional[float] = None) -> None: - self._providers: List[MemoryProvider] = [] - self._tool_to_provider: Dict[str, MemoryProvider] = {} - self._external_prefetch_spill_config: Optional[Dict[str, Any]] = None + self._providers: list[MemoryProvider] = [] + self._tool_to_provider: dict[str, MemoryProvider] = {} + self._external_prefetch_spill_config: Optional[dict[str, Any]] = None self._has_external: bool = False timeout = external_prefetch_timeout timeout = _EXTERNAL_PREFETCH_TIMEOUT_S if timeout is None else float(timeout) if timeout <= 0: raise ValueError("external_prefetch_timeout must be positive") self._external_prefetch_timeout = timeout - self._external_prefetch_threads: Dict[str, threading.Thread] = {} + self._external_prefetch_threads: dict[str, threading.Thread] = {} self._external_prefetch_lock = threading.Lock() # Single-worker background executor for end-of-turn sync/prefetch, created lazily so # the builtin-only path spawns no threads; one worker serializes a provider's writes. @@ -413,17 +413,17 @@ class MemoryManager: self._sync_executor_lock = threading.Lock() # Futures by durability class ("write" / "prefetch") so shutdown can drain FIFO # within a bound, then report exactly what it abandoned. - self._background_futures: Dict[Future, str] = {} + self._background_futures: dict[Future, str] = {} self._shutting_down = False - self._shutdown_drain_state: Dict[str, Any] = { + self._shutdown_drain_state: dict[str, Any] = { "status": "not_started", "abandoned_writes": 0, "abandoned_prefetches": 0, "active_tasks": 0, } def _each_provider(self, label: str, call: Callable[[MemoryProvider], Any], *, level: int = logging.DEBUG, - providers: Optional[List[MemoryProvider]] = None, exc_info: bool = False) -> List[Any]: + providers: Optional[list[MemoryProvider]] = None, exc_info: bool = False) -> list[Any]: """Call ``call(provider)`` per provider, logging+swallowing failures; returns successes in order. ``label`` completes the log line ``Memory provider ''