mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(acp): settle prompts at whole-agent idle, not the first turn/end
A prompt now resolves only when the agent reaches quiescence: the correlated turn/end arms an endReason instead of settling immediately, so steering or injected work that runs further turns before idle no longer splits one prompt across the automation wire. Token-limit turn endings settle as end_turn per the README contract (they are not prompt-level stop reasons); model errors still reject immediately, and explicit ACP cancellation stays 'cancelled'.
This commit is contained in:
@@ -92,6 +92,8 @@ interface SessionRecord {
|
||||
reject: (error: Error) => void
|
||||
messageId: string
|
||||
turn: number | undefined
|
||||
/** The correlated turn's ending, set at turn/end and settled at whole-agent idle. */
|
||||
endReason: TurnEndReason | undefined
|
||||
} | undefined
|
||||
}
|
||||
|
||||
@@ -171,11 +173,12 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
const inflight = record.inflight
|
||||
if (inflight !== undefined && event.type === 'turn/end' && inflight.turn === event.data.turn) {
|
||||
if (event.data.reason.kind === 'error') {
|
||||
// Model failures surface immediately as prompt errors; ordinary
|
||||
// endings wait for whole-agent idle below.
|
||||
record.inflight = undefined
|
||||
rejectFromError(inflight, event.data.reason)
|
||||
} else {
|
||||
record.inflight = undefined
|
||||
inflight.resolve(turnEndToStopReason(event.data.reason))
|
||||
inflight.endReason = event.data.reason
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,7 +285,7 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
// failure (invalid input) must free the slot again or the session
|
||||
// would reject every later prompt as already in flight.
|
||||
const inflight: NonNullable<SessionRecord['inflight']> = {
|
||||
resolve, reject, messageId: message.id, turn: undefined,
|
||||
resolve, reject, messageId: message.id, turn: undefined, endReason: undefined,
|
||||
}
|
||||
record.inflight = inflight
|
||||
try {
|
||||
@@ -297,12 +300,21 @@ export function apply(ctx: Context, config: AcpConfig): void {
|
||||
throw internalError(`prompt was not queued: ${detail}`)
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
// A turnless slot settles only at quiescence: admission discarded
|
||||
// the prompt before it could open a turn.
|
||||
// Settlement waits for whole-agent idle: a correlated turn/end arms
|
||||
// `endReason`, while a turnless slot (admission discarded the
|
||||
// prompt) stays cancelled. Other producers may run further turns
|
||||
// before quiescence; the prompt settles only when the agent stops.
|
||||
void record.agent.whenIdle().then(() => {
|
||||
if (record.inflight !== inflight) return
|
||||
record.inflight = undefined
|
||||
inflight.resolve('cancelled')
|
||||
const end = inflight.endReason
|
||||
if (end === undefined) {
|
||||
inflight.resolve('cancelled')
|
||||
} else {
|
||||
// Token-limit and other non-terminal endings are not prompt-level
|
||||
// stop reasons (see README); only normal quiescence reports end_turn.
|
||||
inflight.resolve(end.kind === 'max-tokens' ? 'end_turn' : turnEndToStopReason(end))
|
||||
}
|
||||
})
|
||||
})
|
||||
return { stopReason }
|
||||
|
||||
@@ -31,11 +31,13 @@ describe('ACP prompt lifecycle', () => {
|
||||
harness = undefined
|
||||
})
|
||||
|
||||
it('maps a max-token turn without losing its committed text', async () => {
|
||||
it('maps a max-token turn to end_turn without losing its committed text', async () => {
|
||||
harness = await makeBridgeHarness({ script: [maxTokensResponse('cut off')] })
|
||||
const sessionId = await newSession(harness)
|
||||
const result = await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })
|
||||
expect(result.stopReason).toBe('max_tokens')
|
||||
// A token-limit turn ending is not a prompt-level stop reason (README):
|
||||
// the prompt settles at whole-agent idle with end_turn.
|
||||
expect(result.stopReason).toBe('end_turn')
|
||||
await vi.waitFor(() => { expect(messageText(harness!)).toBe('cut off') })
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user