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