Skip to main content

horus_cli/frontend/
headless.rs

1use 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}