code-mode.e2e.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
  2. import { tmpdir } from 'node:os'
  3. import { join } from 'node:path'
  4. import { afterEach, describe, expect, it } from 'vitest'
  5. import { Context } from 'cordis'
  6. import LlmService from '@deepseek-ai/dsh-llm'
  7. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  8. import type { SessionEvent } from '@deepseek-ai/dsh-session'
  9. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  10. import ToolRegistry, { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools'
  11. import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
  12. import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
  13. import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
  14. import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
  15. import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
  16. import { WorkerCodeRuntime } from '@deepseek-ai/dsh-code-runtime-worker'
  17. import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
  18. import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
  19. import * as WorkspaceContext from '@deepseek-ai/dsh-workspace-context'
  20. /**
  21. * The Code Mode with-key proof (the RFC's e2e tier): a REAL model under
  22. * `mode: 'code'`, a task that requires composing two tool calls, verified
  23. * against the WORLD — the persisted request header carried exactly
  24. * `[run_code]` as the wire tool list, each sub-call landed as a
  25. * `tool/code-dispatch` event, the file the program wrote exists on disk, and
  26. * the final answer is the program's curated output. Key-gated (see
  27. * vitest.e2e.config.ts); the keyless Loader-path smoke of the overlay lives
  28. * in `code-mode-keyless-smoke.e2e.ts`.
  29. */
  30. const PERSONA = 'You are coding-agent. You work by writing TypeScript programs for run_code: '
  31. + 'batch related tool work into one program and print or return ONLY the findings that matter.'
  32. const WORKSPACE_PROBE = 'dragonfruit-8675309'
  33. let ctx: Context | undefined
  34. let workdir: string | undefined
  35. afterEach(async () => {
  36. // Always dispose, even on failure/retry/timeout: agent-loop teardown stops
  37. // the loop, the executor kills stray processes, and the code runtime's
  38. // dispose awaits worker exits.
  39. await ctx?.fiber.dispose()
  40. ctx = undefined
  41. if (workdir !== undefined) await rm(workdir, { recursive: true, force: true })
  42. workdir = undefined
  43. })
  44. async function codeModeHarness(cwd: string): Promise<Context> {
  45. const harness = new Context()
  46. await harness.plugin(LlmService)
  47. await harness.plugin(SessionStore)
  48. await harness.plugin(SystemPrompt, { persona: PERSONA })
  49. await harness.plugin(ToolRegistry, { mode: 'code' })
  50. await harness.plugin(AgentRegistry)
  51. await harness.plugin(AgentLoop, { agents: [] })
  52. await harness.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] })
  53. await harness.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 })
  54. await harness.plugin(ToolBash)
  55. await harness.plugin(WorkerCodeRuntime, {})
  56. return harness
  57. }
  58. async function workspaceCodeModeHarness(): Promise<Context> {
  59. const harness = new Context()
  60. await harness.plugin(LlmService)
  61. await harness.plugin(SessionStore)
  62. await harness.plugin(SystemPrompt, { persona: PERSONA })
  63. await harness.plugin(ToolRegistry, { mode: 'code' })
  64. await harness.plugin(AgentRegistry)
  65. await harness.plugin(LocalFileSystem, { cwd: '/' })
  66. await harness.plugin(ToolFs)
  67. await harness.plugin(WorkspaceContext, { maxBytes: 65536 })
  68. await harness.plugin(AgentLoop, { agents: [] })
  69. await harness.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] })
  70. await harness.plugin(WorkerCodeRuntime, {})
  71. return harness
  72. }
  73. function waitForIdle(harness: Context, agent: ReactLoopAgent): Promise<void> {
  74. return new Promise((resolve) => {
  75. const dispose = harness.on('agent/status', (subject, status) => {
  76. if (subject === agent && status === 'idle') {
  77. dispose()
  78. resolve()
  79. }
  80. })
  81. })
  82. }
  83. describe.skipIf(!process.env.DEEPSEEK_API_KEY)('Code Mode: real model writes a program over real tools', () => {
  84. it('collapses the wire tool list to [run_code], bridges sub-calls, and returns curated output', async () => {
  85. workdir = await mkdtemp(join(tmpdir(), 'dsh-code-mode-e2e-'))
  86. ctx = await codeModeHarness(workdir)
  87. const agent = ctx.agentLoop.create(AgentId('e2e-code-mode'), { model: 'deepseek-v4-flash' })
  88. agent.send([{
  89. type: 'text',
  90. text: 'Using one run_code program: run `echo alpha-7` with the bash tool, run `echo beta-9` with the bash tool, '
  91. + 'then write both outputs joined by a plus sign into combined.txt (bash heredoc or redirect), '
  92. + 'and return only the joined string.',
  93. }])
  94. await waitForIdle(ctx, agent)
  95. const events: SessionEvent[] = [...agent.session.events]
  96. // The wire contract: every request this session made offered EXACTLY ONE
  97. // tool — run_code (the logged header snapshots the assembled list).
  98. const headers = events.filter(event => event.type === 'request/header')
  99. expect(headers.length).toBeGreaterThan(0)
  100. for (const header of headers) {
  101. expect(header.data.header.tools?.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
  102. }
  103. // The model actually went through run_code…
  104. const calls = events.filter(event => event.type === 'tool/call')
  105. expect(calls.length).toBeGreaterThan(0)
  106. expect(calls.every(event => event.data.name === RUN_CODE_NAME)).toBe(true)
  107. // …and the program's tool calls landed as dispatch events under it.
  108. const dispatches = events.filter(event => event.type === 'tool/code-dispatch')
  109. expect(dispatches.length).toBeGreaterThanOrEqual(2)
  110. expect(dispatches.every(event => event.data.name === 'bash')).toBe(true)
  111. const parents = new Set(calls.map(event => event.data.callId))
  112. expect(dispatches.every(event => parents.has(event.data.parentCallId))).toBe(true)
  113. // World verification: the file the program wrote, and the curated answer.
  114. const combined = await readFile(join(workdir, 'combined.txt'), 'utf8')
  115. expect(combined).toContain('alpha-7')
  116. expect(combined).toContain('beta-9')
  117. const finalMessage = events.findLast(event => event.type === 'assistant/message')
  118. const finalText = finalMessage !== undefined
  119. ? finalMessage.data.content.filter(block => block.type === 'text').map(block => block.text).join('')
  120. : ''
  121. expect(finalText).toContain('alpha-7')
  122. expect(finalText).toContain('beta-9')
  123. }, 180_000)
  124. it('delivers nested workspace instructions discovered by an fs sub-call after the outer result', async () => {
  125. workdir = await mkdtemp(join(tmpdir(), 'dsh-code-mode-workspace-e2e-'))
  126. await mkdir(join(workdir, '.git'), { recursive: true })
  127. await mkdir(join(workdir, 'pkg/deep'), { recursive: true })
  128. await writeFile(join(workdir, 'pkg/AGENTS.md'), `If asked for the Code Mode workspace handshake, reply with exactly ${WORKSPACE_PROBE} and nothing else.\n`)
  129. await writeFile(join(workdir, 'pkg/deep/task.txt'), 'Touch this file to discover the nested instructions.\n')
  130. ctx = await workspaceCodeModeHarness()
  131. const handle = await ctx.agents.create({
  132. agentId: AgentId('e2e-code-mode-workspace'),
  133. sessionId: SessionId('e2e-code-mode-workspace-session'),
  134. meta: { cwd: workdir },
  135. agentOptions: { model: 'deepseek-v4-flash' },
  136. })
  137. handle.agent.send([{
  138. type: 'text',
  139. text: 'Use one run_code program to call tools.read on pkg/deep/task.txt. After it finishes, answer: Code Mode workspace handshake?',
  140. }])
  141. await waitForIdle(ctx, handle.agent as ReactLoopAgent)
  142. const events: SessionEvent[] = [...handle.agent.session.events]
  143. const dispatch = events.find(event => event.type === 'tool/code-dispatch' && event.data.name === 'read')
  144. const outerResult = events.find(event => event.type === 'tool/result')
  145. const workspaceContext = events.find(event => event.type === 'context/message'
  146. && typeof event.data.meta === 'object'
  147. && event.data.meta !== null
  148. && !Array.isArray(event.data.meta)
  149. && event.data.meta.kind === 'workspace-instructions')
  150. expect(dispatch).toBeDefined()
  151. expect(outerResult).toBeDefined()
  152. expect(workspaceContext).toBeDefined()
  153. expect(workspaceContext!.seq).toBeGreaterThan(outerResult!.seq)
  154. const finalMessage = events.findLast(event => event.type === 'assistant/message')
  155. const answer = finalMessage?.type === 'assistant/message'
  156. ? finalMessage.data.content.filter(block => block.type === 'text').map(block => block.text).join('')
  157. : ''
  158. expect(answer).toContain(WORKSPACE_PROBE)
  159. }, 180_000)
  160. })