mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Add promise settlement streaming helpers to code mode (#51126)
## What changed Expose two JavaScript globals for consuming results as inputs settle: - `as_settled(promises)` returns an async iterator of fulfilled and rejected records in settlement order, preserving queued results while the consumer is busy. - `stream_settled(promises, emit)` delivers those records to a callback, awaiting each invocation and propagating callback errors. Records include an `index`, a `status`, and either a `value` or a `reason`. Iterable inputs use positional indices; `Map` inputs retain their keys. Both helpers accept promises, thenables, and plain values. ## Testing Add runtime coverage for ordering, input types, object identity, callback behavior, early iteration exit, and termination. Add stdio and gRPC host tests for partial results across waits, plus a scenario snapshot verifying fulfilled and rejected records reach the model. GitOrigin-RevId: cddc9f3679ae04f2421e154caa0417afb15f550d
This commit is contained in:
committed by
copyberry
parent
402f5b6fdf
commit
28a264fbc7
@@ -0,0 +1,181 @@
|
||||
//! Verifies partial settlement output survives waits on both host transports.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_code_mode::CellId;
|
||||
use codex_code_mode::CodeModeNestedToolCall;
|
||||
use codex_code_mode::CodeModeSessionDelegate;
|
||||
use codex_code_mode::CodeModeSessionProvider;
|
||||
use codex_code_mode::CodeModeToolKind;
|
||||
use codex_code_mode::ExecuteRequest;
|
||||
use codex_code_mode::FunctionCallOutputContentItem;
|
||||
use codex_code_mode::GrpcCodeModeSessionProvider;
|
||||
use codex_code_mode::NotificationFuture;
|
||||
use codex_code_mode::ProcessOwnedCodeModeSessionProvider;
|
||||
use codex_code_mode::RuntimeResponse;
|
||||
use codex_code_mode::ToolDefinition;
|
||||
use codex_code_mode::ToolInvocationFuture;
|
||||
use codex_code_mode::WaitOutcome;
|
||||
use codex_code_mode::WaitRequest;
|
||||
use codex_protocol::ToolName;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tokio::sync::Notify;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[path = "support/host.rs"]
|
||||
mod host;
|
||||
|
||||
#[derive(Default)]
|
||||
struct SlowDelegate {
|
||||
release: Notify,
|
||||
}
|
||||
|
||||
impl CodeModeSessionDelegate for SlowDelegate {
|
||||
fn invoke_tool<'a>(
|
||||
&'a self,
|
||||
_invocation: CodeModeNestedToolCall,
|
||||
cancellation: CancellationToken,
|
||||
) -> ToolInvocationFuture<'a> {
|
||||
Box::pin(async move {
|
||||
tokio::select! {
|
||||
_ = self.release.notified() => Ok(json!("slow")),
|
||||
_ = cancellation.cancelled() => Err("cancelled".to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn notify<'a>(
|
||||
&'a self,
|
||||
_call_id: String,
|
||||
_cell_id: CellId,
|
||||
_text: String,
|
||||
_cancellation: CancellationToken,
|
||||
) -> NotificationFuture<'a> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn cell_closed(&self, _cell_id: &CellId) {}
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
async fn assert_partial_results(provider: &dyn CodeModeSessionProvider) {
|
||||
for consumer in [
|
||||
"for await (const result of as_settled(promises)) text(result);",
|
||||
"await stream_settled(promises, text);",
|
||||
] {
|
||||
let delegate = Arc::new(SlowDelegate::default());
|
||||
let session = provider.create_session().await.expect("create session");
|
||||
let started = session
|
||||
.execute(ExecuteRequest {
|
||||
tool_call_id: "settled".to_string(),
|
||||
enabled_tools: vec![ToolDefinition {
|
||||
name: "slow".to_string(),
|
||||
tool_name: ToolName::plain("slow"),
|
||||
description: String::new(),
|
||||
kind: CodeModeToolKind::Function,
|
||||
input_schema: None,
|
||||
input_schema_max_bytes: None,
|
||||
output_schema: None,
|
||||
}],
|
||||
source: format!(
|
||||
r#"const promises = [tools.slow({{}}), Promise.resolve("fast"), Promise.reject("failed")];
|
||||
{consumer}"#
|
||||
),
|
||||
yield_time_ms: Some(/*value*/ 10),
|
||||
max_output_tokens: None,
|
||||
}, delegate.clone(), /*preempt*/ None)
|
||||
.await
|
||||
.expect("start cell");
|
||||
let cell_id = started.cell_id.clone();
|
||||
let mut response = started.initial_response().await.expect("initial response");
|
||||
let mut partial = Vec::new();
|
||||
loop {
|
||||
let RuntimeResponse::Yielded { content_items, .. } = response else {
|
||||
panic!("cell completed before the slow tool was released: {response:?}");
|
||||
};
|
||||
partial.extend(content_items.into_iter().map(|item| {
|
||||
let FunctionCallOutputContentItem::InputText { text } = item else {
|
||||
panic!("unexpected output: {item:?}");
|
||||
};
|
||||
serde_json::from_str::<serde_json::Value>(&text).expect("JSON output")
|
||||
}));
|
||||
if partial.len() >= 2 {
|
||||
break;
|
||||
}
|
||||
let WaitOutcome::LiveCell(next) = session
|
||||
.wait(
|
||||
WaitRequest {
|
||||
cell_id: cell_id.clone(),
|
||||
yield_time_ms: 10,
|
||||
},
|
||||
/*preempt*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("wait for partial results")
|
||||
else {
|
||||
panic!("cell disappeared while the slow tool was pending");
|
||||
};
|
||||
response = next;
|
||||
}
|
||||
assert_eq!(
|
||||
partial,
|
||||
[
|
||||
json!({"index": 1, "status": "fulfilled", "value": "fast"}),
|
||||
json!({"index": 2, "status": "rejected", "reason": "failed"}),
|
||||
]
|
||||
);
|
||||
|
||||
delegate.release.notify_one();
|
||||
let completed = session
|
||||
.wait(
|
||||
WaitRequest {
|
||||
cell_id: cell_id.clone(),
|
||||
yield_time_ms: 5_000,
|
||||
},
|
||||
/*preempt*/ None,
|
||||
)
|
||||
.await
|
||||
.expect("wait for completion");
|
||||
assert_eq!(
|
||||
completed,
|
||||
WaitOutcome::LiveCell(RuntimeResponse::Result {
|
||||
code_mode_host_duration: completed.code_mode_host_duration(),
|
||||
cell_id,
|
||||
content_items: vec![FunctionCallOutputContentItem::InputText {
|
||||
text: json!({"index": 0, "status": "fulfilled", "value": "slow"}).to_string(),
|
||||
}],
|
||||
error_text: None,
|
||||
})
|
||||
);
|
||||
session.shutdown().await.expect("shutdown session");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn settled_results_stream_over_stdio() {
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(
|
||||
codex_utils_cargo_bin::cargo_bin("codex-code-mode-host").expect("host binary"),
|
||||
);
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(/*secs*/ 20),
|
||||
assert_partial_results(&provider),
|
||||
)
|
||||
.await
|
||||
.expect("settlement streaming timed out");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn settled_results_stream_over_grpc() {
|
||||
let host = host::HostHarness::start("grpc://127.0.0.1:0")
|
||||
.await
|
||||
.expect("start gRPC host");
|
||||
let provider = GrpcCodeModeSessionProvider::new(host.endpoint);
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(/*secs*/ 20),
|
||||
assert_partial_results(&provider),
|
||||
)
|
||||
.await
|
||||
.expect("settlement streaming timed out");
|
||||
}
|
||||
Reference in New Issue
Block a user