Skip to content

Fail fast when a plan cannot finish as declared - #11

Open
minerva-sky wants to merge 1 commit into
codenamev:mainfrom
minerva-sky:quality/execute-plan-validation
Open

Fail fast when a plan cannot finish as declared#11
minerva-sky wants to merge 1 commit into
codenamev:mainfrom
minerva-sky:quality/execute-plan-validation

Conversation

@minerva-sky

Copy link
Copy Markdown
Collaborator

What

execute_plan on a graph with an unknown dependency id or a dependency cycle silently returned a PlanExecutionResult with status :in_progress — from a method that had already returned — leaving the affected tasks stranded in :pending with no error, no log, and no record. Reproduced on main:

o = Agentic::PlanOrchestrator.new
o.add_task(t, ["no-such-id"], agent: ->(task) { {ok: true} })
o.execute_plan.status  # => :in_progress, forever

This PR validates the graph at the top of execute_plan, following the two precedents already in the file: ensure_agents_resolvable! (fail fast at execute time) and rewire_task (ArgumentError with Suggestions did-you-mean hints). All offenders are named in a single raise.

Deliberate scoping (shaped by adversarial review, see Panel below):

  • Only pending tasks are validated, and the cycle check runs on the pending-only subgraph — so a plan pruned with cancel_task (the only prune available for a cycle member) still executes its remainder exactly as before. A pending task wired to a canceled/failed dependency is not a structural error; it strands the same way as a dependency that fails mid-flight (that gap is Dependents of failed tasks finish the plan stranded in :pending with no execution record #10).
  • Validation is a snapshot at execute time: forward references (add_task(b, [a]) before a is added) keep working, and tasks added mid-run by hooks are not re-checked — the docs say so explicitly.

Also in the diff, found during the same review pass: a no-op statement (@dependencies[task_id]) removed from schedule_dependent_tasks, and transition_task_state's YARD doc reattached (it had drifted onto monotonic_now).

Tests

Seven new examples: unknown dep raises with did-you-mean hint; multiple offenders aggregated; cycle raises naming unrunnable tasks (incl. downstream); self-dependency; positional and needs: forward references still execute; cancel-pruned plan still runs its remainder. Full suite: 617 examples, 0 failures; standardrb clean.

Release note for the maintainer

Raising where a (contract-violating) result was previously returned should ride the next 0.3.0 with a Changed entry — per the repo's changelog-at-release convention I left CHANGELOG.md untouched. Suggested line: "execute_plan raises ArgumentError on unknown-dependency ids and dependency cycles among pending tasks; previously the runnable portion executed and the result reported :in_progress."

Intended labels: loop:quality (fork PRs can't self-label).

Panel

  • DHH — cleared. "Returning :in_progress from a method that has already returned is a lie... This is a bug fix wearing a bug fix's clothes." Nitpicks folded in (@raise doc on execute_plan).
  • Obie Fernandez — objected, then cleared. Two blocking objections: (1) validation was execution-state-blind and bricked cancel-pruned plans; (2) the doc promised run-time enforcement it didn't provide. Revision scoped validation to pending tasks (with a spec pinning his exact bricking scenario) and rewrote the contract as an explicit snapshot; he re-walked both scenarios against the shipped code and withdrew both.
  • Vladimir Dementyev — cleared. Verified no N+1 shape (one Kahn pass per execute, Levenshtein on error path only), no spec flake vector; nitpicks folded in (lazy known build, Regexp.escape on interpolated ids).
  • The gem consumer — cleared. Pre-1.0 + the old behavior contradicted execute_plan's own documented contract → bug fix, no deprecation cycle; ships coherently with the unreleased ensure_agents_resolvable! as execute-time validation in 0.3.0. Nitpick folded in (release-note request above).

execute_plan silently returned a result with status :in_progress when
the declared graph was unsatisfiable - a pending task depending on an
id that names no task in the plan, or a dependency cycle among pending
tasks - stranding those tasks in :pending forever with no error.
Validate the graph up front, mirroring the existing
ensure_agents_resolvable! fail-fast and rewire_task's did-you-mean
diagnostics, naming every offender in one pass.

Scoped deliberately: only pending tasks are validated, so a plan
pruned with cancel_task still runs its remainder; validation is a
snapshot at execute time, so forward references still work and tasks
added mid-run by hooks are not re-checked.

Also: drop a no-op statement in schedule_dependent_tasks and reattach
transition_task_state's doc comment, which had drifted onto
monotonic_now.
@minerva-sky minerva-sky added the loop:quality Code quality / refactoring loop label Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

loop:quality Code quality / refactoring loop

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant