Configuration reference
mithai is configured with a config.yaml file. All values support ${ENV_VAR} interpolation — the framework substitutes environment variables at load time. Secrets should always live in .env, not in config.yaml.
Quick example
Section titled “Quick example”bot: name: mithai system_prompt: | You are a concise assistant. Always confirm before irreversible actions.
adapter: type: slack slack: bot_token: ${SLACK_BOT_TOKEN} app_token: ${SLACK_APP_TOKEN} respond: mentions
llm: provider: anthropic model: claude-sonnet-4-6 max_tokens: 4096 anthropic: api_key: ${ANTHROPIC_API_KEY}
skills: paths: - ./skills config: shell: allowed_commands: ["df -h", "uptime"]bot: name: mithai # the agent's name; shown in responses and UI system_prompt: | # prepended to every conversation You are a concise assistant.system_prompt is added before skill prompts and memory. Use it to set the agent’s personality, scope, and any organization-wide rules.
adapter
Section titled “adapter”Adapters connect the agent to communication platforms.
Single adapter
Section titled “Single adapter”adapter: type: slack # slack | telegram | cli | api slack: { ... }Multiple adapters
Section titled “Multiple adapters”adapter: types: - slack - telegram slack: { ... } telegram: { ... }All adapters share the same engine and skills. Human MCP approvals always route back through the adapter that received the original message.
adapter: slack: bot_token: ${SLACK_BOT_TOKEN} # xoxb-... (Bot User OAuth Token) app_token: ${SLACK_APP_TOKEN} # xapp-... (App-Level Token, Socket Mode) respond: mentions # "mentions" (default) or "all"respond: mentions — the bot only responds when @mentioned. Use respond: all to respond to every message in channels it’s invited to.
External Slack channels
Section titled “External Slack channels”allow_posting_in_external_channels is an advanced Slack safety setting for teams that want mithai to read and react to Slack Connect or externally shared channels, but never post assistant text back into those channels.
The default is true, which preserves existing behavior. Set it to false only when the agent should observe or process external shared channel messages without writing text back to those channels.
adapter: slack: allow_posting_in_external_channels: falseWhen set to false, the Slack adapter uses Slack conversations.info metadata to identify external channels:
is_ext_shared: trueis_pending_ext_shared: trueis_shared: truewithis_org_sharednot true
This means channel names, glob patterns, and deployment-specific customer-channel conventions are not part of the policy. Slack is the source of truth for whether a channel is external. Enterprise Grid org-shared channels (is_org_shared: true) are treated as internal and keep normal posting behavior.
For detected external channels, mithai suppresses adapter-originated text posts, including:
- Final assistant responses from Slack message handlers.
- Direct Slack adapter sends through
adapter.send(). - Slack MCP send-message tools before the tool call is routed.
- Human approval prompts and timeout notices.
- Canned app-mention replies and onboarding messages.
Reactions and read-only Slack operations are still allowed. If Slack channel metadata cannot be fetched while this setting is false, mithai fails closed and suppresses the attempted text post rather than risking an external-channel message. Failed metadata lookups are not cached, so a transient Slack API failure can recover on the next attempt.
Make sure the Slack app has enough channel visibility for conversations.info on the channels where the bot runs. If metadata lookup fails, logs include Could not resolve Slack channel info for external posting guard.
Telegram
Section titled “Telegram”adapter: telegram: bot_token: ${TELEGRAM_BOT_TOKEN} allowed_chat_ids: - ${TELEGRAM_CHAT_ID} # whitelist of chat IDsNo configuration required. Use mithai chat to start an interactive session.
Headless adapter for webhook and programmatic use. The process stays alive while the embedded API server (MITHAI_UI_PORT) handles all traffic. No Slack or terminal connection required.
adapter: type: apiStart with:
MITHAI_UI_PORT=8080 MITHAI_UI_TOKEN=secret mithai run --adapter apiSend messages via POST /api/trigger:
curl -X POST http://localhost:8080/api/trigger \ -H "Authorization: Bearer secret" \ -H "Content-Type: application/json" \ -d '{"message": "deploy app", "channel_id": "webhook"}'# → 202 Accepted {"status": "accepted", "channel_id": "webhook"}The response returns 202 immediately and the engine runs in the background. Approval-gated tools are cancelled in this non-interactive mode. Use wait=true&stream=approval-v1 plus the approval decision endpoint for interactive API approvals.
Body fields:
| Field | Required | Description |
|---|---|---|
message | yes | Text to send to the agent |
channel_id | no | Session namespace (default: "trigger") |
user_id | no | User identifier (default: "api") |
With wait=true, the JSON response also includes time_boxed, tool_results,
and a provider-neutral coverage summary. With wait=true&stream=true, the
final NDJSON done event carries the same fields. Coverage is complete,
partial, unknown, or not_applicable; arbitrary tool inputs are not copied
into this public summary.
The public coverage shape is:
{ "coverage": { "status": "partial", "observations": [ {"status": "complete", "result_state": "data"}, {"status": "partial", "result_state": "empty"} ] }}observations contains each distinct categorical (status, result_state) pair
at most once; provider identity and raw scopes do not affect this public
de-duplication. Internal unverified-entry counts are not exposed. If at least
one verified observation exists, a partial declaration or any unverified entry
makes the summary partial; with unverified entries but no verified
observations it is unknown. A turn with no coverage-relevant entries is
not_applicable.
Interactive approval protocol
Section titled “Interactive approval protocol”For callers that need to resolve approve/confirm-level Human MCP requests interactively (rather than having them auto-cancel), open an SSE stream on /api/trigger instead of the fire-and-forget POST above.
Requires ui.auth_token to be configured (interactive endpoints return 503 without it) and adapter.type: api.
curl -N -X POST "http://localhost:8080/api/trigger?wait=1&stream=approval-v1" \ -H "Authorization: Bearer secret" \ -H "Content-Type: application/json" \ -d '{"message": "restart payments-api", "channel_id": "webhook"}'The response is Content-Type: text/event-stream with header X-Mithai-Capability: human-interaction-v1. While the run is between events the stream sends event: heartbeat keep-alives (roughly every 15s of inactivity).
Event envelope
Every event is an SSE event: <type> line with a data: line of JSON carrying a common envelope — protocol_version, sequence (monotonically increasing per run, 1-indexed), run_id, channel_id — plus type-specific fields. Besides approval_requested/approval_resolved/question_requested/question_resolved (below), the stream also carries run_started, assistant_text_start/assistant_text/assistant_text_end, tool_started, tool_completed, run_completed, run_failed, and heartbeat events.
Assistant text
Text arrives incrementally as the LLM generates it. Each LLM round that produces text opens a block with assistant_text_start, emits one assistant_text per delta, and closes with assistant_text_end. A run with tool calls therefore has several blocks — the pre-tool preamble, then the answer — with the tool events in between, so a reader can lay them out in the order they happened. A round that produces no text (tools only) opens no block.
{"protocol_version": "v2", "sequence": 2, "run_id": "…", "channel_id": "webhook", "type": "assistant_text_start"}{"protocol_version": "v2", "sequence": 3, "run_id": "…", "channel_id": "webhook", "type": "assistant_text", "text": "Checking the "}{"protocol_version": "v2", "sequence": 4, "run_id": "…", "channel_id": "webhook", "type": "assistant_text", "text": "payments-api logs."}{"protocol_version": "v2", "sequence": 5, "run_id": "…", "channel_id": "webhook", "type": "assistant_text_end"}A run whose LLM produced no deltas at all (a provider without native streaming that also returned nothing incrementally) instead sends the whole answer as one bare assistant_text with no surrounding block.
A block never opens on whitespace alone: leading whitespace is held back until the round produces printable text, then flushed with it, so a reader never has to render an empty message. A round that emits only whitespace opens no block and counts as having streamed nothing.
run_completed carries text: the run’s authoritative final answer. It is the value to persist — concatenating the deltas would also fold in the pre-tool preamble. An empty text means there is no authoritative answer for this run (the agent returned nothing), and a reader should fall back to whatever the deltas carried.
Every assistant_text_start is matched by an assistant_text_end before the terminal event, including when a run fails partway through a block.
tool_started / tool_completed are emitted for every tool call, gated or not, correlated by tool_call_id (the LLM’s tool-use block id). tool_started carries the same redacted, size-capped tool_input presentation as approval_requested; tool_completed carries duration and approved. human__ask is excluded — it surfaces as question_requested/question_resolved instead.
A denied or cancelled tool emits neither event — it never starts executing, so there is no tool_started to leave dangling; its outcome arrives as approval_resolved. A tool that does start always gets a matching tool_completed, including when it raises (the engine reports that as approved: true — the flag records the human decision, not success).
{"protocol_version": "v2", "sequence": 6, "run_id": "…", "channel_id": "webhook", "type": "tool_started", "approval_id": null, "tool_name": "last9__get_logs", "tool_call_id": "toolu_…", "tool_input": {"service": "payments-api"}}{"protocol_version": "v2", "sequence": 7, "run_id": "…", "channel_id": "webhook", "type": "tool_completed", "approval_id": null, "tool_name": "last9__get_logs", "tool_call_id": "toolu_…", "duration": 1.42, "approved": true}approval_requested / approval_resolved:
{"protocol_version": "v2", "sequence": 2, "run_id": "…", "channel_id": "webhook", "type": "approval_requested", "approval_id": "…", "tool_name": "shell__run", "tool_call_id": "…", "level": "confirm", "safe_description": "Tool: shell__run\nAction: …", "confirmation_challenge": "CONFIRM", "tool_input": {"command": "curl https://example.com/health"}, "expires_at": 1732200000.0}{"protocol_version": "v2", "sequence": 5, "run_id": "…", "channel_id": "webhook", "type": "approval_resolved", "approval_id": "…", "tool_call_id": "…", "outcome": "approved"}question_requested / question_resolved (from ask_human, only available inside this interactive stream):
{"protocol_version": "v2", "sequence": 3, "run_id": "…", "channel_id": "webhook", "type": "question_requested", "question_id": "…", "prompt": "Which environment?", "choices": ["staging", "prod"], "expires_at": 1732200000.0}{"protocol_version": "v2", "sequence": 4, "run_id": "…", "channel_id": "webhook", "type": "question_resolved", "question_id": "…", "outcome": "answered", "answer": "staging"}outcome on approval_resolved is one of approved, denied, timed_out, cancelled. outcome on question_resolved is one of answered, timed_out, cancelled; answer is present only when outcome is answered.
Decision and status routes
All four routes require the same auth as above, plus ui.auth_token configured and adapter.type: api — otherwise 503 {"error": "interactive API authentication required"} or 409 {"error": "interactive API adapter required"}.
| Method & path | Body | Response | Status codes |
|---|---|---|---|
POST /api/approvals/{approval_id}/decision | {"approved": bool, "confirmation": string} | {"approval_id", "run_id", "channel_id", "outcome", "expires_at"} | 200 decided · 400 approved missing/not boolean or confirmation mismatch · 409 already resolved to a different outcome · 410 unknown/expired/cancelled |
GET /api/approvals/{approval_id} | — | same shape as above (outcome: "pending" while unresolved) | 200 · 410 unknown |
POST /api/questions/{question_id}/answer | {"answer": string} | {"question_id", "run_id", "channel_id", "prompt", "choices", "outcome", "expires_at", "answer"} | 200 answered · 400 answer missing/empty/over 4096 chars · 409 already answered differently · 410 unknown/expired/cancelled |
GET /api/questions/{question_id} | — | same shape as above (outcome: "pending" while unresolved) | 200 · 410 unknown |
confirmation is only checked when the approval’s level is confirm and approved is true: it must exactly match the confirmation_challenge sent in the approval_requested event, or the decision is rejected with 400. The challenge is the literal string "CONFIRM"; raw tool arguments are never used as confirmation text or included in presentation events. Denials (approved: false) never check confirmation.
If the client disconnects from the SSE stream before the run completes, the run is cancelled: any pending approval/question resolves as cancelled, and no further tools execute for that run.
engine
Section titled “engine”Optional engine-level guardrails for long-running deployments. All sub-keys are off or empty by default so generic mithai installs behave unchanged; production agents (for example the Last9 supervisor) should set these explicitly.
engine.session
Section titled “engine.session”Per-turn wall-clock budgets and same-session request coalescing limits.
engine: session: deadline_seconds: 150 # default: 150 — raise only when no gateway ceiling applies wrapup_timeout_seconds: 25 # default: 25 inflight_ttl_seconds: 400 # default: 400 — crash-safety bound for in-flight markersdeadline_seconds is the main turn budget before the engine forces a tools-free wrap-up answer. Deployments behind an external gateway with a shorter ceiling (for example ~180s) should set deadline_seconds explicitly rather than relying on the framework default.
engine.tool_budget
Section titled “engine.tool_budget”Generic per-turn caps on operator-chosen tools (not hardcoded to any MCP server).
engine: tool_budget: enabled: true tools: - get_logs - get_service_logs max_per_turn: 5 max_long_range_per_turn: 1 long_range_threshold_minutes: 60 long_range_service_name_heuristic: false replan_enabled: false # opt in to one bounded corrective model roundLegacy alias: engine.log_tool_budget (same shape). When only the legacy key is present, enabled defaults to true for backward compatibility.
When a configured cap is reached, unconfigured tools remain available. Mithai
can synthesize an honest partial answer from successful evidence and, when
replan_enabled is true, offers exactly one corrective round with only calls
that can still execute. Private execution counters and rejection codes are not
persisted or returned to the user.
Tool providers may optionally declare searched coverage without Mithai knowing
their domain. MCP servers place the versioned descriptor in
CallToolResult._meta under mithai/coverage:
{ "mithai/coverage": { "version": 1, "subject": "events", "attempted_scope": {"window": "requested"}, "covered_scope": {"window": "searched"}, "status": "partial", "result_state": "data" }}status accepts exactly complete or partial; result_state accepts exactly
data or empty. Raw subject, attempted_scope, and covered_scope values
are bounded, untrusted provider declarations retained only in the internal
turn ledger. They never enter recovery prompts, response prose, persisted or
public coverage, or API responses. Public coverage observations contain only
the categorical status and result_state values.
For integrators, subject is limited to 128 characters. Each of
attempted_scope and covered_scope must be a JSON object and is limited to a
maximum depth of 4 (root depth 0), 64 members or elements per container, 128
total value nodes including containers, 128 characters per object key, 512
characters per string value, and 4096 bytes when encoded as compact UTF-8 JSON.
Missing, oversized, deeply nested, or otherwise invalid descriptors degrade to
unknown; Mithai does not inspect provider-specific tool names, arguments, or
result payloads to infer coverage.
Native tools attach the equivalent typed CoverageObservation to
ToolExecutionResult.coverage. Native tools that can emit that descriptor
set declares_coverage: true on the TOOLS list. MCP tools advertise the
same capability with _meta: {"mithai/coverage": true} on list_tools.
Pending or truncated calls never produce a runtime descriptor. Mithai treats
those calls as coverage-relevant when the tool is budgeted or registered as
coverage-capable. A registered coverage-capable tool that runs without a valid
descriptor records unknown. A tool that emits a descriptor at runtime without
advertising still records on the executed path; an orphaned first appearance
of such a tool does not change coverage status.
Before enabling replan_enabled, monitor
mithai.tool_budget.recoveries, mithai.tool_budget.blocked_calls, and
mithai.tool_budget.suppressed_retries. Enabling it adds at most one corrective
model round per exhausted turn; compare the replanned, synthesized, and
fallback dispositions with coverage status and turn latency during rollout.
engine.temporal_guardrails
Section titled “engine.temporal_guardrails”Operator-defined temporal normalization for MCP tools — which tools get time-window handling, per-tool argument profiles, and attribute-discovery clamps.
engine: temporal_guardrails: tools: - get_logs - get_traces tool_profiles: get_alerts: alerts_window get_alert_rule_state: epoch_range prometheus_instant_query: instant_time_iso attribute_discovery_tools: - get_log_attributes - get_log_attributes_for_pipeline attribute_discovery_lookback_minutes: 5When tools is empty, temporal guardrails are disabled.
mithai supports three LLM providers:
anthropic(default): direct Claude API access via theanthropicPython SDK.bedrock: AWS Bedrock. Defaults to the unified Converse API (works across Anthropic/Llama/Cohere/Mistral models on Bedrock); setapi: responsesto reach OpenAI models via Bedrock’s Mantle gateway instead. Requirespip install 'mithai[bedrock]'.openai: direct OpenAI Responses API access via theopenaiPython SDK. Requirespip install 'mithai[openai]'.
Switch by setting llm.provider and providing the matching config block.
Anthropic (default)
Section titled “Anthropic (default)”llm: provider: anthropic model: claude-sonnet-4-6 # or claude-opus-4-6, claude-haiku-4-5 max_tokens: 4096 anthropic: api_key: ${ANTHROPIC_API_KEY}Recommended models:
| Model | When to use |
|---|---|
claude-sonnet-4-6 | Default. Best balance of capability and speed. |
claude-opus-4-6 | Complex reasoning, multi-step tasks, high-stakes decisions. |
claude-haiku-4-5 | High-volume, latency-sensitive, simple queries. |
AWS Bedrock
Section titled “AWS Bedrock”llm: provider: bedrock model: anthropic.claude-sonnet-4-20250514-v1:0 # any Bedrock model ID max_tokens: 4096 bedrock: access_key_id: ${AWS_ACCESS_KEY_ID} secret_access_key: ${AWS_SECRET_ACCESS_KEY} region: ${AWS_REGION} session_token: ${AWS_SESSION_TOKEN} # optional — only for temporary (STS) credentialssession_token is only needed when using temporary credentials (STS-issued, assumed roles). Omit it for long-lived IAM user keys.
The model name is the Bedrock model ID, not the Anthropic alias. Some examples:
| Bedrock model ID | Equivalent |
|---|---|
anthropic.claude-sonnet-4-20250514-v1:0 | Claude Sonnet 4 |
anthropic.claude-opus-4-20250514-v1:0 | Claude Opus 4 |
anthropic.claude-haiku-4-5-20251001-v1:0 | Claude Haiku 4.5 |
meta.llama3-3-70b-instruct-v1:0 | Llama 3.3 70B |
The Bedrock Converse API is uniform across model families, so switching models is just a config change. Install: pip install 'mithai[bedrock]'.
IAM permissions: the credentials need bedrock:InvokeModel for every model the agent will use. If the agent is managed by multi-mithai, the credentials additionally need sts:GetCallerIdentity — the orchestrator uses it for connection validation; standalone mithai never calls STS.
OpenAI
Section titled “OpenAI”Direct access to OpenAI’s Responses API via the openai Python SDK. Requires pip install 'mithai[openai]':
llm: provider: openai model: gpt-5.6-terra max_tokens: 16384 openai: api_key: ${OPENAI_API_KEY} reasoning_effort: medium # optional: none | low | medium | high | xhigh | max (default: medium)Supported models: gpt-5.6-terra, gpt-5.6-sol, and gpt-5.6-luna are certified for use with mithai’s tool-calling loop, each verified on both direct OpenAI and AWS Bedrock Mantle (text turn, tool call, tool-result continuation, streaming). Using a different OpenAI model requires independently verifying it supports the Responses API and client-side function calling — mithai does not validate this for you.
gpt-5.6-luna— fast/cheap, for high-volume use.gpt-5.6-terra— balanced, for everyday use.gpt-5.6-sol— most capable, for frontier/agentic work.
Install: pip install 'mithai[openai]'.
Retention: mithai always sends store: false on direct OpenAI Responses requests, matching the Bedrock Mantle policy. OpenAI’s default is store: true, which retains request input and output for 30 days and makes them browsable in the org’s dashboard logs. store: false also requests include: ["reasoning.encrypted_content"] so encrypted reasoning is replayed on the next request instead of relying on server-side state.
OpenAI via AWS Bedrock (Mantle gateway)
Section titled “OpenAI via AWS Bedrock (Mantle gateway)”GPT-5.6 Terra, Sol, and Luna are also reachable through AWS Bedrock’s “Mantle” gateway, which speaks the OpenAI Responses wire format under a Bedrock-specific base URL. This is a distinct transport from the Bedrock Converse API described above: these models support only the Responses API on Bedrock — not Converse, Invoke, or ChatCompletions. So provider: bedrock with an openai.* model requires api: responses explicitly; omitting api: keeps the default converse behavior, which does not support these models.
llm: provider: bedrock model: openai.gpt-5.6-terra # Bedrock Mantle model IDs are prefixed "openai." — direct OpenAI uses the bare id max_tokens: 16384 bedrock: api: responses # required for OpenAI models — "converse" (default) does not support them api_key: ${AWS_BEDROCK_API_KEY} region: us-east-1 reasoning_effort: medium # optional: none | low | medium | high | xhigh | max (default: medium)Key reference:
| Key | Required for OpenAI models | Notes |
|---|---|---|
api | yes | converse (default) or responses. GPT-5.6 Terra/Sol/Luna require responses. |
api_key | yes | Bedrock API key (bearer token) only — this release supports Bedrock API key auth only for api: responses; IAM/SigV4 auth is supported by the SDK but not yet wired up in mithai. AWS recommends short-term keys for production; long-term keys are documented by AWS as exploration-only. |
region | yes | Model-dependent — see the region table below. GPT-5.6 models on Bedrock Mantle are In-Region only — no Geo or Global cross-region routing. |
reasoning_effort | no | none, low, medium, high, xhigh, max (default: medium). |
Region availability differs per model:
| Model | Regions |
|---|---|
openai.gpt-5.6-terra | us-east-1, us-east-2, us-west-2 |
openai.gpt-5.6-luna | us-east-1, us-east-2, us-west-2 |
openai.gpt-5.6-sol | us-east-1, us-east-2 only — narrower per its AWS model card |
Data residency: because there is no Geo/Global routing option for these models on Bedrock, prompts are processed entirely within the region you configure. This matters for deployments outside the US.
Retention: mithai always sends store: false on Bedrock Responses requests. AWS’s default is store: true, which retains request input and output for 30 days in-region — store: false avoids that retention. To keep reasoning context available across tool-call turns without relying on Bedrock to retain server-side state, mithai additionally requests include: ["reasoning.encrypted_content"] so encrypted reasoning is replayed on the next request instead.
Cost and context window differ by transport for the same model: Bedrock Mantle caps context at 272K tokens versus ~1M direct on OpenAI, for all three models.
| Model | Direct input / output (per 1M) | Bedrock input / output (per 1M) |
|---|---|---|
gpt-5.6-luna | $0.20 / $1.20 | $0.22 / $1.32 |
gpt-5.6-terra | $2.00 / $12.00 | $2.20 / $13.20 |
gpt-5.6-sol | $5.00 / $30.00 | $5.50 / $33.00 |
gen_ai.system telemetry label: openai (direct) vs aws.bedrock (Mantle) — same model string, priced differently per transport.
Install: pip install 'mithai[openai]' (the Bedrock Mantle transport reuses the openai package under the hood — the bedrock extra alone is not sufficient for this path).
Common settings
Section titled “Common settings”max_tokens controls the maximum length of each LLM response. 4096 is a good default. Raise it to 8192 or higher for skills that produce long outputs (e.g., log analysis, code review). On provider: openai and provider: bedrock with api: responses, values below 4096 are raised to 4096 because the Responses API bills reasoning tokens against max_output_tokens; set reasoning_effort: none to opt out of that floor.
skills
Section titled “skills”skills: paths: - ./skills # directories to scan for skills - /opt/shared/skills # additional paths config: shell: # skill name → config dict passed as ctx["config"] allowed_commands: - "df -h" - "uptime" approval_auto_promote: 3 services: services: checkout: url: https://checkout.internal/health billing: url: https://billing.internal/healthskills.paths lists directories. Each subdirectory with a prompt.md and tools.py is loaded as a skill.
skills.config maps skill names to arbitrary config dicts. A skill receives its config as ctx["config"] in every handler call.
Controls the human-in-the-loop protocol globally.
human: timeout_seconds: 300 # how long to wait for approval before timing out (default: 300) overrides: shell__run_command: confirm # escalate a tool's approval level kubernetes__get_pods: null # de-escalate to auto-execute services__restart_service: approve # override regardless of resolve_humanoverrides keys are skillname__toolname. Valid values: null, "approve", "confirm".
Overrides take effect after resolve_human — they are the final word on approval level.
verifier
Section titled “verifier”Post-turn fact-checker. After each agent turn, a secondary LLM call checks that the agent’s response does not contradict what the tools actually returned.
verifier: model: claude-haiku-4-5 # cheap model; falls back to main LLM if omittedThe verifier only runs when at least one skill that has opted in via VERIFY = True (in tools.py) was called during the turn. No skill opts in by default — it is opt-in per skill. When a contradiction is detected, the agent’s response is annotated with a ⚠️ warning.
To opt a custom skill into verification, add to its tools.py:
VERIFY = Truelearning
Section titled “learning”Controls the agent’s memory and self-learning behaviors.
learning: enabled: true reflection: true # write a daily reflection after each session approval_auto_promote: 3 # approve N times with 0 denials → auto-execute memory: backend: filesystem filesystem: path: ./memory # root directory for memory filesreflection: true runs a background LLM call after each conversation and appends a brief summary to memory/daily/YYYY-MM-DD.md.
approval_auto_promote is the global default. Skills can override it in their own config (e.g., skills.config.shell.approval_auto_promote).
Persistent key-value store for session state.
state: backend: filesystem filesystem: path: ./.mithai/stateThe state backend stores session history and tool metadata. Don’t change the path unless you know what you’re doing — the agent won’t find past sessions if you move it.
mcp_servers
Section titled “mcp_servers”External Model Context Protocol servers. Skills declare which servers they use via MCP_TOOLS. The framework starts only the servers that are needed.
mcp_servers: linear: transport: sse url: https://mcp.linear.app/sse headers: Authorization: Bearer ${LINEAR_API_KEY}
github: transport: sse url: https://api.githubcopilot.com/mcp/ headers: Authorization: Bearer ${GITHUB_TOKEN}Set tools to restrict which of a server’s tools are exposed to the LLM — useful for servers with a large or sensitive tool surface. The default "*" exposes all discovered tools; with a list, the tool router filters before tools are offered to the model.
mcp_servers: last9: transport: streamablehttp url: ${LAST9_MCP_URL} tools: - get_alerts - get_service_summary - get_exceptionsagents (multi-agent mode)
Section titled “agents (multi-agent mode)”Run multiple independent agents from a single process. Each agent has its own adapter, skill set, system prompt, and memory.
agents: devops: name: "DevOps Agent" system_prompt: | You are a DevOps assistant. Focus on infrastructure and deployments. 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/triageWhen agents is present, the top-level adapter, skills, and llm blocks act as defaults inherited by each agent. Agent-level config overrides defaults.
Environment variables
Section titled “Environment variables”Any ${VAR} in config.yaml is substituted at load time from the process environment. mithai also loads .env in the working directory automatically.
ANTHROPIC_API_KEY=sk-ant-...SLACK_BOT_TOKEN=xoxb-...SLACK_APP_TOKEN=xapp-...If a referenced variable is not set, mithai logs a warning and substitutes an empty string.
CLI flags
Section titled “CLI flags”A subset of config values can be overridden with CLI flags:
mithai run --adapter slack # override adapter.typemithai run --config path/to/config.yaml # use a different config filemithai run --verbose # enable debug loggingmithai chat --agent devops # use a specific agent in multi-agent modeCLI flags take precedence over config.yaml.