mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Add experimental command output deferral to thread item listings (#59601)
## What changed
Allow experimental clients to request `deferOutput: {"commandExecution": true}` in `thread/items/list` for a specific `turnId`. Completed commands with a recorded completion time, exit code zero, and nonempty saved output return `outputDeferred: true` and `item.aggregatedOutput: null`. Clients can retrieve the complete output through `thread/items/read` using the turn and item IDs.
Keep full output by default and for listings across turns. Make conversion from `ThreadItemEntry` to `ThreadItem` fallible so Rust consumers detect deferred output instead of treating an incomplete item as complete.
## Testing
Add integration coverage for experimental capability gating, default and disabled options, command status and output eligibility, listings across turns, deferred-item conversion, and full output retrieval by item ID.
GitOrigin-RevId: d6bfac34bb68f7e3b327fd5553270ed6d0fe87cf
This commit is contained in:
@@ -896,6 +896,7 @@ client_request_definitions! {
|
||||
},
|
||||
ThreadItemsList => "thread/items/list" {
|
||||
params: v2::ThreadItemsListParams,
|
||||
inspect_params: true,
|
||||
// Explicitly concurrent: this primarily reads append-only rollout storage.
|
||||
serialization: None,
|
||||
response: v2::ThreadItemsListResponse,
|
||||
@@ -2757,6 +2758,7 @@ mod tests {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
sort_direction: None,
|
||||
defer_output: None,
|
||||
},
|
||||
};
|
||||
assert_eq!(thread_items_list.serialization_scope(), None);
|
||||
|
||||
@@ -421,6 +421,7 @@ fn thread_items_list_round_trips() {
|
||||
cursor: Some(ThreadItemsListCursor::Opaque("cursor_1".to_string())),
|
||||
limit: Some(50),
|
||||
sort_direction: Some(SortDirection::Asc),
|
||||
defer_output: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
@@ -462,6 +463,7 @@ fn thread_items_list_round_trips() {
|
||||
let response = ThreadItemsListResponse {
|
||||
data: vec![ThreadItemEntry {
|
||||
turn_id: "turn_456".to_string(),
|
||||
output_deferred: false,
|
||||
item: ThreadItem {
|
||||
payload: ThreadItemPayload::ContextCompaction {
|
||||
id: "item_1".to_string(),
|
||||
@@ -501,6 +503,7 @@ fn thread_items_list_round_trips() {
|
||||
cursor: None,
|
||||
limit: None,
|
||||
sort_direction: None,
|
||||
defer_output: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -1837,7 +1837,7 @@ pub struct ThreadTurnsListResponse {
|
||||
pub backwards_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadItemsListParams {
|
||||
@@ -1856,6 +1856,21 @@ pub struct ThreadItemsListParams {
|
||||
/// Optional item pagination direction; defaults to ascending.
|
||||
#[ts(optional = nullable)]
|
||||
pub sort_direction: Option<SortDirection>,
|
||||
/// For a single turn, choose which saved outputs to fetch separately by item ID.
|
||||
#[experimental(nested)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional = nullable)]
|
||||
pub defer_output: Option<ThreadItemOutputDeferral>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadItemOutputDeferral {
|
||||
/// Defer nonempty output from successful commands.
|
||||
#[experimental("thread/items/list.deferOutput.commandExecution")]
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub command_execution: bool,
|
||||
}
|
||||
|
||||
/// Starting position for an item-history page.
|
||||
@@ -1879,13 +1894,17 @@ pub enum ThreadItemsListAnchor {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadItemEntry {
|
||||
/// Turn containing this item.
|
||||
pub turn_id: String,
|
||||
pub item: ThreadItem,
|
||||
/// The saved command output was omitted. Fetch the complete item with `thread/items/read`.
|
||||
#[experimental("thread/items/list.outputDeferred")]
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub output_deferred: bool,
|
||||
/// Unix timestamp (milliseconds) when the item started, if recorded by the producer.
|
||||
#[ts(type = "number | null")]
|
||||
pub started_at_ms: Option<i64>,
|
||||
@@ -1894,13 +1913,30 @@ pub struct ThreadItemEntry {
|
||||
pub completed_at_ms: Option<i64>,
|
||||
}
|
||||
|
||||
impl From<ThreadItemEntry> for ThreadItem {
|
||||
fn from(entry: ThreadItemEntry) -> Self {
|
||||
Self {
|
||||
/// Identifies the item to fetch with `thread/items/read` to obtain its full output.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("output for item {item_id} in turn {turn_id} is deferred")]
|
||||
pub struct DeferredItemOutput {
|
||||
pub turn_id: String,
|
||||
pub item_id: String,
|
||||
}
|
||||
|
||||
impl TryFrom<ThreadItemEntry> for ThreadItem {
|
||||
type Error = DeferredItemOutput;
|
||||
|
||||
fn try_from(entry: ThreadItemEntry) -> Result<Self, Self::Error> {
|
||||
if entry.output_deferred {
|
||||
return Err(DeferredItemOutput {
|
||||
item_id: entry.item.id().to_owned(),
|
||||
turn_id: entry.turn_id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
started_at_ms: entry.item.started_at_ms.or(entry.started_at_ms),
|
||||
completed_at_ms: entry.item.completed_at_ms.or(entry.completed_at_ms),
|
||||
..entry.item
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user