Call an agent
agent.invoke runs an agent from your own code, with no chat on the page; useWebAgent adds live status in a component.
An agent has the same verb as a tool: invoke. Give it a sentence, and it decides which tools to call: the model
asks for a tool, the tool runs on the page under the cursor, the result goes back, until the model answers. It is the
chat’s loop without the chat, for a button that does a whole task (“Track my last order”), a command palette, or an
onboarding step.
A complete example
One task, “Track my last order”, run two ways. Pick the tab that fits where the call is made:
In a component: the hook runs the agent and keeps its status live.
src/components/track-last-order.tsx
import { useWebAgent } from "@webmcp-agents/chat";
import { shopper } from "@/agents";
import { Button } from "@/components/ui/button";
export function TrackLastOrder() {
const agent = useWebAgent(shopper);
const busy = agent.status === "running" || agent.status === "waiting";
return (
<div className="flex flex-col items-start gap-3">
<div className="flex gap-2">
<Button
disabled={busy}
onClick={() => agent.invoke("Where is my last order?")}
>
{busy ? "Working…" : "Track my last order"}
</Button>
{busy && (
<Button variant="ghost" onClick={agent.stop}>
Stop
</Button>
)}
</div>
{/* Each tool call, live: running, waiting for you, done. */}
<ul className="text-sm text-muted-foreground">
{agent.steps.map((step) => (
<li key={step.id}>
{step.tool}: {step.state}
</li>
))}
</ul>
{agent.text && <p>{agent.text}</p>}
{agent.error && <p role="alert">{agent.error.message}</p>}
</div>
);
}Anywhere else (an event handler in plain code, a keyboard shortcut, a test): the agent you defined has invoke on it.
import { shopper } from "@/agents";
const { text, steps } = await shopper.invoke("Where is my last order?", {
onStep: (step) => console.log(step.tool, step.state),
});In a component: the hook runs the agent and keeps its status live.
components/track-last-order.tsx
"use client";
import { useWebAgent } from "@webmcp-agents/chat";
import { shopper } from "@/agents";
import { Button } from "@/components/ui/button";
export function TrackLastOrder() {
const agent = useWebAgent(shopper);
const busy = agent.status === "running" || agent.status === "waiting";
return (
<div className="flex flex-col items-start gap-3">
<div className="flex gap-2">
<Button
disabled={busy}
onClick={() => agent.invoke("Where is my last order?")}
>
{busy ? "Working…" : "Track my last order"}
</Button>
{busy && (
<Button variant="ghost" onClick={agent.stop}>
Stop
</Button>
)}
</div>
{/* Each tool call, live: running, waiting for you, done. */}
<ul className="text-sm text-muted-foreground">
{agent.steps.map((step) => (
<li key={step.id}>
{step.tool}: {step.state}
</li>
))}
</ul>
{agent.text && <p>{agent.text}</p>}
{agent.error && <p role="alert">{agent.error.message}</p>}
</div>
);
}Anywhere else (an event handler in plain code, a keyboard shortcut, a test): the agent you defined has invoke on it.
import { shopper } from "@/agents";
const { text, steps } = await shopper.invoke("Where is my last order?", {
onStep: (step) => console.log(step.tool, step.state),
});In a component: the hook runs the agent and keeps its status live.
src/components/track-last-order.tsx
import { useWebAgent } from "@webmcp-agents/chat";
import { shopper } from "@/agents";
import { Button } from "@/components/ui/button";
export function TrackLastOrder() {
const agent = useWebAgent(shopper);
const busy = agent.status === "running" || agent.status === "waiting";
return (
<div className="flex flex-col items-start gap-3">
<div className="flex gap-2">
<Button
disabled={busy}
onClick={() => agent.invoke("Where is my last order?")}
>
{busy ? "Working…" : "Track my last order"}
</Button>
{busy && (
<Button variant="ghost" onClick={agent.stop}>
Stop
</Button>
)}
</div>
{/* Each tool call, live: running, waiting for you, done. */}
<ul className="text-sm text-muted-foreground">
{agent.steps.map((step) => (
<li key={step.id}>
{step.tool}: {step.state}
</li>
))}
</ul>
{agent.text && <p>{agent.text}</p>}
{agent.error && <p role="alert">{agent.error.message}</p>}
</div>
);
}Anywhere else (an event handler in plain code, a keyboard shortcut, a test): the agent you defined has invoke on it.
import { shopper } from "@/agents";
const { text, steps } = await shopper.invoke("Where is my last order?", {
onStep: (step) => console.log(step.tool, step.state),
});In a component: the hook runs the agent and keeps its status live.
src/components/track-last-order.tsx
import { useWebAgent } from "@webmcp-agents/chat";
import { shopper } from "@/agents";
import { Button } from "@/components/ui/button";
export function TrackLastOrder() {
const agent = useWebAgent(shopper);
const busy = agent.status === "running" || agent.status === "waiting";
return (
<div className="flex flex-col items-start gap-3">
<div className="flex gap-2">
<Button
disabled={busy}
onClick={() => agent.invoke("Where is my last order?")}
>
{busy ? "Working…" : "Track my last order"}
</Button>
{busy && (
<Button variant="ghost" onClick={agent.stop}>
Stop
</Button>
)}
</div>
{/* Each tool call, live: running, waiting for you, done. */}
<ul className="text-sm text-muted-foreground">
{agent.steps.map((step) => (
<li key={step.id}>
{step.tool}: {step.state}
</li>
))}
</ul>
{agent.text && <p>{agent.text}</p>}
{agent.error && <p role="alert">{agent.error.message}</p>}
</div>
);
}Anywhere else (an event handler in plain code, a keyboard shortcut, a test): the agent you defined has invoke on it.
import { shopper } from "@/agents";
const { text, steps } = await shopper.invoke("Where is my last order?", {
onStep: (step) => console.log(step.tool, step.state),
});Both need the /api/chat route from Add the chat, but not the widget: by default an agent runs
on the same shared adapter, which talks to that route. Both run in the browser, on a page inside your provider, so
the agent sees the page’s tools.
What useWebAgent gives you
useWebAgent(agent, { adapter? }) returns:
invoke(text) |
Start a run. A second call stops the first. It never rejects: it resolves undefined when the run was stopped or failed, so onClick={() => agent.invoke("…")} needs no catch. |
status |
idle, running, waiting (a tool is parked on the page until the person clicks), or error. |
steps |
The tool calls of the current or last run, live, each with its tool, input and state. A sub-agent’s calls are included, with agent set. |
text |
The agent’s answer so far, streamed. |
error |
Why the last run failed, with a code. Cleared by the next invoke. |
stop() |
Cancel the run in flight. |
Each invoke is a fresh conversation: the agent remembers nothing from the last one.
What agent.invoke gives you
agent.invoke(text, { adapter?, signal?, onStep? }) is the same run with no component around it. It resolves to { text, steps, messages }: the answer, every tool call it made, and the whole conversation. Unlike the
hook, it rejects: with the adapter’s message and code when the run fails, with an AbortError when its signal
aborts, for an empty message, and on the server. invoke runs in the browser, because the agent’s tools are the
page’s; on the server, register the agent with createChatHandler({ agents }) instead.
The agent file stays plain data that a server can import: invoke loads the browser loop on its first call.
Another model for one call
Both take an adapter. Pass createLocalAdapter() to run the same agent on a model in the
browser, with no server and no key, or a createCloudAdapter({ … }) of your own. Create an adapter once, at module
scope, not on every render.
Try it
Render <TrackLastOrder /> inside your provider, start the app with the key in .env.local, and click Track my last
order. The button reads “Working…”, the list fills with delegate_to_orders, then the helper’s go_to,
list_orders and track_order, the cursor moves to /orders, and the answer appears below. Keep a link to /orders
in your layout: go_to clicks it, so your router navigates without reloading the page (a reload would end the run).
Options you’ll use most
| Option | Where | What it is for |
|---|---|---|
adapter |
both | What runs the model. Default: one shared cloud adapter that talks to /api/chat. |
signal |
agent.invoke |
Stop the run: the model, the tool in flight, any sub-agent. |
onStep(step) |
agent.invoke |
Called when a tool call starts, parks on the person’s confirmation, or ends. |
Reference: useWebAgent and defineWebAgent
(the agent’s invoke).