Skip to content

Core concepts

This page explains the core ideas behind mithai. Read it after the getting started guide and your first skill tutorial.



A skill is the unit of extension in mithai. It’s a folder with two files:

skills/
└── my_skill/
├── prompt.md # what the AI knows about this skill
└── tools.py # what the AI can do with this skill

Loaded into the system prompt at startup. Tells the AI what the skill does, when to use it, and any constraints.

You can check service health and restart services.
Always check health before recommending a restart.
Report response times and status codes clearly.

Keep this focused. The AI doesn’t need implementation details — just behavior.

Defines tools (exposed to the LLM) and implements them (runs on the server).

Required exports:

  • TOOLS — list of tool definitions in Anthropic tool schema format
  • handle(name, input, ctx) — receives tool calls, returns a JSON string

Optional exports:

  • resolve_human(name, input, ctx) — runtime approval decision (see Human MCP below)
  • startup(config) — called once when the engine starts; use for background loops or connection setup
  • bind(engine, adapter) — called after engine and adapters are initialized; gives the skill access to both
  • MCP_TOOLS — declares which external MCP servers this skill needs

Tools are automatically namespaced as skillname__toolname. A tool named check_health in a skill named services becomes services__check_health in the LLM’s tool list. This prevents collisions between skills.

skills/
└── services/ ← skill name
├── prompt.md ← injected into system prompt
└── tools.py ← exports TOOLS + handle()
├── check_health ← tool name defined in TOOLS
└── restart_service
▼ namespaced automatically by the engine
services__check_health
services__restart_service

Every call to handle and resolve_human receives a ctx dict:

ctx = {
"config": dict, # the skill's config block from config.yaml
"state": StateBackend, # key-value store (persists across restarts)
"memory": MemoryBackend, # file-based storage (markdown, JSON)
"channel_id": str, # where the message came from
"user_id": str, # who sent the message
"logger": Logger, # structured logger
}

ctx["config"] is the most commonly used — it gives you the skills.config.<skillname> block from config.yaml.


Human MCP (Model Context Protocol) is how mithai keeps humans in the loop for sensitive actions. Every tool has an approval level.

LevelBehavior
null (default)Auto-execute. No approval needed.
"approve"Show the tool name and inputs. User clicks Approve or Deny.
"confirm"Higher friction. User must type a confirmation string.
"dynamic"The skill’s resolve_human function decides at runtime.

Set the level on the tool definition:

TOOLS = [
{"name": "list_pods", ...}, # auto-execute
{"name": "restart", ..., "human": "approve"}, # button
{"name": "delete_namespace", ..., "human": "confirm"}, # type to confirm
{"name": "run_command", ..., "human": "dynamic"}, # resolve_human decides
]

When "human": "dynamic" is set, or when you want to override the static level at runtime, export resolve_human from tools.py:

def resolve_human(name: str, input: dict, ctx: dict) -> str | None:
if name == "restart":
# Production requires approval; staging is fine
if input.get("environment") == "production":
return "approve"
return None # auto-execute
return None

resolve_human takes precedence over the static "human" key when present.

You can override any tool’s approval level without touching the skill code:

human:
timeout_seconds: 300
overrides:
shell__run_command: confirm # escalate: require typing
kubernetes__get_pods: null # de-escalate: run automatically

The key format is skillname__toolname.

In Slack: an inline message in the thread with the tool name, the inputs, and Approve/Deny buttons.

In the terminal: a text prompt showing the same information. Type approve or deny.

In Telegram: an inline keyboard with approval buttons.

Approval is scoped to the user who made the request. The response routes back through the same adapter that received the original message.


An adapter connects mithai to a communication platform. The same engine and skills run behind every adapter.

┌─────────┐ ┌──────────┐ ┌─────┐
│ Slack │ │ Telegram │ │ CLI │
└────┬────┘ └─────┬────┘ └──┬──┘
│ │ │
└───────────────┼───────────┘
┌──────┴──────┐
│ Engine │
│ (shared) │
└──────┬──────┘
┌───────────┼───────────┐
┌────┴────┐ ┌────┴────┐ ┌───┴──────┐
│ Skills │ │ Human │ │ Memory │
│ │ │ MCP │ │ State │
└─────────┘ └─────────┘ └──────────┘

Human MCP approval requests always route back through the same adapter that received the original message — Slack approvals stay in Slack, CLI prompts stay in the terminal.

Real-time via Socket Mode (no public webhook needed). Each Slack thread is an independent session — the agent has full conversation history within a thread, but a fresh context in a new one. Approvals appear as interactive button messages.

adapter:
type: slack
slack:
bot_token: ${SLACK_BOT_TOKEN} # xoxb-...
app_token: ${SLACK_APP_TOKEN} # xapp-...
respond: mentions # "mentions" or "all"

Thread context: When you @mention the bot in a thread it didn’t start, it fetches prior messages in that thread for context. This means you can drop the bot into an ongoing incident thread and it catches up immediately.

Long-polling (no server needed). Conversations are per-chat. Access control via allowed_chat_ids.

adapter:
type: telegram
telegram:
bot_token: ${TELEGRAM_BOT_TOKEN}
allowed_chat_ids:
- ${TELEGRAM_CHAT_ID}

Interactive REPL. Good for local development and testing skills before deploying. Supports markdown rendering, slash commands, and tab completion.

Terminal window
mithai chat

The CLI adapter also accepts JSON on stdin in piped mode, which lets scripts and schedulers inject per-call context:

Terminal window
echo '{"text": "check staging", "channel_id": "ci", "system_prompt_append": "Focus on the staging environment only."}' \
| mithai run --adapter cli

system_prompt_append is appended to the agent’s system prompt for that turn only; the base prompt is unchanged for other calls.

