Skip to main content

everruns_builtins/
current_time.rs

1//! CurrentTime Capability - provides tools to get current date and time
2
3use 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
12/// CurrentTime capability - provides tools to get current date and time
13pub 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    /// Contribute the current UTC time as a dynamic fact. The runtime appends
53    /// it to a live `<facts>` block at the conversation tail each turn, so the
54    /// model always knows "now" without a tool round-trip and without the
55    /// changing value invalidating the system-prompt cache. The
56    /// `get_current_time` tool remains for explicit timezone/format queries.
57    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
63// ============================================================================
64// Tool: get_current_time
65// ============================================================================
66
67/// Tool that returns the current date and time
68pub 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(&timestamp),
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}