mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
## What changed Add an optional `preempt` signal to `CodeModeSession::execute` and `CodeModeSession::wait` so callers can end an observation early while the cell continues running and remains available for later waits. Support the signal in the in-process runtime and across both remote transports: `operation/yield` with the negotiated `yield-observation` capability for stdio, and `YieldObservation` for gRPC. Preserve signals received before gRPC observation registration, and retain normal timeout behavior for older hosts that lack support. ## Testing Add runtime and transport tests covering early execute and wait yields followed by successful cell completion, yields before observation registration, and suppression of yield frames for older stdio hosts. GitOrigin-RevId: 3f8049d8f0582be6b8f0832cc03f2d974a0b8f18
351 lines
11 KiB
Rust
351 lines
11 KiB
Rust
use std::sync::Arc;
|
|
use std::sync::PoisonError;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use codex_code_mode::CellId;
|
|
use codex_code_mode::CodeModeNestedToolCall;
|
|
use codex_code_mode::CodeModeSession;
|
|
use codex_code_mode::CodeModeSessionDelegate;
|
|
use codex_code_mode::CodeModeSessionProvider;
|
|
use codex_code_mode::ExecuteRequest;
|
|
use codex_code_mode::FunctionCallOutputContentItem;
|
|
use codex_code_mode::GrpcCodeModeSessionProvider;
|
|
use codex_code_mode::NotificationFuture;
|
|
use codex_code_mode::RuntimeResponse;
|
|
use codex_code_mode::ToolInvocationFuture;
|
|
use codex_code_mode::WaitOutcome;
|
|
use codex_code_mode::WaitRequest;
|
|
use pretty_assertions::assert_eq;
|
|
use tokio::sync::Semaphore;
|
|
use tokio::time::timeout;
|
|
use tokio_util::sync::CancellationToken;
|
|
|
|
#[path = "support/host.rs"]
|
|
mod host;
|
|
#[path = "support/recording_delegate.rs"]
|
|
mod recording_delegate;
|
|
|
|
use host::HostHarness;
|
|
use recording_delegate::RecordingDelegate;
|
|
use recording_delegate::cell_id;
|
|
|
|
const TEST_TIMEOUT: Duration = Duration::from_secs(20);
|
|
|
|
struct BlockingNotificationDelegate {
|
|
started: Semaphore,
|
|
release: Semaphore,
|
|
delivered: Semaphore,
|
|
cancelled: Semaphore,
|
|
closed: Semaphore,
|
|
}
|
|
|
|
impl BlockingNotificationDelegate {
|
|
fn new() -> Self {
|
|
Self {
|
|
started: Semaphore::new(/*permits*/ 0),
|
|
release: Semaphore::new(/*permits*/ 0),
|
|
delivered: Semaphore::new(/*permits*/ 0),
|
|
cancelled: Semaphore::new(/*permits*/ 0),
|
|
closed: Semaphore::new(/*permits*/ 0),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CodeModeSessionDelegate for BlockingNotificationDelegate {
|
|
fn invoke_tool<'a>(
|
|
&'a self,
|
|
_invocation: CodeModeNestedToolCall,
|
|
_cancellation: CancellationToken,
|
|
) -> ToolInvocationFuture<'a> {
|
|
Box::pin(async { Err("unexpected tool invocation".to_string()) })
|
|
}
|
|
|
|
fn notify<'a>(
|
|
&'a self,
|
|
_call_id: String,
|
|
_cell_id: CellId,
|
|
_text: String,
|
|
cancellation: CancellationToken,
|
|
) -> NotificationFuture<'a> {
|
|
Box::pin(async move {
|
|
self.started.add_permits(/*n*/ 1);
|
|
tokio::select! {
|
|
_ = cancellation.cancelled() => {
|
|
self.cancelled.add_permits(/*n*/ 1);
|
|
Err("notification cancelled".to_string())
|
|
}
|
|
permit = self.release.acquire() => {
|
|
permit
|
|
.map_err(|_| "notification release closed".to_string())?
|
|
.forget();
|
|
self.delivered.add_permits(/*n*/ 1);
|
|
Ok(())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn cell_closed(&self, _cell_id: &CellId) {
|
|
self.closed.add_permits(/*n*/ 1);
|
|
}
|
|
}
|
|
|
|
fn request(source: &str) -> ExecuteRequest {
|
|
ExecuteRequest {
|
|
tool_call_id: "call-1".to_string(),
|
|
enabled_tools: Vec::new(),
|
|
source: source.to_string(),
|
|
yield_time_ms: Some(/*value*/ 5_000),
|
|
max_output_tokens: Some(/*value*/ 1_000),
|
|
}
|
|
}
|
|
|
|
fn text_response(
|
|
cell: &str,
|
|
value: &str,
|
|
code_mode_host_duration: Option<Duration>,
|
|
) -> RuntimeResponse {
|
|
RuntimeResponse::Result {
|
|
code_mode_host_duration,
|
|
cell_id: cell_id(cell),
|
|
content_items: vec![FunctionCallOutputContentItem::InputText {
|
|
text: value.to_string(),
|
|
}],
|
|
error_text: None,
|
|
}
|
|
}
|
|
|
|
async fn execute(
|
|
session: &Arc<dyn CodeModeSession>,
|
|
request: ExecuteRequest,
|
|
delegate: Arc<dyn CodeModeSessionDelegate>,
|
|
) -> Result<RuntimeResponse> {
|
|
timeout(TEST_TIMEOUT, async {
|
|
session
|
|
.execute(request, delegate.clone(), /*preempt*/ None)
|
|
.await
|
|
.map_err(anyhow::Error::msg)?
|
|
.initial_response()
|
|
.await
|
|
.map_err(anyhow::Error::msg)
|
|
})
|
|
.await
|
|
.context("timed out executing gRPC code-mode cell")?
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn completed_cells_drain_pending_notifications_before_completion() -> Result<()> {
|
|
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
|
|
let provider = GrpcCodeModeSessionProvider::new(host.endpoint);
|
|
let delegate = Arc::new(BlockingNotificationDelegate::new());
|
|
let session = provider
|
|
.create_session()
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
|
|
let executing = Arc::clone(&session);
|
|
let execution_delegate = delegate.clone();
|
|
let completion = tokio::spawn(async move {
|
|
execute(
|
|
&executing,
|
|
request(r#"notify("notice"); text("done");"#),
|
|
execution_delegate,
|
|
)
|
|
.await
|
|
});
|
|
timeout(TEST_TIMEOUT, delegate.started.acquire())
|
|
.await
|
|
.context("notification did not start")??
|
|
.forget();
|
|
assert!(!completion.is_finished());
|
|
delegate.release.add_permits(/*n*/ 1);
|
|
let actual = timeout(TEST_TIMEOUT, completion)
|
|
.await
|
|
.context("completed cell did not finish after notification delivery")???;
|
|
assert_eq!(
|
|
actual,
|
|
text_response("1", "done", actual.code_mode_host_duration())
|
|
);
|
|
timeout(TEST_TIMEOUT, delegate.delivered.acquire())
|
|
.await
|
|
.context("completed cell did not deliver its pending notification")??
|
|
.forget();
|
|
assert!(delegate.cancelled.try_acquire().is_err());
|
|
timeout(TEST_TIMEOUT, delegate.closed.acquire())
|
|
.await
|
|
.context("completed cell was not retired")??
|
|
.forget();
|
|
|
|
let actual = execute(
|
|
&session,
|
|
request(r#"text("still alive");"#),
|
|
delegate.clone(),
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
actual,
|
|
text_response("2", "still alive", actual.code_mode_host_duration())
|
|
);
|
|
|
|
session.shutdown().await.map_err(anyhow::Error::msg)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn completed_waits_drain_pending_notifications_before_returning() -> Result<()> {
|
|
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
|
|
let provider = GrpcCodeModeSessionProvider::new(host.endpoint);
|
|
let delegate = Arc::new(BlockingNotificationDelegate::new());
|
|
let session = provider
|
|
.create_session()
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
let pending = request(r#"yield_control(); notify("notice"); text("done");"#);
|
|
let cell = session
|
|
.execute(pending, delegate.clone(), /*preempt*/ None)
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
let actual = cell.initial_response().await.map_err(anyhow::Error::msg)?;
|
|
assert_eq!(
|
|
actual,
|
|
RuntimeResponse::Yielded {
|
|
code_mode_host_duration: actual.code_mode_host_duration(),
|
|
cell_id: cell_id("1"),
|
|
content_items: Vec::new(),
|
|
}
|
|
);
|
|
|
|
let waiting = Arc::clone(&session);
|
|
let completion = tokio::spawn(async move {
|
|
waiting
|
|
.wait(
|
|
WaitRequest {
|
|
cell_id: cell_id("1"),
|
|
yield_time_ms: 5_000,
|
|
},
|
|
/*preempt*/ None,
|
|
)
|
|
.await
|
|
.map_err(anyhow::Error::msg)
|
|
});
|
|
timeout(TEST_TIMEOUT, delegate.started.acquire())
|
|
.await
|
|
.context("wait notification did not start")??
|
|
.forget();
|
|
assert!(!completion.is_finished());
|
|
delegate.release.add_permits(/*n*/ 1);
|
|
let actual = timeout(TEST_TIMEOUT, completion)
|
|
.await
|
|
.context("wait did not finish after notification delivery")???;
|
|
assert_eq!(
|
|
actual,
|
|
WaitOutcome::LiveCell(RuntimeResponse::Result {
|
|
code_mode_host_duration: actual.code_mode_host_duration(),
|
|
cell_id: cell_id("1"),
|
|
content_items: vec![FunctionCallOutputContentItem::InputText {
|
|
text: "done".to_string(),
|
|
}],
|
|
error_text: None,
|
|
})
|
|
);
|
|
timeout(TEST_TIMEOUT, delegate.delivered.acquire())
|
|
.await
|
|
.context("wait did not deliver its pending notification")??
|
|
.forget();
|
|
assert!(delegate.cancelled.try_acquire().is_err());
|
|
|
|
session.shutdown().await.map_err(anyhow::Error::msg)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn termination_cancels_pending_notifications() -> Result<()> {
|
|
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
|
|
let provider = GrpcCodeModeSessionProvider::new(host.endpoint);
|
|
let delegate = Arc::new(BlockingNotificationDelegate::new());
|
|
let session = provider
|
|
.create_session()
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
let mut pending = request(r#"notify("notice"); await new Promise(() => {});"#);
|
|
pending.yield_time_ms = Some(/*value*/ 1);
|
|
let cell = session
|
|
.execute(pending, delegate.clone(), /*preempt*/ None)
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
|
|
timeout(TEST_TIMEOUT, delegate.started.acquire())
|
|
.await
|
|
.context("notification did not start")??
|
|
.forget();
|
|
let actual = cell.initial_response().await.map_err(anyhow::Error::msg)?;
|
|
assert_eq!(
|
|
actual,
|
|
RuntimeResponse::Yielded {
|
|
code_mode_host_duration: actual.code_mode_host_duration(),
|
|
cell_id: cell_id("1"),
|
|
content_items: Vec::new(),
|
|
}
|
|
);
|
|
let actual = session
|
|
.terminate(cell_id("1"))
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
assert_eq!(
|
|
actual,
|
|
WaitOutcome::LiveCell(RuntimeResponse::Terminated {
|
|
code_mode_host_duration: actual.code_mode_host_duration(),
|
|
cell_id: cell_id("1"),
|
|
content_items: Vec::new(),
|
|
})
|
|
);
|
|
timeout(TEST_TIMEOUT, delegate.cancelled.acquire())
|
|
.await
|
|
.context("termination did not cancel notification delivery")??
|
|
.forget();
|
|
timeout(TEST_TIMEOUT, delegate.closed.acquire())
|
|
.await
|
|
.context("terminated cell was not retired")??
|
|
.forget();
|
|
|
|
session.shutdown().await.map_err(anyhow::Error::msg)?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn oversized_notification_text_is_delivered_unchanged() -> Result<()> {
|
|
let host = HostHarness::start("grpc://127.0.0.1:0").await?;
|
|
let provider = GrpcCodeModeSessionProvider::new(host.endpoint);
|
|
let delegate = Arc::new(RecordingDelegate::default());
|
|
let session = provider
|
|
.create_session()
|
|
.await
|
|
.map_err(anyhow::Error::msg)?;
|
|
|
|
let actual = execute(
|
|
&session,
|
|
request(r#"notify("🦀".repeat(512)); text("done");"#),
|
|
delegate.clone(),
|
|
)
|
|
.await?;
|
|
assert_eq!(
|
|
actual,
|
|
text_response("1", "done", actual.code_mode_host_duration())
|
|
);
|
|
timeout(TEST_TIMEOUT, delegate.notification_delivered.notified())
|
|
.await
|
|
.context("oversized notification was not delivered")?;
|
|
assert_eq!(
|
|
*delegate
|
|
.notifications
|
|
.lock()
|
|
.unwrap_or_else(PoisonError::into_inner),
|
|
vec![("call-1".to_string(), cell_id("1"), "🦀".repeat(512),)]
|
|
);
|
|
|
|
session.shutdown().await.map_err(anyhow::Error::msg)?;
|
|
Ok(())
|
|
}
|