feat: add Anthropic format conversion for chat runs and improvements (#347)

* 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>
This commit is contained in:
ekko
2026-04-30 16:40:37 +08:00
committed by GitHub
parent 2e87cb910c
commit cd14bb1963
25 changed files with 1097 additions and 437 deletions
+77 -22
View File
@@ -26,6 +26,7 @@ export interface Message {
toolArgs?: string
toolResult?: string
toolStatus?: 'running' | 'done' | 'error'
toolDuration?: number // 工具执行时长(秒)
isStreaming?: boolean
attachments?: Attachment[]
// 思考/推理文本。两条来源:
@@ -615,8 +616,10 @@ export const useChatStore = defineStore('chat', () => {
// Helper to clean up this session's stream state
const cleanup = () => {
console.log('[sendMessage] cleanup called, deleting stream state for sid:', sid)
streamStates.value.delete(sid)
serverWorking.value.delete(sid)
console.log('[sendMessage] cleanup done, isStreaming now:', isStreaming.value)
}
// Per-run flags used to detect silently-swallowed errors at run.completed.
@@ -765,7 +768,13 @@ export const useChatStore = defineStore('chat', () => {
)
if (toolMsgs.length > 0) {
const last = toolMsgs[toolMsgs.length - 1]
updateMessage(sid, last.id, { toolStatus: 'done' })
// Check if tool errored
const hasError = (evt as any).error === true
const duration = (evt as any).duration
updateMessage(sid, last.id, {
toolStatus: hasError ? 'error' : 'done',
toolDuration: duration,
})
}
break
@@ -790,17 +799,38 @@ export const useChatStore = defineStore('chat', () => {
// stream). If we never produced assistant text but the gateway
// reports a non-empty output, fall back to rendering it as a
// single assistant message so the user actually sees the reply.
const finalOutput =
typeof evt.output === 'string' ? evt.output : ''
const finalOutputTrimmed = finalOutput.trim()
if (!runProducedAssistantText && finalOutputTrimmed !== '') {
addMessage(sid, {
id: uid(),
role: 'assistant',
content: finalOutput,
timestamp: Date.now(),
})
runProducedAssistantText = true
// Check if backend provided parsed content (from stringified array format)
let finalOutputTrimmed = ''
if ((evt as any).parsed_content !== undefined) {
// Backend has parsed stringified array format, update last assistant message
const msgs = getSessionMsgs(sid)
const lastAssistant = [...msgs].reverse().find(m => m.role === 'assistant')
if (lastAssistant) {
updateMessage(sid, lastAssistant.id, {
content: (evt as any).parsed_content || '',
})
if ((evt as any).parsed_reasoning) {
updateMessage(sid, lastAssistant.id, {
reasoning: (evt as any).parsed_reasoning,
})
}
finalOutputTrimmed = ((evt as any).parsed_content || '').trim()
}
} else {
// Fallback to output field (legacy behavior)
const finalOutput =
typeof evt.output === 'string' ? evt.output : ''
finalOutputTrimmed = finalOutput.trim()
if (!runProducedAssistantText && finalOutputTrimmed !== '') {
addMessage(sid, {
id: uid(),
role: 'assistant',
content: finalOutput,
timestamp: Date.now(),
})
runProducedAssistantText = true
}
}
// Workaround for upstream hermes-agent bug: when the agent
// layer silently swallows an error (e.g. invalid API key,
@@ -875,6 +905,7 @@ export const useChatStore = defineStore('chat', () => {
},
// onDone
() => {
console.log('[sendMessage] onDone callback called, cleaning up stream state')
const msgs = getSessionMsgs(sid)
const last = msgs[msgs.length - 1]
if (last?.isStreaming) {
@@ -1076,7 +1107,11 @@ export const useChatStore = defineStore('chat', () => {
const msgs = getSessionMsgs(sid)
const toolMsgs = msgs.filter(m => m.role === 'tool' && m.toolStatus === 'running')
if (toolMsgs.length > 0) {
updateMessage(sid, toolMsgs[toolMsgs.length - 1].id, { toolStatus: 'done' })
const hasError = (evt as any).error === true
updateMessage(sid, toolMsgs[toolMsgs.length - 1].id, {
toolStatus: hasError ? 'error' : 'done',
toolDuration: (evt as any).duration,
})
}
break
@@ -1096,15 +1131,35 @@ export const useChatStore = defineStore('chat', () => {
target.outputTokens = (evt as any).outputTokens
}
}
const finalOutput = typeof evt.output === 'string' ? evt.output : ''
const finalOutputTrimmed = finalOutput.trim()
if (!runProducedAssistantText && finalOutputTrimmed !== '') {
addMessage(sid, {
id: uid(),
role: 'assistant',
content: finalOutput,
timestamp: Date.now(),
})
// Check if backend provided parsed content (from stringified array format)
let finalOutputTrimmed = ''
if ((evt as any).parsed_content !== undefined) {
// Backend has parsed stringified array format, update last assistant message
const msgs = getSessionMsgs(sid)
const lastAssistant = [...msgs].reverse().find(m => m.role === 'assistant')
if (lastAssistant) {
updateMessage(sid, lastAssistant.id, {
content: (evt as any).parsed_content || '',
})
if ((evt as any).parsed_reasoning) {
updateMessage(sid, lastAssistant.id, {
reasoning: (evt as any).parsed_reasoning,
})
}
finalOutputTrimmed = ((evt as any).parsed_content || '').trim()
}
} else {
// Fallback to output field (legacy behavior)
const finalOutput = typeof evt.output === 'string' ? evt.output : ''
finalOutputTrimmed = finalOutput.trim()
if (!runProducedAssistantText && finalOutputTrimmed !== '') {
addMessage(sid, {
id: uid(),
role: 'assistant',
content: finalOutput,
timestamp: Date.now(),
})
}
}
const swallowedError = !runProducedAssistantText && !runHadToolActivity && finalOutputTrimmed === ''
if (swallowedError) {