OpenHands v1.22.1 SDK
Deep architecture analysis of the OpenHands (formerly OpenDevin) coding agent framework
1. Overview
OpenHands is an open-source autonomous coding agent that interacts with a computer through shell commands, file editing, web browsing, and code execution inside a sandboxed Docker container. Originally called "OpenDevin," the project rebranded to OpenHands and has undergone a major architectural refactor: the monolithic codebase has been decomposed into three separate packages:
openhands-sdk— Core SDK: agent, conversation, LLM, events, context, security, skillsopenhands-tools— Built-in tools: terminal, file editor, browser, delegate, task, etc.openhands(main repo) — App server, web UI, REST API, deployment infrastructure
The SDK is designed as a standalone Python library — you can pip install openhands-sdk and build agents programmatically without the web server. The conversation factory pattern automatically routes to LocalConversation (in-process) or RemoteConversation (connects to an agent server) based on workspace type.
2. Architecture
┌─────────────────────────────────────────────────────────────────────────┐ │ OpenHands SDK (openhands-sdk) │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │ │ Agent │ │ Conversation │ │ Event Stream │ │ │ │ │ │ │ │ │ │ │ │ AgentBase │◄──│ LocalConv. │◄──│ Event (append-only) │ │ │ │ ├─Agent │ │ RemoteConv. │ │ ├─ MessageEvent │ │ │ │ │ └─step() │──►│ │──►│ ├─ ActionEvent │ │ │ │ ├─CriticMix │ │ ┌──────────┐ │ │ ├─ ObservationEvent │ │ │ │ └─DispatchMix│ │ │Condenser│ │ │ ├─ SystemPromptEvent │ │ │ │ │ │ │LLMSummar.│ │ │ ├─ Condensation │ │ │ └──────┬───────┘ │ └──────────┘ │ │ ├─ CondensationRequest │ │ │ │ └──────────────┘ │ ├─ TokenEvent │ │ │ │ │ ├─ AgentErrorEvent │ │ │ ▼ │ └─ UserRejectObservation │ │ │ ┌──────────────┐ └──────────────────────────┘ │ │ │ LLM │ │ │ │ │ ┌──────────────┐ ┌──────────────────────────┐ │ │ │ litellm wrap │ │ Tools │ │ Context / Skills │ │ │ │ retry logic │ │ │ │ │ │ │ │ streaming │ │ ToolDefinitn │ │ AgentContext │ │ │ │ fallback │ │ ToolExecutor │ │ ├─ skills (repo/know) │ │ │ │ Responses API│ │ ToolAnnotatn │ │ ├─ system_message_suffix │ │ │ └──────────────┘ │ MCPToolDef │ │ └─ secrets │ │ │ │ DeclaredResrc │ │ │ │ │ └──────┬───────┘ │ View (event window) │ │ │ │ │ Condenser pipeline │ │ │ ▼ └──────────────────────────┘ │ │ ┌──────────────┐ │ │ │ Security │ ┌──────────────────────────┐ │ │ │ │ │ Server │ │ │ │ Risk levels │ │ (openhands main repo) │ │ │ │ Analyzer │ │ │ │ │ │ ConfirmPoly │ │ FastAPI + SQLAlchemy │ │ │ │ Supply chain │ │ WebSocket events │ │ │ └──────────────┘ │ MCP proxy │ │ │ │ Conversation CRUD │ │ │ └──────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘
Package Decomposition
| Package | Responsibility | Key Modules |
|---|---|---|
openhands-sdk | Core agent logic, LLM, events, conversation, context, security | agent/, llm/, event/, conversation/, context/, security/, skills/, tool/, mcp/ |
openhands-tools | Built-in tool implementations | terminal/, file_editor/, browser_use/, delegate/, task/, apply_patch/, glob/, grep/ |
openhands (main) | Web app, REST API, WebSocket, deployment | app_server/, server/ |
3. Agent Core
Class Hierarchy
AgentBase (DiscriminatedUnionMixin, ABC)
├─ Agent (CriticMixin, ResponseDispatchMixin, AgentBase)
└─ ACPAgent (AgentBase) # Agent Communication Protocol agent
AgentBase — The Foundation
AgentBase is a frozen Pydantic model that defines the contract for all agents:
llm: LLM— Language model instancetools: list[ToolDefinition | str]— Available tools (by class or name)condenser: Condenser | None— Context compression strategyagent_context: AgentContext | None— Skills, secrets, datetime contextsystem_prompt_filename: str— Jinja2 template (default:system_prompt.j2)system_prompt_kwargs: dict— Template variablestool_concurrency_limit: int = 5
Key method: init_state(state, on_event) — emits the SystemPromptEvent as the first event. The system prompt is split into static (cacheable) and dynamic (per-conversation) content blocks for prompt caching optimization.
Agent.step() — The Core Loop
The step() method is the heart of the agent. Each call performs one iteration:
- Check pending actions — If there are unmatched actions (implicit confirmation mode), execute them first
- Check blocked messages — If a hook blocked the user message, finish
- Prepare LLM messages — Convert event stream → message list via
prepare_llm_messages(), applying condensation if needed - Handle condensation — If context window exceeded, emit
CondensationRequestand return - Call LLM —
make_llm_completion(llm, messages, tools, on_token) - Classify response:
TOOL_CALLS→ parse tool calls, create ActionEvents, execute in parallelCONTENT→ emit MessageEvent (agent speaks to user)REASONING_ONLY/EMPTY→ inject followup to continue
ParallelToolExecutor with thread-pool workers (default: 5). Tools declare their resource access via declared_resources() — non-conflicting tools run concurrently, while conflicting ones are serialized via resource-key locking.
ActionBatch — Atomic Multi-Tool Execution
When the LLM returns multiple tool calls (parallel function calling), they are grouped into an _ActionBatch:
@dataclass(frozen=True, slots=True)
class _ActionBatch:
action_events: list[ActionEvent]
has_finish: bool
blocked_reasons: dict[str, str] # hook-blocked actions
results_by_id: dict[str, list[Event]]
@classmethod
def prepare(cls, action_events, state, executor, tool_runner, tools):
# 1. Truncate at FinishTool (discard calls after finish)
# 2. Partition blocked vs executable actions
# 3. Execute batch via ParallelToolExecutor
# 4. Return immutable batch result
def emit(self, on_event): # emit events in original order
def finalize(self, on_event, ...): # handle finish or iterative refinement
CriticMixin
Optional self-critique layer. After generating actions but before execution, the agent can evaluate its own plan via a secondary LLM call. CriticResult is attached to ActionEvent for transparency.
ResponseDispatchMixin
Classifies LLM responses into categories:
class LLMResponseType:
TOOL_CALLS # LLM wants to invoke tools
CONTENT # LLM responds with text to user
REASONING_ONLY # LLM only reasons, no action
EMPTY # No content at all
4. Event System
Event Hierarchy
Event (DiscriminatedUnionMixin, ABC)
├─ LLMConvertibleEvent (can be converted to LLM Message)
│ ├─ ActionEvent # agent tool call (source="agent")
│ ├─ ObservationEvent # tool result (source="environment")
│ ├─ MessageEvent # user/agent text (source="user"|"agent")
│ └─ SystemPromptEvent # initial system prompt (source="agent")
├─ Condensation # context compression event
├─ CondensationRequest # request to condense context
├─ TokenEvent # streaming token delta
├─ AgentErrorEvent # agent error
└─ UserRejectObservation # hook/user rejected an action
Source Types
SourceType = Literal["agent", "user", "environment", "hook"]
Key Design: Immutable & Append-Only
All events are frozen Pydantic models (extra="forbid", frozen=True). Each has a unique id (UUID) and timestamp. The event log is append-only — events are never mutated, only added or "forgotten" via condensation.
LLM Message Conversion
LLMConvertibleEvent.to_llm_message() converts each event to an LLM Message. The static method events_to_messages() handles the tricky case of parallel function calling: multiple ActionEvents from the same LLM response are grouped by llm_response_id and combined into a single assistant message with multiple tool_calls.
# Parallel tool calls from same LLM response
@staticmethod
def events_to_messages(events: list[LLMConvertibleEvent]) -> list[Message]:
# Group ActionEvents by llm_response_id
# Combine into single Message with shared thought + multiple tool_calls
# Regular events → direct conversion
ActionEvent — The Heart of Tool Execution
class ActionEvent(LLMConvertibleEvent):
source: SourceType = "agent"
thought: Sequence[TextContent] # Agent's reasoning
reasoning_content: str | None # For reasoning models
thinking_blocks: list[...] # Anthropic extended thinking
responses_reasoning_item: ... # OpenAI Responses API reasoning
action: Action | None # Parsed tool action (None if non-executable)
tool_name: str
tool_call_id: ToolCallID
tool_call: MessageToolCall # Original LLM tool call
llm_response_id: EventID # Groups parallel calls
security_risk: SecurityRisk # LLM-assessed risk level
critic_result: CriticResult | None # Self-critique
summary: str | None # ~10-word action summary
5. Conversation Layer
Factory Pattern
Conversation.__new__() is a factory that automatically creates the right implementation:
LocalConversation— runs agent in-process (workspace isstr | Path | LocalWorkspace)RemoteConversation— connects to a remote agent server (workspace isRemoteWorkspace)
LocalConversation — Full Lifecycle
class LocalConversation(BaseConversation):
agent: AgentBase
workspace: LocalWorkspace
_state: ConversationState # Mutable conversation state
_visualizer: ConversationVisualizerBase | None
_stuck_detector: StuckDetector | None
_hook_processor: HookEventProcessor | None
max_iteration_per_run: int = 500
# Lazy plugin loading
_plugin_specs: list[PluginSource] | None
_resolved_plugins: list[ResolvedPluginSource] | None
_plugins_loaded: bool
run() — The Main Loop
The run() method drives the agent loop:
- Ensure agent is ready (lazy plugin loading, tool initialization)
- Loop
max_iteration_per_runtimes:- Call
agent.step() - Check stuck detection
- Check execution status (FINISHED, AWAITING_CONFIRMATION, etc.)
- Apply condensation if needed
- Call
- Cleanup on exit
send_message() — User Input
Creates a MessageEvent(source="user"), appends to event log, then calls run().
Stuck Detection
StuckDetector monitors for loops:
- Action-Observation loops — Same action repeated N times
- Action-Error loops — Same failing action repeated
- Monologue — Agent talks to itself without tool use
- Alternating patterns — Two actions cycling back and forth
Thresholds are configurable via StuckDetectionThresholds.
Plugin System
Plugins are loaded lazily on first run() or send_message() call. Sources include GitHub repos, git URLs, or local paths. Plugin merge semantics: skills override by name (last wins), MCP config overrides by key (last wins), hooks concatenate (all run).
6. LLM Integration
Architecture
class LLM(BaseModel, RetryMixin, NonNativeToolCallingMixin):
model: str = "claude-sonnet-4-20250514"
api_key: str | SecretStr | None
base_url: str | None
num_retries: int = 5
retry_multiplier: float = 8.0
timeout: int = 300
max_message_chars: int = 30_000
max_output_tokens: int | None
stream: bool = False
# ... AWS, OpenRouter, Ollama fields
LiteLLM Foundation
All LLM calls go through LiteLLM, providing a unified interface to 100+ providers (OpenAI, Anthropic, Google, AWS Bedrock, Azure, etc.). Key features:
- Dual API support: Chat Completions API and OpenAI Responses API
- Automatic provider detection via
infer_litellm_provider() - Vision support detection via
supports_vision() - Token counting via
token_counter()
Retry Logic (RetryMixin)
Exponential backoff with jitter on transient errors:
LLM_RETRY_EXCEPTIONS = (
APIConnectionError,
RateLimitError,
ServiceUnavailableError,
LiteLLMTimeout,
InternalServerError,
LLMNoResponseError,
)
# Default: 5 retries, multiplier=8.0, min_wait=8s, max_wait=64s
Non-Native Tool Calling Fallback
NonNativeToolCallingMixin handles models that don't support native function calling — it falls back to prompt-based tool invocation and parses the response.
Fallback Strategy
Configurable fallback to alternative models when the primary model fails, with strategies like ALL (try all fallbacks) or STOP (stop after first failure).
Streaming
When stream=True, token deltas are delivered via ConversationTokenCallbackType callbacks. The streaming handler supports both Chat Completions and Responses API streaming formats.
7. Tool System
ToolDefinition — The Core Abstraction
class ToolDefinition[ActionT, ObservationT](DiscriminatedUnionMixin, ABC):
name: ClassVar[str] # Auto-derived from class name
description: str
action_type: type[Action] # Input schema
observation_type: type[Observation] # Output schema
annotations: ToolAnnotations | None # MCP-style annotations
executor: SkipJsonSchema[ToolExecutor | None] # Runtime executor
@classmethod
def create(cls, *args, **kwargs) -> Sequence[Self] # Factory method
def __call__(self, action, conversation) -> Observation # Execute
def to_openai_tool(self, ...) -> ChatCompletionToolParam
def to_responses_tool(self, ...) -> FunctionToolParam
def to_mcp_tool(self, ...) -> dict
ToolAnnotations (MCP-Aligned)
class ToolAnnotations(BaseModel):
title: str | None
readOnlyHint: bool = False
destructiveHint: bool = True
idempotentHint: bool = False
openWorldHint: bool = True
Declared Resources for Parallel Execution
@dataclass(frozen=True, slots=True)
class DeclaredResources:
keys: tuple[str, ...] # e.g., ("file:/path/to/a.py", "terminal:session")
declared: bool # Whether the tool has analyzed its resources
The ParallelToolExecutor uses resource keys to determine which tool calls can safely run concurrently. Tools that declare declared=True, keys=() run freely; declared=False falls back to tool-wide serialization.
Built-in Tools (openhands-tools)
| Tool | Action | Description |
|---|---|---|
TerminalTool | Execute bash commands | tmux/subprocess/PowerShell backends; supports Ctrl+C, timeout, reset; auto-detects platform |
FileEditorTool | str_replace on files | Create, edit, view files with string replacement |
ApplyPatchTool | Apply unified diffs | Apply patch format edits to files |
BrowserUseTool | Web browsing | Navigate, click, type in web pages via browser |
DelegateTool | Spawn sub-agents | Deprecated in favor of TaskToolSet |
TaskToolSet | Sub-agent delegation | Spawn and manage sub-agents for sub-tasks |
GlobTool | File pattern search | Find files by glob pattern |
GrepTool | Content search | Search file contents with regex |
FinishTool | End conversation | Signal task completion |
ThinkTool | Internal reasoning | Structured thinking without side effects |
MCP Tool Integration
MCPToolDefinition wraps Model Context Protocol tools as first-class ToolDefinition instances. The system supports both stdio and SSE MCP servers, with automatic tool discovery and schema conversion.
8. Context & Memory
The Condenser Pipeline
Context window management is handled by a condenser pipeline that progressively compresses the event stream:
┌─────────────────────────────────────────────────────┐ │ Event Stream │ │ [SysPrompt] [Msg1] [Act1] [Obs1] ... [ActN] [ObsN]│ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────┐ │ │ │ View │ │ │ │ Active window over the event stream │ │ │ │ ├─ events: list of LLMConvertibleEvent │ │ │ │ ├─ manipulation_indices: atomic boundaries │ │ │ │ └─ unhandled_condensation_request │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────┐ │ │ │ Condenser Pipeline │ │ │ │ │ │ │ │ 1. NoOpCondenser (identity) │ │ │ │ 2. LLMSummarizingCondenser (LLM summary) │ │ │ │ ├─ max_size: 240 events │ │ │ │ ├─ max_tokens: optional token limit │ │ │ │ ├─ keep_first: 2 (never condense start) │ │ │ │ └─ minimum_progress: 10% │ │ │ │ 3. PipelineCondenser (chain multiple) │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ Condensation Event │ │ ├─ forgotten_event_ids: set of removed IDs │ │ ├─ summary: LLM-generated summary of forgotten │ │ └─ summary_offset: insertion point │ └─────────────────────────────────────────────────────┘
LLMSummarizingCondenser — Three Condensation Reasons
class Reason(Enum):
REQUEST = "request" # User/agent explicitly requested condensation
TOKENS = "tokens" # Token count exceeds max_tokens
EVENTS = "events" # Event count exceeds max_size
When condensation is needed:
- Identify events to forget (respecting
keep_firstand atomic boundaries viamanipulation_indices) - Use a separate LLM to generate a summary of forgotten events
- Create a
Condensationevent with forgotten IDs and summary - On hard context reset, retry with progressively shorter event strings (scaling by 0.8x per attempt)
AgentContext — Prompt Extension Container
class AgentContext(BaseModel):
skills: list[Skill] # Available skills
system_message_suffix: str | None # Appended to system prompt
user_message_suffix: str | None # Appended to user messages
load_user_skills: bool = False
load_public_skills: bool = False
secrets: Mapping[str, SecretValue] | None
current_datetime: datetime | None # Included in system prompt
get_system_message_suffix() assembles the dynamic context: repo skills content, available skills list, secrets, datetime, and runtime info. This is the dynamic block appended after the static system prompt for prompt caching.
9. Prompt Templates
Template System
Prompts use Jinja2 templates resolved from the agent's prompts/ directory. The system prompt is split into static (cacheable) and dynamic (per-conversation) parts.
System Prompt Structure
The main system_prompt.j2 assembles these XML-tagged sections:
| Section | Purpose |
|---|---|
<ROLE> | Primary role definition — execute commands, modify code, solve problems |
<MEMORY> | Use AGENTS.md as persistent memory (like OpenClaw!) |
<EFFICIENCY> | Combine actions, minimize operations |
<FILE_SYSTEM_GUIDELINES> | Don't assume paths, edit in-place, don't create file variants |
<CODE_QUALITY> | Minimal changes, clean code, understand before implementing |
<VERSION_CONTROL> | Git conventions, commit practices |
<PULL_REQUESTS> | PR creation rules — only when asked |
<PROBLEM_SOLVING_WORKFLOW> | 5-step: Explore → Analyze → Test → Implement → Verify |
<SELF_DOCUMENTATION> | Redirect capability questions to docs.openhands.dev |
<SECURITY> | Security policy (conditional include) |
<SECURITY_RISK_ASSESSMENT> | LOW/MEDIUM/HIGH risk classification |
<BROWSER_TOOLS> | Browser usage guidelines (conditional) |
<EXTERNAL_SERVICES> | Use APIs over browser; AI disclosure requirement |
<ENVIRONMENT_SETUP> | Install missing deps, use existing dependency files |
<TROUBLESHOOTING> | 5-7 possible causes, assess likelihood, address systematically |
<PROCESS_MANAGEMENT> | Use specific PIDs, not general pkill |
<IMPORTANT> | Model-specific instructions (auto-include based on model family) |
Model-Specific Overrides
Templates in model_specific/ are automatically included based on model family:
anthropic_claude.j2— Claude-specific instructionsgoogle_gemini.j2— Gemini-specific instructionsopenai_gpt/gpt-5.j2— GPT-5 instructionsopenai_gpt/gpt-5-codex.j2— Codex variant
In-Context Learning Example
The in_context_learning_example.j2 provides a full worked example of a task (Flask web app), demonstrating tool calling syntax with <function=terminal> XML format and security_risk + summary parameters on every action.
Planning Agent Prompt
A separate system_prompt_planning.j2 defines a Planning Agent variant with a 4-phase workflow: Initial Understanding → Planning → Synthesis & User Alignment → Refinement. This is used for sub-agents that only plan, not execute.
Condenser Summary Prompt
summarizing_prompt.j2 is used by LLMSummarizingCondenser to generate summaries of forgotten events.
10. Skills Framework
Skill Model
class Skill(BaseModel):
name: str
content: str # Skill instructions
trigger: TriggerType | None # When to activate
source: str | None # File path
mcp_tools: dict | None # MCP tool configs
is_agentskills_format: bool # SKILL.md standard
description: str | None # Brief description
version: str = "1.0.0" # AgentSkills standard
license: str | None # AgentSkills standard
resources: SkillResources | None # scripts/, references/, assets/
# AgentSkills standard: resources subdirectories
class SkillResources(BaseModel):
scripts: list[str] # Executable scripts
references: list[str] # Reference docs
assets: list[str] # Static assets
Trigger Types
KeywordTrigger— Auto-inject when keywords appear in user messagesTaskTrigger— Auto-inject for specific task types, may require user inputNone— Always active (AgentSkills: on-demand read; legacy: always in system prompt)
Skill Categories
- Repo skills — From repository's
.openhands/or.agents/directory - Knowledge skills — From public OpenHands skills repository
- AgentSkills — Standard
SKILL.mdfiles with frontmatter (cross-platform) - Third-party —
.cursorrules,agents.md,claude.md,gemini.md
Skill Injection
Skills are injected into the prompt in two ways:
<REPO_CONTEXT>— Full content of always-active skills (legacy, trigger=None)<available_skills>— Name + description list; agent callsinvoke_skillto load full content on demand
11. Security Model
Three-Layer Security
- LLM Security Risk Assessment — The LLM predicts a
security_risklevel (LOW/MEDIUM/HIGH) for each tool call - Confirmation Policy — Based on risk level, actions may require user confirmation before execution
- Security Analyzer — Programmatically analyzes pending actions for additional risk assessment
Risk Levels
# CLI mode:
LOW = Read-only actions
MEDIUM = Project-scoped edits/execution
HIGH = System-level, untrusted, or data exfiltration operations
# Sandbox mode:
LOW = Read-only inside container
MEDIUM = Container-scoped edits/installs
HIGH = Data exfiltration, host access, privilege breaks
Supply Chain Rules
Actions influenced by repository context (<UNTRUSTED_CONTENT>, AGENTS.md, .cursorrules) are automatically escalated to HIGH risk if they involve:
- Writing to package manager config files
- Adding custom registry URLs
- Embedding auth tokens/credentials
- Executing
curl|bashpatterns - Writing to system-wide config directories
- Adding lifecycle hooks that execute remote scripts
Hook System
HookEventProcessor processes events through configurable hooks that can block actions, modify messages, or inject followup events. Hooks are contributed by both explicit configuration and plugins.
12. Server / REST API
FastAPI Application
The web server is a FastAPI application with SQLAlchemy for persistence and WebSocket for real-time events:
app = FastAPI(
title='OpenHands',
lifespan=combine_lifespans(mcp_app.lifespan, app_lifespan_.lifespan),
routes=[Mount(path='/mcp', app=mcp_app)], # MCP proxy
)
API Routes (/api/v1)
| Router | Endpoints |
|---|---|
event_router | Event streaming (WebSocket) |
app_conversation_router | Conversation CRUD, start/stop |
pending_message_router | User confirmation responses |
sandbox_router | Docker sandbox management |
sandbox_spec_router | Sandbox configuration specs |
settings_router | LLM and agent settings |
secrets_router | API key/secret management |
user_router | User authentication |
skills_router | Skill management |
webhook_router | Event callback webhooks |
web_client_router | Web client configuration |
git_router | Git operations |
config_router | Configuration API |
MCP Proxy
Built-in FastMCP server at /mcp with a Tavily search proxy. The MCP server exposes tools and resources to MCP-compatible clients.
SPA Frontend
If SERVE_FRONTEND=true, the React frontend is served from ./frontend/build/ as a single-page application.
13. Key Patterns & Design Decisions
1. Event-Sourced Architecture
The entire system is built on an append-only event log. This enables:
- Full conversation replay and debugging
- Context window management via condensation (forget + summarize)
- Persistence and restoration of conversations
- Hook interception at any point in the event stream
2. Static/Dynamic Prompt Split for Caching
The system prompt is split into:
- Static content — The full Jinja2-rendered system prompt (cacheable across conversations)
- Dynamic context — Per-conversation info (skills, secrets, datetime) appended as a second content block
This enables Anthropic-style prompt caching where the static prefix is cached and only the dynamic suffix changes.
3. Discriminated Union Types
Events, Actions, Observations, and Tools all use DiscriminatedUnionMixin — a Pydantic pattern where each concrete subclass has a unique kind field for serialization/deserialization. This enables:
- Polymorphic event streams with type-safe deserialization
- Schema evolution without breaking changes
- Clean MCP tool export
4. Lazy Initialization
Many components use lazy initialization to defer I/O:
- Plugins are loaded on first
run()/send_message(), not in the constructor - Tools are initialized after plugins are loaded (they may contribute tools)
- System prompt is rendered once and cached (
_pin_prompt_cache_key())
5. Atomic Tool Call Batches
When the LLM returns parallel tool calls, they are treated as an atomic batch. The manipulation_indices in the View ensure that condensation boundaries respect these atomic units — you never split a group of parallel calls.
6. Iterative Refinement on Finish
When the agent calls FinishTool, the system checks for iterative refinement — if the agent's finish reason is insufficient, it injects a followup message to continue working. This prevents premature termination.
14. Strengths & Weaknesses
✅ Strengths
- Clean SDK separation — The core agent logic is a standalone pip-installable package, independent of the web server
- Event-sourced architecture — Append-only event log enables replay, debugging, and context management
- Sophisticated context management — LLM-based condensation with atomic boundary awareness, hard reset fallback, and minimum progress guarantees
- Parallel tool execution — Resource-aware parallel execution with declared resources and automatic serialization
- Security in depth — LLM risk assessment + confirmation policy + security analyzer + supply chain rules
- AgentSkills standard — Cross-platform skill format with progressive disclosure (name+description always visible, full content on demand)
- LiteLLM abstraction — Unified interface to 100+ LLM providers with automatic fallback and retry
- Model-specific prompt optimization — Automatic prompt customization per model family
- Static/dynamic prompt split — Enables cross-conversation prompt caching for cost optimization
- MCP integration — First-class MCP tool support and built-in MCP proxy server
❌ Weaknesses
- Pydantic frozen model constraints — All events and agents are frozen, requiring
model_copy(update=...)for any mutation; the_stateis a plain object, not a Pydantic model - Condenser complexity — The LLMSummarizingCondenser is sophisticated but has many failure modes (NoCondensationAvailableException, minimum_progress threshold, hard reset retries); debugging condensation issues is hard
- No built-in vector memory — Unlike frameworks with RAG/memory stores, OpenHands relies purely on the event stream + condensation summaries; long-term cross-session memory is limited to AGENTS.md (file-based)
- Single-agent bias — While DelegateTool exists, multi-agent coordination is not a first-class concept; the TaskToolSet is the newer approach but still evolving
- Complex error handling in step() — The step() method has deeply nested try/except chains for FunctionCallValidationError, LLMMalformedConversationHistoryError, LLMContextWindowExceedError, etc.
- Plugin I/O in runtime path — Lazy loading helps, but plugins fetch from GitHub/git on first run, which can fail and adds latency
- Template language divergence — The in-context learning example uses
<function=terminal>XML format, but actual tool calls are JSON; this can confuse models - Server layer is monolithic — While the SDK is well-factored, the app_server has 8+ database migrations, tightly coupled services, and mixed responsibilities
15. Comparison with Other Frameworks
| Feature | OpenHands | OpenClaw | LangChain/LangGraph | CrewAI |
|---|---|---|---|---|
| Core Architecture | Event-sourced agent loop | Event-driven + cron/heartbeat | Graph-based state machine | Role-based multi-agent |
| Context Management | LLM condensation | Vector memory (LanceDB) | Various retrievers | Delegation-based |
| Tool System | ToolDefinition + MCP | Skills + tools | Tool/StructuredTool | Tool decorators |
| Parallel Execution | Resource-aware thread pool | Subagent spawning | Parallel node execution | Sequential by default |
| Sandboxing | Docker container | Sandbox + node exec | None built-in | None built-in |
| Security | LLM risk + confirmation | Approval system | None built-in | None built-in |
| SDK Usability | Standalone pip package | CLI + API | Pip package | Pip package |
| Web UI | React SPA built-in | Canvas | LangSmith (separate) | None built-in |
| Skill Format | AgentSkills standard | SKILL.md (AgentSkills) | None standard | None standard |
| Prompt Caching | Static/dynamic split | Not built-in | Not built-in | Not built-in |
AGENTS.md. There's no vector memory or semantic retrieval — the condenser only compresses within a single conversation. For persistent knowledge, you need external tooling.
Lessons for OpenClaw
- Static/dynamic prompt split — OpenClaw could adopt this pattern for prompt caching, splitting system prompt into cacheable static part + per-conversation dynamic suffix
- LLM-based condensation — OpenClaw's vector memory is more flexible, but an LLM-based summarization fallback could help when the event stream exceeds context
- Resource-aware parallel execution — The
DeclaredResourcespattern is elegant and could improve OpenClaw's tool execution - Security risk assessment — Having the LLM predict risk levels + confirmation policy + supply chain rules is a comprehensive security model
- AgentSkills standard — Both frameworks already use this; continued alignment is valuable