Repository navigation
Description
We're moving Buzz desktop agents off buzz-acp and onto app plugins. This post covers what changes and why we're doing it.
A quick note on terms: a harness is the agent program that does the actual work, such as Claude or Codex. Buzz's job is to get messages to the harness and let the harness act in Buzz.
Before and after
Buzz does three things for an agent: it wakes the agent when something happens, runs the agent's loop in its harness, and carries the agent's actions back into Buzz. Here's how each one changes.
Triggering
Today, with buzz-acp. Each agent runs as a separate process next to the app: its own buzz-acp listener. The app starts and supervises that listener, restarts it when it exits, and starts a stopped agent when someone mentions it. The listener keeps its own authenticated connection to the relay and subscribes to every channel the agent is in.
On Buzz 1.0, with plugins. The app is already connected to the relay for its owner. Events for all of the owner's agents arrive on that one connection, and the app hands each one to the right agent. There's no separate process to start, supervise or wake.
The agent loop
Today, with buzz-acp. The listener translates Buzz events into ACP (the Agent Client Protocol) and drives a harness adapter such as claude-agent-acp, which then drives the harness. Settings like the model and prompt are fixed when the process starts.
On Buzz 1.0, with plugins. Each harness has one plugin, and every agent that uses that harness runs through it. The plugin talks to the harness in the harness's own protocol: stream-json for Claude Code, the app-server for Codex. Sessions stay warm between turns, and setting changes apply from the next turn. ACP is still a way a plugin might implement that handling, but we use whatever the harness supports best.
Talking to Buzz
Today, with buzz-acp. The agent acts in Buzz by running the buzz CLI from its shell. Each command starts a new process, opens its own connection to the relay, authenticates, and signs with the agent's private key, which sits in the harness's environment.
On Buzz 1.0, with plugins. The agent acts in Buzz through built-in Buzz tools. The app signs each action with the agent's key in its own native code and sends it to the relay, so the harness never sees the key.
What gets better
Much less code
The plugin version is about 10k lines of code, compared with about 63k for buzz-acp and the app code that supported it (about 16k versus 160k with tests). Most of the saving is code we no longer need:
- No translation layer. buzz-acp turned Buzz events into ACP, and an adapter turned ACP into each harness's protocol. Plugins speak the harness's protocol directly.
- No second relay client. buzz-acp had its own code for connecting, authenticating, finding channels, subscribing and replaying, run once per agent. Plugins use the connection the app already has.
- No process lifecycle. The app no longer starts, stops, restarts or wakes agents, or tracks which setting changes are waiting for a restart.
- No plumbing across a process boundary. Agents live inside the app, so settings and views work directly instead of being passed to and from a sidecar.
The app also no longer ships about 254 MB of agent binaries. Agent behaviour now lives in the same repository and language as the rest of the app, so changing it is an ordinary app change.
Harness features work natively
A plugin uses the harness's own interface, so features work directly instead of through a translation layer:
- Steering. A message that arrives while the agent is working goes straight into the running turn. Claude Code folds it in, and Codex uses
turn/steer. Under ACP this relied on non-standard extensions (_goose/unstable/session/steer,_session/steering), and often fell back to cancelling the turn and resubmitting. - System prompt. Passed the way each harness expects. Under ACP it depended on the adapter: a Goose-specific method, the
_meta.systemPromptfield, or text pasted into the first user message. - Model and effort per message. You pick them when you mention the agent, and they apply to that turn without restarting anything. Under ACP, effort only worked where the adapter advertised
thought_level, and changing the model meant editing settings and restarting the agent. - Session resume. Uses the harness's own session IDs, per thread or per channel.
No more starting and stopping agents
Under buzz-acp, an agent is a running process, so the app has to manage its state:
- whether it's started or stopped, and whether it starts when the app launches;
- restarting the listener when it exits;
- waking a stopped agent when someone mentions it.
Its settings (model, prompt, harness, environment) are fixed when the process starts. Changing any of them means restarting the agent, and the app has to track which saved changes are still waiting for a restart.
Plugin agents have none of this. There's no started or stopped state: the agent exists, and the app hands it events. A change to the model, effort, prompt or any other setting applies from the next turn. If a session needs different launch settings, the app restarts it quietly while it's idle.
This is what made the model picker possible. You pick a model and effort in the composer, and that one message runs with them. Under buzz-acp, the same feature would have meant restarting the agent for every message.
About half the overhead from mention to reply
The time Buzz itself adds to each turn, not counting the model's own work, is about half what it was. The median dropped from 1.64 s to 0.89 s, which is 46% or 0.75 s faster. The slowest 10% of turns dropped from 3.85 s to 1.92 s, which is 50% or almost 2 s faster.
That's measured across 179 Claude Code turns on one machine. It covers the time from when a mention is sent until the harness receives it, plus the time to publish the reply. Here's why it's faster:
- The mention reaches the agent sooner. The median went from 1.18 s to about 0.5 s. Under buzz-acp, a mention passed through the agent's own relay connection, the listener, the ACP translation and the adapter before reaching the harness. With plugins, the app hands it straight to a session that's already running.
- Publishing is faster. Publishing the reply went from a median of 0.54 s to 0.23 s. Each CLI call started a new process, opened a new TLS connection and authenticated before it could send anything. Built-in tools skip all three, which speeds up every Buzz action the agent takes, not just the reply: 0.48 s down to 0.24 s at the median.
Less load on the relay
Under buzz-acp, every agent is its own relay client. Each agent:
- opens its own authenticated WebSocket;
- looks up its channels over REST;
- holds a subscription for each channel it's in, plus membership and control subscriptions;
- replays missed events on every reconnect.
So the relay sends each channel message once per agent in that channel, on top of the owner's own copy. While working, each agent also publishes typing updates, reaction markers, a liveness ping every 10 seconds, and usage updates. Every CLI command the agent runs is a separate authenticated HTTP client.
Under plugins, agents add no connections or subscriptions. The owner's single connection carries events for all of their agents. Each agent action is a single signed HTTP request from the app. Relay connections and subscriptions now grow with the number of people, not people × agents.
Also worth calling out
- The agent's key stays out of the model's process. Under buzz-acp, the harness environment includes
BUZZ_PRIVATE_KEY(buzz-acp/src/acp.rs) so the CLI can sign. Under plugins, the key stays in the app's native code: the app signs each action, and the harness never sees the key. - Agents are visible in the app. Session views, per-agent settings and per-message model choices live where you use the agents. Before, they were hidden behind a separate process and its logs.
- New harnesses are new plugins. Supporting another harness means writing an agent type against that harness's interface, not extending a shared runtime.
Activity