Skip to main content

bamboo_subagent/
executor.rs

1//! The `ChildExecutor` seam: how an actor actually runs a task.
2//!
3//! This crate never depends on the agent runtime. The worker process implements
4//! [`ChildExecutor`] backed by the real `agent.execute()`; the transport layer drives it.
5//! [`EchoExecutor`] is a dependency-free stand-in used by the demo worker and tests.
6
7use async_trait::async_trait;
8use std::sync::{
9    atomic::{AtomicUsize, Ordering},
10    Arc,
11};
12use tokio::sync::{mpsc, oneshot, Semaphore};
13use tokio_util::sync::CancellationToken;
14
15use crate::proto::{
16    RunSpec, SessionMessageAdmissionConfirmation, SessionMessageDelivery, TerminalStatus,
17};
18
19/// Non-AgentEvent signals an executor sends to its transport.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum ExecutorControl {
22    SessionMessageAdmitted(SessionMessageAdmissionConfirmation),
23    InitialInputReleaseRequest(crate::proto::InitialInputReleaseRequest),
24}
25
26/// Which kind of host callback a [`HostRequest`] is — selects the wire frame.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum HostRequestKind {
29    /// Proxy a gated-tool approval to the host (→ `ChildFrame::ApprovalRequest`).
30    Approval,
31    /// Read the active logical Child's owned tree from its canonical Host.
32    OwnedTree,
33    /// Invoke the canonical Host's logical SubAgent tool for this active Run.
34    SubAgent,
35}
36
37/// A single host-callback request: an executor proxies a gated-tool approval
38/// back to the host over the run's WS and awaits the reply.
39pub struct HostRequest {
40    pub kind: HostRequestKind,
41    pub body: serde_json::Value,
42    pub reply: oneshot::Sender<serde_json::Value>,
43}
44
45/// Host-callback bridge handed to an executor (via [`EventSink`]) so a nested
46/// sub-agent can proxy a gated-tool approval to the host — over the same
47/// per-child WebSocket, no broker needed. Absent for tests/[`EchoExecutor`].
48#[derive(Clone)]
49pub struct HostBridge {
50    req_tx: mpsc::UnboundedSender<HostRequest>,
51    tree_permits: Arc<Semaphore>,
52    tree_calls: Arc<AtomicUsize>,
53    subagent_permits: Arc<Semaphore>,
54    subagent_calls: Arc<AtomicUsize>,
55}
56
57impl HostBridge {
58    /// Create a bridge + the receiver the transport pumps to the wire.
59    pub fn channel() -> (Self, mpsc::UnboundedReceiver<HostRequest>) {
60        let (req_tx, req_rx) = mpsc::unbounded_channel();
61        (
62            HostBridge {
63                req_tx,
64                tree_permits: Arc::new(Semaphore::new(1)),
65                tree_calls: Arc::new(AtomicUsize::new(0)),
66                subagent_permits: Arc::new(Semaphore::new(1)),
67                subagent_calls: Arc::new(AtomicUsize::new(0)),
68            },
69            req_rx,
70        )
71    }
72
73    /// Proxy one gated-tool approval to the host and await the decision JSON
74    /// (`{"approved": bool}`). The worker's permission flow blocks on this so the
75    /// human decides on the parent (Phase 2).
76    pub async fn approval_call(
77        &self,
78        body: serde_json::Value,
79    ) -> Result<serde_json::Value, String> {
80        self.call(HostRequestKind::Approval, body).await
81    }
82
83    /// Request one bounded read from the canonical Host for the active logical
84    /// Child. Caller identity is never accepted from the Worker payload.
85    pub async fn owned_tree_call(&self, cursor: Option<&str>) -> Result<serde_json::Value, String> {
86        const MAX_CURSOR_BYTES: usize = 128;
87        const MAX_RESULT_BYTES: usize = 8192;
88        const MAX_CALLS_PER_RUN: usize = 16;
89        const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
90        if cursor.is_some_and(|cursor| cursor.is_empty() || cursor.len() > MAX_CURSOR_BYTES) {
91            return Err("invalid owned tree cursor".into());
92        }
93        let _permit = tokio::time::timeout(TIMEOUT, self.tree_permits.acquire())
94            .await
95            .map_err(|_| "owned tree request timed out")?
96            .map_err(|_| "owned tree inspection unavailable")?;
97        if self.tree_calls.fetch_add(1, Ordering::AcqRel) >= MAX_CALLS_PER_RUN {
98            return Err("owned tree request limit reached".into());
99        }
100        let reply = tokio::time::timeout(
101            TIMEOUT,
102            self.call(
103                HostRequestKind::OwnedTree,
104                serde_json::json!({"cursor":cursor}),
105            ),
106        )
107        .await
108        .map_err(|_| "owned tree request timed out")??;
109        let page = reply
110            .get("page")
111            .filter(|page| !page.is_null())
112            .ok_or_else(|| "owned tree inspection rejected".to_string())?;
113        if serde_json::to_vec(page).map_or(true, |bytes| bytes.len() > MAX_RESULT_BYTES) {
114            return Err("owned tree observation exceeds its limit".into());
115        }
116        Ok(page.clone())
117    }
118
119    /// Route a nested Child's logical operation to its canonical Host. The
120    /// Worker sends arguments and a transcript call id, never caller authority.
121    pub async fn subagent_call(
122        &self,
123        args: serde_json::Value,
124        tool_call_id: &str,
125    ) -> Result<serde_json::Value, String> {
126        const MAX_ARGS_BYTES: usize = 64 * 1024;
127        const MAX_REPLY_BYTES: usize = 16 * 1024;
128        const MAX_CALLS_PER_RUN: usize = 64;
129        const TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
130        if tool_call_id.is_empty()
131            || tool_call_id.len() > 128
132            || serde_json::to_vec(&args).map_or(true, |bytes| bytes.len() > MAX_ARGS_BYTES)
133        {
134            return Err("invalid canonical SubAgent request".into());
135        }
136        let _permit = tokio::time::timeout(TIMEOUT, self.subagent_permits.acquire())
137            .await
138            .map_err(|_| "canonical SubAgent request timed out")?
139            .map_err(|_| "canonical SubAgent unavailable")?;
140        if self.subagent_calls.fetch_add(1, Ordering::AcqRel) >= MAX_CALLS_PER_RUN {
141            return Err("canonical SubAgent request limit reached".into());
142        }
143        let reply = tokio::time::timeout(
144            TIMEOUT,
145            self.call(
146                HostRequestKind::SubAgent,
147                serde_json::json!({"args":args,"tool_call_id":tool_call_id}),
148            ),
149        )
150        .await
151        .map_err(|_| "canonical SubAgent request timed out")??;
152        if serde_json::to_vec(&reply).map_or(true, |bytes| bytes.len() > MAX_REPLY_BYTES) {
153            return Err("canonical SubAgent result exceeds its limit".into());
154        }
155        if let Some(error) = reply.get("error").and_then(serde_json::Value::as_str) {
156            return Err(error.to_owned());
157        }
158        reply
159            .get("result")
160            .cloned()
161            .ok_or_else(|| "canonical SubAgent operation rejected".into())
162    }
163
164    async fn call(
165        &self,
166        kind: HostRequestKind,
167        body: serde_json::Value,
168    ) -> Result<serde_json::Value, String> {
169        let (reply, rx) = oneshot::channel();
170        self.req_tx
171            .send(HostRequest { kind, body, reply })
172            .map_err(|_| "host bridge closed".to_string())?;
173        rx.await
174            .map_err(|_| "host bridge dropped reply".to_string())
175    }
176}
177
178/// Sink an executor emits events into; the transport forwards each as a `ChildFrame::Event`.
179#[derive(Clone)]
180pub struct EventSink {
181    tx: mpsc::Sender<serde_json::Value>,
182    control_tx: Option<mpsc::Sender<ExecutorControl>>,
183    host: Option<HostBridge>,
184}
185
186impl EventSink {
187    pub const EVENT_CAPACITY: usize = 256;
188    pub const CONTROL_CAPACITY: usize = 32;
189
190    /// Create a sink + the receiver the transport pumps to the wire.
191    pub fn channel() -> (Self, mpsc::Receiver<serde_json::Value>) {
192        let (tx, rx) = mpsc::channel(Self::EVENT_CAPACITY);
193        (
194            EventSink {
195                tx,
196                control_tx: None,
197                host: None,
198            },
199            rx,
200        )
201    }
202    /// Create a sink with a control channel for protocol-level confirmations.
203    pub fn channel_with_control() -> (
204        Self,
205        mpsc::Receiver<serde_json::Value>,
206        mpsc::Receiver<ExecutorControl>,
207    ) {
208        let (tx, rx) = mpsc::channel(Self::EVENT_CAPACITY);
209        let (control_tx, control_rx) = mpsc::channel(Self::CONTROL_CAPACITY);
210        (
211            EventSink {
212                tx,
213                control_tx: Some(control_tx),
214                host: None,
215            },
216            rx,
217            control_rx,
218        )
219    }
220    /// Attach a host-callback bridge (the transport wires this for real runs).
221    pub fn with_host_bridge(mut self, bridge: HostBridge) -> Self {
222        self.host = Some(bridge);
223        self
224    }
225    /// The host-callback bridge, if this run was wired with one.
226    pub fn host(&self) -> Option<&HostBridge> {
227        self.host.as_ref()
228    }
229    /// Emit one event with bounded producer backpressure. Transport QoS still
230    /// decides whether a sequenced live batch can be dropped; durable events
231    /// cannot accumulate without bound ahead of that transport or be lost here.
232    pub async fn emit(&self, event: serde_json::Value) {
233        let _ = self.tx.send(event).await;
234    }
235    pub async fn request_initial_release(
236        &self,
237        request: crate::proto::InitialInputReleaseRequest,
238    ) -> Result<(), String> {
239        self.control_tx
240            .as_ref()
241            .ok_or("initial release channel missing")?
242            .send(ExecutorControl::InitialInputReleaseRequest(request))
243            .await
244            .map_err(|_| "initial release channel closed".into())
245    }
246    /// Confirm a forwarded SessionInbox message only after the executor has
247    /// observed its durable local admitted receipt.
248    pub async fn confirm_session_message(&self, confirmation: SessionMessageAdmissionConfirmation) {
249        if let Some(tx) = &self.control_tx {
250            let _ = tx
251                .send(ExecutorControl::SessionMessageAdmitted(confirmation))
252                .await;
253        }
254    }
255}
256
257/// Result of running a task to completion (or suspension).
258#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
259pub struct ChildOutcome {
260    pub status: TerminalStatus,
261    /// Set by the transport only after successful event drain and final flush.
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub final_event_watermark: Option<crate::proto::ActorEventWatermark>,
264    pub result: Option<String>,
265    pub error: Option<String>,
266    /// Rolling-wire compatibility field. The current actor host never consumes
267    /// it: canonical session checkpoints are the transcript authority and
268    /// suspend/resume dispatch is not implemented. Keep serializing the empty
269    /// field until the protocol is versioned; do not build new state transfer on
270    /// this payload.
271    #[serde(default)]
272    pub transcript: Vec<serde_json::Value>,
273}
274
275impl ChildOutcome {
276    pub fn completed(result: impl Into<String>) -> Self {
277        Self {
278            status: TerminalStatus::Completed,
279            final_event_watermark: None,
280            result: Some(result.into()),
281            error: None,
282            transcript: Vec::new(),
283        }
284    }
285    pub fn error(msg: impl Into<String>) -> Self {
286        Self {
287            status: TerminalStatus::Error,
288            final_event_watermark: None,
289            result: None,
290            error: Some(msg.into()),
291            transcript: Vec::new(),
292        }
293    }
294    pub fn cancelled() -> Self {
295        Self {
296            status: TerminalStatus::Cancelled,
297            final_event_watermark: None,
298            result: None,
299            error: None,
300            transcript: Vec::new(),
301        }
302    }
303    /// Compatibility constructor for the unimplemented suspend wire path.
304    /// Current hosts reject `Suspended` and do not persist this transcript.
305    pub fn suspended(transcript: Vec<serde_json::Value>) -> Self {
306        Self {
307            status: TerminalStatus::Suspended,
308            final_event_watermark: None,
309            result: None,
310            error: None,
311            transcript,
312        }
313    }
314}
315
316/// Mid-run steering inbox: `ParentFrame::Message` texts arriving while a run is
317/// active. Executors that support in-band steering admit them at a safe point
318/// (the engine's round boundary); others may simply ignore the inbox.
319pub struct SteerInbox {
320    rx: mpsc::UnboundedReceiver<SteerMessage>,
321    // One physical Run owns this inbox. Retain only replies actually accepted
322    // by its initial-input barriers; public input or a new request is no proof.
323    accepted_initial_releases: Vec<crate::proto::InitialInputRelease>,
324}
325
326#[derive(Debug, Clone, PartialEq)]
327pub enum SteerMessage {
328    /// Direct legacy WS text has no durable ingress identity and remains
329    /// explicitly best-effort.
330    Text(String),
331    /// Broker Maildir ingress retains its durable message id so at-least-once
332    /// replay converges on one typed SessionInbox envelope.
333    DurableText {
334        message_id: String,
335        text: String,
336    },
337    SessionMessage(Box<SessionMessageDelivery>),
338    InitialInputRelease(crate::proto::InitialInputRelease),
339}
340
341impl From<String> for SteerMessage {
342    fn from(value: String) -> Self {
343        Self::Text(value)
344    }
345}
346
347impl From<&str> for SteerMessage {
348    fn from(value: &str) -> Self {
349        Self::Text(value.to_string())
350    }
351}
352
353impl SteerInbox {
354    /// Create a sender + inbox pair (the transport holds the sender).
355    pub fn channel() -> (mpsc::UnboundedSender<SteerMessage>, Self) {
356        let (tx, rx) = mpsc::unbounded_channel();
357        (
358            tx,
359            SteerInbox {
360                rx,
361                accepted_initial_releases: Vec::new(),
362            },
363        )
364    }
365    /// An already-closed inbox (for tests / executors that don't steer).
366    pub fn disconnected() -> Self {
367        let (_tx, rx) = mpsc::unbounded_channel();
368        SteerInbox {
369            rx,
370            accepted_initial_releases: Vec::new(),
371        }
372    }
373    /// Next steering message, or `None` once the run's sender is gone.
374    pub async fn recv(&mut self) -> Option<String> {
375        match self.recv_message().await? {
376            SteerMessage::Text(text) => Some(text),
377            SteerMessage::DurableText { text, .. } => Some(text),
378            SteerMessage::SessionMessage(delivery) => serde_json::to_string(&delivery).ok(),
379            SteerMessage::InitialInputRelease(_) => None, // Never flatten authority into text.
380        }
381    }
382    /// Wait before SDK admission. Exact unexpired replies from this inbox's
383    /// completed barriers are duplicates; every other mismatched control fails.
384    pub async fn wait_initial_release(
385        &mut self,
386        request: &crate::proto::InitialInputReleaseRequest,
387        cancel: &CancellationToken,
388        events: &EventSink,
389    ) -> Result<chrono::DateTime<chrono::Utc>, String> {
390        self.wait_initial_release_bounded(
391            request,
392            cancel,
393            events,
394            std::time::Duration::from_secs(60),
395        )
396        .await
397    }
398    async fn wait_initial_release_bounded(
399        &mut self,
400        request: &crate::proto::InitialInputReleaseRequest,
401        cancel: &CancellationToken,
402        events: &EventSink,
403        wait: std::time::Duration,
404    ) -> Result<chrono::DateTime<chrono::Utc>, String> {
405        let timeout = tokio::time::sleep(wait);
406        tokio::pin!(timeout);
407        let mut retry = tokio::time::interval(std::time::Duration::from_secs(5));
408        retry.tick().await;
409        loop {
410            tokio::select! {
411                biased;
412                _ = cancel.cancelled() => return Err("initial release cancelled".into()),
413                _ = &mut timeout => return Err("initial release timed out".into()),
414                message = self.recv_message() => return match message {
415                    Some(SteerMessage::InitialInputRelease(release)) if release.permits(request, chrono::Utc::now()) => {
416                        if self.accepted_initial_releases.iter().any(|accepted| accepted.request == release.request) {
417                            return Err("initial release missing, expired or mismatched".into());
418                        }
419                        if self.accepted_initial_releases.len() >= bamboo_domain::SessionInboxLimits::default().max_claim_batch {
420                            return Err("initial release completed batch limit exceeded".into());
421                        }
422                        let deadline = release.expires_at;
423                        self.accepted_initial_releases.push(release);
424                        Ok(deadline)
425                    },
426                    _ => Err("initial release missing, expired or mismatched".into()),
427                },
428                _ = retry.tick() => events.request_initial_release(request.clone()).await?,
429            }
430        }
431    }
432    /// Receive the typed steering value. Runtime-backed workers use this path
433    /// so SessionInbox envelopes never collapse into ad-hoc text.
434    pub async fn recv_message(&mut self) -> Option<SteerMessage> {
435        loop {
436            let message = self.rx.recv().await?;
437            if let SteerMessage::InitialInputRelease(release) = &message {
438                // A retransmission cannot satisfy a later barrier, extend the
439                // accepted deadline, become text or terminate live steering.
440                if chrono::Utc::now() < release.expires_at
441                    && self.accepted_initial_releases.contains(release)
442                {
443                    continue;
444                }
445            }
446            return Some(message);
447        }
448    }
449}
450
451/// What runs inside an actor. Implemented by the worker with the real runtime.
452#[async_trait]
453pub trait ChildExecutor: Send + Sync + 'static {
454    /// The Host validates this executor's complete event trace before admitting
455    /// its history. Transport may coalesce events but must not drop batches.
456    /// Ordinary observation-only executors retain their existing lossy lanes.
457    fn requires_contiguous_events(&self) -> bool {
458        false
459    }
460
461    /// Advertise only when `run` validates EnvironmentLease before executing
462    /// provider or tools. The broker binds this claim to the authenticated
463    /// subscriber connection; an older worker defaults to unsupported.
464    fn supports_environment_lease_v1(&self) -> bool {
465        false
466    }
467    /// Maximum number of independent Run/Ask/Task executions this instance may
468    /// execute at once. The safe default is one: production executors often
469    /// own mutable permission/provider/child-runner state that must not cross
470    /// session boundaries. An implementation may opt into more slots only when
471    /// all per-run state is isolated.
472    fn max_parallel_executions(&self) -> usize {
473        1
474    }
475
476    async fn run(
477        &self,
478        spec: RunSpec,
479        events: EventSink,
480        steer: SteerInbox,
481        cancel: CancellationToken,
482    ) -> ChildOutcome;
483}
484
485/// Dependency-free executor: streams one `token` event per word, then completes with an echo.
486/// Used by the demo worker and tests to exercise the full transport without a real LLM.
487///
488/// Test hook: an assignment starting with `__sleep_ms:<n>` sleeps (cancellably)
489/// for `n` milliseconds before echoing the rest — this is what lets cancel /
490/// concurrency e2e tests hold a run open deterministically without an LLM.
491pub struct EchoExecutor;
492
493/// Assignment prefix recognized by [`EchoExecutor`] for a cancellable delay.
494pub const ECHO_SLEEP_PREFIX: &str = "__sleep_ms:";
495
496#[async_trait]
497impl ChildExecutor for EchoExecutor {
498    fn max_parallel_executions(&self) -> usize {
499        // Echo has no mutable execution state; keep the high-concurrency fabric
500        // E2E honest without weakening the safe default for real runtimes.
501        256
502    }
503
504    async fn run(
505        &self,
506        spec: RunSpec,
507        events: EventSink,
508        _steer: SteerInbox,
509        cancel: CancellationToken,
510    ) -> ChildOutcome {
511        // Optional cancellable delay: any token `__sleep_ms:<n>` in the
512        // assignment (scanned, not just the prefix — child creation may wrap
513        // the prompt in a template). The marker token itself is not echoed.
514        let mut sleep_ms: Option<u64> = None;
515        let mut words: Vec<&str> = Vec::new();
516        for word in spec.assignment.split_whitespace() {
517            match word
518                .strip_prefix(ECHO_SLEEP_PREFIX)
519                .and_then(|n| n.parse::<u64>().ok())
520            {
521                Some(ms) if sleep_ms.is_none() => sleep_ms = Some(ms),
522                _ => words.push(word),
523            }
524        }
525        if let Some(ms) = sleep_ms {
526            tokio::select! {
527                _ = tokio::time::sleep(std::time::Duration::from_millis(ms)) => {}
528                _ = cancel.cancelled() => return ChildOutcome::cancelled(),
529            }
530        }
531
532        for word in &words {
533            if cancel.is_cancelled() {
534                return ChildOutcome::cancelled();
535            }
536            events
537                .emit(serde_json::json!({ "type": "token", "content": format!("{word} ") }))
538                .await;
539            // tiny yield so cancellation can interleave; not a real delay
540            tokio::task::yield_now().await;
541        }
542        events.emit(serde_json::json!({ "type": "complete" })).await;
543        ChildOutcome::completed(format!("echo: {}", words.join(" ")))
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550
551    fn duplicate_release_request(slot: u64) -> crate::proto::InitialInputReleaseRequest {
552        crate::proto::InitialInputReleaseRequest {
553            version: 1,
554            nonce: uuid::Uuid::new_v4().to_string(),
555            child_id: "duplicate-child".into(),
556            parent_id: "parent".into(),
557            root_id: "parent".into(),
558            created_at: chrono::Utc::now(),
559            spawn_depth: 1,
560            project_id: None,
561            envelope_id: format!("input-{slot}"),
562            generation: slot + 1,
563            activation_run_id: "same-physical-run".into(),
564            execution_epoch: 1,
565        }
566    }
567
568    // Reproduce the actual retry producer with virtual Tokio time, without
569    // sleeping five wall-clock seconds or relying on host scheduling delays.
570    #[tokio::test(start_paused = true)]
571    async fn initial_release_duplicate_retry_between_batch_items_is_harmless() {
572        let first = duplicate_release_request(0);
573        let second = duplicate_release_request(1);
574        let first_release = crate::proto::InitialInputRelease {
575            request: first.clone(),
576            expires_at: chrono::Utc::now() + chrono::Duration::seconds(30),
577        };
578        let second_release = crate::proto::InitialInputRelease {
579            request: second.clone(),
580            expires_at: first_release.expires_at - chrono::Duration::seconds(1),
581        };
582        let (events, _events, mut controls) = EventSink::channel_with_control();
583        let (sender, mut inbox) = SteerInbox::channel();
584        let cancel = CancellationToken::new();
585        let mut waiting = Box::pin(inbox.wait_initial_release(&first, &cancel, &events));
586        assert!(futures_util::poll!(waiting.as_mut()).is_pending());
587        tokio::time::advance(std::time::Duration::from_secs(5)).await;
588        assert!(futures_util::poll!(waiting.as_mut()).is_pending());
589        assert_eq!(
590            controls.try_recv().unwrap(),
591            ExecutorControl::InitialInputReleaseRequest(first.clone())
592        );
593        sender
594            .send(SteerMessage::InitialInputRelease(first_release.clone()))
595            .unwrap();
596        assert_eq!(waiting.await.unwrap(), first_release.expires_at);
597        // The Host may answer both the original request and its retry. A
598        // preceding reply must not abort or satisfy the next item's barrier.
599        sender
600            .send(SteerMessage::InitialInputRelease(first_release.clone()))
601            .unwrap();
602        sender
603            .send(SteerMessage::InitialInputRelease(second_release.clone()))
604            .unwrap();
605        assert_eq!(
606            inbox
607                .wait_initial_release(&second, &cancel, &events)
608                .await
609                .unwrap(),
610            second_release.expires_at
611        );
612        // Duplicates after the final barrier also cannot kill the typed steer
613        // consumer or flatten a release into user text.
614        sender
615            .send(SteerMessage::InitialInputRelease(first_release))
616            .unwrap();
617        sender
618            .send(SteerMessage::InitialInputRelease(second_release))
619            .unwrap();
620        sender
621            .send(SteerMessage::Text("next ordinary steer".into()))
622            .unwrap();
623        assert_eq!(
624            inbox.recv_message().await,
625            Some(SteerMessage::Text("next ordinary steer".into()))
626        );
627    }
628
629    #[tokio::test]
630    async fn initial_release_changed_prior_reply_cannot_be_filtered_or_release_next_input() {
631        for changed in [
632            "nonce",
633            "epoch",
634            "birth",
635            "envelope",
636            "generation",
637            "deadline",
638            "expired",
639        ] {
640            let first = duplicate_release_request(0);
641            let second = duplicate_release_request(1);
642            let original = crate::proto::InitialInputRelease {
643                request: first.clone(),
644                expires_at: chrono::Utc::now() + chrono::Duration::seconds(30),
645            };
646            let (events, _events, _controls) = EventSink::channel_with_control();
647            let (sender, mut inbox) = SteerInbox::channel();
648            let cancel = CancellationToken::new();
649            sender
650                .send(SteerMessage::InitialInputRelease(original.clone()))
651                .unwrap();
652            inbox
653                .wait_initial_release(&first, &cancel, &events)
654                .await
655                .unwrap();
656            let mut altered = original;
657            match changed {
658                "nonce" => altered.request.nonce = uuid::Uuid::new_v4().to_string(),
659                "epoch" => altered.request.execution_epoch += 1,
660                "birth" => altered.request.created_at += chrono::Duration::seconds(1),
661                "envelope" => altered.request.envelope_id = "other-input".into(),
662                "generation" => altered.request.generation += 1,
663                "deadline" => altered.expires_at += chrono::Duration::seconds(1),
664                "expired" => altered.expires_at = chrono::Utc::now() - chrono::Duration::seconds(1),
665                _ => unreachable!(),
666            }
667            sender
668                .send(SteerMessage::InitialInputRelease(altered))
669                .unwrap();
670            let error = inbox
671                .wait_initial_release(&second, &cancel, &events)
672                .await
673                .unwrap_err();
674            assert!(error.contains("mismatched"), "{changed}: {error}");
675        }
676    }
677
678    // Keep the 2ms timer independent of wall-clock descheduling when
679    // cancellation or a disconnected inbox is already ready.
680    #[tokio::test(start_paused = true)]
681    async fn initial_release_wait_observes_timeout_disconnect_and_cancel() {
682        let request = crate::proto::InitialInputReleaseRequest {
683            version: 1,
684            nonce: uuid::Uuid::new_v4().to_string(),
685            child_id: "wait-child".into(),
686            parent_id: "parent".into(),
687            root_id: "parent".into(),
688            created_at: chrono::Utc::now(),
689            spawn_depth: 1,
690            project_id: None,
691            envelope_id: "input".into(),
692            generation: 1,
693            activation_run_id: "run".into(),
694            execution_epoch: 1,
695        };
696        let (events, _rx, _controls) = EventSink::channel_with_control();
697        for case in ["timeout", "disconnect", "cancel"] {
698            let (sender, mut inbox) = SteerInbox::channel();
699            let cancel = CancellationToken::new();
700            if case == "disconnect" {
701                drop(sender);
702            } else if case == "cancel" {
703                cancel.cancel();
704            }
705            let error = inbox
706                .wait_initial_release_bounded(
707                    &request,
708                    &cancel,
709                    &events,
710                    std::time::Duration::from_millis(2),
711                )
712                .await
713                .unwrap_err();
714            assert!(
715                error.contains(match case {
716                    "timeout" => "timed out",
717                    "cancel" => "cancelled",
718                    _ => "missing",
719                }),
720                "initial release case={case}, actual error={error:?}"
721            );
722        }
723    }
724
725    #[tokio::test]
726    async fn full_event_queue_backpressures_without_losing_durable_order_or_blocking_control() {
727        let (sink, mut events, mut controls) = EventSink::channel_with_control();
728        for index in 0..EventSink::EVENT_CAPACITY {
729            sink.emit(serde_json::json!({"type":"tool_start","index":index}))
730                .await;
731        }
732        assert_eq!(events.len(), EventSink::EVENT_CAPACITY);
733        let terminal = sink.emit(serde_json::json!({"type":"complete"}));
734        tokio::pin!(terminal);
735        assert!(futures_util::poll!(&mut terminal).is_pending());
736
737        // Admission control has its own bounded lane even while event
738        // production is parked at capacity.
739        let confirmation = SessionMessageAdmissionConfirmation {
740            target_session_id: "bounded-child".into(),
741            envelope_id: "message".into(),
742            canonical_claim_generation: 1,
743            activation_run_id: "activation".into(),
744        };
745        sink.confirm_session_message(confirmation.clone()).await;
746        assert_eq!(
747            controls.recv().await.unwrap(),
748            ExecutorControl::SessionMessageAdmitted(confirmation)
749        );
750        assert_eq!(events.recv().await.unwrap()["index"], 0);
751        terminal.await;
752        for index in 1..EventSink::EVENT_CAPACITY {
753            assert_eq!(events.recv().await.unwrap()["index"], index);
754        }
755        assert_eq!(events.recv().await.unwrap()["type"], "complete");
756    }
757
758    #[test]
759    fn echo_explicitly_opts_into_high_parallelism() {
760        assert!(EchoExecutor.max_parallel_executions() >= 200);
761    }
762
763    #[tokio::test]
764    async fn echo_streams_then_completes() {
765        let (sink, mut rx) = EventSink::channel();
766        let outcome = EchoExecutor
767            .run(
768                RunSpec {
769                    assignment: "alpha beta".into(),
770                    logical_session: None,
771                    project_id: None,
772                    reasoning_effort: None,
773                    permission_policy: None,
774                    messages: Vec::new(),
775                    activation_run_id: None,
776                    execution_epoch: 0,
777                    initial_session_messages: Vec::new(),
778                    secrets: Default::default(),
779                },
780                sink,
781                SteerInbox::disconnected(),
782                CancellationToken::new(),
783            )
784            .await;
785        assert_eq!(outcome.status, TerminalStatus::Completed);
786        assert_eq!(outcome.result.as_deref(), Some("echo: alpha beta"));
787
788        let mut events = Vec::new();
789        while let Ok(e) = rx.try_recv() {
790            events.push(e);
791        }
792        // two tokens + one complete
793        assert_eq!(events.len(), 3);
794        assert_eq!(events[0]["content"], "alpha ");
795    }
796
797    #[tokio::test]
798    async fn echo_honors_cancel() {
799        let (sink, _rx) = EventSink::channel();
800        let cancel = CancellationToken::new();
801        cancel.cancel();
802        let outcome = EchoExecutor
803            .run(
804                RunSpec {
805                    assignment: "a b c".into(),
806                    logical_session: None,
807                    project_id: None,
808                    reasoning_effort: None,
809                    permission_policy: None,
810                    messages: Vec::new(),
811                    activation_run_id: None,
812                    execution_epoch: 0,
813                    initial_session_messages: Vec::new(),
814                    secrets: Default::default(),
815                },
816                sink,
817                SteerInbox::disconnected(),
818                cancel,
819            )
820            .await;
821        assert_eq!(outcome.status, TerminalStatus::Cancelled);
822    }
823
824    #[tokio::test]
825    async fn approval_call_sends_approval_kind_and_round_trips_reply() {
826        let (bridge, mut req_rx) = HostBridge::channel();
827        let caller = tokio::spawn(async move {
828            bridge
829                .approval_call(serde_json::json!({"resource": "/tmp/x"}))
830                .await
831        });
832        let req = req_rx.recv().await.expect("a host request");
833        assert_eq!(req.kind, HostRequestKind::Approval);
834        assert_eq!(req.body["resource"], "/tmp/x");
835        let _ = req.reply.send(serde_json::json!({"approved": true}));
836        let reply = caller.await.unwrap().expect("decision");
837        assert_eq!(reply["approved"], true);
838    }
839}