feat: added capability for clawdbot to react

This commit is contained in:
Bohdan Podvirnyi
2026-01-13 21:34:40 +02:00
committed by Peter Steinberger
parent d05c3d0659
commit 0e1dcf9cb4
12 changed files with 503 additions and 56 deletions

View File

@@ -43,6 +43,11 @@ export function buildAgentSystemPrompt(params: {
defaultLevel: "on" | "off";
};
};
/** Reaction guidance for the agent (for Telegram minimal/extensive modes). */
reactionGuidance?: {
level: "minimal" | "extensive";
channel: string;
};
}) {
const coreToolSummaries: Record<string, string> = {
read: "Read file contents",
@@ -351,6 +356,29 @@ export function buildAgentSystemPrompt(params: {
if (extraSystemPrompt) {
lines.push("## Group Chat Context", extraSystemPrompt, "");
}
if (params.reactionGuidance) {
const { level, channel } = params.reactionGuidance;
const guidanceText =
level === "minimal"
? [
`Reactions are enabled for ${channel} in MINIMAL mode.`,
"React ONLY when truly relevant:",
"- Acknowledge important user requests or confirmations",
"- Express genuine sentiment (humor, appreciation) sparingly",
"- Avoid reacting to routine messages or your own replies",
"Guideline: at most 1 reaction per 5-10 exchanges.",
].join("\n")
: [
`Reactions are enabled for ${channel} in EXTENSIVE mode.`,
"Feel free to react liberally:",
"- Acknowledge messages with appropriate emojis",
"- Express sentiment and personality through reactions",
"- React to interesting content, humor, or notable events",
"- Use reactions to confirm understanding or agreement",
"Guideline: react whenever it feels natural.",
].join("\n");
lines.push("## Reactions", guidanceText, "");
}
if (reasoningHint) {
lines.push("## Reasoning Format", reasoningHint, "");
}

View File

@@ -33,9 +33,30 @@ describe("handleTelegramAction", () => {
}
});
it("adds reactions", async () => {
it("adds reactions when reactionLevel is minimal", async () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
channels: { telegram: { botToken: "tok", reactionLevel: "minimal" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
action: "react",
chatId: "123",
messageId: "456",
emoji: "✅",
},
cfg,
);
expect(reactMessageTelegram).toHaveBeenCalledWith(
"123",
456,
"✅",
expect.objectContaining({ token: "tok", remove: false }),
);
});
it("adds reactions when reactionLevel is extensive", async () => {
const cfg = {
channels: { telegram: { botToken: "tok", reactionLevel: "extensive" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
@@ -56,7 +77,7 @@ describe("handleTelegramAction", () => {
it("removes reactions on empty emoji", async () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
channels: { telegram: { botToken: "tok", reactionLevel: "minimal" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
@@ -77,7 +98,7 @@ describe("handleTelegramAction", () => {
it("removes reactions when remove flag set", async () => {
const cfg = {
channels: { telegram: { botToken: "tok" } },
channels: { telegram: { botToken: "tok", reactionLevel: "extensive" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
@@ -97,10 +118,48 @@ describe("handleTelegramAction", () => {
);
});
it("respects reaction gating", async () => {
it("blocks reactions when reactionLevel is off", async () => {
const cfg = {
channels: { telegram: { botToken: "tok", reactionLevel: "off" } },
} as ClawdbotConfig;
await expect(
handleTelegramAction(
{
action: "react",
chatId: "123",
messageId: "456",
emoji: "✅",
},
cfg,
),
).rejects.toThrow(/Telegram agent reactions disabled.*reactionLevel="off"/);
});
it("blocks reactions when reactionLevel is ack (default)", async () => {
const cfg = {
channels: { telegram: { botToken: "tok", reactionLevel: "ack" } },
} as ClawdbotConfig;
await expect(
handleTelegramAction(
{
action: "react",
chatId: "123",
messageId: "456",
emoji: "✅",
},
cfg,
),
).rejects.toThrow(/Telegram agent reactions disabled.*reactionLevel="ack"/);
});
it("also respects legacy actions.reactions gating", async () => {
const cfg = {
channels: {
telegram: { botToken: "tok", actions: { reactions: false } },
telegram: {
botToken: "tok",
reactionLevel: "minimal",
actions: { reactions: false },
},
},
} as ClawdbotConfig;
await expect(
@@ -113,7 +172,7 @@ describe("handleTelegramAction", () => {
},
cfg,
),
).rejects.toThrow(/Telegram reactions are disabled/);
).rejects.toThrow(/Telegram reactions are disabled via actions.reactions/);
});
it("sends a text message", async () => {

View File

@@ -1,6 +1,7 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { resolveTelegramReactionLevel } from "../../telegram/reaction-level.js";
import {
deleteMessageTelegram,
reactMessageTelegram,
@@ -82,8 +83,20 @@ export async function handleTelegramAction(
const isActionEnabled = createActionGate(cfg.channels?.telegram?.actions);
if (action === "react") {
// Check reaction level first
const reactionLevelInfo = resolveTelegramReactionLevel({
cfg,
accountId: accountId ?? undefined,
});
if (!reactionLevelInfo.agentReactionsEnabled) {
throw new Error(
`Telegram agent reactions disabled (reactionLevel="${reactionLevelInfo.level}"). ` +
`Set channels.telegram.reactionLevel to "minimal" or "extensive" to enable.`,
);
}
// Also check the existing action gate for backward compatibility
if (!isActionEnabled("reactions")) {
throw new Error("Telegram reactions are disabled.");
throw new Error("Telegram reactions are disabled via actions.reactions.");
}
const chatId = readStringOrNumberParam(params, "chatId", {
required: true,