---
title: "Agent Teams"
description: "The main agent delegates work to sub-agents that run in their own threads and appear as live preview chips inline in chat."
---

# Agent Teams

The agent chat is an **orchestrator**, not a monolith. When the main agent hits a task that's better owned by a specialist — "write this email in my voice," "run a BigQuery analysis," "review this PR" — it spawns a sub-agent in its own thread, tools, and context. The sub-agent shows up as a live preview **chip** inline in the main chat; click it to open the full conversation as a tab.

This keeps the main thread focused, lets sub-agents run in parallel, and gives you a clean audit trail for any delegated work.

Agent Teams runs on the core run-manager: events stream and persist, aborts propagate through SQL, and tasks survive serverless cold starts.

## The mental model {#mental-model}

- **Main chat** — the orchestrator. Reads your request, delegates. Rarely does heavy work itself.
- **Sub-agents** — run with their own thread, their own system prompt, their own tool set. Each maps to a "custom agent" profile in [Agent Resources](/docs/agent-resources).
- **Chips** — the rich preview card that appears inline in the main chat, showing the sub-agent's current step, streaming output, and final summary. Collapsed by default; expands to the full conversation on click.
- **Bidirectional messaging** — the main agent can send follow-ups to a running sub-agent; a sub-agent can message back when it hits an ambiguous point.

Sub-agent state is persisted in the `application_state` SQL table (under `agent-task:<taskId>`), so tasks survive serverless cold starts and work across multiple processes.

<Diagram id="doc-block-hic7ac" title="Orchestrator and specialists" summary="The main chat delegates to sub-agents that run in their own threads and report back as inline chips.">

```html
<div class="at-orc">
  <div class="diagram-card main">
    <span class="diagram-pill accent">Main chat</span
    ><small class="diagram-muted"
      >orchestrator &mdash; reads your request, delegates</small
    >
  </div>
  <div class="at-fan">
    <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
    <div class="at-subs">
      <div class="diagram-box">
        Code review<br /><small class="diagram-muted"
          >own thread &amp; prompt</small
        >
      </div>
      <div class="diagram-box">
        BigQuery analysis<br /><small class="diagram-muted">own tools</small>
      </div>
      <div class="diagram-box">
        Email in voice<br /><small class="diagram-muted">own context</small>
      </div>
    </div>
  </div>
  <div class="diagram-pill">each appears inline as a live chip &#8635;</div>
</div>
```

```css
.at-orc {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
}
.at-orc .diagram-card {
  padding: 14px 18px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  align-items: center;
}
.at-orc .at-fan {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
.at-orc .diagram-arrow {
  font-size: 22px;
}
.at-orc .at-subs {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  justify-content: center;
}
.at-orc .diagram-box {
  text-align: center;
}
```

</Diagram>

## When to spawn a sub-agent {#when-to-spawn}

Spawn when the task:

- Needs a different **system prompt** (a specialist voice or tone, e.g., "code review").
- Has a **long-running** tool chain that would pollute the main context.
- Can run **in parallel** with other work the main agent is doing.
- Is owned by a **different team** that already has a custom agent profile.

Don't spawn for trivial one-shot work — call the action directly.

## Invoking a sub-agent {#invoking}

Three ways to kick off a sub-agent, from least to most explicit:

### 1. `@mention` a custom agent {#mention}

The user types `@agent-name` in the chat composer. A dropdown of workspace sub-agents appears. Selecting one inserts a chip; on submit the main agent delegates the message to that sub-agent.

