Skip to main content

bub-acp-server

Expose Bub as an Agent Client Protocol agent.

What It Provides

  • Bub plugin entry point: acp-server
  • CLI command registered on Bub: bub acp
  • Optional Streamable HTTP and WebSocket transports at /acp, served by Hypercorn with HTTP/2 support
  • Gateway channel: acp-server, with HTTP startup and shutdown managed by Bub
  • ACP agent methods for initialize, session/new, session/load, session/resume, session/list, session/delete, session/close, and session/prompt
  • Streaming ACP session/update events from Bub stream events
  • ACP client-backed replacements for Bub's bash, fs.read, fs.write, and fs.edit tools while the ACP server is running
  • Session-scoped model and reasoning-effort selection through ACP config options
  • Session MCP servers over stdio, Streamable HTTP, and SSE via bub-mcp
  • ACP context-compaction notifications when tape.handoff runs
  • Mid-turn steering through the _lody/session/steer ACP extension
  • Streamed subagent execution events through _lody/subagents/event
  • Automatic session titles pushed as session_info_update

Installation

uv pip install "git+https://github.com/bubbuild/bub-contrib.git#subdirectory=packages/bub-acp-server"

Or from a Bub project:

bub install bub-acp-server@main

Usage

Configure an ACP-compatible client to launch:

bub acp

The previous bub acp serve form remains accepted temporarily and prints a deprecation warning. Other positional arguments are rejected.

The process speaks ACP over stdio. Prompts are sent through Bub's hook pipeline with stream output enabled, so model chunks and tool events can be displayed by the ACP client as they arrive.

The replacement bash tool takes a command parameter, for example {"command": "git status --short"}, with no separate title parameter. ACP tool calls and updates display the command as the title and the output as the body, including when replaying session history.

While the command runs, tool updates attach the client terminal. On completion, a text snapshot of the result replaces the terminal reference so the output remains visible after the terminal is released, including in clients that discard terminal buffers on release.

The agent sends an ACP usage_update whenever the streamed usage snapshot changes, with a final end-of-stream check as a fallback. Missing token usage is reported as 0. If the model provider does not report its context-window size, set BUB_ACP_SERVER_CONTEXT_WINDOW_SIZE; the default is 128000 tokens.

The PromptResponse returned at the end of each turn carries an ACP usage summed over every model call in that turn: inputTokens, outputTokens, and totalTokens, plus thoughtTokens, cachedReadTokens, and cachedWriteTokens when the provider reports them. A turn without any reported usage leaves usage unset.

ACP clients can select both the model and reasoning effort for each session. Reasoning effort defaults to auto; the selected value is persisted with the ACP session and passed into Bub's turn state for subsequent model calls.

Available reasoning efforts are auto, none, minimal, low, medium, high, xhigh, max, and ultra. Values are passed through to the model provider; actual support depends on the selected model and provider.

The ACP stream router reports Bub's built-in tape.handoff as a context-compaction tool call. Compatible clients receive Context compacting and Context compacted updates marked with _meta.contextCompaction.

Bub keeps using its own configuration, tools, skills, and tapes. The ACP client starts the process and displays the session; it does not replace Bub's model setup.

Each ACP connection owns a Bub Agent for each session, with client filesystem, terminal, and elicitation tools. The agent is passed through the inbound message's _runtime_agent field so tape recovery, tool descriptions, and execution use the same instance. Client tools never modify the global REGISTRY, and other agents keep their own tools. This requires Bub's instance-tool API introduced in upstream PR #311.

ACP session IDs remain the protocol-facing chat_id. Bub namespaces its internal session ID with the ACP channel before selecting a tape, so an equal session ID from another channel cannot reuse the ACP tape.

ACP session metadata is stored under Bub home as acp-sessions.json so compatible clients can list sessions again after restarting. Keep BUB_HOME stable if you want the same ACP thread list across editor launches.

