From e8ca1d9de0d97628a8113e29bd0f1f3c6c513b74 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Fri, 8 May 2026 14:40:24 +0200 Subject: [PATCH] extension: add memories extension --- codex-rs/Cargo.lock | 18 ++ codex-rs/Cargo.toml | 3 + codex-rs/ext/memories/BUILD.bazel | 6 + codex-rs/ext/memories/Cargo.toml | 26 +++ .../memories/src/contributors/citations.rs | 34 ++++ codex-rs/ext/memories/src/contributors/mod.rs | 7 + .../ext/memories/src/contributors/prompt.rs | 34 ++++ .../ext/memories/src/contributors/tools.rs | 27 +++ codex-rs/ext/memories/src/ctx.rs | 12 ++ codex-rs/ext/memories/src/lib.rs | 70 +++++++ codex-rs/ext/memories/src/tools/list.rs | 109 ++++++++++ codex-rs/ext/memories/src/tools/mod.rs | 73 +++++++ codex-rs/ext/memories/src/tools/read.rs | 108 ++++++++++ codex-rs/ext/memories/src/tools/search.rs | 192 ++++++++++++++++++ 14 files changed, 719 insertions(+) create mode 100644 codex-rs/ext/memories/BUILD.bazel create mode 100644 codex-rs/ext/memories/Cargo.toml create mode 100644 codex-rs/ext/memories/src/contributors/citations.rs create mode 100644 codex-rs/ext/memories/src/contributors/mod.rs create mode 100644 codex-rs/ext/memories/src/contributors/prompt.rs create mode 100644 codex-rs/ext/memories/src/contributors/tools.rs create mode 100644 codex-rs/ext/memories/src/ctx.rs create mode 100644 codex-rs/ext/memories/src/lib.rs create mode 100644 codex-rs/ext/memories/src/tools/list.rs create mode 100644 codex-rs/ext/memories/src/tools/mod.rs create mode 100644 codex-rs/ext/memories/src/tools/read.rs create mode 100644 codex-rs/ext/memories/src/tools/search.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index af24afbdd8..abc330b458 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3101,6 +3101,24 @@ dependencies = [ "wiremock", ] +[[package]] +name = "codex-memories" +version = "0.0.0" +dependencies = [ + "codex-core", + "codex-extension-api", + "codex-features", + "codex-memories-mcp", + "codex-memories-read", + "codex-protocol", + "codex-tools", + "codex-utils-absolute-path", + "codex-utils-stream-parser", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "codex-memories-mcp" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 6877038af8..0cf2a34c36 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -46,6 +46,7 @@ members = [ "execpolicy-legacy", "ext/extension-api", "ext/git-attribution", + "ext/memories", "external-agent-migration", "external-agent-sessions", "keyring-store", @@ -161,6 +162,7 @@ codex-exec-server = { path = "exec-server" } codex-execpolicy = { path = "execpolicy" } codex-extension-api = { path = "ext/extension-api" } codex-git-attribution = { path = "ext/git-attribution" } +codex-memories = { path = "ext/memories" } codex-external-agent-migration = { path = "external-agent-migration" } codex-external-agent-sessions = { path = "external-agent-sessions" } codex-experimental-api-macros = { path = "codex-experimental-api-macros" } @@ -469,6 +471,7 @@ unwrap_used = "deny" ignored = [ "codex-agent-graph-store", "codex-extension-api", + "codex-memories", "codex-memories-mcp", "icu_provider", "openssl-sys", diff --git a/codex-rs/ext/memories/BUILD.bazel b/codex-rs/ext/memories/BUILD.bazel new file mode 100644 index 0000000000..a95d54b842 --- /dev/null +++ b/codex-rs/ext/memories/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "memories", + crate_name = "codex_memories", +) diff --git a/codex-rs/ext/memories/Cargo.toml b/codex-rs/ext/memories/Cargo.toml new file mode 100644 index 0000000000..28413ac3dc --- /dev/null +++ b/codex-rs/ext/memories/Cargo.toml @@ -0,0 +1,26 @@ +[package] +edition.workspace = true +license.workspace = true +name = "codex-memories" +version.workspace = true + +[lib] +name = "codex_memories" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +codex-core = { workspace = true } +codex-extension-api = { workspace = true } +codex-features = { workspace = true } +codex-memories-mcp = { workspace = true } +codex-memories-read = { workspace = true } +codex-protocol = { workspace = true } +codex-tools = { workspace = true } +codex-utils-absolute-path = { workspace = true } +codex-utils-stream-parser = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { workspace = true, features = ["fs"] } diff --git a/codex-rs/ext/memories/src/contributors/citations.rs b/codex-rs/ext/memories/src/contributors/citations.rs new file mode 100644 index 0000000000..c382a5d429 --- /dev/null +++ b/codex-rs/ext/memories/src/contributors/citations.rs @@ -0,0 +1,34 @@ +use codex_extension_api::ExtensionData; +use codex_extension_api::TurnItemContributionFuture; +use codex_extension_api::TurnItemContributor; +use codex_memories_read::citations::parse_memory_citation; +use codex_protocol::items::AgentMessageContent; +use codex_protocol::items::TurnItem; +use codex_utils_stream_parser::strip_citations; + +#[derive(Debug)] +pub(crate) struct CitationContributor; + +impl TurnItemContributor for CitationContributor { + fn contribute<'a>( + &'a self, + _thread_store: &'a ExtensionData, + _turn_store: &'a ExtensionData, + item: &'a mut TurnItem, + ) -> TurnItemContributionFuture<'a> { + if let TurnItem::AgentMessage(agent_message) = item { + let text = agent_message + .content + .iter() + .map(|entry| match entry { + AgentMessageContent::Text { text } => text.as_str(), + }) + .collect::(); + let (visible_text, citations) = strip_citations(&text); + agent_message.content = vec![AgentMessageContent::Text { text: visible_text }]; + agent_message.memory_citation = parse_memory_citation(citations); + } + + Box::pin(std::future::ready(Ok(()))) + } +} diff --git a/codex-rs/ext/memories/src/contributors/mod.rs b/codex-rs/ext/memories/src/contributors/mod.rs new file mode 100644 index 0000000000..3d49a199d8 --- /dev/null +++ b/codex-rs/ext/memories/src/contributors/mod.rs @@ -0,0 +1,7 @@ +mod citations; +mod prompt; +mod tools; + +pub(crate) use citations::CitationContributor; +pub(crate) use prompt::PromptContributor; +pub(crate) use tools::ToolsContributor; diff --git a/codex-rs/ext/memories/src/contributors/prompt.rs b/codex-rs/ext/memories/src/contributors/prompt.rs new file mode 100644 index 0000000000..d15ab59963 --- /dev/null +++ b/codex-rs/ext/memories/src/contributors/prompt.rs @@ -0,0 +1,34 @@ +use codex_extension_api::ContextContributor; +use codex_extension_api::ExtensionData; +use codex_extension_api::PromptFragment; + +use crate::ctx::read_surface_enabled; + +#[derive(Debug)] +pub(crate) struct PromptContributor { + read_prompt: Option, +} + +impl PromptContributor { + pub(crate) fn new(read_prompt: Option) -> Self { + Self { read_prompt } + } +} + +impl ContextContributor for PromptContributor { + fn contribute( + &self, + _session_store: &ExtensionData, + thread_store: &ExtensionData, + ) -> Vec { + if !read_surface_enabled(thread_store) { + return Vec::new(); + } + + self.read_prompt + .as_deref() + .map(PromptFragment::developer_policy) + .into_iter() + .collect() + } +} diff --git a/codex-rs/ext/memories/src/contributors/tools.rs b/codex-rs/ext/memories/src/contributors/tools.rs new file mode 100644 index 0000000000..513a166825 --- /dev/null +++ b/codex-rs/ext/memories/src/contributors/tools.rs @@ -0,0 +1,27 @@ +use codex_extension_api::ExtensionData; +use codex_extension_api::ToolContribution; +use codex_extension_api::ToolContributor; + +use crate::ctx::read_surface_enabled; +use crate::tools::MemoriesTools; + +#[derive(Debug)] +pub(crate) struct ToolsContributor { + tools: MemoriesTools, +} + +impl ToolsContributor { + pub(crate) fn new(tools: MemoriesTools) -> Self { + Self { tools } + } +} + +impl ToolContributor for ToolsContributor { + fn tools(&self, thread_store: &ExtensionData) -> Vec { + if !read_surface_enabled(thread_store) { + return Vec::new(); + } + + self.tools.contributions() + } +} diff --git a/codex-rs/ext/memories/src/ctx.rs b/codex-rs/ext/memories/src/ctx.rs new file mode 100644 index 0000000000..c9b33370e8 --- /dev/null +++ b/codex-rs/ext/memories/src/ctx.rs @@ -0,0 +1,12 @@ +use codex_extension_api::ExtensionData; + +#[derive(Clone, Copy, Debug, Default)] +pub(crate) struct MemoriesReadConfig { + pub(crate) enabled: bool, +} + +pub(crate) fn read_surface_enabled(thread_store: &ExtensionData) -> bool { + thread_store + .get::() + .is_some_and(|config| config.enabled) +} diff --git a/codex-rs/ext/memories/src/lib.rs b/codex-rs/ext/memories/src/lib.rs new file mode 100644 index 0000000000..3dcb9e49b1 --- /dev/null +++ b/codex-rs/ext/memories/src/lib.rs @@ -0,0 +1,70 @@ +mod contributors; +pub mod ctx; +mod tools; + +use std::path::PathBuf; +use std::sync::Arc; + +use crate::ctx::MemoriesReadConfig; +use codex_core::config::Config; +use codex_extension_api::CodexExtension; +use codex_extension_api::ExtensionData; +use codex_extension_api::ExtensionRegistryBuilder; +use codex_extension_api::ThreadStartContributor; +use codex_features::Feature; +use codex_memories_read::build_memory_tool_developer_instructions; +use codex_memories_read::memory_root; +use codex_utils_absolute_path::AbsolutePathBuf; +use contributors::CitationContributor; +use contributors::PromptContributor; +use contributors::ToolsContributor; +use tools::MemoriesTools; + +/// Extension that contributes memories read surfaces. +#[derive(Clone, Debug)] +pub struct MemoriesExtension { + prompt: Arc, + tools: Arc, + citations: Arc, +} + +impl CodexExtension for MemoriesExtension { + fn install(self: Arc, registry: &mut ExtensionRegistryBuilder) { + registry.thread_start_contributor(self.clone()); + registry.prompt_contributor(self.prompt.clone()); + registry.tool_contributor(self.tools.clone()); + registry.turn_item_contributor(self.citations.clone()); + } +} + +impl ThreadStartContributor for MemoriesExtension { + fn contribute( + &self, + config: &Config, + _session_store: &ExtensionData, + thread_store: &ExtensionData, + ) { + thread_store.insert(MemoriesReadConfig { + enabled: config.features.enabled(Feature::MemoryTool) && config.memories.use_memories, + }); + } +} + +impl MemoriesExtension { + /// Creates a memories extension from the prompt text and backing directory + /// it should expose. + pub fn new(read_prompt: Option, memories_root: impl Into) -> Self { + Self { + prompt: Arc::new(PromptContributor::new(read_prompt)), + tools: Arc::new(ToolsContributor::new(MemoriesTools::new(memories_root))), + citations: Arc::new(CitationContributor), + } + } +} + +pub async fn extension(codex_home: &AbsolutePathBuf) -> Arc { + Arc::new(MemoriesExtension::new( + build_memory_tool_developer_instructions(codex_home).await, + memory_root(codex_home).to_path_buf(), + )) +} diff --git a/codex-rs/ext/memories/src/tools/list.rs b/codex-rs/ext/memories/src/tools/list.rs new file mode 100644 index 0000000000..f2d6188f44 --- /dev/null +++ b/codex-rs/ext/memories/src/tools/list.rs @@ -0,0 +1,109 @@ +use std::collections::BTreeMap; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use codex_extension_api::ToolCallError; +use codex_extension_api::ToolContribution; +use codex_extension_api::ToolHandler; +use codex_memories_mcp::LocalMemoriesBackend; +use codex_memories_mcp::backend::DEFAULT_LIST_MAX_RESULTS; +use codex_memories_mcp::backend::ListMemoriesRequest; +use codex_memories_mcp::backend::MAX_LIST_RESULTS; +use codex_memories_mcp::backend::MemoriesBackend; +use codex_tools::JsonSchema; +use codex_tools::ResponsesApiTool; +use serde::Deserialize; +use serde_json::Value; + +use super::backend_error; +use super::clamp_max_results; +use super::ensure_root; +use super::parse_arguments; +use super::serialize_output; + +const TOOL_NAME: &str = "list_memories"; + +#[derive(Debug)] +pub(super) struct ListMemoriesTool { + backend: LocalMemoriesBackend, +} + +impl ListMemoriesTool { + pub(super) fn new(backend: LocalMemoriesBackend) -> Self { + Self { backend } + } + + pub(super) fn contribution(self: &Arc) -> ToolContribution { + let handler: Arc = self.clone(); + ToolContribution::new(tool_spec(), handler).allow_parallel_calls() + } +} + +impl ToolHandler for ListMemoriesTool { + fn handle<'a>( + &'a self, + arguments: Value, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let args: ListArgs = parse_arguments(arguments)?; + ensure_root(&self.backend).await?; + let response = self + .backend + .list(ListMemoriesRequest { + path: args.path, + cursor: args.cursor, + max_results: clamp_max_results( + args.max_results, + DEFAULT_LIST_MAX_RESULTS, + MAX_LIST_RESULTS, + ), + }) + .await + .map_err(backend_error)?; + serialize_output(response) + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ListArgs { + path: Option, + cursor: Option, + max_results: Option, +} + +fn tool_spec() -> ResponsesApiTool { + let properties = BTreeMap::from([ + ( + "path".to_string(), + JsonSchema::string(Some( + "Optional relative path to list inside the Codex memories store.".to_string(), + )), + ), + ( + "cursor".to_string(), + JsonSchema::string(Some( + "Optional cursor returned by a previous list_memories call.".to_string(), + )), + ), + ( + "max_results".to_string(), + JsonSchema::integer(Some( + "Optional maximum number of entries to return.".to_string(), + )), + ), + ]); + + ResponsesApiTool { + name: TOOL_NAME.to_string(), + description: + "List immediate files and directories under a path in the Codex memories store." + .to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object(properties, /*required*/ None, Some(false.into())), + output_schema: None, + } +} diff --git a/codex-rs/ext/memories/src/tools/mod.rs b/codex-rs/ext/memories/src/tools/mod.rs new file mode 100644 index 0000000000..459c00f589 --- /dev/null +++ b/codex-rs/ext/memories/src/tools/mod.rs @@ -0,0 +1,73 @@ +mod list; +mod read; +mod search; + +use std::fmt::Display; +use std::path::PathBuf; +use std::sync::Arc; + +use codex_extension_api::ToolCallError; +use codex_extension_api::ToolContribution; +use codex_memories_mcp::LocalMemoriesBackend; +use serde::Serialize; +use serde::de::DeserializeOwned; +use serde_json::Value; + +use self::list::ListMemoriesTool; +use self::read::ReadMemoryTool; +use self::search::SearchMemoriesTool; + +#[derive(Debug)] +pub(crate) struct MemoriesTools { + list: Arc, + read: Arc, + search: Arc, +} + +impl MemoriesTools { + pub(crate) fn new(memories_root: impl Into) -> Self { + let backend = LocalMemoriesBackend::from_memory_root(memories_root); + Self { + list: Arc::new(ListMemoriesTool::new(backend.clone())), + read: Arc::new(ReadMemoryTool::new(backend.clone())), + search: Arc::new(SearchMemoriesTool::new(backend)), + } + } + + pub(crate) fn contributions(&self) -> Vec { + vec![ + self.list.contribution(), + self.read.contribution(), + self.search.contribution(), + ] + } +} + +async fn ensure_root(backend: &LocalMemoriesBackend) -> Result<(), ToolCallError> { + tokio::fs::create_dir_all(backend.root()) + .await + .map_err(|err| { + ToolCallError::new(format!( + "failed to create memories root at {}: {err}", + backend.root().display() + )) + }) +} + +fn parse_arguments(arguments: Value) -> Result { + serde_json::from_value(arguments) + .map_err(|err| ToolCallError::new(format!("invalid arguments: {err}"))) +} + +fn serialize_output(output: T) -> Result { + serde_json::to_value(output) + .map_err(|err| ToolCallError::new(format!("failed to serialize output: {err}"))) +} + +fn backend_error(err: impl Display) -> ToolCallError { + ToolCallError::new(err.to_string()) +} + +fn clamp_max_results(requested: Option, default: usize, max: usize) -> usize { + requested.unwrap_or(default).clamp(1, max) +} diff --git a/codex-rs/ext/memories/src/tools/read.rs b/codex-rs/ext/memories/src/tools/read.rs new file mode 100644 index 0000000000..cbdd74b08e --- /dev/null +++ b/codex-rs/ext/memories/src/tools/read.rs @@ -0,0 +1,108 @@ +use std::collections::BTreeMap; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use codex_extension_api::ToolCallError; +use codex_extension_api::ToolContribution; +use codex_extension_api::ToolHandler; +use codex_memories_mcp::LocalMemoriesBackend; +use codex_memories_mcp::backend::DEFAULT_READ_MAX_TOKENS; +use codex_memories_mcp::backend::MemoriesBackend; +use codex_memories_mcp::backend::ReadMemoryRequest; +use codex_tools::JsonSchema; +use codex_tools::ResponsesApiTool; +use serde::Deserialize; +use serde_json::Value; + +use super::backend_error; +use super::ensure_root; +use super::parse_arguments; +use super::serialize_output; + +const TOOL_NAME: &str = "read_memory"; + +#[derive(Debug)] +pub(super) struct ReadMemoryTool { + backend: LocalMemoriesBackend, +} + +impl ReadMemoryTool { + pub(super) fn new(backend: LocalMemoriesBackend) -> Self { + Self { backend } + } + + pub(super) fn contribution(self: &Arc) -> ToolContribution { + let handler: Arc = self.clone(); + ToolContribution::new(tool_spec(), handler).allow_parallel_calls() + } +} + +impl ToolHandler for ReadMemoryTool { + fn handle<'a>( + &'a self, + arguments: Value, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let args: ReadArgs = parse_arguments(arguments)?; + ensure_root(&self.backend).await?; + let response = self + .backend + .read(ReadMemoryRequest { + path: args.path, + line_offset: args.line_offset.unwrap_or(1), + max_lines: args.max_lines, + max_tokens: DEFAULT_READ_MAX_TOKENS, + }) + .await + .map_err(backend_error)?; + serialize_output(response) + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct ReadArgs { + path: String, + line_offset: Option, + max_lines: Option, +} + +fn tool_spec() -> ResponsesApiTool { + let properties = BTreeMap::from([ + ( + "path".to_string(), + JsonSchema::string(Some( + "Relative path of the Codex memory file to read.".to_string(), + )), + ), + ( + "line_offset".to_string(), + JsonSchema::integer(Some( + "Optional 1-indexed line number to start reading from.".to_string(), + )), + ), + ( + "max_lines".to_string(), + JsonSchema::integer(Some( + "Optional maximum number of lines to return.".to_string(), + )), + ), + ]); + + ResponsesApiTool { + name: TOOL_NAME.to_string(), + description: + "Read a Codex memory file by relative path, optionally starting at a 1-indexed line offset and limiting the number of lines returned." + .to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + properties, + Some(vec!["path".to_string()]), + Some(false.into()), + ), + output_schema: None, + } +} diff --git a/codex-rs/ext/memories/src/tools/search.rs b/codex-rs/ext/memories/src/tools/search.rs new file mode 100644 index 0000000000..3ba154901a --- /dev/null +++ b/codex-rs/ext/memories/src/tools/search.rs @@ -0,0 +1,192 @@ +use std::collections::BTreeMap; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; + +use codex_extension_api::ToolCallError; +use codex_extension_api::ToolContribution; +use codex_extension_api::ToolHandler; +use codex_memories_mcp::LocalMemoriesBackend; +use codex_memories_mcp::backend::DEFAULT_SEARCH_MAX_RESULTS; +use codex_memories_mcp::backend::MAX_SEARCH_RESULTS; +use codex_memories_mcp::backend::MemoriesBackend; +use codex_memories_mcp::backend::SearchMatchMode; +use codex_memories_mcp::backend::SearchMemoriesRequest; +use codex_tools::JsonSchema; +use codex_tools::ResponsesApiTool; +use serde::Deserialize; +use serde_json::Value; +use serde_json::json; + +use super::backend_error; +use super::clamp_max_results; +use super::ensure_root; +use super::parse_arguments; +use super::serialize_output; + +const TOOL_NAME: &str = "search_memories"; + +#[derive(Debug)] +pub(super) struct SearchMemoriesTool { + backend: LocalMemoriesBackend, +} + +impl SearchMemoriesTool { + pub(super) fn new(backend: LocalMemoriesBackend) -> Self { + Self { backend } + } + + pub(super) fn contribution(self: &Arc) -> ToolContribution { + let handler: Arc = self.clone(); + ToolContribution::new(tool_spec(), handler).allow_parallel_calls() + } +} + +impl ToolHandler for SearchMemoriesTool { + fn handle<'a>( + &'a self, + arguments: Value, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let args: SearchArgs = parse_arguments(arguments)?; + ensure_root(&self.backend).await?; + let response = self + .backend + .search(args.into_request()) + .await + .map_err(backend_error)?; + serialize_output(response) + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct SearchArgs { + queries: Vec, + match_mode: Option, + path: Option, + cursor: Option, + context_lines: Option, + case_sensitive: Option, + normalized: Option, + max_results: Option, +} + +impl SearchArgs { + fn into_request(self) -> SearchMemoriesRequest { + SearchMemoriesRequest { + queries: self.queries, + match_mode: self.match_mode.unwrap_or(SearchMatchMode::Any), + path: self.path, + cursor: self.cursor, + context_lines: self.context_lines.unwrap_or(0), + case_sensitive: self.case_sensitive.unwrap_or(true), + normalized: self.normalized.unwrap_or(false), + max_results: clamp_max_results( + self.max_results, + DEFAULT_SEARCH_MAX_RESULTS, + MAX_SEARCH_RESULTS, + ), + } + } +} + +fn tool_spec() -> ResponsesApiTool { + let properties = BTreeMap::from([ + ( + "queries".to_string(), + JsonSchema::array( + JsonSchema::string(None), + Some("Substrings to search for.".to_string()), + ), + ), + ("match_mode".to_string(), search_match_mode_schema()), + ( + "path".to_string(), + JsonSchema::string(Some( + "Optional relative path to search inside the Codex memories store.".to_string(), + )), + ), + ( + "cursor".to_string(), + JsonSchema::string(Some( + "Optional cursor returned by a previous search_memories call.".to_string(), + )), + ), + ( + "context_lines".to_string(), + JsonSchema::integer(Some( + "Optional number of context lines to include around each match.".to_string(), + )), + ), + ( + "case_sensitive".to_string(), + JsonSchema::boolean(Some( + "Whether matching should be case-sensitive.".to_string(), + )), + ), + ( + "normalized".to_string(), + JsonSchema::boolean(Some( + "Whether to normalize separators while matching.".to_string(), + )), + ), + ( + "max_results".to_string(), + JsonSchema::integer(Some( + "Optional maximum number of matches to return.".to_string(), + )), + ), + ]); + + ResponsesApiTool { + name: TOOL_NAME.to_string(), + description: + "Search Codex memory files for substring matches, optionally normalizing separators or requiring all query substrings on the same line or within a line window." + .to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + properties, + Some(vec!["queries".to_string()]), + Some(false.into()), + ), + output_schema: None, + } +} + +fn search_match_mode_schema() -> JsonSchema { + JsonSchema::any_of( + vec![ + tagged_match_mode_schema("any", None), + tagged_match_mode_schema("all_on_same_line", None), + tagged_match_mode_schema( + "all_within_lines", + Some(( + "line_count".to_string(), + JsonSchema::integer(Some( + "Positive number of lines in the matching window.".to_string(), + )), + )), + ), + ], + Some("How multiple queries must match.".to_string()), + ) +} + +fn tagged_match_mode_schema( + name: &str, + extra_property: Option<(String, JsonSchema)>, +) -> JsonSchema { + let mut properties = BTreeMap::from([( + "type".to_string(), + JsonSchema::string_enum(vec![json!(name)], None), + )]); + let mut required = vec!["type".to_string()]; + if let Some((name, schema)) = extra_property { + required.push(name.clone()); + properties.insert(name, schema); + } + JsonSchema::object(properties, Some(required), Some(false.into())) +}