use aegis_tools::{Tool, ToolContext};
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PersistentTask {
pub id: String,
pub name: String,
pub prompt: String, pub trigger: String, pub status: String, pub restart_count: u32,
pub last_run: Option<String>, #[serde(default)]
pub session_id: Option<String>,
#[serde(default)]
pub progress: Option<String>,
#[serde(default)]
pub last_done: u32,
#[serde(default)]
pub stall_count: u32,
}
pub struct PersistentTaskManager {
dir: PathBuf,
}
impl Default for PersistentTaskManager {
fn default() -> Self {
Self::new()
}
}
impl PersistentTaskManager {
pub fn new() -> Self {
let dir = dirs_next::home_dir()
.unwrap_or_default()
.join(".aegis/tasks/persistent");
let _ = std::fs::create_dir_all(&dir);
Self { dir }
}
fn save(&self, task: &PersistentTask) -> Result<()> {
let content = serde_json::to_string_pretty(task)?;
std::fs::write(self.dir.join(format!("{}.json", task.id)), content)?;
Ok(())
}
pub fn create(&self, name: &str, prompt: &str, trigger: &str) -> Result<PersistentTask> {
let id = format!("task-{}", &uuid::Uuid::new_v4().to_string()[..8]);
let task = PersistentTask {
id,
name: name.to_string(),
prompt: prompt.to_string(),
trigger: trigger.to_string(),
status: "active".to_string(),
restart_count: 0,
last_run: None,
session_id: None,
progress: None,
last_done: 0,
stall_count: 0,
};
self.save(&task)?;
Ok(task)
}
pub fn register(&self, name: &str, prompt: &str, session_id: &str) -> Result<PersistentTask> {
let id = format!("task-{}", &uuid::Uuid::new_v4().to_string()[..8]);
let task = PersistentTask {
id,
name: name.to_string(),
prompt: prompt.to_string(),
trigger: "manual".to_string(),
status: "active".to_string(),
restart_count: 0,
last_run: Some(chrono::Utc::now().to_rfc3339()),
session_id: Some(session_id.to_string()),
progress: None,
last_done: 0,
stall_count: 0,
};
self.save(&task)?;
Ok(task)
}
pub fn list(&self) -> Vec<PersistentTask> {
let mut tasks = Vec::new();
if let Ok(entries) = std::fs::read_dir(&self.dir) {
for entry in entries.flatten() {
if let Ok(content) = std::fs::read_to_string(entry.path()) {
if let Ok(task) = serde_json::from_str::<PersistentTask>(&content) {
tasks.push(task);
}
}
}
}
tasks
}
pub fn stop(&self, id: &str) -> Result<()> {
if let Some(mut task) = self.list().into_iter().find(|t| t.id.starts_with(id)) {
task.status = "stopped".to_string();
self.save(&task)
} else {
anyhow::bail!("Task not found: {id}")
}
}
pub fn load_active(&self) -> Vec<PersistentTask> {
self.list().into_iter().filter(|t| t.status == "active").collect()
}
pub fn find_active_by_session(&self, session_id: &str) -> Option<PersistentTask> {
self.list().into_iter().find(|t| {
t.status == "active" && t.session_id.as_deref() == Some(session_id)
})
}
pub fn mark_running(&self, id: &str) -> Result<()> {
if let Some(mut task) = self.list().into_iter().find(|t| t.id.starts_with(id)) {
task.last_run = Some(chrono::Utc::now().to_rfc3339());
self.save(&task)
} else {
anyhow::bail!("Task not found: {id}")
}
}
pub fn mark_resumed(&self, id: &str) -> Result<()> {
if let Some(mut task) = self.list().into_iter().find(|t| t.id.starts_with(id)) {
task.restart_count += 1;
task.last_run = Some(chrono::Utc::now().to_rfc3339());
self.save(&task)
} else {
anyhow::bail!("Task not found: {id}")
}
}
pub fn set_progress_by_session(&self, session_id: &str, progress: &str) -> Result<Option<String>> {
if let Some(mut task) = self.find_active_by_session(session_id) {
task.progress = Some(progress.to_string());
task.last_run = Some(chrono::Utc::now().to_rfc3339());
let name = task.name.clone();
self.save(&task)?;
Ok(Some(name))
} else {
Ok(None)
}
}
pub fn complete_by_session(&self, session_id: &str) -> Result<Option<String>> {
if let Some(mut task) = self.find_active_by_session(session_id) {
task.status = "done".to_string();
let name = task.name.clone();
self.save(&task)?;
Ok(Some(name))
} else {
Ok(None)
}
}
pub fn record_resume_progress(&self, id: &str, current_done: u32) -> Result<u32> {
if let Some(mut task) = self.list().into_iter().find(|t| t.id.starts_with(id)) {
if current_done > task.last_done {
task.last_done = current_done;
task.stall_count = 0;
} else {
task.stall_count += 1;
}
let streak = task.stall_count;
self.save(&task)?;
Ok(streak)
} else {
Ok(0)
}
}
}
pub struct TaskTool;
#[async_trait]
impl Tool for TaskTool {
fn name(&self) -> &str {
"task"
}
fn description(&self) -> &str {
"Make a long/multi-session task survive restarts (checkpoint-resume). \
`register` it once when you start a big task: if aegis is interrupted or \
restarted, it reopens THIS SAME session next launch — your todo list, \
memory and files are intact — and you continue from the first unfinished \
step. Use the `todo` tool to track the steps, `task checkpoint` to note \
progress, and `task complete` when the whole task is finished. Actions: \
register (name, goal), checkpoint (note), complete, list."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"action": { "type": "string", "enum": ["register", "checkpoint", "complete", "list"] },
"name": { "type": "string", "description": "Short task name (action=register)" },
"goal": { "type": "string", "description": "The overall task/goal to resume (action=register)" },
"note": { "type": "string", "description": "Progress note (action=checkpoint)" }
},
"required": ["action"]
})
}
async fn execute(&self, args: Value, ctx: &ToolContext<'_>) -> Result<String> {
let mgr = PersistentTaskManager::new();
match args["action"].as_str().unwrap_or("list") {
"register" => {
let name = args["name"].as_str().unwrap_or("task");
let goal = args["goal"]
.as_str()
.or_else(|| args["prompt"].as_str())
.unwrap_or("");
if goal.trim().is_empty() {
return Ok("Error: 'goal' is required to register a resumable task.".to_string());
}
let t = mgr.register(name, goal, &ctx.session_id)?;
Ok(format!(
"Registered resumable task '{}' (id {}). If interrupted, it auto-resumes in this session on next launch. Track steps with the `todo` tool; call `task complete` when done.",
t.name, t.id
))
}
"checkpoint" => {
let note = args["note"].as_str().unwrap_or("");
match mgr.set_progress_by_session(&ctx.session_id, note)? {
Some(name) => Ok(format!("Checkpoint saved for '{name}'.")),
None => Ok("No resumable task registered in this session (use action=register first).".to_string()),
}
}
"complete" => match mgr.complete_by_session(&ctx.session_id)? {
Some(name) => Ok(format!("Task '{name}' marked complete; it will not resume again.")),
None => Ok("No resumable task registered in this session.".to_string()),
},
_ => {
let active = mgr.load_active();
if active.is_empty() {
return Ok("No resumable tasks.".to_string());
}
let mut out = String::from("Resumable tasks:\n");
for t in active {
out.push_str(&format!(
" {} [{}] restarts={} progress={}\n",
t.name,
t.id,
t.restart_count,
t.progress.as_deref().unwrap_or("-")
));
}
Ok(out)
}
}
}
}