02 — Core Business Logic & Data Flow
Agent System
Registry Pattern
All agents are registered via a class-level registry on the Agent ABC
(openhands/controller/agent.py:33-169):
class Agent(ABC):
_registry: dict[str, type['Agent']] = {}
@classmethod
def register(cls, name: str, agent_cls: type['Agent']) -> None:
if name in cls._registry:
raise AgentAlreadyRegisteredError(name)
cls._registry[name] = agent_cls
@classmethod
def get_cls(cls, name: str) -> type['Agent']:
if name not in cls._registry:
raise AgentNotRegisteredError(name)
return cls._registry[name]Agents self-register when their modules are imported (via import openhands.agenthub in main.py:16).
Agent Base Class
The Agent ABC defines the minimal interface every agent must implement:
| Method/Property | Purpose |
|---|---|
step(state) -> Action |
Core method: given current state, produce the next action |
reset() |
Reset completion status between runs |
name |
Returns the class name |
get_system_message() |
Generates the system prompt as a SystemMessageAction |
set_mcp_tools() |
Registers MCP (Model Context Protocol) tools |
CodeActAgent — Primary Implementation
File: openhands/agenthub/codeact_agent/codeact_agent.py:57-229
CodeActAgent (v2.2) implements the CodeAct paradigm: consolidating agent
actions into a unified code action space. At each turn, the agent can either
converse in natural language or execute code (bash, Python, file edits, browser
actions).
Key components initialized in __init__ (line 86):
tools: List of available tool definitions (bash, think, finish, browser, IPython, editor, MCP)conversation_memory:ConversationMemoryfor building LLM message historycondenser:Condenserfor history truncation strategiesllm: May be overridden with a router for multi-model support
Agent Controller
File: openhands/controller/agent_controller.py:109-1387
The AgentController is the orchestration engine. It:
- Subscribes to the
EventStreamto receive events - Determines when the agent should step (via
should_step(), line 410) - Executes the agent step loop (via
_step(), line 860) - Manages delegation to sub-agents
- Detects stuck agents via
StuckDetector(line 199) - Tracks iteration and budget limits via
StateTracker
Step Flow
AgentController._step() [line 860]
│
├── 1. Check state == RUNNING [line 862]
├── 2. Check no pending action [line 870]
├── 3. Sync budget flag with metrics [line 887]
├── 4. Run stuck detection [line 888]
├── 5. Run control flags (iteration/budget) [line 895]
│
├── 6a. If replay mode → replay action [line 903-906]
│
└── 6b. Call agent.step(state) → Action [line 909]
│
├── Handle recoverable errors [line 913-926]
│ (malformed action, no action,
│ validation errors)
│
├── Handle context window exceeded [line 927-954]
│ → trigger condensation
│
└── Process action [line 975-1032]
├── If runnable + confirmation mode
│ → security analysis
│ → maybe AWAITING_USER_CONFIRMATION
├── Set as pending action
├── Attach metrics for frontend
└── Add to EventStream
Event Handling
The controller's on_event() method (line 451) is the callback registered with
the EventStream. It:
- Forwards events to the delegate if one is active
- Adds events to state history via
StateTracker - Handles actions (
_handle_action, line 512) — state changes, delegation, finish - Handles observations (
_handle_observation, line 539) — clears pending actions - Triggers the next step if appropriate (
should_step, line 410)
Agent Step Flow (CodeActAgent)
CodeActAgent.step(state) [line 169]
│
├── 1. Pop pending actions (if any) [line 192]
│
├── 2. Check for /exit command [line 196]
│
├── 3. Condense history [line 207]
│ condenser.condensed_history(state)
│ → View(events) or Condensation(action)
│
├── 4. Build messages [line 220]
│ conversation_memory processes events
│ into LLM-ready Message objects
│
├── 5. LLM completion [line 223-229]
│ params = {messages, tools, extra_body}
│ response = llm.completion(**params)
│
└── 6. Parse response → Action(s)
function_calling.response_to_actions()
→ one or more Action objects
Event System
EventStream
File: openhands/events/stream.py:43-291
The EventStream is the central pub/sub hub connecting all components. It
extends EventStore for persistence and provides:
- Publishing:
add_event(event, source)— assigns an ID, timestamp, replaces secrets, persists toFileStore, and queues for delivery (line 163) - Subscribing:
subscribe(subscriber_id, callback, callback_id)— registers a callback with a dedicated thread pool for isolation (line 130) - Secret management:
set_secrets()/_replace_secrets()— redacts sensitive values before persistence (line 221)
Subscribers
The EventStreamSubscriber enum (line 23) defines the subscriber types:
| Subscriber | Purpose |
|---|---|
AGENT_CONTROLLER |
Processes actions/observations, triggers agent steps |
RUNTIME |
Executes actions in the sandbox, produces observations |
MEMORY |
Handles RecallAction events for microagent retrieval |
SERVER |
Forwards events to the frontend via WebSocket |
MAIN |
CLI mode: handles user input prompts |
RESOLVER |
OpenHands resolver (automated PR/issue handling) |
TEST |
Test infrastructure |
Event Dispatch
Events are dispatched in a dedicated queue thread (_run_queue_loop, line 246).
Each subscriber gets its own ThreadPoolExecutor (1 worker) for isolation. Events
are delivered to all subscribers in sorted key order.
Event Types
All events derive from the Event base dataclass (openhands/events/event.py:26).
Events split into two hierarchies:
Event
├── Action (agent/user intents)
│ ├── CmdRunAction
│ ├── IPythonRunCellAction
│ ├── FileEditAction
│ ├── FileReadAction
│ ├── FileWriteAction
│ ├── BrowseInteractiveAction
│ ├── BrowseURLAction
│ ├── MessageAction
│ ├── SystemMessageAction
│ ├── AgentFinishAction
│ ├── AgentRejectAction
│ ├── AgentDelegateAction
│ ├── AgentThinkAction
│ ├── ChangeAgentStateAction
│ ├── CondensationAction
│ ├── CondensationRequestAction
│ ├── RecallAction
│ ├── LoopRecoveryAction
│ ├── MCPAction
│ └── NullAction
│
└── Observation (environment results)
├── CmdOutputObservation
├── FileReadObservation
├── FileWriteObservation
├── FileEditObservation
├── BrowserOutputObservation
├── ErrorObservation
├── AgentStateChangedObservation
├── AgentDelegateObservation
├── AgentThinkObservation
├── RecallObservation
├── LoopDetectionObservation
├── UserRejectObservation
├── NullObservation
└── TaskTrackingObservation
Each Event carries:
id: Auto-incremented integer (line 37)timestamp: ISO-format datetime (line 44)source:EventSource—AGENT,USER, orENVIRONMENT(line 56)cause: Optional ID of the action that caused this observation (line 63)tool_call_metadata: Optional LLM tool call info (line 102)llm_metrics: Optional cost/token metrics (line 90)
Delegation
The controller supports hierarchical agent delegation for subtasks
(openhands/controller/agent_controller.py:732-858).
Parent AgentController
│
├── agent.step() returns AgentDelegateAction
│
├── start_delegate() [line 732]
│ ├── Look up delegate agent class from registry
│ ├── Create new Agent instance
│ ├── Create child AgentController (is_delegate=True)
│ ├── Child subscribes to EventStream starting from current position
│ └── Post task as MessageAction to delegate
│
├── [delegate runs its own step loop]
│
└── end_delegate() [line 793]
├── Accumulate metrics
├── Close delegate controller
├── Create AgentDelegateObservation with results
└── Resume parent processing
When a delegate is active, the parent's on_event() (line 451) forwards all
events to the delegate instead of processing them itself. The parent only resumes
when the delegate reaches a terminal state (FINISHED, ERROR, REJECTED).
Memory & Condensation
Memory Component
File: openhands/memory/memory.py:42-406
Memory is an EventStream subscriber that handles information retrieval. It
responds to RecallAction events by producing RecallObservation events with:
- Workspace context (first user message): repository info, runtime info, repo instructions from microagents, and matched knowledge microagents
- Knowledge recall (subsequent messages): triggered microagent knowledge based on keyword matching
Microagents
Microagents are loaded from three sources:
- Global:
skills/directory in the repo (line 34) - User:
~/.openhands/microagents/(line 39) - Repository:
.openhands/microagents/in the workspace and optional org-level repository
Two types:
RepoMicroagent: Always-active instructions for a specific repositoryKnowledgeMicroagent: Triggered by keyword matching against user messages
History Condensation
The Condenser system (openhands/memory/condenser/) manages conversation
history truncation to stay within LLM context windows. Strategies include:
- NoOpCondenser: Pass through all events unchanged
- ObservationMaskingCondenser: Mask observation contents
- RecentEventsCondenser: Keep only recent N events
- LLMSummarizingCondenser: Use an LLM to summarize older history
- AmortizedForgettingCondenser: Gradually forget older events
The condenser returns either a View (filtered events for the agent) or a
Condensation (an action that modifies the event history, requiring a re-step).