mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
Split aggregated user instructions into contextual fragments
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -45,7 +45,7 @@ In the codex-rs folder where the rust code lives:
|
||||
- If a fragment represents durable turn/session state that should be rebuilt correctly across resume/fork/compaction/backtracking, implement `ModelVisibleContextFragment::build(...)`.
|
||||
- If a fragment is contextual-user, it must provide stable detection: prefer `contextual_user_markers()` when fixed markers are sufficient, and override `matches_contextual_user_text()` only for genuinely custom matching (for example AGENTS.md).
|
||||
- Use the developer envelope for developer guidance. Custom override text (for example config/app-server `developer_instructions`) should use `CustomDeveloperInstructions`; system-generated developer context should use typed fragments plus the neutral `developer_*_text` helpers rather than reusing the custom override type.
|
||||
- Use contextual-user fragments for contextual user-role state. Turn-state contextual-user fragments such as AGENTS instructions and environment context belong in the contextual-user envelope. Initial-only or runtime contextual-user fragments should still use typed fragments so history parsing treats them as contextual state rather than user intent; some are appended to the initial contextual-user envelope (for example plugin session instructions) and others are emitted as standalone messages (for example skills, shell-command records, and turn-aborted notices).
|
||||
- Use contextual-user fragments for contextual user-role state. Turn-state contextual-user fragments such as custom user instructions, AGENTS instructions, and environment context belong in the contextual-user envelope. Initial-only or runtime contextual-user fragments should still use typed fragments so history parsing treats them as contextual state rather than user intent; some are appended to the initial contextual-user envelope (for example plugin session instructions) and others are emitted as standalone messages (for example skills, shell-command records, and turn-aborted notices).
|
||||
- Use `<environment_context>` specifically for environment facts derived from `TurnContext` that may need turn-to-turn diffs (`cwd`, `shell`, optional `current_date`, optional `timezone`, optional network allow/deny domain summaries). Do not put policy text, plugin/skill listings, or other guidance into `<environment_context>`; those should use dedicated fragments.
|
||||
- Runtime/session-prefix fragments that are not turn-state diffs should usually leave `ModelVisibleContextFragment::build(...)` as `None`.
|
||||
- Register every current fragment exactly once in `REGISTERED_MODEL_VISIBLE_FRAGMENTS`, in the rough order it should appear in model-visible context.
|
||||
|
||||
@@ -53,6 +53,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
summary: turn_context.reasoning_summary,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
@@ -92,6 +93,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
summary: turn_context.reasoning_summary,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
@@ -754,6 +756,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
summary: turn_context.reasoning_summary,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
@@ -826,6 +829,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
summary: turn_context.reasoning_summary,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
@@ -855,6 +859,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
summary: turn_context.reasoning_summary,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
@@ -961,6 +966,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
summary: turn_context.reasoning_summary,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
@@ -1063,6 +1069,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
summary: turn_context.reasoning_summary,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
@@ -1207,6 +1214,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
summary: turn_context.reasoning_summary,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
|
||||
@@ -138,6 +138,16 @@ fn skips_unnamed_image_label_text() {
|
||||
#[test]
|
||||
fn skips_user_instructions_and_env() {
|
||||
let items = vec![
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "<user_instructions>\ncustom guidance\n</user_instructions>"
|
||||
.to_string(),
|
||||
}],
|
||||
end_turn: None,
|
||||
phase: None,
|
||||
},
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".to_string(),
|
||||
|
||||
@@ -53,6 +53,12 @@ use codex_protocol::protocol::TurnContextItem;
|
||||
|
||||
pub(crate) const SKILL_OPEN_TAG: &str = "<skill>";
|
||||
pub(crate) const SKILL_CLOSE_TAG: &str = "</skill>";
|
||||
pub(crate) const JS_REPL_INSTRUCTIONS_OPEN_TAG: &str = "<js_repl_instructions>";
|
||||
pub(crate) const JS_REPL_INSTRUCTIONS_CLOSE_TAG: &str = "</js_repl_instructions>";
|
||||
pub(crate) const SKILLS_SECTION_OPEN_TAG: &str = "<skills_section>";
|
||||
pub(crate) const SKILLS_SECTION_CLOSE_TAG: &str = "</skills_section>";
|
||||
pub(crate) const CHILD_AGENTS_INSTRUCTIONS_OPEN_TAG: &str = "<child_agents_instructions>";
|
||||
pub(crate) const CHILD_AGENTS_INSTRUCTIONS_CLOSE_TAG: &str = "</child_agents_instructions>";
|
||||
pub(crate) const USER_SHELL_COMMAND_OPEN_TAG: &str = "<user_shell_command>";
|
||||
pub(crate) const USER_SHELL_COMMAND_CLOSE_TAG: &str = "</user_shell_command>";
|
||||
pub(crate) const TURN_ABORTED_OPEN_TAG: &str = "<turn_aborted>";
|
||||
|
||||
@@ -30,17 +30,23 @@
|
||||
use crate::codex::TurnContext;
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::features::Feature;
|
||||
use crate::model_visible_context::CHILD_AGENTS_INSTRUCTIONS_CLOSE_TAG;
|
||||
use crate::model_visible_context::CHILD_AGENTS_INSTRUCTIONS_OPEN_TAG;
|
||||
use crate::model_visible_context::ContextualUserContextRole;
|
||||
use crate::model_visible_context::ContextualUserFragmentMarkers;
|
||||
use crate::model_visible_context::ContextualUserTextFragment;
|
||||
use crate::model_visible_context::DeveloperContextRole;
|
||||
use crate::model_visible_context::DeveloperTextFragment;
|
||||
use crate::model_visible_context::JS_REPL_INSTRUCTIONS_CLOSE_TAG;
|
||||
use crate::model_visible_context::JS_REPL_INSTRUCTIONS_OPEN_TAG;
|
||||
use crate::model_visible_context::ModelVisibleContextFragment;
|
||||
use crate::model_visible_context::ModelVisibleContextRole;
|
||||
use crate::model_visible_context::PLUGINS_CLOSE_TAG;
|
||||
use crate::model_visible_context::PLUGINS_OPEN_TAG;
|
||||
use crate::model_visible_context::SKILL_CLOSE_TAG;
|
||||
use crate::model_visible_context::SKILL_OPEN_TAG;
|
||||
use crate::model_visible_context::SKILLS_SECTION_CLOSE_TAG;
|
||||
use crate::model_visible_context::SKILLS_SECTION_OPEN_TAG;
|
||||
use crate::model_visible_context::SUBAGENT_NOTIFICATION_CLOSE_TAG;
|
||||
use crate::model_visible_context::SUBAGENT_NOTIFICATION_OPEN_TAG;
|
||||
use crate::model_visible_context::SUBAGENTS_CLOSE_TAG;
|
||||
@@ -50,7 +56,10 @@ use crate::model_visible_context::TURN_ABORTED_OPEN_TAG;
|
||||
use crate::model_visible_context::TurnContextDiffParams;
|
||||
use crate::model_visible_context::USER_SHELL_COMMAND_CLOSE_TAG;
|
||||
use crate::model_visible_context::USER_SHELL_COMMAND_OPEN_TAG;
|
||||
use crate::project_doc::HIERARCHICAL_AGENTS_MESSAGE;
|
||||
use crate::project_doc::render_js_repl_instructions;
|
||||
use crate::shell::Shell;
|
||||
use crate::skills::render_skills_section;
|
||||
use crate::tools::format_exec_output_str;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::MessageRole;
|
||||
@@ -66,6 +75,8 @@ use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG;
|
||||
use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnContextNetworkItem;
|
||||
use codex_protocol::protocol::USER_INSTRUCTIONS_CLOSE_TAG;
|
||||
use codex_protocol::protocol::USER_INSTRUCTIONS_OPEN_TAG;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::path::PathBuf;
|
||||
@@ -129,7 +140,11 @@ const REGISTERED_MODEL_VISIBLE_FRAGMENTS: &[ModelVisibleFragmentRegistration] =
|
||||
ModelVisibleFragmentRegistration::of::<PersonalityUpdateFragment>(),
|
||||
ModelVisibleFragmentRegistration::of::<SubagentRosterContext>(),
|
||||
ModelVisibleFragmentRegistration::of::<SubagentNotification>(),
|
||||
ModelVisibleFragmentRegistration::of::<UserInstructionsFragment>(),
|
||||
ModelVisibleFragmentRegistration::of::<AgentsMdInstructions>(),
|
||||
ModelVisibleFragmentRegistration::of::<JsReplInstructionsFragment>(),
|
||||
ModelVisibleFragmentRegistration::of::<SkillsSectionFragment>(),
|
||||
ModelVisibleFragmentRegistration::of::<ChildAgentsInstructionsFragment>(),
|
||||
ModelVisibleFragmentRegistration::of::<EnvironmentContext>(),
|
||||
ModelVisibleFragmentRegistration::of::<SkillInstructions>(),
|
||||
ModelVisibleFragmentRegistration::of::<PluginInstructions>(),
|
||||
@@ -463,6 +478,42 @@ pub(crate) fn format_subagent_context_line(agent_id: &str, agent_nickname: Optio
|
||||
// Contextual-user turn-state fragments
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub(crate) struct UserInstructionsFragment {
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl ModelVisibleContextFragment for UserInstructionsFragment {
|
||||
type Role = ContextualUserContextRole;
|
||||
|
||||
fn render_text(&self) -> String {
|
||||
Self::wrap_contextual_user_body(self.text.clone())
|
||||
}
|
||||
|
||||
fn build(
|
||||
turn_context: &TurnContext,
|
||||
reference_context_item: Option<&TurnContextItem>,
|
||||
_params: &TurnContextDiffParams<'_>,
|
||||
) -> Option<Self> {
|
||||
let current = Self {
|
||||
text: turn_context.user_instructions.clone()?,
|
||||
};
|
||||
if reference_context_item.and_then(|previous| previous.user_instructions.as_deref())
|
||||
== Some(current.text.as_str())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(current)
|
||||
}
|
||||
|
||||
fn contextual_user_markers() -> Option<ContextualUserFragmentMarkers> {
|
||||
Some(ContextualUserFragmentMarkers::new(
|
||||
USER_INSTRUCTIONS_OPEN_TAG,
|
||||
USER_INSTRUCTIONS_CLOSE_TAG,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
const AGENTS_MD_START_MARKER: &str = "# AGENTS.md instructions for ";
|
||||
const AGENTS_MD_END_MARKER: &str = "</INSTRUCTIONS>";
|
||||
|
||||
@@ -494,12 +545,12 @@ impl ModelVisibleContextFragment for AgentsMdInstructions {
|
||||
) -> Option<Self> {
|
||||
let current = Self {
|
||||
directory: turn_context.cwd.to_string_lossy().into_owned(),
|
||||
text: turn_context.user_instructions.as_ref()?.clone(),
|
||||
text: turn_context.project_doc_instructions.as_ref()?.clone(),
|
||||
};
|
||||
if let Some(previous) = reference_context_item {
|
||||
let previous_directory = previous.cwd.to_string_lossy().into_owned();
|
||||
if previous_directory == current.directory
|
||||
&& previous.user_instructions.as_deref() == Some(current.text.as_str())
|
||||
&& previous.project_doc_instructions.as_deref() == Some(current.text.as_str())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -517,6 +568,107 @@ impl ModelVisibleContextFragment for AgentsMdInstructions {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct JsReplInstructionsFragment {
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl ModelVisibleContextFragment for JsReplInstructionsFragment {
|
||||
type Role = ContextualUserContextRole;
|
||||
|
||||
fn render_text(&self) -> String {
|
||||
Self::wrap_contextual_user_body(self.text.clone())
|
||||
}
|
||||
|
||||
fn build(
|
||||
turn_context: &TurnContext,
|
||||
reference_context_item: Option<&TurnContextItem>,
|
||||
_params: &TurnContextDiffParams<'_>,
|
||||
) -> Option<Self> {
|
||||
if reference_context_item.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self {
|
||||
text: render_js_repl_instructions(&turn_context.config)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn contextual_user_markers() -> Option<ContextualUserFragmentMarkers> {
|
||||
Some(ContextualUserFragmentMarkers::new(
|
||||
JS_REPL_INSTRUCTIONS_OPEN_TAG,
|
||||
JS_REPL_INSTRUCTIONS_CLOSE_TAG,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SkillsSectionFragment {
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl ModelVisibleContextFragment for SkillsSectionFragment {
|
||||
type Role = ContextualUserContextRole;
|
||||
|
||||
fn render_text(&self) -> String {
|
||||
Self::wrap_contextual_user_body(self.text.clone())
|
||||
}
|
||||
|
||||
fn build(
|
||||
turn_context: &TurnContext,
|
||||
reference_context_item: Option<&TurnContextItem>,
|
||||
_params: &TurnContextDiffParams<'_>,
|
||||
) -> Option<Self> {
|
||||
if reference_context_item.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let skills = turn_context
|
||||
.turn_skills
|
||||
.outcome
|
||||
.allowed_skills_for_implicit_invocation();
|
||||
Some(Self {
|
||||
text: render_skills_section(&skills)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn contextual_user_markers() -> Option<ContextualUserFragmentMarkers> {
|
||||
Some(ContextualUserFragmentMarkers::new(
|
||||
SKILLS_SECTION_OPEN_TAG,
|
||||
SKILLS_SECTION_CLOSE_TAG,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ChildAgentsInstructionsFragment;
|
||||
|
||||
impl ModelVisibleContextFragment for ChildAgentsInstructionsFragment {
|
||||
type Role = ContextualUserContextRole;
|
||||
|
||||
fn render_text(&self) -> String {
|
||||
Self::wrap_contextual_user_body(HIERARCHICAL_AGENTS_MESSAGE.to_string())
|
||||
}
|
||||
|
||||
fn build(
|
||||
turn_context: &TurnContext,
|
||||
reference_context_item: Option<&TurnContextItem>,
|
||||
_params: &TurnContextDiffParams<'_>,
|
||||
) -> Option<Self> {
|
||||
if reference_context_item.is_some()
|
||||
|| !turn_context.features.enabled(Feature::ChildAgentsMd)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self)
|
||||
}
|
||||
|
||||
fn contextual_user_markers() -> Option<ContextualUserFragmentMarkers> {
|
||||
Some(ContextualUserFragmentMarkers::new(
|
||||
CHILD_AGENTS_INSTRUCTIONS_OPEN_TAG,
|
||||
CHILD_AGENTS_INSTRUCTIONS_CLOSE_TAG,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename = "environment_context", rename_all = "snake_case")]
|
||||
pub(crate) struct EnvironmentContext {
|
||||
@@ -899,6 +1051,13 @@ mod tests {
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_user_instructions_fragment() {
|
||||
assert!(is_contextual_user_fragment(&ContentItem::InputText {
|
||||
text: "<user_instructions>\ncustom guidance\n</user_instructions>".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_legacy_subagent_notification_fragment() {
|
||||
assert!(is_contextual_user_fragment(&ContentItem::InputText {
|
||||
|
||||
@@ -21,8 +21,6 @@ use crate::config_loader::default_project_root_markers;
|
||||
use crate::config_loader::merge_toml_values;
|
||||
use crate::config_loader::project_root_markers_from_config;
|
||||
use crate::features::Feature;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::skills::render_skills_section;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use dunce::canonicalize as normalize_path;
|
||||
use std::path::PathBuf;
|
||||
@@ -38,11 +36,7 @@ pub const DEFAULT_PROJECT_DOC_FILENAME: &str = "AGENTS.md";
|
||||
/// Preferred local override for project-level docs.
|
||||
pub const LOCAL_PROJECT_DOC_FILENAME: &str = "AGENTS.override.md";
|
||||
|
||||
/// When both `Config::instructions` and the project doc are present, they will
|
||||
/// be concatenated with the following separator.
|
||||
const PROJECT_DOC_SEPARATOR: &str = "\n\n--- project-doc ---\n\n";
|
||||
|
||||
fn render_js_repl_instructions(config: &Config) -> Option<String> {
|
||||
pub(crate) fn render_js_repl_instructions(config: &Config) -> Option<String> {
|
||||
if !config.features.enabled(Feature::JsRepl) {
|
||||
return None;
|
||||
}
|
||||
@@ -76,59 +70,17 @@ fn render_js_repl_instructions(config: &Config) -> Option<String> {
|
||||
Some(section)
|
||||
}
|
||||
|
||||
/// Combines `Config::instructions` and `AGENTS.md` (if present) into a single
|
||||
/// string of instructions.
|
||||
pub(crate) async fn get_user_instructions(
|
||||
config: &Config,
|
||||
skills: Option<&[SkillMetadata]>,
|
||||
) -> Option<String> {
|
||||
/// Builds the project-doc / AGENTS text that later renders as the AGENTS
|
||||
/// contextual-user fragment.
|
||||
pub(crate) async fn build_project_doc_instructions_text(config: &Config) -> Option<String> {
|
||||
let project_docs = read_project_docs(config).await;
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
if let Some(instructions) = config.user_instructions.clone() {
|
||||
output.push_str(&instructions);
|
||||
}
|
||||
|
||||
match project_docs {
|
||||
Ok(Some(docs)) => {
|
||||
if !output.is_empty() {
|
||||
output.push_str(PROJECT_DOC_SEPARATOR);
|
||||
}
|
||||
output.push_str(&docs);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Ok(Some(docs)) => Some(docs),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
error!("error trying to find project doc: {e:#}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(js_repl_section) = render_js_repl_instructions(config) {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
output.push_str(&js_repl_section);
|
||||
}
|
||||
|
||||
let skills_section = skills.and_then(render_skills_section);
|
||||
if let Some(skills_section) = skills_section {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
output.push_str(&skills_section);
|
||||
}
|
||||
|
||||
if config.features.enabled(Feature::ChildAgentsMd) {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
output.push_str(HIERARCHICAL_AGENTS_MESSAGE);
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(output)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ fn load_test_skills(config: &Config) -> crate::skills::SkillLoadOutcome {
|
||||
async fn no_doc_file_returns_none() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, 4096, None).await, None).await;
|
||||
let res = build_project_doc_instructions_text(&make_config(&tmp, 4096, None).await).await;
|
||||
assert!(
|
||||
res.is_none(),
|
||||
"Expected None when AGENTS.md is absent and no system instructions provided"
|
||||
@@ -97,7 +97,7 @@ async fn doc_smaller_than_limit_is_returned() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "hello world").unwrap();
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, 4096, None).await, None)
|
||||
let res = build_project_doc_instructions_text(&make_config(&tmp, 4096, None).await)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
|
||||
@@ -116,7 +116,7 @@ async fn doc_larger_than_limit_is_truncated() {
|
||||
let huge = "A".repeat(LIMIT * 2); // 2 KiB
|
||||
fs::write(tmp.path().join("AGENTS.md"), &huge).unwrap();
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, LIMIT, None).await, None)
|
||||
let res = build_project_doc_instructions_text(&make_config(&tmp, LIMIT, None).await)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
|
||||
@@ -148,7 +148,7 @@ async fn finds_doc_in_repo_root() {
|
||||
let mut cfg = make_config(&repo, 4096, None).await;
|
||||
cfg.cwd = nested;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = build_project_doc_instructions_text(&cfg)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
assert_eq!(res, "root level doc");
|
||||
@@ -160,7 +160,7 @@ async fn zero_byte_limit_disables_docs() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "something").unwrap();
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, 0, None).await, None).await;
|
||||
let res = build_project_doc_instructions_text(&make_config(&tmp, 0, None).await).await;
|
||||
assert!(
|
||||
res.is_none(),
|
||||
"With limit 0 the function should return None"
|
||||
@@ -175,9 +175,7 @@ async fn js_repl_instructions_are_appended_when_enabled() {
|
||||
.enable(Feature::JsRepl)
|
||||
.expect("test config should allow js_repl");
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let res = render_js_repl_instructions(&cfg).expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- `codex.tool(...)` and `codex.emitImage(...)` keep stable helper identities across cells. Saved references and persisted objects can reuse them in later cells, but async callbacks that fire after a cell finishes still fail because no exec is active.\n- Request full-resolution image processing with `detail: \"original\"` only when the `view_image` tool schema includes a `detail` argument. The same availability applies to `codex.emitImage(...)`: if `view_image.detail` is present, you may also pass `detail: \"original\"` there. Use this when high-fidelity image perception or precise localization is needed, especially for CUA agents.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\", detail: \"original\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\", detail: \"original\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
@@ -194,9 +192,7 @@ async fn js_repl_tools_only_instructions_are_feature_gated() {
|
||||
.set(features)
|
||||
.expect("test config should allow js_repl tool restrictions");
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let res = render_js_repl_instructions(&cfg).expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- `codex.tool(...)` and `codex.emitImage(...)` keep stable helper identities across cells. Saved references and persisted objects can reuse them in later cells, but async callbacks that fire after a cell finishes still fail because no exec is active.\n- Request full-resolution image processing with `detail: \"original\"` only when the `view_image` tool schema includes a `detail` argument. The same availability applies to `codex.emitImage(...)`: if `view_image.detail` is present, you may also pass `detail: \"original\"` there. Use this when high-fidelity image perception or precise localization is needed, especially for CUA agents.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\", detail: \"original\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\", detail: \"original\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Do not call tools directly; use `js_repl` + `codex.tool(...)` for all tool calls, including shell commands.\n- MCP tools (if any) can also be called by name via `codex.tool(...)`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
@@ -213,42 +209,41 @@ async fn js_repl_image_detail_original_does_not_change_instructions() {
|
||||
.set(features)
|
||||
.expect("test config should allow js_repl image detail settings");
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
.await
|
||||
.expect("js_repl instructions expected");
|
||||
let res = render_js_repl_instructions(&cfg).expect("js_repl instructions expected");
|
||||
let expected = "## JavaScript REPL (Node)\n- Use `js_repl` for Node-backed JavaScript with top-level await in a persistent kernel.\n- `js_repl` is a freeform/custom tool. Direct `js_repl` calls must send raw JavaScript tool input (optionally with first-line `// codex-js-repl: timeout_ms=15000`). Do not wrap code in JSON (for example `{\"code\":\"...\"}`), quotes, or markdown code fences.\n- Helpers: `codex.cwd`, `codex.homeDir`, `codex.tmpDir`, `codex.tool(name, args?)`, and `codex.emitImage(imageLike)`.\n- `codex.tool` executes a normal tool call and resolves to the raw tool output object. Use it for shell and non-shell tools alike. Nested tool outputs stay inside JavaScript unless you emit them explicitly.\n- `codex.emitImage(...)` adds one image to the outer `js_repl` function output each time you call it, so you can call it multiple times to emit multiple images. It accepts a data URL, a single `input_image` item, an object like `{ bytes, mimeType }`, or a raw tool response object with exactly one image and no text. It rejects mixed text-and-image content.\n- `codex.tool(...)` and `codex.emitImage(...)` keep stable helper identities across cells. Saved references and persisted objects can reuse them in later cells, but async callbacks that fire after a cell finishes still fail because no exec is active.\n- Request full-resolution image processing with `detail: \"original\"` only when the `view_image` tool schema includes a `detail` argument. The same availability applies to `codex.emitImage(...)`: if `view_image.detail` is present, you may also pass `detail: \"original\"` there. Use this when high-fidelity image perception or precise localization is needed, especially for CUA agents.\n- Example of sharing an in-memory Playwright screenshot: `await codex.emitImage({ bytes: await page.screenshot({ type: \"jpeg\", quality: 85 }), mimeType: \"image/jpeg\", detail: \"original\" })`.\n- Example of sharing a local image tool result: `await codex.emitImage(codex.tool(\"view_image\", { path: \"/absolute/path\", detail: \"original\" }))`.\n- When encoding an image to send with `codex.emitImage(...)` or `view_image`, prefer JPEG at about 85 quality when lossy compression is acceptable; use PNG when transparency or lossless detail matters. Smaller uploads are faster and less likely to hit size limits.\n- Top-level bindings persist across cells. If a cell throws, prior bindings remain available and bindings that finished initializing before the throw often remain usable in later cells. For code you plan to reuse across cells, prefer declaring or assigning it in direct top-level statements before operations that might throw. If you hit `SyntaxError: Identifier 'x' has already been declared`, first reuse the existing binding, reassign a previously declared `let`, or pick a new descriptive name. Use `{ ... }` only for a short temporary block when you specifically need local scratch names; do not wrap an entire cell in block scope if you want those names reusable later. Reset the kernel with `js_repl_reset` only when you need a clean state.\n- Top-level static import declarations (for example `import x from \"./file.js\"`) are currently unsupported in `js_repl`; use dynamic imports with `await import(\"pkg\")`, `await import(\"./file.js\")`, or `await import(\"/abs/path/file.mjs\")` instead. Imported local files must be ESM `.js`/`.mjs` files and run in the same REPL VM context. Bare package imports always resolve from REPL-global search roots (`CODEX_JS_REPL_NODE_MODULE_DIRS`, then cwd), not relative to the imported file location. Local files may statically import only other local relative/absolute/`file://` `.js`/`.mjs` files; package and builtin imports from local files must stay dynamic. `import.meta.resolve()` returns importable strings such as `file://...`, bare package names, and `node:...` specifiers. Local file modules reload between execs, while top-level bindings persist until `js_repl_reset`.\n- Avoid direct access to `process.stdout` / `process.stderr` / `process.stdin`; it can corrupt the JSON line protocol. Use `console.log`, `codex.tool(...)`, and `codex.emitImage(...)`.";
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
/// When both system instructions *and* a project doc are present the two
|
||||
/// should be concatenated with the separator.
|
||||
/// Project-doc assembly ignores config user_instructions, which are now a
|
||||
/// separate contextual-user fragment.
|
||||
#[tokio::test]
|
||||
async fn merges_existing_instructions_with_project_doc() {
|
||||
async fn project_doc_assembly_ignores_config_user_instructions() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "proj doc").unwrap();
|
||||
|
||||
const INSTRUCTIONS: &str = "base instructions";
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await, None)
|
||||
.await
|
||||
.expect("should produce a combined instruction string");
|
||||
let res =
|
||||
build_project_doc_instructions_text(&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await)
|
||||
.await
|
||||
.expect("should produce project-doc instructions");
|
||||
|
||||
let expected = format!("{INSTRUCTIONS}{PROJECT_DOC_SEPARATOR}{}", "proj doc");
|
||||
|
||||
assert_eq!(res, expected);
|
||||
assert_eq!(res, "proj doc");
|
||||
}
|
||||
|
||||
/// If there are existing system instructions but the project doc is
|
||||
/// missing we expect the original instructions to be returned unchanged.
|
||||
/// With no project doc present, config user_instructions do not produce
|
||||
/// project-doc text on their own.
|
||||
#[tokio::test]
|
||||
async fn keeps_existing_instructions_when_doc_missing() {
|
||||
async fn config_user_instructions_do_not_create_project_doc_text() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
const INSTRUCTIONS: &str = "some instructions";
|
||||
|
||||
let res = get_user_instructions(&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await, None).await;
|
||||
let res =
|
||||
build_project_doc_instructions_text(&make_config(&tmp, 4096, Some(INSTRUCTIONS)).await)
|
||||
.await;
|
||||
|
||||
assert_eq!(res, Some(INSTRUCTIONS.to_string()));
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
/// When both the repository root and the working directory contain
|
||||
@@ -275,7 +270,7 @@ async fn concatenates_root_and_cwd_docs() {
|
||||
let mut cfg = make_config(&repo, 4096, None).await;
|
||||
cfg.cwd = nested;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = build_project_doc_instructions_text(&cfg)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
assert_eq!(res, "root doc\n\ncrate doc");
|
||||
@@ -303,7 +298,7 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
|
||||
assert_eq!(discovery[0], expected_parent);
|
||||
assert_eq!(discovery[1], expected_child);
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = build_project_doc_instructions_text(&cfg)
|
||||
.await
|
||||
.expect("doc expected");
|
||||
assert_eq!(res, "parent doc\n\nchild doc");
|
||||
@@ -318,7 +313,7 @@ async fn agents_local_md_preferred() {
|
||||
|
||||
let cfg = make_config(&tmp, 4096, None).await;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = build_project_doc_instructions_text(&cfg)
|
||||
.await
|
||||
.expect("local doc expected");
|
||||
|
||||
@@ -340,7 +335,7 @@ async fn uses_configured_fallback_when_agents_missing() {
|
||||
|
||||
let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md"]).await;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = build_project_doc_instructions_text(&cfg)
|
||||
.await
|
||||
.expect("fallback doc expected");
|
||||
|
||||
@@ -356,7 +351,7 @@ async fn agents_md_preferred_over_fallbacks() {
|
||||
|
||||
let cfg = make_config_with_fallback(&tmp, 4096, None, &["EXAMPLE.md", ".example.md"]).await;
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = build_project_doc_instructions_text(&cfg)
|
||||
.await
|
||||
.expect("AGENTS.md should win");
|
||||
|
||||
@@ -374,10 +369,8 @@ async fn agents_md_preferred_over_fallbacks() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_are_appended_to_project_doc() {
|
||||
async fn render_skills_section_includes_available_skills_and_usage_rules() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(tmp.path().join("AGENTS.md"), "base doc").unwrap();
|
||||
|
||||
let cfg = make_config(&tmp, 4096, None).await;
|
||||
create_skill(
|
||||
cfg.codex_home.clone(),
|
||||
@@ -386,12 +379,9 @@ async fn skills_are_appended_to_project_doc() {
|
||||
);
|
||||
|
||||
let skills = load_test_skills(&cfg);
|
||||
let res = get_user_instructions(
|
||||
&cfg,
|
||||
skills.errors.is_empty().then_some(skills.skills.as_slice()),
|
||||
)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
let skill_list = skills.errors.is_empty().then_some(skills.skills.as_slice());
|
||||
let res = crate::skills::render_skills_section(skill_list.expect("skills expected"))
|
||||
.expect("instructions expected");
|
||||
let expected_path = dunce::canonicalize(
|
||||
cfg.codex_home
|
||||
.join("skills/pdf-processing/SKILL.md")
|
||||
@@ -401,24 +391,21 @@ async fn skills_are_appended_to_project_doc() {
|
||||
let expected_path_str = expected_path.to_string_lossy().replace('\\', "/");
|
||||
let usage_rules = "- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.\n- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.\n- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.\n- How to use a skill (progressive disclosure):\n 1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.\n 2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.\n 3) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.\n 4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.\n 5) If `assets/` or templates exist, reuse them instead of recreating from scratch.\n- Coordination and sequencing:\n - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.\n - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.\n- Context hygiene:\n - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.\n - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.\n - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.\n- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.";
|
||||
let expected = format!(
|
||||
"base doc\n\n## Skills\nA skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.\n### Available skills\n- pdf-processing: extract from pdfs (file: {expected_path_str})\n### How to use skills\n{usage_rules}"
|
||||
"## Skills\nA skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.\n### Available skills\n- pdf-processing: extract from pdfs (file: {expected_path_str})\n### How to use skills\n{usage_rules}"
|
||||
);
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn skills_render_without_project_doc() {
|
||||
async fn render_skills_section_without_project_doc() {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let cfg = make_config(&tmp, 4096, None).await;
|
||||
create_skill(cfg.codex_home.clone(), "linting", "run clippy");
|
||||
|
||||
let skills = load_test_skills(&cfg);
|
||||
let res = get_user_instructions(
|
||||
&cfg,
|
||||
skills.errors.is_empty().then_some(skills.skills.as_slice()),
|
||||
)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
let skill_list = skills.errors.is_empty().then_some(skills.skills.as_slice());
|
||||
let res = crate::skills::render_skills_section(skill_list.expect("skills expected"))
|
||||
.expect("instructions expected");
|
||||
let expected_path =
|
||||
dunce::canonicalize(cfg.codex_home.join("skills/linting/SKILL.md").as_path())
|
||||
.unwrap_or_else(|_| cfg.codex_home.join("skills/linting/SKILL.md"));
|
||||
@@ -438,7 +425,7 @@ async fn apps_feature_does_not_emit_user_instructions_by_itself() {
|
||||
.enable(Feature::Apps)
|
||||
.expect("test config should allow apps");
|
||||
|
||||
let res = get_user_instructions(&cfg, None).await;
|
||||
let res = build_project_doc_instructions_text(&cfg).await;
|
||||
assert_eq!(res, None);
|
||||
}
|
||||
|
||||
@@ -452,7 +439,7 @@ async fn apps_feature_does_not_append_to_project_doc_user_instructions() {
|
||||
.enable(Feature::Apps)
|
||||
.expect("test config should allow apps");
|
||||
|
||||
let res = get_user_instructions(&cfg, None)
|
||||
let res = build_project_doc_instructions_text(&cfg)
|
||||
.await
|
||||
.expect("instructions expected");
|
||||
assert_eq!(res, "base doc");
|
||||
|
||||
@@ -488,6 +488,7 @@ async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Re
|
||||
realtime_active: None,
|
||||
effort: None,
|
||||
summary: ReasoningSummaryConfig::Auto,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
|
||||
@@ -31,23 +31,29 @@ async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
let user_messages = request.message_input_texts("user");
|
||||
let instructions = user_messages
|
||||
let agents_instructions = user_messages
|
||||
.iter()
|
||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
||||
.expect("instructions message");
|
||||
.expect("AGENTS instructions message");
|
||||
assert!(
|
||||
instructions.contains("be nice"),
|
||||
"expected AGENTS.md text included: {instructions}"
|
||||
agents_instructions.contains("be nice"),
|
||||
"expected AGENTS.md text included: {agents_instructions}"
|
||||
);
|
||||
let snippet_pos = instructions
|
||||
.find(HIERARCHICAL_AGENTS_SNIPPET)
|
||||
.expect("expected hierarchical agents snippet");
|
||||
let base_pos = instructions
|
||||
.find("be nice")
|
||||
.expect("expected AGENTS.md text");
|
||||
let child_agents_instructions = user_messages
|
||||
.iter()
|
||||
.find(|text| text.contains(HIERARCHICAL_AGENTS_SNIPPET))
|
||||
.expect("child agents instructions message");
|
||||
let agents_pos = user_messages
|
||||
.iter()
|
||||
.position(|text| std::ptr::eq(text, agents_instructions))
|
||||
.expect("AGENTS instructions position");
|
||||
let child_agents_pos = user_messages
|
||||
.iter()
|
||||
.position(|text| std::ptr::eq(text, child_agents_instructions))
|
||||
.expect("child agents instructions position");
|
||||
assert!(
|
||||
snippet_pos > base_pos,
|
||||
"expected hierarchical agents message appended after base instructions: {instructions}"
|
||||
child_agents_pos > agents_pos,
|
||||
"expected child-agents instructions after AGENTS fragment: {user_messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,12 +78,10 @@ async fn hierarchical_agents_emits_when_no_project_doc() {
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
let user_messages = request.message_input_texts("user");
|
||||
let instructions = user_messages
|
||||
.iter()
|
||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
||||
.expect("instructions message");
|
||||
assert!(
|
||||
instructions.contains(HIERARCHICAL_AGENTS_SNIPPET),
|
||||
"expected hierarchical agents message appended: {instructions}"
|
||||
user_messages
|
||||
.iter()
|
||||
.any(|text| text.contains(HIERARCHICAL_AGENTS_SNIPPET)),
|
||||
"expected hierarchical agents instructions fragment: {user_messages:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ fn resume_history(
|
||||
summary: config
|
||||
.model_reasoning_summary
|
||||
.unwrap_or(ReasoningSummary::Auto),
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
|
||||
@@ -2410,6 +2410,8 @@ pub struct TurnContextItem {
|
||||
pub effort: Option<ReasoningEffortConfig>,
|
||||
pub summary: ReasoningSummaryConfig,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub project_doc_instructions: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_instructions: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub developer_instructions: Option<String>,
|
||||
@@ -4198,6 +4200,7 @@ mod tests {
|
||||
realtime_active: None,
|
||||
effort: None,
|
||||
summary: ReasoningSummaryConfig::Auto,
|
||||
project_doc_instructions: None,
|
||||
user_instructions: None,
|
||||
developer_instructions: None,
|
||||
final_output_json_schema: None,
|
||||
|
||||
@@ -228,9 +228,19 @@ These are typed and registered, but not built from `TurnContext` diffs:
|
||||
These implement `build(...)` and participate in both full initial context and
|
||||
steady-state diffs:
|
||||
|
||||
- `UserInstructionsFragment`
|
||||
- `AgentsMdInstructions`
|
||||
- `JsReplInstructionsFragment`
|
||||
- `SkillsSectionFragment`
|
||||
- `ChildAgentsInstructionsFragment`
|
||||
- `EnvironmentContext`
|
||||
|
||||
Some of these are true steady-state diff fragments (`UserInstructionsFragment`,
|
||||
`AgentsMdInstructions`, `EnvironmentContext`). Others intentionally rebuild only
|
||||
when there is no baseline and therefore behave as initial-context fragments
|
||||
expressed through the same `build(...)` hook (`JsReplInstructionsFragment`,
|
||||
`SkillsSectionFragment`, `ChildAgentsInstructionsFragment`).
|
||||
|
||||
### Registered runtime contextual-user fragments
|
||||
|
||||
These are typed and registered for rendering/detection, but not built from
|
||||
@@ -279,7 +289,11 @@ Examples:
|
||||
- permissions policy
|
||||
- collaboration mode
|
||||
- realtime start/end state
|
||||
- custom user instructions
|
||||
- AGENTS.md instructions
|
||||
- JS REPL guidance
|
||||
- skills catalog guidance
|
||||
- child-AGENTS guidance
|
||||
- environment context
|
||||
|
||||
### Use a registered runtime fragment when:
|
||||
|
||||
Reference in New Issue
Block a user