diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/pi-embedded-subscribe.ts index bc2208c96..24bfdf865 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/pi-embedded-subscribe.ts @@ -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, }, }); } diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 2bb6c9593..27fad87a6 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -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)." diff --git a/src/agents/tools/message-tool.ts b/src/agents/tools/message-tool.ts index b903364fd..40945c043 100644 --- a/src/agents/tools/message-tool.ts +++ b/src/agents/tools/message-tool.ts @@ -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 = { ...MessageToolCommonSchema }; + if (!options.includeButtons) delete props.buttons; + + const schemas: Array> = []; + 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(["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; - const baseProps = (base.properties ?? {}) as Record; - const props: Record = { - ...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 {