fix(policy): add craft category + narrow inventory_query keyword scope - #16
Open
Pablomonte wants to merge 1 commit into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GemmaPolicy's L4 classifier had two collaborating bugs that made any craft-style intent unable to dispatch the actual crafting tools:
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
```
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:
3. Extend 'build' tools + keywords
Bonus cleanup that surfaced while fixing #1:
```diff
```
`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
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
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.