Sub-agents
A sub-agent is an agent exposed to the parent as a regular tool. The parent calls it like any other tool; the sub-agent runs to completion (with its own model, system prompt, and tools) and returns its final answer as the tool result.
Use sub-agents when the work decomposes into specialised slices — "research", "draft", "review" — that benefit from their own context.
Defining a sub-agent
Two flavours.
SubAgentSpec — declarative
from mantis_agent import SubAgentSpec, as_subagent_tool
researcher = SubAgentSpec(
name="researcher",
description="Research a topic and produce a fact-checked summary.",
model="qwen2.5:7b",
system_prompt="You are a careful researcher. Cite sources.",
tools=[web_search, web_fetch],
max_turns=15,
)
researcher_tool = as_subagent_tool(researcher)Existing Agent — imperative
If you already have an Agent instance you want to expose:
from mantis_agent import Agent, as_subagent_tool
researcher = Agent(
model="qwen2.5:7b",
backend="http://localhost:11434",
system="...", # Agent's field is `system`; `system_prompt` is the
tools=[], # SubAgentSpec / MantisAgentOptions spelling
)
researcher_tool = as_subagent_tool(
researcher,
name="researcher",
description="Research a topic and produce a fact-checked summary.",
)Under the hood this wraps the agent in a WrappedAgentTool. Same JSON
schema as SubAgentTool.
Calling from a parent
Add the wrapped sub-agent to the parent's tools list:
options = MantisAgentOptions(
model="qwen2.5:7b",
tools=[researcher_tool, drafter_tool, reviewer_tool],
)The model invokes them like any other tool. The sub-agent runs in isolation and returns its final assistant text.
Isolation
Sub-agents have their own:
- Transcript (separate JSONL file under
~/.mantis-agent/sessions/) - System prompt
- Tool registry
- Permission policy
- Budget cap
Set isolation=IsolationMode.SHARED to share session and transcript
with the parent (the sub-agent's turns interleave into the parent's
transcript). Use this when the sub-agent should append to the
parent's context, not branch off.
Isolation is a property of the spec, not of the wrapper, and
IsolationMode is a string literal type — "asyncio_task" (the default),
"subprocess", or "remote". There is no SHARED member:
from mantis_agent import SubAgentSpec, as_subagent_tool
spec = SubAgentSpec(
name="researcher",
system_prompt="Research the topic and report back.",
model="qwen2.5:7b",
isolation="asyncio_task",
)
researcher_tool = as_subagent_tool(spec)as_subagent_tool(spec_or_agent, *, name=None, description=None, parent_provider=None) takes no isolation argument. Pass
parent_provider=parent.provider to share the parent's HTTP pool —
recommended in asyncio_task mode.
Multiple sub-agents in parallel
If the parent emits two sub-agent calls in the same turn, they run
concurrently (assuming parallel_safe=True, the default). Each gets
its own task group; results thread back in emission order.
options = MantisAgentOptions(
tools=[
as_subagent_tool(spec_a),
as_subagent_tool(spec_b),
as_subagent_tool(spec_c),
],
)The parent model can fan out: "use researcher + drafter + reviewer in parallel". The runtime handles the rest.
Passing prompts and data
Sub-agents accept a single prompt argument by default — whatever the
parent passes. To accept structured input, define a custom schema on
the spec:
SubAgentSpec has no input_schema field — the wrapped tool always exposes a
single prompt argument. For structured input, build the Tool yourself with
the schema you want and dispatch to the sub-agent inside it:
import json
from mantis_agent import SubAgentSpec, Tool, as_subagent_tool
spec = SubAgentSpec(
name="researcher",
system_prompt="Research the topic and report back.",
model="qwen2.5:7b",
description="Research a topic at a given depth.",
)
inner = as_subagent_tool(spec)
async def research(topic: str, depth: str = "shallow") -> str:
return await inner.fn(prompt=json.dumps({"topic": topic, "depth": depth}))
structured = Tool(
name="researcher",
description="Research a topic at a given depth.",
input_schema={
"type": "object",
"properties": {
"topic": {"type": "string"},
"depth": {"type": "string", "enum": ["shallow", "deep"]},
},
"required": ["topic"],
},
fn=research,
)Budgets and limits
Apply caps separately to each sub-agent:
Dollar caps live on a Budget, which the spec carries; max_turns is a field
in its own right:
from mantis_agent import SubAgentSpec
from mantis_agent.budget import Budget
researcher = SubAgentSpec(
name="researcher",
system_prompt="Research.",
model="qwen2.5:7b",
max_turns=8,
budget=Budget(max_usd=0.10),
)
drafter = SubAgentSpec(
name="drafter",
system_prompt="Draft.",
model="qwen2.5:7b",
max_turns=5,
budget=Budget(max_usd=0.05),
)Budget also takes max_input_tokens, max_output_tokens,
max_total_tokens, and a fallback_model to downshift to before the cap is
hit. The parent's own cap rolls up everything: its model calls plus all
sub-agent spend.
Patterns
Three common shapes:
- Specialist pool. Parent decides which expert to consult based on the user's question. Each sub-agent has a narrow domain.
- Pipeline. Parent calls researcher → drafter → reviewer in order, threading outputs through.
- Fan-out. Parent emits N sub-agent calls in parallel to compare outputs, then picks the best.
See mantis_agent/examples/multi_agent_research.py for a worked
example of all three.
Agent types (the task tool)
The mantis terminal ships one delegation tool — task — with selectable
agent types (Claude Code's subagent_type):
| type | tools | steps | for |
|---|---|---|---|
explore |
read-only | 20 | find code/facts, report with file:line |
plan |
read-only | 25 | read the code → step-by-step implementation plan |
general-purpose |
full belt | 100 | autonomous multi-step execution |
Parallel fan-out works: the model can launch several task calls in one
message and they run concurrently. Subagents inherit the parent's
permission gate — a write-capable child prompts you exactly like the
parent would.
User-defined agents
Drop a markdown file in ~/.mantis-agent/agents/<name>.md (user-wide) or
./.mantis/agents/<name>.md (project — wins on collision, can override the
built-ins):
---
name: code-reviewer
description: Reviews a diff for bugs and style problems.
tools: read_file, grep, glob # or "all" / "read-only" (default "all")
model: gpt-5.4-mini # optional; default inherits the parent
max_steps: 30
---
You are a meticulous code reviewer.../agents lists everything discovered. In the SDK, discover_agent_types()
returns the same list, and make_task_tool(..., agent_types=...) builds the
tool.
SDK: options.agents
Claude-SDK-style agent definitions register as delegatable tools named after the agent:
from mantis_agent import AgentDefinition, MantisAgentOptions
options = MantisAgentOptions(
model="qwen2.5:7b",
agents={"reviewer": AgentDefinition(
description="Reviews diffs for bugs.",
prompt="You are a reviewer.",
tools=["read_file", "grep"], # narrows the child's kit
)},
)Twins (the pair tool)
task is fire-and-forget; pair is a conversation. Each named peer — a
twin — is a persistent same-model agent that remembers your whole dialogue
with it: propose → get pushback → revise → converge.
pair(message, peer="skeptic", persona="stress-test every proposal")— named peers are independent minds with separate memories- Twins carry the read-only kit, so their pushback cites real
file:line, and they can't race the parent on writes reset=truewipes one peer
In the terminal, /twin skeptic: <message> lets you join the exact same
conversation the model has been having (state is shared, survives model
switches). SDK: make_pair_tool(model=..., tools=..., conversations=...).