| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- import { createServer } from 'node:http'
- import type { IncomingMessage, Server, ServerResponse } from 'node:http'
- import { mkdtemp, rm } from 'node:fs/promises'
- import { join } from 'node:path'
- import { tmpdir } from 'node:os'
- import { afterEach, describe, expect, it, vi } from 'vitest'
- import { Context } from 'cordis'
- import { AgentId, type Agent, type AgentHandle } from '@deepseek-ai/dsh-agent'
- import { SessionId } from '@deepseek-ai/dsh-session'
- import * as agentCore from '@deepseek-ai/dsh-agent-core'
- import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
- import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
- import SubagentService, { type SubagentRunEndInfo } from '@deepseek-ai/dsh-subagent'
- import { HarnessSdkServer, type JsonRpcTransportPeer } from '../src/index.ts'
- class FakeTransport implements JsonRpcTransportPeer {
- notifications: { method: string; params?: Record<string, unknown> }[] = []
- async request(method: string, params: Record<string, unknown>): Promise<unknown> {
- throw new Error(`the SDK server should not call host JSON-RPC method ${method} with ${JSON.stringify(params)}`)
- }
- notify(method: string, params?: Record<string, unknown>): void {
- this.notifications.push(params === undefined ? { method } : { method, params })
- }
- }
- const servers: Server[] = []
- afterEach(async () => {
- await Promise.all(servers.splice(0).map(server => new Promise(resolve => server.close(resolve))))
- vi.unstubAllEnvs()
- })
- async function mockCompletionServer(): Promise<{ url: string; requests: unknown[]; headers: IncomingMessage['headers'][] }> {
- const requests: unknown[] = []
- const headers: IncomingMessage['headers'][] = []
- const server = createServer((request: IncomingMessage, response: ServerResponse) => {
- let body = ''
- request.on('data', (chunk: Buffer) => { body += chunk.toString('utf8') })
- request.on('end', () => {
- requests.push(JSON.parse(body))
- headers.push(request.headers)
- response.writeHead(200, { 'content-type': 'text/event-stream' })
- response.write('data: {"choices":[{"delta":{"role":"assistant","content":null,"reasoning_content":""}}]}\n\n')
- response.write('data: {"choices":[{"delta":{"content":"done"}}]}\n\n')
- response.write('data: {"choices":[{"delta":{"content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}\n\n')
- response.write('data: [DONE]\n\n')
- response.end()
- })
- })
- servers.push(server)
- await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
- const address = server.address()
- if (address === null || typeof address === 'string') throw new Error('no port')
- return { url: `http://127.0.0.1:${address.port}`, requests, headers }
- }
- async function makeHarness(storageDir: string) {
- const ctx = new Context()
- await ctx.plugin(agentCore)
- await ctx.plugin(SubagentService)
- await ctx.plugin(SessionPersistenceJsonl, { root: storageDir })
- await new Promise(resolve => setTimeout(resolve, 50))
- return ctx
- }
- /** Drive the owning service so test lifecycle events carry the real parent scope. */
- async function settleSubagent(ctx: Context, parent: Agent, info: SubagentRunEndInfo): Promise<void> {
- const disposeProvider = ctx.subagents.registerProvider({
- name: info.provider,
- capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
- inheritsParentContext: false,
- async start() {
- return {
- id: info.id,
- result: info.lastAssistantMessage === undefined
- ? Promise.reject(new Error('synthetic infrastructure failure'))
- : Promise.resolve({ output: info.lastAssistantMessage, stopReason: info.stopReason }),
- dispose: () => Promise.resolve(),
- }
- },
- })
- try {
- const run = await ctx.subagents.start(info.provider, {
- parent,
- prompt: [],
- signal: new AbortController().signal,
- })
- await run.result.then(() => undefined, () => undefined)
- await run.dispose()
- } finally {
- disposeProvider()
- }
- }
- describe('HarnessSdkServer', () => {
- it('creates a harness agent and calls the configured OpenAI-compatible endpoint', async () => {
- const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-'))
- const llmServer = await mockCompletionServer()
- vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
- vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url)
- const ctx = await makeHarness(storageDir)
- try {
- const transport = new FakeTransport()
- const server = new HarnessSdkServer(ctx, transport)
- const init = await server.handleRequest('initialize', {
- cwd: storageDir,
- model: 'dsagent-model',
- }) as { serverInfo: { name: string } }
- expect(init.serverInfo.name).toBe('deepseek-harness-sdk-runtime')
- await server.handleRequest('session/prompt', {
- sessionId: 'main',
- contentBlocks: [{ type: 'text', text: 'fix it' }],
- })
- expect(llmServer.requests).toHaveLength(1)
- const body = llmServer.requests[0] as { model: string; messages: { role: string }[] }
- expect(body.model).toBe('dsagent-model')
- expect(body.messages[0]?.role).toBe('system')
- expect(body.messages.at(-1)?.role).toBe('user')
- expect(llmServer.headers[0]?.authorization).toBe('Bearer test-key')
- expect(transport.notifications.some(n => n.method === 'session.event')).toBe(true)
- expect(transport.notifications.at(-1)).toMatchObject({
- method: 'session.finished',
- params: { sessionId: 'main', status: 'ok' },
- })
- await server.handleRequest('session/prompt', {
- sessionId: 'main',
- contentBlocks: [{ type: 'text', text: 'again' }],
- })
- expect(llmServer.requests).toHaveLength(2)
- const orphanHandle = await ctx.agents.create({
- agentId: AgentId('orphan-agent'),
- sessionId: SessionId('orphan-session'),
- meta: { cwd: storageDir },
- agentOptions: { model: 'dsagent-model' },
- })
- orphanHandle.agent.send([{ type: 'text', text: 'outside the sdk session map' }])
- await orphanHandle.agent.whenIdle()
- await orphanHandle.dispose()
- expect(llmServer.requests).toHaveLength(3)
- await server.handleRequest('shutdown', undefined)
- } finally {
- await ctx.fiber.dispose()
- await rm(storageDir, { recursive: true, force: true })
- }
- })
- it('rejects overlapping prompts for one session without serializing other sessions', async () => {
- let releaseMain: (() => void) | undefined
- const firstMainIdle = new Promise<void>((resolve) => { releaseMain = resolve })
- const mainWhenIdle = vi.fn<() => Promise<void>>()
- .mockReturnValueOnce(firstMainIdle)
- .mockResolvedValue(undefined)
- const mainSend = vi.fn()
- const mainAgent = {
- send: mainSend,
- whenIdle: mainWhenIdle,
- } as unknown as Agent
- const otherSend = vi.fn()
- const otherAgent = {
- send: otherSend,
- whenIdle: vi.fn(() => Promise.resolve()),
- } as unknown as Agent
- const mainHandle = { agent: mainAgent, dispose: vi.fn(() => Promise.resolve()) }
- const otherHandle = { agent: otherAgent, dispose: vi.fn(() => Promise.resolve()) }
- const create = vi.fn(async (options: { agentId: AgentId }) =>
- String(options.agentId) === 'main' ? mainHandle : otherHandle)
- const ctx = {
- on: vi.fn(() => () => undefined),
- agents: { create, get: () => undefined },
- get: () => undefined,
- } as unknown as Context
- const server = new HarnessSdkServer(ctx, new FakeTransport())
- const prompt = (sessionId: string, text: string) => server.prompt({
- sessionId,
- contentBlocks: [{ type: 'text', text }],
- })
- const first = prompt('main', 'first')
- await vi.waitFor(() => { expect(mainSend).toHaveBeenCalledOnce() })
- await expect(prompt('main', 'overlap')).rejects.toThrow('session already has an active prompt: main')
- await expect(prompt('other', 'independent')).resolves.toEqual({ accepted: true })
- releaseMain?.()
- await expect(first).resolves.toEqual({ accepted: true })
- await expect(prompt('main', 'sequential')).resolves.toEqual({ accepted: true })
- mainWhenIdle.mockRejectedValueOnce(new Error('turn wait failed'))
- await expect(prompt('main', 'failing')).rejects.toThrow('turn wait failed')
- await expect(prompt('main', 'after failure')).resolves.toEqual({ accepted: true })
- expect(mainSend).toHaveBeenCalledTimes(4)
- expect(otherSend).toHaveBeenCalledOnce()
- await server.shutdown()
- expect(mainHandle.dispose).toHaveBeenCalledOnce()
- expect(otherHandle.dispose).toHaveBeenCalledOnce()
- })
- it('notifies the host when a child session is created with parent lineage', async () => {
- const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-subagent-'))
- const ctx = await makeHarness(storageDir)
- try {
- const transport = new FakeTransport()
- const server = new HarnessSdkServer(ctx, transport)
- ctx.sessions.create(SessionId('root-session'), {
- meta: { cwd: storageDir },
- })
- ctx.sessions.create(SessionId('child-session'), {
- meta: { cwd: storageDir, parentSession: SessionId('main') },
- })
- expect(transport.notifications).toContainEqual({
- method: 'subagent.started',
- params: {
- parentSessionId: 'main',
- childSessionId: 'child-session',
- },
- })
- await server.shutdown()
- } finally {
- await ctx.fiber.dispose()
- await rm(storageDir, { recursive: true, force: true })
- }
- })
- it('creates an SDK session without an optional system prompt', async () => {
- const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-no-system-'))
- const llmServer = await mockCompletionServer()
- vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
- vi.stubEnv('DEEPSEEK_BASE_URL', llmServer.url)
- const ctx = await makeHarness(storageDir)
- try {
- const server = new HarnessSdkServer(ctx, new FakeTransport())
- await server.initialize({ cwd: storageDir, model: 'plain-model' })
- await server.prompt({
- sessionId: 'plain',
- contentBlocks: [{ type: 'text', text: 'hello' }],
- })
- expect(llmServer.requests).toHaveLength(1)
- await server.shutdown()
- } finally {
- await ctx.fiber.dispose()
- await rm(storageDir, { recursive: true, force: true })
- }
- })
- it('notifies the host when a subagent run settles', async () => {
- const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-subagent-end-'))
- const ctx = await makeHarness(storageDir)
- try {
- const transport = new FakeTransport()
- const server = new HarnessSdkServer(ctx, transport)
- const parentHandle = await ctx.agents.create({
- agentId: AgentId('parent-agent'),
- sessionId: SessionId('main'),
- meta: { cwd: storageDir },
- agentOptions: { model: 'deepseek' },
- })
- const handle = await ctx.agents.create({
- agentId: AgentId('child-agent'),
- sessionId: SessionId('child-session'),
- meta: { cwd: storageDir, parentSession: SessionId('main') },
- agentOptions: { model: 'deepseek' },
- })
- await settleSubagent(ctx, parentHandle.agent, {
- provider: 'spawn',
- id: AgentId('child-agent'),
- stopReason: 'completed',
- lastAssistantMessage: [{ type: 'text', text: 'child done' }],
- })
- expect(transport.notifications).toContainEqual({
- method: 'subagent.finished',
- params: {
- provider: 'spawn',
- agentId: 'child-agent',
- parentSessionId: 'main',
- childSessionId: 'child-session',
- status: 'ok',
- stopReason: 'completed',
- lastAssistantMessage: [{ type: 'text', text: 'child done' }],
- },
- })
- await handle.dispose()
- await parentHandle.dispose()
- await server.shutdown()
- } finally {
- await ctx.fiber.dispose()
- await rm(storageDir, { recursive: true, force: true })
- }
- })
- it('falls back to live agent lineage for uncached subagent end events', async () => {
- const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-subagent-fallback-'))
- const ctx = await makeHarness(storageDir)
- let parentHandle: AgentHandle | undefined
- let handle: AgentHandle | undefined
- let failedHandle: AgentHandle | undefined
- try {
- parentHandle = await ctx.agents.create({
- agentId: AgentId('fallback-parent-agent'),
- sessionId: SessionId('fallback-parent'),
- meta: { cwd: storageDir },
- agentOptions: { model: 'deepseek' },
- })
- handle = await ctx.agents.create({
- agentId: AgentId('fallback-child-agent'),
- sessionId: SessionId('fallback-child-session'),
- meta: { cwd: storageDir, parentSession: SessionId('fallback-parent') },
- agentOptions: { model: 'deepseek' },
- })
- failedHandle = await ctx.agents.create({
- agentId: AgentId('failed-child-agent'),
- sessionId: SessionId('failed-child-session'),
- meta: { cwd: storageDir },
- agentOptions: { model: 'deepseek' },
- })
- const transport = new FakeTransport()
- const server = new HarnessSdkServer(ctx, transport)
- await settleSubagent(ctx, parentHandle.agent, {
- provider: 'fork',
- id: AgentId('fallback-child-agent'),
- stopReason: 'max-tokens',
- lastAssistantMessage: [],
- })
- await settleSubagent(ctx, parentHandle.agent, {
- provider: 'fork',
- id: AgentId('failed-child-agent'),
- stopReason: 'error',
- })
- await settleSubagent(ctx, parentHandle.agent, {
- provider: 'fork',
- id: AgentId('missing-child-agent'),
- stopReason: 'error',
- })
- expect(transport.notifications).toContainEqual({
- method: 'subagent.finished',
- params: {
- provider: 'fork',
- agentId: 'fallback-child-agent',
- parentSessionId: 'fallback-parent',
- childSessionId: 'fallback-child-session',
- status: 'error',
- stopReason: 'max-tokens',
- lastAssistantMessage: [],
- },
- })
- expect(transport.notifications).toContainEqual({
- method: 'subagent.finished',
- params: {
- provider: 'fork',
- agentId: 'failed-child-agent',
- childSessionId: 'failed-child-session',
- status: 'error',
- stopReason: 'error',
- },
- })
- expect(transport.notifications.some(n =>
- n.method === 'subagent.finished'
- && n.params?.agentId === 'missing-child-agent',
- )).toBe(false)
- await server.shutdown()
- } finally {
- await handle?.dispose()
- await failedHandle?.dispose()
- await parentHandle?.dispose()
- await ctx.fiber.dispose()
- await rm(storageDir, { recursive: true, force: true })
- }
- })
- it('does not re-register an LLM adapter that already exists', async () => {
- const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-existing-llm-'))
- const ctx = await makeHarness(storageDir)
- vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
- await ctx.plugin(LlmDeepSeek, { models: ['preinstalled-model'] })
- try {
- const server = new HarnessSdkServer(ctx, new FakeTransport())
- const inspect = server as unknown as { hasAdapterFor(model: string): boolean }
- expect(inspect.hasAdapterFor('preinstalled-model')).toBe(true)
- expect(inspect.hasAdapterFor('missing-model')).toBe(false)
- await server.initialize({ cwd: storageDir, model: 'preinstalled-model' })
- expect(ctx.get('llm')?.models().filter(model => model === 'preinstalled-model')).toEqual(['preinstalled-model'])
- await server.shutdown()
- } finally {
- await ctx.fiber.dispose()
- await rm(storageDir, { recursive: true, force: true })
- }
- })
- it('registers a missing model when an LLM service already exists', async () => {
- const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-new-llm-'))
- const ctx = await makeHarness(storageDir)
- vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
- await ctx.plugin(LlmDeepSeek, { models: ['other-model'] })
- try {
- const server = new HarnessSdkServer(ctx, new FakeTransport())
- await server.initialize({ cwd: storageDir, model: 'new-model' })
- expect(ctx.get('llm')?.models()).toEqual(expect.arrayContaining(['other-model', 'new-model']))
- await server.shutdown()
- } finally {
- await ctx.fiber.dispose()
- await rm(storageDir, { recursive: true, force: true })
- }
- })
- it('classifies defensive finish states', async () => {
- const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-finish-states-'))
- const ctx = await makeHarness(storageDir)
- try {
- const server = new HarnessSdkServer(ctx, new FakeTransport()) as unknown as {
- finishedStatus(reason: unknown): 'ok' | 'error'
- shutdown(): Promise<Record<string, never>>
- }
- expect(server.finishedStatus(undefined)).toBe('error')
- expect(server.finishedStatus({ kind: 'max-tokens' })).toBe('error')
- expect(server.finishedStatus({ kind: 'error' })).toBe('error')
- await server.shutdown()
- } finally {
- await ctx.fiber.dispose()
- await rm(storageDir, { recursive: true, force: true })
- }
- })
- it('reports no adapter when the LLM service is absent', async () => {
- const ctx = new Context()
- try {
- const server = new HarnessSdkServer(ctx, new FakeTransport()) as unknown as {
- hasAdapterFor(model: string): boolean
- shutdown(): Promise<Record<string, never>>
- }
- expect(server.hasAdapterFor('missing-model')).toBe(false)
- await server.shutdown()
- } finally {
- await ctx.fiber.dispose()
- }
- })
- it('rejects unknown JSON-RPC runtime methods', async () => {
- const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-unknown-'))
- const ctx = await makeHarness(storageDir)
- try {
- const server = new HarnessSdkServer(ctx, new FakeTransport())
- await expect(server.handleRequest('does/not/exist', {}))
- .rejects
- .toThrow('unknown DeepSeek Harness SDK runtime method: does/not/exist')
- await server.shutdown()
- } finally {
- await ctx.fiber.dispose()
- await rm(storageDir, { recursive: true, force: true })
- }
- })
- it('coalesces concurrent session creation and retries a failed creation', async () => {
- let resolveShared: ((handle: AgentHandle) => void) | undefined
- const sharedCreation = new Promise<AgentHandle>((resolve) => { resolveShared = resolve })
- const sharedHandle = { agent: {} as Agent, dispose: vi.fn(() => Promise.resolve()) }
- const retryHandle = { agent: {} as Agent, dispose: vi.fn(() => Promise.resolve()) }
- const create = vi.fn<(options: unknown) => Promise<AgentHandle>>()
- .mockReturnValueOnce(sharedCreation)
- .mockRejectedValueOnce(new Error('creation failed'))
- .mockResolvedValueOnce(retryHandle)
- const ctx = {
- on: vi.fn(() => () => undefined),
- agents: { create, get: () => undefined },
- get: () => undefined,
- } as unknown as Context
- const server = new HarnessSdkServer(ctx, new FakeTransport()) as unknown as {
- getOrCreateSession(sessionId: string): Promise<{ handle: AgentHandle }>
- shutdown(): Promise<Record<string, never>>
- }
- const first = server.getOrCreateSession('shared')
- const second = server.getOrCreateSession('shared')
- expect(create).toHaveBeenCalledTimes(1)
- resolveShared?.(sharedHandle)
- const [firstRecord, secondRecord] = await Promise.all([first, second])
- expect(firstRecord).toBe(secondRecord)
- await expect(server.getOrCreateSession('retry')).rejects.toThrow('creation failed')
- await expect(server.getOrCreateSession('retry')).resolves.toMatchObject({ handle: retryHandle })
- expect(create).toHaveBeenCalledTimes(3)
- await server.shutdown()
- expect(sharedHandle.dispose).toHaveBeenCalledOnce()
- expect(retryHandle.dispose).toHaveBeenCalledOnce()
- await expect(server.getOrCreateSession('after-shutdown')).rejects.toThrow('SDK server is shutting down')
- })
- it('resolves a relative cwd before creating the session', async () => {
- const create = vi.fn<(options: unknown) => Promise<AgentHandle>>()
- .mockResolvedValue({ agent: {} as Agent, dispose: () => Promise.resolve() })
- const ctx = {
- on: vi.fn(() => () => undefined),
- agents: { create, get: () => undefined },
- get: () => ({ models: () => ['model'] }),
- } as unknown as Context
- const server = new HarnessSdkServer(ctx, new FakeTransport()) as unknown as {
- initialize(params: { cwd: string; model: string }): Promise<unknown>
- getOrCreateSession(sessionId: string): Promise<unknown>
- shutdown(): Promise<Record<string, never>>
- }
- await server.initialize({ cwd: '.', model: 'model' })
- await server.getOrCreateSession('relative')
- expect(create).toHaveBeenCalledWith(expect.objectContaining({ meta: { cwd: process.cwd() } }))
- await server.shutdown()
- })
- it('settles every teardown and aggregates multiple failures', async () => {
- const firstDispose = vi.fn(() => { throw new Error('first teardown failed') })
- const secondDispose = vi.fn(() => Promise.reject(new Error('second teardown failed')))
- const ctx = {
- on: vi.fn(() => () => undefined),
- agents: { create: vi.fn(), get: () => undefined },
- get: () => undefined,
- } as unknown as Context
- const server = new HarnessSdkServer(ctx, new FakeTransport()) as unknown as {
- sessions: Map<string, { handle: AgentHandle; lastTurnEnd: undefined; activePrompt: boolean }>
- shutdown(): Promise<Record<string, never>>
- }
- server.sessions.set('first', { handle: { agent: {} as Agent, dispose: firstDispose }, lastTurnEnd: undefined, activePrompt: false })
- server.sessions.set('second', { handle: { agent: {} as Agent, dispose: secondDispose }, lastTurnEnd: undefined, activePrompt: false })
- await expect(server.shutdown()).rejects.toThrow('SDK server teardown failed')
- expect(firstDispose).toHaveBeenCalledOnce()
- expect(secondDispose).toHaveBeenCalledOnce()
- })
- it('continues teardown after a subscription disposer fails', async () => {
- let subscription = 0
- const listenerFailure = new Error('listener teardown failed')
- const on = vi.fn(() => {
- subscription += 1
- return subscription === 1 ? () => { throw listenerFailure } : () => undefined
- })
- const ctx = {
- on,
- agents: { create: vi.fn(), get: () => undefined },
- get: () => undefined,
- } as unknown as Context
- const server = new HarnessSdkServer(ctx, new FakeTransport())
- await expect(server.shutdown()).rejects.toBe(listenerFailure)
- expect(on).toHaveBeenCalledTimes(4)
- })
- })
|