use polyc_llm::ToolSpec;
use serde_json::json;
pub const MEMORY_WRITE: &str = "memory_write";
pub const MEMORY_RECALL: &str = "memory_recall";
pub const ALL: &[&str] = &[MEMORY_WRITE, MEMORY_RECALL];
#[must_use]
pub fn all_specs() -> Vec<ToolSpec> {
vec![write_spec(), recall_spec()]
}
#[must_use]
pub fn write_spec() -> ToolSpec {
ToolSpec::new(
MEMORY_WRITE,
"Save one durable note about the person you're talking to, so you \
remember it in later conversations — a preference, a project, a \
standing constraint. Use it when they share something worth keeping \
or ask you to remember something. Keep the note to one \
self-contained sentence. Never save addresses, phone or account \
numbers, government ids, passwords or keys, or health details — \
notes carrying those are refused. Pick who the note is for: \
\"private\" is remembered only in one-on-one conversations with this \
person (and can only be saved from one), \"portable\" travels with \
them everywhere you talk, where other people may hear it too.",
json!({
"type": "object",
"properties": {
"note": {
"type": "string",
"description": "The note to keep — one self-contained sentence."
},
"audience": {
"type": "string",
"enum": ["private", "portable"],
"description": "\"private\" recalls only in one-on-one \
conversations with this person; \"portable\" recalls \
anywhere they talk to you, including rooms other \
people read."
},
"entities": {
"type": "array",
"items": { "type": "string" },
"description": "Names or topics the note mentions, to help recall it later."
}
},
"required": ["note", "audience"],
"additionalProperties": false
}),
)
.titled("Save a note to memory")
.approval_required()
}
#[must_use]
pub fn recall_spec() -> ToolSpec {
ToolSpec::new(
MEMORY_RECALL,
"Read back the notes you have kept about the person you're talking to \
— preferences, projects, standing constraints — most recent first, \
along with short summaries of your earlier conversations with them. \
Use it when you need to check what you already know before asking \
them again, or when they refer to something settled in a past \
conversation. Notes come back by recency, not by topic: there is \
nothing to search on, so ask for a few more and read them. `limit` is \
trimmed to 25, and leaving it out returns 12. Having kept no notes \
yet is an ordinary empty result, not an error. This reads your notes \
about this person and nobody else's, and it saves nothing — to keep \
something new, use memory_write. For what was said inside THIS \
conversation, use conversation_find instead.",
json!({
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Most notes to return, most recent first (default 12, at most 25).",
"minimum": 1
},
"detail": {
"type": "string",
"enum": ["concise", "full"],
"description": "\"concise\" (the default) returns just the note text and when it was learned. \"full\" adds the topics each note mentions, how confident it is, and whether it is a standing fact or one tied to a finished activity."
}
},
"additionalProperties": false
}),
)
.titled("Recall what you know about this person")
.read_only()
.cacheable_approval()
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
#[test]
fn write_spec_is_gated_and_never_cacheable() {
let spec = write_spec();
assert!(
spec.needs_approval,
"memory_write must carry the intrinsic approval gate (INV-C9)"
);
assert!(
!spec.cacheable_approval,
"approving one note must never blanket-approve every future note"
);
assert!(!spec.read_only, "a durable write is not a read");
}
#[test]
fn all_specs_carries_exactly_the_two_memory_tools() {
let names: Vec<String> = all_specs().into_iter().map(|s| s.name).collect();
assert_eq!(
names,
vec![MEMORY_WRITE.to_owned(), MEMORY_RECALL.to_owned()]
);
assert_eq!(ALL, &[MEMORY_WRITE, MEMORY_RECALL]);
}
#[test]
fn recall_is_an_ungated_cacheable_read() {
let spec = recall_spec();
assert!(spec.read_only, "recall persists nothing");
assert!(!spec.destructive);
assert!(
!spec.needs_approval,
"the grant is the control surface for recall, not a per-call pause"
);
assert!(spec.cacheable_approval);
assert!(!spec.open_world, "a persona's own notes are first-party");
assert!(spec.title.is_some());
}
#[test]
fn recall_takes_no_query_argument() {
let schema = recall_spec().schema_json;
assert_eq!(schema["additionalProperties"], json!(false));
let properties = schema["properties"]
.as_object()
.expect("recall's schema has properties");
let mut keys: Vec<&String> = properties.keys().collect();
keys.sort();
assert_eq!(
keys,
vec!["detail", "limit"],
"recall accepts a bound and a width, and nothing that filters"
);
assert!(schema.get("required").is_none(), "every argument optional");
}
}