mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(heartbeat): reuse the shared SDK reasoning classifier in the reply selector
This commit is contained in:
committed by
Vincent Koc
parent
8261858e09
commit
5885fbba0c
@@ -59,4 +59,15 @@ describe("resolveHeartbeatReplyPayload", () => {
|
||||
const blockquoted: ReplyPayload = { text: "Thinking... _weighing the options_" };
|
||||
expect(resolveHeartbeatReplyPayload(blockquoted)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("skips a trailing lowercase 'reasoning:' payload and returns the final answer", () => {
|
||||
const answer: ReplyPayload = { text: "All clear" };
|
||||
const lowercased: ReplyPayload = { text: "reasoning: because nothing changed" };
|
||||
expect(resolveHeartbeatReplyPayload([answer, lowercased])).toBe(answer);
|
||||
});
|
||||
|
||||
it("returns undefined for a Markdown blockquoted thinking payload", () => {
|
||||
const quoted: ReplyPayload = { text: "> thinking... _weighing the options_" };
|
||||
expect(resolveHeartbeatReplyPayload(quoted)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +1,20 @@
|
||||
// Heartbeat reply payload selector for multi-payload auto-reply results.
|
||||
import { hasOutboundReplyContent } from "openclaw/plugin-sdk/reply-payload";
|
||||
import {
|
||||
hasOutboundReplyContent,
|
||||
isReasoningReplyPayload,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
import type { ReplyPayload } from "./types.js";
|
||||
|
||||
// Formatted reasoning prefixes the heartbeat reasoning lane classifies as
|
||||
// reasoning even when the `isReasoning` flag is absent: legacy "Reasoning:"
|
||||
// text and blockquoted "Thinking..._". Kept in sync with
|
||||
// resolveHeartbeatReasoningPayloads (heartbeat-runner) so the selector skips
|
||||
// exactly the payloads the reasoning lane delivers separately.
|
||||
const FORMATTED_REASONING_PREFIX = /^(?:Reasoning:|Thinking\.{0,3}(?=\s*_))/u;
|
||||
|
||||
/** Whether text already carries a formatted reasoning prefix (legacy reasoning). */
|
||||
export function hasFormattedReasoningPrefix(text: string): boolean {
|
||||
return FORMATTED_REASONING_PREFIX.test(text.trimStart());
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a payload is reasoning (flagged or legacy-formatted) and so must not
|
||||
* become the user-visible heartbeat reply.
|
||||
*/
|
||||
export function isReasoningReplyPayload(payload: ReplyPayload): boolean {
|
||||
if (payload.isReasoning === true) {
|
||||
return true;
|
||||
}
|
||||
const text = typeof payload.text === "string" ? payload.text : "";
|
||||
return hasFormattedReasoningPrefix(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the last outbound-capable reply payload for heartbeat delivery.
|
||||
*
|
||||
* Reasoning payloads are skipped: heartbeat reasoning is delivered separately
|
||||
* and only when `includeReasoning` is enabled. Without this guard a trailing
|
||||
* reasoning payload (flagged via `isReasoning` or legacy "Reasoning:" /
|
||||
* blockquoted "Thinking" text, which reasoning models can emit after the final
|
||||
* answer) would be selected as the user-visible heartbeat reply.
|
||||
* Reasoning payloads are skipped using the shared SDK classifier
|
||||
* `isReasoningReplyPayload`, which recognizes the `isReasoning` flag plus the
|
||||
* common reasoning/thinking text prefixes (including lowercased and Markdown
|
||||
* blockquoted forms). Heartbeat reasoning is delivered separately and only when
|
||||
* `includeReasoning` is enabled; without this guard a trailing reasoning
|
||||
* payload (which reasoning models can emit after the final answer) would be
|
||||
* selected as the user-visible heartbeat reply.
|
||||
*/
|
||||
export function resolveHeartbeatReplyPayload(
|
||||
replyResult: ReplyPayload | ReplyPayload[] | undefined,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
hasOutboundReplyContent,
|
||||
isReasoningReplyPayload,
|
||||
resolveSendableOutboundReplyParts,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
import { normalizeOptionalAgentRuntimeId } from "../agents/agent-runtime-id.js";
|
||||
@@ -26,11 +27,7 @@ import { resolveAgentHarnessPolicy } from "../agents/harness/policy.js";
|
||||
import { resolveModelRefFromString, type ModelRef } from "../agents/model-selection.js";
|
||||
import { resolvePersistedSessionRuntimeId } from "../agents/session-runtime-compat.js";
|
||||
import { DEFAULT_HEARTBEAT_FILENAME } from "../agents/workspace.js";
|
||||
import {
|
||||
hasFormattedReasoningPrefix,
|
||||
isReasoningReplyPayload,
|
||||
resolveHeartbeatReplyPayload,
|
||||
} from "../auto-reply/heartbeat-reply-payload.js";
|
||||
import { resolveHeartbeatReplyPayload } from "../auto-reply/heartbeat-reply-payload.js";
|
||||
import {
|
||||
getHeartbeatToolNotificationText,
|
||||
resolveHeartbeatToolResponseFromReplyResult,
|
||||
@@ -757,6 +754,12 @@ function resolveStaleHeartbeatIsolatedSessionKey(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Display-format check (not a classifier): whether reasoning text is already
|
||||
// rendered in the heartbeat "Reasoning:" / "Thinking..._" form, so it should be
|
||||
// delivered as-is rather than re-wrapped by formatReasoningMessage. Reasoning
|
||||
// classification itself is the shared SDK `isReasoningReplyPayload`.
|
||||
const HEARTBEAT_REASONING_DISPLAY_PREFIX = /^(?:Reasoning:|Thinking\.{0,3}(?=\s*_))/u;
|
||||
|
||||
function resolveHeartbeatReasoningPayloads(
|
||||
replyResult: ReplyPayload | ReplyPayload[] | undefined,
|
||||
): ReplyPayload[] {
|
||||
@@ -771,7 +774,9 @@ function resolveHeartbeatReasoningPayloads(
|
||||
continue;
|
||||
}
|
||||
|
||||
const formattedText = hasFormattedReasoningPrefix(text) ? text : formatReasoningMessage(text);
|
||||
const formattedText = HEARTBEAT_REASONING_DISPLAY_PREFIX.test(text.trimStart())
|
||||
? text
|
||||
: formatReasoningMessage(text);
|
||||
if (!formattedText.trim()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user