fix(agent): merge abort signals instead of overriding caller's signal

When both timeoutMs and a caller-provided abortSignal were set, the
timeout signal silently replaced the caller's signal. Now they are
combined via mergeAbortSignals() so either source can cancel the run.

Also removes dead array-handling branch in text-tool-extractor.ts
(extractJSONObjects only returns objects, never arrays).
This commit is contained in:
JackChen 2026-04-05 12:00:16 +08:00
parent bc31008f4e
commit a4a1add8ca
2 changed files with 20 additions and 10 deletions

View File

@ -50,6 +50,19 @@ import {
const ZERO_USAGE: TokenUsage = { input_tokens: 0, output_tokens: 0 }
/**
* Combine two {@link AbortSignal}s so that aborting either one cancels the
* returned signal. Works on Node 18+ (no `AbortSignal.any` required).
*/
function mergeAbortSignals(a: AbortSignal, b: AbortSignal): AbortSignal {
const controller = new AbortController()
if (a.aborted || b.aborted) { controller.abort(); return controller.signal }
const abort = () => controller.abort()
a.addEventListener('abort', abort, { once: true })
b.addEventListener('abort', abort, { once: true })
return controller.signal
}
function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage {
return {
input_tokens: a.input_tokens + b.input_tokens,
@ -298,11 +311,17 @@ export class Agent {
const timeoutSignal = this.config.timeoutMs !== undefined && this.config.timeoutMs > 0
? AbortSignal.timeout(this.config.timeoutMs)
: undefined
// Merge caller-provided abortSignal with the timeout signal so that
// either cancellation source is respected.
const callerAbort = callerOptions?.abortSignal
const effectiveAbort = timeoutSignal && callerAbort
? mergeAbortSignals(timeoutSignal, callerAbort)
: timeoutSignal ?? callerAbort
const runOptions: RunOptions = {
...callerOptions,
onMessage: internalOnMessage,
...(needsRunId ? { runId: generateRunId() } : undefined),
...(timeoutSignal ? { abortSignal: timeoutSignal } : undefined),
...(effectiveAbort ? { abortSignal: effectiveAbort } : undefined),
}
const result = await runner.run(messages, runOptions)

View File

@ -211,15 +211,6 @@ export function extractToolCallsFromText(
const results: ToolUseBlock[] = []
for (const obj of jsonObjects) {
// Handle array of tool calls
if (Array.isArray(obj)) {
for (const item of obj) {
const block = parseToolCallJSON(item, nameSet)
if (block !== null) results.push(block)
}
continue
}
const block = parseToolCallJSON(obj, nameSet)
if (block !== null) results.push(block)
}