mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:10:58 +00:00
Add opt-in reaction tool tracking
This commit is contained in:
committed by
Peter Steinberger
parent
c40f89414c
commit
788cff1df4
@@ -1,4 +1,4 @@
|
||||
import { DEFAULT_EMOJIS } from "openclaw/plugin-sdk/channel-feedback";
|
||||
import { DEFAULT_EMOJIS, DEFAULT_TIMING } from "openclaw/plugin-sdk/channel-feedback";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js";
|
||||
@@ -87,7 +87,11 @@ type DispatchInboundParams = {
|
||||
replyOptions?: {
|
||||
onReasoningStream?: () => Promise<void> | void;
|
||||
onReasoningEnd?: () => Promise<void> | void;
|
||||
onToolStart?: (payload: { name?: string }) => Promise<void> | void;
|
||||
onToolStart?: (payload: {
|
||||
name?: string;
|
||||
phase?: string;
|
||||
args?: Record<string, unknown>;
|
||||
}) => Promise<void> | void;
|
||||
onItemEvent?: (payload: {
|
||||
progressText?: string;
|
||||
summary?: string;
|
||||
@@ -585,7 +589,7 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
it("debounces intermediate phase reactions and jumps to done for short runs", async () => {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onReasoningStream?.();
|
||||
await params?.replyOptions?.onToolStart?.({ name: "exec" });
|
||||
await params?.replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
@@ -600,6 +604,39 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
expect(emojis).not.toContain(DEFAULT_EMOJIS.coding);
|
||||
});
|
||||
|
||||
it("can bind status reactions to an explicitly tracked reaction target", async () => {
|
||||
vi.useFakeTimers();
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onToolStart?.({
|
||||
name: "message",
|
||||
phase: "start",
|
||||
args: {
|
||||
action: "react",
|
||||
channelId: "c1",
|
||||
messageId: "m1",
|
||||
emoji: "📈",
|
||||
trackToolCalls: true,
|
||||
},
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
cfg: { messages: { ackReaction: "👀" } },
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const calls = sendMocks.reactMessageDiscord.mock.calls as unknown as Array<
|
||||
[string, string, string]
|
||||
>;
|
||||
expect(calls).toContainEqual(expect.arrayContaining(["c1", "m1", "📈"]));
|
||||
expect(calls).toContainEqual(expect.arrayContaining(["c1", "m1", "✉️"]));
|
||||
expect(calls).toContainEqual(expect.arrayContaining(["c1", "m1", DEFAULT_EMOJIS.done]));
|
||||
});
|
||||
|
||||
it("shows stall emojis for long no-progress runs", async () => {
|
||||
vi.useFakeTimers();
|
||||
let releaseDispatch!: () => void;
|
||||
|
||||
@@ -82,6 +82,21 @@ type DiscordMessageProcessObserver = {
|
||||
onReplyPlanResolved?: (params: { createdThreadId?: string; sessionKey?: string }) => void;
|
||||
};
|
||||
|
||||
type ToolStartPayload = {
|
||||
name?: string;
|
||||
phase?: string;
|
||||
args?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function readToolStringArg(args: Record<string, unknown>, key: string): string | undefined {
|
||||
const value = args[key];
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function readToolBooleanArg(args: Record<string, unknown>, key: string): boolean {
|
||||
return args[key] === true;
|
||||
}
|
||||
|
||||
export async function processDiscordMessage(
|
||||
ctx: DiscordMessagePreflightContext,
|
||||
observer?: DiscordMessageProcessObserver,
|
||||
@@ -203,7 +218,9 @@ export async function processDiscordMessage(
|
||||
messageId: message.id,
|
||||
reactionContext: ackReactionContext,
|
||||
});
|
||||
const statusReactions = createStatusReactionController({
|
||||
let statusReactionTarget = `${messageChannelId}/${message.id}`;
|
||||
let statusReactionsActive = statusReactionsEnabled;
|
||||
let statusReactions = createStatusReactionController({
|
||||
enabled: statusReactionsEnabled,
|
||||
adapter: discordAdapter,
|
||||
initialEmoji: ackReaction,
|
||||
@@ -213,11 +230,67 @@ export async function processDiscordMessage(
|
||||
logAckFailure({
|
||||
log: logVerbose,
|
||||
channel: "discord",
|
||||
target: `${messageChannelId}/${message.id}`,
|
||||
target: statusReactionTarget,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
});
|
||||
const maybeBindStatusReactionsToToolReaction = (payload: ToolStartPayload) => {
|
||||
if (
|
||||
sourceRepliesAreToolOnly ||
|
||||
cfg.messages?.statusReactions?.enabled === false ||
|
||||
payload.phase !== "start" ||
|
||||
payload.name !== "message" ||
|
||||
!payload.args
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const args = payload.args;
|
||||
const action = readToolStringArg(args, "action")?.toLowerCase();
|
||||
if (action !== "react") {
|
||||
return;
|
||||
}
|
||||
const shouldTrack =
|
||||
readToolBooleanArg(args, "trackToolCalls") || readToolBooleanArg(args, "track_tool_calls");
|
||||
if (!shouldTrack) {
|
||||
return;
|
||||
}
|
||||
const emoji = readToolStringArg(args, "emoji");
|
||||
const remove = readToolBooleanArg(args, "remove");
|
||||
if (!emoji || remove) {
|
||||
return;
|
||||
}
|
||||
const trackedMessageId =
|
||||
readToolStringArg(args, "messageId") ?? readToolStringArg(args, "message_id") ?? message.id;
|
||||
const trackedChannelId =
|
||||
readToolStringArg(args, "channelId") ?? readToolStringArg(args, "to") ?? messageChannelId;
|
||||
statusReactionTarget = `${trackedChannelId}/${trackedMessageId}`;
|
||||
if (statusReactionsActive) {
|
||||
void statusReactions.clear();
|
||||
}
|
||||
const trackedAdapter = createDiscordAckReactionAdapter({
|
||||
channelId: trackedChannelId,
|
||||
messageId: trackedMessageId,
|
||||
reactionContext: ackReactionContext,
|
||||
});
|
||||
statusReactions = createStatusReactionController({
|
||||
enabled: true,
|
||||
adapter: trackedAdapter,
|
||||
initialEmoji: emoji,
|
||||
emojis: cfg.messages?.statusReactions?.emojis,
|
||||
timing: cfg.messages?.statusReactions?.timing,
|
||||
onError: (err) => {
|
||||
logAckFailure({
|
||||
log: logVerbose,
|
||||
channel: "discord",
|
||||
target: statusReactionTarget,
|
||||
error: err,
|
||||
});
|
||||
},
|
||||
});
|
||||
statusReactionsActive = true;
|
||||
void statusReactions.setQueued();
|
||||
};
|
||||
queueInitialDiscordAckReaction({
|
||||
enabled: statusReactionsEnabled,
|
||||
shouldSendAckReaction,
|
||||
@@ -546,6 +619,7 @@ export async function processDiscordMessage(
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
maybeBindStatusReactionsToToolReaction(payload);
|
||||
await statusReactions.setTool(payload.name);
|
||||
draftPreview.pushToolProgress(
|
||||
payload.name ? `tool: ${payload.name}` : "tool running",
|
||||
@@ -632,7 +706,7 @@ export async function processDiscordMessage(
|
||||
markDispatchIdle();
|
||||
}
|
||||
}
|
||||
if (statusReactionsEnabled) {
|
||||
if (statusReactionsActive) {
|
||||
if (dispatchAborted) {
|
||||
if (removeAckAfterReply) {
|
||||
void statusReactions.clear();
|
||||
|
||||
Reference in New Issue
Block a user