Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 0.3.0)'
description: 'Version to release (e.g., 0.5.0)'
required: true
type: string
dry_run:
Expand Down Expand Up @@ -77,7 +77,7 @@ jobs:
- name: Validate version format
run: |
if ! [[ "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must be in format X.Y.Z (e.g., 0.3.0)"
echo "Error: Version must be in format X.Y.Z (e.g., 0.5.0)"
exit 1
fi

Expand Down
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.5.0] - 2026-05-08

### Added
- `AppRunner::create_iterative_session(...)` and `AppRunner::invoke_next(...)` for repeated graph invocations under one durable session lineage.
- `RunnerError::InvalidIterativeEntry` for invalid iterative entry nodes.
- Typed state-slot helpers: `StateKey<T>`, `StateSnapshot::get_typed(...)`, `StateSnapshot::require_typed(...)`, `VersionedState::add_typed_extra(...)`, `VersionedStateBuilder::with_typed_extra(...)`, and `NodePartial::with_typed_extra(...)`.
- Runtime clock injection through `RuntimeConfig::with_clock(...)`, `AppRunnerBuilder::clock(...)`, and `NodeContext::now_unix_ms()`.
- Optional node event metadata for `invocation_id` and `now_unix_ms` when runtime metadata is configured.
- `INVOCATION_END_SCOPE` and `AppRunner::finish_iterative_session(...)` for long-lived iterative event streams.
- Graph and run metadata helpers: `App::graph_metadata()`, `App::graph_definition_hash()`, `RuntimeConfig::config_hash()`, and `AppRunner::run_metadata()`.
- `Reducer::definition_label(...)` so graph metadata can distinguish reducer implementations, not only reducer counts.
- Replay conformance helpers in `weavegraph::runtimes::replay` for normalized event comparison, final-state comparison, and reusable replay assertions.

### Notes
- This feedback package ships as `0.5.0` rather than `0.4.1` because it changes the public runtime surface, adds public error enum variants/types, and extends public structs.
- New public metadata/context structs are marked `#[non_exhaustive]` where they are expected to grow before v1.

## [0.4.0] - 2026-04-01

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "weavegraph"
version = "0.4.0"
version = "0.5.0"
edition = "2024"
description = "Graph-driven, concurrent agent workflow framework with versioned state, deterministic barrier merges, and rich diagnostics."
license = "MIT"
Expand Down
86 changes: 49 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ Weavegraph lets you build robust, concurrent, stateful workflows using a graph-b
- Concurrent graph execution with dependency resolution
- Type-safe, role-based message system
- Versioned state with snapshot isolation
- Typed state slots for schema-versioned JSON payloads
- Structured error handling and diagnostics
- Built-in event streaming and observability
- Flexible persistence: SQLite or in-memory
- Flexible persistence: SQLite, PostgreSQL, or in-memory
- Conditional routing and dynamic edges
- Iterative checkpointed sessions for repeated invocations
- Replay conformance helpers and deterministic graph/run metadata
- Ergonomic APIs and comprehensive examples

## Install
Expand All @@ -33,10 +36,10 @@ Add to your `Cargo.toml`:

```toml
[dependencies]
weavegraph = "0.3"
weavegraph = "0.5"
```

> **Note:** Examples and instructions in this README are current as of 0.3.x. For upgrading from 0.2.x, see [MIGRATION.md](docs/MIGRATION.md).
> **Note:** Examples and instructions in this README are current as of 0.5.x. For upgrade notes across pre-1.0 releases, see [MIGRATION.md](docs/MIGRATION.md).

## Dependency Compatibility

Expand Down Expand Up @@ -64,40 +67,40 @@ See [Cargo.toml](Cargo.toml) for complete dependency versions and feature config

```rust
use weavegraph::{
graphs::GraphBuilder,
message::Message,
node::{Node, NodeContext, NodePartial},
state::VersionedState,
graphs::GraphBuilder,
message::Message,
node::{Node, NodeContext, NodePartial},
state::VersionedState,
};
use async_trait::async_trait;

struct HelloNode;

#[async_trait]
impl Node for HelloNode {
async fn run(
&self,
_snapshot: weavegraph::state::StateSnapshot,
_ctx: NodeContext,
) -> Result<NodePartial, weavegraph::node::NodeError> {
Ok(NodePartial::new().with_messages(vec![Message::assistant("Hello, world!")]))
}
async fn run(
&self,
_snapshot: weavegraph::state::StateSnapshot,
_ctx: NodeContext,
) -> Result<NodePartial, weavegraph::node::NodeError> {
Ok(NodePartial::new().with_messages(vec![Message::assistant("Hello, world!")]))
}
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use weavegraph::types::NodeKind;
let app = GraphBuilder::new()
.add_node(NodeKind::Custom("hello".into()), HelloNode)
.add_edge(NodeKind::Start, NodeKind::Custom("hello".into()))
.add_edge(NodeKind::Custom("hello".into()), NodeKind::End)
.compile()?;
let state = VersionedState::new_with_user_message("Hi!");
let result = app.invoke(state).await?;
for message in result.messages.snapshot() {
println!("{}: {}", message.role, message.content);
}
Ok(())
use weavegraph::types::NodeKind;
let app = GraphBuilder::new()
.add_node(NodeKind::Custom("hello".into()), HelloNode)
.add_edge(NodeKind::Start, NodeKind::Custom("hello".into()))
.add_edge(NodeKind::Custom("hello".into()), NodeKind::End)
.compile()?;
let state = VersionedState::new_with_user_message("Hi!");
let result = app.invoke(state).await?;
for message in result.messages.snapshot() {
println!("{}: {}", message.role, message.content);
}
Ok(())
}
```
> NOTE: `NodeKind::Start` and `NodeKind::End` are virtual structural endpoints.
Expand All @@ -109,27 +112,36 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
For testing and ephemeral workflows use the InMemory checkpointer:

```rust
use weavegraph::runtimes::{AppRunner, CheckpointerType};

// After compiling the graph into an `App`:
let runner = AppRunner::builder()
.app(app)
.checkpointer(CheckpointerType::InMemory)
.build()
.await;
.app(app)
.checkpointer(CheckpointerType::InMemory)
.build()
.await;
```

Run the comprehensive test suite:

```bash
# All tests with output
cargo test --all -- --nocapture
# Full integration test suite
cargo nextest run

# Documentation examples
cargo test --doc

# Lints used by CI
cargo clippy --all-features --all-targets -- -D warnings
cargo clippy --no-default-features --lib -- -D warnings

# Specific test categories
cargo test schedulers:: -- --nocapture
cargo test channels:: -- --nocapture
cargo test integration:: -- --nocapture
cargo test --test schedulers
cargo test --test event_bus
cargo test --test runtimes_runner
```

Property-based testing with `proptest` ensures correctness across edge cases.
Property-based testing with `proptest` and fuzz harnesses under [fuzz/](fuzz/) exercise edge cases across graph routing, event serialization, replay comparison, and typed state slots.

## CI Parity

Expand All @@ -151,7 +163,7 @@ Before merging or cutting a release, run full local parity checks:

## Resources

- **[Migration Guide](docs/MIGRATION.md)** - Upgrade paths between releases (0.2.x → 0.3.x and beyond)
- **[Migration Guide](docs/MIGRATION.md)** - Upgrade paths between pre-1.0 releases
- **[Architecture Guide](docs/ARCHITECTURE.md)** - Deep dive into core design and internals
- **[Examples Directory](examples/)** - Runnable patterns: graph execution, scheduling, streaming, persistence, and more

Expand Down
110 changes: 110 additions & 0 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,116 @@ migration guidance for upgrading your code.

---

## v0.5.0

### Overview

v0.5.0 is the recommended target for the WeaveQuant production feedback work. The changes add new public runtime APIs and a public `RunnerError` variant, so they should not ship as a `0.4.1` patch.

### New Runtime APIs

Use `AppRunner::create_iterative_session(...)` and `AppRunner::invoke_next(...)` when one durable session should process many logical inputs:

```rust
runner
.create_iterative_session(run_id.clone(), initial_state, NodeKind::Start)
.await?;

runner
.invoke_next(&run_id, input_patch, NodeKind::Start)
.await?;
```

`NodeKind::Start` resolves to the graph's normal Start outgoing frontier. A registered custom node can be supplied for narrower re-entry. `NodeKind::End` now returns `RunnerError::InvalidIterativeEntry` when used as an iterative entry.

When an `AppRunner` event stream is subscribed before iterative execution, each `invoke_next(...)` emits `INVOCATION_END_SCOPE` and keeps the stream open for the next logical input. Call `finish_iterative_session(...)` after the final input to emit the normal `STREAM_END_SCOPE` sentinel and close the stream.

### Typed State Slots

Typed state slots are a thin, JSON-compatible layer over `VersionedState.extra`. Define a reusable key in the domain crate, then read and write typed payloads without hand-rolled `serde_json` calls at every node boundary:

```rust
use serde::{Deserialize, Serialize};
use weavegraph::node::NodePartial;
use weavegraph::state::{StateKey, StateSnapshot};

#[derive(Serialize, Deserialize)]
struct PortfolioState {
cash_cents: i64,
}

const PORTFOLIO: StateKey<PortfolioState> = StateKey::new("wq", "portfolio", 1);

fn read(snapshot: &StateSnapshot) -> Result<Option<PortfolioState>, weavegraph::state::StateSlotError> {
snapshot.get_typed(PORTFOLIO)
}

fn write(value: PortfolioState) -> Result<NodePartial, weavegraph::state::StateSlotError> {
NodePartial::new().with_typed_extra(PORTFOLIO, value)
}
```

The storage key is namespaced and versioned as `namespace:name:v{schema_version}`. Untyped `extra` remains available.

### Deterministic Runtime Clock

Use the existing `Clock` abstraction to inject deterministic time into nodes and emitted node-event metadata:

```rust
use std::sync::Arc;
use weavegraph::runtimes::{AppRunner, CheckpointerType};
use weavegraph::utils::clock::MockClock;

let runner = AppRunner::builder()
.app(app)
.checkpointer(CheckpointerType::InMemory)
.clock(Arc::new(MockClock::new(1_700_000_000)))
.build()
.await;
```

Inside a node, call `ctx.now_unix_ms()` and `ctx.invocation_id()`. `NodeContext::new(...)` is now the easiest way to construct contexts in tests.

### Metadata Helpers

Compiled graphs and runners expose deterministic metadata helpers for audit labels and replay manifests:

```rust
let graph = app.graph_metadata();
let graph_hash = app.graph_definition_hash();
let run = runner.run_metadata();
```

The graph hash includes node kinds, edges, conditional edge registrations, and reducer definition labels. It does not inspect closure bodies for conditional predicates. Custom reducers can override `Reducer::definition_label(...)` when a durable audit label is preferable to the default Rust type path.

### Replay Conformance Helpers

Replay helpers live under `weavegraph::runtimes::replay` and are re-exported from `weavegraph::runtimes`:

```rust
use weavegraph::runtimes::{ReplayRun, compare_replay_runs};

let expected = ReplayRun::new(expected_state, expected_events);
let actual = ReplayRun::new(actual_state, actual_events);

compare_replay_runs(&expected, &actual).assert_matches()?;
```

`normalize_event(...)` strips runtime timestamps. Use `compare_event_sequences_with(...)` or `compare_replay_runs_with(...)` when domain events need semantic normalization.

### Compatibility Notes

- `App::invoke(...)`, `AppRunner::create_session(...)`, and `AppRunner::run_until_complete(...)` keep their existing behavior.
- `RunnerError` is an exhaustive public enum. Code that matches every variant must handle `InvalidIterativeEntry` after upgrading.
- `GraphMetadata`, `RunMetadata`, `ReplayRun`, `NodeContext`, and `SchedulerRunContext` are `#[non_exhaustive]`; use provided constructors/builders instead of external struct literals.
- `Reducer` gains a default `definition_label(...)` method for graph metadata. Existing reducer implementations do not need to change unless they want a custom stable label.
- `RuntimeConfig` gains a public `clock` field. Code using struct literals should add `clock: None` or switch to `RuntimeConfig::default()` / builder-style methods.
- `NodeContext` gains `clock` and `invocation_id` fields. Tests should prefer `NodeContext::new(...)` over struct literals.
- Direct calls to `Scheduler::superstep(...)` must pass the optional clock and invocation ID arguments.
- Iterative sessions keep step numbers monotonic across invocations and reload checkpoints through the existing checkpointer path.

---

## v0.4.0

### Overview
Expand Down
Loading
Loading