- Eliminated all instances of the final_answer tool from the agent's configuration and logic, transitioning to a model where the agent concludes responses with plain text instead.
- Updated event handling and response analysis to reflect this change, ensuring that the agent's output is now directly delivered as text without relying on a dedicated final answer tool.
- Adjusted localization strings across multiple languages to remove references to final_answer, enhancing clarity and consistency in user-facing messages.
- Improved the overall structure of the agent's response handling to streamline the process and reduce potential confusion regarding tool usage.
refactor(agent): streamline answer handling and preamble logic
- Updated the agent's response handling to treat any text streamed before a tool call as a preamble, ensuring it is not included in the final answer.
- Removed the superseded marker logic from the event emission process, simplifying the flow of answer content and enhancing clarity in the UI.
- Adjusted the event handling to ensure that preamble segments are correctly retracted and relocated into the steps tree, improving the overall user experience.
- Enhanced the agent's internal logic to better manage answer segments and their states during streaming, leading to a more coherent response structure.
The natural-stop path on the backend (observe.go Case 1) emits the same
content twice: once as streaming thinking chunks (from
streamThinkingToEventBus's per-chunk callback) which the frontend merges
into a single thinking event, and once as one big EventAgentFinalAnswer.
The result is that the intermediate-steps tree contains a thinking card
whose content is identical to the answer card rendered below it.
Detect this duplication and hide the offending thinking event from the
tree:
- New utils `normaliseForComparison` and `thinkingEqualsAnswer` perform
a tolerant comparison: strip <think>…</think> blocks (which leak into
raw thinking chunks but not into the post-StripThinkBlocks answer),
strip final-answer wrappers, collapse whitespace, then exact-equal or
±2% length with matching head/tail.
- `hiddenThinkingEventIds` (replacing the previous single-id
`promotedThinkingEventId`) now collects two cases of duplicate
thinking: the legacy promotion fallback (no answer event present) and
the natural-stop content-match case.
The final_answer-tool path is unaffected because reasoning thinking
events ("我找到了…让我整理…") and the answer payload have different
content and don't match the equivalence check.
Many models (especially smaller or non-OpenAI-style ones) do not reliably
call the structured final_answer tool, even when the system prompt
explicitly requires it. Two visible symptoms in the agent UI:
1. The natural-stop path emits the answer as a final_answer event whose
content is wrapped in <answer>...</answer>, <final_answer>...</final_answer>,
or prefixed with "Final Answer:" / "最终答案:". These wrappers were
rendered verbatim into the answer card.
2. When no answer event arrives at all, the trailing thinking event was
surfaced via the legacy fallback but kept its collapsed "思考" card
styling instead of rendering as a proper answer.
This change adds a small unwrap helper and wires it through the answer
rendering path:
- New util `unwrapFinalAnswerWrappers` strips conservative outer wrappers
(XML-style answer tags, fenced ```final_answer code blocks, and leading
"Final Answer:" / "最终答案:" prefixes) only when they cover the entire
trimmed content, so user markdown that happens to contain similar text
is left alone.
- `renderAnswerContent` and `getActualContent` apply the unwrap before the
markdown renderer / clipboard copy, so users see and copy clean text.
- The legacy "promote trailing thinking to final answer" fallback now
synthesizes a virtual answer event so it renders with the answer card
UI (expanded markdown + copy/add toolbar) instead of the collapsed
thinking card. The promoted thinking event is hidden from the
intermediate-steps tree to avoid duplication.