OpenHands v1.22.1 SDK

Deep architecture analysis of the OpenHands (formerly OpenDevin) coding agent framework

Analyzed: 2026-05-17 · Source: openhands-sdk + openhands-tools PyPI packages · Repo: github.com/All-Hands-AI/OpenHands

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:

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.

Key Insight: OpenHands is fundamentally event-driven. The entire agent loop revolves around an immutable, append-only event stream. Every LLM call, tool execution, user message, and system action is an Event object. The condenser compresses this stream for context window management.

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

PackageResponsibilityKey Modules
openhands-sdkCore agent logic, LLM, events, conversation, context, securityagent/, llm/, event/, conversation/, context/, security/, skills/, tool/, mcp/
openhands-toolsBuilt-in tool implementationsterminal/, file_editor/, browser_use/, delegate/, task/, apply_patch/, glob/, grep/
openhands (main)Web app, REST API, WebSocket, deploymentapp_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:

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:

  1. Check pending actions — If there are unmatched actions (implicit confirmation mode), execute them first
  2. Check blocked messages — If a hook blocked the user message, finish
  3. Prepare LLM messages — Convert event stream → message list via prepare_llm_messages(), applying condensation if needed
  4. Handle condensation — If context window exceeded, emit CondensationRequest and return
  5. Call LLMmake_llm_completion(llm, messages, tools, on_token)
  6. Classify response:
    • TOOL_CALLS → parse tool calls, create ActionEvents, execute in parallel
    • CONTENT → emit MessageEvent (agent speaks to user)
    • REASONING_ONLY / EMPTY → inject followup to continue
Parallel Tool Execution: OpenHands uses a 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 — 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:

  1. Ensure agent is ready (lazy plugin loading, tool initialization)
  2. Loop max_iteration_per_run times:
    • Call agent.step()
    • Check stuck detection
    • Check execution status (FINISHED, AWAITING_CONFIRMATION, etc.)
    • Apply condensation if needed
  3. 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:

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:

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)

ToolActionDescription
TerminalToolExecute bash commandstmux/subprocess/PowerShell backends; supports Ctrl+C, timeout, reset; auto-detects platform
FileEditorToolstr_replace on filesCreate, edit, view files with string replacement
ApplyPatchToolApply unified diffsApply patch format edits to files
BrowserUseToolWeb browsingNavigate, click, type in web pages via browser
DelegateToolSpawn sub-agentsDeprecated in favor of TaskToolSet
TaskToolSetSub-agent delegationSpawn and manage sub-agents for sub-tasks
GlobToolFile pattern searchFind files by glob pattern
GrepToolContent searchSearch file contents with regex
FinishToolEnd conversationSignal task completion
ThinkToolInternal reasoningStructured 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:

  1. Identify events to forget (respecting keep_first and atomic boundaries via manipulation_indices)
  2. Use a separate LLM to generate a summary of forgotten events
  3. Create a Condensation event with forgotten IDs and summary
  4. 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:

SectionPurpose
<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:

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

Skill Categories

Skill Injection

Skills are injected into the prompt in two ways:

  1. <REPO_CONTEXT> — Full content of always-active skills (legacy, trigger=None)
  2. <available_skills> — Name + description list; agent calls invoke_skill to load full content on demand

11. Security Model

Three-Layer Security

  1. LLM Security Risk Assessment — The LLM predicts a security_risk level (LOW/MEDIUM/HIGH) for each tool call
  2. Confirmation Policy — Based on risk level, actions may require user confirmation before execution
  3. 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:

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)

RouterEndpoints
event_routerEvent streaming (WebSocket)
app_conversation_routerConversation CRUD, start/stop
pending_message_routerUser confirmation responses
sandbox_routerDocker sandbox management
sandbox_spec_routerSandbox configuration specs
settings_routerLLM and agent settings
secrets_routerAPI key/secret management
user_routerUser authentication
skills_routerSkill management
webhook_routerEvent callback webhooks
web_client_routerWeb client configuration
git_routerGit operations
config_routerConfiguration 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:

2. Static/Dynamic Prompt Split for Caching

The system prompt is split into:

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:

4. Lazy Initialization

Many components use lazy initialization to defer I/O:

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 _state is 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

FeatureOpenHandsOpenClawLangChain/LangGraphCrewAI
Core ArchitectureEvent-sourced agent loopEvent-driven + cron/heartbeatGraph-based state machineRole-based multi-agent
Context ManagementLLM condensationVector memory (LanceDB)Various retrieversDelegation-based
Tool SystemToolDefinition + MCPSkills + toolsTool/StructuredToolTool decorators
Parallel ExecutionResource-aware thread poolSubagent spawningParallel node executionSequential by default
SandboxingDocker containerSandbox + node execNone built-inNone built-in
SecurityLLM risk + confirmationApproval systemNone built-inNone built-in
SDK UsabilityStandalone pip packageCLI + APIPip packagePip package
Web UIReact SPA built-inCanvasLangSmith (separate)None built-in
Skill FormatAgentSkills standardSKILL.md (AgentSkills)None standardNone standard
Prompt CachingStatic/dynamic splitNot built-inNot built-inNot built-in
OpenHands' Unique Strength: The combination of event-sourced architecture, LLM-based context condensation, resource-aware parallel tool execution, and sandbox-first execution makes it the most production-ready coding agent framework for autonomous code generation and debugging tasks.
OpenHands' Key Gap: Cross-session memory is limited to file-based 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

  1. Static/dynamic prompt split — OpenClaw could adopt this pattern for prompt caching, splitting system prompt into cacheable static part + per-conversation dynamic suffix
  2. LLM-based condensation — OpenClaw's vector memory is more flexible, but an LLM-based summarization fallback could help when the event stream exceeds context
  3. Resource-aware parallel execution — The DeclaredResources pattern is elegant and could improve OpenClaw's tool execution
  4. Security risk assessment — Having the LLM predict risk levels + confirmation policy + supply chain rules is a comprehensive security model
  5. AgentSkills standard — Both frameworks already use this; continued alignment is valuable