Simplify injected message envelopes

This commit is contained in:
Eric Traut
2026-04-12 14:00:06 -07:00
parent f2b9318ea1
commit c3582c8d9d
5 changed files with 62 additions and 41 deletions
+4 -4
View File
@@ -32,6 +32,7 @@ use super::EventMsg;
use super::INITIAL_SUBMIT_ID;
use super::Session;
use crate::messages::MessageInvocationContext;
use crate::messages::MessageInvocationKind;
use crate::messages::MessagePayload;
use crate::messages::db_message_to_thread_message;
use crate::messages::injected_message_event;
@@ -381,11 +382,10 @@ impl Session {
};
let delivery = message.delivery;
let message_context = MessageInvocationContext {
kind: MessageInvocationKind::External,
source: message.source.clone(),
content: message.content.clone(),
instructions: message.instructions.clone(),
meta: message.meta.clone(),
queued_at: message.queued_at,
content: message.content,
instructions: None,
};
let input_item = PendingInputItem::GeneratedMessage(GeneratedMessageInput {
item: message_prompt_input_item(&message_context),
+2 -1
View File
@@ -5067,7 +5067,8 @@ async fn generated_message_records_raw_item_and_emits_injected_event() {
id: None,
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<codex_message><content>hidden wrapper</content></codex_message>".to_string(),
text: "<external_message><content>hidden wrapper</content></external_message>"
.to_string(),
}],
end_turn: None,
phase: None,
+48 -29
View File
@@ -8,8 +8,10 @@ use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeMap;
pub const CODEX_MESSAGE_OPEN_TAG: &str = "<codex_message>";
pub const CODEX_MESSAGE_CLOSE_TAG: &str = "</codex_message>";
const EXTERNAL_MESSAGE_OPEN_TAG: &str = "<external_message>";
const EXTERNAL_MESSAGE_CLOSE_TAG: &str = "</external_message>";
const TIMER_MESSAGE_OPEN_TAG: &str = "<timer_message>";
const TIMER_MESSAGE_CLOSE_TAG: &str = "</timer_message>";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessagePayload {
@@ -32,13 +34,18 @@ pub struct ThreadMessage {
pub queued_at: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum MessageInvocationKind {
External,
Timer { timer_id: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct MessageInvocationContext {
pub(crate) kind: MessageInvocationKind,
pub(crate) source: String,
pub(crate) content: String,
pub(crate) instructions: Option<String>,
pub(crate) meta: BTreeMap<String, String>,
pub(crate) queued_at: i64,
}
pub fn validate_meta_key(key: &str) -> Result<(), String> {
@@ -81,28 +88,24 @@ pub(crate) fn injected_message_event(message: &MessageInvocationContext) -> Inje
pub(crate) fn render_message_prompt(message: &MessageInvocationContext) -> String {
let mut rendered = String::new();
rendered.push_str(CODEX_MESSAGE_OPEN_TAG);
rendered.push('\n');
push_tag(&mut rendered, "source", &message.source);
push_tag(&mut rendered, "queued_at", &message.queued_at.to_string());
push_block_tag(&mut rendered, "content", &message.content);
if let Some(instructions) = message.instructions.as_deref() {
push_block_tag(&mut rendered, "instructions", instructions);
}
if message.meta.is_empty() {
rendered.push_str("<meta />\n");
} else {
rendered.push_str("<meta>\n");
for (key, value) in &message.meta {
rendered.push_str(" <entry id=\"");
rendered.push_str(&xml_escape(key));
rendered.push_str("\">");
rendered.push_str(&xml_escape(value));
rendered.push_str("</entry>\n");
match &message.kind {
MessageInvocationKind::External => {
rendered.push_str(EXTERNAL_MESSAGE_OPEN_TAG);
rendered.push('\n');
push_block_tag(&mut rendered, "content", &message.content);
rendered.push_str(EXTERNAL_MESSAGE_CLOSE_TAG);
}
MessageInvocationKind::Timer { timer_id } => {
rendered.push_str(TIMER_MESSAGE_OPEN_TAG);
rendered.push('\n');
push_tag(&mut rendered, "timer_id", timer_id);
push_block_tag(&mut rendered, "content", &message.content);
if let Some(instructions) = message.instructions.as_deref() {
push_block_tag(&mut rendered, "instructions", instructions);
}
rendered.push_str(TIMER_MESSAGE_CLOSE_TAG);
}
rendered.push_str("</meta>\n");
}
rendered.push_str(CODEX_MESSAGE_CLOSE_TAG);
rendered
}
@@ -162,18 +165,34 @@ mod tests {
use pretty_assertions::assert_eq;
#[test]
fn renders_message_prompt_with_escaped_metadata() {
fn renders_external_message_prompt_with_only_content() {
let message = MessageInvocationContext {
source: "timer timer-1".to_string(),
kind: MessageInvocationKind::External,
source: "external".to_string(),
content: "run <tests>".to_string(),
instructions: Some("stay \"brief\"".to_string()),
meta: BTreeMap::from([("ticket".to_string(), "ABC&123".to_string())]),
queued_at: 100,
};
assert_eq!(
render_message_prompt(&message),
"<codex_message>\n<source>timer timer-1</source>\n<queued_at>100</queued_at>\n<content>\nrun &lt;tests&gt;\n</content>\n<instructions>\nstay &quot;brief&quot;\n</instructions>\n<meta>\n <entry id=\"ticket\">ABC&amp;123</entry>\n</meta>\n</codex_message>"
"<external_message>\n<content>\nrun &lt;tests&gt;\n</content>\n</external_message>"
);
}
#[test]
fn renders_timer_message_prompt_with_timer_id_and_no_metadata() {
let message = MessageInvocationContext {
kind: MessageInvocationKind::Timer {
timer_id: "timer-1".to_string(),
},
source: "timer timer-1".to_string(),
content: "run <tests>".to_string(),
instructions: Some("stay \"brief\"".to_string()),
};
assert_eq!(
render_message_prompt(&message),
"<timer_message>\n<timer_id>timer-1</timer_id>\n<content>\nrun &lt;tests&gt;\n</content>\n<instructions>\nstay &quot;brief&quot;\n</instructions>\n</timer_message>"
);
}
+6 -4
View File
@@ -5,6 +5,7 @@
//! restore timers after a harness restart.
use crate::messages::MessageInvocationContext;
use crate::messages::MessageInvocationKind;
use crate::messages::MessagePayload;
use crate::messages::validate_meta;
use crate::timer_trigger::TimerTrigger;
@@ -488,11 +489,12 @@ pub(crate) fn timer_message_invocation_context(
timer: &TimerInvocationContext,
) -> MessageInvocationContext {
MessageInvocationContext {
kind: MessageInvocationKind::Timer {
timer_id: timer.current_timer_id.clone(),
},
source: format!("timer {}", timer.current_timer_id),
content: timer_message_content(timer),
instructions: timer_message_instructions(timer),
meta: timer.meta.clone(),
queued_at: timer.queued_at,
}
}
@@ -947,7 +949,7 @@ mod tests {
ResponseInputItem::Message {
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<codex_message>\n<source>timer timer-1</source>\n<queued_at>100</queued_at>\n<content>\nTimer fired: run tests\n</content>\n<instructions>\nrun tests\n\nThis timer should keep running on its schedule after this invocation.\nDo not call delete_timer just because you completed this invocation.\nCall delete_timer with {&quot;id&quot;:&quot;timer-1&quot;} only if the user's timer message included an explicit stop condition, such as &quot;until&quot;, &quot;stop when&quot;, or &quot;while&quot;, and that condition is now satisfied.\nDo not expose scheduler internals unless they matter to the user.\n</instructions>\n<meta />\n</codex_message>".to_string(),
text: "<timer_message>\n<timer_id>timer-1</timer_id>\n<content>\nTimer fired: run tests\n</content>\n<instructions>\nrun tests\n\nThis timer should keep running on its schedule after this invocation.\nDo not call delete_timer just because you completed this invocation.\nCall delete_timer with {&quot;id&quot;:&quot;timer-1&quot;} only if the user's timer message included an explicit stop condition, such as &quot;until&quot;, &quot;stop when&quot;, or &quot;while&quot;, and that condition is now satisfied.\nDo not expose scheduler internals unless they matter to the user.\n</instructions>\n</timer_message>".to_string(),
}],
}
);
@@ -969,7 +971,7 @@ mod tests {
ResponseInputItem::Message {
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "<codex_message>\n<source>timer timer-1</source>\n<queued_at>101</queued_at>\n<content>\nTimer fired: run tests once\n</content>\n<instructions>\nrun tests once\n\nuser-specific instruction\n\nThis one-shot timer has already been removed from the schedule, so you do not need to call delete_timer.\nDo not expose scheduler internals unless they matter to the user.\n</instructions>\n<meta>\n <entry id=\"ticket\">ABC_123</entry>\n</meta>\n</codex_message>".to_string(),
text: "<timer_message>\n<timer_id>timer-1</timer_id>\n<content>\nTimer fired: run tests once\n</content>\n<instructions>\nrun tests once\n\nuser-specific instruction\n\nThis one-shot timer has already been removed from the schedule, so you do not need to call delete_timer.\nDo not expose scheduler internals unless they matter to the user.\n</instructions>\n</timer_message>".to_string(),
}],
}
);
+2 -3
View File
@@ -117,14 +117,13 @@ async fn assert_after_turn_timer_starts_and_emits_fired_event() -> Result<()> {
let user_messages = mock.single_request().message_input_texts("user");
let timer_messages = user_messages
.iter()
.filter(|message| message.contains("<codex_message>"))
.filter(|message| message.contains("<timer_message>"))
.collect::<Vec<_>>();
assert_eq!(timer_messages.len(), 1);
let timer_message = timer_messages[0];
assert!(timer_message.contains(&format!("<source>timer {}</source>", created.id)));
assert!(timer_message.contains(&format!("<timer_id>{}</timer_id>", created.id)));
assert!(timer_message.contains("<content>\nTimer fired: run timer\n</content>"));
assert!(timer_message.contains("<instructions>\nrun timer\n\nThis one-shot timer"));
assert!(timer_message.contains("<meta />"));
Ok(())
}