Skip to content

Commit a9a04a8

Browse files
committed
Add local time and timezone
1 parent 6ad3499 commit a9a04a8

13 files changed

Lines changed: 73 additions & 16 deletions

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ regex = "1.11"
2323
serde_json = "1.0"
2424
# Date for today + recent daily notes (YYYYMMDD)
2525
time = { version = "0.3", default-features = false, features = ["formatting", "std"] }
26+
# IANA timezone support for local-time system prompt line (DST-aware)
27+
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
28+
chrono-tz = { version = "0.10", default-features = false }
2629

2730
[dev-dependencies]
2831
wiremock = "0.6"

config.example.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ model = "YOUR_MODEL"
1717
[heartbeat]
1818
interval-minutes = 30
1919

20+
# Your IANA timezone name — used for local time in the agent prompt.
21+
# Handles DST automatically; no need to update when clocks change.
22+
# Default if absent: Europe/London.
23+
# timezone = "Europe/London"
24+
2025
# Optional: Brave web search. Leave commented or set ICRAB_TOOLS_WEB_BRAVE_API_KEY in env.
2126
# [tools.web]
2227
# brave-api-key = "YOUR_BRAVE_API_KEY"

src/agent.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ pub async fn process_message(
159159
registry: &ToolRegistry,
160160
workspace_path: &Path,
161161
model: &str,
162+
timezone: &str,
162163
chat_id: &str,
163164
user_message: &str,
164165
tool_ctx: &ToolCtx,
@@ -179,6 +180,7 @@ pub async fn process_message(
179180
let today = crate::workspace::today_yyyymmdd();
180181
let messages = build_messages(
181182
workspace_path,
183+
timezone,
182184
session.history(),
183185
session.summary(),
184186
user_message,
@@ -208,6 +210,7 @@ pub async fn process_heartbeat_message(
208210
registry: &ToolRegistry,
209211
workspace_path: &Path,
210212
model: &str,
213+
timezone: &str,
211214
chat_id: &str,
212215
user_message: &str,
213216
tool_ctx: &ToolCtx,
@@ -217,6 +220,7 @@ pub async fn process_heartbeat_message(
217220
let today = crate::workspace::today_yyyymmdd();
218221
let messages = build_messages(
219222
workspace_path,
223+
timezone,
220224
&[],
221225
"",
222226
user_message,

src/agent/context.rs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use std::path::Path;
44

5-
use time::OffsetDateTime;
5+
use chrono::Offset as _;
66

77
use crate::llm::{Message, Role};
88
use crate::workspace;
@@ -13,6 +13,7 @@ use crate::workspace;
1313
#[allow(clippy::too_many_arguments)]
1414
pub fn build_messages(
1515
workspace_path: &Path,
16+
timezone: &str,
1617
history: &[Message],
1718
summary: &str,
1819
user_message: &str,
@@ -23,15 +24,30 @@ pub fn build_messages(
2324
) -> Vec<Message> {
2425
let mut system = String::new();
2526

26-
// Identity: current date/time (human-readable + timezone) and Unix, workspace
27-
let now = OffsetDateTime::now_utc();
28-
let now_unix = now.unix_timestamp();
27+
// Identity: current local date/time with UTC offset, Unix timestamp, workspace
28+
let tz: chrono_tz::Tz = timezone
29+
.parse()
30+
.expect("timezone was validated at startup; parse cannot fail here");
31+
let now_utc = chrono::Utc::now();
32+
let now_local = now_utc.with_timezone(&tz);
33+
let offset_secs = now_local.offset().fix().local_minus_utc();
34+
let offset_hours = offset_secs / 3600;
35+
let offset_str = if offset_hours >= 0 {
36+
format!("UTC+{}", offset_hours)
37+
} else {
38+
format!("UTC{}", offset_hours)
39+
};
40+
let now_unix = now_utc.timestamp();
41+
let time_line = format!(
42+
"Current time ({}, local): {} ({}). Unix: {}.",
43+
offset_str,
44+
now_local.format("%A %-d %b %Y %H:%M"),
45+
timezone,
46+
now_unix,
47+
);
2948
system.push_str("You are iCrab, a minimal personal AI assistant. ");
30-
system.push_str("Current time: ");
31-
system.push_str(&format!("{} {} {} UTC. ", now.weekday(), now.date(), now.time()));
32-
system.push_str("Unix: ");
33-
system.push_str(&now_unix.to_string());
34-
system.push_str(". Workspace: ");
49+
system.push_str(&time_line);
50+
system.push_str(" Workspace: ");
3551
system.push_str(workspace_path.to_string_lossy().as_ref());
3652
system.push_str(".\n\n");
3753

@@ -127,6 +143,7 @@ mod tests {
127143
let workspace = std::env::temp_dir();
128144
let messages = build_messages(
129145
&workspace,
146+
"Europe/London",
130147
&[],
131148
"",
132149
"hello",
@@ -137,12 +154,8 @@ mod tests {
137154
);
138155
let system = &messages[0].content;
139156
assert!(
140-
system.contains("Current time:"),
141-
"system prompt should include 'Current time:'"
142-
);
143-
assert!(
144-
system.contains(" UTC."),
145-
"system prompt should include ' UTC.'"
157+
system.contains("Current time (UTC"),
158+
"system prompt should include 'Current time (UTC'"
146159
);
147160
assert!(
148161
system.contains("Unix: "),

src/agent/subagent_manager.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ mod tests {
357357
}),
358358
tools: None,
359359
heartbeat: None,
360+
timezone: None,
360361
};
361362
HttpProvider::from_config(&cfg).expect("stub provider")
362363
}

src/config.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! Single file: `~/.icrab/config.toml`. Override path with `ICRAB_CONFIG`.
44
//! Env overrides (optional): `TELEGRAM_BOT_TOKEN` or `ICRAB_TELEGRAM_BOT_TOKEN`,
55
//! `ICRAB_LLM_API_KEY`, `ICRAB_LLM_API_BASE`, `ICRAB_LLM_MODEL`, `ICRAB_WORKSPACE`,
6-
//! `ICRAB_TOOLS_WEB_BRAVE_API_KEY`.
6+
//! `ICRAB_TOOLS_WEB_BRAVE_API_KEY`, `ICRAB_TIMEZONE`.
77
88
use std::path::PathBuf;
99

@@ -20,6 +20,8 @@ pub struct Config {
2020
pub tools: Option<ToolsConfig>,
2121
pub heartbeat: Option<HeartbeatConfig>,
2222
pub restrict_to_workspace: Option<bool>,
23+
/// IANA timezone name (e.g. "Europe/London"). Default when absent: "Europe/London".
24+
pub timezone: Option<String>,
2325
}
2426

2527
#[derive(Debug, Clone, Default, Deserialize)]
@@ -147,6 +149,9 @@ pub fn load(path: &std::path::Path) -> Result<Config, ConfigError> {
147149
let web = tools.web.get_or_insert_with(WebConfig::default);
148150
web.brave_api_key = Some(v);
149151
}
152+
if let Ok(v) = std::env::var("ICRAB_TIMEZONE") {
153+
cfg.timezone = Some(v);
154+
}
150155

151156
cfg.validate()?;
152157
Ok(cfg)
@@ -187,6 +192,14 @@ impl Config {
187192
"llm section is required".to_string(),
188193
));
189194
}
195+
if let Some(ref tz) = self.timezone {
196+
tz.parse::<chrono_tz::Tz>().map_err(|_| {
197+
ConfigError::Validation(format!(
198+
"timezone '{}' is not a valid IANA timezone (e.g. Europe/London, America/New_York)",
199+
tz
200+
))
201+
})?;
202+
}
190203
Ok(())
191204
}
192205

src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ async fn main() {
4949
.unwrap_or("google/gemini-3-flash-preview");
5050
let workspace = PathBuf::from(cfg.workspace_path());
5151
let restrict = cfg.restrict_to_workspace.unwrap_or(true);
52+
let timezone = cfg
53+
.timezone
54+
.as_deref()
55+
.unwrap_or("Europe/London")
56+
.to_string();
5257

5358
// Build subagent registry (core only — no spawn, no cron).
5459
let subagent_registry = Arc::new(tools::build_core_registry(&cfg));
@@ -128,6 +133,7 @@ async fn main() {
128133
&registry,
129134
&workspace,
130135
model,
136+
&timezone,
131137
&chat_id_str,
132138
&msg.text,
133139
&tool_ctx,
@@ -146,6 +152,7 @@ async fn main() {
146152
&registry,
147153
&workspace,
148154
model,
155+
&timezone,
149156
&chat_id_str,
150157
&msg.text,
151158
&tool_ctx,

src/tools/spawn.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ mod tests {
149149
}),
150150
tools: None,
151151
heartbeat: None,
152+
timezone: None,
152153
};
153154
let llm = crate::llm::HttpProvider::from_config(&cfg).expect("stub");
154155
SubagentManager::new(

src/tools/subagent.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ mod tests {
222222
}),
223223
tools: None,
224224
heartbeat: None,
225+
timezone: None,
225226
};
226227
// This might fail if Config::validate() checks paths, but here we just need types.
227228
// Actually HttpProvider::from_config might check stuff.

tests/agent_tests.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ async fn test_agent_basic_flow() {
4747
&registry,
4848
&ws.root,
4949
"gpt-4-test",
50+
"Europe/London",
5051
"chat_basic",
5152
"Hi",
5253
&ctx,
@@ -149,6 +150,7 @@ async fn test_agent_tool_use_loop() {
149150
&registry,
150151
&ws.root,
151152
"gpt-4-test",
153+
"Europe/London",
152154
"chat_tool",
153155
"Write file test.txt with success",
154156
&ctx,
@@ -206,6 +208,7 @@ async fn test_agent_session_load_on_restart() {
206208
&registry,
207209
&ws.root,
208210
"gpt-4-test",
211+
"Europe/London",
209212
"chat_restart",
210213
"First",
211214
&ctx,
@@ -241,6 +244,7 @@ async fn test_agent_session_load_on_restart() {
241244
&registry,
242245
&ws.root,
243246
"gpt-4-test",
247+
"Europe/London",
244248
"chat_restart",
245249
"Second",
246250
&ctx,
@@ -318,6 +322,7 @@ async fn test_agent_unknown_tool_completes_with_error_in_conversation() {
318322
&registry,
319323
&ws.root,
320324
"gpt-4-test",
325+
"Europe/London",
321326
"chat_unknown_tool",
322327
"Use nonexistent tool",
323328
&ctx,
@@ -389,6 +394,7 @@ async fn test_agent_invalid_tool_args_completes_with_error_in_conversation() {
389394
&registry,
390395
&ws.root,
391396
"gpt-4-test",
397+
"Europe/London",
392398
"chat_bad_args",
393399
"Read file foo.txt",
394400
&ctx,

0 commit comments

Comments
 (0)