assistant-stream.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /** Process-local assistant state retained for reconnecting Web followers. */
  2. import type { AssistantStreamFrame } from '@deepseek-ai/dsh-agent'
  3. import { AssistantStreamAccumulator } from '@deepseek-ai/dsh-llm'
  4. import type { SessionSeqCursor } from '@deepseek-ai/dsh-session'
  5. import type { JsonValue } from '@deepseek-ai/dsh-util-values'
  6. import type {
  7. SessionAssistantStreamAttempt,
  8. SessionAssistantStreamBaseline,
  9. } from './types.ts'
  10. interface MutableAttempt {
  11. readonly attemptId: SessionAssistantStreamAttempt['attemptId']
  12. readonly startedAfterSeq: SessionSeqCursor
  13. readonly turn: number
  14. readonly step: number
  15. readonly stream: AssistantStreamAccumulator
  16. nextIndex: number
  17. }
  18. const EMPTY_BASELINE: SessionAssistantStreamBaseline = { revision: 0 }
  19. /**
  20. * Folds dense Agent frames and materializes one shared immutable reconnect
  21. * baseline per accepted revision.
  22. */
  23. export class SessionAssistantStreamAccumulator {
  24. private activeAttempt: MutableAttempt | undefined
  25. private revision = 0
  26. private snapshotValue: SessionAssistantStreamBaseline = EMPTY_BASELINE
  27. private dirty = false
  28. /**
  29. * Fold one trusted frame from the current attached Agent lifecycle.
  30. * @param frame - next dense process-local Assistant frame.
  31. * @param durableCursor - last committed Session seq when this frame was observed.
  32. */
  33. accept(frame: AssistantStreamFrame, durableCursor: SessionSeqCursor): void {
  34. if (frame.type === 'start' && frame.revision === 1 && this.revision !== 0) {
  35. this.activeAttempt = undefined
  36. this.revision = 0
  37. }
  38. if (frame.revision !== this.revision + 1) {
  39. this.activeAttempt = undefined
  40. this.revision = frame.revision
  41. this.dirty = true
  42. return
  43. }
  44. this.revision = frame.revision
  45. switch (frame.type) {
  46. case 'start':
  47. this.activeAttempt = {
  48. attemptId: frame.attemptId,
  49. startedAfterSeq: durableCursor,
  50. turn: frame.turn,
  51. step: frame.step,
  52. stream: new AssistantStreamAccumulator(),
  53. nextIndex: 0,
  54. }
  55. break
  56. case 'chunk': {
  57. const attempt = this.activeAttempt
  58. if (attempt === undefined
  59. || attempt.attemptId !== frame.attemptId
  60. || frame.index !== attempt.nextIndex) {
  61. this.activeAttempt = undefined
  62. break
  63. }
  64. attempt.stream.push({ time: frame.time, chunk: frame.chunk })
  65. attempt.nextIndex += 1
  66. break
  67. }
  68. case 'end':
  69. this.activeAttempt = undefined
  70. break
  71. }
  72. this.dirty = true
  73. }
  74. /**
  75. * Read the cached reconnect baseline, materializing it after a state change.
  76. * @returns the identity-stable baseline for the latest accepted revision.
  77. */
  78. snapshot(): SessionAssistantStreamBaseline {
  79. if (!this.dirty) return this.snapshotValue
  80. this.snapshotValue = {
  81. revision: this.revision,
  82. ...this.activeAttempt === undefined ? {} : {
  83. activeAttempt: {
  84. attemptId: this.activeAttempt.attemptId,
  85. startedAfterSeq: this.activeAttempt.startedAfterSeq,
  86. turn: this.activeAttempt.turn,
  87. step: this.activeAttempt.step,
  88. nextIndex: this.activeAttempt.nextIndex,
  89. stream: this.activeAttempt.stream.snapshot() as unknown as readonly JsonValue[],
  90. },
  91. },
  92. }
  93. this.dirty = false
  94. return this.snapshotValue
  95. }
  96. }