mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## Why Code-mode tool results could return to the model while an MCP elicitation was still waiting for user input. This differed from parallel tool calling and could let the model continue before the user resolved the request. We need one session-level view of outstanding elicitations so tool runtimes can consistently hold results until every pending elicitation is resolved. ## What changed - Added a counted, session-owned ElicitationService with RAII registrations. - Registered both core-originated and server-originated MCP elicitations with the service. - Migrated out-of-band elicitation tracking and unified exec timeout pausing to the shared service. - Made code-mode functions.exec and functions.wait capture their runtime result normally, then hold it before returning while an elicitation is outstanding. - Kept terminate: true immediate; only its result is held. - Preserved model-visible wall time across the elicitation hold. - Kept the behavior session-scoped, with concurrent elicitations holding the pause until all registrations are released.
101 lines
2.4 KiB
Rust
101 lines
2.4 KiB
Rust
use std::sync::Arc;
|
|
use std::sync::Mutex;
|
|
|
|
use tokio::sync::watch;
|
|
|
|
/// Coordinates user elicitations that pause tool-result delivery for a session.
|
|
///
|
|
/// Registrations are counted so concurrent elicitations keep the session paused until all of them
|
|
/// finish. Consumers can subscribe to pause timeout progress or wait before returning an already
|
|
/// captured result.
|
|
#[derive(Clone)]
|
|
pub(crate) struct ElicitationService {
|
|
inner: Arc<Inner>,
|
|
}
|
|
|
|
struct Inner {
|
|
state: Mutex<State>,
|
|
paused: watch::Sender<bool>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct State {
|
|
outstanding: i64,
|
|
}
|
|
|
|
pub(crate) struct ElicitationRegistration {
|
|
service: ElicitationService,
|
|
}
|
|
|
|
impl ElicitationService {
|
|
pub(crate) fn new() -> Self {
|
|
let (paused, _paused_rx) = watch::channel(false);
|
|
Self {
|
|
inner: Arc::new(Inner {
|
|
state: Mutex::new(State::default()),
|
|
paused,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn register(&self) -> ElicitationRegistration {
|
|
self.increment();
|
|
ElicitationRegistration {
|
|
service: self.clone(),
|
|
}
|
|
}
|
|
|
|
fn increment(&self) {
|
|
let mut state = self
|
|
.inner
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let was_clear = state.outstanding == 0;
|
|
assert_ne!(
|
|
state.outstanding,
|
|
i64::MAX,
|
|
"outstanding elicitation count overflowed"
|
|
);
|
|
state.outstanding += 1;
|
|
if was_clear {
|
|
self.inner.paused.send_replace(true);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn subscribe(&self) -> watch::Receiver<bool> {
|
|
self.inner.paused.subscribe()
|
|
}
|
|
|
|
pub(crate) async fn wait_until_clear(&self) {
|
|
let mut paused = self.subscribe();
|
|
let _ = paused.wait_for(|paused| !*paused).await;
|
|
}
|
|
|
|
fn decrement(&self) {
|
|
let mut state = self
|
|
.inner
|
|
.state
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
assert!(
|
|
state.outstanding > 0,
|
|
"elicitation registration count underflowed"
|
|
);
|
|
state.outstanding -= 1;
|
|
if state.outstanding == 0 {
|
|
self.inner.paused.send_replace(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for ElicitationRegistration {
|
|
fn drop(&mut self) {
|
|
self.service.decrement();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "elicitation_tests.rs"]
|
|
mod tests;
|