Subagents
Define specialized subagents with isolated permissions, custom prompts, and parallel delegation
Subagents
Subagents are specialized agents that run in isolated child sessions with their own permissions, system prompts, and tool access. A3S Code ships with 5 built-in subagents and supports user-defined subagents via YAML or Markdown files.
Built-in Subagents
Prop
Type
Defining Custom Subagents
YAML Format
Create a YAML file in your agents directory:
# ~/.a3s/agents/security-reviewer.yaml
name: security-reviewer
description: Reviews code for security vulnerabilities and best practices
mode: subagent
hidden: false
max_steps: 25
can_spawn_subagents: false
permissions:
allow:
- read
- grep
- glob
- ls
deny:
- write
- edit
- bash
- patch
prompt: |
You are a security code reviewer. Analyze the codebase for:
- SQL injection, XSS, CSRF vulnerabilities
- Hardcoded secrets and credentials
- Insecure cryptographic practices
- Missing input validation
- Unsafe deserialization
Report findings with severity (Critical/High/Medium/Low),
affected file and line, and recommended fix.
Never modify any files — report only.Markdown Format
Or use Markdown with YAML frontmatter (the body becomes the system prompt):
# ~/.a3s/agents/db-migration.md
---
name: db-migration
description: Database migration specialist
mode: subagent
max_steps: 40
permissions:
allow: [read, write, edit, bash, grep, glob]
deny: [web_fetch, web_search]
---
You are a database migration specialist. You can:
- Generate migration files (SQL or ORM-specific)
- Validate migration safety (no data loss, reversibility)
- Run migrations in development environments
- Generate rollback scripts
Always check for:
1. Backward compatibility with the current schema
2. Data preservation during column renames/type changes
3. Index impact on large tables
4. Foreign key constraint orderingWith Model Override
Use a different model for specific subagents via provider/model format:
name: quick-search
description: Fast search agent using a smaller model
mode: subagent
max_steps: 10
model: anthropic/claude-haiku-4-20250514
permissions:
allow: [read, grep, glob, ls]
prompt: |
You are a fast search agent. Find the requested information
quickly and return concise results. Do not explain — just find.Loading Subagents
Configure agent directories in your agent config:
agent_dirs = ["~/.a3s/agents", "/shared/team-agents"]All .yaml and .md files in these directories are automatically loaded into the AgentRegistry at startup.
The delegate-task Built-in Skill
A3S Code includes a built-in delegate-task skill (Instruction kind) that is automatically injected into the system prompt. It teaches the LLM:
- When to delegate work to sub-agents
- Which built-in agents are available and their capabilities
- How to invoke the
taskandparallel_tasktools correctly - Best practices for prompt design and parallelization
This means the LLM can delegate tasks to sub-agents without any additional configuration. See the Skills System page for more on built-in skills.
How Delegation Works
Parent Session
│
├─ task("security-reviewer", "Review auth module")
│ │
│ ├─ 1. Look up AgentDefinition in AgentRegistry
│ ├─ 2. Create child session with agent's permissions
│ ├─ 3. Apply agent's system prompt
│ ├─ 4. Execute task in isolated agentic loop
│ ├─ 5. Return result to parent
│ └─ 6. Child session ID preserved for reference
│
└─ Parent continues with subagent's resultKey isolation guarantees:
- Child session has its own context window (no parent context leakage)
- Permissions are restricted to the agent definition's policy
- Tool access is filtered per the agent's allow/deny rules
max_stepsprevents runaway executioncan_spawn_subagents: falseprevents recursive delegation
AgentDefinition Reference
Prop
Type
Tool Invocation
The LLM delegates to sub-agents via two tools: task (single) and parallel_task (concurrent). Both are also callable from SDK code via session.tool().
task Parameters
Prop
Type
task Result
Prop
Type
Single Delegation
let result = session.tool("task", serde_json::json!({
"agent": "explore",
"description": "Find auth handlers",
"prompt": "Search for files that handle user authentication and list them with line numbers."
})).await?;
println!("{}", result.output);const result = await session.tool('task', {
agent: 'explore',
description: 'Find auth handlers',
prompt: 'Search for files that handle user authentication and list them with line numbers.',
});
console.log(result.output);result = session.tool("task", {
"agent": "explore",
"description": "Find auth handlers",
"prompt": "Search for files that handle user authentication and list them with line numbers.",
})
print(result.output)Parallel Delegation
parallel_task spawns multiple subagents concurrently via tokio::JoinSet and waits for all to complete. Results are returned as an array.
let result = session.tool("parallel_task", serde_json::json!({
"tasks": [
{
"agent": "explore",
"description": "Find API routes",
"prompt": "List all API route definitions with their HTTP methods."
},
{
"agent": "explore",
"description": "Find DB models",
"prompt": "List all database model definitions with their fields."
},
{
"agent": "security-reviewer",
"description": "Review auth module",
"prompt": "Check src/auth/ for security vulnerabilities."
}
]
})).await?;
println!("{}", result.output);
// Each task's result is included with its agent name and success statusconst result = await session.tool('parallel_task', {
tasks: [
{
agent: 'explore',
description: 'Find API routes',
prompt: 'List all API route definitions with their HTTP methods.',
},
{
agent: 'explore',
description: 'Find DB models',
prompt: 'List all database model definitions with their fields.',
},
{
agent: 'security-reviewer',
description: 'Review auth module',
prompt: 'Check src/auth/ for security vulnerabilities.',
},
],
});
console.log(result.output);result = session.tool("parallel_task", {
"tasks": [
{
"agent": "explore",
"description": "Find API routes",
"prompt": "List all API route definitions with their HTTP methods.",
},
{
"agent": "explore",
"description": "Find DB models",
"prompt": "List all database model definitions with their fields.",
},
{
"agent": "security-reviewer",
"description": "Review auth module",
"prompt": "Check src/auth/ for security vulnerabilities.",
},
],
})
print(result.output)Subagent Events
When streaming, subagent lifecycle events are emitted on the parent session's event channel:
let (mut rx, _handle) = session.stream("Analyze this codebase thoroughly").await?;
while let Some(event) = rx.recv().await {
match event {
AgentEvent::SubagentStart { task_id, agent, description, .. } => {
println!("▶ [{agent}] {description} (task: {task_id})");
}
AgentEvent::SubagentEnd { agent, success, output, .. } => {
let status = if success { "✓" } else { "✗" };
println!("{status} [{agent}] {}", output.chars().take(100).collect::<String>());
}
AgentEvent::End { .. } => break,
_ => {}
}
}Multi-Session Parallelism
For workloads that go beyond subagent delegation — e.g., processing multiple repositories or running the same analysis across different workspaces — you can create multiple independent sessions and drive them concurrently using language-native primitives.
use a3s_code_core::Agent;
use tokio::task::JoinSet;
let agent = Agent::new("agent.hcl").await?;
let workspaces = vec![
"/repos/service-auth",
"/repos/service-billing",
"/repos/service-gateway",
];
// Spawn one session per workspace, all running concurrently
let mut join_set = JoinSet::new();
for workspace in workspaces {
let agent = agent.clone();
join_set.spawn(async move {
let session = agent.session(workspace, None)?;
let result = session.send("Find all TODO comments and summarize them").await?;
Ok::<_, a3s_code_core::CodeError>((workspace.to_string(), result.text))
});
}
// Collect results as they complete
while let Some(result) = join_set.join_next().await {
match result? {
Ok((workspace, text)) => println!("=== {workspace} ===\n{text}\n"),
Err(e) => eprintln!("Error: {e}"),
}
}const { Agent } = require('@a3s-lab/code');
const agent = await Agent.create('agent.hcl');
const workspaces = [
'/repos/service-auth',
'/repos/service-billing',
'/repos/service-gateway',
];
// Run all sessions concurrently with Promise.all
const results = await Promise.all(
workspaces.map(async (workspace) => {
const session = agent.session(workspace);
const result = await session.send('Find all TODO comments and summarize them');
return { workspace, text: result.text };
})
);
for (const { workspace, text } of results) {
console.log(`=== ${workspace} ===\n${text}\n`);
}import asyncio
from a3s_code import Agent
agent = Agent.create("agent.hcl")
workspaces = [
"/repos/service-auth",
"/repos/service-billing",
"/repos/service-gateway",
]
async def analyze(workspace: str) -> tuple[str, str]:
session = agent.session(workspace)
result = session.send("Find all TODO comments and summarize them")
return workspace, result.text
# Run all sessions concurrently
results = asyncio.run(asyncio.gather(*[analyze(ws) for ws in workspaces]))
for workspace, text in results:
print(f"=== {workspace} ===\n{text}\n")Note: Each session is independent — separate conversation history, tool context, and workspace boundary. There is no shared state between parallel sessions.
Combining Both Patterns
For fan-out/fan-in workflows, combine multi-session parallelism with subagent delegation:
use a3s_code_core::Agent;
use tokio::task::JoinSet;
let agent = Agent::new("agent.hcl").await?;
// Phase 1: Parallel exploration across workspaces
let mut join_set = JoinSet::new();
for workspace in &["/repos/frontend", "/repos/backend", "/repos/infra"] {
let agent = agent.clone();
let ws = workspace.to_string();
join_set.spawn(async move {
let session = agent.session(&ws, None)?;
// Each session uses parallel_task internally to fan out further
let result = session.tool("parallel_task", serde_json::json!({
"tasks": [
{ "agent": "explore", "description": "Find dependencies", "prompt": "List all external dependencies." },
{ "agent": "security-reviewer", "description": "Security scan", "prompt": "Scan for known vulnerability patterns." }
]
})).await?;
Ok::<_, a3s_code_core::CodeError>((ws, result.output))
});
}
// Phase 2: Collect and summarize
let mut findings = Vec::new();
while let Some(result) = join_set.join_next().await {
if let Ok(Ok((ws, output))) = result {
findings.push(format!("## {ws}\n{output}"));
}
}
// Phase 3: Feed combined results into a summary session
let summary_session = agent.session("/repos", None)?;
let summary = summary_session.send(&format!(
"Summarize these findings into a single security report:\n\n{}",
findings.join("\n\n")
)).await?;
println!("{}", summary.text);Both built-in and custom agents are available by name. Custom agents loaded from agent_dirs appear alongside built-in agents automatically.
API Reference
AgentDefinition fields
Prop
Type
task tool schema
Prop
Type
Built-in agents
Prop
Type