Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Imported skill subagents from langchain
.claude/skills/majiayu000-subagents/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 105% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 194% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 205% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 161% | 0% |
"""Middleware for providing subagents to an agent via a task tool."""
from collections.abc import Awaitable, Callable, Sequence from typing import Any, NotRequired, TypedDict, cast
from langchain.agents import create_agent from langchain.agents.middleware import HumanInTheLoopMiddleware, InterruptOnConfig from langchain.agents.middleware.types import AgentMiddleware, ModelRequest, ModelResponse from langchain.tools import BaseTool, ToolRuntime from langchain_core.language_models import BaseChatModel from langchain_core.messages import HumanMessage, ToolMessage from langchain_core.runnables import Runnable from langchain_core.tools import StructuredTool from langgraph.types import Command
from deepagents.middleware._utils import append_to_system_message
class SubAgent(TypedDict): """Specification for an agent.
When specifying custom agents, the default_middleware from SubAgentMiddleware will be applied first, followed by any middleware specified in this spec. To use only custom middleware without the defaults, pass default_middleware=[] to SubAgentMiddleware.
Required fields: name: Unique identifier for the subagent.
The main agent uses this name when calling the task() tool. description: What this subagent does.
Be specific and action-oriented. The main agent uses this to decide when to delegate. system_prompt: Instructions for the subagent.
Include tool usage guidance and output format requirements. tools: Tools the subagent can use.
Keep this minimal and include only what's needed.
Optional fields: model: Override the main agent's model.
Use the format 'provider:model-name' (e.g., 'openai:gpt-4o'). middleware: Additional middleware for custom behavior, logging, or rate limiting. interrupt_on: Configure human-in-the-loop for specific tools.
Requires a checkpointer. """
name: str """Unique identifier for the subagent."""
description: str """What this subagent does. The main agent uses this to decide when to delegate."""
system_prompt: str """Instructions for the subagent."""
tools: SequenceBaseTool | Callable | dictstr, Any]] """Tools the subagent can use."""
model: NotRequiredstr | BaseChatModel] """Override the main agent's model. Use 'provider:model-name' format."""
middleware: NotRequiredlistAgentMiddleware]] """Additional middleware for custom behavior."""
interrupt_on: NotRequireddictstr, bool | InterruptOnConfig]] """Configure human-in-the-loop for specific tools."""
class CompiledSubAgent(TypedDict): """A pre-compiled agent spec.
!!! note
The runnable's state schema must include a 'messages' key.
This is required for the subagent to communicate results back to the main agent.
When the subagent completes, the final message in the 'messages' list will be extracted and returned as a ToolMessage to the parent agent. """
name: str """Unique identifier for the subagent."""
description: str """What this subagent does."""
runnable: Runnable """A custom agent implementation.
Create a custom agent using either:
create_agent()langgraphIf you're creating a custom graph, make sure the state schema includes a 'messages' key. This is required for the subagent to communicate results back to the main agent. """
DEFAULT_SUBAGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools."
_EXCLUDED_STATE_KEYS = {"messages", "todos", "structured_response"}
TASK_TOOL_DESCRIPTION = """Launch an ephemeral subagent to handle complex, multi-step independent tasks with isolated context windows.
Available agent types and the tools they have access to: {available_agents}
When using the Task tool, you must specify a subagent_type parameter to select which agent type to use.
<example_agent_descriptions> "general-purpose": use this agent for general purpose tasks, it has access to all tools as the main agent. </example_agent_descriptions>
<example> User: "I want to conduct research on the accomplishments of Lebron James, Michael Jordan, and Kobe Bryant, and then compare them." Assistant: Uses the task tool in parallel to conduct isolated research on each of the three players Assistant: Synthesizes the results of the three isolated research tasks and responds to the User <commentary> Research is a complex, multi-step task in it of itself. The research of each individual player is not dependent on the research of the other players. The assistant uses the task tool to break down the complex objective into three isolated tasks. Each research task only needs to worry about context and tokens about one player, then returns synthesized information about each player as the Tool Result. This means each research task can dive deep and spend tokens and context deeply researching each player, but the final result is synthesized information, and saves us tokens in the long run when comparing the players to each other. </commentary> </example>
<example> User: "Analyze a single large code repository for security vulnerabilities and generate a report." Assistant: Launches a single `task` subagent for the repository analysis Assistant: Receives report and integrates results into final summary <commentary> Subagent is used to isolate a large, context-heavy task, even though there is only one. This prevents the main thread from being overloaded with details. If the user then asks followup questions, we have a concise report to reference instead of the entire history of analysis and tool calls, which is good and saves us time and money. </commentary> </example>
<example> User: "Schedule two meetings for me and prepare agendas for each." Assistant: Calls the task tool in parallel to launch two `task` subagents (one per meeting) to prepare agendas Assistant: Returns final schedules and agendas <commentary> Tasks are simple individually, but subagents help silo agenda preparation. Each subagent only needs to worry about the agenda for one meeting. </commentary> </example>
<example> User: "I want to order a pizza from Dominos, order a burger from McDonald's, and order a salad from Subway." Assistant: Calls tools directly in parallel to order a pizza from Dominos, a burger from McDonald's, and a salad from Subway <commentary> The assistant did not use the task tool because the objective is super simple and clear and only requires a few trivial tool calls. It is better to just complete the task directly and NOT use the tasktool. </commentary> </example>
<example_agent_descriptions> "content-reviewer": use this agent after you are done creating significant content or documents "greeting-responder": use this agent when to respond to user greetings with a friendly joke "research-analyst": use this agent to conduct thorough research on complex topics </example_agent_description>
<example> user: "Please write a function that checks if a number is prime" assistant: Sure let me write a function that checks if a number is prime assistant: First let me use the Write tool to write a function that checks if a number is prime assistant: I'm going to use the Write tool to write the following code: <code> function isPrime(n) {{ if (n <= 1) return false for (let i = 2; i i <= n; i++) {{ if (n % i === 0) return false }} return true }} </code> <commentary> Since significant content was created and the task was completed, now use the content-reviewer agent to review the work </commentary> assistant: Now let me use the content-reviewer agent to review the code assistant: Uses the Task tool to launch with the content-reviewer agent </example>
<example> user: "Can you help me research the environmental impact of different renewable energy sources and create a comprehensive report?" <commentary> This is a complex research task that would benefit from using the research-analyst agent to conduct thorough analysis </commentary> assistant: I'll help you research the environmental impact of renewable energy sources. Let me use the research-analyst agent to conduct comprehensive research on this topic. assistant: Uses the Task tool to launch with the research-analyst agent, providing detailed instructions about what research to conduct and what format the report should take </example>
<example> user: "Hello" <commentary> Since the user is greeting, use the greeting-responder agent to respond with a friendly joke </commentary> assistant: "I'm going to use the Task tool to launch with the greeting-responder agent" </example>""" # noqa: E501
TASK_SYSTEM_PROMPT = """## task (subagent spawner)
You have access to a task tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result.
When to use the task tool:
Subagent lifecycle:
When NOT to use the task tool:
task tool to silo independent tasks within a multi-part objective.task tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient.""" # noqa: E501DEFAULT_GENERAL_PURPOSE_DESCRIPTION = "General-purpose agent for researching complex questions, searching for files and content, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. This agent has access to all tools as the main agent." # noqa: E501
def _get_subagents( , default_model: str | BaseChatModel, default_tools: SequenceBaseTool | Callable | dictstr, Any]], default_middleware: listAgentMiddleware] | None, default_interrupt_on: dictstr, bool | InterruptOnConfig] | None, subagents: listSubAgent | CompiledSubAgent], general_purpose_agent: bool, ) -> tupledictstr, Any], liststr]]: """Create subagent instances from specifications.
Args: default_model: Default model for subagents that don't specify one. default_tools: Default tools for subagents that don't specify tools. default_middleware: Middleware to apply to all subagents. If None, no default middleware is applied. default_interrupt_on: The tool configs to use for the default general-purpose subagent. These are also the fallback for any subagents that don't specify their own tool configs. subagents: List of agent specifications or pre-compiled agents. general_purpose_agent: Whether to include a general-purpose subagent.
Returns: Tuple of (agent_dict, description_list) where agent_dict maps agent names to runnable instances and description_list contains formatted descriptions. """ # Use empty list if None (no default middleware) default_subagent_middleware = default_middleware or ]
agents: dictstr, Any] = {} subagent_descriptions = ]
# Create general-purpose agent if enabled if general_purpose_agent: general_purpose_middleware = default_subagent_middleware] if default_interrupt_on: general_purpose_middleware.append(HumanInTheLoopMiddleware(interrupt_on=default_interrupt_on)) general_purpose_subagent = create_agent( default_model, system_prompt=DEFAULT_SUBAGENT_PROMPT, tools=default_tools, middleware=general_purpose_middleware, name="general-purpose", ) agents"general-purpose"] = general_purpose_subagent subagent_descriptions.append(f"- general-purpose: {DEFAULT_GENERAL_PURPOSE_DESCRIPTION}")
# Process custom subagents for agent_ in subagents: subagent_descriptions.append(f"- {agent_'name']}: {agent_'description']}") if "runnable" in agent_: custom_agent = cast("CompiledSubAgent", agent_) agentscustom_agent"name"]] = custom_agent"runnable"] continue _tools = agent_.get("tools", list(default_tools))
subagent_model = agent_.get("model", default_model)
_middleware = default_subagent_middleware, agent_"middleware"]] if "middleware" in agent_ else default_subagent_middleware]
interrupt_on = agent_.get("interrupt_on", default_interrupt_on) if interrupt_on: _middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
agentsagent_"name"]] = create_agent( subagent_model, system_prompt=agent_"system_prompt"], tools=_tools, middleware=_middleware, name=agent_"name"], ) return agents, subagent_descriptions
def _create_task_tool( , default_model: str | BaseChatModel, default_tools: SequenceBaseTool | Callable | dictstr, Any]], default_middleware: listAgentMiddleware] | None, default_interrupt_on: dictstr, bool | InterruptOnConfig] | None, subagents: listSubAgent | CompiledSubAgent], general_purpose_agent: bool, task_description: str | None = None, ) -> BaseTool: """Create a task tool for invoking subagents.
Args: default_model: Default model for subagents. default_tools: Default tools for subagents. default_middleware: Middleware to apply to all subagents. default_interrupt_on: The tool configs to use for the default general-purpose subagent. These are also the fallback for any subagents that don't specify their own tool configs. subagents: List of subagent specifications. general_purpose_agent: Whether to include general-purpose agent. task_description: Custom description for the task tool. If None, uses default template. Supports {available_agents} placeholder.
Returns: A StructuredTool that can invoke subagents by type. """ subagent_graphs, subagent_descriptions = _get_subagents( default_model=default_model, default_tools=default_tools, default_middleware=default_middleware, default_interrupt_on=default_interrupt_on, subagents=subagents, general_purpose_agent=general_purpose_agent, ) subagent_description_str = "\n".join(subagent_descriptions)
def _return_command_with_state_update(result: dict, tool_call_id: str) -> Command: # Validate that the result contains a 'messages' key if "messages" not in result: error_msg = ( "CompiledSubAgent must return a state containing a 'messages' key. " "Custom StateGraphs used with CompiledSubAgent should include 'messages' " "in their state schema to communicate results back to the main agent." ) raise ValueError(error_msg)
state_update = {k: v for k, v in result.items() if k not in _EXCLUDED_STATE_KEYS} # Strip trailing whitespace to prevent API errors with Anthropic message_text = result"messages"]-1].text.rstrip() if result"messages"]-1].text else "" return Command( update={ state_update, "messages": ToolMessage(message_text, tool_call_id=tool_call_id)], } )
def _validate_and_prepare_state(subagent_type: str, description: str, runtime: ToolRuntime) -> tupleRunnable, dict]: """Prepare state for invocation.""" subagent = subagent_graphssubagent_type] # Create a new state dict to avoid mutating the original subagent_state = {k: v for k, v in runtime.state.items() if k not in _EXCLUDED_STATE_KEYS} subagent_state"messages"] = HumanMessage(content=description)] return subagent, subagent_state
# Use custom description if provided, otherwise use default template if task_description is None: task_description = TASK_TOOL_DESCRIPTION.format(available_agents=subagent_description_str) elif "{available_agents}" in task_description: # If custom description has placeholder, format with agent descriptions task_description = task_description.format(available_agents=subagent_description_str)
def task( description: str, subagent_type: str, runtime: ToolRuntime, ) -> str | Command: if subagent_type not in subagent_graphs: allowed_types = ", ".join(f"{k}" for k in subagent_graphs]) return f"We cannot invoke subagent {subagent_type} because it does not exist, the only allowed types are {allowed_types}" subagent, subagent_state = _validate_and_prepare_state(subagent_type, description, runtime) result = subagent.invoke(subagent_state) if not runtime.tool_call_id: value_error_msg = "Tool call ID is required for subagent invocation" raise ValueError(value_error_msg) return _return_command_with_state_update(result, runtime.tool_call_id)
async def atask( description: str, subagent_type: str, runtime: ToolRuntime, ) -> str | Command: if subagent_type not in subagent_graphs: allowed_types = ", ".join(f"{k}" for k in subagent_graphs]) return f"We cannot invoke subagent {subagent_type} because it does not exist, the only allowed types are {allowed_types}" subagent, subagent_state = _validate_and_prepare_state(subagent_type, description, runtime) result = await subagent.ainvoke(subagent_state) if not runtime.tool_call_id: value_error_msg = "Tool call ID is required for subagent invocation" raise ValueError(value_error_msg) return _return_command_with_state_update(result, runtime.tool_call_id)
return StructuredTool.from_function( name="task", func=task, coroutine=atask, description=task_description, )
class SubAgentMiddleware(AgentMiddleware): """Middleware for providing subagents to an agent via a task tool.
This middleware adds a task tool to the agent that can be used to invoke subagents. Subagents are useful for handling complex tasks that require multiple steps, or tasks that require a lot of context to resolve.
A chief benefit of subagents is that they can handle multi-step tasks, and then return a clean, concise response to the main agent.
Subagents are also great for different domains of expertise that require a narrower subset of tools and focus.
This middleware comes with a default general-purpose subagent that can be used to handle the same tasks as the main agent, but with isolated context.
Args: default_model: The model to use for subagents.
Can be a LanguageModelLike or a dict for init_chat_model. default_tools: The tools to use for the default general-purpose subagent. default_middleware: Default middleware to apply to all subagents.
If None, no default middleware is applied.
Pass a list to specify custom middleware. default_interrupt_on: The tool configs to use for the default general-purpose subagent.
These are also the fallback for any subagents that don't specify their own tool configs. subagents: A list of additional subagents to provide to the agent. system_prompt: Full system prompt override. When provided, completely replaces the agent's system prompt. general_purpose_agent: Whether to include the general-purpose agent. task_description: Custom description for the task tool.
If None, uses the default description template.
Example: python from langchain.agents.middleware.subagents import SubAgentMiddleware from langchain.agents import create_agent
# Basic usage with defaults (no default middleware) agent = create_agent( "openai:gpt-4o", middleware= SubAgentMiddleware( default_model="openai:gpt-4o", subagents=], ) ], )
# Add custom middleware to subagents agent = create_agent( "openai:gpt-4o", middleware= SubAgentMiddleware( default_model="openai:gpt-4o", default_middleware=TodoListMiddleware()], subagents=], ) ], ) """
def __init__( self, , default_model: str | BaseChatModel, default_tools: SequenceBaseTool | Callable | dictstr, Any]] | None = None, default_middleware: listAgentMiddleware] | None = None, default_interrupt_on: dictstr, bool | InterruptOnConfig] | None = None, subagents: listSubAgent | CompiledSubAgent] | None = None, system_prompt: str | None = TASK_SYSTEM_PROMPT, general_purpose_agent: bool = True, task_description: str | None = None, ) -> None: """Initialize the SubAgentMiddleware.""" super().__init__() self.system_prompt = system_prompt task_tool = _create_task_tool( default_model=default_model, default_tools=default_tools or ], default_middleware=default_middleware, default_interrupt_on=default_interrupt_on, subagents=subagents or ], general_purpose_agent=general_purpose_agent, task_description=task_description, ) self.tools = task_tool]
def wrap_model_call( self, request: ModelRequest, handler: CallableModelRequest], ModelResponse], ) -> ModelResponse: """Update the system message to include instructions on using subagents.""" if self.system_prompt is not None: new_system_message = append_to_system_message(request.system_message, self.system_prompt) return handler(request.override(system_message=new_system_message)) return handler(request)
async def awrap_model_call( self, request: ModelRequest, handler: CallableModelRequest], AwaitableModelResponse]], ) -> ModelResponse: """(async) Update the system message to include instructions on using subagents.""" if self.system_prompt is not None: new_system_message = append_to_system_message(request.system_message, self.system_prompt) return await handler(request.override(system_message=new_system_message)) return await handler(request)
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 33,038 | 20,586 | -38% | 1 | 1 | 0% | 5,454 | 9,149 | +68% | 0 | 0 | — |
case-02 | fail→fail | 23,904 | 19,691 | -18% | 1 | 1 | 0% | 4,644 | 9,020 | +94% | 0 | 0 | — |
case-03 | fail→pass | 28,270 | 25,705 | -9% | 1 | 1 | 0% | 5,026 | 10,299 | +105% | 0 | 0 | — |
case-04 | pass→pass | 11,248 | 10,695 | -5% | 1 | 1 | 0% | 2,094 | 8,439 | +303% | 0 | 0 | — |
case-05 | pass→pass | 20,064 | 9,925 | -51% | 1 | 1 | 0% | 2,886 | 8,380 | +190% | 0 | 0 | — |
case-06 | pass→pass | 14,659 | 9,522 | -35% | 1 | 1 | 0% | 3,010 | 8,419 | +180% | 0 | 0 | — |
case-07 | fail→pass | 22,289 | 15,119 | -32% | 1 | 1 | 0% | 3,092 | 9,083 | +194% | 0 | 0 | — |
case-08 | fail→fail | 22,276 | 14,814 | -33% | 1 | 1 | 0% | 2,967 | 9,397 | +217% | 0 | 0 | — |
case-09 | pass→pass | 13,039 | 14,005 | +7% | 1 | 1 | 0% | 2,441 | 8,115 | +232% | 0 | 0 | — |
case-10 | fail→pass | 13,545 | 10,808 | -20% | 1 | 1 | 0% | 2,455 | 7,480 | +205% | 0 | 0 | — |
case-11 | fail→pass | 20,796 | 12,163 | -42% | 1 | 1 | 0% | 3,003 | 7,842 | +161% | 0 | 0 | — |
case-12 | pass→pass | 18,844 | 18,344 | -3% | 1 | 1 | 0% | 2,436 | 8,111 | +233% | 0 | 0 | — |
case-13 | fail→pass | 23,660 | 14,712 | -38% | 1 | 1 | 0% | 3,033 | 7,607 | +151% | 0 | 0 | — |
case-14 | pass→pass | 20,675 | 17,430 | -16% | 1 | 1 | 0% | 2,791 | 8,863 | +218% | 0 | 0 | — |
case-15 | fail→pass | 15,708 | 5,664 | -64% | 1 | 1 | 0% | 2,902 | 7,458 | +157% | 0 | 0 | — |
case-16 | pass→pass | 11,237 | 9,669 | -14% | 1 | 1 | 0% | 2,182 | 8,163 | +274% | 0 | 0 | — |
case-17 | pass→pass | 18,197 | 10,938 | -40% | 1 | 1 | 0% | 2,337 | 7,613 | +226% | 0 | 0 | — |
case-18 | fail→pass | 21,336 | 13,660 | -36% | 1 | 1 | 0% | 2,890 | 8,034 | +178% | 0 | 0 | — |
case-19 | fail→pass | 22,107 | 12,257 | -45% | 1 | 1 | 0% | 2,901 | 7,745 | +167% | 0 | 0 | — |
case-20 | pass→pass | 24,977 | 16,368 | -34% | 1 | 1 | 0% | 3,620 | 8,580 | +137% | 0 | 0 | — |
case-21 | fail→pass | 18,718 | 6,200 | -67% | 1 | 1 | 0% | 2,537 | 7,494 | +195% | 0 | 0 | — |
case-22 | fail→pass | 18,142 | 18,309 | +1% | 1 | 1 | 0% | 3,397 | 8,974 | +164% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +50 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.