- 
        Couldn't load subscription status. 
- Fork 61
Description
Just upgraded to 0.2.1 and found that OpenAI is having a bug with using the web search tool.
The error I'm getting is:
8/20/2025, 11:13:03 AM [CONVEX A(agents:executePromptRun)] Uncaught Error: ArgumentValidationError: Value does not match validator.
Path: .messages[0].message
Value: {content: [{args: "<prompt removed>", toolCallId: "ws_68a53d3a37848193a9b1f4b664d3e97603d3ba2133a3a382", toolName: "web_search_preview", type: "tool-call"}, {result: {type: "json", value: {query: "<prompt removed>", status: "completed"}}, toolCallId: "ws_68a53d3a37848193a9b1f4b664d3e97603d3ba2133a3a382", toolName: "web_search_preview", type: "tool-result"}, {providerOptions: {openai: {itemId: "msg_68a53d3c85708193a32374be3fccc95f03d3ba2133a3a382"}}, text: "<Text response>", type: "text"}], role: "assistant"}
Validator: v.union(v.object({content: v.union(v.string(), v.array(v.union(v.object({providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), text: v.string(), type: v.literal("text")}), v.object({image: v.union(v.string(), v.bytes()), mimeType: v.optional(v.string()), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), type: v.literal("image")}), v.object({data: v.union(v.string(), v.bytes()), filename: v.optional(v.string()), mimeType: v.string(), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), type: v.literal("file")})))), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), role: v.literal("user")}), v.object({content: v.union(v.string(), v.array(v.union(v.object({providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), text: v.string(), type: v.literal("text")}), v.object({data: v.union(v.string(), v.bytes()), filename: v.optional(v.string()), mimeType: v.string(), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), type: v.literal("file")}), v.object({providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), signature: v.optional(v.string()), state: v.optional(v.union(v.literal("streaming"), v.literal("done"))), text: v.string(), type: v.literal("reasoning")}), v.object({data: v.string(), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), type: v.literal("redacted-reasoning")}), v.object({args: v.any(), providerExecuted: v.optional(v.boolean()), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), toolCallId: v.string(), toolName: v.string(), type: v.literal("tool-call")})))), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), role: v.literal("assistant")}), v.object({content: v.array(v.object({args: v.optional(v.any()), experimental_content: v.optional(v.array(v.union(v.object({text: v.string(), type: v.literal("text")}), v.object({data: v.string(), mimeType: v.optional(v.string()), type: v.literal("image")})))), isError: v.optional(v.boolean()), providerExecuted: v.optional(v.boolean()), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), result: v.any(), toolCallId: v.string(), toolName: v.string(), type: v.literal("tool-result")})), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), role: v.literal("tool")}), v.object({content: v.string(), providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), role: v.literal("system")}))
Here is the structure a bit better laid out:
{
  "content": [
    {
      "args": "<Prompt>",
      "toolCallId": "ws*68a53749ec888194aac7fa1386ad722a099480cbbec2232f",
      "toolName": "web_search_preview",
      "type": "tool-call"
    },
    {
      "result": {
        "type": "json",
        "value": {
          "query": "<Prompt>",
          "status": "completed"
        }
      },
      "toolCallId": "ws_68a53749ec888194aac7fa1386ad722a099480cbbec2232f",
      "toolName": "web_search_preview",
      "type": "tool-result"
    },
    {
      "providerOptions": {
        "openai": {
          "itemId": "msg_68a5374cec30819496c5bbf1cdc48c61099480cbbec2232f"
        }
      },
      "text": "Response",
      "type": "text"
    }
  ],
  "role": "assistant"
}
I've dug into the code and found that that assistant only expects to have the tool-call, not the tool-result.
https://github.com/get-convex/agent/blob/main/src/validators.ts#L96C1-L107C3
I checked out the code and tried to add that vToolResultPart but the addMessages needs updating to handle that case and I wasn't sure if I was on the right track.
This is using gpt-4.1 with the following config:
await thread.generateText({
  prompt: 'Who is the latest company by market value?',
  tools: {
    web_search_preview: openai.tools.webSearchPreview({
      searchContextSize: 'high',
      userLocation: {
        type: 'approximate',
        city: 'Perth',
        region: 'Western Australia',
        country: 'AU',
        timezone: 'Australia/Perth',
      },
    }),
  },
  toolChoice: {
    type: 'tool',
    toolName: 'web_search_preview',
  },
})
Also tried GPT-5 (on a whim) and after removing the toolChoice I get this error:
8/20/2025, 11:40:55 AM [CONVEX A(agents:executePromptRun)] Uncaught Error: ArgumentValidationError: Value does not match validator.
Path: .messages[0].reasoningDetails[0]
Value: {providerMetadata: {openai: {itemId: "rs_68a543c1b20c8190b487ed7cdd309d760c2d24ffe2fb3ff3", reasoningEncryptedContent: null}}, text: "", type: "reasoning"}
Validator: v.union(v.object({providerOptions: v.optional(v.record(v.string(), v.record(v.string(), v.any()))), signature: v.optional(v.string()), state: v.optional(v.union(v.literal("streaming"), v.literal("done"))), text: v.string(), type: v.literal("reasoning")}), v.object({signature: v.optional(v.string()), text: v.string(), type: v.literal("text")}), v.object({data: v.string(), type: v.literal("redacted")}))
Happy to investigate / test further, just need a steer if adding more pathways to the addMessage is the correct solution.