turbo-editors/turbo-jspublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-js.git
git clone ssh://git@rickub.com/turbo-editors/turbo-js.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

📦 Turbo JS 91999d1 · on v1.0.2 · k33g · 10h ago
acp.md · 239 lines · 14.1 KBmarkdown
Blame HistoryOpen raw

Agents and ACP

Turbo JS is a client for the Agent Client Protocol. It starts each agent as a child process and exchanges JSON-RPC 2.0 messages with it over stdin and stdout, one message per line.

Where the file lives

Path Read Purpose
~/.config/turbo-js/acp.toml first Agents you want in every project
<project>/.turbo-js/acp.toml second Agents belonging to this project

Both are optional. Where an agent's name appears in both, the project's replaces the user's, being the more specific statement — the same rule snippets follow. A missing file is not an error; a file that is present but unreadable is, and is reported under Agent ▸ Agent status rather than silently leaving the menu empty.

TURBO_JS_DIR overrides the directory the user-level file is looked for in. The project file is always .turbo-js/acp.toml under the directory the editor was started in — there is no walk up the tree, for the same reason project settings do not walk up.

File format

One [[agent]] block per agent, in the order you want them in the menu.

[[agent]]
name    = "Bob (llama.cpp)"
command = "docker"
args    = ["agent", "serve", "acp", ".turbo-js/agent.yaml"]
env     = { TELEMETRY_ENABLED = "false" }
cwd     = "."
Key Type Required Meaning
name string yes What the Agent menu shows and what the window is titled. Must be unique within the merged set.
command string yes The executable to run. Looked up on PATH unless it contains a separator.
args list of strings no Its arguments, passed as given — no shell, so no quoting, globbing or &&.
env table of strings no Environment variables added to the ones the editor was started with. A name given here wins.
cwd string no Where the process starts, and the cwd the agent is told about. Relative to the project root. Defaults to the project root.

env may also be written as a sub-table, which is the same thing:

[[agent]]
name    = "Bob (llama.cpp)"
command = "docker"
args    = ["agent", "serve", "acp", ".turbo-js/agent.yaml"]

[agent.env]
TELEMETRY_ENABLED = "false"

What is refused

The file is refused as a whole, rather than partly loaded, when any of these hold. A half-loaded menu offering three of your five agents is worse than an error saying why.

Problem Message
an agent with no name reading …/acp.toml: agent 1 has no name
an agent with no command reading …/acp.toml: agent "Bob" has no command
two agents with the same name reading …/acp.toml: two agents are called "Bob"
a key the format does not define reading …/acp.toml: agent.comand is not a key this file has

The last one is deliberate: a misspelt key that was quietly ignored would look exactly like one that had no effect.

The Agent menu

Alt-A opens it. It is on the bar whether or not any agent is configured, because that is where Create agents file has to be reachable from.

Item Enabled when Effect
one item per agent, by name always Start that agent and open a window on it
Create agents file no acp.toml in the project Write the starter file and open it
Cancel turn a turn is running in the front window session/cancel
Agent status always What was loaded, what each command line is, and what failed

Keys inside an agent window

An agent window is an ordinary window: F6, Alt-1Alt-9, Tile, Maximise, [x] and [■] all work on it. Inside it:

Key Effect
Enter Send the input box as a prompt
Alt-Enter Insert a newline in the input box
Tab Move focus between the conversation and the input box
Ctrl-C, Ctrl-Ins Copy the selection, or the block the cursor is on
Esc Drop the selection; with none, cancel the turn in progress
Ctrl-W Close the window and stop the agent

With the input box focused:

Key Effect
Home End Move the cursor in what you are typing
Backspace Delete Edit it; backspace at the start of a line joins it to the one above
/ as the first character Open the list of the agent's commands — see Commands and mentions
@ Open the list of the project's files, narrowed by what you type after it
PgUp PgDn, list open Move through the list
Tab, list open Take the highlighted entry
Enter, list open Take the highlighted entry; on a word that is already complete, send
Esc, list open Close the list until the text changes

