microclaw_engine/tools/
report_progress.rs1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde_json::json;
5
6use super::{schema_object, Tool, ToolResult};
7use crate::config::Config;
8use crate::internal::channels::channel::deliver_and_store_bot_message;
9use crate::internal::channels::channel_adapter::ChannelRegistry;
10use crate::internal::storage::db::{call_blocking, Database};
11use crate::internal::tool_runtime::runtime::auth_context_from_input;
12use microclaw_core::llm_types::ToolDefinition;
13
14pub struct ReportProgressTool {
18 config: Config,
19 registry: Arc<ChannelRegistry>,
20 db: Arc<Database>,
21}
22
23impl ReportProgressTool {
24 pub fn new(config: &Config, registry: Arc<ChannelRegistry>, db: Arc<Database>) -> Self {
25 ReportProgressTool {
26 config: config.clone(),
27 registry,
28 db,
29 }
30 }
31}
32
33#[async_trait]
34impl Tool for ReportProgressTool {
35 fn name(&self) -> &str {
36 "report_progress"
37 }
38
39 fn definition(&self) -> ToolDefinition {
40 ToolDefinition {
41 name: "report_progress".into(),
42 description: "Post a short progress update to the chat while you work on a long task (e.g. \"checked 3/5 sources, two left\"). Use it at meaningful milestones so the user gets a colleague-style status; keep each update to one line. Updates are throttled to avoid spam. Only available inside a sub-agent run.".into(),
43 input_schema: schema_object(
44 json!({
45 "progress": {
46 "type": "string",
47 "description": "One-line status update on what's done and what's left."
48 },
49 "pct": {
50 "type": "integer",
51 "minimum": 0,
52 "maximum": 100,
53 "description": "Optional rough completion percentage (0-100)."
54 }
55 }),
56 &["progress"],
57 ),
58 }
59 }
60
61 async fn execute(&self, input: serde_json::Value) -> ToolResult {
62 let run_id = input
63 .get("__subagent_runtime")
64 .and_then(|v| v.get("run_id"))
65 .and_then(|v| v.as_str())
66 .map(str::to_string);
67 let Some(run_id) = run_id else {
68 return ToolResult::error(
69 "report_progress is only available inside a sub-agent run".into(),
70 );
71 };
72
73 let progress = input
74 .get("progress")
75 .and_then(|v| v.as_str())
76 .unwrap_or("")
77 .trim()
78 .to_string();
79 if progress.is_empty() {
80 return ToolResult::error("Missing required parameter: progress".into());
81 }
82 let pct = input
83 .get("pct")
84 .and_then(|v| v.as_i64())
85 .map(|p| p.clamp(0, 100));
86
87 let Some(auth) = auth_context_from_input(&input) else {
88 return ToolResult::error("report_progress requires caller auth context".into());
89 };
90 let chat_id = auth.caller_chat_id;
91
92 let run_id_for_record = run_id.clone();
95 let progress_for_record = progress.clone();
96 let prev_progress_at = match call_blocking(self.db.clone(), move |db| {
97 db.record_subagent_progress(&run_id_for_record, &progress_for_record)
98 })
99 .await
100 {
101 Ok(v) => v,
102 Err(e) => return ToolResult::error(format!("Failed recording progress: {e}")),
103 };
104
105 if !self.config.subagents.progress_reports
108 || auth.caller_channel == "weixin"
109 || auth.caller_channel.starts_with("weixin.")
110 {
111 return ToolResult::success(
112 json!({"status": "recorded", "delivered": false, "reason": "disabled"}).to_string(),
113 );
114 }
115
116 let min_interval = self.config.subagents.progress_min_interval_secs as i64;
117 let throttled = prev_progress_at
118 .as_deref()
119 .and_then(|ts| chrono::DateTime::parse_from_rfc3339(ts).ok())
120 .map(|prev| {
121 (chrono::Utc::now() - prev.with_timezone(&chrono::Utc)).num_seconds() < min_interval
122 })
123 .unwrap_or(false);
124
125 if throttled {
126 return ToolResult::success(
127 json!({"status": "recorded", "delivered": false, "reason": "throttled"})
128 .to_string(),
129 );
130 }
131
132 let run_id_for_label = run_id.clone();
134 let label = call_blocking(self.db.clone(), move |db| {
135 db.get_subagent_run(&run_id_for_label, chat_id)
136 })
137 .await
138 .ok()
139 .flatten()
140 .and_then(|r| r.label);
141
142 let header = match label {
143 Some(l) => format!("📊 [{l}]"),
144 None => "📊 progress".to_string(),
145 };
146 let pct_str = pct.map(|p| format!(" ({p}%)")).unwrap_or_default();
147 let message = format!("{header}{pct_str}: {progress}");
148
149 let bot_username = self.config.bot_username_for_channel(&auth.caller_channel);
150 match deliver_and_store_bot_message(
151 self.registry.as_ref(),
152 self.db.clone(),
153 &bot_username,
154 chat_id,
155 &message,
156 )
157 .await
158 {
159 Ok(_) => {
160 ToolResult::success(json!({"status": "recorded", "delivered": true}).to_string())
161 }
162 Err(e) => ToolResult::success(
165 json!({"status": "recorded", "delivered": false, "error": e}).to_string(),
166 ),
167 }
168 }
169}