agent-continuation.worker.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /** Plain-Node measurements of active request history and cold tool-heavy continuation. */
  2. import { performance } from 'node:perf_hooks'
  3. import { scheduler } from 'node:timers/promises'
  4. import { Context } from '@deepseek-ai/cordis'
  5. import AgentLoop from '@deepseek-ai/dsh-agent-loop'
  6. import type { Agent, AgentHandle } from '@deepseek-ai/dsh-agent'
  7. import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
  8. import { createUserMessage, LlmAdapter } from '@deepseek-ai/dsh-llm'
  9. import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
  10. import { SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
  11. import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
  12. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  13. import { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
  14. import { assertBuiltBenchmarkRuntime } from '../support/built-worker.ts'
  15. import { PARENT_ID, response, resultText, syntheticHistory, TIME_ZERO, WORKLOAD } from './workload.ts'
  16. /** Raw timing and retained-memory report from one isolated backend process. */
  17. export interface ContinuationReport {
  18. readonly totalMs: number
  19. readonly resumeMs: number
  20. readonly turnsMs: number
  21. readonly flushMs: number
  22. readonly cpuUserMs: number
  23. readonly cpuSystemMs: number
  24. readonly retainedHeapMb: number
  25. readonly peakRssMb: number
  26. readonly requests: number
  27. readonly toolCalls: number
  28. readonly events: number
  29. }
  30. class SyntheticAdapter extends LlmAdapter {
  31. requests = 0
  32. constructor(private readonly toolsPerTurn: number) { super() }
  33. override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
  34. return Promise.resolve({ provider, id: model, name: model })
  35. }
  36. async * stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
  37. const tools = this.toolsPerTurn > 0 && this.requests % 2 === 0 ? this.toolsPerTurn : 0
  38. const reply = response(100_000 + this.requests++, tools)
  39. yield* reply.chunks
  40. }
  41. }
  42. async function collectHeap(): Promise<number> {
  43. if (globalThis.gc === undefined) throw new Error('backend benchmark requires --expose-gc')
  44. globalThis.gc()
  45. await scheduler.yield()
  46. globalThis.gc()
  47. return process.memoryUsage().heapUsed / 1_048_576
  48. }
  49. async function seed(root: string): Promise<void> {
  50. const ctx = new Context()
  51. try {
  52. await mountAgentLoopTestDependencies(ctx)
  53. await ctx.plugin(JsonlSessionPersistence, { root, compression: 'zstd' })
  54. const handle = await ctx.sessionPersistence.create({
  55. version: SESSION_FORMAT_VERSION, id: PARENT_ID, createdAt: TIME_ZERO, cwd: '/bench', isSeeded: false,
  56. }, {})
  57. try {
  58. await handle.append(syntheticHistory(WORKLOAD.historyTurns))
  59. await handle.flush()
  60. } finally { await handle.close() }
  61. } finally { await ctx.fiber.dispose() }
  62. }
  63. async function runTurns(agent: Agent, turns: number): Promise<void> {
  64. for (let turn = 0; turn < turns; turn++) {
  65. agent.followup(createUserMessage({ content: [{ type: 'text', text: 'Continue synthetic task ' + String(turn) }], source: { kind: 'user' } }))
  66. await agent.whenIdle()
  67. }
  68. }
  69. async function measure(root: string, scenario: string): Promise<ContinuationReport> {
  70. const ctx = new Context()
  71. let handle: AgentHandle | undefined
  72. const toolHeavy = scenario === 'tool-continuation'
  73. const adapter = new SyntheticAdapter(toolHeavy ? WORKLOAD.toolsPerLiveTurn : 0)
  74. let toolCalls = 0
  75. try {
  76. await ctx.plugin(SessionProjectionRegistry)
  77. await mountAgentLoopTestDependencies(ctx)
  78. await ctx.plugin(JsonlSessionPersistence, { root, compression: 'zstd' })
  79. await ctx.plugin(AgentLoop, { agents: [] })
  80. ctx.effect(() => ctx.llm.registerAdapter(['bench'], adapter))
  81. ctx.effect(() => ctx.tools.register(defineContentToolFixture({
  82. name: 'bench_tool', description: 'Read a bounded synthetic module.',
  83. parameters: { ordinal: { type: 'number', required: true } },
  84. isConcurrencySafe: () => true,
  85. execute(args) {
  86. toolCalls++
  87. return Promise.resolve([{ type: 'text', text: resultText(args.ordinal) }])
  88. },
  89. })))
  90. if (!toolHeavy) {
  91. handle = await ctx.agents.resume({ resumeSessionId: PARENT_ID, agentOptions: { provider: 'bench', model: 'bench' } })
  92. }
  93. const beforeHeap = await collectHeap()
  94. const cpuStart = process.cpuUsage()
  95. const start = performance.now()
  96. if (handle === undefined) {
  97. handle = await ctx.agents.resume({ resumeSessionId: PARENT_ID, agentOptions: { provider: 'bench', model: 'bench' } })
  98. }
  99. const resumed = performance.now()
  100. await runTurns(handle.agent, toolHeavy ? WORKLOAD.continuationTurns : WORKLOAD.requestTurns)
  101. const turnsDone = performance.now()
  102. await ctx.sessions.flush(handle.agent.session)
  103. const end = performance.now()
  104. const cpu = process.cpuUsage(cpuStart)
  105. const retainedHeapMb = (await collectHeap()) - beforeHeap
  106. if (adapter.requests !== (toolHeavy ? WORKLOAD.continuationTurns * 2 : WORKLOAD.requestTurns)
  107. || toolCalls !== (toolHeavy ? WORKLOAD.continuationTurns * WORKLOAD.toolsPerLiveTurn : 0)) {
  108. throw new Error('backend benchmark did not complete every requested model/tool step')
  109. }
  110. return {
  111. totalMs: end - start, resumeMs: resumed - start, turnsMs: turnsDone - resumed, flushMs: end - turnsDone,
  112. cpuUserMs: cpu.user / 1_000, cpuSystemMs: cpu.system / 1_000,
  113. retainedHeapMb, peakRssMb: process.resourceUsage().maxRSS / 1_024,
  114. requests: adapter.requests, toolCalls, events: handle.agent.session.seq,
  115. }
  116. } finally {
  117. await handle?.dispose()
  118. await ctx.fiber.dispose()
  119. }
  120. }
  121. assertBuiltBenchmarkRuntime(import.meta.url, Object.fromEntries([
  122. '@deepseek-ai/dsh-agent-loop', '@deepseek-ai/dsh-session', '@deepseek-ai/dsh-llm',
  123. '@deepseek-ai/dsh-tools', '@deepseek-ai/dsh-session-persistence-jsonl',
  124. ].map(name => [name, import.meta.resolve(name)])))
  125. const [root, scenario] = process.argv.slice(2)
  126. if (root === undefined || scenario === undefined || !['seed', 'request-history', 'tool-continuation'].includes(scenario)) {
  127. throw new Error('usage: agent-continuation.worker.js <root> <seed|request-history|tool-continuation>')
  128. }
  129. if (scenario === 'seed') {
  130. await seed(root)
  131. process.stdout.write(JSON.stringify({ seeded: true }) + '\n')
  132. } else {
  133. process.stdout.write(JSON.stringify(await measure(root, scenario)) + '\n')
  134. }