use chrono::{Datelike, Duration, NaiveTime, TimeZone, Utc, Weekday}; /// Comprehensive time context for the model. /// All epoch values are milliseconds. All day boundaries are midnight UTC. pub struct TimeContext { // ── Current moment ── pub now: i64, pub date: String, // 2026-03-22 pub time: String, // 14:35 pub datetime: String, // 2026-03-22T14:35:12Z pub day_of_week: String, // Saturday pub day_of_week_short: String, // Sat // ── Today ── pub today_start: i64, // midnight today pub today_end: i64, // 23:59:59.999 today // ── Yesterday ── pub yesterday_start: i64, pub yesterday_end: i64, pub yesterday_name: String, // Friday // ── Day before yesterday ── pub two_days_ago_start: i64, pub two_days_ago_end: i64, pub two_days_ago_name: String, // ── This week (Monday start) ── pub this_week_start: i64, // ── Last week ── pub last_week_start: i64, pub last_week_end: i64, // ── This month ── pub this_month_start: i64, // ── Last month ── pub last_month_start: i64, pub last_month_end: i64, // ── Rolling offsets from now ── pub ago_1h: i64, pub ago_6h: i64, pub ago_12h: i64, pub ago_24h: i64, pub ago_48h: i64, pub ago_7d: i64, pub ago_14d: i64, pub ago_30d: i64, } fn weekday_name(w: Weekday) -> &'static str { match w { Weekday::Mon => "Monday", Weekday::Tue => "Tuesday", Weekday::Wed => "Wednesday", Weekday::Thu => "Thursday", Weekday::Fri => "Friday", Weekday::Sat => "Saturday", Weekday::Sun => "Sunday", } } fn weekday_short(w: Weekday) -> &'static str { match w { Weekday::Mon => "Mon", Weekday::Tue => "Tue", Weekday::Wed => "Wed", Weekday::Thu => "Thu", Weekday::Fri => "Fri", Weekday::Sat => "Sat", Weekday::Sun => "Sun", } } impl TimeContext { pub fn now() -> Self { let now = Utc::now(); let today = now.date_naive(); let yesterday = today - Duration::days(1); let two_days_ago = today - Duration::days(2); let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); let end_of_day = NaiveTime::from_hms_milli_opt(23, 59, 59, 999).unwrap(); let today_start = Utc.from_utc_datetime(&today.and_time(midnight)).timestamp_millis(); let today_end = Utc.from_utc_datetime(&today.and_time(end_of_day)).timestamp_millis(); let yesterday_start = Utc.from_utc_datetime(&yesterday.and_time(midnight)).timestamp_millis(); let yesterday_end = Utc.from_utc_datetime(&yesterday.and_time(end_of_day)).timestamp_millis(); let two_days_ago_start = Utc.from_utc_datetime(&two_days_ago.and_time(midnight)).timestamp_millis(); let two_days_ago_end = Utc.from_utc_datetime(&two_days_ago.and_time(end_of_day)).timestamp_millis(); // This week (Monday start) let days_since_monday = today.weekday().num_days_from_monday() as i64; let monday = today - Duration::days(days_since_monday); let this_week_start = Utc.from_utc_datetime(&monday.and_time(midnight)).timestamp_millis(); // Last week let last_monday = monday - Duration::days(7); let last_sunday = monday - Duration::days(1); let last_week_start = Utc.from_utc_datetime(&last_monday.and_time(midnight)).timestamp_millis(); let last_week_end = Utc.from_utc_datetime(&last_sunday.and_time(end_of_day)).timestamp_millis(); // This month let first_of_month = today.with_day(1).unwrap(); let this_month_start = Utc.from_utc_datetime(&first_of_month.and_time(midnight)).timestamp_millis(); // Last month let last_month_last_day = first_of_month - Duration::days(1); let last_month_first = last_month_last_day.with_day(1).unwrap(); let last_month_start = Utc.from_utc_datetime(&last_month_first.and_time(midnight)).timestamp_millis(); let last_month_end = Utc.from_utc_datetime(&last_month_last_day.and_time(end_of_day)).timestamp_millis(); let now_ms = now.timestamp_millis(); Self { now: now_ms, date: now.format("%Y-%m-%d").to_string(), time: now.format("%H:%M").to_string(), datetime: now.format("%Y-%m-%dT%H:%M:%SZ").to_string(), day_of_week: weekday_name(today.weekday()).to_string(), day_of_week_short: weekday_short(today.weekday()).to_string(), today_start, today_end, yesterday_start, yesterday_end, yesterday_name: weekday_name(yesterday.weekday()).to_string(), two_days_ago_start, two_days_ago_end, two_days_ago_name: weekday_name(two_days_ago.weekday()).to_string(), this_week_start, last_week_start, last_week_end, this_month_start, last_month_start, last_month_end, ago_1h: now_ms - 3_600_000, ago_6h: now_ms - 21_600_000, ago_12h: now_ms - 43_200_000, ago_24h: now_ms - 86_400_000, ago_48h: now_ms - 172_800_000, ago_7d: now_ms - 604_800_000, ago_14d: now_ms - 1_209_600_000, ago_30d: now_ms - 2_592_000_000, } } /// Full time block for system prompts (~25 values). /// Used in the legacy path template and the conversations API per-message header. pub fn system_block(&self) -> String { format!( "\ ## time\n\ \n\ current: {} {} UTC ({}, {})\n\ epoch_ms: {}\n\ \n\ day boundaries (midnight UTC, use these for search_archive after/before):\n\ today: {} to {}\n\ yesterday ({}): {} to {}\n\ {} ago: {} to {}\n\ this week (Mon): {} to now\n\ last week: {} to {}\n\ this month: {} to now\n\ last month: {} to {}\n\ \n\ rolling offsets:\n\ 1h_ago={} 6h_ago={} 12h_ago={} 24h_ago={}\n\ 48h_ago={} 7d_ago={} 14d_ago={} 30d_ago={}", self.date, self.time, self.day_of_week, self.datetime, self.now, self.today_start, self.today_end, self.yesterday_name, self.yesterday_start, self.yesterday_end, self.two_days_ago_name, self.two_days_ago_start, self.two_days_ago_end, self.this_week_start, self.last_week_start, self.last_week_end, self.this_month_start, self.last_month_start, self.last_month_end, self.ago_1h, self.ago_6h, self.ago_12h, self.ago_24h, self.ago_48h, self.ago_7d, self.ago_14d, self.ago_30d, ) } /// Compact time line for per-message injection (~5 key values). pub fn message_line(&self) -> String { format!( "[time: {} {} UTC | today={}-{} | yesterday={}-{} | now={}]", self.date, self.time, self.today_start, self.today_end, self.yesterday_start, self.yesterday_end, self.now, ) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_time_context_now() { let tc = TimeContext::now(); assert!(tc.now > 0); assert!(tc.today_start <= tc.now); assert!(tc.today_end >= tc.now); assert!(tc.yesterday_start < tc.today_start); assert!(tc.yesterday_end < tc.today_start); assert!(tc.this_week_start <= tc.today_start); assert!(tc.last_week_start < tc.this_week_start); assert!(tc.this_month_start <= tc.today_start); assert!(tc.last_month_start < tc.this_month_start); } #[test] fn test_day_boundaries_are_midnight() { let tc = TimeContext::now(); // today_start should be divisible by 86400000 (midnight) // (not exactly, due to timezone, but should end in 00:00:00.000) assert!(tc.today_start % 1000 == 0); // whole second assert!(tc.yesterday_start % 1000 == 0); // end of day should be .999 assert!(tc.today_end % 1000 == 999); assert!(tc.yesterday_end % 1000 == 999); } #[test] fn test_yesterday_is_24h_before_today() { let tc = TimeContext::now(); assert_eq!(tc.today_start - tc.yesterday_start, 86_400_000); } #[test] fn test_rolling_offsets() { let tc = TimeContext::now(); assert_eq!(tc.now - tc.ago_1h, 3_600_000); assert_eq!(tc.now - tc.ago_24h, 86_400_000); assert_eq!(tc.now - tc.ago_7d, 604_800_000); } #[test] fn test_day_names() { let tc = TimeContext::now(); let valid_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; assert!(valid_days.contains(&tc.day_of_week.as_str())); assert!(valid_days.contains(&tc.yesterday_name.as_str())); } #[test] fn test_system_block_contains_key_values() { let tc = TimeContext::now(); let block = tc.system_block(); assert!(block.contains("epoch_ms:")); assert!(block.contains("today:")); assert!(block.contains("yesterday")); assert!(block.contains("this week")); assert!(block.contains("last week")); assert!(block.contains("this month")); assert!(block.contains("1h_ago=")); assert!(block.contains("30d_ago=")); } #[test] fn test_message_line_compact() { let tc = TimeContext::now(); let line = tc.message_line(); assert!(line.starts_with("[time:")); assert!(line.contains("today=")); assert!(line.contains("yesterday=")); assert!(line.contains("now=")); assert!(line.ends_with(']')); } }