diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 8e818c2f..7760c48f 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -38,9 +38,8 @@ pub(crate) async fn stream_chat_completions( // Build messages array let mut messages = Vec::::new(); - if let Some(instr) = &prompt.instructions { - messages.push(json!({"role": "system", "content": instr})); - } + let full_instructions = prompt.get_full_instructions(); + messages.push(json!({"role": "system", "content": full_instructions})); for item in &prompt.input { if let ResponseItem::Message { role, content } = item { diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index f8f30391..7316e904 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -166,9 +166,10 @@ impl ModelClient { debug!("tools_json: {}", serde_json::to_string_pretty(&tools_json)?); + let full_instructions = prompt.get_full_instructions(); let payload = Payload { model: &self.model, - instructions: prompt.instructions.as_ref(), + instructions: &full_instructions, input: &prompt.input, tools: &tools_json, tool_choice: "auto", diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index fcdac71d..8eb8074b 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -2,12 +2,17 @@ use crate::error::Result; use crate::models::ResponseItem; use futures::Stream; use serde::Serialize; +use std::borrow::Cow; use std::collections::HashMap; use std::pin::Pin; use std::task::Context; use std::task::Poll; use tokio::sync::mpsc; +/// The `instructions` field in the payload sent to a model should always start +/// with this content. +const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md"); + /// API request payload for a single model turn. #[derive(Default, Debug, Clone)] pub struct Prompt { @@ -15,7 +20,8 @@ pub struct Prompt { pub input: Vec, /// Optional previous response ID (when storage is enabled). pub prev_id: Option, - /// Optional initial instructions (only sent on first turn). + /// Optional instructions from the user to amend to the built-in agent + /// instructions. pub instructions: Option, /// Whether to store response on server side (disable_response_storage = !store). pub store: bool, @@ -26,6 +32,18 @@ pub struct Prompt { pub extra_tools: HashMap, } +impl Prompt { + pub(crate) fn get_full_instructions(&self) -> Cow { + match &self.instructions { + Some(instructions) => { + let instructions = format!("{BASE_INSTRUCTIONS}\n{instructions}"); + Cow::Owned(instructions) + } + None => Cow::Borrowed(BASE_INSTRUCTIONS), + } + } +} + #[derive(Debug)] pub enum ResponseEvent { OutputItemDone(ResponseItem), @@ -54,8 +72,7 @@ pub(crate) enum Summary { #[derive(Debug, Serialize)] pub(crate) struct Payload<'a> { pub(crate) model: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) instructions: Option<&'a String>, + pub(crate) instructions: &'a str, // TODO(mbolin): ResponseItem::Other should not be serialized. Currently, // we code defensively to avoid this case, but perhaps we should use a // separate enum for serialization. diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 6a71a45e..4c815ad0 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -10,11 +10,6 @@ use serde::Deserialize; use std::collections::HashMap; use std::path::PathBuf; -/// Embedded fallback instructions that mirror the TypeScript CLI’s default -/// system prompt. These are compiled into the binary so a clean install behaves -/// correctly even if the user has not created `~/.codex/instructions.md`. -const EMBEDDED_INSTRUCTIONS: &str = include_str!("../prompt.md"); - /// Maximum number of bytes of the documentation that will be embedded. Larger /// files are *silently truncated* to this size so we do not take up too much of /// the context window. @@ -42,7 +37,7 @@ pub struct Config { /// who have opted into Zero Data Retention (ZDR). pub disable_response_storage: bool, - /// System instructions. + /// User-provided instructions from instructions.md. pub instructions: Option, /// Optional external notifier command. When set, Codex will spawn this @@ -198,9 +193,7 @@ impl Config { cfg: ConfigToml, overrides: ConfigOverrides, ) -> std::io::Result { - // Instructions: user-provided instructions.md > embedded default. - let instructions = - Self::load_instructions().or_else(|| Some(EMBEDDED_INSTRUCTIONS.to_string())); + let instructions = Self::load_instructions(); // Destructure ConfigOverrides fully to ensure all overrides are applied. let ConfigOverrides { @@ -289,7 +282,14 @@ impl Config { fn load_instructions() -> Option { let mut p = codex_dir().ok()?; p.push("instructions.md"); - std::fs::read_to_string(&p).ok() + std::fs::read_to_string(&p).ok().and_then(|s| { + let s = s.trim(); + if s.is_empty() { + None + } else { + Some(s.to_string()) + } + }) } /// Meant to be used exclusively for tests: `load_with_overrides()` should