use std::time::Duration;
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::{json, Value};
use crate::error::{Error, Result};
use crate::tools::{Tool, ToolContext};
pub const CURRENT_TIME: &str = "current_time";
pub const SLEEP: &str = "sleep";
pub const MAX_SLEEP_SECS: f64 = 14_400.0;
#[cfg(unix)]
fn local_offset_seconds(unix_secs: i64) -> Option<i64> {
unsafe {
let t = unix_secs as libc::time_t;
let mut tm: libc::tm = std::mem::zeroed();
if libc::localtime_r(&t, &mut tm).is_null() {
return None;
}
Some(tm.tm_gmtoff as i64)
}
}
#[cfg(not(unix))]
fn local_offset_seconds(_unix_secs: i64) -> Option<i64> {
None
}
fn format_offset(seconds: i64) -> String {
let sign = if seconds < 0 { '-' } else { '+' };
let abs = seconds.abs();
format!("{sign}{:02}:{:02}", abs / 3600, (abs % 3600) / 60)
}
fn timezone_name() -> Option<String> {
if let Ok(tz) = std::env::var("TZ") {
let tz = tz.trim().trim_start_matches(':');
if !tz.is_empty() {
return Some(tz.to_string());
}
}
let link = std::fs::read_link("/etc/localtime").ok()?;
let text = link.to_string_lossy();
let zone = text.split_once("zoneinfo/").map(|(_, z)| z)?;
if zone.is_empty() {
None
} else {
Some(zone.to_string())
}
}
#[derive(Debug, Default)]
pub struct CurrentTimeTool;
#[derive(Debug, serde::Serialize)]
struct CurrentTime {
utc: String,
unix_ms: i64,
#[serde(skip_serializing_if = "Option::is_none")]
timezone: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
utc_offset: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
local: Option<String>,
}
#[async_trait]
impl Tool for CurrentTimeTool {
fn name(&self) -> &str {
CURRENT_TIME
}
fn description(&self) -> &str {
"Return the current date and time (ISO-8601 UTC, plus the local timezone and offset \
where the platform reports them). Use it instead of assuming the date — a session \
can be resumed days after it started."
}
fn parameters(&self) -> Value {
json!({"type": "object", "properties": {}, "additionalProperties": false})
}
fn structured_output(&self) -> bool {
true
}
async fn execute(&self, _args: Value, _ctx: &ToolContext) -> Result<String> {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
let offset = local_offset_seconds(now_ms.div_euclid(1000));
let payload = CurrentTime {
utc: supercode_interchange::sidecar::ms_to_rfc3339(now_ms),
unix_ms: now_ms,
timezone: timezone_name(),
utc_offset: offset.map(format_offset),
local: offset.map(|o| {
let shifted = supercode_interchange::sidecar::ms_to_rfc3339(now_ms + o * 1000);
format!("{}{}", shifted.trim_end_matches('Z'), format_offset(o))
}),
};
serde_json::to_string(&payload).map_err(|e| Error::tool(self.name(), e.to_string()))
}
}
#[derive(Debug, Deserialize)]
struct SleepArgs {
seconds: f64,
#[serde(default)]
reason: Option<String>,
}
#[derive(Debug, Default)]
pub struct SleepTool;
#[async_trait]
impl Tool for SleepTool {
fn name(&self) -> &str {
SLEEP
}
fn description(&self) -> &str {
"Pause for a number of seconds before continuing (for example, while waiting on a \
background job or a rate limit). The pause is bounded and is cancelled if the turn \
is interrupted."
}
fn parameters(&self) -> Value {
json!({
"type": "object",
"properties": {
"seconds": {
"type": "number",
"minimum": 0,
"maximum": MAX_SLEEP_SECS,
"description": "How long to pause, in seconds."
},
"reason": {
"type": "string",
"description": "Optional note about what is being waited for."
}
},
"required": ["seconds"],
"additionalProperties": false
})
}
async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<String> {
let a: SleepArgs = serde_json::from_value(args).map_err(|e| Error::InvalidArguments {
tool: self.name().to_string(),
message: e.to_string(),
})?;
if !a.seconds.is_finite() || a.seconds < 0.0 {
return Err(Error::InvalidArguments {
tool: self.name().to_string(),
message: "seconds must be a non-negative number".to_string(),
});
}
if a.seconds > MAX_SLEEP_SECS {
return Err(Error::InvalidArguments {
tool: self.name().to_string(),
message: format!(
"seconds must be at most {MAX_SLEEP_SECS} (asked for {}); sleep again if \
you genuinely need longer",
a.seconds
),
});
}
tokio::time::sleep(Duration::from_secs_f64(a.seconds)).await;
Ok(match a.reason {
Some(reason) if !reason.trim().is_empty() => {
format!("Slept {} s ({reason}).", a.seconds)
}
_ => format!("Slept {} s.", a.seconds),
})
}
}