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

  • 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.
  • 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.

Orchestrator and specialists
Main chatorchestrator — reads your request, delegates
Code review
own thread & prompt
BigQuery analysis
own tools
Email in voice
own context
each appears inline as a live chip ↻

The main chat delegates to sub-agents that run in their own threads and report back as inline chips.

When to spawn a sub-agent

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

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

1. @mention a custom agent

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 for the format.

2. The main agent delegates automatically

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

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

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

What spawnTask() does
spawnTask()
create threadnew row in chat_threads, description as first message
persist stateagent-task:<id> → application_state, status=running
streamagent_task_started → chip appears; agent_task_step → chip updates live
completestatus=completed, write summary + preview, emit agent_task_done

Each spawn creates a thread, persists state to SQL, and streams chip events through to completion.

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

From server code or other actions:

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:

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

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 owns the full format (frontmatter, tools, delegate-default, model overrides).

Delegation 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.

Delegation depth guard (default cap 2)
depth 0Top-level chatmay spawn ↓
depth 1Sub-agentmay spawn ↓
depth 2Sub-agent's sub-agentat the cap — may NOT spawn
depth 3Refusedserver-side error

Each level may spawn one deeper until the cap; a spawn past it is refused server-side.

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

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.
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