use crate::command::chat::storage::ChatMessage;
use crate::command::chat::tools::{
PlanDecision, Tool, ToolResult, parse_tool_args, schema_to_tool_params,
};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;
use std::sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
};
use tokio_util::sync::CancellationToken;
const DEFAULT_TIMEOUT_SECS: u64 = 120;
const POLL_INTERVAL_MS: u64 = 100;
#[derive(Deserialize, JsonSchema)]
struct WaitForMessageParams {
#[serde(default = "default_timeout")]
timeout: u64,
#[serde(default)]
from: Option<String>,
#[serde(default)]
keyword: Option<String>,
}
fn default_timeout() -> u64 {
DEFAULT_TIMEOUT_SECS
}
pub struct WaitForMessageTool {
pub pending_user_messages: Arc<Mutex<Vec<ChatMessage>>>,
pub cancel_token: CancellationToken,
}
impl WaitForMessageTool {
pub const NAME: &'static str = "WaitForMessage";
}
impl Tool for WaitForMessageTool {
fn name(&self) -> &str {
Self::NAME
}
fn description(&self) -> &str {
r#"
Block and wait for a message from another agent in the chatroom.
Use this when you need input from a teammate before proceeding.
The tool blocks until a matching message arrives or the timeout expires.
Usage:
- timeout: Max wait time in seconds (default 120). Returns error on timeout.
- from: Optional sender filter. Only messages from this agent will be returned.
Other agents' messages are preserved for your next round.
- keyword: Optional content filter. Only messages containing this keyword will be returned.
Non-matching messages are preserved for your next round.
Examples:
{} // Wait for any message
{"from": "Backend"} // Wait for a message from Backend
{"keyword": "deploy"} // Wait for any message containing "deploy"
{"from": "Main", "keyword": "approved"} // Wait for Main to say "approved"
{"from": "Main", "timeout": 60} // Wait up to 60s for Main's message
IMPORTANT:
- While waiting, you cannot use other tools or respond to messages.
- If you need to do work while waiting, DO NOT call this tool -- stay idle instead.
- After receiving a message, use SendMessage to reply if needed.
- On timeout, consider whether to retry, call WorkDone, or take other action.
"#
}
fn parameters_schema(&self) -> Value {
schema_to_tool_params::<WaitForMessageParams>()
}
fn execute(&self, arguments: &str, cancelled: &Arc<AtomicBool>) -> ToolResult {
let params: WaitForMessageParams = match parse_tool_args(arguments) {
Ok(p) => p,
Err(e) => return e,
};
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(params.timeout);
let poll_interval = std::time::Duration::from_millis(POLL_INTERVAL_MS);
loop {
if cancelled.load(Ordering::Relaxed) || self.cancel_token.is_cancelled() {
return ToolResult {
output: "WaitForMessage cancelled".to_string(),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
if start.elapsed() >= timeout {
return ToolResult {
output: format!(
"WaitForMessage timed out after {}s (no message arrived)",
params.timeout
),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
let drained: Vec<ChatMessage> = match self.pending_user_messages.lock() {
Ok(mut pending) => std::mem::take(&mut *pending),
Err(_) => {
std::thread::sleep(poll_interval);
continue;
}
};
if !drained.is_empty() {
let (matching, non_matching): (Vec<_>, Vec<_>) =
drained.into_iter().partition(|m| {
message_matches(
&m.content,
params.from.as_deref(),
params.keyword.as_deref(),
)
});
if !non_matching.is_empty()
&& let Ok(mut pending) = self.pending_user_messages.lock()
{
let mut combined = non_matching;
combined.append(&mut *pending);
*pending = combined;
}
if !matching.is_empty() {
return ToolResult {
output: format_messages(&matching),
is_error: false,
images: vec![],
plan_decision: PlanDecision::None,
};
}
}
std::thread::sleep(poll_interval);
}
}
fn requires_confirmation(&self) -> bool {
false
}
}
fn message_matches(content: &str, from: Option<&str>, keyword: Option<&str>) -> bool {
if let Some(from) = from {
if from == "Main" {
if !content.starts_with("<Main>") {
return false;
}
} else if !content.starts_with(&format!("<Teammate@{}>", from)) {
return false;
}
}
if let Some(keyword) = keyword
&& !content.contains(keyword)
{
return false;
}
true
}
fn format_messages(messages: &[ChatMessage]) -> String {
messages
.iter()
.map(|m| m.content.clone())
.collect::<Vec<_>>()
.join("\n")
}