178 lines
6.6 KiB
Rust
178 lines
6.6 KiB
Rust
//! Memory, as `loopd` sees it (M5a spec, section 9): the block `memoryd` renders for a new
|
|
//! session's baseline, the schemas of the two core memory tools, and the notice after a turn.
|
|
//! `loopd` keeps no memory itself; `brokerd` decides the tool calls and `memoryd` runs them.
|
|
|
|
use std::os::unix::net::UnixStream;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
|
|
use bxcore::{
|
|
Empty, Envelope, Message, PROTOCOL_VERSION, SessionId, SessionUpdated, read_frame, write_frame,
|
|
};
|
|
|
|
use crate::llama::ToolSchema;
|
|
|
|
/// Where a new session's baseline points when there is no memory block.
|
|
pub const UNAVAILABLE: &str = "see docs/runbook.md#memory-unavailable";
|
|
|
|
/// How long `loopd` waits for `memoryd`.
|
|
pub const MEMORY_WAIT: Duration = Duration::from_secs(10);
|
|
|
|
/// Where the memory block comes from, and who hears that a session has new records.
|
|
pub trait MemorySource: Send + Sync {
|
|
/// The block for a new session. `Err` ends in `UNAVAILABLE`: no session starts without it.
|
|
fn block(&self) -> Result<String, String>;
|
|
/// The session's turn number `turn` has ended, and its records are in the log; best effort,
|
|
/// a failure is only logged by the caller.
|
|
fn updated(&self, session: &SessionId, turn: u64) -> Result<(), String>;
|
|
}
|
|
|
|
/// No memory: for tests and the self-test, never for `loopd serve`.
|
|
pub struct NoMemory;
|
|
|
|
impl MemorySource for NoMemory {
|
|
fn block(&self) -> Result<String, String> {
|
|
Ok(String::new())
|
|
}
|
|
fn updated(&self, _session: &SessionId, _turn: u64) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A client of `memoryd`'s `loopd` socket. One connection per request.
|
|
pub struct MemoryPort {
|
|
socket: PathBuf,
|
|
wait: Duration,
|
|
}
|
|
|
|
impl MemoryPort {
|
|
pub fn new(socket: PathBuf, wait: Duration) -> MemoryPort {
|
|
MemoryPort { socket, wait }
|
|
}
|
|
|
|
fn ask(&self, msg: Message) -> Result<Message, String> {
|
|
let mut stream = match UnixStream::connect(&self.socket) {
|
|
Ok(stream) => stream,
|
|
Err(e) => return Err(self.line(&e)),
|
|
};
|
|
if let Err(e) = stream.set_write_timeout(Some(self.wait)) {
|
|
return Err(self.line(&e));
|
|
}
|
|
if let Err(e) = stream.set_read_timeout(Some(self.wait)) {
|
|
return Err(self.line(&e));
|
|
}
|
|
let env = Envelope {
|
|
v: PROTOCOL_VERSION,
|
|
id: 1,
|
|
r#final: true,
|
|
msg,
|
|
};
|
|
if let Err(e) = write_frame(&mut stream, &env) {
|
|
return Err(self.line(&e));
|
|
}
|
|
match read_frame(&mut stream) {
|
|
Ok(env) => Ok(env.msg),
|
|
Err(e) => Err(self.line(&e)),
|
|
}
|
|
}
|
|
|
|
fn line(&self, e: &dyn std::fmt::Display) -> String {
|
|
format!("memoryd at {}: {e}; {UNAVAILABLE}", self.socket.display())
|
|
}
|
|
}
|
|
|
|
impl MemorySource for MemoryPort {
|
|
fn block(&self) -> Result<String, String> {
|
|
match self.ask(Message::MemoryBlock(Empty {}))? {
|
|
Message::MemoryBlockReply(reply) => Ok(reply.text),
|
|
Message::Error(e) => Err(format!("memoryd: {}; {UNAVAILABLE}", e.detail)),
|
|
_ => Err(format!(
|
|
"memoryd answered with the wrong message; {UNAVAILABLE}"
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn updated(&self, session: &SessionId, turn: u64) -> Result<(), String> {
|
|
match self.ask(Message::SessionUpdated(SessionUpdated {
|
|
session: session.clone(),
|
|
turn,
|
|
}))? {
|
|
Message::Ok(_) => Ok(()),
|
|
Message::Error(e) => Err(format!("memoryd: {}", e.detail)),
|
|
_ => Err("memoryd answered with the wrong message".to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const RECALL: &str = "recall";
|
|
pub const MEMORY: &str = "memory";
|
|
pub const ASK_MEMORY: &str = bxcore::memory::ASK_MEMORY;
|
|
|
|
pub fn recall_schema() -> ToolSchema {
|
|
ToolSchema {
|
|
name: RECALL.to_string(),
|
|
description: "Search your memory: past conversations and notes. Each hit says where it \
|
|
came from: identity, memory, or memory-tainted (from a session that read \
|
|
untrusted content)."
|
|
.to_string(),
|
|
parameters: serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"query": { "type": "string", "description": "Words to search for." },
|
|
"k": { "type": "integer", "description": "How many hits, 1 to 10. Default 5." }
|
|
},
|
|
"required": ["query"]
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// `ask_memory` (M5c spec, section 9), as `bxcore::AskMemoryArgs` reads it: `question` required,
|
|
/// `workspaces` at most two names. Short: every schema counts toward the baseline's budget.
|
|
pub fn ask_memory_schema() -> ToolSchema {
|
|
ToolSchema {
|
|
name: ASK_MEMORY.to_string(),
|
|
description: "Ask Honcho what you and the other agents of the fleet know. Answers can \
|
|
take a minute or more."
|
|
.to_string(),
|
|
parameters: serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"question": { "type": "string" },
|
|
"workspaces": {
|
|
"type": "array",
|
|
"items": { "type": "string" },
|
|
"maxItems": 2,
|
|
"description": "own (the default), untrusted, fleet, or another agent's name."
|
|
}
|
|
},
|
|
"required": ["question"]
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn memory_schema() -> ToolSchema {
|
|
ToolSchema {
|
|
name: MEMORY.to_string(),
|
|
description: "Write memory. note: record something seen or told. add, replace, remove: \
|
|
edit memory.md (your work) or user.md (your owner); entries are named by \
|
|
their exact text. consolidate: rewrite a whole file, moving or dropping \
|
|
entries to notes. Takes effect in the next session."
|
|
.to_string(),
|
|
parameters: serde_json::json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"action": { "type": "string", "enum": ["note", "add", "replace", "remove", "consolidate"] },
|
|
"file": { "type": "string", "enum": ["memory", "user"] },
|
|
"text": { "type": "string", "description": "The new entry, or the note." },
|
|
"old": { "type": "string", "description": "The exact text of the entry to replace or remove." },
|
|
"entries": {
|
|
"type": "array",
|
|
"description": "consolidate: every current entry once, as {old, keep: true}, {old, text}, {old, move: true} or {old, drop: true}; new entries as {text}.",
|
|
"items": { "type": "object" }
|
|
}
|
|
},
|
|
"required": ["action"]
|
|
}),
|
|
}
|
|
}
|