With the conversation focused:

Key Effect
Move the cursor one line
PgUp PgDn Move it a screenful
Home End The start of the conversation, and the end
Shift- any of those Extend the selection instead
Drag with button 1 Select by hand
Wheel Scroll three lines, leaving the cursor where it is

Unlike a terminal window, an agent window does not take the editor's shortcuts: there is no shell to need Ctrl-F, so it keeps its usual meaning. Ctrl-C is the exception, and only because nothing else in an agent window wants it.

Commands and mentions

Two characters open a list over the bottom of the conversation while you type. They are the same two Zed uses, so an agent's own documentation — "type /web to search" — holds here too.

/ — the agent's commands

An agent may announce commands with available_commands_update, at the start of the session or at any point during it. Typing / as the first character of the box lists them: the name, the agent's description, and, in angle brackets, what it expects after the name when it expects something. Keep typing to narrow the list; the match is on the start of the name and ignores case.

Tab completes the highlighted command. A command that takes input is completed with a trailing space, so the next thing you type is its argument; one that takes none is completed to the bare name. Enter completes too, except on a word that already reads exactly as a command, where it sends.

On the wire a command is text: /web agent client protocol goes out as one text block, and the agent recognises it by its first word. That is the whole protocol for commands, and it is why a / anywhere but the start of the box is just a character.

With no commands announced, / is a character and Tab keeps its ordinary meaning. Agent ▸ Agent status lists the commands with their descriptions.

@ — a file from the project

Typing @ anywhere in the box lists the project's files, relative to the project root with forward slashes. What you type after the @ narrows the list: files whose own name begins with it come first, then files whose path merely contains it. Tab or Enter completes the highlighted one and adds a space.

When the prompt is sent, each @name that names a file the list knew becomes a content block in place of the name:

The agent declared The block sent
promptCapabilities.embeddedContext: true resource — the file's uri, mimeType and full text, read the way fs/read_text_file reads it: from the open buffer when the file is open and modified
anything else, or the file could not be read resource_link — the uri, name and mimeType, for the agent to fetch itself

The words either side go as text blocks, so explain @docs/README.md please is three blocks: explain , the file, please. The conversation keeps the line as you typed it.

A word that begins with @ and names no file stays text — an e-mail address in a prompt is not a file — and @main.go does not name main.gopher: the name has to end the word.

The list is the project walked from its root, .git left out, at most 5 000 files, and at most 200 of them shown at once. Past either limit, type one more letter. It is walked afresh each time @ opens the list, so a file the agent just created is in it.

Copying

Selection is by whole lines. Nothing in a conversation is edited, so half a line is never what somebody means, and whole lines keep a copied code block's indentation intact.

With nothing selected, copying takes the region the cursor is on: one fenced code block, one passage of prose, one tool call's output. A speaker's label and a tool call's heading are furniture and are regions of their own, so neither is ever copied with what it sits above.

The indentation the conversation is drawn with is removed, so pasted code is flush.

The text goes to two places at once:

Clipboard How Pasted with
The editor's directly Shift-Ins, into a file open here
The system's OSC 52, through the terminal Ctrl-V, anywhere else

Nothing checks whether the terminal accepted the second: there is no reply to check, and a terminal may refuse OSC 52 for security or need it turned on. The editor's own clipboard has the text either way, and the status bar says how many lines were copied.

How much of the protocol is implemented

Protocol version 1. Turbo JS sends its version in initialize and accepts whatever version the agent answers with, provided it is one it knows.

What the editor calls on the agent

Method Implemented Notes
initialize yes Advertises the fs capability below; terminal is not advertised
session/new yes cwd from the agent's cwd key; mcpServers is always empty — MCP servers are the agent's own business
session/prompt yes Text blocks, and one resource or resource_link block per file named with @ — see Commands and mentions
session/cancel yes Esc, and Agent ▸ Cancel turn
session/load no Conversations do not survive closing the window
authenticate no An agent that lists authMethods is reported as needing a login the editor cannot perform

