loader-composition.spec.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { mkdtemp, rm, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { pathToFileURL } from 'node:url'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. import { Context } from 'cordis'
  7. import Loader from '@cordisjs/plugin-loader'
  8. import Include from '@cordisjs/plugin-include'
  9. import type { Agent } from '@deepseek-ai/dsh-agent'
  10. import CommandService from '@deepseek-ai/dsh-commands'
  11. import {
  12. CompactService,
  13. type CompactAgentContext,
  14. type CompactionResult,
  15. type CompactionTrigger,
  16. type ManualCompactAgentContext,
  17. } from '@deepseek-ai/dsh-compact'
  18. import * as commandCompact from '@deepseek-ai/dsh-command-compact'
  19. import { Session, SessionId } from '@deepseek-ai/dsh-session'
  20. const RESULT: CompactionResult = {
  21. startSeq: 1,
  22. summarySeq: 2,
  23. endSeq: 3,
  24. summary: [{ type: 'text', text: 'loader summary' }],
  25. shadowedRange: { start: 3, end: 8 },
  26. shadowedSeqs: [3, 5, 8],
  27. shadowedTokenCount: 99,
  28. }
  29. class LoaderCompactService extends CompactService {
  30. override compactIfNeeded(
  31. _agent: CompactAgentContext,
  32. _trigger: CompactionTrigger,
  33. _signal: AbortSignal,
  34. ): Promise<CompactionResult | null> {
  35. return Promise.resolve(null)
  36. }
  37. override compactRegion(): Promise<CompactionResult> {
  38. return Promise.resolve(RESULT)
  39. }
  40. override compactNow(
  41. agent: ManualCompactAgentContext,
  42. _signal: AbortSignal,
  43. ): Promise<CompactionResult | null> {
  44. agent.session.append('compact/start', { turn: null })
  45. agent.session.append('compact/summary', {
  46. summary: RESULT.summary,
  47. shadowedRange: RESULT.shadowedRange,
  48. shadowedSeqs: RESULT.shadowedSeqs,
  49. shadowedTokenCount: RESULT.shadowedTokenCount,
  50. provider: 'loader-test',
  51. model: 'loader-test',
  52. })
  53. agent.session.append('compact/end', { turn: null })
  54. return Promise.resolve(RESULT)
  55. }
  56. }
  57. let root: string | undefined
  58. let context: Context | undefined
  59. afterEach(async () => {
  60. await context?.fiber.dispose()
  61. context = undefined
  62. if (root !== undefined) await rm(root, { recursive: true, force: true })
  63. root = undefined
  64. })
  65. describe('command-compact real Loader composition', () => {
  66. it('discovers and executes /compact through the assembled command plane', async () => {
  67. root = await mkdtemp(join(tmpdir(), 'dsh-command-compact-loader-'))
  68. const configPath = join(root, 'cordis.yml')
  69. await writeFile(configPath, [
  70. "- name: '@deepseek-ai/dsh-commands'",
  71. "- name: '@test/compact-backend'",
  72. "- name: '@deepseek-ai/dsh-command-compact'",
  73. '',
  74. ].join('\n'))
  75. context = new Context()
  76. context.baseUrl = pathToFileURL(root).href + '/'
  77. await context.plugin(Loader)
  78. context.loader.builtins.include = Include
  79. const modules = new Map<string, unknown>([
  80. ['@deepseek-ai/dsh-commands', CommandService],
  81. ['@test/compact-backend', LoaderCompactService],
  82. ['@deepseek-ai/dsh-command-compact', commandCompact],
  83. ])
  84. context.loader.internal = {
  85. version: 'v2',
  86. async import(specifier: string) {
  87. if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
  88. return modules.get(specifier)
  89. },
  90. } as unknown as NonNullable<typeof context.loader.internal>
  91. await context.loader.create({
  92. name: 'cordis:include',
  93. config: { path: pathToFileURL(configPath).href },
  94. })
  95. await context.loader.await()
  96. const session = Session.create(SessionId('loader-command-compact'))
  97. const agent = {
  98. session,
  99. status: 'idle',
  100. options: {},
  101. reserveTurnAdmission: () => () => undefined,
  102. } as unknown as Agent
  103. expect(context.commands.list(agent)).toContainEqual({
  104. name: 'compact',
  105. description: 'Compact older conversation history',
  106. })
  107. const execution = await context.commands.execute(agent, '/compact', new AbortController().signal)
  108. if (execution === undefined) throw new Error('Loader composition did not resolve /compact')
  109. expect(execution.result).toEqual({
  110. kind: 'success',
  111. text: 'Compacted 3 history items (~99 tokens).',
  112. sourceEventSeq: RESULT.summarySeq,
  113. })
  114. expect(session.events.map(event => ({ type: event.type, data: event.data }))).toEqual([
  115. {
  116. type: 'command/run',
  117. data: {
  118. commandId: execution.commandId,
  119. name: 'compact',
  120. args: '',
  121. source: { kind: 'user' },
  122. },
  123. },
  124. {
  125. type: 'compact/start',
  126. data: { turn: null },
  127. },
  128. {
  129. type: 'compact/summary',
  130. data: {
  131. summary: RESULT.summary,
  132. shadowedRange: RESULT.shadowedRange,
  133. shadowedSeqs: RESULT.shadowedSeqs,
  134. shadowedTokenCount: RESULT.shadowedTokenCount,
  135. provider: 'loader-test',
  136. model: 'loader-test',
  137. },
  138. },
  139. {
  140. type: 'compact/end',
  141. data: { turn: null },
  142. },
  143. {
  144. type: 'command/done',
  145. data: {
  146. commandId: execution.commandId,
  147. kind: 'success',
  148. text: 'Compacted 3 history items (~99 tokens).',
  149. sourceEventSeq: RESULT.summarySeq,
  150. },
  151. },
  152. ])
  153. expect(session.surface.nodes).toEqual([])
  154. expect(session.deriveMessages()).toEqual([])
  155. })
  156. })