Download codex-rs/protocol/src/items.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 28.3 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/protocol/src/items.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/protocol/src/items.rs
-
curl -L -o items.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/protocol/src/items.rs
28.3 kB
| use crate::AgentPath; | |
| use crate::ResponseItemId; | |
| use crate::ThreadId; | |
| use crate::dynamic_tools::DynamicToolCallOutputContentItem; | |
| use crate::mcp::CallToolResult; | |
| use crate::memory_citation::MemoryCitation; | |
| use crate::models::ContentItem; | |
| use crate::models::FunctionCallOutputBody; | |
| use crate::models::ImageDetail; | |
| use crate::models::ImageReference; | |
| use crate::models::MessagePhase; | |
| use crate::models::ResponseItem; | |
| use crate::models::WebSearchAction; | |
| use crate::openai_models::ReasoningEffort as ReasoningEffortConfig; | |
| use crate::parse_command::ParsedCommand; | |
| use crate::protocol::AgentStatus; | |
| use crate::protocol::CollabAgentRef; | |
| use crate::protocol::ExecCommandSource; | |
| use crate::protocol::ExecCommandStatus; | |
| use crate::protocol::FileChange; | |
| use crate::protocol::PatchApplyStatus; | |
| use crate::protocol::ReviewOutputEvent; | |
| use crate::protocol::ReviewTarget; | |
| use crate::protocol::SubAgentActivityKind; | |
| use crate::user_input::ByteRange; | |
| use crate::user_input::TextElement; | |
| use crate::user_input::UserInput; | |
| use codex_extension_items::ExtensionItem; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use codex_utils_path_uri::PathUri; | |
| use quick_xml::de::from_str as from_xml_str; | |
| use quick_xml::se::to_string as to_xml_string; | |
| use schemars::JsonSchema; | |
| use serde::Deserialize; | |
| use serde::Serialize; | |
| use serde_json::Value as JsonValue; | |
| use std::collections::HashMap; | |
| use std::path::PathBuf; | |
| use std::time::Duration; | |
| use ts_rs::TS; | |
| pub enum TurnItem { | |
| UserMessage(UserMessageItem), | |
| FunctionCallOutput(FunctionCallOutputItem), | |
| HookPrompt(HookPromptItem), | |
| AgentMessage(AgentMessageItem), | |
| Plan(PlanItem), | |
| Reasoning(ReasoningItem), | |
| CommandExecution(CommandExecutionItem), | |
| DynamicToolCall(DynamicToolCallItem), | |
| CollabAgentToolCall(CollabAgentToolCallItem), | |
| SubAgentActivity(SubAgentActivityItem), | |
| /// Hosted Responses API web-search item handled directly by core. | |
| /// | |
| /// Standalone web search uses Self::Extension instead because its display | |
| /// schema is owned by the web-search extension. | |
| WebSearch(WebSearchItem), | |
| ImageView(ImageViewItem), | |
| /// Item whose schema and lifecycle details are owned by an extension. | |
| /// | |
| /// 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. | |
| /// | |
| /// This remains separate from [`Self::Extension`] because core still owns | |
| /// hosted image persistence and legacy-event fanout. | |
| ImageGeneration(ImageGenerationItem), | |
| EnteredReviewMode(EnteredReviewModeItem), | |
| ExitedReviewMode(ExitedReviewModeItem), | |
| FileChange(FileChangeItem), | |
| McpToolCall(McpToolCallItem), | |
| ContextCompaction(ContextCompactionItem), | |
| } | |
| pub struct UserMessageItem { | |
| pub id: String, | |
| pub client_id: Option<String>, | |
| pub content: Vec<UserInput>, | |
| } | |
| pub struct FunctionCallOutputItem { | |
| pub id: String, | |
| pub name: String, | |
| pub namespace: Option<String>, | |
| pub output: FunctionCallOutputBody, | |
| } | |
| pub struct HookPromptItem { | |
| pub id: String, | |
| pub fragments: Vec<HookPromptFragment>, | |
| } | |
| pub struct HookPromptFragment { | |
| pub text: String, | |
| pub hook_run_id: String, | |
| } | |
| struct HookPromptXml { | |
| hook_run_id: String, | |
| text: String, | |
| } | |
| pub enum AgentMessageContent { | |
| Text { text: String }, | |
| } | |
| pub enum AgentMessageDelivery { | |
| Async, | |
| } | |
| pub struct AsyncUserInputQuestion { | |
| pub title: String, | |
| pub options: Option<Vec<String>>, | |
| } | |
| /// Assistant-authored message payload used in turn-item streams. | |
| /// | |
| /// `phase` is optional because not all providers/models emit it. Consumers | |
| /// should use it when present, but retain legacy completion semantics when it | |
| /// is `None`. | |
| pub struct AgentMessageItem { | |
| pub id: String, | |
| pub content: Vec<AgentMessageContent>, | |
| /// Optional phase metadata carried through from `ResponseItem::Message`. | |
| /// | |
| /// This is currently used by TUI rendering to distinguish mid-turn | |
| /// commentary from a final answer and avoid status-indicator jitter. | |
| pub phase: Option<MessagePhase>, | |
| pub memory_citation: Option<MemoryCitation>, | |
| pub delivery: Option<AgentMessageDelivery>, | |
| pub questions: Option<Vec<AsyncUserInputQuestion>>, | |
| } | |
| pub struct EnteredReviewModeItem { | |
| pub id: String, | |
| pub target: ReviewTarget, | |
| pub user_facing_hint: String, | |
| } | |
| pub struct ExitedReviewModeItem { | |
| pub id: String, | |
| pub review_output: Option<ReviewOutputEvent>, | |
| } | |
| pub struct PlanItem { | |
| pub id: String, | |
| pub text: String, | |
| } | |
| pub struct ReasoningItem { | |
| pub id: String, | |
| pub summary_text: Vec<String>, | |
| pub raw_content: Vec<String>, | |
| } | |
| pub enum CommandExecutionStatus { | |
| InProgress, | |
| Completed, | |
| Failed, | |
| Declined, | |
| } | |
| impl From<ExecCommandStatus> for CommandExecutionStatus { | |
| fn from(value: ExecCommandStatus) -> Self { | |
| match value { | |
| ExecCommandStatus::Completed => Self::Completed, | |
| ExecCommandStatus::Failed => Self::Failed, | |
| ExecCommandStatus::Declined => Self::Declined, | |
| } | |
| } | |
| } | |
| /// Returns whether a path is safe to serialize as a trusted plugin-relative path. | |
| /// | |
| /// This validates the cross-platform wire shape only. The trusted plugin resolver | |
| /// remains responsible for establishing that the path actually came from a plugin root. | |
| pub fn is_safe_plugin_relative_path(path: &str) -> bool { | |
| !path.is_empty() | |
| && !path.starts_with('/') | |
| && !path.contains('\\') | |
| && path.split('/').all(|component| { | |
| !component.is_empty() | |
| && !matches!(component, "." | "..") | |
| && !matches!( | |
| component.as_bytes(), | |
| [drive, b':', ..] if drive.is_ascii_alphabetic() | |
| ) | |
| }) | |
| } | |
| /// Immutable model labels carried within command lifecycle events for analytics. | |
| pub struct ModelInvocationContext { | |
| pub model_slug: String, | |
| pub reasoning_effort: Option<String>, | |
| } | |
| pub struct CommandExecutionItem { | |
| pub model_context: Option<ModelInvocationContext>, | |
| pub id: String, | |
| pub plugin_id: Option<String>, | |
| pub script_path: Option<String>, | |
| pub process_id: Option<String>, | |
| pub command: Vec<String>, | |
| pub cwd: PathUri, | |
| pub parsed_cmd: Vec<ParsedCommand>, | |
| pub source: ExecCommandSource, | |
| pub interaction_input: Option<String>, | |
| pub status: CommandExecutionStatus, | |
| pub stdout: Option<String>, | |
| pub stderr: Option<String>, | |
| pub aggregated_output: Option<String>, | |
| pub exit_code: Option<i32>, | |
| pub duration: Option<Duration>, | |
| pub formatted_output: Option<String>, | |
| } | |
| pub enum DynamicToolCallStatus { | |
| InProgress, | |
| Completed, | |
| Failed, | |
| } | |
| pub struct DynamicToolCallItem { | |
| pub id: String, | |
| pub namespace: Option<String>, | |
| pub tool: String, | |
| pub arguments: serde_json::Value, | |
| pub status: DynamicToolCallStatus, | |
| pub content_items: Option<Vec<DynamicToolCallOutputContentItem>>, | |
| pub success: Option<bool>, | |
| pub error: Option<String>, | |
| pub duration: Option<Duration>, | |
| } | |
| pub enum CollabAgentTool { | |
| SpawnAgent, | |
| SendInput, | |
| ResumeAgent, | |
| Wait, | |
| CloseAgent, | |
| SendMessage, | |
| FollowupTask, | |
| InterruptAgent, | |
| ListAgents, | |
| } | |
| pub enum CollabAgentToolCallStatus { | |
| InProgress, | |
| Completed, | |
| Failed, | |
| Interrupted, | |
| } | |
| pub struct CollabAgentToolCallItem { | |
| pub id: String, | |
| pub tool: CollabAgentTool, | |
| pub status: CollabAgentToolCallStatus, | |
| pub sender_thread_id: ThreadId, | |
| pub receiver_thread_ids: Vec<ThreadId>, | |
| pub receiver_agents: Vec<CollabAgentRef>, | |
| pub prompt: Option<String>, | |
| pub model: Option<String>, | |
| pub reasoning_effort: Option<ReasoningEffortConfig>, | |
| pub agents_states: HashMap<ThreadId, AgentStatus>, | |
| } | |
| pub struct SubAgentActivityItem { | |
| pub id: String, | |
| pub kind: SubAgentActivityKind, | |
| pub agent_thread_id: ThreadId, | |
| pub agent_path: AgentPath, | |
| } | |
| pub struct WebSearchItem { | |
| pub id: String, | |
| pub query: String, | |
| pub action: WebSearchAction, | |
| /// Structured search results returned out-of-band by standalone web search. | |
| /// | |
| /// These stay as opaque JSON at the Codex transport boundary so new result | |
| /// fields and result types can pass through without changing model-visible | |
| /// context or requiring a Codex release. | |
| pub results: Option<Vec<JsonValue>>, | |
| } | |
| pub struct ImageViewItem { | |
| pub id: String, | |
| /// Path resolved within the selected execution environment. | |
| /// | |
| /// This core protocol type is not exposed directly in the app-server API. | |
| /// App-server converts the path to `LegacyAppPathString` at its boundary. | |
| pub path: PathUri, | |
| } | |
| pub struct ImageGenerationItem { | |
| pub id: String, | |
| pub status: String, | |
| pub revised_prompt: Option<String>, | |
| pub result: String, | |
| pub saved_path: Option<AbsolutePathBuf>, | |
| } | |
| pub struct FileChangeItem { | |
| pub id: String, | |
| pub changes: HashMap<PathBuf, FileChange>, | |
| pub status: Option<PatchApplyStatus>, | |
| pub auto_approved: Option<bool>, | |
| pub stdout: Option<String>, | |
| pub stderr: Option<String>, | |
| } | |
| /// UI resource and display preference for model invocations, captured from the tool descriptor. | |
| pub struct McpAppUi { | |
| pub resource_uri: String, | |
| pub preferred_model_display_mode: McpAppDisplayMode, | |
| } | |
| pub enum McpAppDisplayMode { | |
| Inline, | |
| Fullscreen, | |
| } | |
| pub struct McpToolCallItem { | |
| pub id: String, | |
| pub server: String, | |
| pub tool: String, | |
| pub arguments: serde_json::Value, | |
| pub connector_id: Option<String>, | |
| /// Legacy compatibility field; prefer `mcp_app_ui.resource_uri` when available. | |
| pub mcp_app_resource_uri: Option<String>, | |
| pub mcp_app_ui: Option<McpAppUi>, | |
| pub link_id: Option<String>, | |
| pub app_name: Option<String>, | |
| pub action_name: Option<String>, | |
| pub plugin_id: Option<String>, | |
| pub read_only_hint: Option<bool>, | |
| pub status: McpToolCallStatus, | |
| pub result: Option<CallToolResult>, | |
| pub error: Option<McpToolCallError>, | |
| pub duration: Option<Duration>, | |
| } | |
| pub enum McpToolCallStatus { | |
| InProgress, | |
| Completed, | |
| Failed, | |
| } | |
| pub struct McpToolCallError { | |
| pub message: String, | |
| } | |
| pub struct ContextCompactionItem { | |
| pub id: String, | |
| } | |
| fn new_item_id() -> String { | |
| uuid::Uuid::now_v7().to_string() | |
| } | |
| impl ContextCompactionItem { | |
| pub fn new() -> Self { | |
| Self { id: new_item_id() } | |
| } | |
| } | |
| impl Default for ContextCompactionItem { | |
| fn default() -> Self { | |
| Self::new() | |
| } | |
| } | |
| impl UserMessageItem { | |
| pub fn new(content: &[UserInput]) -> Self { | |
| Self { | |
| id: new_item_id(), | |
| client_id: None, | |
| content: content.to_vec(), | |
| } | |
| } | |
| pub fn message(&self) -> String { | |
| self.content | |
| .iter() | |
| .map(|c| match c { | |
| UserInput::Text { text, .. } => text.clone(), | |
| _ => String::new(), | |
| }) | |
| .collect::<Vec<String>>() | |
| .join("") | |
| } | |
| pub fn text_elements(&self) -> Vec<TextElement> { | |
| let mut out = Vec::new(); | |
| let mut offset = 0usize; | |
| for input in &self.content { | |
| if let UserInput::Text { | |
| text, | |
| text_elements, | |
| .. | |
| } = input | |
| { | |
| // Text element ranges are relative to each text chunk; offset them so they align | |
| // with the concatenated message returned by `message()`. | |
| for elem in text_elements { | |
| let byte_range = ByteRange { | |
| start: offset + elem.byte_range.start, | |
| end: offset + elem.byte_range.end, | |
| }; | |
| out.push(TextElement::new( | |
| byte_range, | |
| elem.placeholder(text).map(str::to_string), | |
| )); | |
| } | |
| offset += text.len(); | |
| } | |
| } | |
| out | |
| } | |
| pub fn image_urls(&self) -> Vec<String> { | |
| self.content | |
| .iter() | |
| .filter_map(|c| match c { | |
| UserInput::Image { | |
| image: ImageReference::Inline { image_url }, | |
| .. | |
| } => Some(image_url.clone()), | |
| _ => None, | |
| }) | |
| .collect() | |
| } | |
| pub fn image_details(&self) -> Vec<Option<ImageDetail>> { | |
| trim_trailing_default_image_details( | |
| self.content | |
| .iter() | |
| .filter_map(|c| match c { | |
| UserInput::Image { | |
| image: ImageReference::Inline { .. }, | |
| detail, | |
| } => Some(*detail), | |
| _ => None, | |
| }) | |
| .collect(), | |
| ) | |
| } | |
| pub fn local_image_paths(&self) -> Vec<std::path::PathBuf> { | |
| self.content | |
| .iter() | |
| .filter_map(|c| match c { | |
| UserInput::LocalImage { path, .. } => Some(path.clone()), | |
| _ => None, | |
| }) | |
| .collect() | |
| } | |
| pub fn local_image_details(&self) -> Vec<Option<ImageDetail>> { | |
| trim_trailing_default_image_details( | |
| self.content | |
| .iter() | |
| .filter_map(|c| match c { | |
| UserInput::LocalImage { detail, .. } => Some(*detail), | |
| _ => None, | |
| }) | |
| .collect(), | |
| ) | |
| } | |
| pub fn audio_urls(&self) -> Vec<String> { | |
| self.content | |
| .iter() | |
| .filter_map(|c| match c { | |
| UserInput::Audio { audio_url } => Some(audio_url.clone()), | |
| _ => None, | |
| }) | |
| .collect() | |
| } | |
| pub fn local_audio_paths(&self) -> Vec<std::path::PathBuf> { | |
| self.content | |
| .iter() | |
| .filter_map(|c| match c { | |
| UserInput::LocalAudio { path } => Some(path.clone()), | |
| _ => None, | |
| }) | |
| .collect() | |
| } | |
| } | |
| pub(crate) fn trim_trailing_default_image_details( | |
| mut details: Vec<Option<ImageDetail>>, | |
| ) -> Vec<Option<ImageDetail>> { | |
| while matches!(details.last(), Some(None)) { | |
| details.pop(); | |
| } | |
| details | |
| } | |
| impl HookPromptItem { | |
| pub fn from_fragments(id: Option<&str>, fragments: Vec<HookPromptFragment>) -> Self { | |
| Self { | |
| id: id.map(str::to_string).unwrap_or_else(new_item_id), | |
| fragments, | |
| } | |
| } | |
| } | |
| impl HookPromptFragment { | |
| pub fn from_single_hook(text: impl Into<String>, hook_run_id: impl Into<String>) -> Self { | |
| Self { | |
| text: text.into(), | |
| hook_run_id: hook_run_id.into(), | |
| } | |
| } | |
| } | |
| pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<ResponseItem> { | |
| let content = fragments | |
| .iter() | |
| .filter(|fragment| !fragment.hook_run_id.trim().is_empty()) | |
| .filter_map(|fragment| { | |
| serialize_hook_prompt_fragment(&fragment.text, &fragment.hook_run_id) | |
| .map(|text| ContentItem::InputText { text }) | |
| }) | |
| .collect::<Vec<_>>(); | |
| if content.is_empty() { | |
| return None; | |
| } | |
| Some(ResponseItem::Message { | |
| id: Some(ResponseItemId::new("msg")), | |
| role: "user".to_string(), | |
| content, | |
| phase: None, | |
| internal_chat_message_metadata_passthrough: None, | |
| }) | |
| } | |
| pub fn parse_hook_prompt_message( | |
| id: Option<&str>, | |
| content: &[ContentItem], | |
| ) -> Option<HookPromptItem> { | |
| let fragments = content | |
| .iter() | |
| .map(|content_item| { | |
| let ContentItem::InputText { text } = content_item else { | |
| return None; | |
| }; | |
| parse_hook_prompt_fragment(text) | |
| }) | |
| .collect::<Option<Vec<_>>>()?; | |
| if fragments.is_empty() { | |
| return None; | |
| } | |
| Some(HookPromptItem::from_fragments(id, fragments)) | |
| } | |
| pub fn parse_hook_prompt_fragment(text: &str) -> Option<HookPromptFragment> { | |
| let trimmed = text.trim(); | |
| let HookPromptXml { text, hook_run_id } = from_xml_str::<HookPromptXml>(trimmed).ok()?; | |
| if hook_run_id.trim().is_empty() { | |
| return None; | |
| } | |
| Some(HookPromptFragment { text, hook_run_id }) | |
| } | |
| fn serialize_hook_prompt_fragment(text: &str, hook_run_id: &str) -> Option<String> { | |
| if hook_run_id.trim().is_empty() { | |
| return None; | |
| } | |
| to_xml_string(&HookPromptXml { | |
| text: text.to_string(), | |
| hook_run_id: hook_run_id.to_string(), | |
| }) | |
| .ok() | |
| } | |
| impl TurnItem { | |
| pub fn id(&self) -> String { | |
| match self { | |
| TurnItem::UserMessage(item) => item.id.clone(), | |
| TurnItem::FunctionCallOutput(item) => item.id.clone(), | |
| TurnItem::HookPrompt(item) => item.id.clone(), | |
| TurnItem::AgentMessage(item) => item.id.clone(), | |
| TurnItem::Plan(item) => item.id.clone(), | |
| TurnItem::Reasoning(item) => item.id.clone(), | |
| TurnItem::CommandExecution(item) => item.id.clone(), | |
| TurnItem::DynamicToolCall(item) => item.id.clone(), | |
| TurnItem::CollabAgentToolCall(item) => item.id.clone(), | |
| TurnItem::SubAgentActivity(item) => item.id.clone(), | |
| TurnItem::WebSearch(item) => item.id.clone(), | |
| TurnItem::ImageView(item) => item.id.clone(), | |
| TurnItem::Extension(item) => item.id().to_string(), | |
| TurnItem::ImageGeneration(item) => item.id.clone(), | |
| TurnItem::EnteredReviewMode(item) => item.id.clone(), | |
| TurnItem::ExitedReviewMode(item) => item.id.clone(), | |
| TurnItem::FileChange(item) => item.id.clone(), | |
| TurnItem::McpToolCall(item) => item.id.clone(), | |
| TurnItem::ContextCompaction(item) => item.id.clone(), | |
| } | |
| } | |
| } | |
| mod tests { | |
| use super::*; | |
| use codex_extension_items::sleep::SleepItem; | |
| use pretty_assertions::assert_eq; | |
| use serde_json::json; | |
| 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, | |
| }) | |
| ); | |
| } | |
| fn user_message_item_extracts_audio_attachments() { | |
| let item = UserMessageItem::new(&[ | |
| UserInput::Text { | |
| text: "transcribe these".to_string(), | |
| text_elements: Vec::new(), | |
| }, | |
| UserInput::Audio { | |
| audio_url: "https://example.com/remote.mp3".to_string(), | |
| }, | |
| UserInput::LocalAudio { | |
| path: std::path::PathBuf::from("local.wav"), | |
| }, | |
| ]); | |
| assert_eq!( | |
| (item.audio_urls(), item.local_audio_paths()), | |
| ( | |
| vec!["https://example.com/remote.mp3".to_string()], | |
| vec![std::path::PathBuf::from("local.wav")], | |
| ) | |
| ); | |
| } | |
| fn plugin_relative_paths_use_safe_wire_shape() { | |
| assert!(is_safe_plugin_relative_path("scripts/run.py")); | |
| for path in [ | |
| "", | |
| "/home/user/.codex/plugins/cache/sample/scripts/run.py", | |
| "C:/Users/user/.codex/plugins/cache/sample/scripts/run.py", | |
| "scripts/C:/run.py", | |
| r"\\server\share\sample\scripts\run.py", | |
| r"scripts\run.py", | |
| "scripts//run.py", | |
| "scripts/./run.py", | |
| "scripts/../run.py", | |
| ] { | |
| assert!( | |
| !is_safe_plugin_relative_path(path), | |
| "unsafe plugin-relative path should be rejected: {path:?}" | |
| ); | |
| } | |
| } | |
| fn hook_prompt_roundtrips_multiple_fragments() { | |
| let original = vec![ | |
| HookPromptFragment::from_single_hook("Retry with care & joy.", "hook-run-1"), | |
| HookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"), | |
| ]; | |
| let message = build_hook_prompt_message(&original).expect("hook prompt"); | |
| let ResponseItem::Message { id, content, .. } = message else { | |
| panic!("expected hook prompt message"); | |
| }; | |
| assert!(id.is_some_and(|id| id.starts_with("msg_"))); | |
| let parsed = parse_hook_prompt_message(/*id*/ None, &content).expect("parsed hook prompt"); | |
| assert_eq!(parsed.fragments, original); | |
| } | |
| fn hook_prompt_parses_legacy_single_hook_run_id() { | |
| let parsed = parse_hook_prompt_fragment( | |
| r#"<hook_prompt hook_run_id="hook-run-1">Retry with tests.</hook_prompt>"#, | |
| ) | |
| .expect("legacy hook prompt"); | |
| assert_eq!( | |
| parsed, | |
| HookPromptFragment { | |
| text: "Retry with tests.".to_string(), | |
| hook_run_id: "hook-run-1".to_string(), | |
| } | |
| ); | |
| } | |
| } | |