everruns_builtins/
current_time.rs1use super::{Capability, CapabilityLocalization, CapabilityStatus, Fact, FactsContext};
4use crate::tool_types::ToolHints;
5use crate::tools::{Tool, ToolExecutionResult};
6use async_trait::async_trait;
7use chrono::SecondsFormat;
8use serde_json::Value;
9
10pub const CURRENT_TIME_CAPABILITY_ID: &str = "current_time";
11
12pub struct CurrentTimeCapability;
14
15impl Capability for CurrentTimeCapability {
16 fn id(&self) -> &str {
17 CURRENT_TIME_CAPABILITY_ID
18 }
19
20 fn name(&self) -> &str {
21 "Current Time"
22 }
23
24 fn description(&self) -> &str {
25 "Adds a tool to get the current date and time in various formats and timezones."
26 }
27
28 fn localizations(&self) -> Vec<CapabilityLocalization> {
29 vec![CapabilityLocalization::text(
30 "uk",
31 "Поточний час",
32 "Додає інструмент для отримання поточної дати й часу в різних форматах і часових поясах.",
33 )]
34 }
35
36 fn status(&self) -> CapabilityStatus {
37 CapabilityStatus::Available
38 }
39
40 fn icon(&self) -> Option<&str> {
41 Some("clock")
42 }
43
44 fn category(&self) -> Option<&str> {
45 Some("Core")
46 }
47
48 fn tools(&self) -> Vec<Box<dyn Tool>> {
49 vec![Box::new(GetCurrentTimeTool)]
50 }
51
52 fn facts(&self, _config: &Value, _ctx: &FactsContext) -> Vec<Fact> {
58 let now = chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
59 vec![Fact::dynamic("current_time", now)]
60 }
61}
62
63pub struct GetCurrentTimeTool;
69
70#[async_trait]
71impl Tool for GetCurrentTimeTool {
72 fn narrate(
73 &self,
74 _tool_call: &crate::tool_types::ToolCall,
75 phase: crate::tool_narration::ToolNarrationPhase,
76 locale: Option<&str>,
77 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
78 ) -> Option<String> {
79 Some(crate::tool_narration::narrate_current_time(phase, locale))
80 }
81
82 fn name(&self) -> &str {
83 "get_current_time"
84 }
85
86 fn display_name(&self) -> Option<&str> {
87 Some("Get Current Time")
88 }
89
90 fn description(&self) -> &str {
91 "Get the current date and time. Can return time in different formats and timezones."
92 }
93
94 fn parameters_schema(&self) -> Value {
95 serde_json::json!({
96 "type": "object",
97 "properties": {
98 "timezone": {
99 "type": "string",
100 "description": "Timezone to return the time in (e.g., 'UTC', 'America/New_York', 'Europe/London'). Defaults to UTC."
101 },
102 "format": {
103 "type": "string",
104 "enum": ["iso8601", "unix", "human"],
105 "description": "Output format: 'iso8601' for ISO 8601 format, 'unix' for Unix timestamp, 'human' for human-readable format. Defaults to 'iso8601'."
106 }
107 },
108 "additionalProperties": false
109 })
110 }
111
112 fn hints(&self) -> ToolHints {
113 ToolHints::default()
114 .with_readonly(true)
115 .with_idempotent(true)
116 }
117
118 async fn execute(&self, arguments: Value) -> ToolExecutionResult {
119 let format = arguments
120 .get("format")
121 .and_then(|v| v.as_str())
122 .unwrap_or("iso8601");
123
124 let timezone = arguments
125 .get("timezone")
126 .and_then(|v| v.as_str())
127 .unwrap_or("UTC");
128
129 let timezone: chrono_tz::Tz = match timezone.parse() {
130 Ok(timezone) => timezone,
131 Err(_) => return ToolExecutionResult::tool_error("Unknown IANA timezone"),
132 };
133 let now = chrono::Utc::now().with_timezone(&timezone);
134
135 let result = match format {
136 "unix" => serde_json::json!({
137 "timestamp": now.timestamp(),
138 "format": "unix",
139 "timezone": timezone.name()
140 }),
141 "human" => serde_json::json!({
142 "datetime": now.format("%A, %B %d, %Y at %H:%M:%S %Z").to_string(),
143 "format": "human",
144 "timezone": timezone.name()
145 }),
146 _ => serde_json::json!({
147 "datetime": now.to_rfc3339(),
148 "format": "iso8601",
149 "timezone": timezone.name()
150 }),
151 };
152
153 ToolExecutionResult::success(result)
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use chrono::{DateTime, NaiveDateTime, TimeZone, Utc};
161 use serde_json::json;
162
163 #[test]
164 fn dynamic_fact_is_a_current_utc_instant_without_cached_prompt_text() {
165 use crate::capabilities::Volatility;
166 use crate::typed_id::SessionId;
167 let before = Utc::now().timestamp();
168 let facts = CurrentTimeCapability.facts(&Value::Null, &FactsContext::new(SessionId::new()));
169 let after = Utc::now().timestamp();
170 assert_eq!(facts.len(), 1);
171 assert_eq!(facts[0].key, "current_time");
172 assert_eq!(facts[0].volatility, Volatility::Dynamic);
173 let instant = DateTime::parse_from_rfc3339(&facts[0].value).unwrap();
174 assert_eq!(instant.offset().local_minus_utc(), 0);
175 assert!((before..=after).contains(&instant.timestamp()));
176 assert!(CurrentTimeCapability.system_prompt_addition().is_none());
177 }
178
179 #[tokio::test]
180 async fn registered_time_tool_preserves_instant_across_formats_and_timezones() {
181 let tools = CurrentTimeCapability.tools();
182 assert_eq!(tools.len(), 1);
183 let tool = &tools[0];
184 for (zone, offset) in [
185 ("UTC", 0),
186 ("Asia/Kathmandu", 20700),
187 ("America/Phoenix", -25200),
188 ] {
189 for format in ["iso8601", "unix", "human"] {
190 let before = Utc::now().timestamp();
191 let ToolExecutionResult::Success(value) =
192 tool.execute(json!({"timezone":zone,"format":format})).await
193 else {
194 panic!("valid request failed")
195 };
196 let after = Utc::now().timestamp();
197 let timestamp = match format {
198 "unix" => {
199 assert_eq!(
200 value,
201 json!({"timestamp":value["timestamp"],"format":"unix","timezone":zone})
202 );
203 value["timestamp"].as_i64().unwrap()
204 }
205 "iso8601" => {
206 let parsed =
207 DateTime::parse_from_rfc3339(value["datetime"].as_str().unwrap())
208 .unwrap();
209 assert_eq!(parsed.offset().local_minus_utc(), offset);
210 parsed.timestamp()
211 }
212 "human" => {
213 let text = value["datetime"].as_str().unwrap();
214 let parsed =
215 NaiveDateTime::parse_from_str(text, "%A, %B %d, %Y at %H:%M:%S %Z")
216 .unwrap();
217 let local = zone
218 .parse::<chrono_tz::Tz>()
219 .unwrap()
220 .from_local_datetime(&parsed)
221 .single()
222 .unwrap();
223 assert_eq!(
224 text,
225 local.format("%A, %B %d, %Y at %H:%M:%S %Z").to_string()
226 );
227 local.timestamp()
228 }
229 _ => unreachable!(),
230 };
231 assert!(
232 (before..=after).contains(×tamp),
233 "{format} {zone}: {value}"
234 );
235 if format != "unix" {
236 assert_eq!(
237 value,
238 json!({"datetime":value["datetime"],"format":format,"timezone":zone})
239 );
240 }
241 }
242 }
243 let ToolExecutionResult::Success(default) = tool.execute(json!({})).await else {
244 panic!("default failed")
245 };
246 assert_eq!(default["format"], "iso8601");
247 assert_eq!(default["timezone"], "UTC");
248 assert!(DateTime::parse_from_rfc3339(default["datetime"].as_str().unwrap()).is_ok());
249 let ToolExecutionResult::ToolError(message) =
250 tool.execute(json!({"timezone":"not/a-timezone"})).await
251 else {
252 panic!("unknown timezone must be rejected")
253 };
254 assert_eq!(message, "Unknown IANA timezone");
255 }
256}