mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(tui): render tool trail consistently between live and resume
Resumed sessions showed raw JSON tool output in content boxes instead of the compact trail lines seen during live use. The root cause was two separate rendering paths with no shared code. Extract buildToolTrailLine() into lib/text.ts as the single source of truth for formatting tool trail lines. Both the live tool.complete handler and toTranscriptMessages now call it. Server-side, reconstruct tool name and args from the assistant message's tool_calls field (tool_name column is unpopulated) and pass them through _tool_ctx/build_tool_preview — the same path the live tool.start callback uses.
This commit is contained in:
parent
57e8d44af8
commit
cab6447d58
3 changed files with 70 additions and 24 deletions
|
|
@ -484,12 +484,35 @@ def _(rid, params: dict) -> dict:
|
|||
try:
|
||||
db.reopen_session(target)
|
||||
history = db.get_messages_as_conversation(target)
|
||||
messages = [
|
||||
{"role": m["role"], "text": m.get("content") or ""}
|
||||
for m in history
|
||||
if m.get("role") in ("user", "assistant", "tool", "system")
|
||||
and (m.get("content") or "").strip()
|
||||
]
|
||||
messages = []
|
||||
tool_call_args = {}
|
||||
for m in history:
|
||||
role = m.get("role")
|
||||
if role not in ("user", "assistant", "tool", "system"):
|
||||
continue
|
||||
if role == "assistant" and m.get("tool_calls"):
|
||||
for tc in m["tool_calls"]:
|
||||
fn = tc.get("function", {})
|
||||
tc_id = tc.get("id", "")
|
||||
if tc_id and fn.get("name"):
|
||||
try:
|
||||
args = json.loads(fn.get("arguments", "{}"))
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
args = {}
|
||||
tool_call_args[tc_id] = (fn["name"], args)
|
||||
if not (m.get("content") or "").strip():
|
||||
continue
|
||||
if role == "tool":
|
||||
tc_id = m.get("tool_call_id", "")
|
||||
tc_info = tool_call_args.get(tc_id) if tc_id else None
|
||||
name = (tc_info[0] if tc_info else None) or m.get("tool_name") or "tool"
|
||||
args = (tc_info[1] if tc_info else None) or {}
|
||||
ctx = _tool_ctx(name, args)
|
||||
messages.append({"role": "tool", "name": name, "context": ctx})
|
||||
continue
|
||||
if not (m.get("content") or "").strip():
|
||||
continue
|
||||
messages.append({"role": role, "text": m.get("content") or ""})
|
||||
agent = _make_agent(sid, target, session_id=target)
|
||||
_init_session(sid, target, agent, history, cols=int(params.get("cols", 80)))
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { useCompletion } from './hooks/useCompletion.js'
|
|||
import { useInputHistory } from './hooks/useInputHistory.js'
|
||||
import { useQueue } from './hooks/useQueue.js'
|
||||
import { writeOsc52Clipboard } from './lib/osc52.js'
|
||||
import { compactPreview, fmtK, hasInterpolation, isToolTrailResultLine, pick, sameToolTrailGroup } from './lib/text.js'
|
||||
import { buildToolTrailLine, compactPreview, fmtK, hasInterpolation, isToolTrailResultLine, pick, sameToolTrailGroup } from './lib/text.js'
|
||||
import { DEFAULT_THEME, fromSkin, type Theme } from './theme.js'
|
||||
import type {
|
||||
ActiveTool,
|
||||
|
|
@ -107,24 +107,41 @@ const toTranscriptMessages = (rows: unknown): Msg[] => {
|
|||
return []
|
||||
}
|
||||
|
||||
return rows.flatMap(row => {
|
||||
if (!row || typeof row !== 'object') {
|
||||
return []
|
||||
}
|
||||
const result: Msg[] = []
|
||||
let pendingTools: string[] = []
|
||||
|
||||
for (const row of rows) {
|
||||
if (!row || typeof row !== 'object') continue
|
||||
|
||||
const role = (row as any).role
|
||||
const text = (row as any).text
|
||||
|
||||
if (
|
||||
(role !== 'assistant' && role !== 'system' && role !== 'tool' && role !== 'user') ||
|
||||
typeof text !== 'string' ||
|
||||
!text.trim()
|
||||
) {
|
||||
return []
|
||||
if (role === 'tool') {
|
||||
const name = (row as any).name ?? 'tool'
|
||||
const ctx = (row as any).context ?? ''
|
||||
pendingTools.push(buildToolTrailLine(name, ctx))
|
||||
continue
|
||||
}
|
||||
|
||||
return [{ role, text }]
|
||||
})
|
||||
if (typeof text !== 'string' || !text.trim()) continue
|
||||
|
||||
if (role === 'assistant') {
|
||||
const msg: Msg = { role, text }
|
||||
if (pendingTools.length) {
|
||||
msg.tools = pendingTools
|
||||
pendingTools = []
|
||||
}
|
||||
result.push(msg)
|
||||
continue
|
||||
}
|
||||
|
||||
if (role === 'user' || role === 'system') {
|
||||
pendingTools = []
|
||||
result.push({ role, text })
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ── StatusRule ────────────────────────────────────────────────────────
|
||||
|
|
@ -1155,14 +1172,13 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
break
|
||||
case 'tool.complete': {
|
||||
const mark = p.error ? '✗' : '✓'
|
||||
|
||||
toolCompleteRibbonRef.current = null
|
||||
setTools(prev => {
|
||||
const done = prev.find(t => t.id === p.tool_id)
|
||||
const label = TOOL_VERBS[done?.name ?? p.name] ?? done?.name ?? p.name
|
||||
const name = done?.name ?? p.name
|
||||
const ctx = (p.error as string) || done?.context || ''
|
||||
const line = `${label}${ctx ? ': ' + compactPreview(ctx, 72) : ''} ${mark}`
|
||||
const label = TOOL_VERBS[name] ?? name
|
||||
const line = buildToolTrailLine(name, ctx, !!p.error)
|
||||
|
||||
toolCompleteRibbonRef.current = { label, line }
|
||||
const remaining = prev.filter(t => t.id !== p.tool_id)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { INTERPOLATION_RE, LONG_MSG } from '../constants.js'
|
||||
import { INTERPOLATION_RE, LONG_MSG, TOOL_VERBS } from '../constants.js'
|
||||
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const ANSI_RE = /\x1b\[[0-9;]*m/g
|
||||
|
|
@ -35,6 +35,13 @@ export const compactPreview = (s: string, max: number) => {
|
|||
return !one ? '' : one.length > max ? one.slice(0, max - 1) + '…' : one
|
||||
}
|
||||
|
||||
/** Build a single tool trail line — used by both live tool.complete and resume replay. */
|
||||
export const buildToolTrailLine = (name: string, context: string, error?: boolean): string => {
|
||||
const label = TOOL_VERBS[name] ?? name
|
||||
const mark = error ? '✗' : '✓'
|
||||
return `${label}${context ? ': ' + compactPreview(context, 72) : ''} ${mark}`
|
||||
}
|
||||
|
||||
/** Tool completed / failed row in the inline trail (not CoT prose). */
|
||||
export const isToolTrailResultLine = (line: string) => line.endsWith(' ✓') || line.endsWith(' ✗')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue