Architecture

Wactorz is a Python-first asyncio actor-model framework for building multi-agent AI systems. Agents communicate via MQTT and run in a single process — start everything with one command.

Overview

The framework is built on three ideas: every agent is an independent actor with its own message loop; MQTT is the universal communication bus (between agents, to the dashboard, and to external systems); and DynamicAgent allows the LLM to write and spawn new agents at runtime without restarting the process.

┌─────────────────────────────────────────────────────────────────┐
  User interfaces                                                 
  CLI  ·  REST  ·  Discord  ·  WhatsApp  ·  Telegram             
  Web UI (port 8888)                                              
└────────────────────────┬────────────────────────────────────────┘
                           asyncio  (single Python process)
                         
┌────────────────────────────────────────────────────────────────────────┐
  ActorSystem  (wactorz/core/registry.py)                               
                                                                        
  Supervisor  ──  ONE_FOR_ONE restart strategy per actor                
  ActorRegistry  ──  name  actor lookup, MQTT publisher                
                                                                        
  MainActor           LLM orchestrator, intent routing, spawn registry  
  MonitorAgent        heartbeat watchdog, alerts main on failure        
  IOAgent             MQTTUI gateway, routes chat to main             
  CatalogAgent        pre-built recipe library, spawns on request       
  InstallerAgent      pip installs deps for dynamic agents              
  HomeAssistantAgent  HA REST API  entities, services, automations     
  PlannerAgent        spawned per-request, builds multi-agent pipelines 
  DynamicAgent*       LLM-generated Python, spawned at runtime         
└────────────────────────────┬───────────────────────────────────────────┘
                               pub / sub
                             
                    MQTT broker  (embedded, starts with wactorz)
                    :1883 TCP  (default)
                             
              ┌──────────────┴──────────────┐
                                           
   Home Assistant              External systems via MQTT
   WebSocket API               custom topics, any device or service

💡 One command to start — Running wactorz starts everything — the actor system, and the web dashboard. Note a separate MQTT broker process needed.


Components

Core — wactorz/core/

File Responsibility
actor.py Actor base class — message loop, heartbeat, persistence (SQLite / Redis / Pickle), supervisor strategy enum
registry.py ActorSystem, ActorRegistry, Supervisor, _MQTTPublisher (shared aiomqtt connection)

Built-in Agents — wactorz/agents/

See the Agents reference for full documentation. A summary of the core actors:

Agent Role
MainActor LLM orchestrator. Classifies intent, routes messages, manages the spawn registry and user memory.
PlannerAgent Spawned per pipeline request. Generates a multi-agent plan and spawns the required DynamicAgents.
DynamicAgent Executes LLM-generated Python at runtime from a setup() / process() / handle_task() code string.
CatalogAgent Pre-built recipe library. Loads agents from catalogue_agents/ and spawns them on request.
HomeAssistantAgent Wraps the HA REST API — entities, services, automations. Uses internal LLM calls for classification.
HomeAssistantStateBridgeAgent Streams HA state changes to MQTT so pipeline agents can react to device events in real time.
MonitorAgent Tracks heartbeats from all actors. Alerts main when an agent is unresponsive for >60 s.
InstallerAgent Runs pip install in a subprocess. Called automatically before spawning a recipe with declared dependencies.
LLMAgent Base class for LLM-backed agents. Handles conversation history, rolling summarisation, and cost tracking across all providers.

Actor Lifecycle

Every actor in Wactorz follows the same asyncio lifecycle:

class MyAgent(Actor):
    async def on_start(self):
        # Called once when the actor starts.
        # Long-running work (MQTT subscriptions, polling loops)
        # must be launched as asyncio.create_task() — never awaited directly.
        asyncio.create_task(self._my_loop())

    async def handle_message(self, msg: Message):
        # Called for every message in the inbox.
        # msg.type is TASK, RESULT, HEARTBEAT, ERROR, or COMMAND.
        if msg.type == MessageType.TASK:
            result = await self._do_work(msg.payload)
            await self.send(msg.reply_to, MessageType.RESULT, result)

    async def on_stop(self):
        # Optional cleanup.
        pass

The Supervisor wraps each actor with a ONE_FOR_ONE restart policy. If an actor crashes, only that actor is restarted — others keep running. max_restarts and restart_delay are configurable per actor.


Persistence

Wactorz uses a three-tier persistence layer (wactorz/core/persistence.py) that routes each key to the appropriate store:

Store Location Used for
SQLite state/wactorz.db Durable structured data: spawn registry, pipeline rules, user facts, topic contracts, conversation history, time-series sensor/detection/HA-state data
Redis redis://localhost:6379 (falls back to in-memory if Redis is unavailable) Ephemeral fast-access data: observed topic samples, agent metrics, heartbeat state
Pickle state/{actor_name}/state.pkl Arbitrary Python objects: custom agent state dicts, ML models, numpy arrays, cv2 captures

Actor.persist(key, value) and Actor.recall(key) route automatically to the correct store based on the key name. Existing agent code works without changes.

The spawn registry (_spawned_agents) is stored in SQLite. On restart, MainActor re-spawns every entry so dynamic agents and catalog agents survive reboots.

On first startup after upgrading from an older version, migrate_from_pickle() reads existing .pkl files and moves structured keys to SQLite/Redis. Pickle files are left in place for keys that remain in pickle.


MQTT Topics

