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 };
215
216 let response = engine.recall(request).await?;
217 Ok(Json(response))
218}
219
220pub async fn get_memory_handler(
222 State(engine): State<AppState>,
223 Path(id): Path<Uuid>,
224) -> Result<Json<serde_json::Value>, AppError> {
225 let record = engine
226 .storage
227 .get_memory(id)
228 .await?
229 .ok_or_else(|| CoreError::NotFound(format!("memory {id} not found")))?;
230
231 let value = serde_json::json!({
232 "id": record.id,
233 "agent_id": record.agent_id,
234 "content": record.content,
235 "memory_type": record.memory_type,
236 "scope": record.scope,
237 "importance": record.importance,
238 "tags": record.tags,
239 "metadata": record.metadata,
240 "source_type": record.source_type,
241 "source_id": record.source_id,
242 "consolidation_state": record.consolidation_state,
243 "access_count": record.access_count,
244 "org_id": record.org_id,
245 "thread_id": record.thread_id,
246 "created_at": record.created_at,
247 "updated_at": record.updated_at,
248 "last_accessed_at": record.last_accessed_at,
249 "expires_at": record.expires_at,
250 "deleted_at": record.deleted_at,
251 "decay_rate": record.decay_rate,
252 "created_by": record.created_by,
253 "version": record.version,
254 "prev_version_id": record.prev_version_id,
255 "quarantined": record.quarantined,
256 "quarantine_reason": record.quarantine_reason,
257 });
258
259 Ok(Json(value))
260}
261
262pub async fn forget_handler(
264 State(engine): State<AppState>,
265 Path(id): Path<Uuid>,
266 Query(params): Query<ForgetParams>,
267) -> Result<Json<ForgetResponse>, AppError> {
268 let strategy = match params.strategy.as_deref() {
269 Some(s) => Some(match s {
270 "soft_delete" => ForgetStrategy::SoftDelete,
271 "hard_delete" => ForgetStrategy::HardDelete,
272 "decay" => ForgetStrategy::Decay,
273 "consolidate" => ForgetStrategy::Consolidate,
274 "archive" => ForgetStrategy::Archive,
275 "redact" => ForgetStrategy::Redact,
276 other => {
277 return Err(AppError(CoreError::Validation(format!(
278 "invalid forget strategy '{}': expected one of: soft_delete, hard_delete, decay, consolidate, archive, redact",
279 other
280 ))));
281 }
282 }),
283 None => None,
284 };
285
286 let request = ForgetRequest {
287 memory_ids: vec![id],
288 agent_id: params.agent_id,
289 strategy,
290 criteria: None,
291 };
292
293 let response = engine.forget(request).await?;
294 Ok(Json(response))
295}
296
297#[derive(Debug, Deserialize)]
298pub struct ForgetSubjectBody {
299 pub subject_id: String,
300 pub strategy: Option<String>,
301 pub agent_id: Option<String>,
302}
303
304pub async fn forget_subject_handler(
306 State(engine): State<AppState>,
307 Json(body): Json<ForgetSubjectBody>,
308) -> Result<Json<ForgetSubjectResponse>, AppError> {
309 let strategy = match body.strategy.as_deref().unwrap_or("redact") {
310 "redact" => ForgetStrategy::Redact,
311 "hard_delete" => ForgetStrategy::HardDelete,
312 "soft_delete" => ForgetStrategy::SoftDelete,
313 other => {
314 return Err(AppError(CoreError::Validation(format!(
315 "invalid forget_subject strategy '{}': expected one of: redact, hard_delete, soft_delete",
316 other
317 ))));
318 }
319 };
320
321 let request = ForgetSubjectRequest {
322 subject_id: body.subject_id,
323 agent_id: body.agent_id,
324 strategy,
325 };
326
327 let response = engine.forget_subject(request).await?;
328 Ok(Json(response))
329}
330
331pub async fn share_handler(
333 State(engine): State<AppState>,
334 Path(id): Path<Uuid>,
335 Json(body): Json<ShareBody>,
336) -> Result<Json<ShareResponse>, AppError> {
337 let permission = match body.permission.as_deref() {
338 Some(s) => Some(s.parse::<Permission>().map_err(|_| {
339 AppError(CoreError::Validation(format!(
340 "invalid permission '{}': expected one of: read, write, delete, share, delegate, admin",
341 s
342 )))
343 })?),
344 None => None,
345 };
346
347 let request = ShareRequest {
348 memory_id: id,
349 agent_id: body.agent_id,
350 target_agent_id: body.target_agent_id,
351 target_agent_ids: body.target_agent_ids,
352 permission,
353 expires_in_hours: body.expires_in_hours,
354 };
355
356 let response = engine.share(request).await?;
357 Ok(Json(response))
358}
359
360pub async fn checkpoint_handler(
362 State(engine): State<AppState>,
363 Json(request): Json<CheckpointRequest>,
364) -> Result<Json<CheckpointResponse>, AppError> {
365 let response = engine.checkpoint(request).await?;
366 Ok(Json(response))
367}
368
369pub async fn branch_handler(
371 State(engine): State<AppState>,
372 Json(request): Json<BranchRequest>,
373) -> Result<Json<BranchResponse>, AppError> {
374 let response = engine.branch(request).await?;
375 Ok(Json(response))
376}
377
378pub async fn merge_handler(
380 State(engine): State<AppState>,
381 Json(request): Json<MergeRequest>,
382) -> Result<Json<MergeResponse>, AppError> {
383 let response = engine.merge(request).await?;
384 Ok(Json(response))
385}
386
387pub async fn replay_handler(
389 State(engine): State<AppState>,
390 Json(request): Json<ReplayRequest>,
391) -> Result<Json<ReplayResponse>, AppError> {
392 let response = engine.replay(request).await?;
393 Ok(Json(response))
394}
395
396pub async fn verify_handler(
398 State(engine): State<AppState>,
399 Json(body): Json<VerifyBody>,
400) -> Result<Json<serde_json::Value>, AppError> {
401 let result = engine
402 .verify_integrity(body.agent_id, body.thread_id.as_deref())
403 .await?;
404
405 let response = serde_json::json!({
406 "valid": result.valid,
407 "total_records": result.total_records,
408 "verified_records": result.verified_records,
409 "first_broken_at": result.first_broken_at.map(|id| id.to_string()),
410 "error_message": result.error_message,
411 "status": if result.valid { "verified" } else { "integrity_violation" },
412 });
413
414 Ok(Json(response))
415}
416
417pub async fn delegate_handler(
423 State(engine): State<AppState>,
424 Json(body): Json<DelegateRequest>,
425) -> Result<Json<serde_json::Value>, AppError> {
426 let permission: Permission = body
427 .permission
428 .parse()
429 .map_err(|e: CoreError| AppError(e))?;
430
431 let caller_agent_id = body
432 .agent_id
433 .unwrap_or_else(|| engine.default_agent_id.clone());
434
435 let scope = if let Some(ref ids) = body.memory_ids {
436 let parsed: std::result::Result<Vec<Uuid>, _> =
437 ids.iter().map(|s| Uuid::parse_str(s)).collect();
438 match parsed {
439 Ok(uuids) => {
440 for mid in &uuids {
442 let has_perm = engine
443 .storage
444 .check_permission(*mid, &caller_agent_id, Permission::Delegate)
445 .await?;
446 if !has_perm {
447 return Err(AppError(CoreError::PermissionDenied(format!(
448 "agent '{}' lacks delegate permission on memory {}",
449 caller_agent_id, mid
450 ))));
451 }
452 }
453 DelegationScope::ByMemoryId(uuids)
454 }
455 Err(e) => {
456 return Err(AppError(CoreError::Validation(format!(
457 "invalid UUID in memory_ids: {e}"
458 ))));
459 }
460 }
461 } else if let Some(ref tags) = body.tags {
462 DelegationScope::ByTag(tags.clone())
463 } else {
464 DelegationScope::AllMemories
465 };
466
467 let now = chrono::Utc::now();
468 let expires_at = body
469 .expires_in_hours
470 .map(|h| (now + chrono::Duration::seconds((h * 3600.0) as i64)).to_rfc3339());
471
472 let delegation = Delegation {
473 id: Uuid::now_v7(),
474 delegator_id: caller_agent_id,
475 delegate_id: body.delegate_id.clone(),
476 permission,
477 scope,
478 max_depth: body.max_depth.unwrap_or(0),
479 current_depth: 0,
480 parent_delegation_id: None,
481 created_at: now.to_rfc3339(),
482 expires_at,
483 revoked_at: None,
484 };
485
486 engine.storage.insert_delegation(&delegation).await?;
487
488 let response = serde_json::json!({
489 "delegation_id": delegation.id.to_string(),
490 "delegator": delegation.delegator_id,
491 "delegate": delegation.delegate_id,
492 "permission": delegation.permission.to_string(),
493 "status": "delegated",
494 });
495
496 Ok(Json(response))
497}
498
499pub async fn health_handler() -> Json<serde_json::Value> {
501 Json(serde_json::json!({"status": "ok"}))
502}
503
504struct GenAiFields {
509 event_type: EventType,
510 model: Option<String>,
511 tokens_input: Option<i64>,
512 tokens_output: Option<i64>,
513 cost_usd: Option<f64>,
514}
515
516fn extract_genai_fields(span: &serde_json::Value) -> GenAiFields {
519 let attributes = span.get("attributes").and_then(|v| v.as_array());
520
521 let mut model = None;
522 let mut tokens_input = None;
523 let mut tokens_output = None;
524 let mut cost_usd = None;
525 let mut operation_name = None;
526
527 if let Some(attrs) = attributes {
528 for attr in attrs {
529 let key = match attr.get("key").and_then(|k| k.as_str()) {
530 Some(k) => k,
531 None => continue,
532 };
533 let value = attr.get("value");
534
535 match key {
536 "gen_ai.request.model" => {
537 model = value
538 .and_then(|v| v.get("stringValue"))
539 .and_then(|v| v.as_str())
540 .map(|s| s.to_string());
541 }
542 "gen_ai.usage.input_tokens" => {
543 tokens_input = value.and_then(|v| v.get("intValue")).and_then(|v| {
544 v.as_str()
545 .and_then(|s| s.parse::<i64>().ok())
546 .or_else(|| v.as_i64())
547 });
548 }
549 "gen_ai.usage.output_tokens" => {
550 tokens_output = value.and_then(|v| v.get("intValue")).and_then(|v| {
551 v.as_str()
552 .and_then(|s| s.parse::<i64>().ok())
553 .or_else(|| v.as_i64())
554 });
555 }
556 "gen_ai.usage.cost" => {
557 cost_usd = value
558 .and_then(|v| v.get("doubleValue"))
559 .and_then(|v| v.as_f64());
560 }
561 "gen_ai.operation.name" => {
562 operation_name = value
563 .and_then(|v| v.get("stringValue"))
564 .and_then(|v| v.as_str())
565 .map(|s| s.to_string());
566 }
567 _ => {}
568 }
569 }
570 }
571
572 let op = operation_name.or_else(|| {
574 span.get("name")
575 .and_then(|v| v.as_str())
576 .map(|s| s.to_string())
577 });
578
579 let event_type = match op.as_deref() {
581 Some(s) if s.contains("chat") => EventType::AssistantMessage,
582 Some(s) if s.contains("embed") => EventType::RetrievalQuery,
583 Some(s) if s.contains("tool") => EventType::ToolCall,
584 _ => EventType::ToolCall, };
586
587 GenAiFields {
588 event_type,
589 model,
590 tokens_input,
591 tokens_output,
592 cost_usd,
593 }
594}
595
596pub async fn otlp_ingest_handler(
598 State(engine): State<AppState>,
599 Json(body): Json<serde_json::Value>,
600) -> Result<Json<serde_json::Value>, AppError> {
601 let resource_spans = body
602 .get("resourceSpans")
603 .and_then(|v| v.as_array())
604 .cloned()
605 .unwrap_or_default();
606
607 let mut count: u64 = 0;
608
609 for rs in &resource_spans {
610 let resource_agent_id = rs
612 .get("resource")
613 .and_then(|r| r.get("attributes"))
614 .and_then(|attrs| attrs.as_array())
615 .and_then(|attrs| {
616 attrs.iter().find_map(|attr| {
617 let key = attr.get("key")?.as_str()?;
618 if key == "agent.id" || key == "service.name" {
619 attr.get("value")
620 .and_then(|v| v.get("stringValue"))
621 .and_then(|v| v.as_str())
622 .map(|s| s.to_string())
623 } else {
624 None
625 }
626 })
627 });
628
629 let scope_spans = rs
630 .get("scopeSpans")
631 .and_then(|v| v.as_array())
632 .cloned()
633 .unwrap_or_default();
634
635 for ss in &scope_spans {
636 let spans = ss
637 .get("spans")
638 .and_then(|v| v.as_array())
639 .cloned()
640 .unwrap_or_default();
641
642 for span in &spans {
643 let trace_id = span
644 .get("traceId")
645 .and_then(|v| v.as_str())
646 .map(|s| s.to_string());
647
648 let span_id = span
649 .get("spanId")
650 .and_then(|v| v.as_str())
651 .map(|s| s.to_string());
652
653 let agent_id = resource_agent_id
654 .clone()
655 .unwrap_or_else(|| engine.default_agent_id.clone());
656
657 let start_nano: u64 = span
660 .get("startTimeUnixNano")
661 .and_then(|v| {
662 v.as_str()
663 .and_then(|s| s.parse::<u64>().ok())
664 .or_else(|| v.as_u64())
665 })
666 .unwrap_or(0);
667
668 let end_nano: u64 = span
669 .get("endTimeUnixNano")
670 .and_then(|v| {
671 v.as_str()
672 .and_then(|s| s.parse::<u64>().ok())
673 .or_else(|| v.as_u64())
674 })
675 .unwrap_or(0);
676
677 let latency_ms = if end_nano > start_nano {
678 Some(((end_nano - start_nano) / 1_000_000) as i64)
679 } else {
680 None
681 };
682
683 let timestamp = if start_nano > 0 {
685 let secs = (start_nano / 1_000_000_000) as i64;
686 let nsecs = (start_nano % 1_000_000_000) as u32;
687 chrono::DateTime::from_timestamp(secs, nsecs)
688 .map(|dt| dt.to_rfc3339())
689 .unwrap_or_else(|| chrono::Utc::now().to_rfc3339())
690 } else {
691 chrono::Utc::now().to_rfc3339()
692 };
693
694 let payload = span
696 .get("attributes")
697 .cloned()
698 .unwrap_or(serde_json::json!({}));
699
700 let genai = extract_genai_fields(span);
701
702 let content_hash =
703 compute_content_hash(&payload.to_string(), &agent_id, ×tamp);
704
705 let event = AgentEvent {
706 id: Uuid::now_v7(),
707 agent_id,
708 thread_id: None,
709 run_id: None,
710 parent_event_id: None,
711 event_type: genai.event_type,
712 payload,
713 trace_id,
714 span_id,
715 model: genai.model,
716 tokens_input: genai.tokens_input,
717 tokens_output: genai.tokens_output,
718 latency_ms,
719 cost_usd: genai.cost_usd,
720 timestamp,
721 logical_clock: 0,
722 content_hash,
723 prev_hash: None,
724 embedding: None,
725 };
726
727 engine.storage.insert_event(&event).await?;
728 count += 1;
729 }
730 }
731 }
732
733 Ok(Json(serde_json::json!({"accepted": count})))
734}
735
736#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn test_extract_genai_fields_chat_span() {
746 let span = serde_json::json!({
747 "name": "chat gpt-4",
748 "attributes": [
749 {"key": "gen_ai.request.model", "value": {"stringValue": "gpt-4"}},
750 {"key": "gen_ai.usage.input_tokens", "value": {"intValue": "150"}},
751 {"key": "gen_ai.usage.output_tokens", "value": {"intValue": "50"}},
752 {"key": "gen_ai.usage.cost", "value": {"doubleValue": 0.006}},
753 {"key": "gen_ai.operation.name", "value": {"stringValue": "chat"}}
754 ]
755 });
756 let fields = extract_genai_fields(&span);
757 assert_eq!(fields.event_type, EventType::AssistantMessage);
758 assert_eq!(fields.model.as_deref(), Some("gpt-4"));
759 assert_eq!(fields.tokens_input, Some(150));
760 assert_eq!(fields.tokens_output, Some(50));
761 assert!((fields.cost_usd.unwrap() - 0.006).abs() < 1e-9);
762 }
763
764 #[test]
765 fn test_extract_genai_fields_non_genai_default() {
766 let span = serde_json::json!({
767 "name": "http.request",
768 "attributes": [
769 {"key": "http.method", "value": {"stringValue": "GET"}}
770 ]
771 });
772 let fields = extract_genai_fields(&span);
773 assert_eq!(fields.event_type, EventType::ToolCall);
774 assert!(fields.model.is_none());
775 assert!(fields.tokens_input.is_none());
776 }
777}