Skip to main content

Ideal Agent Architecture

Status: 🧪 Beta

This guide explains the recommended architecture for building a multi-agent system with Agent Teams. Following this pattern produces a scalable, maintainable, and predictable pipeline where each layer has a single responsibility.


The Pipeline

Single domain:
User Prompt → Router → Orchestrator → Worker(s) → Task Done

Multiple domains (parallel dispatch):
User Prompt → Router → Orchestrator A ┐
Orchestrator B ┤ → Workers → Aggregator → Task Done
Orchestrator C ┘
Direct dispatch (autonomous workers, no orchestrator needed):
User Prompt → Router → Worker A (autonomous) ┌
Worker B (autonomous) ┤ → Aggregator → Task Done
Worker C (autonomous) ┘```

Each layer transforms the input in a specific way before passing it to the next. No layer skips steps or takes on responsibilities outside its scope.

---

## Layer 1 — User Prompt

The entry point. The user sends a natural-language message through Copilot Chat using `@router`.

@router Refactor the authentication module to use JWT tokens.


**What happens here:**
- The user expresses intent in plain language
- No structure or routing logic is expected from the user
- The message may reference a file, a domain, or a vague goal

**Best practices:**
- Keep `@router` as the single entry point for all requests
- Avoid bypassing the router by addressing a specific agent directly unless you need pinpoint control

---

## Layer 2 — Router

The router is the traffic controller. It receives every `@router` message, analyzes it, and delegates to the most appropriate agent.

**Responsibilities:**
- Parse intent keywords (e.g. `refactor`, `write`, `review`, `fix`)
- Match the active file's path against agent glob patterns
- Score agents by domain vocabulary, expertise areas, and role
- For single-domain tasks: use `agent-teams-handoff` to open a targeted chat with one orchestrator
- For multi-domain tasks: use `agent-teams-dispatch-parallel` to fan out to multiple orchestrators simultaneously

**What the router must NOT do:**
- Execute any task itself
- Make assumptions about implementation details
- Call orchestrators as direct sub-agent tools — always use the dispatch tools (`agent-teams-handoff` or `agent-teams-dispatch-parallel`)

**Example routing decision:**

| Signal | Value | Matched agent |
|---|---|---|
| Intent keyword | `refactor` | `backend-orchestrator` |
| Active file | `src/auth/jwt.ts` | `backend-orchestrator` |
| Domain match | `backend`, `auth` | `backend-orchestrator` |

> The router is configured as `role: router` in its YAML spec. Only one router agent should exist per team.

---

## Layer 3 — Orchestrator

The orchestrator receives a delegated task from the router and breaks it into an ordered sequence of subtasks. It coordinates workers but does not implement anything itself.

**Responsibilities:**
- Decompose the original request into discrete, actionable steps
- Determine which worker agent handles each step
- Define the order and dependencies between steps
- Aggregate or summarize results from workers before returning a response

**What the orchestrator must NOT do:**
- Write, read, or modify files directly
- Execute shell commands or call external APIs
- Short-circuit to the user without completing the plan

**Example decomposition:**

Task: Refactor authentication module to use JWT tokens

Step 1 → [backend-worker] Analyse existing auth code and identify coupling points Step 2 → [backend-worker] Replace session logic with JWT issuance and validation Step 3 → [testing-worker] Generate unit tests for the new JWT functions Step 4 → [docs-worker] Update API reference documentation


> An orchestrator is configured as `role: orchestrator`. A team can have multiple orchestrators, each specialised in a domain (e.g. `frontend-orchestrator`, `backend-orchestrator`).

---

## Layer 4 — Worker(s)

Workers are the agents that actually do the work. Each worker is a domain specialist with a narrow, well-defined scope.

**Responsibilities:**
- Execute the specific subtask delegated by the orchestrator
- Use only the tools and skills defined in its spec
- Return a structured result to the orchestrator
- Escalate if the subtask is outside its scope

**What a worker must NOT do:**
- Accept tasks outside its declared domain
- Delegate to other workers directly (all coordination goes through the orchestrator)
- Return partial results without signalling incompleteness

**Worker examples:**

| Worker | Domain | Typical tasks |
|---|---|---|
| `backend-worker` | `backend` | API logic, database queries, service layer |
| `frontend-worker` | `frontend` | Components, styles, state management |
| `testing-worker` | `testing` | Unit tests, integration tests, coverage |
| `docs-worker` | `documentation` | README updates, API docs, changelogs |

> Workers are configured as `role: worker`. You can have as many workers as needed — each covering a specific domain or subdomain.

### Autonomous Workers (Direct Dispatch)

Workers can optionally operate in **autonomous mode** via `engram.mode: autonomous`. In this mode the worker is dispatched directly by the router — without an intermediate orchestrator — and manages its own task lifecycle:

- Recalls task context from Engram at session start (using the `[Handoff:{taskId}]` or `[Parallel:{taskId}]` prefix injected by the router).
- Persists its result and calls `complete_subtask` when done to notify the aggregator.
- Automatically receives the `complete-subtask` tool; no manual configuration required.

```yaml
id: frontend-worker
name: Frontend Worker
role: worker
engram:
mode: autonomous

Use autonomous mode when the work within a domain is self-contained and does not require orchestration. For tasks that need multi-step coordination within a domain, keep the standard orchestrator → worker flow.


Layer 5 — Aggregator

The aggregator is used exclusively in parallel dispatch flows. When the router fans out a task to multiple orchestrators simultaneously, the aggregator receives all their results once every subtask is complete, merges them, and returns a unified response to the user.

Responsibilities:

  • Load each orchestrator's result from Engram using the task ID
  • Detect conflicts (e.g. two orchestrators modifying the same file)
  • Report conflicts clearly before presenting the unified outcome
  • Persist the merged result to Engram under task:{taskId}:result

