memoryd (files, the check, edits, journal, index, recall, serving), the memory tools and built-in grants in brokerd, the memory block in loopd, bxctl reindex and memory, and the device checks with memoryd. Each task was built as a reference and gated. Each was replayed on its own at its end state (854 to 936 tests) and mutation-checked. Each skeleton compiles and fails its tests. Every todo comment was read beside its signature. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
225 lines
7.5 KiB
Rust
225 lines
7.5 KiB
Rust
//! Memory: the arguments of `recall` and `memory`, their shape checks, and the messages `memoryd`
|
|
//! exchanges with `loopd`, `brokerd` and `bxctl` (M5a spec, sections 5 and 7). Content rules (an
|
|
//! entry matching the file, a file's limit, the check on characters) are `memoryd`'s.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::class::DataClass;
|
|
use crate::ids::SessionId;
|
|
use crate::wire::ToolRequest;
|
|
|
|
/// The tool names.
|
|
pub const RECALL: &str = "recall";
|
|
pub const MEMORY: &str = "memory";
|
|
|
|
/// The most characters a `recall` query may have.
|
|
pub const MAX_QUERY: usize = 500;
|
|
/// The most hits a `recall` may ask for, and how many it gets when it does not say.
|
|
pub const MAX_K: u64 = 10;
|
|
pub const DEFAULT_K: u64 = 5;
|
|
/// The most characters a `note` may have.
|
|
pub const MAX_NOTE: usize = 1_000;
|
|
/// The most characters of any other entry: the larger file's limit.
|
|
pub const MAX_ENTRY: usize = 2_200;
|
|
/// The line that separates entries in `memory.md`, `user.md` and `notes.md`.
|
|
pub const SEPARATOR: &str = "§";
|
|
|
|
/// The arguments for `recall`.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct RecallArgs {
|
|
pub query: String,
|
|
#[serde(default = "default_k")]
|
|
pub k: u64,
|
|
}
|
|
|
|
fn default_k() -> u64 {
|
|
DEFAULT_K
|
|
}
|
|
|
|
/// Which identity file an edit is for.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum MemoryFile {
|
|
Memory,
|
|
User,
|
|
}
|
|
|
|
impl MemoryFile {
|
|
/// The file's name under `memory/`.
|
|
pub fn name(self) -> &'static str {
|
|
match self {
|
|
MemoryFile::Memory => "memory.md",
|
|
MemoryFile::User => "user.md",
|
|
}
|
|
}
|
|
|
|
/// The file's limit in characters.
|
|
pub fn limit(self) -> usize {
|
|
match self {
|
|
MemoryFile::Memory => 2_200,
|
|
MemoryFile::User => 1_375,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The arguments for `memory`: one action. `note` records experience; the others edit identity.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
|
|
pub enum MemoryArgs {
|
|
Add {
|
|
file: MemoryFile,
|
|
text: String,
|
|
},
|
|
Replace {
|
|
file: MemoryFile,
|
|
old: String,
|
|
text: String,
|
|
},
|
|
Remove {
|
|
file: MemoryFile,
|
|
old: String,
|
|
},
|
|
Consolidate {
|
|
file: MemoryFile,
|
|
entries: Vec<ConsolidateEntry>,
|
|
},
|
|
Note {
|
|
text: String,
|
|
},
|
|
}
|
|
|
|
/// One line of a `consolidate`: an existing entry (`old`) with exactly one of `keep`, `text`,
|
|
/// `move`, `drop`; or a new entry (`text` alone).
|
|
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ConsolidateEntry {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub old: Option<String>,
|
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
pub keep: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub text: Option<String>,
|
|
#[serde(default, rename = "move", skip_serializing_if = "std::ops::Not::not")]
|
|
pub moved: bool,
|
|
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
|
pub drop: bool,
|
|
}
|
|
|
|
/// What a `consolidate` line does.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum EntryAction {
|
|
Keep(String),
|
|
Rewrite { old: String, text: String },
|
|
Move(String),
|
|
Drop(String),
|
|
New(String),
|
|
}
|
|
|
|
impl ConsolidateEntry {
|
|
/// The line's action, or why it has none.
|
|
pub fn action(&self) -> Result<EntryAction, &'static str> {
|
|
// Count the flags: keep, moved and drop as 0 or 1 each (usize::from). Then match (old,
|
|
// text, flags): (None, Some(text), 0) -> New(text); (None, _, _) -> Err("an entry without
|
|
// old must have text and nothing else"); (Some(old), Some(text), 0) -> Rewrite { old, text
|
|
// }; (Some(old), None, 1) -> Keep if keep, else Move if moved, else Drop; (Some(_), _, _)
|
|
// -> Err("an entry with old must have exactly one of keep, text, move or drop"). Clone the
|
|
// strings.
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
/// Why an entry's text is not a well-formed entry: empty once trimmed, too long, or holding a line
|
|
/// that is only the separator. `None` when it is fine.
|
|
pub fn entry_problem(text: &str, max: usize) -> Option<String> {
|
|
// In this order: text.trim() empty -> Some("an entry cannot be empty"); more than `max`
|
|
// characters (chars().count(), never len()) -> Some(format!("an entry has at most {max}
|
|
// characters; this has {chars}")); any line of `text.lines()` whose trim() equals SEPARATOR ->
|
|
// Some(format!("an entry cannot hold a line that is only {SEPARATOR}")). Otherwise None.
|
|
todo!()
|
|
}
|
|
|
|
impl RecallArgs {
|
|
/// The shape rules of `recall` (M5a spec, section 5).
|
|
pub fn check(&self) -> Result<(), String> {
|
|
// query.trim() empty, or query.chars().count() > MAX_QUERY -> Err(format!("query must have
|
|
// 1 to {MAX_QUERY} characters")). k == 0 or k > MAX_K -> Err(format!("k must be 1 to
|
|
// {MAX_K}")). Otherwise Ok(()).
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
impl MemoryArgs {
|
|
/// The shape rules of `memory` (M5a spec, section 5): every text is a well-formed entry, and
|
|
/// every `consolidate` line has one action and the list is not empty.
|
|
pub fn check(&self) -> Result<(), String> {
|
|
// Every text goes through `entry_problem` (Some(problem) -> Err(problem)): Add: text,
|
|
// MAX_ENTRY. Replace: old then text, MAX_ENTRY. Remove: old, MAX_ENTRY. Note: text,
|
|
// MAX_NOTE. Consolidate: an empty `entries` -> Err("consolidate needs every entry of the
|
|
// file"); then for each line, `line.action()` (its Err as a String), and check the strings
|
|
// of the action with MAX_ENTRY: Keep, Move and Drop their old; Rewrite its old then its
|
|
// text; New its text. Ok(()) when all pass.
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
/// `memoryd` → `loopd`: the memory block for a new session's baseline.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MemoryBlockReply {
|
|
pub text: String,
|
|
}
|
|
|
|
/// `loopd` → `memoryd`: this session's log has new records.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct SessionUpdated {
|
|
pub session: SessionId,
|
|
}
|
|
|
|
/// `brokerd` → `memoryd`: a decided `recall` or `memory` call, with the session's state when it
|
|
/// was decided.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MemoryCall {
|
|
pub request: ToolRequest,
|
|
pub taint: DataClass,
|
|
pub untrusted: bool,
|
|
}
|
|
|
|
/// `memoryd` → `bxctl`: what a reindex read.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct Reindexed {
|
|
pub logs: u64,
|
|
pub records: u64,
|
|
pub notes: u64,
|
|
}
|
|
|
|
/// The owner's memory commands (M5a spec, section 10).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum MemoryCommand {
|
|
Log,
|
|
Undo,
|
|
Pending,
|
|
Accept,
|
|
Reject,
|
|
}
|
|
|
|
/// `bxctl` → `memoryd`: one command; `arg` is the count for `log`, the edit or pending id for
|
|
/// `undo`, `accept` and `reject`, and empty otherwise.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MemoryAdmin {
|
|
pub command: MemoryCommand,
|
|
pub arg: String,
|
|
}
|
|
|
|
/// `memoryd` → `bxctl`: the text to print.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct MemoryAdminReply {
|
|
pub text: String,
|
|
}
|