Give your Laravel AI agent real-time web knowledge
The Laravel AI SDK is provider-agnostic, but its built-in web tools aren't. Here's how to give your agent real-time web knowledge without losing that independence.
One of the things I like most about the Laravel AI SDK is that it doesn’t lock you into a provider. Swap OpenAI for Anthropic, add Gemini as a failover, and your agent code barely changes. That’s the entire pitch, and it mostly holds up.
Until your agent needs to browse the web. That is where I found the seam, and it is worth understanding before you build a failover chain and assume it covers you.
The provider-agnostic promise has a hole in it
The SDK ships two provider tools for giving agents access to the web: WebSearch and WebFetch. They’re convenient, a couple lines and your agent can search or fetch a page. Here is the catch, and it took me a moment to spot it: they’re not really part of the SDK’s unified layer. They’re implemented natively by whichever AI provider you’re using, which means their availability depends entirely on which model you picked.
WebSearch works on Anthropic, OpenAI, Gemini, and OpenRouter. WebFetch only works on Anthropic and Gemini. So if you configure a failover chain that includes Groq, DeepSeek, Mistral, or xAI, and one of those becomes the active provider, your agent quietly loses the ability to browse. Not an error, not a warning, just an agent that stops citing sources or stops noticing the page it was supposed to check even changed.
That’s a rough thing to discover in production. You built a failover chain specifically so a rate limit or outage wouldn’t take your feature down, and it turns out one of your fallback providers was silently missing a capability the whole time.
Even when it works, it’s pretty shallow
Set the provider problem aside for a second. Even on a provider where WebSearch and WebFetch are both available, what you get back is raw: search results or fetched page content that the model has to reason over itself. There’s no schema. There’s no guaranteed citation tracking. If you want your agent to return a structured, sourced answer, that reasoning happens somewhere in the model’s own inference, not in a layer you control or can test.
For a lot of use cases, that’s fine. For anything where the answer needs to be defensible or reproducible, a research assistant, a competitive brief, anything a user might ask “where did this come from,” it’s not enough.
Build the tool yourself, on infrastructure that doesn’t care which provider is active
So what do we do about it? We stop relying on provider-native web tools and give the agent a custom tool instead, one backed by an API rather than whichever model happens to be answering the prompt. That’s exactly what a Tool class in the Laravel AI SDK is for.
For the client underneath it I’ll use juststeveking/tabstack, which is the PHP library I wrote for the Tabstack API. Its agent()->research() method takes a question, searches multiple sources, synthesizes an answer, and streams back a report with citations, all as one call.
The package doesn’t ship Laravel integration out of the box, so the first step is binding it in a service provider so it can be injected anywhere:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;use JustSteveKing\Tabstack\Tabstack;
class AppServiceProvider extends ServiceProvider{ public function register(): void { $this->app->singleton(Tabstack::class, fn () => Tabstack::make( apiKey: config('services.tabstack.key'), )); }}Add the key to config/services.php:
'tabstack' => [ 'key' => env('TABSTACK_API_KEY'),],Now the tool itself. This wraps agent()->research() and hands the agent back a report with its sources listed:
<?php
namespace App\Ai\Tools;
use Illuminate\Contracts\JsonSchema\JsonSchema;use JustSteveKing\Tabstack\Requests\AgentResearch;use JustSteveKing\Tabstack\Requests\ResearchMode;use JustSteveKing\Tabstack\Tabstack;use Laravel\Ai\Contracts\Tool;use Laravel\Ai\Tools\Request;use Stringable;
class WebResearch implements Tool{ public function __construct( private readonly Tabstack $tabstack, ) {}
public function description(): Stringable|string { return 'Answers a question by researching multiple sources on the web and returns a synthesized, cited report. Use this when you need current information not in your training data.'; }
public function handle(Request $request): Stringable|string { $result = $this->tabstack->agent()->research( params: new AgentResearch( query: $request['query'], mode: ResearchMode::Fast, ), )->result();
$sources = collect($result->metadata->get('citedPages', [])) ->map(fn ($page) => "- {$page['title']}: {$page['url']}") ->implode("\n");
return "{$result->report}\n\nSources:\n{$sources}"; }
public function schema(JsonSchema $schema): array { return [ 'query' => $schema->string() ->required() ->description('The question to research'), ]; }}The ->result() call blocks until the stream finishes and hands back a typed ResearchResult rather than making you consume events yourself, which keeps the tool’s handle method simple. If you’d rather stream progress back to the agent as it happens, the client also gives you ->each() and raw iteration, but for a tool call the blocking result is usually what you want.
Wire it into any agent through the tools method, same as any other tool:
<?php
namespace App\Ai\Agents;
use App\Ai\Tools\WebResearch;use Laravel\Ai\Contracts\Agent;use Laravel\Ai\Contracts\HasTools;use Laravel\Ai\Promptable;
class MarketAnalyst implements Agent, HasTools{ use Promptable;
public function instructions(): string { return 'You help analyze markets and competitors. Use the research tool whenever you need current information. Always mention your sources.'; }
public function tools(): iterable { return [ app(WebResearch::class), ]; }}Now switch providers all you want:
$response = (new MarketAnalyst)->prompt( 'What are the current pricing trends for cloud browser automation APIs?', provider: [Lab::OpenAI, Lab::Anthropic, Lab::Groq],);The agent’s ability to research the web doesn’t depend on which of those three ends up answering the prompt. The tool is yours, it runs the same regardless of which model called it.
Why this beats a raw fetch too
You could build something similar with WebFetch on a supported provider, pointing it at a specific known URL. But that only works when you already know which page has the answer. /research is built for the opposite case, a question with no known source, where the value is in picking the right sources, reading them, and synthesizing a single answer with the receipts attached. That’s a genuinely different job than fetching one page, and it’s the piece the SDK’s built-in tools don’t attempt.
What I would take from this
The SDK’s provider independence is one of its best features, right up until you reach for a tool that isn’t actually part of that independence. That is the bit I want you to walk away with, more than any of the code above. Web access shouldn’t be something your agent quietly loses depending on which model answered the prompt.
The general lesson holds well beyond this one gap: when a capability matters to your feature, own it. Build it as a real Tool class against an API you control the contract with, rather than leaning on whatever the current provider happens to expose. Then swapping providers stays the config change it was sold as.
If you want the client I used here, it’s juststeveking/tabstack on Packagist.
Keep Reading
Eighteen Files And One Nix Module: How My Brain Works
The Obsidian vault I use as a system of record for every project, why the checkouts inside it are gitignored, and why half of it is generated from my NixOS flake.
Aug 2026 · 10 min read
DevToolsA Proper Look at Tabstack
A developer's review of Tabstack, the Mozilla-backed web API that gives AI agents clean extraction, browser automation, and cited research without a scraper.
Jun 2026 · 11 min read
DevToolsGiving your agents a terminal: a first look at the tabstack CLI
A hands-on look at Mozilla's tabstack CLI: a single Go binary that turns any URL into clean Markdown or JSON, runs browser automation, and scripts neatly.
Jun 2026 · 9 min read