Driving an agent
Implement Client and hand it to spawn. Only session_update and
request_permission are required; fs/*, terminal/* and elicitation/*
decline until you implement them.
use cacp::{Client, Result, schema};
use std::sync::Arc;
use tokio::process::Command;
struct Ui;
impl Client for Ui {
async fn session_update(&self, notification: schema::SessionNotification) {
if let schema::SessionUpdate::AgentMessageChunk(chunk) = notification.update {
print!("{:?}", chunk.content);
}
}
async fn request_permission(
&self,
request: schema::RequestPermissionRequest,
) -> Result<schema::RequestPermissionResponse> {
Ok(schema::RequestPermissionResponse::selected(
request.options[0].option_id.clone(),
))
}
}
#[tokio::main]
async fn main() -> Result<()> {
let (agent, _child) = cacp::spawn(Command::new("my-agent").arg("--acp"), Arc::new(Ui), None)?;
agent
.initialize(schema::InitializeRequest::new(Default::default()))
.await?;
let session = agent
.new_session(schema::NewSessionRequest::new("/path/to/repo"))
.await?;
let done = agent
.prompt(schema::PromptRequest::new(
session.session_id,
vec!["explain this repo".into()],
))
.await?;
println!("{:?}", done.stop_reason);
Ok(())
}
That is crates/cacp/examples/client.rs, included verbatim — cargo test
compiles it, so this page cannot drift from working code.
spawn runs the agent as a subprocess and takes its stdin and stdout. stderr is
left as you configured it, since a TUI usually wants it captured and a CLI
usually does not. The agent is killed when the returned Child drops.
connect and connect_on are the same thing over a stream you already hold.
Cancelling
Two different cancellations, easy to confuse:
- Ending a turn is
session/cancel. The agent still answers the prompt, withStopReason::Cancelled— so keep awaiting the prompt future rather than dropping it. - Abandoning one request is what dropping its future does: the peer gets
$/cancel_requestand stops working on something nobody is waiting for.
Forking from saved history
AgentConn::fork_session_from_history(request, &history, before) opens a fresh
session using the entries before an exclusive index. Use 0 for an empty fork
or history.len() for all entries. The source session is never contacted.
Each HistoryEntry carries a user, agent, or tool role and typed content blocks.
The returned HistoryFork stays idle until prompt is called; that first prompt
includes the saved context. Later successful turns omit it. Persist the session
and pending_history() together and use HistoryFork::restore after reconnecting.
This uses ordinary session/new and session/prompt, so it requires no fork
extension. History is supplied as prompt context, not native agent state; files
are not rolled back. Content capabilities and context limits still apply.
On a transport error, reconcile the agent state before retrying: it may already
have received the context. Native fork_session remains available separately.