feat(agentic): Phase 1 — typed durable graph runtime (epic #1544)#1548
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
Warning Review limit reached
More reviews will be available in 55 minutes and 17 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (6)
WalkthroughThis PR introduces a comprehensive agentic state graph execution engine with multi-backend checkpointing support, enabling pause/resume, replay, and human-in-the-loop workflows. It includes a fluent graph builder, execution engine supporting multiple modes (stateless invoke, checkpointed, interrupted, streamed, replayed), in-memory and durable (JSON/SQLite) checkpoint backends, and extensive unit tests. ChangesAgentic State Graph with Checkpointing
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Reasoning: This PR is a substantial multi-file feature introduction with dense logic across graph execution, checkpointing backends, and builder patterns. Key complexity drivers include: (1) Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
…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>
|
Deployment failed with the following error: Learn More: https://vercel.com/franklins-projects-02a0b5a0?upgradeToPro=build-rate-limit |
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>
… 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>
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>
…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>
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Agentic/Graph/Checkpointing/IGraphCheckpointer.cs`:
- Around line 40-45: The XML comment for GetHistoryAsync on
IGraphCheckpointer<TState> is misleading: it currently says "ordered by step
ascending" which allows backend divergence when step values repeat or branching
occurs; update the contract to require history be returned in
append/chronological order (e.g., by persistent sequence id such as Seq ASC) so
replay/time-travel semantics are consistent across backends, and mention
GraphCheckpoint<TState> and threadId in the doc to clarify callers should expect
chronological/append ordering rather than logical step ordering; ensure
implementing classes (SQLite backend using Seq ASC) adhere to and are documented
to follow this chronological sequence.
In `@src/Agentic/Graph/Checkpointing/InMemoryGraphCheckpointer.cs`:
- Around line 66-71: GetAsync in InMemoryGraphCheckpointer currently returns the
first (oldest) checkpoint with a matching checkpointId; change it to return the
newest (latest) match to align with durable backends. In the GetAsync method of
class InMemoryGraphCheckpointer (where it iterates "foreach (var cp in list)"),
iterate the list in reverse or use a Last/LastOrDefault-style lookup to return
the last GraphCheckpoint<TState> whose CheckpointId == checkpointId so the most
recently saved checkpoint is returned.
In `@src/Agentic/Graph/Checkpointing/JsonFileGraphCheckpointer.cs`:
- Around line 117-132: The Load method currently calls
JsonConvert.DeserializeObject on the file at _path without catching
JsonException; wrap the deserialization in a try-catch that catches
JsonException (and optionally IOException) around the
JsonConvert.DeserializeObject call used to produce Dictionary<string,
List<GraphCheckpoint<TState>>> and handle corruption by either logging a warning
and returning a new empty Dictionary<string,
List<GraphCheckpoint<TState>>>(StringComparer.Ordinal) or rethrowing a new,
descriptive exception that includes _path; ensure the method still returns a
valid dictionary on error to avoid propagating an unhandled exception to
callers.
- Around line 134-137: Persist currently uses File.WriteAllText which can leave
the checkpoint file partially written; change JsonFileGraphCheckpointer.Persist
to perform an atomic write: serialize to a temporary file in the same directory
(use Path.Combine(Path.GetDirectoryName(_path), ...) with a unique suffix),
write the bytes using a FileStream and flush/flush-to-disk, then atomically
replace the target file (use File.Replace when available or File.Move/Move with
overwrite=true on .NET 6+, falling back to Delete+Move for older runtimes) and
ensure proper try/finally cleanup of the temp file on error; this preserves
durability and avoids corruption of the checkpoint file.
In `@src/Agentic/Graph/GraphRunOptions.cs`:
- Line 18: GraphRunOptions.MaxSteps is documented as "must be positive" but
currently allows zero/negative values; modify the MaxSteps property setter (or
add a private backing field) to validate the value and throw an
ArgumentOutOfRangeException (with a clear message) when assigned a value <= 0,
ensuring callers get an immediate, descriptive error instead of later runtime
failures.
In `@src/AiDotNet.Storage.Sqlite/SqliteGraphCheckpointer.cs`:
- Around line 31-39: Replace the manual null/whitespace checks in
SqliteGraphCheckpointer<TState> with the project's Guard helpers: in the
constructor SqliteGraphCheckpointer(string connectionString) call
Guard.NotNullOrWhiteSpace(connectionString, nameof(connectionString)); and in
the methods SaveAsync, GetLatestAsync, GetAsync, and GetHistoryAsync replace any
"if (param is null)" style checks with Guard.NotNull(param, nameof(param)) (or
Guard.NotNullOrWhiteSpace where appropriate for strings) to match the rest of
the Graph namespace and maintain consistent validation patterns.
- Around line 138-146: ReadRow deserializes stateJson with
JsonConvert.DeserializeObject<TState> which can yield null; update ReadRow to
guard against null by validating the deserialized state before constructing
GraphCheckpoint<TState> (either throw a descriptive
InvalidDataException/ArgumentException referencing checkpointId/threadId when
state is null, or use the null-forgiving operator with a clear comment asserting
non-null because serialization is controlled elsewhere). Reference ReadRow,
stateJson, JsonConvert.DeserializeObject<TState>, and GraphCheckpoint<TState>
when making the change so callers will get a clear error or documented assertion
if deserialization returns null.
In `@tests/AiDotNet.Tests/UnitTests/Agentic/Graph/StateGraphTests.cs`:
- Around line 113-119: In the test method
AddNode_RejectsDuplicateAndReservedNames remove the no-op await by deleting the
trailing "await Task.CompletedTask;" statement; keep the method signature async
Task AddNode_RejectsDuplicateAndReservedNames() as-is (xUnit may need the async
signature) and leave the existing assertions using StateGraph<int>.AddNode and
StateGraph<int>.End unchanged.
- Around line 90-110: In the
Compile_Validates_EntryPointEdgesAndExclusiveRouting test method remove the
no-op statement `await Task.CompletedTask;` at the end; leave the method
signature as `async Task Compile_Validates_EntryPointEdgesAndExclusiveRouting()`
(so xUnit timeout semantics remain) and simply delete the trailing await
statement to avoid the unnecessary no-op await.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: ba632407-27d0-4cfd-94b0-d4ecd8d3f38e
📒 Files selected for processing (18)
src/Agentic/Graph/Checkpointing/GraphCheckpoint.cssrc/Agentic/Graph/Checkpointing/IGraphCheckpointer.cssrc/Agentic/Graph/Checkpointing/InMemoryGraphCheckpointer.cssrc/Agentic/Graph/Checkpointing/JsonFileGraphCheckpointer.cssrc/Agentic/Graph/CompiledStateGraph.cssrc/Agentic/Graph/GraphRecursionException.cssrc/Agentic/Graph/GraphRunOptions.cssrc/Agentic/Graph/GraphRunResult.cssrc/Agentic/Graph/GraphSpecialNodes.cssrc/Agentic/Graph/GraphStepUpdate.cssrc/Agentic/Graph/StateGraph.cssrc/AiDotNet.Storage.Sqlite/SqliteGraphCheckpointer.cstests/AiDotNet.Tests/UnitTests/Agentic/Graph/GraphCheckpointingTests.cstests/AiDotNet.Tests/UnitTests/Agentic/Graph/GraphCompositionTests.cstests/AiDotNet.Tests/UnitTests/Agentic/Graph/GraphDurableCheckpointerTests.cstests/AiDotNet.Tests/UnitTests/Agentic/Graph/GraphFanOutTests.cstests/AiDotNet.Tests/UnitTests/Agentic/Graph/GraphInterruptTests.cstests/AiDotNet.Tests/UnitTests/Agentic/Graph/StateGraphTests.cs
…ty/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>
…ull-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>
) * 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>
Phase 1 (graph runtime) — slice 1 of the agentic-orchestration epic
Part of #1544. Phase 0 is in #1545; this PR begins Phase 1, the typed graph runtime that is the core differentiator vs LangGraph (typed state, compile-time wiring, deterministic execution — durability/HITL to follow).
What's in this slice (
AiDotNet.Agentic.Graph)StateGraph<TState>— fluent builder:AddNode(sync/async),AddEdge,AddConditionalEdges,SetEntryPoint,Compile(). Fully typed state (no stringly-typed state dicts).Compile()validates the graph (entry exists; edges resolve to a node orEnd; a node can't have both a fixed edge and conditional edges).CompiledStateGraph<TState>—InvokeAsync(returns final state) andStreamAsync(yields aGraphStepUpdate<TState>after each node). Cycles are allowed and bounded byGraphRunOptions.MaxSteps(default 25) →GraphRecursionException.GraphSpecialNodes.Endsentinel; conditional routers return a node name orEnd.Not in this slice (subsequent Phase 1 PRs)
Durable checkpointing (memory/SQLite/Postgres/Redis), resume, time-travel + deterministic record/replay, human-in-the-loop interrupts, channels/reducers + dynamic fan-out (Send/map-reduce), subgraphs, reward-gated edges.
Verification
UnitTests/Agentic/Graph/StateGraphTests.cs: 7/7 pass on net10.0 and net471 (linear, conditional cycle, streaming, recursion-limit, async node, compile validation, duplicate/reserved names).Conventions: generic
<T>,Guard.*, enums/sentinels (no magic strings), "For Beginners" XML docs, and no null-forgiving operators.🤖 Generated with Claude Code
Summary by CodeRabbit