Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ json5 = "0.4"
schemars = "1.0"
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
agent-client-protocol = { version = "=2.0.0", features = ["unstable_session_fork"] }
agent-client-protocol = { version = "=2.0.0", features = ["unstable_elicitation", "unstable_session_fork"] }
clap_complete = "4.5"
ignore = "0.4"
copypasta = "0.10"
Expand Down
43 changes: 27 additions & 16 deletions _docs/acp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,42 @@ Configure a provider in the Crabcode TUI with `/connect` before starting an ACP

## Capability Matrix

| Area | Supported behavior | Current limitations | Planned follow-up |
All Crabcode-side capabilities in this matrix are implemented. Conditions in the last column are editor, model, provider, or protocol requirements rather than incomplete server behavior.

| Area | Full behavior | Status | Runtime or protocol requirements |
| --- | --- | --- | --- |
| Transport | JSON-RPC over stdio through `crabcode acp`; clean stdin EOF shutdown. | stdout must remain protocol-only. | Add protocol-level subprocess integration coverage. |
| Sessions | Create, list, load, resume, close, and fork persisted root sessions. | Replay message IDs are deterministic per load, not durable message IDs. | Persist stable message IDs across streaming snapshots and reloads. |
| Prompts | Text, embedded text resources, and PNG, JPEG, GIF, or WebP image attachments; assistant text and reasoning stream back to the editor. | Images require an image-capable selected model; audio prompt blocks are unsupported. | Store ACP attachments persistently and add audio input support. |
| Modes and models | Visible primary Crabcode agents, selectable `/models` catalog entries, and model-supported reasoning-effort selectors are available as ACP options; changes are session-local. | Reasoning options depend on the selected model catalog metadata. | Add richer provider-specific reasoning configuration. |
| Tools | Tool calls and completed or failed results stream with ACP tool kinds, titles, raw input, and preview output. | No normalized locations, diffs, full outputs, or result images yet. | Preserve structured tool results, locations, patches, and image content in runtime events. |
| Permissions | Existing Crabcode permission prompts are forwarded with allow once, always allow, and reject choices. | Permission requests use a generated ACP call ID because the current internal prompt lacks the originating tool-call ID. | Carry the real tool-call ID and edit patch metadata through permission preflight. |
| Cancellation | `session/cancel` cancels the active turn and keeps the session reusable. | Provider stop reasons are currently reduced to normal completion, cancellation, or a safe failure. | Preserve output-limit and refusal stop reasons from the model runtime. |
| Commands and skills | Session updates publish available commands: project custom slash commands, workspace skills, plus built-in `/skills` and `/mcp`. Leading `/…` prompts expand through the same command and skill templates before the model turn. | Built-in TUI commands such as `/compact` are not ACP-available commands; unknown `/…` lines pass through as plain text. | Add more built-in commands (for example `/compact`) and richer command input schemas. |
| MCP | Project MCP from Crabcode config runs as usual. Editors may also pass MCP servers on `session/new`; those servers are merged into the session config (stdio, HTTP, and SSE). | HTTP and SSE client MCP are advertised; stdio client MCP is accepted and merged even though it is not a separate advertised capability flag. | Surface richer MCP connection status and OAuth for remote client servers. |
| Terminals | — | ACP terminal embedding is not implemented. | Add a client-terminal adapter for long-running shell sessions. |
| Questions | — | Interactive question prompts from the agent are not forwarded over ACP; the runtime skips them rather than blocking the editor. | Map Crabcode questions to ACP permission-style or dedicated question requests. |
| Usage | Estimated context-window usage is emitted when the selected model exposes a context limit. | Provider-authoritative token and cost accounting is not complete; usage is omitted when no context limit is known. | Retain provider input, output, cache, context, and cost data for authoritative usage updates. |
| Transport | JSON-RPC over stdio through `crabcode acp`, protocol-only stdout, clean stdin EOF shutdown, and subprocess initialize/response coverage. | Full | The subprocess wrapper must not write banners or logs to stdout. |
| Sessions | Create, cursor-list, load, resume, close, delete, and fork all persisted sessions, including child sessions. Lists include Crabcode parent/root IDs in ACP `_meta`; forks preserve the source title, regenerate message IDs, copy attachments independently, and publish commands for the new session. Delete removes persisted history and managed attachments. | Full | ACP has no standard nested-session tree field, so hierarchy is exposed through the `crabcode` metadata extension while the standard list remains flat. |
| Prompts | Text, embedded resources, PNG/JPEG/GIF/WebP images, and WAV/MP3 audio; assistant text and reasoning stream back to the editor. Attachments use private per-session storage, survive load/resume, copy independently on fork, delete with persisted sessions, and readable legacy paths migrate automatically on load. | Full | The selected model route must advertise the matching input modality. Audio uses verified OpenAI-compatible Chat Completions `input_audio`; unsupported provider transports return a clear error instead of dropping media. |
| Modes and models | Visible primary agents, selectable model catalog entries, and supported reasoning-effort values are session-local ACP configuration options. | Full | Available reasoning values follow the selected model's catalog capability. |
| Tools | Pending and completed/failed tool calls include ACP kinds, titles, raw input/output, full text plus bounded previews, normalized locations, native editor images/audio/resources, annotations, metadata, and full-file diffs. The model receives the structured textual/raw representation and supported image results. Unknown future MCP blocks are preserved in raw output and rendered as readable JSON text instead of being dropped. | Full | A future content type can only be native when ACP defines a matching content block; the lossless text/raw fallback remains available otherwise. |
| Permissions | Permission requests carry the originating tool-call ID, raw input, normalized locations, and preflight full-file diffs for `edit`, `write`, `write_files`, and multi-file `apply_patch`, with allow once, always allow, and reject choices. Patch previews use the same hunk matching without mutating disk. | Full | If an invalid patch cannot be simulated, the request still shows its raw patch and target locations and remains blocked until the user decides. |
| Cancellation | `session/cancel` interrupts model turns, questions, compaction, and terminal creation/execution while keeping the session reusable. Crabcode maps completion, output limit, configured turn limit, refusal/content filtering, and cancellation to ACP `end_turn`, `max_tokens`, `max_turn_requests`, `refusal`, and `cancelled`. | Full | Provider failures that are not normal stop conditions remain JSON-RPC/tool errors, as required by ACP's stop-reason model. |
| Commands and skills | Session updates publish global/workspace skills, project custom commands, and `/skills`, `/mcp`, and `/compact`. Custom command agent/model overrides apply to that turn. `/skills` and `/mcp` return local results without spending or persisting a model turn; `/mcp` reports live connection/auth/failure status. `/compact` rewrites persisted context. Unknown slash commands return an explicit error. | Full | Editor-native session/model/mode operations replace TUI-only navigation dialogs and pickers rather than duplicating their terminal UI commands. |
| MCP | Project MCP and client-supplied stdio, HTTP, and SSE servers merge into the session. Static headers, structured results, live status, resources, annotations, images, audio, and metadata are preserved. Project-configured remote MCP continues to use Crabcode's OAuth credential flow. | Full | ACP currently advertises only the HTTP/SSE transport flags; its client-server schema has no stdio flag or remote OAuth fields. Client-supplied remote auth can still be provided through headers. |
| Terminals | `terminal_session` and terminal-mode `bash` use the editor terminal host through create, embed, wait, output, kill, and release. Output is bounded for the model, and cancellation also covers terminal creation. | Full | The editor must advertise terminal hosting. User input and resize happen directly in the embedded editor terminal because ACP has no agent-issued stdin/resize requests. |
| Questions | Agent questions use capability-gated ACP form elicitation with validated non-empty prompts/options, unique labels, ordered single/multi-select answers, custom text, cardinality checks, deduplication, length bounds, cancellation, and safe skip behavior. | Full | Form elicitation is an unstable ACP capability and is only sent to editors that advertise it. |
| Usage | Provider input/output/cache-read/cache-write usage is aggregated across multi-step turns and persisted. ACP always receives context occupancy and cumulative USD cost updates; detailed token/cache values and whether the context size is known are included in `crabcode` `_meta`. Catalog pricing is cache-aware. | Full | Providers that omit usage or models without pricing cannot supply authoritative token or cost data; Crabcode still emits estimated context occupancy and marks unknown context size in metadata. |

