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
@@ -0,0 +1,73 @@
|
||||
use codex_extension_api::JsonToolOutput;
|
||||
use codex_extension_api::ToolCall;
|
||||
use codex_extension_api::ToolExecutor;
|
||||
use codex_extension_api::ToolName;
|
||||
use codex_extension_api::ToolSpec;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::ADD_AD_HOC_NOTE_TOOL_NAME;
|
||||
use crate::backend::AddAdHocMemoryNoteRequest;
|
||||
use crate::backend::AddAdHocMemoryNoteResponse;
|
||||
use crate::backend::MemoriesBackend;
|
||||
|
||||
use super::backend_error_to_function_call;
|
||||
use super::memory_function_tool;
|
||||
use super::memory_tool_name;
|
||||
use super::parse_args;
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct AddAdHocNoteArgs {
|
||||
/// Name of the note file to create, in
|
||||
/// YYYY-MM-DDTHH-MM-SS-<slug>.md format. The slug must use only lowercase
|
||||
/// ASCII letters, digits, and hyphens.
|
||||
#[schemars(
|
||||
length(min = 24, max = 128),
|
||||
regex(pattern = r"^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-[a-z0-9][a-z0-9-]{0,79}\.md$")
|
||||
)]
|
||||
filename: String,
|
||||
/// Verbatim Markdown note to append to the ad-hoc memory notes.
|
||||
#[schemars(length(min = 1))]
|
||||
note: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct AddAdHocNoteTool<B> {
|
||||
pub(super) backend: B,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<B> ToolExecutor<ToolCall> for AddAdHocNoteTool<B>
|
||||
where
|
||||
B: MemoriesBackend,
|
||||
{
|
||||
fn tool_name(&self) -> ToolName {
|
||||
memory_tool_name(ADD_AD_HOC_NOTE_TOOL_NAME)
|
||||
}
|
||||
|
||||
fn spec(&self) -> ToolSpec {
|
||||
memory_function_tool::<AddAdHocNoteArgs, AddAdHocMemoryNoteResponse>(
|
||||
ADD_AD_HOC_NOTE_TOOL_NAME,
|
||||
"Create one append-only ad-hoc memory note after the user explicitly asks Codex to remember, forget, or update something.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
call: ToolCall,
|
||||
) -> Result<Box<dyn codex_extension_api::ToolOutput>, codex_extension_api::FunctionCallError>
|
||||
{
|
||||
let backend = self.backend.clone();
|
||||
let args: AddAdHocNoteArgs = parse_args(&call)?;
|
||||
let response = backend
|
||||
.add_ad_hoc_note(AddAdHocMemoryNoteRequest {
|
||||
filename: args.filename,
|
||||
note: args.note,
|
||||
})
|
||||
.await
|
||||
.map_err(backend_error_to_function_call)?;
|
||||
Ok(Box::new(JsonToolOutput::new(json!(response))))
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ use crate::backend::MemoriesBackend;
|
||||
use crate::backend::MemoriesBackendError;
|
||||
use crate::schema;
|
||||
|
||||
mod ad_hoc_note;
|
||||
mod list;
|
||||
mod read;
|
||||
mod search;
|
||||
@@ -28,6 +29,9 @@ where
|
||||
B: MemoriesBackend,
|
||||
{
|
||||
vec![
|
||||
Arc::new(ad_hoc_note::AddAdHocNoteTool {
|
||||
backend: backend.clone(),
|
||||
}),
|
||||
Arc::new(list::ListTool {
|
||||
backend: backend.clone(),
|
||||
}),
|
||||
@@ -82,12 +86,15 @@ fn backend_error_to_function_call(err: MemoriesBackendError) -> FunctionCallErro
|
||||
match err {
|
||||
MemoriesBackendError::InvalidPath { .. }
|
||||
| MemoriesBackendError::InvalidCursor { .. }
|
||||
| MemoriesBackendError::InvalidFilename { .. }
|
||||
| MemoriesBackendError::NotFound { .. }
|
||||
| MemoriesBackendError::InvalidLineOffset
|
||||
| MemoriesBackendError::InvalidMaxLines
|
||||
| MemoriesBackendError::LineOffsetExceedsFileLength
|
||||
| MemoriesBackendError::NotFile { .. }
|
||||
| MemoriesBackendError::EmptyQuery
|
||||
| MemoriesBackendError::EmptyAdHocNote
|
||||
| MemoriesBackendError::AdHocNoteAlreadyExists { .. }
|
||||
| MemoriesBackendError::InvalidMatchWindow => {
|
||||
FunctionCallError::RespondToModel(err.to_string())
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user