diff --git a/frontend/src/toolui/VendoredToolUi.tsx b/frontend/src/toolui/VendoredToolUi.tsx index 0e0179e4..bcf8716a 100644 --- a/frontend/src/toolui/VendoredToolUi.tsx +++ b/frontend/src/toolui/VendoredToolUi.tsx @@ -46,8 +46,68 @@ type Gate = /** Models pad payloads with invented keys; strip ONLY unrecognized-key issues and retry once, so sloppiness self-heals while genuinely wrong shapes still fall back loudly. */ +function slugFor(label: unknown, i: number): string { + const t = typeof label === 'string' ? label.trim().toLowerCase().replace(/\s+/g, '-').slice(0, 40) : ''; + return t || `item-${i + 1}`; +} + +// Mechanical repairs for the mistakes agents actually make (numeric ids, ranked priorities, nested +// row objects, bare action objects). Only ever applied when the strict parse FAILED, and the result +// is re-validated, so a repair can flip fail->pass but never corrupt a valid payload. +function repairCommonAgentShapes(props: Record): Record { + let out: Record; + try { + out = JSON.parse(JSON.stringify(props ?? {}, (_k, v) => (v === undefined ? null : v))); + } catch { + return props; + } + const fixIdLabel = (arr: unknown): unknown => { + if (!Array.isArray(arr)) return arr; + return arr.map((o, i) => { + if (typeof o === 'string') return { id: slugFor(o, i), label: o }; + if (o && typeof o === 'object' && !Array.isArray(o)) { + const obj = { ...(o as Record) }; + if (obj.id == null || obj.id === '') obj.id = slugFor(obj.label, i); + else if (typeof obj.id !== 'string') obj.id = String(obj.id); + if (typeof obj.label !== 'string' || !obj.label) obj.label = String(obj.label ?? obj.id); + return obj; + } + return o; + }); + }; + if ('options' in out) out.options = fixIdLabel(out.options); + if ('actions' in out) { + if (out.actions && !Array.isArray(out.actions) && typeof out.actions === 'object' && 'label' in (out.actions as object)) out.actions = [out.actions]; + out.actions = fixIdLabel(out.actions); + } + const PRIORITY_SYNONYMS: Record = { '1': 'primary', '2': 'secondary', '3': 'tertiary', high: 'primary', medium: 'secondary', low: 'tertiary', primary: 'primary', secondary: 'secondary', tertiary: 'tertiary' }; + if (Array.isArray(out.columns)) { + out.columns = out.columns.map((c) => { + if (c && typeof c === 'object' && 'priority' in (c as object)) { + const mapped = PRIORITY_SYNONYMS[String((c as Record).priority).toLowerCase()]; + const copy = { ...(c as Record) }; + if (mapped) copy.priority = mapped; else delete copy.priority; + return copy; + } + return c; + }); + } + if (Array.isArray(out.data)) { + out.data = out.data.map((row) => { + if (!row || typeof row !== 'object' || Array.isArray(row)) return row; + return Object.fromEntries(Object.entries(row as Record).map(([k, v]) => { + if (v !== null && typeof v === 'object' && !Array.isArray(v)) return [k, JSON.stringify(v)]; + if (Array.isArray(v)) return [k, v.map((x) => (x !== null && typeof x === 'object' ? JSON.stringify(x) : x))]; + return [k, v]; + })); + }); + } + return out; +} + function parseLeniently(schema: { safeParse: (v: unknown) => any }, props: Record): Gate { let result = schema.safeParse(props); + let base: Record = props; if (!result.success) { const issues: Array<{ code: string; keys?: string[]; path: Array; message: string }> = result.error.issues; if (issues.every((i) => i.code === 'unrecognized_keys')) { @@ -55,9 +115,14 @@ function parseLeniently(schema: { safeParse: (v: unknown) => any }, props: Recor for (const issue of issues) { for (const key of issue.keys || []) delete cleaned[key]; } + base = cleaned; result = schema.safeParse(cleaned); } } + if (!result.success) { + const repaired = schema.safeParse(repairCommonAgentShapes(base)); + if (repaired.success) return { state: 'ok', parsed: repaired.data as Record }; + } if (result.success) return { state: 'ok', parsed: result.data as Record }; const issues = result.error.issues.slice(0, 2).map((i: { path: Array; message: string }) => `${i.path.join('.')}: ${i.message}`).join('; '); return { state: 'bad', problem: issues };