Add memory tool call metrics to memories extension (#24583)

## Why

The memories extension now receives a metrics exporter, but the useful
extension-owned signal is the memory tool call itself: which operation
ran, which memory area it touched, whether the backend call succeeded,
and whether the result was truncated.

## What changed

- Added the `codex.memories.tool.call` counter in
`ext/memories/src/metrics.rs`.
- Emit that counter from `memories/add_ad_hoc_note`, `memories/list`,
`memories/read`, and `memories/search` after backend execution.
- Tag each call with `tool`, `operation`, `scope`, `status`, and
`truncated`.
- Pass the existing `MetricsClient` through the memories extension into
the tool executors; tests use `None`.

## Verification

- `just test -p codex-memories-extension`
This commit is contained in:
jif-oai authored and GitHub committed 2026-05-26 15:27:51 +02:00
1 parent b77be36896
commit 01a8bf0ae3
9 files changed
+154 -22

No files matched your search

+12 -2
View File
@@ -3,6 +3,7 @@ use codex_extension_api::ToolCall;
use codex_extension_api::ToolExecutor;
use codex_extension_api::ToolName;
use codex_extension_api::ToolSpec;
use codex_otel::MetricsClient;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
@@ -11,6 +12,7 @@ use crate::ADD_AD_HOC_NOTE_TOOL_NAME;
use crate::backend::AddAdHocMemoryNoteRequest;
use crate::backend::AddAdHocMemoryNoteResponse;
use crate::backend::MemoriesBackend;
use crate::metrics::record_tool_call;
use super::backend_error_to_function_call;
use super::memory_function_tool;
@@ -36,6 +38,7 @@ struct AddAdHocNoteArgs {
#[derive(Clone)]
pub(super) struct AddAdHocNoteTool<B> {
pub(super) backend: B,
pub(super) metrics_client: Option<MetricsClient>,
}
#[async_trait::async_trait]
@@ -66,8 +69,15 @@ where
filename: args.filename,
note: args.note,
})
.await
.map_err(backend_error_to_function_call)?;
.await;
record_tool_call(
self.metrics_client.as_ref(),
ADD_AD_HOC_NOTE_TOOL_NAME,
"ad_hoc_notes",
response.is_ok(),
"not_applicable",
);
let response = response.map_err(backend_error_to_function_call)?;
Ok(Box::new(JsonToolOutput::new(json!(response))))
}
}
+15 -2
View File
@@ -3,6 +3,7 @@ use codex_extension_api::ToolCall;
use codex_extension_api::ToolExecutor;
use codex_extension_api::ToolName;
use codex_extension_api::ToolSpec;
use codex_otel::MetricsClient;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
@@ -13,6 +14,9 @@ use crate::MAX_LIST_RESULTS;
use crate::backend::ListMemoriesRequest;
use crate::backend::ListMemoriesResponse;
use crate::backend::MemoriesBackend;
use crate::metrics::record_tool_call;
use crate::metrics::scope_from_optional_path;
use crate::metrics::truncated_tag;
use super::backend_error_to_function_call;
use super::clamp_max_results;
@@ -32,6 +36,7 @@ struct ListArgs {
#[derive(Clone)]
pub(super) struct ListTool<B> {
pub(super) backend: B,
pub(super) metrics_client: Option<MetricsClient>,
}
#[async_trait::async_trait]
@@ -57,6 +62,7 @@ where
{
let backend = self.backend.clone();
let args: ListArgs = parse_args(&call)?;
let scope = scope_from_optional_path(args.path.as_deref(), "root");
let response = backend
.list(ListMemoriesRequest {
path: args.path,
@@ -67,8 +73,15 @@ where
MAX_LIST_RESULTS,
),
})
.await
.map_err(backend_error_to_function_call)?;
.await;
record_tool_call(
self.metrics_client.as_ref(),
LIST_TOOL_NAME,
scope,
response.is_ok(),
truncated_tag(response.as_ref().ok().map(|response| response.truncated)),
);
let response = response.map_err(backend_error_to_function_call)?;
Ok(Box::new(JsonToolOutput::new(json!(response))))
}
}
+12 -2
View File
@@ -7,6 +7,7 @@ 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;
@@ -24,21 +25,30 @@ mod list;
mod read;
mod search;
pub(crate) fn memory_tools<B>(backend: B) -> Vec<Arc<dyn ToolExecutor<ToolCall>>>
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,
}),
Arc::new(search::SearchTool { backend }),
]
}
+17 -3
View File
@@ -3,6 +3,7 @@ use codex_extension_api::ToolCall;
use codex_extension_api::ToolExecutor;
use codex_extension_api::ToolName;
use codex_extension_api::ToolSpec;
use codex_otel::MetricsClient;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
@@ -12,6 +13,9 @@ use crate::READ_TOOL_NAME;
use crate::backend::MemoriesBackend;
use crate::backend::ReadMemoryRequest;
use crate::backend::ReadMemoryResponse;
use crate::metrics::record_tool_call;
use crate::metrics::scope_from_path;
use crate::metrics::truncated_tag;
use super::backend_error_to_function_call;
use super::memory_function_tool;
@@ -31,6 +35,7 @@ struct ReadArgs {
#[derive(Clone)]
pub(super) struct ReadTool<B> {
pub(super) backend: B,
pub(super) metrics_client: Option<MetricsClient>,
}
#[async_trait::async_trait]
@@ -56,15 +61,24 @@ where
{
let backend = self.backend.clone();
let args: ReadArgs = parse_args(&call)?;
let path = args.path;
let scope = scope_from_path(path.as_str());
let response = backend
.read(ReadMemoryRequest {
path: args.path,
path: path.clone(),
line_offset: args.line_offset.unwrap_or(1),
max_lines: args.max_lines,
max_tokens: DEFAULT_READ_MAX_TOKENS,
})
.await
.map_err(backend_error_to_function_call)?;
.await;
record_tool_call(
self.metrics_client.as_ref(),
READ_TOOL_NAME,
scope,
response.is_ok(),
truncated_tag(response.as_ref().ok().map(|response| response.truncated)),
);
let response = response.map_err(backend_error_to_function_call)?;
Ok(Box::new(JsonToolOutput::new(json!(response))))
}
}
+15 -4
View File
@@ -3,6 +3,7 @@ use codex_extension_api::ToolCall;
use codex_extension_api::ToolExecutor;
use codex_extension_api::ToolName;
use codex_extension_api::ToolSpec;
use codex_otel::MetricsClient;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::json;
@@ -14,6 +15,9 @@ use crate::backend::MemoriesBackend;
use crate::backend::SearchMatchMode;
use crate::backend::SearchMemoriesRequest;
use crate::backend::SearchMemoriesResponse;
use crate::metrics::record_tool_call;
use crate::metrics::scope_from_optional_path;
use crate::metrics::truncated_tag;
use super::backend_error_to_function_call;
use super::clamp_max_results;
@@ -40,6 +44,7 @@ struct SearchArgs {
#[derive(Clone)]
pub(super) struct SearchTool<B> {
pub(super) backend: B,
pub(super) metrics_client: Option<MetricsClient>,
}
#[async_trait::async_trait]
@@ -65,10 +70,16 @@ where
{
let backend = self.backend.clone();
let args: SearchArgs = parse_args(&call)?;
let response = backend
.search(args.into_request())
.await
.map_err(backend_error_to_function_call)?;
let scope = scope_from_optional_path(args.path.as_deref(), "all");
let response = backend.search(args.into_request()).await;
record_tool_call(
self.metrics_client.as_ref(),
SEARCH_TOOL_NAME,
scope,
response.is_ok(),
truncated_tag(response.as_ref().ok().map(|response| response.truncated)),
);
let response = response.map_err(backend_error_to_function_call)?;
Ok(Box::new(JsonToolOutput::new(json!(response))))
}
}