# Talk from your editor

Point an editor at a running instance and talk to its agents, over a protocol editors already speak.

An agent that only answers a route is an agent nobody talks to. The [Agent Client Protocol](https://agentclientprotocol.com) is the protocol editors already implement for exactly this, so an instance that speaks it is reachable from a place a person already has open, with the transcript, the tools and the guardrails the instance already has.

Routecraft serves it as a mount, beside the MCP mount and the HTTP surface, on the same server and behind the same wall. There is nothing new to run and nothing new to secure.

## Serving it

```ts
import { defineConfig } from '@routecraft/routecraft'
import '@routecraft/ai'

export const craftConfig = defineConfig({
  servers: {
    default: { port: 8080 },
  },
  acp: {},
})
```

`craft start` discovers `agents/` for you, and the `acp` key serves every agent the context holds at `/acp`, in whatever order the keys are written. Building a context by hand instead, import `agents` from `@routecraft/ai` and write them under `agent: { agents: await agents('./agents') }`. That form cannot resolve `skills:` frontmatter refs, which need the house and bundle folders only the project runtime has. Every option is in the [configuration reference](/docs/reference/configuration#acp); `acpPlugin()` is the same thing for `plugins`, where it goes after the `agentPlugin()` that registers the agents.

## Connecting an editor

An editor starts one process and speaks the protocol over its standard input and output. `craft acp` is that process:

```json
{
  "agent_servers": {
    "routecraft": {
      "command": "craft",
      "args": ["acp", "--profile", "company"]
    }
  }
}
```

The exact file depends on the editor; the command is the same everywhere. `craft acp` is a pipe that re-establishes itself: every message crosses it verbatim in both directions, an instance that restarts under an open editor window is reconnected to without the editor noticing (see [`craft acp`](/docs/reference/cli#acp) for what that involves), and it never starts an app. `craft start` owns running, which is what lets one editor entry reach a laptop or a company instance by switching a [profile](/docs/reference/cli#profiles) and changing nothing else.

There is no login. The profile's `token:` authenticates, exactly as it does for `craft exec` and `craft ops`, and it travels as `Authorization: Bearer` on every request. See [`craft acp`](/docs/reference/cli#acp) for the flags.

## What a conversation is

A conversation is an [agent session](/docs/reference/adapters/agent#sessions-an-agent-that-remembers): a record in the instance's store, not a process on your machine. That is what the editor's conversation list is reading, and it has consequences worth knowing:

- **It outlives the connection.** Close the editor, restart it, reconnect to the same instance, and the same conversation is there by its id. Loading one replays the transcript into the editor so the history is on screen rather than merely on the server.
- **It outlives the instance.** A restart does not lose it, and an editor window that was open across the restart is still attached to it afterwards: the bridge resumes the conversation by its id when the instance answers again. What the restart interrupted is the exception: a message whose turn was running when the instance went down comes back to the editor as cancelled, and a conversation held only in a memory store is gone with the process and is named on the bridge's standard error.
- **It lives where the store lives.** The conversation is in that instance's [session store](/docs/reference/configuration#sessions), which by default is a file beside the app. Reaching the same conversation from a second machine means reaching the same instance, or pointing both instances at [one shared store](#sharing-one-store-between-instances), which has a limit worth reading before you rely on it.
- **One turn runs at a time.** A message sent while a turn is running is queued and answered by the turn that runs next, and your request for it stays open until that reply has streamed: see [Sending while a turn runs](#sending-while-a-turn-runs). Cancelling from the editor interrupts the running turn and keeps its partial transcript.
- **Somebody else can be in it too.** A webhook or a mail reply dispatching to the same session id posts into the same conversation.

## A conversation belongs to the person who started it

The mount lists, loads and resumes only the conversations whose owner matches the caller's subject. Somebody else's session id is answered as if it did not exist, so probing for one tells the prober nothing about whether it is there.

A mount with no wall has no subjects to tell apart, and every conversation on it belongs to the same unnamed person. That is the right behaviour for a laptop and the wrong one for a shared instance, which is the argument for putting a wall on it: see [Securing capabilities](/docs/advanced/securing-capabilities).

The management API's session listing is the operator's view and deliberately not filtered this way. An operator is looking at the instance rather than at their own conversations.

## Sharing one store between instances

Ownership travels with the record rather than with the connection, so several instances on one shared store all agree about whose conversation is whose: a person moving between them reaches their own and nobody else's.

**They are not coordinated with each other.** The one-turn-at-a-time rule is enforced in the process running the turn, and a sibling process cannot see it. So an instance listing a conversation whose turn another live instance is running reports that turn as `stale`, which is the same thing it reports for a turn a restart cut short, because from where it is standing the two look identical.

Nothing corrupts when that happens. Every write to a record is a compare-and-swap on its version, and a writer that keeps losing gives up with [`AI1010`](/docs/reference/errors#ai-1010) rather than overwriting somebody's turn. What you get is a wrong reading, not a lost transcript.

The safe shape today is one process per store: several editors, several people and several other callers all reaching one instance, which is what a shared store is usually wanted for anyway. Coordinating the turn lock across processes is not in this release.

## One agent per editor entry

**A `craft acp` process serves exactly one Routecraft agent.** `--agent zoe` chooses it, and every conversation that process opens belongs to Zoe for its whole life. Choosing a different agent means running a different process, which is what an editor's own agent list is for.

Both Zed and JetBrains let you register several custom agents, each with its own display name, command and arguments. So each Routecraft agent gets an entry, and the picker you already use to start a thread is the picker that chooses the agent.

In JetBrains IDEs, `~/.jetbrains/acp.json`:

```json
{
  "agent_servers": {
    "Max": {
      "command": "/absolute/path/to/craft",
      "args": ["acp", "--agent", "max"]
    },
    "Zoe": {
      "command": "/absolute/path/to/craft",
      "args": ["acp", "--agent", "zoe"]
    }
  }
}
```

In Zed, under `agent_servers` in `settings.json`:

```json
{
  "agent_servers": {
    "Max": {
      "type": "custom",
      "command": "craft",
      "args": ["acp", "--agent", "max"]
    },
    "Zoe": {
      "type": "custom",
      "command": "craft",
      "args": ["acp", "--agent", "zoe"]
    }
  }
}
```

JetBrains launches the command itself rather than through a shell, so `command` wants the executable's full path: `which craft` prints it, or `where craft` on Windows. Zed resolves a bare name against the environment it launches with, which is usually enough. Both editors read these from the user's own configuration, so check their current documentation if a field has moved. A profile carries `agent` too, so `craft acp --profile work` is the same choice made once in a file rather than in every entry: see [Profiles](/docs/reference/cli#profiles).

Pick Max in the editor and you get Max conversations, as many as you like, each independent. Pick Zoe and you get Zoe's. The Max entry does not list, open or answer a Zoe conversation: it reports one as it reports an id that was never created, because from where Max is standing there is no such conversation. Reach it through the Zoe entry instead.

An entry that names no agent gets the instance's default: the one agent it holds, or the one `acpPlugin({ agent })` names. An instance holding several with no default configured refuses rather than guessing, because picking one for you is a decision the app never made.

The cross-agent view exists, but not here. The management API's `/ops/agent-sessions` lists every conversation an owner holds whatever agent answers it, which is the operator's question rather than the editor's.

## Choosing the model and thinking level

Editors render the protocol's config options as pickers, and the mount advertises two:

| Option | What it changes | Advertised when |
| --- | --- | --- |
| `model` | Which model this conversation runs on | The agent file lists more than one |
| `reasoning` | How hard it thinks per turn | The agent file lists more than one |

A control with nothing to choose is not advertised at all, so an agent naming one model and one thinking level shows no pickers rather than two dead ones. Agent files declare what they offer by writing a list instead of a value: see [Offering a choice](/docs/reference/plugins/agentplugin#offering-a-choice-of-model-or-thinking-level).

Both stay changeable for the life of the conversation, including while a turn is running. A change applies to the next turn rather than to the one in flight, because a turn reads its settings when it starts.

## Why agent selection is not an ACP mode

The protocol has a mode mechanism, and Routecraft does not use it for this. That is a position rather than an omission, and it is worth stating because the alternative looks tempting.

**An agent is an identity, not a setting.** Max is not a mode of Zoe. Two agent files carry different system prompts, different tools, different authorization expectations and different behavioural contracts. A conversation is a transcript one of them wrote, using tools the other may not have, so handing it to a second agent would produce answers that read as if the first had given them.

**Modes change during a session; identity cannot.** The protocol allows a mode to be set at any point in a session, idle or mid-turn. Anything reachable that way is by definition not the thing a session is. If which agent answers were a mode, the invalid state would exist and we would be guarding it. Binding the agent to the harness means the state never exists.

**The editor already solved the selection problem.** Registering several agents with their own names is an abstraction both editors ship. Rebuilding it inside a single session would duplicate it one level lower, and worse.

**A session with no agent yet has no answers to give.** If the agent were chosen after `session/new`, then between creation and that choice a session could not say which tools it has, which system prompt applies, which models it offers, or what a prompt arriving early should do. Choosing at the harness means every session knows all of it from the moment it exists.

Modes stay available for what they are actually for: a behavioural variation *within* one agent, if an agent file ever wants to offer one. A mode must never change which agent file owns the session. Should Routecraft grow a "continue this conversation with another agent" feature, it will be a fork producing a new session seeded from the old transcript, and the original will still be the agent it always was.

## What the editor sees while a turn runs

Token deltas stream as the model produces them, thinking arrives as thought rather than as text, and every tool call appears as it starts and is updated when it ends.

A tool call's arguments and result reach the editor too, which is what makes an expandable call worth expanding. On an instance serving people other than its operator, consider turning that off with `toolCallPayloads: false`: a hand's arguments routinely carry a credential, a customer's address or a mail body, and an editor is a third program that renders and logs whatever it is handed. The call itself stays visible either way.

## Sending while a turn runs

You can keep typing while the agent is answering. A message sent mid-turn is never lost, never answered under a different message, and never shown finished with nothing under it:

- **It is answered by the next turn.** The message waits in the conversation's inbox. When the running turn ends, the next turn starts on its own and consumes everything that queued, so nothing further has to be sent for it to be answered.
- **Several messages get one reply.** Two or three messages sent while the agent was busy are consumed together by that one turn and answered in one reply, the way other agents behave. The turn's tool calls stream too.
- **Your request stays open until the reply has streamed.** The protocol's `session/update` carries no prompt id: an editor attributes whatever streams to whichever `session/prompt` it has open. So a `session/prompt` for a queued message is held until the turn that consumed it has ended, and only then returns `end_turn`. Several requests held on one conversation return in the order they were sent, all after the reply.
- **Stop cancels the running turn only.** `session/cancel` interrupts the turn in flight, whose request returns `cancelled`; a message sent while it ran is still in the inbox, the next turn answers it, and its request returns `end_turn` with that reply. Cancel then prompt is how an editor steers, and it works without any option.
- **Stop reaches the editor too.** A capability that was asking the editor something when you pressed stop has that call cancelled, and nothing new is sent to you from that turn. A capability that had opened something in your editor, a terminal running a command, closes it on the way out through the cleanup it declared beforehand; see [when the turn is cancelled](/docs/reference/adapters/surface#when-the-turn-is-cancelled).

A prompt returns `end_turn` only when its reply has streamed. A message this instance cannot answer (it arrived while the instance was shutting down, or another instance sharing the store consumed it) returns `cancelled` rather than `end_turn`: it is still in the conversation and the next turn answers it, and the editor is not told a turn ended that showed nothing.

There is no option for any of this. How a queued message is answered is the mount's decision, and a `steer` method does not exist in the protocol.

## Reaching back into the editor

The editor is a program that can read a file, run a command and ask its user a question, and a capability running inside a turn can use that:

```ts
import { craft, direct } from '@routecraft/routecraft'
import { surface } from '@routecraft/ai'

craft()
  .id('read-file')
  .from<{ path: string }>(direct())
  .enrich(
    surface('fs/read_text_file', (ex) => ({ path: ex.body.path })),
  )
```

The framework provides the seam and ships none of the capabilities that use it. Reading a file through the editor is an ordinary route somebody owns, with its own `.authorize()` written where a reader of that route can see it. That is the same line the framework draws for tool calls. The [`surface` reference](/docs/reference/adapters/surface) has the roles, the failure codes and the guard that keeps one route working from an editor and from a schedule alike.

### What you still have to build

Connecting an editor gives you a conversation. It does not give you an agent that can work in the project, and the distance between the two is a handful of routes somebody writes: reading a file, writing one, replacing part of one, listing and searching the project, running a command in the editor's terminal, opening a link, showing a plan, and the permission prompt the writing ones ask through.

The routes are small. The decisions inside them are not. A command run this way runs in the editor, as the person, on their own machine, with none of the isolation a sandboxed shell gives you. Which commands may run without asking, which paths are refused before a call is made, how much of a file may reach one turn, and what the permission prompt actually says are all yours to decide, and together they are the whole of what stands between a command and the machine. The wall on the mount and a route's `.authorize()` decide who may start a turn and dispatch a route; neither constrains what a command does once it runs.

### Starting from the harness

[`craft-harness`](https://github.com/routecraftjs/craft-harness) ships that whole set, working, with a starting answer to each of those decisions:

```bash
bunx create-routecraft my-agent --example https://github.com/routecraftjs/craft-harness
```

What arrives is not a permission model compiled into somebody else's tool. It is eight capabilities and the permission prompt they ask through, one folder each under `capabilities/editor/`, with the guardrail that governs a route written in that route beside the code it governs. Narrow the command allowlist, add an `.authorize()` only your on-call rota passes, refuse a path your own rules refuse, or delete a capability you would rather the agent did not have. The posture it starts with is a default in your own source tree, which is the kind a reviewer can read and a diff can change.

## What this is not

**It is not a second security model.** A prompt runs an ordinary route under the principal of the person who typed it, and every tool authorizes that principal on every call. Every agent the context holds is offered to every credential holder: agents are not what carries the security here, hands are.

**It is not a place to configure an app.** The mount serves what the instance already has. Which agents exist, which tools they hold and who may reach them are decisions in `craft.config.ts` and the agent files, where a reviewer reads them.

## Related

- [acpPlugin](/docs/reference/plugins/acpplugin) -- Every option the mount takes, and what each one refuses.
- [craft acp](/docs/reference/cli#acp) -- The bridge an editor runs, and the profile that points it somewhere.
- [surface](/docs/reference/adapters/surface) -- Ask the person's editor something from inside a turn.
- [Durable Agents](/docs/advanced/durable-agents) -- What a session is, and what survives a restart.
