Skip to content

Commit f66d793

Browse files
authored
[codex] route sleep through time providers (#29973)
## Summary - add a cancellable sleep operation to `TimeProvider` - route `clock.sleep` through the configured provider - extend the supported sleep duration to 12 hours - complete the sleep turn item before propagating provider failures ## Why This isolates the core clock abstraction needed by external clock integrations. Existing system and app-server behavior remains wall-clock based in this PR; the stacked follow-up supplies app-server sleeps from an external clock.
1 parent 22f1256 commit f66d793

5 files changed

Lines changed: 130 additions & 12 deletions

File tree

codex-rs/app-server/src/current_time.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use chrono::Utc;
1010
use codex_app_server_protocol::CurrentTimeReadParams;
1111
use codex_app_server_protocol::CurrentTimeReadResponse;
1212
use codex_app_server_protocol::ServerRequestPayload;
13+
use codex_core::SleepFuture;
1314
use codex_core::TimeFuture;
1415
use codex_core::TimeProvider;
1516
use codex_protocol::ThreadId;
@@ -49,6 +50,13 @@ impl TimeProvider for AppServerTimeProvider {
4950
request_current_time(outgoing, thread_state_manager, thread_id).await
5051
})
5152
}
53+
54+
fn sleep(&self, _thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> {
55+
Box::pin(async move {
56+
tokio::time::sleep(duration).await;
57+
Ok(())
58+
})
59+
}
5260
}
5361

