Skip to content

Controllers

Daniel Baldwin edited this page Sep 3, 2026 · 2 revisions

Controllers

A controller is the runtime that actually executes a dispatched agent — the process conductor spawns (or the API it calls) to turn a prompt into work. Every agent runs on the built-in paseo controller unless you configure otherwise; controllers are entirely optional; a config with no controllers: block resolves every agent to paseo and behaves exactly as before. Configuring controllers: lets some or all agents run on a different runtime instead — an ACP agent over stdio, opencode's own HTTP API, agent-deck, or a bare CLI recipe.

Config

controllers:                        # named map, like `agents:`
  gemini-review:                    # `agent:` + no transport → ACP; runs the gemini CLI over
    agent: gemini                   #   stdio. The runtime IS gemini, so provider/model on the
                                    #   agent profile below don't apply.
  opencode:                        # `type: opencode` → opencode's HTTP API (`opencode serve`),
    type: opencode                  #   which routes by provider/model.
  claude-cli:                       # `transport: cli` → a bare per-tool recipe (claude-code and
    agent: claude-code              #   codex ship built-in recipes; anything else needs `command:`).
    transport: cli

agents:
  reviewer:
    controller: gemini-review       # runs gemini over ACP — no provider needed (the controller IS the agent)
  planner:
    controller: opencode            # runs via opencode…
    provider: anthropic             # …which routes to this provider + model
    model: claude-sonnet-4-5
  fixer:
    provider: claude                # no controller: → the built-in paseo runtime runs claude
    model: opus
Field Meaning
controllers.<name> One controller entry, named like an agents: profile.
type Controller kind: paseo (built-in, always registered), opencode, or agent-deck. Omit for an ACP or cli controller — those are inferred from agent:/transport:.
agent For ACP: the agent binary to spawn over stdio (gemini, claude-code-acp, codex-acp, goose, opencode, …). For transport: cli: the CLI recipe name (claude-code, codex, or any name paired with an explicit command:).
transport Overrides the inferred transport. acp is the default when agent: is set and no type: is given. cli selects the bare-subprocess recipe path. native (with agent: opencode) selects opencode's own transport instead of ACP.
default Flags this entry as the fleet default for agents with no explicit controller:. At most one entry may set this — validated at config load.
command Explicit subprocess argv for a cli transport whose agent: name isn't one of the built-in recipes.
agents.<name>.controller Selects a controllers.<name> entry for this agent profile. Unset → falls through the resolution order below.

provider/model on the agent profile (not the controller) select the model for controllers that route by provider — paseo, opencode, and agent-deck. For an acp or cli controller the runtime is the named agent/command, so provider/model on the profile are ignored — don't pair provider: claude with an acp controller pointed at gemini. See Agents for the full agent-profile shape.

Resolution order

  1. An explicit controller: on the agent's profile.
  2. The controller entry flagged default: true (at most one may be; validated at load).
  3. The built-in paseo controller.

Kinds

Config Runtime Session model
type: paseo (built-in, always registered as paseo) The existing paseo CLI dispatcher native
agent: <name> (or explicit transport: acp) An ACP agent over JSON-RPC 2.0 on stdio — gemini, claude-code-acp, codex-acp, goose, opencode over ACP, … (Agent Client Protocol, Zed's open standard; conductor is the ACP client) native, upgraded to resumable if the agent negotiates loadSession
type: opencode (or agent: opencode + transport: native) opencode's native HTTP server (opencode serve) — its own model routing/cost accounting resumable
type: agent-deck The agent-deck CLI orchestrator (launch / session send / session show / remove) native
transport: cli A bare per-tool command recipe run as a direct subprocess (built-in recipes for claude-code and codex; anything else needs an explicit command:) resumable (e.g. claude-code --resume) or oneshot (codex, a generic command:)

An unrecognized type/transport stays registered as a stub: it negotiates capabilities but returns an error when selected instead of silently falling back to another runtime, so config can name a controller this build doesn't drive yet without an agent quietly running somewhere else.

Session models

  • native — the controller owns the whole session lifecycle (paseo, agent-deck).
  • resumable — a session survives by id and is resumed on demand rather than held as a live process (an ACP agent advertising loadSession, opencode, or a cli recipe that can --resume).
  • oneshot — each turn is a fresh process; no persistent session.

Session broker and hand-off channels

Two portable capabilities sit above the controller layer, so a session behaves the same regardless of which runtime it runs on:

  • The session broker keeps one live/resumable session per PR and funnels follow-ups to it — a burst of triggers collapses onto the same session instead of spawning duplicates, and the PR→session binding is persisted (sessions.json) so an interactive hand-off survives a conductor restart — the next follow-up re-attaches by id instead of orphaning the old session.
  • An optional [[Hand-offs|handoffs:]] block adds a portable web-link (or Slack/Discord) review channel:
    handoff:
      web:
        base_url: https://conductor.example.com   # public origin the draft link points at
        listen: :8099                             # inbound address the draft page is served on (default :8099)
    With it configured, a background: true Workflows's review runs over that channel's present → await → revise/submit loop instead of paseo's own UI — the only way to interactively review an agent on a controller with no native interactive surface (cli, or opencode's native transport). With no handoffs: block, review hand-off keeps today's paseo-native behavior, unchanged.

Escalations and hand-off prompts go out through the same Notifications channels as everything else (journal, plus any configured Slack/Discord/ntfy/Pushover/Notifiarr) — controllers don't add a separate notification path.

Testing

The Dockerized e2e harness at test/e2e/ exercises the full controller matrix (every kind above, resolution, the session broker, hand-off, and failure/escalation) against an isolated local forge + mock GitHub API — make e2e (hermetic stubs, no keys, CI-safe) and make e2e-live (the real agent CLIs installed on your box, manual).

Behavior

  • With no controllers: block at all, resolution always yields paseo and existing behavior is unchanged — the block is purely additive.
  • Provider API keys for an ACP agent (e.g. GEMINI_API_KEY) live in that agent's own process environment, not in the controller config — conductor spawns the process, it doesn't proxy credentials into it.
  • A controller is resolved once per dispatched agent, at dispatch time, from the agent profile that action names — not per event or per step, so a multi-step Workflows mixing agents on different controllers works step by step.
  • Selecting a stub controller (an unrecognized type/transport) is a hard error at dispatch, not a silent fallback to paseo — this is deliberate: naming a controller your build doesn't implement should fail loudly rather than run the agent on the wrong runtime.

Explanation

An agent (see Agents) is a named profile — provider, model, mode, workspace policy, guidance — and a controller is what actually runs it. The same fixer profile (provider: claude) can execute on the built-in paseo runtime, on agent-deck, or via opencode's HTTP API by pointing its controller: at a different entry, with no change to the profile itself. The exception is ACP and cli controllers, where the controller entry's own agent:/command: is the runtime being invoked — there's no separate provider/model to route, so those fields on the profile are ignored for that agent. In short: the controller is the runner; the agent's provider/model is what it talks to (when the runner supports routing at all).

Clone this wiki locally