1use std::io::{BufRead, IsTerminal, Write};
19use std::sync::{Arc, Mutex};
20
21use rpi_agent::events::AgentEvent;
22use rpi_ai::types::{AssistantMessage, Content, ImageContent, StopReason};
23use rpi_harness::agent_harness::{AgentHarness, AgentLane, HarnessRunOutcome};
24use rpi_harness::events::{HarnessEvent, RunEndOutcome};
25
26use crate::args::Args;
27
28pub fn assistant_text(msg: &AssistantMessage) -> String {
31 msg.content
32 .iter()
33 .filter_map(|c| match c {
34 Content::Text(t) => Some(t.text.clone()),
35 _ => None,
36 })
37 .collect()
38}
39
40pub fn outcome_exit_code(outcome: &HarnessRunOutcome) -> i32 {
43 match outcome {
44 HarnessRunOutcome::Failed { .. } | HarnessRunOutcome::Aborted { .. } => 1,
45 _ => 0,
46 }
47}
48
49pub async fn print(
53 harness: &AgentHarness,
54 _args: &Args,
55 initial: Option<String>,
56 extra_messages: &[String],
57 initial_images: Vec<ImageContent>,
58) -> i32 {
59 let lane: Arc<dyn AgentLane> = harness.lane("main");
60
61 let mut last_exit = 0;
62 let mut last_msg: Option<AssistantMessage> = None;
63
64 let mut prompts: Vec<String> = Vec::new();
67 if let Some(init) = initial {
68 prompts.push(init);
69 }
70 for m in extra_messages {
71 prompts.push(m.clone());
72 }
73
74 if prompts.is_empty() {
75 return 0;
77 }
78
79 let mut images = initial_images;
80 for prompt in prompts {
81 match lane.prompt_text(&prompt, std::mem::take(&mut images)).await {
82 Ok(result) => {
83 last_exit = outcome_exit_code(&result.outcome);
84 match &result.outcome {
85 HarnessRunOutcome::Completed { final_message, .. }
86 | HarnessRunOutcome::Aborted { final_message, .. } => {
87 last_msg = Some(final_message.clone());
88 }
89 HarnessRunOutcome::Failed {
90 error,
91 final_message,
92 ..
93 } => {
94 if let Some(m) = final_message {
95 if m.stop_reason == StopReason::Error {
96 if let Some(em) = &m.error_message {
97 eprintln!("{em}");
98 }
99 }
100 }
101 eprintln!("run failed: {error:?}");
102 }
103 HarnessRunOutcome::Suspended { .. } => {
104 eprintln!("run suspended (deferred) — resume is not supported in v1");
105 last_exit = 1;
106 }
107 }
108 }
109 Err(e) => {
110 eprintln!("prompt rejected: {e}");
111 return 1;
112 }
113 }
114 }
115
116 if let Some(m) = &last_msg {
118 match m.stop_reason {
119 StopReason::Error => {
120 if let Some(em) = &m.error_message {
121 eprintln!("{em}");
122 }
123 last_exit = 1;
124 }
125 StopReason::Aborted => {
126 eprintln!("request aborted");
127 last_exit = 1;
128 }
129 _ => {
130 let text = assistant_text(m);
131 let mut out = std::io::stdout();
132 let _ = out.write_all(text.as_bytes());
133 if !text.ends_with('\n') {
134 let _ = out.write_all(b"\n");
135 }
136 let _ = out.flush();
137 }
138 }
139 }
140
141 last_exit
142}
143
144pub async fn json(
148 harness: &AgentHarness,
149 _args: &Args,
150 initial: Option<String>,
151 extra_messages: &[String],
152 initial_images: Vec<ImageContent>,
153 mut agent_events: Option<tokio::sync::broadcast::Receiver<AgentEvent>>,
154) -> i32 {
155 let lane: Arc<dyn AgentLane> = harness.lane("main");
156 let collected: Arc<Mutex<Vec<HarnessEvent>>> = Arc::new(Mutex::new(Vec::new()));
157 let collected_for_watch = collected.clone();
158
159 let mut watch = harness.events().watch(|| ());
162 watch.start(Arc::new(move |event: &HarnessEvent| {
163 emit_json_event(event);
165 collected_for_watch.lock().unwrap().push(event.clone());
166 }));
167 std::mem::forget(watch);
170
171 let (agent_done_tx, mut agent_done_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
174 let agent_event_task = agent_events.take().map(|mut rx| {
175 tokio::spawn(async move {
176 let done_tx = agent_done_tx;
177 loop {
178 match rx.recv().await {
179 Ok(event) => {
180 let terminal = event.is_terminal();
181 emit_agent_event(&event);
182 if terminal {
183 let _ = done_tx.send(());
184 }
185 }
186 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
187 Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
188 }
189 }
190 })
191 });
192
193 let mut prompts: Vec<String> = Vec::new();
194 if let Some(init) = initial {
195 prompts.push(init);
196 }
197 for m in extra_messages {
198 prompts.push(m.clone());
199 }
200
201 let mut last_exit = 0;
202 let mut final_outcome: Option<HarnessRunOutcome> = None;
203
204 let mut images = initial_images;
205 for prompt in prompts {
206 match lane.prompt_text(&prompt, std::mem::take(&mut images)).await {
207 Ok(result) => {
208 let _ = tokio::time::timeout(
214 std::time::Duration::from_millis(250),
215 agent_done_rx.recv(),
216 )
217 .await;
218 last_exit = outcome_exit_code(&result.outcome);
219 final_outcome = Some(result.outcome);
220 }
221 Err(e) => {
222 let line = serde_json::json!({
224 "type": "error",
225 "error": e.to_string(),
226 });
227 println!("{line}");
228 if let Some(task) = agent_event_task {
229 task.abort();
230 }
231 return 1;
232 }
233 }
234 }
235
236 let (outcome_str, final_text) = match final_outcome {
238 Some(HarnessRunOutcome::Completed { final_message, .. }) => {
239 ("completed", Some(assistant_text(&final_message)))
240 }
241 Some(HarnessRunOutcome::Aborted { final_message, .. }) => {
242 ("aborted", Some(assistant_text(&final_message)))
243 }
244 Some(HarnessRunOutcome::Failed { final_message, .. }) => {
245 let t = final_message.as_ref().map(assistant_text);
246 ("failed", t)
247 }
248 Some(HarnessRunOutcome::Suspended { .. }) => ("suspended", None),
249 None => ("idle", None),
250 };
251 let result_line = serde_json::json!({
252 "type": "result",
253 "outcome": outcome_str,
254 "finalText": final_text,
255 });
256 println!("{result_line}");
257 if let Some(task) = agent_event_task {
258 task.abort();
259 }
260 last_exit
261}
262
263fn emit_agent_event(event: &AgentEvent) {
264 println!("{}", agent_event_json(event));
265}
266
267fn agent_event_json(event: &AgentEvent) -> serde_json::Value {
271 use rpi_ai::types::AssistantMessageEvent;
272 match event {
273 AgentEvent::AgentStart => serde_json::json!({"type":"agent_start"}),
274 AgentEvent::AgentEnd { messages } => serde_json::json!({
275 "type":"agent_end", "messageCount": messages.len(),
276 "messages": serde_json::to_value(messages).unwrap_or(serde_json::Value::Null)
277 }),
278 AgentEvent::RetryScheduled {
279 attempt,
280 max_retries,
281 delay_ms,
282 error,
283 } => serde_json::json!({
284 "type":"retry_scheduled", "attempt":attempt,
285 "maxRetries":max_retries, "delayMs":delay_ms, "error":error
286 }),
287 AgentEvent::TurnStart => serde_json::json!({"type":"turn_start"}),
288 AgentEvent::TurnEnd {
289 message,
290 tool_results,
291 } => serde_json::json!({
292 "type":"turn_end", "message": serde_json::to_value(message).ok(),
293 "toolResultCount": tool_results.len(),
294 "toolResults": serde_json::to_value(tool_results).unwrap_or(serde_json::Value::Null)
295 }),
296 AgentEvent::MessageStart { message } => serde_json::json!({
297 "type":"message_start", "message": serde_json::to_value(message).ok()
298 }),
299 AgentEvent::MessageEnd { message } => serde_json::json!({
300 "type":"message_end", "message": serde_json::to_value(message).ok()
301 }),
302 AgentEvent::MessageUpdate {
303 message,
304 assistant_message_event,
305 } => {
306 let mut value = serde_json::json!({
307 "type":"message_update",
308 "message": serde_json::to_value(message).unwrap_or(serde_json::Value::Null),
309 "assistantMessageEvent": serde_json::to_value(assistant_message_event)
310 .unwrap_or(serde_json::Value::Null),
311 "eventType": assistant_message_event.type_tag(),
312 });
313 let object = value.as_object_mut().expect("json object");
314 match assistant_message_event {
315 AssistantMessageEvent::TextDelta {
316 content_index,
317 delta,
318 ..
319 }
320 | AssistantMessageEvent::ThinkingDelta {
321 content_index,
322 delta,
323 ..
324 }
325 | AssistantMessageEvent::ToolCallDelta {
326 content_index,
327 delta,
328 ..
329 } => {
330 object.insert("contentIndex".into(), (*content_index).into());
331 object.insert("delta".into(), delta.clone().into());
332 }
333 _ => {}
334 }
335 value
336 }
337 AgentEvent::ToolExecutionStart {
338 tool_call_id,
339 tool_name,
340 args,
341 } => serde_json::json!({
342 "type":"tool_execution_start", "toolCallId":tool_call_id,
343 "toolName":tool_name, "args":args
344 }),
345 AgentEvent::ToolExecutionUpdate {
346 tool_call_id,
347 tool_name,
348 args,
349 partial_result,
350 } => serde_json::json!({
351 "type":"tool_execution_update", "toolCallId":tool_call_id, "toolName":tool_name,
352 "args": args,
353 "partialResult": tool_result_json(partial_result)
354 }),
355 AgentEvent::ToolExecutionEnd {
356 tool_call_id,
357 tool_name,
358 result,
359 is_error,
360 } => serde_json::json!({
361 "type":"tool_execution_end", "toolCallId":tool_call_id,
362 "toolName":tool_name, "isError":is_error,
363 "result": tool_result_json(result)
364 }),
365 }
366}
367
368fn tool_result_json(result: &rpi_agent::types::AgentToolResult) -> serde_json::Value {
369 let content: Vec<serde_json::Value> = result
370 .content
371 .iter()
372 .map(|item| match item {
373 rpi_agent::types::TextContentOrImage::Text(text) => serde_json::json!({
374 "type": "text",
375 "text": text.text,
376 }),
377 rpi_agent::types::TextContentOrImage::Image(image) => {
378 serde_json::to_value(image).unwrap_or(serde_json::Value::Null)
379 }
380 })
381 .collect();
382 serde_json::json!({
383 "content": content,
384 "details": result.details,
385 "usage": result.usage.as_ref().and_then(|usage| serde_json::to_value(usage).ok()),
386 "addedToolNames": result.added_tool_names,
387 "terminate": result.terminate,
388 })
389}
390
391fn emit_json_event(event: &HarnessEvent) {
395 let line = match event {
396 HarnessEvent::RunStart(e) => serde_json::json!({
397 "type": "run_start",
398 "lane": e.lane,
399 "runId": e.run_id,
400 }),
401 HarnessEvent::RunEnd(e) => serde_json::json!({
402 "type": "run_end",
403 "lane": e.lane,
404 "runId": e.run_id,
405 "outcome": run_end_outcome_str(e.outcome),
406 "leafId": e.leaf_id,
407 }),
408 };
409 println!("{line}");
410}
411
412fn run_end_outcome_str(o: RunEndOutcome) -> &'static str {
413 match o {
414 RunEndOutcome::Completed => "completed",
415 RunEndOutcome::Aborted => "aborted",
416 RunEndOutcome::Failed => "failed",
417 }
418}
419
420pub async fn interactive(
429 harness: &AgentHarness,
430 event_rx: Option<tokio::sync::broadcast::Receiver<rpi_agent::AgentEvent>>,
431 args: &Args,
432 model_catalog: Vec<rpi_ai::Model>,
433 initial: Option<String>,
434 extra_messages: &[String],
435 initial_images: Vec<ImageContent>,
436 theme: Option<&str>,
437 no_themes: bool,
438 reload_context: &crate::session::ReloadContext,
439) -> i32 {
440 let force_tui = std::env::var("RPI_FORCE_TUI")
442 .map(|v| v == "1")
443 .unwrap_or(false);
444 if force_tui || crate::interactive_tui::is_tui_supported() {
445 crate::interactive_tui::interactive_tui(
447 harness,
448 event_rx,
449 args,
450 model_catalog,
451 initial,
452 extra_messages,
453 initial_images,
454 theme,
455 no_themes,
456 reload_context,
457 )
458 .await
459 } else {
460 interactive_repl(harness, args, initial, extra_messages, initial_images).await
462 }
463}
464
465pub async fn interactive_repl(
467 harness: &AgentHarness,
468 #[allow(unused_variables)] args: &Args,
469 initial: Option<String>,
470 extra_messages: &[String],
471 initial_images: Vec<ImageContent>,
472) -> i32 {
473 let lane: Arc<dyn AgentLane> = harness.lane("main");
475 let stdin = std::io::stdin();
476 let is_tty = stdin.is_terminal();
477
478 if is_tty {
479 println!(
480 "rpi interactive (v1 minimal REPL). Type /exit to quit, /abort to cancel a run.\n"
481 );
482 }
483
484 let mut prompts: Vec<String> = Vec::new();
486 if let Some(init) = initial {
487 prompts.push(init);
488 }
489 for m in extra_messages {
490 prompts.push(m.clone());
491 }
492 let mut images = initial_images;
493 for prompt in prompts {
494 if let Err(code) = run_one(&lane, &prompt, std::mem::take(&mut images)).await {
495 return code;
496 }
497 }
498
499 let mut line = String::new();
501 loop {
502 if is_tty {
503 print!("> ");
504 let _ = std::io::stdout().flush();
505 }
506 line.clear();
507 match stdin.lock().read_line(&mut line) {
508 Ok(0) => break, Ok(_) => {}
510 Err(_) => break,
511 }
512 let trimmed = line.trim();
513 if trimmed.is_empty() {
514 continue;
515 }
516 if trimmed == "/exit" || trimmed == "/quit" {
517 break;
518 }
519 if trimmed == "/abort" {
520 let _ = lane.abort().await;
521 eprintln!("(aborted)");
522 continue;
523 }
524 if let Err(code) = run_one(&lane, trimmed, Vec::new()).await {
525 return code;
526 }
527 }
528 0
529}
530
531async fn run_one(
535 lane: &Arc<dyn AgentLane>,
536 prompt: &str,
537 images: Vec<ImageContent>,
538) -> Result<(), i32> {
539 match lane.prompt_text(prompt, images).await {
540 Ok(result) => {
541 match &result.outcome {
542 HarnessRunOutcome::Completed { final_message, .. }
543 | HarnessRunOutcome::Aborted { final_message, .. } => {
544 let text = assistant_text(final_message);
545 if !text.is_empty() {
546 println!("{text}");
547 }
548 }
549 HarnessRunOutcome::Failed {
550 error,
551 final_message,
552 ..
553 } => {
554 if let Some(m) = final_message {
555 if let Some(em) = &m.error_message {
556 eprintln!("error: {em}");
557 }
558 }
559 eprintln!("run failed: {error:?}");
560 }
561 HarnessRunOutcome::Suspended { .. } => {
562 eprintln!("run suspended (deferred) — resume not supported in v1");
563 }
564 }
565 Ok(())
566 }
567 Err(e) => {
568 eprintln!("prompt rejected: {e}");
569 Err(1)
570 }
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use rpi_agent::events::AgentEvent;
578 use rpi_agent::types::AgentToolResult;
579 use rpi_ai::types::{
580 AssistantMessage, Content, StopReason, TextContent, TextContentType, Usage,
581 };
582 use rpi_harness::session::types::OperationError;
583
584 fn assistant(text: &str, stop: StopReason) -> AssistantMessage {
585 AssistantMessage {
586 role: rpi_ai::types::AssistantRole,
587 content: vec![Content::Text(TextContent {
588 kind: TextContentType,
589 text: text.into(),
590 text_signature: None,
591 })],
592 api: rpi_ai::Api::AnthropicMessages,
593 provider: "anthropic".into(),
594 model: "claude-sonnet-5".into(),
595 response_model: None,
596 response_id: None,
597 usage: Usage::zero(),
598 stop_reason: stop,
599 deferred: None,
600 error_message: None,
601 raw_stop_reason: None,
602 end_turn: None,
603 timestamp: 0,
604 }
605 }
606
607 #[test]
608 fn assistant_text_concatenates_text_blocks() {
609 let m = assistant("hello", StopReason::Stop);
610 assert_eq!(assistant_text(&m), "hello");
611 }
612
613 #[test]
614 fn outcome_exit_code_maps_failed_aborted_to_1() {
615 let failed = HarnessRunOutcome::Failed {
616 leaf_id: "l".into(),
617 error: OperationError {
618 code: "boom".into(),
619 message: "boom".into(),
620 },
621 final_entry_id: None,
622 final_message: None,
623 };
624 assert_eq!(outcome_exit_code(&failed), 1);
625 let completed = HarnessRunOutcome::Completed {
626 leaf_id: "l".into(),
627 final_entry_id: "e".into(),
628 final_message: assistant("ok", StopReason::Stop),
629 };
630 assert_eq!(outcome_exit_code(&completed), 0);
631 }
632
633 #[test]
634 fn run_end_outcome_str_roundtrip() {
635 assert_eq!(run_end_outcome_str(RunEndOutcome::Completed), "completed");
636 assert_eq!(run_end_outcome_str(RunEndOutcome::Aborted), "aborted");
637 assert_eq!(run_end_outcome_str(RunEndOutcome::Failed), "failed");
638 }
639
640 #[test]
641 fn agent_event_projection_keeps_terminal_and_tool_payloads() {
642 let end = agent_event_json(&AgentEvent::AgentEnd { messages: vec![] });
643 assert_eq!(end["type"], "agent_end");
644 assert_eq!(end["messages"], serde_json::json!([]));
645
646 let tool = agent_event_json(&AgentEvent::ToolExecutionEnd {
647 tool_call_id: "call-1".into(),
648 tool_name: "read".into(),
649 result: AgentToolResult::text("hello"),
650 is_error: false,
651 });
652 assert_eq!(tool["type"], "tool_execution_end");
653 assert_eq!(tool["result"]["content"][0]["text"], "hello");
654 assert_eq!(tool["result"]["terminate"], false);
655
656 let retry = agent_event_json(&AgentEvent::RetryScheduled {
657 attempt: 3,
658 max_retries: 10,
659 delay_ms: 8_000,
660 error: "503 service unavailable".into(),
661 });
662 assert_eq!(retry["type"], "retry_scheduled");
663 assert_eq!(retry["attempt"], 3);
664 assert_eq!(retry["maxRetries"], 10);
665 assert_eq!(retry["delayMs"], 8_000);
666 assert_eq!(retry["error"], "503 service unavailable");
667 }
668}