refactor: unify agent and session identity

This commit is contained in:
Tianyi Cui
2026-07-14 01:59:21 +08:00
parent 7a33ee94be
commit 709cc7200e
105 changed files with 899 additions and 948 deletions

View File

@@ -72,7 +72,6 @@ import {
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { assertNever, CallId } from '@deepseek-ai/dsh-llm'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@deepseek-ai/dsh-bash'
import { APPROVAL_POLICIES, effectiveApprovalPolicy, setApprovalPolicy } from '@deepseek-ai/dsh-user-approval'
@@ -686,7 +685,6 @@ export function apply(ctx: Context, config: AcpConfig): void {
validateMcpServers(params)
const sessionId = SessionId(randomUUID())
const handle = await agents.create({
agentId: AgentId(sessionId),
sessionId,
meta: { cwd: params.cwd },
agentOptions: agentOptions(config),
@@ -757,7 +755,6 @@ export function apply(ctx: Context, config: AcpConfig): void {
}
}
const handle = await agents.resume({
agentId: AgentId(sessionId),
resumeSessionId: sessionId,
agentOptions: agentOptions(config),
})

View File

@@ -4,9 +4,11 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { CallId } from '@deepseek-ai/dsh-llm'
import { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import { type Agent } from '@deepseek-ai/dsh-agent'
import ApprovalService, { type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
import { makeBridgeHarness, type BridgeHarness } from './harness.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
/**
* The bridge's `approval/request` answerer: an ask for an agent the bridge
@@ -31,7 +33,7 @@ describe('acp bridge — approval answerer', () => {
): Promise<{ agent: Agent; request: ApprovalRequest }> {
await h.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await h.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = h.ctx.agents.get(AgentId(sessionId))
const agent = h.ctx.agents.get(SessionId(sessionId))
if (agent === undefined) throw new Error('newSession created no agent')
// In production an ask always fires mid-turn (tool execution); open one so
// request()'s turn-enclosure precondition holds for the direct drive below.

View File

@@ -3,8 +3,8 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { makeBridgeHarness, textResponse, toolCallResponse, type BridgeHarness } from './harness.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
/**
* End-to-end bridge specs over an in-memory transport: a real
@@ -98,7 +98,7 @@ describe('acp bridge', () => {
required: [],
},
})
const toolResult = harness.ctx.agents.get(AgentId(sessionId))!.session.events.find(event => event.type === 'tool/result')
const toolResult = harness.ctx.agents.get(SessionId(sessionId))!.session.events.find(event => event.type === 'tool/result')
const toolResultBlock = toolResult?.type === 'tool/result' ? toolResult.data.content[0] : undefined
const toolResultText = toolResultBlock?.type === 'text' ? toolResultBlock.text : undefined
expect(toolResultText).toBe('{"answers":[{"id":"language","selected":["Python"]}]}')
@@ -127,7 +127,7 @@ describe('acp bridge', () => {
required: ['custom'],
},
})
const toolResult = harness.ctx.agents.get(AgentId(sessionId))!.session.events.find(event => event.type === 'tool/result')
const toolResult = harness.ctx.agents.get(SessionId(sessionId))!.session.events.find(event => event.type === 'tool/result')
expect(JSON.stringify(toolResult)).toContain('apollo')
})
@@ -136,7 +136,7 @@ describe('acp bridge', () => {
harness.onElicitation = () => ({ action: 'accept', content: { custom: 'Use Zig' } })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
const result = await harness.ctx.userInteraction.ask({
agent,
@@ -167,7 +167,7 @@ describe('acp bridge', () => {
harness.onElicitation = () => ({ action: 'accept', content: { choice: 'TypeScript', custom: 'Use Zig' } })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
await expect(harness.ctx.userInteraction.ask({
agent,
@@ -184,7 +184,7 @@ describe('acp bridge', () => {
harness.onElicitation = () => ({ action: 'accept', content: { choice: ['Tests', 'Docs'] } })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
await expect(harness.ctx.userInteraction.ask({
agent,
@@ -201,7 +201,7 @@ describe('acp bridge', () => {
harness = await makeBridgeHarness({ storageDir, withAskUser: true })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
await expect(harness.ctx.userInteraction.ask({ questions: [{ id: 'x', question: 'No agent?' }] }))
.rejects.toMatchObject({ name: 'UserInteractionError', code: 'NO_AGENT' })
@@ -225,7 +225,7 @@ describe('acp bridge', () => {
harness = await makeBridgeHarness({ storageDir, withAskUser: true })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
const alreadyAborted = new AbortController()
alreadyAborted.abort()
@@ -265,8 +265,8 @@ describe('acp bridge', () => {
expect(b.sessionId).toBeTruthy()
expect(a.sessionId).not.toBe(b.sessionId)
// Both agents are live and independently registered.
expect(harness.ctx.agents.get(AgentId(a.sessionId))).toBeDefined()
expect(harness.ctx.agents.get(AgentId(b.sessionId))).toBeDefined()
expect(harness.ctx.agents.get(SessionId(a.sessionId))).toBeDefined()
expect(harness.ctx.agents.get(SessionId(b.sessionId))).toBeDefined()
})
it('rejects a non-absolute cwd but accepts any absolute cwd (per-session workspace)', async () => {
@@ -281,7 +281,7 @@ describe('acp bridge', () => {
const res = await harness.client.newSession({ cwd: '/tmp', mcpServers: [] })
expect(res.sessionId).toBeTruthy()
// The session header records that cwd, so its bash tools run there.
expect(harness.ctx.agents.get(AgentId(res.sessionId))!.session.header.cwd).toBe('/tmp')
expect(harness.ctx.agents.get(SessionId(res.sessionId))!.session.header.cwd).toBe('/tmp')
})
it('rejects non-empty additionalDirectories', async () => {
@@ -321,7 +321,7 @@ describe('acp bridge', () => {
],
})
expect(result.stopReason).toBe('end_turn')
const user = harness.ctx.agents.get(AgentId(sessionId))!.session.events.find(event => event.type === 'user/message')
const user = harness.ctx.agents.get(SessionId(sessionId))!.session.events.find(event => event.type === 'user/message')
expect(JSON.stringify(user)).toContain('resource_link')
})

View File

@@ -4,7 +4,6 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { SessionId } from '@deepseek-ai/dsh-session'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { makeBridgeHarness, textResponse } from './harness.ts'
describe('acp bridge — disposal & HMR safety', () => {
@@ -17,7 +16,7 @@ describe('acp bridge — disposal & HMR safety', () => {
const harness = await makeBridgeHarness({ storageDir, script: ['hang'] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
// Start a prompt that hangs in the model stream.
const promptDone = harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })
@@ -62,10 +61,10 @@ describe('acp bridge — disposal & HMR safety', () => {
const harness = await makeBridgeHarness({ storageDir, script: [] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
expect(harness.ctx.agents.get(AgentId(sessionId))).toBeDefined()
expect(harness.ctx.agents.get(SessionId(sessionId))).toBeDefined()
await harness.acpFiber.dispose() // tear down ONLY the bridge
expect(harness.ctx.agents.get(AgentId(sessionId))).toBeUndefined()
expect(harness.ctx.agents.get(SessionId(sessionId))).toBeUndefined()
await harness.dispose()
})
@@ -92,7 +91,7 @@ describe('acp bridge — disposal & HMR safety', () => {
const harness = await makeBridgeHarness({ storageDir, script: ['hang'] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
// Start a prompt that hangs in the model stream. The prompt RPC will never
// return (its transport is severed), so do not await it.
void harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] }).catch(() => {})
@@ -115,7 +114,7 @@ describe('acp bridge — disposal & HMR safety', () => {
// and its session removed from the store, not merely idled (the old
// behavior). The services live on the root ctx, so they survive this.
await harness.acpFiber.dispose()
expect(harness.ctx.agents.get(AgentId(sessionId))).toBeUndefined()
expect(harness.ctx.agents.get(SessionId(sessionId))).toBeUndefined()
expect(harness.ctx.sessions.get(SessionId(sessionId))).toBeUndefined()
await harness.dispose()
})
@@ -128,7 +127,7 @@ describe('acp bridge — disposal & HMR safety', () => {
const harness = await makeBridgeHarness({ storageDir, script: ['hang'] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
void harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] }).catch(() => {})
await new Promise(r => setTimeout(r, 30))
expect(agent.status).toBe('running')
@@ -145,7 +144,7 @@ describe('acp bridge — disposal & HMR safety', () => {
const harness = await makeBridgeHarness({ storageDir, script: [] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const session = harness.ctx.agents.get(AgentId(sessionId))!.session
const session = harness.ctx.agents.get(SessionId(sessionId))!.session
await harness.ctx.fiber.dispose()
const before = harness.updates.length
@@ -169,12 +168,12 @@ describe('acp bridge — disposal & HMR safety', () => {
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })
const liveEvents = harness.ctx.agents.get(AgentId(sessionId))!.session.events.length
const liveEvents = harness.ctx.agents.get(SessionId(sessionId))!.session.events.length
expect(liveEvents).toBeGreaterThan(0)
// Tear down JUST the bridge (the AgentHandle dispose runs to quiescence).
await harness.acpFiber.dispose()
expect(harness.ctx.agents.get(AgentId(sessionId))).toBeUndefined()
expect(harness.ctx.agents.get(SessionId(sessionId))).toBeUndefined()
// Re-load the session from disk: every live event (incl. the closing
// turn/end) was flushed before the session was detached.
@@ -201,7 +200,7 @@ describe('acp bridge — disposal & HMR safety', () => {
const harness = await makeBridgeHarness({ storageDir, script: ['hang'] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
void harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] }).catch(() => {})
await new Promise(r => setTimeout(r, 30))
expect(agent.status).toBe('running')
@@ -211,7 +210,7 @@ describe('acp bridge — disposal & HMR safety', () => {
// Dispose JUST the bridge: a fiber unload that must STILL honor the ordered
// teardown (the composite effect runs its disposer chain as a unit).
await harness.acpFiber.dispose()
expect(harness.ctx.agents.get(AgentId(sessionId))).toBeUndefined()
expect(harness.ctx.agents.get(SessionId(sessionId))).toBeUndefined()
// The loop's own `turn/end {disposed}` is on disk (re-load: the world, not
// self-report) — NOT a crash-recovery `interrupted` substitute.
@@ -230,21 +229,21 @@ describe('acp bridge — disposal & HMR safety', () => {
// queryable, with its session still in the store.
const harness = await makeBridgeHarness({ storageDir, script: [] })
const handleA = await harness.ctx.agents.create({
agentId: AgentId('sib-a'), sessionId: SessionId('sib-a'), agentOptions: { model: 'mock' },
sessionId: SessionId('sib-a'), agentOptions: { model: 'mock' },
})
const handleB = await harness.ctx.agents.create({
agentId: AgentId('sib-b'), sessionId: SessionId('sib-b'), agentOptions: { model: 'mock' },
sessionId: SessionId('sib-b'), agentOptions: { model: 'mock' },
})
expect(harness.ctx.agents.get(AgentId('sib-a'))).toBe(handleA.agent)
expect(harness.ctx.agents.get(AgentId('sib-b'))).toBe(handleB.agent)
expect(harness.ctx.agents.get(SessionId('sib-a'))).toBe(handleA.agent)
expect(harness.ctx.agents.get(SessionId('sib-b'))).toBe(handleB.agent)
await handleA.dispose()
// A is gone — unregistered AND its session removed from the store.
expect(harness.ctx.agents.get(AgentId('sib-a'))).toBeUndefined()
expect(harness.ctx.agents.get(SessionId('sib-a'))).toBeUndefined()
expect(harness.ctx.sessions.get(SessionId('sib-a'))).toBeUndefined()
expect(handleA.agent.status).toBe('disposed')
// B is wholly unaffected.
expect(harness.ctx.agents.get(AgentId('sib-b'))).toBe(handleB.agent)
expect(harness.ctx.agents.get(SessionId('sib-b'))).toBe(handleB.agent)
expect(harness.ctx.sessions.get(SessionId('sib-b'))).toBeDefined()
expect(handleB.agent.status).not.toBe('disposed')
await harness.dispose()
@@ -262,7 +261,7 @@ describe('acp bridge — disposal & HMR safety', () => {
const harness = await makeBridgeHarness({ storageDir, script: [textResponse('ok')] })
harness.ctx.on('agent/disposed', () => { throw new Error('boom disposed listener') })
const handle = await harness.ctx.agents.create({
agentId: AgentId('guard-a'), sessionId: SessionId('guard-a'), agentOptions: { model: 'mock' },
sessionId: SessionId('guard-a'), agentOptions: { model: 'mock' },
})
handle.agent.send([{ type: 'text', text: 'go' }])
await handle.agent.whenIdle()
@@ -270,7 +269,7 @@ describe('acp bridge — disposal & HMR safety', () => {
// Dispose: the throwing listener must NOT break the chain before detach.
await handle.dispose()
expect(harness.ctx.agents.get(AgentId('guard-a'))).toBeUndefined()
expect(harness.ctx.agents.get(SessionId('guard-a'))).toBeUndefined()
expect(harness.ctx.sessions.get(SessionId('guard-a'))).toBeUndefined() // detach still ran
await harness.dispose()
})
@@ -283,7 +282,7 @@ describe('acp bridge — disposal & HMR safety', () => {
// observe the same quiescence boundary.
const harness = await makeBridgeHarness({ storageDir, script: ['hang'] })
const handle = await harness.ctx.agents.create({
agentId: AgentId('conc-a'), sessionId: SessionId('conc-a'), agentOptions: { model: 'mock' },
sessionId: SessionId('conc-a'), agentOptions: { model: 'mock' },
})
// Drive a turn that hangs in the model stream, so the loop is mid-turn when
// disposed — its exit runs a final session/flush we can gate to hold the
@@ -313,7 +312,7 @@ describe('acp bridge — disposal & HMR safety', () => {
// Release the flush; both resolve together and the session is gone.
releaseFlush()
await Promise.all([first, second])
expect(harness.ctx.agents.get(AgentId('conc-a'))).toBeUndefined()
expect(harness.ctx.agents.get(SessionId('conc-a'))).toBeUndefined()
expect(harness.ctx.sessions.get(SessionId('conc-a'))).toBeUndefined()
await harness.dispose()
})

View File

@@ -3,7 +3,6 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { SessionId } from '@deepseek-ai/dsh-session'
import { makeBridgeHarness, textResponse, type BridgeHarness } from './harness.ts'
@@ -27,7 +26,7 @@ describe('acp bridge — demux & config edges', () => {
await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const before = harness.updates.length
const { agent: foreign } = await harness.ctx.agents.create({ agentId: AgentId('foreign'), sessionId: SessionId('foreign-session'), agentOptions: { model: 'mock' } })
const { agent: foreign } = await harness.ctx.agents.create({ sessionId: SessionId('foreign-session'), agentOptions: { model: 'mock' } })
foreign.send([{ type: 'text', text: 'hi' }])
await foreign.whenIdle()
await new Promise(r => setTimeout(r, 10))

View File

@@ -4,7 +4,6 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { makeBridgeHarness, textResponse, toolCallResponse, type BridgeHarness, type CapturedUpdate } from './harness.ts'
/** Concatenate the text of all agent_message_chunk updates. */
@@ -194,7 +193,7 @@ describe('acp bridge — session/load replay', () => {
release() // resume() finishes AFTER teardown
expect(await loadResult).toBe('rejected')
// No live agent was installed for the closed connection.
expect(loader.ctx.agents.get(AgentId(sessionId))).toBeUndefined()
expect(loader.ctx.agents.get(SessionId(sessionId))).toBeUndefined()
})
it('rejects load when the requested cwd does not match the persisted session cwd', async () => {
@@ -215,11 +214,11 @@ describe('acp bridge — session/load replay', () => {
await loader.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
await expect(loader.client.loadSession({ sessionId: 'elsewhere', cwd: process.cwd(), mcpServers: [] }))
.rejects.toThrow(/cwd mismatch/)
expect(loader.ctx.agents.get(AgentId('elsewhere'))).toBeUndefined()
expect(loader.ctx.agents.get(SessionId('elsewhere'))).toBeUndefined()
const res = await loader.client.loadSession({ sessionId: 'elsewhere', cwd: `${otherCwd}/.`, mcpServers: [] })
expect(res).toBeDefined()
expect(loader.ctx.agents.get(AgentId('elsewhere'))!.session.header.cwd).toBe(otherCwd)
expect(loader.ctx.agents.get(SessionId('elsewhere'))!.session.header.cwd).toBe(otherCwd)
})
it('rejects load for a non-absolute cwd (still required to be absolute)', async () => {
@@ -254,7 +253,7 @@ describe('acp bridge — session/load replay', () => {
// Rejected BEFORE resume (metadata-only check) — no agent was registered, so
// the id is not wedged: a later attempt hits the same clean rejection, not a
// duplicate-registration error.
expect(loader.ctx.agents.get(AgentId('legacy'))).toBeUndefined()
expect(loader.ctx.agents.get(SessionId('legacy'))).toBeUndefined()
await expect(loader.client.loadSession({ sessionId: 'legacy', cwd: process.cwd(), mcpServers: [] }))
.rejects.toThrow(/no absolute persisted cwd/)
})

View File

@@ -3,8 +3,8 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { makeBridgeHarness, textResponse, type BridgeHarness, type CapturedUpdate } from './harness.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
/** Text of the agent_message_chunk updates scoped to one session id. */
function messageTextFor(updates: { sessionId?: string; update: CapturedUpdate }[], sessionId: string): string {
@@ -102,8 +102,8 @@ describe('acp bridge — RFC 011 multi-session isolation', () => {
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
const a = (await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })).sessionId
const b = (await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })).sessionId
const agentA = harness.ctx.agents.get(AgentId(a))!
const agentB = harness.ctx.agents.get(AgentId(b))!
const agentA = harness.ctx.agents.get(SessionId(a))!
const agentB = harness.ctx.agents.get(SessionId(b))!
// Wait deterministically for BOTH agents to enter `running` (not a fixed
// sleep — agent startup latency is unbounded on a loaded worker).

View File

@@ -3,7 +3,6 @@ import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import {
errorResponse,
@@ -13,6 +12,7 @@ import {
toolCallResponse,
type BridgeHarness,
} from './harness.ts'
import { SessionId } from '@deepseek-ai/dsh-session'
/** Boilerplate: initialize + create one session, returning its id. */
async function newSession(h: BridgeHarness, clientCapabilities: Record<string, unknown> = {}): Promise<string> {
@@ -279,7 +279,7 @@ describe('acp bridge — turn outcomes', () => {
// OWN turn with the real model answer.
harness = await makeBridgeHarness({ storageDir, script: [textResponse('real answer')] })
const sessionId = await newSession(harness)
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
// On the queued prompt, synchronously inject a one-shot context turn (idle
// inject writes turn/start{injection} → context/message → turn/end). Fire
// once so it lands between install and the prompt turn.
@@ -335,7 +335,7 @@ describe('acp bridge — turn outcomes', () => {
await harness.client.cancel({ sessionId })
const res = await promptDone
expect(res.stopReason).toBe('cancelled')
const agent = harness.ctx.agents.get(AgentId(sessionId))!
const agent = harness.ctx.agents.get(SessionId(sessionId))!
await agent.whenIdle()
// At most ONE turn ran (the cancelled one) — the cancel cleared the queue, so
// no second turn was batched or leaked. (A best-effort abort that left queued