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
@@ -223,6 +223,7 @@ async fn yielded_cells_retain_their_own_delegate_until_closed() {
let actual = service
.wait(
WaitRequest {
max_output_tokens: None,
cell_id: cell_id("1"),
yield_time_ms: 60_000,
},
@@ -253,7 +254,7 @@ async fn yielded_cells_retain_their_own_delegate_until_closed() {
);
assert!(weak_a.upgrade().is_none());
service.terminate(cell_id("2")).await.unwrap();
service.terminate(cell_id("2").into()).await.unwrap();
assert_eq!(
next_event(&mut events_b).await,
DelegateEvent::ToolCancelled
@@ -293,6 +294,7 @@ async fn yields_and_resumes() {
let actual = service
.wait(
WaitRequest {
max_output_tokens: None,
cell_id: cell_id("1"),
yield_time_ms: 60_000,
},
@@ -418,7 +420,7 @@ async fn observed_natural_completion_wins_over_termination() {
})
.await
.unwrap();
let actual = service.terminate(cell_id("1")).await.unwrap();
let actual = service.terminate(cell_id("1").into()).await.unwrap();
let result_buffered_for = actual.result_buffered_for().unwrap();
assert!(!result_buffered_for.is_zero());
assert_eq!(
@@ -461,7 +463,7 @@ async fn termination_cancels_pending_callbacks_before_responding() {
}
);
assert_eq!(
service.terminate(cell_id("1")).await.unwrap(),
service.terminate(cell_id("1").into()).await.unwrap(),
WaitOutcome::LiveCell(RuntimeResponse::Terminated {
code_mode_host_duration: None,
cell_id: cell_id("1"),
@@ -541,13 +543,13 @@ async fn repeated_termination_is_rejected_while_callback_cleanup_is_pending() {
let terminating_service = Arc::clone(&service);
let first_termination =
tokio::spawn(async move { terminating_service.terminate(cell_id("1")).await });
tokio::spawn(async move { terminating_service.terminate(cell_id("1").into()).await });
assert_eq!(
next_event(&mut events_rx).await,
DelegateEvent::NotificationCancelled
);
let repeated_termination = service.terminate(cell_id("1")).await;
let repeated_termination = service.terminate(cell_id("1").into()).await;
delegate.release_notification();
assert_eq!(
@@ -594,6 +596,7 @@ async fn second_observer_is_rejected_without_displacing_the_first() {
WaitRequest {
cell_id: cell_id("1"),
yield_time_ms: 60_000,
max_output_tokens: None,
},
/*preempt*/ None,
)
@@ -604,6 +607,7 @@ async fn second_observer_is_rejected_without_displacing_the_first() {
WaitRequest {
cell_id: cell_id("1"),
yield_time_ms: 60_000,
max_output_tokens: None,
},
/*preempt*/ None,
)
@@ -618,7 +622,7 @@ async fn second_observer_is_rejected_without_displacing_the_first() {
content_items: Vec::new(),
};
assert_eq!(
service.terminate(cell_id("1")).await.unwrap(),
service.terminate(cell_id("1").into()).await.unwrap(),
WaitOutcome::LiveCell(terminated.clone())
);
assert_eq!(