mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
extension: add memories extension
This commit is contained in:
Generated
+18
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "memories",
|
||||
crate_name = "codex_memories",
|
||||
)
|
||||
@@ -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"] }
|
||||
@@ -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::<String>();
|
||||
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(())))
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
impl PromptContributor {
|
||||
pub(crate) fn new(read_prompt: Option<String>) -> Self {
|
||||
Self { read_prompt }
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextContributor for PromptContributor {
|
||||
fn contribute(
|
||||
&self,
|
||||
_session_store: &ExtensionData,
|
||||
thread_store: &ExtensionData,
|
||||
) -> Vec<PromptFragment> {
|
||||
if !read_surface_enabled(thread_store) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.read_prompt
|
||||
.as_deref()
|
||||
.map(PromptFragment::developer_policy)
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -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<ToolContribution> {
|
||||
if !read_surface_enabled(thread_store) {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.tools.contributions()
|
||||
}
|
||||
}
|
||||
@@ -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::<MemoriesReadConfig>()
|
||||
.is_some_and(|config| config.enabled)
|
||||
}
|
||||
@@ -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<PromptContributor>,
|
||||
tools: Arc<ToolsContributor>,
|
||||
citations: Arc<CitationContributor>,
|
||||
}
|
||||
|
||||
impl CodexExtension<Config> for MemoriesExtension {
|
||||
fn install(self: Arc<Self>, registry: &mut ExtensionRegistryBuilder<Config>) {
|
||||
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<Config> 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<String>, memories_root: impl Into<PathBuf>) -> 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<MemoriesExtension> {
|
||||
Arc::new(MemoriesExtension::new(
|
||||
build_memory_tool_developer_instructions(codex_home).await,
|
||||
memory_root(codex_home).to_path_buf(),
|
||||
))
|
||||
}
|
||||
@@ -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<Self>) -> ToolContribution {
|
||||
let handler: Arc<dyn ToolHandler> = self.clone();
|
||||
ToolContribution::new(tool_spec(), handler).allow_parallel_calls()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for ListMemoriesTool {
|
||||
fn handle<'a>(
|
||||
&'a self,
|
||||
arguments: Value,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Value, ToolCallError>> + 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<String>,
|
||||
cursor: Option<String>,
|
||||
max_results: Option<usize>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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<ListMemoriesTool>,
|
||||
read: Arc<ReadMemoryTool>,
|
||||
search: Arc<SearchMemoriesTool>,
|
||||
}
|
||||
|
||||
impl MemoriesTools {
|
||||
pub(crate) fn new(memories_root: impl Into<PathBuf>) -> 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<ToolContribution> {
|
||||
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<T: DeserializeOwned>(arguments: Value) -> Result<T, ToolCallError> {
|
||||
serde_json::from_value(arguments)
|
||||
.map_err(|err| ToolCallError::new(format!("invalid arguments: {err}")))
|
||||
}
|
||||
|
||||
fn serialize_output<T: Serialize>(output: T) -> Result<Value, ToolCallError> {
|
||||
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<usize>, default: usize, max: usize) -> usize {
|
||||
requested.unwrap_or(default).clamp(1, max)
|
||||
}
|
||||
@@ -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<Self>) -> ToolContribution {
|
||||
let handler: Arc<dyn ToolHandler> = self.clone();
|
||||
ToolContribution::new(tool_spec(), handler).allow_parallel_calls()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for ReadMemoryTool {
|
||||
fn handle<'a>(
|
||||
&'a self,
|
||||
arguments: Value,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Value, ToolCallError>> + 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<usize>,
|
||||
max_lines: Option<usize>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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<Self>) -> ToolContribution {
|
||||
let handler: Arc<dyn ToolHandler> = self.clone();
|
||||
ToolContribution::new(tool_spec(), handler).allow_parallel_calls()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolHandler for SearchMemoriesTool {
|
||||
fn handle<'a>(
|
||||
&'a self,
|
||||
arguments: Value,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Value, ToolCallError>> + 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<String>,
|
||||
match_mode: Option<SearchMatchMode>,
|
||||
path: Option<String>,
|
||||
cursor: Option<String>,
|
||||
context_lines: Option<usize>,
|
||||
case_sensitive: Option<bool>,
|
||||
normalized: Option<bool>,
|
||||
max_results: Option<usize>,
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
Reference in New Issue
Block a user