Run code mode exclusively through the standalone host (#36217)

## What changed

- Move the V8 implementation into a dedicated `codex-code-mode-runtime` crate used by `codex-code-mode-host`, removing the embedded runtime fallback from the Codex process.
- Resolve the host executable from the active installation layout and check its availability before selecting tools.
- Fall back to direct tools with a one-time warning when optional code mode is unavailable. Keep `code_mode_only` and `disable_in_process_fallback` configurations fail-closed.

## Testing

- Cover host discovery for standalone and package layouts, including missing hosts and symlinks.
- Verify direct-tool fallback, one-time warnings, and fail-closed code-mode-only behavior.

GitOrigin-RevId: 5aa3c6f1db148b2231fc24089a2ee0e2b00dbddb
This commit is contained in:
Channing Conger
2026-07-30 20:24:29 +00:00
committed by copyberry
parent acd540f158
commit 97576b1794
51 changed files with 627 additions and 424 deletions
@@ -0,0 +1,128 @@
use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use futures::FutureExt;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use tracing::warn;
use super::CellHost;
use super::CellToolCall;
use crate::TaskFailureHandler;
use crate::runtime::RuntimeCommand;
#[derive(Clone, Copy)]
pub(super) enum CallbackCompletion {
DrainNotifications,
Cancel,
}
pub(super) fn spawn_notification<H: CellHost>(
tasks: &mut JoinSet<()>,
host: Arc<H>,
call_id: String,
text: String,
cancellation_token: CancellationToken,
task_failure_handler: Option<TaskFailureHandler>,
) {
tasks.spawn(async move {
let callback =
AssertUnwindSafe(async move { host.notify(call_id, text, cancellation_token).await })
.catch_unwind()
.await;
match callback {
Ok(Ok(())) => {}
Ok(Err(err)) => warn!("failed to deliver code mode notification: {err}"),
Err(_) => report_task_failure(
task_failure_handler.as_ref(),
"code mode notification task panicked".to_string(),
),
}
});
}
pub(super) fn spawn_tool<H: CellHost>(
tasks: &mut JoinSet<()>,
host: Arc<H>,
invocation: CellToolCall,
runtime_tx: std::sync::mpsc::Sender<RuntimeCommand>,
cancellation_token: CancellationToken,
task_failure_handler: Option<TaskFailureHandler>,
) {
tasks.spawn(async move {
let id = invocation.id.clone();
let callback =
AssertUnwindSafe(async move { host.invoke_tool(invocation, cancellation_token).await })
.catch_unwind()
.await;
let (command, failure_reason) = match callback {
Ok(Ok(result)) => (RuntimeCommand::ToolResponse { id, result }, None),
Ok(Err(error_text)) => (RuntimeCommand::ToolError { id, error_text }, None),
Err(_) => {
let failure_reason = "code mode tool task panicked".to_string();
(
RuntimeCommand::ToolError {
id,
error_text: failure_reason.clone(),
},
Some(failure_reason),
)
}
};
let _ = runtime_tx.send(command);
if let Some(failure_reason) = failure_reason {
report_task_failure(task_failure_handler.as_ref(), failure_reason);
}
});
}
pub(super) async fn finish_callbacks(
cancellation_token: &CancellationToken,
notification_tasks: &mut JoinSet<()>,
tool_tasks: &mut JoinSet<()>,
completion: CallbackCompletion,
task_failure_handler: Option<&TaskFailureHandler>,
) {
if matches!(completion, CallbackCompletion::Cancel) {
cancellation_token.cancel();
}
drain_tasks(notification_tasks, "notification", task_failure_handler).await;
cancellation_token.cancel();
drain_tasks(tool_tasks, "tool", task_failure_handler).await;
}
pub(super) fn report_task_result(
task_result: Option<Result<(), tokio::task::JoinError>>,
description: &str,
task_failure_handler: Option<&TaskFailureHandler>,
) {
if let Some(Err(err)) = task_result
&& !err.is_cancelled()
{
report_task_failure(
task_failure_handler,
format!("code mode {description} task failed: {err}"),
);
}
}
fn report_task_failure(task_failure_handler: Option<&TaskFailureHandler>, failure_reason: String) {
warn!("{failure_reason}");
if let Some(task_failure_handler) = task_failure_handler {
task_failure_handler(failure_reason);
}
}
async fn drain_tasks(
tasks: &mut JoinSet<()>,
description: &str,
task_failure_handler: Option<&TaskFailureHandler>,
) {
while let Some(result) = tasks.join_next().await {
report_task_result(Some(result), description, task_failure_handler);
}
}
#[cfg(test)]
#[path = "callbacks_tests.rs"]
mod tests;
@@ -0,0 +1,132 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::mpsc as std_mpsc;
use std::time::Duration;
use pretty_assertions::assert_eq;
use serde_json::Value as JsonValue;
use tokio::sync::mpsc;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use super::*;
use crate::cell_actor::CellState;
use crate::cell_actor::CompletionCommit;
use crate::runtime::RuntimeCommand;
use crate::session_runtime::CellEvent;
use crate::session_runtime::ToolKind;
use crate::session_runtime::ToolName;
struct PanickingCallbackHost;
impl CellHost for PanickingCallbackHost {
async fn invoke_tool(
&self,
_invocation: CellToolCall,
_cancellation_token: CancellationToken,
) -> Result<JsonValue, String> {
panic!("tool callback panic probe");
}
async fn notify(
&self,
_call_id: String,
_text: String,
_cancellation_token: CancellationToken,
) -> Result<(), String> {
panic!("notification callback panic probe");
}
async fn commit_completion(
&self,
_stored_value_writes: HashMap<String, JsonValue>,
_event: CellEvent,
_pending_initial_yield_items: Option<Vec<crate::session_runtime::OutputItem>>,
_cell_state: Arc<CellState>,
) -> CompletionCommit {
panic!("unexpected completion commit");
}
async fn closed(&self) {}
}
#[tokio::test]
async fn tool_callback_panic_rejects_the_js_promise_and_reports_failure() {
let mut tasks = JoinSet::new();
let (runtime_tx, runtime_rx) = std_mpsc::channel();
let (failure_tx, mut failure_rx) = mpsc::unbounded_channel();
spawn_tool(
&mut tasks,
Arc::new(PanickingCallbackHost),
CellToolCall {
id: "tool-1".to_string(),
name: ToolName {
name: "panic".to_string(),
namespace: None,
},
kind: ToolKind::Function,
input: None,
},
runtime_tx,
CancellationToken::new(),
Some(Arc::new(move |reason| {
let _ = failure_tx.send(reason);
})),
);
tasks
.join_next()
.await
.expect("tool callback task")
.expect("tool callback wrapper");
let command = runtime_rx
.recv_timeout(Duration::from_secs(1))
.expect("tool error command");
let RuntimeCommand::ToolError { id, error_text } = command else {
panic!("expected a tool error command");
};
assert_eq!(id, "tool-1");
assert_eq!(error_text, "code mode tool task panicked");
assert_eq!(failure_rx.recv().await, Some(error_text));
}
#[tokio::test]
async fn notification_callback_panic_reports_failure() {
let mut tasks = JoinSet::new();
let (failure_tx, mut failure_rx) = mpsc::unbounded_channel();
spawn_notification(
&mut tasks,
Arc::new(PanickingCallbackHost),
"notify-1".to_string(),
"hello".to_string(),
CancellationToken::new(),
Some(Arc::new(move |reason| {
let _ = failure_tx.send(reason);
})),
);
tasks
.join_next()
.await
.expect("notification callback task")
.expect("notification callback wrapper");
let failure_reason = failure_rx.recv().await.expect("notification failure");
assert_eq!(failure_reason, "code mode notification task panicked");
}
#[tokio::test]
async fn callback_wrapper_join_error_reports_failure() {
let task_result = tokio::spawn(async {
panic!("callback wrapper panic probe");
})
.await;
let (failure_tx, mut failure_rx) = mpsc::unbounded_channel();
let task_failure_handler: TaskFailureHandler = Arc::new(move |reason| {
let _ = failure_tx.send(reason);
});
report_task_result(Some(task_result), "tool", Some(&task_failure_handler));
let failure_reason = failure_rx.recv().await.expect("wrapper failure");
assert!(failure_reason.contains("code mode tool task failed"));
}
@@ -0,0 +1,63 @@
use codex_code_mode_protocol::CodeModeToolKind;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::FunctionCallOutputContentItem;
use codex_code_mode_protocol::ImageDetail;
use codex_code_mode_protocol::ToolDefinition;
use codex_protocol::ToolName;
use crate::session_runtime::CreateCellRequest as CellRequest;
use crate::session_runtime::ImageDetail as CellImageDetail;
use crate::session_runtime::OutputItem as CellOutputItem;
use crate::session_runtime::ToolKind as CellToolKind;
pub(super) fn runtime_request(request: CellRequest) -> ExecuteRequest {
ExecuteRequest {
tool_call_id: request.tool_call_id,
enabled_tools: request
.enabled_tools
.into_iter()
.map(|definition| ToolDefinition {
name: definition.name,
tool_name: ToolName {
name: definition.tool_name.name,
namespace: definition.tool_name.namespace,
},
description: definition.description,
kind: match definition.kind {
CellToolKind::Function => CodeModeToolKind::Function,
CellToolKind::Freeform => CodeModeToolKind::Freeform,
},
input_schema: None,
output_schema: None,
})
.collect(),
source: request.source,
yield_time_ms: None,
max_output_tokens: None,
}
}
pub(super) fn cell_tool_kind(kind: CodeModeToolKind) -> CellToolKind {
match kind {
CodeModeToolKind::Function => CellToolKind::Function,
CodeModeToolKind::Freeform => CellToolKind::Freeform,
}
}
pub(super) fn output_item(item: FunctionCallOutputContentItem) -> CellOutputItem {
match item {
FunctionCallOutputContentItem::InputText { text } => CellOutputItem::Text { text },
FunctionCallOutputContentItem::InputImage { image_url, detail } => CellOutputItem::Image {
image_url,
detail: detail.map(|detail| match detail {
ImageDetail::Auto => CellImageDetail::Auto,
ImageDetail::Low => CellImageDetail::Low,
ImageDetail::High => CellImageDetail::High,
ImageDetail::Original => CellImageDetail::Original,
}),
},
FunctionCallOutputContentItem::InputAudio { audio_url } => {
CellOutputItem::Audio { audio_url }
}
}
}
@@ -0,0 +1,606 @@
mod callbacks;
mod conversions;
mod types;
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use serde_json::Value as JsonValue;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use self::callbacks::CallbackCompletion;
use self::callbacks::finish_callbacks;
use self::callbacks::report_task_result;
use self::callbacks::spawn_notification;
use self::callbacks::spawn_tool;
use self::conversions::cell_tool_kind;
use self::conversions::output_item;
use self::conversions::runtime_request;
use self::types::CellCommand;
pub(crate) use self::types::CellError;
pub(crate) use self::types::CellEventFuture;
pub(crate) use self::types::CellHandle;
pub(crate) use self::types::CellHost;
pub(crate) use self::types::CellState;
pub(crate) use self::types::CellToolCall;
pub(crate) use self::types::CompletionCommit;
use self::types::CompletionDelivery;
use self::types::ObservationDelivery;
use crate::TaskFailureHandler;
use crate::runtime::PendingRuntimeMode;
use crate::runtime::RuntimeCommand;
use crate::runtime::RuntimeControlCommand;
use crate::runtime::RuntimeEvent;
use crate::runtime::spawn_runtime;
use crate::session_runtime::CellEvent;
use crate::session_runtime::CreateCellRequest as CellRequest;
use crate::session_runtime::ObserveMode;
use crate::session_runtime::OutputItem;
use crate::session_runtime::ToolName as CellToolName;
pub(crate) struct CellActor;
impl CellActor {
pub(crate) fn prepare<H: CellHost>(
request: CellRequest,
stored_values: HashMap<String, JsonValue>,
host: Arc<H>,
initial_observe_mode: ObserveMode,
cell_state: Arc<CellState>,
task_failure_handler: Option<TaskFailureHandler>,
) -> Result<
(
CellHandle,
CellEventFuture,
impl Future<Output = ()> + Send + 'static,
),
String,
> {
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (command_tx, command_rx) = mpsc::unbounded_channel();
let (initial_response_tx, initial_response_rx) = oneshot::channel();
let (runtime_tx, runtime_control_tx, runtime_terminate_handle) = spawn_runtime(
stored_values,
runtime_request(request),
event_tx,
PendingRuntimeMode::PauseUntilResumed,
task_failure_handler.clone(),
)?;
let handle = CellHandle::new(command_tx, Arc::clone(&cell_state));
let task = run_cell(
host,
CellContext {
runtime_tx,
runtime_control_tx,
runtime_terminate_handle,
cell_state,
},
event_rx,
command_rx,
Observer {
mode: initial_observe_mode,
response_tx: initial_response_tx,
},
task_failure_handler,
);
let initial_response =
Box::pin(async move { initial_response_rx.await.unwrap_or(Err(CellError::Closed)) });
Ok((handle, initial_response, task))
}
}
struct CellContext {
runtime_tx: std::sync::mpsc::Sender<RuntimeCommand>,
runtime_control_tx: std::sync::mpsc::Sender<RuntimeControlCommand>,
runtime_terminate_handle: v8::IsolateHandle,
cell_state: Arc<CellState>,
}
struct Observer {
mode: ObserveMode,
response_tx: oneshot::Sender<Result<CellEvent, CellError>>,
}
async fn run_cell<H: CellHost>(
host: Arc<H>,
context: CellContext,
mut event_rx: mpsc::UnboundedReceiver<RuntimeEvent>,
command_rx: mpsc::UnboundedReceiver<CellCommand>,
initial_observer: Observer,
task_failure_handler: Option<TaskFailureHandler>,
) {
let CellContext {
runtime_tx,
runtime_control_tx,
runtime_terminate_handle,
cell_state,
} = context;
let cancellation_token = cell_state.cancellation_token();
let callback_cancellation_token = cancellation_token.child_token();
let mut content_items = Vec::new();
let mut pending_tool_call_ids = Vec::new();
let mut pending_frontier_ready = false;
let mut observer = Some(initial_observer);
let mut termination = false;
let mut runtime_closed = false;
let mut runtime_paused = false;
let mut runtime_failure_reported = false;
let mut yield_timer: Option<std::pin::Pin<Box<tokio::time::Sleep>>> = None;
let mut notification_tasks = JoinSet::new();
let mut tool_tasks = JoinSet::new();
let mut command_rx = Some(command_rx);
loop {
let yield_deadline_elapsed = yield_timer
.as_ref()
.is_some_and(|yield_timer| yield_timer.deadline() <= tokio::time::Instant::now());
tokio::select! {
biased;
_ = cancellation_token.cancelled(), if !termination => {
termination = true;
yield_timer = None;
drop(command_rx.take());
begin_termination(
&runtime_tx,
&runtime_control_tx,
&runtime_terminate_handle,
&cancellation_token,
);
if runtime_closed {
finish_callbacks(
&callback_cancellation_token,
&mut notification_tasks,
&mut tool_tasks,
CallbackCompletion::Cancel,
task_failure_handler.as_ref(),
).await;
finish_termination(
&cell_state,
observer.take().map(|observer| observer.response_tx),
CellEvent::Terminated {
content_items: std::mem::take(&mut content_items),
},
);
break;
}
}
maybe_command = async {
match command_rx.as_mut() {
Some(command_rx) => command_rx.recv().await,
None => std::future::pending::<Option<CellCommand>>().await,
}
} => {
let Some(CellCommand::Observe { mode, response_tx }) = maybe_command else {
cancellation_token.cancel();
continue;
};
if response_tx.is_closed() {
continue;
}
let response_tx = match cell_state.route_observation(mode, response_tx) {
ObservationDelivery::Running(response_tx) => response_tx,
ObservationDelivery::Delivered => break,
ObservationDelivery::Buffered | ObservationDelivery::Closed => continue,
};
if observer
.as_ref()
.is_some_and(|observer| observer.response_tx.is_closed())
{
observer = None;
yield_timer = None;
}
if observer.is_some() || termination {
let _ = response_tx.send(Err(CellError::Busy));
continue;
}
if matches!(mode, ObserveMode::PendingFrontier) && pending_frontier_ready {
pending_frontier_ready = false;
match send_cell_event(
response_tx,
CellEvent::Pending {
content_items: std::mem::take(&mut content_items),
pending_tool_call_ids: std::mem::take(&mut pending_tool_call_ids),
},
) {
Ok(()) => {}
Err(CellEvent::Pending {
content_items: undelivered_items,
pending_tool_call_ids: undelivered_tool_call_ids,
}) => {
content_items = undelivered_items;
pending_tool_call_ids = undelivered_tool_call_ids;
pending_frontier_ready = true;
}
Err(event) => {
panic!("pending delivery returned an unexpected event: {event:?}")
}
}
continue;
}
observer = Some(Observer { mode, response_tx });
yield_timer = observer.as_ref().and_then(observer_timer);
if runtime_paused && matches!(mode, ObserveMode::YieldAfter(_)) {
pending_frontier_ready = false;
pending_tool_call_ids.clear();
}
resume_for_observation(
mode,
&mut runtime_paused,
&runtime_tx,
&runtime_control_tx,
);
}
_ = async {
if let Some(yield_timer) = yield_timer.as_mut() {
yield_timer.await;
} else {
std::future::pending::<()>().await;
}
} => {
yield_timer = None;
restore_undelivered_yield(
send_observer_event(
observer.take(),
CellEvent::Yielded {
content_items: std::mem::take(&mut content_items),
},
),
&mut content_items,
);
}
maybe_event = async {
if runtime_closed {
std::future::pending::<Option<RuntimeEvent>>().await
} else {
event_rx.recv().await
}
}, if !yield_deadline_elapsed => {
let Some(event) = maybe_event else {
runtime_closed = true;
if termination || cancellation_token.is_cancelled() {
finish_callbacks(
&callback_cancellation_token,
&mut notification_tasks,
&mut tool_tasks,
CallbackCompletion::Cancel,
task_failure_handler.as_ref(),
).await;
finish_termination(
&cell_state,
observer.take().map(|observer| observer.response_tx),
CellEvent::Terminated {
content_items: std::mem::take(&mut content_items),
},
);
break;
}
if !runtime_failure_reported
&& let Some(task_failure_handler) = &task_failure_handler
{
runtime_failure_reported = true;
task_failure_handler(
"code-mode V8 runtime thread ended unexpectedly".to_string(),
);
}
finish_callbacks(
&callback_cancellation_token,
&mut notification_tasks,
&mut tool_tasks,
CallbackCompletion::DrainNotifications,
task_failure_handler.as_ref(),
)
.await;
let event = CellEvent::Completed {
content_items: std::mem::take(&mut content_items),
error_text: Some("exec runtime ended unexpectedly".to_string()),
};
let rejected_event = match host
.commit_completion(
HashMap::new(),
event,
/*pending_initial_yield_items*/ None,
Arc::clone(&cell_state),
)
.await
{
CompletionCommit::Committed => None,
CompletionCommit::Rejected(event) => Some(event),
};
match cell_state.deliver_completion(
observer.take().map(|observer| observer.response_tx),
) {
CompletionDelivery::Delivered => break,
CompletionDelivery::Buffered => {}
CompletionDelivery::Rejected(response_tx) => {
finish_termination(
&cell_state,
response_tx,
CellEvent::Terminated {
content_items: rejected_completion_content(rejected_event),
},
);
break;
}
}
continue;
};
match event {
RuntimeEvent::Started => {
yield_timer = observer.as_ref().and_then(observer_timer);
}
RuntimeEvent::Pending => {
runtime_paused = true;
if matches!(
observer.as_ref().map(|observer| observer.mode),
Some(ObserveMode::PendingFrontier)
) {
yield_timer = None;
pending_frontier_ready = false;
match send_observer_event(
observer.take(),
CellEvent::Pending {
content_items: std::mem::take(&mut content_items),
pending_tool_call_ids: std::mem::take(
&mut pending_tool_call_ids,
),
},
) {
Ok(()) => {}
Err(CellEvent::Pending {
content_items: undelivered_items,
pending_tool_call_ids: undelivered_tool_call_ids,
}) => {
content_items = undelivered_items;
pending_tool_call_ids = undelivered_tool_call_ids;
pending_frontier_ready = true;
}
Err(event) => {
panic!("pending delivery returned an unexpected event: {event:?}")
}
}
} else {
pending_tool_call_ids.clear();
let _ = runtime_control_tx.send(RuntimeControlCommand::Continue);
runtime_paused = false;
}
}
RuntimeEvent::ContentItem(item) => content_items.push(output_item(item)),
RuntimeEvent::YieldRequested => {
let yield_observer = matches!(
observer.as_ref().map(|observer| observer.mode),
Some(ObserveMode::YieldAfter(_))
);
if yield_observer {
yield_timer = None;
restore_undelivered_yield(
send_observer_event(
observer.take(),
CellEvent::Yielded {
content_items: std::mem::take(&mut content_items),
},
),
&mut content_items,
);
}
}
RuntimeEvent::Notify { call_id, text } => {
spawn_notification(
&mut notification_tasks,
Arc::clone(&host),
call_id,
text,
callback_cancellation_token.child_token(),
task_failure_handler.clone(),
);
}
RuntimeEvent::ToolCall { id, name, kind, input } => {
pending_tool_call_ids.push(id.clone());
spawn_tool(
&mut tool_tasks,
Arc::clone(&host),
CellToolCall {
id,
name: CellToolName {
name: name.name,
namespace: name.namespace,
},
kind: cell_tool_kind(kind),
input,
},
runtime_tx.clone(),
callback_cancellation_token.child_token(),
task_failure_handler.clone(),
);
}
RuntimeEvent::Result { stored_value_writes, error_text } => {
runtime_closed = true;
yield_timer = None;
if termination || cancellation_token.is_cancelled() {
finish_callbacks(
&callback_cancellation_token,
&mut notification_tasks,
&mut tool_tasks,
CallbackCompletion::Cancel,
task_failure_handler.as_ref(),
).await;
finish_termination(
&cell_state,
observer.take().map(|observer| observer.response_tx),
CellEvent::Terminated {
content_items: std::mem::take(&mut content_items),
},
);
break;
}
finish_callbacks(
&callback_cancellation_token,
&mut notification_tasks,
&mut tool_tasks,
CallbackCompletion::DrainNotifications,
task_failure_handler.as_ref(),
)
.await;
let event = CellEvent::Completed {
content_items: std::mem::take(&mut content_items),
error_text,
};
let rejected_event = match host
.commit_completion(
stored_value_writes,
event,
/*pending_initial_yield_items*/ None,
Arc::clone(&cell_state),
)
.await
{
CompletionCommit::Committed => None,
CompletionCommit::Rejected(event) => Some(event),
};
match cell_state.deliver_completion(
observer.take().map(|observer| observer.response_tx),
) {
CompletionDelivery::Delivered => break,
CompletionDelivery::Buffered => {}
CompletionDelivery::Rejected(response_tx) => {
finish_termination(
&cell_state,
response_tx,
CellEvent::Terminated {
content_items: rejected_completion_content(rejected_event),
},
);
break;
}
}
}
RuntimeEvent::ThreadPanicked => {
runtime_failure_reported = true;
}
}
}
task_result = notification_tasks.join_next(), if !notification_tasks.is_empty() => {
report_task_result(
task_result,
"notification",
task_failure_handler.as_ref(),
);
}
task_result = tool_tasks.join_next(), if !tool_tasks.is_empty() => {
report_task_result(task_result, "tool", task_failure_handler.as_ref());
}
}
}
// Reject requests that arrive while asynchronous terminal cleanup runs.
cell_state.tombstone();
drop(command_rx.take());
begin_termination(
&runtime_tx,
&runtime_control_tx,
&runtime_terminate_handle,
&cancellation_token,
);
finish_callbacks(
&callback_cancellation_token,
&mut notification_tasks,
&mut tool_tasks,
CallbackCompletion::Cancel,
task_failure_handler.as_ref(),
)
.await;
host.closed().await;
}
fn send_observer_event(observer: Option<Observer>, event: CellEvent) -> Result<(), CellEvent> {
let Some(observer) = observer else {
return Err(event);
};
send_cell_event(observer.response_tx, event)
}
fn send_cell_event(
response_tx: oneshot::Sender<Result<CellEvent, CellError>>,
event: CellEvent,
) -> Result<(), CellEvent> {
match response_tx.send(Ok(event)) {
Ok(()) => Ok(()),
Err(Ok(event)) => Err(event),
Err(Err(error)) => panic!("cell event delivery returned an actor error: {error:?}"),
}
}
fn restore_undelivered_yield(delivery: Result<(), CellEvent>, content_items: &mut Vec<OutputItem>) {
match delivery {
Ok(()) => {}
Err(CellEvent::Yielded {
content_items: mut undelivered_items,
}) => {
undelivered_items.append(content_items);
*content_items = undelivered_items;
}
Err(event) => panic!("yield delivery returned an unexpected event: {event:?}"),
}
}
fn rejected_completion_content(event: Option<CellEvent>) -> Vec<OutputItem> {
match event {
Some(CellEvent::Completed { content_items, .. }) => content_items,
None => Vec::new(),
Some(event) => panic!("completion commit rejected an unexpected event: {event:?}"),
}
}
fn finish_termination(
cell_state: &CellState,
observer_tx: Option<oneshot::Sender<Result<CellEvent, CellError>>>,
event: CellEvent,
) {
if let Some(event) = cell_state.finish_termination(event)
&& let Some(observer_tx) = observer_tx
{
let _ = observer_tx.send(Ok(event));
}
}
fn observer_timer(observer: &Observer) -> Option<std::pin::Pin<Box<tokio::time::Sleep>>> {
match observer.mode {
ObserveMode::YieldAfter(duration) => Some(Box::pin(tokio::time::sleep(duration))),
ObserveMode::PendingFrontier => None,
}
}
fn resume_for_observation(
mode: ObserveMode,
runtime_paused: &mut bool,
runtime_tx: &std::sync::mpsc::Sender<RuntimeCommand>,
runtime_control_tx: &std::sync::mpsc::Sender<RuntimeControlCommand>,
) {
if *runtime_paused {
let control = match mode {
ObserveMode::YieldAfter(_) => RuntimeControlCommand::Continue,
ObserveMode::PendingFrontier => RuntimeControlCommand::Resume,
};
let _ = runtime_control_tx.send(control);
*runtime_paused = false;
} else if matches!(mode, ObserveMode::PendingFrontier) {
let _ = runtime_tx.send(RuntimeCommand::ObservePendingFrontier);
}
}
fn begin_termination(
runtime_tx: &std::sync::mpsc::Sender<RuntimeCommand>,
runtime_control_tx: &std::sync::mpsc::Sender<RuntimeControlCommand>,
runtime_terminate_handle: &v8::IsolateHandle,
cancellation_token: &CancellationToken,
) {
cancellation_token.cancel();
let _ = runtime_tx.send(RuntimeCommand::Terminate);
let _ = runtime_control_tx.send(RuntimeControlCommand::Terminate);
let _ = runtime_terminate_handle.terminate_execution();
}
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
@@ -0,0 +1,692 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::mpsc as std_mpsc;
use std::time::Duration;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::FunctionCallOutputContentItem;
use pretty_assertions::assert_eq;
use serde_json::Value as JsonValue;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use super::*;
use crate::session_runtime::OutputItem;
struct TestHost;
#[derive(Default)]
struct RecordingHost {
notified: AtomicBool,
}
impl CellHost for TestHost {
async fn invoke_tool(
&self,
_invocation: CellToolCall,
_cancellation_token: CancellationToken,
) -> Result<JsonValue, String> {
Err("unexpected tool call".to_string())
}
async fn notify(
&self,
_call_id: String,
_text: String,
_cancellation_token: CancellationToken,
) -> Result<(), String> {
Ok(())
}
async fn commit_completion(
&self,
_stored_value_writes: HashMap<String, JsonValue>,
event: CellEvent,
pending_initial_yield_items: Option<Vec<OutputItem>>,
cell_state: Arc<CellState>,
) -> CompletionCommit {
cell_state.commit_completion(event, pending_initial_yield_items, || {})
}
async fn closed(&self) {}
}
impl CellHost for RecordingHost {
async fn invoke_tool(
&self,
_invocation: CellToolCall,
_cancellation_token: CancellationToken,
) -> Result<JsonValue, String> {
Err("unexpected tool call".to_string())
}
async fn notify(
&self,
_call_id: String,
_text: String,
_cancellation_token: CancellationToken,
) -> Result<(), String> {
self.notified.store(true, Ordering::Release);
Ok(())
}
async fn commit_completion(
&self,
_stored_value_writes: HashMap<String, JsonValue>,
event: CellEvent,
pending_initial_yield_items: Option<Vec<OutputItem>>,
cell_state: Arc<CellState>,
) -> CompletionCommit {
cell_state.commit_completion(event, pending_initial_yield_items, || {})
}
async fn closed(&self) {}
}
struct CellActorHarness {
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
handle: CellHandle,
initial_event_rx: oneshot::Receiver<Result<CellEvent, CellError>>,
task: tokio::task::JoinHandle<()>,
runtime_control_rx: std_mpsc::Receiver<RuntimeControlCommand>,
_runtime_event_rx: mpsc::UnboundedReceiver<RuntimeEvent>,
}
fn spawn_cell_actor_harness(initial_observe_mode: ObserveMode) -> CellActorHarness {
spawn_cell_actor_harness_with_host(initial_observe_mode, Arc::new(TestHost))
}
fn spawn_cell_actor_harness_with_host<H: CellHost>(
initial_observe_mode: ObserveMode,
host: Arc<H>,
) -> CellActorHarness {
spawn_cell_actor_harness_with_host_and_failure_handler(
initial_observe_mode,
host,
/*task_failure_handler*/ None,
)
}
fn spawn_cell_actor_harness_with_host_and_failure_handler<H: CellHost>(
initial_observe_mode: ObserveMode,
host: Arc<H>,
task_failure_handler: Option<TaskFailureHandler>,
) -> CellActorHarness {
let (event_tx, event_rx) = mpsc::unbounded_channel();
let (command_tx, command_rx) = mpsc::unbounded_channel();
let (initial_event_tx, initial_event_rx) = oneshot::channel();
let (runtime_event_tx, runtime_event_rx) = mpsc::unbounded_channel();
let (runtime_tx, _runtime_control_tx, runtime_terminate_handle) = spawn_runtime(
HashMap::new(),
ExecuteRequest {
tool_call_id: "call-1".to_string(),
enabled_tools: Vec::new(),
source: "await new Promise(() => {});".to_string(),
yield_time_ms: None,
max_output_tokens: None,
},
runtime_event_tx,
PendingRuntimeMode::PauseUntilResumed,
/*task_failure_handler*/ None,
)
.unwrap();
let (runtime_control_tx, runtime_control_rx) = std_mpsc::channel();
let cell_state = Arc::new(CellState::new(CancellationToken::new()));
let handle = CellHandle::new(command_tx, Arc::clone(&cell_state));
let task = tokio::spawn(run_cell(
host,
CellContext {
runtime_tx,
runtime_control_tx,
runtime_terminate_handle,
cell_state,
},
event_rx,
command_rx,
Observer {
mode: initial_observe_mode,
response_tx: initial_event_tx,
},
task_failure_handler,
));
CellActorHarness {
event_tx,
handle,
initial_event_rx,
task,
runtime_control_rx,
_runtime_event_rx: runtime_event_rx,
}
}
#[tokio::test]
async fn unexpected_runtime_thread_exit_is_reported_to_the_session_owner() {
let (failure_tx, mut failure_rx) = mpsc::unbounded_channel();
let harness = spawn_cell_actor_harness_with_host_and_failure_handler(
ObserveMode::YieldAfter(Duration::from_secs(60)),
Arc::new(TestHost),
Some(Arc::new(move |reason| {
let _ = failure_tx.send(reason);
})),
);
drop(harness.event_tx);
assert_eq!(
tokio::time::timeout(Duration::from_secs(1), failure_rx.recv())
.await
.expect("runtime failure timeout")
.expect("runtime failure"),
"code-mode V8 runtime thread ended unexpectedly"
);
assert!(
harness
.initial_event_rx
.await
.expect("initial event")
.is_ok()
);
harness.task.await.expect("cell task");
}
#[tokio::test]
async fn runtime_thread_panic_remains_a_cell_error_without_owner_supervision() {
let harness = spawn_cell_actor_harness(ObserveMode::YieldAfter(Duration::from_secs(60)));
harness
.event_tx
.send(RuntimeEvent::ThreadPanicked)
.expect("runtime panic event");
drop(harness.event_tx);
assert_eq!(
harness.initial_event_rx.await.expect("initial event"),
Ok(CellEvent::Completed {
content_items: Vec::new(),
error_text: Some("exec runtime ended unexpectedly".to_string()),
})
);
harness.task.await.expect("cell task");
}
async fn wait_for_notification(host: &RecordingHost) {
tokio::time::timeout(Duration::from_secs(1), async {
while !host.notified.load(Ordering::Acquire) {
tokio::task::yield_now().await;
}
})
.await
.expect("notification barrier timed out");
}
#[tokio::test]
async fn yield_timer_preempts_buffered_runtime_output() {
let harness = spawn_cell_actor_harness(ObserveMode::YieldAfter(Duration::ZERO));
harness.event_tx.send(RuntimeEvent::Started).unwrap();
harness
.event_tx
.send(RuntimeEvent::ContentItem(
FunctionCallOutputContentItem::InputText {
text: "queued output".to_string(),
},
))
.unwrap();
assert_eq!(
harness.initial_event_rx.await.unwrap(),
Ok(CellEvent::Yielded {
content_items: Vec::new(),
})
);
let termination = harness.handle.terminate();
drop(harness.event_tx);
assert_eq!(
termination.await,
Ok(CellEvent::Terminated {
content_items: vec![OutputItem::Text {
text: "queued output".to_string(),
}],
})
);
harness.task.await.unwrap();
}
#[tokio::test]
async fn queued_termination_preempts_unobserved_runtime_completion() {
let harness = spawn_cell_actor_harness(ObserveMode::YieldAfter(Duration::from_secs(60)));
harness
.event_tx
.send(RuntimeEvent::Result {
stored_value_writes: HashMap::new(),
error_text: None,
})
.unwrap();
let termination = harness.handle.terminate();
let terminated = Ok(CellEvent::Terminated {
content_items: Vec::new(),
});
assert_eq!(termination.await, terminated.clone());
assert_eq!(harness.initial_event_rx.await.unwrap(), terminated);
harness.task.await.unwrap();
}
#[tokio::test]
async fn observation_dropped_before_dequeue_does_not_consume_output() {
let host = Arc::new(RecordingHost::default());
let harness = spawn_cell_actor_harness_with_host(
ObserveMode::YieldAfter(Duration::from_secs(60)),
Arc::clone(&host),
);
harness.event_tx.send(RuntimeEvent::YieldRequested).unwrap();
assert!(harness.initial_event_rx.await.unwrap().is_ok());
drop(
harness
.handle
.observe(ObserveMode::YieldAfter(Duration::from_secs(60))),
);
harness
.event_tx
.send(RuntimeEvent::ContentItem(
FunctionCallOutputContentItem::InputText {
text: "survives pre-dequeue cancellation".to_string(),
},
))
.unwrap();
harness.event_tx.send(RuntimeEvent::YieldRequested).unwrap();
harness
.event_tx
.send(RuntimeEvent::Notify {
call_id: "after-dropped-command".to_string(),
text: "barrier".to_string(),
})
.unwrap();
wait_for_notification(&host).await;
assert_eq!(
harness
.handle
.observe(ObserveMode::YieldAfter(Duration::ZERO))
.await,
Ok(CellEvent::Yielded {
content_items: vec![OutputItem::Text {
text: "survives pre-dequeue cancellation".to_string(),
}],
})
);
let termination = harness.handle.terminate();
drop(harness.event_tx);
assert_eq!(
termination.await,
Ok(CellEvent::Terminated {
content_items: Vec::new(),
})
);
harness.task.await.unwrap();
}
#[tokio::test]
async fn dropped_yield_observer_preserves_output_for_the_next_observation() {
let host = Arc::new(RecordingHost::default());
let harness = spawn_cell_actor_harness_with_host(
ObserveMode::YieldAfter(Duration::from_secs(60)),
Arc::clone(&host),
);
harness.event_tx.send(RuntimeEvent::YieldRequested).unwrap();
assert!(harness.initial_event_rx.await.unwrap().is_ok());
let dropped_observation = harness
.handle
.observe(ObserveMode::YieldAfter(Duration::from_secs(60)));
assert_eq!(
harness
.handle
.observe(ObserveMode::YieldAfter(Duration::ZERO))
.await,
Err(CellError::Busy)
);
drop(dropped_observation);
harness
.event_tx
.send(RuntimeEvent::ContentItem(
FunctionCallOutputContentItem::InputText {
text: "survives active cancellation".to_string(),
},
))
.unwrap();
harness.event_tx.send(RuntimeEvent::YieldRequested).unwrap();
harness
.event_tx
.send(RuntimeEvent::Notify {
call_id: "after-dropped-observer".to_string(),
text: "barrier".to_string(),
})
.unwrap();
wait_for_notification(&host).await;
assert_eq!(
harness
.handle
.observe(ObserveMode::YieldAfter(Duration::ZERO))
.await,
Ok(CellEvent::Yielded {
content_items: vec![OutputItem::Text {
text: "survives active cancellation".to_string(),
}],
})
);
let termination = harness.handle.terminate();
drop(harness.event_tx);
assert_eq!(
termination.await,
Ok(CellEvent::Terminated {
content_items: Vec::new(),
})
);
harness.task.await.unwrap();
}
#[tokio::test]
async fn dropped_pending_observer_preserves_the_frontier_for_the_next_observation() {
let host = Arc::new(RecordingHost::default());
let harness = spawn_cell_actor_harness_with_host(
ObserveMode::YieldAfter(Duration::from_secs(60)),
Arc::clone(&host),
);
harness.event_tx.send(RuntimeEvent::YieldRequested).unwrap();
assert!(harness.initial_event_rx.await.unwrap().is_ok());
let dropped_observation = harness.handle.observe(ObserveMode::PendingFrontier);
assert_eq!(
harness.handle.observe(ObserveMode::PendingFrontier).await,
Err(CellError::Busy)
);
drop(dropped_observation);
harness
.event_tx
.send(RuntimeEvent::ToolCall {
id: "tool-1".to_string(),
name: codex_protocol::ToolName {
name: "echo".to_string(),
namespace: None,
},
kind: codex_code_mode_protocol::CodeModeToolKind::Function,
input: Some(serde_json::json!({})),
})
.unwrap();
harness.event_tx.send(RuntimeEvent::Pending).unwrap();
harness
.event_tx
.send(RuntimeEvent::Notify {
call_id: "after-dropped-pending".to_string(),
text: "barrier".to_string(),
})
.unwrap();
wait_for_notification(&host).await;
assert_eq!(
harness.handle.observe(ObserveMode::PendingFrontier).await,
Ok(CellEvent::Pending {
content_items: Vec::new(),
pending_tool_call_ids: vec!["tool-1".to_string()],
})
);
assert!(matches!(
harness.runtime_control_rx.try_recv(),
Err(std_mpsc::TryRecvError::Empty)
));
let termination = harness.handle.terminate();
drop(harness.event_tx);
assert_eq!(
termination.await,
Ok(CellEvent::Terminated {
content_items: Vec::new(),
})
);
harness.task.await.unwrap();
}
#[tokio::test]
async fn only_the_first_termination_claims_a_buffered_completion() {
let cell_state = CellState::new(CancellationToken::new());
let completion = CellEvent::Completed {
content_items: Vec::new(),
error_text: None,
};
assert_eq!(
cell_state.commit_completion(
completion.clone(),
/*pending_initial_yield_items*/ None,
|| {}
),
CompletionCommit::Committed
);
assert!(matches!(
cell_state.deliver_completion(/*response_tx*/ None),
CompletionDelivery::Buffered
));
let first_termination = cell_state.request_termination();
assert_eq!(
cell_state.request_termination().await,
Err(CellError::AlreadyTerminating)
);
assert_eq!(first_termination.await, Ok(completion.clone()));
assert_eq!(
cell_state.finish_termination(CellEvent::Terminated {
content_items: Vec::new(),
}),
Some(completion)
);
}
#[tokio::test]
async fn termination_claim_prevents_stored_value_commit() {
let cell_state = CellState::new(CancellationToken::new());
let termination = cell_state.request_termination();
let mut commit_ran = false;
let completion = CellEvent::Completed {
content_items: Vec::new(),
error_text: None,
};
assert_eq!(
cell_state.commit_completion(
completion.clone(),
/*pending_initial_yield_items*/ None,
|| commit_ran = true
),
CompletionCommit::Rejected(completion)
);
assert!(!commit_ran);
let terminated = CellEvent::Terminated {
content_items: Vec::new(),
};
assert_eq!(
cell_state.finish_termination(terminated.clone()),
Some(terminated.clone())
);
assert_eq!(termination.await, Ok(terminated));
}
#[test]
fn failed_completion_delivery_rebuffers_the_event() {
let cell_state = CellState::new(CancellationToken::new());
let event = CellEvent::Completed {
content_items: Vec::new(),
error_text: None,
};
assert_eq!(
cell_state.commit_completion(
event.clone(),
/*pending_initial_yield_items*/ None,
|| {}
),
CompletionCommit::Committed
);
let (response_tx, response_rx) = oneshot::channel();
drop(response_rx);
assert!(matches!(
cell_state.deliver_completion(Some(response_tx)),
CompletionDelivery::Buffered
));
assert!(cell_state.accepting_observations());
let (response_tx, mut response_rx) = oneshot::channel();
assert!(matches!(
cell_state.route_observation(ObserveMode::YieldAfter(Duration::ZERO), response_tx),
ObservationDelivery::Delivered
));
assert_eq!(response_rx.try_recv(), Ok(Ok(event)));
}
#[test]
fn buffered_initial_yield_precedes_buffered_completion_for_yield_observer() {
let cell_state = CellState::new(CancellationToken::new());
let completion = CellEvent::Completed {
content_items: vec![OutputItem::Text {
text: "after".to_string(),
}],
error_text: None,
};
assert_eq!(
cell_state.commit_completion(
completion.clone(),
Some(vec![OutputItem::Text {
text: "before".to_string(),
}]),
|| {}
),
CompletionCommit::Committed
);
assert!(matches!(
cell_state.deliver_completion(/*response_tx*/ None),
CompletionDelivery::Buffered
));
let (response_tx, mut response_rx) = oneshot::channel();
assert!(matches!(
cell_state.route_observation(ObserveMode::YieldAfter(Duration::ZERO), response_tx),
ObservationDelivery::Buffered
));
assert_eq!(
response_rx.try_recv(),
Ok(Ok(CellEvent::Yielded {
content_items: vec![OutputItem::Text {
text: "before".to_string(),
}],
}))
);
let (response_tx, mut response_rx) = oneshot::channel();
assert!(matches!(
cell_state.route_observation(ObserveMode::YieldAfter(Duration::ZERO), response_tx),
ObservationDelivery::Delivered
));
assert_eq!(response_rx.try_recv(), Ok(Ok(completion)));
}
#[test]
fn pending_observer_merges_initial_yield_and_completion_output() {
let cell_state = CellState::new(CancellationToken::new());
assert_eq!(
cell_state.commit_completion(
CellEvent::Completed {
content_items: vec![OutputItem::Text {
text: "after".to_string(),
}],
error_text: None,
},
Some(vec![OutputItem::Text {
text: "before".to_string(),
}]),
|| {}
),
CompletionCommit::Committed
);
assert!(matches!(
cell_state.deliver_completion(/*response_tx*/ None),
CompletionDelivery::Buffered
));
let (response_tx, mut response_rx) = oneshot::channel();
assert!(matches!(
cell_state.route_observation(ObserveMode::PendingFrontier, response_tx),
ObservationDelivery::Delivered
));
assert_eq!(
response_rx.try_recv(),
Ok(Ok(CellEvent::Completed {
content_items: vec![
OutputItem::Text {
text: "before".to_string(),
},
OutputItem::Text {
text: "after".to_string(),
},
],
error_text: None,
}))
);
}
#[test]
fn dropped_pending_observation_preserves_the_initial_yield_boundary() {
let cell_state = CellState::new(CancellationToken::new());
let completion = CellEvent::Completed {
content_items: vec![OutputItem::Text {
text: "after".to_string(),
}],
error_text: None,
};
assert_eq!(
cell_state.commit_completion(
completion.clone(),
Some(vec![OutputItem::Text {
text: "before".to_string(),
}]),
|| {}
),
CompletionCommit::Committed
);
assert!(matches!(
cell_state.deliver_completion(/*response_tx*/ None),
CompletionDelivery::Buffered
));
let (response_tx, response_rx) = oneshot::channel();
drop(response_rx);
assert!(matches!(
cell_state.route_observation(ObserveMode::PendingFrontier, response_tx),
ObservationDelivery::Buffered
));
let (response_tx, mut response_rx) = oneshot::channel();
assert!(matches!(
cell_state.route_observation(ObserveMode::YieldAfter(Duration::ZERO), response_tx),
ObservationDelivery::Buffered
));
assert_eq!(
response_rx.try_recv(),
Ok(Ok(CellEvent::Yielded {
content_items: vec![OutputItem::Text {
text: "before".to_string(),
}],
}))
);
let (response_tx, mut response_rx) = oneshot::channel();
assert!(matches!(
cell_state.route_observation(ObserveMode::YieldAfter(Duration::ZERO), response_tx),
ObservationDelivery::Delivered
));
assert_eq!(response_rx.try_recv(), Ok(Ok(completion)));
}
@@ -0,0 +1,444 @@
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::Mutex;
use serde_json::Value as JsonValue;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use crate::session_runtime::CellEvent;
use crate::session_runtime::ObserveMode;
use crate::session_runtime::OutputItem;
use crate::session_runtime::ToolKind;
use crate::session_runtime::ToolName;
pub(crate) type CellEventFuture =
Pin<Box<dyn Future<Output = Result<CellEvent, CellError>> + Send + 'static>>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CellError {
Busy,
AlreadyTerminating,
Closed,
}
pub(crate) struct CellToolCall {
pub(crate) id: String,
pub(crate) name: ToolName,
pub(crate) kind: ToolKind,
pub(crate) input: Option<JsonValue>,
}
/// Connects a cell actor to session-owned callbacks and stored values.
///
/// Implementations should forward callback cancellation to downstream work.
/// Implementations must not return from `closed` until the session can no longer
/// route requests to the cell.
pub(crate) trait CellHost: Send + Sync + 'static {
fn invoke_tool(
&self,
invocation: CellToolCall,
cancellation_token: CancellationToken,
) -> impl Future<Output = Result<JsonValue, String>> + Send;
fn notify(
&self,
call_id: String,
text: String,
cancellation_token: CancellationToken,
) -> impl Future<Output = Result<(), String>> + Send;
fn commit_completion(
&self,
stored_value_writes: HashMap<String, JsonValue>,
event: CellEvent,
pending_initial_yield_items: Option<Vec<OutputItem>>,
cell_state: Arc<CellState>,
) -> impl Future<Output = CompletionCommit> + Send;
fn closed(&self) -> impl Future<Output = ()> + Send;
}
#[derive(Clone)]
pub(crate) struct CellHandle {
command_tx: mpsc::UnboundedSender<CellCommand>,
state: Arc<CellState>,
}
impl CellHandle {
pub(super) fn new(
command_tx: mpsc::UnboundedSender<CellCommand>,
state: Arc<CellState>,
) -> Self {
Self { command_tx, state }
}
pub(crate) fn observe(&self, mode: ObserveMode) -> CellEventFuture {
if !self.state.accepting_observations() {
return closed_event();
}
let (response_tx, response_rx) = oneshot::channel();
if self
.command_tx
.send(CellCommand::Observe { mode, response_tx })
.is_err()
{
return closed_event();
}
response_event(response_rx)
}
pub(crate) fn terminate(&self) -> CellEventFuture {
self.state.request_termination()
}
}
/// The single linearization point for a cell's terminal outcome.
///
/// The cancellation token is a child of the owning session token. Callback
/// tokens are children of this token, so cancellation flows strictly from the
/// session to the cell and then to its callbacks.
///
/// The mutex is held only for synchronous phase transitions and terminal
/// delivery. Runtime execution, observation waits, and callbacks never run
/// while it is held.
pub(crate) struct CellState {
phase: Mutex<CellPhase>,
cancellation_token: CancellationToken,
}
enum CellPhase {
Running,
Terminating {
response_tx: oneshot::Sender<Result<CellEvent, CellError>>,
},
Completed {
// Set only when `yield_control()` races the create-to-first-observe handoff.
pending_initial_yield_items: Option<Vec<OutputItem>>,
event: CellEvent,
},
CompletionClaimed(CellEvent),
Tombstone,
}
pub(crate) enum CompletionDelivery {
Delivered,
Buffered,
Rejected(Option<oneshot::Sender<Result<CellEvent, CellError>>>),
}
/// Result of atomically publishing a completed cell and its session side effects.
#[derive(Debug, PartialEq)]
pub(crate) enum CompletionCommit {
Committed,
Rejected(CellEvent),
}
pub(crate) enum ObservationDelivery {
Running(oneshot::Sender<Result<CellEvent, CellError>>),
Delivered,
Buffered,
Closed,
}
impl CellState {
pub(crate) fn new(cancellation_token: CancellationToken) -> Self {
Self {
phase: Mutex::new(CellPhase::Running),
cancellation_token,
}
}
pub(crate) fn accepting_observations(&self) -> bool {
let accepting_phase = matches!(
*self
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
CellPhase::Running | CellPhase::Completed { .. }
);
accepting_phase && !self.cancellation_token.is_cancelled()
}
pub(crate) fn request_termination(&self) -> CellEventFuture {
let mut phase = self
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match std::mem::replace(&mut *phase, CellPhase::Tombstone) {
CellPhase::Running => {
let (response_tx, response_rx) = oneshot::channel();
*phase = CellPhase::Terminating { response_tx };
self.cancellation_token.cancel();
response_event(response_rx)
}
CellPhase::Terminating { response_tx } => {
*phase = CellPhase::Terminating { response_tx };
Box::pin(async { Err(CellError::AlreadyTerminating) })
}
CellPhase::Completed {
pending_initial_yield_items,
event,
} => {
let event = prepend_initial_yield(event, pending_initial_yield_items);
*phase = CellPhase::CompletionClaimed(event.clone());
self.cancellation_token.cancel();
ready_event(event)
}
CellPhase::CompletionClaimed(event) => {
*phase = CellPhase::CompletionClaimed(event);
Box::pin(async { Err(CellError::AlreadyTerminating) })
}
CellPhase::Tombstone => closed_event(),
}
}
pub(crate) fn commit_completion(
&self,
event: CellEvent,
pending_initial_yield_items: Option<Vec<OutputItem>>,
commit: impl FnOnce(),
) -> CompletionCommit {
let mut phase = self
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !matches!(*phase, CellPhase::Running) || self.cancellation_token.is_cancelled() {
return CompletionCommit::Rejected(event);
}
commit();
*phase = CellPhase::Completed {
pending_initial_yield_items,
event,
};
CompletionCommit::Committed
}
pub(crate) fn deliver_completion(
&self,
response_tx: Option<oneshot::Sender<Result<CellEvent, CellError>>>,
) -> CompletionDelivery {
let mut phase = self
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (pending_initial_yield_items, event) =
match std::mem::replace(&mut *phase, CellPhase::Tombstone) {
CellPhase::Completed {
pending_initial_yield_items,
event,
} => (pending_initial_yield_items, event),
previous => {
*phase = previous;
return CompletionDelivery::Rejected(response_tx);
}
};
let Some(response_tx) = response_tx else {
*phase = CellPhase::Completed {
pending_initial_yield_items,
event,
};
return CompletionDelivery::Buffered;
};
match response_tx.send(Ok(event)) {
Ok(()) => {
self.cancellation_token.cancel();
CompletionDelivery::Delivered
}
Err(Ok(event)) => {
*phase = CellPhase::Completed {
pending_initial_yield_items,
event,
};
CompletionDelivery::Buffered
}
Err(Err(error)) => {
panic!("completion delivery unexpectedly carried an actor error: {error:?}")
}
}
}
pub(crate) fn route_observation(
&self,
mode: ObserveMode,
response_tx: oneshot::Sender<Result<CellEvent, CellError>>,
) -> ObservationDelivery {
let mut phase = self
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match std::mem::replace(&mut *phase, CellPhase::Tombstone) {
CellPhase::Running => {
*phase = CellPhase::Running;
ObservationDelivery::Running(response_tx)
}
CellPhase::Completed {
pending_initial_yield_items: Some(content_items),
event,
} if matches!(mode, ObserveMode::YieldAfter(_)) => {
match response_tx.send(Ok(CellEvent::Yielded { content_items })) {
Ok(()) => {
*phase = CellPhase::Completed {
pending_initial_yield_items: None,
event,
};
ObservationDelivery::Buffered
}
Err(Ok(CellEvent::Yielded { content_items })) => {
*phase = CellPhase::Completed {
pending_initial_yield_items: Some(content_items),
event,
};
ObservationDelivery::Buffered
}
Err(Ok(event)) => {
panic!("initial yield delivery returned an unexpected event: {event:?}")
}
Err(Err(error)) => {
panic!("initial yield delivery returned an actor error: {error:?}")
}
}
}
CellPhase::Completed {
pending_initial_yield_items,
event,
} => {
let delivered_event =
prepend_initial_yield(event.clone(), pending_initial_yield_items.clone());
match response_tx.send(Ok(delivered_event)) {
Ok(()) => {
self.cancellation_token.cancel();
ObservationDelivery::Delivered
}
Err(Ok(_)) => {
*phase = CellPhase::Completed {
pending_initial_yield_items,
event,
};
ObservationDelivery::Buffered
}
Err(Err(error)) => {
panic!("completion delivery unexpectedly carried an actor error: {error:?}")
}
}
}
CellPhase::Terminating {
response_tx: termination_tx,
} => {
*phase = CellPhase::Terminating {
response_tx: termination_tx,
};
let _ = response_tx.send(Err(CellError::Closed));
ObservationDelivery::Closed
}
CellPhase::CompletionClaimed(event) => {
*phase = CellPhase::CompletionClaimed(event);
let _ = response_tx.send(Err(CellError::Closed));
ObservationDelivery::Closed
}
CellPhase::Tombstone => {
let _ = response_tx.send(Err(CellError::Closed));
ObservationDelivery::Closed
}
}
}
pub(crate) fn finish_termination(&self, event: CellEvent) -> Option<CellEvent> {
let mut phase = self
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let observer_event = match std::mem::replace(&mut *phase, CellPhase::Tombstone) {
CellPhase::Running => Some(event),
CellPhase::Terminating { response_tx } => {
let _ = response_tx.send(Ok(event.clone()));
Some(event)
}
CellPhase::Completed {
pending_initial_yield_items,
event,
} => Some(prepend_initial_yield(event, pending_initial_yield_items)),
CellPhase::CompletionClaimed(completed_event) => Some(completed_event),
CellPhase::Tombstone => None,
};
self.cancellation_token.cancel();
observer_event
}
pub(crate) fn tombstone(&self) {
*self
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = CellPhase::Tombstone;
self.cancellation_token.cancel();
}
pub(crate) fn cancellation_token(&self) -> CancellationToken {
self.cancellation_token.clone()
}
}
fn prepend_initial_yield(
event: CellEvent,
pending_initial_yield_items: Option<Vec<OutputItem>>,
) -> CellEvent {
let Some(mut pending_initial_yield_items) = pending_initial_yield_items else {
return event;
};
match event {
CellEvent::Yielded { mut content_items } => {
pending_initial_yield_items.append(&mut content_items);
CellEvent::Yielded {
content_items: pending_initial_yield_items,
}
}
CellEvent::Pending {
mut content_items,
pending_tool_call_ids,
} => {
pending_initial_yield_items.append(&mut content_items);
CellEvent::Pending {
content_items: pending_initial_yield_items,
pending_tool_call_ids,
}
}
CellEvent::Completed {
mut content_items,
error_text,
} => {
pending_initial_yield_items.append(&mut content_items);
CellEvent::Completed {
content_items: pending_initial_yield_items,
error_text,
}
}
CellEvent::Terminated { mut content_items } => {
pending_initial_yield_items.append(&mut content_items);
CellEvent::Terminated {
content_items: pending_initial_yield_items,
}
}
}
}
pub(super) enum CellCommand {
Observe {
mode: ObserveMode,
response_tx: oneshot::Sender<Result<CellEvent, CellError>>,
},
}
fn response_event(response_rx: oneshot::Receiver<Result<CellEvent, CellError>>) -> CellEventFuture {
Box::pin(async move { response_rx.await.unwrap_or(Err(CellError::Closed)) })
}
fn ready_event(event: CellEvent) -> CellEventFuture {
Box::pin(async move { Ok(event) })
}
fn closed_event() -> CellEventFuture {
Box::pin(async { Err(CellError::Closed) })
}