mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
[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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user