session/delete is available over stdio, HTTP, and WebSocket and is advertised as sessionCapabilities.delete: {}. It removes the session from the persisted list, cancels active and queued turns, releases session MCP resources, and clears the session's tape and configured sidecar tapes through Bub's tape store. Deleting an unknown or already-deleted session succeeds with an empty result. Explicitly loading or resuming the deleted ID starts with empty history. Store failures are reported and the session metadata is retained for retry. session/close retains tape history.

bub-acp-server supports both ACP session load and resume. session/load restores the matching Bub history through the same ACP streaming path used by live turns. session/resume attaches the editor back to the Bub session without replaying history, so later turns keep streaming through Bub's normal hook pipeline.

History is read through Bub's configured tape store. Store errors are reported instead of falling back to a separate local JSONL reader.

Session MCP servers

bub-acp-server depends on bub-mcp>=0.3.0. Initialization advertises mcpCapabilities: {"http": true, "sse": true}; stdio support is implicit in ACP. Pass servers in mcpServers when creating, loading, or resuming a session:

{
  "cwd": "/path/to/workspace",
  "mcpServers": [
    {"name": "local", "command": "/path/to/mcp-server", "args": [], "env": []},
    {"type": "http", "name": "remote", "url": "https://example.com/mcp", "headers": []}
  ]
}

Stdio processes use the session's working directory and supplied environment variables. HTTP/SSE connections receive the supplied headers. Setup waits for tool discovery; a connection failure returns an ACP error and closes the newly opened connections, preserving any previous session connections. Tools use bub-mcp names such as mcp.remote_search and are available only to that session's Agent. Session tools take precedence over configured MCP tools with the same name.

Loading or resuming replaces the entire server list; an omitted or empty list clears it. Active prompts finish before their connections are replaced. Closing a session cancels its pending turns and closes its MCP connections. Stdio, HTTP, and WebSocket server shutdown also releases all MCP connections. HTTP connections share the active session resources, so reconnecting clients can replace them without leaving the old connections running.

MCP server configuration, environment values, and headers are kept in memory and are not written to acp-sessions.json or mcp.json. Clients must resend configuration after a server restart.

HTTP and WebSocket transports

HTTP uses the SDK's experimental web transport, including POST requests, SSE streams, and connection IDs. Install the optional dependencies from a checkout:

uv pip install -e 'packages/bub-acp-server[http]'

Start the HTTP channel with the gateway:

bub gateway --enable-channel acp-server

Or set BUB_ENABLED_CHANNELS=acp-server and run bub gateway. The ACP channel is an Interface: it must be explicitly enabled and is not started by all. Other channels can be enabled alongside it with additional --enable-channel options. Gateway owns the framework/tape-store lifetime and stops the HTTP server and its background steering tasks on shutdown.

Gateway settings use the BUB_ACP_SERVER_ prefix:

  • HOST: listen address, default 127.0.0.1
  • PORT: listen port, default 28200
  • CERTFILE / KEYFILE: optional TLS certificate and private key; provide both

The same fields can be configured under acp-server in Bub's YAML configuration:

enabled_channels: acp-server
acp-server:
  host: 127.0.0.1
  port: 28200

The existing standalone HTTP command remains available:

bub acp --transport http

The default endpoint is http://127.0.0.1:28200/acp; use --host and --port to override it. The default transport remains stdio. HTTP listens on loopback by default and does not add authentication; use it with trusted clients.

For negotiated HTTP/2, configure TLS or terminate HTTP/2 at a reverse proxy:

bub acp --transport http --host 127.0.0.1 --port 8443 \
  --certfile /path/to/cert.pem --keyfile /path/to/key.pem

Hypercorn supports HTTP/2; the TLS endpoint is https://127.0.0.1:8443/acp. Plain HTTP also works with clients that use HTTP/1.1 for local development. SDK HTTP clients negotiate HTTP/2 over TLS; setting http2=True alone does not enable cleartext HTTP/2 negotiation.