Headless adapter for webhooks and automation. The process blocks indefinitely while an embedded HTTP server (MITHAI_UI_PORT) handles all traffic. No Slack connection or terminal required.

Terminal window
MITHAI_UI_PORT=8080 MITHAI_UI_TOKEN=secret mithai run --adapter api

Callers send messages via POST /api/trigger with a Bearer token. The engine fires in the background and returns 202 immediately. Human approval requests are auto-denied in this mode.

See Configuration reference → API adapter for the full request format.

To run Slack and Telegram simultaneously, use types instead of type:

adapter:
types:
- slack
- telegram
slack:
bot_token: ${SLACK_BOT_TOKEN}
app_token: ${SLACK_APP_TOKEN}
telegram:
bot_token: ${TELEGRAM_BOT_TOKEN}
allowed_chat_ids:
- ${TELEGRAM_CHAT_ID}

Each adapter runs in its own thread. They share the same engine and skills. Human MCP approval requests are always routed back through the adapter that received the original message.


A session is a conversation with memory. The agent maintains message history within a session so it can reference earlier context.

Session scope depends on the adapter:

AdapterSession scope
SlackPer Slack thread (thread_ts)
TelegramPer chat
CLIPer process

In Slack: every thread is its own session. The agent doesn’t confuse parallel conversations happening in different threads of the same channel.

Sessions are persisted to .mithai/state/. If the bot restarts, it picks up where it left off in the next message.


Memory is persistent storage that spans sessions and restarts. It’s distinct from session history (which is conversation turns) — memory is more like a knowledge base.

The built-in memory skill exposes three tools to the agent:

  • memory_read(path) — read a file
  • memory_write(path, content, mode) — write or append
  • memory_search(query) — keyword search across all files

The agent uses these to record facts, save playbooks, and look up past decisions:

User: remember that our database primary is at db-1.internal
Agent: memory_write("MEMORY.md", "- DB primary: db-1.internal\n", mode="append")
Saved.

Tools can read and write memory directly via ctx["memory"]:

memory = ctx.get("memory")
if memory:
memory.write("incidents.md", f"- Restarted {service}\n", append=True)
content = memory.read("playbooks/restart.md")
memory/
├── MEMORY.md # main knowledge base, injected into every prompt
├── approvals.json # approval history (used by shell skill for auto-promote)
├── playbooks/ # step-by-step runbooks
│ └── restart.md
└── daily/
└── 2026-03-16.md # daily reflection (written by the agent after each session)

MEMORY.md is special: it’s automatically loaded into every conversation’s system prompt. Use it to record facts your agent should always know — team conventions, environment details, known issues.

When the shell skill sees a command approved enough times with no denials, it stops asking. This is the auto-promote mechanism:

skills:
config:
shell:
approval_auto_promote: 3 # approve 3 times → runs automatically

The approval history is stored in memory/approvals.json. You can inspect or reset it at any time.

The agent also runs a reflection pass after each conversation (when learning.reflection: true), writing a brief summary to memory/daily/YYYY-MM-DD.md. Over time this becomes a log of what the agent has learned and done.


After each turn, when a skill with VERIFY = True was called, mithai runs a secondary LLM call to check that the agent’s response does not contradict what the tools returned. If a numerical or factual contradiction is found, the response is annotated with ⚠️.

This is opt-in per skill. Built-in skills aws and kubernetes opt in by default. To add verification to a custom skill:

skills/my_skill/tools.py
VERIFY = True

To use a separate cheaper model for the fact-check:

config.yaml
verifier:
model: claude-haiku-4-5

The verifier only catches clear contradictions — not omissions, style differences, or paraphrasing. It’s a safety net for skills that return precise numeric data (pod counts, costs, disk sizes) where a hallucinated summary could cause a bad decision.


Understanding the request lifecycle helps when debugging or extending mithai.

User message
┌─────────────┐
│ Adapter │ (Slack / Telegram / CLI)
└──────┬──────┘
│ engine.handle(message, adapter)
┌─────────────┐ session history +
│ Engine │ ◄── system prompt + memory
└──────┬──────┘
┌─────────────┐
│ LLM │ (Claude)
└──────┬──────┘
▼ tool call in response?
┌────┴────┐
│ yes │ no ──────────────────────────┐
▼ │ │
resolve_human() │
│ │
├── null (auto-execute) ──────────┐ │
│ │ │
└── "approve" / "confirm" │ │
│ │ │
▼ │ │
┌─────────────┐ │ │
│ Human MCP │ approval request │ │
│ via Adapter│ ──► User │ │
└──────┬──────┘ approve/deny │ │
│ approved │ │
└────────────────────────────┘ │
│ │
▼ │
handle(name, input, ctx) │
│ │
▼ │
tool_result added to history │
│ │
└──► back to LLM ◄──────┘
│ (loop until no tool calls)
final text response
┌───────────┐
│ Adapter │
└─────┬─────┘
User

For larger organizations, you can run multiple independent agents from a single config.yaml. Each agent has its own Slack app, its own skill set, and its own memory.

agents:
devops:
name: "DevOps Agent"
system_prompt: "You are a DevOps assistant. Focus on infrastructure."
skills:
allowed: [shell, kubernetes, aws, memory]
adapter:
slack:
bot_token: ${DEVOPS_SLACK_BOT_TOKEN}
app_token: ${DEVOPS_SLACK_APP_TOKEN}
memory:
path: ./memory/devops
triage:
name: "Triage Agent"
system_prompt: "You are an incident triage assistant."
skills:
allowed: [shell, github, memory]
adapter:
slack:
bot_token: ${TRIAGE_SLACK_BOT_TOKEN}
app_token: ${TRIAGE_SLACK_APP_TOKEN}
memory:
path: ./memory/triage

Each agent is isolated. mithai run starts all of them.