Files
Aetheel/docs/project/features-guide.md
tanmay11k 82c2640481 feat: openclaw-style secrets (env.vars + \) and per-task model routing
- Replace python-dotenv with config.json env.vars block + \ substitution
- Add models section for per-task model routing (heartbeat, subagent, default)
- Heartbeat/subagent tasks can use different models/providers than main chat
- Remove python-dotenv from dependencies
- Update all docs to reflect new config approach
- Reorganize docs into project/ and research/ subdirectories
2026-02-20 23:49:05 -05:00

24 KiB

Aetheel Features Guide

Complete reference for all Aetheel features, how to use them, and how to test them.


Table of Contents

  1. Dual AI Runtimes
  2. Tool Enablement & MCP Servers
  3. Multi-Channel Adapters
  4. WebChat Browser Interface
  5. Persistent Memory System
  6. Skills System
  7. Scheduler & Action Tags
  8. Heartbeat / Proactive System
  9. Subagents & Agent-to-Agent Communication
  10. Self-Modification
  11. Lifecycle Hooks
  12. Webhooks (External Event Receiver)
  13. CLI Interface
  14. Configuration
  15. Running Tests
  16. Running Tests

1. Dual AI Runtimes

Aetheel supports two AI backends that share the same AgentResponse interface. Switch between them with a single flag.

OpenCode (default)

Uses the OpenCode CLI. Supports CLI mode (subprocess per request) and SDK mode (persistent server connection).

# CLI mode (default)
python main.py

# SDK mode (requires `opencode serve` running)
python main.py --sdk

# Custom model
python main.py --model anthropic/claude-sonnet-4-20250514

OpenCode Advanced Features

Aetheel exposes several OpenCode CLI features via chat commands and config:

Feature Chat Command Config Key
Agent selection agents (list), config to set runtime.agent
Attach to server runtime.attach
Model discovery models, models <provider>
Usage stats stats, stats <days>
File attachments Passed from chat adapters
Session forking Internal (subagent branching)
Session titles Auto-set from first message

Setting runtime.attach to a running opencode serve URL (e.g. "http://localhost:4096") makes CLI mode attach to that server instead of spawning a fresh process per request. This avoids MCP server cold boot times and is significantly faster.

{
  "runtime": {
    "agent": "researcher",
    "attach": "http://localhost:4096"
  }
}

Claude Code

Uses the Claude Code CLI with native --system-prompt support.

python main.py --claude
python main.py --claude --model claude-sonnet-4-20250514

Key differences

Feature OpenCode Claude Code
System prompt XML-injected into message Native --system-prompt flag
Session continuity --continue --session <id> --continue --session-id <id>
Tool access Enabled by default Controlled via --allowedTools
Output format JSONL events JSON result object

How to test

# Echo mode (no AI runtime needed)
python main.py --test

# Verify runtime detection
python cli.py doctor

2. Tool Enablement & MCP Servers

Tool access

Claude Code runtime now has tools enabled by default. The allowed_tools list controls which tools the agent can use:

Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch,
Task, TaskOutput, TaskStop, Skill, TeamCreate, TeamDelete, SendMessage

To disable tools (pure conversation mode), set in ~/.aetheel/config.json:

{
  "claude": {
    "no_tools": true
  }
}

To customize the tool list:

{
  "claude": {
    "no_tools": false,
    "allowed_tools": ["Bash", "Read", "Write", "WebSearch"]
  }
}

MCP server configuration

Add external tool servers via config. Aetheel writes the appropriate config file (.mcp.json for Claude, opencode.json for OpenCode) to the workspace before launching the runtime.

{
  "mcp": {
    "servers": {
      "my-tool": {
        "command": "uvx",
        "args": ["my-mcp-server@latest"],
        "env": { "API_KEY": "..." }
      }
    }
  }
}

How to test

# Run MCP config writer tests
cd Aetheel
python -m pytest tests/test_mcp_config.py -v

3. Multi-Channel Adapters

