Skip to content

fix(policy): add craft category + narrow inventory_query keyword scope - #16

Open
Pablomonte wants to merge 1 commit into
nicoechaniz:feat/canonical-loopfrom
Pablomonte:fix/policy-craft-category-and-inventory-query-narrowing
Open

fix(policy): add craft category + narrow inventory_query keyword scope#16
Pablomonte wants to merge 1 commit into
nicoechaniz:feat/canonical-loopfrom
Pablomonte:fix/policy-craft-category-and-inventory-query-narrowing

Conversation

@Pablomonte

Copy link
Copy Markdown
Contributor

Summary

GemmaPolicy's L4 classifier had two collaborating bugs that made any craft-style intent unable to dispatch the actual crafting tools:

  1. No 'craft' category existed. Captain-side intents like `Craft a stone shovel using cobblestone and sticks` had nothing to match in the keyword table.
  2. 'inventory_query' keyword 'inventory' was too broad. Any intent containing the word "inventory" — including the perfectly natural "…from my inventory" clause that captains routinely emit — matched, narrowing tools to `[get_inventory, ask_clarification, report_execution_error]`.

Observed in production

End-to-end trace from a live Hermes-K2.6 + DaemonCraft session:

```
Captain (Kimi-K2.6) → embodied_plan(intent=
"Craft a stone shovel using cobblestone and sticks from my inventory."
)
→ policy.classify_category() loops CATEGORY_KEYWORDS
→ hits 'inventory_query' via keyword 'inventory'
→ allowed_tools = [ask_clarification, get_inventory, report_execution_error]
→ Gemma-Andy plan: ask_clarification("craft_item is not available
in this turn; should I wait?")
→ captain tells player: "no tengo capacidad de craftear ahora"
```

The user sees a hard wall on every crafting request despite Gemma-Andy and the embodied service being fully capable.

The fix (3 additive changes to `agents/gemma_policy.py`)

1. New 'craft' category

Inserted before mining/build so it wins on overlap. Keywords cover ES+EN imperative forms:

```python
("craft", ["craft ", "craftear", "crafteá", "craftea",
"fabricar", "fabricá", "fabrica",
"smelt", "fundir", "fundí", "fundi",
"cocinar", "cociná", "cocina",
"horno", "furnace", "forja", "forjar", "forge"]),
```

Tool subset:

```python
"craft": ["get_inventory", "view_craftable", "craft_item", "smelt_item",
"check_furnace", "take_from_furnace", "equip_item",
"place_block", "scan_nearby", "goto"],
```

Includes `place_block` for placing crafting_table/furnace first; `goto`/`scan_nearby` for finding a crafting station nearby.

Mapped through `STRATEGY_MAP["craft"] = "embodied_plan"` like every other category.

2. Tighten 'inventory_query' keyword scope

```diff

  • ("inventory_query", ["inventario", "inventory", "decime qué tenés", ...
  • ("inventory_query", ["inventario", "decime qué tenés", ...
  •                   \"list inventory\", \"list my items\"]),
    

```

Drop bare `"inventory"` — too greedy. Keep `"inventario"` (Spanish-only, no overlap), `"show inventory"`, `"list inventory"`, and the existing Spanish phrases. Pure inventory-show intents still classify correctly:

Intent Category
`mostrame el inventario` inventory_query (3 tools)
`inventory check` (no "inventario" or specific phrase) default (38 tools)
`Craft X from my inventory` craft (12 tools) — was inventory_query before

3. Extend 'build' tools + keywords

Bonus cleanup that surfaced while fixing #1:

```diff

  • "build": ["scan_nearby", "goto", "place_block", "equip_item", "get_inventory"],
  • "build": ["scan_nearby", "goto", "place_block", "fill_volume", "ignite",
  •       \"equip_item\", \"get_inventory\"],
    

```

`fill_volume` enables bulk construction without dropping category. `ignite` covers torch/lava placement scenarios.

Build keywords also extended with `"fill "`, `"rellena/rellená"`, `"construir"` (the infinitive — only imperative forms were covered).

