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