Skip to content

Commit 9b56c44

Browse files
Bump spec to v0.19.0; refresh docs and CHANGELOG
Submodule pin advances from v0.18.1 to v0.19.0 (proposal 0010 drain timeout). The retagged v0.19.0 commit carries the fixture 052 results literal fix backported from v0.18.1, so fixture 052 passes cleanly under the new pin. Runtime spec_version pins in pyproject and __init__ updated to match; smoke test asserts v0.19.0. CHANGELOG Unreleased section gains drain timeout + DrainSummary entries under Added; drain() return-type change noted under Changed; cumulative pin-bump summary updated to v0.17.0 -> v0.19.0 across four spec versions absorbed in this cycle. docs/concepts/observability.md drain section rewritten to describe the DrainSummary return value and the new Bounded drain (optional timeout) subsection.
1 parent cc6d258 commit 9b56c44

6 files changed

Lines changed: 38 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,22 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The
88

99
### Added
1010

11+
- **Bounded drain timeout on `CompiledGraph.drain()`** (proposal 0010, accepted in spec v0.19.0). `drain()` accepts an optional `timeout: float | None = None` parameter (non-negative seconds). When supplied, drain returns no later than the deadline; any observer events still queued or in-flight are reported as undelivered. Workers are cancelled cleanly so the compiled graph remains usable for subsequent invocations — partial delivery state from one drain does NOT leak into the next. Solves the "slow / hung / misbehaving observer blocks process exit" footgun for short-lived processes (CLIs, scripts, serverless functions). Observers SHOULD be cancellation-safe (idempotent writes, `try/finally` cleanup); the spec doesn't mandate it but the docs recommend it.
12+
- **`DrainSummary` frozen dataclass** at `openarmature.graph.DrainSummary`. Returned from every `drain()` call (with or without `timeout`). Fields: `undelivered_count: int`, `timeout_reached: bool`. The shape is consistent across timed and untimed drains — callers receive the same dataclass whether the timeout was supplied or not. Per the v0.19.0 contract the two declared fields are the spec-mandated minimum; richer diagnostic detail (per-observer counts, sampled event metadata) is reserved for follow-on PRs.
1113
- **Per-instance fan-out resume contract** (proposal 0009, accepted in spec v0.18.0). The engine now writes a checkpoint record at every `completed` event inside a fan-out instance (in addition to the existing outermost-graph + subgraph-internal + fan-out node completion saves). On resume the engine consults the saved record's `fan_out_progress` field and treats each instance as `completed` (skip, contribution rolls forward), `in_flight` (re-run from subgraph entry), or `not_started` (dispatch normally). The `append` reducer's no-double-merge guarantee holds across resume because `completed` is a one-shot accumulator state.
1214
- **`FanOutProgress` and `FanOutInstanceProgress` public dataclasses** on `openarmature.checkpoint`. The `CheckpointRecord.fan_out_progress` field is now `tuple[FanOutProgress, ...]` (default empty tuple), with per-instance state, result, and `completed_inner_positions` observability. Was a `None` placeholder under proposal 0008.
1315
- **`FanOutInternalSaveBatching` config** on `InMemoryCheckpointer`. Backends MAY opt into batching scoped to fan-out instance internal saves to bound the write volume of high-instance-count fan-outs. Outermost-graph, subgraph-internal, and the fan-out node's own completion save remain synchronous regardless. Default off. Buffered-but-unflushed saves are lost on crash by design; on resume, instances whose `completed` state was only buffered revert and re-run. Surfaces a new optional `save_fan_out_internal` / `save_fan_out_in_flight_failure` Checkpointer Protocol seam; backends that don't implement either fall back to the standard `save`.
1416
- **Patterns docs section** at `docs/patterns/`, sibling to Concepts. Seeded with four recipes drawn from downstream usage and proposal 0008's alternatives section: parameterized entry point, tool-dispatch-as-node, session-as-checkpoint-resume, and bypass-if-output-exists. Patterns are user-level how-to recipes composing existing primitives, not framework contracts; new patterns can be added without spec coordination. Each page follows a problem / approach / snippet / when this is the right pattern / when it isn't / cross-references structure.
1517

1618
### Changed
1719

