//! Generated user-message payloads and model-visible XML envelopes. //! //! This module owns the representation for messages that are delivered into a //! thread by the harness rather than typed directly by the user. The model sees //! the XML envelope recorded in history; transcript clients receive a separate //! structured event with the human-facing text so they do not need to parse or //! hide that XML. #![allow(dead_code)] use crate::timers::TimerDelivery; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseInputItem; use codex_protocol::protocol::InjectedMessageEvent; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; const EXTERNAL_MESSAGE_OPEN_TAG: &str = ""; const EXTERNAL_MESSAGE_CLOSE_TAG: &str = ""; const TIMER_MESSAGE_OPEN_TAG: &str = ""; const TIMER_MESSAGE_CLOSE_TAG: &str = ""; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct MessagePayload { pub content: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub instructions: Option, #[serde(default)] pub meta: BTreeMap, } #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum InjectedMessage { External { source: String, content: String, }, Timer { timer_id: String, content: String, instructions: Option, }, } impl InjectedMessage { pub(crate) fn from_external_row( row: codex_state::ExternalMessage, ) -> Result<(Self, TimerDelivery), String> { let delivery = serde_json::from_value::(serde_json::Value::String( row.delivery.clone(), )) .map_err(|err| format!("invalid message delivery `{}`: {err}", row.delivery))?; Ok(( Self::External { source: row.source, content: row.content, }, delivery, )) } pub(crate) fn prompt_input_item(&self) -> ResponseInputItem { ResponseInputItem::Message { role: "user".to_string(), content: vec![ContentItem::InputText { text: self.render_prompt(), }], } } pub(crate) fn event(&self) -> InjectedMessageEvent { match self { Self::External { source, content } => InjectedMessageEvent { content: content.clone(), source: source.clone(), }, Self::Timer { timer_id, content, .. } => InjectedMessageEvent { content: content.clone(), source: format!("timer {timer_id}"), }, } } fn render_prompt(&self) -> String { let mut rendered = String::new(); match self { Self::External { content, .. } => { rendered.push_str(EXTERNAL_MESSAGE_OPEN_TAG); rendered.push('\n'); push_block_tag(&mut rendered, "content", content); rendered.push_str(EXTERNAL_MESSAGE_CLOSE_TAG); } Self::Timer { timer_id, content, instructions, } => { rendered.push_str(TIMER_MESSAGE_OPEN_TAG); rendered.push('\n'); push_tag(&mut rendered, "timer_id", timer_id); push_block_tag(&mut rendered, "content", content); if let Some(instructions) = instructions.as_deref() { push_block_tag(&mut rendered, "instructions", instructions); } rendered.push_str(TIMER_MESSAGE_CLOSE_TAG); } } rendered } } fn push_tag(rendered: &mut String, tag: &str, value: &str) { rendered.push('<'); rendered.push_str(tag); rendered.push('>'); rendered.push_str(&xml_escape(value)); rendered.push_str("\n"); } fn push_block_tag(rendered: &mut String, tag: &str, value: &str) { rendered.push('<'); rendered.push_str(tag); rendered.push_str(">\n"); rendered.push_str(&xml_escape(value)); rendered.push('\n'); rendered.push_str("\n"); } fn xml_escape(value: &str) -> String { value .replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn renders_external_message_prompt_with_only_content() { let message = InjectedMessage::External { source: "external".to_string(), content: "run ".to_string(), }; assert_eq!( message.render_prompt(), "\n\nrun <tests>\n\n" ); } #[test] fn renders_timer_message_prompt_with_timer_id() { let message = InjectedMessage::Timer { timer_id: "timer-1".to_string(), content: "run ".to_string(), instructions: Some("stay \"brief\"".to_string()), }; assert_eq!( message.render_prompt(), "\ntimer-1\n\nrun <tests>\n\n\nstay "brief"\n\n" ); } }