Skip to content

Repository files navigation

Maia

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.

Quick Start

# 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

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  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)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Model Providers

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.

Built-in Tools

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.

Web Search Backends

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

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

Host Bash Mode

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}"

IM Channels

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
WhatsApp Bidirectional ❌ QR code scan

πŸ‘‰ Full setup guides: docs/CHANNELS.md

WhatsApp Quick Setup

Maia supports WhatsApp via the whatsmeow library (WhatsApp Web protocol). Unlike webhook-based channels, WhatsApp maintains a persistent WebSocket connection.

  1. Enable WhatsApp in config.yaml:

    channels:
      whatsapp:
        enabled: true
        allow_from: ["+15551234567"]
        dm_policy: "allowlist"
  2. 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
  3. Scan the QR code with your phone: WhatsApp β†’ Settings β†’ Linked Devices β†’ Link a device

  4. After pairing, the session is saved in .maia/whatsapp/. Subsequent starts reconnect automatically β€” no need to scan again.

DM Policy

Policy Behavior
allowlist Only numbers in allow_from can chat (default)
open Anyone can chat
disabled No DMs processed

Group Chats

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).

Recommended Setup

Use a dedicated phone number for the bot when possible. Personal-number setups work but may cause confusion with self-chat messages.

Configuration

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.

Project Structure

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

Key Design Decisions

  1. ReAct loop, not a graph engine β€” Simple for-loop over model calls and tool execution. No DAG, no conditional edges.

  2. Immutable state β€” Every state transition produces a new State value. The original is never mutated.

  3. Interface-segregated hooks β€” Middleware implements only the phases it needs (BeforeAgent, AfterModel, WrapModelCall, WrapToolCall, etc.).

  4. Fakes over mocks β€” Test doubles are real implementations with deterministic behaviour.

  5. Multi-protocol providers β€” OpenAI Chat Completions and Anthropic Messages are both supported. Provider-specific quirks are handled via a Quirks struct instead of a class hierarchy.

  6. 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.

  7. 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.

  8. .env file loading β€” The .env file is loaded at startup before config parsing, so $VARIABLE references in config.yaml work without requiring the user to export variables in their shell.

Testing

go test ./... -v        # Run all tests (588 tests)
go test ./... -cover    # With coverage
go vet ./...            # Static analysis

Make Targets

  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

What's Next

  • 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

About

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.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages