Skip to content

Add ACP v1 agent mode (cecli --acp) - #714

Merged
dwash96 merged 2 commits into
cecli-dev:v1.9.0from
1broseidon:feat/acp-stdio
Oct 17, 2026
Merged

dwash96 merged 2 commits into
cecli-dev:v1.9.0from
1broseidon:feat/acp-stdio

Conversation

@1broseidon

Copy link
Copy Markdown

cecli --acp runs cecli as an Agent Client Protocol v1
agent over stdio, so editors that speak ACP (Zed, or any other ACP v1 client) can launch it as
a subprocess and drive it with JSON-RPC on stdin/stdout. Discussed with @dwash96 on Discord
first; I've been using it day to day in Zed.

It only advertises what it implements, so a client never sees a capability cecli doesn't
handle. The WebSocket bridge (bridge.py, protocol.py, ws_server.py) is untouched.

What it does

  • initialize, session/new, session/prompt and session/cancel.
  • Model output streams as messages and reasoning as thoughts. In agent mode, tool calls show up
    as tool cards with their output, the todo list as the client's plan, and the Yield summary
    as a message.
  • cecli's confirmations become session/request_permission (yes / yes to all / skip all / no).
  • Slash commands work as typed and are offered as completions (available_commands_update).
    @-mentioned files are added to the chat; files outside the project are added read-only.
  • Mode, model and effort pickers (session config options):
    • Mode: code, ask, architect, context, agent, and Agent (YOLO), which is agent mode with
      what --yolo turns on.
    • Model: the model that answers in the current mode. The list starts with the configured
      models (startup model, aliases, tag overrides, .cecli.model.settings.yml), followed by the
      chat models of every provider whose API key is set, using the onboarding wizard's provider
      and key lookup. A new optional acp-models setting narrows that list with the same matching
      as /models, e.g. acp-models: [claude-sonnet, gpt-5*].
    • Effort: reasoning effort or thinking budget, whichever the model accepts.
    • Choosing a value runs the matching command (/ask, /model, /reasoning-effort, ...), and
      typing a command updates the picker. Legacy session/set_mode works too, for clients
      without config options.
  • MCP servers configured in the client (stdio, HTTP, SSE) are connected alongside cecli's own
    before the session starts.
  • Sub-agents as child sessions, following the
    Subagent Sessions RFD. It is unstable in
    ACP v1, so this only happens for clients that advertise clientCapabilities.subagents:
    • each sub-agent (Delegate, workspace and workflow agents) is announced with
      subagent_update, and its output, tool calls and permission requests go to its own session;
    • its work state is reported on the parent: running, requires_action while it waits for
      permission, then idle with end_turn, cancelled or error;
    • the delegated task, the result it returns and Broadcast messages are reported as
      session_message;
    • session/cancel on a child stops only that sub-agent, and a blocking Delegate carries on
      with the interrupted summary. Cancelling the parent's turn also stops the sub-agents it
      waits for;
    • clients without the capability see sub-agent output in the parent session.
  • Shell commands run with PAGER=cat. Otherwise git log and friends wait for a keypress
    inside the PTY and the turn hangs until the command times out.

How it works

  • One process serves one project. The first session/new picks the directory, and cecli
    starts there. A new thread on the same project gets a cleared chat.
  • The coder's normal linear run loop drives each turn: AcpInputOutput.get_input hands it the
    next prompt, and the turn ends when the loop asks for input again.
  • The code lives in cecli/helpers/server/acp/agent/: agent.py (protocol handlers, turns,
    permissions, sub-agent sessions), transport.py (a small Transport interface plus
    StdioTransport, so a WebSocket transport could drive the same agent), io.py (the
    InputOutput subclass), updates.py (cecli output as session/update), config.py
    (pickers) and mcp.py (client MCP servers).
  • --acp only counts on the command line, because the project has to be picked before any
    config is read. In a config file or CECLI_ACP it is ignored with a warning.

Changes to existing code

