mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:40:44 +00:00
fix(discord): show reasoning text in progress drafts (#78050)
* fix(discord): show reasoning text in progress drafts * fix(discord): handle reasoning progress snapshots * test: isolate usage-format models fixture
This commit is contained in:
committed by
GitHub
parent
01dda73e9b
commit
d94e7f5114
@@ -82,6 +82,8 @@ export function createDiscordDraftPreviewController(params: {
|
||||
});
|
||||
let previewToolProgressSuppressed = false;
|
||||
let previewToolProgressLines: string[] = [];
|
||||
let reasoningProgressRawText = "";
|
||||
let lastReasoningProgressLine: string | undefined;
|
||||
const progressSeed = `${params.accountId}:${params.deliverChannelId}`;
|
||||
|
||||
const renderProgressDraft = async (options?: { flush?: boolean }) => {
|
||||
@@ -116,6 +118,8 @@ export function createDiscordDraftPreviewController(params: {
|
||||
draftChunker?.reset();
|
||||
previewToolProgressSuppressed = false;
|
||||
previewToolProgressLines = [];
|
||||
reasoningProgressRawText = "";
|
||||
lastReasoningProgressLine = undefined;
|
||||
};
|
||||
|
||||
const forceNewMessageIfNeeded = () => {
|
||||
@@ -163,8 +167,11 @@ export function createDiscordDraftPreviewController(params: {
|
||||
return;
|
||||
}
|
||||
const normalized = line?.replace(/\s+/g, " ").trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
if (discordStreamMode !== "progress") {
|
||||
if (!previewToolProgressEnabled || previewToolProgressSuppressed || !normalized) {
|
||||
if (!previewToolProgressEnabled || previewToolProgressSuppressed) {
|
||||
return;
|
||||
}
|
||||
const previous = previewToolProgressLines.at(-1);
|
||||
@@ -200,6 +207,36 @@ export function createDiscordDraftPreviewController(params: {
|
||||
await renderProgressDraft();
|
||||
}
|
||||
},
|
||||
async pushReasoningProgress(text?: string) {
|
||||
if (!draftStream || discordStreamMode !== "progress" || !text) {
|
||||
return;
|
||||
}
|
||||
reasoningProgressRawText = mergeReasoningProgressText(reasoningProgressRawText, text);
|
||||
const normalized = normalizeReasoningProgressLine(reasoningProgressRawText);
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
if (previewToolProgressEnabled && !previewToolProgressSuppressed) {
|
||||
const priorIndex =
|
||||
lastReasoningProgressLine === undefined
|
||||
? -1
|
||||
: previewToolProgressLines.lastIndexOf(lastReasoningProgressLine);
|
||||
if (priorIndex >= 0) {
|
||||
previewToolProgressLines = [...previewToolProgressLines];
|
||||
previewToolProgressLines[priorIndex] = normalized;
|
||||
} else {
|
||||
previewToolProgressLines = [...previewToolProgressLines, normalized].slice(
|
||||
-resolveChannelProgressDraftMaxLines(params.discordConfig),
|
||||
);
|
||||
}
|
||||
lastReasoningProgressLine = normalized;
|
||||
}
|
||||
const alreadyStarted = progressDraftGate.hasStarted;
|
||||
await progressDraftGate.noteWork();
|
||||
if (alreadyStarted && progressDraftGate.hasStarted) {
|
||||
await renderProgressDraft();
|
||||
}
|
||||
},
|
||||
resolvePreviewFinalText(text?: string) {
|
||||
if (typeof text !== "string") {
|
||||
return undefined;
|
||||
@@ -329,3 +366,29 @@ export function createDiscordDraftPreviewController(params: {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeReasoningProgressLine(text: string): string {
|
||||
return text
|
||||
.replace(/^\s*(?:>\s*)?Reasoning:\s*/i, "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function mergeReasoningProgressText(current: string, incoming: string): string {
|
||||
if (!current) {
|
||||
return incoming;
|
||||
}
|
||||
const normalizedCurrent = normalizeReasoningProgressLine(current);
|
||||
const normalizedIncoming = normalizeReasoningProgressLine(incoming);
|
||||
if (!normalizedIncoming || normalizedIncoming === normalizedCurrent) {
|
||||
return current;
|
||||
}
|
||||
if (isReasoningSnapshotText(incoming) || normalizedIncoming.startsWith(normalizedCurrent)) {
|
||||
return incoming;
|
||||
}
|
||||
return `${current}${incoming}`;
|
||||
}
|
||||
|
||||
function isReasoningSnapshotText(text: string): boolean {
|
||||
return /^\s*(?:>\s*)?Reasoning:\s*/i.test(text);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ type DispatchInboundParams = {
|
||||
sendFinalReply: (payload: ReplyPayload) => boolean | Promise<boolean>;
|
||||
};
|
||||
replyOptions?: {
|
||||
onReasoningStream?: () => Promise<void> | void;
|
||||
onReasoningStream?: (payload?: { text?: string }) => Promise<void> | void;
|
||||
onReasoningEnd?: () => Promise<void> | void;
|
||||
onToolStart?: (payload: {
|
||||
name?: string;
|
||||
@@ -105,6 +105,7 @@ type DispatchInboundParams = {
|
||||
detailMode?: "explain" | "raw";
|
||||
}) => Promise<void> | void;
|
||||
onItemEvent?: (payload: {
|
||||
kind?: string;
|
||||
progressText?: string;
|
||||
summary?: string;
|
||||
title?: string;
|
||||
@@ -1616,6 +1617,72 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
expect(draftStream.update).toHaveBeenCalledWith("Shelling\n🛠️ Exec\n• done");
|
||||
});
|
||||
|
||||
it("shows reasoning text instead of a bare Reasoning progress line", async () => {
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
|
||||
await params?.replyOptions?.onItemEvent?.({
|
||||
kind: "analysis",
|
||||
title: "Reasoning",
|
||||
});
|
||||
await params?.replyOptions?.onReasoningStream?.({ text: "Reading " });
|
||||
await params?.replyOptions?.onReasoningStream?.({ text: "the event projector" });
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
discordConfig: {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: {
|
||||
label: "Clawing...",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(draftStream.update).toHaveBeenCalledWith(
|
||||
"Clawing...\n🛠️ Exec\n• Reading the event projector",
|
||||
);
|
||||
expect(draftStream.update).not.toHaveBeenCalledWith(expect.stringContaining("Reasoning"));
|
||||
});
|
||||
|
||||
it("replaces reasoning snapshots instead of appending duplicates", async () => {
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
|
||||
await params?.replyOptions?.onReasoningStream?.({ text: "Reasoning:\n_Checking files_" });
|
||||
await params?.replyOptions?.onReasoningStream?.({
|
||||
text: "Reasoning:\n_Checking files and tests_",
|
||||
});
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
const ctx = await createAutomaticSourceDeliveryContext({
|
||||
discordConfig: {
|
||||
streaming: {
|
||||
mode: "progress",
|
||||
progress: {
|
||||
label: "Clawing...",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(draftStream.update).toHaveBeenCalledWith(
|
||||
"Clawing...\n🛠️ Exec\n• _Checking files and tests_",
|
||||
);
|
||||
expect(draftStream.update).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("_Checking files_Reasoning:"),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps Discord progress lines across assistant boundaries", async () => {
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
|
||||
|
||||
@@ -660,8 +660,9 @@ export async function processDiscordMessage(
|
||||
onModelSelected,
|
||||
suppressDefaultToolProgressMessages:
|
||||
draftPreview.suppressDefaultToolProgressMessages ? true : undefined,
|
||||
onReasoningStream: async () => {
|
||||
onReasoningStream: async (payload) => {
|
||||
await statusReactions.setThinking();
|
||||
await draftPreview.pushReasoningProgress(payload?.text);
|
||||
},
|
||||
onToolStart: async (payload) => {
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
|
||||
Reference in New Issue
Block a user