## Runtime requirements

- Image and audio prompts require a selected model route with the corresponding input modality.
- ACP terminal embedding and question forms require the editor to advertise those client capabilities during initialization.
- Client-supplied remote MCP OAuth parameters are not part of the current ACP server descriptor. Use static headers from the editor, or configure the MCP server in Crabcode to use Crabcode's OAuth flow.
- Fields under `_meta.crabcode` are backwards-compatible Crabcode extensions for session hierarchy and detailed usage accounting.

## Session behavior

`session/close` detaches the editor and cancels any active turn. It does not delete Crabcode session history. Load replays the stored transcript; resume restores the session configuration without replaying prior content. Fork creates a new persisted session with a copied transcript.
`session/close` detaches the editor and cancels any active turn. It does not delete Crabcode session history. `session/delete` removes persisted history and managed attachments. List cursors page through the complete non-archived result set. Load replays the stored transcript; resume restores the session configuration without replaying prior content. Fork creates a new persisted session with a copied transcript and independently managed attachments. Child sessions are listed as normal entries with hierarchy metadata under `_meta.crabcode`.

## Safety notes

Crabcode applies the same configured permission rules in ACP as it does in the TUI. When a tool needs approval, the editor receives an ACP permission request. If the editor cannot respond or disconnects, Crabcode denies the request rather than continuing unattended.

