An open-source agent system built in Go, inspired by the ReAct agent pattern, Anthropic's MCP protocol, Google's ACP spec, and the broader agent tooling ecosystem.
Maia runs a composable ReAct loop with streaming model calls, concurrent tool execution, 24 middleware hooks, and SQLite persistence — all in a single 30 MB static binary with no Python needed.
# Copy and edit configuration.
make config # Creates config.yaml, .env, extensions_config.json
# Set your API key in .env.
# LLM_AUTH_TOKEN=sk-your-key-here
# Run gateway + frontend locally.
make dev-stop ; make dev-full # Gateway :8002, Frontend :3000
# Open browser localhost:3000┌──────────────────────────────────────────────────────────────────────┐
│ Gateway (HTTP/SSE) — :8002 │
│ ┌──────────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Thread/Run API │ │ Auth API │ │ Channel Webhooks │ │
│ │ Memory / Models │ │ OIDC Login │ │ Feishu/Slack/Telegram │ │
│ │ Skills / MCP │ │ JWT Tokens │ │ │ │
│ └──────────────────┘ └──────────────┘ └───────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ Middleware Hooks (24 layers) │
│ ┌─────────────────────────────────────────────────────────────────┐│
│ │ WrapModelCall: input_sanitization → syscoal → dangling_tc → ││
│ │ llm_error → loop_detection → token_budget ││
│ │ WrapToolCall: tool_budget → tool_error → clarification ││
│ │ BeforeAgent: dynamic_context (date + memory injection) ││
│ │ AfterModel: token_usage, todo, title ││
│ │ AfterAgent: memory (async LLM updates) ││
│ │ Other: guardrails, thread_data, tool_metadata, uploads, ││
│ │ view_image, deferred_tool_filter, skill_activation, ││
│ │ sandbox_audit, subagent_limit, safety_finish ││
│ └─────────────────────────────────────────────────────────────────┘│
├──────────────────────────────────────────────────────────────────────┤
│ Agent (ReAct Loop) │
│ ┌──────────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ Model Provider │──▶│ AfterModel │──▶│ Tool Registry │ │
│ │ OpenAI / Anthropic│ │ hooks │ │ │ │
│ │ vLLM / DeepSeek │ └───────────────┘ │ shell │ │
│ │ Ollama / MiniMax │ │ read_file │ │
│ │ StepFun / Mimo │ │ write_file │ │
│ └──────────────────┘ │ list_files │ │
│ │ present_files │ │
│ ┌──────────────────┐ │ web_search │ │
│ │ Subagent Executor│ │ web_fetch │ │
│ │ delegate_task │ │ skill_manage │ │
│ │ (general-purpose│ │ tool_search │ │
│ │ invoke_acp_agent│ │
│ │ bash subagents)│ │ + MCP tools │ │
│ └──────────────────┘ └──────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ Sandbox │
│ ┌────────────────────────────┐ ┌───────────────────────────────┐ │
│ │ Local sandbox │ │ Host bash mode │ │
│ │ (scoped working directory) │ │ (direct host execution for │ │
│ │ │ │ host tools, SSO, host network) │ │
│ └────────────────────────────┘ └───────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────┤
│ Store (SQLite) Memory (JSON) Skills (filesystem) │
└──────────────────────────────────────────────────────────────────────┘
| Provider | Config Value | Protocol |
|---|---|---|
| OpenAI | openai |
Chat Completions |
| Anthropic / Claude | anthropic |
Messages API |
| vLLM | vllm |
Chat Completions + Quirks |
| DeepSeek | deepseek |
Chat Completions + Quirks |
| Ollama | ollama |
Chat Completions + Quirks |
| MiniMax | minimax |
Chat Completions |
| StepFun | stepfun |
Chat Completions |
| Mimo | mimo |
Chat Completions |
Provider-specific behavioral differences are handled via a Quirks struct
(e.g., DeepSeek returns finish_reason: "stop" for tool calls; vLLM requires
explicit tool_choice: "auto"). No per-provider subclass hierarchy needed.
| Tool | Purpose |
|---|---|
shell |
Execute shell commands (sandbox or host mode) |
read_file |
Read file contents with optional line offset/limit |
write_file |
Write or append content to files |
list_files |
List directory contents with sizes and types |
present_files |
Mark files for presentation in the client UI |
web_search |
Search the web (7 backends: DuckDuckGo, Serper, Tavily, Brave, Exa, SearXNG, Google) |
web_fetch |
Fetch web page content (2 backends: basic HTTP, Jina AI Reader) |
skill_manage |
Install, list, enable/disable skills |
tool_search |
Search available tools by keyword |
invoke_acp_agent |
Invoke external ACP-compatible agents (e.g. Codex) |
invoke_claude |
Invoke Claude Code CLI for coding tasks |
Additional tools can be loaded from MCP servers configured in
extensions_config.json.
The web_search tool supports 7 backends, configured in config.yaml:
web_search:
enabled: true
backend: "duckduckgo" # choose one
api_key: "" # required for all except duckduckgo and searxng
max_results: 5
# base_url: "" # required for searxng
# cx: "" # required for google| Backend | API Key | Free tier | Best for |
|---|---|---|---|
duckduckgo |
Not needed | Unlimited | Quick setup, no account required |
serper |
serper.dev | 2,500 queries/mo | Google-quality results |
tavily |
tavily.com | 1,000 queries/mo | AI-optimized results, built for agents |
brave |
brave.com/search/api | 2,000 queries/mo | Privacy-focused search |
exa |
exa.ai | 1,000 queries/mo | Semantic/neural search, finds by meaning |
searxng |
Not needed (self-hosted) | Unlimited | Aggregates many engines, full control |
google |
Google Cloud | 100 queries/day | Official Google API |
Examples:
# Free search — no API key needed
web_search:
enabled: true
backend: "duckduckgo"
# Tavily — best for AI agents
web_search:
enabled: true
backend: "tavily"
api_key: "tvly-xxxxxxxxxxxxxxxxxxxxxxxx"
# Brave — privacy-focused
web_search:
enabled: true
backend: "brave"
api_key: "BSA-xxxxxxxxxxxxxxxxxxxxxxxx"
# Exa — semantic search (finds pages by meaning, not just keywords)
web_search:
enabled: true
backend: "exa"
api_key: "exa-xxxxxxxxxxxxxxxxxxxxxxxx"
# Serper — Google results via API
web_search:
enabled: true
backend: "serper"
api_key: "xxxxxxxxxxxxxxxxxxxxxxxx"
# SearXNG — self-hosted meta-search (aggregates Google, Bing, DuckDuckGo, etc.)
web_search:
enabled: true
backend: "searxng"
base_url: "http://localhost:8080"
# Google Custom Search — official Google API
web_search:
enabled: true
backend: "google"
api_key: "$GOOGLE_API_KEY"
cx: "xxxxxxxxxxxxxxxxxxxxxxxx"
# Use env var to keep API key out of config
web_search:
enabled: true
backend: "tavily"
api_key: "$TAVILY_API_KEY"The web_fetch tool retrieves full page content and supports 2 backends:
web_fetch:
enabled: true
backend: "basic" # "basic" (raw HTTP) or "jina" (clean markdown)
max_content_length: 4096 # max bytes to return per page
# api_key: "" # optional — Jina free tier works without it| Backend | API Key | Best for |
|---|---|---|
basic |
Not needed | Simple pages, raw HTTP + HTML tag stripping |
jina |
Optional (jina.ai/reader) | Any URL → clean markdown, handles JS-rendered pages |
# Jina AI Reader — URL to clean markdown (recommended)
web_fetch:
enabled: true
backend: "jina"
api_key: "$JINA_API_KEY" # optional — free tier works without key| Middleware | Phase | Purpose |
|---|---|---|
| input_sanitization | WrapModelCall | Sanitize user input |
| syscoal | WrapModelCall | Coalesce consecutive system messages |
| dangling_tool_call | WrapModelCall | Repair incomplete tool call/result pairs |
| llm_error | WrapModelCall | Retry transient LLM errors |
| loop_detection | AfterModel + WrapModelCall | Detect and break repetitive loops |
| token_budget | AfterModel + WrapModelCall | Warn/stop on token budget exceeded |
| safety_finish | AfterModel | Strip tool calls on safety termination |
| tool_budget | WrapToolCall | Truncate large tool output |
| tool_error | WrapToolCall | Catch tool execution errors |
| clarification | WrapToolCall | Intercept ask_clarification calls |
| dynamic_context | BeforeAgent | Inject date + memory as system reminder |
| token_usage | AfterModel | Track token consumption |
| todo | AfterModel | Force-model for incomplete todo items |
| title | AfterAgent | Generate thread title after first exchange |
| subagent_limit | — | Enforce subagent invocation limits |
| guardrails | — | Content safety word filter |
| thread_data | — | Inject thread metadata |
| tool_metadata | WrapToolCall | Add server name to MCP tool results |
| uploads | — | Resolve uploaded file references |
| view_image | — | Convert image paths to content blocks |
| deferred_tool_filter | — | Defer low-priority tool calls |
| skill_activation | — | Activate skills based on slash commands |
| sandbox_audit | — | Audit sandbox operations |
| summarization | BeforeModel | Truncate old messages when limits approached |
| memory | AfterAgent | Async LLM-based memory updates |
When allow_host_bash: true is set in config, the shell tool executes
commands directly on the host machine. This is needed for tools that
require host network access and local credentials.
sandbox:
# Local: use "." so the agent can see project files
dir: "."
# Docker: use ".maia/sandbox" for isolation
# dir: ".maia/sandbox"
allow_host_bash: true
# No host_shell_command needed for local execution.
# For Docker: host_shell_command: "docker exec host-shell bash -c {command}"Maia connects to popular messaging platforms so users can chat with the agent from their preferred app. All channels support the same slash commands (/help, /new, /status).
| Channel | Direction | Editing | Auth |
|---|---|---|---|
| Telegram | Bidirectional | ✅ | Bot token from @BotFather |
| Slack | Bidirectional | ✅ | Bot token + signing secret |
| Discord | Bidirectional | ✅ | Bot token + application ID |
| Feishu | Bidirectional | ✅ | App ID + app secret |
| DingTalk | Bidirectional | ❌ | Webhook URL + secret |
| WeChat Work | Bidirectional | ❌ | App credentials or webhook |
| Bidirectional | ❌ | QR code scan |
👉 Full setup guides: docs/CHANNELS.md
Maia supports WhatsApp via the whatsmeow library (WhatsApp Web protocol). Unlike webhook-based channels, WhatsApp maintains a persistent WebSocket connection.
-
Enable WhatsApp in
config.yaml:channels: whatsapp: enabled: true allow_from: ["+15551234567"] dm_policy: "allowlist"
-
Start Maia and click Connect next to WhatsApp in the web UI, or retrieve the QR code directly:
curl http://localhost:8002/api/channels/whatsapp/qr
-
Scan the QR code with your phone: WhatsApp → Settings → Linked Devices → Link a device
-
After pairing, the session is saved in
.maia/whatsapp/. Subsequent starts reconnect automatically — no need to scan again.
| Policy | Behavior |
|---|---|
allowlist |
Only numbers in allow_from can chat (default) |
open |
Anyone can chat |
disabled |
No DMs processed |
Set group_enabled: true to allow the bot in group chats. Use
group_allow_from to restrict to specific groups by their JID
(e.g. 120363xxx@g.us).
Use a dedicated phone number for the bot when possible. Personal-number setups work but may cause confusion with self-chat messages.
Maia loads configuration from config.yaml (YAML) and .env (environment
variables). The .env file is loaded at startup, so $VARIABLE references
in config.yaml work even if the variable isn't exported in your shell.
Key config sections: server, agent, model, database, auth, sandbox,
title, summarize, dynamic_context, memory, web_search, web_fetch,
skills, channels, subagent_limit, guardrails.
maia/
├── cmd/maia/ # Main entry point
├── internal/
│ ├── agent/ # Agent runtime (ReAct loop)
│ │ └── state/ # Immutable state model
│ ├── auth/ # JWT + OIDC authentication
│ ├── channel/ # IM channel registry
│ │ ├── feishu/ # Feishu/Lark integration
│ │ ├── slack/ # Slack integration
│ │ ├── telegram/ # Telegram integration
│ │ ├── discord/ # Discord integration
│ │ ├── dingtalk/ # DingTalk integration
│ │ ├── wechat/ # WeChat Work / WeCom integration
│ │ └── whatsapp/ # WhatsApp integration (whatsmeow)
│ ├── config/ # Configuration loading + env expansion
│ ├── gateway/ # HTTP API server
│ ├── hook/ # Middleware hook interfaces
│ ├── journal/ # Run event journal
│ ├── mcp/ # Model Context Protocol client
│ ├── memory/ # Memory storage + async updater
│ ├── message/ # Message types + merge logic
│ ├── middleware/ # 24 middleware implementations
│ ├── model/ # Provider interface
│ │ ├── anthropic/ # Anthropic Messages API provider
│ │ ├── fake/ # Fake provider for testing
│ │ └── openai/ # OpenAI-compatible provider (+ quirks)
│ ├── skill/ # Skill storage + SKILL.md parser
│ ├── store/sqlite/ # SQLite persistence + checkpoints
│ ├── subagent/ # Subagent executor
│ └── tool/ # Tool interface + registry
│ └── builtin/ # Built-in tools + sandbox
├── frontend/ # Next.js web interface
├── skills/ # Agent skills (public/ + custom/)
├── scripts/ # Setup and deployment scripts
├── docker/ # Docker Compose + nginx config
└── pkg/types/ # Shared domain types
-
ReAct loop, not a graph engine — Simple for-loop over model calls and tool execution. No DAG, no conditional edges.
-
Immutable state — Every state transition produces a new
Statevalue. The original is never mutated. -
Interface-segregated hooks — Middleware implements only the phases it needs (
BeforeAgent,AfterModel,WrapModelCall,WrapToolCall, etc.). -
Fakes over mocks — Test doubles are real implementations with deterministic behaviour.
-
Multi-protocol providers — OpenAI Chat Completions and Anthropic Messages are both supported. Provider-specific quirks are handled via a
Quirksstruct instead of a class hierarchy. -
Sandboxed execution — All file/command operations are scoped to a sandbox directory with path traversal protection. Host bash mode is opt-in for tools requiring host access.
-
Streaming tool call accumulation — OpenAI streaming delivers tool call arguments as JSON string fragments. Each fragment is
json.Unmarshal'd as a Go string to resolve escape sequences, then concatenated. The final string is a valid JSON object. -
.env file loading — The
.envfile is loaded at startup before config parsing, so$VARIABLEreferences inconfig.yamlwork without requiring the user to export variables in their shell.
go test ./... -v # Run all tests (588 tests)
go test ./... -cover # With coverage
go vet ./... # Static analysis build Build the Maia binary
run Run Maia gateway only (port 8002)
dev Run gateway with auto-reload (requires air)
dev-full Run gateway + frontend (gateway :8002, frontend :3000)
dev-stop Stop locally-running gateway and frontend
test Run all tests
test-race Run tests with race detector
lint Run go vet
config Create config files from templates (first-time)
setup First-time setup
clean Remove binary and runtime data
help Show this help
- PostgreSQL store
- Remote K8s sandbox / provisioner
- CSRF protection
- Run cancel / join / events endpoints
- Skill CRUD API + rollback + history + security scanner
- Skill evolution (agent-managed skill creation)
- Tracing / OpenTelemetry