inbox.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /**
  2. * Per-agent message inbox: queued and steering FIFOs. Purely an in-memory
  3. * mechanism of the loop driver — the public surface is `Agent.send()` and
  4. * `Agent.steer()`.
  5. *
  6. * @module dsh-agent-loop/inbox
  7. */
  8. import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
  9. import type { HookContext } from '@deepseek-ai/dsh-agent'
  10. /** One message waiting in an agent's inbox. */
  11. export interface InboxMessage {
  12. content: ContentBlock[]
  13. source: MessageSource
  14. contexts: HookContext[]
  15. }
  16. /**
  17. * Per-agent inbox: a queued FIFO (dequeued once per turn start) and a steering FIFO
  18. * (drained between steps of a running turn). Purely an in-memory mechanism of
  19. * the loop — the public surface is `Agent.send()` / `Agent.steer()`.
  20. */
  21. export class Inbox {
  22. private queuedMessages: InboxMessage[] = []
  23. private steeringMessages: InboxMessage[] = []
  24. private wakeup: (() => void) | undefined
  25. /** True while queued messages are pending — read by the idle wait's fast path and the loop's turn-start checks. */
  26. get hasQueued(): boolean {
  27. return this.queuedMessages.length > 0
  28. }
  29. /** True while steering messages are pending — read by cancellation and the loop's stop-override check. */
  30. get hasSteering(): boolean {
  31. return this.steeringMessages.length > 0
  32. }
  33. /**
  34. * Add a message to the queued FIFO and wake a parked {@link waitForQueued}.
  35. * @param message - the message to queue for the next turn start.
  36. */
  37. enqueue(message: InboxMessage): void {
  38. this.queuedMessages.push(message)
  39. this.wakeup?.()
  40. }
  41. /**
  42. * Add a message to the steering FIFO. Deliberately no wakeup: steering is
  43. * drained between steps of a running turn, never by the idle wait —
  44. * `Agent.steer()` on an idle agent falls back to `send()` instead.
  45. * @param message - the message to inject between steps of the running turn.
  46. */
  47. steer(message: InboxMessage): void {
  48. this.steeringMessages.push(message)
  49. }
  50. /**
  51. * Remove the oldest queued message for one turn start.
  52. * @returns the oldest message, or `undefined` when the queued FIFO is empty.
  53. */
  54. dequeueQueued(): InboxMessage | undefined {
  55. return this.queuedMessages.shift()
  56. }
  57. /**
  58. * Drain all steering messages (between steps).
  59. * @returns the drained messages in arrival order; the steering FIFO is left empty.
  60. */
  61. drainSteering(): InboxMessage[] {
  62. return this.steeringMessages.splice(0)
  63. }
  64. /**
  65. * Discard all pending messages (queued + steering) without delivering them —
  66. * used by `cancel()`, which drops un-started work rather than draining it into
  67. * a turn. Unlike `dequeueQueued`/`drainSteering`, the messages are thrown away.
  68. */
  69. clear(): void {
  70. this.queuedMessages.length = 0
  71. this.steeringMessages.length = 0
  72. }
  73. /**
  74. * Wait until a queued message arrives or `cancel` resolves.
  75. * @param cancel - a promise whose resolution abandons the wait without a
  76. * message (the driver loop passes the agent's disposed promise so a parked
  77. * loop can exit).
  78. */
  79. waitForQueued(cancel: Promise<void>): Promise<void> {
  80. if (this.hasQueued) return Promise.resolve()
  81. const { promise, resolve } = Promise.withResolvers<void>()
  82. this.wakeup = resolve
  83. void cancel.then(resolve)
  84. return promise.finally(() => {
  85. if (this.wakeup === resolve) this.wakeup = undefined
  86. })
  87. }
  88. }