Files
codex/codex-rs/code-mode-protocol/src/runtime.rs
T
Sean Huang edd1ae37ec Enable server-side output truncation for Code Mode sessions (#57870)
## Why

Code Mode sessions always used client-side output handling, leaving server truncation settings and observation budgets unused.

## What changed

- Honor each session's `output_truncation` setting in the runtime and both host transports, and advertise the bounded-output capability.
- Apply `max_output_tokens` independently to execute, wait, and terminate responses in server mode. Keep client mode as the default when the setting is omitted.
- Use bounded output collection and rendering in server mode, preserving head and tail output with omission notices. Include script errors in the output budget while retaining failure status and a separate diagnostic capped at 64 KiB.
- Estimate audio tokens without the Tokio-dependent cache when rendering bounded output.

## Testing

Add runtime and host regression tests for session mode selection, per-request budgets, output overflow, oversized images, zero-token audio output, diagnostic preservation, and termination under output backpressure.

GitOrigin-RevId: 3d9661edcdd2dde31526bb92eb83fa0614f0fa88
2026-10-29 01:04:48 +00:00

223 lines
7.2 KiB
Rust

use std::error::Error;
use std::fmt;
use std::time::Duration;
use codex_protocol::ToolName;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
use crate::CellId;
use crate::CodeModeToolKind;
use crate::FunctionCallOutputContentItem;
use crate::ToolDefinition;
pub const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000;
pub const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000;
pub const DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL: usize = 10_000;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ExecuteRequest {
pub tool_call_id: String,
pub enabled_tools: Vec<ToolDefinition>,
pub source: String,
pub yield_time_ms: Option<u64>,
pub max_output_tokens: Option<usize>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct WaitRequest {
pub cell_id: CellId,
pub yield_time_ms: u64,
/// Limit for this observation in Server mode; ignored in Client mode.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<usize>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct TerminateRequest {
pub cell_id: CellId,
/// Limit for the final observation in Server mode; ignored in Client mode.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<usize>,
}
impl From<CellId> for TerminateRequest {
fn from(cell_id: CellId) -> Self {
Self {
cell_id,
max_output_tokens: None,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct WaitToPendingRequest {
pub cell_id: CellId,
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub enum WaitOutcome {
LiveCell(RuntimeResponse),
MissingCell(RuntimeResponse),
}
impl WaitOutcome {
/// How long a completed result waited for retrieval. Zero if it was delivered immediately,
/// or `None` if this wait did not retrieve a completed result.
pub fn result_buffered_for(&self) -> Option<Duration> {
match self {
Self::LiveCell(RuntimeResponse::Result {
result_buffered_for,
..
}) => Some(*result_buffered_for),
Self::LiveCell(_) | Self::MissingCell(_) => None,
}
}
/// Returns timing for this wait or termination request, when supplied by its host.
pub fn code_mode_host_duration(&self) -> Option<Duration> {
match self {
Self::LiveCell(response) | Self::MissingCell(response) => {
response.code_mode_host_duration()
}
}
}
/// Records the enclosing host request's duration before wire conversion.
pub fn with_code_mode_host_duration(self, code_mode_host_duration: Duration) -> Self {
match self {
Self::LiveCell(response) => {
Self::LiveCell(response.with_code_mode_host_duration(code_mode_host_duration))
}
Self::MissingCell(response) => {
Self::MissingCell(response.with_code_mode_host_duration(code_mode_host_duration))
}
}
}
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub enum ExecuteToPendingOutcome {
Pending {
cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>,
pending_tool_call_ids: Vec<String>,
},
Completed(RuntimeResponse),
}
#[derive(Debug, Deserialize, PartialEq, Serialize)]
pub enum WaitToPendingOutcome {
LiveCell(ExecuteToPendingOutcome),
MissingCell(RuntimeResponse),
}
impl From<WaitOutcome> for RuntimeResponse {
fn from(outcome: WaitOutcome) -> Self {
match outcome {
WaitOutcome::LiveCell(response) | WaitOutcome::MissingCell(response) => response,
}
}
}
/// Runtime output with optional timing for the host request that observed it.
///
/// The JavaScript session returns untimed output. The host handler records its
/// complete request duration before conversion; wire conversions preserve that
/// field and reject untimed output. Decoded host responses always contain timing,
/// including measured zero. Raw traces also retain timing when present.
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum RuntimeResponse {
Yielded {
cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>,
#[serde(skip_serializing_if = "Option::is_none")]
code_mode_host_duration: Option<Duration>,
},
Terminated {
cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>,
#[serde(skip_serializing_if = "Option::is_none")]
code_mode_host_duration: Option<Duration>,
},
Result {
cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>,
/// None means success; Some (including an empty string) means failure.
/// Client mode supplies the original diagnostic to append before truncation.
/// Server mode includes the diagnostic in the already-budgeted content and
/// supplies an independently bounded copy here for logging; do not append it again.
error_text: Option<String>,
/// Time between completion and retrieval when the result was buffered.
#[serde(default, skip_serializing_if = "Duration::is_zero")]
result_buffered_for: Duration,
#[serde(skip_serializing_if = "Option::is_none")]
code_mode_host_duration: Option<Duration>,
},
}
impl RuntimeResponse {
/// Returns timing for this observation, excluding background work between requests.
pub fn code_mode_host_duration(&self) -> Option<Duration> {
match self {
Self::Yielded {
code_mode_host_duration,
..
}
| Self::Terminated {
code_mode_host_duration,
..
}
| Self::Result {
code_mode_host_duration,
..
} => *code_mode_host_duration,
}
}
/// Records the enclosing host request's duration before wire conversion.
pub fn with_code_mode_host_duration(mut self, code_mode_host_duration: Duration) -> Self {
match &mut self {
Self::Yielded {
code_mode_host_duration: value,
..
}
| Self::Terminated {
code_mode_host_duration: value,
..
}
| Self::Result {
code_mode_host_duration: value,
..
} => *value = Some(code_mode_host_duration),
}
self
}
}
/// An untimed runtime response cannot be encoded for delivery to a host client.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MissingCodeModeHostDuration;
impl fmt::Display for MissingCodeModeHostDuration {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("code-mode response is missing host duration")
}
}
impl Error for MissingCodeModeHostDuration {}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct CodeModeNestedToolCall {
pub cell_id: CellId,
pub runtime_tool_call_id: String,
pub tool_name: ToolName,
pub tool_kind: CodeModeToolKind,
pub input: Option<JsonValue>,
}
#[cfg(test)]
#[path = "runtime_tests.rs"]
mod tests;