import { describe, expect, it } from 'vitest' import { Context, FiberState, type Fiber } from 'cordis' import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent' import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import type { AgentExecutionService } from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' interface Harness { ctx: Context providerFiber: Fiber loopFiber: Fiber } async function harness(adapter: LlmAdapter): Promise { const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) const providerFiber = await ctx.plugin(AgentExecutionProvider) const loopFiber = await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return { ctx, providerFiber, loopFiber } } function waitForIdle(ctx: Context, agent: ReactLoopAgent | Agent): Promise { return new Promise((resolve) => { const dispose = ctx.on('agent/status', (subject, status) => { if (subject === agent && status === 'idle') { dispose() resolve() } }) }) } function send(agent: Agent, text: string): void { agent.send([{ type: 'text', text }]) } /** Adapter that holds both drivers at the same awaited continuation. */ class OverlapAdapter extends LlmAdapter { private readonly bothStarted = Promise.withResolvers() private starts = 0 readonly observations: { sessionId: SessionId | undefined; before: Agent; after: Agent }[] = [] constructor(private readonly ctx: Context) { super() } async * stream(options: GenerateOptions): AsyncIterable { const before = this.ctx.agentExecution.require().agent this.starts += 1 if (this.starts === 2) this.bothStarted.resolve(true) await this.bothStarted.promise await Promise.resolve() const after = this.ctx.agentExecution.require().agent this.observations.push({ sessionId: options.sessionId, before, after }) yield* textResponse('done') } } /** Test-only transport that materializes ambient identity at its request boundary. */ class TestCapabilityTransport { readonly requests: { path: string; headers: Record }[] = [] constructor(private readonly execution: AgentExecutionService) {} async request(path: string): Promise> { await Promise.resolve() const headers = { 'X-Harness-Session-Id': this.execution.require().agent.session.id, } this.requests.push({ path, headers }) return headers } } /** Adapter whose first call waits for cancellation and whose later calls complete. */ class ReloadAdapter extends LlmAdapter { readonly firstStarted = Promise.withResolvers() firstAgentDuringAbort: Agent | undefined laterAgent: Agent | undefined calls = 0 execution: AgentExecutionService | undefined async * stream(options: GenerateOptions): AsyncIterable { const execution = this.execution if (execution === undefined) throw new Error('execution service missing') this.calls += 1 if (this.calls === 1) { this.firstStarted.resolve(true) try { await new Promise((_resolve, reject) => { const abort = (): void => { reject(new Error('aborted')) } if (options.signal?.aborted === true) abort() else options.signal?.addEventListener('abort', abort, { once: true }) }) } catch (error: unknown) { await Promise.resolve() this.firstAgentDuringAbort = execution.require().agent throw error } return } await Promise.resolve() this.laterAgent = execution.require().agent yield* textResponse('reloaded') } } describe('AgentLoop execution context', () => { it('keeps overlapping driver continuations bound to their exact Agents', async () => { const ctx = new Context() const adapter = new OverlapAdapter(ctx) await ctx.plugin(LlmService) await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) const a = ctx.agentLoop.create(AgentId('a'), { provider: 'mock', model: 'mock' }) const b = ctx.agentLoop.create(AgentId('b'), { provider: 'mock', model: 'mock' }) const idleA = waitForIdle(ctx, a) const idleB = waitForIdle(ctx, b) send(a, 'a') send(b, 'b') await Promise.all([idleA, idleB]) expect(adapter.observations).toHaveLength(2) expect(adapter.observations).toEqual(expect.arrayContaining([ { sessionId: a.session.id, before: a, after: a }, { sessionId: b.session.id, before: b, after: b }, ])) expect(ctx.agentExecution.current()).toBeUndefined() await ctx.fiber.dispose() }) it('keeps ALS identity minimal while one explicit signal spans each turn seam', async () => { const adapter = new MockAdapter([ toolCallResponse('observe-call', 'observe', {}), textResponse('first done'), textResponse('second done'), ]) const { ctx } = await harness(adapter) const agent = ctx.agentLoop.create(AgentId('signal-owner'), { provider: 'mock', model: 'mock' }) let signals: AbortSignal[] = [] const capture = (signal: AbortSignal | undefined): void => { if (signal === undefined) throw new Error('turn seam omitted its explicit signal') const execution = ctx.agentExecution.require() expect(Object.keys(execution)).toEqual(['agent']) expect(execution.agent).toBe(agent) signals.push(signal) } ctx.on('system-prompt/assemble', async (_assembly, context, next) => { if (context.agent === agent) capture(context.signal) return next() }) ctx.on('agent/prompt-submit', async (subject, _content, _source, signal, next) => { if (subject === agent) capture(signal) return next() }) ctx.on('agent/session-prefix', async (subject, _prefix, signal, next) => { if (subject === agent) capture(signal) return next() }) ctx.on('agent/pre-step', (subject, _turn, _step, _system, _prefix, signal) => { if (subject === agent) capture(signal) }) ctx.on('agent/request', async (subject, _turn, _step, _config, signal, next) => { if (subject === agent) capture(signal) return next() }) ctx.on('agent/step-result', async (subject, _turn, _step, _message, signal, next) => { if (subject === agent) capture(signal) return next() }) ctx.on('agent/turn-continuation', async (subject, _turn, _decision, signal, next) => { if (subject === agent) capture(signal) return next() }) ctx.on('agent/turn-stop', (subject, _turn, signal) => { if (subject === agent) capture(signal) }) ctx.tools.register(defineTool({ name: 'observe', description: 'observe explicit turn state', parameters: {}, execute: async (_args, exec) => { capture(exec.signal) return [{ type: 'text', text: 'observed' }] }, })) const firstIdle = waitForIdle(ctx, agent) send(agent, 'first') await firstIdle const firstSignal = signals[0] expect(firstSignal).toBeDefined() expect(new Set([...signals, ...adapter.requests.slice(0, 2).map(request => request.signal!)])).toEqual(new Set([firstSignal])) signals = [] const secondIdle = waitForIdle(ctx, agent) send(agent, 'second') await secondIdle const secondSignal = signals[0] expect(secondSignal).toBeDefined() expect(new Set([...signals, adapter.requests[2]!.signal!])).toEqual(new Set([secondSignal])) expect(secondSignal).not.toBe(firstSignal) expect(ctx.agentExecution.current()).toBeUndefined() await ctx.fiber.dispose() }) it('keeps child setup under the parent boundary, switches for the child driver, then restores the parent', async () => { const adapter = new MockAdapter([ toolCallResponse('spawn', 'spawn-child', {}), toolCallResponse('observe', 'observe-child', {}), textResponse('child done'), textResponse('parent done'), ]) const { ctx } = await harness(adapter) let parentDuringSetup: Agent | undefined let explicitChild: Agent | undefined let childDuringDriver: Agent | undefined let parentAfterChild: Agent | undefined let child: Agent | undefined ctx.tools.register(defineTool({ name: 'spawn-child', description: 'create one child agent', parameters: {}, execute: async (_args, exec) => { if (exec.agent === undefined) throw new Error('parent agent missing') const handle = await exec.agent.ctx.agents.create({ agentId: AgentId('child'), sessionId: SessionId('child-session'), agentOptions: { provider: 'mock', model: 'mock' }, setup: (agentCtx) => { parentDuringSetup = ctx.agentExecution.require().agent explicitChild = agentCtx.agent agentCtx.tools.register(defineTool({ name: 'observe-child', description: 'observe child execution identity', parameters: {}, execute: async () => { await Promise.resolve() childDuringDriver = ctx.agentExecution.require().agent return [{ type: 'text', text: 'observed' }] }, })) }, }) child = handle.agent send(handle.agent, 'run child') await handle.agent.whenIdle() parentAfterChild = ctx.agentExecution.require().agent await handle.dispose() return [{ type: 'text', text: 'child completed' }] }, })) const parentHandle = await ctx.agents.create({ agentId: AgentId('parent'), sessionId: SessionId('parent-session'), agentOptions: { provider: 'mock', model: 'mock' }, }) const idle = waitForIdle(ctx, parentHandle.agent) send(parentHandle.agent, 'spawn') await idle expect(parentDuringSetup).toBe(parentHandle.agent) expect(explicitChild).toBe(child) expect(childDuringDriver).toBe(child) expect(parentAfterChild).toBe(parentHandle.agent) expect(ctx.agentExecution.current()).toBeUndefined() await parentHandle.dispose() await ctx.fiber.dispose() }) it('keeps agentless direct tools ambient-free and builds trusted transport headers internally', async () => { const adapter = new MockAdapter([ toolCallResponse('capability', 'capability-request', { path: '/v1/capability' }), textResponse('done'), ]) const { ctx } = await harness(adapter) const transport = new TestCapabilityTransport(ctx.agentExecution) let directAmbient: Agent | undefined let captured: Agent | undefined ctx.tools.register(defineTool({ name: 'agentless-probe', description: 'observe an agentless call', parameters: {}, execute: async () => { await Promise.resolve() directAmbient = ctx.agentExecution.current()?.agent return [{ type: 'text', text: 'ok' }] }, })) ctx.tools.register(defineTool({ name: 'capability-request', description: 'call the test capability transport', parameters: { path: { type: 'string' } }, execute: async (args) => { captured = ctx.agentExecution.require().agent const path = (args as { path: string }).path const headers = await transport.request(path) return [{ type: 'text', text: JSON.stringify(headers) }] }, })) const direct = await ctx.tools.execute({ callId: CallId('direct'), name: 'agentless-probe', arguments: {}, }) expect(direct.isError).toBe(false) expect(directAmbient).toBeUndefined() const handle = await ctx.agents.create({ agentId: AgentId('transport'), sessionId: SessionId('transport-session'), agentOptions: { provider: 'mock', model: 'mock' }, }) const idle = waitForIdle(ctx, handle.agent) send(handle.agent, 'call transport') await idle expect(transport.requests).toEqual([{ path: '/v1/capability', headers: { 'X-Harness-Session-Id': 'transport-session' }, }]) const schema = adapter.requests[0]?.tools?.find(tool => tool.name === 'capability-request') expect(JSON.stringify(schema?.parameters)).not.toMatch(/session|harness/i) const call = handle.agent.session.events.find(event => event.type === 'tool/call') expect(call?.type === 'tool/call' ? call.data.arguments : undefined) .toBe(JSON.stringify({ path: '/v1/capability' })) expect(captured).toBe(handle.agent) await handle.dispose() expect(captured?.status).toBe('disposed') expect(ctx.agentExecution.current()).toBeUndefined() await ctx.fiber.dispose() }) it('keeps AgentLoop inactive until the mandatory provider appears', async () => { const ctx = new Context() await ctx.plugin(LlmService) await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) const loopFiber = ctx.plugin(AgentLoop, { agents: [] }) await Promise.resolve() expect(loopFiber.state).toBe(FiberState.PENDING) await ctx.plugin(AgentExecutionProvider) await loopFiber expect(loopFiber.state).toBe(FiberState.ACTIVE) await ctx.fiber.dispose() }) it('drains the old driver before disabling ALS during provider restart', async () => { const ctx = new Context() const adapter = new ReloadAdapter() const { providerFiber, loopFiber } = await (async (): Promise => { await ctx.plugin(LlmService) await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) const mountedProvider = await ctx.plugin(AgentExecutionProvider) const mountedLoop = await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return { ctx, providerFiber: mountedProvider, loopFiber: mountedLoop } })() const oldService = ctx.agentExecution adapter.execution = oldService const oldHandle = await ctx.agents.create({ agentId: AgentId('before-restart'), sessionId: SessionId('before-restart-session'), agentOptions: { provider: 'mock', model: 'mock' }, }) const oldAgent = oldHandle.agent send(oldAgent, 'block') await adapter.firstStarted.promise await providerFiber.restart() await loopFiber.await() expect(adapter.firstAgentDuringAbort?.id).toBe(oldAgent.id) expect(adapter.firstAgentDuringAbort?.session).toBe(oldAgent.session) expect(oldAgent.status).toBe('disposed') expect(() => oldService.current()).toThrow('agent execution service is disposed') expect(ctx.agentExecution).not.toBe(oldService) adapter.execution = ctx.agentExecution const newHandle = await ctx.agents.create({ agentId: AgentId('after-restart'), sessionId: SessionId('after-restart-session'), agentOptions: { provider: 'mock', model: 'mock' }, }) const newAgent = newHandle.agent const idle = waitForIdle(ctx, newAgent) send(newAgent, 'continue') await idle expect(adapter.laterAgent?.id).toBe(newAgent.id) expect(adapter.laterAgent?.session).toBe(newAgent.session) await ctx.fiber.dispose() }) it('keeps ALS readable while root disposal drains sibling AgentLoop fibers', async () => { const ctx = new Context() const adapter = new ReloadAdapter() await ctx.plugin(LlmService) await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) const service = ctx.agentExecution adapter.execution = service const handle = await ctx.agents.create({ agentId: AgentId('root-dispose'), sessionId: SessionId('root-dispose-session'), agentOptions: { provider: 'mock', model: 'mock' }, }) const agent = handle.agent send(agent, 'block') await adapter.firstStarted.promise await ctx.fiber.dispose() expect(adapter.firstAgentDuringAbort?.id).toBe(agent.id) expect(adapter.firstAgentDuringAbort?.session).toBe(agent.session) expect(agent.status).toBe('disposed') expect(() => service.current()).toThrow('agent execution service is disposed') }) })