Research Agent
Build an agent that delegates research tasks to specialized subagents
Build a research orchestrator that breaks complex questions into subtasks and delegates them to specialized subagents. Each subagent runs independently with its own tools, reports back, and the parent synthesizes the findings.
Quick start
Here is a minimal research agent with a single subagent. The full guide breaks this pattern into composable pieces.
import { Agent, run, subagent, tool } from "stratus-sdk/core";
import { AzureResponsesModel } from "stratus-sdk";
import { z } from "zod";
const model = new AzureResponsesModel({
endpoint: process.env.AZURE_ENDPOINT!,
apiKey: process.env.AZURE_API_KEY!,
deployment: "gpt-5.2",
});
const webSearch = tool({
name: "web_search",
description: "Search the web for information",
parameters: z.object({ query: z.string() }),
execute: async (_ctx, { query }) => {
const results = await searchAPI(query);
return JSON.stringify(results.slice(0, 5));
},
});
const researcher = new Agent({
name: "web_researcher",
model,
instructions: "Search for information and return key facts with source URLs.",
tools: [webSearch],
});
const researchSubagent = subagent({
agent: researcher,
inputSchema: z.object({ topic: z.string() }),
mapInput: (params) => `Research: ${params.topic}`,
});
const orchestrator = new Agent({
name: "orchestrator",
model,
instructions: "Break questions into sub-questions. Use run_web_researcher for each.",
subagents: [researchSubagent],
});
const result = await run(orchestrator, "What is the current state of renewable energy?");
console.log(result.output);What you'll build
A parent orchestrator that delegates to three domain-specific subagents:
Web Researcher
Searches the web and extracts key facts
Data Analyst
Performs calculations and data analysis
Summarizer
Condenses findings into structured reports
Step 1: Define subagent tools
Each subagent gets its own specialized tools. Keep tool sets small and focused -- a subagent with fewer tools produces more reliable results.
import { tool } from "stratus-sdk/core";
import { z } from "zod";
const webSearch = tool({
name: "web_search",
description: "Search the web for information",
parameters: z.object({
query: z.string().describe("Search query"),
}),
execute: async (_ctx, { query }) => {
const results = await searchAPI(query);
return JSON.stringify(results.slice(0, 5));
},
});
const fetchPage = tool({
name: "fetch_page",
description: "Fetch and extract text from a web page",
parameters: z.object({
url: z.string().describe("URL to fetch"),
}),
execute: async (_ctx, { url }, options) => {
const res = await fetch(url, { signal: options?.signal });
const text = await res.text();
return extractMainContent(text).slice(0, 3000);
},
});
const calculate = tool({
name: "calculate",
description: "Evaluate a math expression",
parameters: z.object({
expression: z.string(),
}),
execute: async (_ctx, { expression }) => {
return String(new Function(`return (${expression})`)());
},
});The fetchPage tool forwards options.signal to fetch(). When the parent run is cancelled, the HTTP request cancels too. See Cancellation with abort signal below.
Step 2: Create subagent definitions
Define one agent per research domain. Each gets a focused instruction set and only the tools it needs.
import { Agent, subagent } from "stratus-sdk/core";
const webResearcher = new Agent({
name: "web_researcher",
model,
instructions: `You are a web research specialist. Search for information,
visit relevant pages, and extract key facts. Return factual findings
with source URLs.`,
tools: [webSearch, fetchPage],
});
const dataAnalyst = new Agent({
name: "data_analyst",
model,
instructions: `You are a data analyst. Perform calculations, analyze numbers,
and identify trends. Return precise numerical results.`,
tools: [calculate],
});
const summarizer = new Agent({
name: "summarizer",
model,
instructions: `You are a research summarizer. Take raw findings and synthesize
them into a clear, structured summary with key takeaways.`,
});The summarizer has no tools. It only needs the model to restructure and condense text. Not every subagent needs tool access.
Step 3: Wire subagents to the parent
Use subagent() to create typed bridges between the parent and each child agent. The inputSchema defines what the model passes in, and mapInput converts those parameters into a prompt string.
const researchSubagent = subagent({
agent: webResearcher,
inputSchema: z.object({
topic: z.string().describe("What to research"),
}),
mapInput: (params) => `Research the following topic thoroughly: ${params.topic}`,
});
const analysisSubagent = subagent({
agent: dataAnalyst,
inputSchema: z.object({
question: z.string().describe("The data question to answer"),
data: z.string().describe("Relevant data or numbers to analyze"),
}),
mapInput: (params) => `Analyze: ${params.question}\n\nData: ${params.data}`,
});
const summarySubagent = subagent({
agent: summarizer,
inputSchema: z.object({
findings: z.string().describe("Raw research findings to summarize"),
}),
mapInput: (params) => `Summarize these findings:\n\n${params.findings}`,
});Step 4: Create the orchestrator
The parent agent sees each subagent as a callable tool named run_<agent_name>. Its instructions tell it when and how to use each one.
import { Agent, run } from "stratus-sdk/core";
const researchOrchestrator = new Agent({
name: "research_orchestrator",
model,
instructions: `You are a research orchestrator. When given a question:
1. Break it into sub-questions
2. Use run_web_researcher for factual lookups
3. Use run_data_analyst for numerical analysis
4. Use run_summarizer to compile findings
Be thorough but efficient.`,
subagents: [researchSubagent, analysisSubagent, summarySubagent],
});
const result = await run(
researchOrchestrator,
"What is the current state of renewable energy adoption globally? Include market size, growth rates, and top countries.",
);
console.log(result.output);Subagent names become tool names prefixed with run_. If your agent is named web_researcher, the parent calls it as run_web_researcher. Keep names short and descriptive.
Adding structured output
Get results in a typed format for downstream processing. Set outputType on the orchestrator to a Zod schema, and result.finalOutput is fully typed.
const ReportSchema = z.object({
title: z.string(),
summary: z.string(),
keyFindings: z.array(z.object({
finding: z.string(),
source: z.string().optional(),
confidence: z.enum(["high", "medium", "low"]),
})),
dataPoints: z.array(z.object({
metric: z.string(),
value: z.string(),
})),
});
const researchOrchestrator = new Agent({
name: "research_orchestrator",
model,
instructions: `...same as above...`,
subagents: [researchSubagent, analysisSubagent, summarySubagent],
outputType: ReportSchema,
});
const result = await run(researchOrchestrator, "...");
console.log(result.finalOutput.keyFindings); // Typed arrayWhen using outputType with subagents, the model calls subagents first, then produces the structured JSON in its final response. The subagents themselves return unstructured text unless they also have their own outputType.
Adding tracing
Monitor which subagents run and how long each takes. Wrap the run() call in withTrace() and inspect the resulting spans.
import { withTrace } from "stratus-sdk/core";
const { result, trace } = await withTrace("research_task", () =>
run(researchOrchestrator, "Analyze the EV market in 2025")
);
// See which subagents were called
const subagentSpans = trace.spans
.flatMap((s) => [s, ...s.children])
.filter((s) => s.type === "subagent");
for (const span of subagentSpans) {
console.log(`${span.name}: ${span.duration}ms`);
}
// subagent:web_researcher: 4523ms
// subagent:data_analyst: 1201ms
// subagent:summarizer: 2105msCancellation with abort signal
Cancel long-running research when the user disconnects. The signal propagates through the orchestrator, into every active subagent, and down to their tool executions.
const ac = new AbortController();
// Cancel if user disconnects
req.on("close", () => ac.abort());
try {
const result = await run(researchOrchestrator, question, {
signal: ac.signal,
});
res.json(result.finalOutput);
} catch (error) {
if (error instanceof RunAbortedError) {
console.log("Research cancelled by user");
}
}The abort signal propagates through to all subagent runs and their tool executions, so everything cancels cleanly. You do not need to wire up signals for each subagent individually.
Next steps
Last updated on