fix: gate tool-error fallback replies (#1175) (thanks @vrknetha)

This commit is contained in:
Peter Steinberger 2026-01-18 14:07:34 +00:00
parent f039fdf8e9
commit ace906539f
4 changed files with 73 additions and 22 deletions

View File

@ -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) - 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: 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. - 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 ## 2026.1.18-3

View File

@ -432,6 +432,7 @@ export async function runEmbeddedPiAgent(
toolMetas: attempt.toolMetas, toolMetas: attempt.toolMetas,
lastAssistant: attempt.lastAssistant, lastAssistant: attempt.lastAssistant,
lastToolError: attempt.lastToolError, lastToolError: attempt.lastToolError,
didSendViaMessagingTool: attempt.didSendViaMessagingTool,
config: params.config, config: params.config,
sessionKey: params.sessionKey ?? params.sessionId, sessionKey: params.sessionKey ?? params.sessionId,
verboseLevel: params.verboseLevel, verboseLevel: params.verboseLevel,

View File

@ -147,4 +147,40 @@ describe("buildEmbeddedRunPayloads", () => {
expect(payloads).toHaveLength(1); expect(payloads).toHaveLength(1);
expect(payloads[0]?.text).toBe("All good"); 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);
});
}); });

View File

@ -24,6 +24,7 @@ export function buildEmbeddedRunPayloads(params: {
toolMetas: ToolMetaEntry[]; toolMetas: ToolMetaEntry[];
lastAssistant: AssistantMessage | undefined; lastAssistant: AssistantMessage | undefined;
lastToolError?: { toolName: string; meta?: string; error?: string }; lastToolError?: { toolName: string; meta?: string; error?: string };
didSendViaMessagingTool?: boolean;
config?: ClawdbotConfig; config?: ClawdbotConfig;
sessionKey: string; sessionKey: string;
verboseLevel?: VerboseLevel; 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( const toolSummary = formatToolAggregate(
params.lastToolError.toolName, params.lastToolError.toolName,
params.lastToolError.meta ? [params.lastToolError.meta] : undefined, params.lastToolError.meta ? [params.lastToolError.meta] : undefined,
{ markdown: useMarkdown }, { markdown: useMarkdown },
); );
const errorSuffix = params.lastToolError.error ? `: ${params.lastToolError.error}` : ""; const errorSuffix = params.lastToolError.error ? `: ${params.lastToolError.error}` : "";
replyItems.push({ payloads = buildPayloads([
text: `⚠️ ${toolSummary} failed${errorSuffix}`, {
isError: true, text: `⚠️ ${toolSummary} failed${errorSuffix}`,
}); isError: true,
},
]);
} }
const hasAudioAsVoiceTag = replyItems.some((item) => item.audioAsVoice); return payloads;
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;
});
} }