Skip to main content

lean_ctx/tools/
ctx_task.rs

1use crate::core::a2a::task::{TaskPart, TaskState, TaskStore};
2
3pub fn handle(
4    action: &str,
5    current_agent_id: Option<&str>,
6    task_id: Option<&str>,
7    to_agent: Option<&str>,
8    description: Option<&str>,
9    state: Option<&str>,
10    message: Option<&str>,
11) -> String {
12    let agent = match current_agent_id {
13        Some(id) => id,
14        None if action == "list" || action == "info" => "unknown",
15        None => {
16            return "Error: agent must be registered first (use ctx_agent action=register)"
17                .to_string();
18        }
19    };
20
21    let mut store = TaskStore::load();
22    store.cleanup_old(72);
23
24    let result = match action {
25        "create" => handle_create(&mut store, agent, to_agent, description),
26        "update" => handle_update(&mut store, agent, task_id, state, message),
27        "list" => handle_list(&store, agent),
28        "get" => handle_get(&store, task_id),
29        "cancel" => handle_cancel(&mut store, agent, task_id, message),
30        "message" => handle_message(&mut store, agent, task_id, message),
31        "info" => handle_info(&store),
32        _ => format!(
33            "Unknown action '{action}'. Available: create, update, list, get, cancel, message, info"
34        ),
35    };
36
37    if matches!(action, "create" | "update" | "cancel" | "message") {
38        let _ = store.save();
39    }
40
41    result
42}
43
44fn handle_create(
45    store: &mut TaskStore,
46    from: &str,
47    to: Option<&str>,
48    desc: Option<&str>,
49) -> String {
50    let Some(to_agent) = to else {
51        return "Error: to_agent is required for task creation".to_string();
52    };
53    let description = desc.unwrap_or("(no description)");
54    let id = store.create_task(from, to_agent, description);
55    format!("Task created: {id}\n  From: {from}\n  To: {to_agent}\n  Description: {description}")
56}
57
58fn handle_update(
59    store: &mut TaskStore,
60    agent: &str,
61    task_id: Option<&str>,
62    state: Option<&str>,
63    message: Option<&str>,
64) -> String {
65    let Some(tid) = task_id else {
66        return "Error: task_id is required".to_string();
67    };
68    let new_state = match state {
69        Some(s) => match TaskState::parse_str(s) {
70            Some(st) => st,
71            None => {
72                return format!(
73                    "Error: invalid state '{s}'. Use: working, input-required, completed, failed, canceled"
74                );
75            }
76        },
77        None => return "Error: state is required for update".to_string(),
78    };
79
80    let Some(task) = store.get_task_mut(tid) else {
81        return format!("Error: task '{tid}' not found");
82    };
83
84    if task.to_agent != agent && task.from_agent != agent {
85        return format!("Error: agent '{agent}' is not involved in task '{tid}'");
86    }
87
88    match task.transition(new_state.clone(), message) {
89        Ok(()) => {
90            if let Some(msg) = message {
91                task.add_message(
92                    agent,
93                    vec![TaskPart::Text {
94                        text: msg.to_string(),
95                    }],
96                );
97            }
98            format!(
99                "Task {tid} updated → {new_state}\n  History: {} transitions",
100                task.history.len()
101            )
102        }
103        Err(e) => format!("Error: {e}"),
104    }
105}
106
107fn handle_list(store: &TaskStore, agent: &str) -> String {
108    let tasks = store.tasks_for_agent(agent);
109    if tasks.is_empty() {
110        return "No tasks found for this agent.".to_string();
111    }
112
113    let mut lines = vec![format!("Tasks ({}):", tasks.len())];
114    for task in &tasks {
115        let direction = if task.from_agent == agent {
116            format!("→ {}", task.to_agent)
117        } else {
118            format!("← {}", task.from_agent)
119        };
120        lines.push(format!(
121            "  {} [{}] {} — {}",
122            task.id, task.state, direction, task.description
123        ));
124    }
125
126    let pending = store.pending_tasks_for(agent);
127    if !pending.is_empty() {
128        lines.push(format!(
129            "\n{} pending task(s) assigned to you.",
130            pending.len()
131        ));
132    }
133
134    lines.join("\n")
135}
136
137fn handle_get(store: &TaskStore, task_id: Option<&str>) -> String {
138    let Some(tid) = task_id else {
139        return "Error: task_id is required".to_string();
140    };
141    let Some(task) = store.get_task(tid) else {
142        return format!("Error: task '{tid}' not found");
143    };
144
145    let mut lines = vec![
146        format!("Task: {}", task.id),
147        format!("  State: {}", task.state),
148        format!("  From: {}", task.from_agent),
149        format!("  To: {}", task.to_agent),
150        format!("  Description: {}", task.description),
151        format!("  Created: {}", task.created_at),
152        format!("  Updated: {}", task.updated_at),
153        format!("  Messages: {}", task.messages.len()),
154        format!("  Artifacts: {}", task.artifacts.len()),
155    ];
156
157    if !task.history.is_empty() {
158        lines.push("  History:".to_string());
159        for t in &task.history {
160            lines.push(format!(
161                "    {} → {} ({})",
162                t.from,
163                t.to,
164                t.reason.as_deref().unwrap_or("-")
165            ));
166        }
167    }
168
169    lines.join("\n")
170}
171
172fn handle_cancel(
173    store: &mut TaskStore,
174    agent: &str,
175    task_id: Option<&str>,
176    reason: Option<&str>,
177) -> String {
178    let Some(tid) = task_id else {
179        return "Error: task_id is required".to_string();
180    };
181    let Some(task) = store.get_task_mut(tid) else {
182        return format!("Error: task '{tid}' not found");
183    };
184
185    if task.from_agent != agent {
186        return format!(
187            "Error: only the task creator can cancel (creator: {})",
188            task.from_agent
189        );
190    }
191
192    match task.transition(TaskState::Canceled, reason) {
193        Ok(()) => format!("Task {tid} canceled."),
194        Err(e) => format!("Error: {e}"),
195    }
196}
197
198fn handle_message(
199    store: &mut TaskStore,
200    agent: &str,
201    task_id: Option<&str>,
202    message: Option<&str>,
203) -> String {
204    let Some(tid) = task_id else {
205        return "Error: task_id is required".to_string();
206    };
207    let Some(msg) = message else {
208        return "Error: message is required".to_string();
209    };
210    let Some(task) = store.get_task_mut(tid) else {
211        return format!("Error: task '{tid}' not found");
212    };
213
214    task.add_message(
215        agent,
216        vec![TaskPart::Text {
217            text: msg.to_string(),
218        }],
219    );
220    format!(
221        "Message added to task {tid} ({} messages total)",
222        task.messages.len()
223    )
224}
225
226fn handle_info(store: &TaskStore) -> String {
227    let total = store.tasks.len();
228    let active = store
229        .tasks
230        .iter()
231        .filter(|t| !t.state.is_terminal())
232        .count();
233    let completed = store
234        .tasks
235        .iter()
236        .filter(|t| t.state == TaskState::Completed)
237        .count();
238    let failed = store
239        .tasks
240        .iter()
241        .filter(|t| t.state == TaskState::Failed)
242        .count();
243
244    format!("Task Store: {total} total, {active} active, {completed} completed, {failed} failed")
245}