cd14bb1963
* fix: improve chat compression and tool display Context Compression Fixes: - Remove duplicate token calculation in compress() - Simplify compress() to only execute compression, not judge - Add buildConversationHistory() to preserve tool calls in LLM context - Remove unused estimateMessagesTokens() and contextLength parameter - Move all judgment logic to chat-run-socket.ts (uses accurate DB tokens) Tool Call Display Improvements: - Add tool execution duration display (format: 1.272s) - Add success/error status icons with circular backgrounds - Replace text error with SVG icon (X in red circle) - Replace old checkmark with polished green checkmark icon - Add i18n key 'chat.executionDuration' for all locales Bug Fixes: - Fix streaming-indicator stuck by adding try-finally in handleEvent - Add debug logging for compression flow diagnosis - Fix template syntax error in MessageList.vue Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(chat): convert conversation history to Anthropic format before sending to Gateway - Add convertToAnthropicFormat() to transform OpenAI format to Anthropic format - Handle DeepSeek reasoning_content in thinking blocks - Properly convert tool_use and tool_result blocks - Add convertFromAnthropicFormat() for parsing SSE responses - Handle stringified Python arrays in resume messages - Record debug history files for troubleshooting (original vs converted) - Fix tool_call_id validation to prevent empty ID errors - Clean internal Hermes fields (call_id, response_item_id) from tool_calls Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(chat): optimize message parsing and add debug logging - Only check for stringified arrays in assistant messages (performance) - Improve parsing error handling: keep original content on parse failure - Add debug logging for upstream events (reasoning/thinking tracking) - Log run.completed event keys for troubleshooting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(chat): add message pagination and reasoning sync improvements **Message Pagination:** - Add getSessionDetailPaginated() for paginated message loading - Query with DESC order then reverse in code for optimal performance - Remove listSessionsPaginated() (not needed) **Reasoning Sync:** - Add bidirectional reasoning merge in syncFromHermes - Memory → DB: preserve streamed reasoning from SSE events - DB → Memory: restore reasoning if Hermes Gateway fixes storage - Send resumed event after sync completes with complete messages - Fix reasoning field inconsistency: use unified 'reasoning' field **Message Parsing:** - Only parse stringified arrays for assistant messages (performance) - Improve parse error handling: keep original content on failure - Add debug logging for upstream reasoning/thinking events **Bug Fixes:** - Fix reasoning content display: now works on both SSE and resume - Ensure reasoning is preserved across page refreshes via sync + resumed event Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: increase default pagination limit for messages to 500 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove auto-resumed event trigger and clean up debug code - Remove automatic resumed event trigger in syncFromHermes to avoid timing issues - Clean up unused imports (fs, join) - Remove debug history file logging code - Fix socket parameter passing in handleAbort, markCompleted, and syncFromHermes - Change usage emit from room broadcast to socket-only emit - Remove console.log debug statement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use reasoning field in convertToAnthropicFormat Change convertToAnthropicFormat to read from reasoning field instead of reasoning_content for consistency with database schema and frontend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: parse stringified array content and improve logs - Parse stringified array format in run.completed to extract thinking/text/tool_use - Send parsed content to frontend via parsed_content/parsed_reasoning/parsed_tool_calls - Frontend updates last assistant message with parsed content - Remove ellipsis from log messages, show full content - Add detailed logging for conversion and parsing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: move finalOutputTrimmed outside else block * fix(chat): handle double-serialized content in resumeSession - Remove outer quotes before parsing stringified array format - Updated changelog for v0.5.2 and v0.5.3 with multilingual support - Fixed message pagination with DESC query + array reverse Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(chat): improve error logging for resume parsing - Add detailed logging for double-serialized content parsing - Log content preview when parsing fails to diagnose issues Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * revert(chat): use simple Python-to-JSON replacement - Revert to simple .replace(/'/g, '"') approach - Parsing failures will keep original content as-is Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
196 lines
5.8 KiB
TypeScript
196 lines
5.8 KiB
TypeScript
import { io, type Socket } from 'socket.io-client'
|
|
import { request, getBaseUrlValue, getApiKey } from '../client'
|
|
|
|
export interface ChatMessage {
|
|
role: 'user' | 'assistant' | 'system'
|
|
content: string
|
|
}
|
|
|
|
export interface StartRunRequest {
|
|
input: string | ChatMessage[]
|
|
instructions?: string
|
|
session_id?: string
|
|
model?: string
|
|
}
|
|
|
|
export interface StartRunResponse {
|
|
run_id: string
|
|
status: string
|
|
}
|
|
|
|
// SSE event types from /v1/runs/{id}/events
|
|
export interface RunEvent {
|
|
event: string
|
|
run_id?: string
|
|
delta?: string
|
|
/** Payload text for `reasoning.delta` / `thinking.delta` / `reasoning.available` events. */
|
|
text?: string
|
|
tool?: string
|
|
name?: string
|
|
preview?: string
|
|
timestamp?: number
|
|
error?: string
|
|
/** Final response text on `run.completed`. May be empty/null if the agent
|
|
* silently swallowed an upstream error — see chat store for fallback. */
|
|
output?: string | null
|
|
usage?: {
|
|
input_tokens: number
|
|
output_tokens: number
|
|
total_tokens: number
|
|
}
|
|
/** session_id tag added by server for client-side filtering */
|
|
session_id?: string
|
|
}
|
|
|
|
// ============================
|
|
// Socket.IO chat run connection
|
|
// ============================
|
|
|
|
let chatRunSocket: Socket | null = null
|
|
|
|
export function getChatRunSocket(): Socket | null {
|
|
return chatRunSocket
|
|
}
|
|
|
|
export function connectChatRun(): Socket {
|
|
if (chatRunSocket?.connected) return chatRunSocket
|
|
|
|
// Clean up old socket to prevent duplicate event listeners
|
|
if (chatRunSocket) {
|
|
chatRunSocket.removeAllListeners()
|
|
chatRunSocket.disconnect()
|
|
}
|
|
|
|
const baseUrl = getBaseUrlValue()
|
|
const token = getApiKey()
|
|
const profile = localStorage.getItem('hermes_active_profile_name') || 'default'
|
|
|
|
chatRunSocket = io(`${baseUrl}/chat-run`, {
|
|
auth: { token },
|
|
query: { profile },
|
|
transports: ['websocket', 'polling'],
|
|
reconnection: true,
|
|
reconnectionAttempts: Infinity,
|
|
reconnectionDelay: 1000,
|
|
reconnectionDelayMax: 10000,
|
|
})
|
|
|
|
return chatRunSocket
|
|
}
|
|
|
|
export function disconnectChatRun(): void {
|
|
if (chatRunSocket) {
|
|
chatRunSocket.disconnect()
|
|
chatRunSocket = null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start a chat run via Socket.IO and stream events back.
|
|
* Returns an AbortController-compatible handle for cancellation.
|
|
*/
|
|
/**
|
|
* Resume a session via Socket.IO. Returns messages, working status, and events.
|
|
*/
|
|
export function resumeSession(
|
|
sessionId: string,
|
|
onResumed: (data: { session_id: string; messages: any[]; isWorking: boolean; events: any[]; inputTokens?: number; outputTokens?: number }) => void,
|
|
): Socket {
|
|
const socket = connectChatRun()
|
|
|
|
socket.once('resumed', onResumed)
|
|
socket.emit('resume', { session_id: sessionId })
|
|
|
|
return socket
|
|
}
|
|
|
|
export function startRunViaSocket(
|
|
body: StartRunRequest,
|
|
onEvent: (event: RunEvent) => void,
|
|
onDone: () => void,
|
|
onError: (err: Error) => void,
|
|
onStarted?: (runId: string) => void,
|
|
): { abort: () => void } {
|
|
const socket = connectChatRun()
|
|
let closed = false
|
|
|
|
function cleanup() {
|
|
if (closed) return
|
|
closed = true
|
|
socket.off('run.started', onRunStarted)
|
|
socket.off('run.failed', onRunFailed)
|
|
socket.off('message.delta', onMessageDelta)
|
|
socket.off('reasoning.delta', onReasoningDelta)
|
|
socket.off('thinking.delta', onReasoningDelta)
|
|
socket.off('reasoning.available', onReasoningAvailable)
|
|
socket.off('tool.started', onToolStarted)
|
|
socket.off('tool.completed', onToolCompleted)
|
|
socket.off('run.completed', onRunCompleted)
|
|
socket.off('compression.started', onCompressionStarted)
|
|
socket.off('compression.completed', onCompressionCompleted)
|
|
socket.off('usage.updated', onUsageUpdated)
|
|
}
|
|
|
|
// All event handlers share the same cleanup logic
|
|
const handleEvent = (event: RunEvent) => {
|
|
if (closed) return
|
|
try {
|
|
onEvent(event)
|
|
} finally {
|
|
if (event.event === 'run.completed' || event.event === 'run.failed') {
|
|
console.log('[startRunViaSocket] Run completed/failed, calling cleanup and onDone', event.event)
|
|
cleanup()
|
|
onDone()
|
|
}
|
|
}
|
|
}
|
|
|
|
function onRunStarted(data: RunEvent) {
|
|
handleEvent(data)
|
|
onStarted?.(data.run_id || '')
|
|
}
|
|
function onRunFailed(data: RunEvent) {
|
|
handleEvent(data)
|
|
onError?.(new Error(data.error || 'Run failed'))
|
|
}
|
|
function onMessageDelta(data: RunEvent) { handleEvent(data) }
|
|
function onReasoningDelta(data: RunEvent) { handleEvent(data) }
|
|
function onThinkingDelta(data: RunEvent) { handleEvent(data) }
|
|
function onReasoningAvailable(data: RunEvent) { handleEvent(data) }
|
|
function onToolStarted(data: RunEvent) { handleEvent(data) }
|
|
function onToolCompleted(data: RunEvent) { handleEvent(data) }
|
|
function onRunCompleted(data: RunEvent) { handleEvent(data) }
|
|
function onCompressionStarted(data: RunEvent) { handleEvent(data) }
|
|
function onCompressionCompleted(data: RunEvent) { handleEvent(data) }
|
|
function onUsageUpdated(data: RunEvent) { handleEvent(data) }
|
|
|
|
socket.on('run.started', onRunStarted)
|
|
socket.on('run.failed', onRunFailed)
|
|
socket.on('message.delta', onMessageDelta)
|
|
socket.on('reasoning.delta', onReasoningDelta)
|
|
socket.on('thinking.delta', onThinkingDelta)
|
|
socket.on('reasoning.available', onReasoningAvailable)
|
|
socket.on('tool.started', onToolStarted)
|
|
socket.on('tool.completed', onToolCompleted)
|
|
socket.on('run.completed', onRunCompleted)
|
|
socket.on('compression.started', onCompressionStarted)
|
|
socket.on('compression.completed', onCompressionCompleted)
|
|
socket.on('usage.updated', onUsageUpdated)
|
|
|
|
// Emit run:start with ack callback to get run_id
|
|
socket.emit('run', body)
|
|
|
|
return {
|
|
abort: () => {
|
|
if (!closed) {
|
|
socket.emit('abort', { session_id: body.session_id })
|
|
cleanup()
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
export async function fetchModels(): Promise<{ data: Array<{ id: string }> }> {
|
|
return request('/api/hermes/v1/models')
|
|
}
|