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 {