# 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](#1-dual-ai-runtimes) 2. [Tool Enablement & MCP Servers](#2-tool-enablement--mcp-servers) 3. [Multi-Channel Adapters](#3-multi-channel-adapters) 4. [WebChat Browser Interface](#4-webchat-browser-interface) 5. [Persistent Memory System](#5-persistent-memory-system) 6. [Skills System](#6-skills-system) 7. [Scheduler & Action Tags](#7-scheduler--action-tags) 8. [Heartbeat / Proactive System](#8-heartbeat--proactive-system) 9. [Subagents & Agent-to-Agent Communication](#9-subagents--agent-to-agent-communication) 10. [Self-Modification](#10-self-modification) 11. [Lifecycle Hooks](#11-lifecycle-hooks) 12. [Webhooks (External Event Receiver)](#12-webhooks-external-event-receiver) 13. [CLI Interface](#13-cli-interface) 14. [Configuration](#14-configuration) 15. [Running Tests](#15-running-tests) 13. [Running Tests](#13-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](https://opencode.ai) CLI. Supports CLI mode (subprocess per request) and SDK mode (persistent server connection). ```bash # 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 ``` ### Claude Code Uses the [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI with native `--system-prompt` support. ```bash 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 ` | `--continue --session-id ` | | Tool access | Enabled by default | Controlled via `--allowedTools` | | Output format | JSONL events | JSON result object | ### How to test ```bash # 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`: ```json { "claude": { "no_tools": true } } ``` To customize the tool list: ```json { "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. ```json { "mcp": { "servers": { "my-tool": { "command": "uvx", "args": ["my-mcp-server@latest"], "env": { "API_KEY": "..." } } } } } ``` ### How to test ```bash # 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 `.env`. Starts automatically when tokens are present. ```bash python main.py ``` ### Telegram ```bash # Set TELEGRAM_BOT_TOKEN in .env first python main.py --telegram ``` ### Discord ```bash # Set DISCORD_BOT_TOKEN in .env first python main.py --discord ``` ### Multiple adapters ```bash 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 ```bash 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 ```bash # 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 ```json { "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 ```bash # 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 ```bash # 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//SKILL.md`: ```markdown --- 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 ```bash 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 ` — remove a job Via CLI: ```bash python cli.py cron list python cli.py cron remove ``` ### How to test ```bash # 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 ```markdown # 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 ```json { "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. ### How to test ```bash # 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: ```python 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 ```bash 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//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 ```bash # 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//` with two files: `HOOK.md` — metadata in YAML frontmatter: ```markdown --- 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: ```python 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//HOOK.md` — workspace hooks (per-project) 2. `~/.aetheel/hooks//HOOK.md` — managed hooks (shared across workspaces) ### Programmatic hooks You can also register hooks in Python code: ```python 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 ```json { "hooks": { "enabled": true } } ``` Set `enabled` to `false` to disable all hook discovery and execution. ### How to test ```bash 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 ```json { "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. ```bash 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: ```json { "status": "ok", "response": "I checked your inbox and found 2 urgent items..." } ``` Optionally deliver the response to a messaging channel: ```bash 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: ```bash 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 ` header - `?token=` query parameter (fallback) If no token is configured (`"token": ""`), endpoints are open (dev mode only). ### How to test ```bash 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: ```bash aetheel # Same as `aetheel start` aetheel start # Start with default adapters aetheel --help # Show all commands ``` Or run directly: ```bash 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 ` | Remove a scheduled job | | `aetheel memory search "query"` | Search memory | | `aetheel memory sync` | Force memory re-index | ### How to test ```bash # 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`. Secrets (tokens) stay in `.env`. ### Config hierarchy (highest priority wins) 1. CLI arguments (`--model`, `--claude`, etc.) 2. Environment variables 3. `~/.aetheel/config.json` 4. Dataclass defaults ### Full config.json example ```json { "log_level": "INFO", "runtime": { "mode": "cli", "model": null, "timeout_seconds": 120, "server_url": "http://localhost:4096", "format": "json" }, "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" ] }, "discord": { "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": {} }, "hooks": { "enabled": true }, "webhooks": { "enabled": false, "port": 8090, "host": "127.0.0.1", "token": "" } } ``` ### Environment variables (.env) ```bash # Slack (required for Slack adapter) SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... # Telegram (required for --telegram) TELEGRAM_BOT_TOKEN=... # Discord (required for --discord) DISCORD_BOT_TOKEN=... # Runtime overrides OPENCODE_MODEL=anthropic/claude-sonnet-4-20250514 CLAUDE_MODEL=claude-sonnet-4-20250514 LOG_LEVEL=DEBUG ``` --- ## 15. Running Tests ### Prerequisites ```bash cd Aetheel pip install -e ".[test]" # or uv sync --extra test ``` ### Run all tests ```bash 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: > ```bash > python -m pytest tests/ -v > ``` ### Run specific test files ```bash # 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 ```bash # 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 ```