//! 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, }, 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, #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub keep: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub text: Option, #[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 { // 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 { // 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, }