Verification

Live test of the patched policy:

```python

p = GemmaPolicy()
intents = [
... "Craft a stone shovel using cobblestone and sticks from my inventory.",
... "craft 4 oak planks",
... "fundir 16 arena en vidrio",
... "construir una casa de 5x5",
... "fill volume con cobblestone",
... "mostrame el inventario",
... "inventory check",
... ]
for it in intents:
... cat = p.classify_category(it.lower())
... n = len(p.get_allowed_tools(cat) or []) or "ALL"
... print(f"{cat:18s} [{n:>3}] ← {it[:55]}")

craft [ 12] ← Craft a stone shovel using cobblestone and sticks from
craft [ 12] ← craft 4 oak planks
craft [ 12] ← fundir 16 arena en vidrio
build [ 9] ← construir una casa de 5x5
build [ 9] ← fill volume con cobblestone
inventory_query [ 3] ← mostrame el inventario
default [ 38] ← inventory check
```

Before this patch, the first three returned `inventory_query 3`.

Test plan

  • `pytest tests/test_gemma_policy.py` — 29/29 pass.
  • Manual end-to-end: `@AsciiProbe haceme una pala de piedra` now dispatches `embodied_plan(intent="Craft a stone shovel...")` → policy emits 12 tools → Gemma-Andy proposes `craft_item` → bot crafts.
  • Follow-up PR: add regression test cases for the build/craft/inventory keyword collision matrix.

Upstream sync

This patch lives in `agents/gemma_policy.py` of the DaemonCraft canonical source. The vendored copy in `nicoechaniz/hermes-agent` (`tools/gemma_policy.py`) needs the same diff applied — that goes in a separate PR to the hermes-agent fork.

GemmaPolicy's L4 classifier had two collaborating bugs that made any
craft-style intent unable to dispatch the actual crafting tools:

1. **No 'craft' category existed**. Any captain-side intent like
   "Craft a stone shovel using cobblestone and sticks" had nothing to
   match against in the keyword table, so classification fell back to
   whichever later category matched first.

2. **'inventory_query' keyword 'inventory' was too broad**.  It matched
   any intent that *mentioned* the word "inventory" anywhere — including
   normal craft/mine/build intents that include the perfectly natural
   "from my inventory" clause.  The 'inventory_query' tool subset is
   only `[get_inventory, ask_clarification, report_execution_error]`,
   so the captain ended up with no actionable body tools at all.

Observed end-to-end stack:
  Captain (Kimi-K2.6) emits embodied_plan(intent="Craft a stone shovel
  using cobblestone and sticks from my inventory.")
  → policy.classify_category()
  → loop hits 'inventory_query' via keyword 'inventory'
  → allowed_tools = [ask_clarification, get_inventory, report_execution_error]
  → Gemma-Andy plan: ask_clarification("craft_item is not available
                                         in this turn; should I wait?")
  → captain explains "no tengo capacidad de craftear" to the player

The fix is additive in three places:

a) New 'craft' category, inserted *before* 'mining'/'build' in the
   keyword list so it wins on overlap.  Keywords cover ES+EN imperative
   forms ("craft", "craftear", "fabricar", "smelt", "fundir",
   "cocinar", "horno", "furnace", "forge").

b) CATEGORY_TOOLS['craft'] = [get_inventory, view_craftable, craft_item,
   smelt_item, check_furnace, take_from_furnace, equip_item, place_block,
   scan_nearby, goto].  Includes place_block because the agent may need
   to place a crafting_table/furnace first; includes goto/scan_nearby
   because the agent may need to find a crafting station.

c) STRATEGY_MAP['craft'] = 'embodied_plan' — same execution method as
   every other category; included so the dispatcher doesn't tip-toe on
   a missing key.

Bonus cleanups in the same patch (both load-bearing on this branch):

- Tighten 'inventory_query' keywords: drop bare 'inventory', keep
  'inventario' (Spanish exclusive), 'show inventory', 'list inventory',
  etc.  Pure inventory-show intents still classify correctly.

