Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions codex-rs/core/src/memories/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 EXTENSIONS_SUBDIR: &str = "memories_extensions";
pub(super) const ROLLOUT_SUMMARIES_SUBDIR: &str = "rollout_summaries";
pub(super) const RAW_MEMORIES_FILENAME: &str = "raw_memories.md";
}
Expand Down Expand Up @@ -106,6 +107,10 @@ fn rollout_summaries_dir(root: &Path) -> PathBuf {
root.join(artifacts::ROLLOUT_SUMMARIES_SUBDIR)
}

fn memory_extensions_root(root: &Path) -> PathBuf {
root.with_file_name(artifacts::EXTENSIONS_SUBDIR)
}

fn raw_memories_file(root: &Path) -> PathBuf {
root.join(artifacts::RAW_MEMORIES_FILENAME)
}
Expand Down
81 changes: 76 additions & 5 deletions codex-rs/core/src/memories/prompts.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::memories::memory_extensions_root;
use crate::memories::memory_root;
use crate::memories::phase_one;
use crate::memories::storage::rollout_summary_file_stem_from_parts;
Expand Down Expand Up @@ -31,6 +32,18 @@ static MEMORY_TOOL_DEVELOPER_INSTRUCTIONS_TEMPLATE: LazyLock<Template> = LazyLoc
"memories/read_path.md",
)
});
static MEMORY_EXTENSIONS_FOLDER_STRUCTURE_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
parse_embedded_template(
MEMORY_EXTENSIONS_FOLDER_STRUCTURE,
"memories/extensions_folder_structure.md",
)
});
static MEMORY_EXTENSIONS_PRIMARY_INPUTS_TEMPLATE: LazyLock<Template> = LazyLock::new(|| {
parse_embedded_template(
MEMORY_EXTENSIONS_PRIMARY_INPUTS,
"memories/extensions_primary_inputs.md",
)
});

fn parse_embedded_template(source: &'static str, template_name: &str) -> Template {
match Template::parse(source) {
Expand All @@ -39,24 +52,82 @@ fn parse_embedded_template(source: &'static str, template_name: &str) -> Templat
}
}

const MEMORY_EXTENSIONS_FOLDER_STRUCTURE: &str = r#"
Memory extensions (under {{ memory_extensions_root }}/):

- <extension_name>/instructions.md
- Source-specific guidance for interpreting additional memory signals. If an
extension folder exists, you must read its instructions.md to determine how to use this memory
source.

If the user has any memory extensions, you MUST read the instructions for each extension to
determine how to use the memory source. If it has no extension folders, continue with the standard
memory inputs only.
"#;

const MEMORY_EXTENSIONS_PRIMARY_INPUTS: &str = r#"
Optional source-specific inputs:
Under `{{ memory_extensions_root }}/`:

- `<extension_name>/instructions.md`
- If extension folders exist, read each instructions.md first and follow it when interpreting
that extension's memory source.
"#;

/// Builds the consolidation subagent prompt for a specific memory root.
pub(super) fn build_consolidation_prompt(
memory_root: &Path,
selection: &Phase2InputSelection,
) -> String {
let memory_extensions_root = memory_extensions_root(memory_root);
let memory_extensions_exist = memory_extensions_root.is_dir();
let memory_root = memory_root.display().to_string();
let memory_extensions_root = memory_extensions_root.display().to_string();
let memory_extensions_folder_structure = if memory_extensions_exist {
render_memory_extensions_block(
&MEMORY_EXTENSIONS_FOLDER_STRUCTURE_TEMPLATE,
&memory_extensions_root,
)
} else {
String::new()
};
let memory_extensions_primary_inputs = if memory_extensions_exist {
render_memory_extensions_block(
&MEMORY_EXTENSIONS_PRIMARY_INPUTS_TEMPLATE,
&memory_extensions_root,
)
} else {
String::new()
};
let phase2_input_selection = render_phase2_input_selection(selection);
CONSOLIDATION_PROMPT_TEMPLATE
.render([
("memory_root", memory_root.as_str()),
(
"memory_extensions_folder_structure",
memory_extensions_folder_structure.as_str(),
),
(
"memory_extensions_primary_inputs",
memory_extensions_primary_inputs.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}"
)
})
}