Aetheel connects to messaging platforms via adapters. Each adapter converts platform events into a channel-agnostic IncomingMessage and routes responses back.

Slack (default)

Requires SLACK_BOT_TOKEN and SLACK_APP_TOKEN in config.jsonenv.vars. Starts automatically when tokens are present.

python main.py

Telegram

# Set TELEGRAM_BOT_TOKEN in config.json env.vars first
python main.py --telegram

Discord

# Set DISCORD_BOT_TOKEN in config.json env.vars first
python main.py --discord

Multiple adapters

python main.py --telegram --discord --webchat

When multiple adapters run, all but the last start in background threads. The last one runs blocking.

How to test

python -m pytest tests/test_base_adapter.py -v

4. WebChat Browser Interface

A browser-based chat UI served via aiohttp with WebSocket support. No Slack/Discord/Telegram needed.

Starting WebChat

# Via CLI flag
python main.py --webchat

# Or enable in config permanently
# Set "webchat": {"enabled": true} in ~/.aetheel/config.json

Then open http://127.0.0.1:8080 in your browser.

Configuration

{
  "webchat": {
    "enabled": false,
    "port": 8080,
    "host": "127.0.0.1"
  }
}

Features

  • Dark-themed chat UI at GET /
  • WebSocket endpoint at /ws for real-time messaging
  • Per-connection session isolation (each browser tab gets its own conversation)
  • Auto-reconnect on disconnect (3 second delay)
  • Basic markdown rendering (bold, code blocks, bullet points)
  • Connection status indicator (green/red/gray)

Architecture

Browser  <-->  WebSocket (/ws)  <-->  WebChatAdapter  <-->  ai_handler  <-->  Runtime
                    |
              GET / serves static/chat.html

How to test

# Start with echo handler + webchat
python main.py --test --webchat

# Open http://127.0.0.1:8080 and send a message
# You should see the echo response with message metadata

5. Persistent Memory System

Aetheel maintains persistent memory across sessions using local embeddings and SQLite.

Identity files

Located in ~/.aetheel/workspace/:

File Purpose
SOUL.md Personality, values, communication style
USER.md User preferences, context, background
MEMORY.md Long-term notes, facts to remember

Edit these files directly. Changes are picked up automatically via file watching.

How it works

  1. All .md files in the workspace are chunked and embedded using fastembed (BAAI/bge-small-en-v1.5, 384-dim, runs locally)
  2. Chunks are stored in SQLite with FTS5 full-text search
  3. On each message, Aetheel searches memory using hybrid scoring (0.7 vector + 0.3 BM25)
  4. Relevant results are injected into the system prompt as context
  5. Conversations are logged to daily/YYYY-MM-DD.md

CLI memory commands

# Search memory
python cli.py memory search "python projects"

# Force re-index
python cli.py memory sync

6. Skills System

Skills are markdown files that teach the agent how to handle specific types of requests. They're loaded at startup and injected into the system prompt when trigger words match.

Creating a skill

Create ~/.aetheel/workspace/skills/<name>/SKILL.md:

---
name: weather
description: Check weather for any city
triggers: [weather, forecast, temperature, rain]
---

# Weather Skill

When the user asks about weather, use web search to find current conditions...

