fix: filter empty messages from conversation history to prevent validation errors

The AI SDK can produce assistant messages with empty parts (parts:[]) when
a stream is aborted, and providers reject assistant messages with empty text
content. This adds a validation utility that filters both cases before
sending messages to createAgentUIStreamResponse and when persisting them.
This commit is contained in:
shivammittal274
2026-03-15 17:42:34 +05:30
parent ecd31efcb0
commit 46031ed573
2 changed files with 50 additions and 2 deletions

View File

@@ -0,0 +1,46 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { UIMessage } from 'ai'
/**
* Checks whether a UIMessage has meaningful content that can be sent
* to the AI provider without causing validation errors.
*
* Two layers of validation can reject messages:
*
* 1. **AI SDK** (`validate-ui-messages.ts`):
* - `parts` array must be `.nonempty()` — rejects `parts: []`
*
* 2. **Provider API** (e.g. Gemini `generateContent`, Anthropic, OpenAI):
* - Assistant messages with only empty-string text are rejected
* as semantically empty, even though the SDK schema allows it
*
* This function guards against both layers so callers can filter
* messages before passing them to `createAgentUIStreamResponse`.
*/
export function hasMessageContent(message: UIMessage): boolean {
if (message.parts.length === 0) return false
// A message that contains any non-text part (tool invocation, reasoning,
// file, step-start, etc.) is always considered valid — those part types
// carry meaning regardless of text content.
const hasNonTextPart = message.parts.some((p) => p.type !== 'text')
if (hasNonTextPart) return true
// All parts are text — at least one must have non-whitespace content.
return message.parts.some(
(p) => p.type === 'text' && p.text.trim().length > 0,
)
}
/**
* Filters a UIMessage array, removing messages that would fail
* SDK validation or provider-level content checks.
*/
export function filterValidMessages(messages: UIMessage[]): UIMessage[] {
return messages.filter(hasMessageContent)
}

View File

@@ -8,6 +8,7 @@ import { mkdir, utimes } from 'node:fs/promises'
import path from 'node:path'
import { createAgentUIStreamResponse, type UIMessage } from 'ai'
import { AiSdkAgent } from '../../agent/ai-sdk-agent'
import { filterValidMessages } from '../../agent/message-validation'
import { formatUserMessage } from '../../agent/format-message'
import type { SessionStore } from '../../agent/session-store'
import type { ResolvedAgentConfig } from '../../agent/types'
@@ -139,6 +140,7 @@ export class ChatService {
if (isNewSession && request.previousConversation?.length) {
for (const msg of request.previousConversation) {
if (!msg.content.trim()) continue
session.agent.messages.push({
id: crypto.randomUUID(),
role: msg.role === 'assistant' ? 'assistant' : 'user',
@@ -168,10 +170,10 @@ export class ChatService {
return createAgentUIStreamResponse({
agent: session.agent.toolLoopAgent,
uiMessages: session.agent.messages,
uiMessages: filterValidMessages(session.agent.messages),
abortSignal,
onFinish: async ({ messages }: { messages: UIMessage[] }) => {
session.agent.messages = messages
session.agent.messages = filterValidMessages(messages)
logger.info('Agent execution complete', {
conversationId: request.conversationId,
totalMessages: messages.length,