Skip to main content

mobius_cli/frontend/
headless.rs

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