openhands-sdk 1.8.1__tar.gz → 1.9.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.8.1 → openhands_sdk-1.9.0}/PKG-INFO +6 -1
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/agent.py +64 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/base.py +29 -10
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +7 -5
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/skill.py +59 -1
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/utils.py +6 -65
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/view.py +6 -11
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/base.py +5 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/event_store.py +84 -12
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/impl/local_conversation.py +7 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/impl/remote_conversation.py +16 -3
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/state.py +25 -2
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/visualizer/base.py +23 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/__init__.py +4 -1
- openhands_sdk-1.9.0/openhands/sdk/critic/base.py +35 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/impl/__init__.py +2 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/impl/agent_finished.py +9 -5
- openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/__init__.py +18 -0
- openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/chat_template.py +232 -0
- openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/client.py +313 -0
- openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/critic.py +90 -0
- openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/taxonomy.py +180 -0
- openhands_sdk-1.9.0/openhands/sdk/critic/result.py +148 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/conversation_error.py +12 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/action.py +10 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/message.py +10 -0
- openhands_sdk-1.9.0/openhands/sdk/git/cached_repo.py +459 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/utils.py +118 -3
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/__init__.py +7 -1
- openhands_sdk-1.9.0/openhands/sdk/hooks/config.py +289 -0
- openhands_sdk-1.9.0/openhands/sdk/io/base.py +100 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/io/local.py +25 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/io/memory.py +34 -1
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/llm.py +6 -2
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/model_features.py +3 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/telemetry.py +41 -2
- openhands_sdk-1.9.0/openhands/sdk/plugin/__init__.py +39 -0
- openhands_sdk-1.9.0/openhands/sdk/plugin/fetch.py +231 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/plugin/plugin.py +61 -4
- openhands_sdk-1.9.0/openhands/sdk/plugin/types.py +619 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/secret/secrets.py +19 -4
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/PKG-INFO +6 -1
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/SOURCES.txt +16 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/requires.txt +1 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/pyproject.toml +9 -2
- openhands_sdk-1.8.1/openhands/sdk/critic/base.py +0 -38
- openhands_sdk-1.8.1/openhands/sdk/hooks/config.py +0 -180
- openhands_sdk-1.8.1/openhands/sdk/io/base.py +0 -48
- openhands_sdk-1.8.1/openhands/sdk/plugin/__init__.py +0 -22
- openhands_sdk-1.8.1/openhands/sdk/plugin/types.py +0 -226
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/agent_context.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/prompt.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/exceptions.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/trigger.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/conversation.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/events_list_base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/exceptions.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/persistence_const.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/response_utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/secret_registry.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/serialization_diff.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/title_utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/condenser.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/conversation_state.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_completion_log.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/token.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/user_action.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/exceptions.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/git_changes.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/git_diff.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/executor.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/manager.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/io/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/io/cache.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/exceptions/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/llm_registry.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/llm_response.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/message.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/options/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/options/chat_options.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/options/common.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/options/responses_options.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/router/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/router/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/router/impl/random.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/streaming.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/metrics.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/model_info.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/verified_models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/logger/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/logger/logger.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/logger/rolling.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/client.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/definition.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/exceptions.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/tool.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/observability/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/observability/laminar.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/observability/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/py.typed +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/secret/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/analyzer.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/confirmation_policy.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/llm_analyzer.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/risk.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/builtins/finish.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/builtins/think.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/registry.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/schema.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/spec.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/tool.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/async_executor.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/async_utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/cipher.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/command.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/deprecation.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/github.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/json.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/paging.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/pydantic_diff.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/truncate.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/visualize.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/local.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/remote/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/workspace.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/top_level.txt +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/setup.cfg +0 -0
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.0
|
|
4
4
|
Summary: OpenHands SDK - Core functionality for building AI agents
|
|
5
|
+
Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
|
|
6
|
+
Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
|
|
7
|
+
Project-URL: Documentation, https://docs.openhands.dev/sdk
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/OpenHands/software-agent-sdk/issues
|
|
5
9
|
Requires-Python: >=3.12
|
|
6
10
|
Requires-Dist: deprecation>=2.1.0
|
|
7
11
|
Requires-Dist: fastmcp>=2.11.3
|
|
12
|
+
Requires-Dist: filelock>=3.20.1
|
|
8
13
|
Requires-Dist: httpx>=0.27.0
|
|
9
14
|
Requires-Dist: litellm>=1.80.10
|
|
10
15
|
Requires-Dist: pydantic>=2.12.5
|
|
@@ -17,6 +17,7 @@ from openhands.sdk.conversation import (
|
|
|
17
17
|
LocalConversation,
|
|
18
18
|
)
|
|
19
19
|
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
|
20
|
+
from openhands.sdk.critic.base import CriticResult
|
|
20
21
|
from openhands.sdk.event import (
|
|
21
22
|
ActionEvent,
|
|
22
23
|
AgentErrorEvent,
|
|
@@ -120,6 +121,48 @@ class Agent(AgentBase):
|
|
|
120
121
|
)
|
|
121
122
|
on_event(event)
|
|
122
123
|
|
|
124
|
+
def _should_evaluate_with_critic(self, action: Action | None) -> bool:
|
|
125
|
+
"""Determine if critic should evaluate based on action type and mode."""
|
|
126
|
+
if self.critic is None:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
if self.critic.mode == "all_actions":
|
|
130
|
+
return True
|
|
131
|
+
|
|
132
|
+
# For "finish_and_message" mode, only evaluate FinishAction
|
|
133
|
+
# (MessageEvent will be handled separately in step())
|
|
134
|
+
if isinstance(action, FinishAction):
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def _evaluate_with_critic(
|
|
140
|
+
self, conversation: LocalConversation, event: ActionEvent | MessageEvent
|
|
141
|
+
) -> CriticResult | None:
|
|
142
|
+
"""Run critic evaluation on the current event and history."""
|
|
143
|
+
if self.critic is None:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# Build event history including the current event
|
|
148
|
+
events = list(conversation.state.events) + [event]
|
|
149
|
+
llm_convertible_events = [
|
|
150
|
+
e for e in events if isinstance(e, LLMConvertibleEvent)
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
# Evaluate without git_patch for now
|
|
154
|
+
critic_result = self.critic.evaluate(
|
|
155
|
+
events=llm_convertible_events, git_patch=None
|
|
156
|
+
)
|
|
157
|
+
logger.info(
|
|
158
|
+
f"✓ Critic evaluation: score={critic_result.score:.3f}, "
|
|
159
|
+
f"success={critic_result.success}"
|
|
160
|
+
)
|
|
161
|
+
return critic_result
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.error(f"✗ Critic evaluation failed: {e}", exc_info=True)
|
|
164
|
+
return None
|
|
165
|
+
|
|
123
166
|
def _execute_actions(
|
|
124
167
|
self,
|
|
125
168
|
conversation: LocalConversation,
|
|
@@ -237,6 +280,7 @@ class Agent(AgentBase):
|
|
|
237
280
|
for i, tool_call in enumerate(message.tool_calls):
|
|
238
281
|
action_event = self._get_action_event(
|
|
239
282
|
tool_call,
|
|
283
|
+
conversation=conversation,
|
|
240
284
|
llm_response_id=llm_response.id,
|
|
241
285
|
on_event=on_event,
|
|
242
286
|
security_analyzer=state.security_analyzer,
|
|
@@ -275,6 +319,14 @@ class Agent(AgentBase):
|
|
|
275
319
|
llm_message=message,
|
|
276
320
|
llm_response_id=llm_response.id,
|
|
277
321
|
)
|
|
322
|
+
# Run critic evaluation if configured for finish_and_message mode
|
|
323
|
+
if self.critic is not None and self.critic.mode == "finish_and_message":
|
|
324
|
+
critic_result = self._evaluate_with_critic(conversation, msg_event)
|
|
325
|
+
if critic_result is not None:
|
|
326
|
+
# Create new event with critic result
|
|
327
|
+
msg_event = msg_event.model_copy(
|
|
328
|
+
update={"critic_result": critic_result}
|
|
329
|
+
)
|
|
278
330
|
on_event(msg_event)
|
|
279
331
|
|
|
280
332
|
# Emit VLLM token ids if enabled
|
|
@@ -389,6 +441,7 @@ class Agent(AgentBase):
|
|
|
389
441
|
def _get_action_event(
|
|
390
442
|
self,
|
|
391
443
|
tool_call: MessageToolCall,
|
|
444
|
+
conversation: LocalConversation,
|
|
392
445
|
llm_response_id: str,
|
|
393
446
|
on_event: ConversationCallbackType,
|
|
394
447
|
security_analyzer: analyzer.SecurityAnalyzerBase | None = None,
|
|
@@ -477,6 +530,7 @@ class Agent(AgentBase):
|
|
|
477
530
|
on_event(event)
|
|
478
531
|
return
|
|
479
532
|
|
|
533
|
+
# Create initial action event
|
|
480
534
|
action_event = ActionEvent(
|
|
481
535
|
action=action,
|
|
482
536
|
thought=thought or [],
|
|
@@ -490,6 +544,16 @@ class Agent(AgentBase):
|
|
|
490
544
|
security_risk=security_risk,
|
|
491
545
|
summary=summary,
|
|
492
546
|
)
|
|
547
|
+
|
|
548
|
+
# Run critic evaluation if configured
|
|
549
|
+
if self._should_evaluate_with_critic(action):
|
|
550
|
+
critic_result = self._evaluate_with_critic(conversation, action_event)
|
|
551
|
+
if critic_result is not None:
|
|
552
|
+
# Create new event with critic result
|
|
553
|
+
action_event = action_event.model_copy(
|
|
554
|
+
update={"critic_result": critic_result}
|
|
555
|
+
)
|
|
556
|
+
|
|
493
557
|
on_event(action_event)
|
|
494
558
|
return action_event
|
|
495
559
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
import re
|
|
3
5
|
import sys
|
|
@@ -16,6 +18,7 @@ from pydantic import (
|
|
|
16
18
|
from openhands.sdk.context.agent_context import AgentContext
|
|
17
19
|
from openhands.sdk.context.condenser import CondenserBase
|
|
18
20
|
from openhands.sdk.context.prompts.prompt import render_template
|
|
21
|
+
from openhands.sdk.critic.base import CriticBase
|
|
19
22
|
from openhands.sdk.llm import LLM
|
|
20
23
|
from openhands.sdk.llm.utils.model_prompt_spec import get_model_prompt_spec
|
|
21
24
|
from openhands.sdk.logger import get_logger
|
|
@@ -37,7 +40,6 @@ if TYPE_CHECKING:
|
|
|
37
40
|
ConversationTokenCallbackType,
|
|
38
41
|
)
|
|
39
42
|
|
|
40
|
-
|
|
41
43
|
logger = get_logger(__name__)
|
|
42
44
|
|
|
43
45
|
|
|
@@ -174,6 +176,16 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
174
176
|
],
|
|
175
177
|
)
|
|
176
178
|
|
|
179
|
+
critic: CriticBase | None = Field(
|
|
180
|
+
default=None,
|
|
181
|
+
description=(
|
|
182
|
+
"EXPERIMENTAL: Optional critic to evaluate agent actions and messages "
|
|
183
|
+
"in real-time. API and behavior may change without notice. "
|
|
184
|
+
"May impact performance, especially in 'all_actions' mode."
|
|
185
|
+
),
|
|
186
|
+
examples=[{"kind": "AgentFinishedCritic"}],
|
|
187
|
+
)
|
|
188
|
+
|
|
177
189
|
# Runtime materialized tools; private and non-serializable
|
|
178
190
|
_tools: dict[str, ToolDefinition] = PrivateAttr(default_factory=dict)
|
|
179
191
|
_initialized: bool = PrivateAttr(default=False)
|
|
@@ -226,8 +238,8 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
226
238
|
|
|
227
239
|
def init_state(
|
|
228
240
|
self,
|
|
229
|
-
state:
|
|
230
|
-
on_event:
|
|
241
|
+
state: ConversationState,
|
|
242
|
+
on_event: ConversationCallbackType, # noqa: ARG002
|
|
231
243
|
) -> None:
|
|
232
244
|
"""Initialize the empty conversation state to prepare the agent for user
|
|
233
245
|
messages.
|
|
@@ -238,7 +250,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
238
250
|
"""
|
|
239
251
|
self._initialize(state)
|
|
240
252
|
|
|
241
|
-
def _initialize(self, state:
|
|
253
|
+
def _initialize(self, state: ConversationState):
|
|
242
254
|
"""Create an AgentBase instance from an AgentSpec."""
|
|
243
255
|
|
|
244
256
|
if self._initialized:
|
|
@@ -310,9 +322,9 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
310
322
|
@abstractmethod
|
|
311
323
|
def step(
|
|
312
324
|
self,
|
|
313
|
-
conversation:
|
|
314
|
-
on_event:
|
|
315
|
-
on_token:
|
|
325
|
+
conversation: LocalConversation,
|
|
326
|
+
on_event: ConversationCallbackType,
|
|
327
|
+
on_token: ConversationTokenCallbackType | None = None,
|
|
316
328
|
) -> None:
|
|
317
329
|
"""Taking a step in the conversation.
|
|
318
330
|
|
|
@@ -332,9 +344,9 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
332
344
|
|
|
333
345
|
def verify(
|
|
334
346
|
self,
|
|
335
|
-
persisted:
|
|
336
|
-
events:
|
|
337
|
-
) ->
|
|
347
|
+
persisted: AgentBase,
|
|
348
|
+
events: Sequence[Any] | None = None,
|
|
349
|
+
) -> AgentBase:
|
|
338
350
|
"""Verify that we can resume this agent from persisted state.
|
|
339
351
|
|
|
340
352
|
This PR's goal is to *not* reconcile configuration between persisted and
|
|
@@ -384,6 +396,13 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
384
396
|
if isinstance(event, ActionEvent) and event.tool_name
|
|
385
397
|
}
|
|
386
398
|
|
|
399
|
+
# Add builtin tool names from include_default_tools
|
|
400
|
+
# These are runtime names like 'finish', 'think'
|
|
401
|
+
for tool_class_name in self.include_default_tools:
|
|
402
|
+
tool_class = BUILT_IN_TOOL_CLASSES.get(tool_class_name)
|
|
403
|
+
if tool_class is not None:
|
|
404
|
+
runtime_names.add(tool_class.name)
|
|
405
|
+
|
|
387
406
|
# Only require tools that were actually used in history.
|
|
388
407
|
missing_used_tools = used_tools - runtime_names
|
|
389
408
|
if missing_used_tools:
|
|
@@ -43,6 +43,7 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
|
|
43
43
|
* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
|
|
44
44
|
* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
|
|
45
45
|
* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification.
|
|
46
|
+
* When running git commands that may produce paged output (e.g., `git diff`, `git log`, `git show`), use `git --no-pager <command>` or set `GIT_PAGER=cat` to prevent the command from getting stuck waiting for interactive input.
|
|
46
47
|
</VERSION_CONTROL>
|
|
47
48
|
|
|
48
49
|
<PULL_REQUESTS>
|
|
@@ -42,7 +42,11 @@ class LLMSummarizingCondenser(RollingCondenser):
|
|
|
42
42
|
llm: LLM
|
|
43
43
|
max_size: int = Field(default=240, gt=0)
|
|
44
44
|
max_tokens: int | None = None
|
|
45
|
+
|
|
45
46
|
keep_first: int = Field(default=2, ge=0)
|
|
47
|
+
"""Minimum number of events to preserve at the start of the view. The first
|
|
48
|
+
`keep_first` events in the conversation will never be condensed or summarized.
|
|
49
|
+
"""
|
|
46
50
|
|
|
47
51
|
@model_validator(mode="after")
|
|
48
52
|
def validate_keep_first_vs_max_size(self):
|
|
@@ -236,13 +240,11 @@ class LLMSummarizingCondenser(RollingCondenser):
|
|
|
236
240
|
# Calculate naive forgetting end (without considering atomic boundaries)
|
|
237
241
|
naive_end = len(view) - events_from_tail
|
|
238
242
|
|
|
239
|
-
# Find actual forgetting_start: smallest manipulation index
|
|
240
|
-
forgetting_start = view.find_next_manipulation_index(
|
|
241
|
-
self.keep_first, strict=True
|
|
242
|
-
)
|
|
243
|
+
# Find actual forgetting_start: smallest manipulation index >= keep_first
|
|
244
|
+
forgetting_start = view.find_next_manipulation_index(self.keep_first)
|
|
243
245
|
|
|
244
246
|
# Find actual forgetting_end: smallest manipulation index >= naive_end
|
|
245
|
-
forgetting_end = view.find_next_manipulation_index(naive_end
|
|
247
|
+
forgetting_end = view.find_next_manipulation_index(naive_end)
|
|
246
248
|
|
|
247
249
|
# Extract events to forget using boundary-aware indices
|
|
248
250
|
forgotten_events = view[forgetting_start:forgetting_end]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import re
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Annotated, ClassVar, Union
|
|
4
|
+
from typing import Annotated, ClassVar, Literal, Union
|
|
5
5
|
from xml.sax.saxutils import escape as xml_escape
|
|
6
6
|
|
|
7
7
|
import frontmatter
|
|
@@ -37,6 +37,22 @@ logger = get_logger(__name__)
|
|
|
37
37
|
THIRD_PARTY_SKILL_MAX_CHARS = 10_000
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class SkillInfo(BaseModel):
|
|
41
|
+
"""Lightweight representation of a skill's essential information.
|
|
42
|
+
|
|
43
|
+
This class provides a standardized, serializable format for skill metadata
|
|
44
|
+
that can be used across different components of the system.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
type: Literal["repo", "knowledge", "agentskills"]
|
|
49
|
+
content: str
|
|
50
|
+
triggers: list[str] = Field(default_factory=list)
|
|
51
|
+
source: str | None = None
|
|
52
|
+
description: str | None = None
|
|
53
|
+
is_agentskills_format: bool = False
|
|
54
|
+
|
|
55
|
+
|
|
40
56
|
class SkillResources(BaseModel):
|
|
41
57
|
"""Resource directories for a skill (AgentSkills standard).
|
|
42
58
|
|
|
@@ -560,6 +576,48 @@ class Skill(BaseModel):
|
|
|
560
576
|
logger.debug(f"This skill requires user input: {variables}")
|
|
561
577
|
return len(variables) > 0
|
|
562
578
|
|
|
579
|
+
def get_skill_type(self) -> Literal["repo", "knowledge", "agentskills"]:
|
|
580
|
+
"""Determine the type of this skill.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
"agentskills" for AgentSkills format, "repo" for always-active skills,
|
|
584
|
+
"knowledge" for trigger-based skills.
|
|
585
|
+
"""
|
|
586
|
+
if self.is_agentskills_format:
|
|
587
|
+
return "agentskills"
|
|
588
|
+
elif self.trigger is None:
|
|
589
|
+
return "repo"
|
|
590
|
+
else:
|
|
591
|
+
return "knowledge"
|
|
592
|
+
|
|
593
|
+
def get_triggers(self) -> list[str]:
|
|
594
|
+
"""Extract trigger keywords from this skill.
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
List of trigger strings, or empty list if no triggers.
|
|
598
|
+
"""
|
|
599
|
+
if isinstance(self.trigger, KeywordTrigger):
|
|
600
|
+
return self.trigger.keywords
|
|
601
|
+
elif isinstance(self.trigger, TaskTrigger):
|
|
602
|
+
return self.trigger.triggers
|
|
603
|
+
return []
|
|
604
|
+
|
|
605
|
+
def to_skill_info(self) -> SkillInfo:
|
|
606
|
+
"""Convert this skill to a SkillInfo.
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
SkillInfo containing the skill's essential information.
|
|
610
|
+
"""
|
|
611
|
+
return SkillInfo(
|
|
612
|
+
name=self.name,
|
|
613
|
+
type=self.get_skill_type(),
|
|
614
|
+
content=self.content,
|
|
615
|
+
triggers=self.get_triggers(),
|
|
616
|
+
source=self.source,
|
|
617
|
+
description=self.description,
|
|
618
|
+
is_agentskills_format=self.is_agentskills_format,
|
|
619
|
+
)
|
|
620
|
+
|
|
563
621
|
|
|
564
622
|
def load_skills_from_dir(
|
|
565
623
|
skill_dir: str | Path,
|
|
@@ -5,14 +5,13 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
-
import shutil
|
|
9
|
-
import subprocess
|
|
10
8
|
from pathlib import Path
|
|
11
9
|
from typing import TYPE_CHECKING
|
|
12
10
|
|
|
13
11
|
from fastmcp.mcp_config import MCPConfig
|
|
14
12
|
|
|
15
13
|
from openhands.sdk.context.skills.exceptions import SkillValidationError
|
|
14
|
+
from openhands.sdk.git.cached_repo import try_cached_clone_or_update
|
|
16
15
|
from openhands.sdk.logger import get_logger
|
|
17
16
|
|
|
18
17
|
|
|
@@ -316,77 +315,19 @@ def update_skills_repository(
|
|
|
316
315
|
) -> Path | None:
|
|
317
316
|
"""Clone or update the local skills repository.
|
|
318
317
|
|
|
318
|
+
Uses the shared git caching infrastructure from openhands.sdk.git.cached_repo.
|
|
319
|
+
When updating, performs: fetch -> checkout ref -> reset --hard to origin/ref.
|
|
320
|
+
|
|
319
321
|
Args:
|
|
320
322
|
repo_url: URL of the skills repository.
|
|
321
|
-
branch: Branch name to
|
|
323
|
+
branch: Branch name to checkout and track.
|
|
322
324
|
cache_dir: Directory where the repository should be cached.
|
|
323
325
|
|
|
324
326
|
Returns:
|
|
325
327
|
Path to the local repository if successful, None otherwise.
|
|
326
328
|
"""
|
|
327
329
|
repo_path = cache_dir / "public-skills"
|
|
328
|
-
|
|
329
|
-
try:
|
|
330
|
-
if repo_path.exists() and (repo_path / ".git").exists():
|
|
331
|
-
logger.debug(f"Updating skills repository at {repo_path}")
|
|
332
|
-
try:
|
|
333
|
-
subprocess.run(
|
|
334
|
-
["git", "fetch", "origin"],
|
|
335
|
-
cwd=repo_path,
|
|
336
|
-
check=True,
|
|
337
|
-
capture_output=True,
|
|
338
|
-
timeout=30,
|
|
339
|
-
)
|
|
340
|
-
subprocess.run(
|
|
341
|
-
["git", "reset", "--hard", f"origin/{branch}"],
|
|
342
|
-
cwd=repo_path,
|
|
343
|
-
check=True,
|
|
344
|
-
capture_output=True,
|
|
345
|
-
timeout=10,
|
|
346
|
-
)
|
|
347
|
-
logger.debug("Skills repository updated successfully")
|
|
348
|
-
except subprocess.TimeoutExpired:
|
|
349
|
-
logger.warning("Git pull timed out, using existing cached repository")
|
|
350
|
-
except subprocess.CalledProcessError as e:
|
|
351
|
-
logger.warning(
|
|
352
|
-
f"Failed to update repository: {e.stderr.decode()}, "
|
|
353
|
-
f"using existing cached version"
|
|
354
|
-
)
|
|
355
|
-
else:
|
|
356
|
-
logger.info(f"Cloning public skills repository from {repo_url}")
|
|
357
|
-
if repo_path.exists():
|
|
358
|
-
shutil.rmtree(repo_path)
|
|
359
|
-
|
|
360
|
-
subprocess.run(
|
|
361
|
-
[
|
|
362
|
-
"git",
|
|
363
|
-
"clone",
|
|
364
|
-
"--depth",
|
|
365
|
-
"1",
|
|
366
|
-
"--branch",
|
|
367
|
-
branch,
|
|
368
|
-
repo_url,
|
|
369
|
-
str(repo_path),
|
|
370
|
-
],
|
|
371
|
-
check=True,
|
|
372
|
-
capture_output=True,
|
|
373
|
-
timeout=60,
|
|
374
|
-
)
|
|
375
|
-
logger.debug(f"Skills repository cloned to {repo_path}")
|
|
376
|
-
|
|
377
|
-
return repo_path
|
|
378
|
-
|
|
379
|
-
except subprocess.TimeoutExpired:
|
|
380
|
-
logger.warning(f"Git operation timed out for {repo_url}")
|
|
381
|
-
return None
|
|
382
|
-
except subprocess.CalledProcessError as e:
|
|
383
|
-
logger.warning(
|
|
384
|
-
f"Failed to clone/update repository {repo_url}: {e.stderr.decode()}"
|
|
385
|
-
)
|
|
386
|
-
return None
|
|
387
|
-
except Exception as e:
|
|
388
|
-
logger.warning(f"Error managing skills repository: {str(e)}")
|
|
389
|
-
return None
|
|
330
|
+
return try_cached_clone_or_update(repo_url, repo_path, ref=branch, update=True)
|
|
390
331
|
|
|
391
332
|
|
|
392
333
|
def discover_skill_resources(skill_dir: Path) -> SkillResources:
|
|
@@ -416,27 +416,22 @@ class View(BaseModel):
|
|
|
416
416
|
else:
|
|
417
417
|
return True
|
|
418
418
|
|
|
419
|
-
def find_next_manipulation_index(self, threshold: int
|
|
420
|
-
"""Find the smallest manipulation index greater than
|
|
419
|
+
def find_next_manipulation_index(self, threshold: int) -> int:
|
|
420
|
+
"""Find the smallest manipulation index greater than or equal to a threshold.
|
|
421
421
|
|
|
422
422
|
This is a helper method for condensation logic that needs to find safe
|
|
423
423
|
boundaries for forgetting events. Uses the cached manipulation_indices property.
|
|
424
424
|
|
|
425
425
|
Args:
|
|
426
426
|
threshold: The threshold value to compare against
|
|
427
|
-
strict: If True, finds index > threshold. If False, finds index >= threshold
|
|
428
427
|
|
|
429
428
|
Returns:
|
|
430
|
-
The smallest manipulation index
|
|
431
|
-
|
|
429
|
+
The smallest manipulation index >= threshold, or the threshold itself
|
|
430
|
+
if no such index exists
|
|
432
431
|
"""
|
|
433
432
|
for idx in self.manipulation_indices:
|
|
434
|
-
if
|
|
435
|
-
|
|
436
|
-
return idx
|
|
437
|
-
else:
|
|
438
|
-
if idx >= threshold:
|
|
439
|
-
return idx
|
|
433
|
+
if idx >= threshold:
|
|
434
|
+
return idx
|
|
440
435
|
return threshold
|
|
441
436
|
|
|
442
437
|
@staticmethod
|
|
@@ -162,6 +162,11 @@ class BaseConversation(ABC):
|
|
|
162
162
|
"""Set the confirmation policy for the conversation."""
|
|
163
163
|
...
|
|
164
164
|
|
|
165
|
+
@abstractmethod
|
|
166
|
+
def set_security_analyzer(self, analyzer: SecurityAnalyzerBase | None) -> None:
|
|
167
|
+
"""Set the security analyzer for the conversation."""
|
|
168
|
+
...
|
|
169
|
+
|
|
165
170
|
@property
|
|
166
171
|
def confirmation_policy_active(self) -> bool:
|
|
167
172
|
return not isinstance(self.state.confirmation_policy, NeverConfirm)
|
|
@@ -16,17 +16,34 @@ from openhands.sdk.logger import get_logger
|
|
|
16
16
|
|
|
17
17
|
logger = get_logger(__name__)
|
|
18
18
|
|
|
19
|
+
LOCK_FILE_NAME = ".eventlog.lock"
|
|
20
|
+
LOCK_TIMEOUT_SECONDS = 30
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
class EventLog(EventsListBase):
|
|
24
|
+
"""Persistent event log with locking for concurrent writes.
|
|
25
|
+
|
|
26
|
+
This class provides thread-safe and process-safe event storage using
|
|
27
|
+
the FileStore's locking mechanism. Events are persisted to disk and
|
|
28
|
+
can be accessed by index or event ID.
|
|
29
|
+
|
|
30
|
+
Note:
|
|
31
|
+
For LocalFileStore, file locking via flock() does NOT work reliably
|
|
32
|
+
on NFS mounts or network filesystems. Users deploying with shared
|
|
33
|
+
storage should use alternative coordination mechanisms.
|
|
34
|
+
"""
|
|
35
|
+
|
|
21
36
|
_fs: FileStore
|
|
22
37
|
_dir: str
|
|
23
38
|
_length: int
|
|
39
|
+
_lock_path: str
|
|
24
40
|
|
|
25
41
|
def __init__(self, fs: FileStore, dir_path: str = EVENTS_DIR) -> None:
|
|
26
42
|
self._fs = fs
|
|
27
43
|
self._dir = dir_path
|
|
28
44
|
self._id_to_idx: dict[EventID, int] = {}
|
|
29
45
|
self._idx_to_id: dict[int, EventID] = {}
|
|
46
|
+
self._lock_path = f"{dir_path}/{LOCK_FILE_NAME}"
|
|
30
47
|
self._length = self._scan_and_build_index()
|
|
31
48
|
|
|
32
49
|
def get_index(self, event_id: EventID) -> int:
|
|
@@ -54,7 +71,6 @@ class EventLog(EventsListBase):
|
|
|
54
71
|
if isinstance(idx, slice):
|
|
55
72
|
start, stop, step = idx.indices(self._length)
|
|
56
73
|
return [self._get_single_item(i) for i in range(start, stop, step)]
|
|
57
|
-
# idx is int-like (SupportsIndex)
|
|
58
74
|
return self._get_single_item(idx)
|
|
59
75
|
|
|
60
76
|
def _get_single_item(self, idx: SupportsIndex) -> Event:
|
|
@@ -75,26 +91,82 @@ class EventLog(EventsListBase):
|
|
|
75
91
|
continue
|
|
76
92
|
evt = Event.model_validate_json(txt)
|
|
77
93
|
evt_id = evt.id
|
|
78
|
-
# only backfill mapping if missing
|
|
79
94
|
if i not in self._idx_to_id:
|
|
80
95
|
self._idx_to_id[i] = evt_id
|
|
81
96
|
self._id_to_idx.setdefault(evt_id, i)
|
|
82
97
|
yield evt
|
|
83
98
|
|
|
84
99
|
def append(self, event: Event) -> None:
|
|
100
|
+
"""Append an event with locking for thread/process safety.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
TimeoutError: If the lock cannot be acquired within LOCK_TIMEOUT_SECONDS.
|
|
104
|
+
ValueError: If an event with the same ID already exists.
|
|
105
|
+
"""
|
|
85
106
|
evt_id = event.id
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
with self._fs.lock(self._lock_path, timeout=LOCK_TIMEOUT_SECONDS):
|
|
110
|
+
# Sync with disk in case another process wrote while we waited
|
|
111
|
+
disk_length = self._count_events_on_disk()
|
|
112
|
+
if disk_length > self._length:
|
|
113
|
+
self._sync_from_disk(disk_length)
|
|
114
|
+
|
|
115
|
+
if evt_id in self._id_to_idx:
|
|
116
|
+
existing_idx = self._id_to_idx[evt_id]
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Event with ID '{evt_id}' already exists at index "
|
|
119
|
+
f"{existing_idx}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
target_path = self._path(self._length, event_id=evt_id)
|
|
123
|
+
self._fs.write(target_path, event.model_dump_json(exclude_none=True))
|
|
124
|
+
self._idx_to_id[self._length] = evt_id
|
|
125
|
+
self._id_to_idx[evt_id] = self._length
|
|
126
|
+
self._length += 1
|
|
127
|
+
except TimeoutError:
|
|
128
|
+
logger.error(
|
|
129
|
+
f"Failed to acquire EventLog lock within {LOCK_TIMEOUT_SECONDS}s "
|
|
130
|
+
f"for event {evt_id}"
|
|
91
131
|
)
|
|
132
|
+
raise
|
|
92
133
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
134
|
+
def _count_events_on_disk(self) -> int:
|
|
135
|
+
"""Count event files on disk."""
|
|
136
|
+
try:
|
|
137
|
+
paths = self._fs.list(self._dir)
|
|
138
|
+
except FileNotFoundError:
|
|
139
|
+
# Directory doesn't exist yet - expected for new event logs
|
|
140
|
+
return 0
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning("Error listing event directory %s: %s", self._dir, e)
|
|
143
|
+
return 0
|
|
144
|
+
return sum(
|
|
145
|
+
1
|
|
146
|
+
for p in paths
|
|
147
|
+
if p.rsplit("/", 1)[-1].startswith("event-") and p.endswith(".json")
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def _sync_from_disk(self, disk_length: int) -> None:
|
|
151
|
+
"""Sync state for events written by other processes.
|
|
152
|
+
|
|
153
|
+
Preserves existing index mappings and only scans new events.
|
|
154
|
+
"""
|
|
155
|
+
# Preserve existing mappings
|
|
156
|
+
existing_idx_to_id = dict(self._idx_to_id)
|
|
157
|
+
|
|
158
|
+
# Re-scan to pick up new events
|
|
159
|
+
scanned_length = self._scan_and_build_index()
|
|
160
|
+
|
|
161
|
+
# Restore any mappings that were lost (e.g., for non-UUID event IDs)
|
|
162
|
+
for idx, evt_id in existing_idx_to_id.items():
|
|
163
|
+
if idx not in self._idx_to_id:
|
|
164
|
+
self._idx_to_id[idx] = evt_id
|
|
165
|
+
if evt_id not in self._id_to_idx:
|
|
166
|
+
self._id_to_idx[evt_id] = idx
|
|
167
|
+
|
|
168
|
+
# Use the higher of scanned length or disk_length
|
|
169
|
+
self._length = max(scanned_length, disk_length)
|
|
98
170
|
|
|
99
171
|
def __len__(self) -> int:
|
|
100
172
|
return self._length
|
{openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/impl/local_conversation.py
RENAMED
|
@@ -40,6 +40,7 @@ from openhands.sdk.security.analyzer import SecurityAnalyzerBase
|
|
|
40
40
|
from openhands.sdk.security.confirmation_policy import (
|
|
41
41
|
ConfirmationPolicyBase,
|
|
42
42
|
)
|
|
43
|
+
from openhands.sdk.utils.cipher import Cipher
|
|
43
44
|
from openhands.sdk.workspace import LocalWorkspace
|
|
44
45
|
|
|
45
46
|
|
|
@@ -77,6 +78,7 @@ class LocalConversation(BaseConversation):
|
|
|
77
78
|
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
|
|
78
79
|
) = DefaultConversationVisualizer,
|
|
79
80
|
secrets: Mapping[str, SecretValue] | None = None,
|
|
81
|
+
cipher: Cipher | None = None,
|
|
80
82
|
**_: object,
|
|
81
83
|
):
|
|
82
84
|
"""Initialize the conversation.
|
|
@@ -105,6 +107,10 @@ class LocalConversation(BaseConversation):
|
|
|
105
107
|
a dict with keys: 'action_observation', 'action_error',
|
|
106
108
|
'monologue', 'alternating_pattern'. Values are integers
|
|
107
109
|
representing the number of repetitions before triggering.
|
|
110
|
+
cipher: Optional cipher for encrypting/decrypting secrets in persisted
|
|
111
|
+
state. If provided, secrets are encrypted when saving and
|
|
112
|
+
decrypted when loading. If not provided, secrets are redacted
|
|
113
|
+
(lost) on serialization.
|
|
108
114
|
"""
|
|
109
115
|
super().__init__() # Initialize with span tracking
|
|
110
116
|
# Mark cleanup as initiated as early as possible to avoid races or partially
|
|
@@ -134,6 +140,7 @@ class LocalConversation(BaseConversation):
|
|
|
134
140
|
else None,
|
|
135
141
|
max_iterations=max_iteration_per_run,
|
|
136
142
|
stuck_detection=stuck_detection,
|
|
143
|
+
cipher=cipher,
|
|
137
144
|
)
|
|
138
145
|
|
|
139
146
|
# Default callback: persist every event to state
|