From 8ea8801d06ea9494e765f82c7c96166dedfa96e0 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 24 Jan 2026 08:17:29 +0000 Subject: [PATCH] fix: show tool error fallback for tool-only replies --- CHANGELOG.md | 2 ++ .../pi-embedded-runner/run/payloads.test.ts | 29 +++++++++++++++++++ src/agents/pi-embedded-runner/run/payloads.ts | 11 ++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fe01fea4..e3149459f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ Docs: https://docs.clawd.bot ### Fixes - Gateway: compare Linux process start time to avoid PID recycling lock loops; keep locks unless stale. (#1572) Thanks @steipete. - Skills: gate bird Homebrew install to macOS. (#1569) Thanks @bradleypriest. +- Slack: honor open groupPolicy for unlisted channels in message + slash gating. (#1563) Thanks @itsjaydesu. +- Agents: show tool error fallback when the last assistant turn only invoked tools (prevents silent stops). - Agents: ignore IDENTITY.md template placeholders when parsing identity to avoid placeholder replies. (#1556) - Agents: drop orphaned OpenAI Responses reasoning blocks on model switches. (#1562) Thanks @roshanasingh4. - Docker: update gateway command in docker-compose and Hetzner guide. (#1514) diff --git a/src/agents/pi-embedded-runner/run/payloads.test.ts b/src/agents/pi-embedded-runner/run/payloads.test.ts index f3dfc71e0..7a38cc8d2 100644 --- a/src/agents/pi-embedded-runner/run/payloads.test.ts +++ b/src/agents/pi-embedded-runner/run/payloads.test.ts @@ -148,6 +148,35 @@ describe("buildEmbeddedRunPayloads", () => { expect(payloads[0]?.text).toBe("All good"); }); + it("adds tool error fallback when the assistant only invoked tools", () => { + const payloads = buildEmbeddedRunPayloads({ + assistantTexts: [], + toolMetas: [], + lastAssistant: { + stopReason: "toolUse", + content: [ + { + type: "toolCall", + id: "toolu_01", + name: "exec", + arguments: { command: "echo hi" }, + }, + ], + } as AssistantMessage, + lastToolError: { toolName: "exec", error: "Command exited with code 1" }, + 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("Exec"); + expect(payloads[0]?.text).toContain("code 1"); + }); + it("suppresses recoverable tool errors containing 'required'", () => { const payloads = buildEmbeddedRunPayloads({ assistantTexts: [], diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/pi-embedded-runner/run/payloads.ts index 005402775..9d6a16626 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/pi-embedded-runner/run/payloads.ts @@ -169,7 +169,16 @@ export function buildEmbeddedRunPayloads(params: { } if (params.lastToolError) { - const hasUserFacingReply = replyItems.length > 0; + const lastAssistantHasToolCalls = + Array.isArray(params.lastAssistant?.content) && + params.lastAssistant?.content.some((block) => + block && typeof block === "object" + ? (block as { type?: unknown }).type === "toolCall" + : false, + ); + const lastAssistantWasToolUse = params.lastAssistant?.stopReason === "toolUse"; + const hasUserFacingReply = + replyItems.length > 0 && !lastAssistantHasToolCalls && !lastAssistantWasToolUse; // Check if this is a recoverable/internal tool error that shouldn't be shown to users // when there's already a user-facing reply (the model should have retried). const errorLower = (params.lastToolError.error ?? "").toLowerCase();