mesen-agent is a native, windowless emulator, debugger, and evidence-capture
toolkit designed for AI coding agents. It lets an agent compile a ROM, run it
for a deterministic number of frames, drive controllers, execute Lua,
break/step through machine code, inspect or mutate memory, compare states, and
collect screenshots, gameplay video, audio, traces, and coverage without ever
opening Mesen's graphical interface.
The project is especially useful for agentic NES development: an AI can change
assembly or C code, build a .nes, exercise the exact behavior it changed,
inspect zero-page state and CPU execution, and return machine-readable evidence
in a single terminal loop. The same CLI also supports the other consoles in
Mesen's emulation core.
Note
This is a focused fork of SourMesen/Mesen2
around Mesen 2.1.1. The upstream repository is archived; current GUI-focused
Mesen development continues at nesdev-org/MesenCE.
This fork deliberately publishes mesen-agent CLI binaries, not the Mesen GUI.
Linux and macOS:
curl -fsSL https://raw.githubusercontent.com/atonamy/mesen-agent/master/Headless/install.sh | shThe installer selects the current OS and CPU architecture, verifies the release
checksum, and installs the CLI and its matching runtime in $HOME/.local/bin.
Ensure that directory is on PATH:
export PATH="$HOME/.local/bin:$PATH"
mesen-agent --helpWindows PowerShell:
irm https://raw.githubusercontent.com/atonamy/mesen-agent/master/Headless/install.ps1 | iexThis installs to %USERPROFILE%\.local\bin. Add that directory to PATH, or
invoke mesen-agent.exe by absolute path.
You can also download an archive directly from
GitHub Releases.
Each release contains only mesen-agent, the matching Mesen core runtime,
licenses, and checksums—no Avalonia UI or .NET application.
The release asset must match the host operating system and CPU:
| Host | Release asset |
|---|---|
| macOS Apple silicon | mesen-agent-macos-arm64.tar.gz |
| macOS Intel | mesen-agent-macos-x64.tar.gz |
| Linux arm64 | mesen-agent-linux-arm64.tar.gz |
| Linux x86-64 | mesen-agent-linux-x64.tar.gz |
| Windows x86-64 | mesen-agent-windows-x64.zip |
The following commands download from GitHub Releases, verify the published checksum, and install into the location used by the agent skill.
macOS, automatically selecting Apple silicon or Intel:
INSTALL_DIR="$HOME/.local/bin"
WORK_DIR="$(mktemp -d)"
case "$(uname -m)" in
arm64|aarch64) ASSET=mesen-agent-macos-arm64.tar.gz ;;
x86_64|amd64) ASSET=mesen-agent-macos-x64.tar.gz ;;
*) echo "Unsupported macOS architecture: $(uname -m)" >&2; exit 1 ;;
esac
BASE_URL=https://github.com/atonamy/mesen-agent/releases/latest/download
curl -fL "$BASE_URL/$ASSET" -o "$WORK_DIR/$ASSET"
curl -fL "$BASE_URL/SHA256SUMS" -o "$WORK_DIR/SHA256SUMS"
(cd "$WORK_DIR" && grep " $ASSET$" SHA256SUMS | shasum -a 256 -c -)
tar -xzf "$WORK_DIR/$ASSET" -C "$WORK_DIR"
mkdir -p "$INSTALL_DIR"
install -m 755 "$WORK_DIR/mesen-agent" "$INSTALL_DIR/mesen-agent"
install -m 755 "$WORK_DIR/MesenCore.dylib" "$INSTALL_DIR/MesenCore.dylib"
"$INSTALL_DIR/mesen-agent" --helpLinux, automatically selecting arm64 or x86-64:
INSTALL_DIR="$HOME/.local/bin"
WORK_DIR="$(mktemp -d)"
case "$(uname -m)" in
arm64|aarch64) ASSET=mesen-agent-linux-arm64.tar.gz ;;
x86_64|amd64) ASSET=mesen-agent-linux-x64.tar.gz ;;
*) echo "Unsupported Linux architecture: $(uname -m)" >&2; exit 1 ;;
esac
BASE_URL=https://github.com/atonamy/mesen-agent/releases/latest/download
curl -fL "$BASE_URL/$ASSET" -o "$WORK_DIR/$ASSET"
curl -fL "$BASE_URL/SHA256SUMS" -o "$WORK_DIR/SHA256SUMS"
(cd "$WORK_DIR" && grep " $ASSET$" SHA256SUMS | sha256sum -c -)
tar -xzf "$WORK_DIR/$ASSET" -C "$WORK_DIR"
mkdir -p "$INSTALL_DIR"
install -m 755 "$WORK_DIR/mesen-agent" "$INSTALL_DIR/mesen-agent"
install -m 755 "$WORK_DIR/MesenCore.so" "$INSTALL_DIR/MesenCore.so"
"$INSTALL_DIR/mesen-agent" --helpWindows x64 PowerShell:
$Asset = "mesen-agent-windows-x64.zip"
$BaseUrl = "https://github.com/atonamy/mesen-agent/releases/latest/download"
$WorkDir = Join-Path ([IO.Path]::GetTempPath()) ("mesen-agent-" + [guid]::NewGuid())
$InstallDir = Join-Path $HOME ".local\bin"
New-Item -ItemType Directory -Force $WorkDir, $InstallDir | Out-Null
Invoke-WebRequest "$BaseUrl/$Asset" -OutFile "$WorkDir/$Asset"
Invoke-WebRequest "$BaseUrl/SHA256SUMS" -OutFile "$WorkDir/SHA256SUMS"
$ChecksumLine = Get-Content "$WorkDir/SHA256SUMS" |
Where-Object { $_ -match " $([regex]::Escape($Asset))$" }
if (-not $ChecksumLine) { throw "Checksum for $Asset was not published" }
$Expected = ($ChecksumLine -split '\s+')[0].ToLowerInvariant()
$Actual = (Get-FileHash "$WorkDir/$Asset" -Algorithm SHA256).Hash.ToLowerInvariant()
if ($Actual -ne $Expected) { throw "mesen-agent release checksum mismatch" }
Expand-Archive "$WorkDir/$Asset" -DestinationPath "$WorkDir/extracted" -Force
Copy-Item "$WorkDir/extracted/mesen-agent.exe" $InstallDir -Force
Copy-Item "$WorkDir/extracted/MesenCore.dll" $InstallDir -Force
& "$InstallDir/mesen-agent.exe" --helpAdd $HOME/.local/bin to the shell PATH after installation, but keep the
binary and matching MesenCore library together in that directory.
The repository uses the open Agent Skills layout, so the standard skills CLI discovers and installs it directly from GitHub:
First verify that GitHub discovery finds the skill, then install it globally for all supported coding agents:
npx --yes skills@latest add atonamy/mesen-agent --list
npx --yes skills@latest add atonamy/mesen-agent \
--skill mesen-agent --agent '*' --global --yesThe second command installs the canonical copy under
$HOME/.agents/skills/mesen-agent and wires supported clients such as Codex,
Claude Code, and Pi to it. Verify the registrations with:
npx --yes skills@latest list --global --jsonThe skill lives at skills/mesen-agent, follows the portable SKILL.md
convention, always instructs agents to invoke
$HOME/.local/bin/mesen-agent, and includes the full CLI contract plus advanced
debugging examples.
After installing the binary and skill, ask an agent to use the skill explicitly. These prompts are designed to produce commands, machine-readable evidence, and reproducible artifacts rather than opening a GUI.
Investigate why a game does not leave its title screen:
Use the mesen-agent skill and $HOME/.local/bin/mesen-agent. Do not open any GUI.
Run build/game.nes in an isolated home, press Start deterministically, inspect
NES RAM before and after the input, and use Lua inputPolled callbacks if the
simple schedule is insufficient. Save JSONL events, a final screenshot, AVI,
WAV, and state JSON. Explain the exact state transition or why it failed.
Find and prove a gameplay regression:
Use the mesen-agent skill to compare build/good.nes and build/bad.nes. Give each
run a separate --home directory and identical frame/input schedules. Compare
zero-page RAM, complete state JSON, screenshots, and traces. Locate the first
meaningful divergence and report exact commands and artifact paths.
Debug machine code at a suspected routine:
Use the mesen-agent skill on build/game.nes. Set an execute breakpoint at NES
CPU address $C010, run until it hits, step one instruction, then return the PC,
call stack, nearby disassembly with opcode bytes, relevant RAM, and a bounded
trace. Confirm the final summary and do not infer success only from exit code.
Create a state-aware Lua test:
Use the mesen-agent skill. Write and run a Lua test that reads NES internal RAM,
drives controller input from inputPolled, asserts the expected gameplay state,
checks a RAM write/read round trip, and exits nonzero on failure. Keep Lua IO and
network disabled unless the test genuinely requires them. Capture PNG, AVI,
WAV, save state, and JSONL evidence from $HOME/.local/bin/mesen-agent.
Traditional emulator automation often stops at “launch the game and press some keys.” That is not enough for autonomous debugging. An agent needs stable input, structured observations, bounded execution, deep debugger control, and durable artifacts it can compare between builds.
| Agent need | mesen-agent capability |
|---|---|
| Deterministic tests | Fixed relative frame budgets, isolated homes, explicit wall-clock watchdogs |
| Gameplay automation | Frame schedules and Lua state-dependent controller input |
| Program understanding | Breakpoints, stepping, disassembly, expressions, PC, call stacks, traces, CDL |
| State inspection | Memory reads/dumps, complete JSON state, save-state load/save |
| Controlled experiments | Memory writes, PC changes, Lua assertions, in-memory save-state round trips |
| Visual evidence | PNG screenshots and AVI/GIF gameplay capture |
| Audio evidence | Standalone WAV plus emulated audio in AVI |
| Reliable orchestration | JSON Lines stdout, explicit artifact paths, documented exit codes |
| Safe scripting | Lua IO/network disabled by default, callback watchdog, fail-fast errors |
| CI operation | No window, display server, host audio device, or interactive input backend |
An effective agent loop looks like this:
flowchart LR
A["Edit NES source"] --> B["Build ROM"]
B --> C["Run mesen-agent"]
C --> D{"Observe JSONL"}
D --> E["Memory and state"]
D --> F["Breakpoint and trace"]
D --> G["PNG / AVI / WAV"]
E --> H{"Expected behavior?"}
F --> H
G --> H
H -- "No" --> A
H -- "Yes" --> I["Report reproducible evidence"]
ROM format selects the console automatically. Some systems or games require
firmware placed in the selected --home directory.
| System | Typical ROMs | Example CPU/memory types |
|---|---|---|
| NES / Famicom | .nes, .fds, .nsf |
Nes, NesMemory, NesInternalRam, NesPrgRom |
| SNES / Super Famicom | .sfc, .smc |
Snes, SnesMemory, SnesWorkRam, SnesPrgRom |
| Game Boy / Game Boy Color | .gb, .gbc |
Gameboy, GameboyMemory, GameboyWorkRam |
| Game Boy Advance | .gba |
Gba, GbaMemory, GbaIntWorkRam, GbaVideoRam |
| PC Engine / TurboGrafx-16 | .pce and CD images |
Pce, PceMemory, PceWorkRam, PceVideoRam |
| Master System / Game Gear | .sms, .gg |
Sms, SmsMemory, SmsWorkRam, SmsPrgRom |
| WonderSwan | .ws, .wsc |
Ws, WsMemory, WsWorkRam, WsPrgRom |
Run --frames 0 --list-memory --pc on the actual ROM before assuming a memory
layout. Multi-CPU systems also require the correct --cpu for expressions,
stepping, disassembly, breakpoints, PC, and call-stack operations.
Inspect a newly built NES ROM without advancing emulation:
mesen-agent build/game.nes \
--home /tmp/mesen-game-home \
--frames 0 --list-memory --pcDrive Start, run 300 frames, inspect zero page, and capture state and media:
mesen-agent build/game.nes \
--home /tmp/mesen-game-home \
--frames 300 --timeout 30 \
--input '0:start@40-46' \
--read-memory NesInternalRam:0:256 \
--state-json artifacts/state.json \
--screenshot artifacts/final.png \
--video artifacts/gameplay.avi \
--audio artifacts/audio.wavStop on an execute breakpoint, step one operation, and inspect the result:
mesen-agent build/game.nes \
--home /tmp/mesen-break-home \
--frames 10000 \
--breakpoint 'NesMemory:0xC010:x' \
--step 1 --pc --callstack \
--disassemble 0xC000:32 \
--read-memory NesInternalRam:0x20:32Produce broad execution evidence:
mesen-agent build/game.nes \
--home /tmp/mesen-trace-home \
--frames 1800 \
--trace artifacts/trace.log \
--save-cdl NesPrgRom=artifacts/game.cdl \
--dump-memory NesInternalRam=artifacts/ram.bin \
--save-state artifacts/repro.mss--lua FILE loads Mesen debugger Lua before execution and is repeatable. Scripts
can observe memory/events, assert invariants, inject dynamic input, draw overlays,
take screenshots, create/load in-memory save states, rewind, step, or terminate
the run with emu.stop(code).
Example state-aware NES test:
local polls = 0
emu.addEventCallback(function()
local state = emu.read(0x0E, emu.memType.nesInternalRam)
local shouldStart = state == 0 and polls >= 40 and polls <= 46
emu.setInput({ start = shouldStart }, 0, 0)
polls = polls + 1
end, emu.eventType.inputPolled)
emu.addEventCallback(function()
local state = emu.read(0x0E, emu.memType.nesRam)
if polls == 80 then
assert(state == 2, "expected PLAY state")
emu.log("PLAY reached with valid NES RAM reads")
end
end, emu.eventType.endFrame)nesInternalRam is the canonical physical 2 KiB NES system RAM type;
nesRam is a compatibility alias for FCEUX-style automation. nesMemory
addresses the CPU-visible bus. Invalid memory enum names fail immediately rather
than silently reading memory type zero.
Although inputPolled is the most precise event for controller automation,
explicit values passed to emu.setInput() from frame or memory callbacks are
queued for the next poll, so the core cannot erase them before the ROM samples
the controller.
Lua is sandboxed by default:
- add
--lua-allow-ioonly for trusted scripts that needio,os, or siblingrequire()modules; - add
--lua-allow-networkonly for trusted scripts that need LuaSocket; - use
--lua-timeoutto bound each callback; - require
summary.ok == trueand everylua_log.error_count == 0before trusting script-derived conclusions.
Standard output is JSON Lines: one complete JSON object per line. Diagnostics and usage errors go to stderr; binary artifacts only go to requested files.
{"event":"loaded","rom":"/work/game.nes","console":"Nes","cpu":"Nes"}
{"event":"memory_read","type":"NesInternalRam","start":14,"length":1,"hex":"02"}
{"event":"screenshot_saved","file":"/work/final.png"}
{"event":"summary","ok":true,"reason":"frame_limit","frames_executed":80,"stop_code":0}| Exit | Meaning |
|---|---|
0 |
Successful run |
| script code | Value supplied to emu.stop(code) |
2 |
Invalid CLI arguments |
3 |
ROM, runtime, or artifact failure |
4 |
Overall wall-clock timeout |
5 |
Lua load, callback, assertion, or watchdog failure |
Do not treat every nonzero code as a crash: first inspect the final summary
and determine whether Lua intentionally requested that status.
The CLI is intentionally thin. It does not reimplement an emulator or maintain
a second debugger. It calls a narrow native agent ABI in the same MesenCore
used by the desktop application, then converts observations into stable JSONL.
flowchart TB
Agent["AI coding agent / CI / shell"]
CLI["mesen-agent\nargument parsing + JSONL + artifacts"]
ABI["Agent/debug/record ABI wrappers"]
Core["MesenCore"]
Consoles["NES · SNES · GB/GBC · GBA · PCE · SMS/GG · WS"]
Debugger["Debugger\nbreakpoints · steps · expressions · traces · CDL"]
Lua["Lua runtime\nevents · memory · input · assertions"]
Media["Video/audio pipeline\nPNG · AVI/GIF · WAV"]
Agent -->|"ROM + bounded command"| CLI
CLI --> ABI
ABI --> Core
Core --> Consoles
Core --> Debugger
Core --> Lua
Core --> Media
Debugger -->|"state and stops"| CLI
Lua -->|"logs and stop code"| CLI
Media -->|"requested files"| CLI
CLI -->|"JSON Lines"| Agent
Execution follows a bounded, observable lifecycle:
sequenceDiagram
participant A as Agent
participant C as mesen-agent
participant M as MesenCore
participant L as Lua scripts
A->>C: ROM, frame budget, input, debug requests
C->>M: Initialize headless defaults and load ROM
C->>L: Load scripts with sandbox/watchdog
loop Until frame limit, breakpoint, Lua stop, or timeout
C->>M: Resume emulation
M->>L: Emit memory/frame/input events
L-->>M: Assertions, input, memory changes
end
C->>M: Inspect debugger and serialize artifacts
C-->>A: JSONL events and final summary
The guiding rule is “reuse the core, minimize the fork.” The changes are focused at existing extension points instead of duplicating console implementations.
| Area | Change |
|---|---|
| Native frontend | Added Headless/AgentCli.cpp and native build targets; no Avalonia or .NET dependency |
| Native runtime | Agent builds omit unused SDL, X11, host-audio, and physical-input backends; capture stays inside MesenCore |
| Core ABI | Added small wrappers for frames, console/CPU discovery, state JSON, screenshots, Lua policy, and deterministic input schedules |
| Debugger | Exposed breakpoint, step, expression, PC, call-stack, disassembly, trace, CDL, and memory operations to the CLI |
| Lua reliability | Added fail-fast error counts, callback watchdog behavior, safe teardown, strict required memory arguments, and nesRam compatibility |
| Input timing | Added deterministic CLI schedules and queued off-poll Lua input so frame callbacks cannot lose controller changes |
| Media | Reused Mesen's renderer and recorders without initializing a window, host audio device, or interactive input backend |
| Video safety | Ensured the NES software video filter can render and capture frames without a GUI renderer |
| Automation | Added isolated homes, machine-readable summaries, complete state dumps, artifact paths, and bounded execution |
| Validation | Added a synthetic redistributable NES ROM and regression coverage for debug, state, Lua, input, sandboxing, and media |
| Distribution | Added agent-only macOS/Linux/Windows workflows, portable installers, checksums, and multi-agent skill packaging |
- No GUI window is created.
- No display server is required for capture.
- No host audio output device is opened; emulated audio can still be recorded.
- No keyboard, mouse, or physical controller backend is required.
- Maximum-speed execution is the default;
--no-max-speedopts into pacing. - Every independent run can use its own
--homedirectory. - A finite wall-clock timeout prevents wedged emulation or scripts.
- Lua IO and network capabilities are explicit trust-boundary expansions.
- Requested post-failure memory/state/screenshots remain available for diagnosis.
Linux and macOS need a C++17 compiler. The agent target deliberately excludes the desktop SDL/display/audio/input backends, and does not need SDL2 or the .NET SDK:
make agent -j4
bash Headless/tests/smoke.shWindows uses Visual Studio 2022:
msbuild Mesen.sln -m -p:Configuration=Release -p:Platform=x64 -t:HeadlessSee Headless/README.md for the detailed native CLI guide, COMPILING.md for platform prerequisites, and the skill CLI reference for the complete option/event contract.
The agent-only GitHub workflow builds and smoke-tests native release bundles on:
- macOS arm64 and Intel x64;
- Linux x64 and arm64;
- Windows x64.
Tags matching v* publish those verified bundles and SHA256SUMS as a GitHub
release. Workflow artifacts and release archives never contain the GUI app.
- Report reproducible CLI problems in Issues.
- Attach the exact command, ROM SHA-1 from the
loadedevent, finalsummary, stderr, operating system/architecture, and relevant artifacts. - Do not upload commercial ROMs or copyrighted firmware. Prefer a minimal homebrew reproduction or the synthetic test ROM generator.
- Pull requests should preserve headless operation and include a terminal-level
regression in
Headless/testswhen behavior changes.
Mesen and this fork are available under GPL-3.0-or-later. See LICENSE.
Copyright (C) 2014–2026 Sour and Mesen contributors. Agent-focused changes are maintained in this fork. This project is not affiliated with Nintendo or other console manufacturers.