What the aggregator must NOT do:

  • Execute subtasks itself
  • Proceed before all parallel subtasks have signalled completion
  • Silently discard conflicts — all cross-domain file overlaps must be reported

The aggregator is configured as role: aggregator. It is only needed when your team uses parallel dispatch. One aggregator per team is sufficient.


Full Example: End-to-End Flow

Single domain

User:
@router Refactor the authentication module to use JWT tokens.

Router:
→ Scores agents
→ Selects: backend-orchestrator (intent=refactor, file=src/auth/, domain=backend)
→ Calls agent-teams-handoff → opens chat with backend-orchestrator

Orchestrator (backend-orchestrator):
→ Decomposes task into 4 steps
→ Step 1 → backend-worker (analyse existing code)
→ Step 2 → backend-worker (replace session with JWT)
→ Step 3 → testing-worker (write unit tests)
→ Step 4 → docs-worker (update API docs)
→ Aggregates results
→ Returns summary to user

Task done.

Multiple domains (parallel dispatch)

User:
@router Add a new /payments endpoint with frontend form and full test coverage.

Router:
→ Identifies 3 independent domains: backend, frontend, testing
→ Calls agent-teams-dispatch-parallel with subtasks for each orchestrator
→ Opens 3 parallel chats simultaneously

Orchestrators (in parallel):
backend-orchestrator → implements the REST endpoint
frontend-orchestrator → builds the payment form component
testing-orchestrator → writes end-to-end and unit tests

Each orchestrator calls agent-teams-complete-subtask when done

Aggregator:
→ Loads all 3 results from Engram
→ Detects conflicts (e.g. both backend and frontend touched api-client.ts)
→ Reports conflicts and presents unified outcome

Task done.

Architecture Diagram

Single domain

┌─────────────┐
│ User Prompt │
└──────┬──────┘
│ @router message

┌─────────────┐ agent-teams-handoff
│ Router │ ──────────────────────────────┐
└─────────────┘ │

┌────────────────────────┐
│ Orchestrator │
│ (decomposes the task) │
└────────┬───────────────┘
│ delegates subtasks
┌────────────────┼────────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Worker A │ │ Worker B │ │ Worker C │
│ (backend) │ │ (testing) │ │ (docs) │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
└───────────────┴────────────────┘
│ results

┌──────────────────┐
│ Task Done ✓ │
└──────────────────┘

Multiple domains (parallel dispatch)

┌─────────────┐
│ User Prompt │
└──────┬──────┘
│ @router message

┌─────────────┐ agent-teams-dispatch-parallel
│ Router │ ─────────────────────────────────────────────┐
└─────────────┘ │
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Orchestrator A │ │ Orchestrator B │ │ Orchestrator C │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ workers │ workers │ workers
▼ ▼ ▼
[complete-subtask] [complete-subtask] [complete-subtask]
│ │ │
└──────────────────────┴──────────────────────┘
│ all subtasks done

┌──────────────────┐
│ Aggregator │
│ (merges results, │
│ flags conflicts) │
└────────┬─────────┘


┌──────────────────┐
│ Task Done ✓ │
└──────────────────┘

Design Principles

PrincipleDescription
Single responsibilityEach layer has one job and does not bleed into another
One entry pointAll requests flow through @router — no shortcuts
Orchestrators coordinate, workers executeNever mix planning and doing in the same agent
Explicit handoffsEach agent declares handoffs.delegates_to and handoffs.receives_from
Escalation over failureWorkers escalate to the orchestrator when blocked — they never fail silently
Dispatch tools for multi-domainRouters use agent-teams-handoff or agent-teams-dispatch-parallel — never call orchestrators directly
Aggregator closes the loopParallel flows always end with an aggregator that merges results and surfaces conflicts

Configuring Handoffs in YAML

# backend-orchestrator.yml
id: backend-orchestrator
name: Backend Orchestrator
role: orchestrator
handoffs:
receives_from:
- router
delegates_to:
- backend-worker
- testing-worker
- docs-worker
escalates_to:
- human
# backend-worker.yml
id: backend-worker
name: Backend Worker
role: worker
handoffs:
receives_from:
- backend-orchestrator
delegates_to: []
escalates_to:
- backend-orchestrator
# results-aggregator.yml
id: results-aggregator
name: Results Aggregator
role: aggregator
handoffs:
receives_from:
- backend-orchestrator
- frontend-orchestrator
- testing-orchestrator
delegates_to: []
escalates_to:
- human

Common Anti-Patterns

Anti-patternProblemFix
User bypasses routerNo routing logic applied, wrong agent pickedAlways use @router as the entry point
Router executes tasksRouter becomes a bottleneck and a single point of failureRouter only routes — never acts
Router calls orchestrators directly as toolsBypasses context passing and task tracking via EngramAlways use agent-teams-handoff or agent-teams-dispatch-parallel
Orchestrator writes codeMixes coordination and execution, hard to debugMove execution to a dedicated worker
Worker delegates to another workerCreates hidden dependencies and circular callsAll delegation must go through the orchestrator
Single "do everything" agentUnscalable, context bloat, unpredictable outputSplit by domain into multiple focused workers
Parallel dispatch without an aggregatorResults are never merged; conflicts go undetectedAdd a role: aggregator agent to your team
Aggregator starts before all subtasks completeReads incomplete Engram state, produces wrong outputagent-teams-complete-subtask ensures the aggregator opens only after all subtasks signal completion
Using engram.mode: autonomous for multi-step domain tasksAutonomous workers have no orchestrator to sequence steps or retry failuresUse autonomous mode only for self-contained, single-step domain tasks