openhands-sdk 1.6.0__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.6.0 → openhands_sdk-1.7.0}/PKG-INFO +2 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/__init__.py +9 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/agent.py +4 -11
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/base.py +11 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/utils.py +9 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/__init__.py +2 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/agent_context.py +16 -8
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/prompt.py +40 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +3 -3
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/__init__.py +2 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/skill.py +61 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/view.py +85 -22
- {openhands_sdk-1.6.0 → 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.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/local_conversation.py +27 -5
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/remote_conversation.py +101 -3
- {openhands_sdk-1.6.0 → 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.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/system.py +16 -20
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/message.py +2 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/model_features.py +64 -24
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/verified_models.py +4 -4
- {openhands_sdk-1.6.0 → 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.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/models.py +1 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/PKG-INFO +2 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/requires.txt +1 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/pyproject.toml +2 -2
- openhands_sdk-1.6.0/openhands/sdk/conversation/exceptions.py +0 -25
- openhands_sdk-1.6.0/openhands/sdk/conversation/types.py +0 -15
- openhands_sdk-1.6.0/openhands/sdk/utils/async_executor.py +0 -106
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/exceptions.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/trigger.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/types.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/event_store.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/events_list_base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/persistence_const.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/response_utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/secret_registry.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/serialization_diff.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/state.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/title_utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/condenser.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/conversation_error.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/conversation_state.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_completion_log.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/token.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/types.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/user_action.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/exceptions.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/git_changes.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/git_diff.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/models.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/io/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/io/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/io/local.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/io/memory.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/types.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/llm.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/llm_registry.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/llm_response.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/chat_options.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/common.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/responses_options.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/impl/random.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/streaming.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/metrics.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/model_info.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/logger/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/logger/rolling.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/client.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/definition.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/exceptions.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/tool.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/observability/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/observability/laminar.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/observability/utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/py.typed +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/secret/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/secret/secrets.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/analyzer.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/confirmation_policy.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/llm_analyzer.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/risk.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/builtins/finish.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/builtins/think.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/registry.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/schema.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/spec.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/tool.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/async_utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/cipher.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/command.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/deprecation.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/github.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/json.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/paging.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/pydantic_diff.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/truncate.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/visualize.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/local.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/models.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/workspace.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/SOURCES.txt +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/top_level.txt +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/setup.cfg +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
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
|
|
@@ -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
|
|
|
@@ -121,6 +121,15 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
121
121
|
"- An absolute path (e.g., '/path/to/custom_prompt.j2')"
|
|
122
122
|
),
|
|
123
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
|
+
)
|
|
124
133
|
system_prompt_kwargs: dict[str, object] = Field(
|
|
125
134
|
default_factory=dict,
|
|
126
135
|
description="Optional kwargs to pass to the system prompt Jinja2 template.",
|
|
@@ -165,6 +174,8 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
165
174
|
def system_message(self) -> str:
|
|
166
175
|
"""Compute system message on-demand to maintain statelessness."""
|
|
167
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
|
|
168
179
|
template_kwargs.setdefault("model_name", self.llm.model)
|
|
169
180
|
if (
|
|
170
181
|
"model_family" not in template_kwargs
|
|
@@ -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,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from openhands.sdk.conversation.types import ConversationID
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
ISSUE_URL = "https://github.com/OpenHands/software-agent-sdk/issues/new"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConversationRunError(RuntimeError):
|
|
8
|
+
"""Raised when a conversation run fails.
|
|
9
|
+
|
|
10
|
+
Carries the conversation_id and persistence_dir to make resuming/debugging
|
|
11
|
+
easier while preserving the original exception via exception chaining.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
conversation_id: ConversationID
|
|
15
|
+
persistence_dir: str | None
|
|
16
|
+
original_exception: BaseException
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
conversation_id: ConversationID,
|
|
21
|
+
original_exception: BaseException,
|
|
22
|
+
persistence_dir: str | None = None,
|
|
23
|
+
message: str | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
self.conversation_id = conversation_id
|
|
26
|
+
self.persistence_dir = persistence_dir
|
|
27
|
+
self.original_exception = original_exception
|
|
28
|
+
default_msg = self._build_error_message(
|
|
29
|
+
conversation_id, original_exception, persistence_dir
|
|
30
|
+
)
|
|
31
|
+
super().__init__(message or default_msg)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _build_error_message(
|
|
35
|
+
conversation_id: ConversationID,
|
|
36
|
+
original_exception: BaseException,
|
|
37
|
+
persistence_dir: str | None,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Build a detailed error message with debugging information."""
|
|
40
|
+
lines = [
|
|
41
|
+
f"Conversation run failed for id={conversation_id}: {original_exception}",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
if persistence_dir:
|
|
45
|
+
lines.append(f"\nConversation logs are stored at: {persistence_dir}")
|
|
46
|
+
lines.append("\nTo help debug this issue, please file a bug report at:")
|
|
47
|
+
lines.append(f" {ISSUE_URL}")
|
|
48
|
+
lines.append("and attach the conversation logs from the directory above.")
|
|
49
|
+
|
|
50
|
+
return "\n".join(lines)
|
{openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/local_conversation.py
RENAMED
|
@@ -18,6 +18,7 @@ from openhands.sdk.conversation.types import (
|
|
|
18
18
|
ConversationCallbackType,
|
|
19
19
|
ConversationID,
|
|
20
20
|
ConversationTokenCallbackType,
|
|
21
|
+
StuckDetectionThresholds,
|
|
21
22
|
)
|
|
22
23
|
from openhands.sdk.conversation.visualizer import (
|
|
23
24
|
ConversationVisualizerBase,
|
|
@@ -66,6 +67,9 @@ class LocalConversation(BaseConversation):
|
|
|
66
67
|
token_callbacks: list[ConversationTokenCallbackType] | None = None,
|
|
67
68
|
max_iteration_per_run: int = 500,
|
|
68
69
|
stuck_detection: bool = True,
|
|
70
|
+
stuck_detection_thresholds: (
|
|
71
|
+
StuckDetectionThresholds | Mapping[str, int] | None
|
|
72
|
+
) = None,
|
|
69
73
|
visualizer: (
|
|
70
74
|
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
|
|
71
75
|
) = DefaultConversationVisualizer,
|
|
@@ -92,6 +96,11 @@ class LocalConversation(BaseConversation):
|
|
|
92
96
|
- ConversationVisualizerBase instance: Use custom visualizer
|
|
93
97
|
- None: No visualization
|
|
94
98
|
stuck_detection: Whether to enable stuck detection
|
|
99
|
+
stuck_detection_thresholds: Optional configuration for stuck detection
|
|
100
|
+
thresholds. Can be a StuckDetectionThresholds instance or
|
|
101
|
+
a dict with keys: 'action_observation', 'action_error',
|
|
102
|
+
'monologue', 'alternating_pattern'. Values are integers
|
|
103
|
+
representing the number of repetitions before triggering.
|
|
95
104
|
"""
|
|
96
105
|
super().__init__() # Initialize with span tracking
|
|
97
106
|
# Mark cleanup as initiated as early as possible to avoid races or partially
|
|
@@ -159,7 +168,20 @@ class LocalConversation(BaseConversation):
|
|
|
159
168
|
self.max_iteration_per_run = max_iteration_per_run
|
|
160
169
|
|
|
161
170
|
# Initialize stuck detector
|
|
162
|
-
|
|
171
|
+
if stuck_detection:
|
|
172
|
+
# Convert dict to StuckDetectionThresholds if needed
|
|
173
|
+
if isinstance(stuck_detection_thresholds, Mapping):
|
|
174
|
+
threshold_config = StuckDetectionThresholds(
|
|
175
|
+
**stuck_detection_thresholds
|
|
176
|
+
)
|
|
177
|
+
else:
|
|
178
|
+
threshold_config = stuck_detection_thresholds
|
|
179
|
+
self._stuck_detector = StuckDetector(
|
|
180
|
+
self._state,
|
|
181
|
+
thresholds=threshold_config,
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
self._stuck_detector = None
|
|
163
185
|
|
|
164
186
|
with self._state:
|
|
165
187
|
self.agent.init_state(self._state, on_event=self._on_event)
|
|
@@ -349,10 +371,10 @@ class LocalConversation(BaseConversation):
|
|
|
349
371
|
)
|
|
350
372
|
)
|
|
351
373
|
|
|
352
|
-
# Re-raise with conversation id for better UX
|
|
353
|
-
raise ConversationRunError(
|
|
354
|
-
|
|
355
|
-
|
|
374
|
+
# Re-raise with conversation id and persistence dir for better UX
|
|
375
|
+
raise ConversationRunError(
|
|
376
|
+
self._state.id, e, persistence_dir=self._state.persistence_dir
|
|
377
|
+
) from e
|
|
356
378
|
|
|
357
379
|
def set_confirmation_policy(self, policy: ConfirmationPolicyBase) -> None:
|
|
358
380
|
"""Set the confirmation policy and store it in conversation state."""
|