/** * Opt-in request-preparation clock context. Eligible pre-step attempts append * durable, source-attributed time readings to conversation history. * * @module @deepseek-ai/dsh-time-context */ import type { Context } from 'cordis' import z from 'schemastery' import type { Agent } from '@deepseek-ai/dsh-agent' import { createUserMessage } from '@deepseek-ai/dsh-llm' /** Cordis plugin name used by loader diagnostics. */ export const name = 'time-context' /** The agent registry that owns the pre-step lifecycle seam. */ export const inject = ['agents'] /** Request-preparation clock formatting and append scheduling. Invalid values fail plugin load. */ export interface Config { /** IANA time zone used for the rendered timestamp. Omit to resolve the Node process's system zone at plugin load. */ timeZone?: string /** Minimum milliseconds between durable injections in one session. Omit or set to 0 to inject on every eligible pre-step attempt. */ refreshIntervalMs?: number } /** Schemastery validation for {@link Config}. */ export const Config: z = z.object({ timeZone: z.string(), refreshIntervalMs: z.number(), }) type TimestampPart = 'day' | 'hour' | 'minute' | 'month' | 'second' | 'timeZoneName' | 'year' /** Format an epoch millisecond value as an ISO-shaped timestamp with offset and IANA zone. */ function formatTimestamp(now: number, formatter: Intl.DateTimeFormat, timeZone: string): string { const parts = Object.fromEntries( formatter.formatToParts(now).map(part => [part.type, part.value]), ) as Record const offset = parts.timeZoneName.replace(/^GMT$/, 'GMT+00:00').slice(3) return `${parts['year']}-${parts['month']}-${parts['day']}T${parts['hour']}:${parts['minute']}:${parts['second']}${offset}[${timeZone}]` } /** Format a non-negative elapsed millisecond count as compact whole-second units. */ function formatDuration(elapsedMs: number): string { let seconds = Math.floor(Math.max(0, elapsedMs) / 1000) const days = Math.floor(seconds / 86_400) seconds %= 86_400 const hours = Math.floor(seconds / 3600) seconds %= 3600 const minutes = Math.floor(seconds / 60) seconds %= 60 const parts: string[] = [] if (days > 0) parts.push(`${days}d`) if (hours > 0) parts.push(`${hours}h`) if (minutes > 0) parts.push(`${minutes}m`) parts.push(`${seconds}s`) return parts.join(' ') } /** Find the latest model-visible event, excluding this plugin's pending append. */ function precedingMessageTime(agent: Agent): number | undefined { for (const event of [...agent.session.events].reverse()) { switch (event.type) { case 'user/message': case 'assistant/message': case 'tool/result': case 'steering/message': return event.time default: // Merge-extensible session events: non-surface records are not messages. break } } return undefined } /** Find the preceding time-context event within the open turn. */ function precedingStepContextTime(agent: Agent, turn: number): number | undefined { for (const event of [...agent.session.events].reverse()) { if (event.type === 'turn/start' && event.data.turn === turn) return undefined if (event.type === 'user/message' && event.data.source.kind === 'plugin' && event.data.source.plugin === name) { return event.time } } return undefined } /** Find this plugin's latest durable injection, including a shadowed surface event. */ function latestInjectionTime(agent: Agent): number | undefined { for (const event of [...agent.session.events].reverse()) { if (event.type === 'user/message' && event.data.source.kind === 'plugin' && event.data.source.plugin === name) { return event.time } } return undefined } function renderText( now: number, turn: number, step: number, previous: number | undefined, formatter: Intl.DateTimeFormat, timeZone: string, ): string { const elapsed = previous === undefined ? 'unavailable' : formatDuration(now - previous) const baseline = step === 1 ? 'model-visible message' : 'step context' return `Time sampled while preparing turn ${turn}, step ${step}: ${formatTimestamp(now, formatter, timeZone)}\n` + `Elapsed since the preceding ${baseline}: ${elapsed}.` } /** Reject refresh intervals that cannot represent an exact elapsed-millisecond threshold. */ function validateRefreshInterval(refreshIntervalMs: number | undefined): void { if (refreshIntervalMs !== undefined && ( !Number.isSafeInteger(refreshIntervalMs) || refreshIntervalMs < 0 )) { throw new TypeError( `time-context: refreshIntervalMs must be a non-negative safe integer, got ${String(refreshIntervalMs)}`, ) } } /** * Register a prepended pre-step listener for the lifetime of `ctx`. * @param ctx - plugin context; the listener is disposed with it. * @param config - time zone and durable refresh scheduling configuration. * @throws when the refresh interval is invalid or the configured or process time zone cannot be resolved. */ export function apply(ctx: Context, config: Config): void { const timeZone = config.timeZone const refreshIntervalMs = config.refreshIntervalMs validateRefreshInterval(refreshIntervalMs) let formatter: Intl.DateTimeFormat try { formatter = new Intl.DateTimeFormat('en-US', { ...(timeZone === undefined ? {} : { timeZone }), year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', timeZoneName: 'longOffset', }) } catch (error: unknown) { const message = timeZone === undefined ? 'time-context: failed to resolve the system time zone' : `time-context: invalid IANA timeZone ${JSON.stringify(timeZone)}` throw new Error(message, { cause: error }) } const resolvedTimeZone = formatter.resolvedOptions().timeZone ctx.on('agent/step', ( agent: Agent, turn: number, step: number, signal: AbortSignal, ) => { if (signal.aborted) return const now = Date.now() if (refreshIntervalMs !== undefined && refreshIntervalMs > 0) { const lastInjection = latestInjectionTime(agent) if (lastInjection !== undefined && now >= lastInjection && now - lastInjection < refreshIntervalMs) return } const previous = step === 1 ? precedingMessageTime(agent) : precedingStepContextTime(agent, turn) agent.inject(createUserMessage({ content: [{ type: 'text', text: renderText(now, turn, step, previous, formatter, resolvedTimeZone) }], source: { kind: 'plugin', plugin: name } })) }, { prepend: true }) }