Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .changeset/fix-ag-ui-tool-call-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@voltagent/ag-ui": patch
---

fix: use `input` instead of `args` for tool-call parts in message conversion

When converting CopilotKit assistant messages with tool calls to VoltAgent format,
the adapter was setting `args` on tool-call parts. The AI SDK's `ToolCallPart`
interface expects `input`, causing the Anthropic provider to send `undefined` as
the tool_use input — rejected by the API with:

"messages.N.content.N.tool_use.input: Input should be a valid dictionary"
6 changes: 3 additions & 3 deletions packages/ag-ui/src/voltagent-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ const isToolMessage = (message: Message): message is ToolMessage => message.role

type VoltUIPart =
| { type: "text"; text: string }
| { type: "tool-call"; toolCallId: string; toolName: string; args?: unknown }
| { type: "tool-call"; toolCallId: string; toolName: string; input?: unknown }
| { type: "tool-result"; toolCallId?: string; toolName?: string; output?: unknown };
Comment on lines +279 to 280
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In the current AI SDK / Anthropic tool-use contract, is tool_use.inputrequired to be a JSON object/dictionary, and what is the TypeScript shape ofToolCallPart.input?

💡 Result:

Anthropic tool-use contract (tool_use.input)

Yes—tool_use.input is required, and it is a parsed JSON object (i.e., a dictionary) containing the arguments for the tool, conforming to the tool’s input_schema. (docs.anthropic.com)

AI SDK TypeScript shape (ToolCallPart.input)

In the AI SDK, ToolCallPart.input is typed as unknown (i.e., not restricted to an object at the type level). The shape is: (app.unpkg.com)

export interface ToolCallPart {
  type: 'tool-call';
  toolCallId: string;
  toolName: string;
  input: unknown;
  providerOptions?: ProviderOptions;
  providerExecuted?: boolean;
}

Citations:


Normalize tool-call input to an object, not unknown.

The Anthropic API requires tool_use.input to be a JSON object, but the current code at line 314 can produce a string, array, primitive, or null via safelyParseJson(), and line 279 allows that invalid value through unchecked with the input?: unknown type. This will cause requests to fail at runtime.

Proposed fix
+type VoltToolInput = Record<string, unknown>;
+
 type VoltUIPart =
   | { type: "text"; text: string }
-  | { type: "tool-call"; toolCallId: string; toolName: string; input?: unknown }
+  | { type: "tool-call"; toolCallId: string; toolName: string; input: VoltToolInput }
   | { type: "tool-result"; toolCallId?: string; toolName?: string; output?: unknown };
@@
-        const input = safelyParseJson(call.function.arguments);
+        const parsedInput = safelyParseJson(call.function.arguments);
+        if (
+          parsedInput === null ||
+          typeof parsedInput !== "object" ||
+          Array.isArray(parsedInput)
+        ) {
+          throw new Error(
+            `Invalid tool-call arguments for "${call.function.name}": expected a JSON object`,
+          );
+        }
         toolNameById.set(call.id, call.function.name);
         parts.push({
           type: "tool-call",
           toolCallId: call.id,
           toolName: call.function.name,
-          input,
+          input: parsedInput,
         });

Lines 279–280, 314–320.


type VoltUIMessage = {
Expand Down Expand Up @@ -311,13 +311,13 @@ function convertAGUIMessagesToVoltMessages(messages: Message[]): VoltUIMessage[]
}

for (const call of msg.toolCalls ?? []) {
const args = safelyParseJson(call.function.arguments);
const input = safelyParseJson(call.function.arguments);
toolNameById.set(call.id, call.function.name);
parts.push({
type: "tool-call",
toolCallId: call.id,
toolName: call.function.name,
args,
input,
});
}

Expand Down
Loading