fix: gate tool-error fallback replies (#1175) (thanks @vrknetha)
This commit is contained in:
parent
f039fdf8e9
commit
ace906539f
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user