feat: improve BlueBubbles message action error handling and enhance channel action descriptions

This commit is contained in:
Tyler Yust
2026-01-19 22:32:31 -08:00
committed by Peter Steinberger
parent a5d89e6eb1
commit 2e6c58bf75
13 changed files with 326 additions and 46 deletions

View File

@@ -5,6 +5,7 @@ import { createAgentSession, SessionManager, SettingsManager } from "@mariozechn
import { resolveHeartbeatPrompt } from "../../auto-reply/heartbeat.js";
import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js";
import { listChannelSupportedActions } from "../channel-tools.js";
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { getMachineDisplayName } from "../../infra/machine-name.js";
@@ -237,6 +238,14 @@ export async function compactEmbeddedPiSession(params: {
}
}
}
// Resolve channel-specific message actions for system prompt
const channelActions = runtimeChannel
? listChannelSupportedActions({
cfg: params.config,
channel: runtimeChannel,
})
: undefined;
const runtimeInfo = {
host: machineName,
os: `${os.type()} ${os.release()}`,
@@ -245,6 +254,7 @@ export async function compactEmbeddedPiSession(params: {
model: `${provider}/${modelId}`,
channel: runtimeChannel,
capabilities: runtimeCapabilities,
channelActions,
};
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated);
const reasoningTagHint = isReasoningTagProvider(provider);

View File

@@ -7,6 +7,7 @@ import { streamSimple } from "@mariozechner/pi-ai";
import { createAgentSession, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
import { resolveHeartbeatPrompt } from "../../../auto-reply/heartbeat.js";
import { listChannelSupportedActions } from "../../channel-tools.js";
import { resolveChannelCapabilities } from "../../../config/channel-capabilities.js";
import { getMachineDisplayName } from "../../../infra/machine-name.js";
import { resolveTelegramInlineButtonsScope } from "../../../telegram/inline-buttons.js";
@@ -203,6 +204,14 @@ export async function runEmbeddedAttempt(
});
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox, params.bashElevated);
const reasoningTagHint = isReasoningTagProvider(params.provider);
// Resolve channel-specific message actions for system prompt
const channelActions = runtimeChannel
? listChannelSupportedActions({
cfg: params.config,
channel: runtimeChannel,
})
: undefined;
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
config: params.config,
agentId: sessionAgentId,
@@ -214,6 +223,7 @@ export async function runEmbeddedAttempt(
model: `${params.provider}/${params.modelId}`,
channel: runtimeChannel,
capabilities: runtimeCapabilities,
channelActions,
},
});
const isDefaultAgent = sessionAgentId === defaultAgentId;

View File

@@ -147,4 +147,72 @@ describe("buildEmbeddedRunPayloads", () => {
expect(payloads).toHaveLength(1);
expect(payloads[0]?.text).toBe("All good");
});
it("suppresses recoverable tool errors containing 'required'", () => {
const payloads = buildEmbeddedRunPayloads({
assistantTexts: [],
toolMetas: [],
lastAssistant: undefined,
lastToolError: { toolName: "message", meta: "reply", error: "text required" },
sessionKey: "session:telegram",
inlineToolResultsAllowed: false,
verboseLevel: "off",
reasoningLevel: "off",
toolResultFormat: "plain",
});
// Recoverable errors should not be sent to the user
expect(payloads).toHaveLength(0);
});
it("suppresses recoverable tool errors containing 'missing'", () => {
const payloads = buildEmbeddedRunPayloads({
assistantTexts: [],
toolMetas: [],
lastAssistant: undefined,
lastToolError: { toolName: "message", error: "messageId missing" },
sessionKey: "session:telegram",
inlineToolResultsAllowed: false,
verboseLevel: "off",
reasoningLevel: "off",
toolResultFormat: "plain",
});
expect(payloads).toHaveLength(0);
});
it("suppresses recoverable tool errors containing 'invalid'", () => {
const payloads = buildEmbeddedRunPayloads({
assistantTexts: [],
toolMetas: [],
lastAssistant: undefined,
lastToolError: { toolName: "message", error: "invalid parameter: to" },
sessionKey: "session:telegram",
inlineToolResultsAllowed: false,
verboseLevel: "off",
reasoningLevel: "off",
toolResultFormat: "plain",
});
expect(payloads).toHaveLength(0);
});
it("shows non-recoverable tool errors to the user", () => {
const payloads = buildEmbeddedRunPayloads({
assistantTexts: [],
toolMetas: [],
lastAssistant: undefined,
lastToolError: { toolName: "browser", error: "connection timeout" },
sessionKey: "session:telegram",
inlineToolResultsAllowed: false,
verboseLevel: "off",
reasoningLevel: "off",
toolResultFormat: "plain",
});
// Non-recoverable errors should still be shown
expect(payloads).toHaveLength(1);
expect(payloads[0]?.isError).toBe(true);
expect(payloads[0]?.text).toContain("connection timeout");
});
});

View File

@@ -157,16 +157,33 @@ export function buildEmbeddedRunPayloads(params: {
}
if (replyItems.length === 0 && params.lastToolError) {
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,
});
// Check if this is a recoverable/internal tool error that shouldn't be shown to users.
// These include parameter validation errors that the model should have retried.
const errorLower = (params.lastToolError.error ?? "").toLowerCase();
const isRecoverableError =
errorLower.includes("required") ||
errorLower.includes("missing") ||
errorLower.includes("invalid") ||
errorLower.includes("must be") ||
errorLower.includes("must have") ||
errorLower.includes("needs") ||
errorLower.includes("requires");
// Only show non-recoverable errors to users
if (!isRecoverableError) {
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,
});
}
// Note: Recoverable errors are already in the model's context as tool_result is_error,
// so the model can see them and should retry. We just don't send them to the user.
}
const hasAudioAsVoiceTag = replyItems.some((item) => item.audioAsVoice);

View File

@@ -32,6 +32,8 @@ export function buildEmbeddedSystemPrompt(params: {
provider?: string;
capabilities?: string[];
channel?: string;
/** Supported message actions for the current channel (e.g., react, edit, unsend) */
channelActions?: string[];
};
sandboxInfo?: EmbeddedSandboxInfo;
tools: AgentTool[];