Athyr Agent

Athyr Agent is a YAML-driven agent runner. Define your agent’s behavior, topics, and tools in a YAML file, connect it to an Athyr server, and start processing messages — no code required.

Installation

Homebrew (macOS)

brew install athyr-tech/tap/athyr-agent

Download Binary

Pre-built binaries are available for macOS and Linux from GitHub Releases.

macOS:

# Apple Silicon (M1/M2/M3/M4)
curl -LO https://github.com/athyr-tech/athyr-agent/releases/latest/download/athyr-agent-darwin-arm64.tar.gz

# Intel
curl -LO https://github.com/athyr-tech/athyr-agent/releases/latest/download/athyr-agent-darwin-amd64.tar.gz

tar xzf athyr-agent-darwin-*.tar.gz
sudo mv athyr-agent /usr/local/bin/

Linux:

# AMD64
curl -LO https://github.com/athyr-tech/athyr-agent/releases/latest/download/athyr-agent-linux-amd64.tar.gz

# ARM64
curl -LO https://github.com/athyr-tech/athyr-agent/releases/latest/download/athyr-agent-linux-arm64.tar.gz

tar xzf athyr-agent-linux-*.tar.gz
sudo mv athyr-agent /usr/local/bin/

Build from Source

Requires Go 1.25+:

go install github.com/athyr-tech/athyr-agent/cmd/athyr-agent@latest

Verify

athyr-agent version

Quick Start

Create a file called agent.yaml:

agent:
  name: my-agent
  description: A simple agent
  model: google/gemini-2.5-flash-lite
  instructions: |
    You are a helpful assistant. Respond concisely.
  topics:
    subscribe: [requests.new]
    publish: [requests.done]

Run it:

athyr-agent run agent.yaml --server localhost:9090

Or with the interactive TUI:

athyr-agent run agent.yaml --server localhost:9090 --tui

The agent connects to your Athyr server, subscribes to the requests.new topic, processes messages through the LLM, and publishes responses to requests.done.

YAML Configuration

All configuration lives under the agent key. The three required fields are name, model, and topics.

Full Example

This example shows an agent that mixes Lua plugins with Athyr topics — it watches the filesystem for new files via a plugin, also receives tasks from other agents via an Athyr topic, and publishes results to both an Athyr topic and an external webhook:

agent:
  name: file-processor
  description: Watches for new files and posts results to a webhook
  model: google/gemini-2.5-flash-lite
  instructions: |
    You are a helpful assistant. Summarize any content you receive.

  topics:
    subscribe:
      - file-watcher    # Plugin source — polls the host filesystem for new files
      - tasks.assigned  # Athyr topic — receives messages from other agents
    publish:
      - results.ready   # Athyr topic — other agents can subscribe to this
      - webhook-output  # Plugin destination — posts to an external webhook

  memory:
    enabled: true
    profile:
      type: rolling_window
      max_tokens: 4096
      summarization_threshold: 3000

  mcp:
    servers:
      - name: docker-gateway
        command: ["docker", "mcp", "gateway", "run"]

  plugins:
    - name: file-watcher
      file: ./plugins/watcher.lua
      config:
        path: /var/log/app
        interval: 5

    - name: webhook-output
      file: ./plugins/http-output.lua
      config:
        url: https://hooks.example.com/notify

  connection:
    timeout: 60s
    max_retries: 0
    base_backoff: 1s
    max_backoff: 30s

Agent Fields

FieldTypeRequiredDescription
namestringyesUnique agent name for Athyr registration
descriptionstringnoHuman-readable description
modelstringyesLLM model identifier (e.g., google/gemini-2.5-flash-lite, openai/gpt-4o-mini)
instructionsstringnoSystem prompt sent with every LLM request
topicsobjectyesPub/sub topic configuration
memoryobjectnoSession memory settings
mcpobjectnoMCP tool server connections
pluginslistnoLua plugin definitions
connectionobjectnoConnection tuning

Topics

Defines which topics the agent subscribes to and publishes on. Topic names can refer to Athyr topics or Lua plugin names — when a name matches a plugin’s name, the runner routes through the plugin. Athyr topics and plugins can be mixed freely.

topics:
  subscribe:
    - documents.new
    - documents.updated
  publish:
    - summaries.ready
FieldTypeRequiredDescription
subscribelistyesTopics to receive messages from
publishlistyesDefault topics to send responses to
routeslistnoDynamic routing destinations

Dynamic Routing

Routes let the LLM decide where to send responses. When routes are configured, routing instructions are appended to the system prompt automatically. The LLM returns a JSON response with a route_to field — if the route is valid, the response goes there. Otherwise it falls back to the default publish topics.

agent:
  name: classifier
  description: Classifies and routes support tickets
  model: google/gemini-2.5-flash-lite
  instructions: |
    Analyze the customer's message and classify it as billing or technical.

  topics:
    subscribe:
      - ticket.new
    publish:
      - ticket.unknown    # Fallback if routing fails
    routes:
      - topic: ticket.billing
        description: Payment issues, invoices, refunds
      - topic: ticket.technical
        description: Bugs, errors, crashes, feature requests

Conversation Memory

Enable multi-turn conversations by activating session memory. Messages must include a session_id field in their JSON payload.

memory:
  enabled: true
  profile:
    type: rolling_window
    max_tokens: 4096
    summarization_threshold: 3000
FieldTypeDefaultDescription
enabledboolfalseEnable session memory
session_prefixstringPrefix for session IDs
ttlstringSession expiration (e.g., 1h, 24h)
profile.typestringrolling_windowMemory management strategy
profile.max_tokensint4096Max tokens kept in memory
profile.summarization_thresholdint3000Token count that triggers summarization

Messages with memory use JSON format:

{"session_id": "user-123", "content": "Hello, remember me?"}

Plain text messages (without session_id) are processed normally without memory.

MCP Tools

Connect to MCP servers to give your agent access to external tools. Tools are discovered automatically on startup and passed to the LLM with each request.

mcp:
  servers:
    # Local subprocess (stdio transport)
    - name: docker-gateway
      command: ["docker", "mcp", "gateway", "run"]
      env:
        API_KEY: "sk-..."

    # Remote server (HTTP transport)
    - name: remote-tools
      url: https://mcp.example.com/tools

Each server needs a name and exactly one transport — either command (subprocess) or url (HTTP endpoint). Optional env provides environment variables for subprocess commands.

Connection Tuning

FieldTypeDefaultDescription
timeoutduration60sRequest timeout
max_retriesint0 (infinite)Max reconnection retries
base_backoffduration1sInitial backoff between retries
max_backoffduration30sMaximum backoff duration

Duration values use Go syntax: 30s, 2m, 1h.

Lua Plugins

Plugins extend the agent’s transport layer — they let agents interact with the host machine and external systems alongside Athyr topics. A file-watcher plugin can trigger the agent when new files appear on the host filesystem; an HTTP plugin can post responses to an external webhook. Plugins and Athyr topics work together in the same agent.

Plugins are Lua scripts that implement a simple contract and run in a sandboxed VM.

How It Works

A plugin’s name must appear in topics.subscribe or topics.publish to be active:

Plugin Contract

A plugin is a Lua file that defines one or both of these functions:

-- Source: called when agent starts, runs in its own goroutine
function subscribe(config, callback)
    -- config: table with values from YAML config section
    -- callback: function(string) — sends data to the agent
end

-- Destination: called each time the agent produces a response for this plugin
function publish(config, data)
    -- config: table with values from YAML config section
    -- data: string — the agent's response content
end

Example: File Watcher to Webhook

plugins/watcher.lua (source):

local fs = require("fs")

function subscribe(config, callback)
    local seen = {}
    local interval = config.interval or 5

    -- Build initial set of known files
    local entries = fs.list(config.path)
    for i = 1, #entries do
        seen[entries[i]] = true
    end

    while true do
        sleep(interval)
        local current = fs.list(config.path)
        for i = 1, #current do
            local name = current[i]
            if not seen[name] then
                seen[name] = true
                local content = fs.read(config.path .. "/" .. name)
                callback(content)
                log("info", "file-watcher: new file detected: " .. name)
            end
        end
    end
end

plugins/http-output.lua (destination):

local http = require("http")
local json = require("json")

function publish(config, data)
    local payload = json.encode({
        content = data,
    })

    local headers = {
        ["Content-Type"] = config.content_type or "application/json",
    }

    local resp = http.post(config.url, payload, headers)
    if resp.status >= 400 then
        log("error", "http-output: webhook returned status " .. resp.status)
    else
        log("info", "http-output: posted to " .. config.url)
    end
end

agent.yaml:

agent:
  name: file-processor
  description: Processes new files and sends results to a webhook
  model: google/gemini-2.5-flash-lite
  instructions: |
    You receive the contents of newly created files.
    Analyze the content and provide a brief summary.

  topics:
    subscribe:
      - file-watcher
    publish:
      - http-output

  plugins:
    - name: file-watcher
      file: ./plugins/watcher.lua
      config:
        path: ./watched
        interval: 5

    - name: http-output
      file: ./plugins/http-output.lua
      config:
        url: https://httpbin.org/post
        content_type: application/json

Bridge Modules

Plugins have access to these Go-backed modules:

fs — File System

local fs = require("fs")
local content = fs.read("/path/to/file")
fs.write("/path/to/file", "data")
local entries = fs.list("/path/to/dir")

http — HTTP Client

local http = require("http")
local resp = http.get("https://api.example.com/data")
-- resp.body, resp.status

local resp = http.post("https://api.example.com/webhook", '{"key": "value"}', {
    ["Content-Type"] = "application/json"
})

json — JSON Encoding/Decoding

local json = require("json")
local str = json.encode({name = "test", count = 42})
local tbl = json.decode('{"name": "test"}')

Global Functions

sleep(5)                           -- sleep 5 seconds
sleep(0.1)                         -- sleep 100 milliseconds
log("info", "processing started")  -- levels: debug, info, warn, error

Plugin Restrictions

Limit what a plugin can access using the restrict field:

plugins:
  - name: community-formatter
    file: ./plugins/formatter.lua
    restrict:
      - http           # Blocks all http.* functions
      - fs.write       # Blocks fs.write only
ValueEffect
httpBlocks http.get and http.post
fsBlocks all fs.* functions
fs.writeBlocks fs.write only
fs.readBlocks fs.read only
http.postBlocks http.post only

Sandbox

Each plugin runs in its own Lua VM with:

CLI Reference

CommandDescription
run <file>Run an agent from a YAML config file
validate <file>Check YAML syntax without running
versionDisplay version
disconnect <id>Remove an agent from the Athyr server
FlagDescription
--serverAthyr server address (default: localhost:9090)
--insecureDisable TLS
--tuiEnable interactive terminal UI
--verboseDebug-level logging
--log-formatOutput format: text or json

Validate before running:

athyr-agent validate agent.yaml

Examples

The athyr-agent repository includes ready-to-run examples:

ExampleDescription
simple-test.yamlBasic connectivity testing
summarizer.yamlDocument summarization with structured output
memory-chat.yamlMulti-turn conversations with session memory
mcp-tools.yamlResearch assistant with MCP tool integration
plugin-agent.yamlFile watcher → LLM → webhook pipeline
demo/Multi-agent support system with classifier routing

Next Steps