openhands-sdk 1.26.0__tar.gz → 1.27.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/PKG-INFO +1 -1
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/acp_agent.py +200 -182
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/utils.py +30 -5
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/impl/local_conversation.py +33 -5
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/impl/remote_conversation.py +2 -2
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/secret_registry.py +36 -1
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/exceptions/__init__.py +2 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/exceptions/classifier.py +24 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/llm.py +77 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/options/responses_options.py +10 -4
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/model_info.py +10 -1
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/verified_models.py +2 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/observability/laminar.py +0 -135
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/__init__.py +9 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/acp_providers.py +29 -3
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/api_models.py +15 -3
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/model.py +270 -43
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/PKG-INFO +1 -1
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/pyproject.toml +1 -1
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/acp_models.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/agent.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/critic_mixin.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/parallel_executor.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/response_dispatch.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/banner.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/agent_context.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/prompt.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/skills/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/view.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/cancellation.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/conversation.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/event_store.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/events_list_base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/exceptions.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/persistence_const.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/request.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/response_utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/serialization_diff.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/state.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/title_utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/types.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/client.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/result.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/acp_tool_call.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/condenser.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/conversation_error.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/conversation_state.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/hook_execution.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_completion_log.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/resume_transcript.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/streaming_delta.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/token.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/types.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/user_action.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/fetch.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/info.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/interface.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/manager.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/cached_repo.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/exceptions.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/git_changes.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/git_diff.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/models.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/config.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/executor.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/manager.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/types.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/cache.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/local.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/memory.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/auth/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/auth/credentials.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/auth/openai.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/exceptions/types.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/llm_profile_store.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/llm_registry.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/llm_response.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/message.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/options/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/options/chat_options.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/options/common.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/router/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/router/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/router/impl/random.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/streaming.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/metrics.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/model_features.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/logger/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/logger/logger.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/logger/rolling.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/marketplace/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/marketplace/types.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/client.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/definition.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/exceptions.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/tool.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/observability/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/observability/utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/fetch.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/installed.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/loader.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/plugin.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/source.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/types.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/py.typed +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/secret/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/secret/secrets.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/analyzer.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/confirmation_policy.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/ensemble.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/grayswan/analyzer.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/grayswan/utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/llm_analyzer.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/risk.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/shell_parser.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/metadata.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/exceptions.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/execute.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/fetch.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/installed.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/skill.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/trigger.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/types.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/subagent/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/subagent/load.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/subagent/registry.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/subagent/schema.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/testing/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/testing/test_llm.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/finish.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/think.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/registry.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/schema.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/spec.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/tool.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/async_executor.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/async_utils.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/cipher.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/command.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/datetime.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/deprecation.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/github.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/json.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/models.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/paging.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/path.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/pydantic_diff.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/redact.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/truncate.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/visualize.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/local.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/models.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/remote/base.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/repo.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/workspace.py +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/SOURCES.txt +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/requires.txt +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/top_level.txt +0 -0
- {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.27.0
|
|
4
4
|
Summary: OpenHands SDK - Core functionality for building AI agents
|
|
5
5
|
Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
|
|
6
6
|
Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
|
|
@@ -23,7 +23,7 @@ import os
|
|
|
23
23
|
import threading
|
|
24
24
|
import time
|
|
25
25
|
import uuid
|
|
26
|
-
from collections.abc import Callable, Generator
|
|
26
|
+
from collections.abc import Awaitable, Callable, Generator
|
|
27
27
|
from concurrent.futures import Future
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
from typing import TYPE_CHECKING, Any, Final, Literal, NamedTuple
|
|
@@ -73,7 +73,6 @@ from openhands.sdk.event.conversation_error import ConversationErrorEvent
|
|
|
73
73
|
from openhands.sdk.llm import LLM, ImageContent, Message, MessageToolCall, TextContent
|
|
74
74
|
from openhands.sdk.logger import get_logger
|
|
75
75
|
from openhands.sdk.observability.laminar import maybe_init_laminar, observe
|
|
76
|
-
from openhands.sdk.secret import SecretSource
|
|
77
76
|
from openhands.sdk.settings.acp_providers import (
|
|
78
77
|
ACPFileSecretSpec,
|
|
79
78
|
build_session_model_meta,
|
|
@@ -143,15 +142,21 @@ MAX_ACP_CONTENT_CHARS: int = 30_000
|
|
|
143
142
|
# Env vars that must be removed from the subprocess environment when a
|
|
144
143
|
# particular "dominant" env var is present.
|
|
145
144
|
#
|
|
146
|
-
# Rationale:
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
145
|
+
# Rationale: Claude Code's subscription auth uses CLAUDE_CODE_OAUTH_TOKEN, a
|
|
146
|
+
# bearer validated against api.anthropic.com. A co-present ANTHROPIC_API_KEY
|
|
147
|
+
# would take precedence over the token (silently bypassing the subscription),
|
|
148
|
+
# and an ANTHROPIC_BASE_URL would route the bearer to a proxy that rejects it —
|
|
149
|
+
# either silently breaks the intended OAuth auth. When the OAuth token is the
|
|
150
|
+
# active credential we strip both so the subprocess authenticates with the
|
|
151
|
+
# token against api.anthropic.com.
|
|
152
|
+
#
|
|
153
|
+
# Keyed on the credential itself (CLAUDE_CODE_OAUTH_TOKEN), NOT on
|
|
154
|
+
# CLAUDE_CONFIG_DIR: the config dir is a *location* lever (data-dir isolation,
|
|
155
|
+
# #1019) that is orthogonal to which credential is active. Keying the strip on
|
|
156
|
+
# it wrongly fired during API-key isolation and missed the conflict when the
|
|
157
|
+
# token arrived via env without isolation (#3588).
|
|
153
158
|
_ENV_CONFLICT_MAP: dict[str, frozenset[str]] = {
|
|
154
|
-
"
|
|
159
|
+
"CLAUDE_CODE_OAUTH_TOKEN": frozenset({"ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"}),
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
# Number of trailing characters of an ACP session id retained in log lines
|
|
@@ -807,6 +812,11 @@ class _OpenHandsACPBridge:
|
|
|
807
812
|
# event stream in cleartext. ``None`` ⇒ no-op (bridge used standalone).
|
|
808
813
|
self.mask: Callable[[str], str] | None = None
|
|
809
814
|
self._last_activity_signal: float = float("-inf")
|
|
815
|
+
# Monotonic timestamp of the most recent ``session_update``. Unlike the
|
|
816
|
+
# throttled ``_last_activity_signal``, updated on *every* update so the
|
|
817
|
+
# prompt idle-timeout watchdog sees real progress. Armed per turn via
|
|
818
|
+
# ``arm_activity_clock``.
|
|
819
|
+
self._last_activity_monotonic: float = float("-inf")
|
|
810
820
|
# Telemetry state from UsageUpdate (persists across turns)
|
|
811
821
|
self._last_cost: float = 0.0 # last cumulative cost seen
|
|
812
822
|
self._last_cost_by_session: dict[str, float] = {}
|
|
@@ -833,6 +843,26 @@ class _OpenHandsACPBridge:
|
|
|
833
843
|
# Note: telemetry state (_last_cost, _context_window, _last_activity_signal,
|
|
834
844
|
# etc.) is intentionally NOT cleared — it accumulates across turns.
|
|
835
845
|
|
|
846
|
+
def arm_activity_clock(self) -> None:
|
|
847
|
+
"""Mark "now" as the last activity for the idle-timeout watchdog.
|
|
848
|
+
|
|
849
|
+
Called at the start of each prompt (and each retry) so the idle
|
|
850
|
+
window is measured from the moment the prompt is sent rather than
|
|
851
|
+
from a stale value — a server that legitimately takes a while before
|
|
852
|
+
its first ``session_update`` must not be killed prematurely.
|
|
853
|
+
"""
|
|
854
|
+
self._last_activity_monotonic = time.monotonic()
|
|
855
|
+
|
|
856
|
+
def seconds_since_last_activity(self) -> float:
|
|
857
|
+
"""Seconds since the last ``session_update`` (or ``arm_activity_clock``).
|
|
858
|
+
|
|
859
|
+
Drives the prompt idle-timeout: any streamed token, thought, tool-call
|
|
860
|
+
start/progress, or usage update from the ACP server resets the clock,
|
|
861
|
+
so a steadily-progressing agent never trips the deadline while a
|
|
862
|
+
genuinely silent (hung) server still does.
|
|
863
|
+
"""
|
|
864
|
+
return time.monotonic() - self._last_activity_monotonic
|
|
865
|
+
|
|
836
866
|
def prepare_usage_sync(self, session_id: str) -> asyncio.Event:
|
|
837
867
|
"""Prepare per-turn UsageUpdate synchronization for a session."""
|
|
838
868
|
event = asyncio.Event()
|
|
@@ -890,6 +920,12 @@ class _OpenHandsACPBridge:
|
|
|
890
920
|
) -> None:
|
|
891
921
|
logger.debug("ACP session_update: type=%s", type(update).__name__)
|
|
892
922
|
|
|
923
|
+
# Any update — token, thought, tool-call start/progress, usage — is
|
|
924
|
+
# progress: reset the idle clock so the prompt's inactivity watchdog
|
|
925
|
+
# keeps a steadily-working agent alive (unthrottled, unlike the
|
|
926
|
+
# heartbeat in ``_maybe_signal_activity``).
|
|
927
|
+
self._last_activity_monotonic = time.monotonic()
|
|
928
|
+
|
|
893
929
|
# Route fork session updates to the fork accumulator. ask_agent() joins
|
|
894
930
|
# and returns this text to the caller (a UI/network sink), so mask it
|
|
895
931
|
# like the main-turn path — a secret echoed in a fork session must not
|
|
@@ -1212,8 +1248,13 @@ class ACPAgent(AgentBase):
|
|
|
1212
1248
|
acp_prompt_timeout: float = Field(
|
|
1213
1249
|
default=1800.0,
|
|
1214
1250
|
description=(
|
|
1215
|
-
"
|
|
1216
|
-
"
|
|
1251
|
+
"Inactivity timeout in seconds for a single ACP prompt() call. "
|
|
1252
|
+
"The deadline resets on every update from the ACP server (token, "
|
|
1253
|
+
"thought, tool-call progress, usage), so a steadily-progressing "
|
|
1254
|
+
"agent runs as long as it keeps making progress; the prompt is "
|
|
1255
|
+
"only aborted after this many seconds with no activity at all. "
|
|
1256
|
+
"Prevents indefinite hangs when the ACP server stops responding "
|
|
1257
|
+
"without killing legitimately long-running work."
|
|
1217
1258
|
),
|
|
1218
1259
|
)
|
|
1219
1260
|
acp_model: str | None = Field(
|
|
@@ -1767,25 +1808,18 @@ class ACPAgent(AgentBase):
|
|
|
1767
1808
|
advertisement: their values are written to disk, not injected as env
|
|
1768
1809
|
vars, so advertising them as available env vars would mislead the agent.
|
|
1769
1810
|
"""
|
|
1770
|
-
|
|
1771
|
-
agent_context
|
|
1811
|
+
# Advertise from state.secret_registry alone — it now holds
|
|
1812
|
+
# agent_context.secrets too (seeded at conversation init, with their
|
|
1813
|
+
# descriptions), so it is the single source for the <CUSTOM_SECRETS>
|
|
1814
|
+
# block. Reserved file-content secrets are written to disk, not injected
|
|
1815
|
+
# as env vars, so drop them from the advertisement.
|
|
1772
1816
|
file_secret_names = self._present_file_secret_names(state)
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
if agent_context is not None and agent_context.secrets:
|
|
1780
|
-
agent_context = agent_context.model_copy(
|
|
1781
|
-
update={
|
|
1782
|
-
"secrets": {
|
|
1783
|
-
name: secret
|
|
1784
|
-
for name, secret in agent_context.secrets.items()
|
|
1785
|
-
if name not in file_secret_names
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
)
|
|
1817
|
+
secret_infos = [
|
|
1818
|
+
info
|
|
1819
|
+
for info in state.secret_registry.get_secret_infos()
|
|
1820
|
+
if info.get("name") not in file_secret_names
|
|
1821
|
+
]
|
|
1822
|
+
agent_context = self.agent_context
|
|
1789
1823
|
if agent_context is None:
|
|
1790
1824
|
# No caller-supplied context. Only synthesize an empty one for the
|
|
1791
1825
|
# renderer if we actually have a registry-secret advertisement to
|
|
@@ -1795,53 +1829,29 @@ class ACPAgent(AgentBase):
|
|
|
1795
1829
|
# suppress.
|
|
1796
1830
|
if not secret_infos:
|
|
1797
1831
|
return None
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
)
|
|
1832
|
+
agent_context = AgentContext(current_datetime=None)
|
|
1833
|
+
elif agent_context.secrets:
|
|
1834
|
+
# The registry already carries these (and their descriptions), so
|
|
1835
|
+
# clear the agent_context copy to advertise from the registry alone
|
|
1836
|
+
# rather than re-merging a redundant second source.
|
|
1837
|
+
agent_context = agent_context.model_copy(update={"secrets": {}})
|
|
1801
1838
|
return agent_context.to_acp_prompt_context(additional_secret_infos=secret_infos)
|
|
1802
1839
|
|
|
1803
|
-
def _read_conversation_secret(
|
|
1804
|
-
self, state: ConversationState, name: str
|
|
1805
|
-
) -> str | None:
|
|
1806
|
-
"""Read a secret value from the canonical channel, then the drain.
|
|
1807
|
-
|
|
1808
|
-
Prefers ``state.secret_registry`` — the canonical channel, where
|
|
1809
|
-
``create_request`` lifts ``agent_context.secrets`` on the Python /
|
|
1810
|
-
OpenHands-cloud path and where ``StartConversationRequest.secrets``
|
|
1811
|
-
land — and falls back to ``agent_context.secrets`` for topologies that
|
|
1812
|
-
do not call ``create_request`` (notably canvas-local). See #1022.
|
|
1813
|
-
"""
|
|
1814
|
-
if name in state.secret_registry.secret_sources:
|
|
1815
|
-
value = state.secret_registry.get_secret_value(name)
|
|
1816
|
-
if value:
|
|
1817
|
-
return value
|
|
1818
|
-
if self.agent_context and self.agent_context.secrets:
|
|
1819
|
-
secret = self.agent_context.secrets.get(name)
|
|
1820
|
-
if secret is not None:
|
|
1821
|
-
return (
|
|
1822
|
-
secret.get_value()
|
|
1823
|
-
if isinstance(secret, SecretSource)
|
|
1824
|
-
else str(secret)
|
|
1825
|
-
)
|
|
1826
|
-
return None
|
|
1827
|
-
|
|
1828
1840
|
def _present_file_secret_names(self, state: ConversationState) -> set[str]:
|
|
1829
1841
|
"""Reserved file-content secret names supplied for this conversation.
|
|
1830
1842
|
|
|
1831
1843
|
A name counts as present if it is configured in
|
|
1832
|
-
:attr:`acp_file_secrets` *and*
|
|
1833
|
-
(
|
|
1834
|
-
are materialised to disk and therefore excluded from
|
|
1835
|
-
injection and the ``<CUSTOM_SECRETS>`` advertisement
|
|
1836
|
-
file blobs, not env vars the subprocess can reference
|
|
1844
|
+
:attr:`acp_file_secrets` *and* registered in ``state.secret_registry``
|
|
1845
|
+
(which holds ``agent_context.secrets`` too, seeded at conversation
|
|
1846
|
+
init). These names are materialised to disk and therefore excluded from
|
|
1847
|
+
the plain env-var injection and the ``<CUSTOM_SECRETS>`` advertisement
|
|
1848
|
+
(their values are file blobs, not env vars the subprocess can reference
|
|
1849
|
+
by name).
|
|
1837
1850
|
"""
|
|
1838
1851
|
configured = {spec.secret_name for spec in self.acp_file_secrets}
|
|
1839
1852
|
if not configured:
|
|
1840
1853
|
return set()
|
|
1841
|
-
|
|
1842
|
-
if self.agent_context and self.agent_context.secrets:
|
|
1843
|
-
present |= set(self.agent_context.secrets)
|
|
1844
|
-
return present & configured
|
|
1854
|
+
return set(state.secret_registry.secret_sources) & configured
|
|
1845
1855
|
|
|
1846
1856
|
def _acp_file_secret_dir(self, state: ConversationState, subdir: str) -> Path:
|
|
1847
1857
|
"""Durable per-conversation directory for a credential file.
|
|
@@ -1882,14 +1892,12 @@ class ACPAgent(AgentBase):
|
|
|
1882
1892
|
highest precedence and is honoured as the materialisation target too), so
|
|
1883
1893
|
leave it untouched.
|
|
1884
1894
|
|
|
1885
|
-
Claude
|
|
1886
|
-
|
|
1887
|
-
``
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
cwd-keyed, so the residual shared state is the mostly-inert global
|
|
1892
|
-
config.)
|
|
1895
|
+
Claude note: relocating ``CLAUDE_CONFIG_DIR`` is safe under either auth
|
|
1896
|
+
mode. :data:`_ENV_CONFLICT_MAP` is keyed on the OAuth token
|
|
1897
|
+
(``CLAUDE_CODE_OAUTH_TOKEN``), not on ``CLAUDE_CONFIG_DIR``, so setting
|
|
1898
|
+
the config dir for isolation no longer strips a working
|
|
1899
|
+
``ANTHROPIC_API_KEY`` — API-key Claude gets the same per-conversation
|
|
1900
|
+
isolation (and pause/resume continuity) as OAuth Claude (#3588).
|
|
1893
1901
|
|
|
1894
1902
|
``HOME`` (gemini-cli's only lever — it hard-codes ``~/.gemini`` and
|
|
1895
1903
|
ignores ``XDG``) has a wider blast radius than the surgical
|
|
@@ -1900,11 +1908,11 @@ class ACPAgent(AgentBase):
|
|
|
1900
1908
|
need a narrower scope can pin ``HOME`` via ``acp_env`` (honoured below)
|
|
1901
1909
|
or leave isolation off for Gemini.
|
|
1902
1910
|
|
|
1903
|
-
Ordering
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1911
|
+
Ordering: this runs *after* the ``secret_registry`` injection and the
|
|
1912
|
+
``acp_env`` update in :meth:`_start_acp_server` so an ``acp_env`` pin of
|
|
1913
|
+
the data-dir var is visible and wins. Relocation is now credential-blind
|
|
1914
|
+
(the auth-conflict strip is keyed on ``CLAUDE_CODE_OAUTH_TOKEN``, not on
|
|
1915
|
+
the config dir), so the data-dir var it sets never affects auth.
|
|
1908
1916
|
"""
|
|
1909
1917
|
provider = detect_acp_provider_by_command(self.acp_command)
|
|
1910
1918
|
if provider is None or provider.data_dir_env_var is None:
|
|
@@ -1912,14 +1920,6 @@ class ACPAgent(AgentBase):
|
|
|
1912
1920
|
env_var = provider.data_dir_env_var
|
|
1913
1921
|
if env_var in self.acp_env:
|
|
1914
1922
|
return
|
|
1915
|
-
# Relies on the ordering contract above: ANTHROPIC_API_KEY /
|
|
1916
|
-
# CLAUDE_CODE_OAUTH_TOKEN must already be hydrated into env.
|
|
1917
|
-
if (
|
|
1918
|
-
env_var == "CLAUDE_CONFIG_DIR"
|
|
1919
|
-
and env.get("ANTHROPIC_API_KEY")
|
|
1920
|
-
and "CLAUDE_CODE_OAUTH_TOKEN" not in env
|
|
1921
|
-
):
|
|
1922
|
-
return
|
|
1923
1923
|
data_dir = self._acp_file_secret_dir(state, provider.key)
|
|
1924
1924
|
data_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
1925
1925
|
env[env_var] = str(data_dir)
|
|
@@ -1929,12 +1929,11 @@ class ACPAgent(AgentBase):
|
|
|
1929
1929
|
) -> None:
|
|
1930
1930
|
"""Seed reserved file-content credentials onto disk and point the CLI at them.
|
|
1931
1931
|
|
|
1932
|
-
For each spec in :attr:`acp_file_secrets` whose secret is
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
pinned it via ``acp_env``.
|
|
1932
|
+
For each spec in :attr:`acp_file_secrets` whose secret is registered in
|
|
1933
|
+
``state.secret_registry``, write its value to the spec's durable
|
|
1934
|
+
per-conversation directory (:meth:`_acp_file_secret_dir`) and set the
|
|
1935
|
+
controlling env var (``CODEX_HOME`` / ``GOOGLE_APPLICATION_CREDENTIALS``)
|
|
1936
|
+
unless the caller pinned it via ``acp_env``.
|
|
1938
1937
|
|
|
1939
1938
|
Seed-if-absent: a non-empty existing file is preserved, never clobbered
|
|
1940
1939
|
— so a token the CLI rewrites on refresh (Codex) survives a recycle, and
|
|
@@ -1950,7 +1949,7 @@ class ACPAgent(AgentBase):
|
|
|
1950
1949
|
"""
|
|
1951
1950
|
for spec in self.acp_file_secrets:
|
|
1952
1951
|
name = spec.secret_name
|
|
1953
|
-
value =
|
|
1952
|
+
value = state.secret_registry.get_secret_value(name)
|
|
1954
1953
|
if not value:
|
|
1955
1954
|
continue
|
|
1956
1955
|
# Seed where the data-dir env var will actually point: an explicit
|
|
@@ -2036,28 +2035,17 @@ class ACPAgent(AgentBase):
|
|
|
2036
2035
|
client.mask = state.secret_registry.mask_secrets_in_output
|
|
2037
2036
|
|
|
2038
2037
|
# Build the subprocess environment. Precedence, highest first:
|
|
2039
|
-
# acp_env > state.secret_registry >
|
|
2040
|
-
# > os.environ > default_environment
|
|
2038
|
+
# acp_env > state.secret_registry > os.environ > default_environment
|
|
2041
2039
|
#
|
|
2042
|
-
# Conversation credentials
|
|
2043
|
-
#
|
|
2044
|
-
#
|
|
2045
|
-
#
|
|
2046
|
-
# remote server). acp_env (deprecated) stays highest.
|
|
2040
|
+
# Conversation credentials intentionally OVERRIDE ambient os.environ: an
|
|
2041
|
+
# explicit per-conversation / provider secret must win over a same-named
|
|
2042
|
+
# variable in the agent-server's own environment. acp_env (deprecated)
|
|
2043
|
+
# stays highest.
|
|
2047
2044
|
#
|
|
2048
|
-
#
|
|
2049
|
-
#
|
|
2050
|
-
#
|
|
2051
|
-
#
|
|
2052
|
-
# (StartConversationRequest.secrets; also where create_request lifts
|
|
2053
|
-
# agent_context.secrets on the Python-caller path / OpenHands cloud).
|
|
2054
|
-
# - agent_context.secrets drain: the ONLY channel that delivers
|
|
2055
|
-
# agent_context.secrets on paths that do NOT call create_request —
|
|
2056
|
-
# notably canvas-local, which builds the request in TypeScript and
|
|
2057
|
-
# relies on the server's create_agent() to fold llm.api_key into
|
|
2058
|
-
# agent_context.secrets. There is no server-side agent_context.secrets
|
|
2059
|
-
# → registry lift, so keep this drain until one exists.
|
|
2060
|
-
# On a key collision the registry wins over the drain.
|
|
2045
|
+
# agent_context.secrets are seeded into secret_registry at
|
|
2046
|
+
# LocalConversation.__init__ (lower priority than request.secrets), so
|
|
2047
|
+
# the registry is now the single channel for all secrets including
|
|
2048
|
+
# provider credentials folded in by ACPAgentSettings.create_agent().
|
|
2061
2049
|
env = default_environment()
|
|
2062
2050
|
env.update(os.environ)
|
|
2063
2051
|
if self.acp_env:
|
|
@@ -2076,34 +2064,17 @@ class ACPAgent(AgentBase):
|
|
|
2076
2064
|
# injected as env vars, so exclude their (large blob) names from the
|
|
2077
2065
|
# plain env-injection below; materialisation sets only the path env var.
|
|
2078
2066
|
file_secret_names = self._present_file_secret_names(state)
|
|
2079
|
-
#
|
|
2080
|
-
#
|
|
2081
|
-
#
|
|
2082
|
-
#
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
):
|
|
2091
|
-
continue
|
|
2092
|
-
value = (
|
|
2093
|
-
secret.get_value()
|
|
2094
|
-
if isinstance(secret, SecretSource)
|
|
2095
|
-
else str(secret)
|
|
2096
|
-
)
|
|
2097
|
-
if value:
|
|
2098
|
-
env[name] = value
|
|
2099
|
-
# state.secret_registry overrides the drain and ambient os.environ. Skip
|
|
2100
|
-
# keys acp_env will set (avoids a redundant LookupSecret.get_value()).
|
|
2101
|
-
for name in state.secret_registry.secret_sources:
|
|
2102
|
-
if name in self.acp_env or name in file_secret_names:
|
|
2103
|
-
continue
|
|
2104
|
-
value = state.secret_registry.get_secret_value(name)
|
|
2105
|
-
if value:
|
|
2106
|
-
env[name] = value
|
|
2067
|
+
# Inject the whole registry: an ACP CLI is a black box we can't
|
|
2068
|
+
# name-scan per command (unlike the regular agent's bash tool), so
|
|
2069
|
+
# credentials must be delivered upfront. Registry values override
|
|
2070
|
+
# ambient os.environ. Skip keys acp_env will set last (avoids a
|
|
2071
|
+
# redundant LookupSecret.get_value()) and file secrets (materialised to
|
|
2072
|
+
# disk below).
|
|
2073
|
+
env.update(
|
|
2074
|
+
state.secret_registry.get_all_secrets_as_env_vars(
|
|
2075
|
+
exclude=set(self.acp_env) | file_secret_names
|
|
2076
|
+
)
|
|
2077
|
+
)
|
|
2107
2078
|
# Materialise reserved file-content secrets to disk and point their
|
|
2108
2079
|
# data-dir env vars (CODEX_HOME / GOOGLE_APPLICATION_CREDENTIALS) at the
|
|
2109
2080
|
# written files. Done before acp_env so an explicit acp_env override of
|
|
@@ -2116,18 +2087,16 @@ class ACPAgent(AgentBase):
|
|
|
2116
2087
|
|
|
2117
2088
|
# Relocate the CLI's data/config root to a per-conversation directory so
|
|
2118
2089
|
# sandbox-sharing conversations don't race on a shared HOME (#1019).
|
|
2119
|
-
#
|
|
2120
|
-
#
|
|
2121
|
-
#
|
|
2122
|
-
#
|
|
2123
|
-
# and BEFORE the conflict-strip below (so a CLAUDE_CONFIG_DIR it sets is
|
|
2124
|
-
# still subject to the strip).
|
|
2090
|
+
# Runs after the registry injection and the acp_env update above so an
|
|
2091
|
+
# acp_env pin of the data-dir var wins. Independent of the strip below
|
|
2092
|
+
# (keyed on the OAuth token, not the data-dir var), so ordering relative
|
|
2093
|
+
# to it no longer matters for correctness.
|
|
2125
2094
|
if self.acp_isolate_data_dir:
|
|
2126
2095
|
self._isolate_acp_data_dir(state, env)
|
|
2127
2096
|
|
|
2128
|
-
# Strip env vars that conflict with an active auth mechanism
|
|
2129
|
-
#
|
|
2130
|
-
#
|
|
2097
|
+
# Strip env vars that conflict with an active auth mechanism: an active
|
|
2098
|
+
# CLAUDE_CODE_OAUTH_TOKEN must not coexist with ANTHROPIC_API_KEY (which
|
|
2099
|
+
# takes precedence) or ANTHROPIC_BASE_URL (proxies the bearer). See #3588.
|
|
2131
2100
|
for dominant, conflicts in _ENV_CONFLICT_MAP.items():
|
|
2132
2101
|
if dominant in env:
|
|
2133
2102
|
for conflict in conflicts:
|
|
@@ -2438,6 +2407,9 @@ class ACPAgent(AgentBase):
|
|
|
2438
2407
|
self._client.on_token = on_token
|
|
2439
2408
|
self._client.on_event = on_event
|
|
2440
2409
|
self._client.on_activity = self._on_activity
|
|
2410
|
+
# Start the idle-timeout clock fresh for this attempt so the deadline
|
|
2411
|
+
# is measured from the send (or retry), not from a stale value.
|
|
2412
|
+
self._client.arm_activity_clock()
|
|
2441
2413
|
|
|
2442
2414
|
def _cancel_inflight_tool_calls(self) -> None:
|
|
2443
2415
|
"""Emit a terminal ``failed`` ACPToolCallEvent for every tool call
|
|
@@ -2695,27 +2667,71 @@ class ACPAgent(AgentBase):
|
|
|
2695
2667
|
)
|
|
2696
2668
|
return response
|
|
2697
2669
|
|
|
2670
|
+
def _idle_timeout_message(self) -> str:
|
|
2671
|
+
return (
|
|
2672
|
+
f"ACP prompt timed out after {self.acp_prompt_timeout:.0f}s "
|
|
2673
|
+
"with no activity from the ACP server"
|
|
2674
|
+
)
|
|
2675
|
+
|
|
2676
|
+
async def _await_with_idle_deadline(
|
|
2677
|
+
self,
|
|
2678
|
+
awaitable: Awaitable[PromptResponse | None],
|
|
2679
|
+
*,
|
|
2680
|
+
cancel_on_exit: bool,
|
|
2681
|
+
) -> PromptResponse | None:
|
|
2682
|
+
"""Await *awaitable*, aborting only after a stretch of inactivity.
|
|
2683
|
+
|
|
2684
|
+
The deadline is an *idle* timeout, not a hard turn deadline: any
|
|
2685
|
+
``session_update`` from the ACP server (token, thought, tool-call
|
|
2686
|
+
start/progress, usage) resets ``acp_prompt_timeout``, so a steadily-
|
|
2687
|
+
progressing agent runs as long as it keeps making progress while a
|
|
2688
|
+
genuinely silent (hung) server is still cut off after the idle window.
|
|
2689
|
+
This is what keeps long-running ACP commands alive (issue
|
|
2690
|
+
agent-canvas#1245).
|
|
2691
|
+
|
|
2692
|
+
``asyncio.wait`` (not ``wait_for``) drives the polling so an idle-check
|
|
2693
|
+
slice elapsing never cancels the underlying prompt — only a true idle
|
|
2694
|
+
period raises ``TimeoutError``. ``cancel_on_exit`` controls cleanup:
|
|
2695
|
+
the sync path passes ``True`` to cancel the prompt coroutine it owns;
|
|
2696
|
+
the async path passes ``False`` because the portal task must survive
|
|
2697
|
+
for ``astep``'s ``session/cancel`` + drain handler.
|
|
2698
|
+
"""
|
|
2699
|
+
idle_limit = self.acp_prompt_timeout
|
|
2700
|
+
fut = asyncio.ensure_future(awaitable)
|
|
2701
|
+
try:
|
|
2702
|
+
while True:
|
|
2703
|
+
remaining = idle_limit - self._client.seconds_since_last_activity()
|
|
2704
|
+
if remaining <= 0:
|
|
2705
|
+
raise TimeoutError(self._idle_timeout_message())
|
|
2706
|
+
# wait() returns when the prompt finishes or the slice elapses,
|
|
2707
|
+
# leaving fut untouched either way.
|
|
2708
|
+
await asyncio.wait({fut}, timeout=remaining)
|
|
2709
|
+
if fut.done():
|
|
2710
|
+
return fut.result()
|
|
2711
|
+
# Slice elapsed: only give up if the server produced nothing in
|
|
2712
|
+
# the meantime, otherwise the loop re-arms with a fresh window.
|
|
2713
|
+
if self._client.seconds_since_last_activity() >= idle_limit:
|
|
2714
|
+
raise TimeoutError(self._idle_timeout_message())
|
|
2715
|
+
finally:
|
|
2716
|
+
if cancel_on_exit and not fut.done():
|
|
2717
|
+
fut.cancel()
|
|
2718
|
+
|
|
2698
2719
|
async def _await_prompt_response_with_timeout(
|
|
2699
2720
|
self,
|
|
2700
2721
|
prompt_future: Future[PromptResponse | None],
|
|
2701
2722
|
) -> PromptResponse | None:
|
|
2702
|
-
"""Await an ACP prompt with
|
|
2723
|
+
"""Await an ACP prompt with an idle (inactivity) turn deadline.
|
|
2703
2724
|
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
and closes any in-flight tool cards.
|
|
2725
|
+
Wraps the portal-side prompt future in :meth:`_await_with_idle_deadline`
|
|
2726
|
+
so the prompt is only abandoned after ``acp_prompt_timeout`` seconds
|
|
2727
|
+
with no ACP activity. The timeout handler in ``astep`` sends
|
|
2728
|
+
``session/cancel`` and closes any in-flight tool cards.
|
|
2709
2729
|
"""
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
except TimeoutError as exc:
|
|
2716
|
-
raise TimeoutError(
|
|
2717
|
-
f"ACP prompt timed out after {self.acp_prompt_timeout:.0f}s"
|
|
2718
|
-
) from exc
|
|
2730
|
+
# cancel_on_exit=False: the portal task behind ``prompt_future`` must
|
|
2731
|
+
# outlive an idle timeout / cancellation so astep's drain can observe it.
|
|
2732
|
+
return await self._await_with_idle_deadline(
|
|
2733
|
+
asyncio.wrap_future(prompt_future), cancel_on_exit=False
|
|
2734
|
+
)
|
|
2719
2735
|
|
|
2720
2736
|
@staticmethod
|
|
2721
2737
|
def _prompt_response_was_cancelled(response: PromptResponse | None) -> bool:
|
|
@@ -2797,12 +2813,11 @@ class ACPAgent(AgentBase):
|
|
|
2797
2813
|
state: ConversationState,
|
|
2798
2814
|
on_event: ConversationCallbackType,
|
|
2799
2815
|
) -> None:
|
|
2800
|
-
"""Error path when ``conn.prompt``
|
|
2816
|
+
"""Error path when ``conn.prompt`` went idle past ``acp_prompt_timeout``."""
|
|
2801
2817
|
logger.error(
|
|
2802
|
-
"ACP prompt timed out after %.1fs
|
|
2803
|
-
"The ACP server may have
|
|
2804
|
-
"
|
|
2805
|
-
"%d tool calls.",
|
|
2818
|
+
"ACP prompt timed out after %.1fs with no activity for the last "
|
|
2819
|
+
"%.0fs. The ACP server may have stalled or failed to send the "
|
|
2820
|
+
"JSON-RPC response. Accumulated %d text chunks, %d tool calls.",
|
|
2806
2821
|
elapsed,
|
|
2807
2822
|
self.acp_prompt_timeout,
|
|
2808
2823
|
len(self._client.accumulated_text),
|
|
@@ -2813,9 +2828,10 @@ class ACPAgent(AgentBase):
|
|
|
2813
2828
|
content=[
|
|
2814
2829
|
TextContent(
|
|
2815
2830
|
text=(
|
|
2816
|
-
|
|
2817
|
-
"
|
|
2818
|
-
"the
|
|
2831
|
+
"ACP prompt timed out after "
|
|
2832
|
+
f"{self.acp_prompt_timeout:.0f}s with no activity from "
|
|
2833
|
+
"the agent. The agent may have stalled, or it may have "
|
|
2834
|
+
"completed its work but the response was not received."
|
|
2819
2835
|
)
|
|
2820
2836
|
)
|
|
2821
2837
|
],
|
|
@@ -2940,7 +2956,7 @@ class ACPAgent(AgentBase):
|
|
|
2940
2956
|
t0 = time.monotonic()
|
|
2941
2957
|
try:
|
|
2942
2958
|
logger.info(
|
|
2943
|
-
"Sending ACP prompt (
|
|
2959
|
+
"Sending ACP prompt (idle_timeout=%.0fs, blocks=%d)",
|
|
2944
2960
|
self.acp_prompt_timeout,
|
|
2945
2961
|
len(prompt_blocks),
|
|
2946
2962
|
)
|
|
@@ -2949,14 +2965,16 @@ class ACPAgent(AgentBase):
|
|
|
2949
2965
|
|
|
2950
2966
|
async def _prompt() -> PromptResponse | None:
|
|
2951
2967
|
# Thin closure so existing mocks of ``_executor.run_async``
|
|
2952
|
-
# that take a single positional callable keep working.
|
|
2953
|
-
|
|
2968
|
+
# that take a single positional callable keep working. The idle
|
|
2969
|
+
# deadline is enforced inside (cancel_on_exit=True: this path
|
|
2970
|
+
# owns the coroutine) rather than as a hard run_async timeout.
|
|
2971
|
+
return await self._await_with_idle_deadline(
|
|
2972
|
+
self._do_acp_prompt(prompt_blocks), cancel_on_exit=True
|
|
2973
|
+
)
|
|
2954
2974
|
|
|
2955
2975
|
for attempt in range(max_retries + 1):
|
|
2956
2976
|
try:
|
|
2957
|
-
response = self._executor.run_async(
|
|
2958
|
-
_prompt, timeout=self.acp_prompt_timeout
|
|
2959
|
-
)
|
|
2977
|
+
response = self._executor.run_async(_prompt)
|
|
2960
2978
|
break
|
|
2961
2979
|
except TimeoutError:
|
|
2962
2980
|
raise
|
|
@@ -3081,7 +3099,7 @@ class ACPAgent(AgentBase):
|
|
|
3081
3099
|
prompt_future: Future[PromptResponse | None] | None = None
|
|
3082
3100
|
try:
|
|
3083
3101
|
logger.info(
|
|
3084
|
-
"Sending ACP prompt (
|
|
3102
|
+
"Sending ACP prompt (idle_timeout=%.0fs, blocks=%d, async)",
|
|
3085
3103
|
self.acp_prompt_timeout,
|
|
3086
3104
|
len(prompt_blocks),
|
|
3087
3105
|
)
|
|
@@ -31,6 +31,7 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
|
|
31
31
|
|
|
32
32
|
<CODE_QUALITY>
|
|
33
33
|
* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself.
|
|
34
|
+
* Only add a comment when the code expresses something genuinely unintuitive (a non-obvious invariant, a workaround, a subtle ordering/locking requirement, or a deliberate trade-off). Do NOT restate the code, narrate the diff/change history, or describe non-local behavior — that context belongs in the PR description or commit message, not in the source.
|
|
34
35
|
* When implementing solutions, focus on making the minimal changes needed to solve the problem.
|
|
35
36
|
* Before implementing any changes, first thoroughly understand the codebase through exploration.
|
|
36
37
|
* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate.
|