Web Search
Let's give our agent access to real-time information by adding a web search tool.
Install the OpenAI provider package, which includes a built-in web search tool.
pnpm add @ai-sdk/openaiReplace the cowboy instructions with the web search tool. The openai.tools.webSearch() gives the agent the ability to search the web for up-to-date information.
import { openai } from "@ai-sdk/openai";
import { ToolLoopAgent } from "ai";
export const myAgent = new ToolLoopAgent({
model: "openai/gpt-5.4-mini",
tools: {
webSearch: openai.tools.webSearch(),
},
});Try asking the agent about something recent - it should now be able to search the web and respond with up-to-date information.
Now let's add type safety to our messages. The AI SDK can infer the exact message type from your agent definition using InferAgentUIMessage. This gives you type-safe access to tool-specific parts (like tool-webSearch) in your UI code.
Export the inferred message type from the agent file:
import { openai } from "@ai-sdk/openai";
import { type InferAgentUIMessage, ToolLoopAgent } from "ai";
export const myAgent = new ToolLoopAgent({
model: "openai/gpt-5.4-mini",
tools: {
webSearch: openai.tools.webSearch(),
},
});
export type MyAgentUIMessage = InferAgentUIMessage<typeof myAgent>; Use the type in the route handler:
import { createAgentUIStreamResponse } from "ai";
import { type MyAgentUIMessage, myAgent } from "@/lib/agent";
export const POST = async (req: Request) => {
const { messages }: { messages: MyAgentUIMessage[] } = await req.json();
return createAgentUIStreamResponse({ agent: myAgent, uiMessages: messages });
};And pass the type to useChat on the client:
import { MyAgentUIMessage } from "@/lib/agent";
// ...
const { messages, error, sendMessage } = useChat<MyAgentUIMessage>(); Now that we have type-safe messages, we can render the web search state in the UI. Add a tool-webSearch case to the parts renderer that shows a spinner while searching and a search icon when complete.
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
import { MyAgentUIMessage } from "@/lib/agent";
export default function Chat() {
const [input, setInput] = useState("");
const { messages, error, sendMessage } = useChat<MyAgentUIMessage>();
if (error) return <div>{error.message}</div>;
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
<div className="space-y-4">
{messages.map((m) =>
m.parts.map((p, i) => {
switch (p.type) {
case "text":
return (
<div key={i} className="whitespace-pre-wrap">
<div>
<div className="font-bold">{m.role}</div>
<p>{p.text}</p>
</div>
</div>
);
case "tool-webSearch":
return (
<div
key={i}
className="flex items-center gap-2 text-sm text-gray-500 py-1"
>
{p.state === "output-available" ? (
<>
<svg
className="size-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
/>
</svg>
Searched the web
</>
) : (
<>
<svg
className="size-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
/>
</svg>
Searching the web...
</>
)}
</div>
);
default:
return null;
}
}),
)}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={(e) => setInput(e.currentTarget.value)}
/>
</form>
</div>
);
}Now when the agent searches the web, you'll see a spinner while it's searching and a search icon once it's done. The p.state property lets you distinguish between in-progress and completed tool calls.