A focused demo of the framework agent tools pattern: during a chat turn, the assistant dispatches chat-capable sub-agents, streams their timelines inline, and keeps those sub-agents available for replay and drill-in.
This example now uses the shipped primitives:
agentTool(Researcher, ...)for ordinary LLM-selected helper calls.this.runAgentTool(Researcher, ...)for explicit fan-out from thecomparetool.useAgentToolEvents({ agent })to collectagent-tool-eventframes in React.clearAgentToolRuns()to delete retained child facets when the chat is cleared.
For the general API guide, see docs/agent-tools.md.
npm install
npm startOpen the dev URL and ask for research or planning:
- Research the top three Rust web frameworks and compare their throughput.
- Find me three good arguments for and against monorepos.
- What changed in HTTP/3 versus HTTP/2?
- Plan how I should add rate limiting to a Worker.
The assistant can call research, plan, or compare. Each call starts a real
Think sub-agent (Researcher or Planner) with its own model, tools, messages,
SQLite storage, and resumable chat stream. The parent forwards the child stream
as agent-tool-event frames and the UI renders a live mini-chat panel under the
matching tool call.
Browser ──ws──▶ Assistant DO ──┬──▶ chat stream
│
└──▶ agent-tool-event frames
│
▼
Researcher / Planner facet
The important pieces are:
- Agent tools as real chat agents. Children extend
Think, so each helper has its own prompt, tools, stream durability, and direct chat endpoint. - Inline child timelines. The parent broadcasts
started,chunk, and terminal events tagged with the parenttoolCallId. The React hook groups runs by tool call and rebuilds child message parts from stored AI SDK chunks. - Parallel fan-out.
comparedispatches twoResearcherruns with the same parent tool call id and different display order values, so both panels render under one tool part. - Drill-in. Each panel has an open button that connects directly to
/sub/{agent}/{runId}withuseAgentChat; it is the child agent's real chat, not a synthetic event viewer. - Retention and cleanup. Runs are retained for refresh replay until the user clears the chat, which calls the framework cleanup API.
src/server.ts defines three agents:
Assistant extends Thinkis the parent chat agent. It exposesresearchandplanthroughagentTool(...), and implementscompareby callingrunAgentTool(...)twice in parallel.Researcher extends Thinkuses a simulatedweb_searchtool and summarizes a topic.Planner extends Thinkuses a simulatedinspect_filetool and writes an implementation plan.
The example also keeps the production sub-agent gate:
override async onBeforeSubAgent(_request, child) {
if (!this.hasAgentToolRun(child.className, child.name)) {
return new Response("Not found", { status: 404 });
}
}That prevents callers from inventing arbitrary child names and opening empty facets.
src/client.tsx uses one parent connection:
const agent = useAgent({ agent: "Assistant", name: USER });
const chat = useAgentChat({ agent });
const agentTools = useAgentToolEvents({ agent });agentTools.runsByToolCallId is passed into the message renderer. When a tool
part appears, the renderer looks up child runs with the same toolCallId and
renders a panel for each run.
For drill-in, the panel opens a direct sub-agent connection:
const helperAgent = useAgent({
agent: "Assistant",
name: USER,
sub: [{ agent: helperType, name: runId }]
});
const { messages, sendMessage } = useAgentChat({ agent: helperAgent });Run the example tests and typecheck with:
npm test
npx tsc -p examples/agents-as-tools/tsconfig.json --noEmitThe example-level tests validate the public contract it relies on: retained
agent-tool rows, drill-in gating, and cleanup. Deeper framework coverage lives
in packages/agents and packages/think.