Skip to content

feat(errors): add shared user cancellation error - #1986

Open
aidandaly24 wants to merge 3 commits into
aws:refactorfrom
aidandaly24:feat/user-cancellation-error
Open

feat(errors): add shared user cancellation error#1986
aidandaly24 wants to merge 3 commits into
aws:refactorfrom
aidandaly24:feat/user-cancellation-error

Conversation

@aidandaly24

@aidandaly24 aidandaly24 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Problem

Commander exits and user cancellations bypass the shared CLI error model. The root runnable unwraps causes and checks raw error names, while Runtime invoke defines a resource-specific interruption error and rewraps cancellation during response handling.

Solution

  • classify CommanderError in AgentCoreCLIError.fromError, preserving help as exit 0 and mapping parse failures to usage exit 2
  • add an opt-in SilentCLIError category so only intentionally silent errors skip generic root stderr output
  • add a silent, user-sourced UserCancellationError with exit code 130
  • use the shared cancellation error as the AbortSignal.reason for Runtime invoke and dataset download SIGINT handling
  • preserve Runtime's partial-response interruption summary while propagating the original typed cancellation reason
  • leave TUI-local cancellation and low-level platform AbortError handling unchanged

Verification

  • bun test src/errors/errors.test.tsx src/runnable/index.test.ts src/handlers/runtime/invoke/response.test.ts src/handlers/runtime/invoke/invoke.test.tsx src/handlers/eval/dataset/dataset.test.tsx (97 pass, 0 fail)
  • bun run typecheck
  • bun run lint:check
  • bun run format:check
  • bun run build
  • git diff --check
  • GitHub full unit suites:
    • Linux: 1110 pass, 0 fail
    • Windows: 1110 pass, 0 fail
    • macOS: 1110 pass, 0 fail
  • AgentCore E2E CodeBuild
  • bundled CLI smoke checks:
    • root, command-group, and leaf help: exit 0, help on stdout, no stderr or error log
    • nested unknown option: exit 2, one Commander error line, classified as a user error
    • real SIGINT while reading payload stdin: exit 130, no stdout or stderr, structured UserCancellationError log

@github-actions github-actions Bot added agentcore-harness-reviewing AgentCore Harness review in progress and removed agentcore-harness-reviewing AgentCore Harness review in progress labels Aug 12, 2026
@codecov-commenter

codecov-commenter commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.78%. Comparing base (531d068) to head (5e13303).

Additional details and impacted files
@@             Coverage Diff              @@
##           refactor    #1986      +/-   ##
============================================
- Coverage     96.78%   96.78%   -0.01%     
============================================
  Files           326      326              
  Lines         18061    18052       -9     
============================================
- Hits          17480    17471       -9     
  Misses          581      581              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aidandaly24
aidandaly24 marked this pull request as ready for review August 12, 2026 21:19
@aidandaly24
aidandaly24 marked this pull request as draft August 13, 2026 00:17
@aidandaly24
aidandaly24 marked this pull request as ready for review August 13, 2026 00:22
@aidandaly24
aidandaly24 requested a review from Hweinstock August 13, 2026 16:31

@Hweinstock Hweinstock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like the approach! few comments, but I think some could be follow-ups I could help pick up.

Comment thread src/errors/errors.tsx
export class RuntimeInvokeResponseError extends AgentCoreCLIError {
readonly reported = true;

export class RuntimeInvokeResponseError extends SilentCLIError {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are runtime invoke responses silent? I thought this was the error we get when the stream parsing fails.

Comment thread src/runnable/index.tsx
if ((error as Error)?.name === "AbortError") return ExitCode.INTERRUPTED;
if (caught instanceof AgentCoreCLIError) return caught.exitCode;
return ExitCode.FAILURE;
const error = AgentCoreCLIError.fromError(caught);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, really like how simple this is now!

Comment thread src/index.ts
error_name: error.name,
error_source: error.source,
});
if (error.exitCode !== 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if it makes sense to expand the exit_reason attribute to accept a cancelled value. That way we still get telemetry for these cancellations.

could be a follow-up since we'll need to adjust the backend schema to accommodate.

);
// file is written in addition to the normal metadata output
ctx.require(JsonRendererKey).renderJson({ ...response, filePath });
} finally {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need the same controller.signal.throwIfAborted(); check here?

Also wondering if it makes sense to build an abstraction for this since it seems there's already a few consumers?

Something like:

 async function withCancellation<T>(fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
    const controller = new AbortController();
    const interrupt = () => controller.abort(new UserCancellationError());
    process.once("SIGINT", interrupt);
    try {
      return await fn(controller.signal);
    } catch (error) {
      controller.signal.throwIfAborted();
      throw error;
    } finally {
      controller.abort();
      process.off("SIGINT", interrupt);
    }
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants