fix: survive a malformed message rather than take the viewer down

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-14 09:10:52 +02:00
co-authored by Claude Opus 5
parent b4c0622f3f
commit 2f5354ea7b
2 changed files with 23 additions and 2 deletions
@@ -144,3 +144,17 @@ describe('the max-iterations path', () => {
])
})
})
// A result is whatever a script returned, so every nested field is arbitrary even
// once the messages have roles. A throw in here takes down the whole result
// viewer, including the ordinary error such a payload usually rides on.
describe('a malformed message that still has a role', () => {
it.each([
['tool_calls that are not a list', { role: 'assistant', tool_calls: {} }],
['annotations that are not a list', { role: 'assistant', content: 'hi', annotations: 'abc' }],
['a content object', { role: 'assistant', content: { text: 'hi' } }],
['an agent_action that is not an object', { role: 'tool', agent_action: 'tool_call' }]
])('survives %s', (_label, message) => {
expect(() => buildAgentTrace([message as never])).not.toThrow()
})
})
+9 -2
View File
@@ -46,7 +46,7 @@ function contentText(content: unknown): string {
function sourcesOf(message: AgentMessage): WebSearchSource[] | undefined {
const annotations = message.annotations
if (!annotations?.length) {
if (!Array.isArray(annotations) || annotations.length === 0) {
return undefined
}
const sources = annotations
@@ -61,7 +61,14 @@ export function buildAgentTrace(messages: AgentMessage[]): AgentTraceEntry[] {
// the two are joined by `tool_call_id`.
const argsByCallId = new Map<string, string>()
for (const message of messages) {
for (const call of message.tool_calls ?? []) {
// A job result is whatever its script returned, and the shape check that got
// us here only proves each message has a `role`. Anything nested is still
// arbitrary, and a throw here would take the whole result viewer down with
// it — including the plain error a lookalike payload is usually attached to.
if (!Array.isArray(message.tool_calls)) {
continue
}
for (const call of message.tool_calls) {
if (call.id && typeof call.function?.arguments === 'string') {
argsByCallId.set(call.id, call.function.arguments)
}