session.client.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. /**
  2. * The Remote side of one Session under test: default answers for every
  3. * `session/*` and `subagents/*` endpoint a `Session` or its manager calls, builders for
  4. * the two history-shaped answers, the `session/follow` opening snapshot and
  5. * the `session/page` page, both derived from event lists the way the Host
  6. * derives them from its log, and builders for the attachment references
  7. * the Host's log carries.
  8. */
  9. import { AttachmentId, type FileAttachmentRef, type ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
  10. import { ok, type RemoteMock, type RemoteTable, type StreamScript, type UnaryRuleFn } from '@deepseek-ai/dsh-remote-mock'
  11. import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
  12. import type { RemoteFailure, RemoteResult } from '@deepseek-ai/dsh-typert-protocol'
  13. import type {
  14. SessionAssistantStreamBaseline, SessionFollowFrame, SessionFollowRequest,
  15. SessionPage, SessionPageRequest,
  16. } from '../../src/types.ts'
  17. import { entries, historyValue } from '../event-script.client.ts'
  18. import { followSnapshot, pageThrough } from './history.client.ts'
  19. export { followSnapshot } from './history.client.ts'
  20. /** Endpoints a Session opens and calls for its history. */
  21. export const FOLLOW = 'session/follow'
  22. export const PAGE = 'session/page'
  23. /** A history answer as the Host returns it, possibly still pending. */
  24. export type HistoryAnswer = RemoteResult<SessionPage> | Promise<RemoteResult<SessionPage>>
  25. /** A history answer, or a function of the request that produces one. */
  26. export type HistorySource<Request> = HistoryAnswer | ((request: Request) => HistoryAnswer)
  27. /**
  28. * The failure branch of a Remote result.
  29. * @param error - the owner-declared failure.
  30. * @returns the result.
  31. */
  32. export function err<T>(error: RemoteFailure): RemoteResult<T> {
  33. return { ok: false, error }
  34. }
  35. /**
  36. * The Host's history answer for `events`.
  37. * @param events - events in seq order.
  38. * @param hasMore - whether older history exists.
  39. * @returns the success result.
  40. */
  41. export function history(events: readonly SessionEvent[], hasMore = false): RemoteResult<SessionPage> {
  42. return ok(historyValue(events, hasMore))
  43. }
  44. /**
  45. * One live event frame of a follow stream.
  46. * @param event - the event.
  47. * @returns the frame.
  48. */
  49. export function frame(event: SessionEvent): SessionFollowFrame {
  50. return entries([event])[0]!
  51. }
  52. /**
  53. * Deliver one live event to every open follow stream and wait until the client has consumed it.
  54. * @param mock - the mock holding the streams.
  55. * @param event - the event.
  56. */
  57. export async function pushEvent(mock: RemoteMock, event: SessionEvent): Promise<void> {
  58. mock.streams.push(FOLLOW, frame(event))
  59. await mock.streams.drained(FOLLOW)
  60. }
  61. /**
  62. * `session/follow` script: the opening snapshot built from the history answer,
  63. * then open for pushes. A failed answer fails the stream with its error; a
  64. * rejected one fails it with the rejection.
  65. * @param history - history answer or a function of the follow request.
  66. * @param options - snapshot cursor and initial Assistant stream state; the latter may be read per opening.
  67. * @returns the script.
  68. */
  69. export function followScript(
  70. history: HistorySource<SessionFollowRequest>,
  71. options: {
  72. cursor?: number
  73. assistantStream?: SessionAssistantStreamBaseline | (() => SessionAssistantStreamBaseline)
  74. } = {},
  75. ): StreamScript {
  76. return async ([request], stream) => {
  77. const follow = request as SessionFollowRequest
  78. const result = await answer(history, follow)
  79. if (!result.ok) {
  80. stream.fail(result.error)
  81. return
  82. }
  83. const assistantStream = typeof options.assistantStream === 'function'
  84. ? options.assistantStream()
  85. : options.assistantStream
  86. stream.push(followSnapshot(result.value, follow, options.cursor, assistantStream))
  87. }
  88. }
  89. /**
  90. * `session/page` rule: the history answer cut at the request's `throughSeq`.
  91. * @param history - history answer or a function of the page request.
  92. * @returns the rule.
  93. */
  94. export function pageRule(
  95. history: HistorySource<SessionPageRequest>,
  96. ): UnaryRuleFn<readonly [SessionPageRequest], Promise<RemoteResult<SessionPage>>> {
  97. return async (page) => {
  98. const result = await answer(history, page)
  99. if (!result.ok) return result
  100. return ok(pageThrough(result.value, page.throughSeq))
  101. }
  102. }
  103. function answer<Request>(history: HistorySource<Request>, request: Request): HistoryAnswer {
  104. return typeof history === 'function' ? history(request) : history
  105. }
  106. /**
  107. * An image attachment reference as the Host's durable log carries it.
  108. * @param id - opaque attachment id.
  109. * @returns the reference.
  110. */
  111. export function imageRef(id: string): ImageAttachmentRef {
  112. return { attachmentId: AttachmentId(id), mediaType: 'image/png', bytes: 1, width: 2, height: 2 }
  113. }
  114. /**
  115. * A file attachment reference as the Host's durable log carries it.
  116. * @param id - opaque attachment id.
  117. * @param name - display filename.
  118. * @returns the reference.
  119. */
  120. export function fileRef(id: string, name = 'notes.txt'): FileAttachmentRef {
  121. return { attachmentId: AttachmentId(id), name, bytes: 3 }
  122. }
  123. /** Default answers: every command accepted, empty history and subagent catalog, one attachment of one zero byte. */
  124. export const sessionWorld: RemoteTable = {
  125. unary: {
  126. 'session/prompt': ok({ accepted: true }),
  127. 'session/cancel': ok({ accepted: true }),
  128. 'session/updateQueue': ok({ accepted: true }),
  129. 'session/rename': ok({ title: 'fk-renamed', seq: 0 }),
  130. 'session/attachment': ok({
  131. attachment: { attachmentId: 'a', mediaType: 'image/png', bytes: 1, width: 1, height: 1 },
  132. data: 'AA==',
  133. }),
  134. 'session/page': pageRule(ok({ records: [], hasMore: false })),
  135. 'subagents/list': ok({ entries: [], parentAvailable: true }),
  136. 'subagents/prompt': ok({ messageId: 'fake-message' }),
  137. 'subagents/interruptByParent': ok({ accepted: true }),
  138. },
  139. stream: {
  140. 'session/follow': followScript(ok({ records: [], hasMore: false })),
  141. },
  142. }