A3S Docs
A3S Code

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 ordering

With 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 task and parallel_task tools 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 result

Key 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_steps prevents runaway execution
  • can_spawn_subagents: false prevents 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 status
const 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

On this page