- Extend 'build' tools with 'fill_volume' and 'ignite'.  The agent
  already has 'place_block'; adding 'fill_volume' enables bulk
  construction without dropping out of the build category, and 'ignite'
  covers torch/lava placement scenarios.

- Extend 'build' keywords with 'fill ', 'rellena/rellená', 'construir'
  (infinitive — 'construí'/'construye' covered only imperative forms).

Verification

Bench from the live install (Hermes-K2.6 + DaemonCraft):

  >>> p = GemmaPolicy()
  >>> for it in ["Craft a stone shovel using cobblestone and sticks…",
  ...            "craft 4 oak planks",
  ...            "fundir 16 arena en vidrio",
  ...            "construir una casa de 5x5",
  ...            "fill volume con cobblestone",
  ...            "mostrame el inventario",
  ...            "inventory check"]:
  ...     cat = p.classify_category(it.lower())
  ...     print(cat, len(p.get_allowed_tools(cat) or []) or 'ALL')

  craft           12
  craft           12
  craft           12
  build           9
  build           9
  inventory_query 3
  default         38

Before this patch, the first three returned `inventory_query 3`, so the
agent literally could not dispatch craft_item.

pytest tests/test_gemma_policy.py — 29/29 pass (no test fixtures
covered craft/build keyword collision yet; follow-up PR can add
regression cases).
Pablomonte added a commit to Pablomonte/DaemonCraft that referenced this pull request May 17, 2026
… box

The `place` action in agents/bot/server.js (the canonical
embodied-side dispatcher target for `place_block` tool calls) had a
load-bearing failure mode: when the requested (x, y, z) was inside the
bot's own bounding box, the server silently rejected the placement and
the materialize-verification at the bottom surfaced an opaque
"did not materialize. Retry or choose different coordinates." error.

The captain LLM then had no useful information — Gemma-Andy retried
the same coordinates, hit the same silent reject, and after a few
loops told the player "no puedo construir, el sistema sigue roto".

Root cause: the action accepted (x, y, z) as the literal target and
walked the placement neighbour search without first checking whether
the bot itself occupied that cell. Servers reject `_genericPlace`
calls that would clip the player's hitbox, but the rejection comes
back as a no-op (no exception), so the only signal was the
post-place blockAt() check failing.

Fix: before the existing occupied / neighbour-search logic, check
whether the floor(target) cell equals the bot's feet cell (floor(pos))
or head cell (feet+1). If yes, pick the first candidate from
[(+x), (-x), (+z), (-z), feet+2 above-head, feet-1 below-feet] that
is:
  - not in the bot's own occupied cells
  - air / cave_air
  - has at least one solid neighbour to place against
…and rewrite (x, y, z) to that. Log the shift so operators can see it
in dispatch traces.

If no candidate fits, raise a clear error explaining the placement
cannot proceed without first moving the bot — not the opaque
"did not materialize" the captain previously had to guess at.

End-to-end repro before the fix:

  Captain (Kimi-K2.6) → embodied_plan(intent=
    "Place a crafting_table near my position, then craft a stone_shovel.")
  → Gemma-Andy plan: [place_block(crafting_table, x=bot.x, y=bot.y, z=bot.z),
                      craft_item(stone_shovel)]
  → bot place action: target == bot feet cell → silent server reject
  → materialize check fails → throws "did not materialize"
  → craft_item fails: "needs crafting table nearby"
  → captain to player: "no puedo construir"

After: place action detects the overlap, shifts target to the cell
immediately east of the bot (or first viable adjacent), places the
crafting_table, and the follow-up craft_item finds it within the
4-block radius.

This is one of three concurrent layers being hardened for "build with
confidence" alongside the canonical-loop policy patches
(nicoechaniz#16 — craft category + narrow inventory_query)
and the canonical-loop toolset wiring
(nicoechaniz/hermes-agent#7 + nicoechaniz#8).

No new dependencies, no API change. Pure additive guard inside the
existing place action body.
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.

1 participant