mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
Merge pull request #61147 from NousResearch/bb/desktop-tool-window-merge
feat(desktop): group tool calls across text-less assistant messages
This commit is contained in:
commit
fac85518fc
2 changed files with 67 additions and 2 deletions
|
|
@ -19,7 +19,13 @@ import { ErrorState } from '@/components/ui/error-state'
|
|||
import { getGlobalModelOptions, type HermesGateway } from '@/hermes'
|
||||
import { useI18n } from '@/i18n'
|
||||
import type { ChatMessage } from '@/lib/chat-messages'
|
||||
import { quickModelOptions, sessionTitle, toRuntimeMessage } from '@/lib/chat-runtime'
|
||||
import {
|
||||
coalesceToolOnlyAssistants,
|
||||
createToolMergeCache,
|
||||
quickModelOptions,
|
||||
sessionTitle,
|
||||
toRuntimeMessage
|
||||
} from '@/lib/chat-runtime'
|
||||
import { useIncrementalExternalStoreRuntime } from '@/lib/incremental-external-store-runtime'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ComposerAttachment } from '@/store/composer'
|
||||
|
|
@ -201,6 +207,7 @@ function ChatRuntimeBoundary({
|
|||
const storeMessages = useStore($messages)
|
||||
const messages = suppressMessages ? NO_MESSAGES : storeMessages
|
||||
const runtimeMessageCacheRef = useRef(new WeakMap<ChatMessage, ThreadMessage>())
|
||||
const toolMergeCacheRef = useRef(createToolMergeCache())
|
||||
|
||||
const runtimeMessageRepository = useMemo(() => {
|
||||
const items: { message: ThreadMessage; parentId: string | null }[] = []
|
||||
|
|
@ -208,7 +215,7 @@ function ChatRuntimeBoundary({
|
|||
let visibleParentId: string | null = null
|
||||
let headId: string | null = null
|
||||
|
||||
for (const message of messages) {
|
||||
for (const message of coalesceToolOnlyAssistants(messages, toolMergeCacheRef.current)) {
|
||||
let parentId = visibleParentId
|
||||
|
||||
if (message.role === 'assistant' && message.branchGroupId) {
|
||||
|
|
|
|||
|
|
@ -383,3 +383,61 @@ export function toRuntimeMessage(message: ChatMessage): ThreadMessage {
|
|||
}
|
||||
} as ThreadMessage
|
||||
}
|
||||
|
||||
export type ToolMergeCache = WeakMap<
|
||||
ChatMessage,
|
||||
{ merged: ChatMessage; parts: ChatMessagePart[]; prev: ChatMessage; prevParts: ChatMessagePart[] }
|
||||
>
|
||||
|
||||
export function createToolMergeCache(): ToolMergeCache {
|
||||
return new WeakMap()
|
||||
}
|
||||
|
||||
// A settled assistant message with only tool calls — no prose, no reasoning.
|
||||
// The model routinely emits a follow-up batch of calls as its own text-less
|
||||
// message; on screen it looks like one continuous run, but assistant-ui can't
|
||||
// group tool calls across a message boundary.
|
||||
function isToolOnlyAssistant(message: ChatMessage): boolean {
|
||||
return (
|
||||
message.role === 'assistant' &&
|
||||
!message.pending &&
|
||||
!message.error &&
|
||||
!message.hidden &&
|
||||
message.parts.length > 0 &&
|
||||
message.parts.every(part => part.type === 'tool-call')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold each settled tool-only assistant message into the preceding assistant
|
||||
* message so its calls join that message's tool group (and can collapse into
|
||||
* the auto-scrolling window). Render-only — never mutates the `$messages` store
|
||||
* — and settle-only: pending messages are left alone, so a live turn is never
|
||||
* merged/un-merged mid-stream. `cache` keys merged results by source identity,
|
||||
* so a stable turn yields stable merged objects (no re-render churn).
|
||||
*/
|
||||
export function coalesceToolOnlyAssistants(messages: ChatMessage[], cache: ToolMergeCache): ChatMessage[] {
|
||||
const out: ChatMessage[] = []
|
||||
|
||||
for (const message of messages) {
|
||||
const prev = out.at(-1)
|
||||
|
||||
if (prev && prev.role === 'assistant' && !prev.pending && !prev.hidden && isToolOnlyAssistant(message)) {
|
||||
const cached = cache.get(message)
|
||||
|
||||
const merged =
|
||||
cached && cached.prev === prev && cached.prevParts === prev.parts && cached.parts === message.parts
|
||||
? cached.merged
|
||||
: { ...prev, parts: [...prev.parts, ...message.parts] }
|
||||
|
||||
cache.set(message, { merged, parts: message.parts, prev, prevParts: prev.parts })
|
||||
out[out.length - 1] = merged
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
out.push(message)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue