> ## Documentation Index
> Fetch the complete documentation index at: https://docs.extra-ai.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> How Extra works — from YAML to a running response.

## The big picture

You write a YAML file. The engine validates it, compiles it into an internal graph, and runs it. Every request goes through the same pipeline:

```
agents.yml
  → Validate      checks schema, references, graph structure
  → Compile       builds an immutable typed graph
  → RuntimeEngine created once at startup, shared across requests
  → per-request   route → resolve → render → execute → respond
```

Two distinct phases: **build** (once at startup) and **runtime** (per request).

***

## Build phase

Happens once before the engine accepts any requests.

* Parses and validates the YAML (schema + semantic checks)
* Validates all references — every tool, resolver, MCP id declared by an agent must exist
* Verifies prompt files exist on disk
* Connects to MCP servers and discovers their tools
* Compiles everything into an immutable `CompiledAgentGraph`

If anything is wrong, the engine fails here — before serving a single request.

***

## Runtime phase

Per request:

1. A message arrives
2. A fresh `ExecutionContext` is created (isolated per request)
3. Protected nodes are filtered based on the access plugin
4. The root orchestrator runs — children are exposed to it as callable tools
5. The orchestrator routes to the right child
6. Resolvers run for that child, filling `{{variables}}` in prompts
7. The agent executes with its tools bound
8. The response + trace is returned

The engine itself holds no per-request state. Everything specific to a request lives in `ExecutionContext`.

***

## Two server modes

<CardGroup cols={2}>
  <Card title="Engine (agentctl serve)" icon="bolt">
    Stateless. Takes a `messages` array, returns a response. No memory, no sessions. Port 8090.
  </Card>

  <Card title="agent-manager" icon="database">
    Adds conversation persistence, session management, and SSE streaming on top of the engine. Serves the embeddable widget. Port 8100.
  </Card>
</CardGroup>

The engine has no dependency on `agent-manager`. You can use either independently.

***

## Orchestrators vs. agents

**Orchestrators** are routers. They see their children's `description` fields and decide where to send the request. They don't call tools directly.

**Agents** are executors. They run an LLM with their prompts and can call tools and MCP servers.

Both run a tool-call loop — the model keeps calling tools until it decides it's done.

```
User: "Where is my order?"
  → router (orchestrator)
    → orders_agent (agent)
      → get_order_status() [tool]
      → "Your order is on the way, arriving Thursday."
```

***

## Extension points

The engine contains no business logic. Customer-specific code lives in plugins:

| Plugin            | What it does                                           |
| ----------------- | ------------------------------------------------------ |
| **Resolver**      | Fills `{{variables}}` in prompts before a node runs    |
| **Tool**          | A Python function the LLM can call                     |
| **Hook**          | Trusted code at lifecycle points — auth, audit, policy |
| **Access plugin** | Decides which `protected` nodes a caller can reach     |

All scaffolded by the `generate` command — see [Quickstart](/docs/quickstart). You fill in the business logic.

***

## Security

Prompt text is not a security boundary. Telling the model "only show data for this user" in a prompt can be overridden. Real enforcement happens at the tool and access-plugin layer, where the runtime controls what gets called and with what parameters.

See [Runtime Hooks](/docs/runtime-hooks) for MCP auth and [YAML Reference](/docs/yaml-spec#access-control) for protected nodes.
