Skip to main content

Overview

Plugins are where your business logic lives. The engine is generic — it doesn’t know what a customer is or what a subscription tier means. You teach it through plugins. There are three plugin types:
PluginPurpose
ResolverFill {{variables}} in prompts before a node runs
ToolPython functions the LLM can call
AccessDecide whether a caller can reach a protected node
All live under a single plugins/ package, described by one manifest file.

File layout

plugins/
  __init__.py
  plugins.toml          ← single manifest for all plugins
  resolvers/
    __init__.py
    shared.py           ← SharedResolver (shared methods)
    orders_agent.py     ← per-agent Resolver subclass
    returns_agent.py
  tools/
    __init__.py
    get_order_status.py
    create_return.py
  hooks/
    __init__.py
    mcp_auth.py
  access.py             ← access control (only if you use protected nodes)
Run generate --config agents.yml (see Quickstart) to create all stubs from your YAML.

Resolver plugins

Resolvers fill prompt variables. They run before the agent, they’re chosen by the engine (not the LLM), and they cost no tokens.

Declaration

resolvers:
  current_date:
    scope: shared    # generated once on SharedResolver, all agents inherit it
  customer_name:
    scope: shared
  account_tier:
    scope: agent     # generated per-agent, only agents that declare it get it

Implementation

The generate command produces the file layout. Fill in the methods:
# plugins/resolvers/shared.py
from datetime import datetime

class SharedResolver:
    def current_date(self, ctx) -> str:
        return datetime.now().strftime("%Y-%m-%d")

    def customer_name(self, ctx) -> str:
        # ctx carries request headers and metadata
        return ctx.headers.get("X-Customer-Name", "Customer")
# plugins/resolvers/orders_agent.py
from plugins.resolvers.shared import SharedResolver

class Resolver(SharedResolver):
    def account_tier(self, ctx) -> str:
        customer_id = ctx.headers.get("X-Customer-Id")
        return lookup_account_tier(customer_id)   # your business logic
Resolver inherits current_date and customer_name from SharedResolver. Only account_tier (agent-scoped) needs an implementation here.

Generation modes

generate agents.yml               # default: all stubs
generate agents.yml --mode all    # regenerate everything
generate agents.yml --mode child --agent orders_agent   # one agent
See CLI Reference for how to run these via Docker or a local install. Without --force, existing method bodies are preserved — only missing stubs are added.

Manifest

The plugins.toml manifest maps plugin ids to class paths. Generated automatically:
[resolvers]
shared = "plugins.resolvers.shared:SharedResolver"
orders_agent = "plugins.resolvers.orders_agent:Resolver"

[tools]
get_order_status = "plugins.tools.get_order_status:get_order_status"

[hooks.plugins]
mcp_auth = "plugins.hooks.mcp_auth:McpAuthHook"

Access plugin

Marks certain nodes as protected and gates them behind your own access logic. Protected nodes are hidden from the router entirely if access is denied — the model never knows they exist.

Declaration

agents:
  admin_agent:
    description: "Internal admin operations."
    protected: true
    prompts:
      system: "prompts/admin/system.md"

  vip_agent:
    description: "Exclusive offers for VIP members."
    protected: true
    prompts:
      system: "prompts/vip/system.md"

Implementation

When any node has protected: true, the engine requires plugins/access.py:
# plugins/access.py
class AccessResolver:
    def can_access(self, ctx, node_id: str) -> bool:
        if node_id == "admin_agent":
            return ctx.headers.get("X-Role") == "admin"
        if node_id == "vip_agent":
            return ctx.headers.get("X-Customer-Tier") == "vip"
        return True
ctx carries request headers and metadata — use whatever your auth system provides. The engine calls this before routing and removes any node where can_access returns False.
protected: true without plugins/access.py is a startup error. The engine won’t serve requests until it’s provided.

Making plugins importable

If commands run from different working directories, declare import_roots so Python can always find your package:
plugins:
  import_roots: ["."]   # resolved relative to the agents.yml file
This is the recommended approach. Alternatively, install your package with pip install -e ..