20+
- **`CompiledGraph.drain()` return type** changed from `None` to `DrainSummary` (pre-1.0; per proposal 0010 v0.19.0 contract). Callers that ignored the return are unaffected — `await graph.drain()` discards the returned dataclass exactly as before. Callers that explicitly typed the return as `None` will need to update their annotation.
1821
- **Fan-out resume behavior** flipped from atomic restart (0008's v1 contract) to per-instance resume. A crash mid-fan-out used to re-run the entire fan-out on resume; now only the instances that did not complete-and-record their contribution re-run. The economics matter for large fan-outs of expensive work (LLM calls, long extractions): an 80% complete fan-out crash now restores 80% of its results rather than discarding them.
1922
- **`SQLiteCheckpointer` schema** picks up a new `fan_out_progress_blob` column (added via `ALTER TABLE` for backward compatibility with pre-0009 databases). Pre-0009 rows back-fill as NULL on load and round-trip as the empty-tuple default. Both `pickle` and `json` serialization modes round-trip the new field.
2023

2124
### Notes
2225

23-
- **Pinned spec version bumped from v0.17.0 to v0.18.1 over this Unreleased cycle.** Three spec versions absorbed: v0.17.1 (proposal 0019, multi-provider wire-format extension; purely textual reframe of llm-provider §8 as a catalog of wire-format mappings, OpenAI-compatible body nested under §8.1, code references updated to §8.1 / §8.1.1 / §8.1.2 / §8.1.3 / §8.1.5.1 / §8.1.1.1), v0.18.0 (proposal 0009, per-instance fan-out resume; pipeline-utilities §10.3 / §10.7 revised, §10.11 added with per-instance state machine plus composition rules plus configurable batching; the `append` reducer no-double-merge invariant from §10.11.1 is the load-bearing correctness story; see Added / Changed above), and v0.18.1 (fixture-only patch on `release/v0.18.1` correcting an off-by-one literal in fixture 052's expected `results`). All existing conformance fixtures continue to pass.
26+
- **Pinned spec version bumped from v0.17.0 to v0.19.0 over this Unreleased cycle.** Four spec versions absorbed: v0.17.1 (proposal 0019, multi-provider wire-format extension; purely textual reframe of llm-provider §8 as a catalog of wire-format mappings, OpenAI-compatible body nested under §8.1, code references updated to §8.1 / §8.1.1 / §8.1.2 / §8.1.3 / §8.1.5.1 / §8.1.1.1), v0.18.0 (proposal 0009, per-instance fan-out resume; pipeline-utilities §10.3 / §10.7 revised, §10.11 added with per-instance state machine plus composition rules plus configurable batching; the `append` reducer no-double-merge invariant from §10.11.1 is the load-bearing correctness story; see Added / Changed above), v0.18.1 (fixture-only patch on `release/v0.18.1` correcting an off-by-one literal in fixture 052's expected `results`), and v0.19.0 (proposal 0010, bounded drain timeout; graph-engine §6 amended with the `timeout` parameter and `DrainSummary` return contract; see Added / Changed above). All existing conformance fixtures continue to pass.
2427

2528
## [0.8.0] — 2026-05-23
2629

docs/concepts/observability.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,11 +221,13 @@ finished. For long-running services that's fine. For short-lived
221221
processes (scripts, serverless, CLIs), events dispatched late in the
222222
run may not be delivered before the process exits.
223223

224-
`drain()` blocks until every dispatched event has been delivered:
224+
`drain()` waits until every dispatched event has been delivered and
225+
returns a `DrainSummary` reporting the outcome:
225226

226227
```python
227228
final = await compiled.invoke(initial)
228-
await compiled.drain()
229+
summary = await compiled.drain()
230+
# DrainSummary(undelivered_count=0, timeout_reached=False)
229231
```
230232

231233
- Per-graph, not per-invoke. Drain awaits *all* prior invocations'
@@ -239,6 +241,32 @@ await compiled.drain()
239241
If you forget `drain()` in a CLI, the symptom is an empty trace file
240242
or missing log entries.
241243

244+
### Bounded drain (optional timeout)
245+
246+
`drain()` accepts an optional `timeout` parameter (non-negative
247+
seconds) — `await compiled.drain(timeout=5.0)` bounds the wait at five
248+
seconds. When the deadline fires, in-flight workers are cancelled
249+
cleanly so the compiled graph stays usable for subsequent invocations
250+
— partial delivery state from one drain does NOT leak into the next.
251+
252+
The returned `DrainSummary` carries:
253+
254+
- `timeout_reached: bool``True` only when the timeout actually
255+
fired. A drain that finishes before the deadline reports `False`.
256+
- `undelivered_count: int` — events dispatched but not fully delivered
257+
to every subscribed observer before the deadline. Always `0` when
258+
`timeout_reached is False`.
259+
260+
Observers **should** be cancellation-safe (idempotent writes,
261+
`try/finally` cleanup) so that interruption by drain timeout does not
262+
leave partial side effects in an inconsistent state.
263+
264+
When to set a timeout: short-lived processes (CLIs, scripts,
265+
serverless functions) where a misbehaving observer holding drain
266+
indefinitely would stall process exit. Long-running services that
267+
control their own lifecycle can leave the timeout off and let drain
268+
wait for natural completion.
269+
242270
## Error isolation
243271

244272
An observer that raises:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ Repository = "https://github.com/LunarCommand/openarmature-python"
4848
Specification = "https://github.com/LunarCommand/openarmature-spec"
4949

5050
[tool.openarmature]
51-
spec_version = "0.18.1"
51+
spec_version = "0.19.0"
5252

5353
[dependency-groups]
5454
dev = [

src/openarmature/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""OpenArmature: workflow framework for LLM pipelines and tool-calling agents."""
22

33
__version__ = "0.8.0"
4-
__spec_version__ = "0.18.1"
4+
__spec_version__ = "0.19.0"

tests/test_smoke.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
def test_package_versions() -> None:
1111
assert openarmature.__version__ == "0.8.0"
12-
assert openarmature.__spec_version__ == "0.18.1"
12+
assert openarmature.__spec_version__ == "0.19.0"
1313

1414

1515
def test_spec_version_matches_pyproject() -> None:

0 commit comments

Comments
 (0)