import { knownTimeZone } from "@rakazo/core"; import type { PrismaClient } from "@rakazo/db"; /** * Models have no clock. Without an explicit anchor they infer "now" from training data * or from timestamps that happen to appear in the conversation, and then reason about * deadlines, recency and scheduling from a stale date. Every run states the real present * moment in its system instructions so that never has to be guessed: in the user's time * zone when one is known (with the offset, so it stays unambiguous), otherwise in UTC. */ export function formatCurrentTimeInstruction( timeZone: string | null | undefined, now: Date = new Date(), ): string { const zone = knownTimeZone(timeZone); const clock = zonedClock(now, zone ?? "UTC"); const moment = zone ? `${clock.weekday}, ${clock.local}${zone === "UTC" ? "Z" : clock.offset} (${zone}, the user's time zone)` : `${clock.weekday}, ${clock.local}Z (UTC)`; return [ `Current date and time: ${moment}.`, "Treat this as the present moment for everything you say and do.", zone ? "Use it to judge what is past, upcoming, or overdue, and write absolute dates rather than relative ones." : "Use it to judge what is past, upcoming, or overdue, convert it to the user's time zone when one is known, and write absolute dates rather than relative ones.", "Never infer today's date from your training data or from timestamps quoted in the conversation.", ].join(" "); } /** Today's date as YYYY-MM-DD in the user's time zone, or in UTC when none is known. */ export function formatLocalDate( timeZone: string | null | undefined, now: Date = new Date(), ): string { return zonedClock(now, knownTimeZone(timeZone) ?? "UTC").local.slice(0, 10); } /** The user's time zone from their profile when this runtime knows it. */ export async function loadUserTimeZone( prisma: Pick, userId: string, ): Promise { const profile = await prisma.userProfile.findUnique({ where: { userId }, select: { timeZone: true }, }); return knownTimeZone(profile?.timeZone); } function zonedClock(now: Date, timeZone: string) { const parts = new Map( new Intl.DateTimeFormat("en-US", { timeZone, weekday: "long", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23", }) .formatToParts(now) .map((part) => [part.type, part.value]), ); const part = (type: Intl.DateTimeFormatPartTypes) => parts.get(type) ?? ""; const [year, month, day, hour, minute, second] = ( ["year", "month", "day", "hour", "minute", "second"] as const ).map(part); // The wall clock read as if it were UTC, minus the real instant, is the zone's offset. const wallClock = Date.UTC( Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second), ); const offsetMinutes = Math.round((wallClock - Math.floor(now.getTime() / 1000) * 1000) / 60_000); const absolute = Math.abs(offsetMinutes); const pad = (value: number) => String(value).padStart(2, "0"); return { weekday: part("weekday"), local: `${year}-${month}-${day}T${hour}:${minute}:${second}`, offset: `${offsetMinutes < 0 ? "-" : "+"}${pad(Math.floor(absolute / 60))}:${pad(absolute % 60)}`, }; }