robit_agent/tool/
query_task.rs1use async_trait::async_trait;
14use serde::Deserialize;
15use serde_json::{json, Value};
16
17use super::task_registry::{AsyncTaskRecord, AsyncTaskStatus, TaskRegistry};
18use super::{Tool, ToolContext, ToolResult};
19use crate::error::Result;
20
21#[derive(Debug, Deserialize)]
22struct QueryTaskArgs {
23 #[serde(default)]
24 task_id: Option<String>,
25}
26
27pub struct QueryTaskTool;
28
29impl QueryTaskTool {
30 pub fn new() -> Self {
31 Self
32 }
33}
34
35#[async_trait]
36impl Tool for QueryTaskTool {
37 fn name(&self) -> &str {
38 "query_task"
39 }
40
41 fn description(&self) -> &str {
42 "Query the status of async background tasks (e.g. image generation). \
43 Pass a `task_id` to inspect one task, or omit it to list all currently \
44 running (pending) tasks. Final results of completed tasks are already \
45 delivered to you as system-notification messages; use this tool only to \
46 check what is still in progress or to re-read a task's final status."
47 }
48
49 fn parameters_schema(&self) -> Value {
50 json!({
51 "type": "object",
52 "properties": {
53 "task_id": {
54 "type": "string",
55 "description": "Optional task id to inspect. Omit to list all pending tasks."
56 }
57 }
58 })
59 }
60
61 fn requires_confirmation(&self) -> bool {
62 false
63 }
64
65 async fn execute(&self, args: Value, ctx: &ToolContext) -> Result<ToolResult> {
66 let parsed: QueryTaskArgs = match serde_json::from_value(args) {
67 Ok(a) => a,
68 Err(e) => return Ok(ToolResult::error(format!("Argument parsing failed: {}", e))),
69 };
70
71 let value = match parsed.task_id {
72 Some(id) => match ctx.task_registry.get(&id) {
73 Some(record) => record_to_json(&record),
74 None => json!({
75 "found": false,
76 "task_id": id,
77 "message": "No task with this id is tracked.",
78 }),
79 },
80 None => pending_list_json(ctx.task_registry.clone()),
81 };
82
83 let content = serde_json::to_string_pretty(&value)
84 .unwrap_or_else(|_| value.to_string());
85 Ok(ToolResult::success(content))
86 }
87}
88
89fn pending_list_json(registry: TaskRegistry) -> Value {
90 let pending = registry.list_pending();
91 json!({
92 "pending_count": pending.len(),
93 "pending": pending.iter().map(brief_json).collect::<Vec<_>>(),
94 })
95}
96
97fn brief_json(r: &AsyncTaskRecord) -> Value {
98 json!({
99 "task_id": r.task_id,
100 "tool": r.tool_name,
101 "status": r.status.as_str(),
102 })
103}
104
105fn record_to_json(r: &AsyncTaskRecord) -> Value {
106 json!({
107 "found": true,
108 "task_id": r.task_id,
109 "tool": r.tool_name,
110 "status": r.status.as_str(),
111 "result": r.result_summary,
112 })
113}
114
115#[allow(dead_code)]
117fn _status_used(s: AsyncTaskStatus) -> &'static str {
118 s.as_str()
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use crate::tool::task_registry::AsyncTaskRecord;
125 use std::time::Instant;
126
127 fn record(id: &str, status: AsyncTaskStatus, summary: Option<&str>) -> AsyncTaskRecord {
128 AsyncTaskRecord {
129 task_id: id.into(),
130 tool_name: "generate_image".into(),
131 tool_call_id: "tc".into(),
132 session_id: "sess".into(),
133 status,
134 started_at: Instant::now(),
135 result_summary: summary.map(String::from),
136 }
137 }
138
139 #[tokio::test]
140 async fn list_pending_empty_returns_count_zero() {
141 let tool = QueryTaskTool::new();
142 let registry = TaskRegistry::new();
143 let ctx = make_ctx(registry);
144 let res = tool
145 .execute(serde_json::json!({}), &ctx)
146 .await
147 .unwrap();
148 assert!(!res.is_error);
149 assert!(res.content.contains("\"pending_count\": 0"));
150 }
151
152 #[tokio::test]
153 async fn list_pending_shows_registered_tasks() {
154 let tool = QueryTaskTool::new();
155 let registry = TaskRegistry::new();
156 registry.register(record("t1", AsyncTaskStatus::Pending, None));
157 registry.register(record("t2", AsyncTaskStatus::Pending, None));
158 let ctx = make_ctx(registry);
159 let res = tool.execute(serde_json::json!({}), &ctx).await.unwrap();
160 assert!(res.content.contains("\"pending_count\": 2"));
161 assert!(res.content.contains("t1"));
162 assert!(res.content.contains("t2"));
163 }
164
165 #[tokio::test]
166 async fn query_specific_task_returns_summary() {
167 let tool = QueryTaskTool::new();
168 let registry = TaskRegistry::new();
169 registry.register(record(
170 "t9",
171 AsyncTaskStatus::Completed,
172 Some("saved 1 image to images/cat.png"),
173 ));
174 let ctx = make_ctx(registry);
175 let res = tool
176 .execute(serde_json::json!({ "task_id": "t9" }), &ctx)
177 .await
178 .unwrap();
179 assert!(res.content.contains("\"found\": true"));
180 assert!(res.content.contains("completed"));
181 assert!(res.content.contains("cat.png"));
182 }
183
184 #[tokio::test]
185 async fn query_unknown_task_reports_not_found() {
186 let tool = QueryTaskTool::new();
187 let ctx = make_ctx(TaskRegistry::new());
188 let res = tool
189 .execute(serde_json::json!({ "task_id": "nope" }), &ctx)
190 .await
191 .unwrap();
192 assert!(res.content.contains("\"found\": false"));
193 }
194
195 fn make_ctx(registry: TaskRegistry) -> ToolContext {
198 use std::path::PathBuf;
199 use std::sync::Arc;
200 use tokio::sync::mpsc;
201 use tokio_util::sync::CancellationToken;
202
203 struct DummyFrontend;
205 #[async_trait]
206 impl crate::frontend::Frontend for DummyFrontend {
207 async fn on_event(&self, _: crate::event::AgentEvent) -> Result<()> {
208 Ok(())
209 }
210 async fn request_tool_confirmation(
211 &self,
212 _: &crate::tool::ToolCallInfo,
213 ) -> Result<bool> {
214 Ok(true)
215 }
216 }
217
218 let (done_tx, _done_rx) = mpsc::channel(1);
219 ToolContext {
220 working_dir: PathBuf::from("."),
221 session_id: "sess".to_string(),
222 tool_call_id: "tc".to_string(),
223 frontend: Arc::new(DummyFrontend),
224 extensions: std::collections::HashMap::new(),
225 supports_images: false,
226 async_runner: crate::tool::async_runner::AsyncTaskRunner::new(done_tx),
227 cancel_token: CancellationToken::new(),
228 task_registry: registry,
229 }
230 }
231}