openhands-sdk 1.6.0__tar.gz → 1.7.1__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.1}/PKG-INFO +2 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/__init__.py +9 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/agent.py +35 -12
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/base.py +11 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +1 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/utils.py +18 -4
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/__init__.py +2 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/agent_context.py +16 -8
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/base.py +11 -6
- openhands_sdk-1.7.1/openhands/sdk/context/condenser/llm_summarizing_condenser.py +238 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/no_op_condenser.py +2 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/pipeline_condenser.py +10 -9
- openhands_sdk-1.7.1/openhands/sdk/context/condenser/utils.py +149 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/prompt.py +40 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +3 -3
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/__init__.py +2 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/skill.py +146 -0
- openhands_sdk-1.7.1/openhands/sdk/context/view.py +503 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/conversation.py +19 -0
- openhands_sdk-1.7.1/openhands/sdk/conversation/exceptions.py +50 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/impl/local_conversation.py +60 -8
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/impl/remote_conversation.py +137 -3
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/state.py +41 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/stuck_detector.py +81 -45
- openhands_sdk-1.7.1/openhands/sdk/conversation/types.py +45 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/system.py +16 -20
- openhands_sdk-1.7.1/openhands/sdk/hooks/__init__.py +30 -0
- openhands_sdk-1.7.1/openhands/sdk/hooks/config.py +180 -0
- openhands_sdk-1.7.1/openhands/sdk/hooks/conversation_hooks.py +227 -0
- openhands_sdk-1.7.1/openhands/sdk/hooks/executor.py +155 -0
- openhands_sdk-1.7.1/openhands/sdk/hooks/manager.py +170 -0
- openhands_sdk-1.7.1/openhands/sdk/hooks/types.py +40 -0
- openhands_sdk-1.7.1/openhands/sdk/io/cache.py +85 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/io/local.py +39 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/message.py +2 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/mixins/fn_call_converter.py +61 -16
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/mixins/non_native_fc.py +5 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/model_features.py +64 -24
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/verified_models.py +4 -4
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/logger/logger.py +1 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/schema.py +10 -0
- openhands_sdk-1.7.1/openhands/sdk/utils/async_executor.py +115 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/models.py +1 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/PKG-INFO +2 -2
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/SOURCES.txt +16 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/requires.txt +1 -1
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/pyproject.toml +2 -2
- openhands_sdk-1.6.0/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -89
- openhands_sdk-1.6.0/openhands/sdk/context/view.py +0 -243
- 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.1}/openhands/sdk/agent/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/exceptions.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/trigger.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/types.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/conversation_stats.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/event_store.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/events_list_base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/fifo_lock.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/impl/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/persistence_const.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/response_utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/secret_registry.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/serialization_diff.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/title_utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/visualizer/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/visualizer/default.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/impl/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/impl/agent_finished.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/impl/empty_patch.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/impl/pass_critic.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/condenser.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/conversation_error.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/conversation_state.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_completion_log.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/action.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/message.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/observation.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/token.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/types.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/user_action.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/exceptions.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/git_changes.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/git_diff.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/models.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/io/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/io/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/io/memory.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/exceptions/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/exceptions/classifier.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/exceptions/mapping.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/exceptions/types.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/llm.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/llm_registry.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/llm_response.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/options/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/options/chat_options.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/options/common.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/options/responses_options.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/router/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/router/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/router/impl/random.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/streaming.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/metrics.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/model_info.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/telemetry.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/unverified_models.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/logger/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/logger/rolling.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/client.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/definition.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/exceptions.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/tool.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/observability/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/observability/laminar.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/observability/utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/py.typed +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/secret/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/secret/secrets.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/analyzer.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/confirmation_policy.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/llm_analyzer.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/risk.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/builtins/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/builtins/finish.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/builtins/think.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/registry.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/spec.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/tool.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/async_utils.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/cipher.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/command.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/deprecation.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/github.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/json.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/paging.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/pydantic_diff.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/pydantic_secrets.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/truncate.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/visualize.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/local.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/models.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/remote/__init__.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/remote/base.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/workspace.py +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/dependency_links.txt +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/top_level.txt +0 -0
- {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/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.1
|
|
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
|
]
|
|
@@ -25,6 +25,7 @@ from openhands.sdk.event import (
|
|
|
25
25
|
ObservationEvent,
|
|
26
26
|
SystemPromptEvent,
|
|
27
27
|
TokenEvent,
|
|
28
|
+
UserRejectObservation,
|
|
28
29
|
)
|
|
29
30
|
from openhands.sdk.event.condenser import Condensation, CondensationRequest
|
|
30
31
|
from openhands.sdk.llm import (
|
|
@@ -109,17 +110,10 @@ class Agent(AgentBase):
|
|
|
109
110
|
event = SystemPromptEvent(
|
|
110
111
|
source="agent",
|
|
111
112
|
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
|
-
],
|
|
113
|
+
# Tools are stored as ToolDefinition objects and converted to
|
|
114
|
+
# OpenAI format with security_risk parameter during LLM completion.
|
|
115
|
+
# See make_llm_completion() in agent/utils.py for details.
|
|
116
|
+
tools=list(self.tools_map.values()),
|
|
123
117
|
)
|
|
124
118
|
on_event(event)
|
|
125
119
|
|
|
@@ -151,9 +145,20 @@ class Agent(AgentBase):
|
|
|
151
145
|
self._execute_actions(conversation, pending_actions, on_event)
|
|
152
146
|
return
|
|
153
147
|
|
|
148
|
+
# Check if the last user message was blocked by a UserPromptSubmit hook
|
|
149
|
+
# If so, skip processing and mark conversation as finished
|
|
150
|
+
for event in reversed(list(state.events)):
|
|
151
|
+
if isinstance(event, MessageEvent) and event.source == "user":
|
|
152
|
+
reason = state.pop_blocked_message(event.id)
|
|
153
|
+
if reason is not None:
|
|
154
|
+
logger.info(f"User message blocked by hook: {reason}")
|
|
155
|
+
state.execution_status = ConversationExecutionStatus.FINISHED
|
|
156
|
+
return
|
|
157
|
+
break # Only check the most recent user message
|
|
158
|
+
|
|
154
159
|
# Prepare LLM messages using the utility function
|
|
155
160
|
_messages_or_condensation = prepare_llm_messages(
|
|
156
|
-
state.events, condenser=self.condenser
|
|
161
|
+
state.events, condenser=self.condenser, llm=self.llm
|
|
157
162
|
)
|
|
158
163
|
|
|
159
164
|
# Process condensation event before agent sampels another action
|
|
@@ -469,8 +474,26 @@ class Agent(AgentBase):
|
|
|
469
474
|
|
|
470
475
|
It will call the tool's executor and update the state & call callback fn
|
|
471
476
|
with the observation.
|
|
477
|
+
|
|
478
|
+
If the action was blocked by a PreToolUse hook (recorded in
|
|
479
|
+
state.blocked_actions), a UserRejectObservation is emitted instead
|
|
480
|
+
of executing the action.
|
|
472
481
|
"""
|
|
473
482
|
state = conversation.state
|
|
483
|
+
|
|
484
|
+
# Check if this action was blocked by a PreToolUse hook
|
|
485
|
+
reason = state.pop_blocked_action(action_event.id)
|
|
486
|
+
if reason is not None:
|
|
487
|
+
logger.info(f"Action '{action_event.tool_name}' blocked by hook: {reason}")
|
|
488
|
+
rejection = UserRejectObservation(
|
|
489
|
+
action_id=action_event.id,
|
|
490
|
+
tool_name=action_event.tool_name,
|
|
491
|
+
tool_call_id=action_event.tool_call_id,
|
|
492
|
+
rejection_reason=reason,
|
|
493
|
+
)
|
|
494
|
+
on_event(rejection)
|
|
495
|
+
return rejection
|
|
496
|
+
|
|
474
497
|
tool = self.tools_map.get(action_event.tool_name, None)
|
|
475
498
|
if tool is None:
|
|
476
499
|
raise RuntimeError(
|
|
@@ -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
|
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
* Stream your thinking and responses while staying concise; surface key assumptions and environment prerequisites explicitly.
|
|
2
|
-
*
|
|
3
|
-
* You have access to external resources and should actively use available tools to try accessing them first, rather than claiming you can’t access something without making an attempt.
|
|
2
|
+
* You have access to external resources and should actively use available tools to try accessing them first, rather than claiming you can’t access something without making an attempt.
|
|
@@ -117,6 +117,7 @@ def prepare_llm_messages(
|
|
|
117
117
|
events: Sequence[Event],
|
|
118
118
|
condenser: None = None,
|
|
119
119
|
additional_messages: list[Message] | None = None,
|
|
120
|
+
llm: LLM | None = None,
|
|
120
121
|
) -> list[Message]: ...
|
|
121
122
|
|
|
122
123
|
|
|
@@ -125,6 +126,7 @@ def prepare_llm_messages(
|
|
|
125
126
|
events: Sequence[Event],
|
|
126
127
|
condenser: CondenserBase,
|
|
127
128
|
additional_messages: list[Message] | None = None,
|
|
129
|
+
llm: LLM | None = None,
|
|
128
130
|
) -> list[Message] | Condensation: ...
|
|
129
131
|
|
|
130
132
|
|
|
@@ -132,6 +134,7 @@ def prepare_llm_messages(
|
|
|
132
134
|
events: Sequence[Event],
|
|
133
135
|
condenser: CondenserBase | None = None,
|
|
134
136
|
additional_messages: list[Message] | None = None,
|
|
137
|
+
llm: LLM | None = None,
|
|
135
138
|
) -> list[Message] | Condensation:
|
|
136
139
|
"""Prepare LLM messages from conversation context.
|
|
137
140
|
|
|
@@ -140,13 +143,15 @@ def prepare_llm_messages(
|
|
|
140
143
|
It handles condensation internally and calls the callback when needed.
|
|
141
144
|
|
|
142
145
|
Args:
|
|
143
|
-
|
|
146
|
+
events: Sequence of events to prepare messages from
|
|
144
147
|
condenser: Optional condenser for handling context window limits
|
|
145
148
|
additional_messages: Optional additional messages to append
|
|
146
|
-
|
|
149
|
+
llm: Optional LLM instance from the agent, passed to condenser for
|
|
150
|
+
token counting or other LLM features
|
|
147
151
|
|
|
148
152
|
Returns:
|
|
149
|
-
List of messages ready for LLM completion
|
|
153
|
+
List of messages ready for LLM completion, or a Condensation event
|
|
154
|
+
if condensation is needed
|
|
150
155
|
|
|
151
156
|
Raises:
|
|
152
157
|
RuntimeError: If condensation is needed but no callback is provided
|
|
@@ -160,7 +165,7 @@ def prepare_llm_messages(
|
|
|
160
165
|
# produce a list of events, exactly as expected, or a
|
|
161
166
|
# new condensation that needs to be processed
|
|
162
167
|
if condenser is not None:
|
|
163
|
-
condensation_result = condenser.condense(view)
|
|
168
|
+
condensation_result = condenser.condense(view, agent_llm=llm)
|
|
164
169
|
|
|
165
170
|
match condensation_result:
|
|
166
171
|
case View():
|
|
@@ -195,6 +200,15 @@ def make_llm_completion(
|
|
|
195
200
|
|
|
196
201
|
Returns:
|
|
197
202
|
LLMResponse from the LLM completion call
|
|
203
|
+
|
|
204
|
+
Note:
|
|
205
|
+
Always exposes a 'security_risk' parameter in tool schemas via
|
|
206
|
+
add_security_risk_prediction=True. This ensures the schema remains
|
|
207
|
+
consistent, even if the security analyzer is disabled. Validation of
|
|
208
|
+
this field happens dynamically at runtime depending on the analyzer
|
|
209
|
+
configured. This allows weaker models to omit risk field and bypass
|
|
210
|
+
validation requirements when analyzer is disabled. For detailed logic,
|
|
211
|
+
see `_extract_security_risk` method in agent.py.
|
|
198
212
|
"""
|
|
199
213
|
if llm.uses_responses_api():
|
|
200
214
|
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():
|
|
@@ -3,6 +3,7 @@ from logging import getLogger
|
|
|
3
3
|
|
|
4
4
|
from openhands.sdk.context.view import View
|
|
5
5
|
from openhands.sdk.event.condenser import Condensation
|
|
6
|
+
from openhands.sdk.llm import LLM
|
|
6
7
|
from openhands.sdk.utils.models import (
|
|
7
8
|
DiscriminatedUnionMixin,
|
|
8
9
|
)
|
|
@@ -28,7 +29,7 @@ class CondenserBase(DiscriminatedUnionMixin, ABC):
|
|
|
28
29
|
"""
|
|
29
30
|
|
|
30
31
|
@abstractmethod
|
|
31
|
-
def condense(self, view: View) -> View | Condensation:
|
|
32
|
+
def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
|
|
32
33
|
"""Condense a sequence of events into a potentially smaller list.
|
|
33
34
|
|
|
34
35
|
New condenser strategies should override this method to implement their own
|
|
@@ -37,6 +38,8 @@ class CondenserBase(DiscriminatedUnionMixin, ABC):
|
|
|
37
38
|
|
|
38
39
|
Args:
|
|
39
40
|
view: A view of the history containing all events that should be condensed.
|
|
41
|
+
agent_llm: LLM instance used by the agent. Condensers use this for token
|
|
42
|
+
counting purposes. Defaults to None.
|
|
40
43
|
|
|
41
44
|
Returns:
|
|
42
45
|
View | Condensation: A condensed view of the events or an event indicating
|
|
@@ -77,18 +80,20 @@ class RollingCondenser(PipelinableCondenserBase, ABC):
|
|
|
77
80
|
"""
|
|
78
81
|
|
|
79
82
|
@abstractmethod
|
|
80
|
-
def should_condense(self, view: View) -> bool:
|
|
83
|
+
def should_condense(self, view: View, agent_llm: LLM | None = None) -> bool:
|
|
81
84
|
"""Determine if a view should be condensed."""
|
|
82
85
|
|
|
83
86
|
@abstractmethod
|
|
84
|
-
def get_condensation(
|
|
87
|
+
def get_condensation(
|
|
88
|
+
self, view: View, agent_llm: LLM | None = None
|
|
89
|
+
) -> Condensation:
|
|
85
90
|
"""Get the condensation from a view."""
|
|
86
91
|
|
|
87
|
-
def condense(self, view: View) -> View | Condensation:
|
|
92
|
+
def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
|
|
88
93
|
# If we trigger the condenser-specific condensation threshold, compute and
|
|
89
94
|
# return the condensation.
|
|
90
|
-
if self.should_condense(view):
|
|
91
|
-
return self.get_condensation(view)
|
|
95
|
+
if self.should_condense(view, agent_llm=agent_llm):
|
|
96
|
+
return self.get_condensation(view, agent_llm=agent_llm)
|
|
92
97
|
|
|
93
98
|
# Otherwise we're safe to just return the view.
|
|
94
99
|
else:
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, model_validator
|
|
6
|
+
|
|
7
|
+
from openhands.sdk.context.condenser.base import RollingCondenser
|
|
8
|
+
from openhands.sdk.context.condenser.utils import (
|
|
9
|
+
get_suffix_length_for_token_reduction,
|
|
10
|
+
get_total_token_count,
|
|
11
|
+
)
|
|
12
|
+
from openhands.sdk.context.prompts import render_template
|
|
13
|
+
from openhands.sdk.context.view import View
|
|
14
|
+
from openhands.sdk.event.base import LLMConvertibleEvent
|
|
15
|
+
from openhands.sdk.event.condenser import Condensation
|
|
16
|
+
from openhands.sdk.event.llm_convertible import MessageEvent
|
|
17
|
+
from openhands.sdk.llm import LLM, Message, TextContent
|
|
18
|
+
from openhands.sdk.observability.laminar import observe
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Reason(Enum):
|
|
22
|
+
"""Reasons for condensation."""
|
|
23
|
+
|
|
24
|
+
REQUEST = "request"
|
|
25
|
+
TOKENS = "tokens"
|
|
26
|
+
EVENTS = "events"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LLMSummarizingCondenser(RollingCondenser):
|
|
30
|
+
"""LLM-based condenser that summarizes forgotten events.
|
|
31
|
+
|
|
32
|
+
Uses an independent LLM (stored in the `llm` attribute) for generating summaries
|
|
33
|
+
of forgotten events. The optional `agent_llm` parameter passed to condense() is
|
|
34
|
+
the LLM used by the agent for token counting purposes, and you should not assume
|
|
35
|
+
it is the same as the one defined in this condenser.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
llm: LLM
|
|
39
|
+
max_size: int = Field(default=120, gt=0)
|
|
40
|
+
max_tokens: int | None = None
|
|
41
|
+
keep_first: int = Field(default=4, ge=0)
|
|
42
|
+
|
|
43
|
+
@model_validator(mode="after")
|
|
44
|
+
def validate_keep_first_vs_max_size(self):
|
|
45
|
+
events_from_tail = self.max_size // 2 - self.keep_first - 1
|
|
46
|
+
if events_from_tail <= 0:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"keep_first must be less than max_size // 2 to leave room for "
|
|
49
|
+
"condensation"
|
|
50
|
+
)
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
def handles_condensation_requests(self) -> bool:
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
def get_condensation_reasons(
|
|
57
|
+
self, view: View, agent_llm: LLM | None = None
|
|
58
|
+
) -> set[Reason]:
|
|
59
|
+
"""Determine the reasons why the view should be condensed.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
view: The current view to evaluate.
|
|
63
|
+
agent_llm: The LLM used by the agent. Required if token counting is needed.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A set of Reason enums indicating why condensation is needed.
|
|
67
|
+
"""
|
|
68
|
+
reasons = set()
|
|
69
|
+
|
|
70
|
+
# Reason 1: Unhandled condensation request. The view handles the detection of
|
|
71
|
+
# these requests while processing the event stream.
|
|
72
|
+
if view.unhandled_condensation_request:
|
|
73
|
+
reasons.add(Reason.REQUEST)
|
|
74
|
+
|
|
75
|
+
# Reason 2: Token limit is provided and exceeded.
|
|
76
|
+
if self.max_tokens and agent_llm:
|
|
77
|
+
total_tokens = get_total_token_count(view.events, agent_llm)
|
|
78
|
+
if total_tokens > self.max_tokens:
|
|
79
|
+
reasons.add(Reason.TOKENS)
|
|
80
|
+
|
|
81
|
+
# Reason 3: View exceeds maximum size in number of events.
|
|
82
|
+
if len(view) > self.max_size:
|
|
83
|
+
reasons.add(Reason.EVENTS)
|
|
84
|
+
|
|
85
|
+
return reasons
|
|
86
|
+
|
|
87
|
+
def should_condense(self, view: View, agent_llm: LLM | None = None) -> bool:
|
|
88
|
+
reasons = self.get_condensation_reasons(view, agent_llm)
|
|
89
|
+
return reasons != set()
|
|
90
|
+
|
|
91
|
+
def _get_summary_event_content(self, view: View) -> str:
|
|
92
|
+
"""Extract the text content from the summary event in the view, if any.
|
|
93
|
+
|
|
94
|
+
If there is no summary event or it does not contain text content, returns an
|
|
95
|
+
empty string.
|
|
96
|
+
"""
|
|
97
|
+
summary_event_content: str = ""
|
|
98
|
+
|
|
99
|
+
summary_event = view.summary_event
|
|
100
|
+
if isinstance(summary_event, MessageEvent):
|
|
101
|
+
message_content = summary_event.llm_message.content[0]
|
|
102
|
+
if isinstance(message_content, TextContent):
|
|
103
|
+
summary_event_content = message_content.text
|
|
104
|
+
|
|
105
|
+
return summary_event_content
|
|
106
|
+
|
|
107
|
+
def _generate_condensation(
|
|
108
|
+
self,
|
|
109
|
+
summary_event_content: str,
|
|
110
|
+
forgotten_events: Sequence[LLMConvertibleEvent],
|
|
111
|
+
summary_offset: int,
|
|
112
|
+
) -> Condensation:
|
|
113
|
+
"""Generate a condensation by using the condenser's LLM to summarize forgotten
|
|
114
|
+
events.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
summary_event_content: The content of the previous summary event.
|
|
118
|
+
forgotten_events: The list of events to be summarized.
|
|
119
|
+
summary_offset: The index where the summary event should be inserted.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Condensation: The generated condensation object.
|
|
123
|
+
"""
|
|
124
|
+
# Convert events to strings for the template
|
|
125
|
+
event_strings = [str(forgotten_event) for forgotten_event in forgotten_events]
|
|
126
|
+
|
|
127
|
+
prompt = render_template(
|
|
128
|
+
os.path.join(os.path.dirname(__file__), "prompts"),
|
|
129
|
+
"summarizing_prompt.j2",
|
|
130
|
+
previous_summary=summary_event_content,
|
|
131
|
+
events=event_strings,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
messages = [Message(role="user", content=[TextContent(text=prompt)])]
|
|
135
|
+
|
|
136
|
+
# Do not pass extra_body explicitly. The LLM handles forwarding
|
|
137
|
+
# litellm_extra_body only when it is non-empty.
|
|
138
|
+
llm_response = self.llm.completion(
|
|
139
|
+
messages=messages,
|
|
140
|
+
)
|
|
141
|
+
# Extract summary from the LLMResponse message
|
|
142
|
+
summary = None
|
|
143
|
+
if llm_response.message.content:
|
|
144
|
+
first_content = llm_response.message.content[0]
|
|
145
|
+
if isinstance(first_content, TextContent):
|
|
146
|
+
summary = first_content.text
|
|
147
|
+
|
|
148
|
+
return Condensation(
|
|
149
|
+
forgotten_event_ids=[event.id for event in forgotten_events],
|
|
150
|
+
summary=summary,
|
|
151
|
+
summary_offset=summary_offset,
|
|
152
|
+
llm_response_id=llm_response.id,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _get_forgotten_events(
|
|
156
|
+
self, view: View, agent_llm: LLM | None = None
|
|
157
|
+
) -> tuple[Sequence[LLMConvertibleEvent], int]:
|
|
158
|
+
"""Identify events to be forgotten and the summary offset.
|
|
159
|
+
|
|
160
|
+
Relies on the condensation reasons to determine how many events we need to drop
|
|
161
|
+
in order to maintain our resource constraints. Uses manipulation indices to
|
|
162
|
+
ensure forgetting ranges respect atomic unit boundaries.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
view: The current view from which to identify forgotten events.
|
|
166
|
+
agent_llm: The LLM used by the agent, required for token-based calculations.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
A tuple of (events to forget, summary_offset).
|
|
170
|
+
"""
|
|
171
|
+
reasons = self.get_condensation_reasons(view, agent_llm=agent_llm)
|
|
172
|
+
assert reasons != set(), "No condensation reasons found."
|
|
173
|
+
|
|
174
|
+
suffix_events_to_keep: set[int] = set()
|
|
175
|
+
|
|
176
|
+
if Reason.REQUEST in reasons:
|
|
177
|
+
target_size = len(view) // 2
|
|
178
|
+
suffix_events_to_keep.add(target_size - self.keep_first - 1)
|
|
179
|
+
|
|
180
|
+
if Reason.EVENTS in reasons:
|
|
181
|
+
target_size = self.max_size // 2
|
|
182
|
+
suffix_events_to_keep.add(target_size - self.keep_first - 1)
|
|
183
|
+
|
|
184
|
+
if Reason.TOKENS in reasons:
|
|
185
|
+
# Compute the number of tokens we need to eliminate to be under half the
|
|
186
|
+
# max_tokens value. We know max_tokens and the agent LLM are not None here
|
|
187
|
+
# because we can't have Reason.TOKENS without them.
|
|
188
|
+
assert self.max_tokens is not None
|
|
189
|
+
assert agent_llm is not None
|
|
190
|
+
|
|
191
|
+
total_tokens = get_total_token_count(view.events, agent_llm)
|
|
192
|
+
tokens_to_reduce = total_tokens - (self.max_tokens // 2)
|
|
193
|
+
|
|
194
|
+
suffix_events_to_keep.add(
|
|
195
|
+
get_suffix_length_for_token_reduction(
|
|
196
|
+
events=view.events[self.keep_first :],
|
|
197
|
+
llm=agent_llm,
|
|
198
|
+
token_reduction=tokens_to_reduce,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# We might have multiple reasons to condense, so pick the strictest condensation
|
|
203
|
+
# to ensure all resource constraints are met.
|
|
204
|
+
events_from_tail = min(suffix_events_to_keep)
|
|
205
|
+
|
|
206
|
+
# Calculate naive forgetting end (without considering atomic boundaries)
|
|
207
|
+
naive_end = len(view) - events_from_tail
|
|
208
|
+
|
|
209
|
+
# Find actual forgetting_start: smallest manipulation index > keep_first
|
|
210
|
+
forgetting_start = view.find_next_manipulation_index(
|
|
211
|
+
self.keep_first, strict=True
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Find actual forgetting_end: smallest manipulation index >= naive_end
|
|
215
|
+
forgetting_end = view.find_next_manipulation_index(naive_end, strict=False)
|
|
216
|
+
|
|
217
|
+
# Extract events to forget using boundary-aware indices
|
|
218
|
+
forgotten_events = view[forgetting_start:forgetting_end]
|
|
219
|
+
|
|
220
|
+
# Summary offset is the same as forgetting_start
|
|
221
|
+
return forgotten_events, forgetting_start
|
|
222
|
+
|
|
223
|
+
@observe(ignore_inputs=["view", "agent_llm"])
|
|
224
|
+
def get_condensation(
|
|
225
|
+
self, view: View, agent_llm: LLM | None = None
|
|
226
|
+
) -> Condensation:
|
|
227
|
+
# The condensation is dependent on the events we want to drop and the previous
|
|
228
|
+
# summary.
|
|
229
|
+
summary_event_content = self._get_summary_event_content(view)
|
|
230
|
+
forgotten_events, summary_offset = self._get_forgotten_events(
|
|
231
|
+
view, agent_llm=agent_llm
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return self._generate_condensation(
|
|
235
|
+
summary_event_content=summary_event_content,
|
|
236
|
+
forgotten_events=forgotten_events,
|
|
237
|
+
summary_offset=summary_offset,
|
|
238
|
+
)
|
{openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/no_op_condenser.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from openhands.sdk.context.condenser.base import CondenserBase
|
|
2
2
|
from openhands.sdk.context.view import View
|
|
3
3
|
from openhands.sdk.event.condenser import Condensation
|
|
4
|
+
from openhands.sdk.llm import LLM
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class NoOpCondenser(CondenserBase):
|
|
@@ -9,5 +10,5 @@ class NoOpCondenser(CondenserBase):
|
|
|
9
10
|
Primarily intended for testing purposes.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
|
-
def condense(self, view: View) -> View | Condensation:
|
|
13
|
+
def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation: # noqa: ARG002
|
|
13
14
|
return view
|
{openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/pipeline_condenser.py
RENAMED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
from openhands.sdk.context.condenser.base import CondenserBase
|
|
2
2
|
from openhands.sdk.context.view import View
|
|
3
3
|
from openhands.sdk.event.condenser import Condensation
|
|
4
|
+
from openhands.sdk.llm import LLM
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class PipelineCondenser(CondenserBase):
|
|
7
8
|
"""A condenser that applies a sequence of condensers in order.
|
|
8
9
|
|
|
9
10
|
All condensers are defined primarily by their `condense` method, which takes a
|
|
10
|
-
`View` and
|
|
11
|
-
|
|
12
|
-
condenser returns a `Condensation`.
|
|
11
|
+
`View` and an optional `agent_llm` parameter, returning either a new `View` or a
|
|
12
|
+
`Condensation` event. That means we can chain multiple condensers together by
|
|
13
|
+
passing `View`s along and exiting early if any condenser returns a `Condensation`.
|
|
13
14
|
|
|
14
15
|
For example:
|
|
15
16
|
|
|
@@ -20,20 +21,20 @@ class PipelineCondenser(CondenserBase):
|
|
|
20
21
|
CondenserC(...),
|
|
21
22
|
])
|
|
22
23
|
|
|
23
|
-
result = condenser.condense(view)
|
|
24
|
+
result = condenser.condense(view, agent_llm=agent_llm)
|
|
24
25
|
|
|
25
26
|
# Doing the same thing without the pipeline condenser requires more boilerplate
|
|
26
27
|
# for the monadic chaining
|
|
27
28
|
other_result = view
|
|
28
29
|
|
|
29
30
|
if isinstance(other_result, View):
|
|
30
|
-
other_result = CondenserA(...).condense(other_result)
|
|
31
|
+
other_result = CondenserA(...).condense(other_result, agent_llm=agent_llm)
|
|
31
32
|
|
|
32
33
|
if isinstance(other_result, View):
|
|
33
|
-
other_result = CondenserB(...).condense(other_result)
|
|
34
|
+
other_result = CondenserB(...).condense(other_result, agent_llm=agent_llm)
|
|
34
35
|
|
|
35
36
|
if isinstance(other_result, View):
|
|
36
|
-
other_result = CondenserC(...).condense(other_result)
|
|
37
|
+
other_result = CondenserC(...).condense(other_result, agent_llm=agent_llm)
|
|
37
38
|
|
|
38
39
|
assert result == other_result
|
|
39
40
|
"""
|
|
@@ -41,12 +42,12 @@ class PipelineCondenser(CondenserBase):
|
|
|
41
42
|
condensers: list[CondenserBase]
|
|
42
43
|
"""The list of condensers to apply in order."""
|
|
43
44
|
|
|
44
|
-
def condense(self, view: View) -> View | Condensation:
|
|
45
|
+
def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
|
|
45
46
|
result: View | Condensation = view
|
|
46
47
|
for condenser in self.condensers:
|
|
47
48
|
if isinstance(result, Condensation):
|
|
48
49
|
break
|
|
49
|
-
result = condenser.condense(result)
|
|
50
|
+
result = condenser.condense(result, agent_llm=agent_llm)
|
|
50
51
|
return result
|
|
51
52
|
|
|
52
53
|
def handles_condensation_requests(self) -> bool:
|