refactor(reasoning): unify reasoning extraction across providers
Reasoning surfacing was split across three branches in runner.py plus two separate streaming buffers (loop hook and runner progress stream), with three independent display-side gates in the CLI. This collapsed the policy into one source of truth and fixed two real bugs: - Structured `reasoning_content` was suppressed whenever the answer was streamed, because the runner gated emission on `streamed_content`. Providers don't stream `reasoning_content`; it only arrives on the final response, so the answer stream and the reasoning channel are independent. Added `streamed_reasoning` to `AgentHookContext` to track the right bit. - `channels.showReasoning` was subordinated to `sendProgress`. They are orthogonal — turning off progress streaming shouldn't silence reasoning. Reworked the CLI gates accordingly. Single-helper consolidation: - `extract_reasoning(reasoning_content, thinking_blocks, content)` returns `(reasoning_text, cleaned_content)` with a defined fallback order: dedicated field → Anthropic thinking_blocks → inline `<think>`/`<thought>` tags. Models that expose none of these short-circuit to `(None, content)` — zero overhead. - `IncrementalThinkExtractor` replaces the ad-hoc `emit_incremental_think` function and its hand-rolled "emitted cursor" state in both the loop hook and the runner progress stream. Also documented the new `showReasoning` channel option in docs/configuration.md and noted its independence from sendProgress. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+15
-25
@@ -17,11 +17,11 @@ from nanobot.agent.tools.ask import AskUserInterrupt
|
||||
from nanobot.agent.tools.registry import ToolRegistry
|
||||
from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest
|
||||
from nanobot.utils.helpers import (
|
||||
IncrementalThinkExtractor,
|
||||
build_assistant_message,
|
||||
emit_incremental_think,
|
||||
estimate_message_tokens,
|
||||
estimate_prompt_tokens_chain,
|
||||
extract_think,
|
||||
extract_reasoning,
|
||||
find_legal_message_start,
|
||||
maybe_persist_tool_result,
|
||||
strip_think,
|
||||
@@ -284,24 +284,15 @@ class AgentRunner:
|
||||
context.tool_calls = list(response.tool_calls)
|
||||
self._accumulate_usage(usage, raw_usage)
|
||||
|
||||
if response.reasoning_content:
|
||||
if not context.streamed_content:
|
||||
await hook.emit_reasoning(response.reasoning_content)
|
||||
if response.content:
|
||||
response.content = strip_think(response.content)
|
||||
elif response.thinking_blocks:
|
||||
# Anthropic extended thinking: extract from thinking_blocks.
|
||||
if not context.streamed_content:
|
||||
parts = [tb.get("thinking", "") for tb in response.thinking_blocks if tb.get("type") == "thinking"]
|
||||
if parts:
|
||||
await hook.emit_reasoning("\n\n".join(parts))
|
||||
elif response.content:
|
||||
inline_thinking, clean_content = extract_think(response.content)
|
||||
if inline_thinking:
|
||||
# Only emit if streaming didn't already handle it.
|
||||
if not context.streamed_content:
|
||||
await hook.emit_reasoning(inline_thinking)
|
||||
response.content = clean_content
|
||||
reasoning_text, cleaned_content = extract_reasoning(
|
||||
response.reasoning_content,
|
||||
response.thinking_blocks,
|
||||
response.content,
|
||||
)
|
||||
response.content = cleaned_content
|
||||
if reasoning_text and not context.streamed_reasoning:
|
||||
await hook.emit_reasoning(reasoning_text)
|
||||
context.streamed_reasoning = True
|
||||
|
||||
if response.should_execute_tools:
|
||||
tool_calls = list(response.tool_calls)
|
||||
@@ -654,10 +645,10 @@ class AgentRunner:
|
||||
)
|
||||
elif wants_progress_streaming:
|
||||
stream_buf = ""
|
||||
emitted_thinking = ""
|
||||
think_extractor = IncrementalThinkExtractor()
|
||||
|
||||
async def _stream_progress(delta: str) -> None:
|
||||
nonlocal stream_buf, emitted_thinking
|
||||
nonlocal stream_buf
|
||||
if not delta:
|
||||
return
|
||||
prev_clean = strip_think(stream_buf)
|
||||
@@ -665,9 +656,8 @@ class AgentRunner:
|
||||
new_clean = strip_think(stream_buf)
|
||||
incremental = new_clean[len(prev_clean):]
|
||||
|
||||
emitted_thinking = await emit_incremental_think(
|
||||
stream_buf, emitted_thinking, hook.emit_reasoning,
|
||||
)
|
||||
if await think_extractor.feed(stream_buf, hook.emit_reasoning):
|
||||
context.streamed_reasoning = True
|
||||
|
||||
if incremental:
|
||||
context.streamed_content = True
|
||||
|
||||
Reference in New Issue
Block a user