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