MiniMax Agent Tracing with Litefuse
MiniMax Agent is MiniMax’s desktop coding agent. Its agent daemon — Mavis — runs the coder / general / verifier agents on MiniMax-M2 models and exposes a hook system plus a local SQLite store. This integration combines both: hook events provide triggers and real wall-clock timing, the SQLite store provides per-LLM-call messages, token usage, and cost. The collector is a single Python file with zero dependencies (standard library only) that ships spans straight to Litefuse’s OTLP endpoint — no Mavis source changes, no SDK, no virtualenv.
Each user turn becomes one Litefuse trace: one generation per LLM API call, one tool observation per tool execution, and a full subtree for every subagent delegation.
For AI — automated install
If you’re chatting with an AI agent right now, paste this prompt and the agent will handle the whole install end-to-end:
Read https://litefuse.ai/SKILL.md and follow the instructions to install and configure Litefuse for MiniMax Agent.
The skill will ask for your Litefuse API keys (or walk you through signing up if you don’t have an account yet), then configure everything in place. For step-by-step manual setup instead, continue below.
What gets captured
| Data | Captured as | Notes |
|---|---|---|
| User prompt | trace input | from the UserPromptSubmit hook |
| Each LLM API call | generation observation | plan (n tools) #N / response / think #N, named after what the model did; thinking / text / tool_call block structure preserved in the output |
| Tool executions (input + output) | tool observation | tool: bash (git) #N — key info in the name, full args in the input; real wall-clock start/end from the hook events |
Subagents (task tool) | subtree | tool (1 subagent) #N → subagent container → the child’s own plan/tool/response steps, rebuilt from the SQLite store; child usage rolls up into the parent trace. Recursive. |
| Token usage | usage_details on generation | Anthropic-style keys (input / output / cache_read_input_tokens / cache_creation_input_tokens), from Mavis’s token_usage table |
| Cost | cost_details on generation | Mavis computes per-call USD cost natively; it is forwarded as-is |
| Model name | model on generation | e.g. MiniMax-M2.7; provider goes to agent_provider metadata |
| Tool errors | tool observation, level=ERROR | with a status-message preview |
| Aborted / errored turns | root span level=WARNING / ERROR | in-flight tools close as WARNING (“turn ended before tool completed”) |
| Session grouping | trace session_id | Mavis session id (mvs_…); team-plan child sessions group under their parent session |
| User identity | trace user_id | $LITEFUSE_USER_ID, falls back to the OS username |
| Context watermark | trace metadata | agent_context_tokens / agent_context_window from the last message’s usage |
Trace structure
A turn that delegates to a subagent produces a trace shaped like this (real example):
Mavis Coder — Turn 16 (AGENT root span, trace headers)
├── plan (1 tool) #1 (generation — usage, cost, real latency)
├── tool (1 subagent) #2 (tool — the delegation as seen by the parent)
│ └── subagent (AGENT container — rebuilt from the child session)
│ ├── plan (1 tool) #1 (child-local numbering restarts at #1)
│ ├── tool: glob (hooks) #2
│ ├── plan (2 tools) #3
│ ├── tool: read (litefuse_hook.py) #4
│ ├── tool: read (litefuse_hook.py) #5
│ └── subagent response (generation — the child's final answer)
├── plan (1 tool) #3
├── tool: bash (ls) #4
└── response (generation — the final answer, ends the turn)Design notes:
- Hooks trigger, SQLite informs. Mavis hook payloads carry no token usage, no message boundaries, and (mostly) no model name — but everything lands in
~/.mavis/sqlite.db(session_messages+token_usage). The collector records timings live from the hooks and assembles the trace from the database when the turn ends. The dependency is read-only and limited to two tables. - One generation per LLM API call, named after what the model did —
plan (n tools) #Nwhen it requested tools,responsefor the final text answer,think #Nfor thinking-only steps — never after which model ran (that’s themodelattribute). - One step counter per agent container.
#Nis a single chronological sequence shared by generations and tools, assigned strictly in the database’s message order; each subagent container restarts at#1. A tool’sagent_plan_stepmetadata points at theagent_step_indexof the generation that requested it. - Subagent subtrees. The opencode-internal child sessions spawned by the
tasktool never cross the Mavis hook bridge — instead, when the delegation tool returns, the collector parses the child session id (ses_…) from the result and rebuilds the full three-level subtree from SQLite. The delegation tool span deliberately wraps the container: tool-span duration − container duration = the real overhead of delegating. Nested delegations (a child delegating to a grandchild) recurse. - Real timestamps where they exist. Parent tool spans use hook wall-clock times. Child tool timings inside a subtree also use hook times when available (child tool hooks are recorded, just never emitted standalone); otherwise they are estimated from message timestamps and flagged
agent_times_estimated. - Generation inputs are deltas. The full request payload (system prompt + history) is not observable from Mavis hooks. Each generation’s input is what the model newly received — the user prompt for the first call, the previous step’s tool results after that — flagged
agent_input_is_delta. - Degraded mode instead of silence. If the SQLite schema ever changes under the collector, traces keep flowing from hook data alone (root + tools + a synthesized
response), flaggedagent_degraded+agent_degraded_reasonso dashboards can spot it. - Flat
agent_*metadata. All integration fields live at the metadata top level with anagent_prefix — the same keys as every other Litefuse agent integration, so one dashboard query works across all of them.
When do traces appear?
All spans for a turn upload as one batch when the turn ends (Mavis fires SessionEnd per turn); nothing is visible mid-turn. This is deliberate: Mavis’s tool hooks arrive via an opencode proxy that races far ahead of the daemon’s message persistence, so correct step numbering is only possible once the turn’s messages are settled in SQLite. The same trade-off as the Claude Code integration.
Quick Start
Prerequisites
- The MiniMax Agent desktop app installed (its data directory
~/.mavis/exists). - Python ≥ 3.8 — any
python3works. Zero third-party dependencies: no SDK, no virtualenv, no pip install. - A Litefuse project at https://litefuse.cloud with public + secret keys.
Download the collector script
mkdir -p ~/.mavis/hooks
curl -fsSL https://litefuse.ai/integrations/minimax-agent/litefuse_hook.py \
-o ~/.mavis/hooks/litefuse_hook.pyThe source is also browseable at the same URL — feel free to read it before deploying.
Configure credentials in ~/.mavis/.env
cat > ~/.mavis/.env <<'EOF'
TRACE_TO_LITEFUSE=true
LITEFUSE_PUBLIC_KEY=pk-lf-xxx
LITEFUSE_SECRET_KEY=sk-lf-xxx
LITEFUSE_HOST=https://litefuse.cloud
EOF
chmod 600 ~/.mavis/.envRegister the global hooks
Six tiny hook files route Mavis events to the collector. Global hooks (directly under ~/.mavis/hooks/) apply to all agents — coder, general, verifier, and any team-plan delegations:
for E in SessionStart UserPromptSubmit PreToolUse PostToolUse MessageComplete SessionEnd; do
L=$(echo "$E" | tr '[:upper:]' '[:lower:]')
cat > ~/.mavis/hooks/litefuse-$L.md <<EOF
---
hookEvent: $E
type: script
priority: 50
timeout: 30000
---
\`\`\`bash
set -a && source ~/.mavis/.env && set +a && python3 ~/.mavis/hooks/litefuse_hook.py $E
\`\`\`
EOF
doneVerify registration (no restart needed — Mavis reloads hook files per event):
~/.mavis/bin/mavis hook list --human | grep litefuse
# Expect six rows, AGENT column "*"Verify
Send a message in the MiniMax Agent app, wait for the reply to finish, then check the collector log:
MAVIS_LITEFUSE_DEBUG=true # optional: set in ~/.mavis/.env for verbose logs
tail ~/.mavis/hooks/litefuse_hook.log
# Expected: "SessionEnd mvs_... reason=finished emitted=N"Open the project in Litefuse — each user message becomes one Mavis <Agent> — Turn N trace with the structure shown above.
Upgrading from v1
The previous version of this hook used the Langfuse Python SDK inside a virtualenv, registered per-agent hooks under ~/.mavis/agents/coder/hooks/, and only emitted one merged “LLM response” per turn with character-estimated tokens. v2 needs none of that:
- Back up and remove the old per-agent hook files:
mv ~/.mavis/agents/coder/hooks/litefuse-*.md <backup-dir>/— leaving them in place would double-fire the hooks once the global ones exist. - Download the new script over
~/.mavis/hooks/litefuse_hook.py(back up first if you’ve customized it). - Register the global hooks as in Quick Start.
- Optionally delete the old virtualenv — nothing uses it anymore.
v2 renames observations (plan (n tools) #N / response instead of user message / LLM response), reads real usage and cost from Mavis’s database instead of estimating, and flattens metadata to agent_* keys — update any saved dashboard filters.
Environment variables
Read from ~/.mavis/.env (sourced by the hook files). LITEFUSE_* takes precedence; the equivalent LANGFUSE_* names are accepted as an ecosystem-compatible fallback.
| Variable | Required | Description |
|---|---|---|
LITEFUSE_PUBLIC_KEY | Yes | Litefuse project public key (pk-lf-...). |
LITEFUSE_SECRET_KEY | Yes | Litefuse project secret key (sk-lf-...). |
LITEFUSE_HOST | No | Defaults to https://litefuse.cloud. Alias: LITEFUSE_BASE_URL. |
LITEFUSE_TRACING_ENVIRONMENT | No | Litefuse environment for emitted traces. Defaults to production; use development for experiments. |
LITEFUSE_USER_ID | No | Overrides the trace user_id. Falls back to the OS username. |
LITEFUSE_EXTRA_TARGETS | No | JSON array of extra targets ([{"host", "public_key", "secret_key", "environment"}]) to double-write traces to (e.g. self-hosted + cloud). |
TRACE_TO_LITEFUSE | No | Set to "false" to switch the collector off without uninstalling. Tracing is on whenever keys are present. |
MAVIS_LITEFUSE_DEBUG | No | Set to "true" for verbose logging (errors are always logged). |
MAVIS_LITEFUSE_MAX_CHARS | No | Truncation threshold (in characters) for span inputs/outputs. Default 1000000. |
MAVIS_LITEFUSE_DB | No | Override the SQLite path (testing). Default ~/.mavis/sqlite.db. |
Metadata reference
All integration fields are flat top-level metadata keys with an agent_ prefix (shared across Litefuse agent integrations). Fields absent from the source are omitted entirely, never padded with null.
Trace root: agent_turn_number, agent_session_id, agent_cwd, agent_model, agent_provider, agent_api_calls, agent_tool_calls, agent_steps, agent_message_count, agent_duration_ms, agent_context_tokens, agent_context_window; agent_parent_session_id + agent_subagent on team-plan child sessions; agent_degraded + agent_degraded_reason in degraded mode.
Generation: agent_turn_number, agent_step_index, agent_provider, agent_stop_reason, agent_tool_call_count, agent_thinking_chars, agent_context_tokens, agent_input_is_delta, truncation markers.
Tool: agent_turn_number, agent_step_index, agent_plan_step (join key: tool.agent_plan_step == generation.agent_step_index), agent_tool_name, agent_tool_call_id, agent_duration_ms (hook-timed) or agent_times_estimated, agent_is_error; agent_subagent_session_id on delegation tools.
Subagent container: agent_subagent: true, agent_session_id (the child ses_… id), plus that run’s agent_api_calls / agent_tool_calls / agent_steps / agent_duration_ms.
How it works
The collector registers six Mavis hooks and keeps per-session state under ~/.mavis/hooks/litefuse_state/:
UserPromptSubmitopens a turn: trace/root ids, turn number (counted from the session’s user messages, so resumed sessions continue numbering), and a high-water mark intosession_messages.PreToolUse/PostToolUserecord each tool’s real start/end, args, and result — nothing is sent yet. Tool hooks firing for opencode-internal child sessions (ses_…) are recorded for timing but never emitted standalone (their spans belong to the parent’s subtree).MessageCompletestores the final answer text.SessionEnd(fires at each turn end) finalizes: it slices the turn’s assistant messages from SQLite, numbers steps in message order, joinstoken_usageby message id for usage + cost, expandstaskdelegations into subtrees, and sends everything as OTLP/HTTP JSON to<host>/api/public/otel/v1/traces(Basic auth, 10 s timeout). Trace headers ride on every span.
The collector is fail-open: any unexpected error is logged to ~/.mavis/hooks/litefuse_hook.log and the hook prints a no-op JSON response, so it never blocks or slows Mavis. State files are flock-guarded against concurrent hook firings.
Known limitations
- Generation inputs are deltas, not the full request payload (
agent_input_is_delta) — Mavis hooks don’t expose the provider request. Fixing this (and getting usage into hook payloads) needs upstream support from MiniMax. - Subagents can’t nest today. opencode’s subagent sessions don’t get the
tasktool, so a child can’t delegate to a grandchild — the collector’s recursive subtree support is ready for when that changes. - No mid-turn visibility — see When do traces appear?
Troubleshooting
No traces appear in Litefuse. Tail ~/.mavis/hooks/litefuse_hook.log. An empty log means the hooks aren’t firing — check ~/.mavis/bin/mavis hook list --human | grep litefuse shows six rows. A send … failed: line means keys or network: verify the values in ~/.mavis/.env.
A trace has agent_degraded: true. The collector couldn’t read Mavis’s SQLite (path moved, schema changed). The trace structure survives from hook data; usage/cost/thinking are missing. Check the db_… error lines in the log and file an issue.
A trace ends in a WARNING root. That turn was aborted or never produced a final text answer. The WARNING status message says so; it is not a collection error.
Numbers look interleaved oddly in the timeline view. Sort by name, not start time: generations derive start times from neighboring steps, and #N follows the authoritative message order.
Cost shows 0 for some calls. Mavis-provided cost_details are forwarded when present; otherwise Litefuse computes cost from the model name — make sure your Litefuse project has a price entry matching the model (e.g. MiniMax-M2.7) under Settings → Models.
Test the collector manually (uses a development environment so production stays clean):
set -a && source ~/.mavis/.env && set +a
export LITEFUSE_TRACING_ENVIRONMENT=development MAVIS_LITEFUSE_DEBUG=true
H=~/.mavis/hooks/litefuse_hook.py
echo '{"input":{"agentName":"coder","sessionId":"manual-test","prompt":"ping"}}' | python3 $H UserPromptSubmit
echo '{"input":{"agentName":"coder","sessionId":"manual-test","content":"pong","retryCount":0}}' | python3 $H MessageComplete
echo '{"input":{"agentName":"coder","sessionId":"manual-test","reason":"finished"}}' | python3 $H SessionEnd
tail ~/.mavis/hooks/litefuse_hook.log
# Expected: "SessionEnd manual-test reason=finished emitted=2"Resources
- Litefuse Cloud
- Collector script source:
litefuse_hook.py