Agents Workshop

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/openai

Replace 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.

lib/agent.ts
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:

lib/agent.ts
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:

app/api/chat/route.ts
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:

app/page.tsx
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.

app/page.tsx
"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.