Voluta.Generators
0.1.0
See the version list below for details.
dotnet add package Voluta.Generators --version 0.1.0
NuGet\Install-Package Voluta.Generators -Version 0.1.0
<PackageReference Include="Voluta.Generators" Version="0.1.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="Voluta.Generators" Version="0.1.0" />
<PackageReference Include="Voluta.Generators"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add Voluta.Generators --version 0.1.0
#r "nuget: Voluta.Generators, 0.1.0"
#:package Voluta.Generators@0.1.0
#addin nuget:?package=Voluta.Generators&version=0.1.0
#tool nuget:?package=Voluta.Generators&version=0.1.0
<p align="center"> <a href="https://github.com/dot-stbl/voluta"> <img src="https://raw.githubusercontent.com/dot-stbl/voluta/main/assets/banner.png" alt="Voluta — stateful, cyclic, durable agent graphs for .NET"> </a> </p>
<p align="center"> <a href="https://github.com/dot-stbl/voluta/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/dot-stbl/voluta/actions/workflows/ci.yml/badge.svg" /></a> <a href="https://dotnet.microsoft.com/download/dotnet/10.0"><img alt=".NET 10" src="https://img.shields.io/badge/.NET-10.0-512BD4?style=flat-square&logo=dotnet&logoColor=white" /></a> <a href="https://github.com/dot-stbl/voluta/blob/main/LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" /></a> <a href="https://github.com/dot-stbl/voluta/issues/1"><img alt="Status: pre-release" src="https://img.shields.io/badge/status-pre--release-orange?style=flat-square" /></a> </p>
Stateful agent graphs for .NET — cycles, typed channels, checkpoints, streaming, and human-in-the-loop. You describe nodes and edges; Voluta runs them in Pregel-style supersteps and can pause a thread for days, then resume in another process.
→ cycles not DAGs
→ typed channels not dictionary soup
→ checkpoints not in-process memory
→ .NET-native not a Python port
→ AOT-ready core not a kitchen sink
Pre-release. On main: Pregel engine, checkpointers (InMemory · File · EF Core · S3),
Send / subgraph helpers, source generator, Testing, Agents.AI (MEAI/MAF), MapVolutaUI,
samples, BenchmarkDotNet. Nothing is on NuGet yet; the 0.1 tag is the next milestone
(epic #1). Until then, reference projects
from source — see Quick Start.
See it in action
A ReAct-style loop (agent ⇄ tools) with no LLM — pure simulation so you can run it offline.
Real output of dotnet run --project samples/HelloWorld:
◆ voluta · HelloWorld
simulated ReAct · agent ⇄ tools · no LLM
thread react-sample-1
stream Updates
────────────────────────────────────────
[agent] round 0 · requesting tools
· step 1 Updates [agent]
status ← tools
messages ← agent: call get_weather (round 1)
[tools] executing simulated tool · round 1
· step 2 Updates [tools]
tool_rounds ← 1
messages ← tools: observation — temp=12C (round 1)
status ← agent
…
[agent] enough tool data · finishing
· step 5 Updates [agent]
status ← done
messages ← agent: final answer — cloudy, 12°C in Oslo
· step 5 End [—]
▸ result
status Done
✓ done
The loop exists because one edge is conditional. messages accumulates (Append);
status replaces (LastValue):
var graph = new StateGraph()
.AddChannel("messages", ChannelKind.Append)
.AddChannel("status", ChannelKind.LastValue)
.AddChannel("tool_rounds", ChannelKind.LastValue)
.AddNode("agent", AgentNodeAsync)
.AddNode("tools", ToolsNodeAsync)
.AddEdge(GraphConstants.Start, "agent")
.AddConditionalEdges(
"agent",
static context => context.Read<string>("status") == "tools" ? "tools" : GraphConstants.End)
.AddEdge("tools", "agent")
.Compile(checkpointer, new CompileOptions { RecursionLimit = 32 });
await foreach (var item in graph.StreamAsync(
input,
new RunOptions { ThreadId = "react-1", StreamMode = StreamMode.Updates }))
{
// item.Kind, item.NodeNames, item.Writes
}
<details> <summary><strong>Human-in-the-loop — interrupt, then resume (even later, another process)</strong></summary>
A node returns NodeResult.Interrupt instead of writes. The run stops, the checkpoint holds the
payload, and ResumeInvokeAsync continues with a Command. Real output of
samples/InterruptResume:
▸ invoke · expect interrupt
· step 0 Start [—]
[gate] interrupting for human approval
· step 1 Interrupt [gate]
payload ← { action = transfer, amount = 50, currency = USD }
checkpoint Interrupted
▸ resume · Command.Kind = approve
[gate] resumed · payload=ok · approving
· step 2 End [—]
status Done
✓ done
static Task<NodeResult> GateNodeAsync(GraphContext context, CancellationToken cancellationToken)
{
if (context.ResumePayload is null)
{
return Task.FromResult<NodeResult>(
NodeResult.Interrupt(new { action = "transfer", amount = 50, currency = "USD" }));
}
return Task.FromResult<NodeResult>(
NodeResult.Continue(new ChannelWrite("messages", "gate: transfer approved")));
}
// first process
var terminal = await graph.InvokeAsync(input, new RunOptions { ThreadId = "hitl-1" });
// terminal.Kind == StreamEventKind.Interrupt
// later — same or new process, same checkpointer root / store
var done = await graph.ResumeInvokeAsync(
"hitl-1",
new Command
{
Kind = "approve",
Payload = "ok",
// optional: patch channels before the node re-runs
Values = new Dictionary<string, object?> { ["decision"] = "go" },
});
</details>
<details> <summary><strong>Typed state with <code>[GraphState]</code> (source generator)</strong></summary>
String channel names work. When you want compile-time names and updates that only write what you set:
[GraphState]
public partial class ReviewState
{
[Channel(ChannelKind.Append)]
public IList<object?> Notes { get; set; } = new List<object?>();
[Channel(ChannelKind.LastValue)]
public string? Verdict { get; set; }
}
var graph = new StateGraph()
.AddChannels(ReviewState.CreateSchema())
.AddNode(
"review",
static (context, cancellationToken) => Task.FromResult<NodeResult>(
NodeResult.Continue(
new ReviewState.ReviewStateUpdate { Verdict = "approved" }.ToWrites())))
.AddEdge(GraphConstants.Start, "review")
.AddEdge("review", GraphConstants.End)
.Compile(new InMemoryCheckpointer());
Unset properties emit no write. An explicit null is a clear. Interface-typed properties use
OptionalValue<T>.Of(value).
</details>
<details> <summary><strong>Durable file checkpoint (survive process restart)</strong></summary>
// Host DI (recommended) — same root on every process
services.AddVoluta(v =>
{
v.Checkpoints.UseFile("/var/lib/voluta/threads");
v.Graph((sp, checkpointer) => new StateGraph()
// … nodes …
.Compile(checkpointer, new CompileOptions { Services = sp }));
});
// process A
var graph = sp.GetRequiredService<CompiledGraph>();
await graph.InvokeAsync(input, new RunOptions { ThreadId = "order-9" });
// → Interrupted
// process B (new host, same UseFile root) — resume
await graph.ResumeInvokeAsync("order-9", new Command { Kind = "approve", Payload = "ok" });
Values serialize with System.Text.Json — prefer JSON-friendly types (strings, numbers, lists of
primitives). For tests, InMemoryCheckpointer is enough; every storage implements
ICheckpointer and can run CheckpointerConformance.RunAllAsync. Provider types
(FileCheckpointer, EF, S3) are constructed via Use* only — not new.
</details>
<details> <summary><strong>Fan-out with <code>Send</code> / subgraphs</strong></summary>
// Map one item to many dynamic tasks (runtime-scheduled nodes)
return NodeResult.ContinueWithSends(
new Send("worker", payload: orderLine1),
new Send("worker", payload: orderLine2));
// Nest a compiled graph as a single node
.AddNode("child", Subgraph.AsNode(childCompiledGraph))
CompiledGraph.Describe() returns topology (nodes, edges, channels) for the ops UI or docs.
</details>
<details> <summary><strong>Host with DI + ops console</strong></summary>
// Program.cs
builder.Services.AddVoluta(sp =>
{
var checkpointer = sp.GetRequiredService<ICheckpointer>();
return new StateGraph()
// …
.Compile(checkpointer);
});
// Optional ops UI (ASP.NET)
var session = new VolutaUiSession(graph, checkpointer);
builder.Services.AddVolutaUI(session);
app.MapVolutaUI(options => options.PathPrefix = "/voluta");
// → http://localhost:5188/voluta (see samples/UiHost)
</details>
<details> <summary><strong>Testing without fighting the runtime</strong></summary>
Voluta.Testing ships doubles you’d otherwise write by hand:
| Helper | Role |
|---|---|
RecordingCheckpointer |
Records every Put/Get/List |
FaultInjectingCheckpointer |
Fails the n-th write |
CheckpointerConformance.RunAllAsync |
Suite every ICheckpointer must pass |
GraphFixtures.Linear() / .Cycle() / .Interrupt() |
Ready graphs |
StreamCapture |
Drain a stream in tests |
Unit tests cover linear/conditional/cycle, fan-out, Send, HITL (Command.Values, resume payload),
stream modes (Updates / Events), Append flatten vs string, LastValue sequential overwrite, and
File rehydrate + resume.
dotnet test voluta.slnx
</details>
<details> <summary><strong>Sample: Hybrid-style marketing desk (mock tools)</strong></summary>
Not a product agent — a demo that exercises the graph against Hybrid console.platform nouns (Campaign, SSP, DirectDeal, AdLibrary):
# terminal 1
dotnet run --project samples/MockAdMcp # http://localhost:5190
# terminal 2
dotnet run --project samples/MarketingAgent -- --offline
Flow: brief → creative → setup (create RK → SSP → banner → Active) → review. Tools are a
simplified HTTP tools surface for demos — not the official MCP SDK and not live Hybrid API.
</details>
Why Voluta
- Cycles are the point — think → act → observe is a loop. Conditional edges +
RecursionLimitmake loops terminate on purpose. - The run outlives the process — every superstep can checkpoint. Interrupt, inspect, resume, or
replay from storage;
ICheckpointeris the only seam. - Multi-writer state is defined — same-superstep writes go through reducers (
Append/LastValue). ConcurrentLastValuewriters fail fast instead of last-write-wins races. - Core stays small —
Voluta+ Abstractions + DI areIsAotCompatible, zero third-party deps on the hot path. No LLM SDK, no logging framework required to run a graph.
Voluta is orchestration, not a Claude Code / OpenCode clone. You bring tools, MCP, prompts, and policy; Voluta runs the stateful graph.
Quick Start
Requires .NET 10 SDK (dotnet --version ≥ 10.0.100).
git clone https://github.com/dot-stbl/voluta.git
cd voluta
dotnet build voluta.slnx
dotnet run --project samples/HelloWorld
dotnet run --project samples/InterruptResume
Reference from your app:
dotnet add reference path/to/voluta/src/Voluta/Voluta.csproj
Minimal complete program
Writer + critic loop until score ≥ 8 — prints End after N supersteps:
using Voluta;
using Voluta.Abstractions.Channels;
using Voluta.Abstractions.Results;
using Voluta.Abstractions.Runtime;
using Voluta.Checkpoint;
using Voluta.Graph;
using Voluta.Graph.Builder;
using Voluta.Graph.Options;
var graph = new StateGraph()
.AddChannel("draft", ChannelKind.LastValue)
.AddChannel("notes", ChannelKind.Append)
.AddChannel("score", ChannelKind.LastValue)
.AddNode("write", WriteAsync)
.AddNode("critique", CritiqueAsync)
.AddEdge(GraphConstants.Start, "write")
.AddEdge("write", "critique")
.AddConditionalEdges(
"critique",
static context => (context.Read<int?>("score") ?? 0) < 8 ? "write" : GraphConstants.End)
.Compile(new InMemoryCheckpointer(), new CompileOptions { RecursionLimit = 16 });
var final = await graph.InvokeAsync(
[new ChannelWrite("draft", "checkpoints are nice")],
new RunOptions { ThreadId = "post-42" });
Console.WriteLine($"{final.Kind} after {final.Step} supersteps");
static Task<NodeResult> WriteAsync(GraphContext context, CancellationToken cancellationToken)
{
var round = (context.Read<int?>("score") ?? 0) / 4;
return Task.FromResult<NodeResult>(
NodeResult.Continue(
new ChannelWrite("draft", $"revision {round + 1}"),
new ChannelWrite("notes", $"writer: produced revision {round + 1}")));
}
static Task<NodeResult> CritiqueAsync(GraphContext context, CancellationToken cancellationToken)
{
var score = (context.Read<int?>("score") ?? 0) + 4;
return Task.FromResult<NodeResult>(
NodeResult.Continue(
new ChannelWrite("score", score),
new ChannelWrite("notes", $"critic: scored {score}/10")));
}
Swap InvokeAsync for StreamAsync (StreamMode.Values / Updates / Events). Under a host:
services.AddVoluta(provider => …).
DI nodes + Microsoft AI (optional)
Native path — no extension-method glue:
// 1) IGraphNode + DI (core)
services.AddSingleton<PlanNode>();
services.AddVoluta(v =>
{
v.Checkpoints.UseInMemory();
v.Graph((sp, checkpointer) => new StateGraph()
.AddNode<PlanNode>("plan")
.AddEdge(GraphConstants.Start, "plan")
.AddEdge("plan", GraphConstants.End)
.Compile(checkpointer, new CompileOptions { Services = sp }));
});
// 2) MEAI IChatClient as a node (Voluta.Agents.AI)
ChatClientNodes.Add(
graph, "answer", "answer",
context => [new ChatMessage(ChatRole.User, context.Read<string>("question") ?? "")]);
// 3) Microsoft Agent Framework AIAgent as a node
AgentNodes.Add(graph, "research", agent, outputChannel: "draft", inputChannel: "question");
Host DI — AddVoluta
One composition root: checkpoints + graph (and later UI hooks). Prefer this over raw
AddSingleton<ICheckpointer>(…) / ad-hoc factories.
// Recommended: checkpoints + graph in one call
services.AddVoluta(v =>
{
v.Checkpoints.UseInMemory(); // or UseFile / UseEntityFrameworkCore / UseS3
v.Graph((sp, checkpointer) => new StateGraph()
// …nodes, edges, channels…
.Compile(checkpointer, new CompileOptions { Services = sp }));
});
// Graph only (already compiled, or factory that owns its own checkpointer)
services.AddVoluta(prebuiltGraph);
services.AddVoluta(sp => BuildGraph(sp));
// Checkpoints only (resolve ICheckpointer yourself)
services.AddVolutaCheckpoints(c => c.UseFile("./.voluta/checkpoints"));
Checkpoint providers (v.Checkpoints.Use*)
Exactly one Use* per host:
v.Checkpoints.UseInMemory();
v.Checkpoints.UseFile("./.voluta/checkpoints");
// EF — register IDbContextFactory first (any provider: Npgsql, SqlServer, SQLite, …)
services.AddDbContextFactory<AppDbContext>(o => o.UseNpgsql(connectionString));
v.Checkpoints.UseEntityFrameworkCore<AppDbContext>();
// S3 — register IAmazonS3 first
services.AddSingleton<IAmazonS3>(_ => new AmazonS3Client(RegionEndpoint.EUCentral1));
v.Checkpoints.UseS3(o =>
{
o.BucketName = "voluta";
o.KeyPrefix = "runs";
});
<details> <summary><strong>Host DbContext shape (EF)</strong></summary>
public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
: DbContext(options), IVolutaCheckpointDbContext
{
public DbSet<CheckpointRecord> Checkpoints => Set<CheckpointRecord>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyVolutaCheckpointModel(); // table voluta_checkpoints
// …your entities…
}
}
Schema ownership is yours: migrations or EnsureCreated() in tests. Column names follow
your EF naming convention (no forced snake_case). Snapshot payload is a JSON column via
EF value conversion — the checkpointer itself does not call JsonSerializer.
</details>
<details> <summary><strong>Errors and limits</strong></summary>
- Miss on Get →
null(not an exception). - Storage failures →
CheckpointStoreExceptionwith stableCode(checkpoint.put_failed/get_failed/list_failed). Graph logic still usesGraphExceptionand friends. - Channel values should be JSON-friendly (strings, numbers, lists of primitives). Complex CLR graphs may not round-trip — same wire limits for File, EF, and S3.
- S3 keys:
{prefix}/{safeThreadId}/{step:D12}.json.
Every provider is exercised by CheckpointerConformance.RunAllAsync in Voluta.Testing
(InMemory, File, EF SQLite + EF InMemory, S3 with a fake client).
</details>
How a superstep works
One tick, in order: collect ready nodes → run them concurrently → barrier → merge writes through channel reducers → checkpoint → evaluate edges.
- Nodes in the same superstep never see each other’s writes (barrier isolation).
- Conditional edges run after the merge, on committed state.
Contracts: openspec/specs/ (12 capabilities).
How it compares
vs. LangGraph (Python) — Origin of the execution
model (supersteps, channels, checkpoint-first). Voluta rebuilds the surface on .NET generics, typed
reducers, and IAsyncEnumerable. Not a port; a peer.
vs. Microsoft Agent Framework — Better for multi-agent chat and function calling. It does not give cyclic graphs with durable per-thread state. Compose: run MAF agents inside Voluta nodes.
vs. Durable Functions / Durable Task — Stronger storage ecosystem for business workflows. Orchestrator + replay vs graph + explicit channels — Voluta fits think-act-observe loops more directly.
vs. a hand-rolled while — Works until “state at step 7?”, “resume after restart?”, or “two
nodes wrote the same field”. Those three questions are the library.
Packages
How-to for checkpoints (DI Use* + host DbContext) is under
Durable checkpoints. Behavior contracts:
openspec/specs/checkpoint/. Until NuGet publishes,
browse source under src/ (each package is one folder; no per-package README yet).
| Package | Role | Source |
|---|---|---|
Voluta.Abstractions |
Contracts: channels, checkpoints, NodeResult, Send, streaming |
src/Voluta.Abstractions |
Voluta |
Pregel runtime + InMemory + Subgraph.AsNode + Describe() |
src/Voluta |
Voluta.DependencyInjection |
AddVoluta(v => { v.Checkpoints…; v.Graph… }) |
src/Voluta.DependencyInjection |
Voluta.Generators |
[GraphState] source generator |
src/Voluta.Generators |
Voluta.Testing |
Test doubles + checkpointer conformance suite | src/Voluta.Testing |
Voluta.Checkpoints.File |
JSON file-system checkpointer (UseFile) |
src/Voluta.Checkpoints.File |
Voluta.Checkpoints.EntityFrameworkCore |
Provider-agnostic EF Core (UseEntityFrameworkCore<T>) |
src/Voluta.Checkpoints.EntityFrameworkCore |
Voluta.Checkpoints.S3 |
AWS S3 / S3-compatible (UseS3) |
src/Voluta.Checkpoints.S3 |
Voluta.Agents.AI |
MAF AIAgent + MEAI as IGraphNode |
src/Voluta.Agents.AI |
Voluta.UI |
Ops console: MapVolutaUI (inspector / HITL / topology) |
src/Voluta.UI |
Native AOT applies to the core tier only — Voluta, Abstractions, and
DependencyInjection are IsAotCompatible, with a publish smoke test in samples/AotSmoke.
Checkpoint providers (File / EF / S3), UI, and Agents.AI are regular-CLR packages and do not
claim AOT.
Samples
| Sample | What it shows |
|---|---|
HelloWorld |
Cyclic agent ⇄ tools, StreamMode.Updates |
InterruptResume |
HITL interrupt + Command resume |
AotSmoke |
Native AOT publish smoke |
ReviewBot |
CLI: plan → sandbox tools → review (+ optional HITL) |
DocQ |
Docs Q&A over a sandboxed folder |
MarketingAgent |
Hybrid desk setup over mock tools |
MockAdMcp |
Hybrid-shaped tool server (Campaign / SSP / deal / AdLibrary) |
UiHost |
ASP.NET host for MapVolutaUI |
Index: samples/README.md.
dotnet run --project samples/HelloWorld
dotnet run --project samples/InterruptResume
dotnet run --project samples/ReviewBot -- --offline --root .
dotnet run --project samples/DocQ -- --offline --root . --question "What is Voluta?"
dotnet run --project samples/MockAdMcp # :5190
dotnet run --project samples/MarketingAgent -- --offline
dotnet run --project samples/UiHost # http://localhost:5188/voluta
What isn't here yet
Stated plainly so you can judge the fit:
- No published packages. Source references only until the 0.1 tag.
- PublicAPI surface can still move before
v0.1.0(tracked with PublicApiAnalyzers). - UI is a first cut.
MapVolutaUIcovers inspect / HITL / topology / SSE — not multi-host thread discovery or auth. - Checkpoint serde is best-effort JSON for channel values; versioning/evolution is still open.
- No first-party MCP client/server — samples use a demo HTTP tools surface; real MCP is
ModelContextProtocol(+ AspNetCore) on top of Voluta. - No built-in coding agent (bash/edit/permissions) — Voluta is the graph runtime, not Claude Code.
Development
dotnet build voluta.slnx # 0 warnings, 0 errors — the gate
dotnet test voluta.slnx # xUnit + Shouldly + NSubstitute
dotnet format voluta.slnx --severity hidden # style drift
dotnet run -c Release --project benchmarks/Voluta.Benchmarks
TreatWarningsAsErrors + EnforceCodeStyleInBuild are on. Specs:
openspec/specs/. Agent handbook: CLAUDE.md.
Contributing
Small fixes → PR. Non-trivial changes → issue first (contracts still move).
git config core.hooksPath .githooks # once per clone
Commit shape: [voluta](feat/scope): subject. Details:
CONTRIBUTING.md.
Inspiration
Execution model from LangGraph (MIT) — supersteps, channel/reducer state, checkpoint-first persistence. The API diverges substantially; design mistakes are ours, not theirs.
License
MIT — see LICENSE.
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.