Connect using the SDK (inside an async function, with an ACP Client implementation):

from acp import connect_to_agent
from acp.http import create_http_stream
from acp.schema import TextContentBlock

transport = create_http_stream("http://127.0.0.1:28200/acp")
connection = connect_to_agent(my_client, transport)
try:
    await connection.initialize(protocol_version=1)
    session = await connection.new_session(cwd="/path/to/workspace")
    await connection.prompt(
        session_id=session.session_id,
        prompt=[TextContentBlock(type="text", text="Hello")],
    )
finally:
    await connection.close()

Client-backed tools are selected from the capabilities advertised during initialization:

  • fs.readTextFile enables client-backed fs.read.
  • fs.writeTextFile enables client-backed fs.write.
  • Both filesystem capabilities are required for client-backed fs.edit.
  • terminal enables client-backed bash, bash.output, and bash.kill together.

Missing or false capabilities leave the corresponding Bub local tools in place.

Asking the user

Clients that advertise clientCapabilities.elicitation.form: {} also enable the session's ask_user tool. The tool sends elicitation/create with the current ACP session ID and waits for the client's form response. For example:

{
  "message": "Which approach should I use?",
  "requested_schema": {
    "type": "object",
    "properties": {
      "approach": {"type": "string", "enum": ["minimal", "full"]}
    },
    "required": ["approach"]
  }
}

Use ACP's form JSON Schema for requested_schema; string fields without an enum allow free text. The tool's parameter schema exposes the SDK's ElicitationSchema and its referenced field definitions so the model can see the supported form structure. The tool returns JSON such as {"action":"accept","content":{"approach":"minimal"}}, {"action":"decline"}, or {"action":"cancel"}. Decline and cancel do not supply an answer. Cancelling the active turn interrupts the pending request.

An omitted/null elicitation.form, an empty elicitation object, or URL-only support does not enable this tool. In those cases the agent can ask in its normal reply and wait for the user's next message. Clients must implement form elicitation to display the UI; tool-execution permission dialogs are a separate mechanism.

Connection isolation

Each connection has its own client capabilities and stream router. ACP prompts are serialized across connections and share session metadata so one connection cannot overwrite another's newly created sessions. In gateway mode, the gateway router remains bound; the ACP channel routes each turn's output to its client. Client-backed tools are scoped to ACP turns, and concurrent non-ACP turns continue using the original tools.

The dependency is pinned to agent-client-protocol 1.0.0rc2. HTTP uses BubACPAgent directly and supports initialization, new sessions, session loading and listing, config options, prompts, tool callbacks, and steering. Session stream registration and history replay are handled natively by the SDK, without a local compatibility adapter. Clients should also use SDK 1.0.0rc2 or an equivalent implementation of load request/response correlation, including sessions with empty history.

After initializing a new connection, load an existing session with await connection.load_session(cwd="/path/to/workspace", session_id="existing-session-id"), then continue with connection.prompt(...). Keep the same Bub home and tape store when restarting the server to retain metadata and history.

HTTP does not advertise session resume/close because the SDK web adapter still does not enable those unstable routes. Stdio enables them by default. connection.close() terminates the HTTP connection with DELETE; automatic SSE reconnection is not provided by the SDK.

WebSocket

Start the same web server using the explicit WebSocket option:

bub acp --transport websocket

The default WebSocket endpoint is ws://127.0.0.1:28200/acp. Use the same --host, --port, --certfile, and --keyfile options as HTTP; with TLS, connect using wss://. Both --transport http and --transport websocket start the SDK's combined HTTP/WebSocket endpoint, not protocol-exclusive listeners. The gateway's acp-server channel also accepts WebSocket connections without additional configuration. The same bub-acp-server[http] extra supplies all required dependencies.

In the SDK example above, replace transport creation with:

from acp.ws import create_websocket_stream

