pub(crate) fn task_event_feed(db: &roboticus_db::Database) -> serde_json::Value {
let events = roboticus_db::task_events::recent_task_events(db, 50).unwrap_or_default();
let active = roboticus_db::task_events::active_task_summaries(db).unwrap_or_default();
let active_count = active.len();
serde_json::json!({
"recent_events": events,
"active_tasks": active,
"active_count": active_count,
})
}
pub(crate) fn agent_task_state(
db: &roboticus_db::Database,
agent_name: &str,
) -> &'static str {
use roboticus_db::task_events::TaskLifecycleState;
let events =
roboticus_db::task_events::task_events_for_agent(db, agent_name).unwrap_or_default();
if events.is_empty() {
return "idle";
}
let mut seen = std::collections::HashSet::new();
let mut latest_per_task: Vec<&roboticus_db::task_events::TaskEventRow> = Vec::new();
for evt in &events {
if seen.insert(evt.task_id.as_str()) {
latest_per_task.push(evt);
}
}
let has_failed = latest_per_task
.iter()
.any(|e| e.event_type == TaskLifecycleState::Failed);
if has_failed {
return "failed";
}
let has_running = latest_per_task
.iter()
.any(|e| e.event_type == TaskLifecycleState::Running);
if has_running {
return "running";
}
let has_in_progress = latest_per_task.iter().any(|e| {
matches!(
e.event_type,
TaskLifecycleState::Assigned
| TaskLifecycleState::Progress
| TaskLifecycleState::Retry
| TaskLifecycleState::Pending
)
});
if has_in_progress {
return "working";
}
"idle"
}
pub async fn get_task_events(
State(state): State<AppState>,
Query(params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
let limit = params
.get("limit")
.and_then(|v| v.parse::<i64>().ok())
.unwrap_or(50)
.min(200);
let task_id = params.get("task_id").map(|s| s.as_str());
let events = if let Some(tid) = task_id {
roboticus_db::task_events::task_events_for_task(&state.db, tid).unwrap_or_default()
} else {
roboticus_db::task_events::recent_task_events(&state.db, limit).unwrap_or_default()
};
let count = events.len();
Json(serde_json::json!({
"events": events,
"count": count,
}))
}
pub async fn get_workspace_tasks(State(state): State<AppState>) -> impl IntoResponse {
let agents = state.registry.list_agents().await;
let config = state.config.read().await;
let now = chrono::Utc::now();
let workspace_root = std::path::Path::new(&config.agent.workspace);
let files = workspace_files_snapshot(workspace_root);
let systems: Vec<serde_json::Value> = vec![
serde_json::json!({ "id": "llm", "name": "LLM Inference", "kind": "Inference", "x": 0.18, "y": 0.22 }),
serde_json::json!({ "id": "memory", "name": "Memory", "kind": "Storage", "x": 0.82, "y": 0.22 }),
serde_json::json!({ "id": "exec", "name": "Code Execution", "kind": "Execution", "x": 0.18, "y": 0.78 }),
serde_json::json!({ "id": "blockchain", "name": "Blockchain", "kind": "Blockchain", "x": 0.82, "y": 0.78 }),
serde_json::json!({ "id": "web", "name": "Web / APIs", "kind": "Tool", "x": 0.50, "y": 0.12 }),
serde_json::json!({ "id": "files", "name": "File System", "kind": "Tool", "x": 0.50, "y": 0.88 }),
serde_json::json!({ "id": "tools_plugins", "name": "Tools / Plugins", "kind": "Plugin", "x": 0.965, "y": 0.50 }),
serde_json::json!({ "id": "shelter", "name": "Idle Agents", "kind": "Shelter", "x": 0.035, "y": 0.50 }),
];
let skills = roboticus_db::skills::list_skills(&state.db)
.inspect_err(|e| tracing::error!(error = %e, "workspace-tasks: failed to load skills"))
.unwrap_or_default();
let enabled_skills: Vec<String> = skills
.iter()
.filter(|s| s.enabled)
.map(|s| s.name.clone())
.collect();
let agent_list: Vec<serde_json::Value> = agents
.iter()
.enumerate()
.map(|(i, a)| {
let color = WORKSPACE_PALETTE[(i + 1) % WORKSPACE_PALETTE.len()];
let running = format!("{:?}", a.state).to_lowercase() == "running";
let (workstation, activity, active_skill) =
derive_workspace_activity(&state.db, &a.id, running, now);
let task_state = agent_task_state(&state.db, &a.name);
let mut card = serde_json::json!({
"id": a.id,
"name": a.name,
"role": ROLE_SUBAGENT,
"state": a.state,
"color": color,
"model": a.model,
"current_workstation": workstation,
"activity": activity,
"active_skill": active_skill,
"updated_at": chrono::Utc::now().to_rfc3339(),
"subordinates": [],
"supervisor": config.agent.id,
"task_state": task_state,
});
let events =
roboticus_db::task_events::task_events_for_agent(&state.db, &a.name)
.unwrap_or_default();
if let Some(latest) = events.first() {
card["last_event_at"] = serde_json::json!(latest.created_at);
if task_state != "idle" {
card["active_task_id"] = serde_json::json!(latest.task_id);
if let Some(ref summary) = latest.summary {
card["active_task_summary"] = serde_json::json!(summary);
}
if let Some(pct) = latest.percentage {
card["active_task_percentage"] = serde_json::json!(pct);
}
}
}
card
})
.collect();
let (main_workstation, main_activity, main_active_skill) =
derive_workspace_activity(&state.db, &config.agent.id, true, now);
let main_task_state = agent_task_state(&state.db, &config.agent.name);
let mut main_agent = serde_json::json!({
"id": config.agent.id,
"name": config.agent.name,
"role": "agent",
"state": "Running",
"color": WORKSPACE_PALETTE[0],
"model": config.models.primary,
"current_workstation": main_workstation,
"activity": main_activity,
"active_skill": main_active_skill.or_else(|| enabled_skills.first().cloned()),
"skills": enabled_skills,
"updated_at": chrono::Utc::now().to_rfc3339(),
"subordinates": agent_list.iter()
.filter(|a| a["role"] == ROLE_SUBAGENT)
.map(|a| a["id"].clone())
.collect::<Vec<_>>(),
"supervisor": serde_json::Value::Null,
"task_state": main_task_state,
});
let main_events =
roboticus_db::task_events::task_events_for_agent(&state.db, &config.agent.name)
.unwrap_or_default();
if let Some(latest) = main_events.first() {
main_agent["last_event_at"] = serde_json::json!(latest.created_at);
if main_task_state != "idle" {
main_agent["active_task_id"] = serde_json::json!(latest.task_id);
if let Some(ref summary) = latest.summary {
main_agent["active_task_summary"] = serde_json::json!(summary);
}
if let Some(pct) = latest.percentage {
main_agent["active_task_percentage"] = serde_json::json!(pct);
}
}
}
let mut all_agents = vec![main_agent];
all_agents.extend(agent_list);
let recent = roboticus_db::task_events::recent_task_events(&state.db, 100).unwrap_or_default();
let mut seen_tasks = std::collections::HashSet::new();
let mut flows: Vec<serde_json::Value> = Vec::new();
for evt in &recent {
if seen_tasks.insert(evt.task_id.as_str())
&& let Some(ref agent) = evt.assigned_to
{
flows.push(serde_json::json!({
"task_id": evt.task_id,
"assigned_to": agent,
"state": evt.event_type.as_str(),
"summary": evt.summary,
"updated_at": evt.created_at,
}));
}
}
Json(serde_json::json!({
"agents": all_agents,
"systems": systems,
"files": files,
"interactions": [],
"task_feed": task_event_feed(&state.db),
"delegation_flows": flows,
}))
}