4. Entry Points & Execution Flow
openhands-2
4. Entry Points & Execution Flow
Entry Points
1. Web Server (Primary)
File: openhands/server/__main__.py (30 lines)
# Runs uvicorn on port 3000 (configurable via PORT env var)
# Entry: openhands.server.listen:appApp definition: openhands/server/app.py:74
app = FastAPI(
title='OpenHands',
description='Code Less, Make More',
version=get_version(),
lifespan=combine_lifespans(*lifespans),
)Lifespan events manage startup/shutdown:
- Initialize
ConversationManager(manages agent sessions) - Start MCP server at
/mcp/mcp - Clean up on shutdown
Startup sequence:
uvicorn starts
└── FastAPI lifespan __aenter__
├── ConversationManager.__aenter__()
│ └── Initialize session tracking, cleanup routines
├── MCP server mount at /mcp/mcp
└── Static file serving (frontend build at /assets)
2. Docker Deployment
File: containers/app/Dockerfile
Multi-stage build:
1. Frontend Builder: Node 25.8 → npm build
2. Backend Builder: Python 3.13 + Poetry → install deps
3. Runtime: Copy artifacts + entrypoint
CMD: uvicorn openhands.server.listen:app --host 0.0.0.0 --port 3000
File: docker-compose.yml
services:
openhands:
image: ghcr.io/openhands/agent-server:1.15.0-python
ports: ["3000:3000"]
volumes: ["/var/run/docker.sock:/var/run/docker.sock"] # Docker-in-Docker3. CLI
Entry: openhands/runtime/impl/cli/ -- CLI-specific runtime that runs agents interactively in the terminal.
4. Makefile Targets
make run # Start backend (port 3000) + frontend (port 3001)
make start-backend # poetry run uvicorn openhands.server.listen:app --reload
make start-frontend # cd frontend && npm run dev
make docker-run # docker-compose upMain Execution Flow: User Task → Completion
Phase 1: Session Creation
1. User connects to web UI (localhost:3000)
2. Frontend loads React SPA
3. User creates conversation:
POST /api/conversations → ConversationManager.attach_to_conversation()
4. WebSocket connects:
Socket.IO /socket.io?conversation_id=xxx
→ listen_socket.py:connect()
→ Validate authentication
→ Create/load EventStore
→ Replay existing events
→ Join conversation
Key file: openhands/server/listen_socket.py:43-148
@sio.event
async def connect(connection_id, environ):
# Parse conversation_id from query
# Validate session API key
# Validate user via cookies/authorization
# Create EventStore
# Replay all previous events
# Join conversation via conversation_managerPhase 2: Agent Loop Startup
5. User sends first message:
MessageAction(content="Fix the bug in auth.py", source=USER)
→ EventStream.add_event()
6. AgentController.on_event(MessageAction):
→ Add to state.history
→ _handle_message_action()
→ Create RecallAction(WORKSPACE_CONTEXT)
→ Triggers Memory system
7. Memory._on_workspace_context_recall():
→ Load repo microagents (.openhands/microagents/repo.md)
→ Find matching knowledge microagents
→ Emit RecallObservation with:
- Repository name, directory, branch
- Repository instructions
- Runtime info (working dir, hosts, date)
- Microagent knowledge list
Key file: openhands/memory/memory.py:140-223
Phase 3: Agent Step Loop
8. AgentController._step():
a. Check preconditions:
- agent_state == RUNNING
- no pending_action (waiting for observation)
- iteration/budget not exceeded
- not stuck (StuckDetector)
b. Call agent.step(state):
→ CodeActAgent.step() at codeact_agent.py:168-237
Inside CodeActAgent.step():
def step(self, state: State) -> Action:
# 1. Return pending actions from queue if any
if self.pending_actions:
return self.pending_actions.popleft()
# 2. Check for /exit command
# 3. Condense history if needed
condensed = self.condenser.condensed_history(state)
if isinstance(condensed, Condensation):
return condensed.action # CondensationAction
# 4. Build LLM messages from event history
messages = self.conversation_memory.process_events(
condensed_history=view.events,
initial_user_action=...,
forgotten_event_ids=...,
max_message_chars=...,
vision_is_active=self.llm.vision_is_active(),
)
# 5. Apply prompt caching if enabled
if self.llm.is_caching_prompt_active():
messages[-1].cache_enabled = True
# 6. Call LLM
response = self.llm.completion(
messages=self.llm.format_messages_for_llm(messages),
tools=self.tools,
)
# 7. Convert response to actions
actions = response_to_actions(response, self.mcp_tool_names)
# 8. Queue and return first action
self.pending_actions.extend(actions[1:])
return actions[0]Phase 4: Action Execution
9. AgentController receives action from agent.step():
a. Security analysis:
→ security_analyzer.security_risk(action)
→ Sets ActionSecurityRisk (LOW/MEDIUM/HIGH)
b. Confirmation mode check:
→ If HIGH risk and confirmation_mode=True:
→ Set state to AWAITING_USER_CONFIRMATION
→ Wait for user CONFIRMED/REJECTED
c. Publish to EventStream:
→ EventStream.add_event(action, source=AGENT)
10. Runtime receives action via EventStream subscription:
→ DockerRuntime.on_event(CmdRunAction)
→ HTTP POST to sandbox container Action Execution Server
→ Bash/IPython/Browser/FileOps executed inside container
→ Return Observation
11. Runtime publishes observation:
→ EventStream.add_event(CmdOutputObservation, source=ENVIRONMENT)
12. Server streams to frontend:
→ Socket.IO emit('oh_event', observation)
→ User sees command output in terminal
Phase 5: Loop Continuation
13. AgentController.on_event(CmdOutputObservation):
→ Add to state.history
→ should_step() → True
→ Back to step 8 (call agent.step again)
14. Repeat until:
- Agent returns AgentFinishAction (task done)
- Max iterations reached
- Budget exceeded
- Agent gets stuck (StuckDetector)
- User stops the agent
- Unrecoverable error
Phase 6: Completion
15. AgentFinishAction received:
→ AgentController sets state to FINISHED
→ Publishes AgentStateChangedObservation
→ Server emits final state to frontend
→ User sees completion message
Key State Transitions
| From | Trigger | To |
|---|---|---|
| LOADING | Controller initialized | RUNNING |
| RUNNING | Agent returns action | RUNNING (loop continues) |
| RUNNING | Agent returns AgentFinishAction | FINISHED |
| RUNNING | High-risk action + confirmation mode | AWAITING_USER_CONFIRMATION |
| RUNNING | Agent needs user input | AWAITING_USER_INPUT |
| RUNNING | Max iterations reached | ERROR |
| RUNNING | Budget exceeded | ERROR |
| RUNNING | LLM rate limit | RATE_LIMITED |
| RUNNING | Unrecoverable error | ERROR |
| AWAITING_USER_CONFIRMATION | User confirms | RUNNING |
| AWAITING_USER_CONFIRMATION | User rejects | STOPPED |
| AWAITING_USER_INPUT | User sends message | RUNNING |
| RATE_LIMITED | Retry succeeds | RUNNING |
| Any | User clicks stop | STOPPED |
Iteration & Budget Control
File: openhands/controller/state/control_flags.py
class IterationControlFlag:
current_value: int = 0
max_value: int = 100 # configurable via OH_MAX_ITERATIONS
def step(self):
self.current_value += 1
if self.reached_limit():
raise RuntimeError("Max iterations reached")
class BudgetControlFlag:
current_value: float = 0.0
max_value: float = 0.0 # configurable via max_budget_per_task
def step(self):
if self.reached_limit():
raise RuntimeError("Budget exceeded")Stuck Detection (openhands/controller/stuck.py)
Detects 5 types of loops:
| Loop Type | Detection | Threshold |
|---|---|---|
| Repeating action-observation | 4 identical pairs | 4 |
| Repeating action-error | Same action → same error | 3 |
| Agent monologue | Agent talking to itself | Variable |
| Pattern loop | 6-step repeating pattern | 6 events |
| Context window errors | Repeated context errors | 10 events |
When stuck is detected:
- Headless mode: Raise
AgentStuckInLoopError→ state ERROR - Interactive mode: Create
LoopDetectionObservationwith recovery options, pause agent - CLI mode:
attempt_loop_recovery()→ truncate memory, restart from last user message
Error Recovery Flows
Rate Limit Recovery
RateLimitError raised
→ _react_to_exception() catches it
→ Check retry count vs num_retries (default 5)
→ If retries remain:
→ tenacity exponential backoff (8s min, 64s max, 8x multiplier)
→ Retry LLM call
→ If retries exhausted:
→ Set state to RATE_LIMITED with message
Context Window Recovery
ContextWindowExceededError raised
→ _react_to_exception() catches it
→ If condensation enabled:
→ Trigger CondensationRequestAction
→ Condenser summarizes old events
→ Retry with compressed context
→ If condensation disabled:
→ Raise LLMContextWindowExceedError → state ERROR
LLM No Response Recovery (retry_mixin.py:46-60)
LLMNoResponseError raised
→ Before retry callback:
→ If temperature == 0:
→ Temporarily set temperature = 1.0
→ "Adding randomness to avoid repeated empty responses"
→ Retry with increased randomness