Skip to content

Commit eadf2a3

Browse files
chore(deepagents): verify we can run nested deepagents (#207)
1 parent 2eea576 commit eadf2a3

File tree

5 files changed

+496
-12
lines changed

5 files changed

+496
-12
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/* eslint-disable no-console */
2+
/**
3+
* Hierarchical Deep Agent Example
4+
*
5+
* Demonstrates multi-level agent hierarchies where a deep agent acts as a
6+
* subagent of another deep agent. This pattern enables:
7+
*
8+
* - **Agent-as-subagent**: A full `createDeepAgent` can be used as a
9+
* `CompiledSubAgent` in a parent agent
10+
* - **Multi-level nesting**: Sub-agents can themselves have sub-agents,
11+
* creating arbitrarily deep hierarchies
12+
* - **LLM-driven orchestration**: The parent agent's LLM planner decides
13+
* when to invoke a sub-agent vs. using a tool directly
14+
*
15+
* Architecture:
16+
* ```
17+
* Main Agent (orchestrator)
18+
* ├── Tool: get_weather
19+
* └── Sub Agent: research-specialist (DeepAgent)
20+
* ├── Tool: get_news
21+
* ├── Tool: analyze_data
22+
* └── Sub Agent: fact-checker (simple SubAgent)
23+
* └── Tool: verify_claim
24+
* ```
25+
*
26+
* @see https://github.com/anthropics/deepagentsjs/issues/206
27+
*/
28+
import { tool } from "langchain";
29+
import { z } from "zod";
30+
import { HumanMessage } from "@langchain/core/messages";
31+
32+
import {
33+
createDeepAgent,
34+
type CompiledSubAgent,
35+
type SubAgent,
36+
} from "deepagents";
37+
38+
// ─── Tools ──────────────────────────────────────────────────────────────────
39+
const getWeather = tool(
40+
(input) => `The weather in ${input.location} is sunny and 72°F.`,
41+
{
42+
name: "get_weather",
43+
description: "Get the current weather for a location",
44+
schema: z.object({
45+
location: z.string().describe("The city or location to get weather for"),
46+
}),
47+
},
48+
);
49+
50+
const getNews = tool(
51+
(input) =>
52+
`Latest news for "${input.topic}":\n` +
53+
`1. Major breakthrough in ${input.topic} announced today\n` +
54+
`2. Experts weigh in on the future of ${input.topic}\n` +
55+
`3. New study reveals surprising findings about ${input.topic}`,
56+
{
57+
name: "get_news",
58+
description: "Search for the latest news articles on a topic",
59+
schema: z.object({
60+
topic: z.string().describe("The topic to search news for"),
61+
}),
62+
},
63+
);
64+
65+
const analyzeData = tool(
66+
(input) =>
67+
`Analysis of "${input.data}":\n` +
68+
`The data shows a positive trend with key insights:\n` +
69+
`- Primary finding: significant growth in the area\n` +
70+
`- Secondary finding: emerging patterns suggest continued development\n` +
71+
`- Recommendation: further investigation warranted`,
72+
{
73+
name: "analyze_data",
74+
description: "Analyze provided data and return insights",
75+
schema: z.object({
76+
data: z.string().describe("The data or topic to analyze"),
77+
}),
78+
},
79+
);
80+
81+
const verifyClaim = tool(
82+
(input) =>
83+
`Verification of "${input.claim}":\n` +
84+
`Status: ${input.claim.length > 20 ? "Partially verified" : "Verified"}\n` +
85+
`Confidence: High\n` +
86+
`Sources: 3 independent sources confirmed`,
87+
{
88+
name: "verify_claim",
89+
description: "Verify a factual claim against known sources",
90+
schema: z.object({
91+
claim: z.string().describe("The claim to verify"),
92+
}),
93+
},
94+
);
95+
96+
// ─── Level 2 Sub-Agent: Fact Checker (simple SubAgent within the research agent) ─
97+
const factCheckerSubAgent: SubAgent = {
98+
name: "fact-checker",
99+
description:
100+
"A fact-checking agent that can verify claims. " +
101+
"Use this when you need to validate specific facts or statements.",
102+
systemPrompt:
103+
"You are a fact-checking specialist. Use the verify_claim tool to check " +
104+
"the accuracy of any claims or statements you receive. Always verify before " +
105+
"drawing conclusions.",
106+
tools: [verifyClaim],
107+
};
108+
109+
// ─── Level 1 Sub-Agent: Research Specialist (itself a DeepAgent) ─────────────
110+
/**
111+
* This deep agent acts as a sub-agent of the main agent.
112+
* It has its own tools, sub-agents, and full agent capabilities including
113+
* filesystem, todo management, and summarization.
114+
*/
115+
const researchDeepAgent = createDeepAgent({
116+
systemPrompt:
117+
"You are a research specialist. Your role is to gather news, analyze data, " +
118+
"and produce well-researched findings.\n\n" +
119+
"When asked to research a topic:\n" +
120+
"1. Use the get_news tool to gather the latest information\n" +
121+
"2. Use the analyze_data tool to analyze the findings\n" +
122+
"3. If you need to verify specific claims, delegate to the fact-checker sub-agent\n" +
123+
"4. Write a concise summary of your research findings",
124+
tools: [getNews, analyzeData],
125+
subagents: [factCheckerSubAgent],
126+
});
127+
128+
// ─── Level 0: Main Orchestrator Agent ────────────────────────────────────────
129+
130+
/**
131+
* The main agent orchestrates between its own tools and sub-agents.
132+
* The LLM decides whether to:
133+
* - Use get_weather directly for weather queries
134+
* - Delegate to the research-specialist deep agent for research tasks
135+
*/
136+
export const mainAgent = createDeepAgent({
137+
systemPrompt:
138+
"You are a helpful assistant that coordinates different capabilities.\n\n" +
139+
"- For weather queries, use the get_weather tool directly\n" +
140+
"- For research, analysis, or news queries, delegate to the research-specialist " +
141+
"sub-agent which has specialized tools for these tasks\n\n" +
142+
"Always choose the most appropriate tool or sub-agent for each task.",
143+
tools: [getWeather],
144+
subagents: [
145+
{
146+
name: "research-specialist",
147+
description:
148+
"A specialized research agent with its own tools and sub-agents. " +
149+
"It can search for news, analyze data, and verify facts. " +
150+
"Use this for any research, analysis, or investigation tasks.",
151+
runnable: researchDeepAgent,
152+
} satisfies CompiledSubAgent,
153+
],
154+
});
155+
156+
// ─── Run ─────────────────────────────────────────────────────────────────────
157+
console.log("=== Hierarchical Deep Agent Example ===\n");
158+
159+
// Test 1: Direct tool use (weather) — main agent handles this itself
160+
console.log("--- Query 1: Direct tool use (weather) ---");
161+
const result1 = await mainAgent.invoke({
162+
messages: [new HumanMessage("What's the weather in San Francisco?")],
163+
});
164+
const lastMsg1 = result1.messages[result1.messages.length - 1];
165+
console.log(
166+
"Response:",
167+
typeof lastMsg1.content === "string"
168+
? `${lastMsg1.content.slice(0, 200)}...`
169+
: lastMsg1.content,
170+
);
171+
172+
// Test 2: Delegate to research sub-agent (DeepAgent)
173+
console.log("\n--- Query 2: Delegate to research sub-agent ---");
174+
const result2 = await mainAgent.invoke({
175+
messages: [
176+
new HumanMessage(
177+
"Research the latest developments in renewable energy and provide an analysis.",
178+
),
179+
],
180+
});
181+
const lastMsg2 = result2.messages[result2.messages.length - 1];
182+
console.log(
183+
"Response:",
184+
typeof lastMsg2.content === "string"
185+
? `${lastMsg2.content.slice(0, 200)}...`
186+
: lastMsg2.content,
187+
);
188+
189+
console.log("\n=== Done ===");

libs/deepagents/src/agent.int.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, it, expect } from "vitest";
22
import { AIMessage, HumanMessage } from "@langchain/core/messages";
33
import { createDeepAgent } from "./index.js";
4+
import type { CompiledSubAgent } from "./index.js";
45
import {
56
SAMPLE_MODEL,
67
TOY_BASKETBALL_RESEARCH,
@@ -288,6 +289,122 @@ describe("DeepAgents Integration Tests", () => {
288289
},
289290
);
290291

292+
it.concurrent(
293+
"should use a deep agent as a compiled subagent (agent-as-subagent hierarchy)",
294+
{ timeout: 120 * 1000 }, // 120s
295+
async () => {
296+
// Create a deep agent that will serve as a subagent
297+
const weatherDeepAgent = createDeepAgent({
298+
model: SAMPLE_MODEL,
299+
systemPrompt:
300+
"You are a weather specialist. Use the get_weather tool to get weather information for any location requested.",
301+
tools: [getWeather],
302+
});
303+
304+
// Use the deep agent as a CompiledSubAgent in the parent
305+
const parentAgent = createDeepAgent({
306+
model: SAMPLE_MODEL,
307+
systemPrompt:
308+
"You are an orchestrator. Delegate weather queries to the weather-specialist subagent via the task tool.",
309+
subagents: [
310+
{
311+
name: "weather-specialist",
312+
description:
313+
"A specialized weather agent that can provide detailed weather information for any city.",
314+
runnable: weatherDeepAgent,
315+
} satisfies CompiledSubAgent,
316+
],
317+
});
318+
assertAllDeepAgentQualities(parentAgent);
319+
320+
// Verify the task tool lists the weather-specialist subagent
321+
const tools = extractToolsFromAgent(parentAgent);
322+
expect(tools.task).toBeDefined();
323+
expect(tools.task.description).toContain("weather-specialist");
324+
325+
// Invoke and verify the parent delegates to the weather-specialist
326+
const result = await parentAgent.invoke(
327+
{
328+
messages: [new HumanMessage("What is the weather in Tokyo?")],
329+
},
330+
{ recursionLimit: 100 },
331+
);
332+
333+
const agentMessages = result.messages.filter(AIMessage.isInstance);
334+
const toolCalls = agentMessages.flatMap((msg) => msg.tool_calls || []);
335+
336+
expect(
337+
toolCalls.some(
338+
(tc) =>
339+
tc.name === "task" &&
340+
tc.args?.subagent_type === "weather-specialist",
341+
),
342+
).toBe(true);
343+
},
344+
);
345+
346+
it.concurrent(
347+
"should support multi-level deep agent hierarchy (nested deep agents)",
348+
{ timeout: 120 * 1000 }, // 120s
349+
async () => {
350+
// Level 2: A deep agent with its own subagents
351+
const innerDeepAgent = createDeepAgent({
352+
model: SAMPLE_MODEL,
353+
systemPrompt:
354+
"You are a sports information agent. Use the get_soccer_scores tool to get soccer scores.",
355+
tools: [getSoccerScores],
356+
subagents: [
357+
{
358+
name: "weather-helper",
359+
description: "Gets weather information for match day conditions.",
360+
systemPrompt:
361+
"Use the get_weather tool to get weather information.",
362+
tools: [getWeather],
363+
model: SAMPLE_MODEL,
364+
},
365+
],
366+
});
367+
368+
// Level 1: Parent deep agent using the inner deep agent as a subagent
369+
const parentAgent = createDeepAgent({
370+
model: SAMPLE_MODEL,
371+
systemPrompt:
372+
"You are an orchestrator. Use the sports-info subagent for any sports related questions.",
373+
tools: [sampleTool],
374+
subagents: [
375+
{
376+
name: "sports-info",
377+
description:
378+
"A specialized sports agent that can get soccer scores and check match day weather.",
379+
runnable: innerDeepAgent,
380+
} satisfies CompiledSubAgent,
381+
],
382+
});
383+
assertAllDeepAgentQualities(parentAgent);
384+
385+
const result = await parentAgent.invoke(
386+
{
387+
messages: [
388+
new HumanMessage(
389+
"What are the latest scores for Manchester United?",
390+
),
391+
],
392+
},
393+
{ recursionLimit: 100 },
394+
);
395+
396+
const agentMessages = result.messages.filter(AIMessage.isInstance);
397+
const toolCalls = agentMessages.flatMap((msg) => msg.tool_calls || []);
398+
399+
expect(
400+
toolCalls.some(
401+
(tc) =>
402+
tc.name === "task" && tc.args?.subagent_type === "sports-info",
403+
),
404+
).toBe(true);
405+
},
406+
);
407+
291408
// Note: response_format with ToolStrategy is not yet available in LangChain TS v1
292409
// Skipping test_response_format_tool_strategy for now
293410
});

0 commit comments

Comments
 (0)