1use std::collections::{HashMap, HashSet};
2
3use axum::{extract::Query, http::StatusCode, response::IntoResponse, Json};
4use serde::{Deserialize, Serialize};
5
6use crate::core::context_os::{ContextEventKindV1, ContextEventV1};
7
8#[derive(Deserialize)]
9pub struct SummaryQuery {
10 #[serde(rename = "workspaceId")]
11 pub workspace_id: Option<String>,
12 #[serde(rename = "channelId")]
13 pub channel_id: Option<String>,
14 pub limit: Option<usize>,
15}
16
17#[derive(Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct ContextSummary {
20 pub workspace_id: String,
21 pub channel_id: String,
22 pub total_events: usize,
23 pub latest_version: i64,
24 pub active_agents: Vec<String>,
25 pub recent_decisions: Vec<DecisionSummary>,
26 pub knowledge_delta: Vec<KnowledgeDelta>,
27 pub conflict_alerts: Vec<ConflictAlert>,
28 pub event_counts_by_kind: HashMap<String, usize>,
29}
30
31#[derive(Clone, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct DecisionSummary {
34 pub agent: String,
35 pub tool: String,
36 pub action: Option<String>,
37 pub reasoning: Option<String>,
38 pub timestamp: String,
39}
40
41#[derive(Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct KnowledgeDelta {
44 pub category: String,
45 pub key: String,
46 pub agent: String,
47 pub timestamp: String,
48}
49
50#[derive(Serialize)]
51#[serde(rename_all = "camelCase")]
52pub struct ConflictAlert {
53 pub category: String,
54 pub key: String,
55 pub agents: Vec<String>,
56}
57
58#[derive(Deserialize)]
59pub struct SearchQuery {
60 pub q: String,
61 #[serde(rename = "workspaceId")]
62 pub workspace_id: Option<String>,
63 #[serde(rename = "channelId")]
64 pub channel_id: Option<String>,
65 pub limit: Option<usize>,
66}
67
68pub async fn v1_events_search(Query(q): Query<SearchQuery>) -> impl IntoResponse {
69 let ws = q.workspace_id.unwrap_or_else(|| "default".to_string());
70 let limit = q.limit.unwrap_or(20).min(100);
71
72 let rt = crate::core::context_os::runtime();
73 let results = rt.bus.search(&ws, q.channel_id.as_deref(), &q.q, limit);
74
75 (
76 StatusCode::OK,
77 Json(serde_json::json!({
78 "query": q.q,
79 "workspaceId": ws,
80 "channelId": q.channel_id,
81 "results": results,
82 "count": results.len(),
83 })),
84 )
85}
86
87#[derive(Deserialize)]
88pub struct LineageQuery {
89 pub id: i64,
90 pub depth: Option<usize>,
91}
92
93pub async fn v1_event_lineage(Query(q): Query<LineageQuery>) -> impl IntoResponse {
94 let depth = q.depth.unwrap_or(20).min(50);
95
96 let rt = crate::core::context_os::runtime();
97 let chain = rt.bus.lineage(q.id, depth);
98
99 (
100 StatusCode::OK,
101 Json(serde_json::json!({
102 "eventId": q.id,
103 "chain": chain,
104 "depth": chain.len(),
105 })),
106 )
107}
108
109pub async fn v1_context_summary(Query(q): Query<SummaryQuery>) -> impl IntoResponse {
110 let ws = q.workspace_id.unwrap_or_else(|| "default".to_string());
111 let ch = q.channel_id.unwrap_or_else(|| "default".to_string());
112 let limit = q.limit.unwrap_or(100).min(500);
113
114 let rt = crate::core::context_os::runtime();
115 let events = rt.bus.read(&ws, &ch, 0, limit);
116
117 let summary = build_summary(&ws, &ch, &events);
118 (
119 StatusCode::OK,
120 Json(serde_json::to_value(summary).unwrap_or_default()),
121 )
122}
123
124fn build_summary(ws: &str, ch: &str, events: &[ContextEventV1]) -> ContextSummary {
125 let mut agents: HashSet<String> = HashSet::new();
126 let mut kind_counts: HashMap<String, usize> = HashMap::new();
127 let mut decisions = Vec::new();
128 let mut knowledge_deltas = Vec::new();
129 let mut latest_version: i64 = 0;
130
131 let mut knowledge_writers: HashMap<(String, String), HashSet<String>> = HashMap::new();
133
134 for ev in events {
135 if let Some(ref actor) = ev.actor {
136 agents.insert(actor.clone());
137 }
138 *kind_counts.entry(ev.kind.clone()).or_insert(0) += 1;
139 latest_version = latest_version.max(ev.version);
140
141 let p = &ev.payload;
142 let tool = p
143 .get("tool")
144 .and_then(|v| v.as_str())
145 .unwrap_or("")
146 .to_string();
147 let action = p.get("action").and_then(|v| v.as_str()).map(String::from);
148 let reasoning = p
149 .get("reasoning")
150 .and_then(|v| v.as_str())
151 .map(String::from);
152
153 if ev.kind == ContextEventKindV1::SessionMutated.as_str()
154 || ev.kind == ContextEventKindV1::KnowledgeRemembered.as_str()
155 {
156 decisions.push(DecisionSummary {
157 agent: ev.actor.clone().unwrap_or_default(),
158 tool: tool.clone(),
159 action: action.clone(),
160 reasoning,
161 timestamp: ev.timestamp.to_rfc3339(),
162 });
163 }
164
165 if ev.kind == ContextEventKindV1::KnowledgeRemembered.as_str() {
166 let cat = p
167 .get("category")
168 .and_then(|v| v.as_str())
169 .unwrap_or("")
170 .to_string();
171 let key = p
172 .get("key")
173 .and_then(|v| v.as_str())
174 .unwrap_or("")
175 .to_string();
176 knowledge_deltas.push(KnowledgeDelta {
177 category: cat.clone(),
178 key: key.clone(),
179 agent: ev.actor.clone().unwrap_or_default(),
180 timestamp: ev.timestamp.to_rfc3339(),
181 });
182
183 if let Some(ref actor) = ev.actor {
184 knowledge_writers
185 .entry((cat, key))
186 .or_default()
187 .insert(actor.clone());
188 }
189 }
190 }
191
192 let conflict_alerts: Vec<ConflictAlert> = knowledge_writers
193 .into_iter()
194 .filter(|(_, writers)| writers.len() > 1)
195 .map(|((cat, key), writers)| ConflictAlert {
196 category: cat,
197 key,
198 agents: writers.into_iter().collect(),
199 })
200 .collect();
201
202 let recent_limit = 10;
203 let decisions: Vec<_> = if decisions.len() > recent_limit {
204 decisions[decisions.len() - recent_limit..].to_vec()
205 } else {
206 decisions
207 };
208
209 ContextSummary {
210 workspace_id: ws.to_string(),
211 channel_id: ch.to_string(),
212 total_events: events.len(),
213 latest_version,
214 active_agents: agents.into_iter().collect(),
215 recent_decisions: decisions,
216 knowledge_delta: knowledge_deltas,
217 conflict_alerts,
218 event_counts_by_kind: kind_counts,
219 }
220}