Files
codex/codex-rs/code-mode-protocol/src/session.rs
T
Sean Huang 6d97d6ecb4 Add bounded Code Mode output collection and rendering (#57848)
## What changed

Add output collection and rendering components in `code-mode-runtime`, without wiring them into runtime execution yet.

- Retain a text prefix and rolling tail under a 1 MiB text allowance, with independent allowances of 8 MiB for image URLs and 62 MiB for audio URLs and a separate 32,768-item limit.
- Render retained output using the shared token-budget formatter, preserving media details and reporting collector losses even with a zero-token budget. Summarize scattered losses to keep notices bounded.
- Preserve collected output and omission accounting when delivery fails, and document the separate media limits in the protocol.

## Testing

Add unit tests for Unicode retention, item pressure, independent media allowances, media-space reclamation, failed delivery, default token budgets, and loss-notice placement and aggregation.

GitOrigin-RevId: f419fdbe316a8456cea3b7b097c4d3a658f3150f
2026-10-28 22:31:05 +00:00

279 lines
9.6 KiB
Rust

use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use crate::CodeModeNestedToolCall;
use crate::ExecuteRequest;
use crate::RuntimeResponse;
use crate::TerminateRequest;
use crate::WaitOutcome;
use crate::WaitRequest;
pub type CodeModeSessionResultFuture<'a, T> =
Pin<Box<dyn Future<Output = Result<T, String>> + Send + 'a>>;
pub type CodeModeSessionProviderFuture<'a> =
CodeModeSessionResultFuture<'a, Arc<dyn CodeModeSession>>;
pub type ToolInvocationFuture<'a> =
Pin<Box<dyn Future<Output = Result<JsonValue, String>> + Send + 'a>>;
pub type NotificationFuture<'a> = Pin<Box<dyn Future<Output = Result<(), String>> + Send + 'a>>;
/// Optional resource limits shared by every cell in one code-mode session.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CodeModeSessionCellExecutionLimits {
pub max_yield_time_ms: Option<u64>,
pub max_heap_size_bytes: Option<usize>,
}
/// The owner of the output budget for each exec or wait observation.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CodeModeOutputTruncation {
/// Return raw output; the caller applies its own budget and formatting.
#[default]
Client,
/// The host applies the exec, wait, or terminate observation's budget before
/// transmitting output (10,000 tokens by default). Clients must not truncate
/// it again or append the script error: status and error metadata remain separate.
///
/// A complete observation keeps up to [`crate::host::BOUNDED_OUTPUT_MAX_BYTES`]
/// of text, [`crate::host::BOUNDED_OUTPUT_MAX_IMAGE_BYTES`] of inline image URLs,
/// and [`crate::host::BOUNDED_OUTPUT_MAX_AUDIO_BYTES`] of audio URLs, subject
/// to a separate item limit. While these limits hold, formatting matches
/// Core's client policy. On text overflow the collector keeps a prefix and a
/// rolling tail. Each image or audio clip is admitted if it fits its remaining
/// byte allowance. Eviction at the item limit can free space for later media.
/// Media are indivisible. Bounded loss notices survive even a zero-token budget.
Server,
}
/// Settings shared by every cell in one code-mode session.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CodeModeSessionOptions {
pub cell_execution_limits: CodeModeSessionCellExecutionLimits,
pub output_truncation: CodeModeOutputTruncation,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct CellId(String);
impl CellId {
pub fn new(value: String) -> Self {
Self(value)
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for CellId {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for CellId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
pub struct StartedCell {
pub cell_id: CellId,
initial_response: CodeModeSessionResultFuture<'static, RuntimeResponse>,
}
impl StartedCell {
pub fn new(cell_id: CellId, initial_response_rx: oneshot::Receiver<RuntimeResponse>) -> Self {
Self::from_future(cell_id, async move {
initial_response_rx
.await
.map_err(|_| "exec runtime ended unexpectedly".to_string())
})
}
pub fn from_result_receiver(
cell_id: CellId,
initial_response_rx: oneshot::Receiver<Result<RuntimeResponse, String>>,
) -> Self {
Self::from_future(cell_id, async move {
initial_response_rx
.await
.map_err(|_| "exec runtime ended unexpectedly".to_string())?
})
}
pub fn from_future(
cell_id: CellId,
initial_response: impl Future<Output = Result<RuntimeResponse, String>> + Send + 'static,
) -> Self {
Self {
cell_id,
initial_response: Box::pin(initial_response),
}
}
pub async fn initial_response(self) -> Result<RuntimeResponse, String> {
self.initial_response.await
}
}
/// Host callbacks owned by one code-mode execution.
///
/// The session retains the supplied delegate while starting and running the cell,
/// including across yields, and releases it through its existing close/cancel paths.
pub trait CodeModeSessionDelegate: Send + Sync {
fn invoke_tool<'a>(
&'a self,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> ToolInvocationFuture<'a>;
fn notify<'a>(
&'a self,
call_id: String,
cell_id: CellId,
text: String,
cancellation_token: CancellationToken,
) -> NotificationFuture<'a>;
/// Releases delegate state associated with a cell after it reaches a terminal state.
fn cell_closed(&self, cell_id: &CellId);
}
/// A session delegate for clients that do not expose nested tools or notifications.
pub struct NoopCodeModeSessionDelegate;
impl CodeModeSessionDelegate for NoopCodeModeSessionDelegate {
fn invoke_tool<'a>(
&'a self,
_invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> ToolInvocationFuture<'a> {
Box::pin(async move {
cancellation_token.cancelled().await;
Err("code mode nested tools are unavailable".to_string())
})
}
fn notify<'a>(
&'a self,
_call_id: String,
_cell_id: CellId,
_text: String,
_cancellation_token: CancellationToken,
) -> NotificationFuture<'a> {
Box::pin(async { Ok(()) })
}
fn cell_closed(&self, _cell_id: &CellId) {}
}
/// A durable code-mode session owned by one Codex thread.
///
/// Cells executed in the same session share stored values. Separate sessions
/// must keep those values isolated. Implementations may execute cells
/// in-process or remotely.
pub trait CodeModeSession: Send + Sync {
/// Identifies who applies output truncation for this session's responses.
/// Providers must not change modes when reconnecting to a new host.
fn output_truncation(&self) -> CodeModeOutputTruncation {
CodeModeOutputTruncation::Client
}
/// Executes a cell, yielding its foreground observation if `preempt` is signaled.
/// The cell continues running after its observation yields.
fn execute<'a>(
&'a self,
request: ExecuteRequest,
delegate: Arc<dyn CodeModeSessionDelegate>,
preempt: Option<CancellationToken>,
) -> CodeModeSessionResultFuture<'a, StartedCell>;
/// Waits for a cell, yielding the observation if `preempt` is signaled while the cell continues.
/// Server mode applies the request's output limit; Client mode ignores it.
fn wait<'a>(
&'a self,
request: WaitRequest,
preempt: Option<CancellationToken>,
) -> CodeModeSessionResultFuture<'a, WaitOutcome>;
/// Terminates a cell, applying the request's limit to its final observation
/// in Server mode; Client mode ignores the limit.
fn terminate<'a>(
&'a self,
request: TerminateRequest,
) -> CodeModeSessionResultFuture<'a, WaitOutcome>;
fn shutdown<'a>(&'a self) -> CodeModeSessionResultFuture<'a, ()>;
}
/// Creates code-mode sessions for Codex threads.
///
/// Implementations may share a remote host process across all sessions created
/// by one provider.
pub trait CodeModeSessionProvider: Send + Sync {
/// Reports whether this provider can execute code without starting its host.
fn availability(&self) -> Result<(), String> {
Ok(())
}
/// Observes a host without starting a session. Process-owned providers must
/// not start or replace a host. Network providers may connect or reconnect
/// according to their transport policy. Sampling failures and cancellation
/// must not explicitly reset the shared execution connection.
fn sample_process_metrics(
&self,
) -> CodeModeSessionResultFuture<'_, crate::CodeModeProcessMetricsSample> {
Box::pin(async { Ok(crate::CodeModeProcessMetricsSample::Unsupported) })
}
fn create_session(&self) -> CodeModeSessionProviderFuture<'_>;
/// Creates a session whose cells share the supplied execution limits.
///
/// Existing providers remain compatible with unlimited sessions, but must
/// explicitly implement this method before accepting non-default limits.
fn create_session_with_limits<'a>(
&'a self,
limits: CodeModeSessionCellExecutionLimits,
) -> CodeModeSessionProviderFuture<'a> {
if limits == CodeModeSessionCellExecutionLimits::default() {
self.create_session()
} else {
Box::pin(async {
Err("code-mode session provider does not support resource limits".to_string())
})
}
}
/// Requests output handling for one session, without changing other sessions
/// created by this provider. Server mode requires host support.
fn create_session_with_options<'a>(
&'a self,
options: CodeModeSessionOptions,
) -> CodeModeSessionProviderFuture<'a> {
match options.output_truncation {
CodeModeOutputTruncation::Client => {
self.create_session_with_limits(options.cell_execution_limits)
}
CodeModeOutputTruncation::Server => Box::pin(async {
Err(
"code-mode session provider does not support server output truncation"
.to_string(),
)
}),
}
}
}
#[cfg(test)]
#[path = "session_tests.rs"]
mod tests;