diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index 9897984d..df39e925 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -221,6 +221,7 @@ export class Agent { tools: this._state.tools, model, reasoning, + validateToolCallsAtProvider: false, getQueuedMessages: async () => { // Return queued messages based on queue mode if (this.queueMode === "one-at-a-time") { diff --git a/packages/agent/src/transports/AppTransport.ts b/packages/agent/src/transports/AppTransport.ts index 5beb9dc6..0404f37a 100644 --- a/packages/agent/src/transports/AppTransport.ts +++ b/packages/agent/src/transports/AppTransport.ts @@ -77,6 +77,7 @@ function streamSimpleProxy( temperature: options.temperature, maxTokens: options.maxTokens, reasoning: options.reasoning, + validateToolCallsAtProvider: options.validateToolCallsAtProvider, // Don't send apiKey or signal - those are added server-side }, }), @@ -365,6 +366,7 @@ export class AppTransport implements AgentTransport { model: cfg.model, reasoning: cfg.reasoning, getQueuedMessages: cfg.getQueuedMessages, + validateToolCallsAtProvider: cfg.validateToolCallsAtProvider ?? false, }; // Yield events from the upstream agentLoop iterator diff --git a/packages/agent/src/transports/ProviderTransport.ts b/packages/agent/src/transports/ProviderTransport.ts index c46b16c0..1435b160 100644 --- a/packages/agent/src/transports/ProviderTransport.ts +++ b/packages/agent/src/transports/ProviderTransport.ts @@ -65,6 +65,7 @@ export class ProviderTransport implements AgentTransport { reasoning: cfg.reasoning, apiKey, getQueuedMessages: cfg.getQueuedMessages, + validateToolCallsAtProvider: cfg.validateToolCallsAtProvider ?? false, }; // Yield events from agentLoop diff --git a/packages/agent/src/transports/types.ts b/packages/agent/src/transports/types.ts index d5d4053c..43982a69 100644 --- a/packages/agent/src/transports/types.ts +++ b/packages/agent/src/transports/types.ts @@ -9,6 +9,7 @@ export interface AgentRunConfig { model: Model; reasoning?: "low" | "medium" | "high"; getQueuedMessages?: () => Promise[]>; + validateToolCallsAtProvider?: boolean; } /** diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index eda79305..9cd5b522 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added `validateToolCallsAtProvider` option to streaming and agent APIs to optionally skip provider-level tool-call validation (default on), allowing agent loops to surface schema errors as toolResult messages and retry. + ## [0.13.0] - 2025-12-06 ### Breaking Changes diff --git a/packages/ai/README.md b/packages/ai/README.md index c633fa76..acd4b78e 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -270,6 +270,20 @@ for await (const event of s) { - Full validation only occurs at `toolcall_end` when arguments are complete - The Google provider does not support function call streaming. Instead, you will receive a single `toolcall_delta` event with the full arguments. +### Provider tool-call validation + +By default, providers validate streamed tool calls against your tool schema and abort the stream on validation errors. Set `validateToolCallsAtProvider: false` on `stream`, `streamSimple`, `complete`, `completeSimple`, or `AgentLoopConfig` to skip provider-level validation and let downstream code (for example, `agentLoop` via `executeToolCalls` → `validateToolArguments`) surface schema errors as `toolResult` messages. This enables the model to retry after receiving a validation error. + +```typescript +await streamSimple(model, context, { + apiKey: 'your-key', + validateToolCallsAtProvider: false +}); +``` + +- `true` (default): Provider validates tool calls and emits an error if arguments do not match the schema +- `false`: Provider emits tool calls even when arguments are invalid; callers must validate and handle errors themselves + ### Complete Event Reference All streaming events emitted during assistant message generation: diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index e2e91be2..d3421553 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -92,6 +92,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = ( options?: AnthropicOptions, ): AssistantMessageEventStream => { const stream = new AssistantMessageEventStream(); + const shouldValidateToolCalls = options?.validateToolCallsAtProvider !== false; (async () => { const output: AssistantMessage = { @@ -233,7 +234,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = ( block.arguments = parseStreamingJson(block.partialJson); // Validate tool arguments if tool definition is available - if (context.tools) { + if (shouldValidateToolCalls && context.tools) { const tool = context.tools.find((t) => t.name === block.name); if (tool) { block.arguments = validateToolArguments(tool, block); diff --git a/packages/ai/src/providers/google.ts b/packages/ai/src/providers/google.ts index 9d3ade4f..e571aeb0 100644 --- a/packages/ai/src/providers/google.ts +++ b/packages/ai/src/providers/google.ts @@ -43,6 +43,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = ( options?: GoogleOptions, ): AssistantMessageEventStream => { const stream = new AssistantMessageEventStream(); + const shouldValidateToolCalls = options?.validateToolCallsAtProvider !== false; (async () => { const output: AssistantMessage = { @@ -167,7 +168,7 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = ( }; // Validate tool arguments if tool definition is available - if (context.tools) { + if (shouldValidateToolCalls && context.tools) { const tool = context.tools.find((t) => t.name === toolCall.name); if (tool) { toolCall.arguments = validateToolArguments(tool, toolCall); diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index a3c0a17e..dafacd9e 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -37,6 +37,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( options?: OpenAICompletionsOptions, ): AssistantMessageEventStream => { const stream = new AssistantMessageEventStream(); + const shouldValidateToolCalls = options?.validateToolCallsAtProvider !== false; (async () => { const output: AssistantMessage = { @@ -86,7 +87,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = ( block.arguments = JSON.parse(block.partialArgs || "{}"); // Validate tool arguments if tool definition is available - if (context.tools) { + if (shouldValidateToolCalls && context.tools) { const tool = context.tools.find((t) => t.name === block.name); if (tool) { block.arguments = validateToolArguments(tool, block); diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 76a582be..9b2c4fb6 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -45,6 +45,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( options?: OpenAIResponsesOptions, ): AssistantMessageEventStream => { const stream = new AssistantMessageEventStream(); + const shouldValidateToolCalls = options?.validateToolCallsAtProvider !== false; // Start async processing (async () => { @@ -240,7 +241,7 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = ( }; // Validate tool arguments if tool definition is available - if (context.tools) { + if (shouldValidateToolCalls && context.tools) { const tool = context.tools.find((t) => t.name === toolCall.name); if (tool) { toolCall.arguments = validateToolArguments(tool, toolCall); diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 94fb21b1..9c5b20d7 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -120,6 +120,7 @@ function mapOptionsForApi( maxTokens: options?.maxTokens || Math.min(model.maxTokens, 32000), signal: options?.signal, apiKey: apiKey || options?.apiKey, + validateToolCallsAtProvider: options?.validateToolCallsAtProvider ?? true, }; switch (model.api) { diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index a7269bc8..b3b8a885 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -37,6 +37,12 @@ export interface StreamOptions { maxTokens?: number; signal?: AbortSignal; apiKey?: string; + /** + * Controls whether providers validate streamed tool calls against the tool schema. + * Defaults to true. Set to false to skip provider-level validation and allow + * downstream consumers (e.g., agentLoop) to handle validation failures. + */ + validateToolCallsAtProvider?: boolean; } // Unified options with reasoning passed to streamSimple() and completeSimple()