5462
async fn request_current_time(

codex-rs/core/src/current_time.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::future::Future;
22
use std::pin::Pin;
33
use std::sync::Arc;
4+
use std::time::Duration;
45

56
use anyhow::Result;
67
use anyhow::anyhow;
@@ -12,10 +13,16 @@ use codex_protocol::ThreadId;
1213
use crate::config::CurrentTimeReminderConfig;
1314

1415
pub type TimeFuture<'a> = Pin<Box<dyn Future<Output = Result<DateTime<Utc>>> + Send + 'a>>;
16+
pub type SleepFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
1517

16-
/// Host integration boundary for obtaining the current time.
18+
/// Host integration boundary for reading and waiting on the current time.
1719
pub trait TimeProvider: Send + Sync {
1820
fn current_time(&self, thread_id: ThreadId) -> TimeFuture<'_>;
21+
22+
/// Waits for the given duration on this provider's clock.
23+
///
24+
/// Dropping the returned future cancels the wait.
25+
fn sleep(&self, thread_id: ThreadId, duration: Duration) -> SleepFuture<'_>;
1926
}
2027

2128
pub(crate) struct SystemTimeProvider;
@@ -24,6 +31,13 @@ impl TimeProvider for SystemTimeProvider {
2431
fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> {
2532
Box::pin(async { Ok(Utc::now()) })
2633
}
34+
35+
fn sleep(&self, _thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> {
36+
Box::pin(async move {
37+
tokio::time::sleep(duration).await;
38+
Ok(())
39+
})
40+
}
2741
}
2842

2943
pub(crate) fn resolve_time_provider(

codex-rs/core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ pub use client_common::ResponseEvent;
189189
pub use client_common::ResponseStream;
190190
pub use codex_prompts::REVIEW_PROMPT;
191191
pub use compact::content_items_to_text;
192+
pub use current_time::SleepFuture;
192193
pub use current_time::TimeFuture;
193194
pub use current_time::TimeProvider;
194195
pub use event_mapping::parse_turn_item;

codex-rs/core/src/tools/handlers/sleep.rs

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use std::time::Instant;
2121

2222
const NAMESPACE: &str = "clock";
2323
const TOOL_NAME: &str = "sleep";
24-
const MAX_SLEEP_DURATION_MS: u64 = 3_600_000;
24+
const MAX_SLEEP_DURATION_MS: u64 = 12 * 60 * 60 * 1000;
2525

2626
pub struct SleepHandler;
2727

@@ -102,24 +102,36 @@ impl ToolExecutor<ToolInvocation> for SleepHandler {
102102
.input_queue
103103
.subscribe_activity(turn_state.as_deref())
104104
.await;
105-
let interrupted = if pending_activity.is_some() {
106-
true
105+
let sleep_result: Result<bool, FunctionCallError> = if pending_activity.is_some() {
106+
Ok(true)
107107
} else {
108-
let sleep = tokio::time::sleep(Duration::from_millis(args.duration_ms));
108+
let sleep = session
109+
.services
110+
.time_provider
111+
.sleep(session.thread_id, Duration::from_millis(args.duration_ms));
109112
tokio::pin!(sleep);
110113
tokio::select! {
111-
() = &mut sleep => false,
114+
result = &mut sleep => result
115+
.map(|()| false)
116+
.map_err(|err| {
117+
FunctionCallError::Fatal(format!("failed to sleep: {err:#}"))
118+
}),
112119
result = activity_rx.changed() => {
113120
if result.is_ok() {
114-
true
121+
Ok(true)
115122
} else {
116-
sleep.await;
117-
false
123+
sleep
124+
.await
125+
.map(|()| false)
126+
.map_err(|err| {
127+
FunctionCallError::Fatal(format!("failed to sleep: {err:#}"))
128+
})
118129
}
119130
}
120131
}
121132
};
122133
session.emit_turn_item_completed(turn.as_ref(), item).await;
134+
let interrupted = sleep_result?;
123135

124136
let message = if interrupted {
125137
"Sleep interrupted by new input."

codex-rs/core/tests/suite/current_time_reminder.rs

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
use std::sync::Arc;
22
use std::sync::atomic::AtomicI64;
3+
use std::sync::atomic::AtomicU64;
34
use std::sync::atomic::Ordering;
5+
use std::time::Duration;
46

57
use anyhow::Result;
68
use anyhow::anyhow;
79
use chrono::DateTime;
810
use chrono::Utc;
11+
use codex_core::SleepFuture;
912
use codex_core::TimeFuture;
1013
use codex_core::TimeProvider;
1114
use codex_core::config::CurrentTimeReminderConfig;
@@ -40,22 +43,34 @@ const SECOND_REMINDER: &str = "It is 2026-06-17 17:35:15 UTC.";
4043
const THIRD_REMINDER: &str = "It is 2026-06-17 17:36:15 UTC.";
4144
const FIRST_TIME_UNIX_SECONDS: i64 = 1_781_717_655;
4245

43-
struct TestTimeProvider(AtomicI64);
46+
struct TestTimeProvider {
47+
current_time: AtomicI64,
48+
sleep_seconds: AtomicU64,
49+
}
4450

4551
impl Default for TestTimeProvider {
4652
fn default() -> Self {
47-
Self(AtomicI64::new(FIRST_TIME_UNIX_SECONDS))
53+
Self {
54+
current_time: AtomicI64::new(FIRST_TIME_UNIX_SECONDS),
55+
sleep_seconds: AtomicU64::new(0),
56+
}
4857
}
4958
}
5059

5160
impl TimeProvider for TestTimeProvider {
5261
fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> {
53-
let timestamp = self.0.fetch_add(60, Ordering::Relaxed);
62+
let timestamp = self.current_time.fetch_add(60, Ordering::Relaxed);
5463
Box::pin(async move {
5564
Ok(DateTime::<Utc>::from_timestamp(timestamp, 0)
5665
.expect("test timestamp should be valid"))
5766
})
5867
}
68+
69+
fn sleep(&self, _thread_id: ThreadId, duration: Duration) -> SleepFuture<'_> {
70+
self.sleep_seconds
71+
.store(duration.as_secs(), Ordering::Relaxed);
72+
Box::pin(async { Ok(()) })
73+
}
5974
}
6075

6176
struct FailingTimeProvider;
@@ -64,6 +79,10 @@ impl TimeProvider for FailingTimeProvider {
6479
fn current_time(&self, _thread_id: ThreadId) -> TimeFuture<'_> {
6580
Box::pin(async { Err(anyhow!("test clock unavailable")) })
6681
}
82+
83+
fn sleep(&self, _thread_id: ThreadId, _duration: Duration) -> SleepFuture<'_> {
84+
Box::pin(async { Err(anyhow!("test clock unavailable")) })
85+
}
6786
}
6887

6988
fn current_time_reminders(request: &ResponsesRequest) -> Vec<String> {
@@ -325,3 +344,67 @@ async fn current_time_tool_returns_the_latest_time() -> Result<()> {
325344

326345
Ok(())
327346
}
347+
348+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
349+
async fn sleep_tool_uses_configured_time_provider() -> Result<()> {
350+
skip_if_no_network!(Ok(()));
351+
352+
const CALL_ID: &str = "sleep";
353+
const DURATION_MS: u64 = 12 * 60 * 60 * 1000;
354+
355+
let server = start_mock_server().await;
356+
let responses = mount_sse_sequence(
357+
&server,
358+
vec![
359+
sse(vec![
360+
ev_response_created("resp-1"),
361+
ev_function_call_with_namespace(
362+
CALL_ID,
363+
"clock",
364+
"sleep",
365+
&json!({ "duration_ms": DURATION_MS }).to_string(),
366+
),
367+
ev_completed("resp-1"),
368+
]),
369+
sse(vec![
370+
ev_response_created("resp-2"),
371+
ev_assistant_message("msg-2", "done"),
372+
ev_completed("resp-2"),
373+
]),
374+
],
375+
)
376+
.await;
377+
let time_provider = Arc::new(TestTimeProvider::default());
378+
let test = test_codex()
379+
.with_config(|config| {
380+
enable_current_time_reminder(
381+
config,
382+
/*interval*/ 3_000,
383+
CurrentTimeSource::External,
384+
);
385+
config
386+
.current_time_reminder
387+
.as_mut()
388+
.expect("current-time reminder config should be present")
389+
.sleep_tool = true;
390+
})
391+
.with_external_time_provider(time_provider.clone())
392+
.build(&server)
393+
.await?;
394+
395+
test.submit_turn("sleep").await?;
396+
397+
assert_eq!(
398+
time_provider.sleep_seconds.load(Ordering::Relaxed),
399+
DURATION_MS / 1_000
400+
);
401+
let requests = responses.requests();
402+
assert_eq!(requests.len(), 2);
403+
assert!(
404+
requests[1]
405+
.function_call_output_text(CALL_ID)
406+
.is_some_and(|output| output.ends_with("Sleep completed."))
407+
);
408+
409+
Ok(())
410+
}

0 commit comments

Comments
 (0)