refactor(subagent): unify async readiness and cancellation

This commit is contained in:
Tianyi Cui
2026-07-12 22:41:59 +08:00
parent 02ca71db57
commit bb3f6bd736
49 changed files with 1350 additions and 4147 deletions

View File

@@ -20,7 +20,7 @@ describe('acp bridge — demux & config edges', () => {
it('ignores events from an agent the bridge does not own (strict id demux)', async () => {
// A second agent created directly on the registry (NOT via the bridge) runs
// a turn. Its session/event + agent/status must NOT produce ACP updates and
// a turn. Its session events must NOT produce ACP updates and
// must not settle anything — the bridge demuxes strictly by its own id.
harness = await makeBridgeHarness({ storageDir, script: [textResponse('foreign')] })
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })

View File

@@ -3,7 +3,7 @@ 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, agentEvents } from '@deepseek-ai/dsh-agent'
import { AgentId } from '@deepseek-ai/dsh-agent'
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
import {
errorResponse,
@@ -271,37 +271,6 @@ describe('acp bridge — turn outcomes', () => {
expect(result.stopReason).toBe('end_turn')
})
it('status reconciliation infers the owning message turn when teardown wins after turn/start', async () => {
harness = await makeBridgeHarness({ storageDir, script: [textResponse('background completion')] })
const sessionId = await newSession(harness)
const agent = harness.ctx.agents.get(AgentId(sessionId))!
harness.ctx.on('session/event', (session, event) => {
if (session !== agent.session || event.type !== 'turn/start') return
// Inject the signal ordering the defensive fallback handles: disposal
// status after turn/start commits but before ACP's later live observer.
// This is event-level simulation; it does not mutate the test agent.
agentEvents(harness!.ctx, agent).emit('agent/status', 'disposed')
}, { prepend: true })
const result = await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })
expect(result.stopReason).toBe('cancelled')
})
it('status reconciliation can settle from a committed turn/end before live delivery', async () => {
harness = await makeBridgeHarness({ storageDir, script: [textResponse('answer')] })
const sessionId = await newSession(harness)
const agent = harness.ctx.agents.get(AgentId(sessionId))!
harness.ctx.on('session/event', (session, event) => {
if (session !== agent.session || event.type !== 'turn/end') return
// Inject a reentrant status signal after the boundary commits to exercise
// the defensive log path before ACP's captured callback runs.
agentEvents(harness!.ctx, agent).emit('agent/status', 'idle')
}, { prepend: true })
const result = await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })
expect(result.stopReason).toBe('end_turn')
})
it('a between-turn injection does not settle the prompt early (message-trigger correlation)', async () => {
// A plugin injects context (a one-shot injection-triggered turn) right after
// the prompt is queued but before the prompt's own message turn runs. The