Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/types/__tests__/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,52 @@ describe('Message.fromMessageData', () => {
} as unknown as MessageData
expect(() => Message.fromMessageData(messageData)).toThrow('Unknown ContentBlockData type')
})
it('handles ToolUseBlock data with type discriminator instead of nested wrapper (issue #533)', () => {
// When content blocks are spread ({...block}) or stored by ORMs that flatten
// the nested structure, the data has {type: 'toolUseBlock', name, toolUseId, input}
// instead of {toolUse: {name, toolUseId, input}}
const messageData = {
role: 'assistant' as const,
content: [
{ type: 'toolUseBlock', name: 'greet', toolUseId: 'tu_123', input: { name: 'Alice' } },
],
} as unknown as MessageData
const message = Message.fromMessageData(messageData)
expect(message.content).toHaveLength(1)
expect(message.content[0].type).toBe('toolUseBlock')
const block = message.content[0] as ToolUseBlock
expect(block.name).toBe('greet')
expect(block.toolUseId).toBe('tu_123')
expect(block.input).toEqual({ name: 'Alice' })
})

it('handles TextBlock data with type discriminator instead of nested wrapper', () => {
const messageData = {
role: 'user' as const,
content: [
{ type: 'textBlock', text: 'hello' },
],
} as unknown as MessageData
const message = Message.fromMessageData(messageData)
expect(message.content).toHaveLength(1)
expect(message.content[0].type).toBe('textBlock')
expect((message.content[0] as TextBlock).text).toBe('hello')
})

it('handles ToolResultBlock data with type discriminator instead of nested wrapper', () => {
const messageData = {
role: 'user' as const,
content: [
{ type: 'toolResultBlock', toolUseId: 'tu_123', content: [], status: 'success' },
],
} as unknown as MessageData
const message = Message.fromMessageData(messageData)
expect(message.content).toHaveLength(1)
expect(message.content[0].type).toBe('toolResultBlock')
const block = message.content[0] as ToolResultBlock
expect(block.toolUseId).toBe('tu_123')
})

})

describe('systemPromptFromData', () => {
Expand Down
20 changes: 20 additions & 0 deletions src/types/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,26 @@ export function contentBlockFromData(data: ContentBlockData): ContentBlock {
} else if ('citations' in data) {
return CitationsBlock.fromJSON(data)
} else {
// Fallback: handle data with a type discriminator instead of nested wrappers.
// This occurs when content blocks are spread ({...block}) or stored by ORMs
// that flatten the nested structure produced by toJSON().
const typed = data as Record<string, unknown>
if (typed.type === 'toolUseBlock' && typeof typed.name === 'string' && typeof typed.toolUseId === 'string') {
return new ToolUseBlock({
name: typed.name,
toolUseId: typed.toolUseId,
input: typed.input as JSONValue,
...(typed.reasoningSignature !== undefined && { reasoningSignature: typed.reasoningSignature as string }),
})
} else if (typed.type === 'textBlock' && typeof typed.text === 'string') {
return new TextBlock(typed.text)
} else if (typed.type === 'toolResultBlock' && typeof typed.toolUseId === 'string') {
return new ToolResultBlock({
toolUseId: typed.toolUseId,
content: Array.isArray(typed.content) ? (typed.content as ToolResultContent[]) : [],
status: (typed.status as 'success' | 'error') ?? 'success',
})
}
throw new Error('Unknown ContentBlockData type')
}
}