mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 02:11:48 +00:00
fix(tui): strip <think>…</think> tags from assistant content and route to reasoning panel
Models that emit reasoning inline as <think>/<reasoning>/<thinking>/<thought>/
<REASONING_SCRATCHPAD> tags in the content field (rather than a separate API
reasoning channel) had the raw tags + inner content shown twice: once as body
text with literal <think> markers, and again in the thinking panel when the
reasoning field was populated.
Port v1's tag set to lib/reasoning.ts with a splitReasoning(text) helper that
returns { reasoning, text }. Applied in three spots:
- scheduleStreaming: strips tags from the live streaming view so the user
never sees <think> mid-turn.
- flushStreamingSegment: when a tool interrupts assistant output mid-turn,
the saved segment is the stripped text; extracted reasoning promotes to
reasoningText if the API channel hasn't already populated it.
- recordMessageComplete: final message text is split, extracted reasoning
merges with any existing reasoning (API channel wins on conflicts so we
don't double-count when both are present).
This commit is contained in:
parent
37cba82bfc
commit
4caf6c23dd
3 changed files with 127 additions and 8 deletions
50
ui-tui/src/__tests__/reasoning.test.ts
Normal file
50
ui-tui/src/__tests__/reasoning.test.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { hasReasoningTag, splitReasoning } from '../lib/reasoning.js'
|
||||
|
||||
describe('splitReasoning', () => {
|
||||
it('extracts <think>…</think> and strips it from text', () => {
|
||||
const { reasoning, text } = splitReasoning('<think>plotting</think>\n\nhere is the answer')
|
||||
|
||||
expect(reasoning).toBe('plotting')
|
||||
expect(text).toBe('here is the answer')
|
||||
})
|
||||
|
||||
it('handles multiple tag shapes', () => {
|
||||
const input = '<reasoning>a</reasoning> <THINKING>b</THINKING> <thought>c</thought> body'
|
||||
const { reasoning, text } = splitReasoning(input)
|
||||
|
||||
expect(reasoning).toContain('a')
|
||||
expect(reasoning).toContain('b')
|
||||
expect(reasoning).toContain('c')
|
||||
expect(text).toBe('body')
|
||||
})
|
||||
|
||||
it('treats unclosed trailing <think>… as reasoning', () => {
|
||||
const { reasoning, text } = splitReasoning('answer start <think>still deciding')
|
||||
|
||||
expect(reasoning).toBe('still deciding')
|
||||
expect(text).toBe('answer start')
|
||||
})
|
||||
|
||||
it('returns empty reasoning and untouched text when no tags present', () => {
|
||||
const { reasoning, text } = splitReasoning('plain body with no tags')
|
||||
|
||||
expect(reasoning).toBe('')
|
||||
expect(text).toBe('plain body with no tags')
|
||||
})
|
||||
|
||||
it('preserves text when reasoning block is empty', () => {
|
||||
const { reasoning, text } = splitReasoning('<think></think>only body')
|
||||
|
||||
expect(reasoning).toBe('')
|
||||
expect(text).toBe('only body')
|
||||
})
|
||||
|
||||
it('detects presence of any supported tag', () => {
|
||||
expect(hasReasoningTag('pre <think>x</think> post')).toBe(true)
|
||||
expect(hasReasoningTag('pre <reasoning>x</reasoning>')).toBe(true)
|
||||
expect(hasReasoningTag('<REASONING_SCRATCHPAD>x</REASONING_SCRATCHPAD>')).toBe(true)
|
||||
expect(hasReasoningTag('no tags at all')).toBe(false)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue