horus_cli/frontend/
headless.rs1use horus::protocol::{EventMsg, Op, ReviewDecision, Submission};
2use horus::{Error, Result};
3use horus_gateway::client::{GatewayEvents, GatewaySender};
4use horus_gateway::wire::{ClientMessage, ServerMessage};
5use uuid::Uuid;
6
7pub async fn run(
8 sender: GatewaySender,
9 mut events: GatewayEvents,
10 session_id: String,
11 task: String,
12) -> Result<Option<String>> {
13 let submission_id = Uuid::new_v4().to_string();
14 sender
15 .send(ClientMessage::Submit {
16 session_id: session_id.clone(),
17 submission: Submission {
18 id: submission_id.clone(),
19 op: Op::UserInput {
20 text: task,
21 attachments: Vec::new(),
22 },
23 },
24 })
25 .await
26 .map_err(gateway_error)?;
27
28 let mut turn_id = None;
29 let mut first_error = None;
30 let mut approval_error = None;
31 let mut last_agent_message = None;
32 loop {
33 let frame =
34 events.next().await.map_err(gateway_error)?.ok_or_else(|| {
35 Error::Stopped("gateway disconnected before turn completion".into())
36 })?;
37 let event = match frame.message {
38 ServerMessage::AgentEvent {
39 session_id: actual,
40 record,
41 ..
42 } if actual == session_id => record.event,
43 ServerMessage::Rejected {
44 request_id,
45 message,
46 ..
47 } if request_id == submission_id => return Err(Error::Stopped(message)),
48 ServerMessage::Error { message, .. } => return Err(Error::Stopped(message)),
49 _ => continue,
50 };
51 match event.msg {
52 EventMsg::TurnStarted(turn)
53 if event.submission_id.as_deref() == Some(&submission_id) =>
54 {
55 turn_id = Some(turn.turn_id);
56 }
57 EventMsg::ExecApprovalRequest(request)
58 if turn_id.as_deref() == Some(request.turn_id.as_str()) =>
59 {
60 approval_error = Some(Error::Config(
61 "headless run requested tool approval; configure a no-prompt gateway sandbox policy"
62 .into(),
63 ));
64 sender
65 .send(ClientMessage::Submit {
66 session_id: session_id.clone(),
67 submission: Submission {
68 id: Uuid::new_v4().to_string(),
69 op: Op::ExecApproval {
70 id: request.id,
71 decision: ReviewDecision::Abort,
72 },
73 },
74 })
75 .await
76 .map_err(gateway_error)?;
77 }
78 EventMsg::Error(error)
79 if event.submission_id.as_deref() == Some(submission_id.as_str()) =>
80 {
81 first_error.get_or_insert(error.message);
82 }
83 EventMsg::AgentMessage(message)
84 if turn_id.as_deref() == Some(message.turn_id.as_str()) =>
85 {
86 last_agent_message = Some(message.message);
87 }
88 EventMsg::TurnComplete(turn) if turn_id.as_deref() == Some(turn.turn_id.as_str()) => {
89 if let Some(error) = approval_error {
90 return Err(error);
91 }
92 if let Some(error) = first_error {
93 return Err(Error::Stopped(error));
94 }
95 return Ok(last_agent_message);
96 }
97 EventMsg::TurnAborted(turn) if turn_id.as_deref() == Some(turn.turn_id.as_str()) => {
98 return Err(approval_error
99 .unwrap_or_else(|| Error::Stopped(first_error.unwrap_or(turn.reason))));
100 }
101 _ => {}
102 }
103 }
104}
105
106fn gateway_error(error: horus_gateway::Error) -> Error {
107 Error::Stopped(error.to_string())
108}