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))?;
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.