The Architecture of Handoffs

For OpenHands Agent Canvas, the central architectural question is not where a button appears, but which service owns the consequence of pressing it. This chapter follows the handoff from the Canvas interface through typed service adapters and routing to the runtime that creates conversations, runs agents, or schedules work. The practical payoff is a way to locate responsibility—and therefore likely failure—without treating a multi-service product as one opaque application.

One product, several owners

Agent Canvas is the visible control surface, but visibility is not ownership. The frontend renders conversations, terminals, browser and file surfaces, settings, backend selection, and automation controls. It also maintains the browser state needed to decide which backend is active and what the user is looking at. Those are substantial responsibilities, but they stop short of executing an agent action.

The OpenHands Agent Server and SDK own the runtime side of the boundary. According to the repository’s architecture, that layer owns agents, tools, conversations, workspaces, events, and the canonical server API. When a user asks an agent to edit a file or execute a command, Canvas sends a request; the Agent Server decides how that request becomes runtime work. The workspace and its isolation properties are therefore backend concerns, not features that the React interface can create by itself.

A separate TypeScript client sits between the browser and the Agent Server API. It is not another runtime. Its role is to give the frontend a typed, browser-compatible representation of server operations. The Automation service owns a different slice: automation definitions, schedules, webhooks, run history, and dispatching. Once an automation dispatches work, the Agent Server and SDK execute the conversation. Extensions add yet another distinction. Skills, plugins, and integrations extend the agent’s capabilities; Canvas Extensions are intended to change the Canvas application itself.

These boundaries affect user-visible behavior. Suppose the settings screen displays an option that the installed Agent Server does not understand. The frontend can hide it, degrade gracefully, or surface an incompatibility, but it cannot make the backend accept a new server field. Suppose an automation list loads but a run never starts. The UI may show the state and issue a dispatch request, while the scheduling or execution service owns the transition that failed. Suppose a new MCP endpoint is needed. The repository’s contributor guidance says the typed client must gain that access before Canvas consumes it; a raw browser request is not an acceptable shortcut.

This is why repository boundaries are part of the product’s behavior, not just a way for contributors to keep files tidy. They define the direction in which authority travels. The frontend consumes contracts. The typed client represents those contracts in the browser. The Agent Server executes them. Automation decides when some of them should be invoked. Extensions provide material that another layer loads or runs. Moving code across these lines can create a feature that appears complete in the UI while failing at the boundary where the real decision belongs.

Consider the ordinary request to add a new capability. If the capability requires a new Agent Server endpoint, the server contract has to exist before the browser can use it. The typed client then gives that endpoint a stable request and response shape, and Canvas adds the service and user-facing state around it. If someone skips the client boundary and writes a direct browser request, the first demo may work against one host while quietly duplicating authentication, error handling, and URL assumptions. If someone implements only the screen, the control can render without a runtime that understands its request. The boundary is therefore a dependency order, not a filing preference.

The same principle applies to a failure that looks like a frontend bug. A settings screen may be correctly rendering a value that the active backend refuses, or a conversation page may be waiting for an event that the runtime never emits. The UI can report the state it knows, but the owner of the transition determines the remedy. This keeps the architecture honest: Canvas can improve the explanation and recovery path, while the service that owns the contract must supply the capability. The result is less convenient than pretending every layer can patch every problem, but it gives an operator a meaningful place to investigate.

The request path can be pictured as a set of accountable handoffs rather than a list of components:

flowchart LR person["Developer"] --> canvas["Canvas UI and state"] canvas --> service["Frontend service and query hook"] service --> client["Typed Agent Server client"] client --> ingress["Ingress or Cloud proxy"] ingress --> server["Agent Server and SDK"] server --> runtime["Agent, tools, workspace, events"] canvas --> automation["Automation service"] automation --> server agentExtensions["Skills and plugins"] --> server appExtensions["Canvas Extensions"] --> canvas

The arrows do not mean every request passes through every node. A local Agent Server call may use the typed client directly, while a Cloud call may use a Cloud-specific proxy. Automation can call its own service surface before the Agent Server becomes involved. The value of the diagram is the ownership pattern: the Canvas coordinates; the runtime acts; the service in the middle translates and constrains access.

That pattern also keeps categories from silently expanding. Canvas does not provide LLM credentials outside the configured backend. It does not become a sandbox because its layout has a terminal panel. A Cloud API does not become a local Agent Server merely because both can produce a conversation screen. Every apparent shortcut would erase information about where data lives, which authentication scheme applies, and who can reject or authorize a transition.

How separate services feel like one entry point

The local launcher reduces the friction of these handoffs by composing several processes behind one browser-facing address. In the full local stack, the launcher starts a frontend, an Agent Server, and an Automation backend. In development it can use Vite; in a published or static path it serves a pre-built frontend. A standalone ingress proxy routes requests by path: the more-specific automation prefix goes to the Automation service, API and socket traffic goes to the Agent Server, and unmatched browser paths go to the frontend.

This is a useful form of composition because it solves a real user problem. A person can open one address rather than remembering which port hosts the interface, which port hosts the server API, and which one hosts automations. WebSocket traffic can use the same entry point as ordinary HTTP requests. The static server provides similar proxy behavior while serving the single-page application, so a production build and a development build preserve the same broad topology.

