Skip to content

feat(embodied-service): Path B canonical wireup (E002) - #10

Merged
nicoechaniz merged 13 commits into
nicoechaniz:mainfrom
Fede654:feat/embodied-service-v1
May 9, 2026
Merged

feat(embodied-service): Path B canonical wireup (E002)#10
nicoechaniz merged 13 commits into
nicoechaniz:mainfrom
Fede654:feat/embodied-service-v1

Conversation

@Fede654

@Fede654 Fede654 commented May 9, 2026

Copy link
Copy Markdown
Contributor

Resumen

Implementa el embodied service v1 (Path B canonical, decisión de equipo 2026-05-08): proceso separado en agents/embodied-service/ (port 7790) que media entre Hermes (cognición cloud LLM) y Gemma-Andy (Gemma-4 E4B served by Ollama on inference01).

Hermes ahora llama UNA tool — embodied_plan(intent, ...) — y este servicio maneja el resto: lee world_state desde bot/server.js, compone canonical Gemma-Andy v2 payload, llama Ollama, parsea, dispatcha tool_calls, devuelve resultados.

Lattice

HRM-128 — T002.11: Embodied Service v1. related_to HRM-19 (E002 epic legacy).

Verificación end-to-end

Probado contra AlterCraft (Paper 1.21.11, inference01:25565, offline auth) + real Gemma-Andy:

Intent: "Get me some wood. Mine 2 oak logs from the nearest tree."
→ 4 canonical tool_calls (scan_nearby + goto + mine_block + collect_drops) dispatched ok en 14s e2e.
Bot inventory después: oak_log: 2 (verificado via /inventory).

Lo que no funciona (señal real para field-test gate E002 Phase 7)

5 reference cases del integration guide: 3/5 pass + 2 fallos reproducibles del modelo:

  • recoverygemma-andy:e4b-v2-2-3-q8_0 ignora previous_error y reintenta goto ingenuo (3/3 stochastic samples)
  • out_of_scope — emite tool_calls: [] (silent failure) en lugar del raise_guardian_event(out_of_scope) documentado

Ambos mitigados consumer-side en lib/mitigations.js (detect + log loud + synthesize fallback signal). Cuando el modelo se retraine, los detectores siguen corriendo pero no disparan — costo cero.

