Skip to content

feat(workflows): per-attempt task timeout and continue-on-error - #212

Draft
Tsuyoshi Ushio (TsuyoshiUshio) wants to merge 1 commit into
mainfrom
tsuyoshiushio-workflow-task-timeout-continuation
Draft

feat(workflows): per-attempt task timeout and continue-on-error#212
Tsuyoshi Ushio (TsuyoshiUshio) wants to merge 1 commit into
mainfrom
tsuyoshiushio-workflow-task-timeout-continuation

Conversation

@TsuyoshiUshio

Copy link
Copy Markdown
Contributor

Stack

This is PR 1 of 2 replacing the stale #170 decomposition stack.

Order PR Branch Base
1 this PR — per-attempt timeout + continue_on_error tsuyoshiushio-workflow-task-timeout-continuation main
2 execution observability (telemetry + custom_status v3) not yet opened this branch

The retry foundation these slices depended on landed through #193 and #207, not
through the original #185/#186 branches. #185 is closed; #186 and #187 are stale
and will be closed in favor of this stack.

Why this is a reimplementation, not a rebase

#186's branch still carries its own versions of native_retry.py,
activity.py, engine.py, and schema.py from before #189/#193/#207 landed,
so every file it touches conflicts with the shipped contracts wholesale.
Rebasing would reintroduce already-merged foundation work and enlarge review
scope, so this stack reimplements only the residual slices against current
main. FRD Decision 82 records that.

What this PR contains

Design only. This is the FRD slice for review before implementation
(AGENTS.md lifecycle phase 2). No product code changes yet — implementation
commits follow on this branch once the design is signed off.

docs/frds/0004-dynamic-workflows.md gains:

  • §4 Attempt timeout and continuation — the full design: all three
    execution fields become optional so a deadline may be declared alone;
    per-field @workflow_tool precedence; the attempt deadline at the Activity
    boundary under a new retryable timeout classification; and the
    continuation contract.
  • §4 Delivery plan — the current slice's row, and feat(workflows): declare retry on workflow tools #207's row marked shipped.
  • §5 Decisions 82-96.
  • §6 — contract, attempt-deadline, DAG-continuation, and real-host E2E
    coverage.
  • §7/§8 — docs impact, the reimplementation approval, and the architecture
    review record.

Design highlights

Fail-fast is preserved. The reviewed alternative — collecting every
instance outcome whenever any node in the wave opted into continuation — would
let a non-continuable failure wait behind a slow sibling or a 24-hour timer and
could report a different node as the cause. Instead the selection loop decides
per completed instance: it keeps awaiting only when that instance both declared
continuation and failed continuably. Everything else raises immediately, as
today. Both inputs are persisted or derived from the outcome, so the choice is
replay-stable (Decision 88).

Continuation is a closed contract. Continuability is derived from the
persisted failure kind through a runtime-owned map rather than added to the
outcome envelope, whose exact key set must keep validating failures written by
the previous runtime. timeout, handler_transient, handler_terminal, and
execution_unknown are continuable; authorization and handler_contract
never are, and an opaque Durable failure with no classification still fails the
workflow (Decisions 87, 93).

A continued node commits a bounded object. {"failed": true, "error_code", "error", "kind"} and nothing else — reusing the workflow-level failure
envelope would embed the aggregate results map in every continued instance
and let a fully failed for_each expansion grow quadratically (Decision 94).
The node stays completed, so no scheduler state or persisted aggregate
contract changes.

The deadline bounds the wait, not the worker. A workflow tool handler runs
on a worker thread through asyncio.to_thread and cannot be cancelled from
outside, so a timed-out attempt is reported while it may still run; a Sub Agent
delivery is asynchronous and is cancelled. Both are the at-least-once exposure
the stable idempotency_key already exists for. A Sub Agent's own resolved
specialist timeout stays independent and keeps its handler_transient
classification — the first bound to expire wins (Decisions 85, 95).

Compatibility

  • Both persisted keys are NotRequired and written only when authored, so a
    task that declares neither freezes a payload byte-identical to the one the
    retry-only runtime wrote and replays through exactly the same path
    (Decision 91).
  • A wave with no declared continuation keeps its current failure timing and its
    current reported cause.
  • Shipped error codes are unchanged; workflow_execution_policy_invalid covers
    only what this slice adds.
  • Downgrade is not supported while orchestrations started by this version
    are in flight: an older runtime's closed retryability map rejects the
    timeout kind and ignores both new keys. Drain or terminate first
    (Decision 92).

Reviews

An independent architecture review raised four blocking findings — whole-wave
collection deferring non-continuable failures, unaddressed downgrade, an
underspecified continuation contract, and missing coverage of mixed waves,
cancellation races, and for_each failure shapes. All are resolved above and
recorded in §8.

Out of scope

Telemetry, custom_status schema v3, and the UI — those are PR 2 of this
stack. No config/schema.py change, so front-matter-reference.md is
unaffected.

The retry foundation this slice depended on landed through PRs #193 and #207
rather than through the stale PR #186 branch, whose versions of `schema.py`,
`activity.py`, and `engine.py` now conflict with the shipped contracts
wholesale. Record the remaining timeout and continue-on-error slice as a clean
reimplementation against current `main` instead of a rebase.

The design makes all three `execution` fields optional so a deadline can be
declared alone, resolves `@workflow_tool` precedence per field, bounds one
attempt at the Activity boundary under a new retryable `timeout`
classification, and lets a declared-continuation node commit a bounded
controlled-failure result so its dependents run.

Fail-fast is preserved for everything that cannot be continued: the wave
selection loop keeps awaiting only when the completed instance both declared
continuation and failed continuably, so a non-continuable failure never waits
behind a slow sibling or a long timer. Both persisted keys are written only
when authored, so a task that declares neither freezes and replays the payload
the retry-only runtime wrote.

An independent architecture review raised four blocking findings — whole-wave
collection, unaddressed downgrade, an underspecified continuation contract, and
missing coverage of mixed waves, cancellation races, and `for_each` failure
shapes. Decisions 88 and 92-96 and the expanded test plan resolve them.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f32b40a3-603f-48d9-ad29-43179afe265b
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