zeph_core/agent/
memory_commands.rs1use std::fmt::Write as _;
10use std::future::Future;
11use std::pin::Pin;
12
13use tracing::Instrument as _;
14use zeph_commands::{CommandError, MemoryAccess};
15use zeph_memory::MessageId;
16
17use super::Agent;
18use crate::channel::Channel;
19
20impl<C: Channel + Send + 'static> MemoryAccess for Agent<C> {
21 fn memory_tiers<'a>(
24 &'a mut self,
25 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
26 Box::pin(
27 async move {
28 let Some(memory) = self.services.memory.persistence.memory.clone() else {
29 return Ok("Memory not configured.".to_owned());
30 };
31 match memory.sqlite().count_messages_by_tier().await {
32 Ok((episodic, semantic)) => {
33 let mut out = String::new();
34 let _ = writeln!(out, "Memory tiers:");
35 let _ = writeln!(out, " Working: (current context window — virtual)");
36 let _ = writeln!(out, " Episodic: {episodic} messages");
37 let _ = writeln!(out, " Semantic: {semantic} facts");
38 Ok(out.trim_end().to_owned())
39 }
40 Err(e) => Ok(format!("Failed to query tier stats: {e}")),
41 }
42 }
43 .instrument(tracing::info_span!("core.agent_access.memory_tiers")),
44 )
45 }
46
47 fn memory_promote<'a>(
48 &'a mut self,
49 ids_str: &'a str,
50 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
51 Box::pin(
52 async move {
53 let Some(memory) = self.services.memory.persistence.memory.clone() else {
54 return Ok("Memory not configured.".to_owned());
55 };
56 let ids: Vec<MessageId> = ids_str
57 .split_whitespace()
58 .filter_map(|s| s.parse::<i64>().ok().map(MessageId))
59 .collect();
60 if ids.is_empty() {
61 return Ok(
62 "Usage: /memory promote <id> [id...]\nExample: /memory promote 42 43 44"
63 .to_owned(),
64 );
65 }
66 match memory.sqlite().manual_promote(&ids).await {
67 Ok(count) => Ok(format!("Promoted {count} message(s) to semantic tier.")),
68 Err(e) => Ok(format!("Promotion failed: {e}")),
69 }
70 }
71 .instrument(tracing::info_span!("core.agent_access.memory_promote")),
72 )
73 }
74
75 fn store_command<'a>(
78 &'a mut self,
79 args: &'a str,
80 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
81 Box::pin(
82 async move {
83 const USAGE: &str = "Usage: /store {get <ns> <key> | put <ns> <key> <value...> \
84 | list <ns_prefix> [limit] | delete <ns> <key>}";
85
86 let store_config = self.services.memory.persistence.store_config.clone();
87 if !store_config.enabled {
88 return Ok(
89 "Cross-thread store is disabled ([memory.store].enabled = false)."
90 .to_owned(),
91 );
92 }
93 let Some(memory) = self.services.memory.persistence.memory.clone() else {
94 return Ok("Memory not configured.".to_owned());
95 };
96
97 let owner_key = self.services.session.owner_key.as_str();
98 let mut parts = args.split_whitespace();
99 let Some(sub) = parts.next() else {
100 return Ok(USAGE.to_owned());
101 };
102
103 let result = match sub {
104 "get" => {
105 let (Some(ns), Some(key)) = (parts.next(), parts.next()) else {
106 return Ok("Usage: /store get <namespace> <key>".to_owned());
107 };
108 match memory.sqlite().store_get(owner_key, ns, key).await {
109 Ok(Some(item)) => item.value,
110 Ok(None) => format!("No value found for {ns}/{key}."),
111 Err(e) => return Err(CommandError::new(e.to_string())),
112 }
113 }
114 "put" => {
115 let (Some(ns), Some(key)) = (parts.next(), parts.next()) else {
116 return Ok("Usage: /store put <namespace> <key> <value...>".to_owned());
117 };
118 let value = parts.collect::<Vec<_>>().join(" ");
119 if value.is_empty() {
120 return Ok("Usage: /store put <namespace> <key> <value...>".to_owned());
121 }
122 match memory
123 .sqlite()
124 .store_put(
125 owner_key,
126 ns,
127 key,
128 &value,
129 store_config.max_value_bytes,
130 None,
131 )
132 .await
133 {
134 Ok(item) => format!("Stored {ns}/{key} (version {}).", item.version),
135 Err(e) => return Err(CommandError::new(e.to_string())),
136 }
137 }
138 "list" => {
139 let prefix = parts.next().unwrap_or("");
140 let limit = parts
141 .next()
142 .and_then(|s| s.parse::<usize>().ok())
143 .unwrap_or(0);
144 match memory.sqlite().store_list(owner_key, prefix, limit).await {
145 Ok(items) if items.is_empty() => "No rows found.".to_owned(),
146 Ok(items) => items
147 .iter()
148 .map(|i| {
149 format!(
150 "{}/{} = {} (v{})",
151 i.namespace, i.key, i.value, i.version
152 )
153 })
154 .collect::<Vec<_>>()
155 .join("\n"),
156 Err(e) => return Err(CommandError::new(e.to_string())),
157 }
158 }
159 "delete" => {
160 let (Some(ns), Some(key)) = (parts.next(), parts.next()) else {
161 return Ok("Usage: /store delete <namespace> <key>".to_owned());
162 };
163 match memory.sqlite().store_delete(owner_key, ns, key).await {
164 Ok(true) => format!("Deleted {ns}/{key}."),
165 Ok(false) => format!("No value found for {ns}/{key}."),
166 Err(e) => return Err(CommandError::new(e.to_string())),
167 }
168 }
169 _ => USAGE.to_owned(),
170 };
171 Ok(result)
172 }
173 .instrument(tracing::info_span!("core.agent_access.store_command")),
174 )
175 }
176
177 fn guidelines<'a>(
180 &'a mut self,
181 ) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
182 Box::pin(
183 async move {
184 const MAX_DISPLAY_CHARS: usize = 4096;
185
186 let Some(memory) = &self.services.memory.persistence.memory else {
187 return Ok("No memory backend initialised.".to_owned());
188 };
189
190 let cid = self.services.memory.persistence.conversation_id;
191 let sqlite = memory.sqlite();
192
193 let (version, text) = sqlite
194 .load_compression_guidelines(cid)
195 .await
196 .map_err(|e: zeph_memory::MemoryError| CommandError::new(e.to_string()))?;
197
198 if version == 0 || text.is_empty() {
199 return Ok("No compression guidelines generated yet.".to_owned());
200 }
201
202 let (_, created_at) = sqlite
203 .load_compression_guidelines_meta(cid)
204 .await
205 .unwrap_or((0, String::new()));
206
207 let (body, truncated) = if text.len() > MAX_DISPLAY_CHARS {
208 let end = text.floor_char_boundary(MAX_DISPLAY_CHARS);
209 (&text[..end], true)
210 } else {
211 (text.as_str(), false)
212 };
213
214 let mut output =
215 format!("Compression Guidelines (v{version}, updated {created_at}):\n\n{body}");
216 if truncated {
217 output.push_str("\n\n[truncated]");
218 }
219 Ok(output)
220 }
221 .instrument(tracing::info_span!("core.agent_access.guidelines")),
222 )
223 }
224}