let tool failures be observed by the model instead of killing the run

streamAgent executed tools with no try/catch around the call. A throw
from execTool or from a subflow agent streamed up through streamAgent,
out of trigger's inner catch (which rethrows non-abort errors), and
into the new top-level catch that the previous commit added. That
surfaces the failure — but it ends the run. One misbehaving tool took
down the whole conversation.

Wrap the tool-execution block in a try/catch. On abort, rethrow so the
existing AbortError path still fires. On any other error, convert the
exception into a tool-result payload ({ success: false, error, toolName })
and keep going. The model then sees a tool-result message saying the
tool failed with a specific message and can apologize, retry with
different arguments, pick a different tool, or explain to the user —
the normal recovery moves it already knows how to make.

No change to happy-path tool execution, no change to abort handling,
no change to subflow agent semantics (subflows that themselves error
are treated identically to regular tool errors at the call site).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-04-21 14:38:19 +05:30
parent c81d3cb27b
commit 15567cd1dd

View file

@ -955,27 +955,40 @@ export async function* streamAgent({
subflow: [], subflow: [],
}); });
let result: unknown = null; let result: unknown = null;
if (agent.tools![toolCall.toolName].type === "agent") { try {
const subflowState = state.subflowStates[toolCallId]; if (agent.tools![toolCall.toolName].type === "agent") {
for await (const event of streamAgent({ const subflowState = state.subflowStates[toolCallId];
state: subflowState, for await (const event of streamAgent({
idGenerator, state: subflowState,
runId, idGenerator,
messageQueue, runId,
modelConfigRepo, messageQueue,
signal, modelConfigRepo,
abortRegistry, signal,
})) { abortRegistry,
yield* processEvent({ })) {
...event, yield* processEvent({
subflow: [toolCallId, ...event.subflow], ...event,
}); subflow: [toolCallId, ...event.subflow],
});
}
if (!subflowState.getPendingAskHumans().length && !subflowState.getPendingPermissions().length) {
result = subflowState.finalResponse();
}
} else {
result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments, { runId, signal, abortRegistry });
} }
if (!subflowState.getPendingAskHumans().length && !subflowState.getPendingPermissions().length) { } catch (error) {
result = subflowState.finalResponse(); if ((error instanceof Error && error.name === "AbortError") || signal.aborted) {
throw error;
} }
} else { const message = error instanceof Error ? (error.message || error.name) : String(error);
result = await execTool(agent.tools![toolCall.toolName], toolCall.arguments, { runId, signal, abortRegistry }); _logger.log('tool failed', message);
result = {
success: false,
error: message,
toolName: toolCall.toolName,
};
} }
const resultPayload = result === undefined ? null : result; const resultPayload = result === undefined ? null : result;
const resultMsg: z.infer<typeof ToolMessage> = { const resultMsg: z.infer<typeof ToolMessage> = {