use std::fmt::Write as _;
use anyhow::Result;
use async_trait::async_trait;
use serde_json::json;
use crate::Workspace;
use crate::agent::{CURRENT_TOOL_AGENT_ID, CURRENT_TOOL_USER_NAME};
use crate::alarms::{add_alarm, format_fire_time, list_alarms, remove_alarm};
use crate::tools::Tool;
struct AssistantIdentity {
agent_id: String,
user_name: String,
}
fn identity() -> Result<AssistantIdentity> {
let user_name = CURRENT_TOOL_USER_NAME
.try_with(String::clone)
.unwrap_or_default();
let agent_id = CURRENT_TOOL_AGENT_ID
.try_with(Option::clone)
.ok()
.flatten()
.unwrap_or_default();
anyhow::ensure!(
!agent_id.is_empty() && !user_name.is_empty(),
"No agent identity available to associate the alarm — the Assistant must run in a user session."
);
Ok(AssistantIdentity {
agent_id,
user_name,
})
}
pub struct AddAlarmTool;
#[async_trait]
impl Tool for AddAlarmTool {
fn name(&self) -> &'static str {
"add_alarm"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(
&json!({
"text": {
"type": "string",
"description": "The reminder text to store"
},
"fire_at": {
"type": "string",
"description": "RFC3339/ISO-8601 absolute fire time in UTC (e.g. 2026-08-28T10:30:00Z). Convert user-provided local times to UTC."
},
"interval_seconds": {
"type": "integer",
"description": "Periodic interval in seconds (minimum 10). Exactly one of fire_at or interval_seconds must be provided."
}
}),
&["text"],
)
}
async fn execute(&self, _ws: &Workspace, args: serde_json::Value) -> Result<String> {
let ident = identity()?;
let text = super::get_str(&args, "text")?;
let fire_at = super::get_opt_str(&args, "fire_at");
let interval_seconds = super::get_opt_u64(&args, "interval_seconds");
let alarm = add_alarm(
&ident.agent_id,
&ident.user_name,
text,
fire_at,
interval_seconds,
)
.await?;
let display = format_fire_time(&alarm.next_fire_at)?;
if let Some(interval) = alarm.interval_seconds {
Ok(format!(
"Alarm scheduled every {interval} seconds. Next fire at {display}.\nText: {text}"
))
} else {
Ok(format!("Alarm set for {display}.\nText: {text}"))
}
}
}
pub struct ListAlarmsTool;
#[async_trait]
impl Tool for ListAlarmsTool {
fn name(&self) -> &'static str {
"list_alarms"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(&json!({}), &[])
}
fn side_effects(&self) -> bool {
false }
async fn execute(&self, _ws: &Workspace, _args: serde_json::Value) -> Result<String> {
let session_id = identity()?.agent_id;
let alarms = list_alarms(&session_id).await?;
if alarms.is_empty() {
return Ok("(no active alarms)".to_string());
}
let mut out = String::new();
for alarm in alarms {
let display = format_fire_time(&alarm.next_fire_at)?;
let _ = writeln!(
out,
"- {}, {}, {}, next fire: {}",
alarm.id, alarm.kind, alarm.text, display
);
}
Ok(out.trim().to_string())
}
}
pub struct RemoveAlarmTool;
#[async_trait]
impl Tool for RemoveAlarmTool {
fn name(&self) -> &'static str {
"remove_alarm"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(
&json!({
"id": {
"type": "string",
"description": "The alarm id to remove/stop"
}
}),
&["id"],
)
}
async fn execute(&self, _ws: &Workspace, args: serde_json::Value) -> Result<String> {
let session_id = identity()?.agent_id;
let id = super::get_str(&args, "id")?;
match remove_alarm(&session_id, id).await? {
Some(alarm) => {
let display = format_fire_time(&alarm.next_fire_at)?;
Ok(format!("Removed alarm {id} (was set for {display})."))
}
None => Ok("No active alarm with that id.".to_string()),
}
}
}