How it works

  1. Skills are discovered from ~/.aetheel/workspace/skills/*/SKILL.md
  2. YAML frontmatter defines name, description, and triggers
  3. When a message contains a trigger word, the skill's body is injected into the system prompt
  4. A summary of all available skills is always included

Hot reload

Send reload or /reload in chat to reload skills without restarting.

How to test

python -m pytest tests/test_skills.py -v

7. Scheduler & Action Tags

APScheduler-based system with SQLite persistence for one-shot and recurring jobs.

Action tags

The AI can include action tags in its responses. The system strips them from the visible reply and executes the action.

Tag Effect
[ACTION:remind|5|Drink water!] Sends "Drink water!" to the channel in 5 minutes
[ACTION:cron|0 9 * * *|Good morning!] Sends "Good morning!" every day at 9 AM
[ACTION:spawn|Research Python 3.14] Spawns a background subagent for the task

Managing jobs

In chat:

  • /cron list — list all scheduled jobs
  • /cron remove <id> — remove a job

Via CLI:

python cli.py cron list
python cli.py cron remove <job_id>

How to test

# Scheduler tests require apscheduler installed
python -m pytest tests/test_scheduler.py -v

8. Heartbeat / Proactive System

The heartbeat system runs periodic tasks automatically by parsing a user-editable HEARTBEAT.md file.

How it works

  1. At startup, HeartbeatRunner reads ~/.aetheel/workspace/HEARTBEAT.md
  2. Each ## heading defines a schedule (natural language)
  3. Bullet points under each heading are task prompts
  4. Tasks are registered as cron jobs with the Scheduler
  5. When a task fires, it creates a synthetic message routed through ai_handler

HEARTBEAT.md format

# Heartbeat Tasks

## Every 30 minutes
- Check if any scheduled reminders need attention
- Review recent session logs for anything worth remembering

## Every morning (9:00 AM)
- Summarize yesterday's conversations
- Check for any pending follow-ups in MEMORY.md

## Every evening (6:00 PM)
- Update MEMORY.md with today's key learnings

Supported schedule patterns

Pattern Cron equivalent
Every 30 minutes */30 * * * *
Every hour 0 * * * *
Every 2 hours 0 */2 * * *
Every morning (9:00 AM) 0 9 * * *
Every evening (6:00 PM) 0 18 * * *

Configuration

{
  "heartbeat": {
    "enabled": true,
    "default_channel": "slack",
    "default_channel_id": "C123456",
    "silent": false
  }
}

Set enabled to false to disable heartbeat entirely. If HEARTBEAT.md doesn't exist, a default one is created automatically.

Model routing for heartbeat

Heartbeat tasks can use a cheaper/local model to save costs. Configure in the models section:

{
  "models": {
    "heartbeat": {
      "engine": "opencode",
      "model": "ollama/llama3.2",
      "provider": "ollama"
    }
  }
}

When set, heartbeat jobs use a dedicated runtime instance with the specified model instead of the global default. Regular chat messages are unaffected.

How to test

# Verify heartbeat parsing works
python -c "
from heartbeat.heartbeat import HeartbeatRunner
print(HeartbeatRunner._parse_schedule_header('Every 30 minutes'))   # */30 * * * *
print(HeartbeatRunner._parse_schedule_header('Every morning (9:00 AM)'))  # 0 9 * * *
print(HeartbeatRunner._parse_schedule_header('Every evening (6:00 PM)'))  # 0 18 * * *
"

9. Subagents & Agent-to-Agent Communication

Subagent spawning

The AI can spawn background tasks that run in separate threads with their own runtime instances.

[ACTION:spawn|Research the latest Python 3.14 features and summarize them]

The subagent runs asynchronously and sends results back to the originating channel when done.

Managing subagents

In chat:

  • /subagents — list active subagent tasks with IDs and status

SubagentBus

A thread-safe pub/sub message bus for inter-subagent communication:

from agent.subagent import SubagentManager

mgr = SubagentManager(runtime_factory=..., send_fn=...)

# Subscribe to a channel
mgr.bus.subscribe("results", lambda msg, sender: print(f"{sender}: {msg}"))

# Publish from a subagent
mgr.bus.publish("results", "Task complete!", "subagent-abc123")

Claude Code team tools

When using Claude Code runtime, the agent has access to team coordination tools:

  • TeamCreate, TeamDelete — create/delete agent teams
  • SendMessage — send messages between agents in a team
  • Task, TaskOutput, TaskStop — spawn and manage subagent tasks

How to test

python -m pytest tests/test_subagent_bus.py -v

10. Self-Modification

