event-script.client.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import {
  2. ToolCallId, createMessage, createToolResultMessage, createUserMessage,
  3. } from '@deepseek-ai/dsh-llm'
  4. import { SessionSeq } from '@deepseek-ai/dsh-session/types'
  5. // Minimal SessionEvent builders for orchestration tests (shape mirrors what the
  6. // host emits; only the fields the object layer reads).
  7. import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
  8. import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
  9. import type {
  10. SessionEventEntry,
  11. SessionPage,
  12. SessionWireEvent,
  13. } from '../src/types.ts'
  14. /** One text content block (local helper). */
  15. const text = (t: string): ContentBlock[] => [{ type: 'text', text: t }]
  16. const at = (seq: SessionSeq, e: Record<string, unknown>): SessionEvent =>
  17. ({ seq, time: 1_700_000_000_000 + seq, ...e }) as unknown as SessionEvent
  18. export const ev = {
  19. turnStart: (seq: SessionSeq, turn: number): SessionEvent =>
  20. at(seq, { type: 'turn/start', data: { turn } }),
  21. user: (seq: SessionSeq, body: string): SessionEvent =>
  22. at(seq, { type: 'user/message', surfaceOp: 'append', data: createUserMessage({
  23. content: text(body), source: { kind: 'user' },
  24. }) }),
  25. stepStart: (seq: SessionSeq, turn: number, step = 0): SessionEvent =>
  26. at(seq, { type: 'step/start', data: { turn, step } }),
  27. assistant: (seq: SessionSeq, turn: number, body: string, step = 0): SessionEvent =>
  28. at(seq, { type: 'assistant/message', surfaceOp: 'append', data: {
  29. turn, step,
  30. stream: [
  31. { type: 'chunk', time: 1_700_000_000_000 + seq, chunk: { type: 'block-start', index: 0, blockType: 'text' } },
  32. { type: 'text-chunks', time0: 1_700_000_000_000 + seq, index: 0, dt: [], texts: [body] },
  33. {
  34. type: 'chunk', time: 1_700_000_000_000 + seq,
  35. chunk: { type: 'block-end', index: 0, block: { type: 'text', text: body } },
  36. },
  37. { type: 'chunk', time: 1_700_000_000_000 + seq, chunk: { type: 'finish', reason: { kind: 'stop' } } },
  38. ],
  39. message: createMessage({
  40. role: 'assistant',
  41. content: text(body),
  42. source: {
  43. kind: 'model',
  44. ...{ provider: 'fake', model: 'fk-1' },
  45. },
  46. }),
  47. } }),
  48. toolCall: (seq: SessionSeq, turn: number, callId: string, name: string, args: string, step = 0): SessionEvent =>
  49. at(seq, { type: 'tool/call', data: { turn, step, callId, name, arguments: args } }),
  50. toolResult: (seq: SessionSeq, turn: number, callId: string, body: string, step = 0): SessionEvent =>
  51. at(seq, {
  52. type: 'tool/result',
  53. surfaceOp: 'append',
  54. data: {
  55. turn,
  56. step,
  57. message: createToolResultMessage({
  58. callId: ToolCallId(callId),
  59. content: text(body),
  60. isError: false,
  61. }),
  62. },
  63. }),
  64. codeDispatchStart: (seq: SessionSeq, parentCallId: string, n: number, name: string, args: unknown): SessionEvent =>
  65. at(seq, {
  66. type: 'tool/code-dispatch-start',
  67. data: { rootCallId: parentCallId, parentCallId, subCallId: `${parentCallId}:code:${n}`, name, arguments: args },
  68. }),
  69. codeDispatch: (
  70. seq: SessionSeq,
  71. parentCallId: string,
  72. n: number,
  73. name: string,
  74. args: unknown,
  75. body: string,
  76. isError = false,
  77. ): SessionEvent =>
  78. at(seq, {
  79. type: 'tool/code-dispatch',
  80. data: { rootCallId: parentCallId, parentCallId, subCallId: `${parentCallId}:code:${n}`, name, arguments: args, isError, content: text(body) },
  81. }),
  82. stepEnd: (seq: SessionSeq, turn: number, step = 0): SessionEvent =>
  83. at(seq, { type: 'step/end', data: { turn, step } }),
  84. retry: (
  85. seq: SessionSeq,
  86. turn: number,
  87. step = 0,
  88. retry = 1,
  89. maxRetries = 2,
  90. delayMs = 500,
  91. message = 'temporary transport failure',
  92. ): SessionEvent =>
  93. at(seq, {
  94. type: 'llm/retry',
  95. data: {
  96. turn, step,
  97. provider: 'fake', mode: 'normal', policyKey: 'fake-normal',
  98. retry, maxRetries, delayMs,
  99. failure: { code: 'TRANSPORT', message },
  100. },
  101. }),
  102. turnEnd: (seq: SessionSeq, turn: number, reason: 'completed' | 'aborted' | 'disposed' = 'completed'): SessionEvent =>
  103. at(seq, { type: 'turn/end', data: {
  104. turn,
  105. reason: reason === 'completed'
  106. ? { kind: 'completed' }
  107. : { kind: 'aborted', reason: { kind: reason === 'disposed' ? 'disposed' : 'user' } },
  108. } }),
  109. commandRun: (seq: SessionSeq, commandId: string, name: string, args = ''): SessionEvent =>
  110. at(seq, { type: 'command/run', data: { commandId, name, args, source: { kind: 'user' } } }),
  111. commandRunWithoutInput: (seq: SessionSeq, commandId: string, name: string): SessionEvent =>
  112. at(seq, { type: 'command/run', data: { commandId, name, source: { kind: 'user' } } }),
  113. commandDone: (
  114. seq: SessionSeq,
  115. commandId: string,
  116. kind: 'success' | 'error' = 'success',
  117. text?: string,
  118. sourceEventSeq?: SessionSeq,
  119. ): SessionEvent =>
  120. at(seq, { type: 'command/done', data: {
  121. commandId,
  122. kind,
  123. ...text === undefined ? {} : { text },
  124. ...sourceEventSeq === undefined ? {} : { sourceEventSeq },
  125. } }),
  126. /** A compaction's log-only `compaction/summary` record. */
  127. compactSummary: (seq: SessionSeq, summary: string, start: SessionSeq, end: SessionSeq): SessionEvent =>
  128. at(seq, { type: 'compaction/summary', data: {
  129. summary: text(summary),
  130. shadowedRange: { start, end },
  131. shadowedSeqs: [start, end],
  132. shadowedTokenCount: 100,
  133. provider: 'fake',
  134. model: 'compact-1',
  135. } }),
  136. /** The replacement user message a compaction backend lands (the checkpoint). */
  137. compactCheckpoint: (
  138. seq: SessionSeq,
  139. summarySeq: SessionSeq,
  140. start: SessionSeq,
  141. end: SessionSeq,
  142. ): SessionEvent =>
  143. at(seq, {
  144. type: 'user/message',
  145. surfaceOp: { op: 'replace', start, end },
  146. sourceEventSeqs: [summarySeq, start, end],
  147. data: createUserMessage({
  148. content: text('<context_checkpoint>model only</context_checkpoint>'),
  149. source: { kind: 'plugin', plugin: 'compact' },
  150. }),
  151. }),
  152. }
  153. /** One complete plain turn (turn/start → user → step → assistant → turn/end), 6 events from startSeq. */
  154. export function plainTurn(startSeq: SessionSeq, turn: number, ask: string, answer: string): SessionEvent[] {
  155. return [
  156. ev.turnStart(startSeq, turn),
  157. ev.user(SessionSeq(startSeq + 1), ask),
  158. ev.stepStart(SessionSeq(startSeq + 2), turn),
  159. ev.assistant(SessionSeq(startSeq + 3), turn, answer),
  160. ev.stepEnd(SessionSeq(startSeq + 4), turn),
  161. ev.turnEnd(SessionSeq(startSeq + 5), turn),
  162. ]
  163. }
  164. /** Wrap raw events in the journal envelope returned by history. */
  165. export function entries(events: readonly SessionEvent[]): SessionEventEntry[] {
  166. return events.map(event => ({ type: 'event', event: event as unknown as SessionWireEvent }))
  167. }
  168. /** Build one view-less history response value. */
  169. export function historyValue(events: readonly SessionEvent[], hasMore = false): SessionPage {
  170. return {
  171. records: entries(events),
  172. hasMore,
  173. }
  174. }