Agents: harden message tool sends

This commit is contained in:
Chris Taylor
2026-01-11 10:41:44 +00:00
committed by Peter Steinberger
parent 55da6ca449
commit 3da3e201de
3 changed files with 97 additions and 55 deletions

View File

@@ -96,6 +96,17 @@ function sanitizeToolResult(result: unknown): unknown {
return { ...record, content: sanitized };
}
function isToolResultError(result: unknown): boolean {
if (!result || typeof result !== "object") return false;
const record = result as { details?: unknown };
const details = record.details;
if (!details || typeof details !== "object") return false;
const status = (details as { status?: unknown }).status;
if (typeof status !== "string") return false;
const normalized = status.trim().toLowerCase();
return normalized === "error" || normalized === "timeout";
}
function stripThinkingSegments(text: string): string {
if (!text || !THINKING_TAG_RE.test(text)) return text;
THINKING_TAG_RE.lastIndex = 0;
@@ -613,6 +624,7 @@ export function subscribeEmbeddedPiSession(params: {
(evt as AgentEvent & { isError: boolean }).isError,
);
const result = (evt as AgentEvent & { result?: unknown }).result;
const isToolError = isError || isToolResultError(result);
const sanitizedResult = sanitizeToolResult(result);
const meta = toolMetaById.get(toolCallId);
toolMetas.push({ toolName, meta });
@@ -624,7 +636,7 @@ export function subscribeEmbeddedPiSession(params: {
const pendingTarget = pendingMessagingTargets.get(toolCallId);
if (pendingText) {
pendingMessagingTexts.delete(toolCallId);
if (!isError) {
if (!isToolError) {
messagingToolSentTexts.push(pendingText);
messagingToolSentTextsNormalized.push(
normalizeTextForComparison(pendingText),
@@ -637,7 +649,7 @@ export function subscribeEmbeddedPiSession(params: {
}
if (pendingTarget) {
pendingMessagingTargets.delete(toolCallId);
if (!isError) {
if (!isToolError) {
messagingToolSentTargets.push(pendingTarget);
trimMessagingToolSent();
}
@@ -651,7 +663,7 @@ export function subscribeEmbeddedPiSession(params: {
name: toolName,
toolCallId,
meta,
isError,
isError: isToolError,
result: sanitizedResult,
},
});
@@ -662,7 +674,7 @@ export function subscribeEmbeddedPiSession(params: {
name: toolName,
toolCallId,
meta,
isError,
isError: isToolError,
},
});
}

View File

@@ -313,6 +313,7 @@ export function buildAgentSystemPrompt(params: {
"",
"### message tool",
"- Use `message` for proactive sends + provider actions (polls, reactions, etc.).",
"- For `action=send`, include `to` and `message`.",
"- If multiple providers are configured, pass `provider` (whatsapp|telegram|discord|slack|signal|imessage|msteams).",
telegramInlineButtonsEnabled
? "- Telegram: inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]` (callback_data routes back as a user message)."

View File

@@ -27,45 +27,43 @@ import { handleSlackAction } from "./slack-actions.js";
import { handleTelegramAction } from "./telegram-actions.js";
import { handleWhatsAppAction } from "./whatsapp-actions.js";
const MessageActionSchema = Type.Union([
Type.Literal("send"),
Type.Literal("poll"),
Type.Literal("react"),
Type.Literal("reactions"),
Type.Literal("read"),
Type.Literal("edit"),
Type.Literal("delete"),
Type.Literal("pin"),
Type.Literal("unpin"),
Type.Literal("list-pins"),
Type.Literal("permissions"),
Type.Literal("thread-create"),
Type.Literal("thread-list"),
Type.Literal("thread-reply"),
Type.Literal("search"),
Type.Literal("sticker"),
Type.Literal("member-info"),
Type.Literal("role-info"),
Type.Literal("emoji-list"),
Type.Literal("emoji-upload"),
Type.Literal("sticker-upload"),
Type.Literal("role-add"),
Type.Literal("role-remove"),
Type.Literal("channel-info"),
Type.Literal("channel-list"),
Type.Literal("voice-status"),
Type.Literal("event-list"),
Type.Literal("event-create"),
Type.Literal("timeout"),
Type.Literal("kick"),
Type.Literal("ban"),
]);
const AllMessageActions = [
"send",
"poll",
"react",
"reactions",
"read",
"edit",
"delete",
"pin",
"unpin",
"list-pins",
"permissions",
"thread-create",
"thread-list",
"thread-reply",
"search",
"sticker",
"member-info",
"role-info",
"emoji-list",
"emoji-upload",
"sticker-upload",
"role-add",
"role-remove",
"channel-info",
"channel-list",
"voice-status",
"event-list",
"event-create",
"timeout",
"kick",
"ban",
];
const MessageToolSchema = Type.Object({
action: MessageActionSchema,
const MessageToolCommonSchema = {
provider: Type.Optional(Type.String()),
to: Type.Optional(Type.String()),
message: Type.Optional(Type.String()),
media: Type.Optional(Type.String()),
buttons: Type.Optional(
Type.Array(
@@ -129,6 +127,46 @@ const MessageToolSchema = Type.Object({
gatewayUrl: Type.Optional(Type.String()),
gatewayToken: Type.Optional(Type.String()),
timeoutMs: Type.Optional(Type.Number()),
};
function buildMessageToolSchemaFromActions(
actions: string[],
options: { includeButtons: boolean },
) {
const props: Record<string, unknown> = { ...MessageToolCommonSchema };
if (!options.includeButtons) delete props.buttons;
const schemas: Array<ReturnType<typeof Type.Object>> = [];
if (actions.includes("send")) {
schemas.push(
Type.Object({
action: Type.Literal("send"),
to: Type.String(),
message: Type.String(),
...props,
}),
);
}
const nonSendActions = actions.filter((action) => action !== "send");
if (nonSendActions.length > 0) {
schemas.push(
Type.Object({
action: Type.Union(
nonSendActions.map((action) => Type.Literal(action)),
),
to: Type.Optional(Type.String()),
message: Type.Optional(Type.String()),
...props,
}),
);
}
return schemas.length === 1 ? schemas[0] : Type.Union(schemas);
}
const MessageToolSchema = buildMessageToolSchemaFromActions(AllMessageActions, {
includeButtons: true,
});
type MessageToolOptions = {
@@ -164,7 +202,7 @@ function hasTelegramInlineButtons(cfg: ClawdbotConfig): boolean {
return caps.has("inlinebuttons");
}
function buildMessageActionSchema(cfg: ClawdbotConfig) {
function buildMessageActionList(cfg: ClawdbotConfig) {
const actions = new Set<string>(["send"]);
const discordAccounts = listEnabledDiscordAccounts(cfg).filter(
@@ -281,25 +319,16 @@ function buildMessageActionSchema(cfg: ClawdbotConfig) {
actions.add("ban");
}
return Type.Union(Array.from(actions).map((action) => Type.Literal(action)));
return Array.from(actions);
}
function buildMessageToolSchema(cfg: ClawdbotConfig) {
const base = MessageToolSchema as unknown as Record<string, unknown>;
const baseProps = (base.properties ?? {}) as Record<string, unknown>;
const props: Record<string, unknown> = {
...baseProps,
action: buildMessageActionSchema(cfg),
};
const actions = buildMessageActionList(cfg);
const telegramEnabled = listEnabledTelegramAccounts(cfg).some(
(account) => account.tokenSource !== "none",
);
if (!telegramEnabled || !hasTelegramInlineButtons(cfg)) {
delete props.buttons;
}
return { ...base, properties: props };
const includeButtons = telegramEnabled && hasTelegramInlineButtons(cfg);
return buildMessageToolSchemaFromActions(actions, { includeButtons });
}
function resolveAgentAccountId(value?: string): string | undefined {