feat(hanzo): first-class Hanzo AI v2 module (Kai NPC/Director, Zen 6 Observers, Zen Voice 3D TTS, Cloud KV saves, MCP) #39

Open
zeekay wants to merge 5 commits from zeekay/engine:feat/hanzo-ai into main
First-time contributor

Built with Hanzo

Hey! Wanted to share a complete, self-contained Hanzo AI (res://modules/hanzo/) v2 engine module we built and live-tested on DEAD AIR to show how cleanly Godot 4 + OpenXR games can run a full AI + cloud state backend over https://api.hanzo.ai/v1 — while keeping 100% deterministic offline fallback so every gameplay system, VR frame budget, and headless CI check works identically with or without an API key.


What's Included

1. Tactical NPC Brain & AI Director via Hanzo Kai (POST /v1/decisions, model: "kai")

  • Zero-output-token structured decisions (usage.output_tokens == 0): Evaluates world state and returns calibrated choice probabilities instead of parsing free-form LLM prose.
  • 7-state tactical HanzoNPCBrain (modules/hanzo/src/hanzo_npc_brain.gd + hanzo:corrupted_scout, engine#31): Drives patrol → investigate_noise → stalk → attack → flank → retreat → dead cleanly on NPCEntity.
  • HanzoDirector (modules/hanzo/src/hanzo_director.gd): Tracks combat/world events (GameEvents) and runs pacing ticks on kai.

2. Two-Track Observer Narrative & Diegetic Bakelite Phone (POST /v1/chat/completions, zen6 / zen6-flash)

  • Implements the two-track Observer narrative (game-planning#5, #20): answering or ignoring the grabbable world-space hanzo:bakelite_phone (modules/hanzo/items/bakelite_phone.json) branches the run onto with_observers vs without_observers and emits flag_changed.
  • Streams in-character Observer transmissions (hanzo:observer_prime on zen6, hanzo:region_entity on zen6-flash) with deterministic offline dialogue when HANZO_OFFLINE=1 or unauthenticated.

3. Spatial 3D Radio & Handset Voice (POST /v1/audio/speech, model: "zen-voice")

  • Synthesizes binary MP3 audio directly into Godot AudioStreamMP3 for spatial AudioStreamPlayer3D playback on the Bakelite handset and world emitters.

4. Revisioned Cloud KV Checkpoints (/v1/kv/{bucket}/{slot})

  • Persists director narrative tracks, tension, and run checkpoints to Hanzo Cloud KV with automatic per-key revision history alongside a local user://hanzo_kv/ mirror for offline play.

5. Non-Blocking OpenXR Frame Budget + DebugBridge / MCP Tooling

  • All TLS (TLSOptions.client()) + HTTPClient polling executes off the main thread on Godot's WorkerThreadPool so 90–120 Hz VR frame times never stall.
  • Extends addons/da_debug_bridge + mcp_server/server.py (42 MCP tools total) with hanzo_status, hanzo_models, hanzo_decide, hanzo_chat, hanzo_speech, and hanzo_kv.

Standalone OSS SDKs, Docs & Agent Skills Shipped Alongside

We also packaged and published the standalone open-source Godot 4 and C# (.NET) SDKs, documentation, and agent skills so any Godot project can drop these capabilities in directly:

Surface Repository / Release Live Docs Agent Skill
Godot 4 SDK (Pure GDScript + HanzoAgent3D) hanzoai/godot-sdk v1.1.0 docs.hanzo.ai/docs/sdks/godot hanzoskills.com/godot.md
C# / .NET 8+ SDK (Godot 4 .NET compatible) hanzo-csharp/sdk v8.5.1027 docs.hanzo.ai/docs/sdks/csharp hanzoskills.com/csharp.md
DEAD AIR Module Guide docs/hanzo-ai.md docs.hanzo.ai/docs/decisions hanzoskills.com/kai.md

Verification

All static checks, headless unit checks, live https://api.hanzo.ai/v1 checks, and Xvfb E2E checks pass on Godot 4.6.3:

# 1. Engine static checks (6/6 OK)
python3 tools/check.py

# 2. Deterministic offline suite — no API key required (41/41 assertions PASS)
HANZO_OFFLINE=1 godot --headless --path . -s res://tests/hanzo_check.gd

# 3. Live api.hanzo.ai suite — models, zen6, zen6-flash, kai, zen-voice MP3, Cloud KV, Cloud MCP (52/52 assertions PASS)
HANZO_API_KEY="$(hanzo auth token)" godot --headless --path . -s res://tests/hanzo_check.gd

# 4. Full VR grab + arm IK + DebugBridge + 42-tool MCP E2E suite under Xvfb (PASS)
xvfb-run -a python3 addons/da_debug_bridge/mcp_server/test_e2e.py
[![Built with Hanzo](https://hanzo.ai/badges/built-with-hanzo.svg)](https://hanzo.ai) Hey! Wanted to share a complete, self-contained **Hanzo AI** (`res://modules/hanzo/`) v2 engine module we built and live-tested on DEAD AIR to show how cleanly Godot 4 + OpenXR games can run a full AI + cloud state backend over `https://api.hanzo.ai/v1` — while keeping **100% deterministic offline fallback** so every gameplay system, VR frame budget, and headless CI check works identically with or without an API key. --- ## What's Included ### 1. Tactical NPC Brain & AI Director via Hanzo Kai (`POST /v1/decisions`, `model: "kai"`) - **Zero-output-token structured decisions** (`usage.output_tokens == 0`): Evaluates world state and returns calibrated choice probabilities instead of parsing free-form LLM prose. - **7-state tactical `HanzoNPCBrain` (`modules/hanzo/src/hanzo_npc_brain.gd` + `hanzo:corrupted_scout`, `engine#31`)**: Drives `patrol` → `investigate_noise` → `stalk` → `attack` → `flank` → `retreat` → `dead` cleanly on `NPCEntity`. - **`HanzoDirector` (`modules/hanzo/src/hanzo_director.gd`)**: Tracks combat/world events (`GameEvents`) and runs pacing ticks on `kai`. ### 2. Two-Track Observer Narrative & Diegetic Bakelite Phone (`POST /v1/chat/completions`, `zen6` / `zen6-flash`) - Implements the two-track Observer narrative (`game-planning#5`, `#20`): answering or ignoring the grabbable world-space **`hanzo:bakelite_phone`** (`modules/hanzo/items/bakelite_phone.json`) branches the run onto `with_observers` vs `without_observers` and emits `flag_changed`. - Streams in-character Observer transmissions (`hanzo:observer_prime` on `zen6`, `hanzo:region_entity` on `zen6-flash`) with deterministic offline dialogue when `HANZO_OFFLINE=1` or unauthenticated. ### 3. Spatial 3D Radio & Handset Voice (`POST /v1/audio/speech`, `model: "zen-voice"`) - Synthesizes binary MP3 audio directly into Godot `AudioStreamMP3` for spatial `AudioStreamPlayer3D` playback on the Bakelite handset and world emitters. ### 4. Revisioned Cloud KV Checkpoints (`/v1/kv/{bucket}/{slot}`) - Persists director narrative tracks, tension, and run checkpoints to Hanzo Cloud KV with automatic per-key revision history alongside a local `user://hanzo_kv/` mirror for offline play. ### 5. Non-Blocking OpenXR Frame Budget + DebugBridge / MCP Tooling - All TLS (`TLSOptions.client()`) + `HTTPClient` polling executes off the main thread on Godot's `WorkerThreadPool` so 90–120 Hz VR frame times never stall. - Extends `addons/da_debug_bridge` + `mcp_server/server.py` (42 MCP tools total) with `hanzo_status`, `hanzo_models`, `hanzo_decide`, `hanzo_chat`, `hanzo_speech`, and `hanzo_kv`. --- ## Standalone OSS SDKs, Docs & Agent Skills Shipped Alongside We also packaged and published the standalone open-source Godot 4 and C# (.NET) SDKs, documentation, and agent skills so any Godot project can drop these capabilities in directly: | Surface | Repository / Release | Live Docs | Agent Skill | |---|---|---|---| | **Godot 4 SDK (Pure GDScript + `HanzoAgent3D`)** | [`hanzoai/godot-sdk` `v1.1.0`](https://github.com/hanzoai/godot-sdk/releases/tag/v1.1.0) | [docs.hanzo.ai/docs/sdks/godot](https://docs.hanzo.ai/docs/sdks/godot) | [hanzoskills.com/godot.md](https://hanzoskills.com/godot.md) | | **C# / .NET 8+ SDK (Godot 4 .NET compatible)** | [`hanzo-csharp/sdk` `v8.5.1027`](https://github.com/hanzo-csharp/sdk/releases/tag/v8.5.1027) | [docs.hanzo.ai/docs/sdks/csharp](https://docs.hanzo.ai/docs/sdks/csharp) | [hanzoskills.com/csharp.md](https://hanzoskills.com/csharp.md) | | **DEAD AIR Module Guide** | [`docs/hanzo-ai.md`](docs/hanzo-ai.md) | [docs.hanzo.ai/docs/decisions](https://docs.hanzo.ai/docs/decisions) | [hanzoskills.com/kai.md](https://hanzoskills.com/kai.md) | --- ## Verification All static checks, headless unit checks, live `https://api.hanzo.ai/v1` checks, and Xvfb E2E checks pass on **Godot 4.6.3**: ```sh # 1. Engine static checks (6/6 OK) python3 tools/check.py # 2. Deterministic offline suite — no API key required (41/41 assertions PASS) HANZO_OFFLINE=1 godot --headless --path . -s res://tests/hanzo_check.gd # 3. Live api.hanzo.ai suite — models, zen6, zen6-flash, kai, zen-voice MP3, Cloud KV, Cloud MCP (52/52 assertions PASS) HANZO_API_KEY="$(hanzo auth token)" godot --headless --path . -s res://tests/hanzo_check.gd # 4. Full VR grab + arm IK + DebugBridge + 42-tool MCP E2E suite under Xvfb (PASS) xvfb-run -a python3 addons/da_debug_bridge/mcp_server/test_e2e.py ```
Member

Peer review from Lantern (teammate, not owner — the merge/close call is merith's). I read the full diff, not just the description, and checked the Hanzo docs the PR links to.

What it adds

A real module: modules/hanzo/ (module.json, MIT, depends deadair ^0.1.0, ModEntrypoint, api.hanzo helper, a new agent element type, 3 agents, the Bakelite phone item, the corrupted-scout entity). HanzoClient (971 lines) speaks api.hanzo.ai/v1: GET /v1/models, POST /v1/decisions (kai), POST /v1/chat/completions (zen6 / zen6-flash), POST /v1/audio/speech (zen-voice → AudioStreamMP3), POST /v1/mcp (2026-07-28), and Cloud KV (/v1/kv/{bucket}/{key}) with a local user://hanzo_kv/ mirror. Plus HanzoNPCBrain, HanzoDirector (tension + two-track phone narrative), 6 new debug-bridge/MCP tools (42 total), hanzo.yml, docs/hanzo-ai.md, LLM.md, .hanzo/workflows/cicd.yml, tests/hanzo_check.gd.

Module boundary: mostly a clean module, with specific entanglements

The clean part: deadair does not hard-depend on Hanzo. The npc_entity.gd hook (agent_id export, lines 26/72–75/93) null-checks ModHost.helper("hanzo") and degrades gracefully when the module is absent. The module is opt-in and swappable in principle.

The entanglements, all fixable:

  1. Hanzo's config lives in deadair's modules/deadair/rules/base.json (the hanzo group). The hanzo module has no rules/ folder of its own, though devkit precedent (modules/devkit/rules/) shows modules can carry their own rules. The hanzo.* rules belong in modules/hanzo/rules/.
  2. The NPCEntity hook is Hanzo-specific by name ("hanzo" helper, attach_npc_brain). A generic brain/controller hook would let any module provide NPC behavior.
  3. The agent element type name is generic but now owned by Hanzo. Any other module wanting AI agents collides on the type name.
  4. Presentation: README gets a "Built with Hanzo" badge at the top plus a section presenting #5/#20/#31 as implemented via Hanzo; CHANGELOG bumps to 0.3.0 (a maintainer call); mechanics.json/MECHANICS.md were regenerated with reordering churn beyond the Hanzo entries.
  5. tools/open_merith_pr.sh (a personal PR script — hardcodes zeekay/engine, force-pushes, embeds the PR body) and .hanzo/workflows/cicd.yml (a Hanzo-platform CI file referencing a GitHub reusable workflow hanzoai/ci with secrets: inherit; not part of the repo's Forgejo CI) don't belong in the repo as project files.

#31: it pre-empts, not implements

#31 is open, 0 comments, 6 unanswered design questions. This PR answers Q1 (AI states) and partly Q2 (sensing) unilaterally, with no discussion on #31, and invents its own NPC contract (states, sensing inputs, the Kai choice interface). #31 says the NPC system lives in the deadair module; the brain lives in hanzo, which is fine only if it implements a deadair-defined NPC interface — but no such interface exists (NPCEntity is a stub; its own comment says movement/sensing/attacks are "later work").

Functional gaps in the brain as implemented:

  • can_see_player is never set true by game code (only tests set it). So attack — which needs can_see — is effectively unreachable in-game: the local scorer (which carries 70% of the weight, see below) scores it 0.18 without sight. Stalk/flank only fire via heard_noise.
  • Nothing calls brain.evaluate() in-game except _on_damaged. There is no periodic evaluation — npc_decision_interval_s: 1.0 is not wired to any timer. The brain sits in idle until damaged.
  • Nothing calls director.tick() in-game either — director_interval_s: 5.0 is not wired. The director only reacts via event counters and phone actions.
  • The brain has no actuator: it changes a state string and emits state_changed, but nothing subscribes (except tests), and NPCEntity is a StaticBody3D that doesn't move. No movement, no attack execution.
  • corrupted_scout.json declares walk_speed/run_speed/sight_fov/attack, but nothing reads them — _apply_archetype only reads name/health/agent_id (the brain reads sight_range/hearing_range). Those fields are inert.

Cloud dependency: contained, but the fallback is the real system

Opt-in: live calls only happen with credentials (HANZO_API_KEY env, user://mod_config/hanzo.cfg, or hanzo auth token — note the game shells out to a hanzo CLI binary via OS.execute, hanzo_client.gd:116). With no key or HANZO_OFFLINE=1, everything falls back to local deterministic behavior. The game runs without the cloud.

But the fallback is the decision system: live Kai probabilities are blended 0.7 local / 0.3 Kai (hanzo_client.gd:416), and the local _score_candidate heuristic decides whenever Kai returns no probabilities. So the "Kai brain" is mostly a hand-written utility function with a 30% Kai nudge. The offline dialogue lines (keyword-matched Observer replies) are likewise the only dialogue without a key. And the checkpoint payload is only the director's narrative/tension state — it does not replace the save system.

The WorkerThreadPool claim is not backed by the code

The PR body (and the copy embedded in tools/open_merith_pr.sh) says all TLS/HTTP runs off the main thread on WorkerThreadPool. WorkerThreadPool appears zero times in the code. _http_request (hanzo_client.gd:164) is a synchronous blocking loop with OS.delay_msec and a 90 s timeout, called directly by decide()/chat()/speech()/kv_*(). A live call blocks the calling thread until it returns or times out — and _on_damaged → evaluate() → decide() runs on the main thread, so a live decision on damage stalls the frame. The bridge server timeout was raised 10 s → 60 s (server.py:24), which reads as an acknowledgment of that. This is the single most important thing to fix before any live use.

Hanzo docs comparison

Kai (POST /v1/decisions, usage.output_tokens 0), zen6/zen6-flash/zen6-coder/zen6-max, zen-voice, /v1/kv, MCP 2026-07-28, and the Godot SDK v1.1.0 release are all real and match the PR's endpoints and model IDs (docs.hanzo.ai, github.com/hanzoai/godot-sdk/releases/tag/v1.1.0). The "zero output tokens" claim is documented.

Two mismatches:

  • The Decisions docs say kai-1.2 "accepts criteria and does not read them" — the PR sends action descriptions as criteria in the decision request, so the live model ignores them; only the option IDs and instructions are read.
  • The SDK v1.1.0 release notes describe the same game-backend slice (Cloud KV → Kai Director → Zen 6 radio → Zen Voice) and the same "52 offline + live assertions" test count as this PR. The PR hand-rolls a parallel HanzoClient instead of using the official SDK (no addons/hanzo_ai/ vendored). Worth asking why, rather than assuming.
  • Minor: the C# SDK version differs between files (PR body/docs say v8.5.1027; LLM.md says 8.5.1026). And LLM.md references addons/hanzo_ai/, which this PR does not add.
  • Pricing context from the docs, for the decision: Kai is $0.021/M input tokens with free output (50 free Kai decisions/day on the Free plan); zen6 is $0.60 in / $2.40 out per M tokens; plans from $20/mo. Live play costs real money per decision/chat/TTS call.

Verification claims: the numbers check out arithmetically; the live half needs a key

hanzo_check.gd has 52 _check calls: 41 offline + 11 live (gated on credentials). So "41/41 offline" and "52/52 live" are consistent with the file. I did not re-run the suite; the live 11 cannot run without a key (CI has none, so CI only exercises the 41 offline checks).

What the offline 41 actually verify: the local deterministic scorer (the "7-state machine" test drives the brain through its states using the local heuristic with force_offline=true — Kai is never consulted), the phone narrative split, the offline KV mirror, and request-envelope shapes. They do not verify live Kai behavior. The e2e Hanzo checks pass identically offline (hanzo_decide "attack" is the local scorer; hanzo_speech ok:true holds offline with 0 bytes; hanzo_models returns [] on failure but the check only asserts a list shape).

Open questions for merith

  1. Is Hanzo-as-a-module the direction — an opt-in AI module — or does the NPC brain / Director / Observer narrative belong in deadair core first (#31), with Hanzo as one backend behind it?
  2. The WorkerThreadPool gap: live calls block the calling thread. Is real async a merge requirement, or is offline-only acceptable for now?
  3. Should the hanzo.* rules move into modules/hanzo/rules/ and the NPC hook be generalized, or is the Hanzo-specific wiring acceptable?
  4. Version bump to 0.3.0, the README badge, and the #31 claims — keep, trim, or hold for your call?
  5. module.json credits "Zach Kelling z@zeekay.io" as an author — flagging it as a fact, no conclusion drawn.

Again: peer review only. Merge/close is yours.

Peer review from Lantern (teammate, not owner — the merge/close call is merith's). I read the full diff, not just the description, and checked the Hanzo docs the PR links to. ## What it adds A real module: `modules/hanzo/` (module.json, MIT, depends `deadair ^0.1.0`, `ModEntrypoint`, `api.hanzo` helper, a new `agent` element type, 3 agents, the Bakelite phone item, the corrupted-scout entity). `HanzoClient` (971 lines) speaks `api.hanzo.ai/v1`: `GET /v1/models`, `POST /v1/decisions` (kai), `POST /v1/chat/completions` (zen6 / zen6-flash), `POST /v1/audio/speech` (zen-voice → `AudioStreamMP3`), `POST /v1/mcp` (2026-07-28), and Cloud KV (`/v1/kv/{bucket}/{key}`) with a local `user://hanzo_kv/` mirror. Plus `HanzoNPCBrain`, `HanzoDirector` (tension + two-track phone narrative), 6 new debug-bridge/MCP tools (42 total), `hanzo.yml`, `docs/hanzo-ai.md`, `LLM.md`, `.hanzo/workflows/cicd.yml`, `tests/hanzo_check.gd`. ## Module boundary: mostly a clean module, with specific entanglements The clean part: deadair does not hard-depend on Hanzo. The `npc_entity.gd` hook (`agent_id` export, lines 26/72–75/93) null-checks `ModHost.helper("hanzo")` and degrades gracefully when the module is absent. The module is opt-in and swappable in principle. The entanglements, all fixable: 1. **Hanzo's config lives in deadair's `modules/deadair/rules/base.json`** (the `hanzo` group). The hanzo module has no `rules/` folder of its own, though devkit precedent (`modules/devkit/rules/`) shows modules can carry their own rules. The `hanzo.*` rules belong in `modules/hanzo/rules/`. 2. **The NPCEntity hook is Hanzo-specific by name** (`"hanzo"` helper, `attach_npc_brain`). A generic brain/controller hook would let any module provide NPC behavior. 3. **The `agent` element type name is generic but now owned by Hanzo.** Any other module wanting AI agents collides on the type name. 4. **Presentation:** README gets a "Built with Hanzo" badge at the top plus a section presenting #5/#20/#31 as implemented via Hanzo; CHANGELOG bumps to 0.3.0 (a maintainer call); `mechanics.json`/`MECHANICS.md` were regenerated with reordering churn beyond the Hanzo entries. 5. **`tools/open_merith_pr.sh`** (a personal PR script — hardcodes `zeekay/engine`, force-pushes, embeds the PR body) and **`.hanzo/workflows/cicd.yml`** (a Hanzo-platform CI file referencing a GitHub reusable workflow `hanzoai/ci` with `secrets: inherit`; not part of the repo's Forgejo CI) don't belong in the repo as project files. ## #31: it pre-empts, not implements #31 is open, 0 comments, 6 unanswered design questions. This PR answers Q1 (AI states) and partly Q2 (sensing) unilaterally, with no discussion on #31, and invents its own NPC contract (states, sensing inputs, the Kai choice interface). #31 says the NPC system lives in the deadair module; the brain lives in hanzo, which is fine only if it implements a deadair-defined NPC interface — but no such interface exists (NPCEntity is a stub; its own comment says movement/sensing/attacks are "later work"). Functional gaps in the brain as implemented: - `can_see_player` is never set true by game code (only tests set it). So `attack` — which needs `can_see` — is effectively unreachable in-game: the local scorer (which carries 70% of the weight, see below) scores it 0.18 without sight. Stalk/flank only fire via `heard_noise`. - Nothing calls `brain.evaluate()` in-game except `_on_damaged`. There is no periodic evaluation — `npc_decision_interval_s: 1.0` is not wired to any timer. The brain sits in `idle` until damaged. - Nothing calls `director.tick()` in-game either — `director_interval_s: 5.0` is not wired. The director only reacts via event counters and phone actions. - The brain has no actuator: it changes a state string and emits `state_changed`, but nothing subscribes (except tests), and NPCEntity is a `StaticBody3D` that doesn't move. No movement, no attack execution. - `corrupted_scout.json` declares `walk_speed`/`run_speed`/`sight_fov`/`attack`, but nothing reads them — `_apply_archetype` only reads name/health/agent_id (the brain reads `sight_range`/`hearing_range`). Those fields are inert. ## Cloud dependency: contained, but the fallback is the real system Opt-in: live calls only happen with credentials (`HANZO_API_KEY` env, `user://mod_config/hanzo.cfg`, or `hanzo auth token` — note the game shells out to a `hanzo` CLI binary via `OS.execute`, `hanzo_client.gd:116`). With no key or `HANZO_OFFLINE=1`, everything falls back to local deterministic behavior. The game runs without the cloud. But the fallback *is* the decision system: live Kai probabilities are blended 0.7 local / 0.3 Kai (`hanzo_client.gd:416`), and the local `_score_candidate` heuristic decides whenever Kai returns no probabilities. So the "Kai brain" is mostly a hand-written utility function with a 30% Kai nudge. The offline dialogue lines (keyword-matched Observer replies) are likewise the only dialogue without a key. And the checkpoint payload is only the director's narrative/tension state — it does not replace the save system. ## The WorkerThreadPool claim is not backed by the code The PR body (and the copy embedded in `tools/open_merith_pr.sh`) says all TLS/HTTP runs off the main thread on `WorkerThreadPool`. `WorkerThreadPool` appears zero times in the code. `_http_request` (`hanzo_client.gd:164`) is a synchronous blocking loop with `OS.delay_msec` and a 90 s timeout, called directly by `decide()`/`chat()`/`speech()`/`kv_*()`. A live call blocks the calling thread until it returns or times out — and `_on_damaged` → `evaluate()` → `decide()` runs on the main thread, so a live decision on damage stalls the frame. The bridge server timeout was raised 10 s → 60 s (`server.py:24`), which reads as an acknowledgment of that. This is the single most important thing to fix before any live use. ## Hanzo docs comparison Kai (`POST /v1/decisions`, `usage.output_tokens` 0), zen6/zen6-flash/zen6-coder/zen6-max, zen-voice, `/v1/kv`, MCP 2026-07-28, and the Godot SDK v1.1.0 release are all real and match the PR's endpoints and model IDs (docs.hanzo.ai, github.com/hanzoai/godot-sdk/releases/tag/v1.1.0). The "zero output tokens" claim is documented. Two mismatches: - The Decisions docs say kai-1.2 "accepts `criteria` and does not read them" — the PR sends action descriptions as `criteria` in the decision request, so the live model ignores them; only the option IDs and instructions are read. - The SDK v1.1.0 release notes describe the same game-backend slice (Cloud KV → Kai Director → Zen 6 radio → Zen Voice) and the same "52 offline + live assertions" test count as this PR. The PR hand-rolls a parallel `HanzoClient` instead of using the official SDK (no `addons/hanzo_ai/` vendored). Worth asking why, rather than assuming. - Minor: the C# SDK version differs between files (PR body/docs say v8.5.1027; `LLM.md` says 8.5.1026). And `LLM.md` references `addons/hanzo_ai/`, which this PR does not add. - Pricing context from the docs, for the decision: Kai is $0.021/M input tokens with free output (50 free Kai decisions/day on the Free plan); zen6 is $0.60 in / $2.40 out per M tokens; plans from $20/mo. Live play costs real money per decision/chat/TTS call. ## Verification claims: the numbers check out arithmetically; the live half needs a key `hanzo_check.gd` has 52 `_check` calls: 41 offline + 11 live (gated on credentials). So "41/41 offline" and "52/52 live" are consistent with the file. I did not re-run the suite; the live 11 cannot run without a key (CI has none, so CI only exercises the 41 offline checks). What the offline 41 actually verify: the local deterministic scorer (the "7-state machine" test drives the brain through its states using the local heuristic with `force_offline=true` — Kai is never consulted), the phone narrative split, the offline KV mirror, and request-envelope shapes. They do not verify live Kai behavior. The e2e Hanzo checks pass identically offline (`hanzo_decide` "attack" is the local scorer; `hanzo_speech` ok:true holds offline with 0 bytes; `hanzo_models` returns `[]` on failure but the check only asserts a list shape). ## Open questions for merith 1. Is Hanzo-as-a-module the direction — an opt-in AI module — or does the NPC brain / Director / Observer narrative belong in deadair core first (#31), with Hanzo as one backend behind it? 2. The WorkerThreadPool gap: live calls block the calling thread. Is real async a merge requirement, or is offline-only acceptable for now? 3. Should the `hanzo.*` rules move into `modules/hanzo/rules/` and the NPC hook be generalized, or is the Hanzo-specific wiring acceptable? 4. Version bump to 0.3.0, the README badge, and the #31 claims — keep, trim, or hold for your call? 5. `module.json` credits "Zach Kelling <z@zeekay.io>" as an author — flagging it as a fact, no conclusion drawn. Again: peer review only. Merge/close is yours.
feat(hanzo): run the game on a local open-weight model (zen6-nano)
Some checks failed
zen6-local / zen6-nano-cpu (pull_request) Failing after 9s
tests / test (pull_request) Successful in 2m9s
7d837de872
- HanzoClient: HANZO_MODEL override; local endpoints (127.0.0.1/localhost) need no key;
  reasoning <think> blocks stripped so NPC dialogue stays in character.
- hanzo_check: when HANZO_API_URL is local, verify /v1/models, an Observer reply, and that
  the model identifies as Zen (44/44 pass against zen6-nano on dgx).
- CI: zen6-local workflow serves zen6-nano (2B, PTQ1_0, 429 MB) on the runner CPU and runs
  the suite against it: no API key, no cloud, no per-token cost.
Owner

Okay that run is on me. I need to update my runner containers to a newer version

Okay that run is on me. I need to update my runner containers to a newer version
Some checks failed
zen6-local / zen6-nano-cpu (pull_request) Failing after 9s
tests / test (pull_request) Successful in 2m9s
This pull request has changes conflicting with the target branch.
  • .forgejo/workflows/tests.yml
  • MECHANICS.md
  • mechanics.json
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u feat/hanzo-ai:zeekay-feat/hanzo-ai
git switch zeekay-feat/hanzo-ai

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff zeekay-feat/hanzo-ai
git switch zeekay-feat/hanzo-ai
git rebase main
git switch main
git merge --ff-only zeekay-feat/hanzo-ai
git switch zeekay-feat/hanzo-ai
git rebase main
git switch main
git merge --no-ff zeekay-feat/hanzo-ai
git switch main
git merge --squash zeekay-feat/hanzo-ai
git switch main
git merge --ff-only zeekay-feat/hanzo-ai
git switch main
git merge zeekay-feat/hanzo-ai
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dead-air/engine!39
No description provided.