mirror of
https://github.com/openai/codex.git
synced 2026-03-03 21:23:18 +00:00
Compare commits
2 Commits
fix/notify
...
jif/artifi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
365c5b76fc | ||
|
|
0b2b8c9012 |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -1940,6 +1940,7 @@ dependencies = [
|
||||
"strum_macros 0.27.2",
|
||||
"sys-locale",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"ts-rs",
|
||||
"uuid",
|
||||
|
||||
@@ -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("<personality_spec>")),
|
||||
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();
|
||||
|
||||
@@ -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("<personality_spec>")),
|
||||
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("<personality_spec>")),
|
||||
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("<personality_spec>")),
|
||||
second_developer_texts.iter().any(|text| matches!(
|
||||
ArtificialMessage::parse(text),
|
||||
Ok(ArtificialMessage::PersonalitySpec { .. })
|
||||
)),
|
||||
"expected personality update message in second request, got {second_developer_texts:?}"
|
||||
);
|
||||
|
||||
|
||||
@@ -201,6 +201,7 @@ use crate::util::backoff;
|
||||
use crate::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_async_utils::OrCancelExt;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::artificial_messages::ArtificialMessage;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
@@ -1522,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))
|
||||
@@ -1534,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()
|
||||
@@ -1802,15 +1806,10 @@ impl Session {
|
||||
self.services
|
||||
.otel_manager
|
||||
.counter("codex.model_warning", 1, &[]);
|
||||
let item = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: format!("Warning: {}", message.into()),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
};
|
||||
let item = ArtificialMessage::ModelWarning {
|
||||
body: message.into(),
|
||||
}
|
||||
.to_response_item();
|
||||
|
||||
self.record_conversation_items(ctx, &[item]).await;
|
||||
}
|
||||
@@ -5573,10 +5572,14 @@ mod tests {
|
||||
match last {
|
||||
ResponseItem::Message { role, content, .. } => {
|
||||
assert_eq!(role, "user");
|
||||
let expected_warning = ArtificialMessage::ModelWarning {
|
||||
body: "too many unified exec processes".to_string(),
|
||||
}
|
||||
.render();
|
||||
assert_eq!(
|
||||
content,
|
||||
&vec![ContentItem::InputText {
|
||||
text: "Warning: too many unified exec processes".to_string(),
|
||||
text: expected_warning,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use crate::truncate;
|
||||
use crate::truncate::TruncationPolicy;
|
||||
use codex_git::GhostCommit;
|
||||
use codex_protocol::artificial_messages::ArtificialMessage;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
@@ -60,6 +61,15 @@ fn user_input_text_msg(text: &str) -> ResponseItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn demo_skill_message() -> String {
|
||||
ArtificialMessage::Skill {
|
||||
name: "demo".to_string(),
|
||||
path: "skills/demo/SKILL.md".to_string(),
|
||||
body: "body".to_string(),
|
||||
}
|
||||
.render()
|
||||
}
|
||||
|
||||
fn function_call_output(call_id: &str, content: &str) -> ResponseItem {
|
||||
ResponseItem::FunctionCallOutput {
|
||||
call_id: call_id.to_string(),
|
||||
@@ -445,10 +455,13 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
|
||||
user_input_text_msg(
|
||||
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
|
||||
),
|
||||
user_input_text_msg(&demo_skill_message()),
|
||||
user_input_text_msg(
|
||||
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
|
||||
&ArtificialMessage::UserShellCommand {
|
||||
body: "echo 42".to_string(),
|
||||
}
|
||||
.render(),
|
||||
),
|
||||
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
|
||||
user_input_text_msg("turn 1 user"),
|
||||
assistant_msg("turn 1 assistant"),
|
||||
user_input_text_msg("turn 2 user"),
|
||||
@@ -464,10 +477,13 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
|
||||
user_input_text_msg(
|
||||
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
|
||||
),
|
||||
user_input_text_msg(&demo_skill_message()),
|
||||
user_input_text_msg(
|
||||
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
|
||||
&ArtificialMessage::UserShellCommand {
|
||||
body: "echo 42".to_string(),
|
||||
}
|
||||
.render(),
|
||||
),
|
||||
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
|
||||
user_input_text_msg("turn 1 user"),
|
||||
assistant_msg("turn 1 assistant"),
|
||||
];
|
||||
@@ -480,10 +496,13 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
|
||||
user_input_text_msg(
|
||||
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
|
||||
),
|
||||
user_input_text_msg(&demo_skill_message()),
|
||||
user_input_text_msg(
|
||||
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
|
||||
&ArtificialMessage::UserShellCommand {
|
||||
body: "echo 42".to_string(),
|
||||
}
|
||||
.render(),
|
||||
),
|
||||
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
|
||||
];
|
||||
|
||||
let mut history = create_history_with_items(vec![
|
||||
@@ -492,10 +511,13 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
|
||||
user_input_text_msg(
|
||||
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
|
||||
),
|
||||
user_input_text_msg(&demo_skill_message()),
|
||||
user_input_text_msg(
|
||||
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
|
||||
&ArtificialMessage::UserShellCommand {
|
||||
body: "echo 42".to_string(),
|
||||
}
|
||||
.render(),
|
||||
),
|
||||
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
|
||||
user_input_text_msg("turn 1 user"),
|
||||
assistant_msg("turn 1 assistant"),
|
||||
user_input_text_msg("turn 2 user"),
|
||||
@@ -510,10 +532,13 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
|
||||
user_input_text_msg(
|
||||
"# AGENTS.md instructions for test_directory\n\n<INSTRUCTIONS>\ntest_text\n</INSTRUCTIONS>",
|
||||
),
|
||||
user_input_text_msg(&demo_skill_message()),
|
||||
user_input_text_msg(
|
||||
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
|
||||
&ArtificialMessage::UserShellCommand {
|
||||
body: "echo 42".to_string(),
|
||||
}
|
||||
.render(),
|
||||
),
|
||||
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
|
||||
user_input_text_msg("turn 1 user"),
|
||||
assistant_msg("turn 1 assistant"),
|
||||
user_input_text_msg("turn 2 user"),
|
||||
|
||||
@@ -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<UserMessageItem> {
|
||||
@@ -43,7 +43,12 @@ fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
|
||||
{
|
||||
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 {
|
||||
@@ -146,6 +151,7 @@ pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_turn_item;
|
||||
use codex_protocol::artificial_messages::ArtificialMessage;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::WebSearchItem;
|
||||
@@ -284,6 +290,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn skips_user_instructions_and_env() {
|
||||
let skill_message = ArtificialMessage::Skill {
|
||||
name: "demo".to_string(),
|
||||
path: "skills/demo/SKILL.md".to_string(),
|
||||
body: "body".to_string(),
|
||||
}
|
||||
.render();
|
||||
let items = vec![
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
@@ -316,8 +328,7 @@ mod tests {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
|
||||
.to_string(),
|
||||
text: skill_message,
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
@@ -326,10 +337,13 @@ mod tests {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<user_shell_command>echo 42</user_shell_command>".to_string(),
|
||||
text: ArtificialMessage::UserShellCommand {
|
||||
body: "echo 42".to_string(),
|
||||
}
|
||||
.render(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
phase: None,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use codex_protocol::artificial_messages::ArtificialMessage;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -64,18 +65,12 @@ impl SkillInstructions {
|
||||
|
||||
impl From<SkillInstructions> for ResponseItem {
|
||||
fn from(si: SkillInstructions) -> Self {
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: format!(
|
||||
"<skill>\n<name>{}</name>\n<path>{}</path>\n{}\n</skill>",
|
||||
si.name, si.path, si.contents
|
||||
),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
ArtificialMessage::Skill {
|
||||
name: si.name,
|
||||
path: si.path,
|
||||
body: si.contents,
|
||||
}
|
||||
.to_response_item()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +79,15 @@ mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
fn demo_skill_message() -> String {
|
||||
ArtificialMessage::Skill {
|
||||
name: "demo-skill".to_string(),
|
||||
path: "skills/demo/SKILL.md".to_string(),
|
||||
body: "body".to_string(),
|
||||
}
|
||||
.render()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_user_instructions() {
|
||||
let user_instructions = UserInstructions {
|
||||
@@ -146,18 +150,15 @@ mod tests {
|
||||
panic!("expected one InputText content item");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
text,
|
||||
"<skill>\n<name>demo-skill</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
|
||||
);
|
||||
let expected = demo_skill_message();
|
||||
assert_eq!(text, &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_skill_instructions() {
|
||||
assert!(SkillInstructions::is_skill_instructions(&[
|
||||
ContentItem::InputText {
|
||||
text: "<skill>\n<name>demo-skill</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
|
||||
.to_string(),
|
||||
text: demo_skill_message(),
|
||||
}
|
||||
]));
|
||||
assert!(!SkillInstructions::is_skill_instructions(&[
|
||||
|
||||
@@ -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 = "<user_shell_command>";
|
||||
pub const USER_SHELL_COMMAND_CLOSE: &str = "</user_shell_command>";
|
||||
|
||||
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(
|
||||
"<user_shell_command>\necho hi\n</user_shell_command>"
|
||||
));
|
||||
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,
|
||||
"<user_shell_command>\n<command>\necho hi\n</command>\n<result>\nExit code: 0\nDuration: 1.0000 seconds\nOutput:\nhi\n</result>\n</user_shell_command>"
|
||||
"<user_shell_cmd><command>\necho hi\n</command>\n<result>\nExit code: 0\nDuration: 1.0000 seconds\nOutput:\nhi\n</result></user_shell_cmd>"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -120,7 +105,7 @@ mod tests {
|
||||
let record = format_user_shell_command_record("false", &exec_output, &turn_context);
|
||||
assert_eq!(
|
||||
record,
|
||||
"<user_shell_command>\n<command>\nfalse\n</command>\n<result>\nExit code: 42\nDuration: 0.1200 seconds\nOutput:\ncombined output wins\n</result>\n</user_shell_command>"
|
||||
"<user_shell_cmd><command>\nfalse\n</command>\n<result>\nExit code: 42\nDuration: 0.1200 seconds\nOutput:\ncombined output wins\n</result></user_shell_cmd>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_otel::OtelManager;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::artificial_messages::ArtificialMessage;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
@@ -343,7 +344,13 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
|
||||
);
|
||||
let pos_permissions = messages
|
||||
.iter()
|
||||
.position(|(role, text)| role == "developer" && text.contains("<permissions instructions>"))
|
||||
.position(|(role, text)| {
|
||||
role == "developer"
|
||||
&& matches!(
|
||||
ArtificialMessage::parse(text),
|
||||
Ok(ArtificialMessage::Permission { .. })
|
||||
)
|
||||
})
|
||||
.expect("permissions message");
|
||||
let pos_user_instructions = messages
|
||||
.iter()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
@@ -60,7 +59,10 @@ fn developer_texts(input: &[Value]) -> Vec<String> {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -90,7 +92,10 @@ async fn no_collaboration_instructions_by_default() -> Result<()> {
|
||||
let input = req.single_request().input();
|
||||
let dev_texts = developer_texts(&input);
|
||||
assert_eq!(dev_texts.len(), 1);
|
||||
assert!(dev_texts[0].contains("<permissions instructions>"));
|
||||
assert!(matches!(
|
||||
ArtificialMessage::parse(&dev_texts[0]),
|
||||
Ok(ArtificialMessage::Permission { .. })
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_protocol::artificial_messages::ArtificialMessage;
|
||||
use codex_protocol::models::DeveloperInstructions;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -34,7 +35,10 @@ fn permissions_texts(input: &[serde_json::Value]) -> Vec<String> {
|
||||
.first()?
|
||||
.get("text")?
|
||||
.as_str()?;
|
||||
if text.contains("<permissions instructions>") {
|
||||
if matches!(
|
||||
ArtificialMessage::parse(text),
|
||||
Ok(ArtificialMessage::Permission { .. })
|
||||
) {
|
||||
Some(text.to_string())
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -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("<personality_spec>")),
|
||||
.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("<personality_spec>"),
|
||||
!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("<personality_spec>"))
|
||||
.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("<personality_spec>"));
|
||||
.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("<personality_spec>"));
|
||||
.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("<personality_spec>"))
|
||||
.find(|text| is_personality_spec_message(text))
|
||||
.expect("expected personality update message in developer input");
|
||||
|
||||
assert!(
|
||||
|
||||
@@ -5,6 +5,7 @@ use anyhow::Result;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_protocol::artificial_messages::ArtificialMessage;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
@@ -91,13 +92,15 @@ async fn user_turn_includes_skill_instructions() -> Result<()> {
|
||||
let request = mock.single_request();
|
||||
let user_texts = request.message_input_texts("user");
|
||||
let skill_path_str = skill_path.to_string_lossy();
|
||||
let skill_contents = fs::read_to_string(&skill_path)?;
|
||||
let expected_skill_text = ArtificialMessage::Skill {
|
||||
name: "demo".to_string(),
|
||||
path: skill_path_str.to_string(),
|
||||
body: skill_contents,
|
||||
}
|
||||
.render();
|
||||
assert!(
|
||||
user_texts.iter().any(|text| {
|
||||
text.contains("<skill>\n<name>demo</name>")
|
||||
&& text.contains("<path>")
|
||||
&& text.contains(skill_body)
|
||||
&& text.contains(skill_path_str.as_ref())
|
||||
}),
|
||||
user_texts.iter().any(|text| text == &expected_skill_text),
|
||||
"expected skill instructions in user input, got {user_texts:?}"
|
||||
);
|
||||
|
||||
|
||||
@@ -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("<user_shell_command>"))
|
||||
.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<user_shell_command>\n<command>\n{escaped_command}\n</command>\n<result>\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\nnot-set\n</result>\n</user_shell_command>\z"
|
||||
r"(?m)\A<user_shell_cmd><command>\n{escaped_command}\n</command>\n<result>\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\nnot-set\n</result></user_shell_cmd>\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("<user_shell_command>"))
|
||||
.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<user_shell_command>\n<command>\n{escaped_command}\n</command>\n<result>\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\n{escaped_truncated_body}\n</result>\n</user_shell_command>\z"
|
||||
r"(?m)\A<user_shell_cmd><command>\n{escaped_command}\n</command>\n<result>\nExit code: 0\nDuration: [0-9]+(?:\.[0-9]+)? seconds\nOutput:\n{escaped_truncated_body}\n</result></user_shell_cmd>\z"
|
||||
);
|
||||
assert_regex_match(&expected_pattern, &command_message);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ serde_with = { workspace = true, features = ["macros", "base64"] }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
sys-locale = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
ts-rs = { workspace = true, features = [
|
||||
"uuid-impl",
|
||||
|
||||
240
codex-rs/protocol/src/artificial_messages.rs
Normal file
240
codex-rs/protocol/src/artificial_messages.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
//! Artificial message helpers shared between prompt construction and protocol parsing.
|
||||
//!
|
||||
//! The `artificial_messages!` macro invocation in this module defines:
|
||||
//!
|
||||
//! - The `ArtificialMessage` enum variants declared below (`Skill`, `ModelWarning`,
|
||||
//! `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
|
||||
//! used when this message is emitted as a `ResponseItem`.
|
||||
//! - `ArtificialMessage::render(&self) -> String`: serializes the message to the tagged
|
||||
//! XML-like envelope.
|
||||
//! - `ArtificialMessage::parse(input: &str) -> Result<Self, ArtificialMessageParseError>`:
|
||||
//! parses an envelope back into an enum variant.
|
||||
//! - `ArtificialMessage::detect_tag(input: &str) -> Option<&'static str>`: returns the
|
||||
//! known artificial-message tag if the input is a recognized envelope.
|
||||
//! - `ArtificialMessage::is_artificial(input: &str) -> bool`: convenience boolean check
|
||||
//! built on top of `detect_tag`.
|
||||
//! - `ArtificialMessage::to_response_item(&self) -> ResponseItem`: converts the rendered
|
||||
//! content into a message-shaped `ResponseItem`.
|
||||
//!
|
||||
//! This module also provides `impl From<ArtificialMessage> for ResponseItem` as a
|
||||
//! convenience wrapper around `to_response_item`.
|
||||
//!
|
||||
use crate::models::ContentItem;
|
||||
use crate::models::ResponseItem;
|
||||
use thiserror::Error;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
pub const TAG_SKILL: &str = "skill";
|
||||
pub const TAG_MODEL_WARNING: &str = "warning";
|
||||
pub const TAG_USER_SHELL_COMMAND: &str = "user_shell_cmd";
|
||||
pub const TAG_PERMISSION: &str = "permissions_instructions";
|
||||
pub const TAG_COLLABORATION_MODE: &str = "collaboration_mode";
|
||||
pub const TAG_SPEC: &str = "personality_spec";
|
||||
pub const TAG_EXEC_POLICY_AMENDMENT: &str = "exec_policy_amendment";
|
||||
|
||||
artificial_messages! {
|
||||
Skill {
|
||||
tag: TAG_SKILL,
|
||||
role: "user",
|
||||
fields: {
|
||||
tagged(name, "name"),
|
||||
tagged(path, "path"),
|
||||
raw(body)
|
||||
}
|
||||
},
|
||||
ModelWarning {
|
||||
tag: TAG_MODEL_WARNING,
|
||||
role: "user",
|
||||
fields: {
|
||||
raw(body)
|
||||
}
|
||||
},
|
||||
Permission {
|
||||
tag: TAG_PERMISSION,
|
||||
role: "developer",
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum ArtificialMessageParseError {
|
||||
#[error("invalid artificial message envelope")]
|
||||
InvalidEnvelope,
|
||||
#[error("unknown artificial message tag: {0}")]
|
||||
UnknownTopLevelTag(String),
|
||||
#[error("missing field <{field_tag}> in message <{message_tag}>")]
|
||||
MissingField {
|
||||
message_tag: &'static str,
|
||||
field_tag: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ArtificialMessage> for ResponseItem {
|
||||
fn from(value: ArtificialMessage) -> Self {
|
||||
value.to_response_item()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn render_and_parse_skill_round_trip() {
|
||||
let message = ArtificialMessage::Skill {
|
||||
name: "demo".to_string(),
|
||||
path: "skills/demo/SKILL.md".to_string(),
|
||||
body: "body".to_string(),
|
||||
};
|
||||
|
||||
let rendered = message.render();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>"
|
||||
);
|
||||
|
||||
let parsed = ArtificialMessage::parse(&rendered).expect("parse skill");
|
||||
assert_eq!(parsed, message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_and_parse_warning_round_trip() {
|
||||
let message = ArtificialMessage::ModelWarning {
|
||||
body: "be careful".to_string(),
|
||||
};
|
||||
let rendered = message.render();
|
||||
assert_eq!(rendered, "<warning>be careful</warning>");
|
||||
|
||||
let parsed = ArtificialMessage::parse(&rendered).expect("parse warning");
|
||||
assert_eq!(parsed, message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_and_parse_permission_round_trip() {
|
||||
let message = ArtificialMessage::Permission {
|
||||
body: "policy text".to_string(),
|
||||
};
|
||||
let rendered = message.render();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<permissions_instructions>policy text</permissions_instructions>"
|
||||
);
|
||||
|
||||
let parsed = ArtificialMessage::parse(&rendered).expect("parse permission");
|
||||
assert_eq!(parsed, message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_tag_returns_known_tag() {
|
||||
let text = "<permissions_instructions>abc</permissions_instructions>";
|
||||
assert_eq!(ArtificialMessage::detect_tag(text), Some(TAG_PERMISSION));
|
||||
assert!(ArtificialMessage::is_artificial(text));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_unknown_tag() {
|
||||
let text = "<unknown>abc</unknown>";
|
||||
let err = ArtificialMessage::parse(text).expect_err("unknown tag should fail");
|
||||
assert_eq!(
|
||||
err,
|
||||
ArtificialMessageParseError::UnknownTopLevelTag("unknown".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_and_parse_user_shell_command_round_trip() {
|
||||
let message = ArtificialMessage::UserShellCommand {
|
||||
body: "<command>echo hi</command>".to_string(),
|
||||
};
|
||||
let rendered = message.render();
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"<user_shell_cmd><command>echo hi</command></user_shell_cmd>"
|
||||
);
|
||||
|
||||
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,
|
||||
"<collaboration_mode>plan first</collaboration_mode>"
|
||||
);
|
||||
|
||||
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,
|
||||
"<personality_spec>be pragmatic</personality_spec>"
|
||||
);
|
||||
|
||||
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,
|
||||
"<exec_policy_amendment>Approved command prefix saved:\n- `echo`</exec_policy_amendment>"
|
||||
);
|
||||
|
||||
let parsed = ArtificialMessage::parse(&rendered).expect("parse exec policy amendment");
|
||||
assert_eq!(parsed, message);
|
||||
}
|
||||
}
|
||||
206
codex-rs/protocol/src/artificial_messages/macros.rs
Normal file
206
codex-rs/protocol/src/artificial_messages/macros.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use crate::artificial_messages::ArtificialMessageParseError;
|
||||
|
||||
macro_rules! render_field {
|
||||
($tagged:ident, $raw:ident, tagged, $value:ident, $field_tag:literal) => {{
|
||||
$tagged.push_str(&$crate::artificial_messages::macros::xml(
|
||||
$field_tag, $value,
|
||||
));
|
||||
$tagged.push('\n');
|
||||
}};
|
||||
($tagged:ident, $raw:ident, raw, $value:ident) => {{
|
||||
$raw = Some($value.as_str());
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! render_fields {
|
||||
($($kind:ident($value:ident $(, $field_tag:literal)?)),* $(,)?) => {{
|
||||
#[allow(unused_mut, unused_assignments)]
|
||||
let mut tagged = String::new();
|
||||
#[allow(unused_mut, unused_assignments)]
|
||||
let mut raw = None;
|
||||
$(
|
||||
render_field!(tagged, raw, $kind, $value $(, $field_tag)?);
|
||||
)*
|
||||
|
||||
if tagged.is_empty() {
|
||||
raw.unwrap_or_default().to_string()
|
||||
} else {
|
||||
let mut out = String::new();
|
||||
out.push('\n');
|
||||
out.push_str(&tagged);
|
||||
if let Some(raw) = raw {
|
||||
out.push_str(raw);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! parse_field {
|
||||
($remaining:ident, $consumed_tagged:ident, $msg_tag:expr, tagged, $field:ident, $field_tag:literal) => {
|
||||
let tagged_input = $remaining.strip_prefix('\n').unwrap_or($remaining);
|
||||
let ($field, rest) =
|
||||
$crate::artificial_messages::macros::extract_prefixed_tag(tagged_input, $field_tag)
|
||||
.ok_or_else(|| ArtificialMessageParseError::MissingField {
|
||||
message_tag: $msg_tag,
|
||||
field_tag: $field_tag,
|
||||
})?;
|
||||
let $field = $field.to_string();
|
||||
$remaining = rest;
|
||||
if !$consumed_tagged {
|
||||
$consumed_tagged = true;
|
||||
}
|
||||
};
|
||||
($remaining:ident, $consumed_tagged:ident, $msg_tag:expr, raw, $field:ident) => {
|
||||
let mut value = $remaining;
|
||||
if $consumed_tagged {
|
||||
value = value.strip_prefix('\n').unwrap_or(value);
|
||||
value = value.strip_suffix('\n').unwrap_or(value);
|
||||
}
|
||||
let $field = value.to_string();
|
||||
$remaining = "";
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_variant {
|
||||
($variant:ident, $inner:expr, $msg_tag:expr, $($kind:ident($field:ident $(, $field_tag:literal)?)),* $(,)?) => {{
|
||||
#[allow(unused_mut, unused_assignments)]
|
||||
let mut remaining = $inner;
|
||||
#[allow(unused_mut, unused_assignments)]
|
||||
let mut consumed_tagged = false;
|
||||
$(
|
||||
parse_field!(
|
||||
remaining,
|
||||
consumed_tagged,
|
||||
$msg_tag,
|
||||
$kind,
|
||||
$field
|
||||
$(, $field_tag)?
|
||||
);
|
||||
)*
|
||||
let _ = &remaining;
|
||||
let _ = consumed_tagged;
|
||||
Ok(ArtificialMessage::$variant { $( $field ),* })
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! artificial_messages {
|
||||
(
|
||||
$(
|
||||
$variant:ident {
|
||||
tag: $tag:path,
|
||||
role: $role:literal,
|
||||
fields: { $($kind:ident($field:ident $(, $field_tag:literal)?)),* $(,)? }
|
||||
}
|
||||
),* $(,)?
|
||||
) => {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ArtificialMessage {
|
||||
$(
|
||||
$variant { $( $field: String ),* }
|
||||
),*
|
||||
}
|
||||
|
||||
impl ArtificialMessage {
|
||||
pub fn tag(&self) -> &'static str {
|
||||
match self {
|
||||
$( Self::$variant { .. } => $tag ),*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn role(&self) -> &'static str {
|
||||
match self {
|
||||
$( Self::$variant { .. } => $role ),*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
match self {
|
||||
$(
|
||||
Self::$variant { $( $field ),* } => {
|
||||
let payload = render_fields!($($kind($field $(, $field_tag)?)),*);
|
||||
$crate::artificial_messages::macros::xml($tag, payload)
|
||||
}
|
||||
),*
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(input: &str) -> Result<Self, ArtificialMessageParseError> {
|
||||
let (tag, inner) = $crate::artificial_messages::macros::split_outer_tag(input)?;
|
||||
match tag {
|
||||
$(
|
||||
$tag => parse_variant!($variant, inner, $tag, $($kind($field $(, $field_tag)?)),*),
|
||||
)*
|
||||
unknown => Err(ArtificialMessageParseError::UnknownTopLevelTag(
|
||||
unknown.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_tag(input: &str) -> Option<&'static str> {
|
||||
let (tag, _) = $crate::artificial_messages::macros::split_outer_tag(input).ok()?;
|
||||
match tag {
|
||||
$( $tag => Some($tag), )*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_artificial(input: &str) -> bool {
|
||||
Self::detect_tag(input).is_some()
|
||||
}
|
||||
|
||||
pub fn to_response_item(&self) -> ResponseItem {
|
||||
ResponseItem::Message {
|
||||
role: self.role().to_string(),
|
||||
id: None,
|
||||
content: vec![ContentItem::InputText {
|
||||
text: self.render(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn split_outer_tag(input: &str) -> Result<(&str, &str), ArtificialMessageParseError> {
|
||||
let text = input.trim();
|
||||
if !text.starts_with('<') {
|
||||
return Err(ArtificialMessageParseError::InvalidEnvelope);
|
||||
}
|
||||
|
||||
let Some(open_end) = text.find('>') else {
|
||||
return Err(ArtificialMessageParseError::InvalidEnvelope);
|
||||
};
|
||||
|
||||
let tag = &text[1..open_end];
|
||||
if tag.is_empty() || tag.starts_with('/') || tag.chars().any(char::is_whitespace) {
|
||||
return Err(ArtificialMessageParseError::InvalidEnvelope);
|
||||
}
|
||||
|
||||
let close = format!("</{tag}>");
|
||||
let Some(inner) = text[open_end + 1..].strip_suffix(&close) else {
|
||||
return Err(ArtificialMessageParseError::InvalidEnvelope);
|
||||
};
|
||||
|
||||
Ok((tag, inner))
|
||||
}
|
||||
|
||||
pub(crate) fn extract_prefixed_tag<'a>(
|
||||
text: &'a str,
|
||||
field_tag: &'static str,
|
||||
) -> Option<(&'a str, &'a str)> {
|
||||
let open = format!("<{field_tag}>");
|
||||
let close = format!("</{field_tag}>");
|
||||
let after_open = text.strip_prefix(&open)?;
|
||||
let end = after_open.find(&close)?;
|
||||
let value = &after_open[..end];
|
||||
let rest = &after_open[end + close.len()..];
|
||||
Some((value, rest))
|
||||
}
|
||||
|
||||
pub(crate) fn xml(tag: &str, payload: impl std::fmt::Display) -> String {
|
||||
format!("<{tag}>{payload}</{tag}>")
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod account;
|
||||
pub mod artificial_messages;
|
||||
mod thread_id;
|
||||
pub use thread_id::ThreadId;
|
||||
pub mod approvals;
|
||||
|
||||
@@ -8,11 +8,10 @@ use serde::Serialize;
|
||||
use serde::ser::Serializer;
|
||||
use ts_rs::TS;
|
||||
|
||||
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;
|
||||
@@ -275,10 +274,10 @@ impl DeveloperInstructions {
|
||||
}
|
||||
|
||||
pub fn personality_spec_message(spec: String) -> Self {
|
||||
let message = format!(
|
||||
"<personality_spec> The user has requested a new communication style. Future messages should adhere to the following personality: \n{spec} </personality_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(
|
||||
@@ -322,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())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -336,20 +334,16 @@ impl DeveloperInstructions {
|
||||
request_rule_enabled: bool,
|
||||
writable_roots: Option<Vec<WritableRoot>>,
|
||||
) -> Self {
|
||||
let start_tag = DeveloperInstructions::new("<permissions instructions>");
|
||||
let end_tag = DeveloperInstructions::new("</permissions instructions>");
|
||||
start_tag
|
||||
.concat(DeveloperInstructions::sandbox_text(
|
||||
sandbox_mode,
|
||||
network_access,
|
||||
))
|
||||
let body = DeveloperInstructions::sandbox_text(sandbox_mode, network_access)
|
||||
.concat(DeveloperInstructions::from(
|
||||
approval_policy,
|
||||
exec_policy,
|
||||
request_rule_enabled,
|
||||
))
|
||||
.concat(DeveloperInstructions::from_writable_roots(writable_roots))
|
||||
.concat(end_tag)
|
||||
.into_text();
|
||||
|
||||
DeveloperInstructions::new(ArtificialMessage::Permission { body }.render())
|
||||
}
|
||||
|
||||
fn from_writable_roots(writable_roots: Option<Vec<WritableRoot>>) -> Self {
|
||||
|
||||
Reference in New Issue
Block a user