Observed
When a task fails permanently (retries exhausted or non-retryable), its dependents never run — correct — but they end the plan still in :pending, with no entry in results and nothing recording why they never ran. Reproduced on main:
o = Agentic::PlanOrchestrator.new(retry_policy: {max_retries: 0})
o.add_task(f = failing_task, [], agent: ->(t) { raise "boom" })
o.add_task(d = dependent_task, [f], agent: ->(t) { {ok: true} })
r = o.execute_plan
r.status # => :partial_failure (fine)
o.execution_state[:pending] # => still contains d.id
o.results.key?(d.id) # => false — no record at all
The existing spec "doesn't execute dependent tasks when dependencies fail" pins the not running part, but the execution history is silent about the skip. For a plan with a deep graph, the answer to "why didn't task X run?" is currently "diff the task list against results and infer."
Why it matters (WORLD.md: Direction)
"The plan is the product... better failure recovery, better execution history." A terminal state that says skipped because dependency Y failed is execution history; :pending-forever is its absence. It also matters for tooling: overall_status works today, but any UI/journal rendering of a finished plan shows skipped tasks as if they might still run.
Proposal (L0 — needs maintainer direction, this is public API surface)
Add a :skipped terminal state to execution_state, entered when a task's dependency reaches :failed (or :canceled) with no retry remaining. Record a TaskExecutionResult failure (or a dedicated skip record) naming the failed dependency. Open design questions:
- Is
:skipped distinct from :canceled, or is cascading-cancel the same concept? (I'd argue distinct: canceled = someone chose; skipped = the graph decided.)
- Does
overall_status change? (:partial_failure already covers it; probably no.)
PlanExecutionResult#task_result currently returns nil for these — nil→record is observable behavior; consumers may nil-check today.
Not proposing a PR: state-machine additions ripple into to_h, journal, CLI rendering, and semver. If the direction gets a 👍 I can follow up with a scoped PR.
(Filed by the quality loop. Intended labels applied: loop:quality, status:analyzed.)
Observed
When a task fails permanently (retries exhausted or non-retryable), its dependents never run — correct — but they end the plan still in
:pending, with no entry inresultsand nothing recording why they never ran. Reproduced on main:The existing spec "doesn't execute dependent tasks when dependencies fail" pins the not running part, but the execution history is silent about the skip. For a plan with a deep graph, the answer to "why didn't task X run?" is currently "diff the task list against results and infer."
Why it matters (WORLD.md: Direction)
"The plan is the product... better failure recovery, better execution history." A terminal state that says skipped because dependency Y failed is execution history;
:pending-forever is its absence. It also matters for tooling:overall_statusworks today, but any UI/journal rendering of a finished plan shows skipped tasks as if they might still run.Proposal (L0 — needs maintainer direction, this is public API surface)
Add a
:skippedterminal state toexecution_state, entered when a task's dependency reaches:failed(or:canceled) with no retry remaining. Record aTaskExecutionResultfailure (or a dedicated skip record) naming the failed dependency. Open design questions::skippeddistinct from:canceled, or is cascading-cancel the same concept? (I'd argue distinct: canceled = someone chose; skipped = the graph decided.)overall_statuschange? (:partial_failurealready covers it; probably no.)PlanExecutionResult#task_resultcurrently returns nil for these — nil→record is observable behavior; consumers may nil-check today.Not proposing a PR: state-machine additions ripple into
to_h, journal, CLI rendering, and semver. If the direction gets a 👍 I can follow up with a scoped PR.(Filed by the quality loop. Intended labels applied:
loop:quality,status:analyzed.)