proto becomes bxcore, the one shared crate (M5b task 00)
Done the way the owner asked and the spike mapped: the crate and the manifests renamed, then what the build reported fixed. Layers: 138 "proto unresolved" errors across 169 files; cargo fmt reordering imports; six fixture paths built as strings in brokerd's and bxctl's tests, which the compiler cannot see, and comments naming crates/proto/; the crate-dependency check and its self-test. One more the spike did not reach: a `\nproto =` key inside a printf format in the self-test, which a word-boundary replacement skips because `n` and `p` are both word characters. make gate: gate: ok, 944 tests; no new entry in /tmp. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
//! 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> {
|
||||
let flags = usize::from(self.keep) + usize::from(self.moved) + usize::from(self.drop);
|
||||
match (&self.old, &self.text, flags) {
|
||||
(None, Some(text), 0) => Ok(EntryAction::New(text.clone())),
|
||||
(None, _, _) => Err("an entry without old must have text and nothing else"),
|
||||
(Some(old), Some(text), 0) => Ok(EntryAction::Rewrite {
|
||||
old: old.clone(),
|
||||
text: text.clone(),
|
||||
}),
|
||||
(Some(old), None, 1) => {
|
||||
if self.keep {
|
||||
Ok(EntryAction::Keep(old.clone()))
|
||||
} else if self.moved {
|
||||
Ok(EntryAction::Move(old.clone()))
|
||||
} else {
|
||||
Ok(EntryAction::Drop(old.clone()))
|
||||
}
|
||||
}
|
||||
(Some(_), _, _) => {
|
||||
Err("an entry with old must have exactly one of keep, text, move or drop")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Why an entry's text is not a well-formed entry: empty once trimmed, too long, or holding a line
|
||||
/// that starts with the separator, spaces aside. In `notes.md` such a line would read as a header,
|
||||
/// and a header says where an entry came from. `None` when it is fine.
|
||||
pub fn entry_problem(text: &str, max: usize) -> Option<String> {
|
||||
if text.trim().is_empty() {
|
||||
return Some("an entry cannot be empty".to_string());
|
||||
}
|
||||
let chars = text.chars().count();
|
||||
if chars > max {
|
||||
return Some(format!(
|
||||
"an entry has at most {max} characters; this has {chars}"
|
||||
));
|
||||
}
|
||||
for line in text.lines() {
|
||||
if line.trim_start().starts_with(SEPARATOR) {
|
||||
return Some(format!(
|
||||
"an entry cannot hold a line that starts with {SEPARATOR}"
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// `entry_problem` as a `Result`, so a check can use `?`.
|
||||
fn entry(text: &str, max: usize) -> Result<(), String> {
|
||||
match entry_problem(text, max) {
|
||||
Some(problem) => Err(problem),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
impl RecallArgs {
|
||||
/// The shape rules of `recall` (M5a spec, section 5).
|
||||
pub fn check(&self) -> Result<(), String> {
|
||||
if self.query.trim().is_empty() || self.query.chars().count() > MAX_QUERY {
|
||||
return Err(format!("query must have 1 to {MAX_QUERY} characters"));
|
||||
}
|
||||
if self.k == 0 || self.k > MAX_K {
|
||||
return Err(format!("k must be 1 to {MAX_K}"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
match self {
|
||||
MemoryArgs::Add { text, .. } => entry(text, MAX_ENTRY),
|
||||
MemoryArgs::Replace { old, text, .. } => {
|
||||
entry(old, MAX_ENTRY)?;
|
||||
entry(text, MAX_ENTRY)
|
||||
}
|
||||
MemoryArgs::Remove { old, .. } => entry(old, MAX_ENTRY),
|
||||
MemoryArgs::Note { text } => entry(text, MAX_NOTE),
|
||||
MemoryArgs::Consolidate { entries, .. } => {
|
||||
if entries.is_empty() {
|
||||
return Err("consolidate needs every entry of the file".to_string());
|
||||
}
|
||||
for line in entries {
|
||||
match line.action().map_err(str::to_string)? {
|
||||
EntryAction::Keep(old)
|
||||
| EntryAction::Move(old)
|
||||
| EntryAction::Drop(old) => entry(&old, MAX_ENTRY)?,
|
||||
EntryAction::Rewrite { old, text } => {
|
||||
entry(&old, MAX_ENTRY)?;
|
||||
entry(&text, MAX_ENTRY)?;
|
||||
}
|
||||
EntryAction::New(text) => entry(&text, MAX_ENTRY)?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `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,
|
||||
}
|
||||
Reference in New Issue
Block a user