But one address does not mean one process or one trust boundary. The launcher still has to start services in an order that respects dependencies, wait for the Agent Server to become ready, configure the Automation backend to call it, and arrange shared authentication. A route can return an error because the upstream service is unavailable even while the frontend itself is healthy. A frontend-only mode can intentionally reject API prefixes rather than return the application shell for a request that has no backend behind it. These details reveal the cost of the convenience: composition hides operational complexity from the user, but the complexity remains and must be managed somewhere.

The order matters. Automation depends on an Agent Server that can receive the work it dispatches, so the launcher starts and probes that server before bringing the automation sidecar into the experience. It creates the state directories each service needs and gives the services a shared session-key convention. Only after the relevant services are ready does it place the ingress proxy in front of them. A partial-stack mode changes the route table rather than merely hiding a button: paths with no running backend are rejected, because returning the frontend shell for an API request would make an unavailable service look like a successful page load.

This is also why the launcher supports both dynamic and static frontends without changing the broad product model. A Vite server is useful during development, while a pre-built static server is useful for a packaged or remote deployment. Each still needs to proxy the backend paths and WebSocket upgrades that make a conversation live. The launcher is thus an operational adapter: it turns selected processes and ports into a coherent origin, but its correctness depends on the readiness, routing, and configuration contracts of those processes.

The typed service layer keeps a similar discipline inside the browser. Canvas services obtain the host, session key, and default workspace from the active backend registry through shared client-option helpers. Components are expected to use query hooks rather than call services directly. That lets the query system provide caching, deduplication, loading and error states, and background refresh in one place. More importantly, it means a backend change can be reflected by the connection options and query identity instead of requiring every screen to invent its own interpretation of the current host.

The repository even enforces part of this boundary with a test that rejects direct browser calls to local Agent Server routes. The reason is not aesthetic. If each component could assemble its own URL and authentication header, one screen might use a stale backend, another might omit a session key, and a third might send a Cloud request to a local endpoint. Centralization makes the active backend a dependency of the request rather than an incidental string in a component.

The data layer follows the same rule. Long-lived queries include the active backend, and where relevant organization, in their identity. When that identity changes, the query system treats the result as belonging to a new namespace and fetches it again. This avoids a particularly dangerous kind of success: a screen that looks populated because it is showing valid data, just not data from the backend the user selected. Settings use an analogous distinction for secrets. Redacted values are suitable for display, while encrypted values can make a safe round trip to a conversation-start request; the redacted placeholder must never be mistaken for a credential.

Cloud traffic requires a different handoff. Cloud-specific calls use service modules and a proxy because browser requests to Cloud and runtime sandbox origins may not permit direct cross-origin access. The proxy can use bearer authentication for the Cloud API and session-key authentication for a runtime sandbox host. From the user’s perspective, the conversation still looks like one Canvas flow. From the system’s perspective, the route and credential mode have changed because the authority has changed.

Runtime topology is the point where the handoff becomes visible to the agent itself. The launcher constructs a structured description of the services reachable from the agent’s point of view. Canvas reads that information from the backend’s server-info response and places it in the agent context. The resulting context identifies the Agent Server, the ingress address, the Automation backend when it is running, the automation API prefix, and the relevant authentication environment variable. It also tells the agent not to guess at other ports.

That instruction matters because an agent does not necessarily share the browser’s point of view. A URL that works in the browser may not be the URL that a tool running inside a sandbox can reach. Conversely, the loopback address visible from inside the agent may point to the Agent Server it is running within, not to the neighboring Automation service. Giving the agent explicit, agent-perspective topology is therefore a small but consequential control-plane function. It converts deployment knowledge into a runtime contract and reduces a class of failures caused by environmental guesses.

Imagine an agent asked to create or inspect an automation during a conversation. Without an explicit topology contract, it might send a request to the browser’s public address, assume that the Agent Server’s loopback port is the automation API, or call an endpoint that is not running in a minimal stack. The Canvas launcher cannot guarantee that the agent will make a good decision from ambiguous environment clues. It can, however, supply the service identity, route prefix, and authentication convention that the current deployment actually advertises. That is a modest form of coordination, but it is exactly the kind that prevents a visible feature from depending on a lucky port assumption.

The same mechanism illustrates where this repository’s knowledge ends. Canvas can describe the route it constructs and the request it sends, but it does not establish the internal scheduling algorithm of Automation, the implementation of an Agent Server tool, or the provisioning policy of a Cloud sandbox. Those behaviors belong to sibling runtimes. A frontend integration can be correct while an upstream service is unavailable, incompatible, or operating under a different version contract.

That limit is not a weakness to conceal; it is part of the handoff model. A reliable operator asks which layer owns the next transition and then checks the evidence at that layer. If the interface is wrong, inspect Canvas state and routing. If the request is malformed, inspect the typed client and contract. If the request is accepted but work fails, inspect the Agent Server, workspace, provider, or Automation service. The architecture becomes useful when it narrows the search space without pretending to answer questions outside its boundary.

Agent Canvas therefore achieves a unified entry point through explicit composition. The interface, service adapters, typed client, proxy, ingress, and runtime context make separate authorities feel coordinated, while the boundaries preserve who owns execution, state, and failure. The next architectural problem follows naturally: once a request reaches more than one possible backend, how does Canvas keep identity, credentials, cached data, and conversation scope from crossing the wrong boundary?

References