1use crate::mcp::param_names;
28use crate::models::field_names;
29use crate::{models::Memory, storage as db, validate};
30use serde_json::{Value, json};
31
32#[must_use]
39#[allow(dead_code)]
40pub fn shared_namespace(from_agent_id: &str, to_agent_id: &str) -> String {
41 format!("_shared/{from_agent_id}\u{2192}{to_agent_id}/")
42}
43
44#[allow(dead_code)]
58pub fn handle_share(conn: &rusqlite::Connection, params: &Value) -> Result<Value, String> {
59 let source_memory_id = params[param_names::SOURCE_MEMORY_ID]
60 .as_str()
61 .ok_or("source_memory_id is required")?;
62 let target_agent_id = params[param_names::TARGET_AGENT_ID]
63 .as_str()
64 .ok_or("target_agent_id is required")?;
65
66 validate::validate_id(source_memory_id).map_err(|e| e.to_string())?;
67 validate::validate_agent_id(target_agent_id).map_err(|e| e.to_string())?;
68
69 let source = db::resolve_id(conn, source_memory_id)
70 .map_err(|e| e.to_string())?
71 .ok_or_else(|| format!("source memory {source_memory_id} not found"))?;
72
73 let from_agent_id = source
76 .metadata
77 .get(param_names::AGENT_ID)
78 .and_then(Value::as_str)
79 .unwrap_or("unknown")
80 .to_string();
81
82 let target_namespace = shared_namespace(&from_agent_id, target_agent_id);
83 let now = chrono::Utc::now().to_rfc3339();
84
85 let mut metadata = source.metadata.clone();
88 if let Some(obj) = metadata.as_object_mut() {
89 obj.insert("shared_from_memory_id".into(), json!(source.id.clone()));
90 obj.insert("shared_from_agent_id".into(), json!(from_agent_id.clone()));
91 obj.insert("shared_to_agent_id".into(), json!(target_agent_id));
92 obj.insert("shared_at".into(), json!(now.clone()));
93 obj.insert("agent_id".into(), json!(target_agent_id));
97 }
98
99 let shared_id = uuid::Uuid::new_v4().to_string();
100 let shared = Memory {
101 id: shared_id.clone(),
102 tier: source.tier,
103 namespace: target_namespace.clone(),
104 title: source.title.clone(),
105 content: source.content.clone(),
106 tags: source.tags.clone(),
107 priority: source.priority,
108 confidence: source.confidence,
109 source: "shared".to_string(),
110 access_count: 0,
111 created_at: now.clone(),
112 updated_at: now,
113 last_accessed_at: None,
114 expires_at: None,
115 metadata,
116 reflection_depth: source.reflection_depth,
117 memory_kind: source.memory_kind,
118 entity_id: source.entity_id.clone(),
119 persona_version: source.persona_version,
120 citations: source.citations.clone(),
121 source_uri: source.source_uri.clone(),
122 source_span: source.source_span.clone(),
123 confidence_source: source.confidence_source,
124 confidence_signals: source.confidence_signals.clone(),
125 confidence_decayed_at: source.confidence_decayed_at.clone(),
126 version: 1,
129 };
130
131 db::insert(conn, &shared).map_err(|e| e.to_string())?;
132
133 Ok(json!({
134 "shared_memory_id": shared_id,
135 (field_names::SOURCE_MEMORY_ID): source_memory_id,
136 (field_names::TARGET_NAMESPACE): target_namespace,
137 (field_names::TARGET_AGENT_ID): target_agent_id,
138 (field_names::FROM_AGENT_ID): from_agent_id,
139 }))
140}
141
142use crate::mcp::registry::McpTool;
145use schemars::JsonSchema;
146use serde::Deserialize;
147
148#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
150#[allow(dead_code)]
151pub struct ShareRequest {
152 pub source_memory_id: String,
154
155 pub target_agent_id: String,
157}
158
159#[allow(dead_code)]
161pub struct ShareTool;
162
163impl McpTool for ShareTool {
164 fn name() -> &'static str {
165 crate::mcp::registry::tool_names::MEMORY_SHARE
166 }
167 fn description() -> &'static str {
168 "Share a memory with another agent (copy into _shared/<from>→<to>/)."
169 }
170 fn docs() -> &'static str {
171 "#224/#311 MVP: point-to-point copy into `_shared/<from>→<to>/` with provenance."
172 }
173 fn input_schema() -> Value {
174 crate::mcp::registry::input_schema_for::<ShareRequest>()
175 }
176 fn family() -> &'static str {
177 crate::profile::Family::Power.name()
178 }
179}
180
181#[cfg(test)]
182mod d1_5_986_tests {
183 use super::*;
186 use crate::mcp::parity_test_helpers::{
187 assert_descriptions_match, assert_property_set_parity, derived_props_for,
188 };
189
190 #[test]
191 fn share_parity_986() {
192 let derived = derived_props_for::<ShareRequest>();
193 assert_property_set_parity("memory_share", &derived);
194 assert_descriptions_match("memory_share", &derived);
195 }
196
197 #[test]
198 fn share_tool_metadata_986() {
199 assert_eq!(ShareTool::name(), "memory_share");
200 assert_eq!(ShareTool::family(), "power");
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::models::{Memory, Tier};
208
209 fn fresh_conn() -> rusqlite::Connection {
210 db::open(std::path::Path::new(":memory:")).expect("open in-memory db")
211 }
212
213 fn make_mem(title: &str, namespace: &str, agent_id: &str) -> Memory {
214 let now = chrono::Utc::now().to_rfc3339();
215 Memory {
216 id: uuid::Uuid::new_v4().to_string(),
217 tier: Tier::Mid,
218 namespace: namespace.to_string(),
219 title: title.to_string(),
220 content: format!("content for {title}"),
221 tags: vec!["share-test".to_string()],
222 priority: 5,
223 confidence: 1.0,
224 source: "test".to_string(),
225 access_count: 0,
226 created_at: now.clone(),
227 updated_at: now,
228 last_accessed_at: None,
229 expires_at: None,
230 metadata: json!({"agent_id": agent_id}),
231 reflection_depth: 0,
232 memory_kind: crate::models::MemoryKind::Observation,
233 entity_id: None,
234 persona_version: None,
235 citations: Vec::new(),
236 source_uri: None,
237 source_span: None,
238 confidence_source: crate::models::ConfidenceSource::CallerProvided,
239 confidence_signals: None,
240 confidence_decayed_at: None,
241 version: 1,
242 }
243 }
244
245 #[test]
246 fn share_copies_memory_into_shared_namespace() {
247 let conn = fresh_conn();
248 let src = make_mem("source memo", "alice/notes", "ai:alice");
249 let src_id = db::insert(&conn, &src).expect("insert source");
250
251 let params = json!({
252 "source_memory_id": src_id.clone(),
253 "target_agent_id": "ai:bob",
254 });
255 let resp = handle_share(&conn, ¶ms).expect("share ok");
256
257 let new_id = resp["shared_memory_id"]
258 .as_str()
259 .expect("shared_memory_id present");
260 assert_ne!(new_id, src_id, "shared copy must have new id");
261 assert_eq!(resp["target_agent_id"], "ai:bob");
262 assert_eq!(resp["from_agent_id"], "ai:alice");
263 assert_eq!(resp["target_namespace"], "_shared/ai:alice\u{2192}ai:bob/");
264
265 let copy = db::resolve_id(&conn, new_id)
267 .expect("resolve")
268 .expect("shared copy present");
269 assert_eq!(copy.title, src.title);
270 assert_eq!(copy.content, src.content);
271 assert_eq!(copy.namespace, "_shared/ai:alice\u{2192}ai:bob/");
272 assert_eq!(copy.source, "shared");
273 assert_eq!(
274 copy.metadata["shared_from_memory_id"].as_str(),
275 Some(src_id.as_str())
276 );
277 assert_eq!(
278 copy.metadata["shared_from_agent_id"].as_str(),
279 Some("ai:alice")
280 );
281 assert_eq!(copy.metadata["shared_to_agent_id"].as_str(), Some("ai:bob"));
282 assert_eq!(copy.metadata["agent_id"].as_str(), Some("ai:bob"));
283 }
284
285 #[test]
286 fn share_rejects_missing_source() {
287 let conn = fresh_conn();
288 let nonexistent = uuid::Uuid::new_v4().to_string();
289 let params = json!({
290 "source_memory_id": nonexistent,
291 "target_agent_id": "ai:bob",
292 });
293 let err = handle_share(&conn, ¶ms).expect_err("must fail");
294 assert!(err.contains("not found"), "got: {err}");
295 }
296
297 #[test]
298 fn share_rejects_missing_params() {
299 let conn = fresh_conn();
300 let r1 = handle_share(&conn, &json!({"target_agent_id": "ai:bob"}));
301 assert!(r1.is_err());
302 let r2 = handle_share(
303 &conn,
304 &json!({"source_memory_id": uuid::Uuid::new_v4().to_string()}),
305 );
306 assert!(r2.is_err());
307 }
308}