Question forms are only sent to editors that advertise ACP form elicitation support. Declining, cancelling, disconnecting, or using an editor without that capability returns empty answers to the agent so the session can continue without waiting indefinitely.

Client-supplied MCP servers run with the same trust as project-configured MCP: stdio servers can execute local processes, and remote servers can send the headers and credentials the editor provides. Only attach MCP servers you trust for that workspace.

Image attachments are decoded under a size limit and written to temporary files under the system temp directory (`…/crabcode/acp-images/`) for the model turn. Prefer cleaning those files after long ACP sessions until automatic cleanup lands.
Image and audio attachments are decoded under a 20 MiB-per-file limit and written to private session-managed storage under Crabcode's state directory (`…/crabcode/attachments/<session-id>/`). Audio input accepts WAV and MP3 only. Closing an editor session keeps those files because history remains loadable; deleting the persisted session removes its managed attachment directory. Forks receive independent copies so deleting either session does not break the other. Readable external or temporary attachment paths from older sessions are copied into managed storage the next time Crabcode loads the session.

The capability matrix matches what the ACP server implements today. Crabcode only advertises protocol capability flags it handles (`loadSession`, image and embedded-context prompts, HTTP/SSE MCP, and list/resume/fork/close session ops).
The capability matrix matches what the ACP server implements today. Crabcode only advertises protocol capability flags it handles (`loadSession`, image, audio, and embedded-context prompts, HTTP/SSE MCP, and list/resume/fork/close/delete session ops). Client-side form elicitation and terminal hosting are capability-gated during initialization before Crabcode sends those requests. Stdio MCP is accepted even though the current ACP capability object has no separate stdio flag.
84 changes: 66 additions & 18 deletions src/acp/server.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use agent_client_protocol::schema::v1::{
AgentCapabilities, CancelNotification, CloseSessionRequest, CloseSessionResponse,
ForkSessionRequest, ForkSessionResponse, Implementation, InitializeRequest, InitializeResponse,
ListSessionsRequest, LoadSessionRequest, McpCapabilities, NewSessionRequest,
PromptCapabilities, PromptRequest, ResumeSessionRequest, SessionCapabilities,
SessionCloseCapabilities, SessionForkCapabilities, SessionListCapabilities,
SessionNotification, SessionResumeCapabilities, SessionUpdate, SetSessionConfigOptionRequest,
SetSessionModeRequest, SetSessionModeResponse,
DeleteSessionRequest, DeleteSessionResponse, ForkSessionRequest, ForkSessionResponse,
Implementation, InitializeRequest, InitializeResponse, ListSessionsRequest, LoadSessionRequest,
McpCapabilities, NewSessionRequest, PromptCapabilities, PromptRequest, ResumeSessionRequest,
SessionCapabilities, SessionCloseCapabilities, SessionDeleteCapabilities,
SessionForkCapabilities, SessionListCapabilities, SessionNotification,
SessionResumeCapabilities, SessionUpdate, SetSessionConfigOptionRequest, SetSessionModeRequest,
SetSessionModeResponse,
};
use agent_client_protocol::{Agent, Stdio};
use anyhow::{Context, Result};
Expand All @@ -30,12 +31,14 @@ pub async fn run(cwd: Option<PathBuf>) -> Result<()> {
})?;
let service = crate::acp::service::AcpService::new(&workspace)
.map_err(|_| anyhow::anyhow!("failed to initialize ACP session storage"))?;
let initialize_service = service.clone();

