Skip to main content

klieo_core/
memory.rs

1//! Memory traits — short-term, long-term, episodic.
2
3use crate::error::MemoryError;
4use crate::ids::{FactId, RunId, ThreadId};
5use crate::llm::Message;
6use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10/// Outcome of a tool invocation as recorded in the episodic event stream.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "outcome", rename_all = "snake_case")]
13pub enum ToolResult {
14    /// Tool returned a successful JSON result.
15    Ok {
16        /// Result payload.
17        value: serde_json::Value,
18    },
19    /// Tool returned an error message.
20    Err {
21        /// Error string.
22        message: String,
23    },
24}
25
26/// Conversation buffer scoped to a single thread.
27///
28/// ```
29/// # tokio_test::block_on(async {
30/// use klieo_core::test_utils::InMemoryShortTerm;
31/// use klieo_core::{ShortTermMemory, Message, Role, ThreadId};
32///
33/// let m = InMemoryShortTerm::default();
34/// let thread = ThreadId::new("t1");
35/// m.append(thread.clone(), Message {
36///     role: Role::User, content: "hi".into(),
37///     tool_calls: vec![], tool_call_id: None,
38/// }).await.unwrap();
39/// let loaded = m.load(thread, 1024).await.unwrap();
40/// assert_eq!(loaded.len(), 1);
41/// # });
42/// ```
43#[async_trait]
44pub trait ShortTermMemory: Send + Sync {
45    /// Append a message to the thread's history.
46    async fn append(&self, thread: ThreadId, msg: Message) -> Result<(), MemoryError>;
47
48    /// Load up to `max_tokens` of the most-recent messages, oldest first.
49    /// Implementations approximate token counts (provider-specific).
50    async fn load(&self, thread: ThreadId, max_tokens: usize) -> Result<Vec<Message>, MemoryError>;
51
52    /// Drop all messages for `thread`.
53    async fn clear(&self, thread: ThreadId) -> Result<(), MemoryError>;
54}
55
56/// Namespacing for long-term memory facts.
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
58pub enum Scope {
59    /// Workspace-scoped (multi-agent shared).
60    Workspace(String),
61    /// Per-agent scoped.
62    Agent(String),
63    /// Process-global (use sparingly).
64    Global,
65}
66
67/// One stored fact in long-term memory.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct Fact {
70    /// Plain-text body, embedded for retrieval.
71    pub text: String,
72    /// Caller-supplied metadata, opaque to the store.
73    #[serde(default)]
74    pub metadata: serde_json::Value,
75}
76
77/// Long-term semantic memory.
78///
79/// ```
80/// # tokio_test::block_on(async {
81/// use klieo_core::test_utils::InMemoryLongTerm;
82/// use klieo_core::{Fact, LongTermMemory, Scope};
83///
84/// let m = InMemoryLongTerm::default();
85/// let scope = Scope::Workspace("ws".into());
86/// m.remember(scope.clone(), Fact {
87///     text: "the sky is blue".into(),
88///     metadata: serde_json::Value::Null,
89/// }).await.unwrap();
90/// let hits = m.recall(scope, "sky", 1).await.unwrap();
91/// assert_eq!(hits.len(), 1);
92/// # });
93/// ```
94#[async_trait]
95pub trait LongTermMemory: Send + Sync {
96    /// Store a fact under `scope`. Returns a stable id.
97    async fn remember(&self, scope: Scope, fact: Fact) -> Result<FactId, MemoryError>;
98
99    /// Top-`k` semantic recall under `scope` for the supplied query.
100    async fn recall(&self, scope: Scope, query: &str, k: usize) -> Result<Vec<Fact>, MemoryError>;
101
102    /// Remove a stored fact.
103    async fn forget(&self, id: FactId) -> Result<(), MemoryError>;
104}
105
106/// One event in the episodic event stream of a single agent run.
107///
108/// Marked `#[non_exhaustive]` so additive variants (e.g.
109/// [`Self::SummaryCheckpoint`]) can be introduced without forcing a
110/// SemVer-major bump. Match arms in downstream crates must include a
111/// fallback `_ => …`.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[non_exhaustive]
114pub enum Episode {
115    /// Run started.
116    Started {
117        /// Agent name.
118        agent: String,
119    },
120    /// LLM call completed.
121    LlmCall {
122        /// Total tokens used.
123        tokens: u32,
124        /// Wall-clock latency.
125        latency_ms: u32,
126    },
127    /// Tool call completed.
128    ToolCall {
129        /// Tool name.
130        name: String,
131        /// JSON arguments.
132        args: serde_json::Value,
133        /// Tool outcome.
134        result: ToolResult,
135    },
136    /// Agent published a bus message.
137    BusPublish {
138        /// Subject.
139        subject: String,
140    },
141    /// Agent received a bus message.
142    BusReceive {
143        /// Subject.
144        subject: String,
145    },
146    /// Run completed successfully.
147    Completed,
148    /// Run failed.
149    Failed {
150        /// Error message.
151        error: String,
152    },
153    /// Summarizer checkpoint completed.
154    ///
155    /// Emitted by [`crate::summarize::summarize_history`] in lieu of
156    /// [`Self::LlmCall`] so the audit trail can distinguish summarizer
157    /// overhead from substantive agent reasoning. Downstream
158    /// observability (e.g. `klieo-runlog`) typically projects this as
159    /// a separate step kind so cost / latency attribution stays
160    /// faithful.
161    SummaryCheckpoint {
162        /// Number of older messages folded into the summary call.
163        input_message_count: u32,
164        /// Length of the resulting summary, in Unicode scalar values.
165        summary_chars: u32,
166        /// Wall-clock latency of the summarizer call.
167        latency_ms: u32,
168        /// Total tokens reported by the summarizer LLM (prompt +
169        /// completion).
170        tokens: u32,
171    },
172    /// Operational-layer event (klieo-ops). Body is an opaque
173    /// `serde_json::Value` to keep klieo-core free of an ops dependency.
174    /// klieo-ops provides typed serde conversion helpers via `OpsEvent`.
175    Ops(serde_json::Value),
176}
177
178/// Filter passed to `EpisodicMemory::list_runs`.
179#[derive(Debug, Clone, Default)]
180pub struct RunFilter {
181    /// Filter by agent name (substring match).
182    pub agent: Option<String>,
183    /// Inclusive lower bound on `started_at`.
184    pub since: Option<DateTime<Utc>>,
185    /// Inclusive upper bound on `started_at`.
186    pub until: Option<DateTime<Utc>>,
187    /// Maximum rows returned.
188    pub limit: Option<usize>,
189}
190
191/// Index-row summary returned by `list_runs`.
192#[derive(Debug, Clone, Serialize, Deserialize)]
193pub struct RunSummary {
194    /// Run id.
195    pub run_id: RunId,
196    /// Agent name.
197    pub agent: String,
198    /// First-event timestamp.
199    pub started_at: DateTime<Utc>,
200    /// Last-event timestamp, if completed/failed.
201    pub finished_at: Option<DateTime<Utc>>,
202    /// Number of episodes.
203    pub episode_count: u32,
204}
205
206/// Append-only event log of agent runs.
207///
208/// ```
209/// # tokio_test::block_on(async {
210/// use klieo_core::test_utils::InMemoryEpisodic;
211/// use klieo_core::{Episode, EpisodicMemory, RunId};
212///
213/// let m = InMemoryEpisodic::default();
214/// let run = RunId::new();
215/// m.record(run, Episode::Started { agent: "a".into() }).await.unwrap();
216/// let events = m.replay(run).await.unwrap();
217/// assert_eq!(events.len(), 1);
218/// # });
219/// ```
220#[async_trait]
221pub trait EpisodicMemory: Send + Sync {
222    /// Record an episode for `run`.
223    async fn record(&self, run: RunId, event: Episode) -> Result<(), MemoryError>;
224
225    /// Replay all episodes for `run` in order.
226    async fn replay(&self, run: RunId) -> Result<Vec<Episode>, MemoryError>;
227
228    /// List run summaries matching `filter`.
229    async fn list_runs(&self, filter: RunFilter) -> Result<Vec<RunSummary>, MemoryError>;
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[allow(dead_code)]
237    fn _assert_dyn_short(_: &dyn ShortTermMemory) {}
238    #[allow(dead_code)]
239    fn _assert_dyn_long(_: &dyn LongTermMemory) {}
240    #[allow(dead_code)]
241    fn _assert_dyn_episodic(_: &dyn EpisodicMemory) {}
242}