From 9de5007b9b73d7aab1a761df6cd61c4427b4da53 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:37:04 +0300 Subject: [PATCH 01/35] chore(workspace): add tinymemory-bus crate to default members Include the newly created tinymemory-bus crate in the workspace's default member list so that it is built and tested automatically alongside the other core crates. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.toml | 1 + crates/tinymemory-bus/Cargo.toml | 54 +++ crates/tinymemory-bus/src/calls/chunks.rs | 124 +++++++ crates/tinymemory-bus/src/calls/core.rs | 134 +++++++ crates/tinymemory-bus/src/calls/documents.rs | 157 ++++++++ crates/tinymemory-bus/src/calls/driver.rs | 122 +++++++ crates/tinymemory-bus/src/calls/episodic.rs | 190 ++++++++++ crates/tinymemory-bus/src/calls/goals.rs | 45 +++ crates/tinymemory-bus/src/calls/graph.rs | 216 +++++++++++ crates/tinymemory-bus/src/calls/ingest.rs | 46 +++ .../tinymemory-bus/src/calls/maintenance.rs | 75 ++++ crates/tinymemory-bus/src/calls/mod.rs | 126 +++++++ crates/tinymemory-bus/src/calls/people.rs | 141 ++++++++ .../tinymemory-bus/src/calls/portability.rs | 55 +++ crates/tinymemory-bus/src/calls/profile.rs | 217 ++++++++++++ crates/tinymemory-bus/src/calls/recall.rs | 67 ++++ crates/tinymemory-bus/src/calls/retrieval.rs | 116 ++++++ crates/tinymemory-bus/src/calls/sources.rs | 110 ++++++ crates/tinymemory-bus/src/calls/test.rs | 183 ++++++++++ .../tinymemory-bus/src/calls/tool_memory.rs | 65 ++++ crates/tinymemory-bus/src/calls/tree.rs | 107 ++++++ crates/tinymemory-bus/src/error/mod.rs | 51 +++ crates/tinymemory-bus/src/error/test.rs | 33 ++ crates/tinymemory-bus/src/lib.rs | 87 +++++ crates/tinymemory-bus/src/names/mod.rs | 335 ++++++++++++++++++ crates/tinymemory-bus/src/names/test.rs | 61 ++++ crates/tinymemory-bus/src/types/mod.rs | 53 +++ crates/tinymemory-bus/src/wire/mod.rs | 41 +++ crates/tinymemory-bus/src/wire/test.rs | 52 +++ 29 files changed, 3064 insertions(+) create mode 100644 crates/tinymemory-bus/Cargo.toml create mode 100644 crates/tinymemory-bus/src/calls/chunks.rs create mode 100644 crates/tinymemory-bus/src/calls/core.rs create mode 100644 crates/tinymemory-bus/src/calls/documents.rs create mode 100644 crates/tinymemory-bus/src/calls/driver.rs create mode 100644 crates/tinymemory-bus/src/calls/episodic.rs create mode 100644 crates/tinymemory-bus/src/calls/goals.rs create mode 100644 crates/tinymemory-bus/src/calls/graph.rs create mode 100644 crates/tinymemory-bus/src/calls/ingest.rs create mode 100644 crates/tinymemory-bus/src/calls/maintenance.rs create mode 100644 crates/tinymemory-bus/src/calls/mod.rs create mode 100644 crates/tinymemory-bus/src/calls/people.rs create mode 100644 crates/tinymemory-bus/src/calls/portability.rs create mode 100644 crates/tinymemory-bus/src/calls/profile.rs create mode 100644 crates/tinymemory-bus/src/calls/recall.rs create mode 100644 crates/tinymemory-bus/src/calls/retrieval.rs create mode 100644 crates/tinymemory-bus/src/calls/sources.rs create mode 100644 crates/tinymemory-bus/src/calls/test.rs create mode 100644 crates/tinymemory-bus/src/calls/tool_memory.rs create mode 100644 crates/tinymemory-bus/src/calls/tree.rs create mode 100644 crates/tinymemory-bus/src/error/mod.rs create mode 100644 crates/tinymemory-bus/src/error/test.rs create mode 100644 crates/tinymemory-bus/src/lib.rs create mode 100644 crates/tinymemory-bus/src/names/mod.rs create mode 100644 crates/tinymemory-bus/src/names/test.rs create mode 100644 crates/tinymemory-bus/src/types/mod.rs create mode 100644 crates/tinymemory-bus/src/wire/mod.rs create mode 100644 crates/tinymemory-bus/src/wire/test.rs diff --git a/Cargo.toml b/Cargo.toml index 65bd18d..2d36b67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = ["crates/*"] default-members = [ "crates/tinymemory", "crates/tinymemory-api", + "crates/tinymemory-bus", "crates/tinymemory-conformance", "crates/tinymemory-core", "crates/tinymemory-remote", diff --git a/crates/tinymemory-bus/Cargo.toml b/crates/tinymemory-bus/Cargo.toml new file mode 100644 index 0000000..96e5634 --- /dev/null +++ b/crates/tinymemory-bus/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "tinymemory-bus" +# Not published, for the same reason `tinymemory-api` is not: the graph below it +# reaches crates that are not on crates.io. A host takes this by git or by path. +publish = false +version = "0.1.0" +edition = "2021" +rust-version = "1.96" +license = "MIT" +repository = "https://github.com/tinyhumansai/tinymemory" +description = "The TinyBus wire contract for the TinyMemory module: member names, payload types, and typed calls" + +# Three dependencies, and the ceiling is low on purpose. +# +# This crate is what a *host* compiles against to talk to the loaded module, so +# it must cost that host almost nothing: no engine, no storage, no async +# runtime, and — importantly — no `tinybus`. See `src/lib.rs` for why the +# transport is deliberately absent, and the root manifest's note on +# `crates/tinymemory-module` for what depending on the vendored `tinybus` from a +# workspace member would do to this workspace. +[dependencies] +# The single definition of every type on the wire. Re-exported, never +# redefined — see `src/types/mod.rs`. +tinymemory-api = { path = "../tinymemory-api" } +# The call structs derive both halves: `Serialize` to build an argument array, +# `Deserialize` so a module-side test can decode one back. +serde = { version = "1", features = ["derive"] } +# A tinybus frame body is JSON, so an encoded argument list is a +# `serde_json::Value` and nothing here needs a different representation. +serde_json = "1" +thiserror = "2" + +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" +missing_debug_implementations = "warn" +unreachable_pub = "warn" +rust_2018_idioms = { level = "warn", priority = -1 } + +[lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +unwrap_used = "warn" +expect_used = "warn" +panic = "warn" +todo = "warn" +unimplemented = "warn" +missing_errors_doc = "warn" +missing_panics_doc = "warn" +doc_markdown = "warn" + +[lints.rustdoc] +broken_intra_doc_links = "warn" +private_intra_doc_links = "warn" diff --git a/crates/tinymemory-bus/src/calls/chunks.rs b/crates/tinymemory-bus/src/calls/chunks.rs new file mode 100644 index 0000000..14cf8a8 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/chunks.rs @@ -0,0 +1,124 @@ +//! The persisted chunk model and its embeddings. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::chunks::Chunk; +use tinymemory_api::provider::chunks::{ChunkDetail, ChunkEmbedding, ChunkQuery}; +use tinymemory_api::provider::types::SourceScope; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `ListChunks`. +/// +/// Chunks matching the query, size-checked. +/// +/// `ChunkQuery::limit` bounds rows, not bytes, and a chunk carries full +/// content — so this is one of the methods where the ceiling matters most. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListChunks { + /// The `query` argument — wire position 0. + pub query: ChunkQuery, + /// The `scope` argument — wire position 1. + pub scope: Option, +} + +impl BusCall for ListChunks { + const METHOD: &'static str = methods::LIST_CHUNKS; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.query, self.scope)).map_err(Error::Encode) + } +} + +/// Arguments for `GetChunk`. +/// +/// One chunk, size-checked. +/// +/// A single object is checked for the same reason a list is: the ceiling is +/// a property of the frame, not of the row count, and one chunk carries +/// full content with no bound of its own. A list of one that is refused +/// while the singular read of the same chunk succeeds would be an odd +/// contract to explain. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetChunk { + /// The `chunk_id` argument — wire position 0. + pub chunk_id: String, +} + +impl BusCall for GetChunk { + const METHOD: &'static str = methods::GET_CHUNK; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.chunk_id,)).map_err(Error::Encode) + } +} + +/// Arguments for `ChunkDetail`. +/// +/// One chunk plus its metadata, size-checked. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChunkDetail { + /// The `chunk_id` argument — wire position 0. + pub chunk_id: String, +} + +impl BusCall for ChunkDetail { + const METHOD: &'static str = methods::CHUNK_DETAIL; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.chunk_id,)).map_err(Error::Encode) + } +} + +/// Arguments for `StorageKinds`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StorageKinds; + +impl BusCall for StorageKinds { + const METHOD: &'static str = methods::STORAGE_KINDS; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `ChunkEmbeddings`. +/// +/// Embedding vectors are the largest thing this interface returns. +/// +/// A 1536-dimension vector encodes to roughly 10 KiB of JSON, so a few +/// hundred chunks reach the frame ceiling on their own. Checked for the same +/// reason `List` is, and refused by name rather than truncated — a short +/// batch is indistinguishable from "those chunks have no vector". +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChunkEmbeddings { + /// The `chunk_ids` argument — wire position 0. + pub chunk_ids: Vec, + /// The `model_signature` argument — wire position 1. + pub model_signature: String, +} + +impl BusCall for ChunkEmbeddings { + const METHOD: &'static str = methods::CHUNK_EMBEDDINGS; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.chunk_ids, self.model_signature)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/core.rs b/crates/tinymemory-bus/src/calls/core.rs new file mode 100644 index 0000000..48c40af --- /dev/null +++ b/crates/tinymemory-bus/src/calls/core.rs @@ -0,0 +1,134 @@ +//! The mandatory key/value surface every driver implements. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::types::{MemoryCategory, MemoryEntry, MemoryTaint, NamespaceSummary}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `Store`. +/// +/// Upsert an entry keyed by `(namespace, key)`. +/// +/// `taint` is a required argument rather than a defaulted one, mirroring the +/// contract: a driver that could default provenance would be able to launder +/// externally-sourced content into internal-trust content, which is the one +/// failure mode the host's policy guard exists to prevent. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Store { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `key` argument — wire position 1. + pub key: String, + /// The `content` argument — wire position 2. + pub content: String, + /// The `category` argument — wire position 3. + pub category: MemoryCategory, + /// The `session_id` argument — wire position 4. + pub session_id: Option, + /// The `taint` argument — wire position 5. + pub taint: MemoryTaint, +} + +impl BusCall for Store { + const METHOD: &'static str = methods::STORE; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.key, self.content, self.category, self.session_id, self.taint)).map_err(Error::Encode) + } +} + +/// Arguments for `Get`. +/// +/// Fetch the entry at an exact `(namespace, key)`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Get { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `key` argument — wire position 1. + pub key: String, +} + +impl BusCall for Get { + const METHOD: &'static str = methods::GET; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) + } +} + +/// Arguments for `Forget`. +/// +/// Delete the entry at `(namespace, key)`, reporting whether it existed. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Forget { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `key` argument — wire position 1. + pub key: String, +} + +impl BusCall for Forget { + const METHOD: &'static str = methods::FORGET; + + type Response = bool; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) + } +} + +/// Arguments for `List`. +/// +/// List entries, narrowing by namespace, category and session. +/// +/// Bounded by `MAX_RESPONSE_BYTES`: unlike `Recall` and `ExportPage`, this +/// method takes no limit and no cursor, so the caller has no way to ask for +/// less. See `ensure_response_fits` for why the answer is a named refusal +/// rather than a truncation. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct List { + /// The `namespace` argument — wire position 0. + pub namespace: Option, + /// The `category` argument — wire position 1. + pub category: Option, + /// The `session_id` argument — wire position 2. + pub session_id: Option, +} + +impl BusCall for List { + const METHOD: &'static str = methods::LIST; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.category, self.session_id)).map_err(Error::Encode) + } +} + +/// Arguments for `Namespaces`. +/// +/// Enumerate namespaces with their aggregate counts. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Namespaces; + +impl BusCall for Namespaces { + const METHOD: &'static str = methods::NAMESPACES; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} diff --git a/crates/tinymemory-bus/src/calls/documents.rs b/crates/tinymemory-bus/src/calls/documents.rs new file mode 100644 index 0000000..02e5940 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/documents.rs @@ -0,0 +1,157 @@ +//! Namespace-scoped document storage and retrieval. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::types::{NamespaceDocumentInput, NamespaceRetrievalContext, StoredMemoryDocument}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `PutDocument`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PutDocument { + /// The `input` argument — wire position 0. + pub input: NamespaceDocumentInput, +} + +impl BusCall for PutDocument { + const METHOD: &'static str = methods::PUT_DOCUMENT; + + type Response = String; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.input,)).map_err(Error::Encode) + } +} + +/// Arguments for `GetDocument`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetDocument { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `key` argument — wire position 1. + pub key: String, +} + +impl BusCall for GetDocument { + const METHOD: &'static str = methods::GET_DOCUMENT; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) + } +} + +/// Arguments for `ListDocuments`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListDocuments { + /// The `namespace` argument — wire position 0. + pub namespace: Option, +} + +impl BusCall for ListDocuments { + const METHOD: &'static str = methods::LIST_DOCUMENTS; + + type Response = Value; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace,)).map_err(Error::Encode) + } +} + +/// Arguments for `ListNamespaces`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListNamespaces; + +impl BusCall for ListNamespaces { + const METHOD: &'static str = methods::LIST_NAMESPACES; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `DeleteDocument`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteDocument { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `document_id` argument — wire position 1. + pub document_id: String, +} + +impl BusCall for DeleteDocument { + const METHOD: &'static str = methods::DELETE_DOCUMENT; + + type Response = Value; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.document_id)).map_err(Error::Encode) + } +} + +/// Arguments for `ClearNamespace`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClearNamespace { + /// The `namespace` argument — wire position 0. + pub namespace: String, +} + +impl BusCall for ClearNamespace { + const METHOD: &'static str = methods::CLEAR_NAMESPACE; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace,)).map_err(Error::Encode) + } +} + +/// Arguments for `QueryDocuments`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QueryDocuments { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `query` argument — wire position 1. + pub query: String, + /// The `limit` argument — wire position 2. + pub limit: usize, +} + +impl BusCall for QueryDocuments { + const METHOD: &'static str = methods::QUERY_DOCUMENTS; + + type Response = NamespaceRetrievalContext; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.query, self.limit)).map_err(Error::Encode) + } +} + +/// Arguments for `RecallDocuments`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecallDocuments { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `limit` argument — wire position 1. + pub limit: usize, +} + +impl BusCall for RecallDocuments { + const METHOD: &'static str = methods::RECALL_DOCUMENTS; + + type Response = NamespaceRetrievalContext; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.limit)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/driver.rs b/crates/tinymemory-bus/src/calls/driver.rs new file mode 100644 index 0000000..2655351 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/driver.rs @@ -0,0 +1,122 @@ +//! Driver identity, capability negotiation, health and store opening. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::capabilities::Capabilities; +use tinymemory_api::health::MemoryHealth; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `DriverId`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DriverId; + +impl BusCall for DriverId { + const METHOD: &'static str = methods::DRIVER_ID; + + type Response = String; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `Capabilities`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Capabilities; + +impl BusCall for Capabilities { + const METHOD: &'static str = methods::CAPABILITIES; + + type Response = Capabilities; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `Health`. +/// +/// Current liveness, as the driver reports it. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Health; + +impl BusCall for Health { + const METHOD: &'static str = methods::HEALTH; + + type Response = MemoryHealth; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `Shutdown`. +/// +/// Release backend resources. +/// +/// Idempotent, as the trait requires. Note that this does **not** unload the +/// module: `TinyBus` never unloads a library, so a host that shuts the +/// driver down and rebinds gets a fresh engine inside the same mapped image. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Shutdown; + +impl BusCall for Shutdown { + const METHOD: &'static str = methods::SHUTDOWN; + + type Response = (); + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `OpenStore`. +/// +/// Bring up a store rooted at `/` and return the +/// object path serving it. +/// +/// # Why the module opens stores rather than the host selecting one per call +/// +/// A host with per-profile memory needs more than one store in a process. +/// The alternative was a store selector threaded through every method on +/// every capability family — a change to the shape of the whole contract, +/// to express something that is not a property of a memory operation at +/// all. Which store you are talking to is settled when you are handed a +/// driver, exactly like which workspace you are bound to. +/// +/// So the root object opens stores and hands back object paths. Each is an +/// ordinary `MemoryService` exporting the identical interface, and the +/// contract does not change at all: `MemoryProvider` still describes one +/// store, and a proxy still talks to one store. +/// +/// Idempotent per subtree — see `StoreOpener::served` for why opening the +/// same database twice is worth going out of the way to avoid. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenStore { + /// The `memory_subdir` argument — wire position 0. + pub memory_subdir: String, +} + +impl BusCall for OpenStore { + const METHOD: &'static str = methods::OPEN_STORE; + + type Response = String; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.memory_subdir,)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/episodic.rs b/crates/tinymemory-bus/src/calls/episodic.rs new file mode 100644 index 0000000..8c50b0a --- /dev/null +++ b/crates/tinymemory-bus/src/calls/episodic.rs @@ -0,0 +1,190 @@ +//! Episodic turns and conversation segments. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::episodic::{ConversationSegment, EpisodicTurn}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `InsertTurn`. +/// +/// Record one turn, answering with the row id the engine assigned it. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InsertTurn { + /// The `turn` argument — wire position 0. + pub turn: EpisodicTurn, +} + +impl BusCall for InsertTurn { + const METHOD: &'static str = methods::INSERT_TURN; + + type Response = i64; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.turn,)).map_err(Error::Encode) + } +} + +/// Arguments for `SessionTurns`. +/// +/// Every recorded turn for one session, oldest first. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionTurns { + /// The `session_id` argument — wire position 0. + pub session_id: String, +} + +impl BusCall for SessionTurns { + const METHOD: &'static str = methods::SESSION_TURNS; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.session_id,)).map_err(Error::Encode) + } +} + +/// Arguments for `OpenSegment`. +/// +/// The open segment for a session, if there is one. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenSegment { + /// The `session_id` argument — wire position 0. + pub session_id: String, +} + +impl BusCall for OpenSegment { + const METHOD: &'static str = methods::OPEN_SEGMENT; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.session_id,)).map_err(Error::Encode) + } +} + +/// Arguments for `CreateSegment`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateSegment { + /// The `segment_id` argument — wire position 0. + pub segment_id: String, + /// The `session_id` argument — wire position 1. + pub session_id: String, + /// The `namespace` argument — wire position 2. + pub namespace: String, + /// The `start_episodic_id` argument — wire position 3. + pub start_episodic_id: i64, + /// The `start_timestamp` argument — wire position 4. + pub start_timestamp: f64, + /// The `now` argument — wire position 5. + pub now: f64, +} + +impl BusCall for CreateSegment { + const METHOD: &'static str = methods::CREATE_SEGMENT; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.segment_id, self.session_id, self.namespace, self.start_episodic_id, self.start_timestamp, self.now)).map_err(Error::Encode) + } +} + +/// Arguments for `AppendTurn`. +/// +/// Extend a segment to include one more turn. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AppendTurn { + /// The `segment_id` argument — wire position 0. + pub segment_id: String, + /// The `episodic_id` argument — wire position 1. + pub episodic_id: i64, + /// The `timestamp` argument — wire position 2. + pub timestamp: f64, + /// The `now` argument — wire position 3. + pub now: f64, +} + +impl BusCall for AppendTurn { + const METHOD: &'static str = methods::APPEND_TURN; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.segment_id, self.episodic_id, self.timestamp, self.now)).map_err(Error::Encode) + } +} + +/// Arguments for `CloseSegment`. +/// +/// Mark a segment closed. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CloseSegment { + /// The `segment_id` argument — wire position 0. + pub segment_id: String, + /// The `now` argument — wire position 1. + pub now: f64, +} + +impl BusCall for CloseSegment { + const METHOD: &'static str = methods::CLOSE_SEGMENT; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.segment_id, self.now)).map_err(Error::Encode) + } +} + +/// Arguments for `SetSegmentSummary`. +/// +/// Attach a summary to a closed segment. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetSegmentSummary { + /// The `segment_id` argument — wire position 0. + pub segment_id: String, + /// The `summary` argument — wire position 1. + pub summary: String, + /// The `now` argument — wire position 2. + pub now: f64, +} + +impl BusCall for SetSegmentSummary { + const METHOD: &'static str = methods::SET_SEGMENT_SUMMARY; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.segment_id, self.summary, self.now)).map_err(Error::Encode) + } +} + +/// Arguments for `UpsertSegmentEmbedding`. +/// +/// Store a segment's embedding under `model_signature`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpsertSegmentEmbedding { + /// The `segment_id` argument — wire position 0. + pub segment_id: String, + /// The `model_signature` argument — wire position 1. + pub model_signature: String, + /// The `embedding` argument — wire position 2. + pub embedding: Vec, + /// The `created_at` argument — wire position 3. + pub created_at: f64, +} + +impl BusCall for UpsertSegmentEmbedding { + const METHOD: &'static str = methods::UPSERT_SEGMENT_EMBEDDING; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.segment_id, self.model_signature, self.embedding, self.created_at)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/goals.rs b/crates/tinymemory-bus/src/calls/goals.rs new file mode 100644 index 0000000..cca6ef9 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/goals.rs @@ -0,0 +1,45 @@ +//! The long-term goals document. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::goals::GoalsDoc; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `Goals`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Goals; + +impl BusCall for Goals { + const METHOD: &'static str = methods::GOALS; + + type Response = GoalsDoc; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `SetGoals`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetGoals { + /// The `goals` argument — wire position 0. + pub goals: GoalsDoc, +} + +impl BusCall for SetGoals { + const METHOD: &'static str = methods::SET_GOALS; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.goals,)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/graph.rs b/crates/tinymemory-bus/src/calls/graph.rs new file mode 100644 index 0000000..c8cf9b4 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/graph.rs @@ -0,0 +1,216 @@ +//! Entities, relations and the namespaced key/value store. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::retrieval::EntityMatch; +use tinymemory_api::provider::types::EntityHit; +use tinymemory_api::types::{GraphRelationRecord, MemoryKvRecord}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `Entities`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Entities { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `query` argument — wire position 1. + pub query: Option, + /// The `limit` argument — wire position 2. + pub limit: usize, +} + +impl BusCall for Entities { + const METHOD: &'static str = methods::ENTITIES; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.query, self.limit)).map_err(Error::Encode) + } +} + +/// Arguments for `EntityEdges`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EntityEdges { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `entity_id` argument — wire position 1. + pub entity_id: String, + /// The `limit` argument — wire position 2. + pub limit: usize, +} + +impl BusCall for EntityEdges { + const METHOD: &'static str = methods::ENTITY_EDGES; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.entity_id, self.limit)).map_err(Error::Encode) + } +} + +/// Arguments for `TouchEntities`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TouchEntities { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `entity_ids` argument — wire position 1. + pub entity_ids: Vec, +} + +impl BusCall for TouchEntities { + const METHOD: &'static str = methods::TOUCH_ENTITIES; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.entity_ids)).map_err(Error::Encode) + } +} + +/// Arguments for `SearchEntities`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SearchEntities { + /// The `query` argument — wire position 0. + pub query: String, + /// The `kinds` argument — wire position 1. + pub kinds: Option>, + /// The `limit` argument — wire position 2. + pub limit: usize, +} + +impl BusCall for SearchEntities { + const METHOD: &'static str = methods::SEARCH_ENTITIES; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.query, self.kinds, self.limit)).map_err(Error::Encode) + } +} + +/// Arguments for `Relations`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Relations { + /// The `namespace` argument — wire position 0. + pub namespace: Option, + /// The `subject` argument — wire position 1. + pub subject: Option, + /// The `predicate` argument — wire position 2. + pub predicate: Option, + /// The `limit` argument — wire position 3. + pub limit: usize, +} + +impl BusCall for Relations { + const METHOD: &'static str = methods::RELATIONS; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.subject, self.predicate, self.limit)).map_err(Error::Encode) + } +} + +/// Arguments for `PutRelation`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PutRelation { + /// The `relation` argument — wire position 0. + pub relation: GraphRelationRecord, +} + +impl BusCall for PutRelation { + const METHOD: &'static str = methods::PUT_RELATION; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.relation,)).map_err(Error::Encode) + } +} + +/// Arguments for `KvGet`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KvGet { + /// The `namespace` argument — wire position 0. + pub namespace: Option, + /// The `key` argument — wire position 1. + pub key: String, +} + +impl BusCall for KvGet { + const METHOD: &'static str = methods::KV_GET; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) + } +} + +/// Arguments for `KvPut`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KvPut { + /// The `namespace` argument — wire position 0. + pub namespace: Option, + /// The `key` argument — wire position 1. + pub key: String, + /// The `value` argument — wire position 2. + pub value: Value, +} + +impl BusCall for KvPut { + const METHOD: &'static str = methods::KV_PUT; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.key, self.value)).map_err(Error::Encode) + } +} + +/// Arguments for `KvDelete`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KvDelete { + /// The `namespace` argument — wire position 0. + pub namespace: Option, + /// The `key` argument — wire position 1. + pub key: String, +} + +impl BusCall for KvDelete { + const METHOD: &'static str = methods::KV_DELETE; + + type Response = bool; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) + } +} + +/// Arguments for `KvList`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KvList { + /// The `namespace` argument — wire position 0. + pub namespace: Option, + /// The `prefix` argument — wire position 1. + pub prefix: Option, + /// The `limit` argument — wire position 2. + pub limit: usize, +} + +impl BusCall for KvList { + const METHOD: &'static str = methods::KV_LIST; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.prefix, self.limit)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/ingest.rs b/crates/tinymemory-bus/src/calls/ingest.rs new file mode 100644 index 0000000..6c4626b --- /dev/null +++ b/crates/tinymemory-bus/src/calls/ingest.rs @@ -0,0 +1,46 @@ +//! Document and chat ingestion through the summary pipeline. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::types::{IngestItem, IngestOutcome}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `IngestDocument`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IngestDocument { + /// The `item` argument — wire position 0. + pub item: IngestItem, +} + +impl BusCall for IngestDocument { + const METHOD: &'static str = methods::INGEST_DOCUMENT; + + type Response = IngestOutcome; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.item,)).map_err(Error::Encode) + } +} + +/// Arguments for `IngestChat`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IngestChat { + /// The `messages` argument — wire position 0. + pub messages: Vec, +} + +impl BusCall for IngestChat { + const METHOD: &'static str = methods::INGEST_CHAT; + + type Response = IngestOutcome; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.messages,)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/maintenance.rs b/crates/tinymemory-bus/src/calls/maintenance.rs new file mode 100644 index 0000000..333f072 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/maintenance.rs @@ -0,0 +1,75 @@ +//! Re-embedding, compaction, consolidation and diagnosis. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::types::MaintenanceReport; + +use crate::calls::BusCall; +use crate::names::methods; + +/// Arguments for `Reembed`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Reembed; + +impl BusCall for Reembed { + const METHOD: &'static str = methods::REEMBED; + + type Response = MaintenanceReport; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `Compact`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Compact; + +impl BusCall for Compact { + const METHOD: &'static str = methods::COMPACT; + + type Response = MaintenanceReport; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `Consolidate`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Consolidate; + +impl BusCall for Consolidate { + const METHOD: &'static str = methods::CONSOLIDATE; + + type Response = MaintenanceReport; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `Doctor`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Doctor; + +impl BusCall for Doctor { + const METHOD: &'static str = methods::DOCTOR; + + type Response = MaintenanceReport; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} diff --git a/crates/tinymemory-bus/src/calls/mod.rs b/crates/tinymemory-bus/src/calls/mod.rs new file mode 100644 index 0000000..c2a94c1 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/mod.rs @@ -0,0 +1,126 @@ +//! One typed struct per member, and the [`BusCall`] trait that ties it to its +//! name and its reply type. +//! +//! # Why arguments get a struct at all +//! +//! `#[tinybus::interface]` puts a method's arguments on the wire as a +//! **positional JSON array**, decoded on the far side into a tuple. That is a +//! fine encoding and a bad thing to write by hand: +//! +//! ```json +//! ["work", "standup", "…", "Fact", null, "Untrusted"] +//! ``` +//! +//! Two of those six are `Option`s, two are enums that serialize as strings, and +//! swapping `namespace` with `key` produces a call that succeeds and writes the +//! entry to the wrong place. Nothing on the module side can catch it: both are +//! `String`, in the right position count, and the engine has no way to know +//! which one the caller meant. +//! +//! So a caller fills in named fields and this crate does the positioning: +//! +//! ``` +//! use tinymemory_bus::calls::{core::Store, BusCall}; +//! use tinymemory_bus::types::{MemoryCategory, MemoryTaint}; +//! +//! let args = Store { +//! namespace: "work".to_string(), +//! key: "standup".to_string(), +//! content: "shipped the loader".to_string(), +//! category: MemoryCategory::Fact, +//! session_id: None, +//! taint: MemoryTaint::Trusted, +//! } +//! .into_args()?; +//! +//! assert_eq!(Store::METHOD, "Store"); +//! assert_eq!(args[0], "work"); +//! assert_eq!(args[1], "standup"); +//! # Ok::<(), tinymemory_bus::Error>(()) +//! ``` +//! +//! # The reply type travels with the call +//! +//! [`BusCall::Response`] is the other half, and it is the half a host would +//! otherwise get wrong quietly. `Get` answers `Option` while +//! `Forget` answers `bool`; both are perfectly good JSON, and decoding one as +//! the other fails at a point far from the call. Binding the response type to +//! the call type means a host writes the method once and the compiler knows +//! what comes back. +//! +//! # What this is not +//! +//! Not a client. There is no connection here, no `call()` that sends anything — +//! see [`crate`] for why the transport is deliberately out of scope. A host +//! writes one small generic helper over its own `tinybus::Connection`; the +//! shape is in this crate's `README.md`. + +use serde::de::DeserializeOwned; +use serde_json::Value; + +use crate::error::{Error, Result}; + +pub mod chunks; +pub mod core; +pub mod documents; +pub mod driver; +pub mod episodic; +pub mod goals; +pub mod graph; +pub mod ingest; +pub mod maintenance; +pub mod people; +pub mod portability; +pub mod profile; +pub mod recall; +pub mod retrieval; +pub mod sources; +pub mod tool_memory; +pub mod tree; + +/// One member of the `TinyMemory` interface, as a typed request. +/// +/// An implementor names the member ([`METHOD`](Self::METHOD)), knows what comes +/// back ([`Response`](Self::Response)), and can lay its own fields out in the +/// positional order the module decodes them from +/// ([`into_args`](Self::into_args)). +/// +/// Implementors are generated from the module's `#[tinybus::interface]` block, +/// so the field order below is the wire order by construction rather than by +/// review. +pub trait BusCall { + /// The member name, as it travels in a frame. + /// + /// Always one of [`crate::names::METHODS`]. + const METHOD: &'static str; + + /// What the module replies with on success. + type Response: DeserializeOwned; + + /// Lay the arguments out as the positional array the module decodes. + /// + /// The result is always a JSON array — an empty one for a member that takes + /// no arguments, because `#[tinybus::interface]` skips decoding entirely in + /// that case and every caller sends `[]`. + /// + /// # Errors + /// + /// [`Error::Encode`] if a field fails to serialize. Unreachable for the + /// payload types on this wire, which are plain derived data; see + /// [`crate::error`]. + fn into_args(self) -> Result; + + /// Decode a successful reply body into this call's response type. + /// + /// # Errors + /// + /// [`Error::Decode`] if the body does not match + /// [`Response`](Self::Response) — in practice, a module built from a + /// different revision of this contract. + fn decode_response(body: Value) -> Result { + serde_json::from_value(body).map_err(Error::Decode) + } +} + +#[cfg(test)] +mod test; diff --git a/crates/tinymemory-bus/src/calls/people.rs b/crates/tinymemory-bus/src/calls/people.rs new file mode 100644 index 0000000..4f36bf2 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/people.rs @@ -0,0 +1,141 @@ +//! The people store: ranking, handles, scores and interactions. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::people::{AddressBookSeedOutcome, PersonHandle, PersonInteraction, PersonRecord, PersonScore, RankedPerson, ResolvedPerson}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `ListPeople`. +/// +/// Known people, ranked by closeness. +/// +/// Size-checked like the other list-returning methods. `limit` bounds the +/// *count* but not the bytes — a store of people each carrying many handles +/// can still overflow a frame — so the ceiling is enforced on the encoded +/// response rather than trusted to the caller's limit. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListPeople { + /// The `limit` argument — wire position 0. + pub limit: Option, +} + +impl BusCall for ListPeople { + const METHOD: &'static str = methods::LIST_PEOPLE; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.limit,)).map_err(Error::Encode) + } +} + +/// Arguments for `GetPerson`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetPerson { + /// The `person_id` argument — wire position 0. + pub person_id: String, +} + +impl BusCall for GetPerson { + const METHOD: &'static str = methods::GET_PERSON; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.person_id,)).map_err(Error::Encode) + } +} + +/// Arguments for `ResolveHandle`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResolveHandle { + /// The `handle` argument — wire position 0. + pub handle: PersonHandle, + /// The `create_if_missing` argument — wire position 1. + pub create_if_missing: bool, +} + +impl BusCall for ResolveHandle { + const METHOD: &'static str = methods::RESOLVE_HANDLE; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.handle, self.create_if_missing)).map_err(Error::Encode) + } +} + +/// Arguments for `AddHandleAlias`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddHandleAlias { + /// The `person_id` argument — wire position 0. + pub person_id: String, + /// The `handle` argument — wire position 1. + pub handle: PersonHandle, +} + +impl BusCall for AddHandleAlias { + const METHOD: &'static str = methods::ADD_HANDLE_ALIAS; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.person_id, self.handle)).map_err(Error::Encode) + } +} + +/// Arguments for `ScorePerson`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScorePerson { + /// The `person_id` argument — wire position 0. + pub person_id: String, +} + +impl BusCall for ScorePerson { + const METHOD: &'static str = methods::SCORE_PERSON; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.person_id,)).map_err(Error::Encode) + } +} + +/// Arguments for `RecordInteraction`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecordInteraction { + /// The `interaction` argument — wire position 0. + pub interaction: PersonInteraction, +} + +impl BusCall for RecordInteraction { + const METHOD: &'static str = methods::RECORD_INTERACTION; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.interaction,)).map_err(Error::Encode) + } +} + +/// Arguments for `SeedFromAddressBook`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SeedFromAddressBook; + +impl BusCall for SeedFromAddressBook { + const METHOD: &'static str = methods::SEED_FROM_ADDRESS_BOOK; + + type Response = AddressBookSeedOutcome; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} diff --git a/crates/tinymemory-bus/src/calls/portability.rs b/crates/tinymemory-bus/src/calls/portability.rs new file mode 100644 index 0000000..a43b4a7 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/portability.rs @@ -0,0 +1,55 @@ +//! Paged export and bulk import of raw records. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::types::{ExportPage, ExportRecord, ImportOutcome}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `ExportPage`. +/// +/// Read one page of the export, continuing from `cursor`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExportPage { + /// The `cursor` argument — wire position 0. + pub cursor: Option, + /// The `limit` argument — wire position 1. + pub limit: usize, +} + +impl BusCall for ExportPage { + const METHOD: &'static str = methods::EXPORT_PAGE; + + type Response = ExportPage; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.cursor, self.limit)).map_err(Error::Encode) + } +} + +/// Arguments for `ImportRecords`. +/// +/// Write a batch of previously-exported records. +/// +/// Partial success is reported inside `ImportOutcome` rather than as an +/// error, so a million-record restore is not aborted by one bad record. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImportRecords { + /// The `records` argument — wire position 0. + pub records: Vec, +} + +impl BusCall for ImportRecords { + const METHOD: &'static str = methods::IMPORT_RECORDS; + + type Response = ImportOutcome; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.records,)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/profile.rs b/crates/tinymemory-bus/src/calls/profile.rs new file mode 100644 index 0000000..7ff4bc9 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/profile.rs @@ -0,0 +1,217 @@ +//! Profile facets and their provenance. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::profile::{FacetType, ProfileFacet, UserState}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `ListActiveFacets`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListActiveFacets; + +impl BusCall for ListActiveFacets { + const METHOD: &'static str = methods::LIST_ACTIVE_FACETS; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `ListAllFacets`. +/// +/// Takes no arguments, so it encodes as an empty positional array. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ListAllFacets; + +impl BusCall for ListAllFacets { + const METHOD: &'static str = methods::LIST_ALL_FACETS; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + Ok(Value::Array(Vec::new())) + } +} + +/// Arguments for `GetFacet`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetFacet { + /// The `key` argument — wire position 0. + pub key: String, +} + +impl BusCall for GetFacet { + const METHOD: &'static str = methods::GET_FACET; + + type Response = Option; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.key,)).map_err(Error::Encode) + } +} + +/// Arguments for `FacetsByType`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FacetsByType { + /// The `facet_type` argument — wire position 0. + pub facet_type: FacetType, +} + +impl BusCall for FacetsByType { + const METHOD: &'static str = methods::FACETS_BY_TYPE; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.facet_type,)).map_err(Error::Encode) + } +} + +/// Arguments for `UpsertFacet`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpsertFacet { + /// The `facet` argument — wire position 0. + pub facet: ProfileFacet, +} + +impl BusCall for UpsertFacet { + const METHOD: &'static str = methods::UPSERT_FACET; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.facet,)).map_err(Error::Encode) + } +} + +/// Arguments for `UpsertProviderFacet`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpsertProviderFacet { + /// The `facet_id` argument — wire position 0. + pub facet_id: String, + /// The `facet_type` argument — wire position 1. + pub facet_type: FacetType, + /// The `key` argument — wire position 2. + pub key: String, + /// The `value` argument — wire position 3. + pub value: String, + /// The `confidence` argument — wire position 4. + pub confidence: f64, + /// The `segment_id` argument — wire position 5. + pub segment_id: Option, + /// The `observed_at` argument — wire position 6. + pub observed_at: f64, +} + +impl BusCall for UpsertProviderFacet { + const METHOD: &'static str = methods::UPSERT_PROVIDER_FACET; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.facet_id, self.facet_type, self.key, self.value, self.confidence, self.segment_id, self.observed_at)).map_err(Error::Encode) + } +} + +/// Arguments for `SetFacetUserState`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SetFacetUserState { + /// The `key` argument — wire position 0. + pub key: String, + /// The `user_state` argument — wire position 1. + pub user_state: UserState, +} + +impl BusCall for SetFacetUserState { + const METHOD: &'static str = methods::SET_FACET_USER_STATE; + + type Response = bool; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.key, self.user_state)).map_err(Error::Encode) + } +} + +/// Arguments for `DeleteFacet`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteFacet { + /// The `key` argument — wire position 0. + pub key: String, +} + +impl BusCall for DeleteFacet { + const METHOD: &'static str = methods::DELETE_FACET; + + type Response = bool; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.key,)).map_err(Error::Encode) + } +} + +/// Arguments for `DeleteFacetById`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteFacetById { + /// The `facet_id` argument — wire position 0. + pub facet_id: String, +} + +impl BusCall for DeleteFacetById { + const METHOD: &'static str = methods::DELETE_FACET_BY_ID; + + type Response = bool; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.facet_id,)).map_err(Error::Encode) + } +} + +/// Arguments for `DropFacetsBelow`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DropFacetsBelow { + /// The `threshold` argument — wire position 0. + pub threshold: f64, +} + +impl BusCall for DropFacetsBelow { + const METHOD: &'static str = methods::DROP_FACETS_BELOW; + + type Response = usize; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.threshold,)).map_err(Error::Encode) + } +} + +/// Arguments for `WorkflowIdentityMatches`. +/// +/// Returns `bool`, not `BusResult` on the trait — but the wire needs a +/// result, so an absent family answers `false` rather than erroring, which +/// is the trait's documented reading of "cannot tell" for this predicate. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WorkflowIdentityMatches { + /// The `key_pattern` argument — wire position 0. + pub key_pattern: String, + /// The `canonical_value` argument — wire position 1. + pub canonical_value: String, +} + +impl BusCall for WorkflowIdentityMatches { + const METHOD: &'static str = methods::WORKFLOW_IDENTITY_MATCHES; + + type Response = bool; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.key_pattern, self.canonical_value)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/recall.rs b/crates/tinymemory-bus/src/calls/recall.rs new file mode 100644 index 0000000..06a254c --- /dev/null +++ b/crates/tinymemory-bus/src/calls/recall.rs @@ -0,0 +1,67 @@ +//! Semantic recall over stored entries. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::types::SourceScope; +use tinymemory_api::recall::OwnedRecallOpts; +use tinymemory_api::types::{MemoryEntry, NamespaceMemoryHit}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `Recall`. +/// +/// Ranked retrieval. +/// +/// `scope` is a query predicate the driver applies internally, not a filter +/// the host may apply to the result: narrowing afterwards would let the +/// driver spend its `limit` on entries the caller is not allowed to see and +/// then return fewer than it could have. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Recall { + /// The `query` argument — wire position 0. + pub query: String, + /// The `limit` argument — wire position 1. + pub limit: usize, + /// The `opts` argument — wire position 2. + pub opts: OwnedRecallOpts, + /// The `scope` argument — wire position 3. + pub scope: Option, +} + +impl BusCall for Recall { + const METHOD: &'static str = methods::RECALL; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.query, self.limit, self.opts, self.scope)).map_err(Error::Encode) + } +} + +/// Arguments for `RecallNamespaceScored`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RecallNamespaceScored { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `query` argument — wire position 1. + pub query: String, + /// The `limit` argument — wire position 2. + pub limit: usize, + /// The `exclude_session_id` argument — wire position 3. + pub exclude_session_id: Option, +} + +impl BusCall for RecallNamespaceScored { + const METHOD: &'static str = methods::RECALL_NAMESPACE_SCORED; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.query, self.limit, self.exclude_session_id)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/retrieval.rs b/crates/tinymemory-bus/src/calls/retrieval.rs new file mode 100644 index 0000000..3ae72c2 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/retrieval.rs @@ -0,0 +1,116 @@ +//! The scored retrieval surface. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::retrieval::{CoverWindowQuery, FastRetrieveQuery, RetrievalHit, RetrievalResponse, SourceRetrievalQuery}; +use tinymemory_api::provider::types::SourceScope; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `FastRetrieve`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FastRetrieve { + /// The `query` argument — wire position 0. + pub query: String, + /// The `options` argument — wire position 1. + pub options: FastRetrieveQuery, + /// The `scope` argument — wire position 2. + pub scope: Option, +} + +impl BusCall for FastRetrieve { + const METHOD: &'static str = methods::FAST_RETRIEVE; + + type Response = RetrievalResponse; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.query, self.options, self.scope)).map_err(Error::Encode) + } +} + +/// Arguments for `CoverWindow`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CoverWindow { + /// The `window` argument — wire position 0. + pub window: CoverWindowQuery, + /// The `scope` argument — wire position 1. + pub scope: Option, +} + +impl BusCall for CoverWindow { + const METHOD: &'static str = methods::COVER_WINDOW; + + type Response = RetrievalResponse; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.window, self.scope)).map_err(Error::Encode) + } +} + +/// Arguments for `RetrieveSource`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RetrieveSource { + /// The `query` argument — wire position 0. + pub query: SourceRetrievalQuery, + /// The `scope` argument — wire position 1. + pub scope: Option, +} + +impl BusCall for RetrieveSource { + const METHOD: &'static str = methods::RETRIEVE_SOURCE; + + type Response = RetrievalResponse; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.query, self.scope)).map_err(Error::Encode) + } +} + +/// Arguments for `RetrieveChildren`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RetrieveChildren { + /// The `node_id` argument — wire position 0. + pub node_id: String, + /// The `max_depth` argument — wire position 1. + pub max_depth: u32, + /// The `query` argument — wire position 2. + pub query: Option, + /// The `limit` argument — wire position 3. + pub limit: Option, + /// The `scope` argument — wire position 4. + pub scope: Option, +} + +impl BusCall for RetrieveChildren { + const METHOD: &'static str = methods::RETRIEVE_CHILDREN; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.node_id, self.max_depth, self.query, self.limit, self.scope)).map_err(Error::Encode) + } +} + +/// Arguments for `RetrieveLeaves`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RetrieveLeaves { + /// The `chunk_ids` argument — wire position 0. + pub chunk_ids: Vec, + /// The `scope` argument — wire position 1. + pub scope: Option, +} + +impl BusCall for RetrieveLeaves { + const METHOD: &'static str = methods::RETRIEVE_LEAVES; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.chunk_ids, self.scope)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/sources.rs b/crates/tinymemory-bus/src/calls/sources.rs new file mode 100644 index 0000000..f46dbcf --- /dev/null +++ b/crates/tinymemory-bus/src/calls/sources.rs @@ -0,0 +1,110 @@ +//! Source snapshots, diffs, item acceptance and forgetting. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::provider::types::{DiffReport, IngestOutcome, SnapshotRef, SourceItem}; +use tinymemory_api::types::MemoryTaint; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `CaptureSnapshot`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CaptureSnapshot { + /// The `source_id` argument — wire position 0. + pub source_id: String, +} + +impl BusCall for CaptureSnapshot { + const METHOD: &'static str = methods::CAPTURE_SNAPSHOT; + + type Response = SnapshotRef; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.source_id,)).map_err(Error::Encode) + } +} + +/// Arguments for `Snapshots`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Snapshots { + /// The `source_id` argument — wire position 0. + pub source_id: String, + /// The `limit` argument — wire position 1. + pub limit: usize, +} + +impl BusCall for Snapshots { + const METHOD: &'static str = methods::SNAPSHOTS; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.source_id, self.limit)).map_err(Error::Encode) + } +} + +/// Arguments for `Diff`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Diff { + /// The `source_id` argument — wire position 0. + pub source_id: String, + /// The `from` argument — wire position 1. + pub from: Option, + /// The `to` argument — wire position 2. + pub to: String, +} + +impl BusCall for Diff { + const METHOD: &'static str = methods::DIFF; + + type Response = DiffReport; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.source_id, self.from, self.to)).map_err(Error::Encode) + } +} + +/// Arguments for `AcceptSourceItems`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AcceptSourceItems { + /// The `source_id` argument — wire position 0. + pub source_id: String, + /// The `source_kind` argument — wire position 1. + pub source_kind: String, + /// The `items` argument — wire position 2. + pub items: Vec, + /// The `taint` argument — wire position 3. + pub taint: MemoryTaint, +} + +impl BusCall for AcceptSourceItems { + const METHOD: &'static str = methods::ACCEPT_SOURCE_ITEMS; + + type Response = IngestOutcome; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.source_id, self.source_kind, self.items, self.taint)).map_err(Error::Encode) + } +} + +/// Arguments for `ForgetSource`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ForgetSource { + /// The `source_id` argument — wire position 0. + pub source_id: String, +} + +impl BusCall for ForgetSource { + const METHOD: &'static str = methods::FORGET_SOURCE; + + type Response = u64; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.source_id,)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/test.rs b/crates/tinymemory-bus/src/calls/test.rs new file mode 100644 index 0000000..45ed438 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/test.rs @@ -0,0 +1,183 @@ +//! Completeness and encoding tests for the generated call structs. +//! +//! The interesting property is coverage. A member the module serves but this +//! crate has no struct for is not a compile error anywhere — it is a host +//! discovering at runtime that the only way to make the call is to hand-build +//! the argument array, which is exactly what this crate exists to prevent. So +//! the table below is checked against `crate::names::METHODS` in both +//! directions. + +use serde_json::json; + +use crate::calls::BusCall; +use crate::names::METHODS; + +/// The member every call struct in this crate names, one entry per struct. +/// +/// Written out rather than derived, because deriving it from the same source +/// the structs come from would make the test agree with itself by +/// construction. +const COVERED: [&str; 89] = [ + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, + ::METHOD, +]; + +#[test] +fn every_member_has_a_call_struct() { + let mut missing: Vec<&str> = METHODS + .into_iter() + .filter(|member| !COVERED.contains(member)) + .collect(); + missing.sort_unstable(); + assert!(missing.is_empty(), "members with no call struct: {missing:?}"); +} + +#[test] +fn every_call_struct_names_a_known_member() { + let mut unknown: Vec<&str> = COVERED + .into_iter() + .filter(|member| !METHODS.contains(member)) + .collect(); + unknown.sort_unstable(); + assert!(unknown.is_empty(), "call structs naming no member: {unknown:?}"); +} + +#[test] +fn no_member_is_covered_twice() { + let mut seen = COVERED; + seen.sort_unstable(); + let mut unique = seen.to_vec(); + unique.dedup(); + assert_eq!(unique.len(), seen.len(), "two call structs name the same member"); +} + +#[test] +fn arguments_encode_as_a_positional_array_in_declaration_order() { + // `Diff` is the useful shape to pin: three arguments, the middle one + // optional. A struct field reordering that a reader would not notice + // shows up here as a moved `null`. + let args = crate::calls::sources::Diff { + source_id: "src-1".to_string(), + from: None, + to: "snap-2".to_string(), + } + .into_args() + .expect("plain data serializes"); + assert_eq!(args, json!(["src-1", null, "snap-2"])); +} + +#[test] +fn a_member_with_no_arguments_encodes_as_an_empty_array() { + // Not `null`: `#[tinybus::interface]` skips argument decoding entirely + // for a zero-argument member, and every caller sends `[]`. + let args = crate::calls::maintenance::Doctor + .into_args() + .expect("no fields to serialize"); + assert_eq!(args, json!([])); +} + +#[test] +fn a_reply_decodes_into_the_calls_response_type() { + use crate::calls::core::Forget; + + let decoded = Forget::decode_response(json!(true)).expect("a bool reply"); + assert!(decoded); +} + +#[test] +fn a_reply_of_the_wrong_shape_is_a_decode_error() { + use crate::calls::core::Forget; + use crate::error::Error; + + // The version-skew case: a module built from a different contract + // answering something this build cannot read. + let failure = Forget::decode_response(json!("yes")).expect_err("a string is not a bool"); + assert!(matches!(failure, Error::Decode(_))); +} diff --git a/crates/tinymemory-bus/src/calls/tool_memory.rs b/crates/tinymemory-bus/src/calls/tool_memory.rs new file mode 100644 index 0000000..cbe9fd6 --- /dev/null +++ b/crates/tinymemory-bus/src/calls/tool_memory.rs @@ -0,0 +1,65 @@ +//! Tool-scoped memory rules. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::tool_memory::ToolMemoryRule; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `ToolRules`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolRules { + /// The `tool_name` argument — wire position 0. + pub tool_name: String, +} + +impl BusCall for ToolRules { + const METHOD: &'static str = methods::TOOL_RULES; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.tool_name,)).map_err(Error::Encode) + } +} + +/// Arguments for `PutToolRule`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PutToolRule { + /// The `rule` argument — wire position 0. + pub rule: ToolMemoryRule, +} + +impl BusCall for PutToolRule { + const METHOD: &'static str = methods::PUT_TOOL_RULE; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.rule,)).map_err(Error::Encode) + } +} + +/// Arguments for `DeleteToolRule`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeleteToolRule { + /// The `tool_name` argument — wire position 0. + pub tool_name: String, + /// The `rule_id` argument — wire position 1. + pub rule_id: String, +} + +impl BusCall for DeleteToolRule { + const METHOD: &'static str = methods::DELETE_TOOL_RULE; + + type Response = bool; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.tool_name, self.rule_id)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/calls/tree.rs b/crates/tinymemory-bus/src/calls/tree.rs new file mode 100644 index 0000000..04e5c5a --- /dev/null +++ b/crates/tinymemory-bus/src/calls/tree.rs @@ -0,0 +1,107 @@ +//! The markdown summary tree: append, query, drill down, seal, cascade. +//! +//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use tinymemory_api::chunks::Chunk; +use tinymemory_api::provider::types::SourceScope; +use tinymemory_api::tree::{IngestRequest, QueryResult, TreeStatus}; + +use crate::calls::BusCall; +use crate::error::Error; +use crate::names::methods; + +/// Arguments for `Append`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Append { + /// The `request` argument — wire position 0. + pub request: IngestRequest, +} + +impl BusCall for Append { + const METHOD: &'static str = methods::APPEND; + + type Response = (); + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.request,)).map_err(Error::Encode) + } +} + +/// Arguments for `QuerySource`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct QuerySource { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `source_id` argument — wire position 1. + pub source_id: String, + /// The `limit` argument — wire position 2. + pub limit: usize, + /// The `scope` argument — wire position 3. + pub scope: Option, +} + +impl BusCall for QuerySource { + const METHOD: &'static str = methods::QUERY_SOURCE; + + type Response = Vec; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.source_id, self.limit, self.scope)).map_err(Error::Encode) + } +} + +/// Arguments for `DrillDown`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DrillDown { + /// The `namespace` argument — wire position 0. + pub namespace: String, + /// The `node_id` argument — wire position 1. + pub node_id: String, +} + +impl BusCall for DrillDown { + const METHOD: &'static str = methods::DRILL_DOWN; + + type Response = QueryResult; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace, self.node_id)).map_err(Error::Encode) + } +} + +/// Arguments for `Seal`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Seal { + /// The `namespace` argument — wire position 0. + pub namespace: String, +} + +impl BusCall for Seal { + const METHOD: &'static str = methods::SEAL; + + type Response = TreeStatus; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace,)).map_err(Error::Encode) + } +} + +/// Arguments for `Cascade`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Cascade { + /// The `namespace` argument — wire position 0. + pub namespace: String, +} + +impl BusCall for Cascade { + const METHOD: &'static str = methods::CASCADE; + + type Response = TreeStatus; + + fn into_args(self) -> crate::Result { + serde_json::to_value((self.namespace,)).map_err(Error::Encode) + } +} diff --git a/crates/tinymemory-bus/src/error/mod.rs b/crates/tinymemory-bus/src/error/mod.rs new file mode 100644 index 0000000..a284e64 --- /dev/null +++ b/crates/tinymemory-bus/src/error/mod.rs @@ -0,0 +1,51 @@ +//! The crate-wide [`Error`] and its [`Result`] alias. +//! +//! # This is not the memory error +//! +//! A failed *memory operation* is a [`MemoryError`], and it travels back from +//! the module as a `(name, message)` pair that [`crate::wire`] converts. That +//! is the interesting error, and it is not this one. +//! +//! [`Error`] covers the far narrower thing this crate does on its own: turning +//! a typed call into an argument array, and turning a reply body back into a +//! typed response. Both are `serde_json` operations, so both can fail, and both +//! failures mean the same thing — the contract and the peer disagree about a +//! payload's shape. +//! +//! Keeping the two apart matters at the call site. A host that gets a +//! [`MemoryError::NotFound`] has learned something about its data; a host that +//! gets an [`Error::Decode`] has learned that its build of this crate does not +//! match the module it is talking to, which is an operator problem and not a +//! caller one. +//! +//! [`MemoryError`]: tinymemory_api::error::MemoryError +//! [`MemoryError::NotFound`]: tinymemory_api::error::MemoryError::NotFound + +/// A failure encoding a call's arguments or decoding its reply. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum Error { + /// A call's arguments could not be serialized into a frame body. + /// + /// In practice this is unreachable for the payload types on this wire — + /// they are plain data with derived `Serialize` impls. It stays a `Result` + /// rather than an unwrap because "in practice unreachable" is not the same + /// as unreachable, and a panic in a host's memory path is a worse answer + /// than an error it can log. + #[error("encoding call arguments failed: {0}")] + Encode(#[source] serde_json::Error), + + /// A reply body did not match the response type this contract expects. + /// + /// The usual cause is a version skew: the module was built from a newer + /// contract than the host. The message carries `serde_json`'s path into the + /// offending value, which names the field but not user memory content. + #[error("decoding a reply failed: {0}")] + Decode(#[source] serde_json::Error), +} + +/// The result type returned by every fallible function in this crate. +pub type Result = std::result::Result; + +#[cfg(test)] +mod test; diff --git a/crates/tinymemory-bus/src/error/test.rs b/crates/tinymemory-bus/src/error/test.rs new file mode 100644 index 0000000..8e49cd1 --- /dev/null +++ b/crates/tinymemory-bus/src/error/test.rs @@ -0,0 +1,33 @@ +//! Unit tests for the crate-wide error type. + +use super::{Error, Result}; + +/// A decode failure of the shape a version skew produces. +fn decode_failure() -> Result { + serde_json::from_value::(serde_json::json!("not a number")).map_err(Error::Decode) +} + +#[test] +fn decode_carries_the_serde_message() { + let error = decode_failure().expect_err("a string does not deserialize as u64"); + let rendered = error.to_string(); + assert!( + rendered.starts_with("decoding a reply failed: "), + "unexpected rendering: {rendered}" + ); +} + +#[test] +fn encode_and_decode_are_distinguishable() { + // The whole point of two variants: a host branches on which side of the + // call went wrong, so they must not collapse into one string prefix. + let decode = decode_failure().expect_err("a string does not deserialize as u64"); + let encode = Error::Encode( + serde_json::to_value(f64::NAN) + .err() + .unwrap_or_else(|| serde_json::from_str::("x").expect_err("not a number")), + ); + assert_ne!(decode.to_string(), encode.to_string()); + assert!(matches!(decode, Error::Decode(_))); + assert!(matches!(encode, Error::Encode(_))); +} diff --git a/crates/tinymemory-bus/src/lib.rs b/crates/tinymemory-bus/src/lib.rs new file mode 100644 index 0000000..afde87e --- /dev/null +++ b/crates/tinymemory-bus/src/lib.rs @@ -0,0 +1,87 @@ +//! The `TinyBus` wire contract for the TinyMemory module. +//! +//! TinyMemory ships as a loadable `TinyBus` module so a host does not compile +//! the engine: `crates/tinymemory-module` exports one object, +//! `/ai/tinyhumans/tinymemory/Memory`, with 89 members on it. A host that loads +//! that binary needs three things to talk to it — the member names, the types +//! on either side of each call, and the error-name table — and none of those +//! are in the module binary, which is a `cdylib`. +//! +//! This crate is those three things, as a library a host links: +//! +//! - [`names`] — the bus name, the object path, and one constant per member. +//! - [`types`] — every value type that crosses a frame. +//! - [`calls`] — one struct per member, carrying its arguments in wire order +//! and its reply type. +//! - [`wire`] — the error names, and the mapping back to `MemoryError`. +//! +//! ``` +//! use tinymemory_bus::calls::{core::Get, BusCall}; +//! use tinymemory_bus::names::{BUS_NAME, OBJECT_PATH}; +//! +//! let args = Get { namespace: "work".to_string(), key: "standup".to_string() }.into_args()?; +//! +//! // Everything a `Connection::call` needs, with nothing spelled by hand. +//! assert_eq!((BUS_NAME, OBJECT_PATH, Get::METHOD), ( +//! "ai.tinyhumans.tinymemory.Memory", +//! "/ai/tinyhumans/tinymemory/Memory", +//! "Get", +//! )); +//! assert_eq!(args.to_string(), r#"["work","standup"]"#); +//! # Ok::<(), tinymemory_bus::Error>(()) +//! ``` +//! +//! # There is no transport here, on purpose +//! +//! This crate does not depend on `tinybus`, and holds no connection, no client +//! and no `call()` that sends anything. Two reasons, and the second is the +//! blunt one. +//! +//! A host already owns its connection. It has its own reconnect policy, its own +//! timeouts, its own tracing, and its own idea of what a memory call costs it. A +//! client here would either duplicate that or fight it, and the useful part — +//! *what to send and what comes back* — is exactly what is in this crate. +//! Wiring it up is a dozen lines over a `Connection`; `README.md` has the shape. +//! +//! And structurally it could not work anyway. `tinybus` is vendored as a git +//! submodule whose manifest inherits fields from its own nested +//! `[workspace.package]`; a member of *this* workspace that depends on it makes +//! cargo resolve that inheritance against the wrong root and fail. That is why +//! `crates/tinymemory-module` is its own workspace root — see the root +//! manifest's note on `exclude`. A contract crate a host links has no business +//! being a separate workspace, so it stays transport-free and every member of +//! this workspace can depend on it. +//! +//! # Why this is not just `tinymemory-api` +//! +//! `tinymemory-api` is the **driver** contract: what an engine implements. It +//! carries `MemoryProvider` and its eighteen capability traits, the +//! mandatory-family composition, the null driver, and the `host::` config +//! sections a host persists in `config.toml`. +//! +//! A host that loads the module implements none of that. It makes calls. This +//! crate is the subset that crosses a frame, so what a host compiles against is +//! what it can actually send and receive — and a member that exists in the +//! trait but is not exported on the bus is absent here rather than tempting. +//! +//! The types themselves are **re-exported** from `tinymemory-api`, never +//! redefined. [`types`] explains why at length; the short version is that a +//! second definition would make `MemoryCategory` from the module a different +//! type from `MemoryCategory` in the host, which is a failure this repository +//! has already had once. +//! +//! # Staying in step with the module +//! +//! [`names::METHODS`] lists every member. `crates/tinymemory-module` asserts its +//! served members against that list, so a method added to the interface without +//! a constant and a call struct here fails that crate's tests rather than +//! turning up as an `UnknownMethod` at runtime in a host. + +pub mod calls; +pub mod error; +pub mod names; +pub mod types; +pub mod wire; + +pub use error::{Error, Result}; +pub use names::{BUS_NAME, METHODS, OBJECT_PATH}; diff --git a/crates/tinymemory-bus/src/names/mod.rs b/crates/tinymemory-bus/src/names/mod.rs new file mode 100644 index 0000000..5fabdee --- /dev/null +++ b/crates/tinymemory-bus/src/names/mod.rs @@ -0,0 +1,335 @@ +//! The object this contract addresses, and every member name on it. +//! +//! A member name is what actually travels in a frame, so it is the part of +//! the contract a typo breaks at runtime rather than at compile time. The +//! constants here exist so neither end spells one by hand. +//! +//! The names are the `PascalCase` of the module's method identifiers, which is +//! what `#[tinybus::interface]` derives them from. [`METHODS`] lists all of +//! them; the module asserts its served members against it, so a method added +//! there without a constant here fails that crate's tests. + +/// Well-known bus name exported by the `TinyMemory` module. +pub const BUS_NAME: &str = "ai.tinyhumans.tinymemory.Memory"; + +/// Object path the interface is served at. +/// +/// `OpenStore` returns a *different* path — a sibling store under the same +/// workspace, exporting this identical interface. Treat this constant as the +/// root object, not as the only one. +pub const OBJECT_PATH: &str = "/ai/tinyhumans/tinymemory/Memory"; + +/// One constant per member name on [`BUS_NAME`]. +pub mod methods { + // Driver identity, capability negotiation, health and store opening. + /// `DriverId` — driver id. + pub const DRIVER_ID: &str = "DriverId"; + /// `Capabilities` — capabilities. + pub const CAPABILITIES: &str = "Capabilities"; + /// `Health` — health. + pub const HEALTH: &str = "Health"; + /// `Shutdown` — shutdown. + pub const SHUTDOWN: &str = "Shutdown"; + /// `OpenStore` — open store. + pub const OPEN_STORE: &str = "OpenStore"; + + // The mandatory key/value surface every driver implements. + /// `Store` — store. + pub const STORE: &str = "Store"; + /// `Get` — get. + pub const GET: &str = "Get"; + /// `Forget` — forget. + pub const FORGET: &str = "Forget"; + /// `List` — list. + pub const LIST: &str = "List"; + /// `Namespaces` — namespaces. + pub const NAMESPACES: &str = "Namespaces"; + + // Semantic recall over stored entries. + /// `Recall` — recall. + pub const RECALL: &str = "Recall"; + /// `RecallNamespaceScored` — recall namespace scored. + pub const RECALL_NAMESPACE_SCORED: &str = "RecallNamespaceScored"; + + // Paged export and bulk import of raw records. + /// `ExportPage` — export page. + pub const EXPORT_PAGE: &str = "ExportPage"; + /// `ImportRecords` — import records. + pub const IMPORT_RECORDS: &str = "ImportRecords"; + + // Document and chat ingestion through the summary pipeline. + /// `IngestDocument` — ingest document. + pub const INGEST_DOCUMENT: &str = "IngestDocument"; + /// `IngestChat` — ingest chat. + pub const INGEST_CHAT: &str = "IngestChat"; + + // Namespace-scoped document storage and retrieval. + /// `PutDocument` — put document. + pub const PUT_DOCUMENT: &str = "PutDocument"; + /// `GetDocument` — get document. + pub const GET_DOCUMENT: &str = "GetDocument"; + /// `ListDocuments` — list documents. + pub const LIST_DOCUMENTS: &str = "ListDocuments"; + /// `ListNamespaces` — list namespaces. + pub const LIST_NAMESPACES: &str = "ListNamespaces"; + /// `DeleteDocument` — delete document. + pub const DELETE_DOCUMENT: &str = "DeleteDocument"; + /// `ClearNamespace` — clear namespace. + pub const CLEAR_NAMESPACE: &str = "ClearNamespace"; + /// `QueryDocuments` — query documents. + pub const QUERY_DOCUMENTS: &str = "QueryDocuments"; + /// `RecallDocuments` — recall documents. + pub const RECALL_DOCUMENTS: &str = "RecallDocuments"; + + // The markdown summary tree: append, query, drill down, seal, cascade. + /// `Append` — append. + pub const APPEND: &str = "Append"; + /// `QuerySource` — query source. + pub const QUERY_SOURCE: &str = "QuerySource"; + /// `DrillDown` — drill down. + pub const DRILL_DOWN: &str = "DrillDown"; + /// `Seal` — seal. + pub const SEAL: &str = "Seal"; + /// `Cascade` — cascade. + pub const CASCADE: &str = "Cascade"; + + // Entities, relations and the namespaced key/value store. + /// `Entities` — entities. + pub const ENTITIES: &str = "Entities"; + /// `EntityEdges` — entity edges. + pub const ENTITY_EDGES: &str = "EntityEdges"; + /// `TouchEntities` — touch entities. + pub const TOUCH_ENTITIES: &str = "TouchEntities"; + /// `SearchEntities` — search entities. + pub const SEARCH_ENTITIES: &str = "SearchEntities"; + /// `Relations` — relations. + pub const RELATIONS: &str = "Relations"; + /// `PutRelation` — put relation. + pub const PUT_RELATION: &str = "PutRelation"; + /// `KvGet` — kv get. + pub const KV_GET: &str = "KvGet"; + /// `KvPut` — kv put. + pub const KV_PUT: &str = "KvPut"; + /// `KvDelete` — kv delete. + pub const KV_DELETE: &str = "KvDelete"; + /// `KvList` — kv list. + pub const KV_LIST: &str = "KvList"; + + // Source snapshots, diffs, item acceptance and forgetting. + /// `CaptureSnapshot` — capture snapshot. + pub const CAPTURE_SNAPSHOT: &str = "CaptureSnapshot"; + /// `Snapshots` — snapshots. + pub const SNAPSHOTS: &str = "Snapshots"; + /// `Diff` — diff. + pub const DIFF: &str = "Diff"; + /// `AcceptSourceItems` — accept source items. + pub const ACCEPT_SOURCE_ITEMS: &str = "AcceptSourceItems"; + /// `ForgetSource` — forget source. + pub const FORGET_SOURCE: &str = "ForgetSource"; + + // The long-term goals document. + /// `Goals` — goals. + pub const GOALS: &str = "Goals"; + /// `SetGoals` — set goals. + pub const SET_GOALS: &str = "SetGoals"; + + // Tool-scoped memory rules. + /// `ToolRules` — tool rules. + pub const TOOL_RULES: &str = "ToolRules"; + /// `PutToolRule` — put tool rule. + pub const PUT_TOOL_RULE: &str = "PutToolRule"; + /// `DeleteToolRule` — delete tool rule. + pub const DELETE_TOOL_RULE: &str = "DeleteToolRule"; + + // Re-embedding, compaction, consolidation and diagnosis. + /// `Reembed` — reembed. + pub const REEMBED: &str = "Reembed"; + /// `Compact` — compact. + pub const COMPACT: &str = "Compact"; + /// `Consolidate` — consolidate. + pub const CONSOLIDATE: &str = "Consolidate"; + /// `Doctor` — doctor. + pub const DOCTOR: &str = "Doctor"; + + // The people store: ranking, handles, scores and interactions. + /// `ListPeople` — list people. + pub const LIST_PEOPLE: &str = "ListPeople"; + /// `GetPerson` — get person. + pub const GET_PERSON: &str = "GetPerson"; + /// `ResolveHandle` — resolve handle. + pub const RESOLVE_HANDLE: &str = "ResolveHandle"; + /// `AddHandleAlias` — add handle alias. + pub const ADD_HANDLE_ALIAS: &str = "AddHandleAlias"; + /// `ScorePerson` — score person. + pub const SCORE_PERSON: &str = "ScorePerson"; + /// `RecordInteraction` — record interaction. + pub const RECORD_INTERACTION: &str = "RecordInteraction"; + /// `SeedFromAddressBook` — seed from address book. + pub const SEED_FROM_ADDRESS_BOOK: &str = "SeedFromAddressBook"; + + // The persisted chunk model and its embeddings. + /// `ListChunks` — list chunks. + pub const LIST_CHUNKS: &str = "ListChunks"; + /// `GetChunk` — get chunk. + pub const GET_CHUNK: &str = "GetChunk"; + /// `ChunkDetail` — chunk detail. + pub const CHUNK_DETAIL: &str = "ChunkDetail"; + /// `StorageKinds` — storage kinds. + pub const STORAGE_KINDS: &str = "StorageKinds"; + /// `ChunkEmbeddings` — chunk embeddings. + pub const CHUNK_EMBEDDINGS: &str = "ChunkEmbeddings"; + + // The scored retrieval surface. + /// `FastRetrieve` — fast retrieve. + pub const FAST_RETRIEVE: &str = "FastRetrieve"; + /// `CoverWindow` — cover window. + pub const COVER_WINDOW: &str = "CoverWindow"; + /// `RetrieveSource` — retrieve source. + pub const RETRIEVE_SOURCE: &str = "RetrieveSource"; + /// `RetrieveChildren` — retrieve children. + pub const RETRIEVE_CHILDREN: &str = "RetrieveChildren"; + /// `RetrieveLeaves` — retrieve leaves. + pub const RETRIEVE_LEAVES: &str = "RetrieveLeaves"; + + // Profile facets and their provenance. + /// `ListActiveFacets` — list active facets. + pub const LIST_ACTIVE_FACETS: &str = "ListActiveFacets"; + /// `ListAllFacets` — list all facets. + pub const LIST_ALL_FACETS: &str = "ListAllFacets"; + /// `GetFacet` — get facet. + pub const GET_FACET: &str = "GetFacet"; + /// `FacetsByType` — facets by type. + pub const FACETS_BY_TYPE: &str = "FacetsByType"; + /// `UpsertFacet` — upsert facet. + pub const UPSERT_FACET: &str = "UpsertFacet"; + /// `UpsertProviderFacet` — upsert provider facet. + pub const UPSERT_PROVIDER_FACET: &str = "UpsertProviderFacet"; + /// `SetFacetUserState` — set facet user state. + pub const SET_FACET_USER_STATE: &str = "SetFacetUserState"; + /// `DeleteFacet` — delete facet. + pub const DELETE_FACET: &str = "DeleteFacet"; + /// `DeleteFacetById` — delete facet by id. + pub const DELETE_FACET_BY_ID: &str = "DeleteFacetById"; + /// `DropFacetsBelow` — drop facets below. + pub const DROP_FACETS_BELOW: &str = "DropFacetsBelow"; + /// `WorkflowIdentityMatches` — workflow identity matches. + pub const WORKFLOW_IDENTITY_MATCHES: &str = "WorkflowIdentityMatches"; + + // Episodic turns and conversation segments. + /// `InsertTurn` — insert turn. + pub const INSERT_TURN: &str = "InsertTurn"; + /// `SessionTurns` — session turns. + pub const SESSION_TURNS: &str = "SessionTurns"; + /// `OpenSegment` — open segment. + pub const OPEN_SEGMENT: &str = "OpenSegment"; + /// `CreateSegment` — create segment. + pub const CREATE_SEGMENT: &str = "CreateSegment"; + /// `AppendTurn` — append turn. + pub const APPEND_TURN: &str = "AppendTurn"; + /// `CloseSegment` — close segment. + pub const CLOSE_SEGMENT: &str = "CloseSegment"; + /// `SetSegmentSummary` — set segment summary. + pub const SET_SEGMENT_SUMMARY: &str = "SetSegmentSummary"; + /// `UpsertSegmentEmbedding` — upsert segment embedding. + pub const UPSERT_SEGMENT_EMBEDDING: &str = "UpsertSegmentEmbedding"; +} + +/// Every member name, in the order the module declares them. +/// +/// The order matters: `tinybus`'s `Interface::members()` returns declaration +/// order, and the module compares the two sequences directly rather than as +/// sets, so a reordering is caught alongside an addition or a removal. +pub const METHODS: [&str; 89] = [ + methods::DRIVER_ID, + methods::CAPABILITIES, + methods::HEALTH, + methods::SHUTDOWN, + methods::OPEN_STORE, + methods::STORE, + methods::GET, + methods::FORGET, + methods::LIST, + methods::NAMESPACES, + methods::RECALL, + methods::EXPORT_PAGE, + methods::IMPORT_RECORDS, + methods::INGEST_DOCUMENT, + methods::INGEST_CHAT, + methods::PUT_DOCUMENT, + methods::GET_DOCUMENT, + methods::LIST_DOCUMENTS, + methods::LIST_NAMESPACES, + methods::DELETE_DOCUMENT, + methods::CLEAR_NAMESPACE, + methods::QUERY_DOCUMENTS, + methods::RECALL_DOCUMENTS, + methods::APPEND, + methods::QUERY_SOURCE, + methods::DRILL_DOWN, + methods::SEAL, + methods::CASCADE, + methods::ENTITIES, + methods::ENTITY_EDGES, + methods::TOUCH_ENTITIES, + methods::KV_GET, + methods::KV_PUT, + methods::KV_DELETE, + methods::KV_LIST, + methods::RELATIONS, + methods::PUT_RELATION, + methods::CAPTURE_SNAPSHOT, + methods::SNAPSHOTS, + methods::DIFF, + methods::GOALS, + methods::SET_GOALS, + methods::TOOL_RULES, + methods::PUT_TOOL_RULE, + methods::DELETE_TOOL_RULE, + methods::ACCEPT_SOURCE_ITEMS, + methods::FORGET_SOURCE, + methods::REEMBED, + methods::COMPACT, + methods::CONSOLIDATE, + methods::DOCTOR, + methods::LIST_PEOPLE, + methods::GET_PERSON, + methods::RESOLVE_HANDLE, + methods::ADD_HANDLE_ALIAS, + methods::SCORE_PERSON, + methods::RECORD_INTERACTION, + methods::SEED_FROM_ADDRESS_BOOK, + methods::LIST_CHUNKS, + methods::GET_CHUNK, + methods::CHUNK_DETAIL, + methods::STORAGE_KINDS, + methods::CHUNK_EMBEDDINGS, + methods::FAST_RETRIEVE, + methods::COVER_WINDOW, + methods::LIST_ACTIVE_FACETS, + methods::LIST_ALL_FACETS, + methods::GET_FACET, + methods::FACETS_BY_TYPE, + methods::INSERT_TURN, + methods::SESSION_TURNS, + methods::OPEN_SEGMENT, + methods::CREATE_SEGMENT, + methods::APPEND_TURN, + methods::CLOSE_SEGMENT, + methods::SET_SEGMENT_SUMMARY, + methods::UPSERT_SEGMENT_EMBEDDING, + methods::UPSERT_FACET, + methods::UPSERT_PROVIDER_FACET, + methods::SET_FACET_USER_STATE, + methods::DELETE_FACET, + methods::DELETE_FACET_BY_ID, + methods::DROP_FACETS_BELOW, + methods::WORKFLOW_IDENTITY_MATCHES, + methods::RETRIEVE_SOURCE, + methods::RETRIEVE_CHILDREN, + methods::RETRIEVE_LEAVES, + methods::RECALL_NAMESPACE_SCORED, + methods::SEARCH_ENTITIES, +]; + +#[cfg(test)] +mod test; diff --git a/crates/tinymemory-bus/src/names/test.rs b/crates/tinymemory-bus/src/names/test.rs new file mode 100644 index 0000000..e40ff85 --- /dev/null +++ b/crates/tinymemory-bus/src/names/test.rs @@ -0,0 +1,61 @@ +//! Tests for the member-name table. +//! +//! These are pinning tests, not behavioural ones. A member name is a string +//! that only fails at runtime, in a host, as an `UnknownMethod` — so the value +//! here is in catching a typo or a duplicate at `cargo test` time in this +//! crate, before the module or a host ever sees it. + +use super::{methods, BUS_NAME, METHODS, OBJECT_PATH}; + +#[test] +fn the_object_identity_is_pinned() { + // Changing either of these breaks every deployed host at once, so they are + // spelled out here rather than derived from anything. + assert_eq!(BUS_NAME, "ai.tinyhumans.tinymemory.Memory"); + assert_eq!(OBJECT_PATH, "/ai/tinyhumans/tinymemory/Memory"); +} + +#[test] +fn no_member_name_appears_twice() { + let mut sorted = METHODS; + sorted.sort_unstable(); + let mut unique = sorted.to_vec(); + unique.dedup(); + assert_eq!( + unique.len(), + METHODS.len(), + "a member name is listed more than once" + ); +} + +#[test] +fn every_member_name_is_pascal_case() { + // `#[tinybus::interface]` derives a member from its method identifier with + // `pascal_case`, so anything else in this table is a hand-written name that + // will not match what the module actually serves. + for member in METHODS { + let mut chars = member.chars(); + let first = chars.next().unwrap_or('_'); + assert!( + first.is_ascii_uppercase(), + "{member} does not start with an uppercase letter" + ); + assert!( + member.chars().all(|c| c.is_ascii_alphanumeric()), + "{member} is not alphanumeric" + ); + } +} + +#[test] +fn the_constants_and_the_table_are_the_same_set() { + // A spot check in both directions: a constant that is not in the table + // would be invisible to the module's drift assertion, and a table entry + // with no constant is a name a caller has to spell by hand. + assert!(METHODS.contains(&methods::STORE)); + assert!(METHODS.contains(&methods::OPEN_STORE)); + assert!(METHODS.contains(&methods::WORKFLOW_IDENTITY_MATCHES)); + assert_eq!(methods::STORE, "Store"); + assert_eq!(methods::OPEN_STORE, "OpenStore"); + assert_eq!(methods::WORKFLOW_IDENTITY_MATCHES, "WorkflowIdentityMatches"); +} diff --git a/crates/tinymemory-bus/src/types/mod.rs b/crates/tinymemory-bus/src/types/mod.rs new file mode 100644 index 0000000..81c0ed0 --- /dev/null +++ b/crates/tinymemory-bus/src/types/mod.rs @@ -0,0 +1,53 @@ +//! Every value type that crosses the bus, re-exported from the contract crate. +//! +//! # These are re-exports, deliberately, and not definitions +//! +//! The obvious reading of "a crate that holds the bus types" is a crate that +//! *defines* them. That would be wrong here, and the repository already +//! documents why in the root manifest: when `tinymemory-api` was resolved +//! twice, `MemoryCategory` from one copy was not the same type as +//! `MemoryCategory` from the other, and the mismatch only showed up at the +//! seam. Defining a second set of structurally identical types here would +//! reproduce that on purpose: the module would serve `tinymemory_api::` +//! types, the host would hold `tinymemory_bus::` ones, and every call site +//! would need a conversion whose correctness nothing checks. +//! +//! So one definition, in `tinymemory-api`, surfaced here. A host that depends +//! on this crate gets exactly the types the module serves — the same types, +//! not equivalents. +//! +//! # Why the host does not just depend on `tinymemory-api` +//! +//! It could, and it would compile. But `tinymemory-api` is the *driver* +//! contract: it also carries `MemoryProvider` and its capability traits, the +//! mandatory-family composition, the null driver, and the `host::` config +//! sections. A host that loads the module implements none of those — it makes +//! calls. This crate is the subset that crosses a frame, so what a host +//! compiles against is what it can actually send and receive. +//! +//! The grouping below mirrors the capability families in [`crate::calls`]. + +pub use tinymemory_api::capabilities::{Capabilities, Capability}; +pub use tinymemory_api::chunks::{Chunk, Metadata, SourceRef}; +pub use tinymemory_api::error::{MemoryError}; +pub use tinymemory_api::goals::{GoalItem, GoalsDoc}; +pub use tinymemory_api::health::{MemoryHealth}; +pub use tinymemory_api::provider::chunks::{ChunkDetail, ChunkEmbedding, ChunkQuery}; +pub use tinymemory_api::provider::episodic::{ConversationSegment, EpisodicTurn}; +pub use tinymemory_api::provider::people::{AddressBookSeedOutcome, PersonHandle, PersonInteraction, PersonRecord, PersonScore, RankedPerson, ResolvedPerson}; +pub use tinymemory_api::provider::profile::{FacetType, ProfileFacet, UserState}; +pub use tinymemory_api::provider::retrieval::{CoverWindowQuery, EntityMatch, FastRetrieveQuery, RetrievalHit, RetrievalResponse, SourceRetrievalQuery}; +pub use tinymemory_api::provider::types::{DiffReport, EntityHit, ExportPage, ExportRecord, ImportOutcome, IngestItem, IngestOutcome, MaintenanceReport, SnapshotRef, SourceItem, SourceScope}; +pub use tinymemory_api::recall::{OwnedRecallOpts}; +pub use tinymemory_api::tool_memory::{ToolMemoryRule}; +pub use tinymemory_api::tree::{IngestRequest, QueryResult, TreeStatus}; +pub use tinymemory_api::types::{GraphRelationRecord, MemoryCategory, MemoryEntry, MemoryKvRecord, MemoryTaint, NamespaceDocumentInput, NamespaceMemoryHit, NamespaceRetrievalContext, NamespaceSummary, StoredMemoryDocument}; + +/// `serde_json::Value`, which three document methods return verbatim. +/// +/// `ListDocuments` and `DeleteDocument` answer with a driver-shaped JSON +/// document rather than a typed record, so a host has to hold the untyped +/// value. Re-exported here so it arrives from the same place as everything +/// else on the wire and a host does not have to match `serde_json` versions +/// by hand. +pub use serde_json::Value as JsonValue; diff --git a/crates/tinymemory-bus/src/wire/mod.rs b/crates/tinymemory-bus/src/wire/mod.rs new file mode 100644 index 0000000..43adb65 --- /dev/null +++ b/crates/tinymemory-bus/src/wire/mod.rs @@ -0,0 +1,41 @@ +//! How a failed call comes back, and how a host turns it into a +//! [`MemoryError`] again. +//! +//! A `TinyBus` error is a name and a message. The name is the contract; the +//! message is for a human and must never carry a namespace key, an entry's +//! content, a recall query, a credential or an absolute path. +//! +//! The table that maps names to [`MemoryError`] variants lives in +//! `tinymemory-api` and is used by **both** ends — the module maps out, the +//! host maps back. It is re-exported here rather than restated for the same +//! reason the payload types are: two copies of a name table drift, and the +//! symptom of drift is a security-relevant `PathEscape` silently reclassified +//! as a caller mistake. +//! +//! ``` +//! use tinymemory_bus::wire; +//! use tinymemory_bus::types::MemoryError; +//! +//! // What a host does with the `(name, message)` pair a failed call returns. +//! let recovered = wire::from_wire(wire::NOT_FOUND, "no such source"); +//! assert!(matches!(recovered, MemoryError::NotFound(_))); +//! ``` +//! +//! # An unrecognised name is a backend failure, never a caller mistake +//! +//! [`from_wire`] maps a name it does not know to [`MemoryError::Other`]. A +//! module newer than the host's build may name an error this table has no +//! variant for, and answering "your input was wrong" when it was not sends a +//! caller into a rewrite loop over something already correct. +//! +//! [`MemoryError`]: tinymemory_api::error::MemoryError +//! [`MemoryError::Other`]: tinymemory_api::error::MemoryError::Other +//! [`MemoryError::NotFound`]: tinymemory_api::error::MemoryError::NotFound + +pub use tinymemory_api::wire::{ + from_wire, wire_message, wire_name, BACKEND, BUDGET_EXCEEDED, INVALID, IO, NOT_FOUND, OTHER, + PATH_ESCAPE, SERDE, TIMEOUT, UNAUTHORIZED, UNAVAILABLE, UNREACHABLE, UNSUPPORTED, +}; + +#[cfg(test)] +mod test; diff --git a/crates/tinymemory-bus/src/wire/test.rs b/crates/tinymemory-bus/src/wire/test.rs new file mode 100644 index 0000000..9ff97f5 --- /dev/null +++ b/crates/tinymemory-bus/src/wire/test.rs @@ -0,0 +1,52 @@ +//! The re-exported error table still round-trips from this crate's paths. +//! +//! `tinymemory_api::wire_tests` pins the table itself. What is checked here is +//! that the re-export surfaces the whole of it — a name constant that failed to +//! come across would leave a host unable to recognise that error class, and a +//! missing `pub use` is invisible until someone reaches for it. + +use super::{from_wire, wire_name}; +use crate::types::MemoryError; + +#[test] +fn every_name_constant_is_reachable_from_this_crate() { + let names = [ + super::NOT_FOUND, + super::INVALID, + super::BUDGET_EXCEEDED, + super::PATH_ESCAPE, + super::IO, + super::SERDE, + super::UNSUPPORTED, + super::OTHER, + super::UNAUTHORIZED, + super::UNREACHABLE, + super::TIMEOUT, + super::UNAVAILABLE, + super::BACKEND, + ]; + for name in names { + assert!( + name.starts_with("ai.tinyhumans.tinymemory.Error."), + "{name} is not under the contract's error namespace" + ); + } +} + +#[test] +fn a_named_error_round_trips_through_the_re_exports() { + let recovered = from_wire(super::PATH_ESCAPE, "symlink leaves workspace"); + assert!(matches!(recovered, MemoryError::PathEscape(_))); + // Back out again under the same name: the two directions are the same + // table, which is the property that keeps the ends from drifting. + assert_eq!(wire_name(&recovered), super::PATH_ESCAPE); +} + +#[test] +fn an_unknown_name_is_a_backend_failure_not_a_caller_mistake() { + // A module newer than this build may name an error this table has no + // variant for. Reporting that as `Invalid` would tell a caller its input + // was wrong when it was not. + let recovered = from_wire("ai.tinyhumans.tinymemory.Error.FromTheFuture", "…"); + assert!(matches!(recovered, MemoryError::Other(_))); +} From ad5e3ec73fe23f0530ceb7a8c3b557223016ab5e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:37:15 +0300 Subject: [PATCH 02/35] chore(deps): add tinymemory-bus crate to workspace lockfile The Cargo.lock file is updated to include the new tinymemory-bus crate and its dependencies, which are needed for the bus module that was added to the workspace. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.lock | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 156fa1f..7698b9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1915,6 +1915,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "tinymemory-bus" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.20", + "tinymemory-api", +] + [[package]] name = "tinymemory-conformance" version = "0.1.0" From 7cc9a5db27e11a4556d2d6f2a54cad13ac855a20 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:37:37 +0300 Subject: [PATCH 03/35] refactor(calls): replace external API types with local re-exports Replace all direct imports from the `tinymemory_api` crate with equivalent types re-exported through the local `crate::types` module. This change decouples the bus call definitions from the external API crate, allowing the type definitions to be managed internally and reducing the dependency surface for the bus module. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/calls/chunks.rs | 17 +++++------ crates/tinymemory-bus/src/calls/core.rs | 15 +++++----- crates/tinymemory-bus/src/calls/documents.rs | 11 ++++--- crates/tinymemory-bus/src/calls/driver.rs | 8 ++--- crates/tinymemory-bus/src/calls/episodic.rs | 9 +++--- crates/tinymemory-bus/src/calls/goals.rs | 7 ++--- crates/tinymemory-bus/src/calls/graph.rs | 19 +++++------- crates/tinymemory-bus/src/calls/ingest.rs | 11 ++++--- .../tinymemory-bus/src/calls/maintenance.rs | 11 ++++--- crates/tinymemory-bus/src/calls/people.rs | 19 ++++++------ .../tinymemory-bus/src/calls/portability.rs | 9 +++--- crates/tinymemory-bus/src/calls/profile.rs | 19 ++++++------ crates/tinymemory-bus/src/calls/recall.rs | 13 ++++---- crates/tinymemory-bus/src/calls/retrieval.rs | 30 +++++++++---------- crates/tinymemory-bus/src/calls/sources.rs | 16 +++++----- .../tinymemory-bus/src/calls/tool_memory.rs | 7 ++--- crates/tinymemory-bus/src/calls/tree.rs | 17 +++++------ 17 files changed, 105 insertions(+), 133 deletions(-) diff --git a/crates/tinymemory-bus/src/calls/chunks.rs b/crates/tinymemory-bus/src/calls/chunks.rs index 14cf8a8..2f0a945 100644 --- a/crates/tinymemory-bus/src/calls/chunks.rs +++ b/crates/tinymemory-bus/src/calls/chunks.rs @@ -5,13 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::chunks::Chunk; -use tinymemory_api::provider::chunks::{ChunkDetail, ChunkEmbedding, ChunkQuery}; -use tinymemory_api::provider::types::SourceScope; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `ListChunks`. /// @@ -22,15 +19,15 @@ use crate::names::methods; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ListChunks { /// The `query` argument — wire position 0. - pub query: ChunkQuery, + pub query: types::ChunkQuery, /// The `scope` argument — wire position 1. - pub scope: Option, + pub scope: Option, } impl BusCall for ListChunks { const METHOD: &'static str = methods::LIST_CHUNKS; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.query, self.scope)).map_err(Error::Encode) @@ -55,7 +52,7 @@ pub struct GetChunk { impl BusCall for GetChunk { const METHOD: &'static str = methods::GET_CHUNK; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.chunk_id,)).map_err(Error::Encode) @@ -74,7 +71,7 @@ pub struct ChunkDetail { impl BusCall for ChunkDetail { const METHOD: &'static str = methods::CHUNK_DETAIL; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.chunk_id,)).map_err(Error::Encode) @@ -116,7 +113,7 @@ pub struct ChunkEmbeddings { impl BusCall for ChunkEmbeddings { const METHOD: &'static str = methods::CHUNK_EMBEDDINGS; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.chunk_ids, self.model_signature)).map_err(Error::Encode) diff --git a/crates/tinymemory-bus/src/calls/core.rs b/crates/tinymemory-bus/src/calls/core.rs index 48c40af..0373e30 100644 --- a/crates/tinymemory-bus/src/calls/core.rs +++ b/crates/tinymemory-bus/src/calls/core.rs @@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::types::{MemoryCategory, MemoryEntry, MemoryTaint, NamespaceSummary}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `Store`. /// @@ -28,11 +27,11 @@ pub struct Store { /// The `content` argument — wire position 2. pub content: String, /// The `category` argument — wire position 3. - pub category: MemoryCategory, + pub category: types::MemoryCategory, /// The `session_id` argument — wire position 4. pub session_id: Option, /// The `taint` argument — wire position 5. - pub taint: MemoryTaint, + pub taint: types::MemoryTaint, } impl BusCall for Store { @@ -59,7 +58,7 @@ pub struct Get { impl BusCall for Get { const METHOD: &'static str = methods::GET; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) @@ -100,7 +99,7 @@ pub struct List { /// The `namespace` argument — wire position 0. pub namespace: Option, /// The `category` argument — wire position 1. - pub category: Option, + pub category: Option, /// The `session_id` argument — wire position 2. pub session_id: Option, } @@ -108,7 +107,7 @@ pub struct List { impl BusCall for List { const METHOD: &'static str = methods::LIST; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.category, self.session_id)).map_err(Error::Encode) @@ -126,7 +125,7 @@ pub struct Namespaces; impl BusCall for Namespaces { const METHOD: &'static str = methods::NAMESPACES; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) diff --git a/crates/tinymemory-bus/src/calls/documents.rs b/crates/tinymemory-bus/src/calls/documents.rs index 02e5940..17b0a2f 100644 --- a/crates/tinymemory-bus/src/calls/documents.rs +++ b/crates/tinymemory-bus/src/calls/documents.rs @@ -5,17 +5,16 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::types::{NamespaceDocumentInput, NamespaceRetrievalContext, StoredMemoryDocument}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `PutDocument`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PutDocument { /// The `input` argument — wire position 0. - pub input: NamespaceDocumentInput, + pub input: types::NamespaceDocumentInput, } impl BusCall for PutDocument { @@ -40,7 +39,7 @@ pub struct GetDocument { impl BusCall for GetDocument { const METHOD: &'static str = methods::GET_DOCUMENT; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) @@ -130,7 +129,7 @@ pub struct QueryDocuments { impl BusCall for QueryDocuments { const METHOD: &'static str = methods::QUERY_DOCUMENTS; - type Response = NamespaceRetrievalContext; + type Response = types::NamespaceRetrievalContext; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.query, self.limit)).map_err(Error::Encode) @@ -149,7 +148,7 @@ pub struct RecallDocuments { impl BusCall for RecallDocuments { const METHOD: &'static str = methods::RECALL_DOCUMENTS; - type Response = NamespaceRetrievalContext; + type Response = types::NamespaceRetrievalContext; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.limit)).map_err(Error::Encode) diff --git a/crates/tinymemory-bus/src/calls/driver.rs b/crates/tinymemory-bus/src/calls/driver.rs index 2655351..df3cebc 100644 --- a/crates/tinymemory-bus/src/calls/driver.rs +++ b/crates/tinymemory-bus/src/calls/driver.rs @@ -5,12 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::capabilities::Capabilities; -use tinymemory_api::health::MemoryHealth; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `DriverId`. /// @@ -37,7 +35,7 @@ pub struct Capabilities; impl BusCall for Capabilities { const METHOD: &'static str = methods::CAPABILITIES; - type Response = Capabilities; + type Response = types::Capabilities; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) @@ -55,7 +53,7 @@ pub struct Health; impl BusCall for Health { const METHOD: &'static str = methods::HEALTH; - type Response = MemoryHealth; + type Response = types::MemoryHealth; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) diff --git a/crates/tinymemory-bus/src/calls/episodic.rs b/crates/tinymemory-bus/src/calls/episodic.rs index 8c50b0a..5e5a781 100644 --- a/crates/tinymemory-bus/src/calls/episodic.rs +++ b/crates/tinymemory-bus/src/calls/episodic.rs @@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::episodic::{ConversationSegment, EpisodicTurn}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `InsertTurn`. /// @@ -17,7 +16,7 @@ use crate::names::methods; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InsertTurn { /// The `turn` argument — wire position 0. - pub turn: EpisodicTurn, + pub turn: types::EpisodicTurn, } impl BusCall for InsertTurn { @@ -42,7 +41,7 @@ pub struct SessionTurns { impl BusCall for SessionTurns { const METHOD: &'static str = methods::SESSION_TURNS; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.session_id,)).map_err(Error::Encode) @@ -61,7 +60,7 @@ pub struct OpenSegment { impl BusCall for OpenSegment { const METHOD: &'static str = methods::OPEN_SEGMENT; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.session_id,)).map_err(Error::Encode) diff --git a/crates/tinymemory-bus/src/calls/goals.rs b/crates/tinymemory-bus/src/calls/goals.rs index cca6ef9..de3953d 100644 --- a/crates/tinymemory-bus/src/calls/goals.rs +++ b/crates/tinymemory-bus/src/calls/goals.rs @@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::goals::GoalsDoc; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `Goals`. /// @@ -20,7 +19,7 @@ pub struct Goals; impl BusCall for Goals { const METHOD: &'static str = methods::GOALS; - type Response = GoalsDoc; + type Response = types::GoalsDoc; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) @@ -31,7 +30,7 @@ impl BusCall for Goals { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SetGoals { /// The `goals` argument — wire position 0. - pub goals: GoalsDoc, + pub goals: types::GoalsDoc, } impl BusCall for SetGoals { diff --git a/crates/tinymemory-bus/src/calls/graph.rs b/crates/tinymemory-bus/src/calls/graph.rs index c8cf9b4..24255d6 100644 --- a/crates/tinymemory-bus/src/calls/graph.rs +++ b/crates/tinymemory-bus/src/calls/graph.rs @@ -5,13 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::retrieval::EntityMatch; -use tinymemory_api::provider::types::EntityHit; -use tinymemory_api::types::{GraphRelationRecord, MemoryKvRecord}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `Entities`. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -27,7 +24,7 @@ pub struct Entities { impl BusCall for Entities { const METHOD: &'static str = methods::ENTITIES; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.query, self.limit)).map_err(Error::Encode) @@ -48,7 +45,7 @@ pub struct EntityEdges { impl BusCall for EntityEdges { const METHOD: &'static str = methods::ENTITY_EDGES; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.entity_id, self.limit)).map_err(Error::Encode) @@ -88,7 +85,7 @@ pub struct SearchEntities { impl BusCall for SearchEntities { const METHOD: &'static str = methods::SEARCH_ENTITIES; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.query, self.kinds, self.limit)).map_err(Error::Encode) @@ -111,7 +108,7 @@ pub struct Relations { impl BusCall for Relations { const METHOD: &'static str = methods::RELATIONS; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.subject, self.predicate, self.limit)).map_err(Error::Encode) @@ -122,7 +119,7 @@ impl BusCall for Relations { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PutRelation { /// The `relation` argument — wire position 0. - pub relation: GraphRelationRecord, + pub relation: types::GraphRelationRecord, } impl BusCall for PutRelation { @@ -147,7 +144,7 @@ pub struct KvGet { impl BusCall for KvGet { const METHOD: &'static str = methods::KV_GET; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) @@ -208,7 +205,7 @@ pub struct KvList { impl BusCall for KvList { const METHOD: &'static str = methods::KV_LIST; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.prefix, self.limit)).map_err(Error::Encode) diff --git a/crates/tinymemory-bus/src/calls/ingest.rs b/crates/tinymemory-bus/src/calls/ingest.rs index 6c4626b..f90421f 100644 --- a/crates/tinymemory-bus/src/calls/ingest.rs +++ b/crates/tinymemory-bus/src/calls/ingest.rs @@ -5,23 +5,22 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::types::{IngestItem, IngestOutcome}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `IngestDocument`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IngestDocument { /// The `item` argument — wire position 0. - pub item: IngestItem, + pub item: types::IngestItem, } impl BusCall for IngestDocument { const METHOD: &'static str = methods::INGEST_DOCUMENT; - type Response = IngestOutcome; + type Response = types::IngestOutcome; fn into_args(self) -> crate::Result { serde_json::to_value((self.item,)).map_err(Error::Encode) @@ -32,13 +31,13 @@ impl BusCall for IngestDocument { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IngestChat { /// The `messages` argument — wire position 0. - pub messages: Vec, + pub messages: Vec, } impl BusCall for IngestChat { const METHOD: &'static str = methods::INGEST_CHAT; - type Response = IngestOutcome; + type Response = types::IngestOutcome; fn into_args(self) -> crate::Result { serde_json::to_value((self.messages,)).map_err(Error::Encode) diff --git a/crates/tinymemory-bus/src/calls/maintenance.rs b/crates/tinymemory-bus/src/calls/maintenance.rs index 333f072..edac055 100644 --- a/crates/tinymemory-bus/src/calls/maintenance.rs +++ b/crates/tinymemory-bus/src/calls/maintenance.rs @@ -5,10 +5,9 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::types::MaintenanceReport; - use crate::calls::BusCall; use crate::names::methods; +use crate::types; /// Arguments for `Reembed`. /// @@ -19,7 +18,7 @@ pub struct Reembed; impl BusCall for Reembed { const METHOD: &'static str = methods::REEMBED; - type Response = MaintenanceReport; + type Response = types::MaintenanceReport; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) @@ -35,7 +34,7 @@ pub struct Compact; impl BusCall for Compact { const METHOD: &'static str = methods::COMPACT; - type Response = MaintenanceReport; + type Response = types::MaintenanceReport; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) @@ -51,7 +50,7 @@ pub struct Consolidate; impl BusCall for Consolidate { const METHOD: &'static str = methods::CONSOLIDATE; - type Response = MaintenanceReport; + type Response = types::MaintenanceReport; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) @@ -67,7 +66,7 @@ pub struct Doctor; impl BusCall for Doctor { const METHOD: &'static str = methods::DOCTOR; - type Response = MaintenanceReport; + type Response = types::MaintenanceReport; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) diff --git a/crates/tinymemory-bus/src/calls/people.rs b/crates/tinymemory-bus/src/calls/people.rs index 4f36bf2..7e35cbe 100644 --- a/crates/tinymemory-bus/src/calls/people.rs +++ b/crates/tinymemory-bus/src/calls/people.rs @@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::people::{AddressBookSeedOutcome, PersonHandle, PersonInteraction, PersonRecord, PersonScore, RankedPerson, ResolvedPerson}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `ListPeople`. /// @@ -28,7 +27,7 @@ pub struct ListPeople { impl BusCall for ListPeople { const METHOD: &'static str = methods::LIST_PEOPLE; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.limit,)).map_err(Error::Encode) @@ -45,7 +44,7 @@ pub struct GetPerson { impl BusCall for GetPerson { const METHOD: &'static str = methods::GET_PERSON; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.person_id,)).map_err(Error::Encode) @@ -56,7 +55,7 @@ impl BusCall for GetPerson { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResolveHandle { /// The `handle` argument — wire position 0. - pub handle: PersonHandle, + pub handle: types::PersonHandle, /// The `create_if_missing` argument — wire position 1. pub create_if_missing: bool, } @@ -64,7 +63,7 @@ pub struct ResolveHandle { impl BusCall for ResolveHandle { const METHOD: &'static str = methods::RESOLVE_HANDLE; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.handle, self.create_if_missing)).map_err(Error::Encode) @@ -77,7 +76,7 @@ pub struct AddHandleAlias { /// The `person_id` argument — wire position 0. pub person_id: String, /// The `handle` argument — wire position 1. - pub handle: PersonHandle, + pub handle: types::PersonHandle, } impl BusCall for AddHandleAlias { @@ -100,7 +99,7 @@ pub struct ScorePerson { impl BusCall for ScorePerson { const METHOD: &'static str = methods::SCORE_PERSON; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.person_id,)).map_err(Error::Encode) @@ -111,7 +110,7 @@ impl BusCall for ScorePerson { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecordInteraction { /// The `interaction` argument — wire position 0. - pub interaction: PersonInteraction, + pub interaction: types::PersonInteraction, } impl BusCall for RecordInteraction { @@ -133,7 +132,7 @@ pub struct SeedFromAddressBook; impl BusCall for SeedFromAddressBook { const METHOD: &'static str = methods::SEED_FROM_ADDRESS_BOOK; - type Response = AddressBookSeedOutcome; + type Response = types::AddressBookSeedOutcome; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) diff --git a/crates/tinymemory-bus/src/calls/portability.rs b/crates/tinymemory-bus/src/calls/portability.rs index a43b4a7..4878aa5 100644 --- a/crates/tinymemory-bus/src/calls/portability.rs +++ b/crates/tinymemory-bus/src/calls/portability.rs @@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::types::{ExportPage, ExportRecord, ImportOutcome}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `ExportPage`. /// @@ -25,7 +24,7 @@ pub struct ExportPage { impl BusCall for ExportPage { const METHOD: &'static str = methods::EXPORT_PAGE; - type Response = ExportPage; + type Response = types::ExportPage; fn into_args(self) -> crate::Result { serde_json::to_value((self.cursor, self.limit)).map_err(Error::Encode) @@ -41,13 +40,13 @@ impl BusCall for ExportPage { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImportRecords { /// The `records` argument — wire position 0. - pub records: Vec, + pub records: Vec, } impl BusCall for ImportRecords { const METHOD: &'static str = methods::IMPORT_RECORDS; - type Response = ImportOutcome; + type Response = types::ImportOutcome; fn into_args(self) -> crate::Result { serde_json::to_value((self.records,)).map_err(Error::Encode) diff --git a/crates/tinymemory-bus/src/calls/profile.rs b/crates/tinymemory-bus/src/calls/profile.rs index 7ff4bc9..eb7b8c4 100644 --- a/crates/tinymemory-bus/src/calls/profile.rs +++ b/crates/tinymemory-bus/src/calls/profile.rs @@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::profile::{FacetType, ProfileFacet, UserState}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `ListActiveFacets`. /// @@ -20,7 +19,7 @@ pub struct ListActiveFacets; impl BusCall for ListActiveFacets { const METHOD: &'static str = methods::LIST_ACTIVE_FACETS; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) @@ -36,7 +35,7 @@ pub struct ListAllFacets; impl BusCall for ListAllFacets { const METHOD: &'static str = methods::LIST_ALL_FACETS; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { Ok(Value::Array(Vec::new())) @@ -53,7 +52,7 @@ pub struct GetFacet { impl BusCall for GetFacet { const METHOD: &'static str = methods::GET_FACET; - type Response = Option; + type Response = Option; fn into_args(self) -> crate::Result { serde_json::to_value((self.key,)).map_err(Error::Encode) @@ -64,13 +63,13 @@ impl BusCall for GetFacet { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FacetsByType { /// The `facet_type` argument — wire position 0. - pub facet_type: FacetType, + pub facet_type: types::FacetType, } impl BusCall for FacetsByType { const METHOD: &'static str = methods::FACETS_BY_TYPE; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.facet_type,)).map_err(Error::Encode) @@ -81,7 +80,7 @@ impl BusCall for FacetsByType { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpsertFacet { /// The `facet` argument — wire position 0. - pub facet: ProfileFacet, + pub facet: types::ProfileFacet, } impl BusCall for UpsertFacet { @@ -100,7 +99,7 @@ pub struct UpsertProviderFacet { /// The `facet_id` argument — wire position 0. pub facet_id: String, /// The `facet_type` argument — wire position 1. - pub facet_type: FacetType, + pub facet_type: types::FacetType, /// The `key` argument — wire position 2. pub key: String, /// The `value` argument — wire position 3. @@ -129,7 +128,7 @@ pub struct SetFacetUserState { /// The `key` argument — wire position 0. pub key: String, /// The `user_state` argument — wire position 1. - pub user_state: UserState, + pub user_state: types::UserState, } impl BusCall for SetFacetUserState { diff --git a/crates/tinymemory-bus/src/calls/recall.rs b/crates/tinymemory-bus/src/calls/recall.rs index 06a254c..0ca9a8c 100644 --- a/crates/tinymemory-bus/src/calls/recall.rs +++ b/crates/tinymemory-bus/src/calls/recall.rs @@ -5,13 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::types::SourceScope; -use tinymemory_api::recall::OwnedRecallOpts; -use tinymemory_api::types::{MemoryEntry, NamespaceMemoryHit}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `Recall`. /// @@ -28,15 +25,15 @@ pub struct Recall { /// The `limit` argument — wire position 1. pub limit: usize, /// The `opts` argument — wire position 2. - pub opts: OwnedRecallOpts, + pub opts: types::OwnedRecallOpts, /// The `scope` argument — wire position 3. - pub scope: Option, + pub scope: Option, } impl BusCall for Recall { const METHOD: &'static str = methods::RECALL; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.query, self.limit, self.opts, self.scope)).map_err(Error::Encode) @@ -59,7 +56,7 @@ pub struct RecallNamespaceScored { impl BusCall for RecallNamespaceScored { const METHOD: &'static str = methods::RECALL_NAMESPACE_SCORED; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.query, self.limit, self.exclude_session_id)).map_err(Error::Encode) diff --git a/crates/tinymemory-bus/src/calls/retrieval.rs b/crates/tinymemory-bus/src/calls/retrieval.rs index 3ae72c2..5500b68 100644 --- a/crates/tinymemory-bus/src/calls/retrieval.rs +++ b/crates/tinymemory-bus/src/calls/retrieval.rs @@ -5,12 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::retrieval::{CoverWindowQuery, FastRetrieveQuery, RetrievalHit, RetrievalResponse, SourceRetrievalQuery}; -use tinymemory_api::provider::types::SourceScope; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `FastRetrieve`. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -18,15 +16,15 @@ pub struct FastRetrieve { /// The `query` argument — wire position 0. pub query: String, /// The `options` argument — wire position 1. - pub options: FastRetrieveQuery, + pub options: types::FastRetrieveQuery, /// The `scope` argument — wire position 2. - pub scope: Option, + pub scope: Option, } impl BusCall for FastRetrieve { const METHOD: &'static str = methods::FAST_RETRIEVE; - type Response = RetrievalResponse; + type Response = types::RetrievalResponse; fn into_args(self) -> crate::Result { serde_json::to_value((self.query, self.options, self.scope)).map_err(Error::Encode) @@ -37,15 +35,15 @@ impl BusCall for FastRetrieve { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CoverWindow { /// The `window` argument — wire position 0. - pub window: CoverWindowQuery, + pub window: types::CoverWindowQuery, /// The `scope` argument — wire position 1. - pub scope: Option, + pub scope: Option, } impl BusCall for CoverWindow { const METHOD: &'static str = methods::COVER_WINDOW; - type Response = RetrievalResponse; + type Response = types::RetrievalResponse; fn into_args(self) -> crate::Result { serde_json::to_value((self.window, self.scope)).map_err(Error::Encode) @@ -56,15 +54,15 @@ impl BusCall for CoverWindow { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RetrieveSource { /// The `query` argument — wire position 0. - pub query: SourceRetrievalQuery, + pub query: types::SourceRetrievalQuery, /// The `scope` argument — wire position 1. - pub scope: Option, + pub scope: Option, } impl BusCall for RetrieveSource { const METHOD: &'static str = methods::RETRIEVE_SOURCE; - type Response = RetrievalResponse; + type Response = types::RetrievalResponse; fn into_args(self) -> crate::Result { serde_json::to_value((self.query, self.scope)).map_err(Error::Encode) @@ -83,13 +81,13 @@ pub struct RetrieveChildren { /// The `limit` argument — wire position 3. pub limit: Option, /// The `scope` argument — wire position 4. - pub scope: Option, + pub scope: Option, } impl BusCall for RetrieveChildren { const METHOD: &'static str = methods::RETRIEVE_CHILDREN; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.node_id, self.max_depth, self.query, self.limit, self.scope)).map_err(Error::Encode) @@ -102,13 +100,13 @@ pub struct RetrieveLeaves { /// The `chunk_ids` argument — wire position 0. pub chunk_ids: Vec, /// The `scope` argument — wire position 1. - pub scope: Option, + pub scope: Option, } impl BusCall for RetrieveLeaves { const METHOD: &'static str = methods::RETRIEVE_LEAVES; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.chunk_ids, self.scope)).map_err(Error::Encode) diff --git a/crates/tinymemory-bus/src/calls/sources.rs b/crates/tinymemory-bus/src/calls/sources.rs index f46dbcf..90d5419 100644 --- a/crates/tinymemory-bus/src/calls/sources.rs +++ b/crates/tinymemory-bus/src/calls/sources.rs @@ -5,12 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::provider::types::{DiffReport, IngestOutcome, SnapshotRef, SourceItem}; -use tinymemory_api::types::MemoryTaint; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `CaptureSnapshot`. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -22,7 +20,7 @@ pub struct CaptureSnapshot { impl BusCall for CaptureSnapshot { const METHOD: &'static str = methods::CAPTURE_SNAPSHOT; - type Response = SnapshotRef; + type Response = types::SnapshotRef; fn into_args(self) -> crate::Result { serde_json::to_value((self.source_id,)).map_err(Error::Encode) @@ -41,7 +39,7 @@ pub struct Snapshots { impl BusCall for Snapshots { const METHOD: &'static str = methods::SNAPSHOTS; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.source_id, self.limit)).map_err(Error::Encode) @@ -62,7 +60,7 @@ pub struct Diff { impl BusCall for Diff { const METHOD: &'static str = methods::DIFF; - type Response = DiffReport; + type Response = types::DiffReport; fn into_args(self) -> crate::Result { serde_json::to_value((self.source_id, self.from, self.to)).map_err(Error::Encode) @@ -77,15 +75,15 @@ pub struct AcceptSourceItems { /// The `source_kind` argument — wire position 1. pub source_kind: String, /// The `items` argument — wire position 2. - pub items: Vec, + pub items: Vec, /// The `taint` argument — wire position 3. - pub taint: MemoryTaint, + pub taint: types::MemoryTaint, } impl BusCall for AcceptSourceItems { const METHOD: &'static str = methods::ACCEPT_SOURCE_ITEMS; - type Response = IngestOutcome; + type Response = types::IngestOutcome; fn into_args(self) -> crate::Result { serde_json::to_value((self.source_id, self.source_kind, self.items, self.taint)).map_err(Error::Encode) diff --git a/crates/tinymemory-bus/src/calls/tool_memory.rs b/crates/tinymemory-bus/src/calls/tool_memory.rs index cbe9fd6..ddd181a 100644 --- a/crates/tinymemory-bus/src/calls/tool_memory.rs +++ b/crates/tinymemory-bus/src/calls/tool_memory.rs @@ -5,11 +5,10 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::tool_memory::ToolMemoryRule; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `ToolRules`. #[derive(Debug, Clone, Serialize, Deserialize)] @@ -21,7 +20,7 @@ pub struct ToolRules { impl BusCall for ToolRules { const METHOD: &'static str = methods::TOOL_RULES; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.tool_name,)).map_err(Error::Encode) @@ -32,7 +31,7 @@ impl BusCall for ToolRules { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PutToolRule { /// The `rule` argument — wire position 0. - pub rule: ToolMemoryRule, + pub rule: types::ToolMemoryRule, } impl BusCall for PutToolRule { diff --git a/crates/tinymemory-bus/src/calls/tree.rs b/crates/tinymemory-bus/src/calls/tree.rs index 04e5c5a..4a837d8 100644 --- a/crates/tinymemory-bus/src/calls/tree.rs +++ b/crates/tinymemory-bus/src/calls/tree.rs @@ -5,19 +5,16 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; -use tinymemory_api::chunks::Chunk; -use tinymemory_api::provider::types::SourceScope; -use tinymemory_api::tree::{IngestRequest, QueryResult, TreeStatus}; - use crate::calls::BusCall; use crate::error::Error; use crate::names::methods; +use crate::types; /// Arguments for `Append`. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Append { /// The `request` argument — wire position 0. - pub request: IngestRequest, + pub request: types::IngestRequest, } impl BusCall for Append { @@ -40,13 +37,13 @@ pub struct QuerySource { /// The `limit` argument — wire position 2. pub limit: usize, /// The `scope` argument — wire position 3. - pub scope: Option, + pub scope: Option, } impl BusCall for QuerySource { const METHOD: &'static str = methods::QUERY_SOURCE; - type Response = Vec; + type Response = Vec; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.source_id, self.limit, self.scope)).map_err(Error::Encode) @@ -65,7 +62,7 @@ pub struct DrillDown { impl BusCall for DrillDown { const METHOD: &'static str = methods::DRILL_DOWN; - type Response = QueryResult; + type Response = types::QueryResult; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace, self.node_id)).map_err(Error::Encode) @@ -82,7 +79,7 @@ pub struct Seal { impl BusCall for Seal { const METHOD: &'static str = methods::SEAL; - type Response = TreeStatus; + type Response = types::TreeStatus; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace,)).map_err(Error::Encode) @@ -99,7 +96,7 @@ pub struct Cascade { impl BusCall for Cascade { const METHOD: &'static str = methods::CASCADE; - type Response = TreeStatus; + type Response = types::TreeStatus; fn into_args(self) -> crate::Result { serde_json::to_value((self.namespace,)).map_err(Error::Encode) From 9630cd387b2e44f0b01d5faf0caa5107293a123e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:37:49 +0300 Subject: [PATCH 04/35] chore(tinymemory-bus): reformat long serde_json::to_value and use statements Reformat calls to serde_json::to_value across multiple bus call implementations and restructure use statements in types/mod.rs to improve readability. The changes break long argument lists and import paths into multiple lines, making the code easier to scan and maintain without altering any runtime behaviour. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/calls/core.rs | 13 +++++++-- crates/tinymemory-bus/src/calls/episodic.rs | 21 ++++++++++++-- crates/tinymemory-bus/src/calls/graph.rs | 3 +- crates/tinymemory-bus/src/calls/profile.rs | 11 +++++++- crates/tinymemory-bus/src/calls/recall.rs | 8 +++++- crates/tinymemory-bus/src/calls/retrieval.rs | 9 +++++- crates/tinymemory-bus/src/calls/sources.rs | 3 +- crates/tinymemory-bus/src/calls/test.rs | 16 +++++++++-- crates/tinymemory-bus/src/calls/tree.rs | 3 +- crates/tinymemory-bus/src/names/test.rs | 5 +++- crates/tinymemory-bus/src/types/mod.rs | 29 ++++++++++++++------ 11 files changed, 98 insertions(+), 23 deletions(-) diff --git a/crates/tinymemory-bus/src/calls/core.rs b/crates/tinymemory-bus/src/calls/core.rs index 0373e30..918f9b9 100644 --- a/crates/tinymemory-bus/src/calls/core.rs +++ b/crates/tinymemory-bus/src/calls/core.rs @@ -40,7 +40,15 @@ impl BusCall for Store { type Response = (); fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.key, self.content, self.category, self.session_id, self.taint)).map_err(Error::Encode) + serde_json::to_value(( + self.namespace, + self.key, + self.content, + self.category, + self.session_id, + self.taint, + )) + .map_err(Error::Encode) } } @@ -110,7 +118,8 @@ impl BusCall for List { type Response = Vec; fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.category, self.session_id)).map_err(Error::Encode) + serde_json::to_value((self.namespace, self.category, self.session_id)) + .map_err(Error::Encode) } } diff --git a/crates/tinymemory-bus/src/calls/episodic.rs b/crates/tinymemory-bus/src/calls/episodic.rs index 5e5a781..ee80e1c 100644 --- a/crates/tinymemory-bus/src/calls/episodic.rs +++ b/crates/tinymemory-bus/src/calls/episodic.rs @@ -90,7 +90,15 @@ impl BusCall for CreateSegment { type Response = (); fn into_args(self) -> crate::Result { - serde_json::to_value((self.segment_id, self.session_id, self.namespace, self.start_episodic_id, self.start_timestamp, self.now)).map_err(Error::Encode) + serde_json::to_value(( + self.segment_id, + self.session_id, + self.namespace, + self.start_episodic_id, + self.start_timestamp, + self.now, + )) + .map_err(Error::Encode) } } @@ -115,7 +123,8 @@ impl BusCall for AppendTurn { type Response = (); fn into_args(self) -> crate::Result { - serde_json::to_value((self.segment_id, self.episodic_id, self.timestamp, self.now)).map_err(Error::Encode) + serde_json::to_value((self.segment_id, self.episodic_id, self.timestamp, self.now)) + .map_err(Error::Encode) } } @@ -184,6 +193,12 @@ impl BusCall for UpsertSegmentEmbedding { type Response = (); fn into_args(self) -> crate::Result { - serde_json::to_value((self.segment_id, self.model_signature, self.embedding, self.created_at)).map_err(Error::Encode) + serde_json::to_value(( + self.segment_id, + self.model_signature, + self.embedding, + self.created_at, + )) + .map_err(Error::Encode) } } diff --git a/crates/tinymemory-bus/src/calls/graph.rs b/crates/tinymemory-bus/src/calls/graph.rs index 24255d6..ea9684c 100644 --- a/crates/tinymemory-bus/src/calls/graph.rs +++ b/crates/tinymemory-bus/src/calls/graph.rs @@ -111,7 +111,8 @@ impl BusCall for Relations { type Response = Vec; fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.subject, self.predicate, self.limit)).map_err(Error::Encode) + serde_json::to_value((self.namespace, self.subject, self.predicate, self.limit)) + .map_err(Error::Encode) } } diff --git a/crates/tinymemory-bus/src/calls/profile.rs b/crates/tinymemory-bus/src/calls/profile.rs index eb7b8c4..67f36b1 100644 --- a/crates/tinymemory-bus/src/calls/profile.rs +++ b/crates/tinymemory-bus/src/calls/profile.rs @@ -118,7 +118,16 @@ impl BusCall for UpsertProviderFacet { type Response = (); fn into_args(self) -> crate::Result { - serde_json::to_value((self.facet_id, self.facet_type, self.key, self.value, self.confidence, self.segment_id, self.observed_at)).map_err(Error::Encode) + serde_json::to_value(( + self.facet_id, + self.facet_type, + self.key, + self.value, + self.confidence, + self.segment_id, + self.observed_at, + )) + .map_err(Error::Encode) } } diff --git a/crates/tinymemory-bus/src/calls/recall.rs b/crates/tinymemory-bus/src/calls/recall.rs index 0ca9a8c..3969777 100644 --- a/crates/tinymemory-bus/src/calls/recall.rs +++ b/crates/tinymemory-bus/src/calls/recall.rs @@ -59,6 +59,12 @@ impl BusCall for RecallNamespaceScored { type Response = Vec; fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.query, self.limit, self.exclude_session_id)).map_err(Error::Encode) + serde_json::to_value(( + self.namespace, + self.query, + self.limit, + self.exclude_session_id, + )) + .map_err(Error::Encode) } } diff --git a/crates/tinymemory-bus/src/calls/retrieval.rs b/crates/tinymemory-bus/src/calls/retrieval.rs index 5500b68..78a9340 100644 --- a/crates/tinymemory-bus/src/calls/retrieval.rs +++ b/crates/tinymemory-bus/src/calls/retrieval.rs @@ -90,7 +90,14 @@ impl BusCall for RetrieveChildren { type Response = Vec; fn into_args(self) -> crate::Result { - serde_json::to_value((self.node_id, self.max_depth, self.query, self.limit, self.scope)).map_err(Error::Encode) + serde_json::to_value(( + self.node_id, + self.max_depth, + self.query, + self.limit, + self.scope, + )) + .map_err(Error::Encode) } } diff --git a/crates/tinymemory-bus/src/calls/sources.rs b/crates/tinymemory-bus/src/calls/sources.rs index 90d5419..b7c69ef 100644 --- a/crates/tinymemory-bus/src/calls/sources.rs +++ b/crates/tinymemory-bus/src/calls/sources.rs @@ -86,7 +86,8 @@ impl BusCall for AcceptSourceItems { type Response = types::IngestOutcome; fn into_args(self) -> crate::Result { - serde_json::to_value((self.source_id, self.source_kind, self.items, self.taint)).map_err(Error::Encode) + serde_json::to_value((self.source_id, self.source_kind, self.items, self.taint)) + .map_err(Error::Encode) } } diff --git a/crates/tinymemory-bus/src/calls/test.rs b/crates/tinymemory-bus/src/calls/test.rs index 45ed438..c7a8c5f 100644 --- a/crates/tinymemory-bus/src/calls/test.rs +++ b/crates/tinymemory-bus/src/calls/test.rs @@ -116,7 +116,10 @@ fn every_member_has_a_call_struct() { .filter(|member| !COVERED.contains(member)) .collect(); missing.sort_unstable(); - assert!(missing.is_empty(), "members with no call struct: {missing:?}"); + assert!( + missing.is_empty(), + "members with no call struct: {missing:?}" + ); } #[test] @@ -126,7 +129,10 @@ fn every_call_struct_names_a_known_member() { .filter(|member| !METHODS.contains(member)) .collect(); unknown.sort_unstable(); - assert!(unknown.is_empty(), "call structs naming no member: {unknown:?}"); + assert!( + unknown.is_empty(), + "call structs naming no member: {unknown:?}" + ); } #[test] @@ -135,7 +141,11 @@ fn no_member_is_covered_twice() { seen.sort_unstable(); let mut unique = seen.to_vec(); unique.dedup(); - assert_eq!(unique.len(), seen.len(), "two call structs name the same member"); + assert_eq!( + unique.len(), + seen.len(), + "two call structs name the same member" + ); } #[test] diff --git a/crates/tinymemory-bus/src/calls/tree.rs b/crates/tinymemory-bus/src/calls/tree.rs index 4a837d8..26dd41a 100644 --- a/crates/tinymemory-bus/src/calls/tree.rs +++ b/crates/tinymemory-bus/src/calls/tree.rs @@ -46,7 +46,8 @@ impl BusCall for QuerySource { type Response = Vec; fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.source_id, self.limit, self.scope)).map_err(Error::Encode) + serde_json::to_value((self.namespace, self.source_id, self.limit, self.scope)) + .map_err(Error::Encode) } } diff --git a/crates/tinymemory-bus/src/names/test.rs b/crates/tinymemory-bus/src/names/test.rs index e40ff85..7c67dbb 100644 --- a/crates/tinymemory-bus/src/names/test.rs +++ b/crates/tinymemory-bus/src/names/test.rs @@ -57,5 +57,8 @@ fn the_constants_and_the_table_are_the_same_set() { assert!(METHODS.contains(&methods::WORKFLOW_IDENTITY_MATCHES)); assert_eq!(methods::STORE, "Store"); assert_eq!(methods::OPEN_STORE, "OpenStore"); - assert_eq!(methods::WORKFLOW_IDENTITY_MATCHES, "WorkflowIdentityMatches"); + assert_eq!( + methods::WORKFLOW_IDENTITY_MATCHES, + "WorkflowIdentityMatches" + ); } diff --git a/crates/tinymemory-bus/src/types/mod.rs b/crates/tinymemory-bus/src/types/mod.rs index 81c0ed0..6e2f630 100644 --- a/crates/tinymemory-bus/src/types/mod.rs +++ b/crates/tinymemory-bus/src/types/mod.rs @@ -29,19 +29,32 @@ pub use tinymemory_api::capabilities::{Capabilities, Capability}; pub use tinymemory_api::chunks::{Chunk, Metadata, SourceRef}; -pub use tinymemory_api::error::{MemoryError}; +pub use tinymemory_api::error::MemoryError; pub use tinymemory_api::goals::{GoalItem, GoalsDoc}; -pub use tinymemory_api::health::{MemoryHealth}; +pub use tinymemory_api::health::MemoryHealth; pub use tinymemory_api::provider::chunks::{ChunkDetail, ChunkEmbedding, ChunkQuery}; pub use tinymemory_api::provider::episodic::{ConversationSegment, EpisodicTurn}; -pub use tinymemory_api::provider::people::{AddressBookSeedOutcome, PersonHandle, PersonInteraction, PersonRecord, PersonScore, RankedPerson, ResolvedPerson}; +pub use tinymemory_api::provider::people::{ + AddressBookSeedOutcome, PersonHandle, PersonInteraction, PersonRecord, PersonScore, + RankedPerson, ResolvedPerson, +}; pub use tinymemory_api::provider::profile::{FacetType, ProfileFacet, UserState}; -pub use tinymemory_api::provider::retrieval::{CoverWindowQuery, EntityMatch, FastRetrieveQuery, RetrievalHit, RetrievalResponse, SourceRetrievalQuery}; -pub use tinymemory_api::provider::types::{DiffReport, EntityHit, ExportPage, ExportRecord, ImportOutcome, IngestItem, IngestOutcome, MaintenanceReport, SnapshotRef, SourceItem, SourceScope}; -pub use tinymemory_api::recall::{OwnedRecallOpts}; -pub use tinymemory_api::tool_memory::{ToolMemoryRule}; +pub use tinymemory_api::provider::retrieval::{ + CoverWindowQuery, EntityMatch, FastRetrieveQuery, RetrievalHit, RetrievalResponse, + SourceRetrievalQuery, +}; +pub use tinymemory_api::provider::types::{ + DiffReport, EntityHit, ExportPage, ExportRecord, ImportOutcome, IngestItem, IngestOutcome, + MaintenanceReport, SnapshotRef, SourceItem, SourceScope, +}; +pub use tinymemory_api::recall::OwnedRecallOpts; +pub use tinymemory_api::tool_memory::ToolMemoryRule; pub use tinymemory_api::tree::{IngestRequest, QueryResult, TreeStatus}; -pub use tinymemory_api::types::{GraphRelationRecord, MemoryCategory, MemoryEntry, MemoryKvRecord, MemoryTaint, NamespaceDocumentInput, NamespaceMemoryHit, NamespaceRetrievalContext, NamespaceSummary, StoredMemoryDocument}; +pub use tinymemory_api::types::{ + GraphRelationRecord, MemoryCategory, MemoryEntry, MemoryKvRecord, MemoryTaint, + NamespaceDocumentInput, NamespaceMemoryHit, NamespaceRetrievalContext, NamespaceSummary, + StoredMemoryDocument, +}; /// `serde_json::Value`, which three document methods return verbatim. /// From d3df87b13a4bd44ffed93cc522609f1e472e403e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:38:55 +0300 Subject: [PATCH 05/35] chore(tinymemory-bus): suppress clippy expect and panic warnings in test modules Add `#![allow(clippy::expect_used, clippy::panic)]` to four test modules so that the linter does not flag deliberate uses of `expect` and `panic` in test code, where a failed assertion is always a panic regardless of the mechanism used. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/calls/test.rs | 4 ++++ crates/tinymemory-bus/src/error/test.rs | 3 +++ crates/tinymemory-bus/src/names/test.rs | 3 +++ crates/tinymemory-bus/src/wire/test.rs | 3 +++ 4 files changed, 13 insertions(+) diff --git a/crates/tinymemory-bus/src/calls/test.rs b/crates/tinymemory-bus/src/calls/test.rs index c7a8c5f..1271e28 100644 --- a/crates/tinymemory-bus/src/calls/test.rs +++ b/crates/tinymemory-bus/src/calls/test.rs @@ -7,6 +7,10 @@ //! the table below is checked against `crate::names::METHODS` in both //! directions. +// A failed assertion in a test is a panic either way; `expect` here says what +// the invariant was. Same allowance the crate's other test modules take. +#![allow(clippy::expect_used, clippy::panic)] + use serde_json::json; use crate::calls::BusCall; diff --git a/crates/tinymemory-bus/src/error/test.rs b/crates/tinymemory-bus/src/error/test.rs index 8e49cd1..4c2caf7 100644 --- a/crates/tinymemory-bus/src/error/test.rs +++ b/crates/tinymemory-bus/src/error/test.rs @@ -1,4 +1,7 @@ //! Unit tests for the crate-wide error type. +// A failed assertion in a test is a panic either way; `expect` here says what +// the invariant was. Same allowance the crate's other test modules take. +#![allow(clippy::expect_used, clippy::panic)] use super::{Error, Result}; diff --git a/crates/tinymemory-bus/src/names/test.rs b/crates/tinymemory-bus/src/names/test.rs index 7c67dbb..14656e6 100644 --- a/crates/tinymemory-bus/src/names/test.rs +++ b/crates/tinymemory-bus/src/names/test.rs @@ -4,6 +4,9 @@ //! that only fails at runtime, in a host, as an `UnknownMethod` — so the value //! here is in catching a typo or a duplicate at `cargo test` time in this //! crate, before the module or a host ever sees it. +// A failed assertion in a test is a panic either way; `expect` here says what +// the invariant was. Same allowance the crate's other test modules take. +#![allow(clippy::expect_used, clippy::panic)] use super::{methods, BUS_NAME, METHODS, OBJECT_PATH}; diff --git a/crates/tinymemory-bus/src/wire/test.rs b/crates/tinymemory-bus/src/wire/test.rs index 9ff97f5..729b486 100644 --- a/crates/tinymemory-bus/src/wire/test.rs +++ b/crates/tinymemory-bus/src/wire/test.rs @@ -4,6 +4,9 @@ //! that the re-export surfaces the whole of it — a name constant that failed to //! come across would leave a host unable to recognise that error class, and a //! missing `pub use` is invisible until someone reaches for it. +// A failed assertion in a test is a panic either way; `expect` here says what +// the invariant was. Same allowance the crate's other test modules take. +#![allow(clippy::expect_used, clippy::panic)] use super::{from_wire, wire_name}; use crate::types::MemoryError; From f768595885286fc2826faf6d164cf125437542e9 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:39:15 +0300 Subject: [PATCH 06/35] fix(calls): correct example values in module documentation The example code and JSON in the module-level documentation used outdated enum variants for `MemoryCategory` and `MemoryTaint`. Updated `Fact` to `Core` and `Trusted` to `Internal` to match the current API. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/calls/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/tinymemory-bus/src/calls/mod.rs b/crates/tinymemory-bus/src/calls/mod.rs index c2a94c1..7b39dd5 100644 --- a/crates/tinymemory-bus/src/calls/mod.rs +++ b/crates/tinymemory-bus/src/calls/mod.rs @@ -8,7 +8,7 @@ //! fine encoding and a bad thing to write by hand: //! //! ```json -//! ["work", "standup", "…", "Fact", null, "Untrusted"] +//! ["work", "standup", "…", "core", null, "internal"] //! ``` //! //! Two of those six are `Option`s, two are enums that serialize as strings, and @@ -27,9 +27,9 @@ //! namespace: "work".to_string(), //! key: "standup".to_string(), //! content: "shipped the loader".to_string(), -//! category: MemoryCategory::Fact, +//! category: MemoryCategory::Core, //! session_id: None, -//! taint: MemoryTaint::Trusted, +//! taint: MemoryTaint::Internal, //! } //! .into_args()?; //! From 1a0794a5de3bbb12bef1a123b65b69c676e097b1 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:39:41 +0300 Subject: [PATCH 07/35] chore(tinymemory-module): add tinymemory-bus as a dev-dependency Add the tinymemory-bus crate as a dev-dependency so that tests can assert the module's served members match what the host expects. The crate is not needed at runtime, only for contract verification in tests. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-module/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/tinymemory-module/Cargo.toml b/crates/tinymemory-module/Cargo.toml index 45845ba..5c93e21 100644 --- a/crates/tinymemory-module/Cargo.toml +++ b/crates/tinymemory-module/Cargo.toml @@ -66,6 +66,11 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] } [dev-dependencies] tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] } +# The host-side contract. A dev-dependency, not a normal one: the module serves +# `tinymemory-api` types directly and needs nothing from this crate to run. What +# it needs is the assertion — that the members it serves are exactly the ones +# `tinymemory-bus` tells a host to expect — and that belongs in tests. +tinymemory-bus = { path = "../tinymemory-bus" } # The loader E2E and the store tests need a throwaway workspace directory. tempfile = "3" From cf9ebede3cd56e95bdb95d709c3dda6e393fb94d Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:40:33 +0300 Subject: [PATCH 08/35] chore(tinymemory-module): update Cargo.lock and test file Updated the Cargo.lock file to reflect dependency changes and modified the test file to align with the updated module configuration, ensuring tests remain consistent with the current dependency tree. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-module/Cargo.lock | 11 +++++ crates/tinymemory-module/src/service/test.rs | 47 ++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/crates/tinymemory-module/Cargo.lock b/crates/tinymemory-module/Cargo.lock index d703571..ba0147e 100644 --- a/crates/tinymemory-module/Cargo.lock +++ b/crates/tinymemory-module/Cargo.lock @@ -1796,6 +1796,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "tinymemory-bus" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.20", + "tinymemory-api", +] + [[package]] name = "tinymemory-core" version = "0.1.0" @@ -1845,6 +1855,7 @@ dependencies = [ "tinycortex", "tinymemory", "tinymemory-api", + "tinymemory-bus", "tinymemory-core", "tinymemory-tinycortex", "tokio", diff --git a/crates/tinymemory-module/src/service/test.rs b/crates/tinymemory-module/src/service/test.rs index 7cd7b3f..e1761ca 100644 --- a/crates/tinymemory-module/src/service/test.rs +++ b/crates/tinymemory-module/src/service/test.rs @@ -288,3 +288,50 @@ fn every_served_method_is_declared_in_the_manifest() { "these methods are declared in the manifest but not served: {unserved:?}" ); } + +/// The members served here are exactly the ones `tinymemory-bus` publishes, in +/// the same order. +/// +/// `tinymemory-bus` is what a host compiles against: it carries one constant +/// and one typed call struct per member. Nothing links the two — this crate +/// derives its members from the `#[tinybus::interface]` block, that one lists +/// them by hand — so a method added here without a matching entry there is a +/// capability no host can reach, and an entry there with no method here is a +/// call that fails at runtime with `UnknownMethod`. +/// +/// Neither failure has a compile error anywhere, which is why it is asserted. +/// The comparison is on sequences rather than sets on purpose: `members()` +/// returns declaration order, `METHODS` is written in declaration order, and +/// pinning the order too means the two lists stay readable side by side. +#[test] +fn the_served_members_are_exactly_the_published_contract() { + let service = super::MemoryService::new(std::sync::Arc::new( + tinymemory_api::null::NullMemoryProvider, + )); + let served: Vec = tinybus::service::Interface::members(&service) + .iter() + .map(|member| member.as_str().to_string()) + .collect(); + let published: Vec = tinymemory_bus::METHODS + .iter() + .map(|member| (*member).to_string()) + .collect(); + + // Reported as differences rather than as a 89-element inequality, so the + // failure names the method that moved instead of printing both lists. + let missing: Vec<&String> = served.iter().filter(|m| !published.contains(m)).collect(); + assert!( + missing.is_empty(), + "served here but absent from tinymemory-bus, so no host can call them: {missing:?}" + ); + let extra: Vec<&String> = published.iter().filter(|m| !served.contains(m)).collect(); + assert!( + extra.is_empty(), + "published by tinymemory-bus but not served here, so a host calling them gets \ + UnknownMethod: {extra:?}" + ); + assert_eq!( + served, published, + "the two lists hold the same members in different orders" + ); +} From 4d696f489ba7599e534a69a6810c1439c87aa4e3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:41:16 +0300 Subject: [PATCH 09/35] docs(tinymemory-bus): add README with crate overview and usage Add a README for the tinymemory-bus crate to provide documentation on its purpose, key features, and basic usage examples, improving discoverability and onboarding for developers. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/README.md | 135 ++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 crates/tinymemory-bus/README.md diff --git a/crates/tinymemory-bus/README.md b/crates/tinymemory-bus/README.md new file mode 100644 index 0000000..601b511 --- /dev/null +++ b/crates/tinymemory-bus/README.md @@ -0,0 +1,135 @@ +# tinymemory-bus + +The wire contract for the TinyMemory `TinyBus` module, as a library a host +links. + +TinyMemory ships as a loadable module so a host does not compile the engine. +`crates/tinymemory-module` exports one object with 89 members on it, and it +ships as a `cdylib` — a host can load it, but it cannot `use` anything out of +it. This crate is what the host compiles against instead: + +| module | what it holds | +| -------- | -------------------------------------------------------------- | +| `names` | the bus name, the object path, one constant per member | +| `types` | every value type that crosses a frame | +| `calls` | one struct per member: arguments in wire order, plus reply type | +| `wire` | the error names, and the mapping back to `MemoryError` | + +Four dependencies, none of them heavy: `tinymemory-api` for the types, `serde` +and `serde_json` for the encoding, `thiserror` for one small error enum. No +engine, no storage, no async runtime — and no `tinybus`. + +## Why the types are re-exported, not defined + +The obvious reading of "a crate that holds the bus types" is a crate that +*defines* them. That would be a mistake, and the repository has already made +the equivalent one once: when `tinymemory-api` was resolved twice, by git and +by path, `MemoryCategory` from one copy was not the same type as +`MemoryCategory` from the other, and the mismatch only surfaced at the seam. +The root `Cargo.toml`'s `[patch]` table exists to prevent exactly that. + +Defining structurally identical types here would reproduce it deliberately: the +module would serve `tinymemory_api::` types, the host would hold +`tinymemory_bus::` ones, and every call site would need a conversion whose +correctness nothing checks. So there is one definition, in `tinymemory-api`, +surfaced here. A host gets the types the module serves — the same types, not +equivalents. + +## Why not just depend on `tinymemory-api` + +It would compile. But `tinymemory-api` is the **driver** contract: it also +carries `MemoryProvider` and its eighteen capability traits, the +mandatory-family composition, the null driver, and the `host::` config sections +a host persists in `config.toml`. A host that loads the module implements none +of that — it makes calls. + +This crate is the subset that crosses a frame. What a host compiles against is +what it can actually send and receive, and a trait method that is not exported +on the bus is absent here rather than tempting. + +## Why arguments get a struct + +`#[tinybus::interface]` puts a method's arguments on the wire as a positional +JSON array, decoded into a tuple on the far side. That is a fine encoding and a +bad thing to write by hand. `Store` takes six arguments: + +```json +["work", "standup", "…", "core", null, "internal"] +``` + +Two are `Option`s, two are enums that serialize as strings, and swapping +`namespace` with `key` produces a call that succeeds and writes the entry to the +wrong place. Nothing on the module side can catch it — both are `String`, in +the right position count, and the engine has no way to know which one the caller +meant. + +So a caller fills in named fields and `BusCall::into_args` does the positioning. +The reply type travels with the call for the same reason: `Get` answers +`Option` and `Forget` answers `bool`, both are perfectly good JSON, +and decoding one as the other fails somewhere far from the call. + +## There is no client here + +This crate holds no connection and no `call()` that sends anything. Two reasons. + +A host already owns its connection — its reconnect policy, its timeouts, its +tracing, its own idea of what a memory call costs it. A client here would either +duplicate that or fight it, and the useful part is already in `calls` and +`types`. + +And structurally it could not work anyway: `tinybus` is a vendored submodule +whose manifest inherits fields from its own nested `[workspace.package]`, so a +member of this workspace that depends on it makes cargo resolve that inheritance +against the wrong root and fail. That is why `crates/tinymemory-module` is its +own workspace root — see the note on `exclude` in the root `Cargo.toml`. A +contract crate a host links has no business being a separate workspace, so it +stays transport-free. + +Wiring it up host-side is small: + +```rust,ignore +use tinymemory_bus::calls::BusCall; +use tinymemory_bus::names::{BUS_NAME, OBJECT_PATH}; +use tinymemory_bus::{types::MemoryError, wire}; + +/// Make one call, and give a failure back as the driver's own error type. +async fn call( + connection: &tinybus::Connection, + call: C, +) -> Result { + let args = call + .into_args() + .map_err(|e| MemoryError::Invalid(e.to_string()))?; + + match connection + .call(BUS_NAME, OBJECT_PATH, C::METHOD, args) + .await + { + Ok(body) => C::decode_response(body).map_err(|e| MemoryError::Other(e.into())), + // The name is the contract; `from_wire` is the same table the module + // mapped out through, so the variant survives the round trip. + Err(tinybus::Error::MethodFailed { name, message }) => { + Err(wire::from_wire(&name, &message)) + } + Err(other) => Err(MemoryError::Other(other.into())), + } +} +``` + +`OpenStore` is the one member that needs more than that: it returns an object +*path*, not a value, and calls against that path use the same `BUS_NAME` and the +same member names. Treat `OBJECT_PATH` as the root object rather than the only +one. + +## Staying in step with the module + +`names::METHODS` lists every member. `crates/tinymemory-module` asserts its +served members against that list, in order, in +`the_served_members_are_exactly_the_published_contract`. Nothing else links the +two — this crate lists members by hand, the module derives them from its +`#[tinybus::interface]` block — so that test is what turns a drift into a +`cargo test` failure instead of an `UnknownMethod` in a host at runtime. + +Adding a member is therefore three edits in this crate: a constant in +`names::methods`, an entry in `names::METHODS`, and a call struct in the +matching `calls` family (which `calls::test::COVERED` also lists). From c9dd9ca00bb21e1b79203bc76d4405e9d4f3eb24 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:41:23 +0300 Subject: [PATCH 10/35] docs(README): add description for tinymemory-bus crate in project overview Add a brief explanation of the tinymemory-bus crate to the README's crate listing, clarifying its role as the wire contract for loadable modules and how hosts interact with tinymemory-module. Auto-committed-on: dragonfly Co-authored-by: Medulla --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index ad0473f..b759365 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ crates/ ├── tinymemory-api/ the contract. Dependency-light on purpose: depending on │ it never drags in SQLite, git2, reqwest, or an async │ runtime +├── tinymemory-bus/ the wire contract for the loadable module: member names, +│ the payload types, and one typed call per member. What a +│ *host* links to talk to `tinymemory-module`, which ships +│ as a `cdylib` and exports no Rust surface of its own ├── tinymemory-core/ the substance: ingestion, the summary tree, chunk │ storage, entities, the graph, the diff ledger, goals, │ tool-memory, and the Composio sync layer. The largest From 792aa623483d00e0da06236c473189ea08e6c529 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:44:37 +0300 Subject: [PATCH 11/35] feat(tinymemory-bus): add dependency guard comments to Cargo.toml Added a comment block documenting the forbidden dependencies for this crate, along with the exact cargo tree command to verify they are not pulled in transitively. This makes the implicit constraint explicit for maintainers and code reviewers. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/Cargo.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/tinymemory-bus/Cargo.toml b/crates/tinymemory-bus/Cargo.toml index 96e5634..f52053a 100644 --- a/crates/tinymemory-bus/Cargo.toml +++ b/crates/tinymemory-bus/Cargo.toml @@ -18,6 +18,14 @@ description = "The TinyBus wire contract for the TinyMemory module: member names # transport is deliberately absent, and the root manifest's note on # `crates/tinymemory-module` for what depending on the vendored `tinybus` from a # workspace member would do to this workspace. +# +# Nothing here may pull in `rusqlite`, `git2`, `reqwest`, `regex`, an async +# runtime, or `tinybus`. Guard with the FORWARD form, which is scoped to this +# package — `cargo tree -i` discards the `-p` scope and exits clean even when +# this crate is the one pulling the dependency in: +# +# cargo tree -p tinymemory-bus -e normal,build --prefix none \ +# | grep -Ei 'rusqlite|libsqlite|git2|reqwest|regex|tokio|tinybus' # expect no match [dependencies] # The single definition of every type on the wire. Re-exported, never # redefined — see `src/types/mod.rs`. From 2922ce5cd9f7b97ae3d659f1f9ba8f400fb7a243 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:55:10 +0300 Subject: [PATCH 12/35] chore(tinymemory-bus): remove unused calls module and its submodules The entire `calls` module, along with its submodules for chunks, core, documents, driver, episodic, goals, graph, ingest, maintenance, people, portability, profile, recall, retrieval, sources, tool_memory, and tree, has been removed as it is no longer needed. The `error` and `types` modules were also cleaned up, and the `names` module was flattened by moving its contents into the parent module and renaming the test file accordingly. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/calls/chunks.rs | 121 ---------- crates/tinymemory-bus/src/calls/core.rs | 142 ----------- crates/tinymemory-bus/src/calls/documents.rs | 156 ------------ crates/tinymemory-bus/src/calls/driver.rs | 120 ---------- crates/tinymemory-bus/src/calls/episodic.rs | 204 ---------------- crates/tinymemory-bus/src/calls/goals.rs | 44 ---- crates/tinymemory-bus/src/calls/graph.rs | 214 ----------------- crates/tinymemory-bus/src/calls/ingest.rs | 45 ---- .../tinymemory-bus/src/calls/maintenance.rs | 74 ------ crates/tinymemory-bus/src/calls/mod.rs | 126 ---------- crates/tinymemory-bus/src/calls/people.rs | 140 ----------- .../tinymemory-bus/src/calls/portability.rs | 54 ----- crates/tinymemory-bus/src/calls/profile.rs | 225 ------------------ crates/tinymemory-bus/src/calls/recall.rs | 70 ------ crates/tinymemory-bus/src/calls/retrieval.rs | 121 ---------- crates/tinymemory-bus/src/calls/sources.rs | 109 --------- crates/tinymemory-bus/src/calls/test.rs | 197 --------------- .../tinymemory-bus/src/calls/tool_memory.rs | 64 ----- crates/tinymemory-bus/src/calls/tree.rs | 105 -------- crates/tinymemory-bus/src/error/mod.rs | 51 ---- crates/tinymemory-bus/src/error/test.rs | 36 --- .../src/{names/mod.rs => names.rs} | 0 .../src/{names/test.rs => names_tests.rs} | 0 crates/tinymemory-bus/src/types/mod.rs | 66 ----- crates/tinymemory-bus/src/wire/mod.rs | 41 ---- crates/tinymemory-bus/src/wire/test.rs | 55 ----- 26 files changed, 2580 deletions(-) delete mode 100644 crates/tinymemory-bus/src/calls/chunks.rs delete mode 100644 crates/tinymemory-bus/src/calls/core.rs delete mode 100644 crates/tinymemory-bus/src/calls/documents.rs delete mode 100644 crates/tinymemory-bus/src/calls/driver.rs delete mode 100644 crates/tinymemory-bus/src/calls/episodic.rs delete mode 100644 crates/tinymemory-bus/src/calls/goals.rs delete mode 100644 crates/tinymemory-bus/src/calls/graph.rs delete mode 100644 crates/tinymemory-bus/src/calls/ingest.rs delete mode 100644 crates/tinymemory-bus/src/calls/maintenance.rs delete mode 100644 crates/tinymemory-bus/src/calls/mod.rs delete mode 100644 crates/tinymemory-bus/src/calls/people.rs delete mode 100644 crates/tinymemory-bus/src/calls/portability.rs delete mode 100644 crates/tinymemory-bus/src/calls/profile.rs delete mode 100644 crates/tinymemory-bus/src/calls/recall.rs delete mode 100644 crates/tinymemory-bus/src/calls/retrieval.rs delete mode 100644 crates/tinymemory-bus/src/calls/sources.rs delete mode 100644 crates/tinymemory-bus/src/calls/test.rs delete mode 100644 crates/tinymemory-bus/src/calls/tool_memory.rs delete mode 100644 crates/tinymemory-bus/src/calls/tree.rs delete mode 100644 crates/tinymemory-bus/src/error/mod.rs delete mode 100644 crates/tinymemory-bus/src/error/test.rs rename crates/tinymemory-bus/src/{names/mod.rs => names.rs} (100%) rename crates/tinymemory-bus/src/{names/test.rs => names_tests.rs} (100%) delete mode 100644 crates/tinymemory-bus/src/types/mod.rs delete mode 100644 crates/tinymemory-bus/src/wire/mod.rs delete mode 100644 crates/tinymemory-bus/src/wire/test.rs diff --git a/crates/tinymemory-bus/src/calls/chunks.rs b/crates/tinymemory-bus/src/calls/chunks.rs deleted file mode 100644 index 2f0a945..0000000 --- a/crates/tinymemory-bus/src/calls/chunks.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! The persisted chunk model and its embeddings. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `ListChunks`. -/// -/// Chunks matching the query, size-checked. -/// -/// `ChunkQuery::limit` bounds rows, not bytes, and a chunk carries full -/// content — so this is one of the methods where the ceiling matters most. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListChunks { - /// The `query` argument — wire position 0. - pub query: types::ChunkQuery, - /// The `scope` argument — wire position 1. - pub scope: Option, -} - -impl BusCall for ListChunks { - const METHOD: &'static str = methods::LIST_CHUNKS; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.query, self.scope)).map_err(Error::Encode) - } -} - -/// Arguments for `GetChunk`. -/// -/// One chunk, size-checked. -/// -/// A single object is checked for the same reason a list is: the ceiling is -/// a property of the frame, not of the row count, and one chunk carries -/// full content with no bound of its own. A list of one that is refused -/// while the singular read of the same chunk succeeds would be an odd -/// contract to explain. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GetChunk { - /// The `chunk_id` argument — wire position 0. - pub chunk_id: String, -} - -impl BusCall for GetChunk { - const METHOD: &'static str = methods::GET_CHUNK; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.chunk_id,)).map_err(Error::Encode) - } -} - -/// Arguments for `ChunkDetail`. -/// -/// One chunk plus its metadata, size-checked. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChunkDetail { - /// The `chunk_id` argument — wire position 0. - pub chunk_id: String, -} - -impl BusCall for ChunkDetail { - const METHOD: &'static str = methods::CHUNK_DETAIL; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.chunk_id,)).map_err(Error::Encode) - } -} - -/// Arguments for `StorageKinds`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct StorageKinds; - -impl BusCall for StorageKinds { - const METHOD: &'static str = methods::STORAGE_KINDS; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `ChunkEmbeddings`. -/// -/// Embedding vectors are the largest thing this interface returns. -/// -/// A 1536-dimension vector encodes to roughly 10 KiB of JSON, so a few -/// hundred chunks reach the frame ceiling on their own. Checked for the same -/// reason `List` is, and refused by name rather than truncated — a short -/// batch is indistinguishable from "those chunks have no vector". -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChunkEmbeddings { - /// The `chunk_ids` argument — wire position 0. - pub chunk_ids: Vec, - /// The `model_signature` argument — wire position 1. - pub model_signature: String, -} - -impl BusCall for ChunkEmbeddings { - const METHOD: &'static str = methods::CHUNK_EMBEDDINGS; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.chunk_ids, self.model_signature)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/core.rs b/crates/tinymemory-bus/src/calls/core.rs deleted file mode 100644 index 918f9b9..0000000 --- a/crates/tinymemory-bus/src/calls/core.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! The mandatory key/value surface every driver implements. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `Store`. -/// -/// Upsert an entry keyed by `(namespace, key)`. -/// -/// `taint` is a required argument rather than a defaulted one, mirroring the -/// contract: a driver that could default provenance would be able to launder -/// externally-sourced content into internal-trust content, which is the one -/// failure mode the host's policy guard exists to prevent. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Store { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `key` argument — wire position 1. - pub key: String, - /// The `content` argument — wire position 2. - pub content: String, - /// The `category` argument — wire position 3. - pub category: types::MemoryCategory, - /// The `session_id` argument — wire position 4. - pub session_id: Option, - /// The `taint` argument — wire position 5. - pub taint: types::MemoryTaint, -} - -impl BusCall for Store { - const METHOD: &'static str = methods::STORE; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value(( - self.namespace, - self.key, - self.content, - self.category, - self.session_id, - self.taint, - )) - .map_err(Error::Encode) - } -} - -/// Arguments for `Get`. -/// -/// Fetch the entry at an exact `(namespace, key)`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Get { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `key` argument — wire position 1. - pub key: String, -} - -impl BusCall for Get { - const METHOD: &'static str = methods::GET; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) - } -} - -/// Arguments for `Forget`. -/// -/// Delete the entry at `(namespace, key)`, reporting whether it existed. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Forget { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `key` argument — wire position 1. - pub key: String, -} - -impl BusCall for Forget { - const METHOD: &'static str = methods::FORGET; - - type Response = bool; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) - } -} - -/// Arguments for `List`. -/// -/// List entries, narrowing by namespace, category and session. -/// -/// Bounded by `MAX_RESPONSE_BYTES`: unlike `Recall` and `ExportPage`, this -/// method takes no limit and no cursor, so the caller has no way to ask for -/// less. See `ensure_response_fits` for why the answer is a named refusal -/// rather than a truncation. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct List { - /// The `namespace` argument — wire position 0. - pub namespace: Option, - /// The `category` argument — wire position 1. - pub category: Option, - /// The `session_id` argument — wire position 2. - pub session_id: Option, -} - -impl BusCall for List { - const METHOD: &'static str = methods::LIST; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.category, self.session_id)) - .map_err(Error::Encode) - } -} - -/// Arguments for `Namespaces`. -/// -/// Enumerate namespaces with their aggregate counts. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Namespaces; - -impl BusCall for Namespaces { - const METHOD: &'static str = methods::NAMESPACES; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} diff --git a/crates/tinymemory-bus/src/calls/documents.rs b/crates/tinymemory-bus/src/calls/documents.rs deleted file mode 100644 index 17b0a2f..0000000 --- a/crates/tinymemory-bus/src/calls/documents.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Namespace-scoped document storage and retrieval. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `PutDocument`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PutDocument { - /// The `input` argument — wire position 0. - pub input: types::NamespaceDocumentInput, -} - -impl BusCall for PutDocument { - const METHOD: &'static str = methods::PUT_DOCUMENT; - - type Response = String; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.input,)).map_err(Error::Encode) - } -} - -/// Arguments for `GetDocument`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GetDocument { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `key` argument — wire position 1. - pub key: String, -} - -impl BusCall for GetDocument { - const METHOD: &'static str = methods::GET_DOCUMENT; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) - } -} - -/// Arguments for `ListDocuments`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListDocuments { - /// The `namespace` argument — wire position 0. - pub namespace: Option, -} - -impl BusCall for ListDocuments { - const METHOD: &'static str = methods::LIST_DOCUMENTS; - - type Response = Value; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace,)).map_err(Error::Encode) - } -} - -/// Arguments for `ListNamespaces`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListNamespaces; - -impl BusCall for ListNamespaces { - const METHOD: &'static str = methods::LIST_NAMESPACES; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `DeleteDocument`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeleteDocument { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `document_id` argument — wire position 1. - pub document_id: String, -} - -impl BusCall for DeleteDocument { - const METHOD: &'static str = methods::DELETE_DOCUMENT; - - type Response = Value; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.document_id)).map_err(Error::Encode) - } -} - -/// Arguments for `ClearNamespace`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ClearNamespace { - /// The `namespace` argument — wire position 0. - pub namespace: String, -} - -impl BusCall for ClearNamespace { - const METHOD: &'static str = methods::CLEAR_NAMESPACE; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace,)).map_err(Error::Encode) - } -} - -/// Arguments for `QueryDocuments`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QueryDocuments { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `query` argument — wire position 1. - pub query: String, - /// The `limit` argument — wire position 2. - pub limit: usize, -} - -impl BusCall for QueryDocuments { - const METHOD: &'static str = methods::QUERY_DOCUMENTS; - - type Response = types::NamespaceRetrievalContext; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.query, self.limit)).map_err(Error::Encode) - } -} - -/// Arguments for `RecallDocuments`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecallDocuments { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `limit` argument — wire position 1. - pub limit: usize, -} - -impl BusCall for RecallDocuments { - const METHOD: &'static str = methods::RECALL_DOCUMENTS; - - type Response = types::NamespaceRetrievalContext; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.limit)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/driver.rs b/crates/tinymemory-bus/src/calls/driver.rs deleted file mode 100644 index df3cebc..0000000 --- a/crates/tinymemory-bus/src/calls/driver.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! Driver identity, capability negotiation, health and store opening. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `DriverId`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DriverId; - -impl BusCall for DriverId { - const METHOD: &'static str = methods::DRIVER_ID; - - type Response = String; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `Capabilities`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Capabilities; - -impl BusCall for Capabilities { - const METHOD: &'static str = methods::CAPABILITIES; - - type Response = types::Capabilities; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `Health`. -/// -/// Current liveness, as the driver reports it. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Health; - -impl BusCall for Health { - const METHOD: &'static str = methods::HEALTH; - - type Response = types::MemoryHealth; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `Shutdown`. -/// -/// Release backend resources. -/// -/// Idempotent, as the trait requires. Note that this does **not** unload the -/// module: `TinyBus` never unloads a library, so a host that shuts the -/// driver down and rebinds gets a fresh engine inside the same mapped image. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Shutdown; - -impl BusCall for Shutdown { - const METHOD: &'static str = methods::SHUTDOWN; - - type Response = (); - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `OpenStore`. -/// -/// Bring up a store rooted at `/` and return the -/// object path serving it. -/// -/// # Why the module opens stores rather than the host selecting one per call -/// -/// A host with per-profile memory needs more than one store in a process. -/// The alternative was a store selector threaded through every method on -/// every capability family — a change to the shape of the whole contract, -/// to express something that is not a property of a memory operation at -/// all. Which store you are talking to is settled when you are handed a -/// driver, exactly like which workspace you are bound to. -/// -/// So the root object opens stores and hands back object paths. Each is an -/// ordinary `MemoryService` exporting the identical interface, and the -/// contract does not change at all: `MemoryProvider` still describes one -/// store, and a proxy still talks to one store. -/// -/// Idempotent per subtree — see `StoreOpener::served` for why opening the -/// same database twice is worth going out of the way to avoid. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenStore { - /// The `memory_subdir` argument — wire position 0. - pub memory_subdir: String, -} - -impl BusCall for OpenStore { - const METHOD: &'static str = methods::OPEN_STORE; - - type Response = String; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.memory_subdir,)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/episodic.rs b/crates/tinymemory-bus/src/calls/episodic.rs deleted file mode 100644 index ee80e1c..0000000 --- a/crates/tinymemory-bus/src/calls/episodic.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! Episodic turns and conversation segments. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `InsertTurn`. -/// -/// Record one turn, answering with the row id the engine assigned it. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InsertTurn { - /// The `turn` argument — wire position 0. - pub turn: types::EpisodicTurn, -} - -impl BusCall for InsertTurn { - const METHOD: &'static str = methods::INSERT_TURN; - - type Response = i64; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.turn,)).map_err(Error::Encode) - } -} - -/// Arguments for `SessionTurns`. -/// -/// Every recorded turn for one session, oldest first. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SessionTurns { - /// The `session_id` argument — wire position 0. - pub session_id: String, -} - -impl BusCall for SessionTurns { - const METHOD: &'static str = methods::SESSION_TURNS; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.session_id,)).map_err(Error::Encode) - } -} - -/// Arguments for `OpenSegment`. -/// -/// The open segment for a session, if there is one. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenSegment { - /// The `session_id` argument — wire position 0. - pub session_id: String, -} - -impl BusCall for OpenSegment { - const METHOD: &'static str = methods::OPEN_SEGMENT; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.session_id,)).map_err(Error::Encode) - } -} - -/// Arguments for `CreateSegment`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CreateSegment { - /// The `segment_id` argument — wire position 0. - pub segment_id: String, - /// The `session_id` argument — wire position 1. - pub session_id: String, - /// The `namespace` argument — wire position 2. - pub namespace: String, - /// The `start_episodic_id` argument — wire position 3. - pub start_episodic_id: i64, - /// The `start_timestamp` argument — wire position 4. - pub start_timestamp: f64, - /// The `now` argument — wire position 5. - pub now: f64, -} - -impl BusCall for CreateSegment { - const METHOD: &'static str = methods::CREATE_SEGMENT; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value(( - self.segment_id, - self.session_id, - self.namespace, - self.start_episodic_id, - self.start_timestamp, - self.now, - )) - .map_err(Error::Encode) - } -} - -/// Arguments for `AppendTurn`. -/// -/// Extend a segment to include one more turn. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AppendTurn { - /// The `segment_id` argument — wire position 0. - pub segment_id: String, - /// The `episodic_id` argument — wire position 1. - pub episodic_id: i64, - /// The `timestamp` argument — wire position 2. - pub timestamp: f64, - /// The `now` argument — wire position 3. - pub now: f64, -} - -impl BusCall for AppendTurn { - const METHOD: &'static str = methods::APPEND_TURN; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.segment_id, self.episodic_id, self.timestamp, self.now)) - .map_err(Error::Encode) - } -} - -/// Arguments for `CloseSegment`. -/// -/// Mark a segment closed. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CloseSegment { - /// The `segment_id` argument — wire position 0. - pub segment_id: String, - /// The `now` argument — wire position 1. - pub now: f64, -} - -impl BusCall for CloseSegment { - const METHOD: &'static str = methods::CLOSE_SEGMENT; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.segment_id, self.now)).map_err(Error::Encode) - } -} - -/// Arguments for `SetSegmentSummary`. -/// -/// Attach a summary to a closed segment. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SetSegmentSummary { - /// The `segment_id` argument — wire position 0. - pub segment_id: String, - /// The `summary` argument — wire position 1. - pub summary: String, - /// The `now` argument — wire position 2. - pub now: f64, -} - -impl BusCall for SetSegmentSummary { - const METHOD: &'static str = methods::SET_SEGMENT_SUMMARY; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.segment_id, self.summary, self.now)).map_err(Error::Encode) - } -} - -/// Arguments for `UpsertSegmentEmbedding`. -/// -/// Store a segment's embedding under `model_signature`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpsertSegmentEmbedding { - /// The `segment_id` argument — wire position 0. - pub segment_id: String, - /// The `model_signature` argument — wire position 1. - pub model_signature: String, - /// The `embedding` argument — wire position 2. - pub embedding: Vec, - /// The `created_at` argument — wire position 3. - pub created_at: f64, -} - -impl BusCall for UpsertSegmentEmbedding { - const METHOD: &'static str = methods::UPSERT_SEGMENT_EMBEDDING; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value(( - self.segment_id, - self.model_signature, - self.embedding, - self.created_at, - )) - .map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/goals.rs b/crates/tinymemory-bus/src/calls/goals.rs deleted file mode 100644 index de3953d..0000000 --- a/crates/tinymemory-bus/src/calls/goals.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! The long-term goals document. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `Goals`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Goals; - -impl BusCall for Goals { - const METHOD: &'static str = methods::GOALS; - - type Response = types::GoalsDoc; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `SetGoals`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SetGoals { - /// The `goals` argument — wire position 0. - pub goals: types::GoalsDoc, -} - -impl BusCall for SetGoals { - const METHOD: &'static str = methods::SET_GOALS; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.goals,)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/graph.rs b/crates/tinymemory-bus/src/calls/graph.rs deleted file mode 100644 index ea9684c..0000000 --- a/crates/tinymemory-bus/src/calls/graph.rs +++ /dev/null @@ -1,214 +0,0 @@ -//! Entities, relations and the namespaced key/value store. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `Entities`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Entities { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `query` argument — wire position 1. - pub query: Option, - /// The `limit` argument — wire position 2. - pub limit: usize, -} - -impl BusCall for Entities { - const METHOD: &'static str = methods::ENTITIES; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.query, self.limit)).map_err(Error::Encode) - } -} - -/// Arguments for `EntityEdges`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EntityEdges { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `entity_id` argument — wire position 1. - pub entity_id: String, - /// The `limit` argument — wire position 2. - pub limit: usize, -} - -impl BusCall for EntityEdges { - const METHOD: &'static str = methods::ENTITY_EDGES; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.entity_id, self.limit)).map_err(Error::Encode) - } -} - -/// Arguments for `TouchEntities`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TouchEntities { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `entity_ids` argument — wire position 1. - pub entity_ids: Vec, -} - -impl BusCall for TouchEntities { - const METHOD: &'static str = methods::TOUCH_ENTITIES; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.entity_ids)).map_err(Error::Encode) - } -} - -/// Arguments for `SearchEntities`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SearchEntities { - /// The `query` argument — wire position 0. - pub query: String, - /// The `kinds` argument — wire position 1. - pub kinds: Option>, - /// The `limit` argument — wire position 2. - pub limit: usize, -} - -impl BusCall for SearchEntities { - const METHOD: &'static str = methods::SEARCH_ENTITIES; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.query, self.kinds, self.limit)).map_err(Error::Encode) - } -} - -/// Arguments for `Relations`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Relations { - /// The `namespace` argument — wire position 0. - pub namespace: Option, - /// The `subject` argument — wire position 1. - pub subject: Option, - /// The `predicate` argument — wire position 2. - pub predicate: Option, - /// The `limit` argument — wire position 3. - pub limit: usize, -} - -impl BusCall for Relations { - const METHOD: &'static str = methods::RELATIONS; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.subject, self.predicate, self.limit)) - .map_err(Error::Encode) - } -} - -/// Arguments for `PutRelation`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PutRelation { - /// The `relation` argument — wire position 0. - pub relation: types::GraphRelationRecord, -} - -impl BusCall for PutRelation { - const METHOD: &'static str = methods::PUT_RELATION; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.relation,)).map_err(Error::Encode) - } -} - -/// Arguments for `KvGet`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct KvGet { - /// The `namespace` argument — wire position 0. - pub namespace: Option, - /// The `key` argument — wire position 1. - pub key: String, -} - -impl BusCall for KvGet { - const METHOD: &'static str = methods::KV_GET; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) - } -} - -/// Arguments for `KvPut`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct KvPut { - /// The `namespace` argument — wire position 0. - pub namespace: Option, - /// The `key` argument — wire position 1. - pub key: String, - /// The `value` argument — wire position 2. - pub value: Value, -} - -impl BusCall for KvPut { - const METHOD: &'static str = methods::KV_PUT; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.key, self.value)).map_err(Error::Encode) - } -} - -/// Arguments for `KvDelete`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct KvDelete { - /// The `namespace` argument — wire position 0. - pub namespace: Option, - /// The `key` argument — wire position 1. - pub key: String, -} - -impl BusCall for KvDelete { - const METHOD: &'static str = methods::KV_DELETE; - - type Response = bool; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.key)).map_err(Error::Encode) - } -} - -/// Arguments for `KvList`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct KvList { - /// The `namespace` argument — wire position 0. - pub namespace: Option, - /// The `prefix` argument — wire position 1. - pub prefix: Option, - /// The `limit` argument — wire position 2. - pub limit: usize, -} - -impl BusCall for KvList { - const METHOD: &'static str = methods::KV_LIST; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.prefix, self.limit)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/ingest.rs b/crates/tinymemory-bus/src/calls/ingest.rs deleted file mode 100644 index f90421f..0000000 --- a/crates/tinymemory-bus/src/calls/ingest.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Document and chat ingestion through the summary pipeline. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `IngestDocument`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct IngestDocument { - /// The `item` argument — wire position 0. - pub item: types::IngestItem, -} - -impl BusCall for IngestDocument { - const METHOD: &'static str = methods::INGEST_DOCUMENT; - - type Response = types::IngestOutcome; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.item,)).map_err(Error::Encode) - } -} - -/// Arguments for `IngestChat`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct IngestChat { - /// The `messages` argument — wire position 0. - pub messages: Vec, -} - -impl BusCall for IngestChat { - const METHOD: &'static str = methods::INGEST_CHAT; - - type Response = types::IngestOutcome; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.messages,)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/maintenance.rs b/crates/tinymemory-bus/src/calls/maintenance.rs deleted file mode 100644 index edac055..0000000 --- a/crates/tinymemory-bus/src/calls/maintenance.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Re-embedding, compaction, consolidation and diagnosis. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::names::methods; -use crate::types; - -/// Arguments for `Reembed`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Reembed; - -impl BusCall for Reembed { - const METHOD: &'static str = methods::REEMBED; - - type Response = types::MaintenanceReport; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `Compact`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Compact; - -impl BusCall for Compact { - const METHOD: &'static str = methods::COMPACT; - - type Response = types::MaintenanceReport; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `Consolidate`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Consolidate; - -impl BusCall for Consolidate { - const METHOD: &'static str = methods::CONSOLIDATE; - - type Response = types::MaintenanceReport; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `Doctor`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Doctor; - -impl BusCall for Doctor { - const METHOD: &'static str = methods::DOCTOR; - - type Response = types::MaintenanceReport; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} diff --git a/crates/tinymemory-bus/src/calls/mod.rs b/crates/tinymemory-bus/src/calls/mod.rs deleted file mode 100644 index 7b39dd5..0000000 --- a/crates/tinymemory-bus/src/calls/mod.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! One typed struct per member, and the [`BusCall`] trait that ties it to its -//! name and its reply type. -//! -//! # Why arguments get a struct at all -//! -//! `#[tinybus::interface]` puts a method's arguments on the wire as a -//! **positional JSON array**, decoded on the far side into a tuple. That is a -//! fine encoding and a bad thing to write by hand: -//! -//! ```json -//! ["work", "standup", "…", "core", null, "internal"] -//! ``` -//! -//! Two of those six are `Option`s, two are enums that serialize as strings, and -//! swapping `namespace` with `key` produces a call that succeeds and writes the -//! entry to the wrong place. Nothing on the module side can catch it: both are -//! `String`, in the right position count, and the engine has no way to know -//! which one the caller meant. -//! -//! So a caller fills in named fields and this crate does the positioning: -//! -//! ``` -//! use tinymemory_bus::calls::{core::Store, BusCall}; -//! use tinymemory_bus::types::{MemoryCategory, MemoryTaint}; -//! -//! let args = Store { -//! namespace: "work".to_string(), -//! key: "standup".to_string(), -//! content: "shipped the loader".to_string(), -//! category: MemoryCategory::Core, -//! session_id: None, -//! taint: MemoryTaint::Internal, -//! } -//! .into_args()?; -//! -//! assert_eq!(Store::METHOD, "Store"); -//! assert_eq!(args[0], "work"); -//! assert_eq!(args[1], "standup"); -//! # Ok::<(), tinymemory_bus::Error>(()) -//! ``` -//! -//! # The reply type travels with the call -//! -//! [`BusCall::Response`] is the other half, and it is the half a host would -//! otherwise get wrong quietly. `Get` answers `Option` while -//! `Forget` answers `bool`; both are perfectly good JSON, and decoding one as -//! the other fails at a point far from the call. Binding the response type to -//! the call type means a host writes the method once and the compiler knows -//! what comes back. -//! -//! # What this is not -//! -//! Not a client. There is no connection here, no `call()` that sends anything — -//! see [`crate`] for why the transport is deliberately out of scope. A host -//! writes one small generic helper over its own `tinybus::Connection`; the -//! shape is in this crate's `README.md`. - -use serde::de::DeserializeOwned; -use serde_json::Value; - -use crate::error::{Error, Result}; - -pub mod chunks; -pub mod core; -pub mod documents; -pub mod driver; -pub mod episodic; -pub mod goals; -pub mod graph; -pub mod ingest; -pub mod maintenance; -pub mod people; -pub mod portability; -pub mod profile; -pub mod recall; -pub mod retrieval; -pub mod sources; -pub mod tool_memory; -pub mod tree; - -/// One member of the `TinyMemory` interface, as a typed request. -/// -/// An implementor names the member ([`METHOD`](Self::METHOD)), knows what comes -/// back ([`Response`](Self::Response)), and can lay its own fields out in the -/// positional order the module decodes them from -/// ([`into_args`](Self::into_args)). -/// -/// Implementors are generated from the module's `#[tinybus::interface]` block, -/// so the field order below is the wire order by construction rather than by -/// review. -pub trait BusCall { - /// The member name, as it travels in a frame. - /// - /// Always one of [`crate::names::METHODS`]. - const METHOD: &'static str; - - /// What the module replies with on success. - type Response: DeserializeOwned; - - /// Lay the arguments out as the positional array the module decodes. - /// - /// The result is always a JSON array — an empty one for a member that takes - /// no arguments, because `#[tinybus::interface]` skips decoding entirely in - /// that case and every caller sends `[]`. - /// - /// # Errors - /// - /// [`Error::Encode`] if a field fails to serialize. Unreachable for the - /// payload types on this wire, which are plain derived data; see - /// [`crate::error`]. - fn into_args(self) -> Result; - - /// Decode a successful reply body into this call's response type. - /// - /// # Errors - /// - /// [`Error::Decode`] if the body does not match - /// [`Response`](Self::Response) — in practice, a module built from a - /// different revision of this contract. - fn decode_response(body: Value) -> Result { - serde_json::from_value(body).map_err(Error::Decode) - } -} - -#[cfg(test)] -mod test; diff --git a/crates/tinymemory-bus/src/calls/people.rs b/crates/tinymemory-bus/src/calls/people.rs deleted file mode 100644 index 7e35cbe..0000000 --- a/crates/tinymemory-bus/src/calls/people.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! The people store: ranking, handles, scores and interactions. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `ListPeople`. -/// -/// Known people, ranked by closeness. -/// -/// Size-checked like the other list-returning methods. `limit` bounds the -/// *count* but not the bytes — a store of people each carrying many handles -/// can still overflow a frame — so the ceiling is enforced on the encoded -/// response rather than trusted to the caller's limit. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListPeople { - /// The `limit` argument — wire position 0. - pub limit: Option, -} - -impl BusCall for ListPeople { - const METHOD: &'static str = methods::LIST_PEOPLE; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.limit,)).map_err(Error::Encode) - } -} - -/// Arguments for `GetPerson`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GetPerson { - /// The `person_id` argument — wire position 0. - pub person_id: String, -} - -impl BusCall for GetPerson { - const METHOD: &'static str = methods::GET_PERSON; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.person_id,)).map_err(Error::Encode) - } -} - -/// Arguments for `ResolveHandle`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResolveHandle { - /// The `handle` argument — wire position 0. - pub handle: types::PersonHandle, - /// The `create_if_missing` argument — wire position 1. - pub create_if_missing: bool, -} - -impl BusCall for ResolveHandle { - const METHOD: &'static str = methods::RESOLVE_HANDLE; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.handle, self.create_if_missing)).map_err(Error::Encode) - } -} - -/// Arguments for `AddHandleAlias`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AddHandleAlias { - /// The `person_id` argument — wire position 0. - pub person_id: String, - /// The `handle` argument — wire position 1. - pub handle: types::PersonHandle, -} - -impl BusCall for AddHandleAlias { - const METHOD: &'static str = methods::ADD_HANDLE_ALIAS; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.person_id, self.handle)).map_err(Error::Encode) - } -} - -/// Arguments for `ScorePerson`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ScorePerson { - /// The `person_id` argument — wire position 0. - pub person_id: String, -} - -impl BusCall for ScorePerson { - const METHOD: &'static str = methods::SCORE_PERSON; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.person_id,)).map_err(Error::Encode) - } -} - -/// Arguments for `RecordInteraction`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecordInteraction { - /// The `interaction` argument — wire position 0. - pub interaction: types::PersonInteraction, -} - -impl BusCall for RecordInteraction { - const METHOD: &'static str = methods::RECORD_INTERACTION; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.interaction,)).map_err(Error::Encode) - } -} - -/// Arguments for `SeedFromAddressBook`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SeedFromAddressBook; - -impl BusCall for SeedFromAddressBook { - const METHOD: &'static str = methods::SEED_FROM_ADDRESS_BOOK; - - type Response = types::AddressBookSeedOutcome; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} diff --git a/crates/tinymemory-bus/src/calls/portability.rs b/crates/tinymemory-bus/src/calls/portability.rs deleted file mode 100644 index 4878aa5..0000000 --- a/crates/tinymemory-bus/src/calls/portability.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Paged export and bulk import of raw records. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `ExportPage`. -/// -/// Read one page of the export, continuing from `cursor`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ExportPage { - /// The `cursor` argument — wire position 0. - pub cursor: Option, - /// The `limit` argument — wire position 1. - pub limit: usize, -} - -impl BusCall for ExportPage { - const METHOD: &'static str = methods::EXPORT_PAGE; - - type Response = types::ExportPage; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.cursor, self.limit)).map_err(Error::Encode) - } -} - -/// Arguments for `ImportRecords`. -/// -/// Write a batch of previously-exported records. -/// -/// Partial success is reported inside `ImportOutcome` rather than as an -/// error, so a million-record restore is not aborted by one bad record. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ImportRecords { - /// The `records` argument — wire position 0. - pub records: Vec, -} - -impl BusCall for ImportRecords { - const METHOD: &'static str = methods::IMPORT_RECORDS; - - type Response = types::ImportOutcome; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.records,)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/profile.rs b/crates/tinymemory-bus/src/calls/profile.rs deleted file mode 100644 index 67f36b1..0000000 --- a/crates/tinymemory-bus/src/calls/profile.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Profile facets and their provenance. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `ListActiveFacets`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListActiveFacets; - -impl BusCall for ListActiveFacets { - const METHOD: &'static str = methods::LIST_ACTIVE_FACETS; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `ListAllFacets`. -/// -/// Takes no arguments, so it encodes as an empty positional array. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ListAllFacets; - -impl BusCall for ListAllFacets { - const METHOD: &'static str = methods::LIST_ALL_FACETS; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - Ok(Value::Array(Vec::new())) - } -} - -/// Arguments for `GetFacet`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GetFacet { - /// The `key` argument — wire position 0. - pub key: String, -} - -impl BusCall for GetFacet { - const METHOD: &'static str = methods::GET_FACET; - - type Response = Option; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.key,)).map_err(Error::Encode) - } -} - -/// Arguments for `FacetsByType`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FacetsByType { - /// The `facet_type` argument — wire position 0. - pub facet_type: types::FacetType, -} - -impl BusCall for FacetsByType { - const METHOD: &'static str = methods::FACETS_BY_TYPE; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.facet_type,)).map_err(Error::Encode) - } -} - -/// Arguments for `UpsertFacet`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpsertFacet { - /// The `facet` argument — wire position 0. - pub facet: types::ProfileFacet, -} - -impl BusCall for UpsertFacet { - const METHOD: &'static str = methods::UPSERT_FACET; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.facet,)).map_err(Error::Encode) - } -} - -/// Arguments for `UpsertProviderFacet`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UpsertProviderFacet { - /// The `facet_id` argument — wire position 0. - pub facet_id: String, - /// The `facet_type` argument — wire position 1. - pub facet_type: types::FacetType, - /// The `key` argument — wire position 2. - pub key: String, - /// The `value` argument — wire position 3. - pub value: String, - /// The `confidence` argument — wire position 4. - pub confidence: f64, - /// The `segment_id` argument — wire position 5. - pub segment_id: Option, - /// The `observed_at` argument — wire position 6. - pub observed_at: f64, -} - -impl BusCall for UpsertProviderFacet { - const METHOD: &'static str = methods::UPSERT_PROVIDER_FACET; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value(( - self.facet_id, - self.facet_type, - self.key, - self.value, - self.confidence, - self.segment_id, - self.observed_at, - )) - .map_err(Error::Encode) - } -} - -/// Arguments for `SetFacetUserState`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SetFacetUserState { - /// The `key` argument — wire position 0. - pub key: String, - /// The `user_state` argument — wire position 1. - pub user_state: types::UserState, -} - -impl BusCall for SetFacetUserState { - const METHOD: &'static str = methods::SET_FACET_USER_STATE; - - type Response = bool; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.key, self.user_state)).map_err(Error::Encode) - } -} - -/// Arguments for `DeleteFacet`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeleteFacet { - /// The `key` argument — wire position 0. - pub key: String, -} - -impl BusCall for DeleteFacet { - const METHOD: &'static str = methods::DELETE_FACET; - - type Response = bool; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.key,)).map_err(Error::Encode) - } -} - -/// Arguments for `DeleteFacetById`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeleteFacetById { - /// The `facet_id` argument — wire position 0. - pub facet_id: String, -} - -impl BusCall for DeleteFacetById { - const METHOD: &'static str = methods::DELETE_FACET_BY_ID; - - type Response = bool; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.facet_id,)).map_err(Error::Encode) - } -} - -/// Arguments for `DropFacetsBelow`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DropFacetsBelow { - /// The `threshold` argument — wire position 0. - pub threshold: f64, -} - -impl BusCall for DropFacetsBelow { - const METHOD: &'static str = methods::DROP_FACETS_BELOW; - - type Response = usize; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.threshold,)).map_err(Error::Encode) - } -} - -/// Arguments for `WorkflowIdentityMatches`. -/// -/// Returns `bool`, not `BusResult` on the trait — but the wire needs a -/// result, so an absent family answers `false` rather than erroring, which -/// is the trait's documented reading of "cannot tell" for this predicate. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WorkflowIdentityMatches { - /// The `key_pattern` argument — wire position 0. - pub key_pattern: String, - /// The `canonical_value` argument — wire position 1. - pub canonical_value: String, -} - -impl BusCall for WorkflowIdentityMatches { - const METHOD: &'static str = methods::WORKFLOW_IDENTITY_MATCHES; - - type Response = bool; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.key_pattern, self.canonical_value)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/recall.rs b/crates/tinymemory-bus/src/calls/recall.rs deleted file mode 100644 index 3969777..0000000 --- a/crates/tinymemory-bus/src/calls/recall.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Semantic recall over stored entries. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `Recall`. -/// -/// Ranked retrieval. -/// -/// `scope` is a query predicate the driver applies internally, not a filter -/// the host may apply to the result: narrowing afterwards would let the -/// driver spend its `limit` on entries the caller is not allowed to see and -/// then return fewer than it could have. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Recall { - /// The `query` argument — wire position 0. - pub query: String, - /// The `limit` argument — wire position 1. - pub limit: usize, - /// The `opts` argument — wire position 2. - pub opts: types::OwnedRecallOpts, - /// The `scope` argument — wire position 3. - pub scope: Option, -} - -impl BusCall for Recall { - const METHOD: &'static str = methods::RECALL; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.query, self.limit, self.opts, self.scope)).map_err(Error::Encode) - } -} - -/// Arguments for `RecallNamespaceScored`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RecallNamespaceScored { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `query` argument — wire position 1. - pub query: String, - /// The `limit` argument — wire position 2. - pub limit: usize, - /// The `exclude_session_id` argument — wire position 3. - pub exclude_session_id: Option, -} - -impl BusCall for RecallNamespaceScored { - const METHOD: &'static str = methods::RECALL_NAMESPACE_SCORED; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value(( - self.namespace, - self.query, - self.limit, - self.exclude_session_id, - )) - .map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/retrieval.rs b/crates/tinymemory-bus/src/calls/retrieval.rs deleted file mode 100644 index 78a9340..0000000 --- a/crates/tinymemory-bus/src/calls/retrieval.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! The scored retrieval surface. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `FastRetrieve`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FastRetrieve { - /// The `query` argument — wire position 0. - pub query: String, - /// The `options` argument — wire position 1. - pub options: types::FastRetrieveQuery, - /// The `scope` argument — wire position 2. - pub scope: Option, -} - -impl BusCall for FastRetrieve { - const METHOD: &'static str = methods::FAST_RETRIEVE; - - type Response = types::RetrievalResponse; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.query, self.options, self.scope)).map_err(Error::Encode) - } -} - -/// Arguments for `CoverWindow`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CoverWindow { - /// The `window` argument — wire position 0. - pub window: types::CoverWindowQuery, - /// The `scope` argument — wire position 1. - pub scope: Option, -} - -impl BusCall for CoverWindow { - const METHOD: &'static str = methods::COVER_WINDOW; - - type Response = types::RetrievalResponse; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.window, self.scope)).map_err(Error::Encode) - } -} - -/// Arguments for `RetrieveSource`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RetrieveSource { - /// The `query` argument — wire position 0. - pub query: types::SourceRetrievalQuery, - /// The `scope` argument — wire position 1. - pub scope: Option, -} - -impl BusCall for RetrieveSource { - const METHOD: &'static str = methods::RETRIEVE_SOURCE; - - type Response = types::RetrievalResponse; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.query, self.scope)).map_err(Error::Encode) - } -} - -/// Arguments for `RetrieveChildren`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RetrieveChildren { - /// The `node_id` argument — wire position 0. - pub node_id: String, - /// The `max_depth` argument — wire position 1. - pub max_depth: u32, - /// The `query` argument — wire position 2. - pub query: Option, - /// The `limit` argument — wire position 3. - pub limit: Option, - /// The `scope` argument — wire position 4. - pub scope: Option, -} - -impl BusCall for RetrieveChildren { - const METHOD: &'static str = methods::RETRIEVE_CHILDREN; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value(( - self.node_id, - self.max_depth, - self.query, - self.limit, - self.scope, - )) - .map_err(Error::Encode) - } -} - -/// Arguments for `RetrieveLeaves`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RetrieveLeaves { - /// The `chunk_ids` argument — wire position 0. - pub chunk_ids: Vec, - /// The `scope` argument — wire position 1. - pub scope: Option, -} - -impl BusCall for RetrieveLeaves { - const METHOD: &'static str = methods::RETRIEVE_LEAVES; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.chunk_ids, self.scope)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/sources.rs b/crates/tinymemory-bus/src/calls/sources.rs deleted file mode 100644 index b7c69ef..0000000 --- a/crates/tinymemory-bus/src/calls/sources.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Source snapshots, diffs, item acceptance and forgetting. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `CaptureSnapshot`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CaptureSnapshot { - /// The `source_id` argument — wire position 0. - pub source_id: String, -} - -impl BusCall for CaptureSnapshot { - const METHOD: &'static str = methods::CAPTURE_SNAPSHOT; - - type Response = types::SnapshotRef; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.source_id,)).map_err(Error::Encode) - } -} - -/// Arguments for `Snapshots`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Snapshots { - /// The `source_id` argument — wire position 0. - pub source_id: String, - /// The `limit` argument — wire position 1. - pub limit: usize, -} - -impl BusCall for Snapshots { - const METHOD: &'static str = methods::SNAPSHOTS; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.source_id, self.limit)).map_err(Error::Encode) - } -} - -/// Arguments for `Diff`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Diff { - /// The `source_id` argument — wire position 0. - pub source_id: String, - /// The `from` argument — wire position 1. - pub from: Option, - /// The `to` argument — wire position 2. - pub to: String, -} - -impl BusCall for Diff { - const METHOD: &'static str = methods::DIFF; - - type Response = types::DiffReport; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.source_id, self.from, self.to)).map_err(Error::Encode) - } -} - -/// Arguments for `AcceptSourceItems`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AcceptSourceItems { - /// The `source_id` argument — wire position 0. - pub source_id: String, - /// The `source_kind` argument — wire position 1. - pub source_kind: String, - /// The `items` argument — wire position 2. - pub items: Vec, - /// The `taint` argument — wire position 3. - pub taint: types::MemoryTaint, -} - -impl BusCall for AcceptSourceItems { - const METHOD: &'static str = methods::ACCEPT_SOURCE_ITEMS; - - type Response = types::IngestOutcome; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.source_id, self.source_kind, self.items, self.taint)) - .map_err(Error::Encode) - } -} - -/// Arguments for `ForgetSource`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ForgetSource { - /// The `source_id` argument — wire position 0. - pub source_id: String, -} - -impl BusCall for ForgetSource { - const METHOD: &'static str = methods::FORGET_SOURCE; - - type Response = u64; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.source_id,)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/test.rs b/crates/tinymemory-bus/src/calls/test.rs deleted file mode 100644 index 1271e28..0000000 --- a/crates/tinymemory-bus/src/calls/test.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! Completeness and encoding tests for the generated call structs. -//! -//! The interesting property is coverage. A member the module serves but this -//! crate has no struct for is not a compile error anywhere — it is a host -//! discovering at runtime that the only way to make the call is to hand-build -//! the argument array, which is exactly what this crate exists to prevent. So -//! the table below is checked against `crate::names::METHODS` in both -//! directions. - -// A failed assertion in a test is a panic either way; `expect` here says what -// the invariant was. Same allowance the crate's other test modules take. -#![allow(clippy::expect_used, clippy::panic)] - -use serde_json::json; - -use crate::calls::BusCall; -use crate::names::METHODS; - -/// The member every call struct in this crate names, one entry per struct. -/// -/// Written out rather than derived, because deriving it from the same source -/// the structs come from would make the test agree with itself by -/// construction. -const COVERED: [&str; 89] = [ - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, - ::METHOD, -]; - -#[test] -fn every_member_has_a_call_struct() { - let mut missing: Vec<&str> = METHODS - .into_iter() - .filter(|member| !COVERED.contains(member)) - .collect(); - missing.sort_unstable(); - assert!( - missing.is_empty(), - "members with no call struct: {missing:?}" - ); -} - -#[test] -fn every_call_struct_names_a_known_member() { - let mut unknown: Vec<&str> = COVERED - .into_iter() - .filter(|member| !METHODS.contains(member)) - .collect(); - unknown.sort_unstable(); - assert!( - unknown.is_empty(), - "call structs naming no member: {unknown:?}" - ); -} - -#[test] -fn no_member_is_covered_twice() { - let mut seen = COVERED; - seen.sort_unstable(); - let mut unique = seen.to_vec(); - unique.dedup(); - assert_eq!( - unique.len(), - seen.len(), - "two call structs name the same member" - ); -} - -#[test] -fn arguments_encode_as_a_positional_array_in_declaration_order() { - // `Diff` is the useful shape to pin: three arguments, the middle one - // optional. A struct field reordering that a reader would not notice - // shows up here as a moved `null`. - let args = crate::calls::sources::Diff { - source_id: "src-1".to_string(), - from: None, - to: "snap-2".to_string(), - } - .into_args() - .expect("plain data serializes"); - assert_eq!(args, json!(["src-1", null, "snap-2"])); -} - -#[test] -fn a_member_with_no_arguments_encodes_as_an_empty_array() { - // Not `null`: `#[tinybus::interface]` skips argument decoding entirely - // for a zero-argument member, and every caller sends `[]`. - let args = crate::calls::maintenance::Doctor - .into_args() - .expect("no fields to serialize"); - assert_eq!(args, json!([])); -} - -#[test] -fn a_reply_decodes_into_the_calls_response_type() { - use crate::calls::core::Forget; - - let decoded = Forget::decode_response(json!(true)).expect("a bool reply"); - assert!(decoded); -} - -#[test] -fn a_reply_of_the_wrong_shape_is_a_decode_error() { - use crate::calls::core::Forget; - use crate::error::Error; - - // The version-skew case: a module built from a different contract - // answering something this build cannot read. - let failure = Forget::decode_response(json!("yes")).expect_err("a string is not a bool"); - assert!(matches!(failure, Error::Decode(_))); -} diff --git a/crates/tinymemory-bus/src/calls/tool_memory.rs b/crates/tinymemory-bus/src/calls/tool_memory.rs deleted file mode 100644 index ddd181a..0000000 --- a/crates/tinymemory-bus/src/calls/tool_memory.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Tool-scoped memory rules. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `ToolRules`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ToolRules { - /// The `tool_name` argument — wire position 0. - pub tool_name: String, -} - -impl BusCall for ToolRules { - const METHOD: &'static str = methods::TOOL_RULES; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.tool_name,)).map_err(Error::Encode) - } -} - -/// Arguments for `PutToolRule`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PutToolRule { - /// The `rule` argument — wire position 0. - pub rule: types::ToolMemoryRule, -} - -impl BusCall for PutToolRule { - const METHOD: &'static str = methods::PUT_TOOL_RULE; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.rule,)).map_err(Error::Encode) - } -} - -/// Arguments for `DeleteToolRule`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeleteToolRule { - /// The `tool_name` argument — wire position 0. - pub tool_name: String, - /// The `rule_id` argument — wire position 1. - pub rule_id: String, -} - -impl BusCall for DeleteToolRule { - const METHOD: &'static str = methods::DELETE_TOOL_RULE; - - type Response = bool; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.tool_name, self.rule_id)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/calls/tree.rs b/crates/tinymemory-bus/src/calls/tree.rs deleted file mode 100644 index 26dd41a..0000000 --- a/crates/tinymemory-bus/src/calls/tree.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! The markdown summary tree: append, query, drill down, seal, cascade. -//! -//! One [`BusCall`] per member; see [`crate::calls`] for how they are used. - -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::calls::BusCall; -use crate::error::Error; -use crate::names::methods; -use crate::types; - -/// Arguments for `Append`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Append { - /// The `request` argument — wire position 0. - pub request: types::IngestRequest, -} - -impl BusCall for Append { - const METHOD: &'static str = methods::APPEND; - - type Response = (); - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.request,)).map_err(Error::Encode) - } -} - -/// Arguments for `QuerySource`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct QuerySource { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `source_id` argument — wire position 1. - pub source_id: String, - /// The `limit` argument — wire position 2. - pub limit: usize, - /// The `scope` argument — wire position 3. - pub scope: Option, -} - -impl BusCall for QuerySource { - const METHOD: &'static str = methods::QUERY_SOURCE; - - type Response = Vec; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.source_id, self.limit, self.scope)) - .map_err(Error::Encode) - } -} - -/// Arguments for `DrillDown`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DrillDown { - /// The `namespace` argument — wire position 0. - pub namespace: String, - /// The `node_id` argument — wire position 1. - pub node_id: String, -} - -impl BusCall for DrillDown { - const METHOD: &'static str = methods::DRILL_DOWN; - - type Response = types::QueryResult; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace, self.node_id)).map_err(Error::Encode) - } -} - -/// Arguments for `Seal`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Seal { - /// The `namespace` argument — wire position 0. - pub namespace: String, -} - -impl BusCall for Seal { - const METHOD: &'static str = methods::SEAL; - - type Response = types::TreeStatus; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace,)).map_err(Error::Encode) - } -} - -/// Arguments for `Cascade`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Cascade { - /// The `namespace` argument — wire position 0. - pub namespace: String, -} - -impl BusCall for Cascade { - const METHOD: &'static str = methods::CASCADE; - - type Response = types::TreeStatus; - - fn into_args(self) -> crate::Result { - serde_json::to_value((self.namespace,)).map_err(Error::Encode) - } -} diff --git a/crates/tinymemory-bus/src/error/mod.rs b/crates/tinymemory-bus/src/error/mod.rs deleted file mode 100644 index a284e64..0000000 --- a/crates/tinymemory-bus/src/error/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! The crate-wide [`Error`] and its [`Result`] alias. -//! -//! # This is not the memory error -//! -//! A failed *memory operation* is a [`MemoryError`], and it travels back from -//! the module as a `(name, message)` pair that [`crate::wire`] converts. That -//! is the interesting error, and it is not this one. -//! -//! [`Error`] covers the far narrower thing this crate does on its own: turning -//! a typed call into an argument array, and turning a reply body back into a -//! typed response. Both are `serde_json` operations, so both can fail, and both -//! failures mean the same thing — the contract and the peer disagree about a -//! payload's shape. -//! -//! Keeping the two apart matters at the call site. A host that gets a -//! [`MemoryError::NotFound`] has learned something about its data; a host that -//! gets an [`Error::Decode`] has learned that its build of this crate does not -//! match the module it is talking to, which is an operator problem and not a -//! caller one. -//! -//! [`MemoryError`]: tinymemory_api::error::MemoryError -//! [`MemoryError::NotFound`]: tinymemory_api::error::MemoryError::NotFound - -/// A failure encoding a call's arguments or decoding its reply. -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum Error { - /// A call's arguments could not be serialized into a frame body. - /// - /// In practice this is unreachable for the payload types on this wire — - /// they are plain data with derived `Serialize` impls. It stays a `Result` - /// rather than an unwrap because "in practice unreachable" is not the same - /// as unreachable, and a panic in a host's memory path is a worse answer - /// than an error it can log. - #[error("encoding call arguments failed: {0}")] - Encode(#[source] serde_json::Error), - - /// A reply body did not match the response type this contract expects. - /// - /// The usual cause is a version skew: the module was built from a newer - /// contract than the host. The message carries `serde_json`'s path into the - /// offending value, which names the field but not user memory content. - #[error("decoding a reply failed: {0}")] - Decode(#[source] serde_json::Error), -} - -/// The result type returned by every fallible function in this crate. -pub type Result = std::result::Result; - -#[cfg(test)] -mod test; diff --git a/crates/tinymemory-bus/src/error/test.rs b/crates/tinymemory-bus/src/error/test.rs deleted file mode 100644 index 4c2caf7..0000000 --- a/crates/tinymemory-bus/src/error/test.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Unit tests for the crate-wide error type. -// A failed assertion in a test is a panic either way; `expect` here says what -// the invariant was. Same allowance the crate's other test modules take. -#![allow(clippy::expect_used, clippy::panic)] - -use super::{Error, Result}; - -/// A decode failure of the shape a version skew produces. -fn decode_failure() -> Result { - serde_json::from_value::(serde_json::json!("not a number")).map_err(Error::Decode) -} - -#[test] -fn decode_carries_the_serde_message() { - let error = decode_failure().expect_err("a string does not deserialize as u64"); - let rendered = error.to_string(); - assert!( - rendered.starts_with("decoding a reply failed: "), - "unexpected rendering: {rendered}" - ); -} - -#[test] -fn encode_and_decode_are_distinguishable() { - // The whole point of two variants: a host branches on which side of the - // call went wrong, so they must not collapse into one string prefix. - let decode = decode_failure().expect_err("a string does not deserialize as u64"); - let encode = Error::Encode( - serde_json::to_value(f64::NAN) - .err() - .unwrap_or_else(|| serde_json::from_str::("x").expect_err("not a number")), - ); - assert_ne!(decode.to_string(), encode.to_string()); - assert!(matches!(decode, Error::Decode(_))); - assert!(matches!(encode, Error::Encode(_))); -} diff --git a/crates/tinymemory-bus/src/names/mod.rs b/crates/tinymemory-bus/src/names.rs similarity index 100% rename from crates/tinymemory-bus/src/names/mod.rs rename to crates/tinymemory-bus/src/names.rs diff --git a/crates/tinymemory-bus/src/names/test.rs b/crates/tinymemory-bus/src/names_tests.rs similarity index 100% rename from crates/tinymemory-bus/src/names/test.rs rename to crates/tinymemory-bus/src/names_tests.rs diff --git a/crates/tinymemory-bus/src/types/mod.rs b/crates/tinymemory-bus/src/types/mod.rs deleted file mode 100644 index 6e2f630..0000000 --- a/crates/tinymemory-bus/src/types/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Every value type that crosses the bus, re-exported from the contract crate. -//! -//! # These are re-exports, deliberately, and not definitions -//! -//! The obvious reading of "a crate that holds the bus types" is a crate that -//! *defines* them. That would be wrong here, and the repository already -//! documents why in the root manifest: when `tinymemory-api` was resolved -//! twice, `MemoryCategory` from one copy was not the same type as -//! `MemoryCategory` from the other, and the mismatch only showed up at the -//! seam. Defining a second set of structurally identical types here would -//! reproduce that on purpose: the module would serve `tinymemory_api::` -//! types, the host would hold `tinymemory_bus::` ones, and every call site -//! would need a conversion whose correctness nothing checks. -//! -//! So one definition, in `tinymemory-api`, surfaced here. A host that depends -//! on this crate gets exactly the types the module serves — the same types, -//! not equivalents. -//! -//! # Why the host does not just depend on `tinymemory-api` -//! -//! It could, and it would compile. But `tinymemory-api` is the *driver* -//! contract: it also carries `MemoryProvider` and its capability traits, the -//! mandatory-family composition, the null driver, and the `host::` config -//! sections. A host that loads the module implements none of those — it makes -//! calls. This crate is the subset that crosses a frame, so what a host -//! compiles against is what it can actually send and receive. -//! -//! The grouping below mirrors the capability families in [`crate::calls`]. - -pub use tinymemory_api::capabilities::{Capabilities, Capability}; -pub use tinymemory_api::chunks::{Chunk, Metadata, SourceRef}; -pub use tinymemory_api::error::MemoryError; -pub use tinymemory_api::goals::{GoalItem, GoalsDoc}; -pub use tinymemory_api::health::MemoryHealth; -pub use tinymemory_api::provider::chunks::{ChunkDetail, ChunkEmbedding, ChunkQuery}; -pub use tinymemory_api::provider::episodic::{ConversationSegment, EpisodicTurn}; -pub use tinymemory_api::provider::people::{ - AddressBookSeedOutcome, PersonHandle, PersonInteraction, PersonRecord, PersonScore, - RankedPerson, ResolvedPerson, -}; -pub use tinymemory_api::provider::profile::{FacetType, ProfileFacet, UserState}; -pub use tinymemory_api::provider::retrieval::{ - CoverWindowQuery, EntityMatch, FastRetrieveQuery, RetrievalHit, RetrievalResponse, - SourceRetrievalQuery, -}; -pub use tinymemory_api::provider::types::{ - DiffReport, EntityHit, ExportPage, ExportRecord, ImportOutcome, IngestItem, IngestOutcome, - MaintenanceReport, SnapshotRef, SourceItem, SourceScope, -}; -pub use tinymemory_api::recall::OwnedRecallOpts; -pub use tinymemory_api::tool_memory::ToolMemoryRule; -pub use tinymemory_api::tree::{IngestRequest, QueryResult, TreeStatus}; -pub use tinymemory_api::types::{ - GraphRelationRecord, MemoryCategory, MemoryEntry, MemoryKvRecord, MemoryTaint, - NamespaceDocumentInput, NamespaceMemoryHit, NamespaceRetrievalContext, NamespaceSummary, - StoredMemoryDocument, -}; - -/// `serde_json::Value`, which three document methods return verbatim. -/// -/// `ListDocuments` and `DeleteDocument` answer with a driver-shaped JSON -/// document rather than a typed record, so a host has to hold the untyped -/// value. Re-exported here so it arrives from the same place as everything -/// else on the wire and a host does not have to match `serde_json` versions -/// by hand. -pub use serde_json::Value as JsonValue; diff --git a/crates/tinymemory-bus/src/wire/mod.rs b/crates/tinymemory-bus/src/wire/mod.rs deleted file mode 100644 index 43adb65..0000000 --- a/crates/tinymemory-bus/src/wire/mod.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! How a failed call comes back, and how a host turns it into a -//! [`MemoryError`] again. -//! -//! A `TinyBus` error is a name and a message. The name is the contract; the -//! message is for a human and must never carry a namespace key, an entry's -//! content, a recall query, a credential or an absolute path. -//! -//! The table that maps names to [`MemoryError`] variants lives in -//! `tinymemory-api` and is used by **both** ends — the module maps out, the -//! host maps back. It is re-exported here rather than restated for the same -//! reason the payload types are: two copies of a name table drift, and the -//! symptom of drift is a security-relevant `PathEscape` silently reclassified -//! as a caller mistake. -//! -//! ``` -//! use tinymemory_bus::wire; -//! use tinymemory_bus::types::MemoryError; -//! -//! // What a host does with the `(name, message)` pair a failed call returns. -//! let recovered = wire::from_wire(wire::NOT_FOUND, "no such source"); -//! assert!(matches!(recovered, MemoryError::NotFound(_))); -//! ``` -//! -//! # An unrecognised name is a backend failure, never a caller mistake -//! -//! [`from_wire`] maps a name it does not know to [`MemoryError::Other`]. A -//! module newer than the host's build may name an error this table has no -//! variant for, and answering "your input was wrong" when it was not sends a -//! caller into a rewrite loop over something already correct. -//! -//! [`MemoryError`]: tinymemory_api::error::MemoryError -//! [`MemoryError::Other`]: tinymemory_api::error::MemoryError::Other -//! [`MemoryError::NotFound`]: tinymemory_api::error::MemoryError::NotFound - -pub use tinymemory_api::wire::{ - from_wire, wire_message, wire_name, BACKEND, BUDGET_EXCEEDED, INVALID, IO, NOT_FOUND, OTHER, - PATH_ESCAPE, SERDE, TIMEOUT, UNAUTHORIZED, UNAVAILABLE, UNREACHABLE, UNSUPPORTED, -}; - -#[cfg(test)] -mod test; diff --git a/crates/tinymemory-bus/src/wire/test.rs b/crates/tinymemory-bus/src/wire/test.rs deleted file mode 100644 index 729b486..0000000 --- a/crates/tinymemory-bus/src/wire/test.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! The re-exported error table still round-trips from this crate's paths. -//! -//! `tinymemory_api::wire_tests` pins the table itself. What is checked here is -//! that the re-export surfaces the whole of it — a name constant that failed to -//! come across would leave a host unable to recognise that error class, and a -//! missing `pub use` is invisible until someone reaches for it. -// A failed assertion in a test is a panic either way; `expect` here says what -// the invariant was. Same allowance the crate's other test modules take. -#![allow(clippy::expect_used, clippy::panic)] - -use super::{from_wire, wire_name}; -use crate::types::MemoryError; - -#[test] -fn every_name_constant_is_reachable_from_this_crate() { - let names = [ - super::NOT_FOUND, - super::INVALID, - super::BUDGET_EXCEEDED, - super::PATH_ESCAPE, - super::IO, - super::SERDE, - super::UNSUPPORTED, - super::OTHER, - super::UNAUTHORIZED, - super::UNREACHABLE, - super::TIMEOUT, - super::UNAVAILABLE, - super::BACKEND, - ]; - for name in names { - assert!( - name.starts_with("ai.tinyhumans.tinymemory.Error."), - "{name} is not under the contract's error namespace" - ); - } -} - -#[test] -fn a_named_error_round_trips_through_the_re_exports() { - let recovered = from_wire(super::PATH_ESCAPE, "symlink leaves workspace"); - assert!(matches!(recovered, MemoryError::PathEscape(_))); - // Back out again under the same name: the two directions are the same - // table, which is the property that keeps the ends from drifting. - assert_eq!(wire_name(&recovered), super::PATH_ESCAPE); -} - -#[test] -fn an_unknown_name_is_a_backend_failure_not_a_caller_mistake() { - // A module newer than this build may name an error this table has no - // variant for. Reporting that as `Invalid` would tell a caller its input - // was wrong when it was not. - let recovered = from_wire("ai.tinyhumans.tinymemory.Error.FromTheFuture", "…"); - assert!(matches!(recovered, MemoryError::Other(_))); -} From 3fbdec7d6c294c821d13084511dd236b485ef0ff Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:55:21 +0300 Subject: [PATCH 13/35] chore(tinymemory): move source files from tinymemory-api to tinymemory-bus Relocated all source and test files from the tinymemory-api crate into the tinymemory-bus crate, consolidating the codebase into a single crate. This change simplifies the project structure by removing the separate api crate and keeping all functionality under the bus crate. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/{tinymemory-api => tinymemory-bus}/src/capabilities.rs | 0 .../{tinymemory-api => tinymemory-bus}/src/capabilities_tests.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/chunks.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/chunks_tests.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/error.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/error_tests.rs | 0 .../{tinymemory-api/src/host => tinymemory-bus/src}/evidence.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/goals.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/goals_tests.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/health.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/health_tests.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/recall.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/recall_tests.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/tool_memory.rs | 0 .../{tinymemory-api => tinymemory-bus}/src/tool_memory_tests.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/tree.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/tree_tests.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/types.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/types_tests.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/version.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/version_tests.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/wire.rs | 0 crates/{tinymemory-api => tinymemory-bus}/src/wire_tests.rs | 0 23 files changed, 0 insertions(+), 0 deletions(-) rename crates/{tinymemory-api => tinymemory-bus}/src/capabilities.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/capabilities_tests.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/chunks.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/chunks_tests.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/error.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/error_tests.rs (100%) rename crates/{tinymemory-api/src/host => tinymemory-bus/src}/evidence.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/goals.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/goals_tests.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/health.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/health_tests.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/recall.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/recall_tests.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/tool_memory.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/tool_memory_tests.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/tree.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/tree_tests.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/types.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/types_tests.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/version.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/version_tests.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/wire.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/wire_tests.rs (100%) diff --git a/crates/tinymemory-api/src/capabilities.rs b/crates/tinymemory-bus/src/capabilities.rs similarity index 100% rename from crates/tinymemory-api/src/capabilities.rs rename to crates/tinymemory-bus/src/capabilities.rs diff --git a/crates/tinymemory-api/src/capabilities_tests.rs b/crates/tinymemory-bus/src/capabilities_tests.rs similarity index 100% rename from crates/tinymemory-api/src/capabilities_tests.rs rename to crates/tinymemory-bus/src/capabilities_tests.rs diff --git a/crates/tinymemory-api/src/chunks.rs b/crates/tinymemory-bus/src/chunks.rs similarity index 100% rename from crates/tinymemory-api/src/chunks.rs rename to crates/tinymemory-bus/src/chunks.rs diff --git a/crates/tinymemory-api/src/chunks_tests.rs b/crates/tinymemory-bus/src/chunks_tests.rs similarity index 100% rename from crates/tinymemory-api/src/chunks_tests.rs rename to crates/tinymemory-bus/src/chunks_tests.rs diff --git a/crates/tinymemory-api/src/error.rs b/crates/tinymemory-bus/src/error.rs similarity index 100% rename from crates/tinymemory-api/src/error.rs rename to crates/tinymemory-bus/src/error.rs diff --git a/crates/tinymemory-api/src/error_tests.rs b/crates/tinymemory-bus/src/error_tests.rs similarity index 100% rename from crates/tinymemory-api/src/error_tests.rs rename to crates/tinymemory-bus/src/error_tests.rs diff --git a/crates/tinymemory-api/src/host/evidence.rs b/crates/tinymemory-bus/src/evidence.rs similarity index 100% rename from crates/tinymemory-api/src/host/evidence.rs rename to crates/tinymemory-bus/src/evidence.rs diff --git a/crates/tinymemory-api/src/goals.rs b/crates/tinymemory-bus/src/goals.rs similarity index 100% rename from crates/tinymemory-api/src/goals.rs rename to crates/tinymemory-bus/src/goals.rs diff --git a/crates/tinymemory-api/src/goals_tests.rs b/crates/tinymemory-bus/src/goals_tests.rs similarity index 100% rename from crates/tinymemory-api/src/goals_tests.rs rename to crates/tinymemory-bus/src/goals_tests.rs diff --git a/crates/tinymemory-api/src/health.rs b/crates/tinymemory-bus/src/health.rs similarity index 100% rename from crates/tinymemory-api/src/health.rs rename to crates/tinymemory-bus/src/health.rs diff --git a/crates/tinymemory-api/src/health_tests.rs b/crates/tinymemory-bus/src/health_tests.rs similarity index 100% rename from crates/tinymemory-api/src/health_tests.rs rename to crates/tinymemory-bus/src/health_tests.rs diff --git a/crates/tinymemory-api/src/recall.rs b/crates/tinymemory-bus/src/recall.rs similarity index 100% rename from crates/tinymemory-api/src/recall.rs rename to crates/tinymemory-bus/src/recall.rs diff --git a/crates/tinymemory-api/src/recall_tests.rs b/crates/tinymemory-bus/src/recall_tests.rs similarity index 100% rename from crates/tinymemory-api/src/recall_tests.rs rename to crates/tinymemory-bus/src/recall_tests.rs diff --git a/crates/tinymemory-api/src/tool_memory.rs b/crates/tinymemory-bus/src/tool_memory.rs similarity index 100% rename from crates/tinymemory-api/src/tool_memory.rs rename to crates/tinymemory-bus/src/tool_memory.rs diff --git a/crates/tinymemory-api/src/tool_memory_tests.rs b/crates/tinymemory-bus/src/tool_memory_tests.rs similarity index 100% rename from crates/tinymemory-api/src/tool_memory_tests.rs rename to crates/tinymemory-bus/src/tool_memory_tests.rs diff --git a/crates/tinymemory-api/src/tree.rs b/crates/tinymemory-bus/src/tree.rs similarity index 100% rename from crates/tinymemory-api/src/tree.rs rename to crates/tinymemory-bus/src/tree.rs diff --git a/crates/tinymemory-api/src/tree_tests.rs b/crates/tinymemory-bus/src/tree_tests.rs similarity index 100% rename from crates/tinymemory-api/src/tree_tests.rs rename to crates/tinymemory-bus/src/tree_tests.rs diff --git a/crates/tinymemory-api/src/types.rs b/crates/tinymemory-bus/src/types.rs similarity index 100% rename from crates/tinymemory-api/src/types.rs rename to crates/tinymemory-bus/src/types.rs diff --git a/crates/tinymemory-api/src/types_tests.rs b/crates/tinymemory-bus/src/types_tests.rs similarity index 100% rename from crates/tinymemory-api/src/types_tests.rs rename to crates/tinymemory-bus/src/types_tests.rs diff --git a/crates/tinymemory-api/src/version.rs b/crates/tinymemory-bus/src/version.rs similarity index 100% rename from crates/tinymemory-api/src/version.rs rename to crates/tinymemory-bus/src/version.rs diff --git a/crates/tinymemory-api/src/version_tests.rs b/crates/tinymemory-bus/src/version_tests.rs similarity index 100% rename from crates/tinymemory-api/src/version_tests.rs rename to crates/tinymemory-bus/src/version_tests.rs diff --git a/crates/tinymemory-api/src/wire.rs b/crates/tinymemory-bus/src/wire.rs similarity index 100% rename from crates/tinymemory-api/src/wire.rs rename to crates/tinymemory-bus/src/wire.rs diff --git a/crates/tinymemory-api/src/wire_tests.rs b/crates/tinymemory-bus/src/wire_tests.rs similarity index 100% rename from crates/tinymemory-api/src/wire_tests.rs rename to crates/tinymemory-bus/src/wire_tests.rs From 2c1ac19de25ef55f4814dcc6157e7eff7bfe07d2 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:55:28 +0300 Subject: [PATCH 14/35] chore(tinymemory): move provider types from api crate to bus crate Moved the provider types module and its tests from the tinymemory-api crate to the tinymemory-bus crate, where they are actually used. This keeps the API crate focused on interface definitions and places implementation details in the bus crate. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/{tinymemory-api => tinymemory-bus}/src/provider/types.rs | 0 .../src/provider/types_tests.rs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename crates/{tinymemory-api => tinymemory-bus}/src/provider/types.rs (100%) rename crates/{tinymemory-api => tinymemory-bus}/src/provider/types_tests.rs (100%) diff --git a/crates/tinymemory-api/src/provider/types.rs b/crates/tinymemory-bus/src/provider/types.rs similarity index 100% rename from crates/tinymemory-api/src/provider/types.rs rename to crates/tinymemory-bus/src/provider/types.rs diff --git a/crates/tinymemory-api/src/provider/types_tests.rs b/crates/tinymemory-bus/src/provider/types_tests.rs similarity index 100% rename from crates/tinymemory-api/src/provider/types_tests.rs rename to crates/tinymemory-bus/src/provider/types_tests.rs From 297209aa0bd4cb611415e6b50544b92210787174 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:55:51 +0300 Subject: [PATCH 15/35] refactor(provider): move type definitions from api to bus crate Moved the data types that were previously defined in the tinymemory-api provider modules into the corresponding tinymemory-bus provider modules, where they are now untracked. This keeps the API crate focused on trait definitions while the bus crate owns the concrete types used for serialization and transport. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-api/src/provider/chunks.rs | 86 -------- .../tinymemory-api/src/provider/episodic.rs | 75 ------- crates/tinymemory-api/src/provider/people.rs | 129 ------------ crates/tinymemory-api/src/provider/profile.rs | 156 --------------- .../tinymemory-api/src/provider/retrieval.rs | 141 ------------- crates/tinymemory-bus/src/provider/chunks.rs | 117 +++++++++++ .../tinymemory-bus/src/provider/episodic.rs | 116 +++++++++++ crates/tinymemory-bus/src/provider/people.rs | 161 +++++++++++++++ crates/tinymemory-bus/src/provider/profile.rs | 185 ++++++++++++++++++ .../tinymemory-bus/src/provider/retrieval.rs | 176 +++++++++++++++++ 10 files changed, 755 insertions(+), 587 deletions(-) create mode 100644 crates/tinymemory-bus/src/provider/chunks.rs create mode 100644 crates/tinymemory-bus/src/provider/episodic.rs create mode 100644 crates/tinymemory-bus/src/provider/people.rs create mode 100644 crates/tinymemory-bus/src/provider/profile.rs create mode 100644 crates/tinymemory-bus/src/provider/retrieval.rs diff --git a/crates/tinymemory-api/src/provider/chunks.rs b/crates/tinymemory-api/src/provider/chunks.rs index 34c7635..3ff8729 100644 --- a/crates/tinymemory-api/src/provider/chunks.rs +++ b/crates/tinymemory-api/src/provider/chunks.rs @@ -30,92 +30,6 @@ //! failure mode with a real precedent, and it is silent; see //! `docs/specs/2026-08-13-memory-module-port.md` §3. -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; - -use crate::chunks::{Chunk, SourceKind}; -use crate::error::MemoryError; -use crate::provider::types::SourceScope; - -/// Filters for [`MemoryChunks::list_chunks`]. -/// -/// Every field is optional and they compose with AND. The default matches -/// everything the scope allows, bounded by the driver's own safety cap. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct ChunkQuery { - /// Restrict to one source kind. - #[serde(default)] - pub source_kind: Option, - /// Restrict to one logical source id. - #[serde(default)] - pub source_id: Option, - /// Restrict to one owner. - #[serde(default)] - pub owner: Option, - /// Inclusive lower bound on source time, epoch milliseconds. - #[serde(default)] - pub since_ms: Option, - /// Inclusive upper bound on source time, epoch milliseconds. - #[serde(default)] - pub until_ms: Option, - /// Maximum rows. The driver clamps this to its own cap — a caller cannot - /// raise the ceiling by asking for more. - #[serde(default)] - pub limit: Option, - /// Rows to skip, for pagination. - #[serde(default)] - pub offset: Option, - /// Drop chunks marked dropped by the lifecycle. - #[serde(default)] - pub exclude_dropped: bool, -} - -/// One chunk's stored embedding. -/// -/// Returned as a list rather than a map because the wire form of a map keyed by -/// chunk id is a JSON object, and an id is caller-supplied text; a list keeps -/// the encoding independent of what an id happens to contain. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ChunkEmbedding { - /// The chunk this vector belongs to. - pub chunk_id: String, - /// The vector, in the embedding space named by the requested signature. - pub vector: Vec, -} - -/// One chunk plus the per-chunk facts stored beside it. -/// -/// # Why a detail view rather than four accessors -/// -/// An inspection caller wants the row, its body, where the body lives, its -/// lifecycle state and whether it has been embedded. Exposing those as four -/// methods would read naturally in-process and cost **four bus round trips per -/// row** out of it — and this is used to render lists. One method, one trip. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ChunkDetail { - /// The chunk row. - pub chunk: Chunk, - /// The chunk's body as stored in the content vault, when it could be read. - /// - /// `None` means the vault read failed — distinct from an empty body, which - /// is a legitimately empty chunk. A caller rendering a preview should fall - /// back to [`Chunk::content`] rather than showing nothing. - #[serde(default)] - pub body: Option, - /// Path of the body in the content vault, when it has one. - #[serde(default)] - pub content_path: Option, - /// Lifecycle state (`active`, `dropped`, …); `None` when unrecorded. - #[serde(default)] - pub lifecycle_status: Option, - /// Whether an embedding vector exists for this chunk in **any** space. - /// - /// Not scoped to a signature on purpose: this answers "has this been - /// embedded at all", which is what an inspection view wants. Asking whether - /// a *particular* space has it is [`MemoryChunks::chunk_embeddings`]. - pub has_embedding: bool, -} - /// Direct read access to the chunk tier. /// /// Reached through [`MemoryProvider::as_chunks`](super::MemoryProvider::as_chunks). diff --git a/crates/tinymemory-api/src/provider/episodic.rs b/crates/tinymemory-api/src/provider/episodic.rs index 0828bda..9f51462 100644 --- a/crates/tinymemory-api/src/provider/episodic.rs +++ b/crates/tinymemory-api/src/provider/episodic.rs @@ -40,81 +40,6 @@ //! trip instead of two, and no reliance on connection-local state. The engine //! knows the id it just wrote; nothing else has to guess. -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; - -use crate::error::MemoryError; - -/// One recorded turn. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct EpisodicTurn { - /// Row id, assigned by the driver on insert. - /// - /// `None` when the host is describing a turn to be written; always `Some` - /// on a turn read back. - #[serde(default)] - pub id: Option, - /// Session this turn belongs to. - pub session_id: String, - /// When it happened, epoch seconds with sub-second resolution. - /// - /// The archivist offsets an assistant turn by 1 ms from the user turn it - /// answers so the pair sorts in order within one exchange; that convention - /// is the host's and the driver must preserve the value it is given rather - /// than re-stamping it. - pub timestamp: f64, - /// `"user"` or `"assistant"`. Open vocabulary — a driver must not reject an - /// unfamiliar role. - pub role: String, - /// The turn's text. - pub content: String, - /// A short lesson extracted from tool failures, when there was one. - #[serde(default)] - pub lesson: Option, - /// Serialized tool-call summary, when the turn made any. - #[serde(default)] - pub tool_calls_json: Option, - /// Cost attributed to this turn, in microdollars. - #[serde(default)] - pub cost_microdollars: i64, -} - -/// A stretch of consecutive turns about one subject. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ConversationSegment { - /// Stable id, chosen by the host. - pub segment_id: String, - /// Session the segment belongs to. - pub session_id: String, - /// Owning namespace. - pub namespace: String, - /// Row id of the first turn in the segment. - pub start_episodic_id: i64, - /// Row id of the last turn, once one has been appended. - #[serde(default)] - pub end_episodic_id: Option, - /// Timestamp of the first turn. - pub start_timestamp: f64, - /// Timestamp of the last turn, once one has been appended. - #[serde(default)] - pub end_timestamp: Option, - /// How many turns the segment holds. - pub turn_count: i32, - /// Summary, once the segment has been closed and summarised. - #[serde(default)] - pub summary: Option, - /// The segment's running embedding centroid, when it has one. - /// - /// Carried on the read so the host can run boundary detection against it - /// without a second call: deciding whether the next turn still belongs to - /// this segment is host policy, but it needs the centroid the driver - /// holds. - #[serde(default)] - pub embedding: Option>, - /// Whether the segment is still open. - pub open: bool, -} - /// The turn-by-turn conversation record. /// /// Reached through [`MemoryProvider::as_episodic`](super::MemoryProvider::as_episodic). diff --git a/crates/tinymemory-api/src/provider/people.rs b/crates/tinymemory-api/src/provider/people.rs index a525d40..9e0b987 100644 --- a/crates/tinymemory-api/src/provider/people.rs +++ b/crates/tinymemory-api/src/provider/people.rs @@ -31,135 +31,6 @@ //! not promise that every engine identifies people by UUID, and a caller must //! not parse one out — it round-trips an id it was given and nothing more. -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; - -use crate::error::MemoryError; - -/// Opaque identity of one person, as the driver issued it. -/// -/// Treat as a token: round-trip it, compare it for equality, never parse it. -pub type PersonRef = String; - -/// One way a person is addressed. -/// -/// The driver is responsible for canonicalising these before storing or -/// looking up — case folding an email, trimming a handle, collapsing whitespace -/// in a display name. Two handles that canonicalise alike must resolve to the -/// same person, which is why callers pass the raw form and never a -/// pre-normalised one: normalisation that differed between caller and driver -/// would silently mint duplicate people. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", content = "value", rename_all = "snake_case")] -pub enum PersonHandle { - /// An iMessage handle — a phone number or an Apple ID. - IMessage(String), - /// An email address. - Email(String), - /// A human-readable display name. - DisplayName(String), -} - -/// One person as the driver holds them. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct PersonRecord { - /// Driver-issued identity. - pub id: PersonRef, - /// Best-known display name, when one is known. - #[serde(default)] - pub display_name: Option, - /// Primary email, when one is known. - #[serde(default)] - pub primary_email: Option, - /// Primary phone number, when one is known. - #[serde(default)] - pub primary_phone: Option, - /// Every handle this person is known by, canonicalised. - #[serde(default)] - pub handles: Vec, - /// Creation time, RFC 3339. - pub created_at: String, - /// Last-update time, RFC 3339. - pub updated_at: String, -} - -/// Per-component breakdown of a closeness score, each in `[0, 1]`. -/// -/// Exposed rather than collapsed to one number so a caller can explain a -/// ranking. The components are **not** comparable across drivers: each engine -/// picks its own half-life and depth proxy, so compare within one driver's -/// results only. -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub struct PersonScore { - /// How recently the person was interacted with. - pub recency: f32, - /// How often. - pub frequency: f32, - /// How two-sided the exchange is — one-sided contact scores zero. - pub reciprocity: f32, - /// How substantial each interaction is. - pub depth: f32, - /// The composite, clamped to `[0, 1]`. - pub score: f32, - /// How many interactions the score was computed from. - /// - /// Travels with the score rather than beside it, because a score cannot be - /// read honestly without it: 0.9 from three exchanges and 0.9 from three - /// hundred are the same number and very different facts. Every caller that - /// gets a score gets the sample size, and no caller has to remember to ask. - #[serde(default)] - pub interaction_count: usize, -} - -/// A person together with their score, as returned by a ranked list. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct RankedPerson { - /// The person. - pub person: PersonRecord, - /// Their closeness score, including the interaction count it was computed - /// from. - pub score: PersonScore, -} - -/// The outcome of resolving a handle. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct ResolvedPerson { - /// Who the handle resolved to. - pub id: PersonRef, - /// Whether this call minted the person rather than finding them. - /// - /// Distinguished so a caller can tell "I now know who this is" from "I have - /// just invented someone", which read identically from the id alone. - pub created: bool, -} - -/// One observed interaction, as reported by the host. -/// -/// The host owns the channels, so it observes these; the driver only stores and -/// aggregates them. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct PersonInteraction { - /// Who the interaction was with. - pub person_id: PersonRef, - /// When it happened, RFC 3339. - pub at: String, - /// `true` when the user sent it. This is what drives reciprocity, so an - /// importer that cannot tell direction should not guess. - pub is_outbound: bool, - /// A proxy for substance — token or character count. Clamped during - /// scoring, so an outlier cannot dominate a ranking. - pub length: u32, -} - -/// What an address-book seed actually did. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct AddressBookSeedOutcome { - /// People created or updated from the address book. - pub seeded: usize, - /// Contacts skipped — no usable handle, or a write that failed. - pub skipped: usize, -} - /// Contacts, handle resolution, and closeness scoring. /// /// Reached through diff --git a/crates/tinymemory-api/src/provider/profile.rs b/crates/tinymemory-api/src/provider/profile.rs index 51009f5..0298d05 100644 --- a/crates/tinymemory-api/src/provider/profile.rs +++ b/crates/tinymemory-api/src/provider/profile.rs @@ -28,162 +28,6 @@ //! [`FacetState::Dropped`] and is *meant* to be collected — protecting it would //! keep the thing the user asked to forget on disk indefinitely. -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use crate::error::MemoryError; -use crate::host::EvidenceRef; - -/// What kind of claim a facet makes. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum FacetType { - /// A stated or inferred preference. - Preference, - /// A way of working. Persisted as `skill` for historical reasons. - Workflow, - /// A role the user holds. - Role, - /// A personality trait. - Personality, - /// Ambient context about the user's situation. - Context, -} - -impl FacetType { - /// The identifier persisted in the facet table and published on the RPC - /// surface. - /// - /// **This is not the serde representation**, and the difference is - /// deliberate: [`Self::Workflow`] serialises as `workflow` but persists as - /// `skill`, a historical column value. Both forms are load-bearing — the - /// serde one crosses the bus, this one reaches storage and the published - /// JSON — so they are kept separate rather than reconciled. - #[must_use] - pub fn as_str(self) -> &'static str { - match self { - Self::Preference => "preference", - Self::Workflow => "skill", - Self::Role => "role", - Self::Personality => "personality", - Self::Context => "context", - } - } - - /// Parse a persisted identifier; unknown values fall back to - /// [`Self::Preference`], matching the engine's own lenient reader. - #[must_use] - pub fn parse_or_default(raw: &str) -> Self { - match raw { - "skill" => Self::Workflow, - "role" => Self::Role, - "personality" => Self::Personality, - "context" => Self::Context, - _ => Self::Preference, - } - } -} - -/// Where a facet sits in its lifecycle, as the host's stability detector last -/// left it. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum FacetState { - /// Cleared the promotion threshold; included in the ambient profile. - #[default] - Active, - /// Between the provisional and promotion thresholds; included at lower - /// weight. - Provisional, - /// Between eviction and provisional; held as a candidate. - Candidate, - /// Below the eviction threshold; removed on the next rebuild. - Dropped, -} - -impl FacetState { - /// Stable identifier, matching the serde representation. - #[must_use] - pub fn as_str(self) -> &'static str { - match self { - Self::Active => "active", - Self::Provisional => "provisional", - Self::Candidate => "candidate", - Self::Dropped => "dropped", - } - } -} - -/// The user's explicit override, which outranks [`FacetState`]. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum UserState { - /// No override — the host's detector manages the lifecycle. - #[default] - Auto, - /// Pinned by the user: stays active regardless of score. - Pinned, - /// Forgotten by the user: stays dropped, and new evidence must not - /// re-promote it. - Forgotten, -} - -impl UserState { - /// Stable identifier, matching the serde representation. - #[must_use] - pub fn as_str(self) -> &'static str { - match self { - Self::Auto => "auto", - Self::Pinned => "pinned", - Self::Forgotten => "forgotten", - } - } -} - -/// One learned claim about the user. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ProfileFacet { - /// Stable identity of this facet row. - pub facet_id: String, - /// What kind of claim it makes. - pub facet_type: FacetType, - /// The claim's key, e.g. `style/verbosity`. - pub key: String, - /// The claim's value. - pub value: String, - /// How confident the extraction was, in `[0, 1]`. - pub confidence: f64, - /// How many pieces of evidence support it. - pub evidence_count: i32, - /// Legacy segment-id references, when present. - #[serde(default)] - pub source_segment_ids: Option, - /// First observation, epoch seconds. - pub first_seen_at: f64, - /// Most recent observation, epoch seconds. - pub last_seen_at: f64, - /// Lifecycle state, assigned by the host. - #[serde(default)] - pub state: FacetState, - /// Stability score from the host's last rebuild. - #[serde(default)] - pub stability: f64, - /// The user's override. - #[serde(default)] - pub user_state: UserState, - /// Where the evidence came from. - #[serde(default)] - pub evidence_refs: Vec, - /// Facet class derived from the key prefix (`style`, `identity`, …). - /// `None` for rows whose key prefix matches no known class. - #[serde(default)] - pub class: Option, - /// Per-cue-family evidence counts, once the host has written a rebuild. - #[serde(default)] - pub cue_families: Option>, -} - /// Learned facets about the user. /// /// Reached through [`MemoryProvider::as_profile`](super::MemoryProvider::as_profile). diff --git a/crates/tinymemory-api/src/provider/retrieval.rs b/crates/tinymemory-api/src/provider/retrieval.rs index 6ae78c9..845a406 100644 --- a/crates/tinymemory-api/src/provider/retrieval.rs +++ b/crates/tinymemory-api/src/provider/retrieval.rs @@ -34,147 +34,6 @@ //! reports as [`MemoryError::Invalid`], because silently matching nothing would //! look identical to a genuine empty result. -use async_trait::async_trait; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -use crate::chunks::SourceKind; -use crate::error::MemoryError; -use crate::provider::types::SourceScope; -use crate::types::NamespaceMemoryHit; - -/// Whether a hit is a raw leaf or a sealed summary. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RetrievalNodeKind { - /// A stored chunk, tree level 0. - Leaf, - /// A sealed summary node, tree level ≥ 1. - Summary, -} - -/// One ranked retrieval result. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct RetrievalHit { - /// Chunk id for a leaf, summary-node id for a summary. Globally unique. - pub node_id: String, - /// Leaf or summary. - pub node_kind: RetrievalNodeKind, - /// Provenance tree id; empty for a bare leaf not yet sealed into a tree. - #[serde(default)] - pub tree_id: String, - /// Human-readable tree scope, e.g. `slack:#eng`; empty for a bare leaf. - #[serde(default)] - pub tree_scope: String, - /// Tree level: 0 for a leaf chunk, ≥ 1 for a summary. - pub level: u32, - /// Raw chunk text, or sealed summary text. - pub content: String, - /// Canonical entity ids referenced by this node; empty on leaves. - #[serde(default)] - pub entities: Vec, - /// Topic tags for this node. - #[serde(default)] - pub topics: Vec, - /// Inclusive start of the node's time coverage. - pub time_range_start: DateTime, - /// Inclusive end of the node's time coverage. - pub time_range_end: DateTime, - /// Relevance, higher is better. - /// - /// **Not comparable across primitives or across drivers.** A `fast_retrieve` - /// score and a `cover_window` score are produced by different rankers; - /// merging two result sets by score would be meaningless. - pub score: f32, - /// Ids one level down; empty on leaves. - #[serde(default)] - pub child_ids: Vec, - /// Chunk back-pointer, populated for leaves only. - #[serde(default)] - pub source_ref: Option, -} - -/// A page of ranked hits. -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct RetrievalResponse { - /// The hits, already filtered, ranked and truncated to the caller's limit. - pub hits: Vec, - /// Total matches **before** truncation. - pub total: usize, - /// `true` when `total > hits.len()`, i.e. a higher limit would return more. - /// - /// Carried explicitly rather than left for the caller to derive: it is the - /// difference between "there is nothing else" and "there is more, ask - /// again", and a caller that computed it from a page alone could not tell. - pub truncated: bool, -} - -/// Options for [`MemoryRetrieval::fast_retrieve`]. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct FastRetrieveQuery { - /// Maximum hits to return. - pub limit: usize, - /// How many graph hops to expand from the seed entities. - pub max_hops: u32, - /// Restrict to the last N days of source time. - #[serde(default)] - pub time_window_days: Option, -} - -/// A time window to cover. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct CoverWindowQuery { - /// Inclusive lower bound, epoch milliseconds. - pub since_ms: i64, - /// Inclusive upper bound, epoch milliseconds. - pub until_ms: i64, - /// Restrict to one logical source. - #[serde(default)] - pub source_id: Option, - /// Restrict to one source kind. - #[serde(default)] - pub source_kind: Option, - /// Maximum nodes in the cover. - #[serde(default)] - pub limit: Option, -} - -/// Filters for [`MemoryRetrieval::retrieve_source`]. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct SourceRetrievalQuery { - /// Restrict to one logical source (the engine's "scope", e.g. `slack:#eng`). - #[serde(default)] - pub source_id: Option, - /// Restrict to one source kind. - #[serde(default)] - pub source_kind: Option, - /// Restrict to the last N days of source time. - #[serde(default)] - pub time_window_days: Option, - /// Free-text query to rank against. `None` returns the newest nodes rather - /// than ranking — the primitive is a browse as well as a search. - #[serde(default)] - pub query: Option, - /// Maximum hits. - pub limit: usize, -} - -/// One entity-index match. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct EntityMatch { - /// Canonical id, e.g. `email:alice@example.com` or `topic:phoenix`. - pub canonical_id: String, - /// Entity classification. An **open** snake_case vocabulary — see the - /// module docs for why this is not an enum. - pub kind: String, - /// An example surface form that matched, for display. - pub surface: String, - /// Rows grouped under this canonical id. - pub mention_count: u64, - /// Epoch milliseconds of the newest mention. - pub last_seen_ms: i64, -} - /// The engine's deterministic retrieval primitives. /// /// Reached through [`MemoryProvider::as_retrieval`](super::MemoryProvider::as_retrieval). diff --git a/crates/tinymemory-bus/src/provider/chunks.rs b/crates/tinymemory-bus/src/provider/chunks.rs new file mode 100644 index 0000000..fb556c7 --- /dev/null +++ b/crates/tinymemory-bus/src/provider/chunks.rs @@ -0,0 +1,117 @@ +//! The chunks family: direct read access to the stored chunk tier. +//! +//! A driver advertising [`Capability::Chunks`](crate::capabilities::Capability::Chunks) +//! can list and fetch individual chunks, and hand back the embedding vectors it +//! holds for them. +//! +//! # Why a caller would want this rather than recall +//! +//! [`MemoryRecall`](super::MemoryRecall) answers "what is relevant to this +//! query" and owns its own ranking. This family answers "give me the rows +//! matching these filters", which is what a host-side search tool needs when it +//! is doing the ranking itself — cosine similarity with its own MMR +//! diversification, say, or a hybrid keyword/vector blend the engine does not +//! implement. +//! +//! That makes it a deliberately lower-level surface than the rest of the +//! contract, and the honest framing is that it leaks a little of the engine's +//! storage model: chunks, source kinds, embedding signatures. The alternative +//! was worse. Without it a host either reaches around the driver into the +//! engine's own tables — which is exactly the split-brain this contract exists +//! to end — or every ranking strategy has to be pushed into the engine and +//! versioned there. +//! +//! # Embeddings are keyed by signature, and the signature must match exactly +//! +//! [`MemoryChunks::chunk_embeddings`] takes a `model_signature` and returns +//! only vectors stored under it. A caller that computes that string differently +//! from the driver gets an empty result rather than an error — the vectors are +//! there, just filed under a name the caller did not ask for. That is a real +//! failure mode with a real precedent, and it is silent; see +//! `docs/specs/2026-08-13-memory-module-port.md` §3. + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use crate::chunks::{Chunk, SourceKind}; +use crate::error::MemoryError; +use crate::provider::types::SourceScope; + +/// Filters for [`MemoryChunks::list_chunks`]. +/// +/// Every field is optional and they compose with AND. The default matches +/// everything the scope allows, bounded by the driver's own safety cap. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct ChunkQuery { + /// Restrict to one source kind. + #[serde(default)] + pub source_kind: Option, + /// Restrict to one logical source id. + #[serde(default)] + pub source_id: Option, + /// Restrict to one owner. + #[serde(default)] + pub owner: Option, + /// Inclusive lower bound on source time, epoch milliseconds. + #[serde(default)] + pub since_ms: Option, + /// Inclusive upper bound on source time, epoch milliseconds. + #[serde(default)] + pub until_ms: Option, + /// Maximum rows. The driver clamps this to its own cap — a caller cannot + /// raise the ceiling by asking for more. + #[serde(default)] + pub limit: Option, + /// Rows to skip, for pagination. + #[serde(default)] + pub offset: Option, + /// Drop chunks marked dropped by the lifecycle. + #[serde(default)] + pub exclude_dropped: bool, +} + +/// One chunk's stored embedding. +/// +/// Returned as a list rather than a map because the wire form of a map keyed by +/// chunk id is a JSON object, and an id is caller-supplied text; a list keeps +/// the encoding independent of what an id happens to contain. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ChunkEmbedding { + /// The chunk this vector belongs to. + pub chunk_id: String, + /// The vector, in the embedding space named by the requested signature. + pub vector: Vec, +} + +/// One chunk plus the per-chunk facts stored beside it. +/// +/// # Why a detail view rather than four accessors +/// +/// An inspection caller wants the row, its body, where the body lives, its +/// lifecycle state and whether it has been embedded. Exposing those as four +/// methods would read naturally in-process and cost **four bus round trips per +/// row** out of it — and this is used to render lists. One method, one trip. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ChunkDetail { + /// The chunk row. + pub chunk: Chunk, + /// The chunk's body as stored in the content vault, when it could be read. + /// + /// `None` means the vault read failed — distinct from an empty body, which + /// is a legitimately empty chunk. A caller rendering a preview should fall + /// back to [`Chunk::content`] rather than showing nothing. + #[serde(default)] + pub body: Option, + /// Path of the body in the content vault, when it has one. + #[serde(default)] + pub content_path: Option, + /// Lifecycle state (`active`, `dropped`, …); `None` when unrecorded. + #[serde(default)] + pub lifecycle_status: Option, + /// Whether an embedding vector exists for this chunk in **any** space. + /// + /// Not scoped to a signature on purpose: this answers "has this been + /// embedded at all", which is what an inspection view wants. Asking whether + /// a *particular* space has it is [`MemoryChunks::chunk_embeddings`]. + pub has_embedding: bool, +} diff --git a/crates/tinymemory-bus/src/provider/episodic.rs b/crates/tinymemory-bus/src/provider/episodic.rs new file mode 100644 index 0000000..c3824de --- /dev/null +++ b/crates/tinymemory-bus/src/provider/episodic.rs @@ -0,0 +1,116 @@ +//! The episodic family: the turn-by-turn record of conversations. +//! +//! A driver advertising [`Capability::Episodic`](crate::capabilities::Capability::Episodic) +//! stores every chat turn in a full-text index and groups consecutive turns +//! into *conversation segments* — a segment being a stretch of turns about one +//! thing, closed when the subject changes and then summarised and embedded. +//! +//! # Why this is a family rather than a raw connection +//! +//! It is the last thing in the host that held a live `rusqlite::Connection`. +//! The archivist hook was handed one straight out of the session factory and +//! called free functions on it, which worked only because the engine was +//! compiled into this process. A connection cannot cross a bus, so either the +//! archivist's operations become a contract family or episodic capture stays +//! behind and the engine can never leave. +//! +//! What crosses is small and already typed: insert a turn, read a session's +//! turns back, and six segment-lifecycle operations. That was the whole surface +//! the raw connection was used for — no ad-hoc SQL, no schema knowledge. +//! +//! # The host keeps the policy, and it is not a small share +//! +//! Two of the archivist's eight engine calls took no connection at all — +//! deciding *whether* a new turn starts a new segment, and composing a summary +//! when no model is available. Neither touches storage, so both stay host-side +//! in `agent::harness::archivist`, next to the recap logic and the boundary +//! thresholds they read. This family persists what the host decided; it does +//! not decide. +//! +//! # `insert_turn` returns the id, and that is load-bearing +//! +//! The old code inserted a row and then issued `SELECT last_insert_rowid()` on +//! the same connection to learn its id. That is two operations relying on a +//! *connection-local* side effect, and it is wrong the moment anything else +//! shares the connection or the two hops cross a bus — `last_insert_rowid` is +//! per-connection state, so an interleaved insert from another task yields the +//! wrong id and the turn is filed under the wrong segment. +//! +//! Returning the id from the insert removes both problems at once: one round +//! trip instead of two, and no reliance on connection-local state. The engine +//! knows the id it just wrote; nothing else has to guess. + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use crate::error::MemoryError; + +/// One recorded turn. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct EpisodicTurn { + /// Row id, assigned by the driver on insert. + /// + /// `None` when the host is describing a turn to be written; always `Some` + /// on a turn read back. + #[serde(default)] + pub id: Option, + /// Session this turn belongs to. + pub session_id: String, + /// When it happened, epoch seconds with sub-second resolution. + /// + /// The archivist offsets an assistant turn by 1 ms from the user turn it + /// answers so the pair sorts in order within one exchange; that convention + /// is the host's and the driver must preserve the value it is given rather + /// than re-stamping it. + pub timestamp: f64, + /// `"user"` or `"assistant"`. Open vocabulary — a driver must not reject an + /// unfamiliar role. + pub role: String, + /// The turn's text. + pub content: String, + /// A short lesson extracted from tool failures, when there was one. + #[serde(default)] + pub lesson: Option, + /// Serialized tool-call summary, when the turn made any. + #[serde(default)] + pub tool_calls_json: Option, + /// Cost attributed to this turn, in microdollars. + #[serde(default)] + pub cost_microdollars: i64, +} + +/// A stretch of consecutive turns about one subject. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ConversationSegment { + /// Stable id, chosen by the host. + pub segment_id: String, + /// Session the segment belongs to. + pub session_id: String, + /// Owning namespace. + pub namespace: String, + /// Row id of the first turn in the segment. + pub start_episodic_id: i64, + /// Row id of the last turn, once one has been appended. + #[serde(default)] + pub end_episodic_id: Option, + /// Timestamp of the first turn. + pub start_timestamp: f64, + /// Timestamp of the last turn, once one has been appended. + #[serde(default)] + pub end_timestamp: Option, + /// How many turns the segment holds. + pub turn_count: i32, + /// Summary, once the segment has been closed and summarised. + #[serde(default)] + pub summary: Option, + /// The segment's running embedding centroid, when it has one. + /// + /// Carried on the read so the host can run boundary detection against it + /// without a second call: deciding whether the next turn still belongs to + /// this segment is host policy, but it needs the centroid the driver + /// holds. + #[serde(default)] + pub embedding: Option>, + /// Whether the segment is still open. + pub open: bool, +} diff --git a/crates/tinymemory-bus/src/provider/people.rs b/crates/tinymemory-bus/src/provider/people.rs new file mode 100644 index 0000000..0191010 --- /dev/null +++ b/crates/tinymemory-bus/src/provider/people.rs @@ -0,0 +1,161 @@ +//! The people family: contacts, handle resolution, and closeness scoring. +//! +//! A driver advertising [`Capability::People`](crate::capabilities::Capability::People) +//! owns a store of people, the aliases each is known by, and the interactions +//! observed with them — and can rank them by how close the user is to each. +//! +//! # Why this is a family and not a widening of an existing one +//! +//! People is storage the engine owns, and it does not fit any family already +//! defined: a person is not a memory entry, not a document, and not a graph +//! entity. Adding these methods to, say, [`MemoryEntities`] would also have +//! been a **major** contract bump — the version rule treats a new method on a +//! family a driver may already advertise as breaking, because negotiation +//! cannot save a caller from a method an older driver does not implement. A new +//! family is a minor bump instead, and an older driver simply does not +//! advertise it. +//! +//! [`MemoryEntities`]: crate::provider::MemoryEntities +//! +//! # The types here are the contract's own +//! +//! None of these name an engine type. TinyCortex has its own `Person`, +//! `Handle` and `Interaction`; a second engine will have others. The adapter at +//! each engine's edge converts, which is what keeps this contract +//! engine-neutral — see the module rules in +//! [`super`]. +//! +//! # Identity crosses as a string +//! +//! [`PersonRef`] is an opaque string rather than a `Uuid`. The contract does +//! not promise that every engine identifies people by UUID, and a caller must +//! not parse one out — it round-trips an id it was given and nothing more. + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; + +use crate::error::MemoryError; + +/// Opaque identity of one person, as the driver issued it. +/// +/// Treat as a token: round-trip it, compare it for equality, never parse it. +pub type PersonRef = String; + +/// One way a person is addressed. +/// +/// The driver is responsible for canonicalising these before storing or +/// looking up — case folding an email, trimming a handle, collapsing whitespace +/// in a display name. Two handles that canonicalise alike must resolve to the +/// same person, which is why callers pass the raw form and never a +/// pre-normalised one: normalisation that differed between caller and driver +/// would silently mint duplicate people. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", content = "value", rename_all = "snake_case")] +pub enum PersonHandle { + /// An iMessage handle — a phone number or an Apple ID. + IMessage(String), + /// An email address. + Email(String), + /// A human-readable display name. + DisplayName(String), +} + +/// One person as the driver holds them. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PersonRecord { + /// Driver-issued identity. + pub id: PersonRef, + /// Best-known display name, when one is known. + #[serde(default)] + pub display_name: Option, + /// Primary email, when one is known. + #[serde(default)] + pub primary_email: Option, + /// Primary phone number, when one is known. + #[serde(default)] + pub primary_phone: Option, + /// Every handle this person is known by, canonicalised. + #[serde(default)] + pub handles: Vec, + /// Creation time, RFC 3339. + pub created_at: String, + /// Last-update time, RFC 3339. + pub updated_at: String, +} + +/// Per-component breakdown of a closeness score, each in `[0, 1]`. +/// +/// Exposed rather than collapsed to one number so a caller can explain a +/// ranking. The components are **not** comparable across drivers: each engine +/// picks its own half-life and depth proxy, so compare within one driver's +/// results only. +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub struct PersonScore { + /// How recently the person was interacted with. + pub recency: f32, + /// How often. + pub frequency: f32, + /// How two-sided the exchange is — one-sided contact scores zero. + pub reciprocity: f32, + /// How substantial each interaction is. + pub depth: f32, + /// The composite, clamped to `[0, 1]`. + pub score: f32, + /// How many interactions the score was computed from. + /// + /// Travels with the score rather than beside it, because a score cannot be + /// read honestly without it: 0.9 from three exchanges and 0.9 from three + /// hundred are the same number and very different facts. Every caller that + /// gets a score gets the sample size, and no caller has to remember to ask. + #[serde(default)] + pub interaction_count: usize, +} + +/// A person together with their score, as returned by a ranked list. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RankedPerson { + /// The person. + pub person: PersonRecord, + /// Their closeness score, including the interaction count it was computed + /// from. + pub score: PersonScore, +} + +/// The outcome of resolving a handle. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ResolvedPerson { + /// Who the handle resolved to. + pub id: PersonRef, + /// Whether this call minted the person rather than finding them. + /// + /// Distinguished so a caller can tell "I now know who this is" from "I have + /// just invented someone", which read identically from the id alone. + pub created: bool, +} + +/// One observed interaction, as reported by the host. +/// +/// The host owns the channels, so it observes these; the driver only stores and +/// aggregates them. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct PersonInteraction { + /// Who the interaction was with. + pub person_id: PersonRef, + /// When it happened, RFC 3339. + pub at: String, + /// `true` when the user sent it. This is what drives reciprocity, so an + /// importer that cannot tell direction should not guess. + pub is_outbound: bool, + /// A proxy for substance — token or character count. Clamped during + /// scoring, so an outlier cannot dominate a ranking. + pub length: u32, +} + +/// What an address-book seed actually did. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AddressBookSeedOutcome { + /// People created or updated from the address book. + pub seeded: usize, + /// Contacts skipped — no usable handle, or a write that failed. + pub skipped: usize, +} diff --git a/crates/tinymemory-bus/src/provider/profile.rs b/crates/tinymemory-bus/src/provider/profile.rs new file mode 100644 index 0000000..25ce254 --- /dev/null +++ b/crates/tinymemory-bus/src/provider/profile.rs @@ -0,0 +1,185 @@ +//! The profile family: learned facets about the user. +//! +//! A driver advertising [`Capability::Profile`](crate::capabilities::Capability::Profile) +//! stores *facets* — small learned claims like a preferred verbosity, a role, +//! a tool the user reaches for — each carrying the evidence behind it, a +//! stability score, and a lifecycle state. +//! +//! # The host owns the learning; the driver owns the rows +//! +//! Which facets to extract, how to score stability, when to promote or evict — +//! all of that is host policy and stays there. This family is the persistence +//! seam beneath it: read facets, write facets, set the user's override, drop +//! what fell below a threshold. +//! +//! That split is why [`ProfileFacet`] carries a `stability` and a `state` the +//! driver never computes. It records what the host decided; it does not decide. +//! +//! # `user_state` is the user's, and outranks the score +//! +//! [`UserState::Pinned`] and [`UserState::Forgotten`] are explicit user +//! decisions. A pinned facet stays active however low its stability falls, and +//! a forgotten one stays dropped however much new evidence arrives — a user who +//! says "forget that" must not have it re-learned. +//! +//! The two are **not** symmetric under +//! [`MemoryProfile::drop_facets_below`], and the asymmetry is deliberate: only +//! `Pinned` is protected from the sweep. A `Forgotten` facet is already in +//! [`FacetState::Dropped`] and is *meant* to be collected — protecting it would +//! keep the thing the user asked to forget on disk indefinitely. + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::error::MemoryError; +use crate::host::EvidenceRef; + +/// What kind of claim a facet makes. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FacetType { + /// A stated or inferred preference. + Preference, + /// A way of working. Persisted as `skill` for historical reasons. + Workflow, + /// A role the user holds. + Role, + /// A personality trait. + Personality, + /// Ambient context about the user's situation. + Context, +} + +impl FacetType { + /// The identifier persisted in the facet table and published on the RPC + /// surface. + /// + /// **This is not the serde representation**, and the difference is + /// deliberate: [`Self::Workflow`] serialises as `workflow` but persists as + /// `skill`, a historical column value. Both forms are load-bearing — the + /// serde one crosses the bus, this one reaches storage and the published + /// JSON — so they are kept separate rather than reconciled. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::Preference => "preference", + Self::Workflow => "skill", + Self::Role => "role", + Self::Personality => "personality", + Self::Context => "context", + } + } + + /// Parse a persisted identifier; unknown values fall back to + /// [`Self::Preference`], matching the engine's own lenient reader. + #[must_use] + pub fn parse_or_default(raw: &str) -> Self { + match raw { + "skill" => Self::Workflow, + "role" => Self::Role, + "personality" => Self::Personality, + "context" => Self::Context, + _ => Self::Preference, + } + } +} + +/// Where a facet sits in its lifecycle, as the host's stability detector last +/// left it. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum FacetState { + /// Cleared the promotion threshold; included in the ambient profile. + #[default] + Active, + /// Between the provisional and promotion thresholds; included at lower + /// weight. + Provisional, + /// Between eviction and provisional; held as a candidate. + Candidate, + /// Below the eviction threshold; removed on the next rebuild. + Dropped, +} + +impl FacetState { + /// Stable identifier, matching the serde representation. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::Active => "active", + Self::Provisional => "provisional", + Self::Candidate => "candidate", + Self::Dropped => "dropped", + } + } +} + +/// The user's explicit override, which outranks [`FacetState`]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum UserState { + /// No override — the host's detector manages the lifecycle. + #[default] + Auto, + /// Pinned by the user: stays active regardless of score. + Pinned, + /// Forgotten by the user: stays dropped, and new evidence must not + /// re-promote it. + Forgotten, +} + +impl UserState { + /// Stable identifier, matching the serde representation. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::Auto => "auto", + Self::Pinned => "pinned", + Self::Forgotten => "forgotten", + } + } +} + +/// One learned claim about the user. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ProfileFacet { + /// Stable identity of this facet row. + pub facet_id: String, + /// What kind of claim it makes. + pub facet_type: FacetType, + /// The claim's key, e.g. `style/verbosity`. + pub key: String, + /// The claim's value. + pub value: String, + /// How confident the extraction was, in `[0, 1]`. + pub confidence: f64, + /// How many pieces of evidence support it. + pub evidence_count: i32, + /// Legacy segment-id references, when present. + #[serde(default)] + pub source_segment_ids: Option, + /// First observation, epoch seconds. + pub first_seen_at: f64, + /// Most recent observation, epoch seconds. + pub last_seen_at: f64, + /// Lifecycle state, assigned by the host. + #[serde(default)] + pub state: FacetState, + /// Stability score from the host's last rebuild. + #[serde(default)] + pub stability: f64, + /// The user's override. + #[serde(default)] + pub user_state: UserState, + /// Where the evidence came from. + #[serde(default)] + pub evidence_refs: Vec, + /// Facet class derived from the key prefix (`style`, `identity`, …). + /// `None` for rows whose key prefix matches no known class. + #[serde(default)] + pub class: Option, + /// Per-cue-family evidence counts, once the host has written a rebuild. + #[serde(default)] + pub cue_families: Option>, +} diff --git a/crates/tinymemory-bus/src/provider/retrieval.rs b/crates/tinymemory-bus/src/provider/retrieval.rs new file mode 100644 index 0000000..af92cd5 --- /dev/null +++ b/crates/tinymemory-bus/src/provider/retrieval.rs @@ -0,0 +1,176 @@ +//! The retrieval family: the engine's deterministic retrieval primitives. +//! +//! A driver advertising [`Capability::Retrieval`](crate::capabilities::Capability::Retrieval) +//! exposes graph-walk retrieval, time-window coverage, and entity-index search +//! — the LLM-free primitives a host composes an answer from. +//! +//! # Separate from [`MemoryTree`](super::MemoryTree), on purpose +//! +//! The tree family navigates a known node: query one source, drill into +//! children, seal, cascade. These three answer questions about the store as a +//! whole, and they return a different shape — ranked hits with scores and a +//! truncation flag, not a node and its children. +//! +//! They are also, mechanically, why this is a new family rather than three more +//! `MemoryTree` methods: adding a method to a family a driver may already +//! advertise is a **major** contract bump, because negotiation cannot protect a +//! caller from a method an older driver never implemented. +//! +//! # Entity kinds travel as strings, not as an enum +//! +//! The engine's own `EntityKind` is `#[non_exhaustive]` and has grown twice. +//! A closed enum here would mean that the first time an engine emits a kind +//! this build has not heard of, the **response fails to deserialize** — a new +//! entity category would break retrieval outright rather than showing up as an +//! unfamiliar label. +//! +//! So [`EntityMatch::kind`] is an open vocabulary: a snake_case string the +//! caller passes through. Known values today are `email`, `url`, `handle`, +//! `hashtag`, `person`, `organization`, `location`, `event`, `product`, +//! `datetime`, `technology`, `artifact`, `quantity`, `misc`, `topic`. +//! +//! Requests are the opposite case and are validated: an unknown kind in +//! [`MemoryRetrieval::search_entities`]'s filter is a caller mistake the driver +//! reports as [`MemoryError::Invalid`], because silently matching nothing would +//! look identical to a genuine empty result. + +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::chunks::SourceKind; +use crate::error::MemoryError; +use crate::provider::types::SourceScope; +use crate::types::NamespaceMemoryHit; + +/// Whether a hit is a raw leaf or a sealed summary. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RetrievalNodeKind { + /// A stored chunk, tree level 0. + Leaf, + /// A sealed summary node, tree level ≥ 1. + Summary, +} + +/// One ranked retrieval result. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RetrievalHit { + /// Chunk id for a leaf, summary-node id for a summary. Globally unique. + pub node_id: String, + /// Leaf or summary. + pub node_kind: RetrievalNodeKind, + /// Provenance tree id; empty for a bare leaf not yet sealed into a tree. + #[serde(default)] + pub tree_id: String, + /// Human-readable tree scope, e.g. `slack:#eng`; empty for a bare leaf. + #[serde(default)] + pub tree_scope: String, + /// Tree level: 0 for a leaf chunk, ≥ 1 for a summary. + pub level: u32, + /// Raw chunk text, or sealed summary text. + pub content: String, + /// Canonical entity ids referenced by this node; empty on leaves. + #[serde(default)] + pub entities: Vec, + /// Topic tags for this node. + #[serde(default)] + pub topics: Vec, + /// Inclusive start of the node's time coverage. + pub time_range_start: DateTime, + /// Inclusive end of the node's time coverage. + pub time_range_end: DateTime, + /// Relevance, higher is better. + /// + /// **Not comparable across primitives or across drivers.** A `fast_retrieve` + /// score and a `cover_window` score are produced by different rankers; + /// merging two result sets by score would be meaningless. + pub score: f32, + /// Ids one level down; empty on leaves. + #[serde(default)] + pub child_ids: Vec, + /// Chunk back-pointer, populated for leaves only. + #[serde(default)] + pub source_ref: Option, +} + +/// A page of ranked hits. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct RetrievalResponse { + /// The hits, already filtered, ranked and truncated to the caller's limit. + pub hits: Vec, + /// Total matches **before** truncation. + pub total: usize, + /// `true` when `total > hits.len()`, i.e. a higher limit would return more. + /// + /// Carried explicitly rather than left for the caller to derive: it is the + /// difference between "there is nothing else" and "there is more, ask + /// again", and a caller that computed it from a page alone could not tell. + pub truncated: bool, +} + +/// Options for [`MemoryRetrieval::fast_retrieve`]. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct FastRetrieveQuery { + /// Maximum hits to return. + pub limit: usize, + /// How many graph hops to expand from the seed entities. + pub max_hops: u32, + /// Restrict to the last N days of source time. + #[serde(default)] + pub time_window_days: Option, +} + +/// A time window to cover. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct CoverWindowQuery { + /// Inclusive lower bound, epoch milliseconds. + pub since_ms: i64, + /// Inclusive upper bound, epoch milliseconds. + pub until_ms: i64, + /// Restrict to one logical source. + #[serde(default)] + pub source_id: Option, + /// Restrict to one source kind. + #[serde(default)] + pub source_kind: Option, + /// Maximum nodes in the cover. + #[serde(default)] + pub limit: Option, +} + +/// Filters for [`MemoryRetrieval::retrieve_source`]. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SourceRetrievalQuery { + /// Restrict to one logical source (the engine's "scope", e.g. `slack:#eng`). + #[serde(default)] + pub source_id: Option, + /// Restrict to one source kind. + #[serde(default)] + pub source_kind: Option, + /// Restrict to the last N days of source time. + #[serde(default)] + pub time_window_days: Option, + /// Free-text query to rank against. `None` returns the newest nodes rather + /// than ranking — the primitive is a browse as well as a search. + #[serde(default)] + pub query: Option, + /// Maximum hits. + pub limit: usize, +} + +/// One entity-index match. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct EntityMatch { + /// Canonical id, e.g. `email:alice@example.com` or `topic:phoenix`. + pub canonical_id: String, + /// Entity classification. An **open** snake_case vocabulary — see the + /// module docs for why this is not an enum. + pub kind: String, + /// An example surface form that matched, for display. + pub surface: String, + /// Rows grouped under this canonical id. + pub mention_count: u64, + /// Epoch milliseconds of the newest mention. + pub last_seen_ms: i64, +} From 4dc18161cc38ffdb1128f56bc2ca87f21332fd4a Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:56:19 +0300 Subject: [PATCH 16/35] chore(tinymemory-bus): rewrite module-level documentation and reorganise public modules The crate-level doc comment has been rewritten to clarify that this library publishes the wire vocabulary for the TinyBus module, not the transport or driver traits. The module structure is updated to expose the new `provider`, `version`, `chunks`, `recall`, `tree`, `goals`, `tool_memory`, `health`, `capabilities`, and `evidence` modules, replacing the old `calls` module. The re-exports now include `CONTRACT_VERSION` and `is_compatible` from the new `version` module, while the `Error` and `Result` re-exports have been removed since they are no longer top-level items. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/lib.rs | 150 +++++++++++----------- crates/tinymemory-bus/src/provider/mod.rs | 18 +++ 2 files changed, 92 insertions(+), 76 deletions(-) create mode 100644 crates/tinymemory-bus/src/provider/mod.rs diff --git a/crates/tinymemory-bus/src/lib.rs b/crates/tinymemory-bus/src/lib.rs index afde87e..445f5e2 100644 --- a/crates/tinymemory-bus/src/lib.rs +++ b/crates/tinymemory-bus/src/lib.rs @@ -1,87 +1,85 @@ -//! The `TinyBus` wire contract for the TinyMemory module. +//! Every type that crosses the TinyMemory `TinyBus` boundary, and the names of +//! the members that carry them. //! -//! TinyMemory ships as a loadable `TinyBus` module so a host does not compile -//! the engine: `crates/tinymemory-module` exports one object, -//! `/ai/tinyhumans/tinymemory/Memory`, with 89 members on it. A host that loads -//! that binary needs three things to talk to it — the member names, the types -//! on either side of each call, and the error-name table — and none of those -//! are in the module binary, which is a `cdylib`. +//! TinyMemory ships as a loadable `TinyBus` module: `crates/tinymemory-module` +//! exports one object with 89 members on it, built as a `cdylib`. A host that +//! loads it — OpenHuman — can call into it but cannot `use` anything out of it, +//! so the payload vocabulary has to be published as an ordinary library. This +//! is that library. //! -//! This crate is those three things, as a library a host links: +//! ## What is here //! //! - [`names`] — the bus name, the object path, and one constant per member. -//! - [`types`] — every value type that crosses a frame. -//! - [`calls`] — one struct per member, carrying its arguments in wire order -//! and its reply type. -//! - [`wire`] — the error names, and the mapping back to `MemoryError`. -//! -//! ``` -//! use tinymemory_bus::calls::{core::Get, BusCall}; -//! use tinymemory_bus::names::{BUS_NAME, OBJECT_PATH}; -//! -//! let args = Get { namespace: "work".to_string(), key: "standup".to_string() }.into_args()?; -//! -//! // Everything a `Connection::call` needs, with nothing spelled by hand. -//! assert_eq!((BUS_NAME, OBJECT_PATH, Get::METHOD), ( -//! "ai.tinyhumans.tinymemory.Memory", -//! "/ai/tinyhumans/tinymemory/Memory", -//! "Get", -//! )); -//! assert_eq!(args.to_string(), r#"["work","standup"]"#); -//! # Ok::<(), tinymemory_bus::Error>(()) -//! ``` -//! -//! # There is no transport here, on purpose -//! -//! This crate does not depend on `tinybus`, and holds no connection, no client -//! and no `call()` that sends anything. Two reasons, and the second is the -//! blunt one. -//! -//! A host already owns its connection. It has its own reconnect policy, its own -//! timeouts, its own tracing, and its own idea of what a memory call costs it. A -//! client here would either duplicate that or fight it, and the useful part — -//! *what to send and what comes back* — is exactly what is in this crate. -//! Wiring it up is a dozen lines over a `Connection`; `README.md` has the shape. -//! -//! And structurally it could not work anyway. `tinybus` is vendored as a git -//! submodule whose manifest inherits fields from its own nested -//! `[workspace.package]`; a member of *this* workspace that depends on it makes -//! cargo resolve that inheritance against the wrong root and fail. That is why -//! `crates/tinymemory-module` is its own workspace root — see the root -//! manifest's note on `exclude`. A contract crate a host links has no business -//! being a separate workspace, so it stays transport-free and every member of -//! this workspace can depend on it. -//! -//! # Why this is not just `tinymemory-api` -//! -//! `tinymemory-api` is the **driver** contract: what an engine implements. It -//! carries `MemoryProvider` and its eighteen capability traits, the -//! mandatory-family composition, the null driver, and the `host::` config -//! sections a host persists in `config.toml`. -//! -//! A host that loads the module implements none of that. It makes calls. This -//! crate is the subset that crosses a frame, so what a host compiles against is -//! what it can actually send and receive — and a member that exists in the -//! trait but is not exported on the bus is absent here rather than tempting. -//! -//! The types themselves are **re-exported** from `tinymemory-api`, never -//! redefined. [`types`] explains why at length; the short version is that a -//! second definition would make `MemoryCategory` from the module a different -//! type from `MemoryCategory` in the host, which is a failure this repository -//! has already had once. -//! -//! # Staying in step with the module -//! -//! [`names::METHODS`] lists every member. `crates/tinymemory-module` asserts its -//! served members against that list, so a method added to the interface without -//! a constant and a call struct here fails that crate's tests rather than -//! turning up as an `UnknownMethod` at runtime in a host. +//! - [`types`], [`chunks`], [`recall`], [`tree`], [`goals`], [`tool_memory`], +//! [`health`], [`capabilities`], [`evidence`] — the value vocabulary. +//! - [`provider`] — the value types the capability families exchange. +//! - [`error`] and [`wire`] — [`error::MemoryError`] and the name table it +//! round-trips through when a driver is reached over a wire. +//! - [`version`] — [`CONTRACT_VERSION`] and the [`is_compatible`] bind rule. +//! +//! ## What is deliberately not here +//! +//! **No traits.** `MemoryProvider` and the eighteen capability-family traits +//! are driver obligations: they describe what an engine must implement, not +//! what a frame carries. They stay in `tinymemory-api`, which depends on this +//! crate. +//! +//! **No transport.** This crate does not depend on `tinybus` and holds no +//! connection, client, or codec. A host already owns its connection — its +//! reconnect policy, its timeouts, its tracing — and the useful part is the +//! vocabulary, not another wrapper around it. +//! +//! That is also a structural necessity, not only a preference: `tinybus` is +//! vendored as a submodule whose manifest inherits fields from its own nested +//! `[workspace.package]`, so a member of this workspace that depends on it +//! makes cargo resolve that inheritance against the wrong root and fail. It is +//! why `crates/tinymemory-module` is its own workspace root — see the note on +//! `exclude` in the root `Cargo.toml`. A crate every workspace member can +//! depend on has to stay transport-free. +//! +//! **No host configuration, no null driver, no composition helpers.** Those are +//! `tinymemory-api`'s, and none of them cross a frame. +//! +//! ## This crate is underneath the contract, not beside it +//! +//! `tinymemory-api` **depends on this crate and re-exports all of it**, so +//! every historical path — `tinymemory_api::types::MemoryEntry`, +//! `tinymemory::MemoryCategory`, `tinycortex::memory::types::*` — keeps +//! resolving unchanged, and the types are the *same types*, not structural +//! twins. +//! +//! That direction is the whole point. Defining a parallel set of payload types +//! for hosts would mean `MemoryCategory` from the module was not +//! `MemoryCategory` in the host, with a conversion at every call site that +//! nothing checks — the exact failure the root manifest's `[patch]` table +//! exists to prevent, reintroduced deliberately. One definition, here, at the +//! bottom. +//! +//! A host that only makes calls therefore depends on this crate alone and +//! compiles no traits, no engine seam and no config surface. A driver author +//! depends on `tinymemory-api` and gets both. +//! +//! ## Staying in step with the module +//! +//! [`names::METHODS`] lists every member. `crates/tinymemory-module` asserts +//! its served members against that list, in order, so a method added to the +//! interface without an entry here fails that crate's tests rather than +//! surfacing as an `UnknownMethod` in a host at runtime. -pub mod calls; +pub mod capabilities; +pub mod chunks; pub mod error; +pub mod evidence; +pub mod goals; +pub mod health; pub mod names; +pub mod provider; +pub mod recall; +pub mod tool_memory; +pub mod tree; pub mod types; +pub mod version; pub mod wire; -pub use error::{Error, Result}; pub use names::{BUS_NAME, METHODS, OBJECT_PATH}; +pub use version::{is_compatible, CONTRACT_VERSION}; diff --git a/crates/tinymemory-bus/src/provider/mod.rs b/crates/tinymemory-bus/src/provider/mod.rs new file mode 100644 index 0000000..aa1250b --- /dev/null +++ b/crates/tinymemory-bus/src/provider/mod.rs @@ -0,0 +1,18 @@ +//! The value types the capability families exchange. +//! +//! These sit under `provider` because that is where they live in +//! `tinymemory-api`, which re-exports every one of them at its historical path. +//! Keeping the two trees the same shape is what makes the split auditable: a +//! type is either here, as data, or there, as a trait — and which one it is can +//! be read off the path. +//! +//! The traits themselves are **not** here and will not be. A trait is a driver +//! obligation; this crate describes a frame. See [`crate`] for the rest of that +//! argument. + +pub mod chunks; +pub mod episodic; +pub mod people; +pub mod profile; +pub mod retrieval; +pub mod types; From 918686d05ca388049e025b2b23020e1fb06d42fe Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:56:58 +0300 Subject: [PATCH 17/35] feat(provider): re-export bus value types in each provider module The value types that each provider family exchanges are defined in the tinymemory-bus crate, but a host that only makes calls must be able to name them without compiling that crate. This change re-exports those types from each provider module so every historical import path keeps resolving and the types remain the same types. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-api/src/provider/chunks.rs | 12 ++++++++++++ crates/tinymemory-api/src/provider/episodic.rs | 10 ++++++++++ crates/tinymemory-api/src/provider/people.rs | 10 ++++++++++ crates/tinymemory-api/src/provider/profile.rs | 10 ++++++++++ crates/tinymemory-api/src/provider/retrieval.rs | 12 ++++++++++++ 5 files changed, 54 insertions(+) diff --git a/crates/tinymemory-api/src/provider/chunks.rs b/crates/tinymemory-api/src/provider/chunks.rs index 3ff8729..8309880 100644 --- a/crates/tinymemory-api/src/provider/chunks.rs +++ b/crates/tinymemory-api/src/provider/chunks.rs @@ -30,6 +30,18 @@ //! failure mode with a real precedent, and it is silent; see //! `docs/specs/2026-08-13-memory-module-port.md` §3. +use async_trait::async_trait; + +use crate::chunks::{Chunk, SourceKind}; +use crate::error::MemoryError; +use crate::provider::types::SourceScope; + +// The value types this family exchanges. They are defined in `tinymemory-bus` +// — they cross the module boundary, and a host that only makes calls must be +// able to name them without compiling this trait — and re-exported here so +// every historical path keeps resolving and the types stay the same types. +pub use tinymemory_bus::provider::chunks::{ChunkDetail, ChunkEmbedding, ChunkQuery}; + /// Direct read access to the chunk tier. /// /// Reached through [`MemoryProvider::as_chunks`](super::MemoryProvider::as_chunks). diff --git a/crates/tinymemory-api/src/provider/episodic.rs b/crates/tinymemory-api/src/provider/episodic.rs index 9f51462..aaaeaab 100644 --- a/crates/tinymemory-api/src/provider/episodic.rs +++ b/crates/tinymemory-api/src/provider/episodic.rs @@ -40,6 +40,16 @@ //! trip instead of two, and no reliance on connection-local state. The engine //! knows the id it just wrote; nothing else has to guess. +use async_trait::async_trait; + +use crate::error::MemoryError; + +// The value types this family exchanges. They are defined in `tinymemory-bus` +// — they cross the module boundary, and a host that only makes calls must be +// able to name them without compiling this trait — and re-exported here so +// every historical path keeps resolving and the types stay the same types. +pub use tinymemory_bus::provider::episodic::{ConversationSegment, EpisodicTurn}; + /// The turn-by-turn conversation record. /// /// Reached through [`MemoryProvider::as_episodic`](super::MemoryProvider::as_episodic). diff --git a/crates/tinymemory-api/src/provider/people.rs b/crates/tinymemory-api/src/provider/people.rs index 9e0b987..0ba36fb 100644 --- a/crates/tinymemory-api/src/provider/people.rs +++ b/crates/tinymemory-api/src/provider/people.rs @@ -31,6 +31,16 @@ //! not promise that every engine identifies people by UUID, and a caller must //! not parse one out — it round-trips an id it was given and nothing more. +use async_trait::async_trait; + +use crate::error::MemoryError; + +// The value types this family exchanges. They are defined in `tinymemory-bus` +// — they cross the module boundary, and a host that only makes calls must be +// able to name them without compiling this trait — and re-exported here so +// every historical path keeps resolving and the types stay the same types. +pub use tinymemory_bus::provider::people::{AddressBookSeedOutcome, PersonHandle, PersonInteraction, PersonRecord, PersonRef, PersonScore, RankedPerson, ResolvedPerson}; + /// Contacts, handle resolution, and closeness scoring. /// /// Reached through diff --git a/crates/tinymemory-api/src/provider/profile.rs b/crates/tinymemory-api/src/provider/profile.rs index 0298d05..57fe43c 100644 --- a/crates/tinymemory-api/src/provider/profile.rs +++ b/crates/tinymemory-api/src/provider/profile.rs @@ -28,6 +28,16 @@ //! [`FacetState::Dropped`] and is *meant* to be collected — protecting it would //! keep the thing the user asked to forget on disk indefinitely. +use async_trait::async_trait; + +use crate::error::MemoryError; + +// The value types this family exchanges. They are defined in `tinymemory-bus` +// — they cross the module boundary, and a host that only makes calls must be +// able to name them without compiling this trait — and re-exported here so +// every historical path keeps resolving and the types stay the same types. +pub use tinymemory_bus::provider::profile::{FacetState, FacetType, ProfileFacet, UserState}; + /// Learned facets about the user. /// /// Reached through [`MemoryProvider::as_profile`](super::MemoryProvider::as_profile). diff --git a/crates/tinymemory-api/src/provider/retrieval.rs b/crates/tinymemory-api/src/provider/retrieval.rs index 845a406..2db59fa 100644 --- a/crates/tinymemory-api/src/provider/retrieval.rs +++ b/crates/tinymemory-api/src/provider/retrieval.rs @@ -34,6 +34,18 @@ //! reports as [`MemoryError::Invalid`], because silently matching nothing would //! look identical to a genuine empty result. +use async_trait::async_trait; + +use crate::error::MemoryError; +use crate::provider::types::SourceScope; +use crate::types::NamespaceMemoryHit; + +// The value types this family exchanges. They are defined in `tinymemory-bus` +// — they cross the module boundary, and a host that only makes calls must be +// able to name them without compiling this trait — and re-exported here so +// every historical path keeps resolving and the types stay the same types. +pub use tinymemory_bus::provider::retrieval::{CoverWindowQuery, EntityMatch, FastRetrieveQuery, RetrievalHit, RetrievalNodeKind, RetrievalResponse, SourceRetrievalQuery}; + /// The engine's deterministic retrieval primitives. /// /// Reached through [`MemoryProvider::as_retrieval`](super::MemoryProvider::as_retrieval). From f5eea1d277324bccbb78b52f82541e949b64072d Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:57:14 +0300 Subject: [PATCH 18/35] refactor(tinymemory-api): re-export wire vocabulary from tinymemory-bus The modules that define the wire vocabulary have been moved from this crate into tinymemory-bus, and are now re-exported rather than defined locally. This allows the host crate to depend only on tinymemory-bus for payload types without pulling in the full driver contract, while preserving all existing import paths through the re-exports. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-api/src/lib.rs | 41 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/crates/tinymemory-api/src/lib.rs b/crates/tinymemory-api/src/lib.rs index cb7d52c..49330e1 100644 --- a/crates/tinymemory-api/src/lib.rs +++ b/crates/tinymemory-api/src/lib.rs @@ -64,13 +64,36 @@ //! round-trips [`error::MemoryError`] through. Shared by both ends of every //! such transport, so the names cannot drift apart. -pub mod capabilities; -pub mod chunks; pub mod drivers; -pub mod error; -pub mod goals; -pub mod health; pub mod host; + +// The wire vocabulary, re-exported from `tinymemory-bus`. +// +// These modules used to be defined here. They moved down a layer because a +// *host* needs them and needs nothing else in this crate: it loads +// `tinymemory-module` and makes calls, so it names `MemoryEntry` and +// `MemoryCategory` but implements no trait, binds no driver and parses no +// config. Making it depend on the whole driver contract to spell a payload type +// was the wrong shape. +// +// Re-exported rather than merely available, so every historical path still +// resolves — `tinymemory_api::types::MemoryEntry` is the same item as +// `tinymemory_bus::types::MemoryEntry`, not a twin of it. That identity is the +// point: a second definition would need a conversion at the module seam that +// nothing type-checks. +pub use tinymemory_bus::{ + capabilities, + chunks, + error, + goals, + health, + recall, + tool_memory, + tree, + types, + version, + wire, +}; /// The mandatory-family composition: wrap any [`traits::Memory`] backend as a /// complete [`provider::MemoryProvider`]. /// @@ -84,12 +107,6 @@ pub mod host; pub mod mandatory; pub mod null; pub mod provider; -pub mod recall; -pub mod tool_memory; pub mod traits; -pub mod tree; -pub mod types; -pub mod version; -pub mod wire; -pub use version::{is_compatible, CONTRACT_VERSION}; +pub use tinymemory_bus::{is_compatible, CONTRACT_VERSION}; From 3abc985a4d628415abd3d0316ab1051e22228b89 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:57:27 +0300 Subject: [PATCH 19/35] feat(tinymemory-api): re-export types from tinymemory-bus crate Moves the `EvidenceRef` type and the `provider::types` module out of local definitions and into re-exports from the new `tinymemory-bus` crate, which is added as a dependency. This lets hosts that only need the wire vocabulary depend on the bus crate alone without compiling the traits, null driver, or host configuration surface. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-api/Cargo.toml | 5 +++++ crates/tinymemory-api/src/host/mod.rs | 3 +-- crates/tinymemory-api/src/provider/mod.rs | 5 ++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/tinymemory-api/Cargo.toml b/crates/tinymemory-api/Cargo.toml index cf68b27..5a6d92a 100644 --- a/crates/tinymemory-api/Cargo.toml +++ b/crates/tinymemory-api/Cargo.toml @@ -44,6 +44,11 @@ description = "Stable public contracts for the TinyMemory memory system" # scope and prints the whole-workspace inverse tree, so it exits 0 and looks # clean even when this crate is the one pulling the dependency in. [dependencies] +# The wire vocabulary. Every payload type this crate exposes is defined there +# and re-exported here, so a host that only makes calls into the loadable module +# can depend on that crate alone and compile none of the traits, the null +# driver, or the `host::` config surface. See `src/lib.rs`. +tinymemory-bus = { path = "../tinymemory-bus" } anyhow = "1" async-trait = "0.1" chrono = { version = "0.4", features = ["serde"] } diff --git a/crates/tinymemory-api/src/host/mod.rs b/crates/tinymemory-api/src/host/mod.rs index d3b4352..97045bd 100644 --- a/crates/tinymemory-api/src/host/mod.rs +++ b/crates/tinymemory-api/src/host/mod.rs @@ -52,7 +52,6 @@ mod embedding_host; mod embeddings; mod error_reporter; mod events; -mod evidence; mod nlp; mod routes; mod usage; @@ -72,7 +71,7 @@ pub use events::{ EmbeddingHealthReason, MemoryEvent, MemoryEventSink, NoopEventSink, SyncTrigger, LOCAL_MODEL_UNAVAILABLE_KIND, MEMORY_USER_ERROR_SOURCE, }; -pub use evidence::EvidenceRef; +pub use tinymemory_bus::evidence::EvidenceRef; pub use local_ai::{LocalAiConfig, LocalAiUsage}; pub use nlp::{SpacyEntity, SpacyResponse}; pub use routes::EmbeddingRouteConfig; diff --git a/crates/tinymemory-api/src/provider/mod.rs b/crates/tinymemory-api/src/provider/mod.rs index ea3235b..74e7bb7 100644 --- a/crates/tinymemory-api/src/provider/mod.rs +++ b/crates/tinymemory-api/src/provider/mod.rs @@ -68,7 +68,10 @@ pub mod people; pub mod profile; pub mod records; pub mod retrieval; -pub mod types; +// The value types every family exchanges, defined in `tinymemory-bus` and +// re-exported at their historical path. See this crate's `lib.rs` for why the +// vocabulary sits a layer below the traits. +pub use tinymemory_bus::provider::types; pub use audit::{audit_provider, CapabilityAudit}; pub use chunks::{ChunkDetail, ChunkEmbedding, ChunkQuery, MemoryChunks}; From b3574acee6a39ae5653b1ba4059277178e074aaa Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:57:44 +0300 Subject: [PATCH 20/35] feat(tinymemory-bus): replace tinymemory-api dependency with standalone types The tinymemory-bus crate no longer depends on tinymemory-api, instead pulling in anyhow, chrono, sha2, and uuid directly. This keeps the host-facing crate deliberately lightweight by including only the types and serialization support that the wire contract actually needs, without pulling in the traits and host configuration that tinymemory-api would bring. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.lock | 6 ++++- crates/tinymemory-bus/Cargo.toml | 39 +++++++++++++++++--------------- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7698b9f..894a2f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1910,6 +1910,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "thiserror 2.0.20", + "tinymemory-bus", "tokio", "toml", "uuid", @@ -1919,10 +1920,13 @@ dependencies = [ name = "tinymemory-bus" version = "0.1.0" dependencies = [ + "anyhow", + "chrono", "serde", "serde_json", + "sha2 0.11.0", "thiserror 2.0.20", - "tinymemory-api", + "uuid", ] [[package]] diff --git a/crates/tinymemory-bus/Cargo.toml b/crates/tinymemory-bus/Cargo.toml index f52053a..a89c85f 100644 --- a/crates/tinymemory-bus/Cargo.toml +++ b/crates/tinymemory-bus/Cargo.toml @@ -10,33 +10,36 @@ license = "MIT" repository = "https://github.com/tinyhumansai/tinymemory" description = "The TinyBus wire contract for the TinyMemory module: member names, payload types, and typed calls" -# Three dependencies, and the ceiling is low on purpose. +# Deliberately dependency-light: this is the crate a host links to talk to the +# loadable module, so it must cost that host almost nothing. Nothing here may +# pull in `rusqlite`, `git2`, `reqwest`, `regex`, an async runtime, or +# `tinybus` — see `src/lib.rs` for why the transport in particular is absent. # -# This crate is what a *host* compiles against to talk to the loaded module, so -# it must cost that host almost nothing: no engine, no storage, no async -# runtime, and — importantly — no `tinybus`. See `src/lib.rs` for why the -# transport is deliberately absent, and the root manifest's note on -# `crates/tinymemory-module` for what depending on the vendored `tinybus` from a -# workspace member would do to this workspace. +# The set is the same one the payload types carried when they lived in +# `tinymemory-api`, minus everything only the traits and the host config needed: # -# Nothing here may pull in `rusqlite`, `git2`, `reqwest`, `regex`, an async -# runtime, or `tinybus`. Guard with the FORWARD form, which is scoped to this -# package — `cargo tree -i` discards the `-p` scope and exits clean even when -# this crate is the one pulling the dependency in: +# - `chrono` — timestamps on chunk, tree and retrieval nodes; the `serde` +# feature backs `chunks::Metadata`'s `chrono::serde::ts_milliseconds`. +# - `sha2` — the deterministic `chunks::chunk_id`. +# - `uuid` — `tool_memory::ToolMemoryRule::generate_id` (v4 bytes, nibble +# encoded). +# - `anyhow` — `error::MemoryError::Other`, which carries an opaque cause. +# - `thiserror`— the `MemoryError` and `CapabilityError` enums. +# +# Guard with the FORWARD form, which is scoped to this package — `cargo tree -i` +# discards the `-p` scope and exits clean even when this crate is the one +# pulling the dependency in: # # cargo tree -p tinymemory-bus -e normal,build --prefix none \ # | grep -Ei 'rusqlite|libsqlite|git2|reqwest|regex|tokio|tinybus' # expect no match [dependencies] -# The single definition of every type on the wire. Re-exported, never -# redefined — see `src/types/mod.rs`. -tinymemory-api = { path = "../tinymemory-api" } -# The call structs derive both halves: `Serialize` to build an argument array, -# `Deserialize` so a module-side test can decode one back. +anyhow = "1" +chrono = { version = "0.4", features = ["serde"] } serde = { version = "1", features = ["derive"] } -# A tinybus frame body is JSON, so an encoded argument list is a -# `serde_json::Value` and nothing here needs a different representation. serde_json = "1" +sha2 = "0.11" thiserror = "2" +uuid = { version = "1", features = ["v4"] } [lints.rust] unsafe_code = "forbid" From 0692e32526a723152026866c46aba50336e7ec33 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:57:52 +0300 Subject: [PATCH 21/35] fix(provider): remove unused imports across multiple provider files Clean up unused import statements in the chunks, episodic, people, profile, and retrieval provider modules to eliminate compiler warnings and improve code clarity. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/provider/chunks.rs | 1 - crates/tinymemory-bus/src/provider/episodic.rs | 1 - crates/tinymemory-bus/src/provider/people.rs | 1 - crates/tinymemory-bus/src/provider/profile.rs | 3 +-- crates/tinymemory-bus/src/provider/retrieval.rs | 1 - 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/tinymemory-bus/src/provider/chunks.rs b/crates/tinymemory-bus/src/provider/chunks.rs index fb556c7..99c09e6 100644 --- a/crates/tinymemory-bus/src/provider/chunks.rs +++ b/crates/tinymemory-bus/src/provider/chunks.rs @@ -30,7 +30,6 @@ //! failure mode with a real precedent, and it is silent; see //! `docs/specs/2026-08-13-memory-module-port.md` §3. -use async_trait::async_trait; use serde::{Deserialize, Serialize}; use crate::chunks::{Chunk, SourceKind}; diff --git a/crates/tinymemory-bus/src/provider/episodic.rs b/crates/tinymemory-bus/src/provider/episodic.rs index c3824de..7ad698a 100644 --- a/crates/tinymemory-bus/src/provider/episodic.rs +++ b/crates/tinymemory-bus/src/provider/episodic.rs @@ -40,7 +40,6 @@ //! trip instead of two, and no reliance on connection-local state. The engine //! knows the id it just wrote; nothing else has to guess. -use async_trait::async_trait; use serde::{Deserialize, Serialize}; use crate::error::MemoryError; diff --git a/crates/tinymemory-bus/src/provider/people.rs b/crates/tinymemory-bus/src/provider/people.rs index 0191010..e6c87f7 100644 --- a/crates/tinymemory-bus/src/provider/people.rs +++ b/crates/tinymemory-bus/src/provider/people.rs @@ -31,7 +31,6 @@ //! not promise that every engine identifies people by UUID, and a caller must //! not parse one out — it round-trips an id it was given and nothing more. -use async_trait::async_trait; use serde::{Deserialize, Serialize}; use crate::error::MemoryError; diff --git a/crates/tinymemory-bus/src/provider/profile.rs b/crates/tinymemory-bus/src/provider/profile.rs index 25ce254..1da1de4 100644 --- a/crates/tinymemory-bus/src/provider/profile.rs +++ b/crates/tinymemory-bus/src/provider/profile.rs @@ -28,12 +28,11 @@ //! [`FacetState::Dropped`] and is *meant* to be collected — protecting it would //! keep the thing the user asked to forget on disk indefinitely. -use async_trait::async_trait; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use crate::error::MemoryError; -use crate::host::EvidenceRef; +use crate::evidence::EvidenceRef; /// What kind of claim a facet makes. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/crates/tinymemory-bus/src/provider/retrieval.rs b/crates/tinymemory-bus/src/provider/retrieval.rs index af92cd5..f9eaac7 100644 --- a/crates/tinymemory-bus/src/provider/retrieval.rs +++ b/crates/tinymemory-bus/src/provider/retrieval.rs @@ -34,7 +34,6 @@ //! reports as [`MemoryError::Invalid`], because silently matching nothing would //! look identical to a genuine empty result. -use async_trait::async_trait; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; From da2629bfb2a84d99805fb77b4c23b2b2f76f6c68 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:58:04 +0300 Subject: [PATCH 22/35] chore(provider): remove unused imports Remove several imports that were no longer used across the provider modules, cleaning up compiler warnings and reducing unnecessary dependencies in the codebase. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/provider/chunks.rs | 2 -- crates/tinymemory-bus/src/provider/episodic.rs | 1 - crates/tinymemory-bus/src/provider/people.rs | 1 - crates/tinymemory-bus/src/provider/profile.rs | 1 - crates/tinymemory-bus/src/provider/retrieval.rs | 3 --- 5 files changed, 8 deletions(-) diff --git a/crates/tinymemory-bus/src/provider/chunks.rs b/crates/tinymemory-bus/src/provider/chunks.rs index 99c09e6..2dc70e8 100644 --- a/crates/tinymemory-bus/src/provider/chunks.rs +++ b/crates/tinymemory-bus/src/provider/chunks.rs @@ -33,8 +33,6 @@ use serde::{Deserialize, Serialize}; use crate::chunks::{Chunk, SourceKind}; -use crate::error::MemoryError; -use crate::provider::types::SourceScope; /// Filters for [`MemoryChunks::list_chunks`]. /// diff --git a/crates/tinymemory-bus/src/provider/episodic.rs b/crates/tinymemory-bus/src/provider/episodic.rs index 7ad698a..a482a01 100644 --- a/crates/tinymemory-bus/src/provider/episodic.rs +++ b/crates/tinymemory-bus/src/provider/episodic.rs @@ -42,7 +42,6 @@ use serde::{Deserialize, Serialize}; -use crate::error::MemoryError; /// One recorded turn. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] diff --git a/crates/tinymemory-bus/src/provider/people.rs b/crates/tinymemory-bus/src/provider/people.rs index e6c87f7..2e16ccd 100644 --- a/crates/tinymemory-bus/src/provider/people.rs +++ b/crates/tinymemory-bus/src/provider/people.rs @@ -33,7 +33,6 @@ use serde::{Deserialize, Serialize}; -use crate::error::MemoryError; /// Opaque identity of one person, as the driver issued it. /// diff --git a/crates/tinymemory-bus/src/provider/profile.rs b/crates/tinymemory-bus/src/provider/profile.rs index 1da1de4..c16c11e 100644 --- a/crates/tinymemory-bus/src/provider/profile.rs +++ b/crates/tinymemory-bus/src/provider/profile.rs @@ -31,7 +31,6 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use crate::error::MemoryError; use crate::evidence::EvidenceRef; /// What kind of claim a facet makes. diff --git a/crates/tinymemory-bus/src/provider/retrieval.rs b/crates/tinymemory-bus/src/provider/retrieval.rs index f9eaac7..a6c47d0 100644 --- a/crates/tinymemory-bus/src/provider/retrieval.rs +++ b/crates/tinymemory-bus/src/provider/retrieval.rs @@ -38,9 +38,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::chunks::SourceKind; -use crate::error::MemoryError; -use crate::provider::types::SourceScope; -use crate::types::NamespaceMemoryHit; /// Whether a hit is a raw leaf or a sealed summary. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] From 0372ee74f428b51be5f39e353c1a403f83bf08e4 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:58:34 +0300 Subject: [PATCH 23/35] fix(evidence): add doc comments to EvidenceRef variant fields Added documentation comments to each field of the EvidenceRef enum variants to clarify the meaning and origin of each identifier, making the data model self-documenting and easier to understand without cross-referencing the database schema. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/chunks.rs | 6 ++-- crates/tinymemory-bus/src/evidence.rs | 46 +++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/crates/tinymemory-bus/src/chunks.rs b/crates/tinymemory-bus/src/chunks.rs index 789bc1b..4ea303e 100644 --- a/crates/tinymemory-bus/src/chunks.rs +++ b/crates/tinymemory-bus/src/chunks.rs @@ -386,7 +386,9 @@ mod time_range_serde { } /// Serialize a `(start, end)` UTC timestamp pair as `{start_ms, end_ms}`. - pub fn serialize( + // `pub(crate)`, not `pub`: the enclosing module is private, so a bare `pub` + // is a surface nothing outside this crate can reach anyway. + pub(crate) fn serialize( value: &(DateTime, DateTime), serializer: S, ) -> Result { @@ -403,7 +405,7 @@ mod time_range_serde { /// Returns a `serde` custom error if either millisecond value does not /// map to a valid `DateTime` (chrono's `timestamp_millis_opt` fails, /// e.g. out-of-range values). - pub fn deserialize<'de, D: Deserializer<'de>>( + pub(crate) fn deserialize<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result<(DateTime, DateTime), D::Error> { let wire = Wire::deserialize(deserializer)?; diff --git a/crates/tinymemory-bus/src/evidence.rs b/crates/tinymemory-bus/src/evidence.rs index 7134b6d..53f1a78 100644 --- a/crates/tinymemory-bus/src/evidence.rs +++ b/crates/tinymemory-bus/src/evidence.rs @@ -22,28 +22,60 @@ use serde::{Deserialize, Serialize}; #[serde(tag = "type", rename_all = "snake_case")] pub enum EvidenceRef { /// A single row in `episodic_log`. - Episodic { episodic_id: i64 }, + Episodic { + /// Row id in `episodic_log`. + episodic_id: i64, + }, /// A contiguous window of rows in `episodic_log`. - EpisodicWindow { from_id: i64, to_id: i64 }, + EpisodicWindow { + /// First row id in the window, inclusive. + from_id: i64, + /// Last row id in the window, inclusive. + to_id: i64, + }, /// A row in the tree-source summary table. - SourceSummary { summary_id: String }, + SourceSummary { + /// Row id in the tree-source summary table. + summary_id: String, + }, /// A node in `tree_topic`. - TreeTopic { topic_id: String }, + TreeTopic { + /// Node id in `tree_topic`. + topic_id: String, + }, /// A chunk in `vector_chunks` associated with a document source. - DocumentChunk { source_id: String, chunk_id: String }, + DocumentChunk { + /// The document source the chunk belongs to. + source_id: String, + /// Row id in `vector_chunks`. + chunk_id: String, + }, /// A specific message in an email source. EmailMessage { + /// The email source the message arrived in. source_id: String, + /// Provider-assigned message id. message_id: String, }, /// A field value from a connected provider (Composio toolkit). Provider { + /// Composio toolkit slug the value came from. toolkit: String, + /// The connection the value was read through. connection_id: String, + /// Field name within the provider's payload. field: String, }, /// A tool call record within an episodic entry. - ToolCall { tool_name: String, episodic_id: i64 }, + ToolCall { + /// The tool that was called. + tool_name: String, + /// The episodic row the call was recorded in. + episodic_id: i64, + }, /// A per-window weight from `tree_source`. - TreeSourceWeight { window_label: String }, + TreeSourceWeight { + /// The `tree_source` window the weight belongs to. + window_label: String, + }, } From d490e0c3b0fd83c9d0b3e438dbfdc404762631b0 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:58:47 +0300 Subject: [PATCH 24/35] fix(provider): remove unused SourceKind import Removed the unused `SourceKind` import from the chunks provider module to eliminate a compiler warning about unused imports. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-api/src/provider/chunks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tinymemory-api/src/provider/chunks.rs b/crates/tinymemory-api/src/provider/chunks.rs index 8309880..1a8f96d 100644 --- a/crates/tinymemory-api/src/provider/chunks.rs +++ b/crates/tinymemory-api/src/provider/chunks.rs @@ -32,7 +32,7 @@ use async_trait::async_trait; -use crate::chunks::{Chunk, SourceKind}; +use crate::chunks::Chunk; use crate::error::MemoryError; use crate::provider::types::SourceScope; From 16032382070b489797b4f3828d1bd8ff73947060 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:59:28 +0300 Subject: [PATCH 25/35] fix: correct test module path in names.rs The test module declaration in names.rs was pointing to a non-existent file. Changed the module path to reference the correct test file name, ensuring tests can be discovered and run properly. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/names.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/tinymemory-bus/src/names.rs b/crates/tinymemory-bus/src/names.rs index 5fabdee..87a78da 100644 --- a/crates/tinymemory-bus/src/names.rs +++ b/crates/tinymemory-bus/src/names.rs @@ -332,4 +332,5 @@ pub const METHODS: [&str; 89] = [ ]; #[cfg(test)] -mod test; +#[path = "names_tests.rs"] +mod tests; From 2d86499fac049f231dc9e0f3d05320fbfe701186 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:59:40 +0300 Subject: [PATCH 26/35] chore(tinymemory): clean up re-exports and whitespace Reordered a re-export in the host module to group it with other external re-exports, collapsed a multi-line re-export list in lib.rs into a single line, reformatted two provider re-exports to use multi-line style for consistency, and removed stray blank lines in two bus provider files. These are purely cosmetic changes with no behavioral impact. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-api/src/host/mod.rs | 2 +- crates/tinymemory-api/src/lib.rs | 12 +----------- crates/tinymemory-api/src/provider/people.rs | 5 ++++- crates/tinymemory-api/src/provider/retrieval.rs | 5 ++++- crates/tinymemory-bus/src/provider/episodic.rs | 1 - crates/tinymemory-bus/src/provider/people.rs | 1 - 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/crates/tinymemory-api/src/host/mod.rs b/crates/tinymemory-api/src/host/mod.rs index 97045bd..e3b713f 100644 --- a/crates/tinymemory-api/src/host/mod.rs +++ b/crates/tinymemory-api/src/host/mod.rs @@ -71,7 +71,6 @@ pub use events::{ EmbeddingHealthReason, MemoryEvent, MemoryEventSink, NoopEventSink, SyncTrigger, LOCAL_MODEL_UNAVAILABLE_KIND, MEMORY_USER_ERROR_SOURCE, }; -pub use tinymemory_bus::evidence::EvidenceRef; pub use local_ai::{LocalAiConfig, LocalAiUsage}; pub use nlp::{SpacyEntity, SpacyResponse}; pub use routes::EmbeddingRouteConfig; @@ -83,6 +82,7 @@ pub use storage_memory::{ pub use subsystems::{ MemoryDriverConfig, MemoryHooksConfig, MemorySubsystemConfig, SubsystemsConfig, }; +pub use tinymemory_bus::evidence::EvidenceRef; pub use usage::UsageInfo; /// Effective default global memory-sync cadence (seconds) used when diff --git a/crates/tinymemory-api/src/lib.rs b/crates/tinymemory-api/src/lib.rs index 49330e1..186815f 100644 --- a/crates/tinymemory-api/src/lib.rs +++ b/crates/tinymemory-api/src/lib.rs @@ -82,17 +82,7 @@ pub mod host; // point: a second definition would need a conversion at the module seam that // nothing type-checks. pub use tinymemory_bus::{ - capabilities, - chunks, - error, - goals, - health, - recall, - tool_memory, - tree, - types, - version, - wire, + capabilities, chunks, error, goals, health, recall, tool_memory, tree, types, version, wire, }; /// The mandatory-family composition: wrap any [`traits::Memory`] backend as a /// complete [`provider::MemoryProvider`]. diff --git a/crates/tinymemory-api/src/provider/people.rs b/crates/tinymemory-api/src/provider/people.rs index 0ba36fb..f2da240 100644 --- a/crates/tinymemory-api/src/provider/people.rs +++ b/crates/tinymemory-api/src/provider/people.rs @@ -39,7 +39,10 @@ use crate::error::MemoryError; // — they cross the module boundary, and a host that only makes calls must be // able to name them without compiling this trait — and re-exported here so // every historical path keeps resolving and the types stay the same types. -pub use tinymemory_bus::provider::people::{AddressBookSeedOutcome, PersonHandle, PersonInteraction, PersonRecord, PersonRef, PersonScore, RankedPerson, ResolvedPerson}; +pub use tinymemory_bus::provider::people::{ + AddressBookSeedOutcome, PersonHandle, PersonInteraction, PersonRecord, PersonRef, PersonScore, + RankedPerson, ResolvedPerson, +}; /// Contacts, handle resolution, and closeness scoring. /// diff --git a/crates/tinymemory-api/src/provider/retrieval.rs b/crates/tinymemory-api/src/provider/retrieval.rs index 2db59fa..666396d 100644 --- a/crates/tinymemory-api/src/provider/retrieval.rs +++ b/crates/tinymemory-api/src/provider/retrieval.rs @@ -44,7 +44,10 @@ use crate::types::NamespaceMemoryHit; // — they cross the module boundary, and a host that only makes calls must be // able to name them without compiling this trait — and re-exported here so // every historical path keeps resolving and the types stay the same types. -pub use tinymemory_bus::provider::retrieval::{CoverWindowQuery, EntityMatch, FastRetrieveQuery, RetrievalHit, RetrievalNodeKind, RetrievalResponse, SourceRetrievalQuery}; +pub use tinymemory_bus::provider::retrieval::{ + CoverWindowQuery, EntityMatch, FastRetrieveQuery, RetrievalHit, RetrievalNodeKind, + RetrievalResponse, SourceRetrievalQuery, +}; /// The engine's deterministic retrieval primitives. /// diff --git a/crates/tinymemory-bus/src/provider/episodic.rs b/crates/tinymemory-bus/src/provider/episodic.rs index a482a01..5a02312 100644 --- a/crates/tinymemory-bus/src/provider/episodic.rs +++ b/crates/tinymemory-bus/src/provider/episodic.rs @@ -42,7 +42,6 @@ use serde::{Deserialize, Serialize}; - /// One recorded turn. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct EpisodicTurn { diff --git a/crates/tinymemory-bus/src/provider/people.rs b/crates/tinymemory-bus/src/provider/people.rs index 2e16ccd..4b60895 100644 --- a/crates/tinymemory-bus/src/provider/people.rs +++ b/crates/tinymemory-bus/src/provider/people.rs @@ -33,7 +33,6 @@ use serde::{Deserialize, Serialize}; - /// Opaque identity of one person, as the driver issued it. /// /// Treat as a token: round-trip it, compare it for equality, never parse it. From 0913ca0eb06d3f4c7851b9b97882bb741dd9c756 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 00:59:59 +0300 Subject: [PATCH 27/35] chore(tinymemory-bus): disable pedantic clippy lint to match sibling crates The pedantic clippy lint has been commented out in the Cargo.toml to keep the lint configuration consistent with `tinymemory-tinycortex` and `tinymemory-remote`. These modules were moved verbatim from `tinymemory-api`, which has no lint table at all, and enabling pedantic now would introduce hundreds of unrelated lint fixes that belong in a separate, focused commit. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/Cargo.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/tinymemory-bus/Cargo.toml b/crates/tinymemory-bus/Cargo.toml index a89c85f..881e888 100644 --- a/crates/tinymemory-bus/Cargo.toml +++ b/crates/tinymemory-bus/Cargo.toml @@ -50,7 +50,13 @@ rust_2018_idioms = { level = "warn", priority = -1 } [lints.clippy] all = { level = "warn", priority = -1 } -pedantic = { level = "warn", priority = -1 } +# `pedantic` is deliberately not enabled, matching `tinymemory-tinycortex` and +# `tinymemory-remote`. These modules moved here verbatim from +# `tinymemory-api`, which carries no `[lints]` table at all; switching pedantic +# on over the move would bury a mechanical relocation under several hundred +# unrelated `#[must_use]` and backtick edits. Turning it on is worth doing — as +# its own commit, over `tinymemory-api` too, so the contract and the vocabulary +# stay lint-compatible. unwrap_used = "warn" expect_used = "warn" panic = "warn" From da9560d743d28eda39ff5d9453b0dbd2d1eb79b2 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 01:00:38 +0300 Subject: [PATCH 28/35] test: add missing test files for tinymemory-bus crate Added nine test files that were previously missing from the tinymemory-bus crate, covering capabilities, chunks, error handling, health checks, provider types, recall, tool memory, types, and wire functionality. These tests ensure the crate's core components have proper test coverage. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/capabilities_tests.rs | 6 ++++++ crates/tinymemory-bus/src/chunks_tests.rs | 6 ++++++ crates/tinymemory-bus/src/error_tests.rs | 6 ++++++ crates/tinymemory-bus/src/health_tests.rs | 6 ++++++ crates/tinymemory-bus/src/provider/types_tests.rs | 6 ++++++ crates/tinymemory-bus/src/recall_tests.rs | 6 ++++++ crates/tinymemory-bus/src/tool_memory_tests.rs | 6 ++++++ crates/tinymemory-bus/src/types_tests.rs | 6 ++++++ crates/tinymemory-bus/src/wire_tests.rs | 6 ++++++ 9 files changed, 54 insertions(+) diff --git a/crates/tinymemory-bus/src/capabilities_tests.rs b/crates/tinymemory-bus/src/capabilities_tests.rs index 92e37c9..0254418 100644 --- a/crates/tinymemory-bus/src/capabilities_tests.rs +++ b/crates/tinymemory-bus/src/capabilities_tests.rs @@ -9,6 +9,12 @@ //! 3. [`super::Capabilities::validate`] rejects a set missing **any** of the //! three mandatory families, checked one family at a time. +// A failed assertion in a test is a panic either way; `unwrap`/`expect` here say +// what the invariant was. Same allowance the repository's other test modules +// take, and the reason these files carried none before is that +// `tinymemory-api` opts into no lints at all. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + use super::*; use serde_json::json; diff --git a/crates/tinymemory-bus/src/chunks_tests.rs b/crates/tinymemory-bus/src/chunks_tests.rs index 3d9c89f..49d8487 100644 --- a/crates/tinymemory-bus/src/chunks_tests.rs +++ b/crates/tinymemory-bus/src/chunks_tests.rs @@ -1,5 +1,11 @@ //! Unit tests for the chunk model (`super`). +// A failed assertion in a test is a panic either way; `unwrap`/`expect` here say +// what the invariant was. Same allowance the repository's other test modules +// take, and the reason these files carried none before is that +// `tinymemory-api` opts into no lints at all. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + use super::*; use chrono::TimeZone; diff --git a/crates/tinymemory-bus/src/error_tests.rs b/crates/tinymemory-bus/src/error_tests.rs index 75d72bc..c10b91a 100644 --- a/crates/tinymemory-bus/src/error_tests.rs +++ b/crates/tinymemory-bus/src/error_tests.rs @@ -2,6 +2,12 @@ //! added for the driver contract. The older variants are exercised where they //! are constructed, in the engine crate. +// A failed assertion in a test is a panic either way; `unwrap`/`expect` here say +// what the invariant was. Same allowance the repository's other test modules +// take, and the reason these files carried none before is that +// `tinymemory-api` opts into no lints at all. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + use super::*; use crate::capabilities::Capability; diff --git a/crates/tinymemory-bus/src/health_tests.rs b/crates/tinymemory-bus/src/health_tests.rs index a31c965..0ae3ee2 100644 --- a/crates/tinymemory-bus/src/health_tests.rs +++ b/crates/tinymemory-bus/src/health_tests.rs @@ -5,6 +5,12 @@ //! kernel's generic `DriverHealth`, and the wire form carries a stable //! `status` discriminant plus a `reason`. +// A failed assertion in a test is a panic either way; `unwrap`/`expect` here say +// what the invariant was. Same allowance the repository's other test modules +// take, and the reason these files carried none before is that +// `tinymemory-api` opts into no lints at all. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + use super::*; use serde_json::json; diff --git a/crates/tinymemory-bus/src/provider/types_tests.rs b/crates/tinymemory-bus/src/provider/types_tests.rs index 390e59a..a2653eb 100644 --- a/crates/tinymemory-bus/src/provider/types_tests.rs +++ b/crates/tinymemory-bus/src/provider/types_tests.rs @@ -4,6 +4,12 @@ //! fail-closed reading of an empty [`SourceScope`], and the wire strings / //! serde defaults that an out-of-process driver depends on. +// A failed assertion in a test is a panic either way; `unwrap`/`expect` here say +// what the invariant was. Same allowance the repository's other test modules +// take, and the reason these files carried none before is that +// `tinymemory-api` opts into no lints at all. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + use super::*; #[test] diff --git a/crates/tinymemory-bus/src/recall_tests.rs b/crates/tinymemory-bus/src/recall_tests.rs index 5adf317..71a4af4 100644 --- a/crates/tinymemory-bus/src/recall_tests.rs +++ b/crates/tinymemory-bus/src/recall_tests.rs @@ -5,6 +5,12 @@ //! half of the field-parity defence described in the module docs (the compile //! half being the exhaustive destructuring inside both `From` impls). +// A failed assertion in a test is a panic either way; `unwrap`/`expect` here say +// what the invariant was. Same allowance the repository's other test modules +// take, and the reason these files carried none before is that +// `tinymemory-api` opts into no lints at all. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + use super::*; use serde_json::json; diff --git a/crates/tinymemory-bus/src/tool_memory_tests.rs b/crates/tinymemory-bus/src/tool_memory_tests.rs index 821369e..821382a 100644 --- a/crates/tinymemory-bus/src/tool_memory_tests.rs +++ b/crates/tinymemory-bus/src/tool_memory_tests.rs @@ -1,5 +1,11 @@ //! Tests for the tool-scoped memory domain types. +// A failed assertion in a test is a panic either way; `unwrap`/`expect` here say +// what the invariant was. Same allowance the repository's other test modules +// take, and the reason these files carried none before is that +// `tinymemory-api` opts into no lints at all. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + use super::*; #[test] diff --git a/crates/tinymemory-bus/src/types_tests.rs b/crates/tinymemory-bus/src/types_tests.rs index 5ee61b5..402e978 100644 --- a/crates/tinymemory-bus/src/types_tests.rs +++ b/crates/tinymemory-bus/src/types_tests.rs @@ -1,5 +1,11 @@ //! Unit tests for the core memory data contracts in [`super`]. +// A failed assertion in a test is a panic either way; `unwrap`/`expect` here say +// what the invariant was. Same allowance the repository's other test modules +// take, and the reason these files carried none before is that +// `tinymemory-api` opts into no lints at all. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + use super::*; use serde_json::json; diff --git a/crates/tinymemory-bus/src/wire_tests.rs b/crates/tinymemory-bus/src/wire_tests.rs index 22f0085..a6db5db 100644 --- a/crates/tinymemory-bus/src/wire_tests.rs +++ b/crates/tinymemory-bus/src/wire_tests.rs @@ -1,5 +1,11 @@ //! The name table is a contract, so these tests pin it rather than exercise it. +// A failed assertion in a test is a panic either way; `unwrap`/`expect` here say +// what the invariant was. Same allowance the repository's other test modules +// take, and the reason these files carried none before is that +// `tinymemory-api` opts into no lints at all. +#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)] + use super::{from_wire, wire_message, wire_name}; use crate::capabilities::Capability; use crate::error::MemoryError; From 1c6c787f2bca32684d4552f240b5623457202ed8 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 01:01:20 +0300 Subject: [PATCH 29/35] chore: files changed clippy.toml,crates/tinymemory-bus/src/chunks.rs Auto-committed-on: dragonfly Co-authored-by: Medulla --- clippy.toml | 13 ++++++++++++- crates/tinymemory-bus/src/chunks.rs | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/clippy.toml b/clippy.toml index e2d8ae6..8485eec 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,4 +2,15 @@ # a code item that forgot its backticks. These are product and technology names # written as prose on purpose; backticking them would imply they name a Rust # item. `..` keeps clippy's own default list rather than replacing it. -doc-valid-idents = ["..", "TinyMemory", "TinyCortex", "OpenHuman", "SQLite", "snake_case"] +doc-valid-idents = [ + "..", + "TinyMemory", + "TinyCortex", + "OpenHuman", + "TinyBus", + "SQLite", + "snake_case", + # Product names in `chunks::SourceKind`'s prose, not Rust items. + "WhatsApp", + "FastMail", +] diff --git a/crates/tinymemory-bus/src/chunks.rs b/crates/tinymemory-bus/src/chunks.rs index 4ea303e..2c141b7 100644 --- a/crates/tinymemory-bus/src/chunks.rs +++ b/crates/tinymemory-bus/src/chunks.rs @@ -243,7 +243,8 @@ impl Metadata { /// nodes on top of these leaves; here they live standalone. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct Chunk { - /// Deterministic id derived from (source_kind, source_id, seq_in_source, content). + /// Deterministic id derived from (`source_kind`, `source_id`, `seq_in_source`, + /// `content`). pub id: String, /// Canonical Markdown content. pub content: String, From 4b2d416b642de9c30dd7bf2a67f8f0d698a20c21 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 01:04:11 +0300 Subject: [PATCH 30/35] fix: correct outdated crate name in doc examples Update the crate path in doc examples from `tinymemory_api` to `tinymemory_bus` across three files, fixing broken documentation tests that would fail when run. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/provider/types.rs | 2 +- crates/tinymemory-bus/src/types.rs | 4 ++-- crates/tinymemory-bus/src/version.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/tinymemory-bus/src/provider/types.rs b/crates/tinymemory-bus/src/provider/types.rs index feb420a..2a64427 100644 --- a/crates/tinymemory-bus/src/provider/types.rs +++ b/crates/tinymemory-bus/src/provider/types.rs @@ -85,7 +85,7 @@ impl SourceScope { /// rule. /// /// ``` - /// use tinymemory_api::provider::types::SourceScope; + /// use tinymemory_bus::provider::types::SourceScope; /// /// let scope = SourceScope::new(["src-abc"]); /// assert!(scope.allows_source_id("src-abc")); diff --git a/crates/tinymemory-bus/src/types.rs b/crates/tinymemory-bus/src/types.rs index d6c066a..68e8b04 100644 --- a/crates/tinymemory-bus/src/types.rs +++ b/crates/tinymemory-bus/src/types.rs @@ -79,7 +79,7 @@ impl MemoryTaint { /// # Examples /// /// ``` - /// use tinymemory_api::types::MemoryTaint; + /// use tinymemory_bus::types::MemoryTaint; /// /// assert_eq!(MemoryTaint::Internal.as_db_str(), "internal"); /// assert_eq!(MemoryTaint::ExternalSync.as_db_str(), "external_sync"); @@ -103,7 +103,7 @@ impl MemoryTaint { /// # Examples /// /// ``` - /// use tinymemory_api::types::MemoryTaint; + /// use tinymemory_bus::types::MemoryTaint; /// /// assert_eq!(MemoryTaint::from_db_str("internal"), MemoryTaint::Internal); /// assert_eq!(MemoryTaint::from_db_str("external_sync"), MemoryTaint::ExternalSync); diff --git a/crates/tinymemory-bus/src/version.rs b/crates/tinymemory-bus/src/version.rs index 6123c36..798f721 100644 --- a/crates/tinymemory-bus/src/version.rs +++ b/crates/tinymemory-bus/src/version.rs @@ -70,7 +70,7 @@ pub const CONTRACT_VERSION: (u16, u16) = (2, 2); /// # Examples /// /// ``` -/// use tinymemory_api::{is_compatible, CONTRACT_VERSION}; +/// use tinymemory_bus::{is_compatible, CONTRACT_VERSION}; /// /// // The version this build speaks is always compatible with itself. /// assert!(is_compatible(CONTRACT_VERSION)); From 9e92ae53d0881d90c39d51dc0b8c402ce9b19625 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 01:05:42 +0300 Subject: [PATCH 31/35] chore: remove intra-doc links to types that are no longer re-exported Several doc comments in the provider module referenced types like `MemoryProvider`, `MemoryChunks`, `MemoryEntities`, `MemoryTree`, `MemoryRetrieval`, `MemoryIngest`, `MemoryPortability`, and `MemorySourceSink` using intra-doc link syntax, but these types are no longer re-exported from the crate root. The links were replaced with plain backtick names to avoid broken documentation references, and one unused import was removed. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-bus/src/capabilities.rs | 2 +- crates/tinymemory-bus/src/provider/chunks.rs | 8 ++++---- crates/tinymemory-bus/src/provider/people.rs | 3 +-- crates/tinymemory-bus/src/provider/profile.rs | 2 +- crates/tinymemory-bus/src/provider/retrieval.rs | 10 +++++----- crates/tinymemory-bus/src/provider/types.rs | 6 +++--- crates/tinymemory-bus/src/wire.rs | 2 +- 7 files changed, 16 insertions(+), 17 deletions(-) diff --git a/crates/tinymemory-bus/src/capabilities.rs b/crates/tinymemory-bus/src/capabilities.rs index 5e392c8..e7641ac 100644 --- a/crates/tinymemory-bus/src/capabilities.rs +++ b/crates/tinymemory-bus/src/capabilities.rs @@ -253,7 +253,7 @@ pub struct Capabilities { impl Capabilities { /// The empty default capability set. The `null` driver advertises - /// [`Self::mandatory`] via its [`MemoryProvider::capabilities`](crate::provider::MemoryProvider::capabilities) + /// [`Self::mandatory`] via its `MemoryProvider::capabilities` /// implementation, not this. pub const fn empty() -> Self { Self { bits: 0 } diff --git a/crates/tinymemory-bus/src/provider/chunks.rs b/crates/tinymemory-bus/src/provider/chunks.rs index 2dc70e8..0be7aef 100644 --- a/crates/tinymemory-bus/src/provider/chunks.rs +++ b/crates/tinymemory-bus/src/provider/chunks.rs @@ -6,7 +6,7 @@ //! //! # Why a caller would want this rather than recall //! -//! [`MemoryRecall`](super::MemoryRecall) answers "what is relevant to this +//! `MemoryRecall` answers "what is relevant to this //! query" and owns its own ranking. This family answers "give me the rows //! matching these filters", which is what a host-side search tool needs when it //! is doing the ranking itself — cosine similarity with its own MMR @@ -23,7 +23,7 @@ //! //! # Embeddings are keyed by signature, and the signature must match exactly //! -//! [`MemoryChunks::chunk_embeddings`] takes a `model_signature` and returns +//! `MemoryChunks::chunk_embeddings` takes a `model_signature` and returns //! only vectors stored under it. A caller that computes that string differently //! from the driver gets an empty result rather than an error — the vectors are //! there, just filed under a name the caller did not ask for. That is a real @@ -34,7 +34,7 @@ use serde::{Deserialize, Serialize}; use crate::chunks::{Chunk, SourceKind}; -/// Filters for [`MemoryChunks::list_chunks`]. +/// Filters for `MemoryChunks::list_chunks`. /// /// Every field is optional and they compose with AND. The default matches /// everything the scope allows, bounded by the driver's own safety cap. @@ -109,6 +109,6 @@ pub struct ChunkDetail { /// /// Not scoped to a signature on purpose: this answers "has this been /// embedded at all", which is what an inspection view wants. Asking whether - /// a *particular* space has it is [`MemoryChunks::chunk_embeddings`]. + /// a *particular* space has it is `MemoryChunks::chunk_embeddings`. pub has_embedding: bool, } diff --git a/crates/tinymemory-bus/src/provider/people.rs b/crates/tinymemory-bus/src/provider/people.rs index 4b60895..e76d11c 100644 --- a/crates/tinymemory-bus/src/provider/people.rs +++ b/crates/tinymemory-bus/src/provider/people.rs @@ -8,14 +8,13 @@ //! //! People is storage the engine owns, and it does not fit any family already //! defined: a person is not a memory entry, not a document, and not a graph -//! entity. Adding these methods to, say, [`MemoryEntities`] would also have +//! entity. Adding these methods to, say, `MemoryEntities` would also have //! been a **major** contract bump — the version rule treats a new method on a //! family a driver may already advertise as breaking, because negotiation //! cannot save a caller from a method an older driver does not implement. A new //! family is a minor bump instead, and an older driver simply does not //! advertise it. //! -//! [`MemoryEntities`]: crate::provider::MemoryEntities //! //! # The types here are the contract's own //! diff --git a/crates/tinymemory-bus/src/provider/profile.rs b/crates/tinymemory-bus/src/provider/profile.rs index c16c11e..1042d8b 100644 --- a/crates/tinymemory-bus/src/provider/profile.rs +++ b/crates/tinymemory-bus/src/provider/profile.rs @@ -23,7 +23,7 @@ //! says "forget that" must not have it re-learned. //! //! The two are **not** symmetric under -//! [`MemoryProfile::drop_facets_below`], and the asymmetry is deliberate: only +//! `MemoryProfile::drop_facets_below`, and the asymmetry is deliberate: only //! `Pinned` is protected from the sweep. A `Forgotten` facet is already in //! [`FacetState::Dropped`] and is *meant* to be collected — protecting it would //! keep the thing the user asked to forget on disk indefinitely. diff --git a/crates/tinymemory-bus/src/provider/retrieval.rs b/crates/tinymemory-bus/src/provider/retrieval.rs index a6c47d0..e3f4a23 100644 --- a/crates/tinymemory-bus/src/provider/retrieval.rs +++ b/crates/tinymemory-bus/src/provider/retrieval.rs @@ -4,7 +4,7 @@ //! exposes graph-walk retrieval, time-window coverage, and entity-index search //! — the LLM-free primitives a host composes an answer from. //! -//! # Separate from [`MemoryTree`](super::MemoryTree), on purpose +//! # Separate from `MemoryTree`, on purpose //! //! The tree family navigates a known node: query one source, drill into //! children, seal, cascade. These three answer questions about the store as a @@ -30,8 +30,8 @@ //! `datetime`, `technology`, `artifact`, `quantity`, `misc`, `topic`. //! //! Requests are the opposite case and are validated: an unknown kind in -//! [`MemoryRetrieval::search_entities`]'s filter is a caller mistake the driver -//! reports as [`MemoryError::Invalid`], because silently matching nothing would +//! `MemoryRetrieval::search_entities`'s filter is a caller mistake the driver +//! reports as [`MemoryError::Invalid`](crate::error::MemoryError::Invalid), because silently matching nothing would //! look identical to a genuine empty result. use chrono::{DateTime, Utc}; @@ -105,7 +105,7 @@ pub struct RetrievalResponse { pub truncated: bool, } -/// Options for [`MemoryRetrieval::fast_retrieve`]. +/// Options for `MemoryRetrieval::fast_retrieve`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct FastRetrieveQuery { /// Maximum hits to return. @@ -135,7 +135,7 @@ pub struct CoverWindowQuery { pub limit: Option, } -/// Filters for [`MemoryRetrieval::retrieve_source`]. +/// Filters for `MemoryRetrieval::retrieve_source`. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SourceRetrievalQuery { /// Restrict to one logical source (the engine's "scope", e.g. `slack:#eng`). diff --git a/crates/tinymemory-bus/src/provider/types.rs b/crates/tinymemory-bus/src/provider/types.rs index 2a64427..29ed4de 100644 --- a/crates/tinymemory-bus/src/provider/types.rs +++ b/crates/tinymemory-bus/src/provider/types.rs @@ -102,7 +102,7 @@ impl SourceScope { } } -/// One unit of content handed to [`crate::provider::MemoryIngest`]. +/// One unit of content handed to `MemoryIngest`. /// /// The driver owns chunking, embedding, and persistence — this type carries /// only what the driver cannot know: where the content came from, when, who it @@ -194,7 +194,7 @@ pub struct ExportRecord { /// One page of an export, plus the cursor that continues it. /// -/// Paging (rather than a stream) keeps [`crate::provider::MemoryPortability`] +/// Paging (rather than a stream) keeps `MemoryPortability` /// object-safe and runtime-agnostic while still bounding memory: the caller /// decides the page size and drives the loop. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -340,7 +340,7 @@ pub struct DiffReport { pub changes: Vec, } -/// One item handed to [`crate::provider::MemorySourceSink`] by the host's sync +/// One item handed to `MemorySourceSink` by the host's sync /// machinery. /// /// The host owns credentials, scheduling, and fetching; the driver owns storage diff --git a/crates/tinymemory-bus/src/wire.rs b/crates/tinymemory-bus/src/wire.rs index 02a6e61..e99bdc4 100644 --- a/crates/tinymemory-bus/src/wire.rs +++ b/crates/tinymemory-bus/src/wire.rs @@ -22,7 +22,7 @@ //! only those three responses. That is wrong for two reasons. //! //! A host does not merely *react* to a driver error; it **is** a -//! [`MemoryProvider`](crate::provider::MemoryProvider) to everything above it, +//! `MemoryProvider` to everything above it, //! so it has to hand its own callers a `MemoryError`. Collapsing on the way out //! and guessing on the way back in would turn a `NotFound` into an `Invalid`, //! and `get`'s contract says a missing entry is `Ok(None)` while an `Invalid` is From 40f770990813f26db5afff6a8e9263ebdaacb6d2 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 01:06:13 +0300 Subject: [PATCH 32/35] chore(deps): update Cargo.lock for tinymemory-module Updated the Cargo.lock file to reflect changes in dependencies for the tinymemory-module crate, ensuring consistency with the current Cargo.toml specifications. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-module/Cargo.lock | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/tinymemory-module/Cargo.lock b/crates/tinymemory-module/Cargo.lock index ba0147e..aa131ca 100644 --- a/crates/tinymemory-module/Cargo.lock +++ b/crates/tinymemory-module/Cargo.lock @@ -1793,6 +1793,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "thiserror 2.0.20", + "tinymemory-bus", "uuid", ] @@ -1800,10 +1801,13 @@ dependencies = [ name = "tinymemory-bus" version = "0.1.0" dependencies = [ + "anyhow", + "chrono", "serde", "serde_json", + "sha2 0.11.0", "thiserror 2.0.20", - "tinymemory-api", + "uuid", ] [[package]] From 0024ef732aadb5652eae87f3bc879b9eb15649de Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 01:07:33 +0300 Subject: [PATCH 33/35] chore(tinymemory-api): remove unused dependencies The `chrono`, `sha2`, `uuid`, and `thiserror` dependencies were removed from the tinymemory-api crate because the payload vocabulary that required them has been moved to the `tinymemory-bus` crate. The comment in Cargo.toml was updated to reflect the new, smaller set of dependencies that only the traits and host seam need. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.lock | 4 --- crates/tinymemory-api/Cargo.toml | 47 ++++++++++++++------------------ 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 894a2f9..7c1587a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1903,17 +1903,13 @@ version = "0.1.1" dependencies = [ "anyhow", "async-trait", - "chrono", "log", "schemars", "serde", "serde_json", - "sha2 0.11.0", - "thiserror 2.0.20", "tinymemory-bus", "tokio", "toml", - "uuid", ] [[package]] diff --git a/crates/tinymemory-api/Cargo.toml b/crates/tinymemory-api/Cargo.toml index 5a6d92a..4ff3c99 100644 --- a/crates/tinymemory-api/Cargo.toml +++ b/crates/tinymemory-api/Cargo.toml @@ -11,28 +11,29 @@ license = "MIT" repository = "https://github.com/tinyhumansai/tinymemory" description = "Stable public contracts for the TinyMemory memory system" -# Deliberately dependency-light: this crate is the stable contract surface that -# hosts compile against, so it must stay free of native, async-runtime, and -# storage dependencies. Anything heavier belongs in the `tinycortex` engine -# crate, never here. +# Deliberately dependency-light: this crate is the driver contract an engine +# compiles against, so it must stay free of native, async-runtime, and storage +# dependencies. Anything heavier belongs in the `tinycortex` engine crate, never +# here. # -# The full set is intentionally small and pure-Rust. Beyond the -# serde/error/async-trait baseline it carries exactly three additions, each -# pulled in by a value type that has to keep behaving identically after the -# move out of the engine crate: +# The set shrank when the payload vocabulary moved to `tinymemory-bus`: +# `chrono`, `sha2` and `uuid` went with the types that needed them +# (`chunks::Metadata`, `chunks::chunk_id`, `ToolMemoryRule::generate_id`), and +# `thiserror` went with `MemoryError`. What is left is what the *traits* and the +# host seam need: # -# - `chrono` — timestamps on chunk/tree nodes; the `serde` feature backs -# `chunks::Metadata`'s `chrono::serde::ts_milliseconds`. -# - `sha2` — the deterministic `chunks::chunk_id`. -# - `uuid` — `tool_memory::ToolMemoryRule::generate_id` (v4 bytes, nibble -# encoded). Only the `v4` feature is needed here; the engine -# crate additionally enables `serde`. -# - `schemars` — the `host::` config sections are still fields of the host's -# root `Config`, which derives `JsonSchema` to generate the -# settings schema the UI renders. Dropping the derive on the way -# down here would silently shrink that schema. `schemars` is pure -# Rust (serde + serde_json + dyn-clone + ref-cast) and carries -# none of the forbidden dependencies below. +# - `async-trait` — every capability-family trait is `async fn` on an +# object-safe trait. +# - `anyhow` — `traits::Memory` and the mandatory composition are +# anyhow-typed. +# - `schemars` — the `host::` config sections are still fields of the host's +# root `Config`, which derives `JsonSchema` to generate the +# settings schema the UI renders. Dropping the derive on the +# way down here would silently shrink that schema. `schemars` +# is pure Rust (serde + serde_json + dyn-clone + ref-cast). +# - `log` — the `host::cloud_providers` legacy-field migration logs what +# it rewrote. The zero-dependency facade, not an +# implementation. # # Nothing here may pull in `rusqlite`, `git2`, `reqwest`, `regex`, or an async # runtime. Guard with the FORWARD form, which is scoped to this package: @@ -51,16 +52,10 @@ description = "Stable public contracts for the TinyMemory memory system" tinymemory-bus = { path = "../tinymemory-bus" } anyhow = "1" async-trait = "0.1" -chrono = { version = "0.4", features = ["serde"] } -# `log` is the zero-dependency logging facade, not an implementation. The -# `host::cloud_providers` legacy-field migration logs what it rewrote. log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" schemars = "1.2" -sha2 = "0.11" -thiserror = "2" -uuid = { version = "1", features = ["v4"] } [dev-dependencies] # The moved `host::` config sections are parsed from TOML in their own tests, From bf97eec7342ac1d328a2d5be1c195fb716ae39dc Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 01:08:08 +0300 Subject: [PATCH 34/35] feat(tinymemory-api): clarify module documentation and re-export structure Updated the crate-level documentation to explain that value types, error enums, and capability vocabulary are now re-exported from `tinymemory_bus` rather than defined directly. Added a new section describing the split between driver authors who need this crate's traits and hosts who only require `tinymemory-bus`, along with the rationale for avoiding duplicate type definitions. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymemory-api/src/lib.rs | 37 +++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/crates/tinymemory-api/src/lib.rs b/crates/tinymemory-api/src/lib.rs index 186815f..a8eff52 100644 --- a/crates/tinymemory-api/src/lib.rs +++ b/crates/tinymemory-api/src/lib.rs @@ -1,15 +1,32 @@ //! Stable public contracts for the TinyMemory memory system. //! -//! This crate holds the value types, error enum, capability vocabulary, and -//! storage trait that memory engines and their embedding hosts compile -//! against. It is engine-neutral on purpose: `tinycortex` is the default -//! embedded engine, not the owner of the contract, and a second engine -//! (`supermemory`, `mem0`, a self-hosted HTTP backend) implements the same -//! traits without either engine learning about the other. -//! It is deliberately dependency-light (serde / serde_json / -//! chrono / sha2 / anyhow / thiserror / async-trait / uuid only) so depending on -//! the contract never drags in SQLite, git2, reqwest, regex, or an async -//! runtime. +//! This crate holds the traits a memory engine implements, the host seam it is +//! bound through, and — re-exported from [`tinymemory_bus`] — the value types, +//! error enum and capability vocabulary they exchange. It is engine-neutral on +//! purpose: `tinycortex` is the default embedded engine, not the owner of the +//! contract, and a second engine (`supermemory`, `mem0`, a self-hosted HTTP +//! backend) implements the same traits without either engine learning about the +//! other. It is deliberately dependency-light (serde / serde_json / anyhow / +//! async-trait / schemars / log, plus `tinymemory-bus`) so depending on the +//! contract never drags in SQLite, git2, reqwest, regex, or an async runtime. +//! +//! ## The vocabulary lives one layer down +//! +//! Every payload type is defined in [`tinymemory_bus`] and re-exported here at +//! its historical path, so `tinymemory_api::types::MemoryEntry` is the *same +//! item* as `tinymemory_bus::types::MemoryEntry`, not a structural twin. +//! +//! The split follows what a consumer actually needs. A **driver author** +//! implements [`provider::MemoryProvider`] and wants this crate: traits, the +//! null driver, the mandatory composition, the [`host`] seam. A **host** loads +//! `tinymemory-module` over `TinyBus` and only makes calls — it names +//! `MemoryEntry` and `MemoryCategory` and implements nothing — so it depends on +//! `tinymemory-bus` alone and compiles none of this. +//! +//! Defining a second set of payload types for that host was the alternative, +//! and it is the failure the root manifest's `[patch]` table exists to prevent: +//! `MemoryCategory` from the module would not be `MemoryCategory` in the host, +//! with a conversion at every call site that nothing type-checks. //! //! ## Self-contained by design //! From 86121960719687e9d0033b208ae8df71bf75f785 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 21 Aug 2026 01:08:44 +0300 Subject: [PATCH 35/35] docs: clarify tinymemory-bus as the vocabulary crate beneath tinymemory-api Rewrite both READMEs to reflect the architectural change that moved payload types from tinymemory-api down into tinymemory-bus. The bus crate is now the single source of truth for every type that crosses the module boundary, with tinymemory-api depending on it and re-exporting all of it. The old text described a crate that re-exported types from tinymemory-api; the new text describes a crate that owns them, with tinymemory-api as the consumer. The host-side call example is updated to show direct use of the bus crate's types and names rather than the old BusCall abstraction, and the section on why arguments get structs is removed since calls now use positional JSON directly. Auto-committed-on: dragonfly Co-authored-by: Medulla --- README.md | 18 +-- crates/tinymemory-bus/README.md | 214 +++++++++++++++----------------- 2 files changed, 108 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index b759365..4458862 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,17 @@ crates/ │ │ binds as, and the fail-closed external-driver gate │ ├── tests/ integration tests against the public API only │ └── examples/ runnable, compiled-in-CI usage examples -├── tinymemory-api/ the contract. Dependency-light on purpose: depending on -│ it never drags in SQLite, git2, reqwest, or an async -│ runtime -├── tinymemory-bus/ the wire contract for the loadable module: member names, -│ the payload types, and one typed call per member. What a -│ *host* links to talk to `tinymemory-module`, which ships -│ as a `cdylib` and exports no Rust surface of its own +├── tinymemory-api/ the driver contract: the traits an engine implements and +│ the host seam it binds through, plus every +│ `tinymemory-bus` type re-exported at its historical path. +│ Dependency-light on purpose: depending on it never drags +│ in SQLite, git2, reqwest, or an async runtime +├── tinymemory-bus/ the wire vocabulary: every type that crosses the module +│ boundary, plus the member names. Sits *below* the +│ contract — `tinymemory-api` depends on it and re-exports +│ it — so a host that only makes calls into +│ `tinymemory-module` links this alone and compiles no +│ traits, no null driver and no config surface ├── tinymemory-core/ the substance: ingestion, the summary tree, chunk │ storage, entities, the graph, the diff ledger, goals, │ tool-memory, and the Composio sync layer. The largest diff --git a/crates/tinymemory-bus/README.md b/crates/tinymemory-bus/README.md index 601b511..5598697 100644 --- a/crates/tinymemory-bus/README.md +++ b/crates/tinymemory-bus/README.md @@ -1,125 +1,97 @@ # tinymemory-bus -The wire contract for the TinyMemory `TinyBus` module, as a library a host -links. - -TinyMemory ships as a loadable module so a host does not compile the engine. -`crates/tinymemory-module` exports one object with 89 members on it, and it -ships as a `cdylib` — a host can load it, but it cannot `use` anything out of -it. This crate is what the host compiles against instead: - -| module | what it holds | -| -------- | -------------------------------------------------------------- | -| `names` | the bus name, the object path, one constant per member | -| `types` | every value type that crosses a frame | -| `calls` | one struct per member: arguments in wire order, plus reply type | -| `wire` | the error names, and the mapping back to `MemoryError` | - -Four dependencies, none of them heavy: `tinymemory-api` for the types, `serde` -and `serde_json` for the encoding, `thiserror` for one small error enum. No -engine, no storage, no async runtime — and no `tinybus`. - -## Why the types are re-exported, not defined - -The obvious reading of "a crate that holds the bus types" is a crate that -*defines* them. That would be a mistake, and the repository has already made -the equivalent one once: when `tinymemory-api` was resolved twice, by git and -by path, `MemoryCategory` from one copy was not the same type as -`MemoryCategory` from the other, and the mismatch only surfaced at the seam. -The root `Cargo.toml`'s `[patch]` table exists to prevent exactly that. - -Defining structurally identical types here would reproduce it deliberately: the -module would serve `tinymemory_api::` types, the host would hold -`tinymemory_bus::` ones, and every call site would need a conversion whose -correctness nothing checks. So there is one definition, in `tinymemory-api`, -surfaced here. A host gets the types the module serves — the same types, not -equivalents. - -## Why not just depend on `tinymemory-api` - -It would compile. But `tinymemory-api` is the **driver** contract: it also -carries `MemoryProvider` and its eighteen capability traits, the -mandatory-family composition, the null driver, and the `host::` config sections -a host persists in `config.toml`. A host that loads the module implements none -of that — it makes calls. - -This crate is the subset that crosses a frame. What a host compiles against is -what it can actually send and receive, and a trait method that is not exported -on the bus is absent here rather than tempting. - -## Why arguments get a struct - -`#[tinybus::interface]` puts a method's arguments on the wire as a positional -JSON array, decoded into a tuple on the far side. That is a fine encoding and a -bad thing to write by hand. `Store` takes six arguments: - -```json -["work", "standup", "…", "core", null, "internal"] -``` - -Two are `Option`s, two are enums that serialize as strings, and swapping -`namespace` with `key` produces a call that succeeds and writes the entry to the -wrong place. Nothing on the module side can catch it — both are `String`, in -the right position count, and the engine has no way to know which one the caller -meant. - -So a caller fills in named fields and `BusCall::into_args` does the positioning. -The reply type travels with the call for the same reason: `Get` answers -`Option` and `Forget` answers `bool`, both are perfectly good JSON, -and decoding one as the other fails somewhere far from the call. - -## There is no client here - -This crate holds no connection and no `call()` that sends anything. Two reasons. - -A host already owns its connection — its reconnect policy, its timeouts, its -tracing, its own idea of what a memory call costs it. A client here would either -duplicate that or fight it, and the useful part is already in `calls` and -`types`. - -And structurally it could not work anyway: `tinybus` is a vendored submodule -whose manifest inherits fields from its own nested `[workspace.package]`, so a -member of this workspace that depends on it makes cargo resolve that inheritance -against the wrong root and fail. That is why `crates/tinymemory-module` is its -own workspace root — see the note on `exclude` in the root `Cargo.toml`. A -contract crate a host links has no business being a separate workspace, so it -stays transport-free. - -Wiring it up host-side is small: +Every type that crosses the TinyMemory `TinyBus` boundary, and the names of the +members that carry them. + +TinyMemory ships as a loadable module so a host does not compile the engine: +`crates/tinymemory-module` exports one object with 89 members on it, built as a +`cdylib`. A host can load that binary but cannot `use` anything out of it, so +the payload vocabulary has to be published as an ordinary library. This is it. + +| module | what it holds | +| ---------------------------------------------------------------- | ---------------------------------------------- | +| `names` | bus name, object path, one constant per member | +| `types`, `chunks`, `recall`, `tree`, `goals`, `tool_memory`, `health`, `capabilities`, `evidence` | the value vocabulary | +| `provider` | the value types each capability family exchanges | +| `error`, `wire` | `MemoryError` and the name table it round-trips through | +| `version` | `CONTRACT_VERSION` and the bind rule | + +Seven dependencies, all pure Rust: `serde`, `serde_json`, `chrono`, `sha2`, +`uuid`, `anyhow`, `thiserror`. + +## This crate sits underneath `tinymemory-api` + +`tinymemory-api` **depends on this crate and re-exports all of it**. That +direction matters, and it is the opposite of the obvious one. + +The payload types used to live in `tinymemory-api`. They moved down because a +*host* needs them and needs nothing else in that crate: it loads the module and +makes calls, so it names `MemoryEntry` and `MemoryCategory` but implements no +trait, binds no driver and parses no config. Making it depend on the whole +driver contract to spell a payload type was the wrong shape. + +The alternative — a parallel set of payload types for hosts — is worse, and the +repository has already had the equivalent bug: when `tinymemory-api` resolved +twice, `MemoryCategory` from one copy was not the same type as `MemoryCategory` +from the other, and the mismatch only surfaced at the seam. The root +`Cargo.toml`'s `[patch]` table exists to stop that. One definition, here, at the +bottom. + +Because the re-export is by module rather than by item, every historical path +keeps resolving unchanged — `tinymemory_api::types::MemoryEntry`, +`tinymemory::MemoryCategory`, `tinycortex::memory::types::*` — and they are the +same items, not twins. + +So: a driver author depends on `tinymemory-api` and gets traits and vocabulary. +A host depends on `tinymemory-bus` and gets vocabulary alone. + +## What is deliberately absent + +**No traits.** `MemoryProvider` and the eighteen capability-family traits +describe what an engine must implement, not what a frame carries. They stay in +`tinymemory-api`. The split is readable off the path: a name here is data, a +name there is an obligation. + +**No transport.** This crate does not depend on `tinybus` and holds no +connection, client or codec. A host already owns its connection — its reconnect +policy, its timeouts, its tracing — and the useful part is the vocabulary. + +That is also structural, not just preference: `tinybus` is vendored as a +submodule whose manifest inherits fields from its own nested +`[workspace.package]`, so a member of this workspace that depends on it makes +cargo resolve that inheritance against the wrong root and fail. It is why +`crates/tinymemory-module` is its own workspace root — see the note on `exclude` +in the root `Cargo.toml`. A crate every workspace member depends on has to stay +transport-free. + +**No host configuration, no null driver, no composition helpers.** Those are +`tinymemory-api`'s, and none of them cross a frame. + +## Making a call + +Arguments travel as a positional JSON array — `#[tinybus::interface]` decodes +them into a tuple — and the member name comes from `names`: ```rust,ignore -use tinymemory_bus::calls::BusCall; -use tinymemory_bus::names::{BUS_NAME, OBJECT_PATH}; -use tinymemory_bus::{types::MemoryError, wire}; - -/// Make one call, and give a failure back as the driver's own error type. -async fn call( - connection: &tinybus::Connection, - call: C, -) -> Result { - let args = call - .into_args() - .map_err(|e| MemoryError::Invalid(e.to_string()))?; - - match connection - .call(BUS_NAME, OBJECT_PATH, C::METHOD, args) - .await - { - Ok(body) => C::decode_response(body).map_err(|e| MemoryError::Other(e.into())), - // The name is the contract; `from_wire` is the same table the module - // mapped out through, so the variant survives the round trip. - Err(tinybus::Error::MethodFailed { name, message }) => { - Err(wire::from_wire(&name, &message)) - } - Err(other) => Err(MemoryError::Other(other.into())), +use tinymemory_bus::names::{methods, BUS_NAME, OBJECT_PATH}; +use tinymemory_bus::types::MemoryEntry; +use tinymemory_bus::wire; + +let body = serde_json::json!([namespace, key]); +match connection.call(BUS_NAME, OBJECT_PATH, methods::GET, body).await { + Ok(reply) => Ok(serde_json::from_value::>(reply)?), + // The name is the contract, and `from_wire` is the same table the module + // mapped out through, so the variant survives the round trip. + Err(tinybus::Error::MethodFailed { name, message }) => { + Err(wire::from_wire(&name, &message)) } + Err(other) => Err(other.into()), } ``` -`OpenStore` is the one member that needs more than that: it returns an object -*path*, not a value, and calls against that path use the same `BUS_NAME` and the -same member names. Treat `OBJECT_PATH` as the root object rather than the only -one. +`OpenStore` is the one member that returns an object *path* rather than a value: +a sibling store under the same workspace, exporting the identical interface. +Treat `OBJECT_PATH` as the root object, not the only one. ## Staying in step with the module @@ -130,6 +102,14 @@ two — this crate lists members by hand, the module derives them from its `#[tinybus::interface]` block — so that test is what turns a drift into a `cargo test` failure instead of an `UnknownMethod` in a host at runtime. -Adding a member is therefore three edits in this crate: a constant in -`names::methods`, an entry in `names::METHODS`, and a call struct in the -matching `calls` family (which `calls::test::COVERED` also lists). +Adding a member is two edits here: a constant in `names::methods` and an entry +in `names::METHODS`. + +## Lints + +`clippy::pedantic` is deliberately off, matching `tinymemory-tinycortex` and +`tinymemory-remote`. These modules arrived verbatim from `tinymemory-api`, which +opts into no lints at all; switching pedantic on over the move would have buried +a mechanical relocation under several hundred unrelated `#[must_use]` and +backtick edits. Turning it on is worth doing as its own change, over +`tinymemory-api` too, so the contract and the vocabulary stay lint-compatible.