Skip to content

Commit acfd94f

Browse files
authored
Add hierarchical agent prompt (openai#8996)
1 parent cabf85a commit acfd94f

6 files changed

Lines changed: 123 additions & 29 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Files called AGENTS.md commonly appear in many places inside a container - at "/", in "~", deep within git repositories, or in any other directory; their location is not limited to version-controlled folders.
2+
3+
Their purpose is to pass along human guidance to you, the agent. Such guidance can include coding standards, explanations of the project layout, steps for building or testing, and even wording that must accompany a GitHub pull-request description produced by the agent; all of it is to be followed.
4+
5+
Each AGENTS.md governs the entire directory that contains it and every child directory beneath that point. Whenever you change a file, you have to comply with every AGENTS.md whose scope covers that file. Naming conventions, stylistic rules and similar directives are restricted to the code that falls inside that scope unless the document explicitly states otherwise.
6+
7+
When two AGENTS.md files disagree, the one located deeper in the directory structure overrides the higher-level file, while instructions given directly in the prompt by the system, developer, or user outrank any AGENTS.md content.

codex-rs/core/src/features.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ pub enum Feature {
8686
RemoteModels,
8787
/// Experimental shell snapshotting.
8888
ShellSnapshot,
89+
/// Append additional AGENTS.md guidance to user instructions.
90+
HierarchicalAgents,
8991
/// Experimental TUI v2 (viewport) implementation.
9092
Tui2,
9193
/// Enforce UTF8 output in Powershell.
@@ -352,6 +354,12 @@ pub const FEATURES: &[FeatureSpec] = &[
352354
},
353355
default_enabled: false,
354356
},
357+
FeatureSpec {
358+
id: Feature::HierarchicalAgents,
359+
key: "hierarchical_agents",
360+
stage: Stage::Experimental,
361+
default_enabled: false,
362+
},
355363
FeatureSpec {
356364
id: Feature::ApplyPatchFreeform,
357365
key: "apply_patch_freeform",

codex-rs/core/src/project_doc.rs

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@
1414
//! 3. We do **not** walk past the Git root.
1515
1616
use crate::config::Config;
17+
use crate::features::Feature;
1718
use crate::skills::SkillMetadata;
1819
use crate::skills::render_skills_section;
1920
use dunce::canonicalize as normalize_path;
2021
use std::path::PathBuf;
2122
use tokio::io::AsyncReadExt;
2223
use tracing::error;
2324

25+
pub(crate) const HIERARCHICAL_AGENTS_MESSAGE: &str =
26+
include_str!("../hierarchical_agents_message.md");
27+
2428
/// Default filename scanned for project-level docs.
2529
pub const DEFAULT_PROJECT_DOC_FILENAME: &str = "AGENTS.md";
2630
/// Preferred local override for project-level docs.
@@ -36,35 +40,46 @@ pub(crate) async fn get_user_instructions(
3640
config: &Config,
3741
skills: Option<&[SkillMetadata]>,
3842
) -> Option<String> {
39-
let skills_section = skills.and_then(render_skills_section);
43+
let project_docs = read_project_docs(config).await;
44+
45+
let mut output = String::new();
46+
47+
if let Some(instructions) = config.user_instructions.clone() {
48+
output.push_str(&instructions);
49+
}
4050

41-
let project_docs = match read_project_docs(config).await {
42-
Ok(docs) => docs,
51+
match project_docs {
52+
Ok(Some(docs)) => {
53+
if !output.is_empty() {
54+
output.push_str(PROJECT_DOC_SEPARATOR);
55+
}
56+
output.push_str(&docs);
57+
}
58+
Ok(None) => {}
4359
Err(e) => {
4460
error!("error trying to find project doc: {e:#}");
45-
return config.user_instructions.clone();
4661
}
4762
};
4863

49-
let combined_project_docs = merge_project_docs_with_skills(project_docs, skills_section);
50-
51-
let mut parts: Vec<String> = Vec::new();
52-
53-
if let Some(instructions) = config.user_instructions.clone() {
54-
parts.push(instructions);
64+
let skills_section = skills.and_then(render_skills_section);
65+
if let Some(skills_section) = skills_section {
66+
if !output.is_empty() {
67+
output.push_str("\n\n");
68+
}
69+
output.push_str(&skills_section);
5570
}
5671

57-
if let Some(project_doc) = combined_project_docs {
58-
if !parts.is_empty() {
59-
parts.push(PROJECT_DOC_SEPARATOR.to_string());
72+
if config.features.enabled(Feature::HierarchicalAgents) {
73+
if !output.is_empty() {
74+
output.push_str("\n\n");
6075
}
61-
parts.push(project_doc);
76+
output.push_str(HIERARCHICAL_AGENTS_MESSAGE);
6277
}
6378

64-
if parts.is_empty() {
65-
None
79+
if !output.is_empty() {
80+
Some(output)
6681
} else {
67-
Some(parts.concat())
82+
None
6883
}
6984
}
7085

@@ -217,18 +232,6 @@ fn candidate_filenames<'a>(config: &'a Config) -> Vec<&'a str> {
217232
names
218233
}
219234

220-
fn merge_project_docs_with_skills(
221-
project_doc: Option<String>,
222-
skills_section: Option<String>,
223-
) -> Option<String> {
224-
match (project_doc, skills_section) {
225-
(Some(doc), Some(skills)) => Some(format!("{doc}\n\n{skills}")),
226-
(Some(doc), None) => Some(doc),
227-
(None, Some(skills)) => Some(skills),
228-
(None, None) => None,
229-
}
230-
}
231-
232235
#[cfg(test)]
233236
mod tests {
234237
use super::*;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use codex_core::features::Feature;
2+
use core_test_support::load_sse_fixture_with_id;
3+
use core_test_support::responses::mount_sse_once;
4+
use core_test_support::responses::start_mock_server;
5+
use core_test_support::test_codex::test_codex;
6+
7+
const HIERARCHICAL_AGENTS_SNIPPET: &str =
8+
"Files called AGENTS.md commonly appear in many places inside a container";
9+
10+
fn sse_completed(id: &str) -> String {
11+
load_sse_fixture_with_id("../fixtures/completed_template.json", id)
12+
}
13+
14+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
15+
async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
16+
let server = start_mock_server().await;
17+
let resp_mock = mount_sse_once(&server, sse_completed("resp1")).await;
18+
19+
let mut builder = test_codex().with_config(|config| {
20+
config.features.enable(Feature::HierarchicalAgents);
21+
std::fs::write(config.cwd.join("AGENTS.md"), "be nice").expect("write AGENTS.md");
22+
});
23+
let test = builder.build(&server).await.expect("build test codex");
24+
25+
test.submit_turn("hello").await.expect("submit turn");
26+
27+
let request = resp_mock.single_request();
28+
let user_messages = request.message_input_texts("user");
29+
let instructions = user_messages
30+
.iter()
31+
.find(|text| text.starts_with("# AGENTS.md instructions for "))
32+
.expect("instructions message");
33+
assert!(
34+
instructions.contains("be nice"),
35+
"expected AGENTS.md text included: {instructions}"
36+
);
37+
let snippet_pos = instructions
38+
.find(HIERARCHICAL_AGENTS_SNIPPET)
39+
.expect("expected hierarchical agents snippet");
40+
let base_pos = instructions
41+
.find("be nice")
42+
.expect("expected AGENTS.md text");
43+
assert!(
44+
snippet_pos > base_pos,
45+
"expected hierarchical agents message appended after base instructions: {instructions}"
46+
);
47+
}
48+
49+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
50+
async fn hierarchical_agents_emits_when_no_project_doc() {
51+
let server = start_mock_server().await;
52+
let resp_mock = mount_sse_once(&server, sse_completed("resp1")).await;
53+
54+
let mut builder = test_codex().with_config(|config| {
55+
config.features.enable(Feature::HierarchicalAgents);
56+
});
57+
let test = builder.build(&server).await.expect("build test codex");
58+
59+
test.submit_turn("hello").await.expect("submit turn");
60+
61+
let request = resp_mock.single_request();
62+
let user_messages = request.message_input_texts("user");
63+
let instructions = user_messages
64+
.iter()
65+
.find(|text| text.starts_with("# AGENTS.md instructions for "))
66+
.expect("instructions message");
67+
assert!(
68+
instructions.contains(HIERARCHICAL_AGENTS_SNIPPET),
69+
"expected hierarchical agents message appended: {instructions}"
70+
);
71+
}

codex-rs/core/tests/suite/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ mod exec;
3030
mod exec_policy;
3131
mod fork_thread;
3232
mod grep_files;
33+
mod hierarchical_agents;
3334
mod items;
3435
mod json_result;
3536
mod list_dir;

docs/agents_md.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
# AGENTS.md
22

33
For information about AGENTS.md, see [this documentation](https://developers.openai.com/codex/guides/agents-md).
4+
5+
## Hierarchical agents message
6+
7+
When the `hierarchical_agents` feature flag is enabled (via `[features]` in `config.toml`), Codex appends additional guidance about AGENTS.md scope and precedence to the user instructions message and emits that message even when no AGENTS.md is present.

0 commit comments

Comments
 (0)