Add code-mode output truncation protocol contracts (#57272)

## What changed

Add session options for client- or server-owned output truncation and optional `max_output_tokens` budgets for wait and terminate observations. Introduce `TerminateRequest` and update session implementations and callers to use it.

Extend the stdio and gRPC schemas with optional truncation settings and budgets, and define the `bounded-output-v1` capability. Existing sessions retain client-side truncation; providers reject server mode by default. This change defines the contracts without enabling server-side truncation.

## Testing

Extend protocol serialization coverage for `outputTruncation: "server"` while preserving existing wire shapes when optional fields are absent.

GitOrigin-RevId: 22145897ea16d385c5ba9e7549109f7a669fceb4
This commit is contained in:
Sean Huang
2026-10-26 21:36:35 +00:00
committed by copyberry
parent 7077b1edb9
commit e963cab11a
38 changed files with 329 additions and 55 deletions
@@ -27,6 +27,7 @@ use super::WireContentItem;
use super::WireExecuteRequest;
use super::WireImageDetail;
use super::WireNestedToolCall;
use super::WireOutputTruncation;
use super::WireResult;
use super::WireRuntimeResponse;
use super::WireSessionCellExecutionLimits;
@@ -249,6 +250,7 @@ fn open_session_serializes_optional_cell_execution_limits() {
max_yield_time_ms: Some(250),
max_heap_size_bytes: Some(16 * 1024 * 1024),
}),
output_truncation: None,
},
json!({
"method": "session/open",
@@ -304,9 +306,23 @@ fn client_to_host_v1_variants_are_pinned() {
HostRequest::OpenSession {
session_id: session_id(),
cell_execution_limits: None,
output_truncation: None,
},
json!({ "method": "session/open", "sessionId": "session-1" }),
),
(
request_id(/*value*/ 10),
HostRequest::OpenSession {
session_id: session_id(),
cell_execution_limits: None,
output_truncation: Some(WireOutputTruncation::Server),
},
json!({
"method": "session/open",
"sessionId": "session-1",
"outputTruncation": "server",
}),
),
(
request_id(/*value*/ 2),
HostRequest::Execute {
@@ -353,6 +369,7 @@ fn client_to_host_v1_variants_are_pinned() {
cell_id: cell_id("cell-1"),
yield_time_ms: 50,
},
max_output_tokens: None,
},
json!({
"method": "session/wait",
@@ -365,6 +382,7 @@ fn client_to_host_v1_variants_are_pinned() {
HostRequest::Terminate {
session_id: session_id(),
cell_id: cell_id("cell-1"),
max_output_tokens: None,
},
json!({
"method": "session/terminate",
@@ -15,6 +15,7 @@ use super::SupportedProtocolVersions;
use super::WireCellId;
use super::WireExecuteRequest;
use super::WireNestedToolCall;
use super::WireOutputTruncation;
use super::WireRuntimeResponse;
use super::WireSessionCellExecutionLimits;
use super::WireWaitOutcome;
@@ -186,6 +187,10 @@ pub enum HostRequest {
session_id: SessionId,
#[serde(default, skip_serializing_if = "Option::is_none")]
cell_execution_limits: Option<WireSessionCellExecutionLimits>,
/// Sent only when output truncation support was agreed at connection hello.
/// Omission preserves the legacy session behavior and wire shape.
#[serde(default, skip_serializing_if = "Option::is_none")]
output_truncation: Option<WireOutputTruncation>,
},
#[serde(rename = "session/execute")]
Execute {
@@ -196,11 +201,15 @@ pub enum HostRequest {
Wait {
session_id: SessionId,
request: WireWaitRequest,
#[serde(default, skip_serializing_if = "Option::is_none")]
max_output_tokens: Option<usize>,
},
#[serde(rename = "session/terminate")]
Terminate {
session_id: SessionId,
cell_id: WireCellId,
#[serde(default, skip_serializing_if = "Option::is_none")]
max_output_tokens: Option<usize>,
},
#[serde(rename = "session/shutdown")]
ShutdownSession { session_id: SessionId },
@@ -12,6 +12,12 @@ mod types;
/// Maximum number of unresolved delegate callbacks allowed per host connection.
pub const MAX_PENDING_DELEGATE_CALLS: usize = 1_024;
/// Stdio connection support for the optional `session/open.outputTruncation` field.
/// Each session selects its own setting after the connection hello.
pub const BOUNDED_OUTPUT_CAPABILITY: &str = "bounded-output-v1";
/// Maximum retained bytes between server-truncated observations.
pub const BOUNDED_OUTPUT_MAX_BYTES: usize = 1024 * 1024;
/// Negotiated support for cell execution resource limits on `session/open`.
pub const SESSION_RESOURCE_LIMITS_CAPABILITY: &str = "session-cell-execution-resource-limits";
@@ -38,6 +44,7 @@ pub use payload::WireContentItem;
pub use payload::WireExecuteRequest;
pub use payload::WireImageDetail;
pub use payload::WireNestedToolCall;
pub use payload::WireOutputTruncation;
pub use payload::WireRuntimeResponse;
pub use payload::WireSessionCellExecutionLimits;
pub use payload::WireToolDefinition;
@@ -8,6 +8,7 @@ use serde_json::Value as JsonValue;
use crate::CellId;
use crate::CodeModeNestedToolCall;
use crate::CodeModeOutputTruncation;
use crate::CodeModeSessionCellExecutionLimits;
use crate::CodeModeToolKind;
use crate::ExecuteRequest;
@@ -51,6 +52,32 @@ impl TryFrom<WireSessionCellExecutionLimits> for CodeModeSessionCellExecutionLim
}
}
/// The output truncation setting carried by a V1 session-open request.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum WireOutputTruncation {
Client,
Server,
}
impl From<CodeModeOutputTruncation> for WireOutputTruncation {
fn from(value: CodeModeOutputTruncation) -> Self {
match value {
CodeModeOutputTruncation::Client => Self::Client,
CodeModeOutputTruncation::Server => Self::Server,
}
}
}
impl From<WireOutputTruncation> for CodeModeOutputTruncation {
fn from(value: WireOutputTruncation) -> Self {
match value {
WireOutputTruncation::Client => Self::Client,
WireOutputTruncation::Server => Self::Server,
}
}
}
/// A cell identifier with a wire representation owned by protocol V1.
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(transparent)]
@@ -211,7 +238,8 @@ impl TryFrom<WireExecuteRequest> for ExecuteRequest {
}
}
/// The complete wait request shape supported by protocol V1.
/// The nested wait payload for protocol V1. Its optional budget is carried by
/// the enclosing `HostRequest::Wait`.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct WireWaitRequest {
@@ -233,6 +261,7 @@ impl From<WireWaitRequest> for WaitRequest {
Self {
cell_id: value.cell_id.into(),
yield_time_ms: value.yield_time_ms,
max_output_tokens: None,
}
}
}