Agent
.builder()
.name("crabcode-acp")
.on_receive_request(
async move |request: InitializeRequest, responder, _connection| {
initialize_service.set_client_capabilities(request.client_capabilities.clone());
let response = InitializeResponse::new(request.protocol_version)
.agent_capabilities(capabilities())
.agent_info(Implementation::new("crabcode", env!("CARGO_PKG_VERSION")));
Expand All @@ -46,16 +49,39 @@ pub async fn run(cwd: Option<PathBuf>) -> Result<()> {
.on_receive_request(
{
let service = service.clone();
async move |request: ForkSessionRequest, responder, _connection| {
let result = service
.fork_session(request.session_id.to_string(), request.cwd)
.await
.map(|response| {
ForkSessionResponse::new(response.session_id)
async move |request: DeleteSessionRequest, responder, _connection| {
responder.respond_with_result(
service
.delete_session(&request.session_id.to_string())
.await
.map(|_| DeleteSessionResponse::new()),
)
}
},
agent_client_protocol::on_receive_request!(),
)
.on_receive_request(
{
let service = service.clone();
async move |request: ForkSessionRequest, responder, connection| {
let service = service.clone();
let task_connection = connection.clone();
connection.spawn(async move {
let response = service
.fork_session(request.session_id.to_string(), request.cwd)
.await?;
let session_id = response.session_id.clone();
let commands = service.available_commands(&session_id.to_string()).await?;
responder.respond(
ForkSessionResponse::new(session_id.clone())
.modes(response.modes)
.config_options(response.config_options)
});
responder.respond_with_result(result)
.config_options(response.config_options),
)?;
task_connection.send_notification(SessionNotification::new(
session_id,
SessionUpdate::AvailableCommandsUpdate(commands),
))
})
}
},
agent_client_protocol::on_receive_request!(),
Expand Down Expand Up @@ -188,7 +214,9 @@ pub async fn run(cwd: Option<PathBuf>) -> Result<()> {
{
let service = service.clone();
async move |request: ListSessionsRequest, responder, _connection| {
responder.respond_with_result(service.list_sessions(request.cwd).await)
responder.respond_with_result(
service.list_sessions(request.cwd, request.cursor).await,
)
}
},
agent_client_protocol::on_receive_request!(),
Expand Down Expand Up @@ -240,13 +268,33 @@ pub async fn run(cwd: Option<PathBuf>) -> Result<()> {
fn capabilities() -> AgentCapabilities {
AgentCapabilities::new()
.load_session(true)
.prompt_capabilities(PromptCapabilities::new().embedded_context(true).image(true))
.prompt_capabilities(
PromptCapabilities::new()
.embedded_context(true)
.image(true)
.audio(true),
)
.mcp_capabilities(McpCapabilities::new().http(true).sse(true))
.session_capabilities(
SessionCapabilities::new()
.list(SessionListCapabilities::new())
.resume(SessionResumeCapabilities::new())
.fork(SessionForkCapabilities::new())
.close(SessionCloseCapabilities::new()),
.close(SessionCloseCapabilities::new())
.delete(SessionDeleteCapabilities::new()),
)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn advertises_audio_prompt_support() {
let capabilities = capabilities();
let prompt = capabilities.prompt_capabilities;
assert!(prompt.audio);
assert!(prompt.image);
assert!(capabilities.session_capabilities.delete.is_some());
}
}
Loading
Loading