memoryd: index ToolSummary as kind summary
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! The index: SQLite FTS5 over the session logs and `notes.md` (M5a spec, section 6). Disposable:
|
||||
//! it is rebuilt from those files. What is indexed: the owner's messages, the assistant's
|
||||
//! replies, and each tool call by name and arguments; never a tool's result.
|
||||
//! replies, and each tool call by name and arguments; the summary of an opted-in tool result,
|
||||
//! but never the tool's result itself.
|
||||
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -257,6 +258,23 @@ impl Index {
|
||||
}
|
||||
}
|
||||
}
|
||||
LogRecord::ToolSummary {
|
||||
time,
|
||||
content,
|
||||
class,
|
||||
untrusted,
|
||||
..
|
||||
} if !content.trim().is_empty() && *class <= self.max_class => {
|
||||
let src = Source {
|
||||
kind: "summary".to_string(),
|
||||
time: time.to_rfc3339(),
|
||||
role: "summary".to_string(),
|
||||
class: *class,
|
||||
untrusted: *untrusted,
|
||||
..base.clone()
|
||||
};
|
||||
self.add(content, &src)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
//! Tests that `ToolSummary` is indexed as kind `summary` and found by `recall`. Do not edit.
|
||||
|
||||
#[path = "support/logs.rs"]
|
||||
mod logs;
|
||||
#[path = "support/tmp.rs"]
|
||||
mod tmp;
|
||||
|
||||
use bxcore::{CallId, DataClass, LogRecord, RecallArgs, Timestamp};
|
||||
use logs::{assistant, result, user, write_log};
|
||||
use memoryd::files::Store;
|
||||
use memoryd::index::Index;
|
||||
use memoryd::recall::recall;
|
||||
use tmp::TempDir;
|
||||
|
||||
fn now() -> Timestamp {
|
||||
Timestamp::parse("2026-09-24T21:00:00.000Z").unwrap()
|
||||
}
|
||||
|
||||
fn summary(minute: u32, content: &str, untrusted: bool) -> LogRecord {
|
||||
LogRecord::ToolSummary {
|
||||
time: logs::at(minute),
|
||||
call: CallId(1),
|
||||
tool_call_id: "c0".to_string(),
|
||||
content: content.to_string(),
|
||||
class: DataClass::Public,
|
||||
untrusted,
|
||||
}
|
||||
}
|
||||
|
||||
struct Setup {
|
||||
dir: TempDir,
|
||||
store: Store,
|
||||
index: Index,
|
||||
}
|
||||
|
||||
fn setup(tag: &str) -> Setup {
|
||||
let dir = TempDir::new(tag);
|
||||
let starter = dir.write("etc/soul.md", "Soul.\n");
|
||||
let store = Store::new(&dir.path().join("memory"));
|
||||
store.init(&starter).unwrap();
|
||||
let index = Index::open(&dir.path().join("index/memory.sqlite3"), DataClass::Private).unwrap();
|
||||
Setup { dir, store, index }
|
||||
}
|
||||
|
||||
fn ask(s: &Setup, query: &str) -> memoryd::recall::Recalled {
|
||||
let args = RecallArgs {
|
||||
query: query.to_string(),
|
||||
k: 10,
|
||||
};
|
||||
recall(&s.index, &s.store, &args, "mm-now", now()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tool_summary_is_found_as_kind_summary() {
|
||||
let s = setup("summary-found");
|
||||
write_log(
|
||||
s.dir.path(),
|
||||
"mm-a",
|
||||
&[
|
||||
user(2, "fetch the launch page"),
|
||||
assistant(
|
||||
3,
|
||||
None,
|
||||
&[("http_fetch", r#"{"url":"https://example.org"}"#)],
|
||||
),
|
||||
result(4, "raw html body about tea", DataClass::Public, false),
|
||||
summary(5, "the page is about tea and the launch window", false),
|
||||
],
|
||||
);
|
||||
s.index
|
||||
.catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md"))
|
||||
.unwrap();
|
||||
let got = ask(&s, "tea");
|
||||
assert!(
|
||||
got.text
|
||||
.contains("the page is about tea and the launch window"),
|
||||
"{got:?}"
|
||||
);
|
||||
assert!(
|
||||
got.text.contains("memory · session mm-a"),
|
||||
"tagged as a session hit: {}",
|
||||
got.text
|
||||
);
|
||||
assert!(!got.untrusted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_raw_tool_result_is_still_not_indexed() {
|
||||
let s = setup("summary-no-raw");
|
||||
write_log(
|
||||
s.dir.path(),
|
||||
"mm-a",
|
||||
&[
|
||||
user(2, "fetch"),
|
||||
assistant(3, None, &[("http_fetch", "{}")]),
|
||||
result(4, "UNIQUE_RAW_BODY_XYZ", DataClass::Public, false),
|
||||
summary(5, "a short account of the page", false),
|
||||
],
|
||||
);
|
||||
s.index
|
||||
.catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md"))
|
||||
.unwrap();
|
||||
let got = ask(&s, "UNIQUE_RAW_BODY_XYZ");
|
||||
assert!(
|
||||
got.text.contains("(no matches"),
|
||||
"raw tool result stays unindexed: {}",
|
||||
got.text
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_untrusted_summary_taints_the_recall() {
|
||||
let s = setup("summary-taint");
|
||||
write_log(
|
||||
s.dir.path(),
|
||||
"mm-a",
|
||||
&[
|
||||
user(2, "fetch"),
|
||||
assistant(3, None, &[("http_fetch", "{}")]),
|
||||
result(4, "page", DataClass::Public, true),
|
||||
summary(5, "the fetched page mentions the launch window", true),
|
||||
],
|
||||
);
|
||||
s.index
|
||||
.catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md"))
|
||||
.unwrap();
|
||||
let got = ask(&s, "launch");
|
||||
assert!(got.untrusted, "{got:?}");
|
||||
assert!(got.text.contains("memory-tainted"), "{}", got.text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_trusted_summary_does_not_taint() {
|
||||
let s = setup("summary-trusted");
|
||||
write_log(
|
||||
s.dir.path(),
|
||||
"mm-a",
|
||||
&[
|
||||
user(2, "fetch"),
|
||||
assistant(3, None, &[("http_fetch", "{}")]),
|
||||
result(4, "page", DataClass::Public, false),
|
||||
summary(5, "the fetched page mentions the launch window", false),
|
||||
],
|
||||
);
|
||||
s.index
|
||||
.catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md"))
|
||||
.unwrap();
|
||||
let got = ask(&s, "launch");
|
||||
assert!(!got.untrusted, "{got:?}");
|
||||
assert!(got.text.contains("memory · session mm-a"), "{}", got.text);
|
||||
assert!(!got.text.contains("memory-tainted"), "{}", got.text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_secret_summary_is_skipped_when_the_index_is_private() {
|
||||
let s = setup("summary-secret");
|
||||
write_log(
|
||||
s.dir.path(),
|
||||
"mm-a",
|
||||
&[
|
||||
user(2, "fetch"),
|
||||
assistant(3, None, &[("http_fetch", "{}")]),
|
||||
LogRecord::ToolResult {
|
||||
time: logs::at(4),
|
||||
call: CallId(1),
|
||||
tool_call_id: "c0".to_string(),
|
||||
content: "secret page".to_string(),
|
||||
class: DataClass::Secret,
|
||||
untrusted: false,
|
||||
truncated: false,
|
||||
},
|
||||
LogRecord::ToolSummary {
|
||||
time: logs::at(5),
|
||||
call: CallId(1),
|
||||
tool_call_id: "c0".to_string(),
|
||||
content: "SECRET_SUMMARY_PHRASE".to_string(),
|
||||
class: DataClass::Secret,
|
||||
untrusted: false,
|
||||
},
|
||||
],
|
||||
);
|
||||
s.index
|
||||
.catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md"))
|
||||
.unwrap();
|
||||
let got = ask(&s, "SECRET_SUMMARY_PHRASE");
|
||||
assert!(
|
||||
got.text.contains("(no matches"),
|
||||
"secret summary above max_class: {}",
|
||||
got.text
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user