Forked from
src / toolsProvider.ts
import { tool, Tool, ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
const DEFAULT_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
export async function toolsProvider(_ctl: ToolsProviderController): Promise<Tool[]> {
const currentDatetimeTool = tool({
name: "Current Datetime Advanced",
description:
"Returns the current date and time in structured form, including UTC, local time, timezone, date, time, weekday, and Unix timestamps. Use this whenever the user asks about the current date/time or when interpreting time-sensitive metadata.",
parameters: {
timeZone: z
.string()
.optional()
.describe("Optional IANA timezone, e.g. 'Asia/Tokyo'. Defaults to the system timezone."),
},
implementation: async ({ timeZone }) => {
const resolvedTimeZone = timeZone || DEFAULT_TIMEZONE;
const now = new Date();
return buildCurrentDatetimePayload(now, resolvedTimeZone);
},
});
const interpretDatetimeTool = tool({
name: "Interpret Datetime Advanced",
description:
"Interprets a timestamp or date string relative to the current moment. Converts it to UTC and local time, calculates the difference, and classifies it as past, present, or future. Use this before claiming that a timestamp is old, recent, or in the future.",
parameters: {
input: z
.string()
.describe("Timestamp or date string to interpret, such as an ISO timestamp, Unix seconds, Unix milliseconds, or a date string."),
timeZone: z
.string()
.optional()
.describe("Optional IANA timezone for local interpretation, e.g. 'Asia/Tokyo'. Defaults to the system timezone."),
now: z
.string()
.optional()
.describe("Optional reference time. Usually omit this. Accepts ISO timestamp, Unix seconds, or Unix milliseconds."),
},
implementation: async ({ input, timeZone, now }) => {
const resolvedTimeZone = timeZone || DEFAULT_TIMEZONE;
const inputDate = parseFlexibleDate(input);
const nowDate = now ? parseFlexibleDate(now) : new Date();
if (!inputDate) {
return {
error: "invalid_datetime",
input,
message: "Could not parse the input as an ISO timestamp, Unix seconds, Unix milliseconds, or date string.",
};
}
if (!nowDate) {
return {
error: "invalid_reference_datetime",
now,
message: "Could not parse the reference time.",
};
}
const differenceMs = inputDate.getTime() - nowDate.getTime();
const relation = classifyDifference(differenceMs);
return {
input,
timeZone: resolvedTimeZone,
inputUtc: inputDate.toISOString(),
inputLocal: formatLocalDateTime(inputDate, resolvedTimeZone),
nowUtc: nowDate.toISOString(),
nowLocal: formatLocalDateTime(nowDate, resolvedTimeZone),
differenceMilliseconds: differenceMs,
differenceSeconds: Math.round(differenceMs / 1000),
relation,
humanReadable: humanizeDifference(differenceMs),
caution:
"Use relation and difference values instead of guessing. A UTC timestamp may appear to be a different calendar date in local time.",
};
},
});
return [currentDatetimeTool, interpretDatetimeTool];
}
function buildCurrentDatetimePayload(now: Date, timeZone: string) {
const parts = getLocalParts(now, timeZone);
return {
utc: now.toISOString(),
local: formatLocalDateTime(now, timeZone),
timeZone,
date: parts.date,
time: parts.time,
weekday: parts.weekday,
unixSeconds: Math.floor(now.getTime() / 1000),
unixMilliseconds: now.getTime(),
hint:
"Use this structured datetime instead of relying on model memory. Compare timestamps with Interpret Datetime Advanced before saying they are past or future.",
};
}
function parseFlexibleDate(input: string): Date | null {
const trimmed = String(input || "").trim();
if (!trimmed) return null;
const numeric = Number(trimmed);
if (Number.isFinite(numeric)) {
if (numeric > 1_000_000_000_000 && numeric < 10_000_000_000_000) {
return new Date(numeric);
}
if (numeric > 1_000_000_000 && numeric < 10_000_000_000) {
return new Date(numeric * 1000);
}
}
const compactDate = trimmed.match(/^(\d{4})(\d{2})(\d{2})$/);
if (compactDate) {
return new Date(`${compactDate[1]}-${compactDate[2]}-${compactDate[3]}T00:00:00.000Z`);
}
const parsed = new Date(trimmed);
if (!Number.isNaN(parsed.getTime())) {
return parsed;
}
return null;
}
function classifyDifference(differenceMs: number): "past" | "present" | "future" {
const toleranceMs = 60 * 1000;
if (Math.abs(differenceMs) <= toleranceMs) {
return "present";
}
return differenceMs < 0 ? "past" : "future";
}
function humanizeDifference(differenceMs: number): string {
const absSeconds = Math.abs(Math.round(differenceMs / 1000));
const direction = differenceMs < 0 ? "ago" : "from now";
if (absSeconds < 60) return `${absSeconds} seconds ${direction}`;
const absMinutes = Math.round(absSeconds / 60);
if (absMinutes < 60) return `${absMinutes} minutes ${direction}`;
const absHours = Math.round(absMinutes / 60);
if (absHours < 48) return `${absHours} hours ${direction}`;
const absDays = Math.round(absHours / 24);
if (absDays < 60) return `${absDays} days ${direction}`;
const absMonths = Math.round(absDays / 30);
if (absMonths < 24) return `${absMonths} months ${direction}`;
const absYears = Math.round(absDays / 365);
return `${absYears} years ${direction}`;
}
function formatLocalDateTime(date: Date, timeZone: string): string {
const parts = getLocalParts(date, timeZone);
return `${parts.date} ${parts.time} ${parts.timeZoneName}`;
}
function getLocalParts(date: Date, timeZone: string) {
const dateFormatter = new Intl.DateTimeFormat("en-CA", {
timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
});
const timeFormatter = new Intl.DateTimeFormat("en-GB", {
timeZone,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZoneName: "short",
});
const weekdayFormatter = new Intl.DateTimeFormat("en-US", {
timeZone,
weekday: "long",
});
const timeParts = timeFormatter.formatToParts(date);
const time = [
timeParts.find((part) => part.type === "hour")?.value,
timeParts.find((part) => part.type === "minute")?.value,
timeParts.find((part) => part.type === "second")?.value,
].join(":");
return {
date: dateFormatter.format(date),
time,
weekday: weekdayFormatter.format(date),
timeZoneName: timeParts.find((part) => part.type === "timeZoneName")?.value || "",
};
}