openhands-sdk 1.5.2__tar.gz → 1.7.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.5.2 → openhands_sdk-1.7.0}/PKG-INFO +3 -3
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/__init__.py +9 -1
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/agent.py +4 -11
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/base.py +24 -0
- openhands_sdk-1.7.0/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +3 -0
- openhands_sdk-1.7.0/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +1 -0
- openhands_sdk-1.7.0/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +3 -0
- openhands_sdk-1.7.0/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +3 -0
- openhands_sdk-1.7.0/openhands/sdk/agent/prompts/self_documentation.j2 +15 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt.j2 +29 -1
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/utils.py +9 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/__init__.py +2 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/agent_context.py +16 -8
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/prompt.py +40 -2
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +3 -3
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/__init__.py +2 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/skill.py +61 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/view.py +85 -22
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/conversation.py +13 -0
- openhands_sdk-1.7.0/openhands/sdk/conversation/exceptions.py +50 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/local_conversation.py +27 -5
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/remote_conversation.py +101 -3
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/state.py +1 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/stuck_detector.py +81 -45
- openhands_sdk-1.7.0/openhands/sdk/conversation/types.py +45 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/system.py +16 -20
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/llm.py +3 -2
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/message.py +4 -3
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/model_features.py +64 -24
- openhands_sdk-1.7.0/openhands/sdk/llm/utils/model_prompt_spec.py +98 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/verified_models.py +4 -4
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/logger/logger.py +1 -1
- openhands_sdk-1.7.0/openhands/sdk/utils/async_executor.py +115 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/models.py +1 -1
- openhands_sdk-1.7.0/openhands/sdk/utils/paging.py +63 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/PKG-INFO +3 -3
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/SOURCES.txt +14 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/requires.txt +2 -2
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/pyproject.toml +3 -3
- openhands_sdk-1.5.2/openhands/sdk/conversation/exceptions.py +0 -25
- openhands_sdk-1.5.2/openhands/sdk/conversation/types.py +0 -15
- openhands_sdk-1.5.2/openhands/sdk/utils/async_executor.py +0 -106
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/exceptions.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/trigger.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/types.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/event_store.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/events_list_base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/persistence_const.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/response_utils.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/secret_registry.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/serialization_diff.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/title_utils.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/critic/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/critic/base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/condenser.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/conversation_error.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/conversation_state.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_completion_log.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/token.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/types.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/event/user_action.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/git/exceptions.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/git/git_changes.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/git/git_diff.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/git/models.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/git/utils.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/io/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/io/base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/io/local.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/io/memory.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/types.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/llm_registry.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/llm_response.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/chat_options.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/common.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/responses_options.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/impl/random.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/streaming.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/metrics.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/model_info.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/logger/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/logger/rolling.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/mcp/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/mcp/client.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/mcp/definition.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/mcp/exceptions.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/mcp/tool.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/mcp/utils.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/observability/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/observability/laminar.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/observability/utils.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/py.typed +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/secret/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/secret/secrets.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/security/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/security/analyzer.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/security/confirmation_policy.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/security/llm_analyzer.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/security/risk.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/tool/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/tool/builtins/finish.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/tool/builtins/think.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/tool/registry.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/tool/schema.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/tool/spec.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/tool/tool.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/async_utils.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/cipher.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/command.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/deprecation.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/github.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/json.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/pydantic_diff.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/truncate.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/utils/visualize.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/workspace/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/workspace/base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/workspace/local.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/workspace/models.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/base.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands/sdk/workspace/workspace.py +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/top_level.txt +0 -0
- {openhands_sdk-1.5.2 → openhands_sdk-1.7.0}/setup.cfg +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: OpenHands SDK - Core functionality for building AI agents
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: deprecation>=2.1.0
|
|
7
7
|
Requires-Dist: fastmcp>=2.11.3
|
|
8
8
|
Requires-Dist: httpx>=0.27.0
|
|
9
|
-
Requires-Dist: litellm>=1.80.
|
|
9
|
+
Requires-Dist: litellm>=1.80.10
|
|
10
10
|
Requires-Dist: pydantic>=2.11.7
|
|
11
11
|
Requires-Dist: python-frontmatter>=1.1.0
|
|
12
12
|
Requires-Dist: python-json-logger>=3.3.0
|
|
13
13
|
Requires-Dist: tenacity>=9.1.2
|
|
14
14
|
Requires-Dist: websockets>=12
|
|
15
|
-
Requires-Dist: lmnr>=0.7.
|
|
15
|
+
Requires-Dist: lmnr>=0.7.24
|
|
16
16
|
Provides-Extra: boto3
|
|
17
17
|
Requires-Dist: boto3>=1.35.0; extra == "boto3"
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
from importlib.metadata import PackageNotFoundError, version
|
|
2
2
|
|
|
3
3
|
from openhands.sdk.agent import Agent, AgentBase
|
|
4
|
-
from openhands.sdk.context import
|
|
4
|
+
from openhands.sdk.context import (
|
|
5
|
+
AgentContext,
|
|
6
|
+
load_project_skills,
|
|
7
|
+
load_skills_from_dir,
|
|
8
|
+
load_user_skills,
|
|
9
|
+
)
|
|
5
10
|
from openhands.sdk.context.condenser import (
|
|
6
11
|
LLMSummarizingCondenser,
|
|
7
12
|
)
|
|
@@ -99,5 +104,8 @@ __all__ = [
|
|
|
99
104
|
"Workspace",
|
|
100
105
|
"LocalWorkspace",
|
|
101
106
|
"RemoteWorkspace",
|
|
107
|
+
"load_project_skills",
|
|
108
|
+
"load_skills_from_dir",
|
|
109
|
+
"load_user_skills",
|
|
102
110
|
"__version__",
|
|
103
111
|
]
|
|
@@ -109,17 +109,10 @@ class Agent(AgentBase):
|
|
|
109
109
|
event = SystemPromptEvent(
|
|
110
110
|
source="agent",
|
|
111
111
|
system_prompt=TextContent(text=self.system_message),
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
# configured. This allows weaker models to omit risk field
|
|
117
|
-
# and bypass validation requirements when analyzer is disabled.
|
|
118
|
-
# For detailed logic, see `_extract_security_risk` method.
|
|
119
|
-
tools=[
|
|
120
|
-
t.to_openai_tool(add_security_risk_prediction=True)
|
|
121
|
-
for t in self.tools_map.values()
|
|
122
|
-
],
|
|
112
|
+
# Tools are stored as ToolDefinition objects and converted to
|
|
113
|
+
# OpenAI format with security_risk parameter during LLM completion.
|
|
114
|
+
# See make_llm_completion() in agent/utils.py for details.
|
|
115
|
+
tools=list(self.tools_map.values()),
|
|
123
116
|
)
|
|
124
117
|
on_event(event)
|
|
125
118
|
|
|
@@ -12,6 +12,7 @@ from openhands.sdk.context.agent_context import AgentContext
|
|
|
12
12
|
from openhands.sdk.context.condenser import CondenserBase, LLMSummarizingCondenser
|
|
13
13
|
from openhands.sdk.context.prompts.prompt import render_template
|
|
14
14
|
from openhands.sdk.llm import LLM
|
|
15
|
+
from openhands.sdk.llm.utils.model_prompt_spec import get_model_prompt_spec
|
|
15
16
|
from openhands.sdk.logger import get_logger
|
|
16
17
|
from openhands.sdk.mcp import create_mcp_tools
|
|
17
18
|
from openhands.sdk.tool import BUILT_IN_TOOLS, Tool, ToolDefinition, resolve_tool
|
|
@@ -120,6 +121,15 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
120
121
|
"- An absolute path (e.g., '/path/to/custom_prompt.j2')"
|
|
121
122
|
),
|
|
122
123
|
)
|
|
124
|
+
security_policy_filename: str = Field(
|
|
125
|
+
default="security_policy.j2",
|
|
126
|
+
description=(
|
|
127
|
+
"Security policy template filename. Can be either:\n"
|
|
128
|
+
"- A relative filename (e.g., 'security_policy.j2') loaded from the "
|
|
129
|
+
"agent's prompts directory\n"
|
|
130
|
+
"- An absolute path (e.g., '/path/to/custom_security_policy.j2')"
|
|
131
|
+
),
|
|
132
|
+
)
|
|
123
133
|
system_prompt_kwargs: dict[str, object] = Field(
|
|
124
134
|
default_factory=dict,
|
|
125
135
|
description="Optional kwargs to pass to the system prompt Jinja2 template.",
|
|
@@ -164,6 +174,20 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
164
174
|
def system_message(self) -> str:
|
|
165
175
|
"""Compute system message on-demand to maintain statelessness."""
|
|
166
176
|
template_kwargs = dict(self.system_prompt_kwargs)
|
|
177
|
+
# Add security_policy_filename to template kwargs
|
|
178
|
+
template_kwargs["security_policy_filename"] = self.security_policy_filename
|
|
179
|
+
template_kwargs.setdefault("model_name", self.llm.model)
|
|
180
|
+
if (
|
|
181
|
+
"model_family" not in template_kwargs
|
|
182
|
+
or "model_variant" not in template_kwargs
|
|
183
|
+
):
|
|
184
|
+
spec = get_model_prompt_spec(
|
|
185
|
+
self.llm.model, getattr(self.llm, "model_canonical_name", None)
|
|
186
|
+
)
|
|
187
|
+
if "model_family" not in template_kwargs and spec.family:
|
|
188
|
+
template_kwargs["model_family"] = spec.family
|
|
189
|
+
if "model_variant" not in template_kwargs and spec.variant:
|
|
190
|
+
template_kwargs["model_variant"] = spec.variant
|
|
167
191
|
system_message = render_template(
|
|
168
192
|
prompt_dir=self.prompt_dir,
|
|
169
193
|
template_name=self.system_prompt_filename,
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
* Try to follow the instructions exactly as given - don't make extra or fewer actions if not asked.
|
|
2
|
+
* Avoid unnecessary defensive programming; do not add redundant fallbacks or default values — fail fast instead of masking misconfigurations.
|
|
3
|
+
* When backward compatibility expectations are unclear, confirm with the user before making changes that could break existing behavior.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* Avoid being too proactive. Fulfill the user's request thoroughly: if they ask questions/investigations, answer them; if they ask for implementations, provide them. But do not take extra steps beyond what is requested.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
* Stream your thinking and responses while staying concise; surface key assumptions and environment prerequisites explicitly.
|
|
2
|
+
* ALWAYS send a brief preamble to the user explaining what you're about to do before each tool call, using 8 - 12 words, with a friendly and curious tone.
|
|
3
|
+
* You have access to external resources and should actively use available tools to try accessing them first, rather than claiming you can’t access something without making an attempt.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
* Stream your thinking and responses while staying concise; surface key assumptions and environment prerequisites explicitly.
|
|
2
|
+
* ALWAYS send a brief preamble to the user explaining what you're about to do before each tool call, using 8 - 12 words, with a friendly and curious tone.
|
|
3
|
+
* You have access to external resources and should actively use available tools to try accessing them first, rather than claiming you can’t access something without making an attempt.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
When the user directly asks about any of the following:
|
|
2
|
+
- OpenHands capabilities (e.g., "can OpenHands do...", "does OpenHands have...")
|
|
3
|
+
- what you're able to do in second person (e.g., "are you able...", "can you...")
|
|
4
|
+
- how to use a specific OpenHands feature or product
|
|
5
|
+
- how to use the OpenHands SDK, CLI, GUI, or other OpenHands products
|
|
6
|
+
|
|
7
|
+
Get accurate information from the official OpenHands documentation at <https://docs.openhands.dev/>. The documentation includes:
|
|
8
|
+
|
|
9
|
+
**OpenHands SDK** (`/sdk/*`): Python library for building AI agents; Getting Started, Architecture, Guides (agent, llm, conversation, tools), API Reference
|
|
10
|
+
**OpenHands CLI** (`/openhands/usage/run-openhands/cli-mode`): Command-line interface
|
|
11
|
+
**OpenHands GUI** (`/openhands/usage/run-openhands/local-setup`): Local GUI and REST API
|
|
12
|
+
**OpenHands Cloud** (`/openhands/usage/run-openhands/cloud`): Hosted solution with integrations
|
|
13
|
+
**OpenHands Enterprise**: Self-hosted deployment with extended support
|
|
14
|
+
|
|
15
|
+
Always provide links to the relevant documentation pages for users who want to learn more.
|
|
@@ -5,6 +5,13 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
|
|
5
5
|
* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question.
|
|
6
6
|
</ROLE>
|
|
7
7
|
|
|
8
|
+
<MEMORY>
|
|
9
|
+
* Use `.openhands/skills/repo.md` under the repository root as your persistent memory for repository-specific knowledge and context.
|
|
10
|
+
* Add important insights, patterns, and learnings to this file to improve future task performance.
|
|
11
|
+
* This repository skill is automatically loaded for every conversation and helps maintain context across sessions.
|
|
12
|
+
* For more information about skills, see: https://docs.openhands.dev/overview/skills
|
|
13
|
+
</MEMORY>
|
|
14
|
+
|
|
8
15
|
<EFFICIENCY>
|
|
9
16
|
* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once.
|
|
10
17
|
* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations.
|
|
@@ -62,8 +69,12 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
|
|
62
69
|
5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests.
|
|
63
70
|
</PROBLEM_SOLVING_WORKFLOW>
|
|
64
71
|
|
|
72
|
+
<SELF_DOCUMENTATION>
|
|
73
|
+
{% include 'self_documentation.j2' %}
|
|
74
|
+
</SELF_DOCUMENTATION>
|
|
75
|
+
|
|
65
76
|
<SECURITY>
|
|
66
|
-
{% include
|
|
77
|
+
{% include security_policy_filename %}
|
|
67
78
|
</SECURITY>
|
|
68
79
|
|
|
69
80
|
{% if llm_security_analyzer %}
|
|
@@ -102,3 +113,20 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
|
|
102
113
|
- Prefer using `ps aux` to find the exact process ID (PID) first, then kill that specific PID
|
|
103
114
|
- When possible, use more targeted approaches like finding the PID from a pidfile or using application-specific shutdown commands
|
|
104
115
|
</PROCESS_MANAGEMENT>
|
|
116
|
+
|
|
117
|
+
{%- set _imp -%}
|
|
118
|
+
{%- if model_family -%}
|
|
119
|
+
{%- include "model_specific/" ~ model_family ~ ".j2" ignore missing -%}
|
|
120
|
+
{%- if model_variant -%}
|
|
121
|
+
{%- include "model_specific/" ~ model_family ~ "/" ~ model_variant ~ ".j2" ignore missing -%}
|
|
122
|
+
{%- endif -%}
|
|
123
|
+
{%- endif -%}
|
|
124
|
+
{%- endset -%}
|
|
125
|
+
|
|
126
|
+
{%- set _imp_trimmed = _imp | trim -%}
|
|
127
|
+
{%- if _imp_trimmed %}
|
|
128
|
+
|
|
129
|
+
<IMPORTANT>
|
|
130
|
+
{{ _imp_trimmed }}
|
|
131
|
+
</IMPORTANT>
|
|
132
|
+
{%- endif %}
|
|
@@ -195,6 +195,15 @@ def make_llm_completion(
|
|
|
195
195
|
|
|
196
196
|
Returns:
|
|
197
197
|
LLMResponse from the LLM completion call
|
|
198
|
+
|
|
199
|
+
Note:
|
|
200
|
+
Always exposes a 'security_risk' parameter in tool schemas via
|
|
201
|
+
add_security_risk_prediction=True. This ensures the schema remains
|
|
202
|
+
consistent, even if the security analyzer is disabled. Validation of
|
|
203
|
+
this field happens dynamically at runtime depending on the analyzer
|
|
204
|
+
configured. This allows weaker models to omit risk field and bypass
|
|
205
|
+
validation requirements when analyzer is disabled. For detailed logic,
|
|
206
|
+
see `_extract_security_risk` method in agent.py.
|
|
198
207
|
"""
|
|
199
208
|
if llm.uses_responses_api():
|
|
200
209
|
return llm.responses(
|
|
@@ -7,6 +7,7 @@ from openhands.sdk.context.skills import (
|
|
|
7
7
|
SkillKnowledge,
|
|
8
8
|
SkillValidationError,
|
|
9
9
|
TaskTrigger,
|
|
10
|
+
load_project_skills,
|
|
10
11
|
load_skills_from_dir,
|
|
11
12
|
load_user_skills,
|
|
12
13
|
)
|
|
@@ -21,6 +22,7 @@ __all__ = [
|
|
|
21
22
|
"SkillKnowledge",
|
|
22
23
|
"load_skills_from_dir",
|
|
23
24
|
"load_user_skills",
|
|
25
|
+
"load_project_skills",
|
|
24
26
|
"render_template",
|
|
25
27
|
"SkillValidationError",
|
|
26
28
|
]
|
|
@@ -14,7 +14,7 @@ from openhands.sdk.context.skills import (
|
|
|
14
14
|
)
|
|
15
15
|
from openhands.sdk.llm import Message, TextContent
|
|
16
16
|
from openhands.sdk.logger import get_logger
|
|
17
|
-
from openhands.sdk.secret import SecretValue
|
|
17
|
+
from openhands.sdk.secret import SecretSource, SecretValue
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
logger = get_logger(__name__)
|
|
@@ -136,15 +136,23 @@ class AgentContext(BaseModel):
|
|
|
136
136
|
logger.warning(f"Failed to load public skills: {str(e)}")
|
|
137
137
|
return self
|
|
138
138
|
|
|
139
|
-
def
|
|
140
|
-
"""Get
|
|
139
|
+
def get_secret_infos(self) -> list[dict[str, str]]:
|
|
140
|
+
"""Get secret information (name and description) from the secrets field.
|
|
141
141
|
|
|
142
142
|
Returns:
|
|
143
|
-
List of
|
|
143
|
+
List of dictionaries with 'name' and 'description' keys.
|
|
144
|
+
Returns an empty list if no secrets are configured.
|
|
145
|
+
Description will be None if not available.
|
|
144
146
|
"""
|
|
145
147
|
if not self.secrets:
|
|
146
148
|
return []
|
|
147
|
-
|
|
149
|
+
secret_infos = []
|
|
150
|
+
for name, secret_value in self.secrets.items():
|
|
151
|
+
description = None
|
|
152
|
+
if isinstance(secret_value, SecretSource):
|
|
153
|
+
description = secret_value.description
|
|
154
|
+
secret_infos.append({"name": name, "description": description})
|
|
155
|
+
return secret_infos
|
|
148
156
|
|
|
149
157
|
def get_system_message_suffix(self) -> str | None:
|
|
150
158
|
"""Get the system message with repo skill content and custom suffix.
|
|
@@ -158,15 +166,15 @@ class AgentContext(BaseModel):
|
|
|
158
166
|
repo_skills = [s for s in self.skills if s.trigger is None]
|
|
159
167
|
logger.debug(f"Triggered {len(repo_skills)} repository skills: {repo_skills}")
|
|
160
168
|
# Build the workspace context information
|
|
161
|
-
|
|
162
|
-
if repo_skills or self.system_message_suffix or
|
|
169
|
+
secret_infos = self.get_secret_infos()
|
|
170
|
+
if repo_skills or self.system_message_suffix or secret_infos:
|
|
163
171
|
# TODO(test): add a test for this rendering to make sure they work
|
|
164
172
|
formatted_text = render_template(
|
|
165
173
|
prompt_dir=str(PROMPT_DIR),
|
|
166
174
|
template_name="system_message_suffix.j2",
|
|
167
175
|
repo_skills=repo_skills,
|
|
168
176
|
system_message_suffix=self.system_message_suffix or "",
|
|
169
|
-
|
|
177
|
+
secret_infos=secret_infos,
|
|
170
178
|
).strip()
|
|
171
179
|
return formatted_text
|
|
172
180
|
elif self.system_message_suffix and self.system_message_suffix.strip():
|
|
@@ -4,7 +4,45 @@ import re
|
|
|
4
4
|
import sys
|
|
5
5
|
from functools import lru_cache
|
|
6
6
|
|
|
7
|
-
from jinja2 import
|
|
7
|
+
from jinja2 import (
|
|
8
|
+
BaseLoader,
|
|
9
|
+
Environment,
|
|
10
|
+
FileSystemBytecodeCache,
|
|
11
|
+
Template,
|
|
12
|
+
TemplateNotFound,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FlexibleFileSystemLoader(BaseLoader):
|
|
17
|
+
"""A Jinja2 loader that supports both relative paths (within a base directory)
|
|
18
|
+
and absolute paths anywhere on the filesystem.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, searchpath: str):
|
|
22
|
+
self.searchpath = os.path.abspath(searchpath)
|
|
23
|
+
|
|
24
|
+
def get_source(self, environment, template): # noqa: ARG002
|
|
25
|
+
# If template is an absolute path, use it directly
|
|
26
|
+
if os.path.isabs(template):
|
|
27
|
+
path = template
|
|
28
|
+
else:
|
|
29
|
+
# Otherwise, look for it in the searchpath
|
|
30
|
+
path = os.path.join(self.searchpath, template)
|
|
31
|
+
|
|
32
|
+
if not os.path.exists(path):
|
|
33
|
+
raise TemplateNotFound(template)
|
|
34
|
+
|
|
35
|
+
mtime = os.path.getmtime(path)
|
|
36
|
+
with open(path, encoding="utf-8") as f:
|
|
37
|
+
source = f.read()
|
|
38
|
+
|
|
39
|
+
def uptodate():
|
|
40
|
+
try:
|
|
41
|
+
return os.path.getmtime(path) == mtime
|
|
42
|
+
except OSError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
return source, path, uptodate
|
|
8
46
|
|
|
9
47
|
|
|
10
48
|
def refine(text: str) -> str:
|
|
@@ -27,7 +65,7 @@ def _get_env(prompt_dir: str) -> Environment:
|
|
|
27
65
|
os.makedirs(cache_folder, exist_ok=True)
|
|
28
66
|
bcc = FileSystemBytecodeCache(directory=cache_folder)
|
|
29
67
|
env = Environment(
|
|
30
|
-
loader=
|
|
68
|
+
loader=FlexibleFileSystemLoader(prompt_dir),
|
|
31
69
|
bytecode_cache=bcc,
|
|
32
70
|
autoescape=False,
|
|
33
71
|
)
|
|
@@ -14,7 +14,7 @@ Please follow them while working.
|
|
|
14
14
|
|
|
15
15
|
{{ system_message_suffix }}
|
|
16
16
|
{% endif %}
|
|
17
|
-
{% if
|
|
17
|
+
{% if secret_infos %}
|
|
18
18
|
<CUSTOM_SECRETS>
|
|
19
19
|
### Credential Access
|
|
20
20
|
* Automatic secret injection: When you reference a registered secret key in your bash command, the secret value will be automatically exported as an environment variable before your command executes.
|
|
@@ -25,8 +25,8 @@ Please follow them while working.
|
|
|
25
25
|
* If it still fails, report it to the user.
|
|
26
26
|
|
|
27
27
|
You have access to the following environment variables
|
|
28
|
-
{% for
|
|
29
|
-
* **${{
|
|
28
|
+
{% for secret_info in secret_infos %}
|
|
29
|
+
* **${{ secret_info.name }}**{% if secret_info.description %} - {{ secret_info.description }}{% endif %}
|
|
30
30
|
{% endfor %}
|
|
31
31
|
</CUSTOM_SECRETS>
|
|
32
32
|
{% endif %}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from openhands.sdk.context.skills.exceptions import SkillValidationError
|
|
2
2
|
from openhands.sdk.context.skills.skill import (
|
|
3
3
|
Skill,
|
|
4
|
+
load_project_skills,
|
|
4
5
|
load_public_skills,
|
|
5
6
|
load_skills_from_dir,
|
|
6
7
|
load_user_skills,
|
|
@@ -21,6 +22,7 @@ __all__ = [
|
|
|
21
22
|
"SkillKnowledge",
|
|
22
23
|
"load_skills_from_dir",
|
|
23
24
|
"load_user_skills",
|
|
25
|
+
"load_project_skills",
|
|
24
26
|
"load_public_skills",
|
|
25
27
|
"SkillValidationError",
|
|
26
28
|
]
|
|
@@ -398,6 +398,67 @@ def load_user_skills() -> list[Skill]:
|
|
|
398
398
|
return all_skills
|
|
399
399
|
|
|
400
400
|
|
|
401
|
+
def load_project_skills(work_dir: str | Path) -> list[Skill]:
|
|
402
|
+
"""Load skills from project-specific directories.
|
|
403
|
+
|
|
404
|
+
Searches for skills in {work_dir}/.openhands/skills/ and
|
|
405
|
+
{work_dir}/.openhands/microagents/ (legacy). Skills from both
|
|
406
|
+
directories are merged, with skills/ taking precedence for
|
|
407
|
+
duplicate names.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
work_dir: Path to the project/working directory.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
List of Skill objects loaded from project directories.
|
|
414
|
+
Returns empty list if no skills found or loading fails.
|
|
415
|
+
"""
|
|
416
|
+
if isinstance(work_dir, str):
|
|
417
|
+
work_dir = Path(work_dir)
|
|
418
|
+
|
|
419
|
+
all_skills = []
|
|
420
|
+
seen_names = set()
|
|
421
|
+
|
|
422
|
+
# Load project-specific skills from .openhands/skills and legacy microagents
|
|
423
|
+
project_skills_dirs = [
|
|
424
|
+
work_dir / ".openhands" / "skills",
|
|
425
|
+
work_dir / ".openhands" / "microagents", # Legacy support
|
|
426
|
+
]
|
|
427
|
+
|
|
428
|
+
for project_skills_dir in project_skills_dirs:
|
|
429
|
+
if not project_skills_dir.exists():
|
|
430
|
+
logger.debug(
|
|
431
|
+
f"Project skills directory does not exist: {project_skills_dir}"
|
|
432
|
+
)
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
logger.debug(f"Loading project skills from {project_skills_dir}")
|
|
437
|
+
repo_skills, knowledge_skills = load_skills_from_dir(project_skills_dir)
|
|
438
|
+
|
|
439
|
+
# Merge repo and knowledge skills
|
|
440
|
+
for skills_dict in [repo_skills, knowledge_skills]:
|
|
441
|
+
for name, skill in skills_dict.items():
|
|
442
|
+
if name not in seen_names:
|
|
443
|
+
all_skills.append(skill)
|
|
444
|
+
seen_names.add(name)
|
|
445
|
+
else:
|
|
446
|
+
logger.warning(
|
|
447
|
+
f"Skipping duplicate skill '{name}' from "
|
|
448
|
+
f"{project_skills_dir}"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
except Exception as e:
|
|
452
|
+
logger.warning(
|
|
453
|
+
f"Failed to load project skills from {project_skills_dir}: {str(e)}"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
logger.debug(
|
|
457
|
+
f"Loaded {len(all_skills)} project skills: {[s.name for s in all_skills]}"
|
|
458
|
+
)
|
|
459
|
+
return all_skills
|
|
460
|
+
|
|
461
|
+
|
|
401
462
|
# Public skills repository configuration
|
|
402
463
|
PUBLIC_SKILLS_REPO = "https://github.com/OpenHands/skills"
|
|
403
464
|
PUBLIC_SKILLS_BRANCH = "main"
|
|
@@ -89,38 +89,72 @@ class View(BaseModel):
|
|
|
89
89
|
raise ValueError(f"Invalid key type: {type(key)}")
|
|
90
90
|
|
|
91
91
|
@staticmethod
|
|
92
|
-
def
|
|
92
|
+
def _build_action_batches(
|
|
93
93
|
events: Sequence[Event],
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
) -> tuple[
|
|
95
|
+
dict[EventID, list[EventID]], dict[EventID, EventID], dict[EventID, ToolCallID]
|
|
96
|
+
]:
|
|
97
|
+
"""Build a map of llm_response_id -> list of ActionEvent IDs.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
A tuple of:
|
|
101
|
+
- batches: dict mapping llm_response_id to list of ActionEvent IDs
|
|
102
|
+
- action_id_to_response_id: dict mapping ActionEvent ID to llm_response_id
|
|
103
|
+
- action_id_to_tool_call_id: dict mapping ActionEvent ID to tool_call_id
|
|
101
104
|
"""
|
|
102
105
|
batches: dict[EventID, list[EventID]] = {}
|
|
106
|
+
action_id_to_response_id: dict[EventID, EventID] = {}
|
|
107
|
+
action_id_to_tool_call_id: dict[EventID, ToolCallID] = {}
|
|
108
|
+
|
|
103
109
|
for event in events:
|
|
104
110
|
if isinstance(event, ActionEvent):
|
|
105
111
|
llm_response_id = event.llm_response_id
|
|
106
112
|
if llm_response_id not in batches:
|
|
107
113
|
batches[llm_response_id] = []
|
|
108
114
|
batches[llm_response_id].append(event.id)
|
|
115
|
+
action_id_to_response_id[event.id] = llm_response_id
|
|
116
|
+
action_id_to_tool_call_id[event.id] = event.tool_call_id
|
|
117
|
+
|
|
118
|
+
return batches, action_id_to_response_id, action_id_to_tool_call_id
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _enforce_batch_atomicity(
|
|
122
|
+
events: Sequence[Event],
|
|
123
|
+
removed_event_ids: set[EventID],
|
|
124
|
+
) -> set[EventID]:
|
|
125
|
+
"""Ensure that if any ActionEvent in a batch is removed, all ActionEvents
|
|
126
|
+
in that batch are removed.
|
|
127
|
+
|
|
128
|
+
This prevents partial batches from being sent to the LLM, which can cause
|
|
129
|
+
API errors when thinking blocks are separated from their tool calls.
|
|
109
130
|
|
|
110
|
-
|
|
131
|
+
Args:
|
|
132
|
+
events: The original list of events
|
|
133
|
+
removed_event_ids: Set of event IDs that are being removed
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Updated set of event IDs that should be removed (including all
|
|
137
|
+
ActionEvents in batches where any ActionEvent was removed)
|
|
138
|
+
"""
|
|
139
|
+
batches, action_id_to_response_id, _ = View._build_action_batches(events)
|
|
140
|
+
|
|
141
|
+
if not batches:
|
|
142
|
+
return removed_event_ids
|
|
143
|
+
|
|
144
|
+
updated_removed_ids = set(removed_event_ids)
|
|
111
145
|
|
|
112
146
|
for llm_response_id, batch_event_ids in batches.items():
|
|
113
|
-
# Check if any
|
|
114
|
-
if any(event_id in
|
|
115
|
-
# If so,
|
|
116
|
-
|
|
147
|
+
# Check if any ActionEvent in this batch is being removed
|
|
148
|
+
if any(event_id in removed_event_ids for event_id in batch_event_ids):
|
|
149
|
+
# If so, remove all ActionEvents in this batch
|
|
150
|
+
updated_removed_ids.update(batch_event_ids)
|
|
117
151
|
logger.debug(
|
|
118
|
-
f"Enforcing batch atomicity:
|
|
152
|
+
f"Enforcing batch atomicity: removing entire batch "
|
|
119
153
|
f"with llm_response_id={llm_response_id} "
|
|
120
154
|
f"({len(batch_event_ids)} events)"
|
|
121
155
|
)
|
|
122
156
|
|
|
123
|
-
return
|
|
157
|
+
return updated_removed_ids
|
|
124
158
|
|
|
125
159
|
@staticmethod
|
|
126
160
|
def filter_unmatched_tool_calls(
|
|
@@ -129,18 +163,47 @@ class View(BaseModel):
|
|
|
129
163
|
"""Filter out unmatched tool call events.
|
|
130
164
|
|
|
131
165
|
Removes ActionEvents and ObservationEvents that have tool_call_ids
|
|
132
|
-
but don't have matching pairs.
|
|
166
|
+
but don't have matching pairs. Also enforces batch atomicity - if any
|
|
167
|
+
ActionEvent in a batch is filtered out, all ActionEvents in that batch
|
|
168
|
+
are also filtered out.
|
|
133
169
|
"""
|
|
134
170
|
action_tool_call_ids = View._get_action_tool_call_ids(events)
|
|
135
171
|
observation_tool_call_ids = View._get_observation_tool_call_ids(events)
|
|
136
172
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
173
|
+
# Build batch info for batch atomicity enforcement
|
|
174
|
+
_, _, action_id_to_tool_call_id = View._build_action_batches(events)
|
|
175
|
+
|
|
176
|
+
# First pass: identify which events would NOT be kept based on matching
|
|
177
|
+
removed_event_ids: set[EventID] = set()
|
|
178
|
+
for event in events:
|
|
179
|
+
if not View._should_keep_event(
|
|
141
180
|
event, action_tool_call_ids, observation_tool_call_ids
|
|
142
|
-
)
|
|
143
|
-
|
|
181
|
+
):
|
|
182
|
+
removed_event_ids.add(event.id)
|
|
183
|
+
|
|
184
|
+
# Second pass: enforce batch atomicity for ActionEvents
|
|
185
|
+
# If any ActionEvent in a batch is removed, all ActionEvents in that
|
|
186
|
+
# batch should also be removed
|
|
187
|
+
removed_event_ids = View._enforce_batch_atomicity(events, removed_event_ids)
|
|
188
|
+
|
|
189
|
+
# Third pass: also remove ObservationEvents whose ActionEvents were removed
|
|
190
|
+
# due to batch atomicity
|
|
191
|
+
tool_call_ids_to_remove: set[ToolCallID] = set()
|
|
192
|
+
for action_id in removed_event_ids:
|
|
193
|
+
if action_id in action_id_to_tool_call_id:
|
|
194
|
+
tool_call_ids_to_remove.add(action_id_to_tool_call_id[action_id])
|
|
195
|
+
|
|
196
|
+
# Filter out removed events
|
|
197
|
+
result = []
|
|
198
|
+
for event in events:
|
|
199
|
+
if event.id in removed_event_ids:
|
|
200
|
+
continue
|
|
201
|
+
if isinstance(event, ObservationBaseEvent):
|
|
202
|
+
if event.tool_call_id in tool_call_ids_to_remove:
|
|
203
|
+
continue
|
|
204
|
+
result.append(event)
|
|
205
|
+
|
|
206
|
+
return result
|
|
144
207
|
|
|
145
208
|
@staticmethod
|
|
146
209
|
def _get_action_tool_call_ids(events: list[LLMConvertibleEvent]) -> set[ToolCallID]:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import TYPE_CHECKING, Self, overload
|
|
3
4
|
|
|
@@ -7,6 +8,7 @@ from openhands.sdk.conversation.types import (
|
|
|
7
8
|
ConversationCallbackType,
|
|
8
9
|
ConversationID,
|
|
9
10
|
ConversationTokenCallbackType,
|
|
11
|
+
StuckDetectionThresholds,
|
|
10
12
|
)
|
|
11
13
|
from openhands.sdk.conversation.visualizer import (
|
|
12
14
|
ConversationVisualizerBase,
|
|
@@ -56,6 +58,9 @@ class Conversation:
|
|
|
56
58
|
token_callbacks: list[ConversationTokenCallbackType] | None = None,
|
|
57
59
|
max_iteration_per_run: int = 500,
|
|
58
60
|
stuck_detection: bool = True,
|
|
61
|
+
stuck_detection_thresholds: (
|
|
62
|
+
StuckDetectionThresholds | Mapping[str, int] | None
|
|
63
|
+
) = None,
|
|
59
64
|
visualizer: (
|
|
60
65
|
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
|
|
61
66
|
) = DefaultConversationVisualizer,
|
|
@@ -73,6 +78,9 @@ class Conversation:
|
|
|
73
78
|
token_callbacks: list[ConversationTokenCallbackType] | None = None,
|
|
74
79
|
max_iteration_per_run: int = 500,
|
|
75
80
|
stuck_detection: bool = True,
|
|
81
|
+
stuck_detection_thresholds: (
|
|
82
|
+
StuckDetectionThresholds | Mapping[str, int] | None
|
|
83
|
+
) = None,
|
|
76
84
|
visualizer: (
|
|
77
85
|
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
|
|
78
86
|
) = DefaultConversationVisualizer,
|
|
@@ -90,6 +98,9 @@ class Conversation:
|
|
|
90
98
|
token_callbacks: list[ConversationTokenCallbackType] | None = None,
|
|
91
99
|
max_iteration_per_run: int = 500,
|
|
92
100
|
stuck_detection: bool = True,
|
|
101
|
+
stuck_detection_thresholds: (
|
|
102
|
+
StuckDetectionThresholds | Mapping[str, int] | None
|
|
103
|
+
) = None,
|
|
93
104
|
visualizer: (
|
|
94
105
|
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
|
|
95
106
|
) = DefaultConversationVisualizer,
|
|
@@ -114,6 +125,7 @@ class Conversation:
|
|
|
114
125
|
token_callbacks=token_callbacks,
|
|
115
126
|
max_iteration_per_run=max_iteration_per_run,
|
|
116
127
|
stuck_detection=stuck_detection,
|
|
128
|
+
stuck_detection_thresholds=stuck_detection_thresholds,
|
|
117
129
|
visualizer=visualizer,
|
|
118
130
|
workspace=workspace,
|
|
119
131
|
secrets=secrets,
|
|
@@ -126,6 +138,7 @@ class Conversation:
|
|
|
126
138
|
token_callbacks=token_callbacks,
|
|
127
139
|
max_iteration_per_run=max_iteration_per_run,
|
|
128
140
|
stuck_detection=stuck_detection,
|
|
141
|
+
stuck_detection_thresholds=stuck_detection_thresholds,
|
|
129
142
|
visualizer=visualizer,
|
|
130
143
|
workspace=workspace,
|
|
131
144
|
persistence_dir=persistence_dir,
|