CodeDocs Vault

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

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

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-Docker

3. 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 up

Main 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_manager

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

  1. Headless mode: Raise AgentStuckInLoopError → state ERROR
  2. Interactive mode: Create LoopDetectionObservation with recovery options, pause agent
  3. 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