Agent as Tool
Delegate bounded sub-tasks to a specialist agent exposed as a regular AI SDK tool
Turn any agent into a callable tool with asTool(). The parent agent sees it as
a normal tool; under the hood it forks the context, runs a one-shot generate(),
and returns the result without touching the parent's persisted thread.
Scenario: Research-Backed Article Writer
A writer agent drafts articles. When it needs facts, it calls a researcher agent exposed as a tool. The researcher queries a search API, synthesizes findings, and hands a brief back to the writer.
import { groq } from '@ai-sdk/groq';
import { tool } from 'ai';
import z from 'zod';
import {
ContextEngine,
InMemoryContextStore,
agent,
role,
user,
} from '@deepagents/context';
// -- Researcher agent --------------------------------------------------------
const researchStore = new InMemoryContextStore();
const researchContext = new ContextEngine({
store: researchStore,
chatId: 'researcher',
userId: 'system',
}).set(
role(
'You are a research assistant. Gather facts, cite sources, and return a concise brief.',
),
);
const webSearch = tool({
description: 'Search the web for a query and return results.',
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => {
// Replace with your real search API
return `[stub] Top results for "${query}"`;
},
});
const researcher = agent({
name: 'researcher',
context: researchContext,
model: groq('gpt-oss-20b'),
tools: { webSearch },
});
// -- Writer agent ------------------------------------------------------------
const writerStore = new InMemoryContextStore();
const writerContext = new ContextEngine({
store: writerStore,
chatId: 'writer',
userId: 'system',
}).set(
role(
'You are a staff writer. Draft well-structured articles backed by research.',
),
user(
'Write a 500-word article about the impact of LLMs on software testing.',
),
);
const writer = agent({
name: 'writer',
context: writerContext,
model: groq('gpt-oss-20b'),
tools: {
research: researcher.asTool({
toolDescription:
'Research a topic and return a fact brief. Pass the topic as input.',
outputExtractor: async (result) => result.text,
}),
},
});
const stream = await writer.stream({});
for await (const part of stream.toUIMessageStream()) {
if (part.type === 'text-delta') {
process.stdout.write(part.delta);
}
}What Happens at Runtime
writer.stream()
├─ model decides to call research("LLMs in software testing")
│ └─ asTool() forks researchContext
│ └─ researcher.generate() with forked context + user input
│ ├─ model calls webSearch("LLMs software testing")
│ └─ returns synthesized brief
├─ writer receives the brief as a tool result
└─ writer drafts the article using the briefThe fork means the researcher's messages never leak into the writer's stored conversation, and vice versa.
Scenario: Multi-Step Data Pipeline
A coordinator agent orchestrates two specialist agents — one extracts data, the other transforms it — each exposed as a tool.
const extractor = agent({
name: 'extractor',
context: extractorContext,
model: groq('gpt-oss-20b'),
tools: { queryDatabase },
});
const transformer = agent({
name: 'transformer',
context: transformerContext,
model: groq('gpt-oss-20b'),
});
const coordinator = agent({
name: 'coordinator',
context: coordinatorContext,
model: groq('gpt-oss-20b'),
tools: {
extract: extractor.asTool({
toolDescription:
'Query raw data from the database. Pass the question as input.',
}),
transform: transformer.asTool({
toolDescription:
'Clean and reshape raw data. Pass the raw data as input, describe the desired format in output.',
}),
},
});The coordinator calls extract first, pipes the result into transform, and
combines both outputs into a final report.
asTool() Options
| Option | Type | Description |
|---|---|---|
toolDescription | string | Override the auto-generated tool description |
outputExtractor | (result) => T | Promise<T> | Transform the sub-agent result before returning it to the parent |
metadata | JSONObject | Not sent to the model; surfaces as toolMetadata on tool call/result and UI message parts |
toModelOutput | Tool['toModelOutput'] | Narrow a structured output down to what the model should read |
When outputExtractor is omitted, the tool returns the sub-agent's final text
(result.text).
outputExtractor may return any serializable value, not just a string. Pair a
structured return with toModelOutput so the parent model reads only the answer
while the UI keeps the full payload:
analyst.asTool<{ answer: string }>({
metadata: { kind: 'subagent' },
outputExtractor: (result) => ({ answer: result.text.trim() }),
toModelOutput: ({ output }) => ({
type: 'text',
value: typeof output === 'string' ? output : output.answer,
}),
});The sub-agent's own context is what gets forked, once per invocation. To give
it a system prompt of its own rather than the caller's, build that context from
the sub-agent's own fragments and hand it to agent({ context }) — asTool()
forks it for each call, so invocations stay isolated from each other.
Pass the output type explicitly when you combine outputExtractor with
toModelOutput. TypeScript contextually types toModelOutput before
outputExtractor can infer it, so without the type argument the output collapses
to string.
The typeof output === 'string' guard is required: when the sub-agent throws a
non-abort error, asTool() returns a diagnostic string regardless of the
extractor's return type.
Errors and cancellation
A failing sub-agent returns an An error thrown from a tool call. string so the
parent model can recover. An AbortError is re-thrown instead, producing a
tool-error part rather than a tool result the model would mistake for an
answer.
When to Use asTool()
Use asTool() when the parent agent needs a bounded, self-contained sub-task
completed — research a topic, extract data, generate a summary. The sub-agent
runs to completion and returns a result; it does not persist state or participate
in the parent's ongoing conversation.
For open-ended strategic guidance where the advisor sees the full conversation, use asAdvisor() instead.