request-zone.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. /** Browser-zone derivation and model-facing policy text for one open request turn. */
  2. import type { UserMessage } from '@deepseek-ai/dsh-llm'
  3. import { assertNever } from '@deepseek-ai/dsh-util-values'
  4. const IANA_TIME_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/
  5. /** Browser-zone facts derived from user-rpc messages in one open turn. */
  6. export type BrowserTimeZoneContext =
  7. | { readonly kind: 'resolved'; readonly timeZone: string }
  8. | { readonly kind: 'mixed'; readonly timeZones: readonly string[] }
  9. | { readonly kind: 'missing' }
  10. /** Read and validate a Host-canonicalized browser zone from one ordinary user-rpc message. */
  11. function browserTimeZone(message: UserMessage): string | undefined {
  12. const source = message.source
  13. const value = source.kind === 'user'
  14. && 'rpcId' in source
  15. && typeof source.rpcId === 'string'
  16. && 'clientTimeZone' in source
  17. && typeof source.clientTimeZone === 'string'
  18. ? source.clientTimeZone
  19. : undefined
  20. if (value === undefined) return undefined
  21. if (value !== 'UTC' && !IANA_TIME_ZONE.test(value)) {
  22. throw new TypeError(
  23. `browser time zone must be canonical UTC or IANA Area/Location: ${JSON.stringify(value)}`,
  24. )
  25. }
  26. let canonical: string
  27. try {
  28. canonical = new Intl.DateTimeFormat('en-US', { timeZone: value }).resolvedOptions().timeZone
  29. } catch (error: unknown) {
  30. throw new TypeError(`browser time zone is unsupported: ${JSON.stringify(value)}`, { cause: error })
  31. }
  32. if (canonical !== value) {
  33. throw new TypeError(`browser time zone must be canonical: ${JSON.stringify(value)}`)
  34. }
  35. return value
  36. }
  37. /**
  38. * Derive the unique, mixed, or missing browser zone for one open turn.
  39. * @param messages - Entered and proposed user messages belonging to the turn.
  40. * @returns Sorted, duplicate-free browser-zone facts.
  41. * @throws TypeError when a user-rpc source carries an invalid or noncanonical zone.
  42. */
  43. export function deriveBrowserTimeZoneContext(
  44. messages: readonly UserMessage[],
  45. ): BrowserTimeZoneContext {
  46. const timeZones = [...new Set(messages.flatMap((message) => {
  47. const timeZone = browserTimeZone(message)
  48. return timeZone === undefined ? [] : [timeZone]
  49. }))].sort()
  50. const [timeZone, ...remaining] = timeZones
  51. if (timeZone === undefined) return { kind: 'missing' }
  52. if (remaining.length === 0) return { kind: 'resolved', timeZone }
  53. return { kind: 'mixed', timeZones }
  54. }
  55. /**
  56. * Render the model instruction for one browser-zone context.
  57. * @param context - Browser-zone facts for the open turn.
  58. * @returns One durable policy line.
  59. */
  60. export function renderBrowserTimeZoneContext(context: BrowserTimeZoneContext): string {
  61. switch (context.kind) {
  62. case 'resolved':
  63. return `Browser time zone for this request: ${context.timeZone}. `
  64. + 'Interpret otherwise-unqualified dates and times in this zone.'
  65. case 'mixed':
  66. return `Browser time zone for this request: mixed ${JSON.stringify(context.timeZones)}. `
  67. + 'Ask the user to clarify otherwise-unqualified dates and times.'
  68. case 'missing':
  69. return 'Browser time zone for this request: unavailable. '
  70. + 'Ask the user to clarify otherwise-unqualified dates and times.'
  71. /* v8 ignore next 2 -- the closed BrowserTimeZoneContext union is exhausted above. */
  72. default:
  73. return assertNever(context, 'BrowserTimeZoneContext')
  74. }
  75. }