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