What the agent may call on the editor

Method Implemented Notes
session/update yes See the table below
session/request_permission yes A modal dialog carrying the agent's own options
fs/read_text_file yes From the open buffer when the file is open and modified, otherwise from disk
fs/write_text_file yes Into the open buffer when the file is open, otherwise to disk
terminal/* no Not advertised, so a conforming agent will not ask

Session updates

sessionUpdate Shown as
agent_message_chunk The agent's reply, appended as it arrives
agent_thought_chunk The same, in the comment colour, under a thinking label
user_message_chunk Your own message, as the agent echoes it back
tool_call A line naming the tool and its title, with its status
tool_call_update Folded onto the line the toolCallId matches, carrying its output
plan The entries as a list, each with its status
available_commands_update The list / opens in the input box; also listed, with descriptions, by Agent ▸ Agent status
usage_update The token count on the status bar while the window is in front
anything else Ignored, and counted; the count is in Agent status

While a turn is running, the rule between the panes turns a spinner. It is drawn from the clock rather than from a counter, so two windows thinking at once turn in step and nothing has to be reset when a turn begins. The window's title deliberately does not animate: it is also what the window list and the Alt-digit menu show, and a name changing eight times a second makes both flicker.

An unknown update is ignored rather than refused: the protocol grows, and an editor that stopped talking to an agent because it learnt a new kind of message would be wrong more often than it was right.

Colouring

The conversation is drawn with keys every theme already sets, so none of them needed touching:

Part Class
A speaker's name syntax.keyword
A thought syntax.comment
A tool call and its status syntax.type
A failed tool call, and the editor's own notices diagnostic.error
A selected line, and the cursor's bar editor.selection
Code inside a fence the scanner for the fence's language
Everything else the window's plain text

A fenced block naming a language the editor colours — javascript, json, toml, yaml, markdown, html, xml, dockerfile, bash — is coloured by that scanner. One naming anything else, or nothing at all, is left plain.

Tracing the conversation with an agent

Variable Effect
TURBO_ACP_TRACE=<file> Append every message to and from every agent to that file, one per line, stamped with the time and marked -> (sent) or <- (received)

It is for the one question the screen cannot answer — what did the agent actually send? An update this editor cannot decode is counted under Agent ▸ Agent status, which also names the last one and its error; the trace shows the message itself. A file that cannot be opened means no trace and nothing else: the trace is never allowed to break the editor.

Limits

  • One session per window. Closing the window ends the session; there is no resume.
  • Text and files only. The editor sends text, and the files you name with @; not images or audio, whatever the agent's promptCapabilities say.
  • No authentication. An agent that requires a login must be logged in by its own CLI before the editor starts it.
  • args are not a shell command. command = "sh", args = ["-c", "…"] is how to get one deliberately.
  • One entry is capped at a megabyte of text. An agent printing a whole build log cannot make the window unusable; what was dropped is said in the entry itself.

See also

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# Agents and ACP

Turbo JS is a client for the [Agent Client Protocol](https://agentclientprotocol.com). It starts each agent as a child process and exchanges JSON-RPC 2.0 messages with it over stdin and stdout, one message per line.

## Where the file lives

| Path | Read | Purpose |
| --- | --- | --- |
| `~/.config/turbo-js/acp.toml` | first | Agents you want in every project |
| `<project>/.turbo-js/acp.toml` | second | Agents belonging to this project |

Both are optional. Where an agent's `name` appears in both, the project's replaces the user's, being the more specific statement — the same rule [snippets](snippets.md) follow. A missing file is not an error; a file that is present but unreadable is, and is reported under **Agent ▸ Agent status** rather than silently leaving the menu empty.

`TURBO_JS_DIR` overrides the directory the user-level file is looked for in. The project file is always `.turbo-js/acp.toml` under the directory the editor was started in — there is no walk up the tree, for the same reason [project settings](project-settings.md) do not walk up.

## File format

One `[[agent]]` block per agent, in the order you want them in the menu.

```toml
[[agent]]
name    = "Bob (llama.cpp)"
command = "docker"
args    = ["agent", "serve", "acp", ".turbo-js/agent.yaml"]
env     = { TELEMETRY_ENABLED = "false" }
cwd     = "."
```

| Key | Type | Required | Meaning |
| --- | --- | --- | --- |
| `name` | string | **yes** | What the Agent menu shows and what the window is titled. Must be unique within the merged set. |
| `command` | string | **yes** | The executable to run. Looked up on `PATH` unless it contains a separator. |
| `args` | list of strings | no | Its arguments, passed as given — no shell, so no quoting, globbing or `&&`. |
| `env` | table of strings | no | Environment variables added to the ones the editor was started with. A name given here wins. |
| `cwd` | string | no | Where the process starts, and the `cwd` the agent is told about. Relative to the project root. Defaults to the project root. |

`env` may also be written as a sub-table, which is the same thing:

```toml
[[agent]]
name    = "Bob (llama.cpp)"
command = "docker"
args    = ["agent", "serve", "acp", ".turbo-js/agent.yaml"]

[agent.env]
TELEMETRY_ENABLED = "false"
```

### What is refused

The file is refused as a whole, rather than partly loaded, when any of these hold. A half-loaded menu offering three of your five agents is worse than an error saying why.

| Problem | Message |
| --- | --- |
| an agent with no `name` | `reading …/acp.toml: agent 1 has no name` |
| an agent with no `command` | `reading …/acp.toml: agent "Bob" has no command` |
| two agents with the same `name` | `reading …/acp.toml: two agents are called "Bob"` |
| a key the format does not define | `reading …/acp.toml: agent.comand is not a key this file has` |

The last one is deliberate: a misspelt key that was quietly ignored would look exactly like one that had no effect.

## The Agent menu

`Alt-A` opens it. It is on the bar whether or not any agent is configured, because that is where **Create agents file** has to be reachable from.

| Item | Enabled when | Effect |
| --- | --- | --- |
| *one item per agent, by name* | always | Start that agent and open a window on it |
| **Create agents file** | no `acp.toml` in the project | Write the starter file and open it |
| **Cancel turn** | a turn is running in the front window | `session/cancel` |
| **Agent status** | always | What was loaded, what each command line is, and what failed |

## Keys inside an agent window

An agent window is an ordinary window: `F6`, `Alt-1``Alt-9`, Tile, Maximise, `[x]` and `[■]` all work on it. Inside it:

| Key | Effect |
| --- | --- |
| `Enter` | Send the input box as a prompt |
| `Alt-Enter` | Insert a newline in the input box |
| `Tab` | Move focus between the conversation and the input box |
| `Ctrl-C`, `Ctrl-Ins` | Copy the selection, or the block the cursor is on |
| `Esc` | Drop the selection; with none, cancel the turn in progress |
| `Ctrl-W` | Close the window and stop the agent |

With the **input box** focused:

| Key | Effect |
| --- | --- |
| `↑` `↓` `←` `→` `Home` `End` | Move the cursor in what you are typing |
| `Backspace` `Delete` | Edit it; backspace at the start of a line joins it to the one above |
| `/` as the first character | Open the list of the agent's commands — see [Commands and mentions](#commands-and-mentions) |
| `@` | Open the list of the project's files, narrowed by what you type after it |
| `↑` `↓` `PgUp` `PgDn`, list open | Move through the list |
| `Tab`, list open | Take the highlighted entry |
| `Enter`, list open | Take the highlighted entry; on a word that is already complete, send |
| `Esc`, list open | Close the list until the text changes |

With the **conversation** focused:

| Key | Effect |
| --- | --- |
| `↑` `↓` | Move the cursor one line |
| `PgUp` `PgDn` | Move it a screenful |
| `Home` `End` | The start of the conversation, and the end |
| `Shift-` any of those | Extend the selection instead |
| Drag with button 1 | Select by hand |
| Wheel | Scroll three lines, leaving the cursor where it is |

Unlike a terminal window, an agent window does **not** take the editor's shortcuts: there is no shell to need `Ctrl-F`, so it keeps its usual meaning. `Ctrl-C` is the exception, and only because nothing else in an agent window wants it.

## Commands and mentions

Two characters open a list over the bottom of the conversation while you type. They are the same two Zed uses, so an agent's own documentation — "type `/web` to search" — holds here too.

### `/` — the agent's commands

An agent may announce commands with `available_commands_update`, at the start of the session or at any point during it. Typing `/` as the **first character** of the box lists them: the name, the agent's description, and, in angle brackets, what it expects after the name when it expects something. Keep typing to narrow the list; the match is on the start of the name and ignores case.

`Tab` completes the highlighted command. A command that takes input is completed with a trailing space, so the next thing you type is its argument; one that takes none is completed to the bare name. `Enter` completes too, except on a word that already reads exactly as a command, where it sends.

On the wire a command is **text**: `/web agent client protocol` goes out as one text block, and the agent recognises it by its first word. That is the whole protocol for commands, and it is why a `/` anywhere but the start of the box is just a character.

With no commands announced, `/` is a character and `Tab` keeps its ordinary meaning. **Agent ▸ Agent status** lists the commands with their descriptions.

### `@` — a file from the project

Typing `@` anywhere in the box lists the project's files, relative to the project root with forward slashes. What you type after the `@` narrows the list: files whose own name begins with it come first, then files whose path merely contains it. `Tab` or `Enter` completes the highlighted one and adds a space.

When the prompt is sent, each `@name` that names a file the list knew becomes a content block **in place of the name**:

| The agent declared | The block sent |
| --- | --- |
| `promptCapabilities.embeddedContext: true` | `resource` — the file's `uri`, `mimeType` and full `text`, read the way `fs/read_text_file` reads it: from the open buffer when the file is open and modified |
| anything else, or the file could not be read | `resource_link` — the `uri`, `name` and `mimeType`, for the agent to fetch itself |

The words either side go as text blocks, so `explain @docs/README.md please` is three blocks: `explain `, the file, ` please`. The conversation keeps the line as you typed it.

A word that begins with `@` and names no file stays text — an e-mail address in a prompt is not a file — and `@main.go` does not name `main.gopher`: the name has to end the word.

The list is the project walked from its root, `.git` left out, at most 5 000 files, and at most 200 of them shown at once. Past either limit, type one more letter. It is walked afresh each time `@` opens the list, so a file the agent just created is in it.

## Copying

Selection is by **whole lines**. Nothing in a conversation is edited, so half a line is never what somebody means, and whole lines keep a copied code block's indentation intact.

With nothing selected, copying takes the **region the cursor is on**: one fenced code block, one passage of prose, one tool call's output. A speaker's label and a tool call's heading are furniture and are regions of their own, so neither is ever copied with what it sits above.

The indentation the conversation is drawn with is removed, so pasted code is flush.

The text goes to two places at once:

| Clipboard | How | Pasted with |
| --- | --- | --- |
| The editor's | directly | `Shift-Ins`, into a file open here |
| The system's | OSC 52, through the terminal | `Ctrl-V`, anywhere else |

Nothing checks whether the terminal accepted the second: there is no reply to check, and a terminal may refuse OSC 52 for security or need it turned on. The editor's own clipboard has the text either way, and the status bar says how many lines were copied.

## How much of the protocol is implemented

Protocol version **1**. Turbo JS sends its version in `initialize` and accepts whatever version the agent answers with, provided it is one it knows.

### What the editor calls on the agent

| Method | Implemented | Notes |
| --- | --- | --- |
| `initialize` | yes | Advertises the `fs` capability below; `terminal` is not advertised |
| `session/new` | yes | `cwd` from the agent's `cwd` key; `mcpServers` is always empty — MCP servers are the agent's own business |
| `session/prompt` | yes | Text blocks, and one `resource` or `resource_link` block per file named with `@` — see [Commands and mentions](#commands-and-mentions) |
| `session/cancel` | yes | `Esc`, and **Agent ▸ Cancel turn** |
| `session/load` | **no** | Conversations do not survive closing the window |
| `authenticate` | **no** | An agent that lists `authMethods` is reported as needing a login the editor cannot perform |

### What the agent may call on the editor

| Method | Implemented | Notes |
| --- | --- | --- |
| `session/update` | yes | See the table below |
| `session/request_permission` | yes | A modal dialog carrying the agent's own options |
| `fs/read_text_file` | yes | From the open buffer when the file is open and modified, otherwise from disk |
| `fs/write_text_file` | yes | Into the open buffer when the file is open, otherwise to disk |
| `terminal/*` | **no** | Not advertised, so a conforming agent will not ask |

### Session updates

| `sessionUpdate` | Shown as |
| --- | --- |
| `agent_message_chunk` | The agent's reply, appended as it arrives |
| `agent_thought_chunk` | The same, in the comment colour, under a *thinking* label |
| `user_message_chunk` | Your own message, as the agent echoes it back |
| `tool_call` | A line naming the tool and its title, with its status |
| `tool_call_update` | Folded onto the line the `toolCallId` matches, carrying its output |
| `plan` | The entries as a list, each with its status |
| `available_commands_update` | The list `/` opens in the input box; also listed, with descriptions, by **Agent ▸ Agent status** |
| `usage_update` | The token count on the status bar while the window is in front |
| anything else | Ignored, and counted; the count is in **Agent status** |

While a turn is running, the rule between the panes turns a spinner. It is drawn from the clock rather than from a counter, so two windows thinking at once turn in step and nothing has to be reset when a turn begins. The window's *title* deliberately does not animate: it is also what the window list and the `Alt`-digit menu show, and a name changing eight times a second makes both flicker.

An unknown update is ignored rather than refused: the protocol grows, and an editor that stopped talking to an agent because it learnt a new kind of message would be wrong more often than it was right.

## Colouring

The conversation is drawn with keys every theme already sets, so none of them needed touching:

| Part | Class |
| --- | --- |
| A speaker's name | `syntax.keyword` |
| A thought | `syntax.comment` |
| A tool call and its status | `syntax.type` |
| A failed tool call, and the editor's own notices | `diagnostic.error` |
| A selected line, and the cursor's bar | `editor.selection` |
| Code inside a fence | the scanner for the fence's language |
| Everything else | the window's plain text |

A fenced block naming a language the editor colours — `javascript`, `json`, `toml`, `yaml`, `markdown`, `html`, `xml`, `dockerfile`, `bash` — is coloured by that scanner. One naming anything else, or nothing at all, is left plain.

## Tracing the conversation with an agent

| Variable | Effect |
| --- | --- |
| `TURBO_ACP_TRACE=<file>` | Append every message to and from every agent to that file, one per line, stamped with the time and marked `->` (sent) or `<-` (received) |

It is for the one question the screen cannot answer — *what did the agent actually send?* An update this editor cannot decode is counted under **Agent ▸ Agent status**, which also names the last one and its error; the trace shows the message itself. A file that cannot be opened means no trace and nothing else: the trace is never allowed to break the editor.

## Limits

- **One session per window.** Closing the window ends the session; there is no resume.
- **Text and files only.** The editor sends text, and the files you name with `@`; not images or audio, whatever the agent's `promptCapabilities` say.
- **No authentication.** An agent that requires a login must be logged in by its own CLI before the editor starts it.
- **`args` are not a shell command.** `command = "sh"`, `args = ["-c", "…"]` is how to get one deliberately.
- **One entry is capped** at a megabyte of text. An agent printing a whole build log cannot make the window unusable; what was dropped is said in the entry itself.

## See also

- The task: [How to talk to a coding agent from the editor](../how-to/talk-to-an-agent.md)
- The reasoning: [Agent windows](../explanation/agent-windows.md)
- The protocol: [agentclientprotocol.com](https://agentclientprotocol.com)