Topic pattern Publisher Subscriber Payload
io/chat IOAgent / UI main {from, content}
agents/{id}/chat any actor UI / IOAgent {role, content, interface}
agents/{id}/heartbeat every actor MonitorAgent {name, state, ts, ...}
agents/{id}/logs any actor dashboard {type, message, ts}
agents/{id}/manifest any actor main capabilities, input/output schema
homeassistant/state_changes/# HA state bridge pipeline agents {entity_id, domain, new_state, old_state}

State bridge topic format — When HA_STATE_BRIDGE_PER_ENTITY=1 the bridge publishes to homeassistant/state_changes/{domain}/{entity_id}. When =0 (default) it publishes everything to the flat topic homeassistant/state_changes. Always subscribe to the wildcard # and filter by entity_id in the payload.


Message Flow

User → Agent (any interface)

User types:  "@my-agent {"action": "status"}"
  
  
Interface (CLIInterface / DiscordInterface / RESTInterface / WhatsAppInterface / TelegramInterface)
    calls main_actor.process_user_input(text)
  
MainActor._classify_intent()      one LLM call: HA | PIPELINE | OTHER
  
  ├── OTHER    main.chat()        conversational reply
  ├── HA       send to home-assistant-agent
  └── @mention detected    send directly to named actor
          
          
      target-agent.handle_message(msg)
          
          
      handle_task(agent, payload)    returns dict result
          
          
      main receives RESULT, formats, returns to user

Pipeline (HA state → Discord notification)

HA state changes (lamp turns on)
  │
  ▼
HomeAssistantStateBridgeAgent
  │  publishes homeassistant/state_changes  (flat topic, per_entity=0)
  ▼
Mosquitto
  │
  ▼
lamp-on-discord-notify (DynamicAgent)
  │  setup(): agent.subscribe("homeassistant/state_changes/#", on_state)
  │  on_state(): if entity_id == "light.wiz_..." and new_state["state"] == "on":
  │      httpx.post(discord_webhook, {"content": "Lamp is on!"})
  ▼
Discord channel

LLM Providers

Provider Flag Env var Default model
AnthropicProvider --llm anthropic ANTHROPIC_API_KEY claude-sonnet-4-6
OpenAIProvider --llm openai OPENAI_API_KEY gpt-4o
OllamaProvider --llm ollama --ollama-model llama3 local
NIMProvider --llm nim --nim-model meta/llama-3.3-70b-instruct NIM_API_KEY free tier
GeminiProvider --llm gemini --gemini-model gemini-2.5-flash GEMINI_API_KEY gemini-2.5-flash

All providers implement complete(messages, system) → (text, usage) and stream(messages, system) → AsyncGenerator. Cost tracking (USD per 1M tokens) is built into every provider and accumulated in LLMAgent.metrics.

For Ollama, system is encoded as the first {"role": "system"} entry in the native /api/chat messages array for both blocking and streaming calls. This keeps local model behavior aligned with the hosted providers, which already receive system instructions through their chat-message APIs.


Supervision Tree

The supervisor is configured in cli.py inside build_system(). Each actor is registered with a factory function (called fresh on each restart), a strategy, and restart limits:

system.supervisor
  .supervise("main",     make_main,     strategy=ONE_FOR_ONE, max_restarts=10)
  .supervise("monitor",  make_monitor,  strategy=ONE_FOR_ONE, max_restarts=10)
  .supervise("catalog",  make_catalog,  strategy=ONE_FOR_ONE, max_restarts=10)
  .supervise("home-assistant-agent", make_ha_agent, max_restarts=5)
  # ... etc

Dynamic agents (spawned by main or planner) are not in the supervision tree — they are managed by the spawn registry. On restart, main re-spawns them from the saved code in state/main/state.pkl.


Running Wactorz

The wactorz command starts everything — the actor system, an embedded MQTT broker, and the web dashboard. No separate broker process is needed.

# Start with Anthropic Claude (default interface: CLI)
wactorz

# Choose a different LLM provider
wactorz --llm gemini --gemini-model gemini-2.5-flash
wactorz --llm openai
wactorz --llm ollama --ollama-model llama3
wactorz --llm nim --nim-model meta/llama-3.3-70b-instruct

# Hot-reload in dev (restarts on source file changes)
wactorz --reload

# Discord bot
wactorz --interface discord --discord-token $DISCORD_BOT_TOKEN

# WhatsApp (via Twilio)
wactorz --interface whatsapp

# Telegram
wactorz --interface telegram --telegram-token $TELEGRAM_BOT_TOKEN

# REST API
wactorz --interface rest --port 8000

# Custom MQTT broker (external)
wactorz --mqtt-broker 192.168.1.10 --mqtt-port 1883

# Disable web dashboard
wactorz --no-monitor

Interfaces

Interface Flag Notes
CLI --interface cli Default. Interactive terminal with streaming responses.
REST --interface rest HTTP API on --port (default 8000). POST /chat, GET /agents.
MCP separate wactorz-mcp process Stdio Model Context Protocol server backed by Wactorz REST.
Discord --interface discord Bot responds in channels and DMs. Requires DISCORD_BOT_TOKEN.
WhatsApp --interface whatsapp Via Twilio. Requires TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_WHATSAPP_NUMBER.
Telegram --interface telegram Bot API. Requires TELEGRAM_BOT_TOKEN.

The web dashboard starts automatically on http://localhost:8888 regardless of which interface is active, unless --no-monitor is passed. It shows live agent status, heartbeats, logs, and a chat interface.