1use std::sync::Arc;
2
3use axum::Json;
4use axum::extract::{Path, Query, State};
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use serde::Deserialize;
8use uuid::Uuid;
9
10use mnemo_core::error::Error as CoreError;
11use mnemo_core::hash::compute_content_hash;
12use mnemo_core::model::acl::Permission;
13use mnemo_core::model::delegation::{Delegation, DelegationScope};
14use mnemo_core::model::event::{AgentEvent, EventType};
15use mnemo_core::model::memory::{MemoryType, Scope};
16use mnemo_core::query::MnemoEngine;
17use mnemo_core::query::branch::{BranchRequest, BranchResponse};
18use mnemo_core::query::checkpoint::{CheckpointRequest, CheckpointResponse};
19use mnemo_core::query::forget::{
20 ForgetRequest, ForgetResponse, ForgetStrategy, ForgetSubjectRequest, ForgetSubjectResponse,
21};
22use mnemo_core::query::merge::{MergeRequest, MergeResponse};
23use mnemo_core::query::recall::{RecallRequest, RecallResponse};
24use mnemo_core::query::remember::{RememberRequest, RememberResponse};
25use mnemo_core::query::replay::{ReplayRequest, ReplayResponse};
26use mnemo_core::query::share::{ShareRequest, ShareResponse};
27
28type AppState = Arc<MnemoEngine>;
29
30pub struct AppError(CoreError);
35
36impl IntoResponse for AppError {
37 fn into_response(self) -> Response {
38 let (status, msg) = match &self.0 {
39 CoreError::Validation(m) => (StatusCode::BAD_REQUEST, m.clone()),
40 CoreError::PermissionDenied(m) => (StatusCode::FORBIDDEN, m.clone()),
41 CoreError::NotFound(m) => (StatusCode::NOT_FOUND, m.clone()),
42 other => {
43 tracing::error!("internal error: {other}");
44 (
45 StatusCode::INTERNAL_SERVER_ERROR,
46 "internal server error".to_string(),
47 )
48 }
49 };
50 (status, Json(serde_json::json!({"error": msg}))).into_response()
51 }
52}
53
54impl From<CoreError> for AppError {
55 fn from(e: CoreError) -> Self {
56 AppError(e)
57 }
58}
59
60#[derive(Debug, Deserialize)]
65pub struct RecallParams {
66 pub query: String,
67 pub agent_id: Option<String>,
68 pub limit: Option<usize>,
69 pub memory_type: Option<String>,
70 pub scope: Option<String>,
71 pub min_importance: Option<f32>,
72 pub tags: Option<String>,
73 pub org_id: Option<String>,
74 pub strategy: Option<String>,
75 pub as_of: Option<String>,
76 pub memory_types: Option<String>,
77 pub hybrid_weights: Option<String>,
78 pub rrf_k: Option<f32>,
79 pub explain: Option<bool>,
80}
81
82#[derive(Debug, Deserialize)]
83pub struct ForgetParams {
84 pub strategy: Option<String>,
85 pub agent_id: Option<String>,
86}
87
88#[derive(Debug, Deserialize)]
89pub struct ShareBody {
90 pub target_agent_id: String,
91 pub target_agent_ids: Option<Vec<String>>,
92 pub permission: Option<String>,
93 pub expires_in_hours: Option<f64>,
94 pub agent_id: Option<String>,
95}
96
97#[derive(Debug, Deserialize)]
98pub struct VerifyBody {
99 pub agent_id: Option<String>,
100 pub thread_id: Option<String>,
101}
102
103#[derive(Debug, Deserialize)]
104pub struct DelegateRequest {
105 pub delegate_id: String,
106 pub permission: String,
107 pub memory_ids: Option<Vec<String>>,
108 pub tags: Option<Vec<String>>,
109 pub max_depth: Option<u32>,
110 pub expires_in_hours: Option<f64>,
111 pub agent_id: Option<String>,
114}
115
116pub async fn remember_handler(
122 State(engine): State<AppState>,
123 Json(request): Json<RememberRequest>,
124) -> Result<Json<RememberResponse>, AppError> {
125 let response = engine.remember(request).await?;
126 Ok(Json(response))
127}
128
129pub async fn recall_handler(
131 State(engine): State<AppState>,
132 Query(params): Query<RecallParams>,
133) -> Result<Json<RecallResponse>, AppError> {
134 let memory_type = match params.memory_type.as_deref() {
135 Some(s) => Some(s.parse::<MemoryType>().map_err(|_| {
136 AppError(CoreError::Validation(format!(
137 "invalid memory_type '{}': expected one of: episodic, semantic, procedural, working",
138 s
139 )))
140 })?),
141 None => None,
142 };
143
144 let scope = match params.scope.as_deref() {
145 Some(s) => Some(s.parse::<Scope>().map_err(|_| {
146 AppError(CoreError::Validation(format!(
147 "invalid scope '{}': expected one of: private, shared, public, global",
148 s
149 )))
150 })?),
151 None => None,
152 };
153
154 let tags = params.tags.as_deref().map(|t| {
155 t.split(',')
156 .map(|s| s.trim().to_string())
157 .collect::<Vec<_>>()
158 });
159
160 let memory_types = match params.memory_types.as_deref() {
161 Some(s) => {
162 let mut parsed = Vec::new();
163 for t in s.split(',') {
164 let trimmed = t.trim();
165 let mt = trimmed.parse::<MemoryType>().map_err(|_| {
166 AppError(CoreError::Validation(format!(
167 "invalid memory_type '{}' in memory_types: expected one of: episodic, semantic, procedural, working",
168 trimmed
169 )))
170 })?;
171 parsed.push(mt);
172 }
173 Some(parsed)
174 }
175 None => None,
176 };
177
178 let hybrid_weights = match params.hybrid_weights.as_deref() {
179 Some(s) => {
180 let mut weights = Vec::new();
181 for w in s.split(',') {
182 let trimmed = w.trim();
183 let val = trimmed.parse::<f32>().map_err(|_| {
184 AppError(CoreError::Validation(format!(
185 "invalid weight '{}' in hybrid_weights: expected a floating-point number",
186 trimmed
187 )))
188 })?;
189 weights.push(val);
190 }
191 Some(weights)
192 }
193 None => None,
194 };
195
196 let request = RecallRequest {
197 query: params.query,
198 agent_id: params.agent_id,
199 limit: params.limit,
200 memory_type,
201 memory_types,
202 scope,
203 min_importance: params.min_importance,
204 tags,
205 org_id: params.org_id,
206 strategy: params.strategy,
207 temporal_range: None,
208 recency_half_life_hours: None,
209 hybrid_weights,
210 rrf_k: params.rrf_k,
211 as_of: params.as_of,
212 explain: params.explain,
213 with_provenance: None,
214 mode: None,
215 };
216
217 let response = engine.recall(request).await?;
218 Ok(Json(response))
219}
220
221pub async fn get_memory_handler(
223 State(engine): State<AppState>,
224 Path(id): Path<Uuid>,
225) -> Result<Json<serde_json::Value>, AppError> {
226 let record = engine
227 .storage
228 .get_memory(id)
229 .await?
230 .ok_or_else(|| CoreError::NotFound(format!("memory {id} not found")))?;
231
232 let value = serde_json::json!({
233 "id": record.id,
234 "agent_id": record.agent_id,
235 "content": record.content,
236 "memory_type": record.memory_type,
237 "scope": record.scope,
238 "importance": record.importance,
239 "tags": record.tags,
240 "metadata": record.metadata,
241 "source_type": record.source_type,
242 "source_id": record.source_id,
243 "consolidation_state": record.consolidation_state,
244 "access_count": record.access_count,
245 "org_id": record.org_id,
246 "thread_id": record.thread_id,
247 "created_at": record.created_at,
248 "updated_at": record.updated_at,
249 "last_accessed_at": record.last_accessed_at,
250 "expires_at": record.expires_at,
251 "deleted_at": record.deleted_at,
252 "decay_rate": record.decay_rate,
253 "created_by": record.created_by,
254 "version": record.version,
255 "prev_version_id": record.prev_version_id,
256 "quarantined": record.quarantined,
257 "quarantine_reason": record.quarantine_reason,
258 });
259
260 Ok(Json(value))
261}
262
263pub async fn forget_handler(
265 State(engine): State<AppState>,
266 Path(id): Path<Uuid>,
267 Query(params): Query<ForgetParams>,
268) -> Result<Json<ForgetResponse>, AppError> {
269 let strategy = match params.strategy.as_deref() {
270 Some(s) => Some(match s {
271 "soft_delete" => ForgetStrategy::SoftDelete,
272 "hard_delete" => ForgetStrategy::HardDelete,
273 "decay" => ForgetStrategy::Decay,
274 "consolidate" => ForgetStrategy::Consolidate,
275 "archive" => ForgetStrategy::Archive,
276 "redact" => ForgetStrategy::Redact,
277 other => {
278 return Err(AppError(CoreError::Validation(format!(
279 "invalid forget strategy '{}': expected one of: soft_delete, hard_delete, decay, consolidate, archive, redact",
280 other
281 ))));
282 }
283 }),
284 None => None,
285 };
286
287 let request = ForgetRequest {
288 memory_ids: vec![id],
289 agent_id: params.agent_id,
290 strategy,
291 criteria: None,
292 };
293
294 let response = engine.forget(request).await?;
295 Ok(Json(response))
296}
297
298#[derive(Debug, Deserialize)]
299pub struct ForgetSubjectBody {
300 pub subject_id: String,
301 pub strategy: Option<String>,
302 pub agent_id: Option<String>,
303}
304
305pub async fn forget_subject_handler(
307 State(engine): State<AppState>,
308 Json(body): Json<ForgetSubjectBody>,
309) -> Result<Json<ForgetSubjectResponse>, AppError> {
310 let strategy = match body.strategy.as_deref().unwrap_or("redact") {
311 "redact" => ForgetStrategy::Redact,
312 "hard_delete" => ForgetStrategy::HardDelete,
313 "soft_delete" => ForgetStrategy::SoftDelete,
314 other => {
315 return Err(AppError(CoreError::Validation(format!(
316 "invalid forget_subject strategy '{}': expected one of: redact, hard_delete, soft_delete",
317 other
318 ))));
319 }
320 };
321
322 let request = ForgetSubjectRequest {
323 subject_id: body.subject_id,
324 agent_id: body.agent_id,
325 strategy,
326 };
327
328 let response = engine.forget_subject(request).await?;
329 Ok(Json(response))
330}
331
332pub async fn share_handler(
334 State(engine): State<AppState>,
335 Path(id): Path<Uuid>,
336 Json(body): Json<ShareBody>,
337) -> Result<Json<ShareResponse>, AppError> {
338 let permission = match body.permission.as_deref() {
339 Some(s) => Some(s.parse::<Permission>().map_err(|_| {
340 AppError(CoreError::Validation(format!(
341 "invalid permission '{}': expected one of: read, write, delete, share, delegate, admin",
342 s
343 )))
344 })?),
345 None => None,
346 };
347
348 let request = ShareRequest {
349 memory_id: id,
350 agent_id: body.agent_id,
351 target_agent_id: body.target_agent_id,
352 target_agent_ids: body.target_agent_ids,
353 permission,
354 expires_in_hours: body.expires_in_hours,
355 };
356
357 let response = engine.share(request).await?;
358 Ok(Json(response))
359}
360
361pub async fn checkpoint_handler(
363 State(engine): State<AppState>,
364 Json(request): Json<CheckpointRequest>,
365) -> Result<Json<CheckpointResponse>, AppError> {
366 let response = engine.checkpoint(request).await?;
367 Ok(Json(response))
368}
369
370pub async fn branch_handler(
372 State(engine): State<AppState>,
373 Json(request): Json<BranchRequest>,
374) -> Result<Json<BranchResponse>, AppError> {
375 let response = engine.branch(request).await?;
376 Ok(Json(response))
377}
378
379pub async fn merge_handler(
381 State(engine): State<AppState>,
382 Json(request): Json<MergeRequest>,
383) -> Result<Json<MergeResponse>, AppError> {
384 let response = engine.merge(request).await?;
385 Ok(Json(response))
386}
387
388pub async fn replay_handler(
390 State(engine): State<AppState>,
391 Json(request): Json<ReplayRequest>,
392) -> Result<Json<ReplayResponse>, AppError> {
393 let response = engine.replay(request).await?;
394 Ok(Json(response))
395}
396
397pub async fn verify_handler(
399 State(engine): State<AppState>,
400 Json(body): Json<VerifyBody>,
401) -> Result<Json<serde_json::Value>, AppError> {
402 let result = engine
403 .verify_integrity(body.agent_id, body.thread_id.as_deref())
404 .await?;
405
406 let response = serde_json::json!({
407 "valid": result.valid,
408 "total_records": result.total_records,
409 "verified_records": result.verified_records,
410 "first_broken_at": result.first_broken_at.map(|id| id.to_string()),
411 "error_message": result.error_message,
412 "status": if result.valid { "verified" } else { "integrity_violation" },
413 });
414
415 Ok(Json(response))
416}
417
418pub async fn delegate_handler(
424 State(engine): State<AppState>,
425 Json(body): Json<DelegateRequest>,
426) -> Result<Json<serde_json::Value>, AppError> {
427 let permission: Permission = body
428 .permission
429 .parse()
430 .map_err(|e: CoreError| AppError(e))?;
431
432 let caller_agent_id = body
433 .agent_id
434 .unwrap_or_else(|| engine.default_agent_id.clone());
435
436 let scope = if let Some(ref ids) = body.memory_ids {
437 let parsed: std::result::Result<Vec<Uuid>, _> =
438 ids.iter().map(|s| Uuid::parse_str(s)).collect();
439 match parsed {
440 Ok(uuids) => {
441 for mid in &uuids {
443 let has_perm = engine
444 .storage
445 .check_permission(*mid, &caller_agent_id, Permission::Delegate)
446 .await?;
447 if !has_perm {
448 return Err(AppError(CoreError::PermissionDenied(format!(
449 "agent '{}' lacks delegate permission on memory {}",
450 caller_agent_id, mid
451 ))));
452 }
453 }
454 DelegationScope::ByMemoryId(uuids)
455 }
456 Err(e) => {
457 return Err(AppError(CoreError::Validation(format!(
458 "invalid UUID in memory_ids: {e}"
459 ))));
460 }
461 }
462 } else if let Some(ref tags) = body.tags {
463 DelegationScope::ByTag(tags.clone())
464 } else {
465 DelegationScope::AllMemories
466 };
467
468 let now = chrono::Utc::now();
469 let expires_at = body
470 .expires_in_hours
471 .map(|h| (now + chrono::Duration::seconds((h * 3600.0) as i64)).to_rfc3339());
472
473 let delegation = Delegation {
474 id: Uuid::now_v7(),
475 delegator_id: caller_agent_id,
476 delegate_id: body.delegate_id.clone(),
477 permission,
478 scope,
479 max_depth: body.max_depth.unwrap_or(0),
480 current_depth: 0,
481 parent_delegation_id: None,
482 created_at: now.to_rfc3339(),
483 expires_at,
484 revoked_at: None,
485 };
486
487 engine.storage.insert_delegation(&delegation).await?;
488
489 let response = serde_json::json!({
490 "delegation_id": delegation.id.to_string(),
491 "delegator": delegation.delegator_id,
492 "delegate": delegation.delegate_id,
493 "permission": delegation.permission.to_string(),
494 "status": "delegated",
495 });
496
497 Ok(Json(response))
498}
499
500pub async fn health_handler() -> Json<serde_json::Value> {
502 Json(serde_json::json!({"status": "ok"}))
503}
504
505struct GenAiFields {
510 event_type: EventType,
511 model: Option<String>,
512 tokens_input: Option<i64>,
513 tokens_output: Option<i64>,
514 cost_usd: Option<f64>,
515}
516
517fn extract_genai_fields(span: &serde_json::Value) -> GenAiFields {
520 let attributes = span.get("attributes").and_then(|v| v.as_array());
521
522 let mut model = None;
523 let mut tokens_input = None;
524 let mut tokens_output = None;
525 let mut cost_usd = None;
526 let mut operation_name = None;
527
528 if let Some(attrs) = attributes {
529 for attr in attrs {
530 let key = match attr.get("key").and_then(|k| k.as_str()) {
531 Some(k) => k,
532 None => continue,
533 };
534 let value = attr.get("value");
535
536 match key {
537 "gen_ai.request.model" => {
538 model = value
539 .and_then(|v| v.get("stringValue"))
540 .and_then(|v| v.as_str())
541 .map(|s| s.to_string());
542 }
543 "gen_ai.usage.input_tokens" => {
544 tokens_input = value.and_then(|v| v.get("intValue")).and_then(|v| {
545 v.as_str()
546 .and_then(|s| s.parse::<i64>().ok())
547 .or_else(|| v.as_i64())
548 });
549 }
550 "gen_ai.usage.output_tokens" => {
551 tokens_output = value.and_then(|v| v.get("intValue")).and_then(|v| {
552 v.as_str()
553 .and_then(|s| s.parse::<i64>().ok())
554 .or_else(|| v.as_i64())
555 });
556 }
557 "gen_ai.usage.cost" => {
558 cost_usd = value
559 .and_then(|v| v.get("doubleValue"))
560 .and_then(|v| v.as_f64());
561 }
562 "gen_ai.operation.name" => {
563 operation_name = value
564 .and_then(|v| v.get("stringValue"))
565 .and_then(|v| v.as_str())
566 .map(|s| s.to_string());
567 }
568 _ => {}
569 }
570 }
571 }
572
573 let op = operation_name.or_else(|| {
575 span.get("name")
576 .and_then(|v| v.as_str())
577 .map(|s| s.to_string())
578 });
579
580 let event_type = match op.as_deref() {
582 Some(s) if s.contains("chat") => EventType::AssistantMessage,
583 Some(s) if s.contains("embed") => EventType::RetrievalQuery,
584 Some(s) if s.contains("tool") => EventType::ToolCall,
585 _ => EventType::ToolCall, };
587
588 GenAiFields {
589 event_type,
590 model,
591 tokens_input,
592 tokens_output,
593 cost_usd,
594 }
595}
596
597pub async fn otlp_ingest_handler(
599 State(engine): State<AppState>,
600 Json(body): Json<serde_json::Value>,
601) -> Result<Json<serde_json::Value>, AppError> {
602 let resource_spans = body
603 .get("resourceSpans")
604 .and_then(|v| v.as_array())
605 .cloned()
606 .unwrap_or_default();
607
608 let mut count: u64 = 0;
609
610 for rs in &resource_spans {
611 let resource_agent_id = rs
613 .get("resource")
614 .and_then(|r| r.get("attributes"))
615 .and_then(|attrs| attrs.as_array())
616 .and_then(|attrs| {
617 attrs.iter().find_map(|attr| {
618 let key = attr.get("key")?.as_str()?;
619 if key == "agent.id" || key == "service.name" {
620 attr.get("value")
621 .and_then(|v| v.get("stringValue"))
622 .and_then(|v| v.as_str())
623 .map(|s| s.to_string())
624 } else {
625 None
626 }
627 })
628 });
629
630 let scope_spans = rs
631 .get("scopeSpans")
632 .and_then(|v| v.as_array())
633 .cloned()
634 .unwrap_or_default();
635
636 for ss in &scope_spans {
637 let spans = ss
638 .get("spans")
639 .and_then(|v| v.as_array())
640 .cloned()
641 .unwrap_or_default();
642
643 for span in &spans {
644 let trace_id = span
645 .get("traceId")
646 .and_then(|v| v.as_str())
647 .map(|s| s.to_string());
648
649 let span_id = span
650 .get("spanId")
651 .and_then(|v| v.as_str())
652 .map(|s| s.to_string());
653
654 let agent_id = resource_agent_id
655 .clone()
656 .unwrap_or_else(|| engine.default_agent_id.clone());
657
658 let start_nano: u64 = span
661 .get("startTimeUnixNano")
662 .and_then(|v| {
663 v.as_str()
664 .and_then(|s| s.parse::<u64>().ok())
665 .or_else(|| v.as_u64())
666 })
667 .unwrap_or(0);
668
669 let end_nano: u64 = span
670 .get("endTimeUnixNano")
671 .and_then(|v| {
672 v.as_str()
673 .and_then(|s| s.parse::<u64>().ok())
674 .or_else(|| v.as_u64())
675 })
676 .unwrap_or(0);
677
678 let latency_ms = if end_nano > start_nano {
679 Some(((end_nano - start_nano) / 1_000_000) as i64)
680 } else {
681 None
682 };
683
684 let timestamp = if start_nano > 0 {
686 let secs = (start_nano / 1_000_000_000) as i64;
687 let nsecs = (start_nano % 1_000_000_000) as u32;
688 chrono::DateTime::from_timestamp(secs, nsecs)
689 .map(|dt| dt.to_rfc3339())
690 .unwrap_or_else(|| chrono::Utc::now().to_rfc3339())
691 } else {
692 chrono::Utc::now().to_rfc3339()
693 };
694
695 let payload = span
697 .get("attributes")
698 .cloned()
699 .unwrap_or(serde_json::json!({}));
700
701 let genai = extract_genai_fields(span);
702
703 let content_hash =
704 compute_content_hash(&payload.to_string(), &agent_id, ×tamp);
705
706 let event = AgentEvent {
707 id: Uuid::now_v7(),
708 agent_id,
709 thread_id: None,
710 run_id: None,
711 parent_event_id: None,
712 event_type: genai.event_type,
713 payload,
714 trace_id,
715 span_id,
716 model: genai.model,
717 tokens_input: genai.tokens_input,
718 tokens_output: genai.tokens_output,
719 latency_ms,
720 cost_usd: genai.cost_usd,
721 timestamp,
722 logical_clock: 0,
723 content_hash,
724 prev_hash: None,
725 embedding: None,
726 };
727
728 engine.storage.insert_event(&event).await?;
729 count += 1;
730 }
731 }
732 }
733
734 Ok(Json(serde_json::json!({"accepted": count})))
735}
736
737#[cfg(test)]
742mod tests {
743 use super::*;
744
745 #[test]
746 fn test_extract_genai_fields_chat_span() {
747 let span = serde_json::json!({
748 "name": "chat gpt-4",
749 "attributes": [
750 {"key": "gen_ai.request.model", "value": {"stringValue": "gpt-4"}},
751 {"key": "gen_ai.usage.input_tokens", "value": {"intValue": "150"}},
752 {"key": "gen_ai.usage.output_tokens", "value": {"intValue": "50"}},
753 {"key": "gen_ai.usage.cost", "value": {"doubleValue": 0.006}},
754 {"key": "gen_ai.operation.name", "value": {"stringValue": "chat"}}
755 ]
756 });
757 let fields = extract_genai_fields(&span);
758 assert_eq!(fields.event_type, EventType::AssistantMessage);
759 assert_eq!(fields.model.as_deref(), Some("gpt-4"));
760 assert_eq!(fields.tokens_input, Some(150));
761 assert_eq!(fields.tokens_output, Some(50));
762 assert!((fields.cost_usd.unwrap() - 0.006).abs() < 1e-9);
763 }
764
765 #[test]
766 fn test_extract_genai_fields_non_genai_default() {
767 let span = serde_json::json!({
768 "name": "http.request",
769 "attributes": [
770 {"key": "http.method", "value": {"stringValue": "GET"}}
771 ]
772 });
773 let fields = extract_genai_fields(&span);
774 assert_eq!(fields.event_type, EventType::ToolCall);
775 assert!(fields.model.is_none());
776 assert!(fields.tokens_input.is_none());
777 }
778}