Skip to content

feat: recovery_candidates wired into process_plan_tick + Pipeline 3 telemetry + experiments 008/009 - #12

Open
Fede654 wants to merge 9 commits into
nicoechaniz:mainfrom
Fede654:feat/recovery-candidates-loop-wire
Open

feat: recovery_candidates wired into process_plan_tick + Pipeline 3 telemetry + experiments 008/009#12
Fede654 wants to merge 9 commits into
nicoechaniz:mainfrom
Fede654:feat/recovery-candidates-loop-wire

Conversation

@Fede654

@Fede654 Fede654 commented May 10, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Adds Codex Path D — deterministic recovery-candidate synthesis — at the right architectural layer (the autonomous loop's per-step retry handler in `agent_loop.py:process_plan_tick`). Plus 4 commits of additive telemetry on the embodied service and 2 new primitives_lab experiments that empirically motivate the work.

The body model `gemma-andy:e4b-v2-2-3-q8_0` is a selector, not a searcher: given an explicit substitute it executes 100% (lesson 7 + experiment 009: 20/20), given only a negation it re-emits the failing target 60-100% (lesson 8). The right division of labour: the autonomous loop has world state in scope (via `fetch_bot_*` REST calls) → it synthesises candidates deterministically → Andy executes via selection.

What lands

Architectural — recovery_candidates at the autonomous-loop retry layer

  • `agents/recovery_candidates.py` (new, 195 lines, pure-Python, no I/O) — `maybe_synthesize_substitute(step, embodied_result, *, bot_position, nearby_blocks, inventory) -> Optional[str]`.
    • Subtype detection via regex on `execution_results[0].details`:
      • `target_occupied` → `_find_air_with_neighbor` returns nearest face-adjacent air voxel with a solid neighbor
      • `no_solid_neighbor` → 2-step "support-then-target" using existing column solids
      • `bot_self_position` → horizontal neighbor of the bot with a solid block below
      • `missing_inventory` → swap to first available block from `_placement_substitutes_in_inventory`
    • Returns `None` for unmodeled subtypes (preserves existing retry-with-backoff fallback).
  • `tests/test_recovery_candidates.py` (new, 162 lines, 6 unit tests, all pass).
  • `agents/agent_loop.py:process_plan_tick` (+34 lines) — between `verify_step` failure and `step.retries++`, when retries remaining ≥ 2, calls the synthesiser using fresh `fetch_bot_status/nearby/inventory` and re-runs `call_embodied` with the substitute intent in the SAME tick. Substitute intent is NOT persisted to `plan.json` (Plan stays under Steve's authority). Logs new events: `substitute_synthesised`, `substitute_verify`, `substitute_synthesis_failed` (replayable per the loop's structured-JSON convention).

Pipeline 3 — Embodied-service telemetry

Field session 2026-05-10 (during PR #11 work) spent debug time correlating bot HTTP errors back to dispatcher state. These four commits surface the data the operator needs without changing service behaviour:

  • `tool_dispatch` logEvent now carries `details` (verbatim bot error message). One line, hooks the existing `r.details` field that `foldBotResponse` already produces.
  • `ollama_call_done` logEvent now carries `think_excerpt` (first 500 chars of `` block). Enables grep-friendly auditing without dumping nested `plan` JSON.
  • `parse_failed` logEvent now carries `raw_length_chars` + `truncated` boolean (heuristic: non-empty AND no `}` in last 50 chars OR no `}` at all). The 2026-05-10 num_predict=512 truncation incident motivated this — distinguishes "model output truncated" from "model output unparseable".
  • Heuristic refinement (post-review) — drop the `]` check and short-circuit on `last_brace === -1` to fix two correctness bugs flagged by code review (length-dependent verdict on missing `}`; AND-combinator missing one-sided truncation when the inner `]` lands near end but outer `}` doesn't).

primitives_lab experiments

  • `008_recovery_generalization.yaml` — 4 variants, each narrating a distinct `bot_action_failed` mode WITHOUT naming a substitute. Result: 17/20 re-emit the failing position (negative result). This is what motivated the architectural reframe.
  • `009_explicit_substitute_recovery.yaml` — same 4 modes but recovery intent NAMES the substitute. Result: 20/20 substitute-match, 0/20 re-emit (e2e validated — sample 1 of each variant placed in-world). This is the empirical basis for `recovery_candidates.py`.

Empirical context (vault links external to this repo, summary inline)

The body model's behaviour now characterised by 3 ladder runs:

  • Lesson 7 (experiment 007, n=10): 9-10/10 when intent NAMES substitute ("Use oak_planks instead of oak_log")
  • Lesson 8 (experiment 008, n=5): 0-2/5 when intent only NARRATES failure ("do not re-emit (7, 65, 35)")
  • Lesson 9 (experiment 009, n=5): 5/5 across ALL FOUR 008 modes when intent NAMES substitute

The pattern: selection works, search doesn't. The candidate synthesiser does the search; the model does the select.

The architectural reframe was suggested by Codex (`gpt-5.4`) in a single-pass audit (job `opinion-1778391655600-e69694b4`, log at `~/.bridge-ai/opinions/20260510-024055-hermes-agent-codex-second-opinion.md` on Fede's machine — quoted findings: "the architectural bug is outsourcing feasible local recovery search to the body model" / "missing boundary is which layer is responsible for generating feasible alternatives"). Confidence: medium-high.

Per-commit list (bottom → top)

f540419 test(recovery_candidates): TDD step 1 — failing tests for substitute synthesiser
c0eb53e feat(daemoncraft): recovery_candidates — deterministic substitute synthesis
ef894f6 feat(agent_loop): wire recovery_candidates into process_plan_tick
456a4b7 feat(embodied-service): tool_dispatch logEvent now carries details field
40654d8 feat(embodied-service): ollama_call_done log carries think_excerpt
1a90bcb feat(embodied-service): parse_failed log carries length + truncation flag
4f555aa fix(embodied-service): tighten parse_failed truncation heuristic
fb44fff feat(primitives_lab): experiment 008 — recovery generalization
789ff2d feat(primitives_lab): experiment 009 — explicit substitute recovery

7 files changed, +583/-1.

Test plan

  • `pytest tests/test_recovery_candidates.py` → 6/6 pass (committed at `c0eb53e` + cherry-picked here).
  • `python -c "import ast; ast.parse(open('agents/agent_loop.py').read())"` → SYNTAX OK.
  • `python -c "import sys; sys.path.insert(0, 'agents'); from agent_loop import process_plan_tick"` → IMPORT OK.
  • Telemetry verification curl probes: each new field (`details`, `think_excerpt`, `raw_length_chars`, `truncated`) confirmed present in subsequent log events post-restart of the embodied service.
  • Experiment 009 e2e: sample 1 of each variant placed substitute block in-world; samples 2-5 returned `ok=false` because their substitute coord was now occupied (correctly reflecting world-state change).
  • Live run with a Plan-driven Sparky exercising `process_plan_tick` substitute path — deferred to first session after merge.

Notes for the reviewer

  • The substitute is computed in-memory and never written to `plan.json`. Plan stays under Steve's (cloud LLM) authority. The synthesiser is bounded to one extra `call_embodied` per tick.
  • The `step.retries < step.max_retries - 1` guard preserves the last retry budget for the un-substituted fallback case — if synthesis somehow makes things worse, the loop still has at least one un-substituted retry before `step.exhausted`.
  • `recovery_candidates.py` covers 4 subtypes derived from `bot/server.js` `place()` throw sites. Other failure modes (`reach_too_far`, `face_obstructed`, `server_blockUpdate_timeout`, mining failures) fall through to existing retry. Subtype-by-subtype additions are cheap once observed.

🤖 Generated with Claude Code

Fede654 and others added 9 commits May 10, 2026 16:11
…synthesiser

Six tests pin the contract for a new agents/recovery_candidates.py
module that deterministically synthesises substitute Step.intent
strings when verify_step fails with known subtypes. Implementation
lands in the next commit.

Subtypes covered:
- target_occupied → adjacent air with solid neighbor
- no_solid_neighbor → 2-step support-then-target
- bot_self_position → horizontal neighbor with solid below
- missing_inventory → swap to held block from inventory
- returns_none on unmodeled failure (preserves existing fallback)
- returns_none when embodied_result.ok is True

Empirical basis: primitives_lab experiment 009 (20/20
substitute-match) confirmed the body model executes named substitutes
100% across all 4 of experiment 008's failure modes. The candidate
generator does the search; Andy does the select.

Expected state at this commit: 6 errors (module not yet created).

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

Pure-Python module + 6 unit tests implementing Codex's Path D at the
right architectural layer (the autonomous loop's per-step retry).

Empirical basis: experiment 009_explicit_substitute_recovery validated
20/20 substitute-match (vs 008's 17/20 re-emit) when the intent names
an explicit alternative coord/block. The body model is a SELECTOR, not
a SEARCHER — synthesise the substitute deterministically using world
state already in scope of the autonomous loop, hand the model a select
action.

Subtypes covered:
- target_occupied → adjacent air voxel with solid neighbor
- no_solid_neighbor → 2-step support-then-target
- bot_self_position → horizontal neighbor with solid below
- missing_inventory → swap to held block from inventory

Returns None when subtype is unrecognised or no feasible candidate
exists, preserving the loop's existing retry-with-backoff fallback.

Tests at tests/test_recovery_candidates.py (committed in 2fe1583);
this commit is what makes them pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When verify_step fails with bot-side details and we have retries
remaining (step.retries < step.max_retries - 1), synthesise a
deterministic substitute via maybe_synthesize_substitute and re-run
call_embodied with the rewritten intent in the same tick. If synthesis
returns None or the substitute also fails verify, fall through to the
existing retry-with-backoff (unchanged).

The substitute intent is NOT persisted to plan.json — Plan stays under
Steve's authority. The rewrite only affects this tick's call to the
body. body_session reports the substituted execution; Steve sees both
the original step.intent and the gemma_tool_calls from the rewrite via
the next heartbeat.

Logs new events: substitute_synthesised, substitute_verify,
substitute_synthesis_failed (replayable audit trail per the loop's
structured-JSON convention).

Empirical basis: experiment 009 confirmed 20/20 substitute-match when
the intent names an explicit alternative. The body model is a
SELECTOR; this wires the SEARCHER (recovery_candidates) into the right
architectural layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dispatcher already produces r.details (bot's HTTP error message,
verbatim). The log event was dropping it. Without it, post-hoc
debugging of place_block / mine_block failures requires correlating
the dispatcher response shape against the wall-clock — painful
during iteration. This is a one-line fix that unblocks Pipeline 1
and 2 validation downstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`had_think:bool` told you a think block existed but not what it said.
Greping a 500-char excerpt makes post-hoc auditing of why Andy chose
tool X possible without dumping the whole nested plan JSON. The full
think field is still emitted (already was), this just lifts a flat
preview to top-level for tooling.

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

When parse_failed fires we want to know FAST whether the cause was
output truncation (model hit num_predict cap mid-emit) vs the model
genuinely emitted unparseable text. The 2026-05-10 field session
spent debug time on this exact question. Heuristic: if raw is
non-empty and neither `}` nor `]` appears in the last 50 chars,
mark truncated=true. raw_length_chars is unconditional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two correctness fixes for the heuristic added in 74e07e0:

1. When raw output has no `}` at all, the previous check
   `last_brace < length - 50` evaluated differently based on length
   alone (false for length<=50, true for length>50). Same content
   shape, opposite verdicts. Fixed by short-circuiting on
   `last_brace === -1` regardless of length.

2. The AND combinator on `}` and `]` missed a real truncation mode:
   model closes the inner `]` but runs out of tokens before closing
   the outer `}`. AND said "not truncated" because the bracket was
   near the end. Removed the `]` check — `}` is ground truth for
   the response shape.

Heuristic now reads: truncated iff the response is non-empty AND
either there's no `}` at all OR the last `}` is more than 50 chars
from the end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Validates Pipeline 2 of the 2026-05-10 communication-pipelines plan.
Lesson 7 (round 2) validated in-intent narrative recovery for the
block-substitution case. This experiment generalises across all four
bot_action_failed modes the place_block dispatcher emits:
- target space occupied (e.g. leaf_litter)
- no solid adjacent block to place against
- target equals bot's own position
- inventory missing the requested block name

Each variant N=5. The load-bearing analytical question (manual,
post-run): what fraction of samples RE-EMIT the failing position?
Lower = better generalisation. Result JSON consumed by the lessons-008
vault page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Confirms Codex Path D at 20/20 substitute-match (vs 008's 0/20).
Each variant matches an 008 failure mode but the recovery intent
names a specific alternative coord/block. Result file:
results/009-explicit-substitute-recovery_20260510_025255.json.

Empirical basis for daemoncraft/agents/recovery_candidates.py
(committed on branch feat/recovery-candidates-loop-wire).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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