About 200 lines outside the new package:

  • args.py, main.py: the --acp and --acp-models flags and startup wiring.
  • io.py: no-op tool_call_event and subagent_event hooks. The coders call
    tool_call_event at three points; AgentService and Broadcast call subagent_event.
  • The usage report is tagged type="usage-report", so ACP can leave the token/cost line out
    of the chat (it still goes to the history file).
  • onboarding/providers.py: an optional modes filter on get_models_for_provider, so the
    picker leaves out image-generation models. Onboarding itself is unchanged.
  • io_proxy.py: tool_call_event is tagged with the coder's uuid, like other output.
  • agents/service.py: a new cancel_sub_agent(), plus fixes found while cancelling a single
    sub-agent, which also apply in the terminal:
    • a blocking Delegate no longer turns a cancelled sub-agent into a cancelled parent; it
      returns the interrupted summary, and still cancels the sub-agent when the parent is
      cancelled;
    • a cancelled sub-agent is reported as interrupted even when streaming absorbed the
      cancellation (it turns into a ^C and generate() returns normally);
    • Yield (tools/_yield.py) no longer raises when a sub-agent it waits for was cancelled.

Testing

  • tests/helpers/server/acp/test_agent.py (35 tests), sub-agent events and cancellation in
    tests/subagents/test_service.py, plus one each in tests/basic/test_main.py and
    tests/helpers/test_onboarding.py.
  • Full pytest on Python 3.14 (macOS): 2453 passed, 21 skipped. pre-commit passes on all files.
  • Manual: Zed, plus a scripted stdio client covering the pickers, YOLO, permissions, an MCP
    server passed in session/new, and sub-agents (blocking and parallel background
    delegations, cancelling a child, cancelling the parent) with a local model.
  • Zed doesn't advertise subagents yet, so in Zed sub-agents still show in the main thread.
  • Not tested on Windows or Python 3.10; the test workflows only run for PRs into main.

Docs: cecli/website/docs/config/acp.md

Not in this PR

  • A WebSocket transport for this agent
  • Several user sessions per process
  • session/load, image prompts, and cost and context usage via usage_update
  • The PAGER fix for the terminal agent mode, which has the same git log hang
  • A cancelled or failed background sub-agent's result never reaches its parent:
    _inject_sub_agent_result runs before the status is set in those branches. That predates
    this PR, so it's left for a separate fix.

1broseidon and others added 2 commits October 15, 2026 10:57
Lets editors that speak the Agent Client Protocol, such as Zed, launch cecli
as an agent over stdin/stdout.

- Prompts drive the normal linear run loop; slash commands are announced
  as available commands and @-mentioned files are added to the chat
- Model output and reasoning stream as message and thought chunks; agent
  tool calls become tool cards, the todo list becomes the plan, and the
  Yield summary is shown as a message
- Confirmations become permission requests that show their question;
  cancel interrupts the running request
- Mode (including Agent YOLO), model and reasoning effort are offered as
  session config options backed by the matching cecli commands; the model
  list adds the chat models of every provider with an API key set, and
  --acp-models narrows it
- MCP servers from the editor (stdio, HTTP, SSE) are connected alongside
  cecli's own
- Sub-agents are reported as ACP subagent sessions (unstable RFD) when the
  client advertises the subagents capability: each gets its own session,
  work state, messages between sessions and its own cancel
- Pagers are disabled for commands, and the per-request token/cost line is
  kept out of the editor

The agent lives in cecli/helpers/server/acp/agent/. Core changes are
InputOutput.tool_call_event and subagent_event hooks (no-ops outside ACP),
a usage-report output type, AgentService.cancel_sub_agent, the --acp and
--acp-models flags and startup wiring in main.py. Cancelling a sub-agent no
longer fails a blocking Delegate or a Yield waiting on it, and is reported
as interrupted even when streaming absorbed the cancel.
@dwash96
dwash96 changed the base branch from v1.8.1 to v1.9.0 October 17, 2026 02:59
@dwash96
dwash96 merged commit ece2596 into cecli-dev:v1.9.0 Oct 17, 2026
1 check passed
@dwash96 dwash96 mentioned this pull request Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants