diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index bf2e21ceb1..8cdaa316a8 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3848,6 +3848,7 @@ version = "0.0.0" dependencies = [ "anyhow", "chrono", + "codex-extension-items", "codex-file-search", "codex-git-utils", "codex-otel", diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index edac4ac177..d9546e9db0 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -410,7 +410,7 @@ impl TurnToolCounts { | ThreadItem::Plan { .. } | ThreadItem::Reasoning { .. } | ThreadItem::ImageView { .. } - | ThreadItem::Sleep { .. } + | ThreadItem::Sleep(_) | ThreadItem::EnteredReviewMode { .. } | ThreadItem::ExitedReviewMode { .. } | ThreadItem::ContextCompaction { .. } => return, @@ -1747,7 +1747,7 @@ fn tracked_tool_item_id(item: &ThreadItem) -> Option<&str> { | ThreadItem::Reasoning { .. } | ThreadItem::SubAgentActivity { .. } | ThreadItem::ImageView { .. } - | ThreadItem::Sleep { .. } + | ThreadItem::Sleep(_) | ThreadItem::EnteredReviewMode { .. } | ThreadItem::ExitedReviewMode { .. } | ThreadItem::ContextCompaction { .. } => None, diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index ab03782849..6c73af08af 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -4569,6 +4569,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index b841d5243c..fe0deb8bd4 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -18648,6 +18648,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 0d05498df3..e637ea4563 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -16427,6 +16427,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index bc2b354bd4..e9176c977f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -1152,6 +1152,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index a226ce129b..1180415836 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -1152,6 +1152,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index db96b3c149..dbb79ef66c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -1297,6 +1297,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index ad36cab226..6bd6415b43 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1825,6 +1825,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 9b1ecd6007..702910a24f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1616,6 +1616,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index f9cd2152bc..0515964e8b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -1616,6 +1616,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 20587d98c6..69dd3e36bd 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1616,6 +1616,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index e44a3c638b..ca5249bfb0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1825,6 +1825,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index f69cab4f24..9a57d822e9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -1616,6 +1616,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index a697e9d91a..66b5ccca38 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1825,6 +1825,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 32220637e9..d4202df338 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1616,6 +1616,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 16da7ae525..5e1321c75e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1616,6 +1616,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 73cf9e470d..fd9c02bf0b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -1297,6 +1297,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index a11434c512..06658fd0ae 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -1297,6 +1297,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 78123128e2..e5b456ae31 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -1297,6 +1297,7 @@ "type": "object" }, { + "description": "Display item emitted by the interruptible `clock.sleep` tool.", "properties": { "durationMs": { "format": "uint64", diff --git a/codex-rs/app-server-protocol/schema/typescript/SleepItem.ts b/codex-rs/app-server-protocol/schema/typescript/SleepItem.ts new file mode 100644 index 0000000000..b399551c8b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/SleepItem.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Display item emitted by the interruptible `clock.sleep` tool. + */ +export type SleepItem = { id: string, durationMs: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 8afbbcc981..30f145590c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -77,6 +77,7 @@ export type { ServerNotificationEnvelope } from "./ServerNotificationEnvelope"; export type { ServerRequest } from "./ServerRequest"; export type { SessionSource } from "./SessionSource"; export type { Settings } from "./Settings"; +export type { SleepItem } from "./SleepItem"; export type { SubAgentSource } from "./SubAgentSource"; export type { ThreadId } from "./ThreadId"; export type { ThreadMemoryMode } from "./ThreadMemoryMode"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index a445385581..df1a5dbe34 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -5,6 +5,7 @@ import type { ImageGenerationItem } from "../ImageGenerationItem"; import type { LegacyAppPathString } from "../LegacyAppPathString"; import type { MessagePhase } from "../MessagePhase"; import type { ReasoningEffort } from "../ReasoningEffort"; +import type { SleepItem } from "../SleepItem"; import type { WebSearchItem } from "../WebSearchItem"; import type { JsonValue } from "../serde_json/JsonValue"; import type { CollabAgentState } from "./CollabAgentState"; @@ -105,4 +106,4 @@ reasoningEffort: ReasoningEffort | null, /** * Last known status of the target agents, when available. */ -agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep", id: string, durationMs: number, } | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; +agentsStates: { [key in string]?: CollabAgentState }, } | { "type": "subAgentActivity", id: string, kind: SubAgentActivityKind, agentThreadId: string, agentPath: string, } | { "type": "webSearch" } & WebSearchItem | { "type": "imageView", id: string, path: LegacyAppPathString, } | { "type": "sleep" } & SleepItem | { "type": "imageGeneration" } & ImageGenerationItem | { "type": "enteredReviewMode", id: string, review: string, } | { "type": "exitedReviewMode", id: string, review: string, } | { "type": "contextCompaction", id: string, }; diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index de611a131a..deac74a698 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -599,8 +599,7 @@ impl ThreadHistoryBuilder { ); let should_upsert = match item { codex_protocol::items::TurnItem::Plan(plan) => !plan.text.is_empty(), - codex_protocol::items::TurnItem::Sleep(_) - | codex_protocol::items::TurnItem::HookPrompt(_) + codex_protocol::items::TurnItem::HookPrompt(_) | codex_protocol::items::TurnItem::CommandExecution(_) | codex_protocol::items::TurnItem::DynamicToolCall(_) | codex_protocol::items::TurnItem::CollabAgentToolCall(_) @@ -1588,6 +1587,7 @@ mod tests { use super::*; use crate::protocol::v2::CommandExecutionSource; use codex_extension_items::ExtensionItem as CoreExtensionItem; + use codex_extension_items::sleep::SleepItem as CoreSleepItem; use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; use codex_protocol::items::CommandExecutionItem as CoreCommandExecutionItem; @@ -1595,7 +1595,6 @@ mod tests { use codex_protocol::items::EnteredReviewModeItem as CoreEnteredReviewModeItem; use codex_protocol::items::ExitedReviewModeItem as CoreExitedReviewModeItem; use codex_protocol::items::HookPromptFragment as CoreHookPromptFragment; - use codex_protocol::items::SleepItem as CoreSleepItem; use codex_protocol::items::TurnItem as CoreTurnItem; use codex_protocol::items::UserMessageItem as CoreUserMessageItem; use codex_protocol::items::build_hook_prompt_message; @@ -1959,10 +1958,10 @@ mod tests { fn rebuilds_sleep_item_from_persisted_completion() { let turn_id = "turn-1"; let thread_id = ThreadId::new(); - let sleep_item = CoreTurnItem::Sleep(CoreSleepItem { + let sleep_item = CoreTurnItem::Extension(CoreExtensionItem::Sleep(CoreSleepItem { id: "sleep-1".to_string(), duration_ms: 1_000, - }); + })); let events = vec![ EventMsg::TurnStarted(TurnStartedEvent { turn_id: turn_id.to_string(), @@ -1997,10 +1996,10 @@ mod tests { assert_eq!(turns.len(), 1); assert_eq!( turns[0].items, - vec![ThreadItem::Sleep { + vec![ThreadItem::Sleep(CoreSleepItem { id: "sleep-1".to_string(), duration_ms: 1_000, - }] + })] ); } diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index 9fb41c1be6..8b88a8f92e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -14,6 +14,7 @@ use crate::protocol::item_builders::review_output_text; use codex_experimental_api_macros::ExperimentalApi; use codex_extension_items::ExtensionItem; pub use codex_extension_items::image_generation::ImageGenerationItem; +pub use codex_extension_items::sleep::SleepItem; pub use codex_extension_items::web_search::WebSearchAction; pub use codex_extension_items::web_search::WebSearchItem; use codex_protocol::approvals::GuardianAssessmentAction as CoreGuardianAssessmentAction; @@ -368,13 +369,7 @@ pub enum ThreadItem { id: String, path: LegacyAppPathString, }, - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Sleep { - id: String, - #[ts(type = "number")] - duration_ms: u64, - }, + Sleep(SleepItem), ImageGeneration(ImageGenerationItem), #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -430,11 +425,11 @@ impl ThreadItem { | ThreadItem::CollabAgentToolCall { id, .. } | ThreadItem::SubAgentActivity { id, .. } | ThreadItem::ImageView { id, .. } - | ThreadItem::Sleep { id, .. } | ThreadItem::EnteredReviewMode { id, .. } | ThreadItem::ExitedReviewMode { id, .. } | ThreadItem::ContextCompaction { id, .. } => id, ThreadItem::WebSearch(item) => &item.id, + ThreadItem::Sleep(item) => &item.id, ThreadItem::ImageGeneration(item) => &item.id, } } @@ -908,12 +903,9 @@ impl From for ThreadItem { id: image.id, path: image.path.into(), }, - CoreTurnItem::Sleep(sleep) => ThreadItem::Sleep { - id: sleep.id, - duration_ms: sleep.duration_ms, - }, CoreTurnItem::Extension(extension) => match extension { ExtensionItem::ImageGeneration(item) => ThreadItem::ImageGeneration(item), + ExtensionItem::Sleep(item) => ThreadItem::Sleep(item), ExtensionItem::WebSearch(item) => ThreadItem::WebSearch(item), }, CoreTurnItem::ImageGeneration(image) => { diff --git a/codex-rs/app-server/tests/suite/v2/sleep.rs b/codex-rs/app-server/tests/suite/v2/sleep.rs index 96885ae8d8..bdbd29f63a 100644 --- a/codex-rs/app-server/tests/suite/v2/sleep.rs +++ b/codex-rs/app-server/tests/suite/v2/sleep.rs @@ -7,6 +7,7 @@ use codex_app_server_protocol::ItemStartedNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::SleepItem; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; @@ -112,10 +113,10 @@ async fn external_sleep_polls_current_time_and_emits_items() -> Result<()> { ) .await??; - let expected_item = ThreadItem::Sleep { + let expected_item = ThreadItem::Sleep(SleepItem { id: CALL_ID.to_string(), duration_ms: DURATION_MS, - }; + }); assert!(completed.completed_at_ms >= started.started_at_ms); assert_eq!( started, @@ -151,7 +152,7 @@ async fn wait_for_sleep_started( .await??; let started: ItemStartedNotification = serde_json::from_value(notification.params.expect("item/started params"))?; - if matches!(&started.item, ThreadItem::Sleep { id, .. } if id == call_id) { + if matches!(&started.item, ThreadItem::Sleep(item) if item.id == call_id) { return Ok(started); } } @@ -169,7 +170,7 @@ async fn wait_for_sleep_completed( .await??; let completed: ItemCompletedNotification = serde_json::from_value(notification.params.expect("item/completed params"))?; - if matches!(&completed.item, ThreadItem::Sleep { id, .. } if id == call_id) { + if matches!(&completed.item, ThreadItem::Sleep(item) if item.id == call_id) { return Ok(completed); } } diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index a575f32df5..de9d118e9c 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -37,6 +37,7 @@ codex-core-plugins = { workspace = true } codex-core-skills = { workspace = true } codex-exec-server = { workspace = true } codex-extension-api = { workspace = true } +codex-extension-items = { workspace = true } codex-features = { workspace = true } codex-feedback = { workspace = true } codex-file-system = { workspace = true } @@ -134,7 +135,6 @@ openssl-sys = { workspace = true, features = ["vendored"] } codex-shell-escalation = { workspace = true } [dev-dependencies] -codex-extension-items = { workspace = true } assert_cmd = { workspace = true } assert_matches = { workspace = true } codex-image-generation-extension = { workspace = true } diff --git a/codex-rs/core/src/tools/handlers/sleep.rs b/codex-rs/core/src/tools/handlers/sleep.rs index acb45ca909..ecc55c2e15 100644 --- a/codex-rs/core/src/tools/handlers/sleep.rs +++ b/codex-rs/core/src/tools/handlers/sleep.rs @@ -6,7 +6,8 @@ use crate::tools::context::boxed_tool_output; use crate::tools::handlers::parse_arguments; use crate::tools::registry::CoreToolRuntime; use crate::tools::registry::ToolExecutor; -use codex_protocol::items::SleepItem; +use codex_extension_items::ExtensionItem; +use codex_extension_items::sleep::SleepItem; use codex_protocol::items::TurnItem; use codex_tools::JsonSchema; use codex_tools::ResponsesApiNamespace; @@ -89,10 +90,10 @@ impl ToolExecutor for SleepHandler { } let started = Instant::now(); - let item = TurnItem::Sleep(SleepItem { + let item = TurnItem::Extension(ExtensionItem::Sleep(SleepItem { id: call_id, duration_ms: args.duration_ms, - }); + })); session.emit_turn_item_started(turn.as_ref(), &item).await; let turn_state = session .input_queue diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 9c9852a276..2f15ff4953 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -3,9 +3,10 @@ use std::sync::Arc; use codex_core::CodexThread; use codex_core::config::CurrentTimeReminderConfig; +use codex_extension_items::ExtensionItem; +use codex_extension_items::sleep::SleepItem; use codex_features::Feature; use codex_protocol::AgentPath; -use codex_protocol::items::SleepItem; use codex_protocol::items::TurnItem; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; @@ -244,14 +245,17 @@ async fn wait_for_sleep_item_started(codex: &CodexThread, call_id: &str, duratio matches!( event, EventMsg::ItemStarted(started) - if matches!(&started.item, TurnItem::Sleep(item) if item.id == call_id) + if matches!( + &started.item, + TurnItem::Extension(ExtensionItem::Sleep(item)) if item.id == call_id + ) ) }) .await; let EventMsg::ItemStarted(started) = event else { unreachable!("wait predicate only accepts item/started events"); }; - let TurnItem::Sleep(item) = started.item else { + let TurnItem::Extension(ExtensionItem::Sleep(item)) = started.item else { unreachable!("wait predicate only accepts sleep items"); }; assert_eq!( @@ -268,14 +272,17 @@ async fn wait_for_sleep_item_completed(codex: &CodexThread, call_id: &str, durat matches!( event, EventMsg::ItemCompleted(completed) - if matches!(&completed.item, TurnItem::Sleep(item) if item.id == call_id) + if matches!( + &completed.item, + TurnItem::Extension(ExtensionItem::Sleep(item)) if item.id == call_id + ) ) }) .await; let EventMsg::ItemCompleted(completed) = event else { unreachable!("wait predicate only accepts item/completed events"); }; - let TurnItem::Sleep(item) = completed.item else { + let TurnItem::Extension(ExtensionItem::Sleep(item)) = completed.item else { unreachable!("wait predicate only accepts sleep items"); }; assert_eq!( @@ -442,7 +449,7 @@ async fn any_new_input_interrupts_sleep() { .filter_map(|line| serde_json::from_str::(line).ok()) .filter_map(|line| match line.item { RolloutItem::EventMsg(EventMsg::ItemCompleted(event)) => match event.item { - TurnItem::Sleep(item) => Some(item), + TurnItem::Extension(ExtensionItem::Sleep(item)) => Some(item), _ => None, }, _ => None, diff --git a/codex-rs/ext/items/src/lib.rs b/codex-rs/ext/items/src/lib.rs index 2fe91cfac8..e1725f7c53 100644 --- a/codex-rs/ext/items/src/lib.rs +++ b/codex-rs/ext/items/src/lib.rs @@ -9,6 +9,7 @@ use serde::Serialize; use ts_rs::TS; pub mod image_generation; +pub mod sleep; pub mod web_search; /// Canonical extension-owned turn item carried through core lifecycle events. @@ -35,6 +36,9 @@ pub enum ExtensionItem { #[serde(rename = "image_gen.generation")] #[ts(rename = "image_gen.generation")] ImageGeneration(image_generation::ImageGenerationItem), + #[serde(rename = "clock.sleep")] + #[ts(rename = "clock.sleep")] + Sleep(sleep::SleepItem), #[serde(rename = "web.search")] #[ts(rename = "web.search")] WebSearch(web_search::WebSearchItem), @@ -46,6 +50,7 @@ impl ExtensionItem { pub fn id(&self) -> &str { match self { Self::ImageGeneration(item) => &item.id, + Self::Sleep(item) => &item.id, Self::WebSearch(item) => &item.id, } } diff --git a/codex-rs/ext/items/src/sleep.rs b/codex-rs/ext/items/src/sleep.rs new file mode 100644 index 0000000000..4cca51e16e --- /dev/null +++ b/codex-rs/ext/items/src/sleep.rs @@ -0,0 +1,14 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +/// Display item emitted by the interruptible `clock.sleep` tool. +#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase")] +pub struct SleepItem { + pub id: String, + #[ts(type = "number")] + pub duration_ms: u64, +} diff --git a/codex-rs/ext/items/src/tests.rs b/codex-rs/ext/items/src/tests.rs index 18c5085a13..036d4254e2 100644 --- a/codex-rs/ext/items/src/tests.rs +++ b/codex-rs/ext/items/src/tests.rs @@ -3,6 +3,7 @@ use serde_json::json; use super::ExtensionItem; use super::image_generation::ImageGenerationItem; +use super::sleep::SleepItem; use super::web_search::WebSearchAction; use super::web_search::WebSearchItem; @@ -84,6 +85,28 @@ fn web_search_item_preserves_stable_wire_shape() { ); } +#[test] +fn sleep_item_preserves_stable_wire_shape() { + let item = ExtensionItem::Sleep(SleepItem { + id: "sleep-1".to_string(), + duration_ms: 1_000, + }); + let value = serde_json::to_value(&item).expect("serialize extension item"); + + assert_eq!( + value, + json!({ + "kind": "clock.sleep", + "id": "sleep-1", + "durationMs": 1_000, + }) + ); + assert_eq!( + serde_json::from_value::(value).expect("deserialize extension item"), + item + ); +} + #[test] fn unknown_extension_kind_is_rejected() { let value = json!({ diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index 14792421a4..68eab25692 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -57,11 +57,10 @@ pub enum TurnItem { /// schema is owned by the web-search extension. WebSearch(WebSearchItem), ImageView(ImageViewItem), - Sleep(SleepItem), /// Item whose schema and lifecycle details are owned by an extension. /// - /// Standalone image generation and web search use this path. App-server - /// wraps the same typed items in their public variants. + /// Standalone image generation, sleep, and web search use this path. + /// App-server wraps the same typed items in their public variants. Extension(ExtensionItem), /// Hosted Responses API image-generation item handled directly by core. /// @@ -320,12 +319,6 @@ pub struct ImageViewItem { pub path: PathUri, } -#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq, Eq)] -pub struct SleepItem { - pub id: String, - pub duration_ms: u64, -} - #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)] pub struct ImageGenerationItem { pub id: String, @@ -645,7 +638,6 @@ impl TurnItem { TurnItem::SubAgentActivity(item) => item.id.clone(), TurnItem::WebSearch(item) => item.id.clone(), TurnItem::ImageView(item) => item.id.clone(), - TurnItem::Sleep(item) => item.id.clone(), TurnItem::Extension(item) => item.id().to_string(), TurnItem::ImageGeneration(item) => item.id.clone(), TurnItem::EnteredReviewMode(item) => item.id.clone(), @@ -660,7 +652,27 @@ impl TurnItem { #[cfg(test)] mod tests { use super::*; + use codex_extension_items::sleep::SleepItem; use pretty_assertions::assert_eq; + use serde_json::json; + + #[test] + fn sleep_extension_item_preserves_type_and_kind() { + let item = TurnItem::Extension(ExtensionItem::Sleep(SleepItem { + id: "sleep-1".to_string(), + duration_ms: 1_000, + })); + + assert_eq!( + serde_json::to_value(item).expect("serialize sleep extension item"), + json!({ + "type": "Extension", + "kind": "clock.sleep", + "id": "sleep-1", + "durationMs": 1_000, + }) + ); + } #[test] fn hook_prompt_roundtrips_multiple_fragments() { diff --git a/codex-rs/protocol/src/legacy_events.rs b/codex-rs/protocol/src/legacy_events.rs index fa6677bfc9..a778711881 100644 --- a/codex-rs/protocol/src/legacy_events.rs +++ b/codex-rs/protocol/src/legacy_events.rs @@ -516,7 +516,6 @@ impl TurnItem { path: item.path.clone(), })] } - TurnItem::Sleep(_) => Vec::new(), TurnItem::Extension(_) => Vec::new(), TurnItem::ImageGeneration(item) => vec![item.as_legacy_event()], TurnItem::EnteredReviewMode(_) | TurnItem::ExitedReviewMode(_) => Vec::new(), diff --git a/codex-rs/rollout/Cargo.toml b/codex-rs/rollout/Cargo.toml index 838081bb53..0949202dfc 100644 --- a/codex-rs/rollout/Cargo.toml +++ b/codex-rs/rollout/Cargo.toml @@ -15,6 +15,7 @@ workspace = true [dependencies] anyhow = { workspace = true } chrono = { workspace = true, features = ["serde"] } +codex-extension-items = { workspace = true } codex-file-search = { workspace = true } codex-git-utils = { workspace = true } codex-otel = { workspace = true } diff --git a/codex-rs/rollout/src/persistence_metrics.rs b/codex-rs/rollout/src/persistence_metrics.rs index a3ee1dcb18..d2b4db188a 100644 --- a/codex-rs/rollout/src/persistence_metrics.rs +++ b/codex-rs/rollout/src/persistence_metrics.rs @@ -255,7 +255,6 @@ fn turn_item_type(item: &TurnItem) -> &'static str { TurnItem::SubAgentActivity(_) => "sub_agent_activity", TurnItem::WebSearch(_) => "web_search", TurnItem::ImageView(_) => "image_view", - TurnItem::Sleep(_) => "sleep", TurnItem::Extension(_) => "extension", TurnItem::ImageGeneration(_) => "image_generation", TurnItem::EnteredReviewMode(_) => "entered_review_mode", diff --git a/codex-rs/rollout/src/policy.rs b/codex-rs/rollout/src/policy.rs index 347bfd7c73..d466f2f46c 100644 --- a/codex-rs/rollout/src/policy.rs +++ b/codex-rs/rollout/src/policy.rs @@ -1,5 +1,6 @@ use crate::protocol::EventMsg; use crate::protocol::RolloutItem; +use codex_extension_items::ExtensionItem; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::ThreadHistoryMode; @@ -89,7 +90,10 @@ pub fn should_persist_event_msg(ev: &EventMsg, history_mode: ThreadHistoryMode) // Paginated rollouts store TurnItems. // Legacy rollouts keep only items with no raw ResponseItem or legacy equivalent. matches!(history_mode, ThreadHistoryMode::Paginated) - || matches!(event.item, TurnItem::Plan(_) | TurnItem::Sleep(_)) + || matches!( + event.item, + TurnItem::Plan(_) | TurnItem::Extension(ExtensionItem::Sleep(_)) + ) } EventMsg::TokenCount(_) | EventMsg::ThreadGoalUpdated(_) diff --git a/codex-rs/thread-store/src/local/thread_history.rs b/codex-rs/thread-store/src/local/thread_history.rs index 64e586725a..fd644b5541 100644 --- a/codex-rs/thread-store/src/local/thread_history.rs +++ b/codex-rs/thread-store/src/local/thread_history.rs @@ -329,7 +329,7 @@ WHERE thread_id = ? AND turn_id = ? | ThreadItem::SubAgentActivity { .. } | ThreadItem::WebSearch(_) | ThreadItem::ImageView { .. } - | ThreadItem::Sleep { .. } + | ThreadItem::Sleep(_) | ThreadItem::ImageGeneration(_) | ThreadItem::EnteredReviewMode { .. } | ThreadItem::ExitedReviewMode { .. } diff --git a/codex-rs/tui/src/app/agent_status_feed.rs b/codex-rs/tui/src/app/agent_status_feed.rs index 96f2c67bcd..efaa6c5ad8 100644 --- a/codex-rs/tui/src/app/agent_status_feed.rs +++ b/codex-rs/tui/src/app/agent_status_feed.rs @@ -189,9 +189,9 @@ fn activity_summary(item: &ThreadItem) -> Option { ThreadItem::EnteredReviewMode { .. } => return Some("Entered review mode".to_string()), ThreadItem::ExitedReviewMode { .. } => return Some("Exited review mode".to_string()), ThreadItem::ContextCompaction { .. } => return Some("Compacted context".to_string()), - ThreadItem::UserMessage { .. } - | ThreadItem::HookPrompt { .. } - | ThreadItem::Sleep { .. } => return None, + ThreadItem::UserMessage { .. } | ThreadItem::HookPrompt { .. } | ThreadItem::Sleep(_) => { + return None; + } }; bounded_summary(summary) } diff --git a/codex-rs/tui/src/chatwidget/replay.rs b/codex-rs/tui/src/chatwidget/replay.rs index e639e46c7f..e497cfd176 100644 --- a/codex-rs/tui/src/chatwidget/replay.rs +++ b/codex-rs/tui/src/chatwidget/replay.rs @@ -199,7 +199,7 @@ impl ChatWidget { }), item @ ThreadItem::SubAgentActivity { .. } => self.on_sub_agent_activity(item), ThreadItem::DynamicToolCall { .. } => {} - ThreadItem::Sleep { .. } => {} + ThreadItem::Sleep(_) => {} } if matches!(replay_kind, Some(ReplayKind::ThreadSnapshot)) && turn_id.is_empty() { diff --git a/codex-rs/tui/src/thread_transcript.rs b/codex-rs/tui/src/thread_transcript.rs index cee7959168..e9b54a7769 100644 --- a/codex-rs/tui/src/thread_transcript.rs +++ b/codex-rs/tui/src/thread_transcript.rs @@ -230,7 +230,7 @@ fn fallback_transcript_cell(item: &ThreadItem) -> Option { | ThreadItem::AgentMessage { .. } | ThreadItem::Plan { .. } | ThreadItem::Reasoning { .. } - | ThreadItem::Sleep { .. } => return None, + | ThreadItem::Sleep(_) => return None, }; (!lines.is_empty()).then(|| PlainHistoryCell::new(lines)) }