transport = await create_websocket_stream("ws://127.0.0.1:28200/acp")

Initialization, session loading, prompts and tool callbacks work over the socket; connection.close() closes it. WebSocket does not require HTTP/2. Like HTTP, it does not advertise the SDK's disabled unstable resume/close routes and adds no authentication.

Lody extension capabilities

Everything Lody adds lives under agentCapabilities._meta.lody, and that block is reported only to clients that identify themselves as Lody in InitializeRequest.clientInfo:

{
  "clientInfo": { "name": "lody", "title": "Lody", "version": "1" }
}

The name is matched case-insensitively as a prefix, so variants such as lody-cli are recognized; an absent or unknown identity is not Lody. Other clients receive no Lody-namespaced capability and no Lody-specific behavior. The generic {"steering": {"supported": true}} field on the initialize response is not Lody-namespaced and stays unconditional, so clients using the original Codex steering route can still detect it.

A Lody client sees the complete set:

{
  "agentCapabilities": {
    "_meta": {
      "lody": {
        "steering": {
          "version": 1,
          "transport": "request",
          "upstreamTurn": "same",
          "configPolicy": "active"
        },
        "subagentEvents": { "version": 1 },
        "sessionTitle": { "version": 1 }
      }
    }
  }
}

Each section below describes one entry.

Steering

Clients can detect steering support in the initialize response:

{
  "_meta": {
    "steering": {
      "supported": true
    }
  }
}

Clients that require acknowledged steering can also negotiate the Lody extension under agentCapabilities._meta, which Lody clients receive in the capability block above:

{
  "agentCapabilities": {
    "_meta": {
      "lody": {
        "steering": {
          "version": 1,
          "transport": "request",
          "upstreamTurn": "same",
          "configPolicy": "active"
        }
      }
    }
  }
}

Send a private ACP extension request while a turn is running or after it has become idle:

{
  "method": "_lody/session/steer",
  "params": {
    "sessionId": "session-id",
    "prompt": [
      {
        "type": "text",
        "text": "Stop the current approach and inspect the failing test first."
      }
    ],
    "steerId": "client-generated-steer-id"
  }
}

The response outcome is injected when Bub consumes the message at the next model-step boundary, startedNewTurn when the previous turn has already passed its final boundary, or failed for an unexpected internal failure. Steering requests are serialized per session and preserve arrival order. The extension is private rather than part of the standard ACP method set, so clients must opt into it explicitly.

When the message is applied, Bub sends _lody/session/steer_applied with the same sessionId and steerId, and returns injected. This lets the client distinguish submission from application.

For compatibility with clients using the original Codex steering extension, Bub also accepts _session/steering. Its optional steerId uses the matching _session/steering_applied notification.

Subagent events

Bub's subagent tool consumes the child agent's stream internally, so an ACP client would otherwise see only the outer tool call. Clients that want the child run can negotiate the Lody subagentEvents contract. Both sides opt in independently, so the agent advertises the capability:

{
  "agentCapabilities": {
    "_meta": {
      "lody": {
        "subagentEvents": {
          "version": 1
        }
      }
    }
  }
}

and a client opts in by sending the same version in InitializeRequest.clientCapabilities._meta.lody. Only then does Bub replace that session's subagent tool with a streaming variant and publish _lody/subagents/event notifications. The advertisement is part of the Lody capability block, so only Lody clients see it; the client's answer remains the switch, and an opt-in is honored even from a client that never saw the advertisement. The replacement keeps the tool's name, description, parameters, renderer and output schema, and still returns Bub's {session_id, output, errors} result to the model, so delegation behaves exactly as before.

Each event carries the ACP sessionId, an opaque per-execution runId, and one of three payloads:

type Payload
snapshot Task state (running, completed, failed, cancelled), description, model, summary
progress Sparse absolute observations such as lastToolName, toolCallCount, turnCount, durationMs
output One child stream chunk as an ACP session update: text, thought, or tool call/update

