workflow-worker-thread.e2e.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. import { afterEach, describe, expect, it } from 'vitest'
  2. import { Context } from '@deepseek-ai/cordis'
  3. import LlmRuntime from '@deepseek-ai/dsh-llm'
  4. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  5. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  6. import ToolRuntime from '@deepseek-ai/dsh-tools'
  7. import AgentRegistry from '@deepseek-ai/dsh-agent'
  8. import AgentLoop from '@deepseek-ai/dsh-agent-loop'
  9. import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
  10. import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
  11. import SubagentRuntime from '@deepseek-ai/dsh-subagent'
  12. import * as Spawn from '@deepseek-ai/dsh-subagent-spawn-in-process'
  13. import WorkerThreadWorkflowEngine from '../src/index.ts'
  14. /**
  15. * With-key e2e: a REAL script in a REAL worker thread
  16. * drives REAL spawn children against the live DeepSeek API — one plain child
  17. * and one schema'd child through the real structured-output runtime — and
  18. * the run's value, events, and child sessions are asserted from the outside
  19. * (never the script's self-report alone). Key-gated (self-skips without
  20. * DEEPSEEK_API_KEY).
  21. */
  22. let ctx: Context | undefined
  23. afterEach(async () => {
  24. await ctx?.fiber.dispose()
  25. ctx = undefined
  26. })
  27. async function harness(): Promise<Context> {
  28. const built = new Context()
  29. await built.plugin(LlmRuntime)
  30. await built.plugin(SessionStore)
  31. await built.plugin(SessionProjectionRegistry)
  32. await built.plugin(SystemPrompt)
  33. await built.plugin(ToolRuntime)
  34. await built.plugin(AgentRegistry)
  35. await built.plugin(AgentLoop, { agents: [] })
  36. await built.plugin(LlmDeepSeek)
  37. await built.plugin(SubagentRuntime)
  38. await built.plugin(Spawn, { providerName: 'spawn' })
  39. await built.plugin(WorkerThreadWorkflowEngine, { provider: 'spawn' })
  40. return built
  41. }
  42. const META = {
  43. name: 'e2e-worker-arithmetic',
  44. description: 'two real children through a worker thread: one prose, one structured',
  45. phases: [{ title: 'Ask' }, { title: 'Judge' }],
  46. }
  47. const SCRIPT = `phase('Ask')
  48. log('asking the prose child')
  49. const prose = await agent('Reply with exactly one short sentence: what is 2 + 2?')
  50. phase('Judge')
  51. const judged = await agent(
  52. 'Here is an answer to the question "what is 2+2": ' + prose
  53. + ' — report whether it contains the number 4 and your confidence between 0 and 1.',
  54. { schema: { type: 'object', properties: { containsFour: { type: 'boolean' }, confidence: { type: 'number' } }, required: ['containsFour'] } },
  55. )
  56. return { prose, containsFour: judged === null ? null : judged.containsFour }`
  57. describe.skipIf(!process.env.DEEPSEEK_API_KEY)('worker workflow engine with-key e2e', () => {
  58. it('runs a two-phase script in a worker thread over real children, one through the structured runtime', async () => {
  59. ctx = await harness()
  60. const parentHandle = await ctx.agents.create({
  61. sessionId: 'wf-worker-e2e-session' as never,
  62. agentOptions: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  63. })
  64. const events: string[] = []
  65. const childIds: string[] = []
  66. for (const name of ['workflow/start', 'workflow/phase', 'workflow/log', 'workflow/agent-start', 'workflow/agent-end', 'workflow/end'] as const) {
  67. ctx.on(name, (...payload: unknown[]) => {
  68. events.push(name)
  69. if (name === 'workflow/agent-start') childIds.push((payload[1] as { childId: string }).childId)
  70. })
  71. }
  72. const run = ctx.workflowEngine.start({ script: SCRIPT, meta: META, parent: parentHandle.agent })
  73. const result = await run.result
  74. await run.dispose()
  75. expect(result.stopReason).toBe('completed')
  76. expect(result.agentsStarted).toBe(2)
  77. const value = result.value as { prose: string; containsFour: boolean | null }
  78. // World checks: the prose child really answered (a real completion), and
  79. // the structured child judged it against the REAL schema-forced tool.
  80. expect(value.prose.length).toBeGreaterThan(0)
  81. expect(value.containsFour).toBe(true)
  82. expect(events[0]).toBe('workflow/start')
  83. expect(events.at(-1)).toBe('workflow/end')
  84. expect(events.filter(name => name === 'workflow/phase').length).toBe(2)
  85. expect(events.filter(name => name === 'workflow/agent-start').length).toBe(2)
  86. expect(childIds.length).toBe(2)
  87. // The children were disposed to quiescence after collection.
  88. for (const childId of childIds) {
  89. expect(ctx.agents.get(SessionId(childId))).toBeUndefined()
  90. }
  91. await parentHandle.dispose()
  92. }, 240_000)
  93. })