diff --git a/interfaces/admin/static/chat.js b/interfaces/admin/static/chat.js index 0f5f0b3c..9c168406 100644 --- a/interfaces/admin/static/chat.js +++ b/interfaces/admin/static/chat.js @@ -1547,10 +1547,12 @@ function hydrateMermaidIn(root) { const RE_REDACTED_THINKING = new RegExp("\\s*([\\s\\S]*?)\\s*", "i"); const RE_THINK_TAG = new RegExp("\\s*([\\s\\S]*?)\\s*", "i"); +const RE_THINKING_TAG = new RegExp("\\s*([\\s\\S]*?)\\s*", "i"); +const RE_THOUGHT_TAG = new RegExp("\\s*([\\s\\S]*?)\\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(); diff --git a/runtime/direct_loop.py b/runtime/direct_loop.py index 82b28569..43584e72 100644 --- a/runtime/direct_loop.py +++ b/runtime/direct_loop.py @@ -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*\s*", flags=re.IGNORECASE | re.DOTALL) +_THINK_BLOCK_RE = re.compile( + r"<\s*(?:antml:)?(think|thinking|thought|redacted_thinking)\s*>\s*(.*?)\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() diff --git a/tests/test_direct_loop_reasoning_split.py b/tests/test_direct_loop_reasoning_split.py new file mode 100644 index 00000000..14210415 --- /dev/null +++ b/tests/test_direct_loop_reasoning_split.py @@ -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("\ninner\n\nvisible") + assert chunks == ["inner"] + assert body == "visible" + + +def test_split_reasoning_extracts_thought_tags() -> None: + chunks, body = _split_reasoning_and_body("xafter") + 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 = "\nZ\n\nok" + chunks, body = _split_reasoning_and_body(raw) + assert chunks == ["Z"] + assert body == "ok"