workflow.e2e.ts 5.3 KB

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