mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-21 16:18:55 +00:00
a reader of this subclass can't recover from hermes code alone that the metadata.isOptimistic flag drives core's off-branch eviction and export() omission, so a future core change to it would silently break placeholder cleanup. flagged in the upgrade review.
198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
import {
|
|
AssistantRuntimeImpl,
|
|
BaseAssistantRuntimeCore,
|
|
ExternalStoreThreadListRuntimeCore,
|
|
ExternalStoreThreadRuntimeCore,
|
|
hasUpcomingMessage
|
|
} from '@assistant-ui/core/internal'
|
|
import {
|
|
type AssistantRuntime,
|
|
type ExternalStoreAdapter,
|
|
fromThreadMessageLike,
|
|
generateId,
|
|
type ThreadMessage,
|
|
useRuntimeAdapters
|
|
} from '@assistant-ui/react'
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
const EMPTY_ARRAY = Object.freeze([])
|
|
|
|
const shallowEqual = (a: object, b: object): boolean => {
|
|
const aKeys = Object.keys(a)
|
|
|
|
if (aKeys.length !== Object.keys(b).length) {
|
|
return false
|
|
}
|
|
|
|
for (const key of aKeys) {
|
|
if (a[key as keyof typeof a] !== b[key as keyof typeof b]) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
const getThreadListAdapter = (store: ExternalStoreAdapter) => store.adapters?.threadList ?? {}
|
|
|
|
function syncRepositoryIncrementally(
|
|
runtime: ExternalStoreThreadRuntimeCore,
|
|
messageRepository: NonNullable<ExternalStoreAdapter['messageRepository']>
|
|
): readonly ThreadMessage[] {
|
|
const repository = (runtime as unknown as { repository: ExternalStoreThreadRuntimeCore['repository'] }).repository
|
|
const incomingIds = new Set(messageRepository.messages.map(({ message }) => message.id))
|
|
|
|
for (const { message, parentId } of messageRepository.messages) {
|
|
repository.addOrUpdateMessage(parentId, message)
|
|
}
|
|
|
|
for (const { message } of repository.export().messages) {
|
|
if (!incomingIds.has(message.id)) {
|
|
repository.deleteMessage(message.id)
|
|
}
|
|
}
|
|
|
|
const headId = messageRepository.headId ?? messageRepository.messages.at(-1)?.message.id ?? null
|
|
|
|
repository.resetHead(headId)
|
|
|
|
return repository.getMessages()
|
|
}
|
|
|
|
class IncrementalExternalStoreThreadRuntimeCore extends ExternalStoreThreadRuntimeCore {
|
|
override __internal_setAdapter(store: ExternalStoreAdapter): void {
|
|
if (!store.messageRepository) {
|
|
super.__internal_setAdapter(store)
|
|
|
|
return
|
|
}
|
|
|
|
const self = this as unknown as {
|
|
_assistantOptimisticId: null | string
|
|
_capabilities: object
|
|
_messages: readonly ThreadMessage[]
|
|
_notifyEventSubscribers: (event: string, payload: object) => void
|
|
_notifySubscribers: () => void
|
|
_store?: ExternalStoreAdapter
|
|
}
|
|
|
|
if (self._store === store) {
|
|
return
|
|
}
|
|
|
|
const isRunning = store.isRunning ?? false
|
|
this.isDisabled = store.isDisabled ?? false
|
|
|
|
const oldStore = self._store
|
|
self._store = store
|
|
|
|
if (this.extras !== store.extras) {
|
|
this.extras = store.extras
|
|
}
|
|
|
|
const newSuggestions = store.suggestions ?? EMPTY_ARRAY
|
|
|
|
if (!shallowEqual(this.suggestions, newSuggestions)) {
|
|
this.suggestions = newSuggestions
|
|
}
|
|
|
|
const newCapabilities = {
|
|
switchToBranch: store.setMessages !== undefined,
|
|
switchBranchDuringRun: false,
|
|
edit: store.onEdit !== undefined,
|
|
reload: store.onReload !== undefined,
|
|
cancel: store.onCancel !== undefined,
|
|
speech: store.adapters?.speech !== undefined,
|
|
dictation: store.adapters?.dictation !== undefined,
|
|
voice: store.adapters?.voice !== undefined,
|
|
unstable_copy: store.unstable_capabilities?.copy !== false,
|
|
attachments: !!store.adapters?.attachments,
|
|
feedback: !!store.adapters?.feedback,
|
|
queue: false
|
|
}
|
|
|
|
if (!shallowEqual(self._capabilities, newCapabilities)) {
|
|
self._capabilities = newCapabilities
|
|
}
|
|
|
|
if (oldStore && oldStore.isRunning === store.isRunning && oldStore.messageRepository === store.messageRepository) {
|
|
self._notifySubscribers()
|
|
|
|
return
|
|
}
|
|
|
|
if (self._assistantOptimisticId) {
|
|
this.repository.deleteMessage(self._assistantOptimisticId)
|
|
self._assistantOptimisticId = null
|
|
}
|
|
|
|
const messages = syncRepositoryIncrementally(this, store.messageRepository)
|
|
|
|
if (messages.length > 0) {
|
|
this.ensureInitialized()
|
|
}
|
|
|
|
if ((oldStore?.isRunning ?? false) !== (store.isRunning ?? false)) {
|
|
self._notifyEventSubscribers(store.isRunning ? 'runStart' : 'runEnd', {})
|
|
}
|
|
|
|
// metadata.isOptimistic keeps this placeholder ephemeral: core evicts
|
|
// off-branch optimistic messages on head moves and omits them from export().
|
|
if (hasUpcomingMessage(isRunning, messages)) {
|
|
const optimisticId = generateId()
|
|
this.repository.addOrUpdateMessage(
|
|
messages.at(-1)?.id ?? null,
|
|
fromThreadMessageLike(
|
|
{ role: 'assistant', content: [], metadata: { isOptimistic: true } },
|
|
optimisticId,
|
|
{ type: 'running' }
|
|
)
|
|
)
|
|
self._assistantOptimisticId = optimisticId
|
|
}
|
|
|
|
this.repository.resetHead(self._assistantOptimisticId ?? messages.at(-1)?.id ?? null)
|
|
self._messages = this.repository.getMessages()
|
|
self._notifySubscribers()
|
|
}
|
|
}
|
|
|
|
class IncrementalExternalStoreRuntimeCore extends BaseAssistantRuntimeCore {
|
|
threads: ExternalStoreThreadListRuntimeCore
|
|
|
|
constructor(adapter: ExternalStoreAdapter) {
|
|
super()
|
|
|
|
this.threads = new ExternalStoreThreadListRuntimeCore(
|
|
getThreadListAdapter(adapter),
|
|
() => new IncrementalExternalStoreThreadRuntimeCore(this._contextProvider, adapter)
|
|
)
|
|
}
|
|
|
|
setAdapter(adapter: ExternalStoreAdapter): void {
|
|
this.threads.__internal_setAdapter(getThreadListAdapter(adapter))
|
|
this.threads.getMainThreadRuntimeCore().__internal_setAdapter(adapter)
|
|
}
|
|
}
|
|
|
|
export function useIncrementalExternalStoreRuntime<T extends ThreadMessage>(
|
|
store: ExternalStoreAdapter<T>
|
|
): AssistantRuntime {
|
|
const [runtime] = useState(() => new IncrementalExternalStoreRuntimeCore(store as ExternalStoreAdapter))
|
|
|
|
useEffect(() => {
|
|
runtime.setAdapter(store as ExternalStoreAdapter)
|
|
})
|
|
|
|
const { modelContext } = useRuntimeAdapters() ?? {}
|
|
|
|
useEffect(() => {
|
|
if (!modelContext) {
|
|
return undefined
|
|
}
|
|
|
|
return runtime.registerModelContextProvider(modelContext)
|
|
}, [modelContext, runtime])
|
|
|
|
return useMemo(() => new AssistantRuntimeImpl(runtime), [runtime])
|
|
}
|