fix: allow continuing after interrupting assistant (#178)

## Description
This PR fixes the issue where the CLI can't continue after interrupting
the assistant with ESC ESC (Fixes #114). The problem was caused by
duplicate code in the `cancel()` method and improper state reset after
cancellation.

## Changes
- Fixed duplicate code in the `cancel()` method of the `AgentLoop` class
- Added proper reset of the `currentStream` property in the `cancel()`
method
- Created a new `AbortController` after aborting the current one to
ensure future tool calls work
- Added a system message to indicate the interruption to the user
- Added a comprehensive test to verify the fix

## Benefits
- Users can now continue using the CLI after interrupting the assistant
- Improved user experience by providing feedback when interruption
occurs
- Better state management in the agent loop

## Testing
- Added a dedicated test that verifies the agent can process new input
after cancellation
- Manually tested the fix by interrupting the assistant and confirming
that new input is processed correctly

---------

Signed-off-by: crazywolf132 <crazywolf132@gmail.com>
This commit is contained in:
Brayden Moon
2025-04-17 15:20:19 +10:00
committed by GitHub
parent b5fad66e2c
commit b0ccca5556
3 changed files with 173 additions and 17 deletions

View File

@@ -108,6 +108,9 @@ export class AgentLoop {
if (this.terminated) {
return;
}
// Reset the current stream to allow new requests
this.currentStream = null;
if (isLoggingEnabled()) {
log(
`AgentLoop.cancel() invoked currentStream=${Boolean(
@@ -122,22 +125,16 @@ export class AgentLoop {
)?.controller?.abort?.();
this.canceled = true;
// Abort any in-progress tool calls
this.execAbortController?.abort();
// Create a new abort controller for future tool calls
this.execAbortController = new AbortController();
if (isLoggingEnabled()) {
log("AgentLoop.cancel(): execAbortController.abort() called");
}
// If we have *not* seen any function_call IDs yet there is nothing that
// needs to be satisfied in a followup request. In that case we clear
// the stored lastResponseId so a subsequent run starts a clean turn.
if (this.pendingAborts.size === 0) {
try {
this.onLastResponseId("");
} catch {
/* ignore */
}
}
// NOTE: We intentionally do *not* clear `lastResponseId` here. If the
// stream produced a `function_call` before the user cancelled, OpenAI now
// expects a corresponding `function_call_output` that must reference that
@@ -155,11 +152,6 @@ export class AgentLoop {
}
}
// NOTE: We intentionally do *not* clear `lastResponseId` here. If the
// stream produced a `function_call` before the user cancelled, OpenAI now
// expects a corresponding `function_call_output` that must reference that
// very same response ID. We therefore keep the ID around so the
// followup request can still satisfy the contract.
this.onLoading(false);
/* Inform the UI that the run was aborted by the user. */
@@ -400,8 +392,10 @@ export class AgentLoop {
// identified and dropped.
const thisGeneration = ++this.generation;
// Reset cancellation flag for a fresh run.
// Reset cancellation flag and stream for a fresh run.
this.canceled = false;
this.currentStream = null;
// Create a fresh AbortController for this run so that tool calls from a
// previous run do not accidentally get signalled.
this.execAbortController = new AbortController();