|
| 1 | +--- |
| 2 | +title: "ACP (Zed)" |
| 3 | +icon: "code" |
| 4 | +--- |
| 5 | + |
| 6 | +The **ACP Zed adapter** exposes a single BeeAI agent as a [Zed Agent Client Protocol (ACP)](https://github.com/zed-industries/agent-client-protocol) program over stdio, so it shows up directly inside [Zed](https://zed.dev/)'s agent panel. |
| 7 | + |
| 8 | +Unlike the HTTP-based [ACP integration](./acp.mdx), Zed's ACP runs **one agent per stdio process**: Zed launches your Python script, talks JSON-RPC over stdin/stdout, and renders responses, tool calls, and terminal output in the editor UI. |
| 9 | + |
| 10 | +The adapter also routes BeeAI's generic coding tools (`ShellTool`, `FileReadTool`, `FileEditTool`) through Zed's filesystem and terminal capabilities for the duration of a turn — so file edits show up as diffs in unsaved buffers, and shell commands open in Zed's terminal widget. |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## Prerequisites |
| 15 | + |
| 16 | +- **[Zed](https://zed.dev/)** editor installed |
| 17 | +- **BeeAI Framework** installed with the `acp-zed` extra: |
| 18 | + |
| 19 | + ```bash |
| 20 | + pip install "beeai-framework[acp-zed]" |
| 21 | + ``` |
| 22 | + |
| 23 | +- A configured LLM backend (Ollama, OpenAI, watsonx, …) |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Quick start |
| 28 | + |
| 29 | +### 1. Build and serve an agent |
| 30 | + |
| 31 | +The smallest viable coding agent — three generic tools, no ACP-specific code: |
| 32 | + |
| 33 | +{/* <!-- embedme python/examples/serve/acp_zed/simple.py --> */} |
| 34 | +```py Python [expandable] |
| 35 | +"""Smallest viable coding agent over Zed's Agent Client Protocol (stdio). |
| 36 | +
|
| 37 | +Three generic tools — `FileReadTool`, `FileEditTool`, `ShellTool` — and nothing |
| 38 | +ACP-specific in the agent definition. When served through `ACPZedServer`, the |
| 39 | +adapter installs ACP-routed backends for the turn, so file reads/writes flow |
| 40 | +through `fs/read_text_file` / `fs/write_text_file` and shell commands open in |
| 41 | +Zed's terminal widget. The same agent definition would run unchanged as a CLI |
| 42 | +script or over any other serve mode. |
| 43 | +
|
| 44 | +Launch from Zed (`~/.config/zed/settings.json`): |
| 45 | +
|
| 46 | + { |
| 47 | + "agent_servers": { |
| 48 | + "beeai": { |
| 49 | + "command": "python", |
| 50 | + "args": ["/absolute/path/to/examples/serve/acp_zed/simple.py"] |
| 51 | + } |
| 52 | + } |
| 53 | + } |
| 54 | +
|
| 55 | +Prereqs: `pip install "beeai-framework[acp-zed]"`, Ollama running with |
| 56 | +`granite4:micro`. |
| 57 | +""" |
| 58 | + |
| 59 | +from __future__ import annotations |
| 60 | + |
| 61 | +import asyncio |
| 62 | +import sys |
| 63 | + |
| 64 | +from dotenv import load_dotenv |
| 65 | + |
| 66 | +from beeai_framework.adapters.acp_zed import ACPZedServer |
| 67 | +from beeai_framework.agents.requirement import RequirementAgent |
| 68 | +from beeai_framework.backend import ChatModel |
| 69 | +from beeai_framework.memory import UnconstrainedMemory |
| 70 | +from beeai_framework.tools.code import ShellTool |
| 71 | +from beeai_framework.tools.filesystem import FileEditTool, FileReadTool |
| 72 | + |
| 73 | +load_dotenv() |
| 74 | + |
| 75 | + |
| 76 | +async def _build_agent() -> RequirementAgent: |
| 77 | + return RequirementAgent( |
| 78 | + llm=ChatModel.from_name("ollama:granite4:micro"), |
| 79 | + tools=[FileReadTool(), FileEditTool(), ShellTool()], |
| 80 | + memory=UnconstrainedMemory(), |
| 81 | + name="beeai-coder", |
| 82 | + description="A BeeAI RequirementAgent running as a Zed ACP agent.", |
| 83 | + ) |
| 84 | + |
| 85 | + |
| 86 | +def main() -> None: |
| 87 | + agent = asyncio.run(_build_agent()) |
| 88 | + ACPZedServer().register(agent).serve() |
| 89 | + |
| 90 | + |
| 91 | +if __name__ == "__main__": |
| 92 | + try: |
| 93 | + main() |
| 94 | + except KeyboardInterrupt: |
| 95 | + sys.exit(0) |
| 96 | + |
| 97 | +``` |
| 98 | + |
| 99 | +### 2. Register the agent in Zed |
| 100 | + |
| 101 | +Add the entry to `~/.config/zed/settings.json`: |
| 102 | + |
| 103 | +```json |
| 104 | +{ |
| 105 | + "agent_servers": { |
| 106 | + "beeai": { |
| 107 | + "command": "python", |
| 108 | + "args": ["/absolute/path/to/examples/serve/acp_zed/simple.py"] |
| 109 | + } |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +Open Zed's agent panel and pick **beeai** from the dropdown — the script is launched on demand and JSON-RPC frames flow over stdio. |
| 115 | + |
| 116 | +--- |
| 117 | + |
| 118 | +## How it works |
| 119 | + |
| 120 | +`ACPZedServer` wraps the agent in an `ACPZedServerAgent` (one process, one agent) and runs it under the [`acp` Python SDK](https://github.com/zed-industries/agent-client-protocol). For every `session/prompt` call from the editor it: |
| 121 | + |
| 122 | +1. Creates a session-scoped clone of the agent (so memory persists across prompts in the same chat but is isolated between chats). |
| 123 | +2. Installs ACP-routed backends for the duration of the turn — `ShellBackend`, `FileBackend`, and `io_confirm` are swapped onto implementations that talk to the editor. |
| 124 | +3. Streams assistant text via `agent_message_chunk`, tool calls and tool results via `agent_thought_chunk`, so users see the agent's intermediate steps live. |
| 125 | + |
| 126 | +The wiring lives in three places: |
| 127 | + |
| 128 | +- `ACPZedServer` / `ACPZedServerAgent` — the server + per-session lifecycle. |
| 129 | +- `FsBridge` — agent-side handle to the editor's `fs/*` and `terminal/*` methods, with the active `session_id` carried through a `ContextVar`. |
| 130 | +- `ACPZedIOContext` — the per-turn context manager that swaps backends in and out. |
| 131 | + |
| 132 | +### Supported agent types |
| 133 | + |
| 134 | +The adapter ships factories for every BeeAI agent type, so `register(agent)` works out of the box: |
| 135 | + |
| 136 | +- `RequirementAgent` |
| 137 | +- `ToolCallingAgent` |
| 138 | +- `ReActAgent` |
| 139 | +- `LiteAgent` |
| 140 | + |
| 141 | +Each factory streams the right events for that agent's emitter shape (e.g. `final_answer` for `LiteAgent`, `partial_update` for `ReActAgent`). |
| 142 | + |
| 143 | +### stdio framing |
| 144 | + |
| 145 | +Zed's ACP transport uses **stdout exclusively for JSON-RPC frames**. A stray `print()` or log line on stdout will corrupt the stream. The adapter rebinds any logging handler bound to `sys.stdout` onto `sys.stderr` at startup; if you have your own logging configuration, make sure it follows suit. |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +## Auto-routed coding tools |
| 150 | + |
| 151 | +The headline benefit of running under `ACPZedServer` is that the [generic coding tools](/modules/tools/#coding-tools) become **editor-aware** with no code changes. The same `ShellTool()` / `FileReadTool()` / `FileEditTool()` that runs against local disk in a CLI script will, when served through this adapter, transparently route through: |
| 152 | + |
| 153 | +| Tool | Local default | Under `ACPZedServer` | |
| 154 | +|:----------------|:---------------------------|:----------------------------------------------------| |
| 155 | +| `FileReadTool` | `pathlib.Path.read_text` | `fs/read_text_file` — respects unsaved buffers | |
| 156 | +| `FileEditTool` | `pathlib.Path.write_text` | `fs/write_text_file` — editor renders diffs | |
| 157 | +| `ShellTool` | local `asyncio` subprocess | `terminal/*` — opens Zed's terminal widget | |
| 158 | +| `io_confirm` | console prompt | `session/request_permission` — modal in editor | |
| 159 | + |
| 160 | +Tools see this through a `ContextVar`-backed dispatch — no ACP-specific subclasses required. Outside an ACP turn, every tool falls back to its local default. |
| 161 | + |
| 162 | +```py |
| 163 | +from beeai_framework.adapters.acp_zed import ACPZedServer |
| 164 | +from beeai_framework.agents.lite import LiteAgent |
| 165 | +from beeai_framework.backend import ChatModel |
| 166 | +from beeai_framework.tools.code import ShellTool |
| 167 | +from beeai_framework.tools.filesystem import FileEditTool, FileReadTool, GlobTool, GrepTool |
| 168 | + |
| 169 | +agent = LiteAgent( |
| 170 | + llm=ChatModel.from_name("openai:gpt-4o-mini"), |
| 171 | + tools=[FileReadTool(), FileEditTool(), ShellTool(), GlobTool(), GrepTool()], |
| 172 | +) |
| 173 | + |
| 174 | +ACPZedServer().register(agent).serve() |
| 175 | +``` |
| 176 | + |
| 177 | +`GlobTool` and `GrepTool` are not routed — they hit the local filesystem directly, since the ACP capability set has no equivalent. They still work, but they don't see Zed's unsaved buffers. |
| 178 | + |
| 179 | +### ACP-specific behavior of routed backends |
| 180 | + |
| 181 | +A few things worth knowing about the ACP backends: |
| 182 | + |
| 183 | +- **`ShellTool` stdin** — `input_text` raises `ToolError` under the ACP terminal backend; the protocol has no way to feed stdin to a terminal. |
| 184 | +- **`ShellTool` streams** — Zed's terminal conflates stdout and stderr. The ACP backend returns the combined output in `stdout` and leaves `stderr` empty. |
| 185 | +- **`ShellTool` timeouts** — enforced by `asyncio.wait_for` around `wait_for_terminal_exit`, with an explicit `kill_terminal` + `release_terminal` on timeout. |
| 186 | +- **`io_read` is unsupported** — ACP has no free-form text-input method; calls raise `RuntimeError`. Use a tool call or `io_confirm` (which routes through `session/request_permission`) instead. |
| 187 | +- **Capability gating** — every routed call checks the client's advertised `ClientCapabilities` before dispatching. Reads/writes/terminals fail fast with a `ToolError` if Zed didn't advertise the matching capability. |
| 188 | + |
| 189 | +--- |
| 190 | + |
| 191 | +## Configuration |
| 192 | + |
| 193 | +`ACPZedServerConfig` is intentionally minimal — there's no port or host because Zed manages the process: |
| 194 | + |
| 195 | +```py |
| 196 | +from beeai_framework.adapters.acp_zed import ACPZedServer, ACPZedServerConfig |
| 197 | + |
| 198 | +ACPZedServer( |
| 199 | + config=ACPZedServerConfig( |
| 200 | + agent_name="beeai-coder", |
| 201 | + agent_description="A coding agent backed by BeeAI.", |
| 202 | + ) |
| 203 | +).register(agent).serve() |
| 204 | +``` |
| 205 | + |
| 206 | +If `agent_name` / `agent_description` are omitted, the adapter uses the agent's own metadata. |
| 207 | + |
| 208 | +--- |
| 209 | + |
| 210 | +## More examples |
| 211 | + |
| 212 | +<CardGroup cols={2}> |
| 213 | + <Card |
| 214 | + title="LiteAgent + full toolset" |
| 215 | + icon="hammer" |
| 216 | + href="https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/serve/acp_zed/lite.py" |
| 217 | + > |
| 218 | + Streams tokens via `LiteAgent`'s emitter callback and bridges through an `asyncio.Queue` to ACP. |
| 219 | + </Card> |
| 220 | + <Card |
| 221 | + title="Kiwi.com flight-search" |
| 222 | + icon="plane" |
| 223 | + href="https://github.com/i-am-bee/beeai-framework/blob/main/python/examples/serve/acp_zed/kiwi.py" |
| 224 | + > |
| 225 | + Domain-specific agent that pulls its toolset from a public MCP server. No file/shell tools. |
| 226 | + </Card> |
| 227 | +</CardGroup> |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +## Limitations |
| 232 | + |
| 233 | +- **One agent per process** — `serve()` raises if more than one agent is registered. Run multiple agents as separate `agent_servers` entries in `settings.json`. |
| 234 | +- **No image/audio prompt blocks** — the prompt converter currently extracts text and embedded-resource text only; image and audio blocks are dropped pending stable multi-part `UserMessage` support across all backends. |
| 235 | +- **`MCPTool` from Zed-managed servers** — `new_session` accepts `mcp_servers` from the editor but the adapter does not currently spin them up. Configure MCP servers on the agent itself instead. |
0 commit comments