openclaw/extensions/telegram/src/reasoning-lane-coordinator.ts
2026-03-16 21:16:32 -07:00

137 lines
3.6 KiB
TypeScript

import { formatReasoningMessage } from "openclaw/plugin-sdk/agent-runtime";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
import { findCodeRegions, isInsideCode } from "openclaw/plugin-sdk/text-runtime";
import { stripReasoningTagsFromText } from "openclaw/plugin-sdk/text-runtime";
const REASONING_MESSAGE_PREFIX = "Reasoning:\n";
const REASONING_TAG_PREFIXES = [
"<think",
"<thinking",
"<thought",
"<antthinking",
"</think",
"</thinking",
"</thought",
"</antthinking",
];
const THINKING_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought|antthinking)\b[^<>]*>/gi;
function extractThinkingFromTaggedStreamOutsideCode(text: string): string {
if (!text) {
return "";
}
const codeRegions = findCodeRegions(text);
let result = "";
let lastIndex = 0;
let inThinking = false;
THINKING_TAG_RE.lastIndex = 0;
for (const match of text.matchAll(THINKING_TAG_RE)) {
const idx = match.index ?? 0;
if (isInsideCode(idx, codeRegions)) {
continue;
}
if (inThinking) {
result += text.slice(lastIndex, idx);
}
const isClose = match[1] === "/";
inThinking = !isClose;
lastIndex = idx + match[0].length;
}
if (inThinking) {
result += text.slice(lastIndex);
}
return result.trim();
}
function isPartialReasoningTagPrefix(text: string): boolean {
const trimmed = text.trimStart().toLowerCase();
if (!trimmed.startsWith("<")) {
return false;
}
if (trimmed.includes(">")) {
return false;
}
return REASONING_TAG_PREFIXES.some((prefix) => prefix.startsWith(trimmed));
}
export type TelegramReasoningSplit = {
reasoningText?: string;
answerText?: string;
};
export function splitTelegramReasoningText(text?: string): TelegramReasoningSplit {
if (typeof text !== "string") {
return {};
}
const trimmed = text.trim();
if (isPartialReasoningTagPrefix(trimmed)) {
return {};
}
if (
trimmed.startsWith(REASONING_MESSAGE_PREFIX) &&
trimmed.length > REASONING_MESSAGE_PREFIX.length
) {
return { reasoningText: trimmed };
}
const taggedReasoning = extractThinkingFromTaggedStreamOutsideCode(text);
const strippedAnswer = stripReasoningTagsFromText(text, { mode: "strict", trim: "both" });
if (!taggedReasoning && strippedAnswer === text) {
return { answerText: text };
}
const reasoningText = taggedReasoning ? formatReasoningMessage(taggedReasoning) : undefined;
const answerText = strippedAnswer || undefined;
return { reasoningText, answerText };
}
export type BufferedFinalAnswer = {
payload: ReplyPayload;
text: string;
};
export function createTelegramReasoningStepState() {
let reasoningStatus: "none" | "hinted" | "delivered" = "none";
let bufferedFinalAnswer: BufferedFinalAnswer | undefined;
const noteReasoningHint = () => {
if (reasoningStatus === "none") {
reasoningStatus = "hinted";
}
};
const noteReasoningDelivered = () => {
reasoningStatus = "delivered";
};
const shouldBufferFinalAnswer = () => {
return reasoningStatus === "hinted" && !bufferedFinalAnswer;
};
const bufferFinalAnswer = (value: BufferedFinalAnswer) => {
bufferedFinalAnswer = value;
};
const takeBufferedFinalAnswer = (): BufferedFinalAnswer | undefined => {
const value = bufferedFinalAnswer;
bufferedFinalAnswer = undefined;
return value;
};
const resetForNextStep = () => {
reasoningStatus = "none";
bufferedFinalAnswer = undefined;
};
return {
noteReasoningHint,
noteReasoningDelivered,
shouldBufferFinalAnswer,
bufferFinalAnswer,
takeBufferedFinalAnswer,
resetForNextStep,
};
}