Custom agents live in agent resources at `agents/<slug>.md` — a Markdown file with YAML frontmatter. See [Custom Agents](/docs/agent-resources#custom-agents) for the format.

### 2. The main agent delegates automatically {#auto-delegate}

The framework gives the main agent an `agent-teams` tool. When the model decides a task fits a registered sub-agent profile, it calls the tool with `action: "spawn"` and an optional `agent` parameter naming a profile from `agents/*.md`. A chip appears; the sub-agent runs. The main agent waits (or moves on in parallel) and incorporates the result when the sub-agent finishes.

The full `agent-teams` action set is:

| Action        | Purpose                              |
| ------------- | ------------------------------------ |
| `spawn`       | Start a new sub-agent task           |
| `status`      | Check a running sub-agent's progress |
| `read-result` | Get a finished sub-agent's output    |
| `send`        | Message a running sub-agent          |
| `list`        | See all tasks for the current user   |

### 3. Programmatic spawn {#programmatic-spawn}

For framework-level integrations, use `spawnTask()` from `@agent-native/core/server`:

```ts
import { spawnTask } from "@agent-native/core/server";

const task = await spawnTask({
  description: "Draft an outreach email to this lead",
  instructions: "Match the user's voice from memory/MEMORY.md.",
  ownerEmail: user.email,
  systemPrompt: mailAgentSystemPrompt,
  actions: mailActions,
  // Pass either apiKey or engine — engine takes precedence.
  apiKey: process.env.ANTHROPIC_API_KEY, // optional if engine is provided
  parentSend: emit, // streaming sender for the parent chat response
});
```

Most app code won't call this directly — the framework does it under the hood for `@mentions` and for the `agent-teams` tool. Reach for `spawnTask()` only when you're wiring a new entry point (e.g., a button that kicks off a background job that runs as a sub-agent).

## Task lifecycle {#lifecycle}

<Diagram id="doc-block-1tm68y5" title="What spawnTask() does" summary="Each spawn creates a thread, persists state to SQL, and streams chip events through to completion.">

```html
<div class="at-life">
  <div class="diagram-box"><code>spawnTask()</code></div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">create thread</span
    ><small class="diagram-muted"
      >new row in <code>chat_threads</code>, description as first message</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill">persist state</span
    ><small class="diagram-muted"
      ><code>agent-task:&lt;id&gt;</code> &rarr; <code>application_state</code>,
      status=running</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card">
    <span class="diagram-pill accent">stream</span
    ><small class="diagram-muted"
      ><code>agent_task_started</code> &rarr; chip appears;
      <code>agent_task_step</code> &rarr; chip updates live</small
    >
  </div>
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&darr;</div>
  <div class="diagram-card ok">
    <span class="diagram-pill ok">complete</span
    ><small class="diagram-muted"
      >status=completed, write summary + preview, emit
      <code>agent_task_done</code></small
    >
  </div>
</div>
```

```css
.at-life {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  max-width: 560px;
}
.at-life .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 10px 14px;
}
.at-life .diagram-box {
  align-self: flex-start;
}
.at-life .diagram-arrow {
  font-size: 18px;
  align-self: center;
}
```

</Diagram>

At any point the parent agent can resume the sub-agent with a follow-up via `sendToTask(taskId, message)`. If the sub-agent errors, `markTaskErrored(taskId, reason)` records the failure and surfaces it to the user.

Two-way messaging is durable. Parent follow-ups to running sub-agents are
delivered through the task lifecycle; if the sub-agent cannot consume them in
the current step, they should remain queued and be applied at a safe
continuation point. Sub-agents can also message back when they need clarification
instead of blocking invisibly.

## Reading task state {#reading-state}

From server code or other actions:

```ts
import { getTask, listTasks } from "@agent-native/core/server";

const task = await getTask(taskId); // single task
const tasks = await listTasks(); // all tasks for the user (sorted newest first)
```

`AgentTask` key fields:

```ts
interface AgentTask {
  taskId: string;
  threadId: string;
  description: string;
  status: "running" | "completed" | "errored";
  preview: string; // short one-liner for the chip
  summary: string; // full summary once completed
  currentStep: string; // latest step label (updated while running)
  createdAt: number;
  delegationDepth?: number; // 1 for a direct sub-agent, 2 for a sub-agent of that sub-agent, etc.
  // Additional fields: parentThreadId, name, updatedAt, startedAt,
  // completedAt, runId, error
}
```

## Custom agent profiles {#profiles}

Sub-agents map to custom agent profiles — Markdown files at `agents/<slug>.md` in agent resources that appear in the `@mention` dropdown and serve as delegation targets. [Agent Resources — Custom Agents](/docs/agent-resources#custom-agents) owns the full format (frontmatter, `tools`, `delegate-default`, model overrides).

## Delegation depth guard {#depth-guard}

Sub-agents can spawn sub-agents, which is a runaway/cost risk: an unbounded chain of delegations could fan out indefinitely. The framework enforces a **hard cap on delegation depth**, server-side, independent of any tool-level guard.

The top-level chat is depth `0`. A sub-agent it spawns is depth `1`; that sub-agent may spawn once more (depth `2`); a spawn that would create a depth-`3` sub-agent is **refused**. The default cap is **2**.

<Diagram id="doc-block-1raa8ja" title="Delegation depth guard (default cap 2)" summary={"Each level may spawn one deeper until the cap; a spawn past it is refused server-side."}>

```html
<div class="at-depth">
  <div class="diagram-card ok">
    <span class="diagram-pill">depth 0</span><strong>Top-level chat</strong
    ><small class="diagram-muted ok">may spawn &darr;</small>
  </div>
  <div class="diagram-card ok">
    <span class="diagram-pill">depth 1</span><strong>Sub-agent</strong
    ><small class="diagram-muted ok">may spawn &darr;</small>
  </div>
  <div class="diagram-card warn">
    <span class="diagram-pill warn">depth 2</span
    ><strong>Sub-agent's sub-agent</strong
    ><small class="diagram-muted">at the cap &mdash; may NOT spawn</small>
  </div>
  <div class="diagram-card">
    <span class="diagram-pill warn">depth 3</span><strong>Refused</strong
    ><small class="diagram-muted">server-side error</small>
  </div>
</div>
```

```css
.at-depth {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.at-depth .diagram-card {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 10px 14px;
}
.at-depth .rung-1,
.at-depth .diagram-card:nth-child(2) {
  margin-inline-start: 24px;
}
.at-depth .diagram-card:nth-child(3) {
  margin-inline-start: 48px;
}
.at-depth .diagram-card:nth-child(4) {
  margin-inline-start: 72px;
}
```

</Diagram>

Enforcement is ambient: each sub-agent runs inside an `AsyncLocalStorage` that records its own depth, so any `spawnTask` reached transitively from that run reads its parent's depth and refuses once the cap is hit — even if the `agent-teams` tool was handed to a sub-agent that should not have had it. The decision is exposed as a pure, unit-testable `evaluateSubagentDepth(parentDepth)`. A refused spawn returns a clear error: _"Delegation depth limit reached (max N); cannot spawn another sub-agent."_

### Configuring the cap {#depth-guard-config}

Override the default at deploy time with `AGENT_NATIVE_MAX_SUBAGENT_DEPTH`:

| Value           | Effect                                                                                                                                |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| _(unset)_       | Default cap of `2`.                                                                                                                   |
| `0`             | **No sub-agents may be spawned** — the top-level agent does all work.                                                                 |
| `1`…`16`        | That many levels of delegation.                                                                                                       |
| invalid / `>16` | A non-integer / negative / NaN value falls back to `2`; anything above `16` is clamped to `16` so a typo can never disable the guard. |

```bash
AGENT_NATIVE_MAX_SUBAGENT_DEPTH=1   # sub-agents allowed, but they can't sub-delegate
```

When a sub-agent is at or below the cap, the framework injects a line into its runtime context telling it how deep it sits and whether it may delegate further, so the model spends its budget appropriately.

## What's next

- [**Agent Resources — Custom Agents**](/docs/agent-resources#custom-agents) — the profile format
- [**A2A Protocol**](/docs/a2a-protocol) — when the "sub-agent" lives in a different app entirely
- [**Actions**](/docs/actions) — the tools a sub-agent calls
