feat(task): surface explicit abort reasons for subagent results
- Add a dedicated abortReason field to SingleResult and thread it through task execution paths so aborted subagents show actionable context instead of a generic badge. - Populate abortReason for signal cancellation, pre-start cancellation, submit_result aborted status, and missing submit_result after reminders Fixes #248
This commit is contained in:
@@ -499,12 +499,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
description: options.description,
|
||||
exitCode: 1,
|
||||
output: "",
|
||||
stderr: "Aborted before start",
|
||||
stderr: "Cancelled before start",
|
||||
truncated: false,
|
||||
durationMs: 0,
|
||||
tokens: 0,
|
||||
modelOverride,
|
||||
error: "Aborted",
|
||||
error: "Cancelled before start",
|
||||
aborted: true,
|
||||
abortReason: "Cancelled before start",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -612,6 +614,17 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
signal.addEventListener("abort", onAbort, { once: true, signal: listenerSignal });
|
||||
}
|
||||
|
||||
const resolveSignalAbortReason = (): string => {
|
||||
const reason = signal?.reason;
|
||||
if (reason instanceof Error) {
|
||||
const message = reason.message.trim();
|
||||
if (message.length > 0) return message;
|
||||
} else if (typeof reason === "string") {
|
||||
const message = reason.trim();
|
||||
if (message.length > 0) return message;
|
||||
}
|
||||
return "Cancelled by caller";
|
||||
};
|
||||
const PROGRESS_COALESCE_MS = 150;
|
||||
let lastProgressEmitMs = 0;
|
||||
let progressTimeoutId: NodeJS.Timeout | null = null;
|
||||
@@ -898,16 +911,20 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
exitCode: number;
|
||||
error?: string;
|
||||
aborted?: boolean;
|
||||
abortReason?: string;
|
||||
durationMs: number;
|
||||
}> => {
|
||||
const sessionAbortController = new AbortController();
|
||||
let exitCode = 0;
|
||||
let error: string | undefined;
|
||||
let aborted = false;
|
||||
|
||||
let abortReasonText: string | undefined;
|
||||
const checkAbort = () => {
|
||||
if (abortSignal.aborted) {
|
||||
aborted = abortReason === "signal" || abortReason === undefined;
|
||||
if (aborted) {
|
||||
abortReasonText ??= resolveSignalAbortReason();
|
||||
}
|
||||
exitCode = 1;
|
||||
throw new ToolAbortError();
|
||||
}
|
||||
@@ -1096,6 +1113,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
if (!submitResultCalled && !abortSignal.aborted) {
|
||||
aborted = true;
|
||||
exitCode = 1;
|
||||
abortReasonText ??= SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
|
||||
error ??= SUBAGENT_WARNING_MISSING_SUBMIT_RESULT;
|
||||
}
|
||||
|
||||
@@ -1103,6 +1121,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
if (lastAssistant) {
|
||||
if (lastAssistant.stopReason === "aborted") {
|
||||
aborted = abortReason === "signal" || abortReason === undefined;
|
||||
if (aborted) {
|
||||
abortReasonText ??= resolveSignalAbortReason();
|
||||
}
|
||||
exitCode = 1;
|
||||
} else if (lastAssistant.stopReason === "error") {
|
||||
exitCode = 1;
|
||||
@@ -1117,6 +1138,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
} finally {
|
||||
if (abortSignal.aborted) {
|
||||
aborted = abortReason === "signal" || abortReason === undefined;
|
||||
if (aborted) {
|
||||
abortReasonText ??= resolveSignalAbortReason();
|
||||
}
|
||||
if (exitCode === 0) exitCode = 1;
|
||||
}
|
||||
sessionAbortController.abort();
|
||||
@@ -1143,6 +1167,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
exitCode,
|
||||
error,
|
||||
aborted,
|
||||
abortReason: aborted ? abortReasonText : undefined,
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
};
|
||||
@@ -1178,6 +1203,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
rawOutput = finalized.rawOutput;
|
||||
exitCode = finalized.exitCode;
|
||||
stderr = finalized.stderr;
|
||||
const lastSubmitResult = submitResultItems?.[submitResultItems.length - 1];
|
||||
const submitResultAbortReason =
|
||||
lastSubmitResult?.status === "aborted" ? lastSubmitResult.error || "Subagent aborted task" : undefined;
|
||||
const { abortedViaSubmitResult, hasSubmitResult } = finalized;
|
||||
const { content: truncatedOutput, truncated } = truncateTail(rawOutput, {
|
||||
maxBytes: MAX_OUTPUT_BYTES,
|
||||
@@ -1203,6 +1231,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
|
||||
// Update final progress
|
||||
const wasAborted = abortedViaSubmitResult || (!hasSubmitResult && (done.aborted || signal?.aborted || false));
|
||||
const finalAbortReason = wasAborted
|
||||
? abortedViaSubmitResult
|
||||
? submitResultAbortReason
|
||||
: (done.abortReason ?? (signal?.aborted ? resolveSignalAbortReason() : "Subagent aborted task"))
|
||||
: undefined;
|
||||
progress.status = wasAborted ? "aborted" : exitCode === 0 ? "completed" : "failed";
|
||||
scheduleProgress(true);
|
||||
|
||||
@@ -1223,6 +1256,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
||||
modelOverride,
|
||||
error: exitCode !== 0 && stderr ? stderr : undefined,
|
||||
aborted: wasAborted,
|
||||
abortReason: finalAbortReason,
|
||||
usage: hasUsage ? accumulatedUsage : undefined,
|
||||
outputPath,
|
||||
extractedToolData: progress.extractedToolData,
|
||||
|
||||
Reference in New Issue
Block a user