1use std::{
2 ffi::OsString,
3 path::{Path, PathBuf},
4};
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::AiderCliError;
10
11#[derive(Debug, Clone, Eq, PartialEq)]
12pub struct AiderStreamJsonRunRequest {
13 prompt: String,
14 model: Option<String>,
15 working_dir: Option<PathBuf>,
16}
17
18impl AiderStreamJsonRunRequest {
19 pub fn new(prompt: impl Into<String>) -> Self {
20 Self {
21 prompt: prompt.into(),
22 model: None,
23 working_dir: None,
24 }
25 }
26
27 pub fn model(mut self, model: impl Into<String>) -> Self {
28 self.model = Some(model.into());
29 self
30 }
31
32 pub fn working_dir(mut self, path: impl Into<PathBuf>) -> Self {
33 self.working_dir = Some(path.into());
34 self
35 }
36
37 pub fn prompt(&self) -> &str {
38 &self.prompt
39 }
40
41 pub fn model_name(&self) -> Option<&str> {
42 self.model.as_deref()
43 }
44
45 pub fn working_directory(&self) -> Option<&Path> {
46 self.working_dir.as_deref()
47 }
48
49 pub(crate) fn argv(&self) -> Result<Vec<OsString>, AiderCliError> {
50 if self.prompt.trim().is_empty() {
51 return Err(AiderCliError::InvalidRequest(
52 "prompt must not be empty".to_string(),
53 ));
54 }
55
56 let mut argv = vec![
57 OsString::from("--prompt"),
58 OsString::from(self.prompt.as_str()),
59 OsString::from("--message-format"),
60 OsString::from("stream-json"),
61 ];
62
63 if let Some(model) = normalize_non_empty(self.model.as_deref()) {
64 argv.push(OsString::from("--model"));
65 argv.push(OsString::from(model));
66 }
67
68 Ok(argv)
69 }
70}
71
72fn normalize_non_empty(value: Option<&str>) -> Option<String> {
73 value
74 .map(str::trim)
75 .filter(|value| !value.is_empty())
76 .map(ToOwned::to_owned)
77}
78
79#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
80pub struct AiderToolResultError {
81 #[serde(default)]
82 pub r#type: Option<String>,
83 #[serde(default)]
84 pub message: Option<String>,
85}
86
87#[derive(Debug, Clone, Eq, PartialEq)]
88pub struct AiderStreamJsonResultPayload {
89 pub status: String,
90 pub error_type: Option<String>,
91 pub error_message: Option<String>,
92 pub stats: Option<Value>,
93 pub raw: Value,
94}
95
96#[derive(Debug, Clone, Eq, PartialEq)]
97pub enum AiderStreamJsonEvent {
98 Init {
99 session_id: String,
100 model: String,
101 raw: Value,
102 },
103 Message {
104 role: String,
105 content: String,
106 delta: bool,
107 raw: Value,
108 },
109 ToolUse {
110 tool_name: String,
111 tool_id: String,
112 parameters: Value,
113 raw: Value,
114 },
115 ToolResult {
116 tool_id: String,
117 status: String,
118 output: Option<String>,
119 error: Option<AiderToolResultError>,
120 raw: Value,
121 },
122 Error {
123 severity: String,
124 message: String,
125 raw: Value,
126 },
127 Result {
128 payload: AiderStreamJsonResultPayload,
129 },
130 Unknown {
131 event_type: String,
132 raw: Value,
133 },
134}
135
136impl AiderStreamJsonEvent {
137 pub fn raw(&self) -> &Value {
138 match self {
139 AiderStreamJsonEvent::Init { raw, .. } => raw,
140 AiderStreamJsonEvent::Message { raw, .. } => raw,
141 AiderStreamJsonEvent::ToolUse { raw, .. } => raw,
142 AiderStreamJsonEvent::ToolResult { raw, .. } => raw,
143 AiderStreamJsonEvent::Error { raw, .. } => raw,
144 AiderStreamJsonEvent::Result { payload } => &payload.raw,
145 AiderStreamJsonEvent::Unknown { raw, .. } => raw,
146 }
147 }
148
149 pub fn event_type(&self) -> &str {
150 match self {
151 AiderStreamJsonEvent::Init { .. } => "init",
152 AiderStreamJsonEvent::Message { .. } => "message",
153 AiderStreamJsonEvent::ToolUse { .. } => "tool_use",
154 AiderStreamJsonEvent::ToolResult { .. } => "tool_result",
155 AiderStreamJsonEvent::Error { .. } => "error",
156 AiderStreamJsonEvent::Result { .. } => "result",
157 AiderStreamJsonEvent::Unknown { event_type, .. } => event_type.as_str(),
158 }
159 }
160}
161
162#[derive(Debug, Clone, Copy, Eq, PartialEq)]
163pub enum AiderStreamJsonErrorCode {
164 JsonParse,
165 TypedParse,
166}
167
168#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
169#[error("{message}")]
170pub struct AiderStreamJsonError {
171 pub code: AiderStreamJsonErrorCode,
172 pub message: String,
173 pub details: String,
174}
175
176impl AiderStreamJsonError {
177 fn new(code: AiderStreamJsonErrorCode, message: String) -> Self {
178 Self {
179 code,
180 details: message.clone(),
181 message,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Default)]
187pub struct AiderStreamJsonParser;
188
189impl AiderStreamJsonParser {
190 pub fn new() -> Self {
191 Self
192 }
193
194 pub fn parse_line(
195 &mut self,
196 line: &str,
197 ) -> Result<Option<AiderStreamJsonEvent>, AiderStreamJsonError> {
198 let line = line.strip_suffix('\r').unwrap_or(line);
199 if line.chars().all(|ch| ch.is_whitespace()) {
200 return Ok(None);
201 }
202
203 let value: Value = serde_json::from_str(line).map_err(|err| {
204 AiderStreamJsonError::new(
205 AiderStreamJsonErrorCode::JsonParse,
206 format!("invalid JSON: {err}"),
207 )
208 })?;
209
210 self.parse_json(&value)
211 }
212
213 pub fn parse_json(
214 &mut self,
215 value: &Value,
216 ) -> Result<Option<AiderStreamJsonEvent>, AiderStreamJsonError> {
217 let obj = value.as_object().ok_or_else(|| {
218 AiderStreamJsonError::new(
219 AiderStreamJsonErrorCode::TypedParse,
220 "expected JSON object".to_string(),
221 )
222 })?;
223
224 let event_type = get_required_str(obj, "type").map_err(|message| {
225 AiderStreamJsonError::new(AiderStreamJsonErrorCode::TypedParse, message)
226 })?;
227
228 let event = match event_type.as_str() {
229 "init" => AiderStreamJsonEvent::Init {
230 session_id: get_required_str(obj, "session_id").map_err(typed_parse_error)?,
231 model: get_required_str(obj, "model").map_err(typed_parse_error)?,
232 raw: value.clone(),
233 },
234 "message" => AiderStreamJsonEvent::Message {
235 role: get_required_str(obj, "role").map_err(typed_parse_error)?,
236 content: get_required_str(obj, "content").map_err(typed_parse_error)?,
237 delta: obj.get("delta").and_then(Value::as_bool).unwrap_or(false),
238 raw: value.clone(),
239 },
240 "tool_use" => AiderStreamJsonEvent::ToolUse {
241 tool_name: get_required_str(obj, "tool_name").map_err(typed_parse_error)?,
242 tool_id: get_required_str(obj, "tool_id").map_err(typed_parse_error)?,
243 parameters: obj
244 .get("parameters")
245 .cloned()
246 .ok_or_else(|| typed_parse_error("expected field `parameters`".to_string()))?,
247 raw: value.clone(),
248 },
249 "tool_result" => AiderStreamJsonEvent::ToolResult {
250 tool_id: get_required_str(obj, "tool_id").map_err(typed_parse_error)?,
251 status: get_required_str(obj, "status").map_err(typed_parse_error)?,
252 output: obj
253 .get("output")
254 .and_then(Value::as_str)
255 .map(ToOwned::to_owned),
256 error: obj
257 .get("error")
258 .cloned()
259 .map(serde_json::from_value)
260 .transpose()
261 .map_err(|_| typed_parse_error("expected object field `error`".to_string()))?,
262 raw: value.clone(),
263 },
264 "error" => AiderStreamJsonEvent::Error {
265 severity: get_required_str(obj, "severity").map_err(typed_parse_error)?,
266 message: get_required_str(obj, "message").map_err(typed_parse_error)?,
267 raw: value.clone(),
268 },
269 "result" => AiderStreamJsonEvent::Result {
270 payload: AiderStreamJsonResultPayload {
271 status: get_required_str(obj, "status").map_err(typed_parse_error)?,
272 error_type: obj
273 .get("error")
274 .and_then(Value::as_object)
275 .and_then(|error| error.get("type"))
276 .and_then(Value::as_str)
277 .map(ToOwned::to_owned),
278 error_message: obj
279 .get("error")
280 .and_then(Value::as_object)
281 .and_then(|error| error.get("message"))
282 .and_then(Value::as_str)
283 .map(ToOwned::to_owned),
284 stats: obj.get("stats").cloned(),
285 raw: value.clone(),
286 },
287 },
288 _ => AiderStreamJsonEvent::Unknown {
289 event_type,
290 raw: value.clone(),
291 },
292 };
293
294 Ok(Some(event))
295 }
296}
297
298fn typed_parse_error(message: String) -> AiderStreamJsonError {
299 AiderStreamJsonError::new(AiderStreamJsonErrorCode::TypedParse, message)
300}
301
302#[derive(Debug, Clone, Eq, PartialEq)]
303pub struct AiderStreamJsonLine {
304 pub line_number: usize,
305 pub raw: String,
306}
307
308#[derive(Debug, Clone, Eq, PartialEq)]
309pub enum AiderStreamJsonLineOutcome {
310 Ok {
311 line: AiderStreamJsonLine,
312 event: AiderStreamJsonEvent,
313 },
314 Err {
315 line: AiderStreamJsonLine,
316 error: AiderStreamJsonError,
317 },
318}
319
320pub fn parse_stream_json_lines(input: &str) -> Vec<AiderStreamJsonLineOutcome> {
321 let mut parser = AiderStreamJsonParser::new();
322 let mut outcomes = Vec::new();
323
324 for (index, raw) in input.lines().enumerate() {
325 let line = AiderStreamJsonLine {
326 line_number: index + 1,
327 raw: raw.to_string(),
328 };
329
330 match parser.parse_line(raw) {
331 Ok(Some(event)) => outcomes.push(AiderStreamJsonLineOutcome::Ok { line, event }),
332 Ok(None) => {}
333 Err(error) => outcomes.push(AiderStreamJsonLineOutcome::Err { line, error }),
334 }
335 }
336
337 outcomes
338}
339
340fn get_required_str(obj: &serde_json::Map<String, Value>, key: &str) -> Result<String, String> {
341 obj.get(key)
342 .and_then(Value::as_str)
343 .map(ToOwned::to_owned)
344 .ok_or_else(|| format!("expected string field `{key}`"))
345}