Skip to content

Bug: invalid tool-call input string can be persisted and replayed as normal tool input #1336

@lzj960515

Description

@lzj960515

Summary

A malformed tool-call argument string can be persisted as a normal UI tool part with input as a string. When that conversation is later replayed to an Anthropic model, the provider rejects the historical tool use with:

messages.N.content.M.tool_use.input: Input should be a valid dictionary

This appears to happen after the AI SDK marks a tool call as invalid because its JSON arguments could not be parsed. VoltAgent then converts that invalid model tool call into a UI tool part and persists the raw string under input.

Environment

Observed in an app using:

@voltagent/core: 2.7.4
@voltagent/postgres: 2.1.2
ai: 6.0.190
@ai-sdk/provider-utils: 4.0.27
Anthropic via Google Vertex

Investigation notes

1. Production error in logs

We first found a stream error in production logs:

AI_APICallError: messages.1.content.1.tool_use.input: Input should be a valid dictionary

The failing request was a continuation of a conversation that had a previous assistant tool call.

2. The stored conversation message had a string input

In the Postgres memory table (voltagent_memory_messages), the assistant message contained a tool UI part like this:

{
  "type": "tool-createImageFromText",
  "toolCallId": "toolu_...",
  "state": "output-available",
  "input": "{\"prompts\": [\"Full body fashion editorial portrait of a stunning 5'7\" woman ...\"], \"imageSize\": \"1024x1536\"}",
  "output": {
    "type": "error-text",
    "value": "Invalid input for tool createImageFromText: JSON parsing failed: ... Expected ',' or ']' after array element ..."
  },
  "providerExecuted": false
}

The important part is that input is a JSON string, not an object. In this real case the string was also malformed because the prompt included an unescaped quote: 5'7" woman.

3. Normal successful messages store input as an object

Later successful tool calls in the same conversation were stored like this:

{
  "type": "tool-createImageFromText",
  "toolCallId": "toolu_...",
  "state": "output-available",
  "input": {
    "prompts": ["..."],
    "imageSize": "1024x1536"
  },
  "output": {
    "type": "text",
    "value": "[\"https://...\"]"
  },
  "providerExecuted": false
}

So the issue is not that Postgres JSONB always stringifies tool inputs. It only happened for the invalid/malformed tool call.

4. AI SDK appears to preserve raw malformed input on invalid tool calls

Looking at the installed ai package, parseToolCall catches parse failures and returns an invalid tool call with the raw string as input:

const parsedInput = await safeParseJSON({ text: toolCall.input });
const input = parsedInput.success ? parsedInput.value : toolCall.input;

return {
  type: "tool-call",
  toolCallId: toolCall.toolCallId,
  toolName: toolCall.toolName,
  input,
  dynamic: true,
  invalid: true,
  error,
  providerExecuted: toolCall.providerExecuted,
  providerMetadata: toolCall.providerMetadata,
};

This explains why the raw malformed string exists after parsing failed.

5. VoltAgent converts that model message to a UI message and stores the raw string input

In @voltagent/core, convertModelMessagesToUIMessages converts model tool calls like this:

case "tool-call": {
  const toolPart = {
    type: `tool-${contentPart.toolName}`,
    toolCallId: contentPart.toolCallId,
    state: "input-available",
    input: contentPart.input || {},
    ...contentPart.providerOptions ? { callProviderMetadata: contentPart.providerOptions } : {},
    ...contentPart.providerExecuted != null ? { providerExecuted: contentPart.providerExecuted } : {}
  };
  ui.parts.push(toolPart);
  break;
}

Because contentPart.input is the raw string from the invalid AI SDK tool call, the UI message gets input as a string.

The persisted message did not appear to preserve the invalid: true / error information from the parsed model tool call, so on later replay this stored part looks like a normal tool call except that input is a string.

6. Replay to Anthropic fails

When that persisted conversation is used as history in a later model call, the tool part is converted/replayed as an Anthropic tool_use. Anthropic requires tool_use.input to be a dictionary/object, not a string, so it returns:

Input should be a valid dictionary

Impact

A single malformed tool-call argument string can poison the persisted conversation history. Future turns may fail at the model API layer even if the user simply sends a follow-up message such as "continue".

The original tool parsing error was understandable, but the confusing part is that the invalid raw input was persisted/replayed in a form that later triggers a provider-level schema error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions