feat: recovery_candidates wired into process_plan_tick + Pipeline 3 telemetry + experiments 008/009 - #12
Open
Fede654 wants to merge 9 commits into
Open
Conversation
…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>
5 tasks
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.
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
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:
primitives_lab experiments
Empirical context (vault links external to this repo, summary inline)
The body model's behaviour now characterised by 3 ladder runs:
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)
7 files changed, +583/-1.
Test plan
Notes for the reviewer
🤖 Generated with Claude Code