ext/memories/src/tools/mod.rs
113 lines
use std::sync::Arc;use codex_extension_api::FunctionCallError;use codex_extension_api::ResponsesApiTool;use codex_extension_api::ToolCall;use codex_extension_api::ToolExecutor;use codex_extension_api::ToolName;use codex_extension_api::ToolSpec;use codex_extension_api::parse_tool_input_schema;use codex_otel::MetricsClient;use codex_tools::ResponsesApiNamespace;use codex_tools::ResponsesApiNamespaceTool;use codex_tools::default_namespace_description;use schemars::JsonSchema;use serde::Deserialize;use serde_json::Value;use crate::MEMORY_TOOLS_NAMESPACE;use crate::backend::MemoriesBackend;use crate::backend::MemoriesBackendError;use crate::schema;mod ad_hoc_note;mod list;mod read;mod search;pub(crate) fn memory_tools<B>( backend: B, metrics_client: Option<MetricsClient>,) -> Vec<Arc<dyn ToolExecutor<ToolCall>>>where B: MemoriesBackend,{ vec![ Arc::new(ad_hoc_note::AddAdHocNoteTool { backend: backend.clone(), metrics_client: metrics_client.clone(), }), Arc::new(list::ListTool { backend: backend.clone(), metrics_client: metrics_client.clone(), }), Arc::new(read::ReadTool { backend: backend.clone(), metrics_client: metrics_client.clone(), }), Arc::new(search::SearchTool { backend, metrics_client, }), ]}pub(super) fn memory_tool_name(name: &str) -> ToolName { ToolName::namespaced(MEMORY_TOOLS_NAMESPACE, name)}pub(super) fn memory_function_tool<I: JsonSchema, O: JsonSchema>( name: &str, description: &str,) -> ToolSpec { let tool = ResponsesApiTool { name: name.to_string(), description: description.to_string(), strict: false, defer_loading: None, parameters: parse_tool_input_schema(&schema::input_schema_for::<I>()) .unwrap_or_else(|err| panic!("generated input schema for {name} should parse: {err}")), output_schema: Some(schema::output_schema_for::<O>()), }; ToolSpec::Namespace(ResponsesApiNamespace { name: MEMORY_TOOLS_NAMESPACE.to_string(), description: default_namespace_description(MEMORY_TOOLS_NAMESPACE), tools: vec![ResponsesApiNamespaceTool::Function(tool)], })}fn parse_args<T: for<'de> Deserialize<'de>>(call: &ToolCall) -> Result<T, FunctionCallError> { let arguments = call.function_arguments()?; let value = if arguments.trim().is_empty() { Value::Object(serde_json::Map::new()) } else { serde_json::from_str(arguments) .map_err(|err| FunctionCallError::RespondToModel(err.to_string()))? }; serde_json::from_value(value).map_err(|err| FunctionCallError::RespondToModel(err.to_string()))}fn clamp_max_results(requested: Option<usize>, default: usize, max: usize) -> usize { requested.unwrap_or(default).clamp(1, max)}fn backend_error_to_function_call(err: MemoriesBackendError) -> FunctionCallError { 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()) } MemoriesBackendError::Io(_) => FunctionCallError::Fatal(err.to_string()), }}