1use std::str::FromStr;
8use std::sync::Arc;
9
10use agent_client_protocol::schema::{
11 ContentBlock, InitializeRequest, ProtocolVersion, RequestPermissionOutcome,
12 RequestPermissionRequest, RequestPermissionResponse, SelectedPermissionOutcome,
13 SessionNotification, SessionUpdate,
14};
15use agent_client_protocol::{Agent, Client, ConnectionTo};
16use agent_client_protocol_tokio::AcpAgent;
17use tokio::sync::mpsc;
18use tracing::{info, warn};
19
20use crate::connection::AcpAgentConfig;
21use crate::error::{AcpError, Result};
22use crate::permissions::{
23 PermissionDecision, PermissionOption, PermissionPolicy, PermissionRequest,
24};
25use crate::status::{AgentStatus, StatusTracker};
26
27#[derive(Debug, Clone)]
29pub enum OutputChunk {
30 Text(String),
32 Thought(String),
34 ToolCall {
36 title: String,
38 },
39 ToolCallComplete {
41 title: String,
43 },
44 PermissionRequested {
46 title: String,
48 approved: bool,
50 },
51 Done,
53 Error(String),
55}
56
57pub type OutputStream = mpsc::Receiver<OutputChunk>;
77
78pub async fn stream_prompt(
83 config: &AcpAgentConfig,
84 prompt: &str,
85 policy: Arc<PermissionPolicy>,
86 status: StatusTracker,
87) -> Result<OutputStream> {
88 info!(command = %config.command, "starting streaming ACP prompt");
89
90 let command_with_env = crate::connection::build_command_with_env(&config.command, &config.env);
91
92 let agent = AcpAgent::from_str(&command_with_env).map_err(|e| {
93 AcpError::InvalidConfig(format!("invalid command '{}': {e}", config.command))
94 })?;
95
96 let (chunk_tx, chunk_rx) = mpsc::channel::<OutputChunk>(64);
97 let prompt_text = prompt.to_string();
98 let working_dir = config.working_dir.clone();
99
100 status.set(AgentStatus::Starting);
101
102 tokio::spawn(async move {
103 let chunk_tx_err = chunk_tx.clone();
104 let status_inner = status.clone();
105 let policy_clone = policy.clone();
106 let chunk_tx_perm = chunk_tx.clone();
107
108 let outcome = Client
109 .builder()
110 .on_receive_notification(
111 {
112 let tx = chunk_tx.clone();
113 async move |notif: SessionNotification, _cx: ConnectionTo<Agent>| {
114 match notif.update {
115 SessionUpdate::AgentMessageChunk(chunk) => {
116 if let ContentBlock::Text(text_content) = chunk.content {
117 let _ = tx
118 .send(OutputChunk::Text(text_content.text.to_string()))
119 .await;
120 }
121 }
122 SessionUpdate::AgentThoughtChunk(chunk) => {
123 if let ContentBlock::Text(text_content) = chunk.content {
124 let _ = tx
125 .send(OutputChunk::Thought(text_content.text.to_string()))
126 .await;
127 }
128 }
129 SessionUpdate::ToolCall(tool_call) => {
130 let _ = tx
131 .send(OutputChunk::ToolCall {
132 title: tool_call.title.to_string(),
133 })
134 .await;
135 }
136 _ => {}
137 }
138 Ok(())
139 }
140 },
141 agent_client_protocol::on_receive_notification!(),
142 )
143 .on_receive_request(
144 {
145 let status = status_inner.clone();
146 async move |request: RequestPermissionRequest,
147 responder,
148 _cx: ConnectionTo<Agent>| {
149 status.set(AgentStatus::WaitingPermission);
150
151 let title = request
152 .options
153 .first()
154 .map(|o| o.name.to_string())
155 .unwrap_or_else(|| "Unknown".to_string());
156
157 let perm_request = PermissionRequest {
158 title: title.clone(),
159 options: request
160 .options
161 .iter()
162 .map(|o| PermissionOption {
163 id: o.option_id.to_string(),
164 name: o.name.to_string(),
165 })
166 .collect(),
167 };
168
169 let decision = policy_clone.decide(&perm_request);
170 let approved = matches!(decision, PermissionDecision::Allow(_));
171
172 let _ = chunk_tx_perm
173 .send(OutputChunk::PermissionRequested {
174 title: title.clone(),
175 approved,
176 })
177 .await;
178
179 status.set(AgentStatus::Running);
180
181 match decision {
182 PermissionDecision::Allow(id) => responder.respond(
183 RequestPermissionResponse::new(RequestPermissionOutcome::Selected(
184 SelectedPermissionOutcome::new(id),
185 )),
186 ),
187 PermissionDecision::Deny => responder.respond(
188 RequestPermissionResponse::new(RequestPermissionOutcome::Cancelled),
189 ),
190 }
191 }
192 },
193 agent_client_protocol::on_receive_request!(),
194 )
195 .connect_with(agent, {
196 let status = status_inner.clone();
197 let tx = chunk_tx.clone();
198 |connection: ConnectionTo<Agent>| async move {
199 status.set(AgentStatus::Starting);
200
201 connection
202 .send_request(InitializeRequest::new(ProtocolVersion::V1))
203 .block_task()
204 .await?;
205
206 status.set(AgentStatus::Running);
207
208 connection
209 .build_session(&working_dir)
210 .block_task()
211 .run_until(async |mut session| {
212 session.send_prompt(&prompt_text)?;
213 let _ = session.read_to_string().await?;
215 let _ = tx.send(OutputChunk::Done).await;
216 Ok(())
217 })
218 .await?;
219
220 status.set(AgentStatus::Idle);
221 Ok(())
222 }
223 })
224 .await;
225
226 if let Err(e) = outcome {
227 warn!(error = %e, "streaming ACP session ended with error");
228 let _ = chunk_tx_err.send(OutputChunk::Error(e.to_string())).await;
229 }
230
231 status_inner.set(AgentStatus::Stopped);
232 });
233
234 Ok(chunk_rx)
235}