mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Add ad-hoc memory note tool (#24562)
## Why Codex memory updates currently rely on instructions that tell agents to create ad-hoc note files directly in the memory workspace. The memories extension already has a `MemoriesBackend` abstraction for local storage and future non-filesystem backends, so the ad-hoc note writer should live behind that same interface instead of baking local filesystem assumptions into the tool shape. ## What - Adds a `memories/add_ad_hoc_note` tool to the existing memories tool bundle. - Extends `MemoriesBackend` with `add_ad_hoc_note` plus request/response types so remote memory stores can implement the same operation later. - Implements the local backend by creating append-only notes under `extensions/ad_hoc/notes`. - Validates the tool-provided filename contract (`YYYY-MM-DDTHH-MM-SS-<slug>.md`), rejects path-like filenames, rejects empty notes, and uses create-new semantics so existing notes are never overwritten. - Keeps memories tool contribution behind the existing commented-out registration path; this defines the tool surface without newly exposing it through app-server. ## Test Plan - `just test -p codex-memories-extension`
This commit is contained in:
1 parent
de513a83f3
commit
3936ed221d
7 files changed
+361
-1
No files matched your search
@@ -69,6 +69,7 @@ fn tools_are_contributed_when_enabled() {
|
||||
assert_eq!(
|
||||
tool_names,
|
||||
vec![
|
||||
memory_tool_name(crate::ADD_AD_HOC_NOTE_TOOL_NAME),
|
||||
memory_tool_name(crate::LIST_TOOL_NAME),
|
||||
memory_tool_name(crate::READ_TOOL_NAME),
|
||||
memory_tool_name(crate::SEARCH_TOOL_NAME),
|
||||
@@ -76,6 +77,26 @@ fn tools_are_contributed_when_enabled() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ad_hoc_tool_definition_includes_filename_contract() {
|
||||
let tool = memory_tool(
|
||||
Path::new("/tmp/codex-home/memories"),
|
||||
crate::ADD_AD_HOC_NOTE_TOOL_NAME,
|
||||
);
|
||||
let spec = serde_json::to_value(tool.spec()).expect("serialize tool spec");
|
||||
|
||||
let filename = spec
|
||||
.pointer("/tools/0/parameters/properties/filename")
|
||||
.expect("filename parameter should be in tool schema");
|
||||
assert_eq!(filename.pointer("/type"), Some(&json!("string")));
|
||||
assert!(
|
||||
filename
|
||||
.pointer("/description")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|description| description.contains("YYYY-MM-DDTHH-MM-SS-<slug>.md"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prompt_contribution_uses_memory_summary_when_enabled() {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
@@ -110,6 +131,79 @@ async fn prompt_contribution_uses_memory_summary_when_enabled() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_ad_hoc_note_tool_creates_note_file() {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
let memory_root = tempdir.path().join("memories");
|
||||
let tool = memory_tool(&memory_root, crate::ADD_AD_HOC_NOTE_TOOL_NAME);
|
||||
let payload = ToolPayload::Function {
|
||||
arguments: json!({
|
||||
"filename": "2026-05-26T13-42-08-remember-review-style.md",
|
||||
"note": "Remember to keep PR review comments concise.",
|
||||
})
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
let output = tool
|
||||
.handle(ToolCall {
|
||||
turn_id: "turn-1".to_string(),
|
||||
call_id: "call-1".to_string(),
|
||||
tool_name: memory_tool_name(crate::ADD_AD_HOC_NOTE_TOOL_NAME),
|
||||
truncation_policy: TruncationPolicy::Bytes(1024),
|
||||
conversation_history: codex_extension_api::ConversationHistory::default(),
|
||||
payload: payload.clone(),
|
||||
})
|
||||
.await
|
||||
.expect("ad-hoc note should be written");
|
||||
|
||||
assert_eq!(
|
||||
output.post_tool_use_response("call-1", &payload),
|
||||
Some(json!({}))
|
||||
);
|
||||
assert_eq!(
|
||||
tokio::fs::read_to_string(
|
||||
memory_root
|
||||
.join("extensions/ad_hoc/notes")
|
||||
.join("2026-05-26T13-42-08-remember-review-style.md")
|
||||
)
|
||||
.await
|
||||
.expect("read ad-hoc note"),
|
||||
"Remember to keep PR review comments concise."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_ad_hoc_note_tool_rejects_paths_as_filenames() {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
let memory_root = tempdir.path().join("memories");
|
||||
let tool = memory_tool(&memory_root, crate::ADD_AD_HOC_NOTE_TOOL_NAME);
|
||||
let payload = ToolPayload::Function {
|
||||
arguments: json!({
|
||||
"filename": "../2026-05-26T13-42-08-remember-review-style.md",
|
||||
"note": "Remember to keep PR review comments concise.",
|
||||
})
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
let result = tool
|
||||
.handle(ToolCall {
|
||||
turn_id: "turn-1".to_string(),
|
||||
call_id: "call-1".to_string(),
|
||||
tool_name: memory_tool_name(crate::ADD_AD_HOC_NOTE_TOOL_NAME),
|
||||
truncation_policy: TruncationPolicy::Bytes(1024),
|
||||
conversation_history: codex_extension_api::ConversationHistory::default(),
|
||||
payload,
|
||||
})
|
||||
.await;
|
||||
let err = match result {
|
||||
Ok(_) => panic!("path-like filename should be rejected"),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
assert!(err.to_string().contains("filename"));
|
||||
assert!(err.to_string().contains("YYYY-MM-DDTHH-MM-SS"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_tool_reads_memory_file() {
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
|
||||
Reference in new issue
Block a user