diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index d6b1013b6d..c4624eadcf 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -150,7 +150,11 @@ export class ReactLoopAgent implements Agent { * all owned by the factory. */ cancel(cause: AgentInterruptReason, options: CancelOptions = {}): void { - if (this.abort !== undefined || this.queued.length > 0 || this.outbox.length > 0) { + // Effective only when it aborts the active turn or actually discards + // pending work: a keepInbox call with no active turn is a documented + // no-op, so it must not emit cancel-requested for consumers to misread. + const discards = !options.keepInbox && (this.queued.length > 0 || this.outbox.length > 0) + if (this.abort !== undefined || discards) { // Observe-only: coordination consumers update their state before the // inboxes clear; listener failures are contained by the dispatcher. if (cause.kind !== 'disposed') emitAgentEvent(this.loopCtx, this, 'agent/cancel-requested', cause) diff --git a/packages/core/agent-loop/tests/cancel.spec.ts b/packages/core/agent-loop/tests/cancel.spec.ts index 6dcc832f49..d0d11ed856 100644 --- a/packages/core/agent-loop/tests/cancel.spec.ts +++ b/packages/core/agent-loop/tests/cancel.spec.ts @@ -104,12 +104,17 @@ describe('Agent.cancel()', () => { const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' }) const discards: unknown[] = [] ctx.on('agent/inbox/discard', (subject, items) => { if (subject === agent) discards.push(items) }) + const cancelRequests: unknown[] = [] + ctx.on('agent/cancel-requested', (subject, cause) => { if (subject === agent) cancelRequests.push(cause) }) // Queue a turn WITHOUT waking the driver, so it sits in the inbox. agent.send({ content: [{ type: 'text', text: 'preserved' }], source: { kind: 'user' } }, { target: 'next-turn', wakeup: false }) - // keepInbox cancel: no active turn, work preserved, no discard event. + // keepInbox cancel: no active turn, work preserved, no discard event. With + // nothing to abort and nothing discarded, the call is a documented no-op, + // so it emits no cancel-requested either. agent.cancel({ kind: 'user' }, { keepInbox: true }) expect(discards).toEqual([]) + expect(cancelRequests).toEqual([]) // The preserved item still runs once the driver is woken by a later send. send(agent, 'wake it')