use crate::mcp::param_names;
use crate::models::field_names;
use crate::{models::Memory, storage as db, validate};
use serde_json::{Value, json};
#[must_use]
#[allow(dead_code)]
pub fn shared_namespace(from_agent_id: &str, to_agent_id: &str) -> String {
format!("_shared/{from_agent_id}\u{2192}{to_agent_id}/")
}
#[allow(dead_code)]
pub fn handle_share(conn: &rusqlite::Connection, params: &Value) -> Result<Value, String> {
let source_memory_id = params[param_names::SOURCE_MEMORY_ID]
.as_str()
.ok_or("source_memory_id is required")?;
let target_agent_id = params[param_names::TARGET_AGENT_ID]
.as_str()
.ok_or("target_agent_id is required")?;
validate::validate_id(source_memory_id).map_err(|e| e.to_string())?;
validate::validate_agent_id(target_agent_id).map_err(|e| e.to_string())?;
let source = db::resolve_id(conn, source_memory_id)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("source memory {source_memory_id} not found"))?;
let from_agent_id = source
.metadata
.get(param_names::AGENT_ID)
.and_then(Value::as_str)
.unwrap_or("unknown")
.to_string();
let target_namespace = shared_namespace(&from_agent_id, target_agent_id);
let now = chrono::Utc::now().to_rfc3339();
let mut metadata = source.metadata.clone();
if let Some(obj) = metadata.as_object_mut() {
obj.insert("shared_from_memory_id".into(), json!(source.id.clone()));
obj.insert("shared_from_agent_id".into(), json!(from_agent_id.clone()));
obj.insert("shared_to_agent_id".into(), json!(target_agent_id));
obj.insert("shared_at".into(), json!(now.clone()));
obj.insert("agent_id".into(), json!(target_agent_id));
}
let shared_id = uuid::Uuid::new_v4().to_string();
let shared = Memory {
id: shared_id.clone(),
tier: source.tier,
namespace: target_namespace.clone(),
title: source.title.clone(),
content: source.content.clone(),
tags: source.tags.clone(),
priority: source.priority,
confidence: source.confidence,
source: "shared".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata,
reflection_depth: source.reflection_depth,
memory_kind: source.memory_kind,
entity_id: source.entity_id.clone(),
persona_version: source.persona_version,
citations: source.citations.clone(),
source_uri: source.source_uri.clone(),
source_span: source.source_span.clone(),
confidence_source: source.confidence_source,
confidence_signals: source.confidence_signals.clone(),
confidence_decayed_at: source.confidence_decayed_at.clone(),
version: 1,
};
db::insert(conn, &shared).map_err(|e| e.to_string())?;
Ok(json!({
"shared_memory_id": shared_id,
(field_names::SOURCE_MEMORY_ID): source_memory_id,
(field_names::TARGET_NAMESPACE): target_namespace,
(field_names::TARGET_AGENT_ID): target_agent_id,
(field_names::FROM_AGENT_ID): from_agent_id,
}))
}
use crate::mcp::registry::McpTool;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
#[allow(dead_code)]
pub struct ShareRequest {
pub source_memory_id: String,
pub target_agent_id: String,
}
#[allow(dead_code)]
pub struct ShareTool;
impl McpTool for ShareTool {
fn name() -> &'static str {
crate::mcp::registry::tool_names::MEMORY_SHARE
}
fn description() -> &'static str {
"Share a memory with another agent (copy into _shared/<from>→<to>/)."
}
fn docs() -> &'static str {
"#224/#311 MVP: point-to-point copy into `_shared/<from>→<to>/` with provenance."
}
fn input_schema() -> Value {
crate::mcp::registry::input_schema_for::<ShareRequest>()
}
fn family() -> &'static str {
crate::profile::Family::Power.name()
}
}
#[cfg(test)]
mod d1_5_986_tests {
use super::*;
use crate::mcp::parity_test_helpers::{
assert_descriptions_match, assert_property_set_parity, derived_props_for,
};
#[test]
fn share_parity_986() {
let derived = derived_props_for::<ShareRequest>();
assert_property_set_parity("memory_share", &derived);
assert_descriptions_match("memory_share", &derived);
}
#[test]
fn share_tool_metadata_986() {
assert_eq!(ShareTool::name(), "memory_share");
assert_eq!(ShareTool::family(), "power");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Memory, Tier};
fn fresh_conn() -> rusqlite::Connection {
db::open(std::path::Path::new(":memory:")).expect("open in-memory db")
}
fn make_mem(title: &str, namespace: &str, agent_id: &str) -> Memory {
let now = chrono::Utc::now().to_rfc3339();
Memory {
id: uuid::Uuid::new_v4().to_string(),
tier: Tier::Mid,
namespace: namespace.to_string(),
title: title.to_string(),
content: format!("content for {title}"),
tags: vec!["share-test".to_string()],
priority: 5,
confidence: 1.0,
source: "test".to_string(),
access_count: 0,
created_at: now.clone(),
updated_at: now,
last_accessed_at: None,
expires_at: None,
metadata: json!({"agent_id": agent_id}),
reflection_depth: 0,
memory_kind: crate::models::MemoryKind::Observation,
entity_id: None,
persona_version: None,
citations: Vec::new(),
source_uri: None,
source_span: None,
confidence_source: crate::models::ConfidenceSource::CallerProvided,
confidence_signals: None,
confidence_decayed_at: None,
version: 1,
}
}
#[test]
fn share_copies_memory_into_shared_namespace() {
let conn = fresh_conn();
let src = make_mem("source memo", "alice/notes", "ai:alice");
let src_id = db::insert(&conn, &src).expect("insert source");
let params = json!({
"source_memory_id": src_id.clone(),
"target_agent_id": "ai:bob",
});
let resp = handle_share(&conn, ¶ms).expect("share ok");
let new_id = resp["shared_memory_id"]
.as_str()
.expect("shared_memory_id present");
assert_ne!(new_id, src_id, "shared copy must have new id");
assert_eq!(resp["target_agent_id"], "ai:bob");
assert_eq!(resp["from_agent_id"], "ai:alice");
assert_eq!(resp["target_namespace"], "_shared/ai:alice\u{2192}ai:bob/");
let copy = db::resolve_id(&conn, new_id)
.expect("resolve")
.expect("shared copy present");
assert_eq!(copy.title, src.title);
assert_eq!(copy.content, src.content);
assert_eq!(copy.namespace, "_shared/ai:alice\u{2192}ai:bob/");
assert_eq!(copy.source, "shared");
assert_eq!(
copy.metadata["shared_from_memory_id"].as_str(),
Some(src_id.as_str())
);
assert_eq!(
copy.metadata["shared_from_agent_id"].as_str(),
Some("ai:alice")
);
assert_eq!(copy.metadata["shared_to_agent_id"].as_str(), Some("ai:bob"));
assert_eq!(copy.metadata["agent_id"].as_str(), Some("ai:bob"));
}
#[test]
fn share_rejects_missing_source() {
let conn = fresh_conn();
let nonexistent = uuid::Uuid::new_v4().to_string();
let params = json!({
"source_memory_id": nonexistent,
"target_agent_id": "ai:bob",
});
let err = handle_share(&conn, ¶ms).expect_err("must fail");
assert!(err.contains("not found"), "got: {err}");
}
#[test]
fn share_rejects_missing_params() {
let conn = fresh_conn();
let r1 = handle_share(&conn, &json!({"target_agent_id": "ai:bob"}));
assert!(r1.is_err());
let r2 = handle_share(
&conn,
&json!({"source_memory_id": uuid::Uuid::new_v4().to_string()}),
);
assert!(r2.is_err());
}
}