From 15567cd1dd47190ff165a76b28dd888e83e942a3 Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:38:19 +0530 Subject: [PATCH] let tool failures be observed by the model instead of killing the run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/x/packages/core/src/agents/runtime.ts | 51 ++++++++++++++-------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index 81421358..ae69d60c 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -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 = {