mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-13 14:02:16 +00:00
opentui(v6): trust gateway payload.error — drop client-side result sniffing
This commit is contained in:
parent
0dafcdd9e3
commit
ca791f4000
2 changed files with 11 additions and 30 deletions
|
|
@ -228,29 +228,6 @@ function readOptNum(payload: { readonly [k: string]: unknown }, key: string): nu
|
|||
return typeof v === 'number' ? v : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* A failed tool's failure signal, derived from the raw `result`: hermes tools
|
||||
* return `{"error": "…"}` JSON on failure, and the gateway never sets a
|
||||
* top-level `error` on tool.complete — so this is THE live trigger for the
|
||||
* failed lifecycle state (Epic 2.5). Flattened to one line for the header
|
||||
* subtitle; absent/empty/non-dict results yield undefined (not failed).
|
||||
*/
|
||||
function resultError(result: unknown): string | undefined {
|
||||
let v: unknown = result
|
||||
if (typeof v === 'string') {
|
||||
try {
|
||||
v = JSON.parse(v)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
if (!v || typeof v !== 'object' || Array.isArray(v)) return undefined
|
||||
const e = (v as Record<string, unknown>)['error']
|
||||
if (typeof e !== 'string') return undefined
|
||||
const flat = e.replace(/\s+/g, ' ').trim()
|
||||
return flat ? flat.slice(0, 400) : undefined
|
||||
}
|
||||
|
||||
/** Render a raw tool `result` for display: strings as-is, anything else pretty
|
||||
* JSON — both then go through the same envelope-strip pipeline as result_text. */
|
||||
function stringifyResult(v: unknown): string | undefined {
|
||||
|
|
@ -673,7 +650,7 @@ export function createSessionStore() {
|
|||
const name = readStr(event.payload, 'name')
|
||||
// explicit payload error wins; else derive from the `{"error": …}` result
|
||||
// convention (the only failure signal the live gateway actually ships).
|
||||
const error = readStr(event.payload, 'error') ?? resultError(event.payload['result'])
|
||||
const error = readStr(event.payload, 'error')
|
||||
const summary = readStr(event.payload, 'summary')
|
||||
// `result_text` is verbose-gated, but the raw `result` is ALWAYS sent —
|
||||
// when the verbose text is absent, derive the display body from `result`
|
||||
|
|
|
|||
|
|
@ -450,21 +450,25 @@ describe('tool lifecycle states — running / done / failed (Epic 2.5)', () => {
|
|||
}
|
||||
})
|
||||
|
||||
test('store: a `{"error": …}` result derives part.error (the gateway never ships a top-level error)', () => {
|
||||
test('store: part.error comes ONLY from payload.error (gateway owns the result convention)', () => {
|
||||
// The gateway derives failure from the result convention server-side
|
||||
// (tui_gateway _tool_error_from_result) and ships payload.error; the
|
||||
// client must NOT sniff results itself (false positives on tools whose
|
||||
// legitimate output embeds an "error" key — review finding, Epic 2.5).
|
||||
const store = createSessionStore()
|
||||
seedTool(
|
||||
store,
|
||||
{ tool_id: 'l2', name: 'read_file' },
|
||||
{ tool_id: 'l2', name: 'read_file', result: { error: 'File not found:\n /nope.txt' } }
|
||||
{ tool_id: 'l2', name: 'read_file', error: 'File not found: /nope.txt' }
|
||||
)
|
||||
const last = store.state.messages[store.state.messages.length - 1]
|
||||
const part = last?.parts?.find((p): p is ToolPartState => p.type === 'tool' && p.id === 'l2')
|
||||
expect(part?.error).toBe('File not found: /nope.txt') // flattened to one line
|
||||
// …and a clean result stays un-failed
|
||||
expect(part?.error).toBe('File not found: /nope.txt')
|
||||
// a result that EMBEDS an error key, without payload.error, stays un-failed
|
||||
seedTool(
|
||||
store,
|
||||
{ tool_id: 'l3', name: 'terminal' },
|
||||
{ tool_id: 'l3', name: 'terminal', result: { exit_code: 0, output: 'ok' } }
|
||||
{ tool_id: 'l3', name: 'web_search' },
|
||||
{ tool_id: 'l3', name: 'web_search', result: { results: [1], error: 'some shards failed' } }
|
||||
)
|
||||
const part3 = store.state.messages[store.state.messages.length - 1]?.parts?.find(
|
||||
(p): p is ToolPartState => p.type === 'tool' && p.id === 'l3'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue