EvaluatedApplications.EvalApp
4.1.2
See the version list below for details.
dotnet add package EvaluatedApplications.EvalApp --version 4.1.2
NuGet\Install-Package EvaluatedApplications.EvalApp -Version 4.1.2
<PackageReference Include="EvaluatedApplications.EvalApp" Version="4.1.2" />
<PackageVersion Include="EvaluatedApplications.EvalApp" Version="4.1.2" />
<PackageReference Include="EvaluatedApplications.EvalApp" />
paket add EvaluatedApplications.EvalApp --version 4.1.2
#r "nuget: EvaluatedApplications.EvalApp, 4.1.2"
#:package EvaluatedApplications.EvalApp@4.1.2
#addin nuget:?package=EvaluatedApplications.EvalApp&version=4.1.2
#tool nuget:?package=EvaluatedApplications.EvalApp&version=4.1.2
EvalApp
Async pipeline runtime for .NET. You describe a process as a typed data record and a sequence of
steps; EvalApp compiles that description into an immutable, reusable execution path, gates every
parallel operation behind a declared resource budget (CPU/IO/Network/Database/custom), and — when
you opt in — an adaptive or Bayesian tuner discovers the right amount of parallelism for your
hardware while it runs. Zero dependencies, multi-targets net8.0/net10.0, ships binary-only (no
source, no XML docs — IP protection, not obfuscation). This page is the only documentation that
ships with the package — there is no source, no docs site, and nothing below is license-gated.
Author against the root EvalApp namespace only — using EvalApp; brings in the entire public
surface (fluent API, step base classes, context base classes, licensing, the extension seams).
Everything else in the assembly is the internal engine and is not part of the supported API.
Every code block below that starts with a using directive and a full Eval.App(...)...Build();
chain is a complete, standalone program — pasted verbatim into a throwaway console project,
dotnet build+dotnet run, with real output, before this page shipped. A handful of short
bullet-point fragments (e.g. the plain AddStep overload list, a bare .If(...)/.AddSubTaskFor(...)
line) are illustrative continuations of the surrounding example, not complete programs on their own.
Table of contents
- Quick start
- Core concepts: App → Domain → Task → Pipeline
- Step kinds
- Adding steps, fallback, and conditional branches
- Parallel groups and custom merge strategies
- Sub-tasks and
ForEach,ChunkedWork - Cross-pipeline bridges and streaming
Materialize - Resource gating:
ResourceKind,Tunable,Gate - Pressure signals and window budgets
- Sagas: compensation on failure
- Middleware
- Dependency injection (step factories)
- Context-aware steps
- Licensing
- Tuning modes (adaptive / Bayesian / custom settings)
- Inspecting a pipeline
- Anti-patterns to avoid
- Full public surface reference
- Migrating from 1.x
- Composing pipelines: layer and recurse through context
- Exclusive resources: the domain-level safety harness
1. Quick start <a name="quick-start"></a>
dotnet add package EvaluatedApplications.EvalApp
using EvalApp;
Eval.App("Orders")
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddStep("Validate", d =>
{
if (d.Total <= 0) throw new InvalidOperationException("Total must be positive");
return d with { Status = "Validated" };
})
.AddStep("ApplyDiscount", d => d with { Total = d.Total * 0.9m })
.AddStep("Complete", d => d with { Status = "Complete" })
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build(); // no license key needed: nothing is license-gated today; runs at full concurrency
// Build once (e.g. at startup, register `pipeline` as a singleton). Run many times:
var result = await pipeline.RunAsync(new OrderData("Alice", 99.99m));
switch (result)
{
case PipelineResult<OrderData>.Success success:
Console.WriteLine($"{success.Data.Status}: {success.Data.Total}");
break;
case PipelineResult<OrderData>.Failure failure:
Console.WriteLine($"Failed: {failure.Exception.Message}");
break;
case PipelineResult<OrderData>.Skipped skipped:
Console.WriteLine($"Skipped: {skipped.Reason}");
break;
}
// Top-level statements must precede type declarations in a Program.cs, so the record goes last:
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
Read the fluent chain top to bottom and it reads like the process: define an app, define a
domain, define a task, add steps, run it, build it. Run(out ICompiledPipeline<T> pipeline) hands
you the compiled, reusable pipeline; .Build(licenseKey?) then finalizes the whole app.
Rebuild once at startup — rebuilding per request throws away all accumulated tuner state (see
Anti-patterns).
2. Core concepts <a name="core-concepts"></a>
| Type | One-line purpose |
|---|---|
Eval.App(name = "Pipeline") |
The single entry point. Returns IAppBuilder. |
IAppBuilder |
Top-level app configuration: resources, tuning, context, licensing. |
IDomainBuilder |
A named domain; defines one or more tasks (and nested domains). |
IEmptyTaskBuilder<T> |
A task before its first step — Run() is not available yet. |
ITaskBuilder<T> |
The same task after ≥1 step — now Run()/Run(out pipeline) are available. |
ICompiledPipeline<T> |
Opaque, immutable, reusable compiled pipeline: Name + RunAsync(data, ct). |
PipelineResult<T> |
Discriminated union: Success(Data) / Failure(Data, Exception, Message?) / Skipped(Data, Reason), plus GetData()/IsSuccess/IsFailure/IsSkipped. |
using EvalApp;
Eval.App("Orders")
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddStep("Noop", d => d)
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
var result = await pipeline.RunAsync(new OrderData("Quin", 1m));
Console.WriteLine(result.IsSuccess); // bool, no pattern-match needed
Console.WriteLine(result.GetData().Total); // unwraps regardless of outcome
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
Invariant: Build() finalizes the whole app; ICompiledPipeline<T> is immutable and reusable
— build once, call RunAsync many times from multiple threads safely.
A domain can nest another domain (IDomainBuilder.DefineDomain(name, domainContext?)), and an
app can define more than one top-level domain — each is an independent namespace of tasks sharing
the same app-level resources/tuning/context.
3. Step kinds <a name="step-kinds"></a>
| Type | Purpose | Added via |
|---|---|---|
IStep<T> |
The raw interface: ValueTask<T> ExecuteAsync(T data, CancellationToken ct = default). |
any AddStep overload taking IStep<T> |
PureStep<T> |
Synchronous, side-effect-free transform. Override T Execute(T data). |
AddStep(name, PureStep<T>) or AddStep<TStep>() |
AsyncStep<T> |
Asynchronous transform (I/O-bound work). Override ExecuteAsync. |
AddStep(name, AsyncStep<T>) or AddStep<TStep>() |
SideEffectStep<T> |
A step whose job is a side effect (DB write, event publish, email). Declares virtual ResourceKind? ResourceKind. |
AddStep<TStep>()/AddStep<TStep>(name, factory) only — see gotcha below |
CrossDomainStep<T,TShared> |
A SideEffectStep<T> that also carries a Shared object (service/bus) injected at construction. |
AddStep<TStep>(name, factory) (no new() needed) or AddStep<TStep>() if it has a parameterless ctor |
using EvalApp;
// Top-level statements must come before ALL type declarations in a Program.cs file — the
// Eval.App(...) call goes first, every supporting type (including OrderData) goes after it.
Eval.App("StepKinds")
.WithResource(ResourceKind.Network) // REQUIRED: see gotcha below
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddStep("Tax", new ApplyTaxStep()) // PureStep<T> instance
.AddStep("Fetch", new FetchTotalStep()) // AsyncStep<T> instance
.AddStep<SendConfirmationStep>("SendConfirmation") // SideEffectStep<T>: AddStep<TStep>() only
.AddStep<PublishEventStep>("Publish", () => new PublishEventStep(new InMemoryBus())) // factory overload: real injected dependency, no parameterless ctor needed
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New", decimal Tax = 0m);
public sealed class ApplyTaxStep : PureStep<OrderData>
{
public override OrderData Execute(OrderData data) => data with { Tax = data.Total * 0.08m };
}
public sealed class FetchTotalStep : AsyncStep<OrderData>
{
public override async ValueTask<OrderData> ExecuteAsync(OrderData data, CancellationToken ct)
{
await Task.Yield(); // real code: await the actual I/O here
return data with { Total = data.Total + 1m };
}
}
public sealed class SendConfirmationStep : SideEffectStep<OrderData>
{
public override ResourceKind? ResourceKind => EvalApp.ResourceKind.Network;
public override async ValueTask<OrderData> ExecuteAsync(OrderData data, CancellationToken ct)
{
await Task.Yield();
return data with { Status = "Confirmed" };
}
}
public interface IOrderEventBus { ValueTask PublishAsync(string evt, CancellationToken ct); }
public sealed class InMemoryBus : IOrderEventBus
{
public ValueTask PublishAsync(string evt, CancellationToken ct) => ValueTask.CompletedTask;
}
// No parameterless ctor — a real constructor-injected dependency, same shape as a DI-registered
// service. AddStep<TStep>()'s `new()` constraint can never accept this; AddStep<TStep>(name, factory)
// (no `new()` required) is the sanctioned way to add it.
public sealed class PublishEventStep(IOrderEventBus bus) : CrossDomainStep<OrderData, IOrderEventBus>(bus)
{
public override ResourceKind? ResourceKind => EvalApp.ResourceKind.Network;
public override async ValueTask<OrderData> ExecuteAsync(OrderData data, CancellationToken ct)
{
await Shared.PublishAsync("OrderPlaced", ct);
return data with { Status = "Published" };
}
}
Gotchas / invariants:
SideEffectStep<T>(and anything deriving from it, likeCrossDomainStep<T,TShared>) cannot be added withAddStep(name, instance)— passing an instance binds to an[Obsolete(error: true)]overload and fails to compile, including inside a.Gate(...). Add one viaAddStep<TStep>()(needs a parameterless ctor) orAddStep<TStep>(name, factory)(no ctor shape requirement — the factory delegate builds the instance) — both auto-gate it on its declaredResourceKind.- That auto-gate requires the resource to already be registered on the app builder —
either
AddStep<TStep>form, for a step whoseResourceKindisNetwork, throwsInvalidOperationException("ResourceKind 'network' has not been registered...")at pipeline-build time unless you called.WithResource(ResourceKind.Network, ...)first. AddStep<TStep>(name, factory)runsfactory()immediately, synchronously, at build time (inside the.AddStep(...)call) — not deferred toRunAsync. Anull-returning factory throwsArgumentNullExceptionat build time, not at run time.- If you'd rather gate explicitly without a dedicated
SideEffectStep<T>subclass, write the side effect as a plain async lambda inside a.Gate(...)instead (see Resource gating). - Plain
IStep<T>/PureStep<T>/AsyncStep<T>do not auto-catch exceptions — an unhandled throw propagates up and the pipeline reportsFailure. Only context-aware steps (Context-aware steps) catch internally. - Always propagate the
CancellationTokeninto every awaited call inside a step.
4. Adding steps, fallback, and conditional branches <a name="adding-steps"></a>
AddStep accepts any of these forms (same set on IEmptyTaskBuilder<T>, ITaskBuilder<T>,
ISubTaskBuilder<T>, and IConditionalBranchBuilder<T>):
.AddStep("Name", d => d with { Status = "X" }) // Func<T,T>
.AddStep("Name", async (d, ct) => { await Task.Yield(); return d; }) // async lambda
.AddStep("Name", someIStepInstance) // IStep<T> instance
.AddStep("Name", somePureStepInstance) // PureStep<T> instance
.AddStep("Name", someAsyncStepInstance) // AsyncStep<T> instance
.AddStep<MyStep>("Name") // resolved via IStepFactory
AddStep<TStep>(name, factory) — now available on IEmptyTaskBuilder<T>, ITaskBuilder<T>,
ISubTaskBuilder<T>, IConditionalBranchBuilder<T>, and ISagaBuilder<T> (and as
AddBranch<TStep>(name, factory) on IParallelGroupBuilder<T>) — builds the instance from a
Func<TStep> instead of a parameterless constructor — the sanctioned way to add a
SideEffectStep<T>/CrossDomainStep<T,TShared> that needs a constructor-injected dependency (see
Step kinds). Same dispatch/auto-gating on every one of them:
.ForEach(select: d => d.Items, merge: (d, r) => d with { Items = r.ToList() }, collectionName: "Items",
configure: sub => sub.AddStep<MyStep>("Name", () => new MyStep(injectedDep)))
.If(d => d.X, then: b => b.AddStep<MyStep>("Name", () => new MyStep(injectedDep)))
.BeginSaga().AddStep<MyStep>("Name", () => new MyStep(injectedDep)).EndSaga()
.AddParallelGroup(g => g.AddBranch<MyStep>("Name", () => new MyStep(injectedDep)))
AddStepWithFallback — run a primary step, and if it throws, run a fallback instead (two
overloads: lambdas, or IStep<T> instances):
using EvalApp;
Eval.App("Fallback")
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddStepWithFallback(
"Risky",
primary: d => throw new InvalidOperationException("primary unavailable"),
fallback: d => d with { Status = "FallbackUsed" })
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
var result = await pipeline.RunAsync(new OrderData("Carl", 10m));
// result.GetData().Status == "FallbackUsed"
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
If — conditional branch (two overloads: builder actions, or IStep<T> instances for
then/else):
.If(d => d.Total > 100,
then: b => b.AddStep("VipDiscount", d => d with { Total = d.Total * 0.8m, Status = "VIP" }),
@else: b => b.AddStep("StandardProcess", d => d with { Status = "Standard" }))
IConditionalBranchBuilder<T> (the then/@else parameter type) supports its own AddStep
overloads, nested If, ForEach, and Gate — branches can be arbitrarily deep.
If no @else is given and the predicate is false, the step reports Skipped, not Success.
Classify <a name="classify"></a> — first-match-wins classification cascade, on
IEmptyTaskBuilder<T>/ITaskBuilder<T>/ISubTaskBuilder<T>. Equivalent to nested If calls with
the "not yet matched, try the next rule" guard threaded automatically — names the pattern instead of
repeating N near-identical If blocks:
.Classify(
(t => t.Amount > 1000, b => b.AddStep("High", t => t with { Tier = "High" })),
(t => t.Amount > 100, b => b.AddStep("Medium", t => t with { Tier = "Medium" })),
(t => true, b => b.AddStep("Low", t => t with { Tier = "Low" })))
Cases are tried in order; the first When that returns true runs its Then branch and the rest are
skipped (exactly like a chain of If(...).@else(If(...).@else(If(...)))). Put a catch-all t => true
last if every input must be classified.
Classify<TResult> (added 2026-10-03) — same first-match-wins shape, but Then receives the
SAME value When just computed, so a case that needs its predicate's own computed result doesn't
have to recompute it or thread it through a mutable field:
// Self-contained — ClassifyInput/Classify<int> compiled and run exactly as written (verified via
// EvalApp.Tests/Core/ChunkedWorkBenchmark.cs's real Classify<TResult> usage, same signature shape).
public sealed record ClassifyInput(int[] Items, string Tier = "");
.Classify<int>(
((ClassifyInput t) => (t.Items.Length > 1 && t.Items.All(i => i == t.Items[0]), t.Items.Length),
(ClassifyInput t, int repeatCount) => t with { Tier = $"Repeated x{repeatCount}" }),
((ClassifyInput t) => (true, 0),
(ClassifyInput t, int _) => t with { Tier = "Unique" }))
When returns (bool Matched, TResult Value); Then is Func<T, TResult, T>. Compiles to ONE
plain step (not a branch-builder chain), so each Then here is a single transform — for a case that
needs multiple steps/gates, use the branch-builder Classify above instead. Anti-pattern note:
neither Classify overload nor ForEach is intended for hot, pure-CPU, no-await-anywhere
classification at per-row granularity (sub-microsecond comparisons on thousands-to-millions of rows,
re-run many times) — see ChunkedWork below.
5. Parallel groups and custom merge strategies <a name="parallel-groups"></a>
AddParallelGroup runs a fixed set of branches concurrently (not per-item — see
ForEach for that), then merges their results. Branches are genuinely dispatched onto
separate tasks (fixed 2026-10-03 — previously a synchronous branch ran to completion on the calling
thread before the next branch's call was even made, silently serializing two sync branches; MEASURED
after the fix: a 5ms/5ms two-branch group now lands at parity with a hand-rolled Task.Run+
Task.WaitAll fork-join, see EvalApp/CLAUDE.md "3.0 fix-round additions"). IParallelGroupBuilder<T> .AddBranch has three overloads — lambda, generic new(), and IStep<T> instance:
using EvalApp; // IMergeStrategy<T> is one of the public extension seams, in the same root namespace
Eval.App("Parallel")
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddParallelGroup(
pg => pg
.AddBranch("CalcTax", d => d with { Tax = d.Total * 0.08m }) // Func<T,T>
.AddBranch<CalcShippingBranch>("CalcShipping") // generic new()
.AddBranch("CalcHandling", new CalcHandlingFeeStep()), // IStep<T> instance
mergeStrategy: new SumNumericFieldsMerge())
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New",
decimal Tax = 0m, decimal Shipping = 0m, decimal Handling = 0m);
public sealed class CalcShippingBranch : PureStep<OrderData>
{
public override OrderData Execute(OrderData data) => data with { Shipping = 9.99m };
}
public sealed class CalcHandlingFeeStep : IStep<OrderData>
{
public ValueTask<OrderData> ExecuteAsync(OrderData data, CancellationToken ct = default)
=> ValueTask.FromResult(data with { Handling = 1.5m });
}
public sealed class SumNumericFieldsMerge : IMergeStrategy<OrderData>
{
public OrderData Merge(OrderData original, IReadOnlyList<OrderData> results)
=> original with
{
Tax = results.Max(r => r.Tax),
Shipping = results.Max(r => r.Shipping),
Handling = results.Max(r => r.Handling),
};
}
IMergeStrategy<T> is one of only four public extension seams that live directly in the root
EvalApp namespace alongside everything else (see the reference table). Omit
mergeStrategy to get the default ("first branch to change a field wins"); supply your own when
two branches might legitimately write the same field and that should be treated as a real conflict
rather than silently resolved.
What the default merge copies. It builds a NEW T from the original plus each branch's changes (the
group hands the next step that new instance; the original is not mutated from the caller's point of view),
and it carries every public read-write property and every public non-readonly field. State that
cannot be set (get-only properties, readonly fields, private members) is not carried, so a type with
nothing mergeable (for example a bare int, or a type whose state is all get-only) is refused where you
declare the group (InvalidOperationException naming the type) instead of silently producing an empty copy.
Give such a type settable state or pass your own IMergeStrategy<T>.
Note on step kinds in AddBranch: there is no AddBranch(name, PureStep<T>) overload — only
Func<T,T>, AddBranch<TStep>(), and AddBranch(name, IStep<T> instance). A PureStep<T>
instance must go through the generic AddBranch<TStep>() form (needs a parameterless constructor)
or be wrapped in a lambda calling .Execute(...).
6. Sub-tasks and ForEach <a name="foreach"></a>
AddSubTaskFor — run a nested pipeline over a single property, then merge the (possibly
different-typed) result back:
.AddSubTaskFor(
select: d => d.Total,
merge: (d, newTotal) => d with { Total = newTotal },
propertyName: "Total",
configure: sub => sub.AddStep("Double", (decimal v) => v * 2))
ForEach — run an independent, data-parallel pipeline over each item of a collection, then
merge the results back. Every overload shares select (extracts the collection), merge (combines
results into the parent), collectionName (diagnostics label), and configure (the per-item
pipeline); they differ only in how parallelism/failure handling is declared:
using EvalApp;
Action<ISubTaskBuilder<LineItem>> calc =
sub => sub.AddStep("Calc", (LineItem i) => i with { LineTotal = i.Price * i.Quantity });
// (a) default parallelism
Eval.App("ForEachA").DefineDomain("Orders").DefineTask<LineItemsData>("CalcLineTotals")
.ForEach(
select: d => d.Items,
merge: (d, items) => d with { Items = items.ToList() },
collectionName: "Items",
configure: calc)
.Run(out ICompiledPipeline<LineItemsData> pipelineA).Build();
// (b) fixed int parallelism
Eval.App("ForEachB").DefineDomain("Orders").DefineTask<LineItemsData>("CalcLineTotals")
.ForEach(select: d => d.Items, merge: (d, items) => d with { Items = items.ToList() },
collectionName: "Items", maxParallelism: 2, configure: calc)
.Run(out ICompiledPipeline<LineItemsData> pipelineB).Build();
// (c) Func<int> parallelism — re-evaluated per run (e.g. from live config)
var liveMax = 3;
Eval.App("ForEachC").DefineDomain("Orders").DefineTask<LineItemsData>("CalcLineTotals")
.ForEach(select: d => d.Items, merge: (d, items) => d with { Items = items.ToList() },
collectionName: "Items", getMaxParallelism: () => liveMax, configure: calc)
.Run(out ICompiledPipeline<LineItemsData> pipelineC).Build();
// (d) tuner-friendly TunableConfig parallelism
Eval.App("ForEachD").DefineDomain("Orders").DefineTask<LineItemsData>("CalcLineTotals")
.ForEach(select: d => d.Items, merge: (d, items) => d with { Items = items.ToList() },
collectionName: "Items", parallelism: Tunable.ForItems(), configure: calc)
.Run(out ICompiledPipeline<LineItemsData> pipelineD).Build();
// (e) TunableConfig + a separate minItemsForParallel threshold
Eval.App("ForEachE").DefineDomain("Orders").DefineTask<LineItemsData>("CalcLineTotals")
.ForEach(select: d => d.Items, merge: (d, items) => d with { Items = items.ToList() },
collectionName: "Items", parallelism: Tunable.Between(1, 8, 4),
minItemsForParallel: Tunable.FixedAt(10), configure: calc)
.Run(out ICompiledPipeline<LineItemsData> pipelineE).Build();
// (f) int parallelism + ForEachFailureMode — honoured at every item count (see table below)
Eval.App("ForEachF").DefineDomain("Orders").DefineTask<LineItemsData>("CalcLineTotals")
.ForEach(select: d => d.Items, merge: (d, items) => d with { Items = items.ToList() },
collectionName: "Items", maxParallelism: 2, failureMode: ForEachFailureMode.ContinueOnError,
configure: sub => sub.AddStep("Calc", (LineItem i) =>
i.Name == "Bad" ? throw new InvalidOperationException("bad item") : i with { LineTotal = i.Price * i.Quantity }))
.Run(out ICompiledPipeline<LineItemsData> pipelineF).Build();
// (g) TunableConfig parallelism + ForEachFailureMode
Eval.App("ForEachG").DefineDomain("Orders").DefineTask<LineItemsData>("CalcLineTotals")
.ForEach(select: d => d.Items, merge: (d, items) => d with { Items = items.ToList() },
collectionName: "Items", parallelism: Tunable.ForItems(), failureMode: ForEachFailureMode.CollectAndThrow,
configure: calc)
.Run(out ICompiledPipeline<LineItemsData> pipelineG).Build();
public sealed record LineItem(string Name, int Quantity, decimal Price, decimal LineTotal = 0m);
public sealed record LineItemsData(List<LineItem> Items);
ForEachFailureMode:
| Value | Behavior |
|---|---|
CollectAndThrow (default) |
Run every item; if any failed, report Failure with every exception collected. Full visibility in one run. |
FailFast |
Cancel remaining items as soon as one fails. |
ContinueOnError |
Drop failed items; report Success with only the successful ones merged. Best-effort / partial results. |
Below the effective MinItemsForParallel threshold (default 4, from Tunable.ForItems()/the
adaptive tuner defaults), ForEach runs a sequential inline path instead of the parallel dispatch
path (pure scheduling-overhead optimisation, not a behavior change) — it honours ForEachFailureMode
identically on both paths. ContinueOnError keeps every survivor and reports Success;
CollectAndThrow runs every item to completion and aggregates every failure; FailFast still stops
at the first failing item. This holds at every item count, below or at/above the threshold.
Per-item progress: ISubTaskBuilder<T>.OnItemCompleted(callback). Registered inside any
ForEach/AddSubTaskFor configure block, fired once per item right after its result (success,
failure, or skipped) is known — on both the sequential and parallel execution paths. The sanctioned
way to drive a live progress bar (e.g. a Showroom drag-and-drop upload over several files) without
rolling a hand-written loop, which .ForEach itself exists to replace (see
Anti-patterns #1):
.ForEach(select: d => d.Items, merge: (d, r) => d with { Items = r.ToList() }, collectionName: "Items",
configure: sub => sub
.OnItemCompleted((item, result) => progress.Report((item, result.IsSuccess)))
.AddStep("Calc", (LineItem i) => i with { LineTotal = i.Price * i.Quantity }))
Only the last registration wins if called more than once on the same sub-builder. Works identically
on every ForEach overload (and AddSubTaskFor) because they all share this one configure
delegate — no per-overload wiring needed.
Parallelism is always declared bounds, not a hard promise: without tuning enabled a gate/ForEach
holds at its declared default; with .WithTuning()/.WithBayesianTuning() the tuner adjusts within
[Min, Max] at runtime (see Tuning modes).
Adaptive dispatch (on by default for every ForEach). Whether a call actually fans out is decided from
the MEASURED per-item cost of that ForEach against the learned dispatch overhead (about 70 us per fan-out on
the dev machine), not from a fixed item count. The first calls fan out and time their items (safe for heavy or
rendezvous-style items); once the cost is known, a cheap-item call runs inline on the caller with no
thread-pool wake, a heavy-item call fans out, and the cost is re-sampled so a node that gets heavier is noticed.
MEASURED: a pre-built ForEach over 4-64 trivial items cost 72-84 us per call when it always fanned out, and
now costs the same as its sequential form (0.7-7 us); at 1024 trivial items it matches sequential instead of
losing to it. maxParallelism and minItemsForParallel are still hard bounds on top of this. A ForEach
is still a pipeline step (~0.6 us fixed plus ~50 ns per item): for a tight cheap loop use ChunkedWork.
ChunkedWork: chunk-level fan-out for cheap rows, many repetitions <a name="chunkedwork"></a>
ForEach<TItem> is per-ITEM: a fresh TItem plus whatever scratch a step allocates, every row,
every call. That's the right cost model for I/O-bound/resource-gated work, and the wrong one for a
tight, pure-CPU, no-I/O loop run thousands of times over a large cheap collection (a numeric fit
re-run every outer optimizer iteration; a classification cascade over tens of thousands of rows) —
MEASURED 2026-10-03: one real conversion onto ForEach regressed 11x (8.81s→98.42s) because
scratch buffers got reallocated per row per outer iteration instead of once. ChunkedWork.Run
(static, root EvalApp, NOT part of the fluent builder chain — call it from inside a plain AddStep)
partitions into ~core-count contiguous ranges instead of per-item dispatch, so scratch is allocated
ONCE PER CHUNK:
using EvalApp;
// Illustrative — GetRows()/ProcessRow() stand in for your own data/work; the ChunkedWork.Run call
// itself is the real, verified API (exercised as written in EvalApp.Tests/Core/ChunkedWorkBenchmark.cs).
var rows = GetRows(); // IReadOnlyList<double>, cheap per-row work, called many times
var partialSums = ChunkedWork.Run(rows, (items, startIndex, count) =>
{
var scratch = new double[10]; // ONE allocation per chunk, not per row
double sum = 0;
for (int i = startIndex; i < startIndex + count; i++)
sum += ProcessRow(scratch, items[i]);
return sum;
});
double total = partialSums.Sum(); // the caller's own reduction — ChunkedWork never merges for you
``ChunkedWorkis **adaptive**: it learns the per-item cost of each call site (the first calls probe one item inline) and fans out across workers only when the work saved beats the measured dispatch overhead; otherwise the whole range runs inline on the caller at about the cost of a delegate call. So never write a small-N bypass in front of it; pass every size through it. Because an inline run is one chunk and a probe is its own chunk, the number of partials returned varies: reduce them associatively (a sum, a max, a merge).chunkCountis an upper bound on workers (defaultEnvironment.ProcessorCount). There is no cancellation token, no middleware, no StepContext— that overhead is exactly what this primitive exists to avoid. It does not replaceForEach` for I/O-bound
or resource-gated per-item work; it exists for the opposite shape.
For the very common map into an output array shape there is an allocation-free form, ChunkedWork.For(count, body), where body(i) runs exactly once for every i in [0, count) (bodies must write disjoint state).
The call site is identified by the compiler (file and line), so each site learns its own cost:
using EvalApp;
var data = Enumerable.Range(0, 1_000_000).ToArray();
int chunks = Environment.ProcessorCount, per = (data.Length + chunks - 1) / chunks;
var counts = new long[chunks];
// Call it for EVERY size, no hand-written small-N bypass: EvalApp times this call site and runs it inline
// whenever fanning out would cost more than it saves, and fans out when the work is big enough.
ChunkedWork.For(chunks, c =>
{
int lo = c * per, hi = Math.Min(data.Length, lo + per);
long n = 0;
for (int i = lo; i < hi; i++) if (data[i] % 7 == 0) n++;
counts[c] = n; // disjoint write: each chunk owns its slot
});
Console.WriteLine(counts.Sum()); // 142858
MEASURED (2026-10-03, this machine): one call at 256 rows split into 16 chunks costs about the same as the hand loop (1.09 us vs 1.07 us); at 2M rows it fans out (0.70 ms vs 3.8 ms). The old pipeline-per-call route cost 1.7-29 us for the same tiny chunk counts.
7. Cross-pipeline bridges and streaming Materialize <a name="bridges-and-materialize"></a>
AddReadOnlyBridge — run an independent IStep<T> once against a seed value, then merge its
result into the current pipeline's data without that step's result overwriting it wholesale.
Despite the parameter name, sourcePipeline is any IStep<T> — not a compiled
ICompiledPipeline<T> (there is no overload that takes one; wrap its RunAsync in your own
IStep<T> if you need to bridge an actual compiled pipeline):
using EvalApp;
Eval.App("Bridge")
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddStep("Init", d => d)
.AddReadOnlyBridge(
"ReadConfig",
sourcePipeline: new LoadConfigStep(), // any IStep<T>, run once against bridgeData
bridgeData: new OrderData("", 0m),
merge: (current, configResult) => current with { Total = current.Total + configResult.Total })
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
var result = await pipeline.RunAsync(new OrderData("Frank", 10m));
// result.GetData().Total == 52 (10 + the bridge step's 42)
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
public sealed class LoadConfigStep : IStep<OrderData>
{
public ValueTask<OrderData> ExecuteAsync(OrderData data, CancellationToken ct = default)
=> ValueTask.FromResult(data with { Total = 42m });
}
Materialize — drain an IAsyncEnumerable<TItem> property (e.g. a gRPC server-streaming
response) into a List<TItem> before any ForEach/parallel node needs a concrete collection,
with cancellation propagated through the drain:
using EvalApp;
Eval.App("Materialize")
.DefineDomain("Orders")
.DefineTask<StreamedData>("DrainStream")
.Materialize<int>(
"DrainScores",
source: d => Source(1, 2, 3), // Func<T, IAsyncEnumerable<TItem>>
merge: (d, items) => d with { Scores = items })
.Run(out ICompiledPipeline<StreamedData> pipeline)
.Build();
var result = await pipeline.RunAsync(new StreamedData([]));
// result.GetData().Scores == [1, 2, 3]
async IAsyncEnumerable<int> Source(params int[] values)
{
foreach (var v in values) { await Task.Yield(); yield return v; }
}
public sealed record StreamedData(List<int> Scores);
8. Resource gating: ResourceKind, Tunable, Gate <a name="resource-gating"></a>
ResourceKind identifies a named, shared pool that a Gate contends for:
ResourceKind.Network ResourceKind.DiskIO ResourceKind.Cpu ResourceKind.Database
ResourceKind.Of("report-engine") // your own custom-named pool
Tunable factory methods build a TunableConfig(Min, Max, Default) record:
| Method | Shape |
|---|---|
Tunable.Between(min, max, default) |
explicit bounds |
Tunable.FixedAt(value) |
Min == Max == Default == value — no tuning possible |
Tunable.ForCpu() |
Min = max(1, cpus/2), Max = cpus*2, Default = cpus |
Tunable.ForItems() |
Min = 4, Max = int.MaxValue, Default = cpus*2 |
Tunable.ForItems(default) |
Min = 1, Max = int.MaxValue, Default = default |
Tunable.InlineBelow(threshold) |
fixed at threshold (inline/no-parallel shorthand) |
Tunable.InlineBelowBetween(min, max, default) |
same shape as Between |
Tunable.ModelOps() |
fixed at 1 — exclusive access (e.g. GPU training) |
Tunable.DiskIo() |
Min=1, Max=4, Default=2 |
Tunable.Network() |
Min=1, Max=2, Default=1 |
Declare a resource's bounds at the app level, then Gate any block of steps against it (two
Gate overloads: built-in/custom ResourceKind, or a (name, TunableConfig) pair that registers
its own named pool inline):
using EvalApp;
Eval.App("Gate")
.WithResource(ResourceKind.Database, Tunable.Between(1, 8, 2))
.WithResource(ResourceKind.Of("report-engine"), maxConcurrency: 1)
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.Gate(ResourceKind.Database, onWaiting: null, configure: g => g
.AddStep("CheckInventory", d => d with { Status = "Checked" }))
.Gate("CustomPool", Tunable.FixedAt(1),
onWaiting: d => Console.WriteLine($"waiting: {d.Status}"),
configure: g => g.AddStep("RunReport", d => d with { Status = "Reported" }))
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
onWaiting (nullable Action<T>) fires if the step has to queue for the resource — use it for a
"waiting" log line or UI state; pass null to ignore it. WithResource itself has three
overloads: (kind) (bounds default to the engine's internal default), (kind, TunableConfig), and
(kind, int maxConcurrency) (shorthand for FixedAt).
9. Pressure signals and window budgets <a name="pressure"></a>
Unlike a Gate, a pressure resource never blocks — it only measures. .Pressure(name, configure)
wraps a block of steps, times how long they actually take, and calls IPressureResource.Consume
with the elapsed milliseconds; steps inside (or elsewhere, by reading the resource) decide their
own degradation strategy from the resulting Pressure (0.0 = empty, 1.0 = at budget, >1.0 = over).
using EvalApp; // IPressureResource — one of the 4 public extension seams, same root namespace
Eval.App("Pressure")
.WithPressure(new SimpleBudget("render-budget"))
.WithWindowBudget(cyclesPerSecond: 60) // registers the BUILT-IN resource named "window-budget"
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddStep("Init", d => d) // Pressure/WindowBudget need ITaskBuilder<T>
.Pressure("render-budget", configure: sub => sub
.AddStep("Heavy", d => d with { Status = "Rendered" }))
.WindowBudget(configure: sub => sub // shorthand for .Pressure("window-budget", ...)
.AddStep("PerFrame", d => d with { Total = d.Total + 1 }))
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
public sealed class SimpleBudget : IPressureResource
{
public SimpleBudget(string name) => Name = name;
public string Name { get; }
private float _accumulated;
public float Pressure => _accumulated / 10f; // 10ms budget, your own scale
public void Consume(float amount) => _accumulated += amount;
public void Reset() => _accumulated = 0f;
}
Gotchas:
.Pressure(name, ...)/.WindowBudget(...)are only onITaskBuilder<T>/ISubTaskBuilder<T>(post-first-step) — calling them directly on an empty task (before anyAddStep) is a compile error; add a step first..Pressure(name, ...)throwsInvalidOperationExceptionat build time ifnamewas never registered viaWithPressure(...)orWithWindowBudget(...)on the app builder.WithWindowBudget(cyclesPerSecond)registers under the fixed name"window-budget";WithWindowBudget(name, cyclesPerSecond)registers the sameWindowBudgetPressureunder a custom name so you can run more than one frame-budget scope at once.
10. Sagas: compensation on failure <a name="sagas"></a>
BeginSaga(CompensationPolicy policy = BestEffort) / EndSaga() wrap a block of forward steps,
some of which declare a compensating action. If a later step throws, every prior step's
compensation runs in reverse (LIFO) order.
using EvalApp;
Eval.App("Saga")
.WithResource(ResourceKind.Database, Tunable.FixedAt(2))
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.BeginSaga(CompensationPolicy.BestEffort) // BeginSaga() as the very first call works too
.AddStepWithCompensation(
"ReserveInventory",
forward: d => d with { Status = "Reserved" },
compensate: d => d with { Status = "Unreserved" })
.AddGate(
ResourceKind.Database,
onWaiting: null,
configure: g => g.AddStep("WriteLedger", d => d with { Total = d.Total }),
compensate: new RevertLedgerStep())
.AddStep(
"ChargePayment",
d => d.Total > 1_000_000m
? throw new InvalidOperationException("Payment declined")
: d with { Status = "Charged" })
.EndSaga()
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
var ok = await pipeline.RunAsync(new OrderData("Ivy", 10m)); // Success, Status == "Charged"
var failed = await pipeline.RunAsync(new OrderData("Jin", 2_000_000m)); // Failure — compensations ran
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
public sealed class RevertLedgerStep : IStep<OrderData>
{
public ValueTask<OrderData> ExecuteAsync(OrderData data, CancellationToken ct = default)
=> ValueTask.FromResult(data with { Status = "LedgerReverted" });
}
ISagaBuilder<T> also has AddStep (lambda / async lambda / IStep<T> / generic new()),
AddForEach<TItem> (two overloads — with/without a compensate step, plus sideEffectOnly), and
AddMaterialize<TItem>.
CompensationPolicy:
| Value | Behavior |
|---|---|
BestEffort (default) |
Run every compensation even if one of them throws. |
AbortOnFirst |
Stop compensating at the first compensation failure. |
SwallowErrors |
Run every compensation, swallow any compensation-step exceptions. |
.BeginSaga() works correctly as the very first call on a task (before any .AddStep(...)), and
identically after a prior AddStep — both forms compile and run the same way.
11. Middleware <a name="middleware"></a>
IStepMiddleware<T> wraps EACH named step individually (not the chain once) for cross-cutting
concerns (timing, retry, audit, per-step tracing). No named middlewares ship pre-built — this is
a seam you implement, not a library of ready-made ones.
BREAKING in 3.0 (2026-10-03): ExecuteAsync gained a string stepName parameter — the step
being wrapped, closing the "no per-step tracing seam" gap the CouncilAudit case study found
(todo/evalapp-native-apps.md). Since middleware already wraps every step one at a time, this is
the name of that step, not a whole-chain label. Add the parameter to any existing implementation;
there is no other change.
using EvalApp;
using System.Diagnostics;
Eval.App("Middleware")
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.WithMiddleware(new TimingMiddleware<OrderData>()) // on IEmptyTaskBuilder<T> or ITaskBuilder<T>
.AddStep("Validate", d => d with { Status = "Validated" })
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
public sealed class TimingMiddleware<T> : IStepMiddleware<T>
{
public async ValueTask<PipelineResult<T>> ExecuteAsync(
T data, string stepName, Func<T, CancellationToken, ValueTask<PipelineResult<T>>> next, CancellationToken ct = default)
{
var sw = Stopwatch.StartNew();
var result = await next(data, ct);
Console.WriteLine($"step '{stepName}' took {sw.ElapsedMilliseconds}ms");
return result;
}
}
Plain steps (IStep<T>/PureStep<T>/AsyncStep<T>) do not catch their own exceptions — a
middleware implementing retry must catch inside next(...) itself.
12. Dependency injection (step factories) <a name="di"></a>
AddStep<TStep>(name)/AddBranch<TStep>(name) resolve TStep through an IStepFactory. The
default, DefaultStepFactory (a shared .Instance singleton), uses
Activator.CreateInstance — the step needs a public parameterless constructor. To resolve from
your own container instead, use ServiceProviderStepFactory, which checks the container first and
falls back to Activator.CreateInstance for anything not registered:
using EvalApp;
// Any real IServiceProvider works (Microsoft.Extensions.DependencyInjection's ServiceCollection,
// etc.); this minimal one keeps the snippet dependency-free:
var services = new SimpleServiceProvider();
services.Register(typeof(ValidateOrderStep), new ValidateOrderStep("DI-resolved"));
Eval.App("Di")
.WithStepFactory(new ServiceProviderStepFactory(services))
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddStep<ValidateOrderStep>("Validate") // resolved from DI, not Activator
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
public sealed class ValidateOrderStep : IStep<OrderData>
{
private readonly string _tag;
// A literal parameterless ctor to satisfy AddStep<TStep>()'s `new()` constraint (the
// Activator.CreateInstance fallback path); the tagged ctor is what your DI container resolves.
public ValidateOrderStep() : this("default") { }
public ValidateOrderStep(string tag) => _tag = tag;
public ValueTask<OrderData> ExecuteAsync(OrderData data, CancellationToken ct = default)
=> ValueTask.FromResult(data with { Status = _tag });
}
public sealed class SimpleServiceProvider : IServiceProvider
{
private readonly Dictionary<Type, object> _services = new();
public void Register(Type type, object instance) => _services[type] = instance;
public object? GetService(Type serviceType) => _services.TryGetValue(serviceType, out var s) ? s : null;
}
Gotcha: the generic constraint where TStep : class, new() on AddStep<TStep>() is enforced
by the compiler, independent of which IStepFactory you register. A constructor with
all-optional parameters does not satisfy new() — the type needs a literal zero-parameter
constructor, even if you intend DI to always supply the real one via a different overload.
Note: this IStepFactory path is for plain IStep<T> implementers only — SideEffectStep<T>/
CrossDomainStep<T,TShared> are constructed directly via AddStep<TStep>()'s own Activator
branch (bypassing any registered IStepFactory entirely), so a registered DI container never
resolves them either way. If one needs a constructor-injected dependency, use
AddStep<TStep>(name, factory) instead (see Adding steps) — no new() required.
13. Context-aware steps <a name="context"></a>
If a step needs data that's constant for a whole app run (config, a connection factory) or a whole
domain (a tenant ID, a region), subclass GlobalContext/DomainContext instead of threading it
through every record:
using EvalApp;
Eval.App("Context")
.WithContext(new AppGlobal { Prefix = "ORD" })
.DefineDomain("Orders", new OrderDomain { Region = "US" })
.DefineTask<OrderData>("ProcessOrder")
.AddStep<TagWithRegionStep>("TagRegion") // ContextPureStep: AddStep<TStep>() only
.AddStep<NotifyStep>("Notify") // ContextSideEffectStep: same rule
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
public sealed class AppGlobal : GlobalContext { public string Prefix { get; init; } = ""; }
public sealed class OrderDomain : DomainContext { public string Region { get; init; } = ""; }
public sealed class TagWithRegionStep : ContextPureStep<AppGlobal, OrderDomain, OrderData>
{
protected override ValueTask<OrderData> TransformAsync(
OrderData data, AppGlobal global, OrderDomain domain, CancellationToken ct)
=> ValueTask.FromResult(data with { Status = $"{global.Prefix}:{domain.Region}" });
}
public sealed class NotifyStep : ContextSideEffectStep<AppGlobal, OrderDomain, OrderData>
{
protected override async ValueTask<OrderData> ExecuteAsync(
OrderData data, AppGlobal global, OrderDomain domain, CancellationToken ct)
{
await Task.Yield(); // real code: the actual notification I/O
return data with { Status = data.Status + ":Notified" };
}
}
If a step needs DomainContext but no global state, use the built-in NullGlobalContext (shown
here continuing the same OrderDomain/OrderData types as above):
public sealed class TagWithRegionOnlyStep : ContextPureStep<NullGlobalContext, OrderDomain, OrderData>
{
protected override ValueTask<OrderData> TransformAsync(
OrderData data, NullGlobalContext global, OrderDomain domain, CancellationToken ct)
=> ValueTask.FromResult(data with { Status = domain.Region });
}
Gotchas / invariants:
- Always add a context step with
AddStep<TStep>(), neverAddStep(name, instance).ContextPureStep<TGlobal,TDomain,T>/ContextSideEffectStep<TGlobal,TDomain,T>implement the internal engine's step contract (needed to inject context before the step runs), not the publicIStep<T>the instance-overload expects —AddStep("x", new MyContextStep())is aCS1503compile error from outside the assembly. - Both catch their own exceptions internally and translate a thrown exception into a
Failureresult — unlike plainIStep<T>/PureStep<T>/AsyncStep<T>. Global/Domainare populated as properties on the step instance before it runs; there is noStepContextparameter anywhere on these public base classes.WithContext(globalContext)is app-level (one per app);DefineDomain(name, domainContext)is per-domain. If you never callWithContext, context-aware steps whoseTGlobalisNullGlobalContextstill work (no global state needed); for any otherTGlobalyou must supply one.
14. Licensing <a name="licensing"></a>
No feature on this page is gated behind a tier or a license key — every capability above runs at
full concurrency whether or not you pass one; Build(null) and Build(key) behave identically at
runtime. See License at the bottom of this page for the commercial terms. The license
system below is still the real, fully-functional offline verifier every Evaluated Applications
product shares — kept ready for a possible future advanced tier — but it currently caps nothing.
Everything licensing-related — the high-level validator AND the lower-level verifier types — lives
in the same root EvalApp namespace as everything else. (In 1.x these were split across
EvalApp.Consumer and EvalApp.Licensing, with two distinct LicenseMode enums; 2.0 merged both
into one EvalApp.LicenseMode, used everywhere, no cast needed.)
using EvalApp;
string? licenseKey = null; // or a real key string — both paths below never throw
// High-level, recommended surface:
var validator = LicenseValidator.ForEvalApp(); // or LicenseValidator.For("your-product")
LicenseMode mode = validator.Check(licenseKey); // Unlicensed | Licensed — never throws
LicenseMode cached = validator.CheckPeriodic(licenseKey, TimeSpan.FromSeconds(5)); // 60fps-loop-safe
Eval.App("Orders")
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddStep("Noop", d => d)
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build(licenseKey); // null/empty/invalid/expired key all degrade to Unlicensed — never throws
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
Lower-level types (same system, used internally by LicenseValidator and available directly if
you're integrating licensing outside a pipeline):
using EvalApp;
string? licenseKey = null;
License license = LicenseVerifier.Verify(licenseKey); // invalid/missing -> License.Free, never throws
LicenseTier tier = license.TierFor("evalapp"); // Free | Pro | Enterprise
bool allowed = license.Allows("evalapp", LicenseTier.Pro);
// The claims/payload/encoding types behind a key (useful if you're building your own license
// tooling against the same format; the signing half never ships in this package):
var claims = new LicenseClaims("Acme", "trial", DateOnly.FromDateTime(DateTime.UtcNow.AddYears(1)),
new Dictionary<string, LicenseTier> { ["evalapp"] = LicenseTier.Pro });
byte[] payload = LicensePayload.Serialize(claims);
LicenseClaims? roundTripped = LicensePayload.Parse(payload);
string encoded = Base64Url.Encode(payload);
byte[] decoded = Base64Url.Decode(encoded);
Invariants: verification is 100% offline (ECDsa P-256) — there is no network call, ever.
License.Free is the always-available baseline (no key needed). A "*" grant in a key's claims
unlocks every product at once.
15. Tuning modes <a name="tuning"></a>
Two opt-in modes (enable either by calling it — neither is license-gated):
using EvalApp;
Eval.App("Tuned")
.WithTuning(storePath: "tuning.json") // reactive: rediscovers the optimum from scratch each restart
// .WithBayesianTuning(storePath: "bayes.json") // predictive: persists a performance model across restarts
.DefineDomain("Orders")
.DefineTask<OrderData>("ProcessOrder")
.AddStep("Noop", d => d)
.Run(out ICompiledPipeline<OrderData> pipeline)
.Build();
public sealed record OrderData(string CustomerName, decimal Total, string Status = "New");
Both take an optional store path (defaults if omitted) and share the same underlying phase machine
— switching between them doesn't reset state. You don't need to configure the phase machine for
good behavior; the defaults are derived from a simulation harness. To override them explicitly,
load your own JSON file with .WithTunerSettings(path) — the file is read eagerly (missing/invalid
path throws at the point you call it), and must match this exact shape:
{
"ImprovementThreshold": 0.04,
"ConvergenceStddevThreshold": 0.04,
"ConvergingRunsNeeded": 5,
"MonitoringRegressionThreshold": 0.19,
"MonitoringImprovementThreshold": 0.3,
"MaxProbeInterval": 18,
"MaxHistorySize": 50,
"ExplorationWindowSize": 28,
"EmergencyDegradationThreshold": 0.36,
"EmergencyRunsNeeded": 4,
"ConvergingInstabilityThreshold": 1.5,
"MinItemsForParallel": 4
}
(These are the same production defaults .WithTuning() uses out of the box — informational, not a
stability contract; a future release may re-derive them.) MinItemsForParallel here is the
app-wide fallback for the gotcha in ForEach — an individual ForEach call's own
minItemsForParallel/TunableConfig argument, if given, takes precedence over this file.
16. Inspecting a pipeline <a name="inspecting"></a>
ICompiledPipeline<T> is intentionally opaque: its entire public surface is Name (the DefineTask name) and RunAsync(data, ct). There is no exposed step tree and no
visualization/export API in the public facade.
17. Anti-patterns to avoid <a name="anti-patterns"></a>
EvalApp's tuner and gates can only account for work they can see — anything that happens outside the declared pipeline topology is invisible to both:
Task.WhenAllinside a step body instead of a declared.ForEach(...).- A background
Taskstarted and stored on the data record — it races the pipeline, ungated. - Async work before/after
RunAsync, outside the pipeline — unthrottled, untuned. - A shared mutable collection written from inside a
ForEachitem — not thread-safe; let each item own its own result and collect them inmerge. - Mutating the data record instead of using
with— breaks the no-lock parallel-ForEachguarantee. - Instance fields on a step class holding per-run state — steps are singletons reused across every concurrent invocation; per-run state belongs on the data record only.
- Rebuilding the pipeline per request — discards all accumulated tuner state; build once at startup, run many times.
- Blocking I/O inside a sync step — always use the async overload for I/O.
- Classifying/transforming a million cheap rows a second through
Classify/ForEach/plain steps. Not what the step/middleware/StepContextmachinery is priced for — MEASURED: a real conversion ontoForEachregressed 11x on a cheap, many-repetition loop. Use [ChunkedWork] (#chunkedwork) for that shape instead. - A hand-written loop or recursion around
RunAsync(an outerwhile, a service method that calls the pipeline per batch) instead of a pipeline that runs pipelines: the loop is then invisible to gates and the tuner. Layer and recurse through context instead (§20). - Guarding a non-thread-safe singleton with a comment ("never two GPU branches") instead of an
ExclusiveResource(§21).
18. Full public surface reference <a name="reference"></a>
Everything public lives directly in the root EvalApp namespace. using EvalApp; is the only
using directive a consumer ever needs for this package's own types — if a type isn't reachable
that way, it's internal engine and not part of the supported surface, regardless of what a
decompiler shows.
- The fluent API —
Eval,IAppBuilder,IDomainBuilder,IEmptyTaskBuilder<T>,ITaskBuilder<T>,IConditionalBranchBuilder<T>,IParallelGroupBuilder<T>,ISubTaskBuilder<T>,ISagaBuilder<T>,ICompiledPipeline<T>,PipelineResult<T>. - Step base classes —
IStep<T>,PureStep<T>,AsyncStep<T>,SideEffectStep<T>,CrossDomainStep<T,TShared>,ContextPureStep<TGlobal,TDomain,T>,ContextSideEffectStep<TGlobal,TDomain,T>. - Context base classes —
GlobalContext,DomainContext,NullGlobalContext. - DI step factories —
IStepFactory,DefaultStepFactory,ServiceProviderStepFactory. - Config/value types —
ResourceKind,Tunable,TunableConfig,ForEachFailureMode,CompensationPolicy,IStepMiddleware<T>,IMergeStrategy<T>,IPressureResource. - Static helpers —
ChunkedWork(chunk-level fan-out, §6). - Composition and safety —
PipelineRef<T>,PipelineTrace/PipelineFrame/PipelineRecursionException(§20);ExclusiveResource,ExclusivePolicy,ExclusiveViolationException,.Exclusive(...),.Run(PipelineRef<T>)(§21). - Licensing —
LicenseValidator,LicenseMode,LicenseTier,LicenseClaims,License,LicenseVerifier,LicensePayload,Base64Url.
Everything else — every internal engine namespace (EvalAppEngine.*: Fluent, Core,
Abstractions, Steps, Middleware, Extensions, Compatibility, Diagnostics, Parallel,
Tuning, Licensing, Facade) — is internal to the assembly, reachable only by a handful of
trusted first-party dependent packages via [InternalsVisibleTo]. It is not part of the supported
public surface and is not covered by this page.
19. Migrating from 1.x <a name="migrating"></a>
EvalApp 2.0 is a deliberate breaking change: the public surface moved from the EvalApp.Consumer
namespace into the root EvalApp namespace, and the old EvalApp static class (the one that
collided with the assembly's own root namespace, forcing an awkward EvalApp.Consumer.EvalApp.App(...)
fully-qualified call) is gone.
1. Change the using.
- using EvalApp.Consumer;
+ using EvalApp;
That's the whole migration for the vast majority of code — every type you were already using
(IAppBuilder, IStep<T>, PureStep<T>, ResourceKind, Tunable, TunableConfig,
ICompiledPipeline<T>, PipelineResult<T>, GlobalContext, DomainContext, ...) kept its exact
name and shape; only the namespace it lives in changed.
2. Eval.App(...) is the only entry point. The legacy EvalApp.App(...) static-class alias
(already [Obsolete] in 1.9) is removed entirely in 2.0. If you were still calling it (or its
fully-qualified EvalApp.Consumer.EvalApp.App(...) form), switch to Eval.App(...).
3. Licensing: one LicenseMode, no cast. 1.x had two distinct LicenseMode enums
(EvalApp.Consumer.LicenseMode and EvalApp.Licensing.LicenseMode) that were easy to collide by
using both namespaces together. 2.0 has exactly one: EvalApp.LicenseMode. LicenseValidator.Check/
CheckPeriodic return it directly — drop any (LicenseMode)(int)... cast you had.
EvalApp.Licensing.* (the lower-level License/LicenseVerifier/LicenseTier/etc.) also moved to
the root EvalApp namespace — same fix: change the using, keep the code.
4. If you used [InternalsVisibleTo] trusted-first-party seams (BuildAuthorized,
WithProductLicense, InstallTuner — Tracer, HoloDb, EvalApp.Neural, AlgFormer): the internal
engine moved from EvalApp.* (e.g. EvalApp.Consumer.Internal, EvalApp.Fluent, EvalApp.Core,
EvalApp.Abstractions) to a separate, non-nested EvalAppEngine.* root (e.g.
EvalAppEngine.Consumer, EvalAppEngine.Fluent, EvalAppEngine.Core,
EvalAppEngine.Abstractions). Update any using EvalApp.Consumer.Internal; (or similar internal
namespace) to using EvalAppEngine.Consumer; (etc.). This split exists specifically so the public
EvalApp namespace and the internal engine can never silently collide on a shared simple name
(IStep<T>, ITaskBuilder<T>, ResourceKind, ...) — nesting the engine under EvalApp.Engine.*
was tried first and rejected: C# namespace lookup resolves an enclosing namespace's direct members
before any using directive, so every internal-engine file would have silently bound to the
public same-named type instead of its own internal one. If you have your own code under an
EvalApp.*-prefixed namespace that also usings an internal EvalApp namespace, the same hazard
applies to you — don't nest your own namespace under EvalApp.*.
No other behavior changed: every example on this page runs identically to 1.x once the using is
updated.
Migrating from 2.x to 3.0 <a name="migrating-3"></a>
3.0 removes "Consumer" as a product concept entirely (the public namespace has been flat root
EvalApp since 2.0 — this is just the lingering internal naming catching up) and adds the
per-step tracing seam:
IStepMiddleware<T>.ExecuteAsyncgained astring stepNameparameter (inserted beforenext). Add the parameter to any implementation; behavior is otherwise unchanged — see §11 Middleware.- If you used the
[InternalsVisibleTo]trusted seam, the internal namespaceEvalAppEngine.Consumer(introduced in 2.0) is renamedEvalAppEngine.Facade. Update anyusing EvalAppEngine.Consumer;tousing EvalAppEngine.Facade;. No publicEvalApptype or namespace is affected — this is purely an internal rename. ForEach/AddSubTaskForgainedISubTaskBuilder<T>.OnItemCompleted(callback)andITaskBuilder<T>/ISubTaskBuilder<T>gained.Classify(...)— both purely additive, see §6 and §17.AddStep<TStep>(name, factory)is now also available onISubTaskBuilder<T>,IConditionalBranchBuilder<T>,ISagaBuilder<T>, andIParallelGroupBuilder<T>.AddBranch<TStep>(name, factory)— also purely additive.
20. Composing pipelines: layer and recurse through context <a name="composing"></a>
A compiled pipeline is an ordinary value: ICompiledPipeline<T> has one method, RunAsync(data, ct).
That makes pipelines composable with no new concept: a step may run another pipeline, a pipeline may
run itself, and a pipeline can be held in a GlobalContext/DomainContext so a higher level
orchestrates lower-level ones. Layer by pipeline-in-pipeline instead of writing a loop around
RunAsync; the loop is a pipeline, so the gates, the tuner and the safety harness see it.
The one trick is forward declaration. A context object is created before the pipelines exist, and a pipeline that recurses is referenced from inside its own steps, so you need a handle before the build:
PipelineRef<T>: create it first (new PipelineRef<T>("Name")), put it in a context or capture it in a lambda, bind it with.Run(ref). Calling it before it is bound throws a clear error naming it.- Or the plain form: declare
ICompiledPipeline<T> self = null!;ahead of time and bind it with.Run(out self); lambdas capture the variable, and it is assigned before the firstRunAsync.
Recursion: a pipeline that calls itself, fanned out by ForEach.
using EvalApp;
// A directory tree summed by a pipeline that calls ITSELF once per child directory.
ICompiledPipeline<Dir> self = null!; // declared ahead of time; Run(out self) fills it in
Eval.App("TreeSum")
.WithContext(new NoGlobal()) // compiled path (the production shape)
.DefineDomain("Tree")
.DefineTask<Dir>("SumDir")
.ForEach(
select: d => d.Children,
merge: (d, kids) => d with { Children = kids },
collectionName: "Children",
configure: sub => sub.AddStep("Recurse", async (Dir child, CancellationToken ct) =>
{
var r = await self.RunAsync(child, ct); // same pipeline, one level down
return r is PipelineResult<Dir>.Failure f ? throw f.Exception : r.GetData();
}))
.AddStep("Total", d => d with { Total = d.Size + d.Children.Sum(c => c.Total) })
.Run(out self) // bind: from here `self` is the compiled pipeline
.Build();
var tree = new Dir("root", 1, [
new Dir("src", 10, [new Dir("a", 100, []), new Dir("b", 200, [])]),
new Dir("docs", 20, [])]);
var result = await self.RunAsync(tree);
Console.WriteLine($"total = {result.GetData().Total}"); // 1 + (10 + 100 + 200) + 20 = 331
sealed class NoGlobal : GlobalContext { }
record Dir(string Name, long Size, IReadOnlyList<Dir> Children, long Total = 0);
Layers: an outer schedule pipeline drives an inner per-batch pipeline held in the context. Both are
declared in one domain; the outer step reads the inner pipeline from GlobalContext (the one sanctioned
shared seam, rubric principle 3) and loops it until a condition holds. Pass the step's ct down so
cancelling the outer run cancels the inner one.
using EvalApp;
// Context object FIRST: it already holds a PipelineRef to the inner pipeline, which does not exist yet.
var ctx = new TrainContext();
var schedule = new PipelineRef<Batch>("Schedule");
Eval.App("Train")
.WithContext(ctx)
.DefineDomain("Train", new TrainDomain())
// inner level: one optimiser step on one batch
.DefineTask<Batch>("BatchStep")
.AddStep("Descend", b => b with { Loss = Math.Max(0, b.Loss - 3), Epoch = b.Epoch + 1 })
.Run(ctx.BatchStep) // bind the ref held in the context
// outer level: the schedule drives the inner pipeline until the loss reaches zero
.DefineTask<Batch>("Schedule")
.AddStep<RunUntilConverged>("RunUntilConverged")
.Run(schedule)
.Build();
var done = (await schedule.RunAsync(new Batch(Loss: 10, Epoch: 0))).GetData();
Console.WriteLine($"loss={done.Loss} epochs={done.Epoch} innerRuns={ctx.InnerRuns}"); // loss=0 epochs=4 innerRuns=4
record Batch(int Loss, int Epoch);
sealed class TrainDomain : DomainContext { }
sealed class TrainContext : GlobalContext
{
public PipelineRef<Batch> BatchStep { get; } = new("BatchStep");
public int InnerRuns;
}
sealed class RunUntilConverged : ContextSideEffectStep<TrainContext, TrainDomain, Batch>
{
protected override async ValueTask<Batch> ExecuteAsync(
Batch b, TrainContext global, TrainDomain domain, CancellationToken ct)
{
while (b.Loss > 0)
{
var r = await global.BatchStep.RunAsync(b, ct); // ct flows down: cancelling the outer run cancels the inner
if (r is PipelineResult<Batch>.Failure f) throw f.Exception;
b = r.GetData();
global.InnerRuns++;
}
return b;
}
}
What composes cleanly, measured by EvalApp.Tests/Core/PipelineCompositionTests.cs:
- Re-entrancy. Every
RunAsyncrents its own context, so a pipeline running inside itself (or two pipelines calling each other) is safe. ResourceGates are re-entrant for the same flow: a nested run that reaches aGateits own caller already holds (recursion through a capacity-1 gate) runs inside that permit instead of waiting on it forever. This applies when the pipeline holding the gate composes: it is bound to aPipelineRef(Run(ref)) or uses.AddStep(name, pipeline); a plain pipeline publishes nothing and pays nothing. It follows the flow, never the gate: two independent runs still queue, and work forked from inside the gate (aForEachworker, a parallel branch) is not the holder and takes its own permit (so a fork that re-enters a capacity-1 gate its parent holds waits for it; cancel or restructure).ExclusiveResourceis re-entrant the same way. - Cancellation propagates down because you pass the step's
ctto the innerRunAsync. It then surfaces on either of the two documented surfaces (aFailureresult or a thrownOperationCanceledException); the outer step turns it into a failure of its own step. - Failure does not throw across levels: an inner failure is a
PipelineResult.Failurethe outer step decides what to do with (rethrow it as the snippets do, or recover). - A pipeline as a step. When the outer step just means "run that pipeline and carry on", say so:
.AddStep("Inner", innerPipeline)(on any task, sub-task, branch or saga builder). It runs the pipeline on the current data, hands its output to the next step, and turns an innerFailureinto this step's failure with its original exception (type and stack kept), so the failure channel is never dropped by accident. The plan reports the step as aNestednode, and the pipeline that uses it is framed inPipelineTrace. Pass aPipelineRef<T>for a pipeline that is built later or recurses; a plain variable is captured by value when the step is declared (anull!placeholder fails right there with a message saying so).
using EvalApp;
var inner = new PipelineRef<Batch>("Inner");
Eval.App("Layers")
.WithContext(new NoGlobal())
.DefineDomain("D")
.DefineTask<Batch>("Inner")
.AddStep("Descend", b => b with { Loss = b.Loss - 3 })
.Run(inner)
.DefineTask<Batch>("Outer")
.AddStep("Warm", b => b with { Epoch = b.Epoch + 1 })
.AddStep("RunInner", inner) // the whole inner pipeline is one step here
.Run(out ICompiledPipeline<Batch> outer)
.Build();
var r = await outer.RunAsync(new Batch(Loss: 10, Epoch: 0));
Console.WriteLine($"loss={r.GetData().Loss} epoch={r.GetData().Epoch}"); // loss=7 epoch=1
sealed class NoGlobal : GlobalContext { }
record Batch(int Loss, int Epoch);
- Context flow. Each pipeline sees the
WithContextglobal and its own domain's context. Pipelines defined in the same app share itsWithResourcegates (one semaphore per kind), so an outer level and an inner level contend on the same declared budget, which is what you want. - Tracing across levels. A named-step middleware only receives a step's name, so two pipelines that
both have a step called
"Score"look identical. ReadPipelineTrace.Pathin the middleware:"Outer > Inner/Score".PipelineTrace.Depth/Currentgive the nesting depth and the caller chain. Tracking is off unless needed: only a pipeline bound to aPipelineRef(and anything running inside one) records its call chain, so an ordinary top-level pipeline pays nothing. Bind the pipelines you compose with.Run(ref); a pipeline that recurses through a bareRun(out self)variable is not tracked (no path, no depth guard). - Runaway recursion is a failure, not a stack overflow: a run nested deeper than
PipelineTrace.MaxDepth(default 256) fails withPipelineRecursionException. A recursive pipeline needs anIfthat stops it. - Tuning.
WithTuningobserves each run as one sample; an outer run that contains the inner runs is timed as one long run next to the short inner ones. Do not attach adaptive tuning to a pipeline that recurses into itself and expect clean per-level numbers.
21. Exclusive resources: the domain-level safety harness <a name="exclusive"></a>
Some things must never be touched by two flows at once: a GPU Accelerator singleton, a native handle,
a single-writer file. "Never two concurrent GPU branches" should be a declaration the runtime enforces,
not a comment. Declare an ExclusiveResource once, next to the thing it protects, then guard the
places that touch it:
.Exclusive(resource, g => g.AddStep(...)): a scope on any task or sub-task builder..WithMiddleware(resource.Guard<T>("StepA", "StepB")): guards named steps (all steps if none are named). This is how to protectAddParallelGroupbranches, which have no scope to wrap.Build()fails, naming the resource, if a name matches no step in any pipeline the guard instance is attached to, so renaming a step can never silently un-guard it. One guard instance may serve several tasks (each task owns a different subset of the names); a name that exists in none of them is the error.
Policy ExclusivePolicy.Fail (default) refuses a concurrent second entry: the intruder's step fails
with ExclusiveViolationException (naming the resource, the holder, the intruder and how long it was
held) and the holder is untouched. ExclusivePolicy.Queue serialises instead (the wait honours the
token). Entry is re-entrant for the same async flow, so recursion and an inner pipeline run from a
guarded step are not mistaken for a second instance.
using EvalApp;
// Declared ONCE, next to the thing it protects (a non-thread-safe singleton such as a GPU accelerator).
var gpu = new ExclusiveResource("gpu", ExclusivePolicy.Fail);
Eval.App("TrainBatchCoop")
.WithContext(new NoGlobal())
.DefineDomain("Train")
.DefineTask<Work>("Coop")
// Branches have no scope to wrap, so guard them BY NAME with middleware.
.WithMiddleware(gpu.Guard<Work>("GpuForward", "GpuBackward"))
.AddParallelGroup(pg => pg
.AddBranch("GpuForward", w => { Thread.Sleep(100); return w; })
.AddBranch("GpuBackward", w => { Thread.Sleep(100); return w; })) // BUG: two GPU branches
.Run(out ICompiledPipeline<Work> pipeline)
.Build();
var result = await pipeline.RunAsync(new Work(1));
if (result is PipelineResult<Work>.Failure { Exception: var ex })
{
var violation = (ex as AggregateException)?.InnerException as ExclusiveViolationException
?? (ExclusiveViolationException)ex;
Console.WriteLine($"caught: {violation.ResourceName} held by '{violation.Holder}', intruder '{violation.Intruder}'");
Console.WriteLine($"violations counted: {gpu.ViolationCount}");
}
// The same resource as a SCOPE around steps that run one after another inside ONE pipeline:
var queue = new ExclusiveResource("single-writer", ExclusivePolicy.Queue); // serialise instead of failing
Eval.App("Writer")
.WithContext(new NoGlobal())
.DefineDomain("IO")
.DefineTask<Work>("Write")
.Exclusive(queue, g => g.AddStep("Append", w => { Thread.Sleep(20); return w; }))
.Run(out ICompiledPipeline<Work> writer)
.Build();
var runs = await Task.WhenAll(Enumerable.Range(0, 4).Select(i => Task.Run(() => writer.RunAsync(new Work(i)).AsTask())));
Console.WriteLine($"queued writers all succeeded: {runs.All(r => r.IsSuccess)}");
sealed class NoGlobal : GlobalContext { }
record Work(int Id);
Rules worth knowing: work forked from inside a held scope (a ForEach/AddParallelGroup nested
in it, or an inner pipeline's branches) does NOT silently inherit the hold: a fork that enters the resource
again competes with its sibling forks (Fail refuses the second concurrent fork, Queue serialises them),
while items run one at a time on the same flow stay re-entrant. A cheap ForEach that adaptive dispatch
runs inline is not forked, so it never overlaps itself. The resource is per object, not per app: share the static readonly instance across every
pipeline that touches the protected thing. Measured no-contention cost (2026-10-03, this machine,
Release, after the scope chain): about +160-200 ns and +152 B per scope entry, +180-280 ns per guarded step over a no-op middleware.
License
Proprietary — this package ships the compiled library only; see LICENSE for the full terms.
Installing and using unmodified copies for internal evaluation and non-commercial development or
testing is licensed; commercial, production, or revenue-generating use requires a separate
commercial licence from the owner. No feature on this page is gated behind a tier or a licence key
— every capability described works identically with or without one. Copyright (c) 2026 Dongyang
Stephen Chen, trading as Evaluated Applications. All rights reserved.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (6)
Showing the top 5 NuGet packages that depend on EvaluatedApplications.EvalApp:
| Package | Downloads |
|---|---|
|
EvaluatedApplications.HoloDb
An embeddable SQL database for .NET that builds its own fast access paths from the queries it runs, so there is no index to design. Tables are typed columns, held in memory or on disk, with ACID transactions over a write-ahead log, larger-than-memory paged storage whose every page is checksummed, and exact aggregates: integer sums are 128-bit and throw instead of wrapping. It is for applications that need dashboards, change feeds and analytics over their own data without running a database server, and it includes similarity search (NEAREST), NULL support, ClickBench-compatible analytic SQL, atomic change batches, and an EvalApp extension with async execution. |
|
|
EvaluatedApplications.AlgFormer
The reusable phasor-model engine: define a model, train it data-parallel through an EvalApp pipeline, and run inference (predict / generate, with an optional KV cache). Ships two param-for-param comparable cores — AlgFormer (softmax dot-product attention) and HoloFormer (holographic-native attention: bind the key to its value, causally bundle into one superposed memory trace, then resonate the query to retrieve, O(T·d) with O(1) KV-serve) — plus HoloShape capacity-law sizing, compute-equivalent (invisible-param) accounting, a built-in subword tokenizer, checkpoint save/load with in-place growth (context/shifts/vocabulary), and MQTT-based distributed/swarm training. HoloFormer also ships an opt-in, default-OFF per-layer Adam gate ("ADAM & EVE") that nudges a layer's update toward a codebook-derived target norm range; default off and byte-identical when unused, but live in production via the GPU-fused trainer (see AlgFormer.Gpu) since 2026-09-24. Pure double[] + SIMD, no external ML runtime. Built on the Phasor VSA codec and the EvalApp pipeline runtime. |
|
|
EvaluatedApplications.AlgFormer.Gpu
ILGPU/CUDA acceleration for the AlgFormer package's HoloFormer model: GpuHoloTrainer runs batched GPU forward/backward training (CPU Adam by default, with an opt-in fused on-GPU Adam step) and GPU inference (Logits, StackIterLogits). GpuDevice auto-detects a CUDA device at startup and otherwise runs the SAME kernels on ILGPU's own CPU accelerator, so one build works with or without a CUDA card; genuinely over-length windows still run on the CPU HoloFormer directly. Every kernel is gradchecked against the CPU HoloFormer as its correctness oracle (float-close fp32, not bit-identical). Also ships an opt-in, default-OFF "ADAM & EVE" per-layer directional gradient gate layered on the fused on-GPU Adam step, live in production training (not an unused experiment) since 2026-09-24, plus preference (DPO) training on the GPU: per-sequence log-probabilities (SequenceLogProb) and a signed-weight all-positions training step (TrainBatchAllPosWeighted), at the same weight-tied depth the trainer trains at, with a frozen reference model (NewReference); exact bit-identical GPU page transforms for Phasor's Pleroma key band (PleromaPageTransform: Forward, Inverse, Dft over a 61-bit or sub-40-bit prime field, with an ILGPU CPU and a managed backend) and the Pleroma Cascade Newton pass (PleromaNewtonGpu); and a deprecated non-holo GpuModel/GpuTrainer twin for the older softmax-attention AlgFormer core. Add this package only if you want GPU acceleration; the core AlgFormer package stays dependency-free and AOT/trim-safe without it. |
|
|
EvaluatedApplications.Tracer
Fast pathfinding and game-AI SDK for real-time simulations with many moving agents. Bakes triangle navmeshes (flat, hierarchical, or multi-layer for ground/air/water) and routes single agents or whole goal-sharing swarms across them, reacts to dynamic obstacles without a full re-bake, and adds line-of-sight, fog-of-war, influence maps, congestion analysis, and spatial-hashed combat engagement for AI perception and tactics. Includes composable steering behaviors (Seek, Wander, Patrol, Flee, Queue, FlowLane) via a fluent Navigator, load-aware adaptive clustering for large populations, and a separate bake-free grid-tactics lane (tile line-of-sight, movement range, cover, target selection) for tile/square-grid games. Built on the EvalApp pipeline runtime for parallel, adaptively-tuned execution. |
|
|
EvaluatedApplications.Prose
Grammar-driven synthetic corpus generator: mines nouns, verbs and adjectives and the syntax that joins them from real text with the rules of grammar, stores them in HoloDb, and recombines them into new grammatical sentences built only from words and pairings the text used, plus reading-comprehension Q&A pairs (single-sentence, multi-sentence passage, and two-turn). Rules-first (deterministic, zero-training) part-of-speech tagging, chunking and role assignment; mining fans out across cores through EvalApp (generation does too, opt-in); an optional mini-HoloFormer scorer re-ranks generated candidates by next-lemma likelihood once trained (POS tagging stays 100% rules-based). |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 6.0.2 | 61 | 11/10/2026 |
| 6.0.1 | 148 | 11/10/2026 |
| 6.0.0 | 58 | 11/10/2026 |
| 5.0.1 | 97 | 11/9/2026 |
| 5.0.0 | 127 | 11/9/2026 |
| 4.4.2 | 225 | 10/19/2026 |
| 4.4.1 | 375 | 10/9/2026 |
| 4.4.0 | 151 | 10/6/2026 |
| 4.3.1 | 120 | 10/5/2026 |
| 4.3.0 | 95 | 10/5/2026 |
| 4.2.1 | 98 | 10/5/2026 |
| 4.2.0 | 149 | 10/3/2026 |
| 4.1.2 | 105 | 10/3/2026 |
| 4.1.1 | 103 | 10/3/2026 |
| 4.1.0 | 117 | 10/3/2026 |
| 4.0.0 | 141 | 10/3/2026 |
| 3.1.0 | 102 | 10/3/2026 |
| 3.0.0 | 194 | 10/3/2026 |
| 2.0.0 | 484 | 10/2/2026 |
| 1.9.0 | 243 | 10/2/2026 |