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 to_agent = match to {
51        Some(t) => t,
52        None => return "Error: to_agent is required for task creation".to_string(),
53    };
54    let description = desc.unwrap_or("(no description)");
55    let id = store.create_task(from, to_agent, description);
56    format!("Task created: {id}\n  From: {from}\n  To: {to_agent}\n  Description: {description}")
57}
58
59fn handle_update(
60    store: &mut TaskStore,
61    agent: &str,
62    task_id: Option<&str>,
63    state: Option<&str>,
64    message: Option<&str>,
65) -> String {
66    let tid = match task_id {
67        Some(id) => id,
68        None => return "Error: task_id is required".to_string(),
69    };
70    let new_state = match state {
71        Some(s) => match TaskState::parse_str(s) {
72            Some(st) => st,
73            None => return format!("Error: invalid state '{s}'. Use: working, input-required, completed, failed, canceled"),
74        },
75        None => return "Error: state is required for update".to_string(),
76    };
77
78    let task = match store.get_task_mut(tid) {
79        Some(t) => t,
80        None => return format!("Error: task '{tid}' not found"),
81    };
82
83    if task.to_agent != agent && task.from_agent != agent {
84        return format!("Error: agent '{agent}' is not involved in task '{tid}'");
85    }
86
87    match task.transition(new_state.clone(), message) {
88        Ok(()) => {
89            if let Some(msg) = message {
90                task.add_message(
91                    agent,
92                    vec![TaskPart::Text {
93                        text: msg.to_string(),
94                    }],
95                );
96            }
97            format!(
98                "Task {tid} updated → {new_state}\n  History: {} transitions",
99                task.history.len()
100            )
101        }
102        Err(e) => format!("Error: {e}"),
103    }
104}
105
106fn handle_list(store: &TaskStore, agent: &str) -> String {
107    let tasks = store.tasks_for_agent(agent);
108    if tasks.is_empty() {
109        return "No tasks found for this agent.".to_string();
110    }
111
112    let mut lines = vec![format!("Tasks ({}):", tasks.len())];
113    for task in &tasks {
114        let direction = if task.from_agent == agent {
115            format!("→ {}", task.to_agent)
116        } else {
117            format!("← {}", task.from_agent)
118        };
119        lines.push(format!(
120            "  {} [{}] {} — {}",
121            task.id, task.state, direction, task.description
122        ));
123    }
124
125    let pending = store.pending_tasks_for(agent);
126    if !pending.is_empty() {
127        lines.push(format!(
128            "\n{} pending task(s) assigned to you.",
129            pending.len()
130        ));
131    }
132
133    lines.join("\n")
134}
135
136fn handle_get(store: &TaskStore, task_id: Option<&str>) -> String {
137    let tid = match task_id {
138        Some(id) => id,
139        None => return "Error: task_id is required".to_string(),
140    };
141    let task = match store.get_task(tid) {
142        Some(t) => t,
143        None => return format!("Error: task '{tid}' not found"),
144    };
145
146    let mut lines = vec![
147        format!("Task: {}", task.id),
148        format!("  State: {}", task.state),
149        format!("  From: {}", task.from_agent),
150        format!("  To: {}", task.to_agent),
151        format!("  Description: {}", task.description),
152        format!("  Created: {}", task.created_at),
153        format!("  Updated: {}", task.updated_at),
154        format!("  Messages: {}", task.messages.len()),
155        format!("  Artifacts: {}", task.artifacts.len()),
156    ];
157
158    if !task.history.is_empty() {
159        lines.push("  History:".to_string());
160        for t in &task.history {
161            lines.push(format!(
162                "    {} → {} ({})",
163                t.from,
164                t.to,
165                t.reason.as_deref().unwrap_or("-")
166            ));
167        }
168    }
169
170    lines.join("\n")
171}
172
173fn handle_cancel(
174    store: &mut TaskStore,
175    agent: &str,
176    task_id: Option<&str>,
177    reason: Option<&str>,
178) -> String {
179    let tid = match task_id {
180        Some(id) => id,
181        None => return "Error: task_id is required".to_string(),
182    };
183    let task = match store.get_task_mut(tid) {
184        Some(t) => t,
185        None => return format!("Error: task '{tid}' not found"),
186    };
187
188    if task.from_agent != agent {
189        return format!(
190            "Error: only the task creator can cancel (creator: {})",
191            task.from_agent
192        );
193    }
194
195    match task.transition(TaskState::Canceled, reason) {
196        Ok(()) => format!("Task {tid} canceled."),
197        Err(e) => format!("Error: {e}"),
198    }
199}
200
201fn handle_message(
202    store: &mut TaskStore,
203    agent: &str,
204    task_id: Option<&str>,
205    message: Option<&str>,
206) -> String {
207    let tid = match task_id {
208        Some(id) => id,
209        None => return "Error: task_id is required".to_string(),
210    };
211    let msg = match message {
212        Some(m) => m,
213        None => return "Error: message is required".to_string(),
214    };
215    let task = match store.get_task_mut(tid) {
216        Some(t) => t,
217        None => return format!("Error: task '{tid}' not found"),
218    };
219
220    task.add_message(
221        agent,
222        vec![TaskPart::Text {
223            text: msg.to_string(),
224        }],
225    );
226    format!(
227        "Message added to task {tid} ({} messages total)",
228        task.messages.len()
229    )
230}
231
232fn handle_info(store: &TaskStore) -> String {
233    let total = store.tasks.len();
234    let active = store
235        .tasks
236        .iter()
237        .filter(|t| !t.state.is_terminal())
238        .count();
239    let completed = store
240        .tasks
241        .iter()
242        .filter(|t| t.state == TaskState::Completed)
243        .count();
244    let failed = store
245        .tasks
246        .iter()
247        .filter(|t| t.state == TaskState::Failed)
248        .count();
249
250    format!("Task Store: {total} total, {active} active, {completed} completed, {failed} failed")
251}