use std::sync::Arc;
use chrono::{DateTime, Duration, Utc};
use serde::Serialize;
use super::{
DecisionPointData, ErrorData, Event, EventStore, EventType, SubAgentSpawnData, TaskEndData,
TaskStartData, TaskStatus, ThinkingStartData, ToolCallData, ToolResultData,
};
use crate::utils::truncate_str;
#[derive(Debug, Clone, Serialize, Default)]
pub struct SessionContext {
#[serde(skip_serializing_if = "Option::is_none")]
pub current_task: Option<CurrentTask>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_completed_task: Option<CompletedTask>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_error: Option<RecentError>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub recent_tools: Vec<RecentTool>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub recent_diagnostics: Vec<RecentDiagnostic>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_iteration: Option<u32>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub active_sub_agents: Vec<ActiveSubAgent>,
pub event_count: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct CurrentTask {
pub task_id: String,
pub description: String,
pub started_at: DateTime<Utc>,
pub elapsed_secs: u64,
pub iterations: u32,
pub tool_calls: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct CompletedTask {
pub task_id: String,
pub description: String,
pub status: TaskStatus,
pub duration_secs: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub completed_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RecentError {
pub message: String,
pub error_type: String,
pub occurred_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub task_context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
pub recovered: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct RecentTool {
pub name: String,
pub summary: String,
pub success: bool,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize)]
pub struct RecentDiagnostic {
pub severity: String,
pub code: String,
pub summary: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ActiveSubAgent {
pub child_session_id: String,
pub mission: String,
pub depth: u32,
pub started_at: DateTime<Utc>,
}
impl SessionContext {
pub fn is_empty(&self) -> bool {
self.current_task.is_none()
&& self.last_completed_task.is_none()
&& self.last_error.is_none()
&& self.recent_tools.is_empty()
&& self.recent_diagnostics.is_empty()
&& self.active_sub_agents.is_empty()
}
pub fn format_for_prompt(&self) -> String {
if self.is_empty() {
return String::new();
}
let mut lines = vec!["## Current Session Activity".to_string()];
let active_task_is_uploaded_artifact = self
.current_task
.as_ref()
.is_some_and(|task| task.description.contains("[File received:"));
if let Some(task) = &self.current_task {
lines.push(format!(
"- **Active task:** \"{}\" (running {}s, iteration {}, {} tool calls)",
truncate_str(&task.description, 60),
task.elapsed_secs,
task.iterations,
task.tool_calls
));
}
if !active_task_is_uploaded_artifact {
if let Some(task) = &self.last_completed_task {
let status_str = match task.status {
TaskStatus::Completed => "completed",
TaskStatus::Cancelled => "CANCELLED",
TaskStatus::Failed => "FAILED",
};
let error_suffix = task
.error
.as_ref()
.map(|e| format!(" - Error: {}", truncate_str(e, 50)))
.unwrap_or_default();
lines.push(format!(
"- **Last task:** \"{}\" - {} ({}s){}",
truncate_str(&task.description, 50),
status_str,
task.duration_secs,
error_suffix
));
}
}
if let Some(error) = &self.last_error {
let tool_suffix = error
.tool_name
.as_ref()
.map(|t| format!(" in {}", t))
.unwrap_or_default();
let recovered_suffix = if error.recovered { " (recovered)" } else { "" };
lines.push(format!(
"- **Recent error:** {}{}{}",
truncate_str(&error.message, 60),
tool_suffix,
recovered_suffix
));
}
if !self.recent_tools.is_empty() {
let tool_summary: Vec<String> = self
.recent_tools
.iter()
.take(5)
.map(|t| {
let status = if t.success { "ok" } else { "err" };
format!("{}({})", t.name, status)
})
.collect();
lines.push(format!("- **Recent tools:** {}", tool_summary.join(", ")));
}
if !self.recent_diagnostics.is_empty() {
let diag_summary: Vec<String> = self
.recent_diagnostics
.iter()
.take(3)
.map(|d| format!("{}({})", truncate_str(&d.code, 40), d.severity))
.collect();
lines.push(format!(
"- **Recent diagnostics:** {}",
diag_summary.join(", ")
));
}
if !self.active_sub_agents.is_empty() {
let sub_agent_summary: Vec<String> = self
.active_sub_agents
.iter()
.map(|sa| format!("depth {}: \"{}\"", sa.depth, truncate_str(&sa.mission, 30)))
.collect();
lines.push(format!(
"- **Sub-agents:** {}",
sub_agent_summary.join("; ")
));
}
lines.push(String::new());
lines.push(
"IMPORTANT: This session activity is reference context only. \
When the user sends a new message, respond to THAT message. \
Do NOT continue or resume previous tasks unless the user explicitly asks \
(e.g., \"continue\", \"keep going\", \"resume\"). \
If the user asks about your recent activity (\"what did you do?\", \
\"why didn't you respond?\"), explain using this context — do not take new actions."
.to_string(),
);
lines.join("\n")
}
}
pub struct SessionContextCompiler {
store: Arc<EventStore>,
}
impl SessionContextCompiler {
pub fn new(store: Arc<EventStore>) -> Self {
Self { store }
}
pub async fn compile(
&self,
session_id: &str,
window: Duration,
) -> anyhow::Result<SessionContext> {
let since = Utc::now() - window;
let events = self.store.query_events(session_id, since).await?;
Ok(self.compile_from_events(&events))
}
pub fn compile_from_events(&self, events: &[Event]) -> SessionContext {
let mut context = SessionContext {
event_count: events.len(),
..Default::default()
};
if events.is_empty() {
return context;
}
let mut task_starts: std::collections::HashMap<String, (Event, TaskStartData)> =
std::collections::HashMap::new();
let mut task_ends: std::collections::HashMap<String, (Event, TaskEndData)> =
std::collections::HashMap::new();
let mut task_iterations: std::collections::HashMap<String, u32> =
std::collections::HashMap::new();
let mut task_tool_calls: std::collections::HashMap<String, u32> =
std::collections::HashMap::new();
let mut sub_agent_starts: std::collections::HashMap<String, (Event, SubAgentSpawnData)> =
std::collections::HashMap::new();
let mut completed_sub_agents: std::collections::HashSet<String> =
std::collections::HashSet::new();
let mut recent_tools: Vec<RecentTool> = Vec::new();
let mut recent_diagnostics: Vec<RecentDiagnostic> = Vec::new();
let mut last_error: Option<(Event, ErrorData)> = None;
for event in events {
match event.event_type {
EventType::TaskStart => {
if let Ok(data) = event.parse_data::<TaskStartData>() {
task_starts.insert(data.task_id.clone(), (event.clone(), data));
}
}
EventType::TaskEnd => {
if let Ok(data) = event.parse_data::<TaskEndData>() {
task_ends.insert(data.task_id.clone(), (event.clone(), data));
}
}
EventType::ThinkingStart => {
if let Ok(data) = event.parse_data::<ThinkingStartData>() {
*task_iterations.entry(data.task_id.clone()).or_insert(0) = data.iteration;
}
}
EventType::ToolCall => {
if let Ok(data) = event.parse_data::<ToolCallData>() {
if let Some(task_id) = &data.task_id {
*task_tool_calls.entry(task_id.clone()).or_insert(0) += 1;
}
}
}
EventType::ToolResult => {
if let Ok(data) = event.parse_data::<ToolResultData>() {
recent_tools.push(RecentTool {
name: data.name,
summary: truncate_str(&data.result, 50).to_string(),
success: data.success,
timestamp: event.created_at,
});
}
}
EventType::Error => {
if let Ok(data) = event.parse_data::<ErrorData>() {
last_error = Some((event.clone(), data));
}
}
EventType::DecisionPoint => {
if let Ok(data) = event.parse_data::<DecisionPointData>() {
if data.severity.is_warning_or_higher() {
recent_diagnostics.push(RecentDiagnostic {
severity: data.severity.as_str().to_string(),
code: data
.code
.unwrap_or_else(|| data.decision_type.as_str().to_string()),
summary: truncate_str(&data.summary, 80).to_string(),
timestamp: event.created_at,
});
}
}
}
EventType::SubAgentSpawn => {
if let Ok(data) = event.parse_data::<SubAgentSpawnData>() {
sub_agent_starts
.insert(data.child_session_id.clone(), (event.clone(), data));
}
}
EventType::SubAgentComplete => {
if let Ok(data) = event.parse_data::<super::SubAgentCompleteData>() {
completed_sub_agents.insert(data.child_session_id);
}
}
_ => {}
}
}
let now = Utc::now();
for (task_id, (start_event, start_data)) in &task_starts {
if !task_ends.contains_key(task_id) {
let elapsed = (now - start_event.created_at).num_seconds().max(0) as u64;
context.current_task = Some(CurrentTask {
task_id: task_id.clone(),
description: start_data.description.clone(),
started_at: start_event.created_at,
elapsed_secs: elapsed,
iterations: task_iterations.get(task_id).copied().unwrap_or(0),
tool_calls: task_tool_calls.get(task_id).copied().unwrap_or(0),
});
context.current_iteration = task_iterations.get(task_id).copied();
break; }
}
if let Some((_task_id, (end_event, end_data))) =
task_ends.iter().max_by_key(|(_, (e, _))| e.created_at)
{
let description = task_starts
.get(&end_data.task_id)
.map(|(_, d)| d.description.clone())
.unwrap_or_else(|| "Unknown task".to_string());
context.last_completed_task = Some(CompletedTask {
task_id: end_data.task_id.clone(),
description,
status: end_data.status,
duration_secs: end_data.duration_secs,
error: end_data.error.clone(),
completed_at: end_event.created_at,
});
}
if let Some((error_event, error_data)) = last_error {
context.last_error = Some(RecentError {
message: error_data.message,
error_type: format!("{:?}", error_data.error_type),
occurred_at: error_event.created_at,
task_context: error_data.context,
tool_name: error_data.tool_name,
recovered: error_data.recovered,
});
}
recent_tools.reverse();
recent_tools.truncate(10);
context.recent_tools = recent_tools;
recent_diagnostics.reverse();
recent_diagnostics.truncate(5);
context.recent_diagnostics = recent_diagnostics;
for (child_session_id, (spawn_event, spawn_data)) in sub_agent_starts {
if !completed_sub_agents.contains(&child_session_id) {
context.active_sub_agents.push(ActiveSubAgent {
child_session_id,
mission: spawn_data.mission,
depth: spawn_data.depth,
started_at: spawn_event.created_at,
});
}
}
context
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_context() {
let ctx = SessionContext::default();
assert!(ctx.is_empty());
assert_eq!(ctx.format_for_prompt(), "");
}
#[test]
fn test_context_formatting() {
let ctx = SessionContext {
current_task: Some(CurrentTask {
task_id: "task_1".to_string(),
description: "Add blog posts".to_string(),
started_at: Utc::now(),
elapsed_secs: 120,
iterations: 5,
tool_calls: 8,
}),
last_error: Some(RecentError {
message: "Command failed".to_string(),
error_type: "ToolError".to_string(),
occurred_at: Utc::now(),
task_context: None,
tool_name: Some("terminal".to_string()),
recovered: false,
}),
recent_tools: vec![RecentTool {
name: "terminal".to_string(),
summary: "git status".to_string(),
success: true,
timestamp: Utc::now(),
}],
recent_diagnostics: vec![RecentDiagnostic {
severity: "warning".to_string(),
code: "soft_iteration_warning".to_string(),
summary: "Soft iteration warning threshold reached".to_string(),
timestamp: Utc::now(),
}],
..Default::default()
};
let formatted = ctx.format_for_prompt();
assert!(formatted.contains("Active task"));
assert!(formatted.contains("Add blog posts"));
assert!(formatted.contains("Recent error"));
assert!(formatted.contains("terminal"));
assert!(formatted.contains("Recent diagnostics"));
assert!(formatted.contains("soft_iteration_warning"));
}
}