Files
Hermes-ui/packages/server/src/services/hermes/run-chat/bridge-message.ts
T
2026-05-15 10:08:52 +08:00

204 lines
6.3 KiB
TypeScript

/**
* Bridge message management — flush pending content to DB,
* track tool calls, manage assistant message lifecycle.
*/
import { addMessage } from '../../../db/hermes/session-store'
import { logger } from '../../logger'
import type { SessionMessage, SessionState } from './types'
export function flushBridgePendingToDb(state: SessionState, sessionId: string, runMarker?: string) {
const content = state.bridgePendingAssistantContent || ''
const reasoning = state.bridgePendingReasoningContent || ''
if (!content.trim()) return
if (runMarker) {
const last = findOpenBridgeAssistantMessage(state, runMarker)
if (last) syncBridgeReasoningToMessage(last, reasoning)
}
addMessage({
session_id: sessionId,
role: 'assistant',
content,
reasoning: reasoning || null,
reasoning_content: reasoning || null,
timestamp: Math.floor(Date.now() / 1000),
})
state.bridgePendingAssistantContent = ''
state.bridgePendingReasoningContent = ''
if (runMarker) {
const last = findOpenBridgeAssistantMessage(state, runMarker)
if (last && last.finish_reason == null) last.finish_reason = 'stop'
}
}
export function findOpenBridgeAssistantMessage(state: SessionState, runMarker: string): SessionMessage | undefined {
return [...state.messages]
.reverse()
.find(m => m.runMarker === runMarker && m.role === 'assistant' && m.finish_reason == null)
}
export function ensureOpenBridgeAssistantMessage(
state: SessionState,
sessionId: string,
runMarker: string,
): SessionMessage {
const existing = findOpenBridgeAssistantMessage(state, runMarker)
if (existing) return existing
const message: SessionMessage = {
id: state.messages.length + 1,
session_id: sessionId,
runMarker,
role: 'assistant',
content: '',
timestamp: Math.floor(Date.now() / 1000),
}
state.messages.push(message)
return message
}
export function syncBridgeReasoningToMessage(message: SessionMessage, reasoning?: string) {
if (!reasoning) return
message.reasoning = reasoning
message.reasoning_content = reasoning
}
export function recordBridgeToolStarted(
state: SessionState,
sessionId: string,
runMarker: string,
toolName: string,
args: Record<string, unknown> | undefined,
rawToolCallId: unknown,
): { id: string; name: string; arguments: string } {
const id = bridgeToolCallId(state, rawToolCallId, toolName)
const argsString = args ? JSON.stringify(args) : '{}'
const reasoning = state.bridgePendingReasoningContent || ''
const toolCall = {
id,
type: 'function',
function: {
name: toolName,
arguments: argsString,
},
}
const timestamp = Math.floor(Date.now() / 1000)
state.bridgePendingTools = state.bridgePendingTools || []
state.bridgePendingTools.push({
id,
name: toolName,
arguments: argsString,
startedAt: Date.now(),
})
const openMessage = findOpenBridgeAssistantMessage(state, runMarker)
if (openMessage && !openMessage.content && !openMessage.tool_calls?.length) {
openMessage.tool_calls = [toolCall]
openMessage.finish_reason = 'tool_calls'
openMessage.reasoning = reasoning || openMessage.reasoning || null
openMessage.reasoning_content = reasoning || openMessage.reasoning_content || null
openMessage.timestamp = timestamp
} else {
state.messages.push({
id: state.messages.length + 1,
session_id: sessionId,
runMarker,
role: 'assistant',
content: '',
tool_calls: [toolCall],
finish_reason: 'tool_calls',
reasoning: reasoning || null,
reasoning_content: reasoning || null,
timestamp,
})
}
addMessage({
session_id: sessionId,
role: 'assistant',
content: '',
tool_calls: [toolCall],
finish_reason: 'tool_calls',
reasoning: reasoning || null,
reasoning_content: reasoning || null,
timestamp,
})
state.bridgePendingReasoningContent = ''
return { id, name: toolName, arguments: argsString }
}
export function recordBridgeToolCompleted(
state: SessionState,
sessionId: string,
runMarker: string,
toolName: string,
ev: Record<string, unknown>,
): { id: string; output: string; duration?: number } {
state.bridgePendingTools = state.bridgePendingTools || []
const rawId = ev.tool_call_id
let idx = rawId
? state.bridgePendingTools.findIndex(tool => tool.id === String(rawId))
: -1
if (idx < 0 && toolName) {
idx = state.bridgePendingTools.findIndex(tool => tool.name === toolName)
}
if (idx < 0) {
idx = state.bridgePendingTools.length - 1
}
const pending = idx >= 0 ? state.bridgePendingTools.splice(idx, 1)[0] : undefined
const id = pending?.id || bridgeToolCallId(state, rawId, toolName)
const output = bridgeToolOutput(ev)
const timestamp = Math.floor(Date.now() / 1000)
logger.info(
'[chat-run-socket][bridge] recording CLI tool result session=%s tool=%s tool_call_id=%s raw_tool_call_id=%s output_len=%d has_result=%s has_output=%s has_result_preview=%s has_preview=%s event_keys=%s',
sessionId,
toolName,
id,
String(rawId || ''),
output.length,
String(ev.result != null),
String(ev.output != null),
String(ev.result_preview != null),
String(ev.preview != null),
Object.keys(ev).join(','),
)
state.messages.push({
id: state.messages.length + 1,
session_id: sessionId,
runMarker,
role: 'tool',
content: output,
tool_call_id: id,
tool_name: toolName || pending?.name || null,
timestamp,
})
addMessage({
session_id: sessionId,
role: 'tool',
content: output,
tool_call_id: id,
tool_name: toolName || pending?.name || null,
timestamp,
})
const duration = pending?.startedAt
? Math.round((Date.now() - pending.startedAt) / 10) / 100
: undefined
return { id, output, duration }
}
export function bridgeToolCallId(state: SessionState, rawToolCallId: unknown, toolName: string): string {
const raw = String(rawToolCallId || '').trim()
if (raw) return raw
state.bridgeToolCounter = (state.bridgeToolCounter || 0) + 1
const safeName = (toolName || 'tool').replace(/[^a-zA-Z0-9_-]/g, '_')
return `cli_${safeName}_${state.bridgeToolCounter}`
}
export function bridgeToolOutput(ev: Record<string, unknown>): string {
const value = ev.result ?? ev.output ?? ev.result_preview ?? ev.preview ?? ''
return typeof value === 'string' ? value : JSON.stringify(value ?? '')
}