use std::sync::Arc;
use async_trait::async_trait;
use serde_json::json;
use super::{schema_object, Tool, ToolResult};
use crate::config::Config;
use crate::internal::channels::channel::deliver_and_store_bot_message;
use crate::internal::channels::channel_adapter::ChannelRegistry;
use crate::internal::storage::db::{call_blocking, Database};
use crate::internal::tool_runtime::runtime::auth_context_from_input;
use microclaw_core::llm_types::ToolDefinition;
pub struct ReportProgressTool {
config: Config,
registry: Arc<ChannelRegistry>,
db: Arc<Database>,
}
impl ReportProgressTool {
pub fn new(config: &Config, registry: Arc<ChannelRegistry>, db: Arc<Database>) -> Self {
ReportProgressTool {
config: config.clone(),
registry,
db,
}
}
}
#[async_trait]
impl Tool for ReportProgressTool {
fn name(&self) -> &str {
"report_progress"
}
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "report_progress".into(),
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(),
input_schema: schema_object(
json!({
"progress": {
"type": "string",
"description": "One-line status update on what's done and what's left."
},
"pct": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"description": "Optional rough completion percentage (0-100)."
}
}),
&["progress"],
),
}
}
async fn execute(&self, input: serde_json::Value) -> ToolResult {
let run_id = input
.get("__subagent_runtime")
.and_then(|v| v.get("run_id"))
.and_then(|v| v.as_str())
.map(str::to_string);
let Some(run_id) = run_id else {
return ToolResult::error(
"report_progress is only available inside a sub-agent run".into(),
);
};
let progress = input
.get("progress")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
if progress.is_empty() {
return ToolResult::error("Missing required parameter: progress".into());
}
let pct = input
.get("pct")
.and_then(|v| v.as_i64())
.map(|p| p.clamp(0, 100));
let Some(auth) = auth_context_from_input(&input) else {
return ToolResult::error("report_progress requires caller auth context".into());
};
let chat_id = auth.caller_chat_id;
let run_id_for_record = run_id.clone();
let progress_for_record = progress.clone();
let prev_progress_at = match call_blocking(self.db.clone(), move |db| {
db.record_subagent_progress(&run_id_for_record, &progress_for_record)
})
.await
{
Ok(v) => v,
Err(e) => return ToolResult::error(format!("Failed recording progress: {e}")),
};
if !self.config.subagents.progress_reports
|| auth.caller_channel == "weixin"
|| auth.caller_channel.starts_with("weixin.")
{
return ToolResult::success(
json!({"status": "recorded", "delivered": false, "reason": "disabled"}).to_string(),
);
}
let min_interval = self.config.subagents.progress_min_interval_secs as i64;
let throttled = prev_progress_at
.as_deref()
.and_then(|ts| chrono::DateTime::parse_from_rfc3339(ts).ok())
.map(|prev| {
(chrono::Utc::now() - prev.with_timezone(&chrono::Utc)).num_seconds() < min_interval
})
.unwrap_or(false);
if throttled {
return ToolResult::success(
json!({"status": "recorded", "delivered": false, "reason": "throttled"})
.to_string(),
);
}
let run_id_for_label = run_id.clone();
let label = call_blocking(self.db.clone(), move |db| {
db.get_subagent_run(&run_id_for_label, chat_id)
})
.await
.ok()
.flatten()
.and_then(|r| r.label);
let header = match label {
Some(l) => format!("📊 [{l}]"),
None => "📊 progress".to_string(),
};
let pct_str = pct.map(|p| format!(" ({p}%)")).unwrap_or_default();
let message = format!("{header}{pct_str}: {progress}");
let bot_username = self.config.bot_username_for_channel(&auth.caller_channel);
match deliver_and_store_bot_message(
self.registry.as_ref(),
self.db.clone(),
&bot_username,
chat_id,
&message,
)
.await
{
Ok(_) => {
ToolResult::success(json!({"status": "recorded", "delivered": true}).to_string())
}
Err(e) => ToolResult::success(
json!({"status": "recorded", "delivered": false, "error": e}).to_string(),
),
}
}
}