mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-22 16:25:58 +00:00
fmt(js): npm run fix on merge (#68135)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
470c7e2a60
commit
aa274364bb
7 changed files with 42 additions and 30 deletions
|
|
@ -409,6 +409,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) {
|
|||
if (sessionId) {
|
||||
flushQueuedDeltas(sessionId)
|
||||
const text = coerceGatewayText(payload?.text)
|
||||
|
||||
if (text) {
|
||||
finalizeInterimAssistantMessage(sessionId, text)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -379,6 +379,7 @@ export function useMessageStream({
|
|||
}
|
||||
|
||||
const authoritativeText = renderMediaTags(text).trim()
|
||||
|
||||
if (!authoritativeText) {
|
||||
return state
|
||||
}
|
||||
|
|
@ -386,29 +387,30 @@ export function useMessageStream({
|
|||
const streamId = state.streamId
|
||||
|
||||
const replaceTextPart = (parts: ChatMessagePart[]) => {
|
||||
const visibleText = stripGeneratedImageEchoes(
|
||||
authoritativeText, generatedImageEchoSources(parts)
|
||||
).trim()
|
||||
const visibleText = stripGeneratedImageEchoes(authoritativeText, generatedImageEchoSources(parts)).trim()
|
||||
|
||||
return mergeFinalAssistantText(parts, visibleText)
|
||||
}
|
||||
|
||||
let nextMessages = state.messages
|
||||
|
||||
if (streamId && nextMessages.some(m => m.id === streamId)) {
|
||||
// Finalize the existing streaming bubble in place
|
||||
nextMessages = nextMessages.map(m =>
|
||||
m.id === streamId
|
||||
? { ...m, parts: replaceTextPart(m.parts), pending: false }
|
||||
: m
|
||||
m.id === streamId ? { ...m, parts: replaceTextPart(m.parts), pending: false } : m
|
||||
)
|
||||
} else {
|
||||
// No streaming bubble — create a standalone interim message
|
||||
nextMessages = [...nextMessages, {
|
||||
id: `assistant-interim-${Date.now()}`,
|
||||
role: 'assistant' as const,
|
||||
parts: [assistantTextPart(authoritativeText)],
|
||||
pending: false,
|
||||
branchGroupId: state.pendingBranchGroup ?? undefined
|
||||
}]
|
||||
nextMessages = [
|
||||
...nextMessages,
|
||||
{
|
||||
id: `assistant-interim-${Date.now()}`,
|
||||
role: 'assistant' as const,
|
||||
parts: [assistantTextPart(authoritativeText)],
|
||||
pending: false,
|
||||
branchGroupId: state.pendingBranchGroup ?? undefined
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -451,6 +453,7 @@ export function useMessageStream({
|
|||
|
||||
const replaceTextPart = (parts: ChatMessagePart[]) => {
|
||||
const visibleFinalText = stripGeneratedImageEchoes(finalText, generatedImageEchoSources(parts)).trim()
|
||||
|
||||
return mergeFinalAssistantText(parts, visibleFinalText)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { useEffect, useRef } from 'react'
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ClientSessionState } from '@/app/types'
|
||||
import { createClientSessionState } from '@/lib/chat-runtime'
|
||||
import { chatMessageText } from '@/lib/chat-messages'
|
||||
import { $todosBySession, clearSessionTodos, setSessionTodos } from '@/store/todos'
|
||||
import { createClientSessionState } from '@/lib/chat-runtime'
|
||||
import { clearSessionTodos } from '@/store/todos'
|
||||
import type { RpcEvent } from '@/types/hermes'
|
||||
|
||||
import { useMessageStream } from './index'
|
||||
|
|
@ -55,10 +55,13 @@ async function mountStream() {
|
|||
|
||||
const start = () => act(() => handleEvent!({ payload: {}, session_id: SID, type: 'message.start' }))
|
||||
const delta = (text: string) => act(() => handleEvent!({ payload: { text }, session_id: SID, type: 'message.delta' }))
|
||||
|
||||
const interim = (text: string) =>
|
||||
act(() => handleEvent!({ payload: { text, already_streamed: true }, session_id: SID, type: 'message.interim' }))
|
||||
|
||||
const complete = (text: string) =>
|
||||
act(() => handleEvent!({ payload: { text }, session_id: SID, type: 'message.complete' }))
|
||||
|
||||
const completePreviewed = (text: string) =>
|
||||
act(() => handleEvent!({ payload: { text, response_previewed: true }, session_id: SID, type: 'message.complete' }))
|
||||
|
||||
|
|
@ -69,11 +72,13 @@ function getState(): ClientSessionState {
|
|||
function assistantText(): string {
|
||||
const state = getState()
|
||||
const last = [...state.messages].reverse().find(m => m.role === 'assistant' && !m.hidden)
|
||||
|
||||
return last ? chatMessageText(last) : ''
|
||||
}
|
||||
|
||||
function assistantMessages(): string[] {
|
||||
const state = getState()
|
||||
|
||||
return state.messages
|
||||
.filter(m => m.role === 'assistant' && !m.hidden)
|
||||
.map(m => chatMessageText(m))
|
||||
|
|
@ -222,7 +227,9 @@ describe('useMessageStream interim text sealing', () => {
|
|||
// Empty text
|
||||
await act(() => handleEvent!({ payload: { text: '' }, session_id: SID, type: 'message.interim' } as RpcEvent))
|
||||
// Undefined text
|
||||
await act(() => handleEvent!({ payload: { text: undefined }, session_id: SID, type: 'message.interim' } as RpcEvent))
|
||||
await act(() =>
|
||||
handleEvent!({ payload: { text: undefined }, session_id: SID, type: 'message.interim' } as RpcEvent)
|
||||
)
|
||||
|
||||
// Turn continues without finalizing or throwing
|
||||
expect(getState().busy).toBe(true)
|
||||
|
|
|
|||
|
|
@ -804,10 +804,7 @@ describe('mergeFinalAssistantText', () => {
|
|||
})
|
||||
|
||||
it('drops reasoning that the final text fully covers (reasoning ⊆ final)', () => {
|
||||
const parts = [
|
||||
reasoningPart('Let me check the files.'),
|
||||
{ type: 'text' as const, text: 'streamed' }
|
||||
]
|
||||
const parts = [reasoningPart('Let me check the files.'), { type: 'text' as const, text: 'streamed' }]
|
||||
|
||||
const result = mergeFinalAssistantText(parts, 'Let me check the files. Everything looks good.')
|
||||
|
||||
|
|
@ -819,7 +816,9 @@ describe('mergeFinalAssistantText', () => {
|
|||
// #61447: a short final ("Done.") must NOT swallow a longer reasoning block
|
||||
// that merely starts with it.
|
||||
const parts = [
|
||||
reasoningPart('Done. The root cause was a bare catch block swallowing Stripe errors. The fix adds proper error logging.'),
|
||||
reasoningPart(
|
||||
'Done. The root cause was a bare catch block swallowing Stripe errors. The fix adds proper error logging.'
|
||||
),
|
||||
{ type: 'text' as const, text: 'streamed' }
|
||||
]
|
||||
|
||||
|
|
@ -842,10 +841,7 @@ describe('mergeFinalAssistantText', () => {
|
|||
})
|
||||
|
||||
it('handles empty final text', () => {
|
||||
const parts = [
|
||||
{ type: 'text' as const, text: 'streamed' },
|
||||
reasoningPart('some reasoning')
|
||||
]
|
||||
const parts = [{ type: 'text' as const, text: 'streamed' }, reasoningPart('some reasoning')]
|
||||
|
||||
const result = mergeFinalAssistantText(parts, '')
|
||||
|
||||
|
|
|
|||
|
|
@ -152,10 +152,7 @@ const normalizeWs = (value: string) => value.replace(/\s+/g, ' ').trim()
|
|||
* - Keeps all other part types (tool-call, image, etc.).
|
||||
* - Appends the final text as a new text part.
|
||||
*/
|
||||
export function mergeFinalAssistantText(
|
||||
parts: ChatMessagePart[],
|
||||
finalText: string
|
||||
): ChatMessagePart[] {
|
||||
export function mergeFinalAssistantText(parts: ChatMessagePart[], finalText: string): ChatMessagePart[] {
|
||||
const dedupeReference = normalizeWs(finalText)
|
||||
|
||||
const kept = parts.filter(part => {
|
||||
|
|
|
|||
|
|
@ -948,12 +948,14 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
return
|
||||
case 'message.interim': {
|
||||
const text = ev.payload?.text
|
||||
|
||||
if (typeof text === 'string' && text.trim()) {
|
||||
turnController.recordInterimMessage(text)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
case 'message.complete': {
|
||||
const { finalMessages, finalText, wasInterrupted } = turnController.recordMessageComplete(ev.payload ?? {})
|
||||
|
||||
|
|
|
|||
|
|
@ -555,7 +555,12 @@ class TurnController {
|
|||
this.flushPendingNotice()
|
||||
}
|
||||
|
||||
recordMessageComplete(payload: { rendered?: string; reasoning?: string; response_previewed?: boolean; text?: string }) {
|
||||
recordMessageComplete(payload: {
|
||||
rendered?: string
|
||||
reasoning?: string
|
||||
response_previewed?: boolean
|
||||
text?: string
|
||||
}) {
|
||||
this.closeReasoningSegment()
|
||||
|
||||
// Ink renders markdown via <Md>; the gateway's Rich-rendered ANSI
|
||||
|
|
@ -687,6 +692,7 @@ class TurnController {
|
|||
}
|
||||
|
||||
const authoritativeText = text.trimStart()
|
||||
|
||||
if (!authoritativeText) {
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue