Skip to main content

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

Engine (agentctl serve)

Stateless. Takes a messages array, returns a response. No memory, no sessions. Port 8090.

agent-manager

Adds conversation persistence, session management, and SSE streaming on top of the engine. Serves the embeddable widget. Port 8100.
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:
PluginWhat it does
ResolverFills {{variables}} in prompts before a node runs
ToolA Python function the LLM can call
HookTrusted code at lifecycle points — auth, audit, policy
Access pluginDecides which protected nodes a caller can reach
All scaffolded by the generate command — see 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 for MCP auth and YAML Reference for protected nodes.