82 lines
3.3 KiB
TypeScript
82 lines
3.3 KiB
TypeScript
/** Browser-zone derivation and model-facing policy text for one open request turn. */
|
|
|
|
import { assertNever } from '@deepseek-ai/dsh-llm'
|
|
import type { UserMessage } from '@deepseek-ai/dsh-llm'
|
|
|
|
const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/
|
|
|
|
/** Browser-zone facts derived from user-rpc messages in one open turn. */
|
|
export type BrowserTimeZoneContext =
|
|
| { readonly kind: 'resolved'; readonly timeZone: string }
|
|
| { readonly kind: 'mixed'; readonly timeZones: readonly string[] }
|
|
| { readonly kind: 'missing' }
|
|
|
|
/** Read and validate a Host-canonicalized browser zone from one ordinary user-rpc message. */
|
|
function browserTimeZone(message: UserMessage): string | undefined {
|
|
const source = message.source
|
|
const value = source.kind === 'user'
|
|
&& 'rpcId' in source
|
|
&& typeof source.rpcId === 'string'
|
|
&& 'clientTimeZone' in source
|
|
&& typeof source.clientTimeZone === 'string'
|
|
? source.clientTimeZone
|
|
: undefined
|
|
if (value === undefined) return undefined
|
|
if (value !== 'UTC' && !IANA_TIME_ZONE.test(value)) {
|
|
throw new TypeError(
|
|
`browser time zone must be canonical UTC or IANA Area/Location: ${JSON.stringify(value)}`,
|
|
)
|
|
}
|
|
let canonical: string
|
|
try {
|
|
canonical = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone
|
|
} catch (error: unknown) {
|
|
throw new TypeError(`browser time zone is unsupported: ${JSON.stringify(value)}`, { cause: error })
|
|
}
|
|
if (canonical !== value) {
|
|
throw new TypeError(`browser time zone must be canonical: ${JSON.stringify(value)}`)
|
|
}
|
|
return value
|
|
}
|
|
|
|
/**
|
|
* Derive the unique, mixed, or missing browser zone for one open turn.
|
|
* @param messages - Entered and proposed user messages belonging to the turn.
|
|
* @returns Sorted, duplicate-free browser-zone facts.
|
|
* @throws TypeError when a user-rpc source carries an invalid or noncanonical zone.
|
|
*/
|
|
export function deriveBrowserTimeZoneContext(
|
|
messages: readonly UserMessage[],
|
|
): BrowserTimeZoneContext {
|
|
const timeZones = [...new Set(messages.flatMap((message) => {
|
|
const timeZone = browserTimeZone(message)
|
|
return timeZone === undefined ? [] : [timeZone]
|
|
}))].sort()
|
|
const [timeZone, ...remaining] = timeZones
|
|
if (timeZone === undefined) return { kind: 'missing' }
|
|
if (remaining.length === 0) return { kind: 'resolved', timeZone }
|
|
return { kind: 'mixed', timeZones }
|
|
}
|
|
|
|
/**
|
|
* Render the model instruction for one browser-zone context.
|
|
* @param context - Browser-zone facts for the open turn.
|
|
* @returns One durable policy line.
|
|
*/
|
|
export function renderBrowserTimeZoneContext(context: BrowserTimeZoneContext): string {
|
|
switch (context.kind) {
|
|
case 'resolved':
|
|
return `Browser time zone for this request: ${context.timeZone}. `
|
|
+ 'Interpret otherwise-unqualified dates and times in this zone.'
|
|
case 'mixed':
|
|
return `Browser time zone for this request: mixed ${JSON.stringify(context.timeZones)}. `
|
|
+ 'Ask the user to clarify otherwise-unqualified dates and times.'
|
|
case 'missing':
|
|
return 'Browser time zone for this request: unavailable. '
|
|
+ 'Ask the user to clarify otherwise-unqualified dates and times.'
|
|
/* v8 ignore next 2 -- the closed BrowserTimeZoneContext union is exhausted above. */
|
|
default:
|
|
return assertNever(context, 'BrowserTimeZoneContext')
|
|
}
|
|
}
|