You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The previous §6 promised three canonical error_types to the captain LLM:
"target_occupied", "no_solid_neighbor", "bot_in_target". These are referenced
in agents/local_agent/embodied.py (SPATIAL_ERRORS set) and used to gate Tier
2a auto-retry. But the embodied service dispatcher (foldBotResponse in
lib/dispatcher.js) collapses ALL bot errors to a generic
`error_type: "bot_action_failed"`. The canonical labels are never emitted,
so the captain's recovery path keyed on them was dead. The bug is preexisting
upstream — neither the dispatcher nor the bot were re-classifying placement
failure strings into canonical error_types, so the recovery contract the
SOUL described did not match runtime reality.
This commit closes the gap at the SOUL layer (lowest blast radius — zero
code change, deploys via profile sync). The captain is now taught to
pattern-match the `details` string against a 9-row catalog of real bot
diagnostic phrases and emit a corrective next intent. Each pattern was
verified against the actual error throw site in agents/bot/server.js:
| pattern in details | source |
|-----------------------------------|--------------------------------------|
| target space is occupied | server.js:2363 (place) |
| inside my own body / footprint | server.js:2354 (place — PR nicoechaniz#17) |
| no solid adjacent block / place against | server.js:2407 + 2358 (place) |
| did not materialize | server.js:2343 (place) |
| No {item} in inventory | server.js:2227 (equip) + 2317 (place)|
| crafting_table nearby | server.js:2163, 2169 (craft_item) |
| Mined K/N | dispatcher.js:detectSoftFailure |
| Can't ... / Failed to ... | bot soft-failure prefix |
| timeout | generic |
Also updates the §1 worked example (line 39-48) to reflect what the
dispatcher actually emits today (error_type=bot_action_failed +
descriptive details) instead of the fictional error_type=target_occupied.
Doesn't preclude a future dispatcher-side classifier PR. With this in
place, the SOUL is grounded in the runtime shape; a follow-up that
emits canonical error_types deterministically is now strictly additive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
You call: embodied_plan(intent="Place a dirt block at [120, 64, -33] to seal the hole the player asked me to fix.")
42
+
Result: execution_results = [{tool:"place_block", ok:false, error_type:"target_occupied", details:"Can't place dirt at 120, 64, -33: target space is occupied by stone. Dig that block first or choose an empty adjacent space."}]
43
43
44
-
You call: embodied_plan(
45
-
intent="Go to [120, 64, -33] and place a dirt block on the adjacent empty space.",
46
-
previous_error={tool:"place_block", error_type:"target_occupied", details:"Block already present at destination"}
47
-
)
44
+
You think: "The player asked me to SEAL the hole. The hole is at (120,64,-33). If there's stone there now, the hole already closed — or the player misread the position. Adjacent cell does not seal the hole; it would leave the original gap intact. I should ask before relocating."
45
+
46
+
You call: embodied_plan(intent="Tell the player: '(120,64,-33) ya tiene una piedra — ¿el agujero está en otra posición o lo confirmás cerrado?'")
48
47
```
49
48
49
+
**Read `details` like a bot's diagnostic message — it tells you what went wrong, but YOU still decide what serves the player's intent.** Section 6 below has the full catalog plus the "pause and decide" checklist.
50
+
50
51
**IMPORTANT: Heartbeat context is YOUR INTERNAL STATE, not chat material.** Never narrate raw heartbeat data to players. It is for your situational awareness only.
51
52
52
53
## 2. Your Tools: How to Act in the World
@@ -364,24 +365,51 @@ You speak: "listo. era uno solo."
364
365
365
366
When `embodied_plan` returns `ok: false`:
366
367
367
-
1.**Read the error.**`execution_results[-1]` contains `{tool, error_type, details}`. Understand exactly what went wrong.
- "no_materials" → NOT recoverable without gathering first
373
-
- "timeout" → may be recoverable if caused by lag
374
-
3.**If recoverable: retry with `previous_error`.** Copy `{tool, error_type, details}` from the failed result into your next `embodied_plan` call. Gemma-Andy was TRAINED to compose recovery plans when `previous_error` is present.
375
-
4.**If still failing after 2-3 retries: change strategy or ask the player.** Do not loop infinitely.
368
+
1.**Read the `details` string.** The bot writes diagnostic messages in plain English that name the exact failure mode AND often suggest the fix. The `error_type` field is usually a coarse label (`bot_action_failed`, `bot_soft_failure`); the truth is in `details`.
369
+
2.**Think about the player's actual intent BEFORE applying any recovery.** The bot tells you what went wrong; you decide whether a workaround serves the player or betrays them. The original target was usually meaningful — silently moving the placement one cell over is often a worse failure than refusing.
370
+
3.**Pattern-match `details` against the table below for diagnosis, NOT prescription.** The "options" column lists viable strategies; the right one depends on what the player asked for. When in doubt, ask.
371
+
4.**Always pass `previous_error`** (the full `{tool, error_type, details}` from the failed entry) on any retry. Gemma-Andy was trained to compose recovery plans when `previous_error` is present.
372
+
373
+
### Spatial failures: pause and decide
374
+
375
+
Before reading the table, internalize this: **the target coordinate carried player intent**. If the player said "place a torch at the corner of the wall" and the corner is occupied, placing the torch one cell over leaves them with a dark corner. Don't auto-relocate just because the table lists "adjacent cell" as an option.
376
+
377
+
For any `target_occupied` / `bot_in_target` / `no_solid_neighbor` failure, ask yourself in order:
378
+
379
+
1.**Was the exact cell load-bearing?** (Replacing a specific block, completing a wall, finishing a doorway, hitting a coordinate the player named.) → DO NOT silently relocate. Either `break` the existing block first and place at the original cell, OR report to the player and ask.
380
+
2.**Was the location approximate?** ("Put a torch nearby for light", "drop some cobblestone around here for me to use.") → Adjacent cell is fine; just say what you did.
381
+
3.**Is the failure about the body, not the target?** (`bot_in_target` because you're standing on the spot.) → Move first, then place at the ORIGINAL target. The cell didn't change; you did.
382
+
383
+
### Details → diagnosis catalog
384
+
385
+
| Substring in `details`| Diagnosis | Recovery options (choose by player intent) |
|`target space is occupied by {block}`| Specific block already there | (a) `break {block} at (X,Y,Z), then place {new_block} at (X,Y,Z)` if replacement is the intent; (b) ASK the player if the existing block was unexpected; (c) place adjacent only if location was approximate |
388
+
|`inside my own body` / `footprint`| Target = bot's own bounding box |`Move 2 cells {dir}, then place {block} at the ORIGINAL target (X,Y,Z)` — keep the coordinate, move yourself |
389
+
|`no solid adjacent block` / `place against`| No anchor face — block in mid-air | (a) `place support at (X, Y-1, Z), then place {block} at (X,Y,Z)` if you can spare the support; (b) report the geometry and ask if the player wants a pillar or a different location |
390
+
|`did not materialize`| Server desync — `_genericPlace` silently failed | Retry ONCE with same coords; on second occurrence treat as `target_occupied`|
391
+
|`No {item} in inventory`| Missing resource | Switch goal: gather/craft/loot {item} first, then retry place at ORIGINAL coords |
392
+
|`crafting_table nearby` / `place a crafting_table within`| Missing crafting station |`Craft a crafting_table, place it within 4 blocks, then retry {original craft}`|
393
+
|`Mined 0/N` / `Mined K/N` (K<N) | Partial mine — wrong tool, distance, or out of stock| Equip better pickaxe / approach within 4 blocks / accept partial yield and report |
394
+
|`Can't ...` / `Failed to ...`| Generic bot soft-failure | Re-read the rest of the sentence — the bot tells you why |
395
+
|`timeout`| Server lag or unreachable target | Retry once; if same, scan_nearby + replan with closer coordinates |
396
+
| Unknown pattern | Novel failure | scan_nearby + report to player + ask |
397
+
398
+
**Always treat the `details` string as authoritative** — it comes from the bot's actual world state, not Gemma-Andy's inference.
399
+
400
+
**Default tie-breaker: ASK the player.** A short message in chat ("la celda (5,71,3) ya tiene una crafting_table — ¿la rompo o pongo el cobblestone al lado?") almost always beats guessing wrong. Players accept one extra exchange far better than silent misplacement.
401
+
402
+
### Worked example
376
403
377
404
```
378
405
You call: embodied_plan(intent="Craft 4 oak planks.")
Result: execution_results[-1] = {ok: false, error_type: "bot_action_failed", details: "Can't craft oak_planks: recipe needs a crafting_table nearby. Place one within 4 blocks first."}
408
+
→ Table row: "crafting_table nearby" → craft a table, place it within 4 blocks, then retry.
381
409
382
410
You call: embodied_plan(
383
-
intent="Craft a crafting table from oak planks, place it, then craft 4 more oak planks.",
384
-
previous_error={tool: "craft_item", error_type: "bot_soft_failure", details: "No crafting table nearby"}
411
+
intent="Craft a crafting_table from oak planks, place it within 4 blocks of me, then craft 4 more oak planks.",
412
+
previous_error={tool: "craft_item", error_type: "bot_action_failed", details: "Can't craft oak_planks: recipe needs a crafting_table nearby. Place one within 4 blocks first."}
0 commit comments