From ace906539f3c03dd87c228e331807270bbc12573 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 14:07:34 +0000 Subject: [PATCH] fix: gate tool-error fallback replies (#1175) (thanks @vrknetha) --- CHANGELOG.md | 1 + src/agents/pi-embedded-runner/run.ts | 1 + .../pi-embedded-runner/run/payloads.test.ts | 36 ++++++++++++ src/agents/pi-embedded-runner/run/payloads.ts | 57 ++++++++++++------- 4 files changed, 73 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a130bc1e..0de7848ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.clawd.bot - macOS: avoid touching launchd in Remote over SSH so quitting the app no longer disables the remote gateway. (#1105) - Memory: index atomically so failed reindex preserves the previous memory database. (#1151) — thanks @gumadeiras. - Memory: avoid sqlite-vec unique constraint failures when reindexing duplicate chunk ids. (#1151) — thanks @gumadeiras. +- Agents: surface tool failures when no assistant output is emitted. (#1175) — thanks @vrknetha. ## 2026.1.18-3 diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 82bafe0b4..9c98facf6 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -432,6 +432,7 @@ export async function runEmbeddedPiAgent( toolMetas: attempt.toolMetas, lastAssistant: attempt.lastAssistant, lastToolError: attempt.lastToolError, + didSendViaMessagingTool: attempt.didSendViaMessagingTool, config: params.config, sessionKey: params.sessionKey ?? params.sessionId, verboseLevel: params.verboseLevel, diff --git a/src/agents/pi-embedded-runner/run/payloads.test.ts b/src/agents/pi-embedded-runner/run/payloads.test.ts index 78766e112..5e6ee0f55 100644 --- a/src/agents/pi-embedded-runner/run/payloads.test.ts +++ b/src/agents/pi-embedded-runner/run/payloads.test.ts @@ -147,4 +147,40 @@ describe("buildEmbeddedRunPayloads", () => { expect(payloads).toHaveLength(1); expect(payloads[0]?.text).toBe("All good"); }); + + it("adds tool error fallback when assistant output is NO_REPLY", () => { + const payloads = buildEmbeddedRunPayloads({ + assistantTexts: ["NO_REPLY"], + toolMetas: [], + lastAssistant: { stopReason: "end_turn" } as AssistantMessage, + lastToolError: { toolName: "browser", error: "tab not found" }, + sessionKey: "session:telegram", + inlineToolResultsAllowed: false, + verboseLevel: "off", + reasoningLevel: "off", + toolResultFormat: "plain", + }); + + expect(payloads).toHaveLength(1); + expect(payloads[0]?.isError).toBe(true); + expect(payloads[0]?.text).toContain("browser"); + expect(payloads[0]?.text).toContain("tab not found"); + }); + + it("skips tool error fallback when messaging tool already sent", () => { + const payloads = buildEmbeddedRunPayloads({ + assistantTexts: [], + toolMetas: [], + lastAssistant: undefined, + lastToolError: { toolName: "browser", error: "tab not found" }, + didSendViaMessagingTool: true, + sessionKey: "session:telegram", + inlineToolResultsAllowed: false, + verboseLevel: "off", + reasoningLevel: "off", + toolResultFormat: "plain", + }); + + expect(payloads).toHaveLength(0); + }); }); diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index 2d831cd00..f7e56777d 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -24,6 +24,7 @@ export function buildEmbeddedRunPayloads(params: { toolMetas: ToolMetaEntry[]; lastAssistant: AssistantMessage | undefined; lastToolError?: { toolName: string; meta?: string; error?: string }; + didSendViaMessagingTool?: boolean; config?: ClawdbotConfig; sessionKey: string; verboseLevel?: VerboseLevel; @@ -156,34 +157,46 @@ export function buildEmbeddedRunPayloads(params: { }); } - if (replyItems.length === 0 && params.lastToolError) { + const buildPayloads = (items: typeof replyItems) => { + const hasAudioAsVoiceTag = items.some((item) => item.audioAsVoice); + return items + .map((item) => ({ + text: item.text?.trim() ? item.text.trim() : undefined, + mediaUrls: item.media?.length ? item.media : undefined, + mediaUrl: item.media?.[0], + isError: item.isError, + replyToId: item.replyToId, + replyToTag: item.replyToTag, + replyToCurrent: item.replyToCurrent, + audioAsVoice: item.audioAsVoice || Boolean(hasAudioAsVoiceTag && item.media?.length), + })) + .filter((p) => { + if (!p.text && !p.mediaUrl && (!p.mediaUrls || p.mediaUrls.length === 0)) return false; + if (p.text && isSilentReplyText(p.text, SILENT_REPLY_TOKEN)) return false; + return true; + }); + }; + + let payloads = buildPayloads(replyItems); + + if ( + payloads.length === 0 && + params.lastToolError && + params.didSendViaMessagingTool !== true + ) { const toolSummary = formatToolAggregate( params.lastToolError.toolName, params.lastToolError.meta ? [params.lastToolError.meta] : undefined, { markdown: useMarkdown }, ); const errorSuffix = params.lastToolError.error ? `: ${params.lastToolError.error}` : ""; - replyItems.push({ - text: `⚠️ ${toolSummary} failed${errorSuffix}`, - isError: true, - }); + payloads = buildPayloads([ + { + text: `⚠️ ${toolSummary} failed${errorSuffix}`, + isError: true, + }, + ]); } - const hasAudioAsVoiceTag = replyItems.some((item) => item.audioAsVoice); - return replyItems - .map((item) => ({ - text: item.text?.trim() ? item.text.trim() : undefined, - mediaUrls: item.media?.length ? item.media : undefined, - mediaUrl: item.media?.[0], - isError: item.isError, - replyToId: item.replyToId, - replyToTag: item.replyToTag, - replyToCurrent: item.replyToCurrent, - audioAsVoice: item.audioAsVoice || Boolean(hasAudioAsVoiceTag && item.media?.length), - })) - .filter((p) => { - if (!p.text && !p.mediaUrl && (!p.mediaUrls || p.mediaUrls.length === 0)) return false; - if (p.text && isSilentReplyText(p.text, SILENT_REPLY_TOKEN)) return false; - return true; - }); + return payloads; }