Snapshots replace previously known task metadata, and output follows the child's connection order. There are no sequence numbers, no replay guarantees and no cross-reconnect deduplication, and progress counters are display observations rather than usage accounting. A client that cannot receive an event never fails the tool.

outputRead is advertised as none and cancel as false, because runs are not addressable through the separate _lody/subagents/list|cancel|output request surface. Publishing is best effort: a client that cannot receive an event never fails the run, and the tool still returns its result.

The capability is always advertised, and client negotiation alone decides whether events are published. Clients that never opt in keep Bub's stock subagent tool unchanged.

Session titles

This contract has no client-side capability to negotiate, so it rides on the Lody client identification described above; other clients keep their own title process.

After the first turn of a session, Bub asks the session's model for a short title with a single completion that carries no tools and no skills, then pushes the result on the standard ACP callback:

{
  "sessionId": "session-id",
  "update": {
    "sessionUpdate": "session_info_update",
    "title": "Fix login redirect",
    "_meta": { "lody": { "titleSource": "generated" } }
  }
}

The title is stored in acp-sessions.json and returned by session/list afterwards. Advertising the capability tells a Lody client that it can skip its own title generator for the session. Generation happens at most once per session, is skipped for sessions that already have a title, and is best effort: a failure or a timeout leaves the client's draft title in place and no second generator is requested.

Use In Zed

Zed supports external terminal agents through ACP. Custom agents are configured in Zed's settings.json under agent_servers.

Prerequisites:

  • bub is installed and available to Zed.
  • bub-acp-server is installed in the Bub environment:
bub install bub-acp-server@main

Open Zed's settings with the zed: open settings command and add a custom agent server:

{
  "agent_servers": {
    "Bub": {
      "type": "custom",
      "command": "bub",
      "args": ["acp"],
      "env": {}
    }
  }
}

If Zed cannot find bub, use the absolute path printed by command -v bub:

{
  "agent_servers": {
    "Bub": {
      "type": "custom",
      "command": "/absolute/path/to/bub",
      "args": ["acp"],
      "env": {}
    }
  }
}

After saving the settings, open Zed's agent panel with cmd-? on macOS or ctrl-? on Linux/Windows, then start a new thread and select Bub.

Useful Zed commands while testing:

  • dev: open acp logs shows the JSON-RPC traffic between Zed and Bub.
  • zed: open settings opens settings.json.

Notes:

  • Zed launches Bub as a separate ACP process. Bub reads its own local configuration and credentials directly.
  • Use env only for settings your Bub installation actually needs.
  • If your Bub configuration is loaded from a project .env, use a wrapper command that loads that file before running bub acp.

References:

Metadata

Release files for bub-acp-server 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bub-acp-server 0.1.1
File Size Uploaded
bub_acp_server-0.1.1.tar.gz 36.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bub-acp-server 0.1.1
File Interpreter ABI Platform
bub_acp_server-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 75.3 kB

Release files / bub_acp_server-0.1.1.tar.gz

Download URL bub_acp_server-0.1.1.tar.gz
Size 36.2 kB
Tags Source
SHA-256 checksum
How to use checksums
ee27b7332a5589d675915c0ffbc73fed66b4186b3c0ea4618801ef1986364ba5
BLAKE2b-256 checksum
How to use checksums
dd2a6a249bbbda187e9b9f4600f0ceb38c21d95e6d70a7e6767d82c3a801518f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / bub_acp_server-0.1.1-py3-none-any.whl

Download URL bub_acp_server-0.1.1-py3-none-any.whl
Size 39.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0ed8e690418947542989b32b2c883ae48f7ed05e102d417839784a588ebad251
BLAKE2b-256 checksum
How to use checksums
d0334d5e879ea80ab8f16b8db789c17ed00241255b617b5ef0b267602b8dc67c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page