Commit 2a39306
* feat(agentic): Phase 1 — typed StateGraph runtime core (#1544)
Introduces the typed graph runtime (AiDotNet.Agentic.Graph), the LangGraph-surpassing core:
- StateGraph<TState> builder: AddNode (sync/async), AddEdge, AddConditionalEdges, SetEntryPoint, Compile() with validation (entry exists, edges resolve, a node can't have both a fixed edge and conditional edges).
- CompiledStateGraph<TState>: InvokeAsync (final state) + StreamAsync (per-node GraphStepUpdate); fully typed state threading; cycles bounded by GraphRunOptions.MaxSteps (default 25) -> GraphRecursionException.
- GraphSpecialNodes.End sentinel; conditional routers return a node name or End (validated at runtime).
- 7 tests (linear, conditional cycle, streaming, recursion-limit, async node, compile validation, dup/reserved names) green on net10.0 + net471. No null-forgiving. Checkpointing/HITL/fan-out/subgraphs are the next slices.
Part of epic #1544 (Phase 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agentic): Phase 1 — durable checkpointing, resume & time-travel (#1544)
Adds durable execution to the graph runtime (AiDotNet.Agentic.Graph.Checkpointing):
- GraphCheckpoint<TState> (threadId, checkpointId, step, nextNode, state, IsComplete); IGraphCheckpointer<TState> (Save/GetLatest/Get/GetHistory); InMemoryGraphCheckpointer<TState> (thread-safe).
- CompiledStateGraph: checkpointed InvokeAsync(state, checkpointer, threadId) auto-resumes from the latest checkpoint (saves after every step; cumulative step budget); ResumeFromAsync(threadId, checkpointId) replays from any past checkpoint (time-travel). Shared RunCoreAsync powers plain + checkpointed runs.
- 5 tests (fresh+history, resume-skips-earlier, re-invoke-completed, time-travel replay, unknown-checkpoint throws); 12 graph tests total green on net10.0 + net471. No null-forgiving. NEXT: HITL interrupts, channels/reducers+fan-out, DB-backed checkpointers.
Part of epic #1544 (Phase 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agentic): Phase 1 — human-in-the-loop interrupts (#1544)
Adds interrupt-before HITL to the graph runtime, built on checkpointing:
- StateGraph.AddInterruptBefore(node) marks pause points (validated in Compile).
- GraphRunResult<TState> (IsComplete/IsInterrupted/State/InterruptedBefore).
- CompiledStateGraph.RunAsync(state, checkpointer, threadId, applyOnResume?): fresh run pauses just before the first interrupt node and returns Interrupted; re-invoking the same thread resumes past the pause (optionally applying the human's state edit) until the next interrupt or completion. RunCoreAsync now returns GraphRunResult and honors interrupts; plain/checkpointed InvokeAsync run straight through (no pause).
- 4 HITL tests (pause+resume, human edit on resume, interrupt-before-entry, InvokeAsync-ignores-interrupts); 16 graph tests total green on net10.0 + net471. No null-forgiving. NEXT: channels/reducers + dynamic fan-out, DB-backed checkpointers, subgraphs.
Part of epic #1544 (Phase 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agentic): Phase 1 — subgraphs, reward-gated edges, deterministic replay (#1544)
- StateGraph.AddSubgraph(name, compiledGraph): run a whole graph as one step (composition).
- StateGraph.AddRewardGatedEdges(from, reward, threshold, ifMeets, ifBelow): route by a critic/reward score (differentiator); sugar over conditional edges.
- CompiledStateGraph.ReplayAsync + GetRecordedStateAsync: deterministically reproduce a recorded run's (node,state) trajectory from checkpoint history without executing nodes.
- 3 tests; 19 graph tests total green on net10.0 + net471. No null-forgiving.
Part of epic #1544 (Phase 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agentic): Phase 1 — dynamic fan-out / map-reduce nodes (#1544)
StateGraph.AddFanOutNode<TItem,TResult>(map, branch, reduce, maxDegreeOfParallelism?): derives items from state, runs branches in parallel (optional SemaphoreSlim throttle), and reduces results back into the typed state — the typed equivalent of LangGraph Send/map-reduce, as a single composable node. 4 tests (map-reduce, throttled, empty, composed); 23 graph tests total green on net10.0 + net471. No null-forgiving.
Part of epic #1544 (Phase 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agentic): Phase 1 — durable checkpointers (JSON-file + SQLite) (#1544)
- JsonFileGraphCheckpointer<TState> (core, zero new deps): persists all threads' checkpoints to a JSON file for single-process durability across restarts.
- SqliteGraphCheckpointer<TState> (opt-in AiDotNet.Storage.Sqlite): durable DB-backed checkpoints (table + JSON state column) for cross-process/cross-machine durability; keeps the SQLite dep out of core. Uses explicit arg checks (core's Guard is internal).
- Durability tests prove a fresh checkpointer instance over the same store reads history + resumes. JSON test runs on both TFMs; SQLite test is net10-gated because the e_sqlite3 native lib isn't deployed to the net471 shadow-copy test host (pre-existing, repo-wide; also affects SqliteSqlSyntaxValidatorTests). 25 graph tests green net10.0 / 24 net471. No null-forgiving.
Postgres/Redis checkpointers are interface-ready follow-ups (need live servers; not unit-testable in CI).
Part of epic #1544 (Phase 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agentic): address PR #1548 review — checkpointer ordering/atomicity/corruption
- IGraphCheckpointer.GetHistoryAsync: clarify the contract to chronological/append order
(persistent sequence, not logical step) so replay/time-travel is consistent across backends.
- InMemoryGraphCheckpointer.GetAsync: return the NEWEST matching checkpoint (LastOrDefault),
aligning with the durable backends.
- JsonFileGraphCheckpointer.Load: throw a descriptive InvalidOperationException on a corrupt
file (with the path) instead of silently returning empty (which would let Save overwrite
and lose recoverable data).
- JsonFileGraphCheckpointer.Persist: atomic write — temp file + flush-to-disk + atomic replace
(File.Move overwrite on net5+, File.Replace/Move fallback on net471), with temp cleanup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(agentic): address PR #1548 review — MaxSteps validation, Sqlite null-state, test cleanup
- GraphRunOptions.MaxSteps: validate > 0 in the setter (ArgumentOutOfRangeException) instead
of allowing zero/negative.
- SqliteGraphCheckpointer.ReadRow: throw a descriptive InvalidOperationException (with
checkpointId/threadId) when the persisted state deserializes to null, rather than passing
null into GraphCheckpoint. (Constructor null-checks kept as-is: Guard is internal to core
and not accessible from the opt-in AiDotNet.Storage.Sqlite project.)
- StateGraphTests: drop the no-op `await Task.CompletedTask;` from two synchronous tests by
making them non-async Task methods returning Task.CompletedTask (preserves [Fact(Timeout)],
avoids the CS1998 that deleting the await alone would introduce).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: franklinic <franklin@ivorycloud.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 29e1bea commit 2a39306
18 files changed
Lines changed: 1974 additions & 0 deletions
File tree
- src
- Agentic/Graph
- Checkpointing
- AiDotNet.Storage.Sqlite
- tests/AiDotNet.Tests/UnitTests/Agentic/Graph
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
Lines changed: 88 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
0 commit comments