fix(chat): persist/split <thinking>/<thought> blocks like stream UI

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-05-12 19:32:19 +08:00
parent 7ea02e66ca
commit 46ebfeb54f
3 changed files with 36 additions and 3 deletions

View file

@ -1547,10 +1547,12 @@ function hydrateMermaidIn(root) {
const RE_REDACTED_THINKING = new RegExp("<redacted_thinking>\\s*([\\s\\S]*?)\\s*</redacted_thinking>", "i");
const RE_THINK_TAG = new RegExp("<think\\s*>\\s*([\\s\\S]*?)\\s*</think\\s*>", "i");
const RE_THINKING_TAG = new RegExp("<thinking\\s*>\\s*([\\s\\S]*?)\\s*</thinking\\s*>", "i");
const RE_THOUGHT_TAG = new RegExp("<thought\\s*>\\s*([\\s\\S]*?)\\s*</thought\\s*>", "i");
function _findEarliestReasoningBlock(remaining) {
let best = null;
for (const re of [RE_REDACTED_THINKING, RE_THINK_TAG]) {
for (const re of [RE_REDACTED_THINKING, RE_THINK_TAG, RE_THINKING_TAG, RE_THOUGHT_TAG]) {
const m = re.exec(remaining);
re.lastIndex = 0;
if (!m) continue;
@ -1657,7 +1659,7 @@ function extractThinkingFromText(text) {
const raw = String(text || "");
if (!raw) return "";
const blocks = [];
for (const re of [RE_REDACTED_THINKING, RE_THINK_TAG]) {
for (const re of [RE_REDACTED_THINKING, RE_THINK_TAG, RE_THINKING_TAG, RE_THOUGHT_TAG]) {
const rr = new RegExp(re.source, "gi");
for (const m of raw.matchAll(rr)) {
const inner = String(m[1] || "").trim();

View file

@ -35,7 +35,10 @@ _DIRECT_LOOP_OC_STAGE: dict[str, str] = {
"tool_result_context_guard": "tool_context_guard",
"tool_pairing_guard": "tool_pairing_guard",
}
_THINK_BLOCK_RE = re.compile(r"<(think|redacted_thinking)>\s*(.*?)\s*</\1>\s*", flags=re.IGNORECASE | re.DOTALL)
_THINK_BLOCK_RE = re.compile(
r"<\s*(?:antml:)?(think|thinking|thought|redacted_thinking)\s*>\s*(.*?)\s*</\s*(?:antml:)?\1\s*>",
flags=re.IGNORECASE | re.DOTALL,
)
_DSML_INVOKE_NAME_RE = re.compile(r"invoke\s+name\s*=\s*['\"]([^'\"\s>]+)['\"]", flags=re.IGNORECASE)
_JSON_TOOL_NAME_RE = re.compile(r"['\"]name['\"]\s*:\s*['\"]([^'\"\s]{1,120})['\"]", flags=re.IGNORECASE)
_TOOL_WIRE_CACHE_LOCK = threading.Lock()

View file

@ -0,0 +1,28 @@
from __future__ import annotations
from oclaw.runtime.direct_loop import _split_reasoning_and_body
def test_split_reasoning_extracts_thinking_tags() -> None:
chunks, body = _split_reasoning_and_body("<thinking>\ninner\n</thinking>\nvisible")
assert chunks == ["inner"]
assert body == "visible"
def test_split_reasoning_extracts_thought_tags() -> None:
chunks, body = _split_reasoning_and_body("<thought>x</thought>after")
assert chunks == ["x"]
assert body == "after"
def test_split_reasoning_explicit_when_no_tags() -> None:
chunks, body = _split_reasoning_and_body("body only", explicit_reasoning=" rc ")
assert chunks == ["rc"]
assert body == "body only"
def test_split_reasoning_antml_optional_prefix() -> None:
raw = "<antml:thinking>\nZ\n</antml:thinking>\nok"
chunks, body = _split_reasoning_and_body(raw)
assert chunks == ["Z"]
assert body == "ok"