1use async_trait::async_trait;
8use tokio::sync::{mpsc, oneshot};
9use tokio_util::sync::CancellationToken;
10
11use crate::proto::{
12 RunSpec, SessionMessageAdmissionConfirmation, SessionMessageDelivery, TerminalStatus,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum ExecutorControl {
18 SessionMessageAdmitted(SessionMessageAdmissionConfirmation),
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum HostRequestKind {
24 Approval,
26}
27
28pub struct HostRequest {
31 pub kind: HostRequestKind,
32 pub body: serde_json::Value,
33 pub reply: oneshot::Sender<serde_json::Value>,
34}
35
36#[derive(Clone)]
40pub struct HostBridge {
41 req_tx: mpsc::UnboundedSender<HostRequest>,
42}
43
44impl HostBridge {
45 pub fn channel() -> (Self, mpsc::UnboundedReceiver<HostRequest>) {
47 let (req_tx, req_rx) = mpsc::unbounded_channel();
48 (HostBridge { req_tx }, req_rx)
49 }
50
51 pub async fn approval_call(
55 &self,
56 body: serde_json::Value,
57 ) -> Result<serde_json::Value, String> {
58 self.call(HostRequestKind::Approval, body).await
59 }
60
61 async fn call(
62 &self,
63 kind: HostRequestKind,
64 body: serde_json::Value,
65 ) -> Result<serde_json::Value, String> {
66 let (reply, rx) = oneshot::channel();
67 self.req_tx
68 .send(HostRequest { kind, body, reply })
69 .map_err(|_| "host bridge closed".to_string())?;
70 rx.await
71 .map_err(|_| "host bridge dropped reply".to_string())
72 }
73}
74
75#[derive(Clone)]
77pub struct EventSink {
78 tx: mpsc::UnboundedSender<serde_json::Value>,
79 control_tx: Option<mpsc::UnboundedSender<ExecutorControl>>,
80 host: Option<HostBridge>,
81}
82
83impl EventSink {
84 pub fn channel() -> (Self, mpsc::UnboundedReceiver<serde_json::Value>) {
86 let (tx, rx) = mpsc::unbounded_channel();
87 (
88 EventSink {
89 tx,
90 control_tx: None,
91 host: None,
92 },
93 rx,
94 )
95 }
96 pub fn channel_with_control() -> (
98 Self,
99 mpsc::UnboundedReceiver<serde_json::Value>,
100 mpsc::UnboundedReceiver<ExecutorControl>,
101 ) {
102 let (tx, rx) = mpsc::unbounded_channel();
103 let (control_tx, control_rx) = mpsc::unbounded_channel();
104 (
105 EventSink {
106 tx,
107 control_tx: Some(control_tx),
108 host: None,
109 },
110 rx,
111 control_rx,
112 )
113 }
114 pub fn with_host_bridge(mut self, bridge: HostBridge) -> Self {
116 self.host = Some(bridge);
117 self
118 }
119 pub fn host(&self) -> Option<&HostBridge> {
121 self.host.as_ref()
122 }
123 pub fn emit(&self, event: serde_json::Value) {
125 let _ = self.tx.send(event);
126 }
127 pub fn confirm_session_message(&self, confirmation: SessionMessageAdmissionConfirmation) {
130 if let Some(tx) = &self.control_tx {
131 let _ = tx.send(ExecutorControl::SessionMessageAdmitted(confirmation));
132 }
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
138pub struct ChildOutcome {
139 pub status: TerminalStatus,
140 pub result: Option<String>,
141 pub error: Option<String>,
142 pub transcript: Vec<serde_json::Value>,
145}
146
147impl ChildOutcome {
148 pub fn completed(result: impl Into<String>) -> Self {
149 Self {
150 status: TerminalStatus::Completed,
151 result: Some(result.into()),
152 error: None,
153 transcript: Vec::new(),
154 }
155 }
156 pub fn error(msg: impl Into<String>) -> Self {
157 Self {
158 status: TerminalStatus::Error,
159 result: None,
160 error: Some(msg.into()),
161 transcript: Vec::new(),
162 }
163 }
164 pub fn cancelled() -> Self {
165 Self {
166 status: TerminalStatus::Cancelled,
167 result: None,
168 error: None,
169 transcript: Vec::new(),
170 }
171 }
172 pub fn suspended(transcript: Vec<serde_json::Value>) -> Self {
175 Self {
176 status: TerminalStatus::Suspended,
177 result: None,
178 error: None,
179 transcript,
180 }
181 }
182}
183
184pub struct SteerInbox {
188 rx: mpsc::UnboundedReceiver<SteerMessage>,
189}
190
191#[derive(Debug, Clone, PartialEq)]
192pub enum SteerMessage {
193 Text(String),
196 DurableText {
199 message_id: String,
200 text: String,
201 },
202 SessionMessage(Box<SessionMessageDelivery>),
203}
204
205impl From<String> for SteerMessage {
206 fn from(value: String) -> Self {
207 Self::Text(value)
208 }
209}
210
211impl From<&str> for SteerMessage {
212 fn from(value: &str) -> Self {
213 Self::Text(value.to_string())
214 }
215}
216
217impl SteerInbox {
218 pub fn channel() -> (mpsc::UnboundedSender<SteerMessage>, Self) {
220 let (tx, rx) = mpsc::unbounded_channel();
221 (tx, SteerInbox { rx })
222 }
223 pub fn disconnected() -> Self {
225 let (_tx, rx) = mpsc::unbounded_channel();
226 SteerInbox { rx }
227 }
228 pub async fn recv(&mut self) -> Option<String> {
230 match self.rx.recv().await? {
231 SteerMessage::Text(text) => Some(text),
232 SteerMessage::DurableText { text, .. } => Some(text),
233 SteerMessage::SessionMessage(delivery) => serde_json::to_string(&delivery).ok(),
234 }
235 }
236 pub async fn recv_message(&mut self) -> Option<SteerMessage> {
239 self.rx.recv().await
240 }
241}
242
243#[async_trait]
245pub trait ChildExecutor: Send + Sync + 'static {
246 async fn run(
247 &self,
248 spec: RunSpec,
249 events: EventSink,
250 steer: SteerInbox,
251 cancel: CancellationToken,
252 ) -> ChildOutcome;
253}
254
255pub struct EchoExecutor;
262
263pub const ECHO_SLEEP_PREFIX: &str = "__sleep_ms:";
265
266#[async_trait]
267impl ChildExecutor for EchoExecutor {
268 async fn run(
269 &self,
270 spec: RunSpec,
271 events: EventSink,
272 _steer: SteerInbox,
273 cancel: CancellationToken,
274 ) -> ChildOutcome {
275 let mut sleep_ms: Option<u64> = None;
279 let mut words: Vec<&str> = Vec::new();
280 for word in spec.assignment.split_whitespace() {
281 match word
282 .strip_prefix(ECHO_SLEEP_PREFIX)
283 .and_then(|n| n.parse::<u64>().ok())
284 {
285 Some(ms) if sleep_ms.is_none() => sleep_ms = Some(ms),
286 _ => words.push(word),
287 }
288 }
289 if let Some(ms) = sleep_ms {
290 tokio::select! {
291 _ = tokio::time::sleep(std::time::Duration::from_millis(ms)) => {}
292 _ = cancel.cancelled() => return ChildOutcome::cancelled(),
293 }
294 }
295
296 for word in &words {
297 if cancel.is_cancelled() {
298 return ChildOutcome::cancelled();
299 }
300 events.emit(serde_json::json!({ "type": "token", "content": format!("{word} ") }));
301 tokio::task::yield_now().await;
303 }
304 events.emit(serde_json::json!({ "type": "complete" }));
305 ChildOutcome::completed(format!("echo: {}", words.join(" ")))
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[tokio::test]
314 async fn echo_streams_then_completes() {
315 let (sink, mut rx) = EventSink::channel();
316 let outcome = EchoExecutor
317 .run(
318 RunSpec {
319 assignment: "alpha beta".into(),
320 logical_session: None,
321 project_id: None,
322 reasoning_effort: None,
323 permission_policy: None,
324 messages: Vec::new(),
325 activation_run_id: None,
326 initial_session_messages: Vec::new(),
327 secrets: Default::default(),
328 },
329 sink,
330 SteerInbox::disconnected(),
331 CancellationToken::new(),
332 )
333 .await;
334 assert_eq!(outcome.status, TerminalStatus::Completed);
335 assert_eq!(outcome.result.as_deref(), Some("echo: alpha beta"));
336
337 let mut events = Vec::new();
338 while let Ok(e) = rx.try_recv() {
339 events.push(e);
340 }
341 assert_eq!(events.len(), 3);
343 assert_eq!(events[0]["content"], "alpha ");
344 }
345
346 #[tokio::test]
347 async fn echo_honors_cancel() {
348 let (sink, _rx) = EventSink::channel();
349 let cancel = CancellationToken::new();
350 cancel.cancel();
351 let outcome = EchoExecutor
352 .run(
353 RunSpec {
354 assignment: "a b c".into(),
355 logical_session: None,
356 project_id: None,
357 reasoning_effort: None,
358 permission_policy: None,
359 messages: Vec::new(),
360 activation_run_id: None,
361 initial_session_messages: Vec::new(),
362 secrets: Default::default(),
363 },
364 sink,
365 SteerInbox::disconnected(),
366 cancel,
367 )
368 .await;
369 assert_eq!(outcome.status, TerminalStatus::Cancelled);
370 }
371
372 #[tokio::test]
373 async fn approval_call_sends_approval_kind_and_round_trips_reply() {
374 let (bridge, mut req_rx) = HostBridge::channel();
375 let caller = tokio::spawn(async move {
376 bridge
377 .approval_call(serde_json::json!({"resource": "/tmp/x"}))
378 .await
379 });
380 let req = req_rx.recv().await.expect("a host request");
381 assert_eq!(req.kind, HostRequestKind::Approval);
382 assert_eq!(req.body["resource"], "/tmp/x");
383 let _ = req.reply.send(serde_json::json!({"approved": true}));
384 let reply = caller.await.unwrap().expect("decision");
385 assert_eq!(reply["approved"], true);
386 }
387}