Scope code mode callback delegates to individual executions (#44865)

## Why

A delegate bound at session creation cannot provide different callbacks for cells sharing that session. Each execution needs to retain its own delegate across yields and release it when the cell is cleaned up.

## What changed

- Pass `CodeModeSessionDelegate` to `execute` instead of session creation.
- Route tool calls, notifications, and cell closure callbacks through the execution's delegate in the in-process runtime and the gRPC and stdio transports.
- Retain delegates with pending executions and live cells, releasing them through closure and cancellation cleanup.

## Testing

Add coverage for distinct delegates across yielded cells, gRPC callbacks before cell admission, and delegate release after completion or abandoned execution cleanup. Update transport tests to verify callbacks reach the owning cell's delegate.

GitOrigin-RevId: 7469f52104b993e790a40c65fb800ad02b7fd606
This commit is contained in:
Abhinav
2026-09-11 17:00:07 +00:00
committed by copyberry
parent bc5957eac9
commit 3305c4f31d
30 changed files with 1027 additions and 637 deletions
@@ -133,12 +133,12 @@ impl SessionInner {
}
let invocation_id = call.invocation_id.clone();
let cancellation = match admission {
CallbackAdmission::Active(cancellation) => Ok(cancellation),
CallbackAdmission::Active(cancellation, delegate) => Ok((cancellation, delegate)),
CallbackAdmission::Cancelled => return Ok(()),
CallbackAdmission::Closed => Err(format!("code-mode cell {} is closed", call.cell_id)),
CallbackAdmission::Rejected(error) => Err(error),
};
let cancellation = match cancellation {
let (cancellation, delegate) = match cancellation {
Ok(cancellation) => cancellation,
Err(error) => {
let inner = Arc::clone(self);
@@ -157,19 +157,18 @@ impl SessionInner {
let result = match invocation {
Ok(invocation) => {
let callback = AssertUnwindSafe(async {
inner
.delegate
delegate
.invoke_tool(invocation, cancellation.child_token())
.await
})
.catch_unwind();
tokio::select! {
biased;
_ = cancellation.cancelled() => return,
result = callback => match result {
Ok(result) => result,
Err(_) => Err("code-mode tool delegate panicked".to_string()),
},
biased;
_ = cancellation.cancelled() => return,
result = callback => match result {
Ok(result) => result,
Err(_) => Err("code-mode tool delegate panicked".to_string()),
},
}
}
Err(error) => Err(error),
@@ -223,8 +222,8 @@ impl SessionInner {
.lock()
.unwrap_or_else(PoisonError::into_inner)
.admit_notification(&notification)?;
let cancellation = match admission {
CallbackAdmission::Active(cancellation) => cancellation,
let (cancellation, delegate) = match admission {
CallbackAdmission::Active(cancellation, delegate) => (cancellation, delegate),
CallbackAdmission::Cancelled | CallbackAdmission::Closed => return Ok(()),
CallbackAdmission::Rejected(error) => {
warn!("code-mode notification was dropped: {error}");
@@ -237,8 +236,7 @@ impl SessionInner {
// them without waiting for arbitrary delegate work to complete.
tokio::spawn(async move {
let callback = AssertUnwindSafe(async {
inner
.delegate
delegate
.notify(
notification.call_id,
CellId::new(notification.cell_id),