feat: add memory usage event metrics

This commit is contained in:
2026-05-11 21:29:48 +09:00
parent 1d8db0aadd
commit 6c93ec38df
10 changed files with 701 additions and 93 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ use crate::workspace::{RecordKind, WorkspaceLayout};
pub use edit::edit_tool;
pub use query::{QueryConfig, knowledge_query_tool, memory_query_tool};
pub use read::read_tool;
pub use read::{read_tool, read_tool_with_usage};
pub use write::write_tool;
/// Kinds the memory tools accept as input. `Workflow` is intentionally
+23
View File
@@ -568,6 +568,29 @@ mod tests {
assert!(records.is_empty(), "got records: {:?}", out.content);
}
#[tokio::test]
async fn query_hits_do_not_log_usage() {
let (dir, layout) = setup();
write_decision(dir.path(), "alpha", "needle line\n");
write_knowledge(
dir.path(),
"policy",
"policy",
"needle desc",
"needle body\n",
);
let (_, memory_tool) = memory_query_tool(layout.clone(), QueryConfig::default())();
let (_, knowledge_tool) = knowledge_query_tool(layout.clone(), QueryConfig::default())();
let inp = serde_json::json!({ "query": "needle" });
memory_tool.execute(&inp.to_string()).await.unwrap();
knowledge_tool.execute(&inp.to_string()).await.unwrap();
let report = crate::usage::build_usage_report(&layout).unwrap();
assert!(report.records.is_empty());
assert!(!layout.usage_events_path().exists());
}
#[tokio::test]
async fn memory_query_respects_result_limit() {
let (dir, layout) = setup();
+51
View File
@@ -12,6 +12,7 @@ use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use serde::Deserialize;
use crate::tool::MemoryToolKind;
use crate::usage::{self, UsageSource};
use crate::workspace::WorkspaceLayout;
const DESCRIPTION: &str = "Read a memory or knowledge record by `kind` + `slug`. \
@@ -38,6 +39,7 @@ struct ReadParams {
struct ReadTool {
layout: WorkspaceLayout,
usage_session_id: Option<String>,
}
#[async_trait]
@@ -58,6 +60,22 @@ impl Tool for ReadTool {
})?;
let text = String::from_utf8_lossy(&bytes).into_owned();
if let Some(session_id) = self.usage_session_id.as_deref() {
let usage_slug = params.slug.as_deref().unwrap_or("summary");
let snapshot = usage::snapshot_record_from_bytes(
params.kind.record_kind(),
usage_slug.to_string(),
&bytes,
);
if let Err(err) = usage::append_use_event(
&self.layout,
session_id.to_string(),
UsageSource::MemoryRead,
vec![snapshot],
) {
tracing::warn!(error = %err, "failed to append MemoryRead usage event");
}
}
let offset = params.offset.unwrap_or(0);
let limit = params.limit.unwrap_or(DEFAULT_LIMIT).max(1);
let rendered = render_numbered(&text, offset, limit);
@@ -117,6 +135,17 @@ fn render_numbered(text: &str, offset: usize, limit: usize) -> Rendered {
}
pub fn read_tool(layout: WorkspaceLayout) -> ToolDefinition {
read_tool_inner(layout, None)
}
pub fn read_tool_with_usage(
layout: WorkspaceLayout,
session_id: impl Into<String>,
) -> ToolDefinition {
read_tool_inner(layout, Some(session_id.into()))
}
fn read_tool_inner(layout: WorkspaceLayout, usage_session_id: Option<String>) -> ToolDefinition {
Arc::new(move || {
let schema = schemars::schema_for!(ReadParams);
let schema_value = serde_json::to_value(schema).unwrap_or(serde_json::json!({}));
@@ -125,6 +154,7 @@ pub fn read_tool(layout: WorkspaceLayout) -> ToolDefinition {
.input_schema(schema_value);
let tool: Arc<dyn Tool> = Arc::new(ReadTool {
layout: layout.clone(),
usage_session_id: usage_session_id.clone(),
});
(meta, tool)
})
@@ -209,6 +239,27 @@ mod tests {
assert!(out.content.unwrap().contains("k"));
}
#[tokio::test]
async fn read_logs_explicit_use_when_usage_session_is_set() {
let (dir, layout) = setup();
let path = dir.path().join(".insomnia/memory/decisions/foo.md");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "alpha\n").unwrap();
let (_, tool) = read_tool_with_usage(layout.clone(), "session-1")();
let inp = serde_json::json!({ "kind": "decision", "slug": "foo" });
tool.execute(&inp.to_string()).await.unwrap();
let report = usage::build_usage_report(&layout).unwrap();
assert_eq!(report.records.len(), 1);
let record = &report.records[0];
assert_eq!(record.kind, "decision");
assert_eq!(record.slug, "foo");
assert_eq!(record.use_count, 1);
assert_eq!(record.source_breakdown["MemoryRead"], 1);
assert_eq!(record.resident_exposure_count, 0);
}
#[tokio::test]
async fn missing_file_returns_execution_failed() {
let (_dir, layout) = setup();