feat(agentic): Phase 0 — IChatClient<T> model abstraction (epic #1544)#1545
Conversation
…on (#1544) Introduces the foundational chat-model abstraction for the new agentic orchestration subsystem (AiDotNet.Agentic.Models), superseding the text-in/text-out IChatModel<T>/ILanguageModel<T>: - IChatClient<T>: message-based, native tool-calling, streaming (IAsyncEnumerable), structured output - DTOs: ChatRole/ChatFinishReason/ToolChoiceMode/ChatResponseFormatKind enums; AiContent hierarchy (Text/Image/ToolCall/ToolResult); ChatMessage (+factories); AiToolDefinition (JObject JSON-schema); ChatOptions; ChatUsage; ChatResponse; ChatResponseUpdate; StreamingToolCallUpdate - 15 unit tests, green on net10.0 and net471 (incl. streaming round-trip) Part of epic #1544 (Phase 0: foundations). 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 21 minutes and 9 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)
WalkthroughAdds agentic chat/message/tool contracts, a connector base plus OpenAI/Anthropic/Azure/MEAI adapters, tool runtime/schema generation and a source generator, and migrates reasoning, benchmarking, and builder code to use ChangesAgentic chat stack migration
Sequence Diagram sequenceDiagram
participant AiModelResult
participant IChatClientT
participant ChatClientBaseT
participant ChatConnector
participant IAgentTool
participant ToolCollection
AiModelResult->>IChatClientT: ReasonAsync/EvaluateBenchmarkAsync
IChatClientT->>ChatClientBaseT: GetResponseAsync/GetStreamingResponseAsync
ChatClientBaseT->>ChatConnector: provider-specific request/response flow
ChatConnector->>IAgentTool: InvokeAsync for tool calls
ToolCollection->>IAgentTool: dispatch tool invocation
ChatConnector-->>IChatClientT: ChatResponse/ChatResponseUpdate
IChatClientT-->>AiModelResult: response text / reasoning output
Estimated code review effort Possibly related PRs
Suggested labels
✨ Finishing Touches🧪 Generate unit tests (beta)
|
…1544) Executable tool abstraction for native function calling: - IAgentTool / AgentToolBase / ToolInvocationResult; [AgentTool]/[ToolParameter] attributes - JsonSchemaGenerator: reflection-based C# type -> JSON Schema (primitives, enums, arrays, string-keyed dicts, nested objects w/ depth+cycle guard); required inferred from defaults/nullability - DelegateAgentTool: binds model JSON args to typed params (CancellationToken injected, hidden from schema), supports sync/Task/Task<T>, serializes results, surfaces failures as error results - AgentToolFactory (delegate + [AgentTool] scanning) and ToolCollection (registry -> AiToolDefinition list, dispatch tool-calls -> ChatRole.Tool messages) - 14 unit tests green on net10.0 and net471 Part of epic #1544 (Phase 0). 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 |
…pe enum (#1544) Type-safety over fragile strings: image format is now a closed enum (Png/Jpeg/Gif/Webp) with ToMimeType()/TryParseMimeType() wire conversions, instead of a raw 'image/png' string that could be misspelled. Tests updated; green on net10.0 + net471. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#1544) First new-stack connectors (replacing legacy LanguageModels): - ChatClientExtensions.GenerateTextAsync (prompt-in/text-out bridge used by reasoning + simple callers) - ChatClientBase<T>: shared HTTP transport, retry-with-backoff (non-streaming), API-key validation, message validation - OpenAIChatClient<T>: Chat Completions mapping with native tool calling, tool_choice, structured output (json/json_schema), multimodal image input, SSE streaming; provider strings (roles, finish_reason) mapped to enums - Mock-HTTP tests for request shaping, tool-call/finish-reason/usage parsing, and SSE reconstruction; green on net10.0 + net471 Part of epic #1544 (Phase 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the new-stack connector trio: - AzureOpenAIChatClient<T>: derives from OpenAIChatClient (shared Chat Completions wire), overrides endpoint (per-deployment URL + api-version) and auth (api-key header). OpenAI client refactored to expose protected ApiKey/Endpoint + virtual ApplyAuthentication. - AnthropicChatClient<T>: Messages API with top-level system, content-block messages (text/image/tool_use/tool_result), required max_tokens, tool_choice mapping, stop_reason->enum, and event-based SSE streaming (message_start/content_block_delta/message_delta). - Mock-HTTP tests for both; 7 connector tests green on net10.0 + net471. Part of epic #1544 (Phase 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1544) Migrates the entire Reasoning module (17 files: strategies, components, verification, training, domain reasoners, base) and AiModelResult reasoning methods off the legacy IChatModel<T>/ITool onto the new Agentic IChatClient<T>/IAgentTool. Adds ChatClientExtensions.GenerateResponseAsync alias so call sites are unchanged. Facade CreateChatModelFromAgentConfig now builds the new OpenAI/Anthropic/Azure connectors. ReasoningStrategyBase.ExecuteTool -> async ExecuteToolAsync via IAgentTool.InvokeAsync. Core builds clean both TFMs. WIP: legacy deletion + agent-assistance removal + test cleanup follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… drop AgentConfig/AgentRecommendation (#1544) [WIP] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…terfaces + agent-config types (#1544) [WIP] Removes src/{Agents,Tools,LanguageModels,PromptEngineering/Chains,PromptEngineering/Tools}, 6 legacy interfaces (IChatModel/ILanguageModel/IAgent/IChain/ITool/IFunctionTool), and Agent{Configuration,Recommendation,GlobalConfigurationBuilder,AssistanceOptions,AssistanceOptionsBuilder} + the stale global using. Facade fixes (agent-assistance + PromptChain feature removal) follow in subsequent commits; build is intentionally red until then. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#1544) [WIP] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… green (#1544) Removes the legacy 'LLM picks model/hyperparameters' pipeline (Internals.cs Get/ApplyAgentRecommendations, CreateChatModel, _agentConfig/_agentOptions, ConfigureAgentAssistance/AskAgentAsync) and the PromptChain feature (built on deleted IChain<,>): AiModelResult PromptChain prop/RunChain/RunChainAsync/HasPromptChain, options fields, AttachPromptEngineering promptChain param, IConfiguredView/IAiModelBuilder members. Retypes ConfigureTool->IAgentTool and ConfigureRLAgent->IRLAgent<T>. EvaluateBenchmark[s]Async now takes an injected IChatClient<T>. Deletes IPromptChain. Core builds clean on net10.0 + net471 with zero legacy remaining. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…IChatClient (#1544) Deletes tests for purged features (Agents/Tools/LanguageModels/ToolRegistry/PromptEngineering-chains + agent-assistance Bucket11 cases + MergedPRBugFix LanguageModels region). Rewires ReasoningExtended MockChatModel to IChatClient<T>. Test project builds clean on net10.0 + net471; Agentic + Reasoning suites green except a pre-existing MCTS visit-count failure unrelated to this change (MonteCarloTreeSearch untouched). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y test dirs (#1544) MonteCarloTreeSearch.SearchAsync had selection/expansion/simulation but step 4 (backpropagation) was an empty comment — visit counts/total_value were never updated, so root.Metadata[visits] stayed 0 and UCB1 was degenerate. Select now returns the root->leaf path and backprop accumulates visits+value along it. Fixes pre-existing MCTS_Backpropagation_VisitCountsIncrease (now 118/118 green). Also deletes orphaned, uncompiled legacy test dirs tests/UnitTests + tests/Reasoning (not in any csproj). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…deserialized) (#1544) StructuredOutputExtensions.GetStructuredResponseAsync<T,TResult>: derives a JSON schema from TResult via JsonSchemaGenerator, sets ResponseFormat=JsonSchema, and deserializes the reply into TResult. 2 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ving operators (#1544) - MeaiChatClient<T> adapts a Microsoft.Extensions.AI IChatClient to AiDotNet's IChatClient<T> (text+sampling+streaming; tools throw NotSupported for now). Adds Microsoft.Extensions.AI.Abstractions 10.6.0 to core (CPM). 3 adapter tests. - Removed every null-forgiving operator (!) introduced in the agentic work (AgentToolFactory, ToolCallContent, MeaiChatClient + 4 test files) per project policy; replaced with is-null narrowing / null-conditional navigation. Verified zero remain via scan. All Agentic suites green on net10.0 + net471 (41 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n-free) (#1544) AgentToolSchemaGenerator (Roslyn incremental) emits a CreateAgentTools() extension per [AgentTool]-bearing type, producing FunctionAgentTool instances with compile-time JSON schemas (JObject.Parse of a generation-time-computed string) and typed argument binders (sync/async split to avoid CS1998; #nullable disable so generated code needs no null-forgiving). Accessibility matches the owner type. Supports primitives/enums/arrays/IEnumerable/string-keyed dicts (+object fallback). Completes Phase 0. Test green on net10.0 + net471. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
src/Interfaces/IAiModelBuilder.cs (1)
1467-1479:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBlocking: the public
ConfigureReasoningexample still references removedConfigureAgentAssistance.This XML doc now advertises a call chain that no longer exists on
IAiModelBuilder, so the published API example is broken the moment users paste it. Production-ready docs here should compile against the new chat-client/tooling surface or remove the outdated step entirely.🛠 Suggested fix
- /// var agentConfig = new AgentConfiguration<double> - /// { - /// ApiKey = "sk-...", - /// Provider = LLMProvider.OpenAI, - /// IsEnabled = true - /// }; - /// /// var result = await new AiModelBuilder<double, Matrix<double>, Vector<double>>() - /// .ConfigureAgentAssistance(agentConfig) /// .ConfigureReasoning() /// .BuildAsync();If reasoning now requires a different configuration path, replace the removed step with that new supported setup instead of leaving the stale example in place.
🤖 Prompt for 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. In `@src/Interfaces/IAiModelBuilder.cs` around lines 1467 - 1479, The XML doc example for IAiModelBuilder is stale: it shows chaining ConfigureAgentAssistance(...) before ConfigureReasoning(), but ConfigureAgentAssistance no longer exists; update the example under ConfigureReasoning to use the current initialization/configuration flow for IAiModelBuilder (e.g., remove the ConfigureAgentAssistance call or replace it with the new supported method used to enable agent-related settings), ensuring the snippet compiles against AiModelBuilder<TModel,...> and references only existing public APIs (e.g., ConfigureReasoning, the current agent/config setup method) so the example is valid.Source: Coding guidelines
src/Models/Results/AiModelResult.cs (1)
1441-1456:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRefresh the public XML docs for the new
IChatClient<T>contract.
EvaluateBenchmarksAsyncaddschatClientwithout documenting it, and the benchmark/reasoning docs still describe the removed agent-configured flow and examples that omit the new required argument. That leaves the facade docs out of sync with the public API, and some sample calls no longer compile. Please update the XML comments/examples and call out this break in migration notes.📝 Suggested doc updates
- /// <param name="cancellationToken">Cancellation token.</param> + /// <param name="chatClient">Chat client used for benchmark suites that need model-generated answers.</param> + /// <param name="cancellationToken">Cancellation token.</param> - /// <exception cref="InvalidOperationException">Thrown when agent configuration is not set up.</exception> + /// <exception cref="ArgumentNullException">Thrown when <paramref name="chatClient"/> is null.</exception> - /// - Agent configuration must be set (ConfigureAgentAssistance during building) + /// - A chat client must be supplied - /// var result = await modelResult.ReasonAsync( - /// "If a train travels 60 mph for 2.5 hours, how far does it go?", - /// ReasoningMode.ChainOfThought - /// ); + /// var result = await modelResult.ReasonAsync( + /// "If a train travels 60 mph for 2.5 hours, how far does it go?", + /// chatClient, + /// ReasoningMode.ChainOfThought + /// );Based on learnings: for this repo’s public API surface, prefer clean breaking changes rather than shims, but document the break in release notes and/or migration notes so downstream consumers can update.
Also applies to: 5911-5927, 6562-6600, 6619-6640, 6652-6682, 6694-6726
🤖 Prompt for 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. In `@src/Models/Results/AiModelResult.cs` around lines 1441 - 1456, Update the public XML documentation for EvaluateBenchmarksAsync to describe the new IChatClient<T> parameter (its purpose, when to provide null, and how it affects evaluation), update method summary/params/returns to reference BenchmarkingOptions, IChatClient<T>, and BenchmarkReport, refresh any sample XML example calls to include a chatClient argument or show how to call with null/default, and add a brief migration note indicating this is a breaking change so downstream callers must supply an IChatClient<T> or adjust their call sites; apply the same documentation updates to the other affected overloads/locations referenced in the review (the ranges noted) to keep the public API docs consistent.Source: Learnings
src/Reasoning/DomainSpecific/CodeReasoner.cs (1)
74-81:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDocument this public API break across the domain reasoners.
These constructors now require
IChatClient<T>/IAgentTool>instead of the legacy abstractions, so downstream callers will hit both source and binary breaks when upgrading. Please add explicit migration or release-note guidance alongside this PR.Based on learnings, clean breaking changes are acceptable here, but they should be called out in release or migration notes so downstream consumers can update intentionally.
🤖 Prompt for 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. In `@src/Reasoning/DomainSpecific/CodeReasoner.cs` around lines 74 - 81, The public constructor for CodeReasoner now takes IChatClient<T> and IEnumerable<IAgentTool> causing a source/binary break; update the PR to include clear migration guidance and release-note text explaining the API change and how consumers should migrate (e.g., replace legacy chat/agent abstractions with IChatClient<T> and IAgentTool, update construction of ChainOfThoughtStrategy<T>, TreeOfThoughtsStrategy<T>, CriticModel<T>, and SelfRefinementEngine<T> usages), and add a short example snippet or checklist for downstream maintainers to follow when upgrading their codebases.Source: Learnings
src/Reasoning/Components/ThoughtEvaluator.cs (2)
66-67:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBlocking: the
IChatClient<T>migration dropped cancellation propagation in two reasoning components.Both components accept a
CancellationToken, but neither passes it into the outbound model call. That leaves canceled reasoning runs waiting on the remote LLM anyway. Thread the token through bothGenerateResponseAsynccalls.As per coding guidelines, missing error handling at system boundaries is a blocking production-readiness issue.
🤖 Prompt for 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. In `@src/Reasoning/Components/ThoughtEvaluator.cs` around lines 66 - 67, The GenerateResponseAsync calls dropped CancellationToken propagation after the IChatClient<T> migration; update the calls in ThoughtEvaluator (and the other reasoning component that calls IChatClient<T>.GenerateResponseAsync) to pass the incoming CancellationToken through to GenerateResponseAsync so cancellations abort the remote LLM call (e.g., change string response = await _chatModel.GenerateResponseAsync(prompt); to call the overload that accepts the token), and ensure you use the same token parameter name (CancellationToken token) from the method signature so cancellation flows end-to-end.Source: Coding guidelines
120-124:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBlocking: validate the
"score"token before converting it.This is parsing external model output. If the model returns
"score": "high"or an object/array,Value<double>()throws and the evaluator fails instead of falling back to the regex/default path.Suggested fix
var scoreToken = root["score"]; if (scoreToken != null) { - double score = scoreToken.Value<double>(); - return MathHelper.Clamp(score, 0.0, 1.0); + try + { + double score = scoreToken.Value<double>(); + return MathHelper.Clamp(score, 0.0, 1.0); + } + catch (FormatException) + { + // Continue to fallback parsing below. + } + catch (InvalidCastException) + { + // Continue to fallback parsing below. + } }As per coding guidelines, missing validation of external inputs is a blocking production-readiness issue.
🤖 Prompt for 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. In `@src/Reasoning/Components/ThoughtEvaluator.cs` around lines 120 - 124, The code reads root["score"] and calls scoreToken.Value<double>() directly; validate the token before conversion to avoid exceptions on non-numeric model output. In the block where scoreToken is obtained (root["score"]), check scoreToken.Type (or use scoreToken.ToString()) and attempt a safe parse with double.TryParse(..., CultureInfo.InvariantCulture) or only accept JTokenType.Float/Integer; if parsing fails, do not call Value<double>() and instead fall back to the existing regex/default path; then clamp the parsed double with MathHelper.Clamp as before. Ensure you reference scoreToken, Value<double>(), MathHelper.Clamp and the fallback path in the ThoughtEvaluator logic.Source: Coding guidelines
src/Reasoning/DomainSpecific/MathematicalReasoner.cs (1)
129-132:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBlocking: guard
ReasoningChainbefore entering verification.
VerifyAndRefineAsyncimmediately iterateschain.Steps, but the caller only checksresult.Success. The other reasoners in this PR already treatReasoningChainas nullable, so a successful result without a populated chain will throw here.Suggested fix
- if (useVerification && result.Success) + if (useVerification && result.Success && result.ReasoningChain != null) { result = await VerifyAndRefineAsync(result, config, cancellationToken); }private async Task<ReasoningResult<T>> VerifyAndRefineAsync( ReasoningResult<T> result, ReasoningConfig config, CancellationToken cancellationToken) { + if (result.ReasoningChain == null) + return result; + var chain = result.ReasoningChain; bool hasCalculationErrors = false; var verificationResults = new List<string>();As per coding guidelines, missing null checks on boundary data are blocking production-readiness issues.
Also applies to: 146-156
🤖 Prompt for 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. In `@src/Reasoning/DomainSpecific/MathematicalReasoner.cs` around lines 129 - 132, The code calls VerifyAndRefineAsync whenever result.Success is true but doesn't ensure result.ReasoningChain is non-null, which causes iteration over chain.Steps to throw; update the guard to require both result.Success and result.ReasoningChain != null before calling VerifyAndRefineAsync (and mirror the same null-check where similar verification is triggered around the 146-156 block), or alternatively add a defensive null check at the start of VerifyAndRefineAsync to return/skip if the incoming ReasoningChain is null so iteration over chain.Steps is never attempted on a null object.Source: Coding guidelines
src/Reasoning/Components/ThoughtGenerator.cs (1)
141-165:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBlocking: don't silently return zero thoughts for valid-but-unusable JSON.
Right now only
JsonExceptiontriggers the line-based fallback. A response like{ "items": [...] }or{ "thoughts": "single string" }parses successfully, butParseThoughtsreturns an empty list and the generator silently does nothing.Suggested fix
private List<string> ParseThoughts(string response, int expectedCount) { var thoughts = new List<string>(); try { // Try JSON parsing string jsonContent = ExtractJsonFromResponse(response); var root = JObject.Parse(jsonContent); if (root["thoughts"] is JArray thoughtsArray) { foreach (var thought in thoughtsArray) { string thoughtText = thought.Value<string>() ?? ""; if (!string.IsNullOrWhiteSpace(thoughtText)) { thoughts.Add(thoughtText.Trim()); } } } } catch (JsonException) { - // Fallback to line-based parsing - thoughts = ParseThoughtsFromLines(response, expectedCount); + thoughts = ParseThoughtsFromLines(response, expectedCount); } + + if (thoughts.Count == 0) + { + thoughts = ParseThoughtsFromLines(response, expectedCount); + } return thoughts; }As per coding guidelines, half-implemented code paths that silently do nothing are blocking issues.
🤖 Prompt for 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. In `@src/Reasoning/Components/ThoughtGenerator.cs` around lines 141 - 165, The current JSON parsing block in ThoughtGenerator (using ExtractJsonFromResponse and JObject.Parse) only falls back to ParseThoughtsFromLines on JsonException, which lets valid-but-unusable JSON (e.g., missing "thoughts" key or "thoughts" not being a JArray) produce an empty thoughts list silently; update the logic so that after parsing the JObject you verify that root["thoughts"] is a non-empty JArray and contains string items, and if not (null, wrong type, or yields no valid strings) call ParseThoughtsFromLines(response, expectedCount) as the fallback; preserve throwing/parsing errors, but ensure you do not return an empty list silently—optionally emit a warning via the existing logging mechanism when falling back.Source: Coding guidelines
src/Reasoning/Strategies/ChainOfThoughtStrategy.cs (2)
149-157:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBlocking: remove the fake verification path.
When
EnableVerificationis on, this branch ships aTODOand marks every step as verified anyway. That creates false-positive verification and contaminates downstream confidence/metrics. Production-ready code here should either call a real verifier or fail fast until one is wired.💡 Suggested fix
if (config.EnableVerification && parsedSteps.Count > 0) { - AppendTrace("Verification enabled - checking reasoning quality..."); - // TODO: Implement verification when ICriticModel concrete implementation is available - // For now, mark all steps as verified with high confidence - foreach (var step in chain.Steps) - { - step.IsVerified = true; - } + throw new NotSupportedException( + "Chain-of-thought verification requires a concrete verifier. " + + "Disable ReasoningConfig.EnableVerification until verification is implemented."); }As per coding guidelines, “Every PR must contain production-ready code” and both
// TODOplaceholders and “hardcoded values instead of proper logic” are blocking issues.🤖 Prompt for 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. In `@src/Reasoning/Strategies/ChainOfThoughtStrategy.cs` around lines 149 - 157, The code in ChainOfThoughtStrategy currently fakes verification when config.EnableVerification is true by marking all chain.Steps as verified; remove this fake path and instead fail fast or call the real verifier: if config.EnableVerification is true and no concrete ICriticModel/verifier is configured, throw a clear NotImplementedException (or a specific InvalidOperationException) with context via AppendTrace rather than setting step.IsVerified, otherwise invoke the real verifier API (the ICriticModel implementation) to set each step's IsVerified and confidence; ensure the branch references config.EnableVerification, ChainOfThoughtStrategy, AppendTrace, and chain.Steps so reviewers can locate and wire the proper verifier or enforce the fail-fast behavior.Source: Coding guidelines
201-216:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winThe JSON example is malformed.
The literal
...insidereasoning_stepsmakes the sample invalid JSON, so the model is more likely to echo invalid syntax and force the regex fallback even when_useJsonFormatis enabled.💡 Suggested fix
Respond in JSON format: {{ ""reasoning_steps"": [ ""Step 1: [your first reasoning step with explanation]"", - ""Step 2: [your second reasoning step with explanation]"", - ... + ""Step 2: [your second reasoning step with explanation]"" ], ""final_answer"": ""your final answer to the query"" }} + +Add more items to `reasoning_steps` as needed, up to the configured maximum.🤖 Prompt for 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. In `@src/Reasoning/Strategies/ChainOfThoughtStrategy.cs` around lines 201 - 216, The JSON example in ChainOfThoughtStrategy.cs is malformed because the literal "..." inside the reasoning_steps array makes the sample invalid; update the prompt template used by the method (search for the string starting with "Respond in JSON format:" in ChainOfThoughtStrategy or the variable that contains this template, and references to _useJsonFormat) to provide a valid JSON example—remove the "..." and either include a fixed small valid example entry or annotate with a JSON-safe placeholder (e.g., an explicit array with up to {config.MaxSteps} numbered step examples) and ensure the MaxSteps placeholder is rendered correctly so the template produces valid JSON when sent to the model.src/Reasoning/Training/ReinforcementLearner.cs (2)
477-489:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftBlocking: the checkpoint API does not actually checkpoint model state.
These methods are presented as save/load for resumable training, but they only persist counters/config/data-collector state. The injected model’s weights/state are never saved, so loading resumes bookkeeping against whatever model instance happens to be in memory.
💡 Suggested fix
+internal interface ICheckpointableModel +{ + Task<byte[]> SaveStateAsync(CancellationToken cancellationToken = default); + Task LoadStateAsync(byte[] state, CancellationToken cancellationToken = default); +} + internal class TrainingCheckpoint<T> { @@ public TrainingDataCollector<T>? TrainingData { get; set; } + public string? ModelStateBase64 { get; set; } } @@ public async Task SaveCheckpointAsync(string filePath, CancellationToken cancellationToken = default) { + if (_model is not ICheckpointableModel checkpointableModel) + throw new NotSupportedException("The configured model does not support checkpointing."); + var checkpoint = new TrainingCheckpoint<T> { @@ - TrainingData = _dataCollector + TrainingData = _dataCollector, + ModelStateBase64 = Convert.ToBase64String( + await checkpointableModel.SaveStateAsync(cancellationToken)) }; @@ public async Task LoadCheckpointAsync(string filePath, CancellationToken cancellationToken = default) { @@ + if (_model is not ICheckpointableModel checkpointableModel || checkpoint.ModelStateBase64 == null) + throw new NotSupportedException("The checkpoint does not contain model state."); + + await checkpointableModel.LoadStateAsync( + Convert.FromBase64String(checkpoint.ModelStateBase64), + cancellationToken);As per coding guidelines, “Future enhancement” comments and incomplete features that only partially implement the advertised behavior are blocking issues.
Also applies to: 494-505, 524-556
🤖 Prompt for 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. In `@src/Reasoning/Training/ReinforcementLearner.cs` around lines 477 - 489, The SaveCheckpointAsync currently only persists training counters/config/data collector but omits the injected model weights, so make SaveCheckpointAsync serialize the model state too (e.g., call the injected model's Save/Export method or implement a GetWeights/Serialize method on the model interface) and write that bytes/blob into the same checkpoint (or alongside with a clear filename/version); update the corresponding LoadCheckpointAsync to restore those serialized weights into the injected model (e.g., call model.Load/Import or SetWeights) before restoring epoch/bestAccuracy/earlyStopping counters and TrainingDataCollector state, and include versioning/validation so mismatched model formats throw a clear error and respect the cancellationToken during I/O.Source: Coding guidelines
247-285:⚠️ Potential issue | 🟠 Major | ⚡ Quick winBlocking: remove
Console.WriteLinefrom the library training path.This library code writes progress directly to stdout during normal operation. That is a non-production pattern here and gives hosts no control over formatting, routing, or suppression. Use the existing events for progress, and push any human-readable output behind an injected logger abstraction.
💡 Suggested fix
- Console.WriteLine($"\n=== Epoch {epoch + 1}/{_config.Epochs} ==="); @@ - Console.WriteLine($"Validation Accuracy: {evalMetrics.Accuracy:P2}"); @@ - Console.WriteLine($"Early stopping at epoch {epoch + 1}"); @@ - Console.WriteLine($"\n=== STaR Epoch {epoch + 1}/{_config.Epochs} ==="); @@ - Console.WriteLine($"STaR Chains Collected: {trainingMetrics.ChainCount}"); - Console.WriteLine($"Average Reward: {Convert.ToDouble(trainingMetrics.AverageReward):F3}"); @@ - Console.WriteLine($"Validation Accuracy: {evalMetrics.Accuracy:P2}"); @@ - Console.WriteLine($" Batch {batchIdx + 1}/{batches.Count}");As per coding guidelines, “Using
Console.WriteLinefor logging instead of proper logging abstractions” is a blocking non-production pattern.Also applies to: 317-347, 424-425
🤖 Prompt for 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. In `@src/Reasoning/Training/ReinforcementLearner.cs` around lines 247 - 285, Replace direct Console.WriteLine calls in the training loop with the library's logging/event mechanism: remove Console.WriteLine calls in the epoch loop (the lines around the Epoch header, validation accuracy, and early-stopping message) and instead emit the existing progress event or call the injected logger (use the same abstraction other parts of the class use). Specifically, in the method containing TrainEpochAsync and EvaluateAsync, replace prints for the epoch start, Validation Accuracy, and "Early stopping" with event invocations or ILogger methods (use the same event name or logger instance already present on the class), and ensure SaveCheckpointAsync and metrics updates remain unchanged; run unit/integration tests to confirm no Console.WriteLine remains in the indicated regions.Source: Coding guidelines
src/Reasoning/Training/PolicyGradientTrainer.cs (1)
142-163:⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy liftBlocking: this “trainer” still cannot train an
IChatClient<T>.
TrainBatchAsynccomputes rewards and losses, but it never uses_modelor applies any policy update. After this migration the dependency is explicitly an inference client, so the class now advertises RL training while performing zero model-state changes.💡 Suggested fix
-internal class PolicyGradientTrainer<T> +internal interface ITrainableChatPolicy<T> : IChatClient<T> +{ + Task ApplyPolicyUpdateAsync( + IReadOnlyList<ReasoningChain<T>> chains, + Vector<T> advantages, + double learningRate, + CancellationToken cancellationToken = default); +} + +internal class PolicyGradientTrainer<T> { - private readonly IChatClient<T> _model; + private readonly ITrainableChatPolicy<T> _model; @@ - public PolicyGradientTrainer( - IChatClient<T> model, + public PolicyGradientTrainer( + ITrainableChatPolicy<T> model, IRewardModel<T>? rewardModel = null, @@ var totalLoss = _numOps.Add( policyLoss, _numOps.Multiply(_numOps.FromDouble(_entropyCoefficient), entropy) ); metrics.TotalLoss = totalLoss; + + await _model.ApplyPolicyUpdateAsync( + chains, + advantages, + _learningRate, + cancellationToken);As per coding guidelines, “All methods have complete, production-ready implementations” and simplified implementations that stop at metrics instead of doing the real work are blocking issues.
Also applies to: 219-245
🤖 Prompt for 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. In `@src/Reasoning/Training/PolicyGradientTrainer.cs` around lines 142 - 163, TrainBatchAsync currently computes rewards and losses but never updates model parameters; modify the trainer to perform actual policy updates by deriving gradients from the computed loss and applying them to the model via the _model field: (1) if IChatClient<T> already exposes parameter update APIs, call the appropriate method (e.g., ApplyGradients/UpdateParameters) with gradients scaled by _learningRate, including entropy regularization using _entropyCoefficient and discounting with _discountFactor; (2) if IChatClient<T> is inference-only, introduce/require a trainable interface (e.g., ITrainableChatClient<T> or methods like GetParameters/SetParameters/ApplyGradients) and cast or accept that interface in the constructor; (3) update the running baseline (_baseline) using returns and _useBaseline to compute advantages before applying gradients; and (4) ensure TrainBatchAsync updates model state synchronously/atomically and handles _numOps for numeric ops when computing gradients and advantages so the trainer actually mutates the model rather than only reporting metrics.Source: Coding guidelines
src/Reasoning/Verification/CriticModel.cs (1)
182-188:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winThe sample JSON in the prompt is malformed.
"feedback"runs directly into"strengths"here, so the example object is not valid JSON. That makes the model more likely to return invalid JSON and fall back to the text parser.💡 Suggested fix
Respond in JSON format: {{ ""score"": 0.85, - ""feedback"": ""The reasoning chain...,""strengths"": [...], + ""feedback"": ""The reasoning chain..."", + ""strengths"": [...], ""weaknesses"": [...], ""suggestions"": [...] }}🤖 Prompt for 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. In `@src/Reasoning/Verification/CriticModel.cs` around lines 182 - 188, The sample JSON in the prompt inside CriticModel is malformed because the "feedback" value runs into the "strengths" key; update the prompt construction in the CriticModel class (where the JSON example is assembled) so the example is valid JSON — include the missing comma and ensure arrays for "strengths", "weaknesses", and "suggestions" are properly bracketed and quoted and that the entire object is well-formed; adjust the string literal or formatter used in the method that builds the prompt to emit a syntactically correct JSON example (e.g., {"score":0.85,"feedback":"...","strengths":[],"weaknesses":[],"suggestions":[]}) so downstream parsers receive valid JSON.src/Reasoning/Verification/OutcomeRewardModel.cs (1)
216-226:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBLOCKING: Remove TODO comment and implement verification results or remove incomplete code section.
This TODO comment with commented-out code is not production-ready. The
CalculateUnsupervisedRewardAsyncmethod is incomplete—it promises a 4-component heuristic ("Verification: Did it pass any verifiers?") but only implements 3 components (completeness, confidence, coherence) and leaves the verification scoring as a placeholder.Per the production readiness guidelines, TODO comments and incomplete implementations are blocking issues. Either:
- Implement the verification results integration now, or
- Remove the commented-out code and the TODO, and update the method documentation to accurately reflect that only 3 heuristics are used
Production-ready code should not ship with known gaps documented in TODO comments.
🔧 Suggested fix: Remove incomplete code section
} // 3. Coherence: Are steps logically connected? if (chain.Steps.Count > 1) { bool hasCoherence = chain.Steps.All(s => !string.IsNullOrWhiteSpace(s.Content)); if (hasCoherence) { reward += 0.2; } } - // 4. Verification: Did it pass any verifiers? - // TODO: ReasoningChain<T> doesn't have VerificationResults property yet - // if (chain.VerificationResults?.Count > 0) - // { - // double verificationScore = chain.VerificationResults - // .Where(v => v.Passed) - // .Select(v => Convert.ToDouble(v.Confidence)) - // .DefaultIfEmpty(0.0) - // .Average(); - // - // reward += verificationScore * 0.1; - // } - return _numOps.FromDouble(MathHelper.Clamp(reward, 0.0, 1.0)); }Also update the method's logic comments (lines 178-213) to remove references to the 4th component, or implement the verification results integration if
ReasoningChain<T>now supports it.🤖 Prompt for 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. In `@src/Reasoning/Verification/OutcomeRewardModel.cs` around lines 216 - 226, The CalculateUnsupervisedRewardAsync method currently contains a TODO and commented-out verification logic; either remove the TODO and the entire commented block and update the method summary/comments to state it uses only three heuristics (completeness, confidence, coherence), or implement the verification integration by reading ReasoningChain<T>.VerificationResults (ensure the property exists), compute verificationScore as the average Confidence for Passed results (default 0.0), multiply by the intended weight (0.1) and add to reward, and remove the TODO/comment once implemented; reference the CalculateUnsupervisedRewardAsync method and ReasoningChain<T>.VerificationResults when making the change.Source: Coding guidelines
src/Reasoning/Verification/SelfRefinementEngine.cs (1)
75-75: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winConsider propagating cancellationToken to the chat client call.
The
RefineStepAsyncmethod accepts aCancellationTokenparameter (line 60), but line 75 callsGenerateResponseAsync(prompt)without passing it. Propagating the token would allow refinement operations to be canceled during the LLM call.♻️ Suggested enhancement
- string refinedContent = await _chatModel.GenerateResponseAsync(prompt); + string refinedContent = await _chatModel.GenerateResponseAsync(prompt, cancellationToken);Note:
ProcessRewardModel.csline 85 already follows this pattern.🤖 Prompt for 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. In `@src/Reasoning/Verification/SelfRefinementEngine.cs` at line 75, RefineStepAsync is not propagating the received CancellationToken to the LLM call; update the call to _chatModel.GenerateResponseAsync to pass the CancellationToken parameter (the same token passed into RefineStepAsync) so the LLM call can be canceled (match the pattern used in ProcessRewardModel.cs where GenerateResponseAsync(..., cancellationToken) is used); ensure you use the correct parameter name (cancellationToken) and overload/signature on _chatModel.GenerateResponseAsync.
🤖 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/Models/Connectors/ChatClientBase.cs`:
- Around line 58-62: The ChatClientBase constructor currently overwrites a
caller-owned HttpClient.Timeout; change protected ChatClientBase(HttpClient?
httpClient) so it sets Timeout only when it creates the HttpClient internally
(i.e., when httpClient is null) and do not mutate the passed-in HttpClient; use
per-call cancellation tokens instead of changing caller-owned Timeout. Also
tighten IsRetryable(HttpRequestException ex): on frameworks lacking
HttpRequestException.StatusCode (net471) inspect ex.InnerException (e.g.,
WebException/HttpWebResponse) and return true only for network timeouts and HTTP
status codes 408, 429, or any 5xx; return false for other permanent failures.
Ensure you modify the ChatClientBase constructor, the HttpClient property usage,
and the IsRetryable(HttpRequestException) implementation accordingly.
- Around line 151-163: ChatClientBase.IsRetryable currently returns true for all
HttpRequestException on non-NET5 builds; update the error flow so
OpenAIChatClient and AnthropicChatClient preserve HTTP status codes (do not
swallow them into plain HttpRequestException) by introducing and throwing a
custom exception (e.g., HttpResponseException containing an HttpStatusCode
property) when a non-success response is received, then change
ChatClientBase.IsRetryable(HttpRequestException) to detect that custom exception
(or check ex for an embedded StatusCode) and return true only for genuine
network errors or when status is 429, 408, or >=500; update unit tests and call
sites to throw/handle the new exception type accordingly.
In `@src/Agentic/Models/Connectors/MeaiChatClient.cs`:
- Around line 172-173: The current cast of usage.InputTokenCount and
usage.OutputTokenCount to int (variables input and output) can overflow; change
these to use long (e.g., declare input and output as long and assign
usage.InputTokenCount ?? 0 and usage.OutputTokenCount ?? 0) or, if downstream
APIs require int, clamp values to int.MaxValue (or throw) instead of direct
casting; update any downstream uses in MeaiChatClient (references to input,
output, InputTokenCount, OutputTokenCount) accordingly to preserve correctness
and avoid overflow.
In `@src/Agentic/Models/Connectors/OpenAIChatClient.cs`:
- Around line 420-438: BuildResponseFormat currently returns null when
ResponseFormat == ChatResponseFormatKind.JsonSchema but ResponseJsonSchema is
null, allowing requests to proceed with no response_format; instead validate
this combination early and fail fast. In BuildResponseFormat (and where
BuildRequest is assembled) check if options.ResponseFormat ==
ChatResponseFormatKind.JsonSchema and options.ResponseJsonSchema is null, and
throw an ArgumentException or InvalidOperationException with a clear message;
update callers so they cannot omit response_format for JsonSchema and ensure the
error surfaces before sending the request (referencing BuildResponseFormat and
BuildRequest to locate changes).
- Around line 73-108: In GetResponseCoreAsync, stop fabricating an empty
assistant message when the OpenAI 200 payload is missing required fields:
validate that root["choices"] exists, that choices.FirstOrDefault() and its
"message" are non-null, and if they are missing throw a descriptive exception
instead of returning ChatResponse with TextContent(string.Empty) (affecting the
code that builds contents and the final return of new
ChatResponse(ChatMessage.Assistant(contents), ...)). In BuildResponseFormat,
ensure that when response format kind is JsonSchema you validate
options.ResponseJsonSchema is non-null and throw an error if it is missing
instead of returning null/omitting response_format; reference ResponseJsonSchema
and the enum ChatResponseFormatKind.JsonSchema so callers fail fast on
misconfiguration.
In `@src/Agentic/Models/ImageMediaTypeExtensions.cs`:
- Around line 34-55: Change the fallback assignment in TryParseMimeType so it
sets the out parameter to the enum default instead of a specific image type;
replace the current mediaType = ImageMediaType.Png (in the default branch of
TryParseMimeType) with mediaType = default(ImageMediaType) to avoid misleading
non-default values when the method returns false.
In `@src/Agentic/Models/ToolCallContent.cs`:
- Around line 30-36: The ToolCallContent constructor currently assigns
ArgumentsJson without verifying JSON; update the constructor in class
ToolCallContent (the ToolCallContent(string callId, string toolName, string?
argumentsJson = null) method) to validate non-empty argumentsJson by attempting
to parse it (e.g., with JObject.Parse or JsonDocument) and on parse failure
throw an ArgumentException (including the original JsonException as inner
exception), while preserving the existing behavior of defaulting null/whitespace
to "{}".
In `@src/Agentic/Tools/FunctionAgentTool.cs`:
- Around line 47-51: Wrap the invocation inside
FunctionAgentTool.InvokeCoreAsync in a try-catch: call _invoker(arguments,
cancellationToken) as before but catch Exception, and on error return
ToolInvocationResult.Error (including the exception message/stack or an
appropriate diagnostic string) instead of letting the exception propagate; keep
ConfigureAwait(false) and preserve cancellationToken handling so the behavior
matches DelegateAgentTool's exception-to-ToolInvocationResult.Error pattern.
In `@src/Agentic/Tools/StructuredOutputExtensions.cs`:
- Around line 30-34: The deserialization is too lenient: update
JsonSchemaGenerator.ForType to populate a "required" array for
non-nullable/required CLR members (e.g., for properties without nullable
annotation or marked required) and then change
JsonSettings.MissingMemberHandling from MissingMemberHandling.Ignore to
MissingMemberHandling.Error so
StructuredOutputExtensions.GetStructuredResponseAsync will fail when JSON is
missing required fields; also update GetStructuredResponseAsync to catch and
surface those deserialization errors (or rethrow with context) so missing fields
are reported clearly.
In `@src/AiDotNet.Generators/AgentToolSchemaGenerator.cs`:
- Around line 164-169: The generated binder in AgentToolSchemaGenerator.cs
currently uses default({pType}) when an argument is missing, losing declared
parameter defaults; implement a helper (e.g.,
GetParameterDefaultLiteral(IParameterSymbol p)) that returns the C# literal for
p.ExplicitDefaultValue when p.HasExplicitDefaultValue (handle null,
strings/chars with escaping and quoting, numeric/bool with invariant formatting,
enums by emitting named constant or casted value), and change the invoker
generation (the lines that build local/__p_{p.Name} and token/__t_{p.Name}) to
use that helper’s result instead of default({pType}); ensure the fallback
remains $"default({pType})" if no explicit default exists.
- Around line 271-283: IsRequired currently only treats System_Nullable_T as
nullable; update IsRequired to also consider nullable-reference annotations by
checking p.NullableAnnotation == NullableAnnotation.Annotated (in addition to
the existing INamedTypeSymbol nullable value-type check) so parameters declared
like string? or MyDto? are not marked required. In EmitTool's binder generation,
replace the unconditional fallback ": default({pType});" with logic that emits
the method-declared optional default when present: if
(p.HasExplicitDefaultValue) emit the literal for p.ExplicitDefaultValue
(preserving null/primitive/string values and any symbol-to-literal formatting
used elsewhere) else emit default({pType}); so runtime invocation honors
declared optional defaults.
In `@src/Reasoning/ReasoningStrategyBase.cs`:
- Around line 309-323: The current ExecuteToolAsync forces every tool to receive
a single {"input": string} property and lets InvokeAsync exceptions bubble up;
change ExecuteToolAsync (and use FindTool, _tools, InvokeAsync, AppendTrace) to
preserve schema-shaped arguments by attempting to parse the provided input
string as JSON (JToken.Parse) and, if it yields a JObject/JArray, pass that
token directly to InvokeAsync; otherwise fall back to wrapping the raw string
under ["input"]. Also wrap the call to tool.InvokeAsync in a try/catch, on
exception AppendTrace a clear "Tool '{toolName}' exception: {ex.Message}" and
return an error string instead of letting the exception escape so one bad tool
call doesn't fail the whole reasoning run.
---
Outside diff comments:
In `@src/Interfaces/IAiModelBuilder.cs`:
- Around line 1467-1479: The XML doc example for IAiModelBuilder is stale: it
shows chaining ConfigureAgentAssistance(...) before ConfigureReasoning(), but
ConfigureAgentAssistance no longer exists; update the example under
ConfigureReasoning to use the current initialization/configuration flow for
IAiModelBuilder (e.g., remove the ConfigureAgentAssistance call or replace it
with the new supported method used to enable agent-related settings), ensuring
the snippet compiles against AiModelBuilder<TModel,...> and references only
existing public APIs (e.g., ConfigureReasoning, the current agent/config setup
method) so the example is valid.
In `@src/Models/Results/AiModelResult.cs`:
- Around line 1441-1456: Update the public XML documentation for
EvaluateBenchmarksAsync to describe the new IChatClient<T> parameter (its
purpose, when to provide null, and how it affects evaluation), update method
summary/params/returns to reference BenchmarkingOptions, IChatClient<T>, and
BenchmarkReport, refresh any sample XML example calls to include a chatClient
argument or show how to call with null/default, and add a brief migration note
indicating this is a breaking change so downstream callers must supply an
IChatClient<T> or adjust their call sites; apply the same documentation updates
to the other affected overloads/locations referenced in the review (the ranges
noted) to keep the public API docs consistent.
In `@src/Reasoning/Components/ThoughtEvaluator.cs`:
- Around line 66-67: The GenerateResponseAsync calls dropped CancellationToken
propagation after the IChatClient<T> migration; update the calls in
ThoughtEvaluator (and the other reasoning component that calls
IChatClient<T>.GenerateResponseAsync) to pass the incoming CancellationToken
through to GenerateResponseAsync so cancellations abort the remote LLM call
(e.g., change string response = await _chatModel.GenerateResponseAsync(prompt);
to call the overload that accepts the token), and ensure you use the same token
parameter name (CancellationToken token) from the method signature so
cancellation flows end-to-end.
- Around line 120-124: The code reads root["score"] and calls
scoreToken.Value<double>() directly; validate the token before conversion to
avoid exceptions on non-numeric model output. In the block where scoreToken is
obtained (root["score"]), check scoreToken.Type (or use scoreToken.ToString())
and attempt a safe parse with double.TryParse(..., CultureInfo.InvariantCulture)
or only accept JTokenType.Float/Integer; if parsing fails, do not call
Value<double>() and instead fall back to the existing regex/default path; then
clamp the parsed double with MathHelper.Clamp as before. Ensure you reference
scoreToken, Value<double>(), MathHelper.Clamp and the fallback path in the
ThoughtEvaluator logic.
In `@src/Reasoning/Components/ThoughtGenerator.cs`:
- Around line 141-165: The current JSON parsing block in ThoughtGenerator (using
ExtractJsonFromResponse and JObject.Parse) only falls back to
ParseThoughtsFromLines on JsonException, which lets valid-but-unusable JSON
(e.g., missing "thoughts" key or "thoughts" not being a JArray) produce an empty
thoughts list silently; update the logic so that after parsing the JObject you
verify that root["thoughts"] is a non-empty JArray and contains string items,
and if not (null, wrong type, or yields no valid strings) call
ParseThoughtsFromLines(response, expectedCount) as the fallback; preserve
throwing/parsing errors, but ensure you do not return an empty list
silently—optionally emit a warning via the existing logging mechanism when
falling back.
In `@src/Reasoning/DomainSpecific/CodeReasoner.cs`:
- Around line 74-81: The public constructor for CodeReasoner now takes
IChatClient<T> and IEnumerable<IAgentTool> causing a source/binary break; update
the PR to include clear migration guidance and release-note text explaining the
API change and how consumers should migrate (e.g., replace legacy chat/agent
abstractions with IChatClient<T> and IAgentTool, update construction of
ChainOfThoughtStrategy<T>, TreeOfThoughtsStrategy<T>, CriticModel<T>, and
SelfRefinementEngine<T> usages), and add a short example snippet or checklist
for downstream maintainers to follow when upgrading their codebases.
In `@src/Reasoning/DomainSpecific/MathematicalReasoner.cs`:
- Around line 129-132: The code calls VerifyAndRefineAsync whenever
result.Success is true but doesn't ensure result.ReasoningChain is non-null,
which causes iteration over chain.Steps to throw; update the guard to require
both result.Success and result.ReasoningChain != null before calling
VerifyAndRefineAsync (and mirror the same null-check where similar verification
is triggered around the 146-156 block), or alternatively add a defensive null
check at the start of VerifyAndRefineAsync to return/skip if the incoming
ReasoningChain is null so iteration over chain.Steps is never attempted on a
null object.
In `@src/Reasoning/Strategies/ChainOfThoughtStrategy.cs`:
- Around line 149-157: The code in ChainOfThoughtStrategy currently fakes
verification when config.EnableVerification is true by marking all chain.Steps
as verified; remove this fake path and instead fail fast or call the real
verifier: if config.EnableVerification is true and no concrete
ICriticModel/verifier is configured, throw a clear NotImplementedException (or a
specific InvalidOperationException) with context via AppendTrace rather than
setting step.IsVerified, otherwise invoke the real verifier API (the
ICriticModel implementation) to set each step's IsVerified and confidence;
ensure the branch references config.EnableVerification, ChainOfThoughtStrategy,
AppendTrace, and chain.Steps so reviewers can locate and wire the proper
verifier or enforce the fail-fast behavior.
- Around line 201-216: The JSON example in ChainOfThoughtStrategy.cs is
malformed because the literal "..." inside the reasoning_steps array makes the
sample invalid; update the prompt template used by the method (search for the
string starting with "Respond in JSON format:" in ChainOfThoughtStrategy or the
variable that contains this template, and references to _useJsonFormat) to
provide a valid JSON example—remove the "..." and either include a fixed small
valid example entry or annotate with a JSON-safe placeholder (e.g., an explicit
array with up to {config.MaxSteps} numbered step examples) and ensure the
MaxSteps placeholder is rendered correctly so the template produces valid JSON
when sent to the model.
In `@src/Reasoning/Training/PolicyGradientTrainer.cs`:
- Around line 142-163: TrainBatchAsync currently computes rewards and losses but
never updates model parameters; modify the trainer to perform actual policy
updates by deriving gradients from the computed loss and applying them to the
model via the _model field: (1) if IChatClient<T> already exposes parameter
update APIs, call the appropriate method (e.g., ApplyGradients/UpdateParameters)
with gradients scaled by _learningRate, including entropy regularization using
_entropyCoefficient and discounting with _discountFactor; (2) if IChatClient<T>
is inference-only, introduce/require a trainable interface (e.g.,
ITrainableChatClient<T> or methods like
GetParameters/SetParameters/ApplyGradients) and cast or accept that interface in
the constructor; (3) update the running baseline (_baseline) using returns and
_useBaseline to compute advantages before applying gradients; and (4) ensure
TrainBatchAsync updates model state synchronously/atomically and handles _numOps
for numeric ops when computing gradients and advantages so the trainer actually
mutates the model rather than only reporting metrics.
In `@src/Reasoning/Training/ReinforcementLearner.cs`:
- Around line 477-489: The SaveCheckpointAsync currently only persists training
counters/config/data collector but omits the injected model weights, so make
SaveCheckpointAsync serialize the model state too (e.g., call the injected
model's Save/Export method or implement a GetWeights/Serialize method on the
model interface) and write that bytes/blob into the same checkpoint (or
alongside with a clear filename/version); update the corresponding
LoadCheckpointAsync to restore those serialized weights into the injected model
(e.g., call model.Load/Import or SetWeights) before restoring
epoch/bestAccuracy/earlyStopping counters and TrainingDataCollector state, and
include versioning/validation so mismatched model formats throw a clear error
and respect the cancellationToken during I/O.
- Around line 247-285: Replace direct Console.WriteLine calls in the training
loop with the library's logging/event mechanism: remove Console.WriteLine calls
in the epoch loop (the lines around the Epoch header, validation accuracy, and
early-stopping message) and instead emit the existing progress event or call the
injected logger (use the same abstraction other parts of the class use).
Specifically, in the method containing TrainEpochAsync and EvaluateAsync,
replace prints for the epoch start, Validation Accuracy, and "Early stopping"
with event invocations or ILogger methods (use the same event name or logger
instance already present on the class), and ensure SaveCheckpointAsync and
metrics updates remain unchanged; run unit/integration tests to confirm no
Console.WriteLine remains in the indicated regions.
In `@src/Reasoning/Verification/CriticModel.cs`:
- Around line 182-188: The sample JSON in the prompt inside CriticModel is
malformed because the "feedback" value runs into the "strengths" key; update the
prompt construction in the CriticModel class (where the JSON example is
assembled) so the example is valid JSON — include the missing comma and ensure
arrays for "strengths", "weaknesses", and "suggestions" are properly bracketed
and quoted and that the entire object is well-formed; adjust the string literal
or formatter used in the method that builds the prompt to emit a syntactically
correct JSON example (e.g.,
{"score":0.85,"feedback":"...","strengths":[],"weaknesses":[],"suggestions":[]})
so downstream parsers receive valid JSON.
In `@src/Reasoning/Verification/OutcomeRewardModel.cs`:
- Around line 216-226: The CalculateUnsupervisedRewardAsync method currently
contains a TODO and commented-out verification logic; either remove the TODO and
the entire commented block and update the method summary/comments to state it
uses only three heuristics (completeness, confidence, coherence), or implement
the verification integration by reading ReasoningChain<T>.VerificationResults
(ensure the property exists), compute verificationScore as the average
Confidence for Passed results (default 0.0), multiply by the intended weight
(0.1) and add to reward, and remove the TODO/comment once implemented; reference
the CalculateUnsupervisedRewardAsync method and
ReasoningChain<T>.VerificationResults when making the change.
In `@src/Reasoning/Verification/SelfRefinementEngine.cs`:
- Line 75: RefineStepAsync is not propagating the received CancellationToken to
the LLM call; update the call to _chatModel.GenerateResponseAsync to pass the
CancellationToken parameter (the same token passed into RefineStepAsync) so the
LLM call can be canceled (match the pattern used in ProcessRewardModel.cs where
GenerateResponseAsync(..., cancellationToken) is used); ensure you use the
correct parameter name (cancellationToken) and overload/signature on
_chatModel.GenerateResponseAsync.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
…lidation hardening - ChatClientBase: don't mutate a caller-owned HttpClient.Timeout (only set it when we create the client); tighten IsRetryable to classify net471 failures via the inner WebException (timeouts/connect/DNS → retry; HTTP 408/429/5xx → retry; else no). - HttpResponseException: new HttpRequestException subclass preserving the status code on all TFMs; OpenAI/Anthropic connectors throw it on non-success so IsRetryable classifies uniformly (net471 HttpRequestException has no StatusCode). - MeaiChatClient: clamp long? token counts into int range instead of overflow-casting. - OpenAIChatClient: validate the 200 payload shape (choices/message/contents) and throw instead of fabricating an empty assistant message; require ResponseJsonSchema when ResponseFormat is JsonSchema (fail fast during request assembly). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Agentic/Models/Connectors/AnthropicChatClient.cs (1)
107-110: 🧹 Nitpick | 🔵 Trivial | 💤 Low valueConsider throwing on empty response content for consistency with OpenAI connector.
OpenAIChatClientthrowsInvalidOperationExceptionwhen the response contains neither text content nor tool calls (lines 113-117 in that file), treating it as a malformed response or API contract violation. Here, the Anthropic connector silently returns an assistant message with empty text, which could hide API contract changes or malformed responses from the caller.This inconsistency may surprise users who expect uniform error behavior across connectors.
♻️ Suggested change for stricter validation
if (contents.Count == 0) { - contents.Add(new TextContent(string.Empty)); + throw new InvalidOperationException( + $"Anthropic response from '{ModelId}' contained no content blocks."); }🤖 Prompt for 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. In `@src/Agentic/Models/Connectors/AnthropicChatClient.cs` around lines 107 - 110, The Anthropic connector currently silently substitutes an empty TextContent when the parsed response has no contents; change this to throw an InvalidOperationException to match OpenAIChatClient behavior. In AnthropicChatClient (the method that builds/parses the assistant response where the local variable contents is populated), remove the contents.Add(new TextContent(string.Empty)) fallback and instead throw new InvalidOperationException with a clear message indicating the response contained neither text content nor tool calls (mirroring OpenAIChatClient's validation). Ensure the exception message references the Anthropic response being malformed so callers can detect API contract violations consistently across connectors.
🤖 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.
Outside diff comments:
In `@src/Agentic/Models/Connectors/AnthropicChatClient.cs`:
- Around line 107-110: The Anthropic connector currently silently substitutes an
empty TextContent when the parsed response has no contents; change this to throw
an InvalidOperationException to match OpenAIChatClient behavior. In
AnthropicChatClient (the method that builds/parses the assistant response where
the local variable contents is populated), remove the contents.Add(new
TextContent(string.Empty)) fallback and instead throw new
InvalidOperationException with a clear message indicating the response contained
neither text content nor tool calls (mirroring OpenAIChatClient's validation).
Ensure the exception message references the Anthropic response being malformed
so callers can detect API contract violations consistently across connectors.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3edebcce-06c6-4b4c-9f19-784a50ee6568
📒 Files selected for processing (5)
src/Agentic/Models/Connectors/AnthropicChatClient.cssrc/Agentic/Models/Connectors/ChatClientBase.cssrc/Agentic/Models/Connectors/HttpResponseException.cssrc/Agentic/Models/Connectors/MeaiChatClient.cssrc/Agentic/Models/Connectors/OpenAIChatClient.cs
…r handling, schema required - ImageMediaTypeExtensions.TryParseMimeType: return default(ImageMediaType) on failure instead of Png, so a false return isn't mistaken for a parsed PNG. - FunctionAgentTool.InvokeCoreAsync: catch exceptions and return ToolInvocationResult.Error (re-throwing OperationCanceledException), matching DelegateAgentTool's graceful pattern. - JsonSchemaGenerator.ForType: emit a "required" array for non-nullable members (value types; non-nullable reference types via NullabilityInfoContext on net6+) so structured-output schema enforcement covers required fields. - ToolCallContent: documented (via XML remark) that arguments JSON is intentionally validated at invocation (graceful error to the model), not construction (would crash the agent loop). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g tool dispatch
- AgentToolSchemaGenerator: generated binder now reproduces a parameter's DECLARED default
(via GetParameterDefaultLiteral) instead of default(T) when an arg is omitted; IsRequired
now treats nullable-annotated reference types (string?, MyDto?) as optional.
- ReasoningStrategyBase.ExecuteToolAsync: pass through caller-supplied JSON-object arguments
(parse, fall back to {"input": raw}); wrap InvokeAsync in try/catch so a throwing tool
returns an error string instead of failing the whole reasoning run (matches its doc contract).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1544) Foundational single-model agent for the multi-agent layer: - IAgent<T>: uniform named/described/RunAsync contract that both leaf agents and coordinators (supervisor/swarm) implement, so agents nest and any agent can be surfaced as a tool to another. - AgentExecutor<T>: drives an IChatClient<T> in a provider-native function-calling loop (call model -> run requested tools -> feed ToolResult messages back -> repeat) until a final answer or the iteration cap. Replaces the legacy prompt-parsed ReAct loop. Aggregates token usage; returns the full transcript. - AgentExecutorOptions (nullable defaults: name/description/system-prompt/max-iterations/sampling/tool-choice) + AgentRunResult (FinalText/Messages/Iterations/Completed/Usage). - Tests (10, green net10.0 + net471) via a deterministic ScriptedChatClient + RecordingTool: plain answer, tool round-trip with arg + result-passthrough assertions, system-prompt prepend, tool advertisement on/off, iteration-cap -> Completed=false, usage aggregation, empty/null guards. No null-forgiving. Stacked on Phase 0 (#1545); part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1544) Foundational single-model agent for the multi-agent layer: - IAgent<T>: uniform named/described/RunAsync contract that both leaf agents and coordinators (supervisor/swarm) implement, so agents nest and any agent can be surfaced as a tool to another. - AgentExecutor<T>: drives an IChatClient<T> in a provider-native function-calling loop (call model -> run requested tools -> feed ToolResult messages back -> repeat) until a final answer or the iteration cap. Replaces the legacy prompt-parsed ReAct loop. Aggregates token usage; returns the full transcript. - AgentExecutorOptions (nullable defaults: name/description/system-prompt/max-iterations/sampling/tool-choice) + AgentRunResult (FinalText/Messages/Iterations/Completed/Usage). - Tests (10, green net10.0 + net471) via a deterministic ScriptedChatClient + RecordingTool: plain answer, tool round-trip with arg + result-passthrough assertions, system-prompt prepend, tool advertisement on/off, iteration-cap -> Completed=false, usage aggregation, empty/null guards. No null-forgiving. Stacked on Phase 0 (#1545); part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1544) Foundational single-model agent for the multi-agent layer: - IAgent<T>: uniform named/described/RunAsync contract that both leaf agents and coordinators (supervisor/swarm) implement, so agents nest and any agent can be surfaced as a tool to another. - AgentExecutor<T>: drives an IChatClient<T> in a provider-native function-calling loop (call model -> run requested tools -> feed ToolResult messages back -> repeat) until a final answer or the iteration cap. Replaces the legacy prompt-parsed ReAct loop. Aggregates token usage; returns the full transcript. - AgentExecutorOptions (nullable defaults: name/description/system-prompt/max-iterations/sampling/tool-choice) + AgentRunResult (FinalText/Messages/Iterations/Completed/Usage). - Tests (10, green net10.0 + net471) via a deterministic ScriptedChatClient + RecordingTool: plain answer, tool round-trip with arg + result-passthrough assertions, system-prompt prepend, tool advertisement on/off, iteration-cap -> Completed=false, usage aggregation, empty/null guards. No null-forgiving. Stacked on Phase 0 (#1545); part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1544) Foundational single-model agent for the multi-agent layer: - IAgent<T>: uniform named/described/RunAsync contract that both leaf agents and coordinators (supervisor/swarm) implement, so agents nest and any agent can be surfaced as a tool to another. - AgentExecutor<T>: drives an IChatClient<T> in a provider-native function-calling loop (call model -> run requested tools -> feed ToolResult messages back -> repeat) until a final answer or the iteration cap. Replaces the legacy prompt-parsed ReAct loop. Aggregates token usage; returns the full transcript. - AgentExecutorOptions (nullable defaults: name/description/system-prompt/max-iterations/sampling/tool-choice) + AgentRunResult (FinalText/Messages/Iterations/Completed/Usage). - Tests (10, green net10.0 + net471) via a deterministic ScriptedChatClient + RecordingTool: plain answer, tool round-trip with arg + result-passthrough assertions, system-prompt prepend, tool advertisement on/off, iteration-cap -> Completed=false, usage aggregation, empty/null guards. No null-forgiving. Stacked on Phase 0 (#1545); part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1544) Foundational single-model agent for the multi-agent layer: - IAgent<T>: uniform named/described/RunAsync contract that both leaf agents and coordinators (supervisor/swarm) implement, so agents nest and any agent can be surfaced as a tool to another. - AgentExecutor<T>: drives an IChatClient<T> in a provider-native function-calling loop (call model -> run requested tools -> feed ToolResult messages back -> repeat) until a final answer or the iteration cap. Replaces the legacy prompt-parsed ReAct loop. Aggregates token usage; returns the full transcript. - AgentExecutorOptions (nullable defaults: name/description/system-prompt/max-iterations/sampling/tool-choice) + AgentRunResult (FinalText/Messages/Iterations/Completed/Usage). - Tests (10, green net10.0 + net471) via a deterministic ScriptedChatClient + RecordingTool: plain answer, tool round-trip with arg + result-passthrough assertions, system-prompt prepend, tool advertisement on/off, iteration-cap -> Completed=false, usage aggregation, empty/null guards. No null-forgiving. Stacked on Phase 0 (#1545); part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1544) Foundational single-model agent for the multi-agent layer: - IAgent<T>: uniform named/described/RunAsync contract that both leaf agents and coordinators (supervisor/swarm) implement, so agents nest and any agent can be surfaced as a tool to another. - AgentExecutor<T>: drives an IChatClient<T> in a provider-native function-calling loop (call model -> run requested tools -> feed ToolResult messages back -> repeat) until a final answer or the iteration cap. Replaces the legacy prompt-parsed ReAct loop. Aggregates token usage; returns the full transcript. - AgentExecutorOptions (nullable defaults: name/description/system-prompt/max-iterations/sampling/tool-choice) + AgentRunResult (FinalText/Messages/Iterations/Completed/Usage). - Tests (10, green net10.0 + net471) via a deterministic ScriptedChatClient + RecordingTool: plain answer, tool round-trip with arg + result-passthrough assertions, system-prompt prepend, tool advertisement on/off, iteration-cap -> Completed=false, usage aggregation, empty/null guards. No null-forgiving. Stacked on Phase 0 (#1545); part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1544) Foundational single-model agent for the multi-agent layer: - IAgent<T>: uniform named/described/RunAsync contract that both leaf agents and coordinators (supervisor/swarm) implement, so agents nest and any agent can be surfaced as a tool to another. - AgentExecutor<T>: drives an IChatClient<T> in a provider-native function-calling loop (call model -> run requested tools -> feed ToolResult messages back -> repeat) until a final answer or the iteration cap. Replaces the legacy prompt-parsed ReAct loop. Aggregates token usage; returns the full transcript. - AgentExecutorOptions (nullable defaults: name/description/system-prompt/max-iterations/sampling/tool-choice) + AgentRunResult (FinalText/Messages/Iterations/Completed/Usage). - Tests (10, green net10.0 + net471) via a deterministic ScriptedChatClient + RecordingTool: plain answer, tool round-trip with arg + result-passthrough assertions, system-prompt prepend, tool advertisement on/off, iteration-cap -> Completed=false, usage aggregation, empty/null guards. No null-forgiving. Stacked on Phase 0 (#1545); part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…1544) Foundational single-model agent for the multi-agent layer: - IAgent<T>: uniform named/described/RunAsync contract that both leaf agents and coordinators (supervisor/swarm) implement, so agents nest and any agent can be surfaced as a tool to another. - AgentExecutor<T>: drives an IChatClient<T> in a provider-native function-calling loop (call model -> run requested tools -> feed ToolResult messages back -> repeat) until a final answer or the iteration cap. Replaces the legacy prompt-parsed ReAct loop. Aggregates token usage; returns the full transcript. - AgentExecutorOptions (nullable defaults: name/description/system-prompt/max-iterations/sampling/tool-choice) + AgentRunResult (FinalText/Messages/Iterations/Completed/Usage). - Tests (10, green net10.0 + net471) via a deterministic ScriptedChatClient + RecordingTool: plain answer, tool round-trip with arg + result-passthrough assertions, system-prompt prepend, tool advertisement on/off, iteration-cap -> Completed=false, usage aggregation, empty/null guards. No null-forgiving. Stacked on Phase 0 (#1545); part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(agentic): Phase 2 — IAgent + native tool-calling AgentExecutor (#1544) Foundational single-model agent for the multi-agent layer: - IAgent<T>: uniform named/described/RunAsync contract that both leaf agents and coordinators (supervisor/swarm) implement, so agents nest and any agent can be surfaced as a tool to another. - AgentExecutor<T>: drives an IChatClient<T> in a provider-native function-calling loop (call model -> run requested tools -> feed ToolResult messages back -> repeat) until a final answer or the iteration cap. Replaces the legacy prompt-parsed ReAct loop. Aggregates token usage; returns the full transcript. - AgentExecutorOptions (nullable defaults: name/description/system-prompt/max-iterations/sampling/tool-choice) + AgentRunResult (FinalText/Messages/Iterations/Completed/Usage). - Tests (10, green net10.0 + net471) via a deterministic ScriptedChatClient + RecordingTool: plain answer, tool round-trip with arg + result-passthrough assertions, system-prompt prepend, tool advertisement on/off, iteration-cap -> Completed=false, usage aggregation, empty/null guards. No null-forgiving. Stacked on Phase 0 (#1545); part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 2 — supervisor multi-agent via agent-as-tool handoffs (#1544) - AgentAsTool<T>: adapts any IAgent<T> into an IAgentTool (transfer_to_<worker>, single required 'task' arg). Worker name is sanitized to a valid tool name; the worker's final answer becomes the tool result. Surfaces partial output as an error when a worker hits its step budget. - SupervisorAgent<T> (itself an IAgent<T>, so supervisors nest into hierarchical teams): wraps each worker as a handoff tool and delegates routing to AgentExecutor's native tool-calling loop — no bespoke control flow. Auto-generates a routing system prompt listing workers + specialties when none is supplied. - SupervisorOptions (nullable defaults: name/description/system-prompt/max-iterations/temperature). - 6 tests (green net10.0 + net471): single-worker route + result-passthrough, correct-worker-among-many, one-handoff-tool-per-worker advertisement, default-prompt lists workers, nested supervisors compose, empty-team guard. No null-forgiving. Part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 2 — swarm (peer-to-peer control handoff) (#1544) - Swarm<T> (an IAgent<T>): peer-to-peer team with no central coordinator. The active member answers directly or transfers the whole shared conversation to a peer via a native transfer_to_<peer> tool, which the swarm intercepts at the loop level (switch active member + re-run the turn with that member's instructions/tools) rather than executing. Members' own tools run normally. - SwarmMember<T>: name + client + per-member system prompt + own tools + allowed handoff peers (null = any peer, empty = terminal). SwarmOptions: name/description/shared MaxIterations budget (guarantees termination on ping-pong) /temperature. - Each turn prepends the *active* member's system prompt to the shared history; handoff tools are advertised only for allowed peers. - AgentRunResult gains AgentName (which agent produced the final answer); AgentExecutor now reports its own name. - ToolNaming helper centralizes handoff tool-name sanitization (shared with AgentAsTool). - 8 tests (green net10.0 + net471): direct answer, handoff-over-shared-history (+active system prompt), own-tool execution, allowed-peers-only advertisement, ping-pong terminates at cap, unknown-entry/unknown-peer/duplicate-name guards. No null-forgiving. Part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 2 — conversation threads (short-term memory) (#1544) - IConversationStore: per-thread dialogue persistence (Append/Get/ListThreads/Clear). - InMemoryConversationStore (zero-config, process-local) + JsonFileConversationStore (durable across restarts; persists role+text dialogue via Newtonsoft, in-process lock, no native dependency so it runs on both TFMs). - ThreadedAgent<T>: wraps any IAgent<T> with conversation memory — loads a thread's history, prepends it to new input, runs the inner agent, then persists the new user+assistant turn. Turns a stateless agent (executor/supervisor/swarm) into a stateful chat. Persists clean user/assistant dialogue; intermediate tool/system turns stay on the per-turn AgentRunResult. - 8 tests (green net10.0 + net471): cross-turn memory within a thread, thread isolation, empty-id guard, in-memory round-trip + unknown-thread-empty, JSON-file persists-across-instances + threaded-agent-survives-new-instance + clear. No null-forgiving. Part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 2 — long-term semantic memory + SQLite conversation store (#1544) Long-term cross-thread memory (the durable counterpart to per-thread IConversationStore): - IAgentMemoryStore (AddAsync/SearchAsync/GetAllAsync/RemoveAsync) + AgentMemory/ScoredMemory. - InMemoryAgentMemoryStore: lexical-overlap ranking (zero-config, no model). - EmbeddingAgentMemoryStore<T>: TRUE semantic recall — embeds memories+query via IEmbeddingModel<T> and ranks by the RAG stack's ISimilarityMetric<T> (cosine by default), reusing existing RAG infrastructure. Drop-in over the lexical default (same interface). - MemoryAugmentedAgent<T> (IAgent<T>): retrieves memories relevant to the latest user message and injects them as system context before delegating to the inner agent (executor/supervisor/swarm). Read-only by design — what gets remembered stays under app control. Composes with ThreadedAgent (long-term recall + short-term thread memory). Durable conversation backend: - SqliteConversationStore (AiDotNet.Storage.Sqlite): DB-backed IConversationStore (auto-incrementing Seq preserves order, schema created on demand), keeping the native SQLite dep out of core. Mirrors the SQLite graph checkpointer. - 12 tests (9 memory green net10.0+net471; 3 SQLite conversation net10-gated for the e_sqlite3 native-lib reason — pre-existing repo-wide). No null-forgiving. Part of epic #1544 (Phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(pr1551): cancellation honour + readability + persistence safety (1-11) CodeRabbit PR #1551 — first 11 unresolved threads: - **SupervisorAgent.cs** (1): swap the `is { } x && x.Trim().Length > 0` style for `string.IsNullOrWhiteSpace` while still using a pattern-match to narrow nullability (net471's IsNullOrWhiteSpace lacks NotNullWhen attribution). - **EmbeddingAgentMemoryStore** (2-5): honour CancellationToken at the entry to every async method (IEmbeddingModel<T>.EmbedAsync has no token overload, so we ThrowIfCancellationRequested BEFORE the embed call; GetAllAsync / RemoveAsync get the same check up-front). - **InMemoryAgentMemoryStore** (6): same ThrowIfCancellationRequested pattern on AddAsync / SearchAsync / GetAllAsync / RemoveAsync. - **InMemoryConversationStore** (7): same on AppendAsync / GetAsync / ListThreadsAsync / ClearAsync. - **JsonFileConversationStore** (8-10): - canonicalise filePath via Path.GetFullPath in the constructor (defends against relative-path surprises without restricting to a base directory); - wrap Load()'s JsonConvert.DeserializeObject in try/catch so a corrupted file surfaces as InvalidOperationException with the file path instead of a raw JsonException; - Save() now writes-then-renames via File.Move(overwrite=true) (or File.Replace on older targets) so a crash mid-write can't corrupt the backing file. - **ChatMessage.cs** (11): reject empty content lists at the constructor with ArgumentException — the type contract says "one or more content parts". * fix(pr1551): validation + safety + perf cleanup (12-31) - **AnthropicChatClient** (12, 13): validate the optional custom endpoint as an absolute URI in the ctor; ParseArguments now throws ArgumentException on malformed tool-call JSON instead of silently substituting {}. - **MeaiChatClient.ToMeaiMessages** (14): null-check each message element so a null entry fails fast at the boundary, not deep inside .Contents. - **ImageContent** (15, 16): reject empty byte[] in FromBytes; require an absolute URI in FromUri (fully-qualified System.Uri to avoid clash with the class's own Uri member). - **SqliteConversationStore** (17, 18, 19): switch transaction.Commit() to CommitAsync; cache per-connection-string schema-init flag so DDL only runs once; ParseRole uses TryParse with a descriptive InvalidOperationException naming the bad value. - **AiModelResult.EvaluateBenchmarksAsync** (20, 21): add a forwarding overload that preserves the old (options, cancellationToken) call shape; XML-doc the chatClient null contract for reasoning suites. - **CodeReasoner** (22): document the IChatModel->IChatClient + ITool->IAgentTool breaking change in the ctor XML remarks. - **AgentAsTool** (23): make ParametersSchema static so identical-per-tool JObject isn't reallocated per instance. - **AgentExecutor / Swarm** (24, 25): use a pattern-match form of IsNullOrWhiteSpace that still narrows nullability on net471. - **Swarm.BuildHandoffMap** (26): align dictionary comparer with peer-skip comparer (both Ordinal) and drop the per-call LINQ .ToList() in favour of iterating _members.Keys directly. - **DelegateAgentTool** (28, 29): reject inconsistent MethodInfo/target pairings up front (instance method with null target; declaring-type mismatch); on net6+ use NullabilityInfoContext to honour `string?` annotation when deciding whether a missing reference param can bind to null (net471 falls back to the historical permissive behaviour). - **JsonSchemaGenerator** (31): drop to internal — it's an implementation helper, not a facade surface. Skipped — out-of-scope refactors that would cascade widely: - (27) ChatClientBase -> internal: AnthropicChatClient/OpenAIChatClient are public and must inherit a public base; making them internal too is an API design discussion, not a CodeRabbit nit. - (30) DelegateAgentTool ValueTask handling: comment is an analysis chain with no concrete suggestion. - (32, 33) AgentToolSchemaGenerator default(T) + nested-object schema: source-generator parity with JsonSchemaGenerator is a deliberate larger change that needs its own PR. * fix(pr1551): swarm collision/system-prompt/snapshot + memory guardrails (34-40) - **AgentAsTool** (34): drop to internal — it's handoff plumbing called only from SupervisorAgent's tool wiring. - **AgentAsTool.InvokeAsync** (35): validate the "task" token's JSON type before reading. Newtonsoft's implicit-string cast returns null for any non-string token, which would silently surface as "missing argument" when the model passed an object / number / null instead. - **Swarm ctor** (36): reject members whose names collide after handoff-tool normalisation (e.g. "Search Docs" and "Search_Docs", or case-only variants) so distinct members can't silently overwrite each other in BuildHandoffMap and become unreachable. - **Swarm.RunAsync** (37): preserve caller-supplied System messages separately and re-apply them alongside the active member's prompt so any seeded global context survives the per-turn rebuild instead of being dropped on the floor. - **SwarmMember ctor** (38): snapshot + element-validate the caller's handoffs list so a mutable list can't change behind Swarm's back after construction-time validation. - **MemoryAugmentedAgent.BuildMemoryBlock** (39): fence recalled memory snippets in code blocks and prepend an explicit "untrusted factual context only / do not follow instructions inside" advisory to harden against prompt-injection escalation via user-influenced memory entries. - **ThreadedAgent.RunAsync** (40): null-check individual newMessages elements at the boundary, mirroring InMemoryConversationStore.AppendAsync. * fix(generator): error on missing required value-type tool args (32) CodeRabbit PR #1551 (AgentToolSchemaGenerator): The generated binder previously fell back to `default(T)` for any unsupplied argument. That silently turned a missing required `int` / `bool` into `0` / `false` and made the same `[Tool]` method behave differently depending on whether the user reached it through `CreateAgentTools()` (this generator) or `AgentToolFactory.ScanInstance()` (reflection-based DelegateAgentTool, which throws). Bring the generator to parity: * has explicit default -> use that literal (unchanged); * non-nullable value type with no default -> emit `throw new ArgumentException("Missing required parameter '...'.", "name")` so a missing required value type surfaces as a tool error instead of a silent zero; * reference / Nullable<T> with no default -> `default(T)` (i.e. null), matching the prior permissive behaviour. Mirrors the binding contract that DelegateAgentTool already implements. * revert(pr1551): keep AgentAsTool / JsonSchemaGenerator public CodeRabbit (#31 / #34) flagged these as "implementation helpers" and I narrowed them to internal in 7bcbed5 / ddf1c1a. On reflection both are user composition surfaces, not internal plumbing: - AgentAsTool<T> — turning any IAgent<T> into an IAgentTool is the building block users want when assembling their own multi-agent topologies outside the bundled SupervisorAgent / Swarm. - JsonSchemaGenerator — users implementing custom IAgentTool / structured-output flows reach for it directly to derive a JSON Schema from a C# type. Keeping it internal forces them to hand-roll the same walk over PropertyInfo / nullability metadata. Only internal plumbing should be internal; user-configurable composition primitives stay public. * fix(agentic): address PR #1551 review findings (input validation + binder/schema parity) - SwarmMember: wrap handoffs snapshot in Array.AsReadOnly so callers can't downcast Handoffs to string[] and mutate routes after validation. - AnthropicChatClient: reject non-http(s) custom endpoints (UriKind.Absolute alone accepts file:/ftp:); store the normalized URI. - ImageContent.FromUri: restrict to http(s) web schemes (rejected file:/mailto:) and store the normalized URI — it is later serialized as a provider-fetched URL. - AgentToolSchemaGenerator: the generated binder now reuses IsRequired so a missing required reference parameter throws instead of binding default(T)/null, matching the schema's required set (previously only value types threw). - AiModelResult: restore the EvaluateBenchmarksAsync(chatClient, ct) facade overload (forwards with options: null) for back-compat. * fix(agentic): nested-object tool schemas + net471 sqlite commit build error - AgentToolSchemaGenerator.TypeSchemaJson now recurses into complex object types (public readable properties -> properties + required), cycle/depth-guarded (MaxSchemaDepth=6), mirroring the runtime JsonSchemaGenerator.ForType so the compile-time CreateAgentTools() path emits the same nested-object detail as AgentToolFactory.ScanInstance() instead of a bare {"type":"object"} that stripped DTO structure from the model's view. Required-property semantics match ForType (Nullable<T> optional; value type required; reference type required only when explicitly non-nullable). - SqliteConversationStore: DbTransaction.CommitAsync is netstandard2.1/.NET Core 3.0+, absent on net471 -> CS1061 broke the solution build. Commit synchronously under NETFRAMEWORK, async elsewhere. Solution builds 0 errors on net10.0 + net471; 32 agentic schema/tool tests green. --------- Co-authored-by: franklinic <franklin@ivorycloud.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#1545) * feat(agentic): Phase 0 — message-based IChatClient<T> model abstraction (#1544) Introduces the foundational chat-model abstraction for the new agentic orchestration subsystem (AiDotNet.Agentic.Models), superseding the text-in/text-out IChatModel<T>/ILanguageModel<T>: - IChatClient<T>: message-based, native tool-calling, streaming (IAsyncEnumerable), structured output - DTOs: ChatRole/ChatFinishReason/ToolChoiceMode/ChatResponseFormatKind enums; AiContent hierarchy (Text/Image/ToolCall/ToolResult); ChatMessage (+factories); AiToolDefinition (JObject JSON-schema); ChatOptions; ChatUsage; ChatResponse; ChatResponseUpdate; StreamingToolCallUpdate - 15 unit tests, green on net10.0 and net471 (incl. streaming round-trip) Part of epic #1544 (Phase 0: foundations). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 0 — tool/function layer with auto JSON-schema (#1544) Executable tool abstraction for native function calling: - IAgentTool / AgentToolBase / ToolInvocationResult; [AgentTool]/[ToolParameter] attributes - JsonSchemaGenerator: reflection-based C# type -> JSON Schema (primitives, enums, arrays, string-keyed dicts, nested objects w/ depth+cycle guard); required inferred from defaults/nullability - DelegateAgentTool: binds model JSON args to typed params (CancellationToken injected, hidden from schema), supports sync/Task/Task<T>, serializes results, surfaces failures as error results - AgentToolFactory (delegate + [AgentTool] scanning) and ToolCollection (registry -> AiToolDefinition list, dispatch tool-calls -> ChatRole.Tool messages) - 14 unit tests green on net10.0 and net471 Part of epic #1544 (Phase 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(agentic): replace ImageContent MIME string with ImageMediaType enum (#1544) Type-safety over fragile strings: image format is now a closed enum (Png/Jpeg/Gif/Webp) with ToMimeType()/TryParseMimeType() wire conversions, instead of a raw 'image/png' string that could be misspelled. Tests updated; green on net10.0 + net471. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 0 — IChatClient connector base + OpenAI connector (#1544) First new-stack connectors (replacing legacy LanguageModels): - ChatClientExtensions.GenerateTextAsync (prompt-in/text-out bridge used by reasoning + simple callers) - ChatClientBase<T>: shared HTTP transport, retry-with-backoff (non-streaming), API-key validation, message validation - OpenAIChatClient<T>: Chat Completions mapping with native tool calling, tool_choice, structured output (json/json_schema), multimodal image input, SSE streaming; provider strings (roles, finish_reason) mapped to enums - Mock-HTTP tests for request shaping, tool-call/finish-reason/usage parsing, and SSE reconstruction; green on net10.0 + net471 Part of epic #1544 (Phase 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 0 — Anthropic + Azure OpenAI connectors (#1544) Completes the new-stack connector trio: - AzureOpenAIChatClient<T>: derives from OpenAIChatClient (shared Chat Completions wire), overrides endpoint (per-deployment URL + api-version) and auth (api-key header). OpenAI client refactored to expose protected ApiKey/Endpoint + virtual ApplyAuthentication. - AnthropicChatClient<T>: Messages API with top-level system, content-block messages (text/image/tool_use/tool_result), required max_tokens, tool_choice mapping, stop_reason->enum, and event-based SSE streaming (message_start/content_block_delta/message_delta). - Mock-HTTP tests for both; 7 connector tests green on net10.0 + net471. Part of epic #1544 (Phase 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(reasoning): rewire Reasoning + facade onto IChatClient<T> (#1544) Migrates the entire Reasoning module (17 files: strategies, components, verification, training, domain reasoners, base) and AiModelResult reasoning methods off the legacy IChatModel<T>/ITool onto the new Agentic IChatClient<T>/IAgentTool. Adds ChatClientExtensions.GenerateResponseAsync alias so call sites are unchanged. Facade CreateChatModelFromAgentConfig now builds the new OpenAI/Anthropic/Azure connectors. ReasoningStrategyBase.ExecuteTool -> async ExecuteToolAsync via IAgentTool.InvokeAsync. Core builds clean both TFMs. WIP: legacy deletion + agent-assistance removal + test cleanup follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(facade): AiModelResult reasoning takes injected IChatClient; drop AgentConfig/AgentRecommendation (#1544) [WIP] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(agentic): delete legacy Agents/Tools/Chains/LanguageModels + interfaces + agent-config types (#1544) [WIP] Removes src/{Agents,Tools,LanguageModels,PromptEngineering/Chains,PromptEngineering/Tools}, 6 legacy interfaces (IChatModel/ILanguageModel/IAgent/IChain/ITool/IFunctionTool), and Agent{Configuration,Recommendation,GlobalConfigurationBuilder,AssistanceOptions,AssistanceOptionsBuilder} + the stale global using. Facade fixes (agent-assistance + PromptChain feature removal) follow in subsequent commits; build is intentionally red until then. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(agentic): drop dead global usings (Agents/LanguageModels/Tools) (#1544) [WIP] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(facade): excise agent-assistance + PromptChain features; src green (#1544) Removes the legacy 'LLM picks model/hyperparameters' pipeline (Internals.cs Get/ApplyAgentRecommendations, CreateChatModel, _agentConfig/_agentOptions, ConfigureAgentAssistance/AskAgentAsync) and the PromptChain feature (built on deleted IChain<,>): AiModelResult PromptChain prop/RunChain/RunChainAsync/HasPromptChain, options fields, AttachPromptEngineering promptChain param, IConfiguredView/IAiModelBuilder members. Retypes ConfigureTool->IAgentTool and ConfigureRLAgent->IRLAgent<T>. EvaluateBenchmark[s]Async now takes an injected IChatClient<T>. Deletes IPromptChain. Core builds clean on net10.0 + net471 with zero legacy remaining. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(agentic): remove legacy feature tests; rewire reasoning mock to IChatClient (#1544) Deletes tests for purged features (Agents/Tools/LanguageModels/ToolRegistry/PromptEngineering-chains + agent-assistance Bucket11 cases + MergedPRBugFix LanguageModels region). Rewires ReasoningExtended MockChatModel to IChatClient<T>. Test project builds clean on net10.0 + net471; Agentic + Reasoning suites green except a pre-existing MCTS visit-count failure unrelated to this change (MonteCarloTreeSearch untouched). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(reasoning): implement MCTS backpropagation; remove orphaned legacy test dirs (#1544) MonteCarloTreeSearch.SearchAsync had selection/expansion/simulation but step 4 (backpropagation) was an empty comment — visit counts/total_value were never updated, so root.Metadata[visits] stayed 0 and UCB1 was degenerate. Select now returns the root->leaf path and backprop accumulates visits+value along it. Fixes pre-existing MCTS_Backpropagation_VisitCountsIncrease (now 118/118 green). Also deletes orphaned, uncompiled legacy test dirs tests/UnitTests + tests/Reasoning (not in any csproj). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): typed structured-output helper (schema-constrained -> deserialized) (#1544) StructuredOutputExtensions.GetStructuredResponseAsync<T,TResult>: derives a JSON schema from TResult via JsonSchemaGenerator, sets ResponseFormat=JsonSchema, and deserializes the reply into TResult. 2 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Microsoft.Extensions.AI adapter; remove all null-forgiving operators (#1544) - MeaiChatClient<T> adapts a Microsoft.Extensions.AI IChatClient to AiDotNet's IChatClient<T> (text+sampling+streaming; tools throw NotSupported for now). Adds Microsoft.Extensions.AI.Abstractions 10.6.0 to core (CPM). 3 adapter tests. - Removed every null-forgiving operator (!) introduced in the agentic work (AgentToolFactory, ToolCallContent, MeaiChatClient + 4 test files) per project policy; replaced with is-null narrowing / null-conditional navigation. Verified zero remain via scan. All Agentic suites green on net10.0 + net471 (41 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): source-generated tool schemas (compile-time, reflection-free) (#1544) AgentToolSchemaGenerator (Roslyn incremental) emits a CreateAgentTools() extension per [AgentTool]-bearing type, producing FunctionAgentTool instances with compile-time JSON schemas (JObject.Parse of a generation-time-computed string) and typed argument binders (sync/async split to avoid CS1998; #nullable disable so generated code needs no null-forgiving). Accessibility matches the owner type. Supports primitives/enums/arrays/IEnumerable/string-keyed dicts (+object fallback). Completes Phase 0. Test green on net10.0 + net471. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(agentic): address PR #1545 review — HTTP client/retry/response-validation hardening - ChatClientBase: don't mutate a caller-owned HttpClient.Timeout (only set it when we create the client); tighten IsRetryable to classify net471 failures via the inner WebException (timeouts/connect/DNS → retry; HTTP 408/429/5xx → retry; else no). - HttpResponseException: new HttpRequestException subclass preserving the status code on all TFMs; OpenAI/Anthropic connectors throw it on non-success so IsRetryable classifies uniformly (net471 HttpRequestException has no StatusCode). - MeaiChatClient: clamp long? token counts into int range instead of overflow-casting. - OpenAIChatClient: validate the 200 payload shape (choices/message/contents) and throw instead of fabricating an empty assistant message; require ResponseJsonSchema when ResponseFormat is JsonSchema (fail fast during request assembly). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(agentic): address PR #1545 review — media-type default, tool error handling, schema required - ImageMediaTypeExtensions.TryParseMimeType: return default(ImageMediaType) on failure instead of Png, so a false return isn't mistaken for a parsed PNG. - FunctionAgentTool.InvokeCoreAsync: catch exceptions and return ToolInvocationResult.Error (re-throwing OperationCanceledException), matching DelegateAgentTool's graceful pattern. - JsonSchemaGenerator.ForType: emit a "required" array for non-nullable members (value types; non-nullable reference types via NullabilityInfoContext on net6+) so structured-output schema enforcement covers required fields. - ToolCallContent: documented (via XML remark) that arguments JSON is intentionally validated at invocation (graceful error to the model), not construction (would crash the agent loop). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(agentic): address PR #1545 review — generator defaults + reasoning tool dispatch - AgentToolSchemaGenerator: generated binder now reproduces a parameter's DECLARED default (via GetParameterDefaultLiteral) instead of default(T) when an arg is omitted; IsRequired now treats nullable-annotated reference types (string?, MyDto?) as optional. - ReasoningStrategyBase.ExecuteToolAsync: pass through caller-supplied JSON-object arguments (parse, fall back to {"input": raw}); wrap InvokeAsync in try/catch so a throwing tool returns an error string instead of failing the whole reasoning run (matches its doc contract). 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 0 (foundations) — slice 1 of the agentic-orchestration epic
Part of #1544 (Epic: Agentic Orchestration — surpass Semantic Kernel & LangGraph).
This PR lands the model-abstraction layer — the message-based chat-model contract everything else in Phase 0 builds on. It supersedes the legacy text-in/text-out
IChatModel<T>/ILanguageModel<T>(which cannot express native tool calls, streaming, or structured output).What's in this slice
New namespace
AiDotNet.Agentic.Models(src/Agentic/Models/):IChatClient<T>— message-based, native tool-calling, streaming (IAsyncEnumerable), structured output.ChatRole,ChatFinishReason,ToolChoiceMode,ChatResponseFormatKind.AiContent+TextContent/ImageContent/ToolCallContent/ToolResultContent.ChatMessage(System/User/Assistant/Tool factories,.Text/.ToolCallsprojections).AiToolDefinition(JSON-SchemaJObject),ChatOptions,ChatUsage,ChatResponse,ChatResponseUpdate,StreamingToolCallUpdate.Not in this slice (subsequent Phase 0 PRs)
Tool/function abstraction + source-generated JSON schema; reimplemented OpenAI/Anthropic/Azure connectors on
IChatClient<T>;Microsoft.Extensions.AIadapter; typed structured-output helper.Verification
AiDotNet.csprojbuilds clean on net10.0 + net471, 0 errors (warnings-as-errors on).UnitTests/Agentic/Models/ChatModelAbstractionTests.cs: 15/15 pass on net10.0 and net471, including a fake-client streaming round-trip (confirmsIAsyncEnumerableon the .NET Framework polyfill path).Conventions followed: generic
<T>,Guard.*checks, enums-not-strings, "For Beginners" XML docs,#nullable disable(not!) for null-guard tests.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Breaking Changes
Chores