From 365c5b76fc0bc6803e1c9531143f0213d96006af Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 3 Feb 2026 18:06:54 +0000 Subject: [PATCH] better --- .../tests/suite/v2/thread_resume.rs | 8 +- .../app-server/tests/suite/v2/turn_start.rs | 22 +++-- codex-rs/core/src/codex.rs | 11 ++- codex-rs/core/src/context_manager/history.rs | 9 +- .../core/src/context_manager/history_tests.rs | 35 ++++++- codex-rs/core/src/event_mapping.rs | 16 +++- codex-rs/core/src/user_shell_command.rs | 25 +---- .../tests/suite/collaboration_instructions.rs | 7 +- codex-rs/core/tests/suite/override_updates.rs | 8 +- codex-rs/core/tests/suite/personality.rs | 20 ++-- codex-rs/core/tests/suite/user_shell_cmd.rs | 19 +++- codex-rs/protocol/src/artificial_messages.rs | 91 ++++++++++++++++++- codex-rs/protocol/src/models.rs | 13 +-- 13 files changed, 212 insertions(+), 72 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 358fec351f..d98cbb1057 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -16,6 +16,7 @@ use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::config_types::Personality; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; @@ -439,9 +440,10 @@ async fn thread_resume_accepts_personality_override() -> Result<()> { let request = response_mock.single_request(); let developer_texts = request.message_input_texts("developer"); assert!( - developer_texts - .iter() - .any(|text| text.contains("")), + developer_texts.iter().any(|text| matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::PersonalitySpec { .. }) + )), "expected a personality update message in developer input, got {developer_texts:?}" ); let instructions_text = request.instructions_text(); diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 1b53a5cfef..5eb165d4f9 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -37,6 +37,7 @@ use codex_app_server_protocol::UserInput as V2UserInput; use codex_core::features::FEATURES; use codex_core::features::Feature; use codex_core::protocol_config_types::ReasoningSummary; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; @@ -475,9 +476,10 @@ async fn turn_start_accepts_personality_override_v2() -> Result<()> { } assert!( - developer_texts - .iter() - .any(|text| text.contains("")), + developer_texts.iter().any(|text| matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::PersonalitySpec { .. }) + )), "expected personality update message in developer input, got {developer_texts:?}" ); @@ -578,17 +580,19 @@ async fn turn_start_change_personality_mid_thread_v2() -> Result<()> { let first_developer_texts = requests[0].message_input_texts("developer"); assert!( - first_developer_texts - .iter() - .all(|text| !text.contains("")), + first_developer_texts.iter().all(|text| !matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::PersonalitySpec { .. }) + )), "expected no personality update message in first request, got {first_developer_texts:?}" ); let second_developer_texts = requests[1].message_input_texts("developer"); assert!( - second_developer_texts - .iter() - .any(|text| text.contains("")), + second_developer_texts.iter().any(|text| matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::PersonalitySpec { .. }) + )), "expected personality update message in second request, got {second_developer_texts:?}" ); diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index ecd87e8f7e..fd98fb871b 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1523,8 +1523,9 @@ impl Session { warn!("execpolicy amendment for {sub_id} had no command prefix"); return; }; - let text = format!("Approved command prefix saved:\n{prefixes}"); - let message: ResponseItem = DeveloperInstructions::new(text.clone()).into(); + let body = format!("Approved command prefix saved:\n{prefixes}"); + let message = + ArtificialMessage::ExecPolicyAmendment { body: body.clone() }.to_response_item(); if let Some(turn_context) = self.turn_context_for_sub_id(sub_id).await { self.record_conversation_items(&turn_context, std::slice::from_ref(&message)) @@ -1535,7 +1536,9 @@ impl Session { if self .inject_response_items(vec![ResponseInputItem::Message { role: "developer".to_string(), - content: vec![ContentItem::InputText { text }], + content: vec![ContentItem::InputText { + text: ArtificialMessage::ExecPolicyAmendment { body }.render(), + }], }]) .await .is_err() @@ -1805,7 +1808,7 @@ impl Session { .counter("codex.model_warning", 1, &[]); let item = ArtificialMessage::ModelWarning { body: message.into(), - }; + } .to_response_item(); self.record_conversation_items(ctx, &[item]).await; diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index a29f7df7e0..e0edaa0960 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -8,7 +8,7 @@ use crate::truncate::approx_token_count; use crate::truncate::approx_tokens_from_byte_count; use crate::truncate::truncate_function_output_items_with_policy; use crate::truncate::truncate_text; -use crate::user_shell_command::is_user_shell_command_text; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; @@ -378,7 +378,12 @@ pub(crate) fn is_user_turn_boundary(item: &ResponseItem) -> bool { for content_item in content { match content_item { ContentItem::InputText { text } => { - if is_session_prefix(text) || is_user_shell_command_text(text) { + if is_session_prefix(text) + || matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::UserShellCommand { .. }) + ) + { return false; } } diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 69b4bee814..9805316b18 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -456,7 +456,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), user_input_text_msg(&demo_skill_message()), - user_input_text_msg("echo 42"), + user_input_text_msg( + &ArtificialMessage::UserShellCommand { + body: "echo 42".to_string(), + } + .render(), + ), user_input_text_msg("turn 1 user"), assistant_msg("turn 1 assistant"), user_input_text_msg("turn 2 user"), @@ -473,7 +478,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), user_input_text_msg(&demo_skill_message()), - user_input_text_msg("echo 42"), + user_input_text_msg( + &ArtificialMessage::UserShellCommand { + body: "echo 42".to_string(), + } + .render(), + ), user_input_text_msg("turn 1 user"), assistant_msg("turn 1 assistant"), ]; @@ -487,7 +497,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), user_input_text_msg(&demo_skill_message()), - user_input_text_msg("echo 42"), + user_input_text_msg( + &ArtificialMessage::UserShellCommand { + body: "echo 42".to_string(), + } + .render(), + ), ]; let mut history = create_history_with_items(vec![ @@ -497,7 +512,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), user_input_text_msg(&demo_skill_message()), - user_input_text_msg("echo 42"), + user_input_text_msg( + &ArtificialMessage::UserShellCommand { + body: "echo 42".to_string(), + } + .render(), + ), user_input_text_msg("turn 1 user"), assistant_msg("turn 1 assistant"), user_input_text_msg("turn 2 user"), @@ -513,7 +533,12 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() { "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", ), user_input_text_msg(&demo_skill_message()), - user_input_text_msg("echo 42"), + user_input_text_msg( + &ArtificialMessage::UserShellCommand { + body: "echo 42".to_string(), + } + .render(), + ), user_input_text_msg("turn 1 user"), assistant_msg("turn 1 assistant"), user_input_text_msg("turn 2 user"), diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index d9b4adafe9..a3843db602 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -1,3 +1,4 @@ +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; use codex_protocol::items::ReasoningItem; @@ -20,7 +21,6 @@ use uuid::Uuid; use crate::instructions::SkillInstructions; use crate::instructions::UserInstructions; use crate::session_prefix::is_session_prefix; -use crate::user_shell_command::is_user_shell_command_text; use crate::web_search::web_search_action_detail; fn parse_user_message(message: &[ContentItem]) -> Option { @@ -43,7 +43,12 @@ fn parse_user_message(message: &[ContentItem]) -> Option { { continue; } - if is_session_prefix(text) || is_user_shell_command_text(text) { + if is_session_prefix(text) + || matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::UserShellCommand { .. }) + ) + { return None; } content.push(UserInput::Text { @@ -332,10 +337,13 @@ mod tests { id: None, role: "user".to_string(), content: vec![ContentItem::InputText { - text: "echo 42".to_string(), + text: ArtificialMessage::UserShellCommand { + body: "echo 42".to_string(), + } + .render(), }], end_turn: None, - phase: None, + phase: None, }, ]; diff --git a/codex-rs/core/src/user_shell_command.rs b/codex-rs/core/src/user_shell_command.rs index 80128df006..3df0c0951d 100644 --- a/codex-rs/core/src/user_shell_command.rs +++ b/codex-rs/core/src/user_shell_command.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; @@ -7,15 +8,6 @@ use crate::codex::TurnContext; use crate::exec::ExecToolCallOutput; use crate::tools::format_exec_output_str; -pub const USER_SHELL_COMMAND_OPEN: &str = ""; -pub const USER_SHELL_COMMAND_CLOSE: &str = ""; - -pub fn is_user_shell_command_text(text: &str) -> bool { - let trimmed = text.trim_start(); - let lowered = trimmed.to_ascii_lowercase(); - lowered.starts_with(USER_SHELL_COMMAND_OPEN) -} - fn format_duration_line(duration: Duration) -> String { let duration_seconds = duration.as_secs_f64(); format!("Duration: {duration_seconds:.4} seconds") @@ -48,7 +40,7 @@ pub fn format_user_shell_command_record( turn_context: &TurnContext, ) -> String { let body = format_user_shell_command_body(command, exec_output, turn_context); - format!("{USER_SHELL_COMMAND_OPEN}\n{body}\n{USER_SHELL_COMMAND_CLOSE}") + ArtificialMessage::UserShellCommand { body }.render() } pub fn user_shell_command_record_item( @@ -72,16 +64,9 @@ mod tests { use super::*; use crate::codex::make_session_and_context; use crate::exec::StreamOutput; + use codex_protocol::models::ContentItem; use pretty_assertions::assert_eq; - #[test] - fn detects_user_shell_command_text_variants() { - assert!(is_user_shell_command_text( - "\necho hi\n" - )); - assert!(!is_user_shell_command_text("echo hi")); - } - #[tokio::test] async fn formats_basic_record() { let exec_output = ExecToolCallOutput { @@ -102,7 +87,7 @@ mod tests { }; assert_eq!( text, - "\n\necho hi\n\n\nExit code: 0\nDuration: 1.0000 seconds\nOutput:\nhi\n\n" + "\necho hi\n\n\nExit code: 0\nDuration: 1.0000 seconds\nOutput:\nhi\n" ); } @@ -120,7 +105,7 @@ mod tests { let record = format_user_shell_command_record("false", &exec_output, &turn_context); assert_eq!( record, - "\n\nfalse\n\n\nExit code: 42\nDuration: 0.1200 seconds\nOutput:\ncombined output wins\n\n" + "\nfalse\n\n\nExit code: 42\nDuration: 0.1200 seconds\nOutput:\ncombined output wins\n" ); } } diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 90b399de11..6b2cf34d08 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -1,6 +1,4 @@ use anyhow::Result; -use codex_core::protocol::COLLABORATION_MODE_CLOSE_TAG; -use codex_core::protocol::COLLABORATION_MODE_OPEN_TAG; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_protocol::artificial_messages::ArtificialMessage; @@ -61,7 +59,10 @@ fn developer_texts(input: &[Value]) -> Vec { } fn collab_xml(text: &str) -> String { - format!("{COLLABORATION_MODE_OPEN_TAG}{text}{COLLABORATION_MODE_CLOSE_TAG}") + ArtificialMessage::CollaborationMode { + body: text.to_string(), + } + .render() } fn count_exact(texts: &[String], target: &str) -> usize { diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index 897bb665d8..12a0ccceaa 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -1,13 +1,12 @@ use anyhow::Result; use codex_core::config::Constrained; use codex_core::protocol::AskForApproval; -use codex_core::protocol::COLLABORATION_MODE_CLOSE_TAG; -use codex_core::protocol::COLLABORATION_MODE_OPEN_TAG; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::RolloutItem; use codex_core::protocol::RolloutLine; use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; @@ -34,7 +33,10 @@ fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMod } fn collab_xml(text: &str) -> String { - format!("{COLLABORATION_MODE_OPEN_TAG}{text}{COLLABORATION_MODE_CLOSE_TAG}") + ArtificialMessage::CollaborationMode { + body: text.to_string(), + } + .render() } async fn read_rollout_text(path: &Path) -> anyhow::Result { diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 87978ceb5a..abc617d58b 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -6,6 +6,7 @@ use codex_core::protocol::AskForApproval; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; +use codex_protocol::artificial_messages::ArtificialMessage; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::openai_models::ConfigShellToolType; use codex_protocol::openai_models::ModelInfo; @@ -46,6 +47,13 @@ fn sse_completed(id: &str) -> String { sse(vec![ev_response_created(id), ev_completed(id)]) } +fn is_personality_spec_message(text: &str) -> bool { + matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::PersonalitySpec { .. }) + ) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn personality_does_not_mutate_base_instructions_without_template() { let codex_home = TempDir::new().expect("create temp dir"); @@ -116,7 +124,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res assert!( !developer_texts .iter() - .any(|text| text.contains("")), + .any(|text| is_personality_spec_message(text)), "did not expect a personality update message when personality is None" ); @@ -169,7 +177,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< let developer_texts = request.message_input_texts("developer"); for text in developer_texts { assert!( - !text.contains(""), + !is_personality_spec_message(&text), "expected no personality update message in developer input" ); } @@ -258,7 +266,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> let developer_texts = request.message_input_texts("developer"); let personality_text = developer_texts .iter() - .find(|text| text.contains("")) + .find(|text| is_personality_spec_message(text)) .expect("expected personality update message in developer input"); assert!( @@ -355,7 +363,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho let developer_texts = request.message_input_texts("developer"); let personality_text = developer_texts .iter() - .find(|text| text.contains("")); + .find(|text| is_personality_spec_message(text)); assert!( personality_text.is_none(), "expected no personality preamble for unchanged personality, got {personality_text:?}" @@ -461,7 +469,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> let developer_texts = request.message_input_texts("developer"); let personality_text = developer_texts .iter() - .find(|text| text.contains("")); + .find(|text| is_personality_spec_message(text)); assert!( personality_text.is_none(), "expected no personality preamble, got {personality_text:?}" @@ -835,7 +843,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - let developer_texts = request.message_input_texts("developer"); let personality_text = developer_texts .iter() - .find(|text| text.contains("")) + .find(|text| is_personality_spec_message(text)) .expect("expected personality update message in developer input"); assert!( diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index 45c91126d1..17ea46a111 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -7,6 +7,7 @@ use codex_core::protocol::ExecOutputStream; use codex_core::protocol::Op; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::TurnAbortReason; +use codex_protocol::artificial_messages::ArtificialMessage; use core_test_support::assert_regex_match; use core_test_support::responses; use core_test_support::responses::ev_assistant_message; @@ -187,12 +188,17 @@ async fn user_shell_command_history_is_persisted_and_shared_with_model() -> anyh let command_message = request .message_input_texts("user") .into_iter() - .find(|text| text.contains("")) + .find(|text| { + matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::UserShellCommand { .. }) + ) + }) .expect("command message recorded in request"); let command_message = command_message.replace("\r\n", "\n"); let escaped_command = escape(&command); let expected_pattern = format!( - r"(?m)\A\n\n{escaped_command}\n\n\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\nnot-set\n\n\z" + r"(?m)\A\n{escaped_command}\n\n\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\nnot-set\n\z" ); assert_regex_match(&expected_pattern, &command_message); @@ -244,7 +250,12 @@ async fn user_shell_command_output_is_truncated_in_history() -> anyhow::Result<( let command_message = request .message_input_texts("user") .into_iter() - .find(|text| text.contains("")) + .find(|text| { + matches!( + ArtificialMessage::parse(text), + Ok(ArtificialMessage::UserShellCommand { .. }) + ) + }) .expect("command message recorded in request"); let command_message = command_message.replace("\r\n", "\n"); @@ -255,7 +266,7 @@ async fn user_shell_command_output_is_truncated_in_history() -> anyhow::Result<( let escaped_command = escape(&command); let escaped_truncated_body = escape(&truncated_body); let expected_pattern = format!( - r"(?m)\A\n\n{escaped_command}\n\n\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\n{escaped_truncated_body}\n\n\z" + r"(?m)\A\n{escaped_command}\n\n\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\n{escaped_truncated_body}\n\z" ); assert_regex_match(&expected_pattern, &command_message); diff --git a/codex-rs/protocol/src/artificial_messages.rs b/codex-rs/protocol/src/artificial_messages.rs index 3b43462bda..e4a2f1ece4 100644 --- a/codex-rs/protocol/src/artificial_messages.rs +++ b/codex-rs/protocol/src/artificial_messages.rs @@ -3,7 +3,8 @@ //! The `artificial_messages!` macro invocation in this module defines: //! //! - The `ArtificialMessage` enum variants declared below (`Skill`, `ModelWarning`, -//! `Permission`) with their configured string fields. +//! `Permission`, `UserShellCommand`, `CollaborationMode`, `PersonalitySpec`, +//! `ExecPolicyAmendment`) with their configured string fields. //! - `ArtificialMessage::tag(&self) -> &'static str`: returns the top-level XML tag for //! the current variant (for example `skill`). //! - `ArtificialMessage::role(&self) -> &'static str`: returns the role that should be @@ -60,6 +61,34 @@ artificial_messages! { fields: { raw(body) } + }, + UserShellCommand { + tag: TAG_USER_SHELL_COMMAND, + role: "user", + fields: { + raw(body) + } + }, + CollaborationMode { + tag: TAG_COLLABORATION_MODE, + role: "developer", + fields: { + raw(body) + } + }, + PersonalitySpec { + tag: TAG_SPEC, + role: "developer", + fields: { + raw(body) + } + }, + ExecPolicyAmendment { + tag: TAG_EXEC_POLICY_AMENDMENT, + role: "developer", + fields: { + raw(body) + } } } @@ -148,4 +177,64 @@ mod tests { ArtificialMessageParseError::UnknownTopLevelTag("unknown".to_string()) ); } + + #[test] + fn render_and_parse_user_shell_command_round_trip() { + let message = ArtificialMessage::UserShellCommand { + body: "echo hi".to_string(), + }; + let rendered = message.render(); + assert_eq!( + rendered, + "echo hi" + ); + + let parsed = ArtificialMessage::parse(&rendered).expect("parse user shell command"); + assert_eq!(parsed, message); + } + + #[test] + fn render_and_parse_collaboration_mode_round_trip() { + let message = ArtificialMessage::CollaborationMode { + body: "plan first".to_string(), + }; + let rendered = message.render(); + assert_eq!( + rendered, + "plan first" + ); + + let parsed = ArtificialMessage::parse(&rendered).expect("parse collaboration mode"); + assert_eq!(parsed, message); + } + + #[test] + fn render_and_parse_personality_spec_round_trip() { + let message = ArtificialMessage::PersonalitySpec { + body: "be pragmatic".to_string(), + }; + let rendered = message.render(); + assert_eq!( + rendered, + "be pragmatic" + ); + + let parsed = ArtificialMessage::parse(&rendered).expect("parse personality spec"); + assert_eq!(parsed, message); + } + + #[test] + fn render_and_parse_exec_policy_amendment_round_trip() { + let message = ArtificialMessage::ExecPolicyAmendment { + body: "Approved command prefix saved:\n- `echo`".to_string(), + }; + let rendered = message.render(); + assert_eq!( + rendered, + "Approved command prefix saved:\n- `echo`" + ); + + let parsed = ArtificialMessage::parse(&rendered).expect("parse exec policy amendment"); + assert_eq!(parsed, message); + } } diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 686e379850..5dcc8e329e 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -12,8 +12,6 @@ use crate::artificial_messages::ArtificialMessage; use crate::config_types::CollaborationMode; use crate::config_types::SandboxMode; use crate::protocol::AskForApproval; -use crate::protocol::COLLABORATION_MODE_CLOSE_TAG; -use crate::protocol::COLLABORATION_MODE_OPEN_TAG; use crate::protocol::NetworkAccess; use crate::protocol::SandboxPolicy; use crate::protocol::WritableRoot; @@ -276,10 +274,10 @@ impl DeveloperInstructions { } pub fn personality_spec_message(spec: String) -> Self { - let message = format!( - " The user has requested a new communication style. Future messages should adhere to the following personality: \n{spec} " + let body = format!( + " The user has requested a new communication style. Future messages should adhere to the following personality: \n{spec} " ); - DeveloperInstructions::new(message) + DeveloperInstructions::new(ArtificialMessage::PersonalitySpec { body }.render()) } pub fn from_policy( @@ -323,9 +321,8 @@ impl DeveloperInstructions { .as_ref() .filter(|instructions| !instructions.is_empty()) .map(|instructions| { - DeveloperInstructions::new(format!( - "{COLLABORATION_MODE_OPEN_TAG}{instructions}{COLLABORATION_MODE_CLOSE_TAG}" - )) + let body = instructions.to_string(); + DeveloperInstructions::new(ArtificialMessage::CollaborationMode { body }.render()) }) }