The AI agent knows it can modify its own files. The system prompt tells it about:

  • ~/.aetheel/config.json — edit configuration
  • ~/.aetheel/workspace/skills/<name>/SKILL.md — create new skills
  • ~/.aetheel/workspace/SOUL.md — update personality
  • ~/.aetheel/workspace/USER.md — update user profile
  • ~/.aetheel/workspace/MEMORY.md — update long-term memory
  • ~/.aetheel/workspace/HEARTBEAT.md — modify periodic tasks

Hot reload

After the agent edits config or skills, send reload or /reload in chat to apply changes without restarting:

You: /reload
Aetheel: 🔄 Config and skills reloaded.

How to test

# Verify the system prompt contains self-modification instructions
python -c "
from agent.opencode_runtime import build_aetheel_system_prompt
prompt = build_aetheel_system_prompt()
assert 'Self-Modification' in prompt
assert 'config.json' in prompt
assert '/reload' in prompt
print('Self-modification prompt sections present ✅')
"

11. Lifecycle Hooks

Event-driven lifecycle hooks inspired by OpenClaw's internal hook system. Hooks fire on gateway/agent lifecycle events and let you run custom Python code at those moments.

Supported events

Event When it fires
gateway:startup Gateway process starts (after adapters connect)
gateway:shutdown Gateway process is shutting down
command:reload User sends /reload
command:new User starts a fresh session
agent:bootstrap Before workspace files are injected into context
agent:response After the agent produces a response

Creating a hook

Create a directory in ~/.aetheel/workspace/hooks/<name>/ with two files:

HOOK.md — metadata in YAML frontmatter:

---
name: session-logger
description: Log session starts to a file
events: [gateway:startup, command:reload]
enabled: true
---
# Session Logger Hook

Logs gateway lifecycle events for debugging.

handler.py — Python handler with a handle(event) function:

def handle(event):
    """Called when a matching event fires."""
    print(f"Hook fired: {event.event_key}")
    # Push messages back to the user
    event.messages.append("Hook executed!")

Hook discovery locations

Hooks are discovered from two directories (in order):

  1. ~/.aetheel/workspace/hooks/<name>/HOOK.md — workspace hooks (per-project)
  2. ~/.aetheel/hooks/<name>/HOOK.md — managed hooks (shared across workspaces)

Programmatic hooks

You can also register hooks in Python code:

from hooks import HookManager, HookEvent

mgr = HookManager(workspace_dir="~/.aetheel/workspace")
mgr.register("gateway:startup", lambda e: print("Gateway started!"))
mgr.trigger(HookEvent(type="gateway", action="startup"))

Configuration

{
  "hooks": {
    "enabled": true
  }
}

Set enabled to false to disable all hook discovery and execution.

How to test

python -m pytest tests/test_hooks.py -v

12. Webhooks (External Event Receiver)

HTTP endpoints that accept POST requests from external systems (GitHub, Jira, email services, custom scripts) and route them through the AI handler as synthetic messages. Inspired by OpenClaw's /hooks/* gateway endpoints.

Endpoints

Endpoint Method Auth Description
/hooks/health GET No Health check
/hooks/wake POST Yes Wake the agent with a text prompt
/hooks/agent POST Yes Send a message to a specific agent session

Enabling webhooks

{
  "webhooks": {
    "enabled": true,
    "port": 8090,
    "host": "127.0.0.1",
    "token": "your-secret-token"
  }
}

The webhook server starts automatically when webhooks.enabled is true.

POST /hooks/wake

Wake the agent with a text prompt. The agent processes it and returns the response.

curl -X POST http://127.0.0.1:8090/hooks/wake \
  -H "Authorization: Bearer your-secret-token" \
  -H "Content-Type: application/json" \
  -d '{"text": "Check my email for urgent items"}'

Response:

{
  "status": "ok",
  "response": "I checked your inbox and found 2 urgent items..."
}

Optionally deliver the response to a messaging channel:

