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
This commit is contained in:
Sean Huang
2026-10-29 01:04:48 +00:00
committed by copyberry
parent 4e2dfd2230
commit edd1ae37ec
22 changed files with 1148 additions and 166 deletions
@@ -474,8 +474,11 @@ async fn run_cell<H: CellHost>(
host.closed().await;
}
/// Construct completion only after it has won the terminal decision.
fn take_completed_event(output: &mut OutputCollector, error_text: Option<String>) -> CellEvent {
/// Construct completion only after the terminal outcome is settled.
pub(crate) fn take_completed_event(
output: &mut OutputCollector,
error_text: Option<String>,
) -> CellEvent {
let error_text = error_text.map(|error_text| output.collect_error(error_text));
CellEvent::Completed {
content_items: output.take(),
@@ -178,6 +178,66 @@ fn spawn_cell_actor_harness_with_host_and_failure_handler<H: CellHost>(
}
}
#[tokio::test]
async fn termination_drains_a_send_already_blocked_by_backpressure() {
let harness = spawn_cell_actor_harness_with_host_and_failure_handler(
ObserveMode::YieldAfter(Duration::from_secs(60)),
Arc::new(TestHost::default()),
CodeModeOutputTruncation::Server,
/*task_failure_handler*/ None,
);
let (termination, capacity) = {
let RuntimeEventSender::Bounded(tx) = &harness.event_tx else {
panic!("server runtime must use a bounded sender");
};
// This test owns the V8-side events. The current-thread actor cannot run
// before our first await; fill its actual FIFO and register a sender.
for _ in 0..tx.max_capacity() {
tx.try_send(RuntimeEvent::ContentItem(OutputItem::InputText {
text: "queued".to_string(),
}))
.unwrap();
}
let mut send = std::pin::pin!(tx.send(RuntimeEvent::ContentItem(OutputItem::InputText {
text: "blocked".to_string(),
})));
assert!(futures::poll!(send.as_mut()).is_pending());
// terminate() synchronously cancels. No event was consumed between the
// Pending poll and cancellation; run_cell must drain while terminating.
let termination = harness.handle.terminate();
tokio::time::timeout(Duration::from_secs(1), send)
.await
.expect("termination must release the registered sender")
.expect("actor must drain the blocked item");
(termination, tx.max_capacity())
};
drop(harness.event_tx);
let event = tokio::time::timeout(Duration::from_secs(1), termination)
.await
.expect("termination must finish after draining")
.expect("termination event");
let mut expected = OutputCollector::new(CodeModeOutputTruncation::Server);
for _ in 0..capacity {
expected.push(OutputItem::InputText {
text: "queued".to_string(),
});
}
expected.push(OutputItem::InputText {
text: "blocked".to_string(),
});
assert_eq!(
event,
CellEvent::Terminated {
content_items: expected,
}
);
assert_eq!(harness.initial_event_rx.await.unwrap(), Ok(event));
tokio::time::timeout(Duration::from_secs(1), harness.task)
.await
.expect("actor must close")
.unwrap();
}
#[tokio::test]
async fn unexpected_runtime_thread_exit_is_reported_to_the_session_owner() {
let (failure_tx, mut failure_rx) = mpsc::unbounded_channel();