Compare commits

...

1 Commits

Author SHA1 Message Date
Kevin Liu
73472c8967 [codex] add consolidation module extension paths 2026-03-30 12:36:16 -07:00
5 changed files with 113 additions and 13 deletions

View File

@@ -13,6 +13,9 @@ Memory prompt templates live under `codex-rs/core/templates/memories/`.
- `read_path.md`
- In `codex`, edit those undated template files in place.
- The dated snapshot-copy workflow is used in the separate `openai/project/agent_memory/write` harness repo, not here.
- At runtime, Phase 2 also appends any `*.md` files from
`~/.codex/memories/consolidation/` to the end of the consolidation prompt, in
sorted path order. Missing directories are skipped.
## When it runs

View File

@@ -25,6 +25,7 @@ pub(crate) use control::clear_memory_root_contents;
pub(crate) use start::start_memories_startup_task;
mod artifacts {
pub(super) const CONSOLIDATION_SUBDIR: &str = "consolidation";
pub(super) const ROLLOUT_SUMMARIES_SUBDIR: &str = "rollout_summaries";
pub(super) const RAW_MEMORIES_FILENAME: &str = "raw_memories.md";
}
@@ -106,6 +107,10 @@ fn rollout_summaries_dir(root: &Path) -> PathBuf {
root.join(artifacts::ROLLOUT_SUMMARIES_SUBDIR)
}
fn consolidation_prompt_modules_dir(root: &Path) -> PathBuf {
root.join(artifacts::CONSOLIDATION_SUBDIR)
}
fn raw_memories_file(root: &Path) -> PathBuf {
root.join(artifacts::RAW_MEMORIES_FILENAME)
}

View File

@@ -127,7 +127,7 @@ pub(super) async fn run(session: &Arc<Session>, config: Arc<Config>) {
}
// 5. Spawn the agent
let prompt = agent::get_prompt(config, &selection);
let prompt = agent::get_prompt(config, &selection).await;
let source = SessionSource::SubAgent(SubAgentSource::MemoryConsolidation);
let thread_id = match session
.services
@@ -320,12 +320,12 @@ mod agent {
Some(agent_config)
}
pub(super) fn get_prompt(
pub(super) async fn get_prompt(
config: Arc<Config>,
selection: &codex_state::Phase2InputSelection,
) -> Vec<UserInput> {
let root = memory_root(&config.codex_home);
let prompt = build_consolidation_prompt(&root, selection);
let prompt = build_consolidation_prompt(&root, selection).await;
vec![UserInput::Text {
text: prompt,
text_elements: vec![],

View File

@@ -1,3 +1,4 @@
use crate::memories::consolidation_prompt_modules_dir;
use crate::memories::memory_root;
use crate::memories::phase_one;
use crate::memories::storage::rollout_summary_file_stem_from_parts;
@@ -40,23 +41,89 @@ fn parse_embedded_template(source: &'static str, template_name: &str) -> Templat
}
/// Builds the consolidation subagent prompt for a specific memory root.
pub(super) fn build_consolidation_prompt(
pub(super) async fn build_consolidation_prompt(
memory_root: &Path,
selection: &Phase2InputSelection,
) -> String {
// additional prompt modules can be added here
let consolidation_prompt_modules = read_consolidation_prompt_modules(memory_root).await;
let memory_root = memory_root.display().to_string();
let phase2_input_selection = render_phase2_input_selection(selection);
CONSOLIDATION_PROMPT_TEMPLATE
let mut prompt = CONSOLIDATION_PROMPT_TEMPLATE
.render([
("memory_root", memory_root.as_str()),
("phase2_input_selection", phase2_input_selection.as_str()),
])
.unwrap_or_else(|err| {
warn!("failed to render memories consolidation prompt template: {err}");
format!(
"## Memory Phase 2 (Consolidation)\nConsolidate Codex memories in: {memory_root}\n\n{phase2_input_selection}"
)
})
warn!("failed to render memories consolidation prompt template: {err}");
format!(
"## Memory Phase 2 (Consolidation)\nConsolidate Codex memories in: {memory_root}\n\n{phase2_input_selection}"
)
});
if !consolidation_prompt_modules.is_empty() {
prompt.push_str("\n\n");
prompt.push_str(&consolidation_prompt_modules);
}
prompt
}
async fn read_consolidation_prompt_modules(memory_root: &Path) -> String {
let modules_dir = consolidation_prompt_modules_dir(memory_root);
let mut dir = match fs::read_dir(&modules_dir).await {
Ok(dir) => dir,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return String::new(),
Err(err) => {
warn!(
"failed to read memories consolidation prompt modules dir {}: {err}",
modules_dir.display()
);
return String::new();
}
};
let mut module_paths = Vec::new();
loop {
match dir.next_entry().await {
Ok(Some(entry)) => {
let path = entry.path();
if path.extension().and_then(|extension| extension.to_str()) == Some("md") {
module_paths.push(path);
}
}
Ok(None) => break,
Err(err) => {
warn!(
"failed to list memories consolidation prompt modules dir {}: {err}",
modules_dir.display()
);
return String::new();
}
}
}
module_paths.sort();
let mut modules_concat = String::new();
for path in module_paths {
let module = match fs::read_to_string(&path).await {
Ok(module) => module,
Err(err) => {
warn!(
"failed to read memories consolidation prompt module {}: {err}",
path.display()
);
continue;
}
};
let module = module.trim();
if module.is_empty() {
continue;
}
modules_concat.push_str("\n\n");
modules_concat.push_str(module);
}
modules_concat
}
fn render_phase2_input_selection(selection: &Phase2InputSelection) -> String {

View File

@@ -53,16 +53,41 @@ fn build_stage_one_input_message_uses_default_limit_when_model_context_window_mi
assert!(message.contains(&expected_truncated));
}
#[test]
fn build_consolidation_prompt_renders_embedded_template() {
#[tokio::test]
async fn build_consolidation_prompt_renders_embedded_template_without_modules() {
let prompt =
build_consolidation_prompt(Path::new("/tmp/memories"), &Phase2InputSelection::default());
build_consolidation_prompt(Path::new("/tmp/memories"), &Phase2InputSelection::default())
.await;
assert!(prompt.contains("Folder structure (under /tmp/memories/):"));
assert!(prompt.contains("**Diff since last consolidation:**"));
assert!(prompt.contains("- selected inputs this run: 0"));
}
#[tokio::test]
async fn build_consolidation_prompt_appends_modules_in_sorted_order() {
let temp = tempdir().unwrap();
let memories_dir = temp.path();
let modules_dir = memories_dir.join("consolidation");
tokio_fs::create_dir_all(&modules_dir).await.unwrap();
tokio_fs::write(modules_dir.join("02-second.md"), "second module\n")
.await
.unwrap();
tokio_fs::write(modules_dir.join("01-first.md"), "first module\n")
.await
.unwrap();
tokio_fs::write(modules_dir.join("ignored.txt"), "ignored module\n")
.await
.unwrap();
let prompt = build_consolidation_prompt(memories_dir, &Phase2InputSelection::default()).await;
let first_index = prompt.find("first module").unwrap();
let second_index = prompt.find("second module").unwrap();
assert!(first_index < second_index);
assert!(!prompt.contains("ignored module"));
}
#[tokio::test]
async fn build_memory_tool_developer_instructions_renders_embedded_template() {
let temp = tempdir().unwrap();