[codex] add memory extensions (#16276)

# External (non-OpenAI) Pull Request Requirements

Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md

If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.

Include a link to a bug report or enhancement request.
This commit is contained in:
Kevin Liu
2026-04-09 10:45:02 -07:00
committed by GitHub
parent 12f0e0b0eb
commit 76de99ff25
4 changed files with 128 additions and 10 deletions

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 EXTENSIONS_SUBDIR: &str = "memories_extensions";
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 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)
}

View File

@@ -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;
@@ -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) {
@@ -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 {

View File

@@ -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");
assert!(prompt.contains("Folder structure (under /tmp/memories/):"));
let prompt = build_consolidation_prompt(&memories_dir, &Phase2InputSelection::default());
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();

View File

@@ -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)
============================================================
@@ -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`