cortex_mcp/tools/
memory_outcome.rs1use std::sync::{Arc, Mutex};
12
13use chrono::Utc;
14use cortex_core::{MemoryId, OutcomeMemoryRelation};
15use cortex_store::repo::{MemoryRepo, MemorySessionUse, OutcomeMemoryRelationRecord};
16use cortex_store::Pool;
17use serde_json::{json, Value};
18
19use crate::tool_handler::{GateId, ToolError, ToolHandler};
20
21const MEMORY_NOT_FOUND_INVARIANT: &str = "memory.outcome.memory_not_found";
23
24#[derive(Debug)]
36pub struct CortexMemoryOutcomeTool {
37 pool: Arc<Mutex<Pool>>,
38}
39
40impl CortexMemoryOutcomeTool {
41 #[must_use]
43 pub fn new(pool: Arc<Mutex<Pool>>) -> Self {
44 Self { pool }
45 }
46}
47
48impl ToolHandler for CortexMemoryOutcomeTool {
49 fn name(&self) -> &'static str {
50 "cortex_memory_outcome"
51 }
52
53 fn gate_set(&self) -> &'static [GateId] {
54 &[GateId::SessionWrite]
55 }
56
57 fn call(&self, params: Value) -> Result<Value, ToolError> {
58 let memory_id_str = params["memory_id"]
59 .as_str()
60 .filter(|s| !s.is_empty())
61 .ok_or_else(|| ToolError::InvalidParams("memory_id is required".into()))?;
62
63 let session_id = params["session_id"]
64 .as_str()
65 .filter(|s| !s.trim().is_empty())
66 .ok_or_else(|| ToolError::InvalidParams("session_id is required".into()))?;
67
68 let result_str = params["result"]
69 .as_str()
70 .ok_or_else(|| ToolError::InvalidParams("result is required".into()))?;
71
72 let note = params["note"].as_str().map(ToOwned::to_owned);
73
74 let memory_id: MemoryId = memory_id_str.parse().map_err(|err| {
75 ToolError::InvalidParams(format!("memory_id `{memory_id_str}` is invalid: {err}"))
76 })?;
77
78 let (relation, result_name) = match result_str {
79 "helpful" => (OutcomeMemoryRelation::Used, "helpful"),
80 "not-helpful" => (OutcomeMemoryRelation::Rejected, "not-helpful"),
81 other => {
82 return Err(ToolError::InvalidParams(format!(
83 "result must be `helpful` or `not-helpful`, got `{other}`"
84 )));
85 }
86 };
87
88 tracing::info!(
89 "cortex_memory_outcome via MCP: memory_id={} result={}",
90 memory_id,
91 result_name
92 );
93
94 let pool = self
95 .pool
96 .lock()
97 .map_err(|err| ToolError::Internal(format!("pool lock poisoned: {err}")))?;
98 let repo = MemoryRepo::new(&pool);
99
100 let memory = match repo.get_by_id(&memory_id) {
102 Ok(Some(m)) if m.status == "active" => m,
103 Ok(Some(_)) => {
104 return Err(ToolError::InvalidParams(format!(
105 "{MEMORY_NOT_FOUND_INVARIANT}: memory {memory_id} exists but is not active"
106 )));
107 }
108 Ok(None) => {
109 return Err(ToolError::InvalidParams(format!(
110 "{MEMORY_NOT_FOUND_INVARIANT}: memory {memory_id} not found"
111 )));
112 }
113 Err(err) => {
114 return Err(ToolError::Internal(format!(
115 "failed to look up memory {memory_id}: {err}"
116 )));
117 }
118 };
119
120 let now = Utc::now();
121
122 let session_use = MemorySessionUse {
123 memory_id: memory.id,
124 session_id: session_id.to_owned(),
125 first_used_at: now,
126 last_used_at: now,
127 use_count: 1,
128 };
129 repo.record_session_use(&session_use)
130 .map_err(|err| ToolError::Internal(format!("failed to record session use: {err}")))?;
131
132 let outcome_ref = format!("outcome:{session_id}:{memory_id}:{result_name}");
133
134 let outcome_ref_with_note = match note {
139 Some(ref n) if !n.is_empty() => format!("{outcome_ref}:note:{n}"),
140 _ => outcome_ref.clone(),
141 };
142
143 let relation_record = OutcomeMemoryRelationRecord {
144 outcome_ref: outcome_ref_with_note,
145 memory_id,
146 relation,
147 recorded_at: now,
148 source_event_id: None,
149 validation_scope: None,
150 validating_principal_id: None,
151 evidence_ref: None,
152 };
153
154 repo.record_outcome_relation(&relation_record, None)
155 .map_err(|err| {
156 ToolError::Internal(format!("failed to record outcome relation: {err}"))
157 })?;
158
159 Ok(json!({ "recorded": true }))
160 }
161}