invariant.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. /** Package-owned durable clock-context invariants. @module @deepseek-ai/dsh-time-context/invariant */
  2. import type { Context } from 'cordis'
  3. import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
  4. import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
  5. const PACKAGE_NAME = '@deepseek-ai/dsh-time-context'
  6. const SOURCE_NAME = 'time-context'
  7. const READING = new RegExp(
  8. '^Time sampled while preparing turn (\\d+), step (\\d+): '
  9. + '(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2})\\[[^\\]]+\\])\\n'
  10. + 'Elapsed since the preceding (model-visible message|step context): '
  11. + '(?:unavailable|(?:(?:\\d+d )?(?:\\d+h )?(?:\\d+m )?\\d+s))\\.$',
  12. )
  13. /** Cordis companion plugin name. */
  14. export const name = 'time-context-invariant'
  15. /** Service required before the companion can reserve package ownership. */
  16. export const inject = ['invariants']
  17. /** Derive the entered step boundary at which a time-context reading may append. */
  18. function preparationPosition(history: readonly SessionEvent[], fail: InvariantFailure): { turn: number; step: number } {
  19. for (const event of history.slice().reverse()) {
  20. switch (event.type) {
  21. case 'step/start':
  22. return { turn: event.data.turn, step: event.data.step }
  23. case 'turn/start':
  24. case 'step/end':
  25. case 'turn/end':
  26. case 'request/header':
  27. case 'assistant/chunk':
  28. case 'assistant/message':
  29. case 'tool/call':
  30. case 'tool/result':
  31. fail('time-context reading must be appended at a prompt boundary')
  32. break
  33. default:
  34. break
  35. }
  36. }
  37. fail('time-context reading must be appended at a prompt boundary')
  38. }
  39. /** Validate one plugin-attributed time reading against its session position and timestamp. */
  40. function validateReading(
  41. history: readonly SessionEvent[],
  42. event: SessionEvent<'user/message'>,
  43. fail: InvariantFailure,
  44. ): void {
  45. const [block] = event.data.content
  46. if (event.data.content.length !== 1 || block?.type !== 'text') {
  47. fail('time-context messages must contain exactly one text block')
  48. }
  49. const match = READING.exec(block.text)
  50. if (match === null) fail('time-context message does not match the durable reading format')
  51. const turn = Number(match[1])
  52. const step = Number(match[2])
  53. if (!Number.isSafeInteger(turn) || turn < 1 || !Number.isSafeInteger(step) || step < 1) {
  54. fail('time-context turn and step must be positive safe integers')
  55. }
  56. const expected = preparationPosition(history, fail)
  57. if (turn !== expected.turn || step !== expected.step) {
  58. fail(`time-context reading names turn ${turn}/step ${step}, expected turn ${expected.turn}/step ${expected.step}`)
  59. }
  60. const baseline = match[4]
  61. if ((step === 1) !== (baseline === 'model-visible message')) {
  62. fail(`time-context step ${step} uses the wrong elapsed-time baseline ${JSON.stringify(baseline)}`)
  63. }
  64. const rendered = match[3]
  65. /* v8 ignore next -- the preceding fixed regexp always supplies capture group three. */
  66. if (rendered === undefined) fail('time-context reading omitted its rendered timestamp')
  67. const renderedTime = Date.parse(rendered.replace(/\[[^\]]+\]$/, ''))
  68. if (!Number.isFinite(renderedTime) || !Number.isSafeInteger(event.time)
  69. || event.time < renderedTime) {
  70. fail('time-context rendered timestamp must parse and not postdate its durable event')
  71. }
  72. }
  73. /* jscpd:ignore-start -- package companions share replay and dispatch plumbing */
  74. /** Validate all package-owned readings already present in one session. */
  75. function validateSession(session: Session, fail: InvariantFailure): void {
  76. for (const [index, event] of session.events.entries()) {
  77. if (event.type !== 'user/message'
  78. || event.data.source.kind !== 'plugin'
  79. || event.data.source.plugin !== SOURCE_NAME) continue
  80. validateReading(session.events.slice(0, index), event, fail)
  81. }
  82. }
  83. /** Install validation for loaded and newly appended context readings. */
  84. const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
  85. for (const session of ctx.sessions.list()) validateSession(session, fail)
  86. ctx.on('internal/dispatch', (_mode, eventName, args) => {
  87. if (eventName !== 'session/event') return
  88. const [session, event] = args as [Session, SessionEvent]
  89. if (event.type !== 'user/message'
  90. || event.data.source.kind !== 'plugin'
  91. || event.data.source.plugin !== SOURCE_NAME) return
  92. validateReading(session.events, event, fail)
  93. }, { global: true })
  94. }, { inject: ['sessions'] })
  95. /* jscpd:ignore-end */
  96. /**
  97. * Register the time-context invariant companion.
  98. * @param ctx - Cordis context carrying the invariant service.
  99. * @returns the installed registration's disposer after setup succeeds.
  100. */
  101. export const apply = (ctx: Context): Promise<() => void> =>
  102. Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))