# OpenHands/OpenHands

> Agent Canvas: a React control plane that configures and streams OpenHands or ACP agents running in a separate Python Agent Server.

- Category: [Open-source coding agents](https://llms-technical-reviews.com/coding-agents/)
- Repository: https://github.com/OpenHands/OpenHands (reviewed at commit `b0a1a2d1368a50a890b69ef45c54e1a74140b677`, 2026-10-06)
- Stars: 90112 · Language: TypeScript · License: MIT
- Canonical page: https://llms-technical-reviews.com/p/openhands/

## Overview

At this commit, the `OpenHands/OpenHands` repository no longer contains the OpenHands agent. It contains **Agent Canvas**: a React 19 / React Router web app, an npm launcher, an Electron shell and a Docker image. Together they start, watch and steer coding-agent conversations that run inside a separate Python **Agent Server**. The repo's own contributor notes say this directly. Agents, tools, conversations, events and the REST/WebSocket contract belong to `OpenHands/software-agent-sdk`. This repo owns "Agent Canvas UI, frontend state, backend selection, frontend service integration, and local-stack orchestration".

So if you want to read how the agent decides, edits or sandboxes, the code is not here. What is here is the control plane. It decides *which* agent runs (the native OpenHands `CodeActAgent` through LiteLLM, or an external ACP agent such as Claude Code, Codex or Gemini CLI), with which model, skills, MCP servers, hooks, confirmation policy and security analyzer, in which workspace (local folder, git worktree or Docker), and on which backend (local Agent Server or OpenHands Cloud). It also renders the event stream and handles approvals, condensation, planning and child conversations.

It is aimed at developers who want one self-hosted dashboard for several agents and backends, plus scheduled or webhook-triggered automations. It is not a terminal agent.

## Architecture

```mermaid
flowchart LR
  USER["Browser / Electron"] --> UI["Canvas React app"]
  UI --> SVC["ConversationService"]
  SVC --> AD["agent-server-adapter"]
  AD --> TSC["@openhands/typescript-client"]
  TSC -->|"REST"| AS["Agent Server (Python SDK)"]
  AS -->|"WebSocket frames"| WS["ConversationWebSocketProvider"]
  WS --> STORE["Zustand event store"]
  STORE --> UI
  AS --> OH["OpenHands agent via LiteLLM"]
  AS --> ACP["ACP subprocess: Claude Code / Codex / Gemini"]
  AS --> WSP["Local, worktree or Docker workspace"]
  LAUNCH["agent-canvas launcher"] --> AS
  LAUNCH --> AUTO["Automation backend"]
  LAUNCH --> ING["Ingress proxy + static build"]
```

| Component | Path | Role |
|---|---|---|
| Launcher | `bin/agent-canvas.mjs`, `scripts/dev-*.mjs` | Starts Agent Server and the automation backend through `uvx`, plus the static frontend behind an ingress proxy |
| Version pins | `config/defaults.json` | Agent Server and automation versions, minimum compatible server, ports, paths |
| Conversation builder | `src/api/agent-server-adapter.ts` | Turns saved settings into the `StartConversationRequest` (agent kind, tools, skills, policy, workspace) |
| Conversation service | `src/api/conversation-service/` | Create, send, condense, fork, switch profile/model; local vs cloud paths |
| Event stream | `src/contexts/conversation-websocket-context.tsx` | Routes `/sockets/session/{id}` frames into the event store, with streaming deltas and resume cursor |
| Wire types | `src/types/agent-server/` | TypeScript mirrors of SDK actions, observations and events |
| Client tools | `src/api/canvas-ui-client-tool.ts`, `launch-child-conversation-client-tool.ts` | Tools the agent calls that Canvas itself fulfils |
| Backends | `src/api/backend-registry/` | Local and cloud backend records, health and auth |
| Desktop / Docker | `electron/`, `docker/` | Desktop packaging; an image layered on the Agent Server image |

## How a request flows

1. **Stack startup.** `npx @openhands/agent-canvas` checks for a frontend build and hands off to `dev-with-automation.mjs` in static mode ([agent-canvas.mjs](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/bin/agent-canvas.mjs#L140-L177)). The launcher builds the Agent Server command (a pinned PyPI version, a git ref or a local checkout) and spawns it with its own state directory and ports ([dev-static.mjs](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/scripts/dev-static.mjs#L321-L360), [defaults.json](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/config/defaults.json#L1-L20)).
2. **Create.** A new chat calls the conversation service. On a local backend, it resolves a working directory (by default a new git worktree), then builds the payload with encrypted settings and posts it through `ConversationClient.createConversation` ([agent-server-conversation-service.api.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/conversation-service/agent-server-conversation-service.api.ts#L533-L578)).
3. **Pick the agent.** `buildConfiguredAgentSettings` branches on `agent_kind`. ACP agents get an ACP command (the provider's default if none is set), a model, and forwarded MCP config. OpenHands agents get a normalised LiteLLM config with `stream = true`, plus skills and tools ([agent-server-adapter.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L959-L1091)).
4. **Wrap the conversation.** `buildStartConversationRequest` adds the workspace (`DockerExecutionWorkspace` or `LocalWorkspace`), the confirmation policy, `max_iterations` (default 500), `stuck_detection: true`, the two Canvas client tools (OpenHands agents only), hooks, secrets as `LookupSecret` references, and the security analyzer ([agent-server-adapter.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L1261-L1365), [L1112-L1127](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L1112-L1127)).
5. **Run.** The Agent Server runs the agent loop: LLM call, action, observation, repeat. None of that code is in this repository.
6. **Stream back.** Canvas subscribes to `/sockets/session/{id}`. `routeSessionFrame` sends `delta` frames to a batcher for token streaming, opens and aborts streaming slots, advances the resume cursor on `durable` frames, and shows `error` frames ([conversation-websocket-context.tsx](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/contexts/conversation-websocket-context.tsx#L137-L187)).
7. **Approve.** When the server reports `waiting_for_confirmation`, the confirmation buttons call `EventService.respondToConfirmation` ([event-service.api.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/event-service/event-service.api.ts#L40-L55)).

## Key components

### The conversation adapter

`agent-server-adapter.ts` (about 1,800 lines) is the most important file in the repo. It is where Canvas decides what an agent *is*. Confirmation is a small mapping. `confirmation_mode` off gives `NeverConfirm`. On with the LLM analyzer, it gives `ConfirmRisky` at threshold `HIGH`, with unknown risk also confirmed. Otherwise it gives `AlwaysConfirm`. The analyzer itself is `LLMSecurityAnalyzer`, `PatternSecurityAnalyzer` or `PolicyRailSecurityAnalyzer` ([agent-server-adapter.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L730-L755)). The shipped defaults are `confirmation_mode: false` with the LLM analyzer, a 240-event condenser, and `openai/gpt-5.6-sol` as the model ([settings.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/services/settings.ts#L5-L59)). Out of the box, nothing asks before running.

### Local planner

"Create a Plan" starts a second, hidden conversation that is linked to the parent. Its raw agent spec has only `glob`, `grep` and a planning file editor scoped to `PLAN.md`, its own system prompt, an `LLMSummarizingCondenser` (`max_size` 100, `keep_first` 6) and `NeverConfirm` ([agent-server-adapter.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L1450-L1572)). This is a clean way to keep planning read-only. The tool list itself enforces it, not a prompt.

### Client tools

Canvas registers tools that the agent calls but the browser fulfils. `canvas_ui_control` switches the right-hand panel to a file, preview or tab, and its description tells the model when to call it ([canvas-ui-client-tool.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/canvas-ui-client-tool.ts#L61-L91)). `launch_child_conversation` lets the agent start an independent child conversation locally or on OpenHands Cloud.

### Wire types

`src/types/agent-server/` mirrors the SDK's event model. Editing goes through `FileEditorAction` with `view`, `create`, `str_replace`, `insert` and `undo_edit` ([action.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/action.ts#L65-L95)). Subagents use `TaskAction` with a `subagent_type` and optional `resume` ([action.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/action.ts#L282-L300)). Context compaction arrives as a `CondensationEvent` listing forgotten event ids and a summary ([condensation-event.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/events/condensation-event.ts#L5-L27)). These types are the best map of what the server-side agent can do, but they are copies, and the server is the authority.

## Extending it

- **Skills**: public skills are bundled from `@openhands/extensions`. User and project skills are loaded by the server, and per-skill enable/disable lists live in settings.
- **MCP**: stdio, SSE and HTTP servers in `mcp_config`. For ACP agents they are forwarded to the subprocess.
- **Hooks**: `.openhands/hooks.json` in the workspace root, loaded through the Agent Server's hooks endpoint ([hooks-service.ts](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/hooks-service.ts#L1-L30)).
- **ACP agents**: any agent with an ACP command line can be a backend agent, chosen in settings.
- **Canvas extensions**: manifest-described pages rendered in iframes, installed from git or local paths.
- **Agent behaviour itself** (new tools, a different loop, a runtime) is extended in the Python SDK, not here.

## Running it

- `npx @openhands/agent-canvas` needs Node.js 24 or newer and `uv`. It starts Agent Server, the automation backend and the UI, binds to localhost, and generates an API key that it injects into the page. `--public` requires `LOCAL_BACKEND_API_KEY` and does not inject the key.
- The Docker image is built on the Agent Server image and serves everything on one port. A desktop build is available through Electron.
- LLM keys and models are set in the web UI, not through environment variables.

## Strengths and caveats

- **Strength: agent-agnostic front end.** The same UI, approvals, skills, MCP config and automations work for the native OpenHands agent and for ACP agents such as Claude Code or Codex.
- **Strength: honest version contract.** `defaults.json` pins the server version and a minimum compatible server, and contributor rules require raising it when Canvas depends on new server behaviour.
- **Strength: per-conversation isolation by default.** On a local backend, new conversations get their own git worktree unless you attach a folder.
- **Caveat: the agent is not here.** The loop, tools, sandbox, condenser and security analyzers are all in the Python SDK. A review of this repo is a review of the control plane.
- **Caveat: permissive defaults.** Confirmation is off by default, and the local workspace runs commands directly on the host. Docker isolation depends on how the server was started.
- **Caveat: large, fast-moving adapter.** Much of the logic is version negotiation with the SDK ("older servers ignore the field") in one big adapter file. Expect churn.

*Sources: code at b0a1a2d, verified Q&A.*

## How OpenHands/OpenHands answers the Open-source coding agents questions

### How is the agent loop implemented? (answered)

Agent Canvas is a frontend that delegates the agent loop to a backend Agent Server (Python, `software-agent-sdk`). Canvas builds the start-conversation payload in `src/api/agent-server-adapter.ts` (lines 1072–1091), which selects between two agent kinds: **OpenHands** (direct LLM via LiteLLM) or **ACP** (external CLI subprocess like Claude Code/Codex/Gemini). The payload includes `max_iterations` (default 500, line 147), `stuck_detection: true`, a `confirmation_policy`, and a `security_analyzer`. A second **local planner** agent is spawned via `buildStartPlanningConversationRequest` (lines 1450–1572) with a `PlanningFileEditorAction` tool operating on `PLAN.md`; it uses its own `LLMSummarizingCondenser` (`max_size: 100, keep_first: 6`, lines 1520–1525). Once the conversation is created, the frontend communicates over a WebSocket (`conversation-websocket-context.tsx`, line 1–86) that streams events: user messages, actions (tool calls), observations (tool results), and conversation state updates. The turn structure flows as: user sends a message → `ConversationClient.sendEvent` (api line 464–468) → server processes → events stream back over WebSocket → the frontend renders them. The action type system (`src/types/agent-server/core/base/action.ts`, lines 6–370) defines ~20 tool types including `ExecuteBashAction`, `FileEditorAction`, `GlobAction`, `GrepAction`, `BrowserNavigateAction`, `TaskAction` (for sub-agents), `SwitchLLMAction`, and `MCPToolAction`. Sub-agents are launched via `TaskAction` with fields `prompt`, `subagent_type`, `description`, and `resume` (lines 282–299); their results arrive as `TaskObservation` with `task_id`, `subagent`, and `status` (observation.ts lines 305–326). Stop conditions are expressed through `ExecutionStatus` (common.ts lines 67–75): `idle`, `running`, `paused`, `waiting_for_confirmation`, `finished`, `error`, `stuck`.


Citations: [src/api/agent-server-adapter.ts:1072-1091](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L1072-L1091) · [src/api/agent-server-adapter.ts:1450-1572](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L1450-L1572) · [src/types/agent-server/core/base/observation.ts:289-326](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/observation.ts#L289-L326) · [src/types/agent-server/core/base/common.ts:67-75](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/common.ts#L67-L75) · [src/contexts/conversation-websocket-context.tsx:1-86](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/contexts/conversation-websocket-context.tsx#L1-L86)

### How is repository context gathered and kept within the context window? (answered)

Context management has three main mechanisms. **1. Skills (knowledge context):** Bundled public skills from `@openhands/extensions` are loaded into `agent_context.skills` (`agent-server-adapter.ts` lines 828–853) alongside user- and project-authored skills. Skills are filtered via allow/deny lists (`buildSkillEnablementFilter`, line 878) and invoked by keyword triggers or explicit name. **2. Search/grep tools:** The agent has `GlobAction` (pattern + path) and `GrepAction` (regex + path + glob filter) at its disposal (action.ts lines 249–273), plus `FileEditorAction` with a `view` command that reads file ranges (`view_range`, lines 89–94). There is no separate embeddings-based retrieval or repo-map generation in the Canvas frontend — the agent-server SDK handles those if configured. **3. Context-window compaction:** Two compaction strategies exist. The **LLMSummarizingCondenser** is a server-side rolling summarizer configured on the planning agent (agent-server-adapter.ts lines 1520–1525: `max_size: 100, keep_first: 6`) and on OpenHands agents via settings (`condenser: { enabled: true, max_size: 240 }`, services/settings.ts lines 43–46). It fires automatically as the conversation grows, emitting `CondensationEvent` objects (condensation-event.ts lines 5–27: `forgotten_event_ids` and `summary`). The **manual compact** action (`use-compact-context-action.ts` lines 26–119) lets the user request compaction on demand via `useCondenseConversation`; it captures a baseline token count, posts a condense request, and awaits the resulting `CondensationEvent` with fewer tokens. The context-window state is displayed via `useContextWindowUsage` which reads `per_turn_token` and `context_window` from the metrics store. Token usage is tracked per usage-id (`usage_to_metrics` in conversation-state-event.ts lines 47–53), including caching stats (`cache_read_tokens`, `cache_write_tokens`).


Citations: [src/api/agent-server-adapter.ts:828-930](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L828-L930) · [src/types/agent-server/core/base/action.ts:249-273](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/action.ts#L249-L273) · [src/hooks/use-compact-context-action.ts:26-119](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/hooks/use-compact-context-action.ts#L26-L119) · [src/types/agent-server/core/events/condensation-event.ts:5-52](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/events/condensation-event.ts#L5-L52) · [src/types/agent-server/core/events/conversation-state-event.ts:47-53](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/events/conversation-state-event.ts#L47-L53) · [src/services/settings.ts:40-50](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/services/settings.ts#L40-L50)

### How are code edits applied? (answered)

Code editing is done through the **FileEditorAction** tool (action.ts lines 65–95). It supports five commands: `view`, `create`, `str_replace`, `insert`, and `undo_edit`. The `str_replace` command takes `old_str` and `new_str` (exact string replacement — not a diff/patch format). The `insert` command inserts `new_str` after line `insert_line`. The `create` command takes `file_text` for the full file body. There is also a **StrReplaceEditorAction** (lines 96–125) with the same command set as a separate tool name, and a **PlanningFileEditorAction** (lines 218–247) used exclusively by the local planner to edit `PLAN.md`. Each edit returns a **FileEditorObservation** (observation.ts lines 111–146) carrying `command`, `output`, `path`, `prev_exist` (whether the file existed before), `old_content`, `new_content`, and `error`. The `undo_edit` command reverts the last edit to a single file — the `old_content`/`new_content` fields let the agent see the delta. Edits are validated server-side by the agent-server's file editor implementation (not by Canvas's frontend): there is no separate lint-run or test-run step in the tool chain visible in this codebase. Git integration is available through the workspace: the frontend provides a diff view via the Files tab (referenced in the Canvas UI tool description at canvas-ui-client-tool.ts lines 39–40: "The Files tab automatically renders a diff view when the workspace has uncommitted git changes"), and there are `git_service` endpoints on the agent-server, but there is no client-side git-commit-after-edit hook in the Canvas UI code.


Citations: [src/types/agent-server/core/base/action.ts:65-125](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/action.ts#L65-L125) · [src/types/agent-server/core/base/action.ts:218-247](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/action.ts#L218-L247) · [src/types/agent-server/core/base/observation.ts:111-184](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/observation.ts#L111-L184) · [src/api/canvas-ui-client-tool.ts:36-55](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/canvas-ui-client-tool.ts#L36-L55)

### How are shell commands and file writes kept safe? (answered)

Safety is enforced through layered mechanisms. **1. Confirmation policies** are set per conversation at creation time (`agent-server-adapter.ts` lines 730–755): `NeverConfirm` (no prompts), `ConfirmRisky` (threshold `HIGH` + confirm unknown), or `AlwaysConfirm`. The policy is sent in the start-conversation payload as `confirmation_policy` (line 1358). **2. Security analyzers** (line 744–755) can be `LLMSecurityAnalyzer` (an LLM judges command risk), `PatternSecurityAnalyzer` (pattern/rule-based), or `PolicyRailSecurityAnalyzer`. The default is `security_analyzer: "llm"` (services/settings.ts line 14) and `confirmation_mode: false` (line 13). The settings page lives at `verification-settings.tsx` (lines 1–44). **3. Execution runtime modes** are reported by the agent-server as `execution_runtime: "local" | "docker"` (agent-server-compatibility.ts line 38). Docker mode uses `DockerExecutionWorkspace` with working dir `/workspace` (adapter.ts lines 517–522), providing container-level isolation. Local mode uses `LocalWorkspace` with the configured `working_dir`. **4. Secrets** are encrypted (Fernet tokens with prefix `gAAAAA`, adapter.ts line 643) and decrypted server-side. They flow as `LookupSecret` objects (lines 1144–1149) rather than plaintext env vars — the agent-server resolves them from its own store. **5. Network restrictions**: Docker mode sandboxes the agent inside a container, naturally restricting network access. Checkpoint/sandbox lifecycle (pause, resume, missing) is managed via the cloud `SandboxService` (cloud/sandbox-service.api.ts). There is no separate Landlock/seatbelt mechanism visible in Canvas — that is an agent-server concern.

> **Editor's note.** Correction: nothing in this repo restricts network access for Docker execution; Canvas only picks `DockerExecutionWorkspace` vs `LocalWorkspace`, and with the shipped defaults (`confirmation_mode: false`) the policy is `NeverConfirm`, so a local-workspace agent runs commands on the host without asking.

Citations: [src/api/agent-server-adapter.ts:730-755](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L730-L755) · [src/api/agent-server-adapter.ts:1350-1370](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L1350-L1370) · [src/services/settings.ts:1-60](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/services/settings.ts#L1-L60) · [src/api/agent-server-compatibility.ts:35-40](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-compatibility.ts#L35-L40)

### Which models are supported and how are they called? (answered)

Two agent kinds determine model support. **OpenHands agents** use LiteLLM, which makes any LLM accessible via its provider prefix (e.g. `openai/gpt-5.6-sol`, the default at services/settings.ts line 6). The model config includes `model`, `api_key`, `base_url`, and supports subscriptions (ChatGPT via `LLMSubscriptionService`, subscription-service.ts lines 1–30, with a device-OAuth flow). **ACP agents** delegate to external CLI subprocesses: Claude Code (`claude-code`), Codex (`codex`), Gemini CLI (`gemini-cli`), or a custom command. These are defined in `acp-providers.ts` (lines 1–80) which reads from `@openhands/typescript-client`'s ACP registry. ACP models use `acp_model`, `acp_command`, `acp_server` fields (adapter.ts lines 959–1027). **Model routing** is done through saved **LLM profiles** (named model configs) and **meta-profiles** (classifier-based routers). The `SwitchLLMAction` (action.ts lines 301–310) allows mid-conversation model switching via `profile_name` + `reason`, returning a `SwitchLLMObservation` with `active_model`. The `ClassifyAndSwitchLLMObservation` (observation.ts lines 364–387) supports the **Model Router** meta-profile feature: a classifier selects from saved LLM profiles. The `route_task_to_model` tool is attached by the agent-server when a meta-profile is active, gated by `run_router_at_conversation_start` (adapter.ts lines 364–379). **Cost tracking** is handled by `MetricsSnapshot` (conversation-state-event.ts lines 25–41) with per-usage-id costs, token counts, response latencies, and prompt/completion/cache breakdowns. An `LLMBalanceService` (llm-balance-service.ts lines 1–50) provides credit-balance queries for OpenRouter. The default model is `openai/gpt-5.6-sol` (settings.ts line 6). Support for `stream: true` is forced for both OpenHands and ACP agents (adapter.ts lines 1041, 1470).


Citations: [src/services/settings.ts:1-60](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/services/settings.ts#L1-L60) · [src/api/agent-server-adapter.ts:1030-1070](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L1030-L1070) · [src/types/agent-server/core/base/action.ts:300-315](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/action.ts#L300-L315) · [src/types/agent-server/core/base/observation.ts:341-387](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/agent-server/core/base/observation.ts#L341-L387) · [src/api/agent-server-adapter.ts:364-379](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L364-L379) · [src/api/llm-balance-service.ts:1-50](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/llm-balance-service.ts#L1-L50)

### How can it be extended and customised? (answered)

Extensibility is built at multiple levels. **1. MCP (Model Context Protocol)**: The `mcp_config` setting supports stdio, SSE, and HTTP servers, including OAuth2 device-auth flows. The `MCPService` (mcp-service.api.ts, lines 1–100) proxies test/ping/health operations for both local and cloud backends. MCP health monitoring runs via `probe-mcp-server-health.ts`. **2. Canvas Extensions** (the "Apps" system): Plugins defined by a `canvas-extension.json` manifest can contribute custom pages (`CanvasExtensionPageContribution` in canvas-extension.ts lines 4–11). Extensions are installed from git sources or local paths through `CanvasExtensionsService` (canvas-extensions-service.ts lines 1–60), which has an app-backend bridge capability. They render in iframes with validated session tokens (canvas-extension-app-view.ts lines 32–59). **3. Skills**: Public skills are bundled at build time from `@openhands/extensions` (agent-server-adapter.ts lines 828–853, using the `SKILLS_CATALOG`). User and project skills are loaded from disk by the agent-server (`load_user_skills: true`, `load_project_skills: true`, line 909–910). Skills use the AGENTS.md format with keyword triggers, descriptions, and compatibility constraints. **4. Hooks system**: Workspace hooks are loaded from `.openhands/hooks.json` by `HooksService` (hooks-service.ts lines 1–50). Hooks fire on `stop`, `pre_tool_use`, `post_tool_use` events (types lines 264–271), with command or prompt executors and configurable timeouts. **5. Client tools**: The `ClientToolSpec` interface (canvas-ui-client-tool.ts lines 9–20) allows registering custom tools with JSON Schema parameters and annotations (`readOnlyHint`, `destructiveHint`, etc.). Two built-in client tools are `canvas_ui_control` (UI navigation) and `launch_child_conversation` (spawn sub-conversations). **6. SDK**: The `@openhands/typescript-client` npm package provides typed API clients for headless use. The ACP Protocol lets any external agent (Claude Code, Codex, Gemini, custom ACP) be used as a backend.


Citations: [src/api/mcp-service/mcp-service.api.ts:1-100](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/mcp-service/mcp-service.api.ts#L1-L100) · [src/api/canvas-extensions-service.ts:1-60](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/canvas-extensions-service.ts#L1-L60) · [src/types/canvas-extension.ts:1-40](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/types/canvas-extension.ts#L1-L40) · [src/api/canvas-ui-client-tool.ts:1-91](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/canvas-ui-client-tool.ts#L1-L91) · [src/api/agent-server-adapter.ts:828-910](https://github.com/OpenHands/OpenHands/blob/b0a1a2d1368a50a890b69ef45c54e1a74140b677/src/api/agent-server-adapter.ts#L828-L910)
