1use std::sync::Arc;
12
13use schemars::JsonSchema;
14use serde::Deserialize;
15use zeph_common::{ClockSource, SystemClock, ToolName};
16
17use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput, deserialize_params};
18use crate::registry::{InvocationHint, ToolDef};
19
20const TOOL_NAME: &str = "get_current_time";
21
22const TOOL_DESCRIPTION: &str = "Returns the current UTC date and time. Use this whenever you \
23need to reason about \"today\", deadlines, or relative dates (\"next Monday\", \"in 3 days\") — \
24never assume the current date from training data. UTC only; convert to a local timezone \
25yourself if the user needs one.";
26
27#[derive(Debug, PartialEq, Eq)]
30enum TimeFormat {
31 Rfc3339,
33 Unix,
35}
36
37#[derive(Debug, Deserialize, JsonSchema)]
38struct TimeParams {
39 #[serde(default)]
41 format: Option<String>,
42}
43
44pub struct GetCurrentTimeExecutor {
50 clock: Arc<dyn ClockSource>,
51}
52
53impl GetCurrentTimeExecutor {
54 #[must_use]
56 pub fn new(clock: Arc<dyn ClockSource>) -> Self {
57 Self { clock }
58 }
59}
60
61impl Default for GetCurrentTimeExecutor {
62 fn default() -> Self {
63 Self::new(Arc::new(SystemClock))
64 }
65}
66
67impl std::fmt::Debug for GetCurrentTimeExecutor {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 f.debug_struct("GetCurrentTimeExecutor")
70 .finish_non_exhaustive()
71 }
72}
73
74impl ToolExecutor for GetCurrentTimeExecutor {
75 async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
76 if call.tool_id != TOOL_NAME {
77 return Ok(None);
78 }
79 let params: TimeParams = deserialize_params(&call.params)?;
80 let now = self.clock.now();
81 let format = match params.format.as_deref() {
82 Some("unix") => TimeFormat::Unix,
83 _ => TimeFormat::Rfc3339,
84 };
85 let summary = match format {
86 TimeFormat::Rfc3339 => zeph_common::timestamp::rfc3339_from(now),
87 TimeFormat::Unix => now
88 .duration_since(std::time::UNIX_EPOCH)
89 .map_or(0, |d| d.as_secs())
90 .to_string(),
91 };
92
93 Ok(Some(ToolOutput {
94 tool_name: ToolName::new(TOOL_NAME),
95 summary,
96 blocks_executed: 1,
97 filter_stats: None,
98 diff: None,
99 streamed: false,
100 terminal_id: None,
101 locations: None,
102 raw_response: None,
103 claim_source: None,
104 ..Default::default()
105 }))
106 }
107
108 fn tool_definitions(&self) -> Vec<ToolDef> {
109 vec![ToolDef {
110 id: TOOL_NAME.into(),
111 description: TOOL_DESCRIPTION.into(),
112 schema: schemars::schema_for!(TimeParams),
113 invocation: InvocationHint::ToolCall,
114 output_schema: None,
115 server_id: None,
116 }]
117 }
118
119 fn is_tool_retryable(&self, _tool_id: &str) -> bool {
120 true
121 }
122
123 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
124 Ok(None)
125 }
126
127 crate::tool_executor_no_inner_defaults!();
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use std::time::{Duration, UNIX_EPOCH};
134 use zeph_common::FixedClock;
135
136 fn make_call(format: Option<&str>) -> ToolCall {
137 let mut params = serde_json::Map::new();
138 if let Some(f) = format {
139 params.insert("format".to_owned(), serde_json::Value::String(f.to_owned()));
140 }
141 ToolCall {
142 tool_id: ToolName::new(TOOL_NAME),
143 params,
144 caller_id: None,
145 context: None,
146 tool_call_id: String::new(),
147 skill_name: None,
148 }
149 }
150
151 fn fixed_executor() -> GetCurrentTimeExecutor {
152 let t = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
153 GetCurrentTimeExecutor::new(Arc::new(FixedClock(t)))
154 }
155
156 #[tokio::test]
157 async fn returns_rfc3339_by_default() {
158 let executor = fixed_executor();
159 let result = executor
160 .execute_tool_call(&make_call(None))
161 .await
162 .unwrap()
163 .unwrap();
164 assert_eq!(result.summary, "2023-11-14T22:13:20Z");
165 }
166
167 #[tokio::test]
168 async fn returns_rfc3339_when_explicitly_requested() {
169 let executor = fixed_executor();
170 let result = executor
171 .execute_tool_call(&make_call(Some("rfc3339")))
172 .await
173 .unwrap()
174 .unwrap();
175 assert_eq!(result.summary, "2023-11-14T22:13:20Z");
176 }
177
178 #[tokio::test]
179 async fn returns_unix_seconds_when_requested() {
180 let executor = fixed_executor();
181 let result = executor
182 .execute_tool_call(&make_call(Some("unix")))
183 .await
184 .unwrap()
185 .unwrap();
186 assert_eq!(result.summary, "1700000000");
187 }
188
189 #[tokio::test]
190 async fn unrecognized_format_falls_back_to_rfc3339() {
191 let executor = fixed_executor();
194 let result = executor
195 .execute_tool_call(&make_call(Some("banana")))
196 .await
197 .unwrap()
198 .unwrap();
199 assert_eq!(result.summary, "2023-11-14T22:13:20Z");
200 }
201
202 #[tokio::test]
203 async fn returns_none_for_unknown_tool() {
204 let executor = fixed_executor();
205 let call = ToolCall {
206 tool_id: ToolName::new("other_tool"),
207 params: serde_json::Map::new(),
208 caller_id: None,
209 context: None,
210 tool_call_id: String::new(),
211 skill_name: None,
212 };
213 assert!(executor.execute_tool_call(&call).await.unwrap().is_none());
214 }
215
216 #[test]
217 fn tool_definitions_contains_get_current_time() {
218 let executor = GetCurrentTimeExecutor::default();
219 let defs = executor.tool_definitions();
220 assert_eq!(defs.len(), 1);
221 assert_eq!(defs[0].id.as_ref(), TOOL_NAME);
222 }
223
224 #[test]
225 fn is_retryable() {
226 let executor = GetCurrentTimeExecutor::default();
227 assert!(executor.is_tool_retryable(TOOL_NAME));
228 }
229}