curl -X POST http://127.0.0.1:8090/hooks/wake \
  -H "Authorization: Bearer your-secret-token" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Summarize today'\''s calendar",
    "channel": "slack",
    "channel_id": "C123456"
  }'

POST /hooks/agent

Send a message to a specific agent session with channel delivery:

curl -X POST http://127.0.0.1:8090/hooks/agent \
  -H "Authorization: Bearer your-secret-token" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "New GitHub issue: Fix login bug #42",
    "channel": "slack",
    "channel_id": "C123456",
    "sender": "GitHub"
  }'

Use cases

  • GitHub webhook → POST to /hooks/agent → agent triages the issue
  • Email service → POST to /hooks/wake → agent summarizes new emails
  • Cron script → POST to /hooks/wake → agent runs a daily report
  • IoT sensor → POST to /hooks/agent → agent alerts on anomalies

Authentication

All POST endpoints require a bearer token. Pass it via:

  • Authorization: Bearer <token> header
  • ?token=<token> query parameter (fallback)

If no token is configured ("token": ""), endpoints are open (dev mode only).

How to test

python -m pytest tests/test_webhooks.py -v

13. CLI Interface

Aetheel includes a Click-based CLI with subcommands for all major operations.

Installation

After installing with pip install -e . or uv sync, the aetheel command is available:

aetheel              # Same as `aetheel start`
aetheel start        # Start with default adapters
aetheel --help       # Show all commands

Or run directly:

python cli.py start --discord --webchat
python cli.py chat "What is Python?"
python cli.py doctor

Commands

Command Description
aetheel / aetheel start Start with configured adapters
aetheel start --discord Start with Discord adapter
aetheel start --telegram Start with Telegram adapter
aetheel start --webchat Start with WebChat adapter
aetheel start --claude Use Claude Code runtime
aetheel start --test Echo handler (no AI)
aetheel chat "message" One-shot AI query (prints to stdout)
aetheel status Show runtime status
aetheel doctor Run diagnostics (check runtimes, tokens, workspace)
aetheel config show Print current config.json
aetheel config edit Open config in $EDITOR
aetheel config init Reset config to defaults
aetheel cron list List scheduled jobs
aetheel cron remove <id> Remove a scheduled job
aetheel memory search "query" Search memory
aetheel memory sync Force memory re-index

How to test

# Verify CLI structure
python cli.py --help
python cli.py start --help
python cli.py config --help
python cli.py cron --help
python cli.py memory --help

# Run diagnostics
python cli.py doctor

14. Configuration

All configuration lives in ~/.aetheel/config.json, including secrets (in the env.vars block).

Config hierarchy (highest priority wins)

  1. CLI arguments (--model, --claude, etc.)
  2. Process environment variables
  3. env.vars block in config.json
  4. ${VAR} substitution in config values
  5. ~/.aetheel/config.json static values
  6. Dataclass defaults

Full config.json example

{
  "env": {
    "vars": {
      "SLACK_BOT_TOKEN": "xoxb-...",
      "SLACK_APP_TOKEN": "xapp-...",
      "TELEGRAM_BOT_TOKEN": "",
      "DISCORD_BOT_TOKEN": "",
      "ANTHROPIC_API_KEY": ""
    }
  },
  "log_level": "INFO",
  "runtime": {
    "mode": "cli",
    "model": null,
    "timeout_seconds": 120,
    "server_url": "http://localhost:4096",
    "format": "json",
    "agent": null,
    "attach": null
  },
  "claude": {
    "model": null,
    "timeout_seconds": 120,
    "max_turns": 3,
    "no_tools": false,
    "allowed_tools": [
      "Bash", "Read", "Write", "Edit", "Glob", "Grep",
      "WebSearch", "WebFetch",
      "Task", "TaskOutput", "TaskStop", "Skill",
      "TeamCreate", "TeamDelete", "SendMessage"
    ]
  },
  "slack": {
    "enabled": true,
    "bot_token": "${SLACK_BOT_TOKEN}",
    "app_token": "${SLACK_APP_TOKEN}"
  },
  "telegram": {
    "enabled": false,
    "bot_token": "${TELEGRAM_BOT_TOKEN}"
  },
  "discord": {
    "enabled": false,
    "bot_token": "${DISCORD_BOT_TOKEN}",
    "listen_channels": []
  },
  "memory": {
    "workspace": "~/.aetheel/workspace",
    "db_path": "~/.aetheel/memory.db"
  },
  "scheduler": {
    "db_path": "~/.aetheel/scheduler.db"
  },
  "heartbeat": {
    "enabled": true,
    "default_channel": "slack",
    "default_channel_id": "",
    "silent": false
  },
  "webchat": {
    "enabled": false,
    "port": 8080,
    "host": "127.0.0.1"
  },
  "mcp": {
    "servers": {}
  },
  "models": {
    "heartbeat": null,
    "subagent": null,
    "default": null
  },
  "hooks": {
    "enabled": true
  },
  "webhooks": {
    "enabled": false,
    "port": 8090,
    "host": "127.0.0.1",
    "token": ""
  }
}

