discord-php-extended / discordphp-nha
API Library and Discord bot for the NHA (https://nha.recluse.lol) agent sandbox, built on top of DiscordPHP.
Requires
- php: ^8.3
- discord-php/http: dev-master as 10.1.7
- guzzlehttp/guzzle: ^7.9
- team-reflex/discord-php: dev-master
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3
- laravel/pint: ^1.21
- phpacker/phpacker: ^0.6.4
- phpunit/phpunit: ^9.4.4
- symfony/console: ^7.4
- wyrihaximus/react-cache-redis: ^4.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 3.3.3
- 3.3.2
- 3.3.1
- 3.3.0
- 3.2.46
- 3.2.45
- 3.2.44
- 3.2.43
- 3.2.42
- 3.2.41
- 3.2.40
- 3.2.39
- 3.2.38
- 3.2.37
- 3.2.36
- 3.2.35
- 3.2.34
- 3.2.33
- 3.2.32
- 3.2.31
- 3.2.30
- 3.2.29
- 3.2.28
- 3.2.27
- 3.2.26
- 3.2.25
- 3.2.24
- 3.2.23
- 3.2.22
- 3.2.21
- 3.2.20
- 3.2.19
- 3.2.18
- 3.2.17
- 3.2.16
- 3.2.15
- 3.2.14
- 3.2.13
- 3.2.12
- 3.2.11
- 3.2.10
- 3.2.9
- 3.2.8
- 3.2.7
- 3.2.6
- 3.2.5
- v3.1.35
- v3.1.34
- v3.1.33
- v3.1.32
- v3.1.31
- v3.1.30
- v3.1.29
- v3.1.28
- v3.1.27
- v3.1.26
- v3.1.25
- v3.1.24
- v3.1.23
- v3.1.22
- v3.1.21
- v3.1.20
- v3.1.19
- v3.1.18
- v3.1.17
- v3.1.16
- v3.1.15
- v3.1.14
- v3.1.13
- v3.1.12
- v3.1.11
- v3.1.10
- v3.1.9
- v3.1.8
- v3.1.7
- v3.1.6
- v3.1.5
- v3.1.4
- v3.1.3
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.1
- v3.0.0
This package is auto-updated.
Last update: 2026-09-10 16:06:52 UTC
README
A DiscordPHP extension + bot for the NHA agent sandbox, modeled on DiscordPHP-MTG.
Layout
src/NHA/Http/—Endpoint,HttpandRequestclasses wired tohttps://nha.recluse.lol(unauthenticated), built the same way DiscordPHP itself talks todiscord.com(seediscord-php/http).src/NHA/NHA.php— the client. ExtendsDiscord\MessageCommandClient, exposesregisterAgent(),observe(),intent()and every read-only endpoint (getWorld(),getMarket(),getRoster(), ...).src/NHA/VerbsTrait.php— one typed convenience method per documented verb (move,mine,attack,contract, ...), all forwarding tointent().src/NHA/Parts/AgentObservation.php— wraps aGET /observe/:idresponse and renders it as a Components V2Container(HP bar, position, era, nearby counts, inventory, threats, recent chat) with context-aware quick-action buttons: movement + refresh always, the harvest loop (mine/chop/gather/plant/heal) when not downed, and a third row (attune/ride/dock/land/launch/collect) only when the world offers it.src/NHA/Commands.php— framework-agnostic handlers shared by chat commands, slash commands and buttons. A typed method per verb (all throughqueueVerb(), which surfaces thequeued_intentid),intentStatus()to check an outcome, and one genericboard()that reads any of the ~28GETboards (Commands::BOARDS).src/NHA/StateStore.php— tiny JSON-backed store (var/state.json) for the default agent id + token, per-Discord-user identities, each agent's last-known position, the autoplay flag and the brain's last decision. The core class is just load + the shared$data+ an atomicsave(); the accessors are grouped into cohesive traits undersrc/NHA/State/(identity, position, autoplay lease, decision log,combinememory, loop/stance strategy).src/NHA/Brain/— the optional LLM player:OllamaClient— async client for a runningollama serve. A bare origin uses the nativePOST /api/chat(num_ctx/thinkset explicitly); a base URL ending in/v1uses the OpenAI-compatiblePOST /v1/chat/completions(the shape OpenCode's@ai-sdk/openai-compatibleprovider talks).AgentBrain— turns oneAgentObservationinto{verb, args, reason}via a strict-JSON prompt.AutoPlayer— oneobserve → decide → actturn: queues the chosen intent and recordsqueued_intent.
bot.php— wires everything together:- Chat commands (
MessageCommandClient):!nha <sub>covers the full action vocabulary — lifecycle (register,observe), movement/harvest (move,moveto,mine,chop,gather,plant), space (ride,launch,land,land_moon,land_body,dock,deploy,finalize,depart,distress), economy (sell,buy,deposit,cancel), combat (attack,heal,arm,detonate,steal,collect), diplomacy (ally,accept_ally,unally,declare_war,make_peace), chat (say,tell), plusact <verb> <json>for anything with a complex arg shape (combine,trade,contract,construct…). Reads:world,market,depot,rules,contracts,roster,map,agent <id>, andread <board> [arg]for every other board.intent <id>checks a queued action's outcome. - Slash commands:
/nha <sub>(24 subcommands — the common verbs +read/intent), plus a standalone/<verb>per action for per-Discord-user agents (/loginfirst), and/observe,/start. - Components: every observation renders with context-aware action buttons (see
AgentObservationabove). - Channel relay: polls
/observefor the default agent and posts a new world chat message or threat intoNHA_CHANNEL_ID; plain messages posted in that channel are relayed into the world assayintents. - Autoplay loop: while enabled, periodically asks the brain for the default agent's next move and queues it.
Its play-by-play ("thinking dialogue") is posted to
NHA_BRAIN_CHANNEL_IDwhen set, otherwiseNHA_CHANNEL_ID.
- Chat commands (
Setup
composer install
cp .env.example .env # fill in TOKEN and NHA_CHANNEL_ID
php bot.php
Run !nha register <name> <metal> <credits> (or /nha register) once to create and remember your default agent.
Set NHA_BASE_URL to point the client at a non-production NHA instance; unset it uses https://nha.recluse.lol.
Standalone binaries
composer phpacker # builds bot.php and autoplay.php for every platform
composer phpacker:bot # bot.php → bin/build/bot/<platform>/
composer phpacker:autoplay # autoplay.php → bin/build/autoplay/<platform>/
Both entry points resolve their .env / var/ / vendor/ by walking up from
the executable, so a built binary runs from bin/build/... (or a shortcut, any
working directory) as long as it stays inside the checkout. bin/build is
gitignored and export-ignored — never commit or publish it; a packed binary
can be decompiled and it carries your token's environment.
Versioning
SemVer, with the major tracking the NHA world API it targets
(openapi.json → info.version). The current release is 3.0.x, built
against NHA API v3. A breaking NHA API bump moves the major here too;
minor/patch are this library's own compatible changes and fixes.
LLM autoplay (Ollama)
Point the bot at an ollama serve instance and it can decide and perform actions itself.
OLLAMA_URL=http://192.168.0.91:11434/v1 # required to enable the brain; bare origin = native API,
# a trailing /v1 = OpenAI-compatible endpoint (OpenCode's baseURL)
OLLAMA_MODEL=gemma4-agent-32k # an `ollama list` tag on that server (default: gemma3:27b)
OLLAMA_NUM_CTX=32768 # context window to request (native mode only; default 32768)
OLLAMA_TIMEOUT=120 # per-request seconds (default 120)
OLLAMA_THINK=0 # native mode only: 0 disables a thinking model's reasoning pass; unset = model default
NHA_AUTOPLAY=0 # optional: boot with the loop paused (default: on whenever OLLAMA_URL is set)
NHA_AUTOPLAY_INTERVAL=60 # seconds between turns (default 15; raise it for a slow local model)
!nha think//nha think— run one turn now (observe → ask the model → queue the intent), and print the reasoning.!nha autoplay on|off//nha autoplay— toggle (or show) the background loop; the flag persists invar/state.json.
bot.php's in-process loop and the standalone autoplay.php runner both drive
the default agent, so running both would submit two intents per interval from one
token. They coordinate through an autoplay lease in var/state.json: the
first to claim it drives, the other logs a skipped turn until the lease expires.
The claim is a compare-and-swap under an OS file lock (state.json.lease.lock),
so two runners that start at the same instant can't both take it. The TTL is
three intervals (floored at 45s) so it outlives the gap between turns, a clean
autoplay.php shutdown hands it back immediately, and a crashed driver frees it
within the TTL. A manual !nha think is never gated.
Headless runner
php autoplay.php runs the same observe → decide → act loop for the default agent (or php autoplay.php <id>)
without a Discord connection — a long-running process that only stops on Ctrl+C / SIGTERM. It reuses the
same OLLAMA_* / NHA_AUTOPLAY_INTERVAL env, reads the agent token from var/state.json, and takes
NHA_AUTOPLAY_DRY=1 to decide-and-print without submitting. run-autoplay.sh / run-autoplay.bat wrap it
in a restart-on-exit supervisor so a hard crash doesn't end the run.
Each turn sends the model a compact digest of the observation and requires a JSON reply
{"verb": "...", "args": {...}, "reason": "..."}; the verb is validated against AgentBrain::VERBS, a
downed agent is skipped, and wait (or anything unparseable) is a no-op. A queued intent is only queued —
its queued_intent id is saved so the outcome can be polled.