diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 33244c118..b57b6164b 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1883,6 +1883,7 @@ dependencies = [ "codex-features", "codex-git-utils", "codex-hooks", + "codex-instructions", "codex-login", "codex-network-proxy", "codex-otel", @@ -2174,6 +2175,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "codex-instructions" +version = "0.0.0" +dependencies = [ + "codex-protocol", + "pretty_assertions", + "serde", +] + [[package]] name = "codex-keyring-store" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 3d2ee9948..126351131 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -25,6 +25,7 @@ members = [ "skills", "core", "hooks", + "instructions", "secrets", "exec", "exec-server", @@ -123,6 +124,7 @@ codex-features = { path = "features" } codex-file-search = { path = "file-search" } codex-git-utils = { path = "git-utils" } codex-hooks = { path = "hooks" } +codex-instructions = { path = "instructions" } codex-keyring-store = { path = "keyring-store" } codex-linux-sandbox = { path = "linux-sandbox" } codex-lmstudio = { path = "lmstudio" } diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index a3868e435..d335dc6cc 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -42,6 +42,7 @@ codex-skills = { workspace = true } codex-execpolicy = { workspace = true } codex-git-utils = { workspace = true } codex-hooks = { workspace = true } +codex-instructions = { workspace = true } codex-network-proxy = { workspace = true } codex-otel = { workspace = true } codex-artifacts = { workspace = true } diff --git a/codex-rs/core/src/contextual_user_message.rs b/codex-rs/core/src/contextual_user_message.rs index 4df05f0da..68a86cc88 100644 --- a/codex-rs/core/src/contextual_user_message.rs +++ b/codex-rs/core/src/contextual_user_message.rs @@ -1,14 +1,12 @@ +use codex_instructions::AGENTS_MD_FRAGMENT; +use codex_instructions::ContextualUserFragmentDefinition; +use codex_instructions::SKILL_FRAGMENT; use codex_protocol::items::HookPromptItem; use codex_protocol::items::parse_hook_prompt_fragment; use codex_protocol::models::ContentItem; -use codex_protocol::models::ResponseItem; use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG; use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; -pub(crate) const AGENTS_MD_START_MARKER: &str = "# AGENTS.md instructions for "; -pub(crate) const AGENTS_MD_END_MARKER: &str = ""; -pub(crate) const SKILL_OPEN_TAG: &str = ""; -pub(crate) const SKILL_CLOSE_TAG: &str = ""; pub(crate) const USER_SHELL_COMMAND_OPEN_TAG: &str = ""; pub(crate) const USER_SHELL_COMMAND_CLOSE_TAG: &str = ""; pub(crate) const TURN_ABORTED_OPEN_TAG: &str = ""; @@ -16,64 +14,11 @@ pub(crate) const TURN_ABORTED_CLOSE_TAG: &str = ""; pub(crate) const SUBAGENT_NOTIFICATION_OPEN_TAG: &str = ""; pub(crate) const SUBAGENT_NOTIFICATION_CLOSE_TAG: &str = ""; -#[derive(Clone, Copy)] -pub(crate) struct ContextualUserFragmentDefinition { - start_marker: &'static str, - end_marker: &'static str, -} - -impl ContextualUserFragmentDefinition { - pub(crate) const fn new(start_marker: &'static str, end_marker: &'static str) -> Self { - Self { - start_marker, - end_marker, - } - } - - pub(crate) fn matches_text(&self, text: &str) -> bool { - let trimmed = text.trim_start(); - let starts_with_marker = trimmed - .get(..self.start_marker.len()) - .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.start_marker)); - let trimmed = trimmed.trim_end(); - let ends_with_marker = trimmed - .get(trimmed.len().saturating_sub(self.end_marker.len())..) - .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.end_marker)); - starts_with_marker && ends_with_marker - } - - pub(crate) const fn start_marker(&self) -> &'static str { - self.start_marker - } - - pub(crate) const fn end_marker(&self) -> &'static str { - self.end_marker - } - - pub(crate) fn wrap(&self, body: String) -> String { - format!("{}\n{}\n{}", self.start_marker, body, self.end_marker) - } - - pub(crate) fn into_message(self, text: String) -> ResponseItem { - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { text }], - end_turn: None, - phase: None, - } - } -} - -pub(crate) const AGENTS_MD_FRAGMENT: ContextualUserFragmentDefinition = - ContextualUserFragmentDefinition::new(AGENTS_MD_START_MARKER, AGENTS_MD_END_MARKER); pub(crate) const ENVIRONMENT_CONTEXT_FRAGMENT: ContextualUserFragmentDefinition = ContextualUserFragmentDefinition::new( ENVIRONMENT_CONTEXT_OPEN_TAG, ENVIRONMENT_CONTEXT_CLOSE_TAG, ); -pub(crate) const SKILL_FRAGMENT: ContextualUserFragmentDefinition = - ContextualUserFragmentDefinition::new(SKILL_OPEN_TAG, SKILL_CLOSE_TAG); pub(crate) const USER_SHELL_COMMAND_FRAGMENT: ContextualUserFragmentDefinition = ContextualUserFragmentDefinition::new( USER_SHELL_COMMAND_OPEN_TAG, diff --git a/codex-rs/core/src/contextual_user_message_tests.rs b/codex-rs/core/src/contextual_user_message_tests.rs index f71ca35f6..93db860ea 100644 --- a/codex-rs/core/src/contextual_user_message_tests.rs +++ b/codex-rs/core/src/contextual_user_message_tests.rs @@ -1,6 +1,7 @@ use super::*; use codex_protocol::items::HookPromptFragment; use codex_protocol::items::build_hook_prompt_message; +use codex_protocol::models::ResponseItem; #[test] fn detects_environment_context_fragment() { diff --git a/codex-rs/core/src/instructions/mod.rs b/codex-rs/core/src/instructions/mod.rs index 9f1d95d2f..0ef48ff62 100644 --- a/codex-rs/core/src/instructions/mod.rs +++ b/codex-rs/core/src/instructions/mod.rs @@ -1,5 +1,3 @@ -mod user_instructions; - -pub(crate) use user_instructions::SkillInstructions; -pub use user_instructions::USER_INSTRUCTIONS_PREFIX; -pub(crate) use user_instructions::UserInstructions; +pub(crate) use codex_instructions::SkillInstructions; +pub use codex_instructions::USER_INSTRUCTIONS_PREFIX; +pub(crate) use codex_instructions::UserInstructions; diff --git a/codex-rs/instructions/BUILD.bazel b/codex-rs/instructions/BUILD.bazel new file mode 100644 index 000000000..d3cdbd19d --- /dev/null +++ b/codex-rs/instructions/BUILD.bazel @@ -0,0 +1,16 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "instructions", + crate_name = "codex_instructions", + compile_data = glob( + include = ["**"], + exclude = [ + "BUILD.bazel", + "Cargo.toml", + ], + allow_empty = True, + ) + [ + "//codex-rs:node-version.txt", + ], +) diff --git a/codex-rs/instructions/Cargo.toml b/codex-rs/instructions/Cargo.toml new file mode 100644 index 000000000..cdaa3d736 --- /dev/null +++ b/codex-rs/instructions/Cargo.toml @@ -0,0 +1,20 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-instructions" +version.workspace = true + +[lib] +doctest = false +name = "codex_instructions" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +codex-protocol = { workspace = true } +serde = { workspace = true, features = ["derive"] } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/instructions/src/fragment.rs b/codex-rs/instructions/src/fragment.rs new file mode 100644 index 000000000..590dac379 --- /dev/null +++ b/codex-rs/instructions/src/fragment.rs @@ -0,0 +1,61 @@ +use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseItem; + +pub(crate) const AGENTS_MD_START_MARKER: &str = "# AGENTS.md instructions for "; +pub(crate) const AGENTS_MD_END_MARKER: &str = ""; +pub(crate) const SKILL_OPEN_TAG: &str = ""; +pub(crate) const SKILL_CLOSE_TAG: &str = ""; + +#[derive(Clone, Copy)] +pub struct ContextualUserFragmentDefinition { + start_marker: &'static str, + end_marker: &'static str, +} + +impl ContextualUserFragmentDefinition { + pub const fn new(start_marker: &'static str, end_marker: &'static str) -> Self { + Self { + start_marker, + end_marker, + } + } + + pub fn matches_text(&self, text: &str) -> bool { + let trimmed = text.trim_start(); + let starts_with_marker = trimmed + .get(..self.start_marker.len()) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.start_marker)); + let trimmed = trimmed.trim_end(); + let ends_with_marker = trimmed + .get(trimmed.len().saturating_sub(self.end_marker.len())..) + .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.end_marker)); + starts_with_marker && ends_with_marker + } + + pub const fn start_marker(&self) -> &'static str { + self.start_marker + } + + pub const fn end_marker(&self) -> &'static str { + self.end_marker + } + + pub fn wrap(&self, body: String) -> String { + format!("{}\n{}\n{}", self.start_marker, body, self.end_marker) + } + + pub fn into_message(self, text: String) -> ResponseItem { + ResponseItem::Message { + id: None, + role: "user".to_string(), + content: vec![ContentItem::InputText { text }], + end_turn: None, + phase: None, + } + } +} + +pub const AGENTS_MD_FRAGMENT: ContextualUserFragmentDefinition = + ContextualUserFragmentDefinition::new(AGENTS_MD_START_MARKER, AGENTS_MD_END_MARKER); +pub const SKILL_FRAGMENT: ContextualUserFragmentDefinition = + ContextualUserFragmentDefinition::new(SKILL_OPEN_TAG, SKILL_CLOSE_TAG); diff --git a/codex-rs/instructions/src/lib.rs b/codex-rs/instructions/src/lib.rs new file mode 100644 index 000000000..1b1f81550 --- /dev/null +++ b/codex-rs/instructions/src/lib.rs @@ -0,0 +1,11 @@ +//! User and skill instruction payloads and contextual user fragment markers for Codex prompts. + +mod fragment; +mod user_instructions; + +pub use fragment::AGENTS_MD_FRAGMENT; +pub use fragment::ContextualUserFragmentDefinition; +pub use fragment::SKILL_FRAGMENT; +pub use user_instructions::SkillInstructions; +pub use user_instructions::USER_INSTRUCTIONS_PREFIX; +pub use user_instructions::UserInstructions; diff --git a/codex-rs/core/src/instructions/user_instructions.rs b/codex-rs/instructions/src/user_instructions.rs similarity index 79% rename from codex-rs/core/src/instructions/user_instructions.rs rename to codex-rs/instructions/src/user_instructions.rs index a0389c9ff..4fd266e76 100644 --- a/codex-rs/core/src/instructions/user_instructions.rs +++ b/codex-rs/instructions/src/user_instructions.rs @@ -3,20 +3,21 @@ use serde::Serialize; use codex_protocol::models::ResponseItem; -use crate::contextual_user_message::AGENTS_MD_FRAGMENT; -use crate::contextual_user_message::SKILL_FRAGMENT; +use crate::fragment::AGENTS_MD_FRAGMENT; +use crate::fragment::AGENTS_MD_START_MARKER; +use crate::fragment::SKILL_FRAGMENT; -pub const USER_INSTRUCTIONS_PREFIX: &str = "# AGENTS.md instructions for "; +pub const USER_INSTRUCTIONS_PREFIX: &str = AGENTS_MD_START_MARKER; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename = "user_instructions", rename_all = "snake_case")] -pub(crate) struct UserInstructions { +pub struct UserInstructions { pub directory: String, pub text: String, } impl UserInstructions { - pub(crate) fn serialize_to_text(&self) -> String { + pub fn serialize_to_text(&self) -> String { format!( "{prefix}{directory}\n\n\n{contents}\n{suffix}", prefix = AGENTS_MD_FRAGMENT.start_marker(), @@ -35,14 +36,12 @@ impl From for ResponseItem { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename = "skill_instructions", rename_all = "snake_case")] -pub(crate) struct SkillInstructions { +pub struct SkillInstructions { pub name: String, pub path: String, pub contents: String, } -impl SkillInstructions {} - impl From for ResponseItem { fn from(si: SkillInstructions) -> Self { SKILL_FRAGMENT.into_message(SKILL_FRAGMENT.wrap(format!( diff --git a/codex-rs/core/src/instructions/user_instructions_tests.rs b/codex-rs/instructions/src/user_instructions_tests.rs similarity index 94% rename from codex-rs/core/src/instructions/user_instructions_tests.rs rename to codex-rs/instructions/src/user_instructions_tests.rs index 58442600a..75f35d11b 100644 --- a/codex-rs/core/src/instructions/user_instructions_tests.rs +++ b/codex-rs/instructions/src/user_instructions_tests.rs @@ -1,7 +1,11 @@ use super::*; use codex_protocol::models::ContentItem; +use codex_protocol::models::ResponseItem; use pretty_assertions::assert_eq; +use crate::fragment::AGENTS_MD_FRAGMENT; +use crate::fragment::SKILL_FRAGMENT; + #[test] fn test_user_instructions() { let user_instructions = UserInstructions {