refactor(reasoning): make channel plugins own reasoning rendering
Reasoning was being shipped to every channel as a generic progress message with a `_reasoning: true` flag. Two problems with that: 1. Channels without a low-emphasis UI primitive (Telegram, Slack, Discord, Feishu...) would dump raw model thoughts as ordinary replies, polluting the conversation. 2. The agent loop double-gated by inspecting `channels_config`, which coupled the loop to display policy. Treat reasoning as its own plugin action — `BaseChannel.send_reasoning` defaults to a documented no-op; channels that have a fitting affordance override. ChannelManager routes `_reasoning` outbounds to that method only when the channel opts in via `show_reasoning` (camelCase alias `showReasoning` mirrors `sendProgress`). Plugins that don't override silently drop reasoning — "no fit, no leak" is the contract. Reference implementation lands for WebSocket / WebUI: a new `kind: "reasoning"` frame, parked on the active assistant bubble as a collapsible `Thinking` group above the answer. CLI keeps its existing direct path (it doesn't go through the bus). `ChannelsConfig.show_reasoning` flips to `true` by default — only adapted channels surface anything, others stay quiet. Loop net diff is -3 lines: the `channels_config.show_reasoning` check moves out, leaving emit_reasoning a one-liner that publishes and trusts the channel to decide. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -183,10 +183,43 @@ export function useNanobotStream(
|
||||
if (ev.event === "message") {
|
||||
if (
|
||||
suppressStreamUntilTurnEndRef.current &&
|
||||
(ev.kind === "tool_hint" || ev.kind === "progress")
|
||||
(ev.kind === "tool_hint" || ev.kind === "progress" || ev.kind === "reasoning")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Model reasoning rides its own channel: stash it on the next
|
||||
// assistant turn so the bubble renders it as a subordinate trace.
|
||||
// If the assistant message hasn't materialized yet (typical, since
|
||||
// reasoning fires before tool calls/answers), park it on a sentinel
|
||||
// pending row that the next assistant message absorbs.
|
||||
if (ev.kind === "reasoning") {
|
||||
const line = ev.text;
|
||||
if (!line) return;
|
||||
setMessages((prev) => {
|
||||
for (let i = prev.length - 1; i >= 0; i -= 1) {
|
||||
const candidate = prev[i];
|
||||
if (candidate.role === "assistant" && candidate.kind !== "trace") {
|
||||
const merged: UIMessage = {
|
||||
...candidate,
|
||||
reasoning: [...(candidate.reasoning ?? []), line],
|
||||
};
|
||||
return [...prev.slice(0, i), merged, ...prev.slice(i + 1)];
|
||||
}
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
role: "assistant",
|
||||
content: "",
|
||||
isStreaming: true,
|
||||
reasoning: [line],
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
];
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Intermediate agent breadcrumbs (tool-call hints, raw progress).
|
||||
// Attach them to the last trace row if it was the last emitted item
|
||||
// so a sequence of calls collapses into one compact trace group.
|
||||
|
||||
Reference in New Issue
Block a user