fix(buzz-agent): promote reasoning_content to visible text when content is empty - #5819
fix(buzz-agent): promote reasoning_content to visible text when content is empty#5819csh0101 wants to merge 1 commit into
Conversation
…nt is empty DeepSeek and other reasoning models sometimes put the entire answer in reasoning_content while leaving content empty (or with a gateway placeholder like '[System: Empty message content sanitised...]'). The parser extracted text='' and reasoning='answer', so agent_message_chunk was never emitted (gated on !text.is_empty()), and the turn ended silently with end_turn — no error, no warning, just an empty reply. Changes: 1. llm.rs: openai_content_parts() — recognize the gateway placeholder prefix as empty content, so reasoning_content is promoted to visible text. 2. llm.rs: parse_openai() — after extracting text+reasoning, if text is empty and reasoning is non-empty, swap them (reasoning→text, text→reasoning). This makes the model's answer visible to the agent's publish path. 3. llm.rs: parse_responses() — same promotion for the Responses API route. 4. llm.rs: parse_anthropic() — same promotion for Anthropic extended thinking. 5. agent.rs: Add warn-level log when end_turn fires with no publish attempt and require_reply is enabled, giving operators a signal in the logs. 6. llm.rs tests: 7 new tests covering reasoning_content promotion (empty content + gateway placeholder), empty end_turn (allowed), no promotion when content present, empty text with tool calls, Responses API reasoning promotion, and Responses API empty end_turn. Signed-off-by: AI Agent <agent@example.com> Signed-off-by: cs <shi.chen@robotics.cc>
|
promoting the two |
Chessing234
left a comment
There was a problem hiding this comment.
the promotion isn't gated on the round having no tool call. parse_openai does the swap before tool_calls are parsed, and agent.rs:638 emits agent_message_chunk whenever text is non-empty regardless of tool_calls — so an r1-style model returning content:"" + reasoning_content:"" + a buzz_reply tool call (the normal shape for those models doing tool use) would publish its chain of thought as the visible message and then publish the real reply from the tool. requiring tool_calls.is_empty() before the swap keeps the fix to the case you actually hit.
the anthropic hunk has the same shape but bites harder: empty text with non-empty thinking is mostly the max_tokens / thinking-budget case, and agent.rs:660 pushes response.text into history as the assistant message when stop is MaxTokens, so a truncated chain of thought becomes the turn's answer. that hunk also has no test — the seven listed are all parse_openai/parse_responses.
Problem
When using OpenAI-compatible reasoning models (e.g. DeepSeek, etc.), the model often puts the entire answer in
reasoning_contentwhile leavingcontentempty (or with a gateway placeholder like[System: Empty message content sanitised...]). Theparse_openaifunction extractedtext=""andreasoning="answer", butagent_message_chunkwas never emitted (gated on!text.is_empty()), so the turn ended silently withend_turn— no error, no warning, just an empty reply.The user sees the agent "turn" complete normally, but the channel gets no message.
Root Cause
parse_openaiincrates/buzz-agent/src/llm.rsdid not check whetherreasoning_contentshould be promoted to visible text whencontentis empty. The same gap existed inparse_responses(Responses API route) andparse_anthropic(Anthropic extended thinking).Fix
openai_content_parts(): Recognize a gateway placeholder prefix ([System: Empty message content sanitised...]) as empty content, soreasoning_contentis promoted to visible text.parse_openai(): After extractingtext+reasoning, iftextis empty andreasoningis non-empty, swap them so the model's answer reaches the agent's publish path.parse_responses(): Same promotion logic for the Responses API route.parse_anthropic(): Same promotion logic for Anthropic extended thinking blocks.agent.rs: Addedwarn!-level logging whenend_turnfires with no publish attempt andrequire_replyis enabled, giving operators a signal in the logs to detect silent drops.Tests
7 new tests covering:
parse_openai_promotes_reasoning_content_to_text_when_content_empty— emptycontentwithreasoning_content→ text promotedparse_openai_promotes_reasoning_content_when_content_is_gateway_placeholder— gateway placeholder recognized as emptyparse_openai_empty_end_turn_with_no_reasoning_is_warned— genuinely empty end_turn is allowed (model may choose silence)parse_openai_does_not_promote_reasoning_when_content_present— normal case: both surviveparse_openai_does_not_reject_empty_end_turn_with_tool_calls— tool calls with empty text is validparse_responses_promotes_reasoning_to_text_when_text_empty— Responses API reasoning promotionparse_responses_empty_end_turn_with_no_reasoning_is_ok— Responses API empty end_turn allowedAll 484 unit tests pass.
Local Reproduction