mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
## Why Memory search currently treats separators literally, so callers need to know whether a stored term uses spaces, hyphens, or no separators at all. That makes recall brittle for terms such as `MultiAgentV2` vs. `multi agent v2` and `cold-resume` vs. `cold resume`. ## What changed - Add an opt-in `normalized` mode to memory search that removes non-alphanumeric separators after any requested case folding. - Thread the new flag through the MCP `search` tool into the local backend while keeping existing literal matching as the default. - Reject queries that normalize to an empty string, and add regression coverage for both normalized matching and that validation path. ## Testing - `cargo test -p codex-memories-mcp`
165 lines
4.8 KiB
Rust
165 lines
4.8 KiB
Rust
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::future::Future;
|
|
|
|
pub const DEFAULT_LIST_MAX_RESULTS: usize = 2_000;
|
|
pub const MAX_LIST_RESULTS: usize = 2_000;
|
|
pub const DEFAULT_SEARCH_MAX_RESULTS: usize = 200;
|
|
pub const MAX_SEARCH_RESULTS: usize = 200;
|
|
pub const DEFAULT_READ_MAX_TOKENS: usize = 20_000;
|
|
|
|
/// Storage interface behind the memories MCP tools.
|
|
///
|
|
/// Implementations should return paths relative to the memory store and enforce
|
|
/// their own storage-specific access rules. The local implementation uses the
|
|
/// filesystem today; a later implementation can satisfy the same contract from a
|
|
/// remote backend.
|
|
pub trait MemoriesBackend: Clone + Send + Sync + 'static {
|
|
fn list(
|
|
&self,
|
|
request: ListMemoriesRequest,
|
|
) -> impl Future<Output = Result<ListMemoriesResponse, MemoriesBackendError>> + Send;
|
|
|
|
fn read(
|
|
&self,
|
|
request: ReadMemoryRequest,
|
|
) -> impl Future<Output = Result<ReadMemoryResponse, MemoriesBackendError>> + Send;
|
|
|
|
fn search(
|
|
&self,
|
|
request: SearchMemoriesRequest,
|
|
) -> impl Future<Output = Result<SearchMemoriesResponse, MemoriesBackendError>> + Send;
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ListMemoriesRequest {
|
|
pub path: Option<String>,
|
|
pub cursor: Option<String>,
|
|
pub max_results: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
|
|
#[schemars(deny_unknown_fields)]
|
|
pub struct ListMemoriesResponse {
|
|
pub path: Option<String>,
|
|
pub entries: Vec<MemoryEntry>,
|
|
pub next_cursor: Option<String>,
|
|
pub truncated: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ReadMemoryRequest {
|
|
pub path: String,
|
|
pub line_offset: usize,
|
|
pub max_lines: Option<usize>,
|
|
pub max_tokens: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
|
|
#[schemars(deny_unknown_fields)]
|
|
pub struct ReadMemoryResponse {
|
|
pub path: String,
|
|
pub start_line_number: usize,
|
|
pub content: String,
|
|
pub truncated: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct SearchMemoriesRequest {
|
|
pub queries: Vec<String>,
|
|
pub match_mode: SearchMatchMode,
|
|
pub path: Option<String>,
|
|
pub cursor: Option<String>,
|
|
pub context_lines: usize,
|
|
pub case_sensitive: bool,
|
|
pub normalized: bool,
|
|
pub max_results: usize,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
|
|
#[schemars(deny_unknown_fields)]
|
|
pub struct SearchMemoriesResponse {
|
|
pub queries: Vec<String>,
|
|
pub match_mode: SearchMatchMode,
|
|
pub path: Option<String>,
|
|
pub matches: Vec<MemorySearchMatch>,
|
|
pub next_cursor: Option<String>,
|
|
pub truncated: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum SearchMatchMode {
|
|
Any,
|
|
AllOnSameLine,
|
|
AllWithinLines {
|
|
#[schemars(range(min = 1))]
|
|
line_count: usize,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
|
|
#[schemars(deny_unknown_fields)]
|
|
pub struct MemoryEntry {
|
|
pub path: String,
|
|
pub entry_type: MemoryEntryType,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum MemoryEntryType {
|
|
File,
|
|
Directory,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
|
|
#[schemars(deny_unknown_fields)]
|
|
pub struct MemorySearchMatch {
|
|
pub path: String,
|
|
pub match_line_number: usize,
|
|
pub content_start_line_number: usize,
|
|
pub content: String,
|
|
pub matched_queries: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum MemoriesBackendError {
|
|
#[error("path '{path}' {reason}")]
|
|
InvalidPath { path: String, reason: String },
|
|
#[error("cursor '{cursor}' {reason}")]
|
|
InvalidCursor { cursor: String, reason: String },
|
|
#[error("path '{path}' was not found")]
|
|
NotFound { path: String },
|
|
#[error("line_offset must be a 1-indexed line number")]
|
|
InvalidLineOffset,
|
|
#[error("max_lines must be a positive integer")]
|
|
InvalidMaxLines,
|
|
#[error("line_offset exceeds file length")]
|
|
LineOffsetExceedsFileLength,
|
|
#[error("path '{path}' is not a file")]
|
|
NotFile { path: String },
|
|
#[error("queries must not be empty or contain empty strings")]
|
|
EmptyQuery,
|
|
#[error("all_within_lines.line_count must be a positive integer")]
|
|
InvalidMatchWindow,
|
|
#[error("I/O error while reading memories: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
impl MemoriesBackendError {
|
|
pub fn invalid_path(path: impl Into<String>, reason: impl Into<String>) -> Self {
|
|
Self::InvalidPath {
|
|
path: path.into(),
|
|
reason: reason.into(),
|
|
}
|
|
}
|
|
|
|
pub fn invalid_cursor(cursor: impl Into<String>, reason: impl Into<String>) -> Self {
|
|
Self::InvalidCursor {
|
|
cursor: cursor.into(),
|
|
reason: reason.into(),
|
|
}
|
|
}
|
|
}
|