fn render_memory_extensions_block(template: &Template, memory_extensions_root: &str) -> String {
template
.render([("memory_extensions_root", memory_extensions_root)])
.unwrap_or_else(|err| {
warn!("failed to render memories extension prompt block: {err}");
String::new()
})
}

fn render_phase2_input_selection(selection: &Phase2InputSelection) -> String {
Expand Down
48 changes: 45 additions & 3 deletions codex-rs/core/src/memories/prompts_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,56 @@ fn build_stage_one_input_message_uses_default_limit_when_model_context_window_mi

#[test]
fn build_consolidation_prompt_renders_embedded_template() {
let prompt =
build_consolidation_prompt(Path::new("/tmp/memories"), &Phase2InputSelection::default());
let temp = tempdir().unwrap();
let memories_dir = temp.path().join("memories");

let prompt = build_consolidation_prompt(&memories_dir, &Phase2InputSelection::default());

assert!(prompt.contains("Folder structure (under /tmp/memories/):"));
assert!(prompt.contains(&format!(
"Folder structure (under {}/):",
memories_dir.display()
)));
assert!(!prompt.contains("Memory extensions (under"));
assert!(!prompt.contains("<extension_name>/instructions.md"));
assert!(prompt.contains("**Diff since last consolidation:**"));
assert!(prompt.contains("- selected inputs this run: 0"));
}

#[tokio::test]
async fn build_consolidation_prompt_points_to_extensions_without_inlining_them() {
let temp = tempdir().unwrap();
let memories_dir = temp.path().join("memories");
let extension_dir = temp.path().join("memories_extensions/tape_recorder");
tokio_fs::create_dir_all(extension_dir.join("resources"))
.await
.unwrap();
tokio_fs::write(
extension_dir.join("instructions.md"),
"source-specific instructions\n",
)
.await
.unwrap();
tokio_fs::write(
extension_dir.join("resources/notes.md"),
"source-specific resource\n",
)
.await
.unwrap();

let prompt = build_consolidation_prompt(&memories_dir, &Phase2InputSelection::default());
let memory_extensions_dir = temp.path().join("memories_extensions");

assert!(prompt.contains(&format!(
"Memory extensions (under {}/)",
memory_extensions_dir.display()
)));
assert!(prompt.contains(&format!("Under `{}/`:", memory_extensions_dir.display())));
assert!(prompt.contains("<extension_name>/instructions.md"));
assert!(prompt.contains("Optional source-specific inputs:"));
assert!(!prompt.contains("source-specific instructions"));
assert!(!prompt.contains("source-specific resource"));
}

#[tokio::test]
async fn build_memory_tool_developer_instructions_renders_embedded_template() {
let temp = tempdir().unwrap();
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/core/templates/memories/consolidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Folder structure (under {{ memory_root }}/):
- Recap of the rollout, including lessons learned, reusable knowledge,
pointers/references, and pruned raw evidence snippets. Distilled version of
everything valuable from the raw rollout.

{{ memory_extensions_folder_structure }}
============================================================
GLOBAL SAFETY, HYGIENE, AND NO-FILLER RULES (STRICT)
============================================================
Expand Down Expand Up @@ -135,7 +135,7 @@ Under `{{ memory_root }}/`:
- read the existing summary so updates stay consistent
- `skills/*`
- read existing skills so updates are incremental and non-duplicative

{{ memory_extensions_primary_inputs }}
Mode selection:

- INIT phase: existing artifacts are missing/empty (especially `memory_summary.md`
Expand Down
Loading