openhands-sdk 1.8.1__tar.gz → 1.8.2__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.8.2}/PKG-INFO +2 -1
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/base.py +7 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +7 -5
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/view.py +6 -11
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/event_store.py +84 -12
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/impl/local_conversation.py +7 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/state.py +25 -2
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/conversation_error.py +12 -0
- openhands_sdk-1.8.2/openhands/sdk/io/base.py +100 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/io/local.py +25 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/io/memory.py +34 -1
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/llm.py +6 -2
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/utils/telemetry.py +41 -2
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/secret/secrets.py +19 -4
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands_sdk.egg-info/PKG-INFO +2 -1
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands_sdk.egg-info/requires.txt +1 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/pyproject.toml +3 -2
- openhands_sdk-1.8.1/openhands/sdk/io/base.py +0 -48
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/agent.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/agent/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/agent_context.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/condenser/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/condenser/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/condenser/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/prompts/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/prompts/prompt.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/skills/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/skills/exceptions.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/skills/skill.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/skills/trigger.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/skills/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/context/skills/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/conversation.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/conversation_stats.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/events_list_base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/exceptions.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/fifo_lock.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/impl/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/impl/remote_conversation.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/persistence_const.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/response_utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/secret_registry.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/serialization_diff.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/stuck_detector.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/title_utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/visualizer/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/conversation/visualizer/default.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/critic/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/critic/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/critic/impl/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/critic/impl/agent_finished.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/critic/impl/empty_patch.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/critic/impl/pass_critic.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/condenser.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/conversation_state.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/llm_completion_log.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/llm_convertible/action.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/llm_convertible/message.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/llm_convertible/observation.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/llm_convertible/system.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/token.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/event/user_action.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/git/exceptions.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/git/git_changes.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/git/git_diff.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/git/models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/git/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/hooks/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/hooks/config.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/hooks/conversation_hooks.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/hooks/executor.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/hooks/manager.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/hooks/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/io/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/io/cache.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/exceptions/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/exceptions/classifier.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/exceptions/mapping.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/exceptions/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/llm_registry.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/llm_response.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/message.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/options/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/options/chat_options.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/options/common.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/options/responses_options.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/router/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/router/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/router/impl/random.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/streaming.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/utils/metrics.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/utils/model_features.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/utils/model_info.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/utils/unverified_models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/llm/utils/verified_models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/logger/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/logger/logger.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/logger/rolling.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/mcp/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/mcp/client.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/mcp/definition.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/mcp/exceptions.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/mcp/tool.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/mcp/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/observability/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/observability/laminar.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/observability/utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/plugin/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/plugin/plugin.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/plugin/types.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/py.typed +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/secret/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/security/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/security/analyzer.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/security/confirmation_policy.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/security/llm_analyzer.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/security/risk.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/tool/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/tool/builtins/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/tool/builtins/finish.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/tool/builtins/think.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/tool/registry.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/tool/schema.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/tool/spec.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/tool/tool.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/async_executor.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/async_utils.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/cipher.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/command.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/deprecation.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/github.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/json.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/paging.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/pydantic_diff.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/pydantic_secrets.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/truncate.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/utils/visualize.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/workspace/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/workspace/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/workspace/local.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/workspace/models.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/workspace/remote/__init__.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/workspace/remote/base.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands/sdk/workspace/workspace.py +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands_sdk.egg-info/SOURCES.txt +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands_sdk.egg-info/dependency_links.txt +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/openhands_sdk.egg-info/top_level.txt +0 -0
- {openhands_sdk-1.8.1 → openhands_sdk-1.8.2}/setup.cfg +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands-sdk
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.2
|
|
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
|
+
Requires-Dist: filelock>=3.20.1
|
|
8
9
|
Requires-Dist: httpx>=0.27.0
|
|
9
10
|
Requires-Dist: litellm>=1.80.10
|
|
10
11
|
Requires-Dist: pydantic>=2.12.5
|
|
@@ -384,6 +384,13 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
384
384
|
if isinstance(event, ActionEvent) and event.tool_name
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
# Add builtin tool names from include_default_tools
|
|
388
|
+
# These are runtime names like 'finish', 'think'
|
|
389
|
+
for tool_class_name in self.include_default_tools:
|
|
390
|
+
tool_class = BUILT_IN_TOOL_CLASSES.get(tool_class_name)
|
|
391
|
+
if tool_class is not None:
|
|
392
|
+
runtime_names.add(tool_class.name)
|
|
393
|
+
|
|
387
394
|
# Only require tools that were actually used in history.
|
|
388
395
|
missing_used_tools = used_tools - runtime_names
|
|
389
396
|
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]
|
|
@@ -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
|
|
@@ -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.8.2}/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
|
|
@@ -23,6 +23,7 @@ from openhands.sdk.security.confirmation_policy import (
|
|
|
23
23
|
ConfirmationPolicyBase,
|
|
24
24
|
NeverConfirm,
|
|
25
25
|
)
|
|
26
|
+
from openhands.sdk.utils.cipher import Cipher
|
|
26
27
|
from openhands.sdk.utils.models import OpenHandsModel
|
|
27
28
|
from openhands.sdk.workspace.base import BaseWorkspace
|
|
28
29
|
|
|
@@ -124,6 +125,7 @@ class ConversationState(OpenHandsModel):
|
|
|
124
125
|
# ===== Private attrs (NOT Fields) =====
|
|
125
126
|
_fs: FileStore = PrivateAttr() # filestore for persistence
|
|
126
127
|
_events: EventLog = PrivateAttr() # now the storage for events
|
|
128
|
+
_cipher: Cipher | None = PrivateAttr(default=None) # cipher for secret encryption
|
|
127
129
|
_autosave_enabled: bool = PrivateAttr(
|
|
128
130
|
default=False
|
|
129
131
|
) # to avoid recursion during init
|
|
@@ -166,8 +168,20 @@ class ConversationState(OpenHandsModel):
|
|
|
166
168
|
def _save_base_state(self, fs: FileStore) -> None:
|
|
167
169
|
"""
|
|
168
170
|
Persist base state snapshot (no events; events are file-backed).
|
|
171
|
+
|
|
172
|
+
If a cipher is configured, secrets will be encrypted. Otherwise, they
|
|
173
|
+
will be redacted (serialized as '**********').
|
|
169
174
|
"""
|
|
170
|
-
|
|
175
|
+
context = {"cipher": self._cipher} if self._cipher else None
|
|
176
|
+
# Warn if secrets exist but no cipher is configured
|
|
177
|
+
if not self._cipher and self.secret_registry.secret_sources:
|
|
178
|
+
logger.warning(
|
|
179
|
+
f"Saving conversation state without cipher - "
|
|
180
|
+
f"{len(self.secret_registry.secret_sources)} secret(s) will be "
|
|
181
|
+
"redacted and lost on restore. Consider providing a cipher to "
|
|
182
|
+
"preserve secrets."
|
|
183
|
+
)
|
|
184
|
+
payload = self.model_dump_json(exclude_none=True, context=context)
|
|
171
185
|
fs.write(BASE_STATE, payload)
|
|
172
186
|
|
|
173
187
|
# ===== Factory: open-or-create (no load/save methods needed) =====
|
|
@@ -180,6 +194,7 @@ class ConversationState(OpenHandsModel):
|
|
|
180
194
|
persistence_dir: str | None = None,
|
|
181
195
|
max_iterations: int = 500,
|
|
182
196
|
stuck_detection: bool = True,
|
|
197
|
+
cipher: Cipher | None = None,
|
|
183
198
|
) -> "ConversationState":
|
|
184
199
|
"""Create a new conversation state or resume from persistence.
|
|
185
200
|
|
|
@@ -203,6 +218,10 @@ class ConversationState(OpenHandsModel):
|
|
|
203
218
|
persistence_dir: Directory for persisting state and events
|
|
204
219
|
max_iterations: Maximum iterations per run
|
|
205
220
|
stuck_detection: Whether to enable stuck detection
|
|
221
|
+
cipher: Optional cipher for encrypting/decrypting secrets in
|
|
222
|
+
persisted state. If provided, secrets are encrypted when
|
|
223
|
+
saving and decrypted when loading. If not provided, secrets
|
|
224
|
+
are redacted (lost) on serialization.
|
|
206
225
|
|
|
207
226
|
Returns:
|
|
208
227
|
ConversationState ready for use
|
|
@@ -224,7 +243,9 @@ class ConversationState(OpenHandsModel):
|
|
|
224
243
|
|
|
225
244
|
# ---- Resume path ----
|
|
226
245
|
if base_text:
|
|
227
|
-
|
|
246
|
+
# Use cipher context for decrypting secrets if provided
|
|
247
|
+
context = {"cipher": cipher} if cipher else None
|
|
248
|
+
state = cls.model_validate(json.loads(base_text), context=context)
|
|
228
249
|
|
|
229
250
|
# Restore the conversation with the same id
|
|
230
251
|
if state.id != id:
|
|
@@ -236,6 +257,7 @@ class ConversationState(OpenHandsModel):
|
|
|
236
257
|
# Attach event log early so we can read history for tool verification
|
|
237
258
|
state._fs = file_store
|
|
238
259
|
state._events = EventLog(file_store, dir_path=EVENTS_DIR)
|
|
260
|
+
state._cipher = cipher
|
|
239
261
|
|
|
240
262
|
# Verify compatibility (agent class + tools)
|
|
241
263
|
agent.verify(state.agent, events=state._events)
|
|
@@ -272,6 +294,7 @@ class ConversationState(OpenHandsModel):
|
|
|
272
294
|
)
|
|
273
295
|
state._fs = file_store
|
|
274
296
|
state._events = EventLog(file_store, dir_path=EVENTS_DIR)
|
|
297
|
+
state._cipher = cipher
|
|
275
298
|
state.stats = ConversationStats()
|
|
276
299
|
|
|
277
300
|
state._save_base_state(file_store) # initial snapshot
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from pydantic import Field
|
|
2
|
+
from rich.text import Text
|
|
2
3
|
|
|
3
4
|
from openhands.sdk.event.base import Event
|
|
4
5
|
|
|
@@ -23,3 +24,14 @@ class ConversationErrorEvent(Event):
|
|
|
23
24
|
|
|
24
25
|
code: str = Field(description="Code for the error - typically a type")
|
|
25
26
|
detail: str = Field(description="Details about the error")
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def visualize(self) -> Text:
|
|
30
|
+
"""Return Rich Text representation of this conversation error event."""
|
|
31
|
+
content = Text()
|
|
32
|
+
content.append("Conversation Error\n", style="bold")
|
|
33
|
+
content.append("Code: ", style="bold")
|
|
34
|
+
content.append(self.code)
|
|
35
|
+
content.append("\n\nDetail:\n", style="bold")
|
|
36
|
+
content.append(self.detail)
|
|
37
|
+
return content
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FileStore(ABC):
|
|
7
|
+
"""Abstract base class for file storage operations.
|
|
8
|
+
|
|
9
|
+
This class defines the interface for file storage backends that can
|
|
10
|
+
handle basic file operations like reading, writing, listing, and deleting files.
|
|
11
|
+
|
|
12
|
+
Implementations should provide a locking mechanism via the `lock()` context
|
|
13
|
+
manager for thread/process-safe operations.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def write(self, path: str, contents: str | bytes) -> None:
|
|
18
|
+
"""Write contents to a file at the specified path.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
path: The file path where contents should be written.
|
|
22
|
+
contents: The data to write, either as string or bytes.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def read(self, path: str) -> str:
|
|
27
|
+
"""Read and return the contents of a file as a string.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
path: The file path to read from.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
The file contents as a string.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def list(self, path: str) -> list[str]:
|
|
38
|
+
"""List all files and directories at the specified path.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
path: The directory path to list contents from.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A list of file and directory names in the specified path.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def delete(self, path: str) -> None:
|
|
49
|
+
"""Delete the file or directory at the specified path.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
path: The file or directory path to delete.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def exists(self, path: str) -> bool:
|
|
57
|
+
"""Check if a file or directory exists at the specified path.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: The file or directory path to check.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if the path exists, False otherwise.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def get_absolute_path(self, path: str) -> str:
|
|
68
|
+
"""Get the absolute filesystem path for a given relative path.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
path: The relative path within the file store.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The absolute path on the filesystem.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
@contextmanager
|
|
79
|
+
def lock(self, path: str, timeout: float = 30.0) -> Iterator[None]:
|
|
80
|
+
"""Acquire an exclusive lock for the given path.
|
|
81
|
+
|
|
82
|
+
This context manager provides thread and process-safe locking.
|
|
83
|
+
Implementations may use file-based locking, threading locks, or
|
|
84
|
+
other mechanisms as appropriate.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
path: The path to lock (used to identify the lock).
|
|
88
|
+
timeout: Maximum seconds to wait for lock acquisition.
|
|
89
|
+
|
|
90
|
+
Yields:
|
|
91
|
+
None when lock is acquired.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
TimeoutError: If lock cannot be acquired within timeout.
|
|
95
|
+
|
|
96
|
+
Note:
|
|
97
|
+
File-based locking (flock) does NOT work reliably on NFS mounts
|
|
98
|
+
or network filesystems.
|
|
99
|
+
"""
|
|
100
|
+
yield # pragma: no cover
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shutil
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
|
|
6
|
+
from filelock import FileLock, Timeout
|
|
3
7
|
|
|
4
8
|
from openhands.sdk.io.cache import MemoryLRUCache
|
|
5
9
|
from openhands.sdk.logger import get_logger
|
|
@@ -117,3 +121,24 @@ class LocalFileStore(FileStore):
|
|
|
117
121
|
|
|
118
122
|
except Exception as e:
|
|
119
123
|
logger.error(f"Error clearing local file store: {str(e)}")
|
|
124
|
+
|
|
125
|
+
def exists(self, path: str) -> bool:
|
|
126
|
+
"""Check if a file or directory exists."""
|
|
127
|
+
return os.path.exists(self.get_full_path(path))
|
|
128
|
+
|
|
129
|
+
def get_absolute_path(self, path: str) -> str:
|
|
130
|
+
"""Get absolute filesystem path."""
|
|
131
|
+
return self.get_full_path(path)
|
|
132
|
+
|
|
133
|
+
@contextmanager
|
|
134
|
+
def lock(self, path: str, timeout: float = 30.0) -> Iterator[None]:
|
|
135
|
+
"""Acquire file-based lock using flock."""
|
|
136
|
+
lock_path = self.get_full_path(path)
|
|
137
|
+
os.makedirs(os.path.dirname(lock_path), exist_ok=True)
|
|
138
|
+
file_lock = FileLock(lock_path)
|
|
139
|
+
try:
|
|
140
|
+
with file_lock.acquire(timeout=timeout):
|
|
141
|
+
yield
|
|
142
|
+
except Timeout:
|
|
143
|
+
logger.error(f"Failed to acquire lock within {timeout}s: {lock_path}")
|
|
144
|
+
raise TimeoutError(f"Lock acquisition timed out: {path}")
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import threading
|
|
3
|
+
import uuid
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
2
6
|
|
|
3
7
|
from openhands.sdk.io.base import FileStore
|
|
4
8
|
from openhands.sdk.logger import get_logger
|
|
@@ -9,9 +13,13 @@ logger = get_logger(__name__)
|
|
|
9
13
|
|
|
10
14
|
class InMemoryFileStore(FileStore):
|
|
11
15
|
files: dict[str, str]
|
|
16
|
+
_instance_id: str
|
|
17
|
+
_lock: threading.Lock
|
|
12
18
|
|
|
13
19
|
def __init__(self, files: dict[str, str] | None = None) -> None:
|
|
14
20
|
self.files = {}
|
|
21
|
+
self._instance_id = uuid.uuid4().hex
|
|
22
|
+
self._lock = threading.Lock()
|
|
15
23
|
if files is not None:
|
|
16
24
|
self.files = files
|
|
17
25
|
|
|
@@ -51,4 +59,29 @@ class InMemoryFileStore(FileStore):
|
|
|
51
59
|
del self.files[key]
|
|
52
60
|
logger.debug(f"Cleared in-memory file store: {path}")
|
|
53
61
|
except Exception as e:
|
|
54
|
-
logger.error(f"Error clearing in-memory file store: {
|
|
62
|
+
logger.error(f"Error clearing in-memory file store: {e}")
|
|
63
|
+
|
|
64
|
+
def exists(self, path: str) -> bool:
|
|
65
|
+
"""Check if a file exists."""
|
|
66
|
+
if path in self.files:
|
|
67
|
+
return True
|
|
68
|
+
return any(f.startswith(path + "/") for f in self.files)
|
|
69
|
+
|
|
70
|
+
def get_absolute_path(self, path: str) -> str:
|
|
71
|
+
"""Get absolute path (uses temp dir with unique instance ID)."""
|
|
72
|
+
import tempfile
|
|
73
|
+
|
|
74
|
+
return os.path.join(
|
|
75
|
+
tempfile.gettempdir(), f"openhands_inmemory_{self._instance_id}", path
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@contextmanager
|
|
79
|
+
def lock(self, path: str, timeout: float = 30.0) -> Iterator[None]:
|
|
80
|
+
"""Acquire thread lock for in-memory store."""
|
|
81
|
+
acquired = self._lock.acquire(timeout=timeout)
|
|
82
|
+
if not acquired:
|
|
83
|
+
raise TimeoutError(f"Lock acquisition timed out: {path}")
|
|
84
|
+
try:
|
|
85
|
+
yield
|
|
86
|
+
finally:
|
|
87
|
+
self._lock.release()
|
|
@@ -424,8 +424,11 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
424
424
|
) -> None:
|
|
425
425
|
if self.retry_listener is not None:
|
|
426
426
|
self.retry_listener(attempt_number, num_retries, _err)
|
|
427
|
-
|
|
428
|
-
|
|
427
|
+
# NOTE: don't call Telemetry.on_error here.
|
|
428
|
+
# This function runs for each retried failure (before the next attempt),
|
|
429
|
+
# which would create noisy duplicate error logs.
|
|
430
|
+
# The completion()/responses() exception handlers call Telemetry.on_error
|
|
431
|
+
# after retries are exhausted (final failure), which is what we want to log.
|
|
429
432
|
|
|
430
433
|
# =========================================================================
|
|
431
434
|
# Serializers
|
|
@@ -697,6 +700,7 @@ class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
|
|
|
697
700
|
telemetry_ctx.update(
|
|
698
701
|
{
|
|
699
702
|
"llm_path": "responses",
|
|
703
|
+
"instructions": instructions,
|
|
700
704
|
"input": input_items[:],
|
|
701
705
|
"tools": tools,
|
|
702
706
|
"kwargs": {k: v for k, v in call_kwargs.items()},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
|
+
import traceback
|
|
4
5
|
import uuid
|
|
5
6
|
import warnings
|
|
6
7
|
from collections.abc import Callable
|
|
@@ -121,7 +122,46 @@ class Telemetry(BaseModel):
|
|
|
121
122
|
return self.metrics.deep_copy()
|
|
122
123
|
|
|
123
124
|
def on_error(self, _err: BaseException) -> None:
|
|
124
|
-
#
|
|
125
|
+
# Best-effort logging for failed requests (so we can debug malformed
|
|
126
|
+
# request payloads, e.g. orphaned Responses reasoning items).
|
|
127
|
+
self._last_latency = time.time() - (self._req_start or time.time())
|
|
128
|
+
|
|
129
|
+
if not self.log_enabled:
|
|
130
|
+
return
|
|
131
|
+
if not self.log_dir and not self._log_completions_callback:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
filename = (
|
|
136
|
+
f"{self.model_name.replace('/', '__')}-"
|
|
137
|
+
f"{time.time():.3f}-"
|
|
138
|
+
f"{uuid.uuid4().hex[:4]}-error.json"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
data = self._req_ctx.copy()
|
|
142
|
+
data["error"] = {
|
|
143
|
+
"type": type(_err).__name__,
|
|
144
|
+
"message": str(_err),
|
|
145
|
+
"repr": repr(_err),
|
|
146
|
+
"traceback": "".join(
|
|
147
|
+
traceback.format_exception(type(_err), _err, _err.__traceback__)
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
data["timestamp"] = time.time()
|
|
151
|
+
data["latency_sec"] = self._last_latency
|
|
152
|
+
data["cost"] = 0.0
|
|
153
|
+
|
|
154
|
+
log_data = json.dumps(data, default=_safe_json, ensure_ascii=False)
|
|
155
|
+
|
|
156
|
+
if self._log_completions_callback:
|
|
157
|
+
self._log_completions_callback(filename, log_data)
|
|
158
|
+
elif self.log_dir:
|
|
159
|
+
os.makedirs(self.log_dir, exist_ok=True)
|
|
160
|
+
fname = os.path.join(self.log_dir, filename)
|
|
161
|
+
with open(fname, "w", encoding="utf-8") as f:
|
|
162
|
+
f.write(log_data)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
warnings.warn(f"Telemetry error logging failed: {e}")
|
|
125
165
|
return
|
|
126
166
|
|
|
127
167
|
# ---------- Helpers ----------
|
|
@@ -335,7 +375,6 @@ class Telemetry(BaseModel):
|
|
|
335
375
|
os.makedirs(self.log_dir, exist_ok=True)
|
|
336
376
|
if not os.access(self.log_dir, os.W_OK):
|
|
337
377
|
raise PermissionError(f"log_dir is not writable: {self.log_dir}")
|
|
338
|
-
|
|
339
378
|
fname = os.path.join(self.log_dir, filename)
|
|
340
379
|
with open(fname, "w", encoding="utf-8") as f:
|
|
341
380
|
f.write(log_data)
|