loader-composition.spec.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. /** Real Loader and AgentLoop composition with only external native/model fixtures. */
  2. import { mkdtemp, rm, writeFile } from 'node:fs/promises'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { pathToFileURL } from 'node:url'
  6. import { afterEach, expect, it, vi } from 'vitest'
  7. import { Context } from '@deepseek-ai/cordis'
  8. import Loader from '@deepseek-ai/cordis-plugin-loader'
  9. import Include from '@deepseek-ai/cordis-plugin-include'
  10. import AgentRegistry from '@deepseek-ai/dsh-agent'
  11. import AgentLoop from '@deepseek-ai/dsh-agent-loop'
  12. import ComputerUseRegistry from '@deepseek-ai/dsh-computer-use'
  13. import LocalAttachmentStore from '@deepseek-ai/dsh-attachment-local'
  14. import LlmRuntime, { LlmAdapter, ToolCallId, createUserMessage } from '@deepseek-ai/dsh-llm'
  15. import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
  16. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  17. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  18. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  19. import ToolRuntime from '@deepseek-ai/dsh-tools'
  20. import * as NativeProvider from '../src/index.ts'
  21. import { resetFixture, screenshotBase64 } from './fixtures/cua-driver.ts'
  22. vi.mock('@trycua/cua-driver', async () => import('./fixtures/cua-driver.ts'))
  23. class VisualModel extends LlmAdapter {
  24. readonly requests: GenerateOptions[] = []
  25. override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
  26. return Promise.resolve({ provider, id: model, name: model, inputModalities: ['text', 'image'] })
  27. }
  28. async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
  29. this.requests.push(options)
  30. if (this.requests.length === 1) {
  31. const call = { type: 'tool-call' as const, id: ToolCallId('native-window'), name: 'cua_driver_native__get_window_state', arguments: '{"pid":9,"window_id":7}' }
  32. yield { type: 'block-start', index: 0, blockType: 'tool-call' }
  33. yield { type: 'block-end', index: 0, block: call }
  34. yield { type: 'finish', reason: { kind: 'tool-calls' } }
  35. return
  36. }
  37. yield { type: 'block-start', index: 0, blockType: 'text' }
  38. yield { type: 'block-end', index: 0, block: { type: 'text', text: 'Observed the fixture window.' } }
  39. yield { type: 'finish', reason: { kind: 'stop' } }
  40. }
  41. }
  42. let ctx: Context | undefined
  43. let root: string | undefined
  44. afterEach(async () => {
  45. await ctx?.fiber.dispose()
  46. if (root !== undefined) await rm(root, { recursive: true, force: true })
  47. ctx = undefined
  48. root = undefined
  49. })
  50. it('loads from cordis.yml and logs the native screenshot before the next model request', async () => {
  51. resetFixture()
  52. root = await mkdtemp(join(tmpdir(), 'dsh-native-composition-'))
  53. const configPath = join(root, 'cordis.yml')
  54. const modules = new Map<string, unknown>([
  55. ['@deepseek-ai/dsh-llm', LlmRuntime],
  56. ['@deepseek-ai/dsh-session', SessionStore],
  57. ['@deepseek-ai/dsh-session-projection', SessionProjectionRegistry],
  58. ['@deepseek-ai/dsh-system-prompt', SystemPrompt],
  59. ['@deepseek-ai/dsh-tools', ToolRuntime],
  60. ['@deepseek-ai/dsh-agent', AgentRegistry],
  61. ['@deepseek-ai/dsh-agent-loop', AgentLoop],
  62. ['@deepseek-ai/dsh-attachment-local', LocalAttachmentStore],
  63. ['@deepseek-ai/dsh-computer-use', ComputerUseRegistry],
  64. ['@deepseek-ai/dsh-experimental-computer-use-cua-driver-native', NativeProvider],
  65. ])
  66. await writeFile(configPath, [...modules.keys()].flatMap(name => [
  67. `- name: '${name}'`,
  68. ...name === '@deepseek-ai/dsh-attachment-local' ? [' config:', ` dshHome: ${JSON.stringify(root)}`] : [],
  69. ]).join('\n') + '\n')
  70. const context = ctx = new Context()
  71. context.baseUrl = pathToFileURL(root).href + '/'
  72. await context.plugin(Loader)
  73. context.loader.builtins.include = Include
  74. context.loader.internal = {
  75. version: 'v2',
  76. async import(specifier: string) {
  77. if (!modules.has(specifier)) throw new Error(`Unexpected fixture module: ${specifier}`)
  78. return modules.get(specifier)
  79. },
  80. } as unknown as NonNullable<typeof context.loader.internal>
  81. await context.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(configPath).href } })
  82. await context.loader.await()
  83. for (const entry of context.loader.entries()) await entry.fiber?.await()
  84. const model = new VisualModel()
  85. context.llm.registerAdapter(['native-fixture'], model)
  86. const agent = await context.agentLoop.create(SessionId('native-loader'), { provider: 'native-fixture', model: 'vision' })
  87. const idle: PromiseWithResolvers<void> = Promise.withResolvers()
  88. const stop = context.on('agent/status', ({ agent: subject, status }) => {
  89. if (subject === agent && status === 'idle') idle.resolve()
  90. })
  91. agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Observe the fixture window.' }], source: { kind: 'user' } }))
  92. await idle.promise
  93. stop()
  94. expect(model.requests).toHaveLength(2)
  95. expect(JSON.stringify(model.requests[0])).toContain('cua_driver_native__get_window_state')
  96. expect(JSON.stringify(model.requests[0])).toContain('Cua Driver native computer-use tools operate the host desktop.')
  97. const toolEvent = agent.session.snapshotEvents().find(event => event.type === 'tool/result')
  98. expect(toolEvent?.data.message.source.callId).toBe('native-window')
  99. const toolResult = agent.session.deriveMessages().flatMap(message => message.content).find(block => block.type === 'tool-result')
  100. const image = toolResult?.type === 'tool-result' ? toolResult.content.find(block => block.type === 'image') : undefined
  101. expect(image?.type).toBe('image')
  102. if (image?.type !== 'image') throw new Error('Native screenshot was not admitted')
  103. expect(image.attachment).toMatchObject({ mediaType: 'image/png', width: 1, height: 1 })
  104. const stored = await context.attachments.readImage(image.attachment)
  105. expect(Buffer.from(stored.data).toString('base64')).toBe(screenshotBase64)
  106. expect(JSON.stringify(model.requests[1]?.messages)).toContain(JSON.stringify(image.attachment))
  107. expect(JSON.stringify(toolResult?.content)).not.toContain(screenshotBase64)
  108. const direct = await context.tools.execute({
  109. agent, signal: new AbortController().signal, callId: ToolCallId('programmatic-window'),
  110. name: 'cua_driver_native__get_window_state', arguments: { pid: 9, window_id: 7 },
  111. })
  112. if (direct.isError) throw new Error('Programmatic native screenshot failed')
  113. expect(direct.value).toMatchObject({ structuredContent: { window_id: 7, clicked: false } })
  114. })