Skip to content

Latest commit

 

History

History
227 lines (199 loc) · 13.1 KB

File metadata and controls

227 lines (199 loc) · 13.1 KB

Changelog

0.5.1 — 2026-07-12

Fixed

  • Release CI on Python 3.10. Documented a known limitation: cancellation reason strings (task.cancel("reason")) only propagate through asyncio.wait/task-await on Python 3.11+. On 3.10 the reason label is dropped (the CancelledError arrives with empty args); cancellation control flow — stop, drain of in-flight siblings and nested subgraph children, and snapshot/resume invariants — is unaffected. The tests that assert the reason label are now skipped below Python 3.11, and the behaviour is called out under "Known Limitations" in the README. No engine behaviour changed.

0.5.0 — 2026-07-11

First-class sub-workflows (NodeKind.SUBGRAPH)

A node can now carry a complete child workflow in config={"graph": {...}}. The child runs on its own scheduler instance — sharing the registry, event bus, event log, and snapshot store — under the derived run id {parent}/{node}@{visit}, with its own concurrency budget (unlimited by default; config={"concurrency_limit": N} or a runtime manager via Scheduler(..., child_concurrency=...)). The child's single TERMINAL node's payload becomes the node's result, and terminal execution ends the child run (in-flight child work is cancelled and awaited). Nesting can never deadlock: the parent node holds one parent slot while child nodes draw only on the child manager, and wiring an ancestor's manager into a child is rejected. Hierarchical resume, per-visit fresh re-entry (bounded by max_visits), black-box failure equivalence, and single-stream observability via the namespaced-id public contract all ship with it. If a crash lands after a child failure is persisted but before the parent records it, resume does not retry the failed handler. Because snapshot format 2 stores failure detail rather than the original Python exception type, that recovered failure surfaces as a descriptive RuntimeError and is recorded through the parent's ordinary node-failure path. Handler names gain the qualified publisher.package/function convention over the unchanged registry, with a transient "namespace" authoring directive (never emitted by to_dict()) and name@version reserved. New: UnlimitedPolicy, Scheduler(child_concurrency=..., max_subworkflow_depth=64) (inclusive positive-integer nesting backstop; ValueError on invalid values, chain-naming RuntimeError when crossed).

Forward incompatibility (release note): a serialized definition containing a subgraph node raises ValueError at NodeKind(...) parse on 0.4.x and earlier — older versions reject rather than misload it.

Intentional compatibility exception — role-aware event ids continue: every public root run now consults the configured event log at start and seeds its counter from the highest identifier for that graph_id, even when no snapshot remains. Private child runs consult the log only when their own snapshot loaded or the parent identified the visit as a recovery candidate. A fresh child visit in a non-recovered parent run does not read stale log-only history. A recovered parent conservatively supplies the hint on each SUBGRAPH node's first dispatch in the current run process, so that child may consult the log even when the derived visit is new. An event-log LookupError for an unknown id is treated as empty history only when lookup yields no events; after any event was yielded it propagates as truncated history whose maximum is unknowable. Actual resumes fall back to snapshot last_event_id values when the log has no parseable ids. Resumed runs therefore continue numbering instead of restarting at 1 and public-root and recovery-seeded-child (graph_id, event_id) pairs stay unique across crash/resume in append-only logs. Any composed invocation that reuses a root graph_id without loading its parent snapshot — including crash recovery with an event log but no snapshot store, where log-only recovery keeps the root's own ids collision-free but not child namespaces — may re-derive retained child ids whose engine-fresh counters restart at 1. Use a new root id to preserve lifetime pair uniqueness. Clearing or partitioning retained child logs instead starts a new consumer epoch; reset child cursors/deduplication state and include that epoch or partition outside the pair. Consumer migration: remove restart-at-1 assumptions for roots and recovery-seeded children, and retain cursors plus (graph_id, event_id) deduplication state across those recoveries; resumed snapshots carry the continued last_event_id values. No snapshot-format migration is required (format stays 2). Outside the counter-seeding and enumerated correctness corrections below, results, event types/order/payloads, and fresh-run state with no retained history are byte-identical (pinned by pre-implementation 0.4.0 golden fixtures).

Correctness corrections:

  • EventBus.emit() accounts for every ordinary listener task and routes every ordinary listener exception through configured on_error before propagating the first callback failure. If the caller is cancelled during the initial listener wait, unfinished listeners are cancelled and fully drained; failures completed before cancellation or raised during cleanup still reach on_error (or a warning). Repeated cancel() calls cannot replace the first caller-cancellation arguments. Listener cancellation, callback failure, and scheduler-suppressed sibling failures are warning-visible. A callback-owned CancelledError surfaces as RuntimeError with that cancellation as its cause, while actual cancellation of the emit() caller propagates unchanged after listener/callback accounting. These drains absorb repeated cancel requests, so a listener or on_error callback that never returns blocks cancellation indefinitely — callbacks must be written to terminate. The distinction prevents callback failure from being silently misclassified, as required by Constitution Principle XV.
  • Inherited 0.4 first-k/partial recovery now preserves the exact fire-decision input across a crash while the node is queued or running, including inside a sub-workflow. When snapshot persistence is configured, every decision-sensitive activation freezes a private, visit-scoped envelope in the existing version-2 last_payload field. Every aggregate with required < fan-in and every multi-input partial activation emits one immediate extra snapshot. Fresh execution retains original input identity; replay receives a new isolated marker copy, so mutable fresh-handler input cannot alter the decision in a reference-retaining snapshot. Snapshot keys and format remain unchanged. Re-entered aggregates now select candidates only from current-activation upstream marks, so stale prior-visit results cannot displace the input that reopened the quorum.
  • Lifecycle-dispatch failures now leave coherent resumable state. A propagated NODE_SCHEDULED failure closes/persists FAILED even though the handler never ran; a NODE_FAILED callback error cannot replace the original handler/child exception; a NODE_COMPLETED callback error propagates after completion and downstream readiness persist. Format-2 snapshots have no failure-origin marker, so resume treats the scheduled-listener failure as terminal; a deliberate retry requires backend-specific discard, migration, or reset of affected child and ancestor snapshots after the run is inactive, with the normal cleanup/re-execution consequences.
  • Cancelling a queued PrioritySemaphorePolicy acquisition removes its heap entry and wakes peers, so later users of a shared policy cannot deadlock on a stale waiter. Plain no-SUBGRAPH roots retain 0.4 cancellation timing: their cancelled run() may surface while in-flight handler tasks continue and later emit events or persist snapshots. Composed roots cancel and drain descendants before surfacing cancellation.
  • Composed definitions reject one-shot child iterators before consumption; repeatable iterables remain accepted. Validation/canonical serialization use iterative composition walks, so Python recursion limits do not preempt the configurable scheduler depth guard. Graph.to_dict() reparses embedded SUBGRAPH bodies; unparseable bodies and composition cycles raise ValueError. Canonical output drops unsupported schema-level keys while retaining arbitrary data inside supported Node.config and Edge.metadata containers.

These corrections require no data migration. Operators may observe additional warning records and coherent snapshots where failing lifecycle callbacks previously interrupted persistence, plus the narrow extra save for every decision-sensitive fire decision and corrected outputs in the previously broken queued/running recovery or aggregate re-entry cases.

Reserved identifier space: run ids and node ids of workflows that use sub-workflows must not contain / or @; in shared stores, treat root/…@n-shaped ids as engine-owned once any run uses sub-workflows.

State retention: every visit's child state is retained; cleanup is a backend-specific administrative action against the {root}/{node}@{visit} prefix. Never delete state for an active or resumable root run — deleting an unfinished child snapshot makes resume re-run that work. The core SnapshotStore protocol gains no list/delete operations.

0.4.0 — 2026-07-07

Concurrent execution of ready nodes within a single run

The scheduler now dispatches every ready node the moment it becomes ready (eager dispatch), bounded by the configured ConcurrencyManager. Fan-out branches genuinely run in parallel before their merge point; the capacity limit finally contends within a run, not just across runs.

Unchanged (no migration, byte-identical behavior):

  • Pipelines, switch chains, single loops, and wait-for-all merges produce identical results, event sequences, visit counts, and snapshots.
  • Serialized graph definitions and version-2 snapshots load and resume without modification. The snapshot format version remains 2.
  • SemaphorePolicy(limit=1) reproduces pre-0.4 sequential execution exactly (at most one node in flight, never more than one running entry in any persisted snapshot).
  • Failure surface: the first uncaught handler exception propagates from run() with its original type — never wrapped.

These bullets describe the 0.4.0 baseline. The 0.5.0 correctness section above adds one schema-preserving intermediate checkpoint for each decision-sensitive first-k/partial activation; ordinary results, events, and final snapshots remain unchanged outside the corrected recovery/re-entry cases.

New semantics (fan-out workflows):

  • First-k merges: an AGGREGATE with config={"required": k} fires at its k-th input completion, exactly once per activation, with the k earliest-completing results in graph-definition order (frozen at the fire decision — capacity queuing never changes the payload). k defaults to the full fan-in, i.e. wait-for-all. Inputs completing after the merge fired are recorded silently: no re-fire, no error. Previously, quorum merges could execute twice depending on the process hash seed; the new behavior is deterministic.
  • Partial-input nodes (allow_partial_upstream=True) receive the result of the input whose completion made them ready (previously the first-in-topology upstream's result, which could be stale or None).
  • Deterministic dispatch order: simultaneously-ready nodes dispatch in graph-definition order (previously seed-dependent set order).
  • Edge classification: an edge lying on a cycle keeps full loop re-entry semantics, including the defensive hard-stop for back-edges into non-terminal targets; forward (acyclic) edges into an already-activated target record the completion without error.
  • Failure teardown: in-flight sibling nodes are cancelled and awaited before run() raises; no lifecycle events or snapshot writes occur after the failure surfaces.

Observable changes to note when upgrading fan-out workflows:

  • Lifecycle events of concurrent nodes interleave. Per node and per visit, NODE_SCHEDULED still always precedes NODE_COMPLETED, and exactly one pair is emitted per visit.
  • NODE_SCHEDULED now means execution actually started (concurrency slot granted), not "queued".
  • Mid-run snapshots may contain several running entries; resume resets them to pending and re-executes them (at-least-once, as before).
  • Handlers in parallel branches can interleave at their await points. Handlers sharing mutable state must coordinate it themselves — error tolerance likewise stays handler-owned: catch inside the branch, return the error in the payload, and enforce success thresholds in the merge handler.

See specs/003-concurrent-scheduler/ for the full specification, behavioral contract, and design decisions.

0.3.0

  • Activation-frontier scheduling semantics (spec 002): pending seeds from entry nodes only; unselected SWITCH branches never enter pending.
  • Snapshot format version 2 with fail-fast rejection of unsupported versions.

0.2.x and earlier

  • Initial public iterations: loop-first graph engine, event bus, pluggable concurrency policies, snapshot/event-log persistence.