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
+1
View File
@@ -234,6 +234,7 @@ impl GrpcCodeModeHost {
let request = WaitRequest {
cell_id: CellId::new(request.cell_id),
yield_time_ms: request.yield_time_ms,
max_output_tokens: None,
};
let outcome = tokio::select! {
biased;
@@ -255,6 +255,7 @@ async fn session_closure_cancels_pending_termination() {
let termination = host.terminate(Request::new(proto::TerminateRequest {
session_id,
cell_id,
max_output_tokens: None,
}));
tokio::pin!(termination);
assert!(termination.as_mut().now_or_never().is_none());
@@ -14,6 +14,7 @@ pub(super) async fn open_session(
let mut stream = host
.open_session(Request::new(proto::OpenSessionRequest {
cell_execution_limits: None,
output_truncation: None,
}))
.await
.expect("open code-mode session")
@@ -120,6 +121,7 @@ async fn execute_stream_starts_immediately_and_wait_preserves_missing_cells() {
cell_id: cell_id.clone(),
wait_id: "wait-1".to_string(),
yield_time_ms: 60_000,
max_output_tokens: None,
}))
.await
.expect("wait for completion")
@@ -163,6 +165,7 @@ async fn execute_stream_starts_immediately_and_wait_preserves_missing_cells() {
cell_id: "missing-cell".to_string(),
wait_id: "wait-missing".to_string(),
yield_time_ms: 1,
max_output_tokens: None,
}))
.await
.expect("missing cells remain successful wait outcomes")
@@ -355,6 +358,7 @@ async fn cancellation_before_wait_admission_is_preserved() {
cell_id: cell_id.clone(),
wait_id: "pre-cancelled".to_string(),
yield_time_ms: 60_000,
max_output_tokens: None,
}))
.await
.unwrap_err();
@@ -363,6 +367,7 @@ async fn cancellation_before_wait_admission_is_preserved() {
host.terminate(Request::new(proto::TerminateRequest {
session_id,
cell_id,
max_output_tokens: None,
}))
.await
.unwrap();
+2 -1
View File
@@ -124,6 +124,7 @@ impl GrpcHostState {
.try_send(Ok(proto::SessionEvent {
event: Some(proto::session_event::Event::Opened(proto::SessionOpened {
session_id: id.to_string(),
output_truncation: None,
})),
}))
.map_err(|_| Status::internal("failed to publish the opened code-mode session"))?;
@@ -263,7 +264,7 @@ impl GrpcSession {
_ = self.closed.cancelled() => {
Err(Status::cancelled("code-mode session is closed"))
}
result = self.runtime.terminate(cell_id) => {
result = self.runtime.terminate(cell_id.into()) => {
result.map_err(Status::failed_precondition)
}
}
+6 -1
View File
@@ -127,6 +127,7 @@ async fn handshake_and_multiple_session_lifecycles_are_ordered() {
request: HostRequest::OpenSession {
session_id: session_id(id),
cell_execution_limits: None,
output_truncation: None,
},
})
.await
@@ -198,6 +199,7 @@ async fn disconnect_cancels_a_backpressured_host_writer() {
request: HostRequest::OpenSession {
session_id: session_id("backpressured-session"),
cell_execution_limits: None,
output_truncation: None,
},
})
.await
@@ -339,6 +341,7 @@ async fn incompatible_or_invalid_handshake_is_rejected() {
request: HostRequest::OpenSession {
session_id: session_id("session-1"),
cell_execution_limits: None,
output_truncation: None,
},
})
.await
@@ -405,6 +408,7 @@ async fn session_id_cannot_be_reused_after_shutdown() {
HostRequest::OpenSession {
session_id: id.clone(),
cell_execution_limits: None,
output_truncation: None,
},
),
(
@@ -433,6 +437,7 @@ async fn session_id_cannot_be_reused_after_shutdown() {
request: HostRequest::OpenSession {
session_id: id,
cell_execution_limits: None,
output_truncation: None,
},
})
.await
@@ -556,7 +561,7 @@ async fn execute_request_id_remains_active_until_initial_response() {
state
.session(&session_id)
.expect("session")
.terminate(cell_id.into())
.terminate(codex_code_mode_protocol::CellId::from(cell_id).into())
.await
.expect("terminate cell");
state.disconnect().await;
+14 -7
View File
@@ -382,6 +382,7 @@ impl HostState {
HostRequest::OpenSession {
session_id,
cell_execution_limits,
..
} => {
let result = CodeModeSessionCellExecutionLimits::try_from(
cell_execution_limits.unwrap_or_default(),
@@ -452,6 +453,7 @@ impl HostState {
HostRequest::Wait {
session_id,
request,
..
} => {
let result = match self.session(&session_id) {
Ok(session) => {
@@ -476,15 +478,20 @@ impl HostState {
HostRequest::Terminate {
session_id,
cell_id,
..
} => {
let result = match self.session(&session_id) {
Ok(session) => session.terminate(cell_id.into()).await.and_then(|outcome| {
let outcome = outcome.with_code_mode_host_duration(received_at.elapsed());
Ok(HostResponse::WaitCompleted {
outcome: WireWaitOutcome::try_from(outcome)
.map_err(|error| error.to_string())?,
})
}),
Ok(session) => session
.terminate(codex_code_mode_protocol::CellId::from(cell_id).into())
.await
.and_then(|outcome| {
let outcome =
outcome.with_code_mode_host_duration(received_at.elapsed());
Ok(HostResponse::WaitCompleted {
outcome: WireWaitOutcome::try_from(outcome)
.map_err(|error| error.to_string())?,
})
}),
Err(err) => Err(err),
};
self.respond(request_id, result);