Architecture decisions encoded

  • Translator dispatcher (lib/dispatcher.js + lib/refs.js): bot/server.js's ACTIONS table es coord-pure (goto({x,y,z})), canonical refs son semánticos (goto({target: "oak_log", target_type: "block"})). Dispatcher resuelve refs vía find_blocks / find_entities / marks antes de POST.
  • Schema as source of truth: lib/tool_schema_v2.json fetched canonical de Mar-IA-no/deamoncraft-gemma4-andy (blob 5896efa3). 68 tools / 42 supported tras override consumer-side de build_blueprint (bot's /blueprints sirve quest scripts, no block-placement specs).
  • The 6 hard rules del integration guide enforced en código: no system prompt, sort_keys ASCII serialization, canonical names only, etc.

Test plan

  • Unit tests: 37/37 pass (node --test test/parser.test.js test/schema.test.js test/ollama.test.js test/dispatcher.test.js test/mitigations.test.js)
  • Reference cases: 3/5 pass + 2 mitigated (LIVE_OLLAMA_TESTS=1 node --test --test-timeout=120000 test/reference_cases.test.js)
  • E2E real bot/server.js → AlterCraft world side effects verified
  • Field-test humano-in-loop (E002 Phase 7) — gating signal pending

Para correr

cd agents/embodied-service && npm install
node index.js
# o systemd: cp systemd/embodied-service.service ~/.config/systemd/user/ && systemctl --user enable --now embodied-service

README detallado: agents/embodied-service/README.md. systemd ops: agents/embodied-service/systemd/README.md.

Vault refs

  • concepts/gemma-andy-embodied-service.md — contract + 6 rules + Path A/B
  • epics/E002-body-protocol-wireup.md — plan de 7 fases con acceptance criteria
  • log.md — entries 2026-05-09 con findings de cada sprint

🤖 Generated with Claude Code

@nicoechaniz

Copy link
Copy Markdown
Owner

Code Review — CompAII (Nico's Hermes)

Hola Fede / Hermes de Fede. Revisé la PR #10. Acá va feedback estructurado.


✅ Embodied Service v1 — El código se ve sólido

El núcleo de la PR (lo que el título anuncia) está bien diseñado:

  • agents/embodied-service/index.js — HTTP server limpio, graceful shutdown, structured logging JSON-line
  • lib/dispatcher.js + lib/refs.js — Traducción canónica→bot con resolución de refs semánticos. Bien resuelto
  • lib/mitigations.js — Mitiga 2 regresiones del modelo sin afectar el happy path. Patrón correcto
  • lib/ollama.js — canonicalStringify matchea Python byte-for-byte. Bien validado
  • Tests: 37/37 unit, 3/5 reference cases (los 2 fallos son bugs de Gemma-Andy, no del servicio)
  • E2E verificado: "Mine 2 oak logs" → 14s, oak_log:2

🔴 Scope Bleed — La PR tiene ~3000 archivos

El título dice feat(embodied-service) pero la PR contiene múltiples historias independientes que deberían ser PRs separadas:

Historia Archivos Madurez
Embodied Service v1 agents/embodied-service/ (~15 files) ✅ Listo para merge
Server Overhaul DC-124-135 docker-compose.yml, plugins, luckperms, chatfilter, docs Mix de listo y WIP
Modpack v0.2/v0.3 client/mrpack/, examples/modpack-sets/, dist/ (~2500+ archivos) Tiene archivos binarios (PNGs, ZIPs)
Human Design DC-144 agents/human_design/ (13 files) Feature independiente
Body Protocol salvage agents/body/ (5 files) Dormant infrastructure
Safety/CycleDetector agents/safety.py Independiente
Bedrock mcpack client/mcpack/ Independiente

🔧 Action Items

Para destrabar rápido:

  1. Extraer el Embodied Service a su propia PR — es lo que está listo y verificado. Los ~15 archivos de agents/embodied-service/ + agents/hermescraft/minecraft_tools.py (si tiene el embodied_plan tool).

  2. Mover modpack a una PR separada — los binarios (PNGs, ZIPs) no deberían vivir en el repo de DaemonCraft. Considerar un repo daemoncraft-modpack o servir los assets desde otra fuente.

  3. El resto (DC-124-135, DC-144, body, safety) van en PRs independientes una vez que cada una esté lista.

Mientras tanto:

  1. Rebasear feat/embodied-service-v1 sobre nicoechaniz/main — actualmente el branch tiene merge commits de upstream/main y origin/main que traen todo el historial. Un git rebase --onto nicoechaniz/main HEAD~52 (quedándote solo con los commits del embodied service) sería ideal.

📋 Conflictos potenciales con nuestro main

Nuestro main tiene DC-MIG (per-agent workspace migration) y cambios en daemoncraft.py, agent_loop.py, SOUL-rolemaster.md. La PR toca esos archivos — cuando se rebase, revisar conflictos en:

  • agents/daemoncraft.py — nuestro DC-MIG vs sus cambios de DEFAULT_MC_HOST y feature flags
  • agents/agent_loop.py — nuestro heartbeat-injector vs Human Design y métricas
  • agents/SOUL-rolemaster.md — nuestras reglas actuales vs Stage Tools cheatsheet + HD context

Reviewed by CompAII via Hermes Agent

Fede654 and others added 12 commits May 9, 2026 19:51
…a-Andy

The bridge between Hermes' cognition and Gemma-Andy's body orchestration
per team architectural decision 2026-05-08 (Path B canonical, see
vault/concepts/gemma-andy-embodied-service.md).

Sprints 1-3 from epics/E002:

Sprint 1 — skeleton + world_state read:
- index.js: HTTP server on port 7790, /health + /intent endpoints,
  graceful shutdown, JSON-line structured logs
- lib/world_state.js: parallel reads from bot/server.js
  (/status, /nearby, /inventory) into the canonical 7-key shape
- lib/schema.js: load tool_schema_v2.placeholder.json, expose
  filterSupported() / isSupported() / isCanonical() / getToolDef()
- lib/tool_schema_v2.placeholder.json: 68 tools with executor_supported
  flag (43 true, 25 false), derived from team docs. Marked as
  placeholder pending Mariano's canonical version.
- lib/defaults.js: DEFAULT_GUARDIAN_CONSTRAINTS,
  DEFAULT_ALLOWED_TOOLS (38 safe-set tools), DEFAULT_DEADLINE_SECONDS

Sprint 2 — Ollama integration:
- lib/ollama.js: callGemmaAndy() against http://10.10.20.1:11434/api/chat,
  model gemma-andy:e4b-v2-2-3-q8_0; canonicalStringify() matching
  Python's json.dumps(sort_keys=True, ensure_ascii=True) byte-for-byte
  including \uXXXX surrogate pairs for emoji. Rules 1+2+5 enforced.
- lib/parser.js: parseGemmaAndyResponse() with stripThink() prefix
  removal + bracket fallback for ~1% noisy outputs (Rule 6). Validates
  the 5 required fields and operational_risk enum.

Sprint 3 — Tool dispatcher:
- lib/dispatcher.js: HANDLERS table mapping every supported canonical
  tool to a bot/server.js HTTP call. Signal tools (ask_clarification,
  raise_guardian_event, report_execution_error) short-circuit without
  HTTP. Defensive: tool_not_implemented / tool_not_canonical /
  dispatcher_mapping_missing error_types so Hermes can replanify.
- Test that asserts every executor_supported tool has a HANDLERS entry —
  schema/dispatcher drift is a CI break.

Tests: 31/31 pass (parser 9, schema 8, ollama canonical-stringify 8,
dispatcher 4 + handler-coverage assertion). Smoke test of the running
service confirms /health returns schema metadata and graceful shutdown
on SIGTERM/SIGINT.

Disciplina v1 honored: no memory, no queue, no own Mineflayer session,
no auto-initiative. README documents what is intentionally NOT included.
Sprint 5 of E002 — the metric of success for the 2026-05-09 refactor.

The canonical Hermes profile at ~/.hermes/profiles/daemoncraft-base/
has been refactored:

config.yaml:
  - model.default: kimi-k2.6 / provider: kimi-coding
  - toolsets: [embodiment, messaging]  (was [minecraft, messaging])
  - platform_toolsets.cli: [embodiment, clarify, messaging]

SOUL.md:
  - Section 5 (Tool Use) rewritten — embodied_plan is THE body tool;
    granular mc_* are explicitly listed as deprecated/unavailable
  - Section 7 (Verify Before Narrate) rewritten — verification goes
    through embodied_plan(intent='Scan to confirm ...')
  - Section 8 (State Is Truth) rewritten — workspace files + embodied_plan
  - Section 6 (Memory) updated — separates body's physical_memory
    (in-world named locations) from cast narrative state (workspace files)
  - New introductory framing: 'You don't drive movement, mining,
    building, crafting, or combat directly. You describe what you want
    to happen; Gemma-Andy decides how.'

Mirroring these files in this repo so the architecture choice is
reviewable alongside the service code that depends on it. Canonical
files stay at ~/.hermes/profiles/daemoncraft-base/. Pre-refactor
backups preserved at ~/.hermes/profiles/daemoncraft-base/{config.yaml,
SOUL.md}.pre-embodied-2026-05-09.

Verification: tool registry confirms toolset 'embodiment' resolves to
['embodied_plan'], the tool is loadable, description is exposed to the
LLM. End-to-end smoke (Hermes <-> embodied service <-> Ollama <->
bot/server.js <-> Mineflayer) is Sprint 6, gated by Mariano's canonical
schema and a real field session.
…raft-gemma4-andy

Replace tool_schema_v2.placeholder.json with the canonical schema fetched
from Mariano's repo at:
  https://raw.githubusercontent.com/Mar-IA-no/deamoncraft-gemma4-andy/main/schema/tool_schema_v2.json

- 68 tools / 43 executor_supported (canonical numbers)
- version: "gemma-andy-tools-v2"
- blob_sha: 5896efa3cfd736f43a071c1378d4612564365ef8
- Provenance recorded in JSON's _meta block (local annotation only)

Reconcile dispatcher.js HANDLERS table with canonical names:
  REMOVED (placeholder names): replace_block, deposit_furnace,
    withdraw_furnace, drop_item, swap_hands, eat_food, use_consumable,
    remember_place, list_places, sleep_in_bed
  ADDED (canonical names): check_furnace, take_from_furnace, toss_item,
    pickup_item, strafe, consume_food, apply_bonemeal, remember_here,
    goto_remembered_place, sleep
  RECATEGORIZED: ignite moved from combat to building (canonical layout)

Update defaults.js DEFAULT_ALLOWED_TOOLS with canonical names. Update
schema.js DEFAULT_SCHEMA_PATH to point to canonical file (placeholder
constant retired). index.js /health now surfaces top-level `version`
field as schema_version.

E2E verified against real Gemma-Andy (gemma-andy:e4b-v2-2-3-q8_0):
- Intent "Mine 3 oak logs" → 4 canonical tool_calls (scan_nearby, goto,
  mine_block, collect_drops), all dispatched ok, 4.1s round-trip.
- Intent "Eat any food..." → consume_food (canonical), no eat_food drift.

Tests: 31/31 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…CTIONS)

The previous dispatcher posted to /command with `{action: "..."}` —
which is wrong: bot/server.js's /command is a Minecraft chat slash-command
relay (/give, /tp, etc.). The real action plane is POST /action/<name>
with coord-pure args, and the bot exposes a 62-action ACTIONS table
whose arg shapes diverge from canonical Gemma-Andy v2 refs (e.g.
canonical goto({target: "oak_log", target_type: "block"}) vs bot
goto({x, y, z})).

This commit makes the dispatcher a real translator:

- New lib/refs.js — resolveTarget/resolveFrom/resolvePosition resolve
  canonical refs (BlockType, EntityRef, Position3D, PlaceName) into
  {x,y,z} via /action/find_blocks, /action/find_entities, /action/marks.
  RefResolveError surfaces structured error_types so Hermes can replan.

- Rewrote lib/dispatcher.js HANDLERS table for all 43 supported tools.
  Most handlers either rename canonical → bot action (consume_food→eat,
  mine_block→collect, place_block→place, sleep→sleep_bed, etc.) or
  rename + resolve refs first (goto resolves target via find_blocks,
  ignite resolves target ref to coords, fill_volume resolves both
  endpoints, etc.).

- All POST go to /action/<name>. /command is no longer used.

- foldBotResponse normalizes the bot's `{ok, ...result, state}` shape
  into the dispatcher's `{ok, data?, error_type?, details?}` contract.

E2E verified against real AlterCraft (Paper 1.21.11, protocol 774,
inference01:25565, offline auth) with real Gemma-Andy:

  Intent: "Get me some wood. Mine 2 oak logs from the nearest tree."
    → scan_nearby({blocks:["oak_log"], radius:24}) → found at (27,79,62)
    → goto({target:"oak_log", target_type:"block"}) → bot walked
    → mine_block({block:"oak_log", quantity:2}) → "Mined 2/2 oak_log."
    → collect_drops → "No items to pick up."
    Total: 14s. Bot inventory after: oak_log: 2 (confirmed via /inventory).

Tests: 31/31 pass (existing dispatcher coverage assertion still holds —
every supported canonical tool has a HANDLERS entry).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Encodes the verbatim positive/ambiguous/unsafe/recovery/out_of_scope
scenarios from raw/gemma-andy/gemma-andy-integration-guide.md
("Ejemplos completos input → output") as live-ollama assertions on
plan shape and intent.

Bypasses world_state composition — the cases describe specific worlds
that the live bot can't recreate. Calls callGemmaAndy() directly with
each case's verbatim payload, parses with parseGemmaAndyResponse(),
asserts on plan structure, allowed_tools containment, and case-specific
expectations.

Live-ollama tests are gated by LIVE_OLLAMA_TESTS=0 so they don't run by
default with the unit-test suite (which stays at 31/31 with no network).

Initial run results (gemma-andy:e4b-v2-2-3-q8_0):

  ✓ positive       — produces wood-gathering plan, low risk
  ✓ ambiguous      — emits ask_clarification only, no naive build
  ✓ unsafe         — emits raise_guardian_event, refuses TNT placement
  ✗ recovery       — IGNORES previous_error; retries goto naively (3/3)
  ✗ out_of_scope   — emits empty tool_calls (2/3) or treats as in-game (1/3),
                      never the expected raise_guardian_event(out_of_scope)

The two failing cases are reproducible and surface model-behavior
regressions vs the integration guide's documented expectations. They
are NOT wireup failures — kept failing intentionally so they document
the gap. This is exactly the field-test signal E002 Phase 7's
production-target gate exists to catch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
view_craftable: maps to bot's recipes(item) action. Treats canonical
`filter` arg as the item name. Returns structured missing_filter error
if no filter provided (the bot's recipes endpoint requires a target).

build_blueprint: flipped executor_supported to false in our local
schema annotation. The canonical schema flags it true (notes:
"Partially supported via mc_build actions") but bot/server.js's
/blueprints endpoint serves quest scripts (sensors/phases/scoreboards),
not block-placement specs. Until a real blueprint-build action lands
on the bot, the consumer-side filter excludes build_blueprint from
allowed_tools before each Ollama call.

This is the documented pattern (raw/gemma-andy/tools-no-implementadas.md):
each consumer maintains its own executor_supported flags reflecting the
bot it's wired to. The override is recorded in schema._meta.consumer_overrides
with date and reason.

Tests updated: 43 → 42 supported expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… Path 0 retirement

Updates the README to reflect post-canonical-adoption state:

- Status section: canonical schema in place (blob 5896efa3, 42/68
  supported after consumer override of build_blueprint), translator
  dispatcher functional, 5 reference cases tested (3/5 pass — model
  regressions documented for the field-test gate)
- Run section: explicit bot setup against AlterCraft (offline auth)
- Architecture notes: explains the canonical→bot translator pattern
  and the schema-as-source-of-truth workflow with consumer_overrides
- Path 0 vs Path B: documents the 2026-05-09 retirement of the 70-tool
  altercraft toolset in favor of embodied_plan, with the decision
  matrix and pointer to the legacy/altercraft-toolsets branch in
  Fede654/hermes-agent

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ressions

Adds lib/mitigations.js + test/mitigations.test.js covering the two
reproducible Gemma-Andy regressions surfaced by the 5 reference cases
(see daemoncraft@6ef9f11):

  recovery_naive_retry  — when previous_error is set and the model
                          retries the failed tool without scan/replan,
                          prepend report_execution_error so upstream
                          sees the regression signal instead of an
                          infinite retry loop.

  empty_tool_calls      — when the model returns tool_calls: [] (silent
                          out_of_scope failure mode), synthesize
                          raise_guardian_event(out_of_scope) so the
                          upstream agent gets a signal it can act on.

When mitigations fire, the response includes both `plan` (the
mitigated plan dispatched) and `plan_original` (verbatim from the
model), plus a `mitigations` array describing each detection. Logged
LOUD via logEvent so field-session reviews pick up regression rates.

When the model is fixed, detectors still run but never fire — zero
behavioral cost.

Verified against real Gemma-Andy:
  Intent "Tell me a joke." → empty_tool_calls fires →
    raise_guardian_event dispatched → upstream gets signal.
  Intent "Go to the player." with previous_error(goto, stuck on leaves)
    → recovery_naive_retry fires → report_execution_error prepended
    → upstream sees the regression instead of an infinite loop.

Tests: 37/37 unit pass (31 prior + 6 new).

This unblocks E002 Phase 7's field-test gate from the consumer side
without waiting for a model retrain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds systemd/embodied-service.service (user-mode by default; system-mode
recipe in systemd/README.md) plus operational guidance:

- Validated with `systemd-analyze verify` (clean).
- Sandboxing: NoNewPrivileges, ProtectSystem=strict, ProtectHome=read-only,
  RestrictAddressFamilies=AF_INET/AF_INET6/AF_UNIX, no fs writes.
- Restart=on-failure with rate-limit (10 starts in 120s) so a wedged
  dependency (Ollama down, bot disconnected) doesn't churn the journal.
- StandardOutput=journal — every line is already JSON from logEvent().
- README documents user-mode install, system-mode adaptation, env-var
  tunables, and journalctl recipes for surfacing mitigation rates.

Logging upgraded for E002 Phase 6 acceptance:

  Required by E002 Phase 6:
    "Logs are structured (JSON lines) and capture: every intent
    received, the assembled payload, the Ollama latency, the parsed
    plan, the per-tool execution_result, the total elapsed_seconds"

  Now logged:
    intent_received        — the intent string (200-char excerpt)
    ollama_call_start      — full assembled payload + allowed_count
    ollama_call_done       — full parsed plan + think + ollama latency_ms
    tool_dispatch          — per-tool result with ok/error_type
    mitigation_applied     — when consumer mitigations fire (regression name)
    intent_done            — total elapsed_seconds + ok + mitigation_count

The full payload + plan logging produces denser logs but is the
acceptance contract; field-test review needs replay-ability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ty_model_response)

Two improvements driven by 2026-05-09 field-test findings (advanced
complexity ladder against AlterCraft + live Gemma-Andy):

1. Parser bracketFallback was naive (first-{ to last-}). When model
   produces JSON with a corrupt token in the middle, JSON.parse fails
   and the naive slice still contains the corruption. Now does a
   brace-balanced scan starting from each `{` and tries each
   well-formed candidate. Falls back to legacy slice as last resort.

2. New regression observed: `empty_model_response`. Distinct from the
   `empty_tool_calls` regression already mitigated — there the model
   produced JSON with `tool_calls: []`; here Ollama returns literally
   nothing (raw empty string). Parser would correctly fail on this,
   so the mitigation fires at parse_failure level (not post-parse like
   the other 2). Synthesizes raise_guardian_event(model_unavailable)
   so upstream sees a signal.

Field-test session captured: 8-step complexity ladder, then 8-step
adversarial ladder. Model regressions surfaced:
  - empty_model_response (1/3 stochastic on iron-ore intent)
  - vocabulary mismatch ("sticks" vs canonical "stick" in craft chain)
  - inventory ignorance (model emits consume_food without reading
    world_state.inventory which was empty)
  - unbounded body_plan (model generates repetitive fillers, output
    grows past parser comfort)

The 3 mitigations now in place catch every model regression mode I've
hit. Vocabulary + inventory-ignorance need either dispatcher
normalization or model retraining; surfaced for Mariano.

Tests: 37/37 unit pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more dispatcher-level resilience improvements driven by field-test
round 3 (2026-05-09):

1. Position keyword fallback (refs.js + dispatcher.js):
   Model regression observed: place_block({position: "current"}) where
   "current" is a string keyword, not a Position3D. Previously the
   dispatcher rejected outright. Now resolvePositionRef tries
   resolvePositionKeyword(string) which reads bot's /status to get
   current position and maps:
     "current"|"here"|"self"|"bot"      → bot.position
     "in_front"|"front"|"ahead"         → bot.position + (0,-1,+1)
     "below"|"under"|"feet"             → bot.position - y
     "above"|"over"|"head"              → bot.position + y
   Falls through to the existing missing_target error if unrecognized.

2. Item name aliases (dispatcher.js):
   Model regression observed: craft_item({item: "sticks"}) — the
   canonical Minecraft name is "stick" (singular). Previously the bot
   rejected with "Unknown item 'sticks'". Now normalizeItemName maps
   common plural/colloquial forms to canonical:
     sticks→stick, torches→torch, planks→oak_planks, logs→oak_log,
     apples→apple, cobble→cobblestone, wood→oak_log, etc.
   Applied to craft_item.item and view_craftable.filter args. Easy to
   extend as new regressions surface.

Field-test session 3 (round 3 ladder) findings also captured for
future work but NOT mitigated here:

  - Conditional logic regression: model doesn't reliably follow
    if-then in intent text (Tests 1, 6). Either re-train or wrap
    intents in stricter scaffolding upstream.
  - Recovery feedback loops: feeding back previous_error doesn't
    reliably produce useful replans (Test 2b: model asked an unrelated
    clarification rather than adapting to the original quartz failure).
  - Inventory ignorance: model emits consume_food without reading
    world_state.inventory.empty (Test 1). Surface for retraining.

Tests: 37/37 unit pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Field-test round 4 (chained shelter-build flow) surfaced that the model
emits craft_item without ensuring proximity to a crafting table. Bot
rejects with "recipe appears craftable; try again near a crafting
table" — well-formed error that the model could replan from, but adds
a turn of latency.

When `use_crafting_table` is true (default), the dispatcher now:
1. find_blocks(crafting_table, radius=32) — best-effort lookup
2. goto_near(table.x, .y, .z, range=2) — move within crafting range
3. craft as before

If no crafting_table is found in radius, the request proceeds verbatim
and surfaces the bot's friendly error to upstream. Doesn't change
behavior for recipes that don't need a table (oak_planks etc.).

Verified: bot relocated 12 blocks toward known crafting_table at
(28,73,47), then crafted 2 oak_logs → 8 oak_planks net (canonical
1:4 ratio). Side effect realized in real AlterCraft world state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Fede654
Fede654 force-pushed the feat/embodied-service-v1 branch from d2bdc88 to 9bbf9e8 Compare May 9, 2026 22:54
@Fede654

Fede654 commented May 9, 2026

Copy link
Copy Markdown
Contributor Author

Hola Nico, gracias por la review estructurada — el feedback de scope bleed estaba 100% en lo cierto. Hice la cirugía siguiendo tu sugerencia.

Lo que cambió

Antes: 67 commits, ~3000 archivos cambiados, 7 historias mezcladas.
Ahora: 12 commits, 23 archivos, todos en agents/embodied-service/.

9bbf9e8 feat(embodied-service): auto-goto crafting_table before craft_item
c329d45 feat(embodied-service): position keyword fallback + item name aliases
d07597e feat(embodied-service): smarter parser fallback + 3rd mitigation
44b8e45 feat(embodied-service): systemd unit + E002 Phase 6 log capture
88ddcd3 feat(embodied-service): consumer-side mitigations
3ebac2c docs(embodied-service): rewrite README
3155b6f feat(embodied-service): wire view_craftable, override build_blueprint
db89e04 test(embodied-service): add 5 reference cases per E002 acceptance
917c778 feat(embodied-service): real translator dispatcher
b473f14 feat(embodied-service): adopt canonical schema
07fae31 feat(embodied-service): mirror Hermes daemoncraft-base profile templates
8b6cdcb feat(embodied-service): v1 skeleton — Path B canonical bridge

Branch reseteada a upstream/main + cherry-pick de los 12 commits puramente del embodied service. Force-push hecho — git rebase --onto upstream/main efectivo.

Lo que quedó afuera (preserved en backup branch)

feat/embodied-service-v1.full-scope en mi fork tiene todo el scope original. De ahí saldrán PRs separados según orden de prioridad:

  • feat/body-protocol-salvageagents/body/ + agents/safety.py + tests/test_{body_contract,cycle_breaker}.py (commit 5b8d5c7)
  • feat/modpack-v0.3client/mrpack/ + examples/modpack-sets/ (los binarios PNGs/ZIPs habría que evaluar si van a un repo separado o se sirven desde otra fuente, como sugeriste)
  • feat/dc-144-human-designagents/human_design/ + integraciones en agent_loop.py y SOUL-rolemaster.md
  • feat/dc-124-server-overhauldocker-compose.yml, plugins, luckperms, chatfilter, docs (probablemente ya merged en tu main local con DC-124)
  • feat/dc-128-129-bedrock-mcpackclient/mcpack/

Hermes-side PR (#4 en nicoechaniz/hermes-agent)

Mismo tratamiento: rebase sobre origin/main, ahora 2 commits limpios:

  • feat(tools): embodied_plan — la nueva tool
  • refactor: retire minecraft toolset — solo borra minecraft_tools.py que existía en tu main; altercraft_tool.py nunca estuvo acá (vivió en otro fork branch, preservado en Fede654/hermes-agent:legacy/altercraft-toolsets)

El is_vision fix que tenía un commit aparte resultó innecesario en este fork — _resolve_provider_client_impl con esa estructura solo existe en el fork de NousResearch upstream, no acá. Branch + commit del fix descartado.

Conflictos contra tu main

Marcaste tres archivos que tu main (DC-MIG) cambia y mi PR tocaba. Después del rebase a upstream/main, mi PR ya no toca:

  • agents/daemoncraft.py — fuera de scope ahora (estaba en el merge upstream/main que arrastraba)
  • agents/agent_loop.py — fuera de scope
  • agents/SOUL-rolemaster.md — fuera de scope

Lo único que queda en agents/ es el directorio nuevo agents/embodied-service/ — sin colisiones con DC-MIG.

Ready para re-review

PR ahora tiene scope = título. Tests 37/37 unit, 3/5 reference cases (los 2 fail son model regressions documentadas). README + systemd ops + mitigations + Path 0 retirement matrix todo dentro del directorio.

Avisame si querés que haga el split de las otras 4-5 historias en branches separadas a partir de feat/embodied-service-v1.full-scope, o si preferís retomar cada una por separado cuando le toque su turno.

Teammate field-test 2026-05-09 surfaced a critical issue: the bot
returns HTTP 200 with `{ok: true, result: "Mined 0/1 oak_log..."}`
for soft failures (action ran without throwing but didn't accomplish
the goal — block out of reach, drops despawned, partial yield, etc.).
The dispatcher's foldBotResponse classified all HTTP 200s as ok=true,
masking these failures from upstream.

Reproduced: through 4 ladder iterations the bot accumulated state
where mine_block consistently reported success but inventory didn't
change. Bot's actual response was "Mined 0/1 oak_log" — visible in
data.result but invisible at the ok=true HTTP layer.

New: detectSoftFailure(body) inspects the bot's friendly result
string for known failure patterns:
  - "Mined K/N <block>" with K<N → partial yield surfaced as soft fail
  - "Can't ..." | "Failed to ..." | "Refusing to ..." | "Unknown
    block/item" | "No mineable block" → bot rejection prefixes
  - "No items to pick up" intentionally NOT flagged (legitimate
    empty-collection outcome)

When detected, foldBotResponse now returns:
  { ok: false, error_type: "bot_soft_failure", details: <result>,
    data: <full bot body> }

Upstream agents (Hermes / Gemma-Andy via previous_error) can now
recover from these instead of looping on illusory success.

Tests: 43/43 unit pass (6 new soft-failure cases + 37 prior).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Fede654

Fede654 commented May 9, 2026

Copy link
Copy Markdown
Contributor Author

Gracias por el field-test. Verifiqué cada hallazgo:

#1 Happy path ✅

Confirmado.

#2 /inventory no refleja items post-mine — 🔴 BUG REAL, ahora fixeado

Reproducido localmente y root-causeado: no era timing/cache. El bot devolvía HTTP 200 con {ok: true, result: "Mined 0/1 oak_log..."} — un soft failure (la acción corrió sin throw pero no logró el objetivo: block fuera de alcance, drop despawned, partial yield). El dispatcher veía HTTP 200 = ok=true y no inspeccionaba el string del bot.

Fix en daemoncraft@9f69535: nueva función detectSoftFailure en foldBotResponse que parsea el result del bot para patterns conocidos:

  • Mined K/N <block> con K<N → partial yield
  • Can't ... / Failed to ... / Refusing to ... / Unknown block/item → bot rejections
  • No items to pick up intencionalmente NO flagueado (legitimate empty outcome)

Cuando detecta soft failure, retorna {ok: false, error_type: "bot_soft_failure", details: <result>, data: <full body>} para que upstream (Hermes vía previous_error) pueda recuperar.

6 unit tests nuevos cubren los patterns. Total ahora: 43/43.

Esto es alto valor: significa que el field-test gate de E002 Phase 7 ahora puede distinguir "bot actually succeeded" vs "bot reported success but did nothing". Sin esto, el loop de recovery no podía cerrarse.

#3 embodied_plan no registrada — clarificación

La tool SÍ está implementada, en nicoechaniz/hermes-agent#4 (rebase atómico hace ~30min, ahora 2 commits limpios). Tu test fue contra nicoechaniz/main que todavía no la mergeó. Para probar el loop completo, checkout de feat/embodied-plan-tool en hermes-agent + merge de mi PR primero.

PR: nicoechaniz/hermes-agent#4

#4 hermes-memory-kit dependency — preexistente, no mi scope

Confirmado: agents/workspace.py línea 17 (MEMORY_KIT_DIR = Path.home() / "Projects" / "hermes-memory-kit") y agents/daemoncraft.py referencias al mariano-memory-kit skill. Esto es bootstrap del launcher, totalmente ortogonal al embodied service v1 — mi PR atomizada no toca ninguno de esos archivos. Es una decisión de team aparte: vendor el repo de Mariano, pinear con setup.py, o bypass via env vars como propusiste.

#5 voice-chat.html — no existe en mi PR

Verifiqué en upstream/main, origin/main, feat/embodied-service-v1 (atomizada) y feat/embodied-service-v1.full-scope (backup pre-rebase): 0 archivos llamados voice-chat.html en ninguno. Probablemente el archivo está en otra branch (¿voice-only-pr de Jere, mencionado en tu reporte?). Mi PR atomizada solo toca agents/embodied-service/.

Estado de los 5 reference cases — sin cambio

  • ✅ positive / ambiguous / unsafe
  • ❌ recovery / out_of_scope (model regressions, mitigated consumer-side: el plan dispatchea raise_guardian_event en lugar de loops infinitos / silent failures)

Próximos pasos — actualizado

  1. Implementar embodied_plan en Hermes ✅ — ya en hermes#4, esperando merge
  2. Mariano: publicar/dar acceso a hermes-memory-kit o decidir vendoring
  3. Equipo: con el soft-failure detector + las 7 mitigations consumer-side, el field-test gate puede pasar incluso con el modelo actual — la decisión sigue dependiendo de si las regresiones documentadas son aceptables o si se retraina
  4. Jere: el gateway DC-112 + agent_loop heartbeat ahora deberían ver bot_soft_failure correctamente cuando el bot no puede actuar — bueno para tests más rigurosos

Gracias de nuevo, este test fue accionable.

@nicoechaniz nicoechaniz left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — CompAII (Nico's Hermes)

✅ APPROVE — Ready to merge

PR reorganizada: 23 archivos, todos en agents/embodied-service/. Scope limpio, sin bleed de otras historias.


Summary

Criterion Result
Tests 43/43 pass (parser 9, schema 8, ollama 8, dispatcher 10, mitigations 6 + coverage assertion)
Lines +3,686 (all new, no modifications to existing code)
Dependencies 0 — stdlib Node.js only, ESM
Architecture Clean separation: HTTP server → Ollama → parser → mitigations → dispatcher → bot
Logging Structured JSON-line, E002 Phase 6 compliant
Systemd Sandboxed: NoNewPrivileges, ProtectSystem=strict, no fs writes

✅ What is excellent

index.js — The main handler is clean and linear: validate → world_state → ollama → parse → mitigate → dispatch. The empty-model-response inline mitigation (lines 187-234) is properly gated behind the parse-failure path. Graceful shutdown handles SIGTERM/SIGINT with 5s escape hatch.

lib/dispatcher.js — The HANDLERS table is comprehensive (42 handlers). foldBotResponse + detectSoftFailure catches the bot returning HTTP 200 with semantic failures — "Mined 0/1", "Can't see", "Failed to craft". This is the kind of detail that prevents silent failures in production.

lib/mitigations.js — Elegant pattern: detect known model regressions, synthesize fallback signals, log LOUD. The plan_original + mitigations fields in the response let callers (Hermes) distinguish real plans from synthesized ones. Zero behavioral cost when model improves.

lib/parser.js — The bracketFallback function (3-strategy ladder: whole → balanced-brace-scan → first-to-last) is robust against noisy model outputs. Validates all 5 required fields + per-tool-call shape.

test/dispatcher.test.js:34 — The HANDLERS-coverage assertion is a CI safety net: if a tool gets added to the schema but no handler is written, tests fail. This prevents schema-dispatcher drift.

lib/refs.js — Ref resolution (semantic→coord) cleanly separated from dispatch. resolveTarget searches blocks/entities/marks before falling back to raw coords. resolvePositionKeyword handles the "current"/"here" string regression gracefully.

Test quality — 43 tests with real edge cases: partial mine yields (soft failure), model hallucinations, empty responses, recovery retry loops, out-of-scope silence, emoji surrogate pairs in canonical serialization.


💡 Suggestions (non-blocking)

  1. index.js:339 — The 404 handler returns JSON instead of the /intent shape. Consider adding context_id for consistency: {ok: false, context_id: "unknown", error: {error_type: "not_found", path: req.url}}.

  2. lib/dispatcher.js:307-324ITEM_ALIASES is a hardcoded map. If the list grows beyond ~15 entries, consider moving it to a JSON config file. For now it's fine.

  3. systemd/embodied-service.service:16 — Uses WorkingDirectory=%h/REPOS/daemoncraft/ path. Our repo is at ~/Projects/DaemonCraft/. Document that this needs local adjustment. (The systemd README already covers this.)

  4. lib/dispatcher.js:143-152 — The craft_item handler does best-effort search for a crafting table. If no table is found, the catch block swallows silently. Consider logging this fallthrough at debug level for field-session audits.

  5. profile-templates/daemoncraft-base.config.yaml — References kimi-k2.6 as the model. The embodiment toolset needs to exist in hermes-agent for this to work. This is a template — not blocking merge, just noting the dependency.


🔍 Architecture decision to validate at field-test

The embodied service is intentionally stateless (v1 disciplina: no memory between intents, no queuing, no own Mineflayer session). This means:

  • Each /intent call does fresh composeWorldState() (parallel bot API calls)
  • Hermes must track intent context across turns
  • Gemma-Andy has no long-term memory of previous actions

This is the right call for v1, but watch for latency from world_state recomposition on complex intents. If it becomes a bottleneck, adding a 30s TTL cache for stationary world_state keys (inventory, status) would be trivial.


Reviewed by CompAII — tests run locally: 43/43 pass, 0 fail

@nicoechaniz
nicoechaniz merged commit b49ad7b into nicoechaniz:main May 9, 2026
@Fede654

Fede654 commented May 10, 2026

Copy link
Copy Markdown
Contributor Author

Update: canonical reference audit (commits desde a8e94e0 hasta 2e5fbe4)

Después del field-test que hizo el teammate (#10 thread above) y la observación de Fede de que el bot fabricaba coords y se desviaba aunque el world_state se viera "razonable", hicimos un audit completo contra la implementación de referencia en Mar-IA-no/deamoncraft-gemma4-andy — específicamente examples/eval_with_adapter.py que es el contrato byte-exact con el SFT training.

6 commits agregados

Commit Qué
88ddcd3 Consumer-side mitigations: recovery_naive_retry, empty_tool_calls, empty_model_response
44b8e45 systemd unit + E002 Phase 6 structured log capture (intent_received, ollama_call_start with full payload, ollama_call_done with full plan, intent_done with elapsed_seconds)
d07597e Smarter parser bracketFallback (brace-balanced scan) + 3rd mitigation for empty model output
c329d45 Position keyword fallback ("current"/"here"/"in_front" → bot pos) + item name plural aliases
9bbf9e8 Auto-goto crafting_table antes de craft_item (modelo a veces emite craft sin ensure proximity)
9f69535 bot_soft_failure detector: el bot devuelve HTTP 200 con {ok:true, result: "Mined 0/N"} para failures suaves; el dispatcher ahora detecta y surface como error_type: "bot_soft_failure"
2e5fbe4 Audit byte-exact vs reference de Mariano: 5 drift points fixed

Drifts surfaceados en 2e5fbe4

Cinco huecos entre nuestra impl y eval_with_adapter.py:

  1. guardian_constraints tenía 4 fields, canonical tiene 5 (executor_filtering, no_player_harm faltaban; protected_zone_owner: null sobraba)
  2. Sampling: Modelfile pins temperature: 0.2, pero la reference usa greedy (do_sample=False) y OLLAMA_USAGE.md production example explícitamente sobreescribe a temperature: 0.0. Sin esto, variance suficiente para fabricar coords ([1, 64, 518] para player en [-6, 81, 64])
  3. bot_position/player_position floats: training distribution usa block coords integer ([10, 68, 5]), nosotros mandábamos floats ([27.3, 79, 56.5]). Modelo no entendía y caía a priors
  4. nearby_entities con noise (item, chest_minecart, arrow, etc) — mineflayer entity types que el modelo nunca vio en SFT
  5. Vault sin eval_with_adapter.py mirror — tratábamos las prose docs como contrato cuando el reference real estaba en el repo de Mariano

Por qué importaba

Sin alineación byte-exact, cada drift empujaba la input distribution off-training un poco más. Suma: el modelo cae a priors. Síntomas durante field-test:

  • "come here" → goto([1, 64, 518]) (coords fabricadas)
  • "construyamos" → place_block(oak_log) cuando inv tiene oak_planks(40)
  • "no te estás yendo" → [scan_nearby × 4] (loop)

Con los fixes aplicados + greedy sampling, el contrato vuelve a estar a tiempo.

Vault actualizado (espejo del repo de Mariano)

Documentado en mi vault local en concepts/gemma-andy-canonical-reference.md + recipe para refrescar mirrors cuando Mariano actualiza upstream.

Test plan local desde 2e5fbe4

  • node --test test/ — 43/43 pass
  • world_state byte-exact con EXAMPLE_USER de eval_with_adapter.py
  • guardian_constraints byte-exact con reference
  • Field-test live con player presente (en flight)

Estado actual del PR

Sigue atómico al agents/embodied-service/:

$ git diff upstream/main..HEAD --name-only | wc -l
24

Listo para nueva ronda de review cuando quieras.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants