mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
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:
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user