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 pub limit: Option<usize>,
64}
65
66pub async fn v1_events_search(Query(q): Query<SearchQuery>) -> impl IntoResponse {
67 let ws = q.workspace_id.unwrap_or_else(|| "default".to_string());
68 let limit = q.limit.unwrap_or(20).min(100);
69
70 let rt = crate::core::context_os::runtime();
71 let results = rt.bus.search(&ws, &q.q, limit);
72
73 (
74 StatusCode::OK,
75 Json(serde_json::json!({
76 "query": q.q,
77 "workspaceId": ws,
78 "results": results,
79 "count": results.len(),
80 })),
81 )
82}
83
84#[derive(Deserialize)]
85pub struct LineageQuery {
86 pub id: i64,
87 pub depth: Option<usize>,
88}
89
90pub async fn v1_event_lineage(Query(q): Query<LineageQuery>) -> impl IntoResponse {
91 let depth = q.depth.unwrap_or(20).min(50);
92
93 let rt = crate::core::context_os::runtime();
94 let chain = rt.bus.lineage(q.id, depth);
95
96 (
97 StatusCode::OK,
98 Json(serde_json::json!({
99 "eventId": q.id,
100 "chain": chain,
101 "depth": chain.len(),
102 })),
103 )
104}
105
106pub async fn v1_context_summary(Query(q): Query<SummaryQuery>) -> impl IntoResponse {
107 let ws = q.workspace_id.unwrap_or_else(|| "default".to_string());
108 let ch = q.channel_id.unwrap_or_else(|| "default".to_string());
109 let limit = q.limit.unwrap_or(100).min(500);
110
111 let rt = crate::core::context_os::runtime();
112 let events = rt.bus.read(&ws, &ch, 0, limit);
113
114 let summary = build_summary(&ws, &ch, &events);
115 (
116 StatusCode::OK,
117 Json(serde_json::to_value(summary).unwrap_or_default()),
118 )
119}
120
121fn build_summary(ws: &str, ch: &str, events: &[ContextEventV1]) -> ContextSummary {
122 let mut agents: HashSet<String> = HashSet::new();
123 let mut kind_counts: HashMap<String, usize> = HashMap::new();
124 let mut decisions = Vec::new();
125 let mut knowledge_deltas = Vec::new();
126 let mut latest_version: i64 = 0;
127
128 let mut knowledge_writers: HashMap<(String, String), HashSet<String>> = HashMap::new();
130
131 for ev in events {
132 if let Some(ref actor) = ev.actor {
133 agents.insert(actor.clone());
134 }
135 *kind_counts.entry(ev.kind.clone()).or_insert(0) += 1;
136 latest_version = latest_version.max(ev.version);
137
138 let p = &ev.payload;
139 let tool = p
140 .get("tool")
141 .and_then(|v| v.as_str())
142 .unwrap_or("")
143 .to_string();
144 let action = p.get("action").and_then(|v| v.as_str()).map(String::from);
145 let reasoning = p
146 .get("reasoning")
147 .and_then(|v| v.as_str())
148 .map(String::from);
149
150 if ev.kind == ContextEventKindV1::SessionMutated.as_str()
151 || ev.kind == ContextEventKindV1::KnowledgeRemembered.as_str()
152 {
153 decisions.push(DecisionSummary {
154 agent: ev.actor.clone().unwrap_or_default(),
155 tool: tool.clone(),
156 action: action.clone(),
157 reasoning,
158 timestamp: ev.timestamp.to_rfc3339(),
159 });
160 }
161
162 if ev.kind == ContextEventKindV1::KnowledgeRemembered.as_str() {
163 let cat = p
164 .get("category")
165 .and_then(|v| v.as_str())
166 .unwrap_or("")
167 .to_string();
168 let key = p
169 .get("key")
170 .and_then(|v| v.as_str())
171 .unwrap_or("")
172 .to_string();
173 knowledge_deltas.push(KnowledgeDelta {
174 category: cat.clone(),
175 key: key.clone(),
176 agent: ev.actor.clone().unwrap_or_default(),
177 timestamp: ev.timestamp.to_rfc3339(),
178 });
179
180 if let Some(ref actor) = ev.actor {
181 knowledge_writers
182 .entry((cat, key))
183 .or_default()
184 .insert(actor.clone());
185 }
186 }
187 }
188
189 let conflict_alerts: Vec<ConflictAlert> = knowledge_writers
190 .into_iter()
191 .filter(|(_, writers)| writers.len() > 1)
192 .map(|((cat, key), writers)| ConflictAlert {
193 category: cat,
194 key,
195 agents: writers.into_iter().collect(),
196 })
197 .collect();
198
199 let recent_limit = 10;
200 let decisions: Vec<_> = if decisions.len() > recent_limit {
201 decisions[decisions.len() - recent_limit..].to_vec()
202 } else {
203 decisions
204 };
205
206 ContextSummary {
207 workspace_id: ws.to_string(),
208 channel_id: ch.to_string(),
209 total_events: events.len(),
210 latest_version,
211 active_agents: agents.into_iter().collect(),
212 recent_decisions: decisions,
213 knowledge_delta: knowledge_deltas,
214 conflict_alerts,
215 event_counts_by_kind: kind_counts,
216 }
217}