Process environment variable overrides

Process env vars still override everything. Useful for CI, Docker, or systemd:

OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514
CLAUDE_MODEL=claude-sonnet-4-20250514
LOG_LEVEL=DEBUG

15. Running Tests

Prerequisites

cd Aetheel
pip install -e ".[test]"
# or
uv sync --extra test

Run all tests

python -m pytest tests/ -v --ignore=tests/test_scheduler.py

Note: test_scheduler.py requires apscheduler installed. If you have it, run the full suite:

python -m pytest tests/ -v

Run specific test files

# Base adapter tests
python -m pytest tests/test_base_adapter.py -v

# Skills system tests
python -m pytest tests/test_skills.py -v

# MCP config writer tests
python -m pytest tests/test_mcp_config.py -v

# SubagentBus pub/sub tests
python -m pytest tests/test_subagent_bus.py -v

# Scheduler tests (requires apscheduler)
python -m pytest tests/test_scheduler.py -v

# Hook system tests
python -m pytest tests/test_hooks.py -v

# Webhook receiver tests
python -m pytest tests/test_webhooks.py -v

Test summary

Test file What it covers Count
test_base_adapter.py BaseAdapter dispatch, handler registration, error handling 9
test_skills.py Skill loading, trigger matching, frontmatter parsing, context building 21
test_mcp_config.py MCP config writer (Claude/OpenCode formats, round-trip, edge cases) 8
test_subagent_bus.py SubagentBus subscribe/publish, isolation, error resilience, thread safety 10 + 2
test_hooks.py Hook discovery, trigger, programmatic hooks, error resilience, messages 14
test_webhooks.py Webhook endpoints (wake, agent, health), auth, channel delivery 10
test_scheduler.py Scheduler one-shot/cron jobs, persistence, removal varies

Quick smoke tests

# Verify config loads correctly
python -c "from config import load_config; c = load_config(); print(f'Tools enabled: {not c.claude.no_tools}, Tools: {len(c.claude.allowed_tools)}')"

# Verify system prompt has new sections
python -c "
from agent.opencode_runtime import build_aetheel_system_prompt
p = build_aetheel_system_prompt()
for section in ['Your Tools', 'Self-Modification', 'Subagents & Teams']:
    assert section in p, f'Missing: {section}'
    print(f'  ✅ {section}')
"

# Verify heartbeat parser
python -c "
from heartbeat.heartbeat import HeartbeatRunner
tests = [('Every 30 minutes', '*/30 * * * *'), ('Every morning (9:00 AM)', '0 9 * * *'), ('Every evening (6:00 PM)', '0 18 * * *')]
for header, expected in tests:
    result = HeartbeatRunner._parse_schedule_header(header)
    assert result == expected, f'{header}: got {result}, expected {expected}'
    print(f'  ✅ {header} -> {result}')
"

# Verify CLI commands exist
python cli.py --help