Files
sol/src/time_context.rs
Sienna Meridian Satterwhite 1058afb635 add TimeContext: 25 pre-computed time values for the model
midnight-based day boundaries (today, yesterday, 2 days ago),
week/month boundaries, rolling offsets (1h to 30d). injected
into system prompt via {time_block} and per-message via compact
time line. models no longer need to compute epoch timestamps.
2026-03-23 01:41:44 +00:00

279 lines
9.6 KiB
Rust

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(']'));
}
}