use std::sync::{Arc, Mutex};
use chrono::Utc;
use cortex_core::{MemoryId, OutcomeMemoryRelation};
use cortex_store::repo::{MemoryRepo, MemorySessionUse, OutcomeMemoryRelationRecord};
use cortex_store::Pool;
use serde_json::{json, Value};
use crate::tool_handler::{GateId, ToolError, ToolHandler};
const MEMORY_NOT_FOUND_INVARIANT: &str = "memory.outcome.memory_not_found";
#[derive(Debug)]
pub struct CortexMemoryOutcomeTool {
pool: Arc<Mutex<Pool>>,
}
impl CortexMemoryOutcomeTool {
#[must_use]
pub fn new(pool: Arc<Mutex<Pool>>) -> Self {
Self { pool }
}
}
impl ToolHandler for CortexMemoryOutcomeTool {
fn name(&self) -> &'static str {
"cortex_memory_outcome"
}
fn gate_set(&self) -> &'static [GateId] {
&[GateId::SessionWrite]
}
fn call(&self, params: Value) -> Result<Value, ToolError> {
let memory_id_str = params["memory_id"]
.as_str()
.filter(|s| !s.is_empty())
.ok_or_else(|| ToolError::InvalidParams("memory_id is required".into()))?;
let session_id = params["session_id"]
.as_str()
.filter(|s| !s.trim().is_empty())
.ok_or_else(|| ToolError::InvalidParams("session_id is required".into()))?;
let result_str = params["result"]
.as_str()
.ok_or_else(|| ToolError::InvalidParams("result is required".into()))?;
let note = params["note"].as_str().map(ToOwned::to_owned);
let memory_id: MemoryId = memory_id_str.parse().map_err(|err| {
ToolError::InvalidParams(format!("memory_id `{memory_id_str}` is invalid: {err}"))
})?;
let (relation, result_name) = match result_str {
"helpful" => (OutcomeMemoryRelation::Used, "helpful"),
"not-helpful" => (OutcomeMemoryRelation::Rejected, "not-helpful"),
other => {
return Err(ToolError::InvalidParams(format!(
"result must be `helpful` or `not-helpful`, got `{other}`"
)));
}
};
tracing::info!(
"cortex_memory_outcome via MCP: memory_id={} result={}",
memory_id,
result_name
);
let pool = self
.pool
.lock()
.map_err(|err| ToolError::Internal(format!("pool lock poisoned: {err}")))?;
let repo = MemoryRepo::new(&pool);
let memory = match repo.get_by_id(&memory_id) {
Ok(Some(m)) if m.status == "active" => m,
Ok(Some(_)) => {
return Err(ToolError::InvalidParams(format!(
"{MEMORY_NOT_FOUND_INVARIANT}: memory {memory_id} exists but is not active"
)));
}
Ok(None) => {
return Err(ToolError::InvalidParams(format!(
"{MEMORY_NOT_FOUND_INVARIANT}: memory {memory_id} not found"
)));
}
Err(err) => {
return Err(ToolError::Internal(format!(
"failed to look up memory {memory_id}: {err}"
)));
}
};
let now = Utc::now();
let session_use = MemorySessionUse {
memory_id: memory.id,
session_id: session_id.to_owned(),
first_used_at: now,
last_used_at: now,
use_count: 1,
};
repo.record_session_use(&session_use)
.map_err(|err| ToolError::Internal(format!("failed to record session use: {err}")))?;
let outcome_ref = format!("outcome:{session_id}:{memory_id}:{result_name}");
let outcome_ref_with_note = match note {
Some(ref n) if !n.is_empty() => format!("{outcome_ref}:note:{n}"),
_ => outcome_ref.clone(),
};
let relation_record = OutcomeMemoryRelationRecord {
outcome_ref: outcome_ref_with_note,
memory_id,
relation,
recorded_at: now,
source_event_id: None,
validation_scope: None,
validating_principal_id: None,
evidence_ref: None,
};
repo.record_outcome_relation(&relation_record, None)
.map_err(|err| {
ToolError::Internal(format!("failed to record outcome relation: {err}"))
})?;
Ok(json!({ "recorded": true }))
}
}