v0.5.0
0.5.0
Tag: v0.5.0 · Date: 2026-08-06
The middleware stops guessing: the auto-emitted "Starting request"/"Thinking..." statuses are gone, replaced by a detection-driven Reasoning/ReasoningCompleted status pair that fires only when the model actually streams reasoning content — opened the moment reasoning is detected, closed (with the elapsed reasoning time) the moment the answer or the next tool call starts — plus a public Custom status factory so applications emit their own request-level statuses — in the exact synthetic shape the middleware uses. The UI package gains a reasoning-text surface (ReasoningDelta events folding into AssistantStatusSnapshot.ReasoningText), and a new sample runs the whole pipeline over the Azure OpenAI Responses API with stable packages only.
Breaking changes
ChatProgressKind.Thinkingis renamed toChatProgressKind.Reasoning(Andes.Extensions.AI/Progress/ChatProgressKind.cs; the underlying value1is preserved). Consumers switching on the enum renameThinking→Reasoning; persisted numeric values are unaffected.ChatProgressKind.RequestStartedis renamed toChatProgressKind.Custom(same file; the underlying value0is preserved): a developer-constructed status carrying any application-supplied message, never emitted by the middleware. Its factory isChatProgressUpdate.CreateCustom(string message)— the message is required (ArgumentException.ThrowIfNullOrEmpty:ArgumentNullExceptiononnull,ArgumentExceptionon empty), replacing the interimCreateRequestStarted(string? message = null)from earlier drafts of this release. The interimCreateReasoning(...)factory is removed without replacement:Reasoningis strictly detection-driven (model-only), so applications can never emit it via a factory.- The middleware no longer auto-emits request-level statuses. Previous versions opened every request (both call styles) with a "Starting request" status and a
Thinking("Thinking...") status, and re-emitted "Thinking..." after each tool round-trip. Those emissions are removed —RequestTracker.EmitRequestStarted/EmitThinkingare deleted, andAdvanceIterationnow only advances the turn counter and re-arms reasoning detection — so the first in-band event of a tool-calling request is now theToolInvokingheader.- Migration: UIs that relied on the automatic opening status prepend their own —
yield return ChatProgressUpdate.CreateCustom("Starting request").ToResponseUpdate();ahead of streaming the tracked response (see Added below) — or handle the absence (the UI package'sAssistantStatusSnapshot.AssistantStatusnow staysnulluntil the first request-level event arrives).
- Migration: UIs that relied on the automatic opening status prepend their own —
Added
- Detection-driven
Reasoningstatus.ToolTrackingChatClient.Inspectwatches the stream forMicrosoft.Extensions.AI.TextReasoningContentand calls the newRequestTracker.OnReasoningDetected(), which emits oneReasoningevent per model turn (message "Reasoning...", root scope, depth 0), re-armed after each tool round-trip. Updates are inspected before they are forwarded, so the status enters the channel ahead of the update carrying the reasoning content. Detection is content-based and therefore provider-agnostic: the OpenAI Responses API producesTextReasoningContenttoday, while plain Chat Completions never streams reasoning — chat pipelines simply never see the status. Non-streamingGetResponseAsyncmirrors this post-hoc: if any response message containsTextReasoningContent, at most oneReasoningevent is raised (observers only — turns are indistinguishable in an aggregated response). The event never carries the reasoning text itself (privacy invariant unchanged), and with no public factory for the kind, it only ever originates from this detection. ChatProgressKind.ReasoningCompletedcloses each detected reasoning turn. New enum member (ReasoningCompleted = 8,Andes.Extensions.AI/Progress/ChatProgressKind.cs), raised at most once per model turn by aRequestTracker.OnReasoningCompleted()latch — re-armed alongside detection byAdvanceIteration()after each tool round-trip — when the first answer text (non-emptyTextContent) orFunctionCallContentfollows detected reasoning, or when the stream ends on a reasoning-only final turn. The function-call hook fires beforeOnFunctionCall, so the close precedes anyToolInvokingheader; the stream-end close happens inPumpAsyncbefore the channel completes, so the event stays in-band ahead of the trailingRequestCompleted. Message "Reasoning completed", root scope, depth 0.ChatProgressUpdate.Durationcarries the elapsed reasoning time — first detection to the first completion trigger — when streaming; the non-streaming post-hoc mirror passesmeasured: false, soDurationstaysnullthere while observers now receive a balancedReasoning+ReasoningCompletedpair per request. LikeReasoning, the kind has no public factory and never carries reasoning text (privacy invariant unchanged), and the failure path is untouched — a request that faults after detection raisesRequestFailedwith no reasoning completion. The UI package needed zero code changes:MapKindcollapses the new kind toAssistantUiEventKind.Status(status line "Reasoning completed") andToUiEventalready mapsDuration→DurationSecondsgenerically, so UIs receive the reasoning duration on thatStatusevent — the shipped TypeScript contract is untouched (ChatProgressKindnever crosses the wire). Visible in the Responses demo: the header now flips from "Reasoning..." to "Reasoning completed" the moment the answer (or the next tool call) starts, instead of "Reasoning..." lingering until the next event.- Developer-owned request statuses. The public static factory
ChatProgressUpdate.CreateCustom(string message)constructs a request-levelCustomupdate carrying any application-supplied message, stamped with the new public constantChatProgressUpdate.ExternalScopeId("scope-external"— never collides with the middleware's per-request scope identifiers), depth 0, and the current UTC time. The new extensionChatProgressUpdateExtensions.ToResponseUpdate()(Andes.Extensions.AI/Progress/ChatProgressUpdateExtensions.cs) wraps an update into a role-lessChatResponseUpdatecarrying a singleChatProgressContent— the exact synthetic shape the middleware emits — so apps can prepend or interleave their own statuses into the stream a UI consumes (for example, ahead ofToStatusSnapshotsAsync()). Both sample apps demonstrate the prepend pattern withCreateCustom("Starting request")in theirStreamTurnlocal function. Andes.Extensions.AI.UI: the model's reasoning summary text now propagates through the UI contract. NewAssistantUiEventKind.ReasoningDelta(betweenTextDeltaandFinished):ChatResponseUiExtensions.ToUiEventsAsyncemits one per in-bandTextReasoningContentwith non-empty text — encrypted-only reasoning items (empty text) carry nothing renderable and are skipped — with the chunk inAssistantUiEvent.Text.AssistantStatusReduceraccumulates the deltas into the newAssistantStatusSnapshot.ReasoningText: verbatim concatenation across the whole request, including tool round-trips, with no synthetic separators. The shipped TypeScript mirror (typescript/andes-assistant-ui.ts) gains the"ReasoningDelta"union member,reasoningText?: string, and the matchingfoldAssistantEventscase. The privacy boundary holds: reasoning text is sourced only from in-band model content the stream already carries — core progress events remain text-free.- New sample:
samples/Andes.Extensions.AI.Demo.Responses. A console chat over the Azure OpenAI Responses API with stable packages only: the plainOpenAIClient(stableOpenAI2.12.0) targets the OpenAI-v1-compatible endpoint ({endpoint}/openai/v1) — the stableAzure.AI.OpenAI2.1.0 has no Responses surface, which is why the plain-client route is used — andGetResponsesClient().AsIChatClient(deployment)adapts it to the tracked pipeline.ChatOptions.Reasoning = new ReasoningOptions { Output = ReasoningOutput.Full }makes reasoning summaries stream back asTextReasoningContent, lighting up the liveReasoningstatus (see Fixed for whySummarydoes not work on gpt-5-series deployments), and the demo'sStatusRenderershows the accumulatingAssistantStatusSnapshot.ReasoningTextin a dimmed "reasoning" panel — the last six lines while the Live frame streams, and the full text in the persistent final frame (and on failures), with the total reasoning time in the panel header summed from the recordedReasoningCompletedstatuses'Duration. Requires a reasoning-capable deployment (gpt-5 family / o-series); builds withNoWarnOPENAI001because the Responses surface is still[Experimental]in OpenAI 2.12.0. Registered inAndes.Extensions.slnx;IsPackable=falselike the existing sample — it never ships to NuGet. - New optional integration setting
AzureOpenAI:ResponsesDeployment, gating the new[SkippableFact]suitetests/Andes.Extensions.AI.Integration.Test/ResponsesStreamingIntegrationTests.cs: end to end against the Responses API, it asserts that noCustomstatus appears (the middleware never emits one), that exactly one "Reasoning..." status is raised for a single-turn request, that the status precedes the first reasoning content, and that a singleReasoningCompletedwith a non-nullDurationfollows it — pair ordering rather than fixed positions, since real turn structure varies by deployment. When the setting is absent those tests skip cleanly while the chat-deployment tests still run. - Root README badges — NuGet version badges for all four packages, the NuGet Publish workflow status, the MIT license, and .NET 10.
Changed
- Dependency pins (
Directory.Packages.props):Microsoft.Agents.AI1.16.0 → 1.17.0 (the Agent satellite's new floor) andModelContextProtocol/ModelContextProtocol.Core2.0.0 → 2.1.0 (the MCP satellite now floors Core>= 2.1.0; the fullModelContextProtocolpackage remains test-and-demo-only). A new explicit pinOpenAI2.12.0 is consumed only by the Responses sample and the core integration test project — nothing shipped to NuGet references it. - The interactive demo (
samples/Andes.Extensions.AI.Demo) prependsChatProgressUpdate.CreateCustom("Starting request").ToResponseUpdate()in itsStreamTurntee — outside the recording loop, so the Live header lights up immediately while the synthetic update never enters chat history or the usage report. Andes.Extensions.AI.UI: request-level statuses keep flowing through the existingStatuscontract — request-level kinds still collapse toAssistantUiEventKind.Statuswith the message passed through, so the middleware's detectedReasoningstatus, its closingReasoningCompleted(whose reasoning duration arrives as theStatusevent'sDurationSeconds— see Added), and a developer-prependedCustomupdate all driveAssistantStatusSnapshot.AssistantStatusunchanged; the reasoning text travels separately on the newReasoningDeltasurface (see Added). Doc-comment example strings switch "Thinking…" → "Reasoning…" inAssistantStatusSnapshot,AssistantUiEventKind, and the shippedtypescript/andes-assistant-ui.ts.- Docs updated for the new status model: Getting started (kinds, transcripts, and a new Emit request-level statuses yourself section), Architecture (the detection design and the non-streaming post-hoc note), the MCP and Agent transcripts, UI support, and the Progress Board example. The root README gains "Reasoning detection" and "Developer-owned request statuses" bullets, an "Emit your own statuses" section, and the Responses sample README.
- All four packages version in lockstep at 0.5.0; the satellites depend on core
>= 0.5.0.
Fixed
- Reasoning summaries actually stream on gpt-5-series Azure deployments:
ReasoningOutput.Full, notSummary. Earlier drafts of this release requestedOutput = ReasoningOutput.Summary, which Microsoft.Extensions.AI.OpenAI 10.8.3 maps to the Responses summary verbosity"concise"— a value gpt-5-series Azure deployments do not support (supported:auto,detailed) — so no summaries streamed and theReasoningstatus never fired.Fullmaps to"detailed", which streams. NoEffortoverride is set either:ExtraHighmaps to"xhigh", accepted only by gpt-5.1+. Applied insamples/Andes.Extensions.AI.Demo.Responses/Program.csandtests/Andes.Extensions.AI.Integration.Test/ResponsesStreamingIntegrationTests.cs.
Verification
New unit coverage pins the behavior change: ReasoningDetectionTests (one Reasoning status per turn, ordered ahead of the reasoning content; one measured ReasoningCompleted closing the turn ahead of the first answer text — an empty text chunk does not close it; a reasoning-only turn closed at stream end, ahead of the trailing RequestCompleted; re-emission of the full pair per turn across a tool round-trip, the first close landing before the tool header; no request-level statuses of any kind when nothing streams reasoning; non-streaming observers notified once for detection and with a balanced pair whose completion carries a null Duration) and ChatProgressUpdateFactoryTests (CreateCustom populating the well-known fields, its null/empty-message guards, the ToResponseUpdate() shape, and its null guard) in the core suite. The UI suite adds ToStatusSnapshotsAsync_DevPrependedCustomStatus_SetsAssistantStatus, proving a developer-prepended Custom status drives AssistantStatus through the unchanged contract, plus the reasoning-text surface tests: ToUiEventsAsync_ReasoningContent_EmitsReasoningDeltasInOrder, ToUiEventsAsync_EmptyReasoningContent_EmitsNoReasoningDelta, and ToStatusSnapshotsAsync_ReasoningAcrossToolRoundTrip_AccumulatesReasoningText in ChatResponseUiExtensionsTests; Apply_ReasoningDelta_AccumulatesReasoningText and Apply_ReasoningDeltaAcrossActivities_KeepsAccumulating in AssistantStatusReducerTests; and Serialize_SnapshotWithReasoningText_EmitsCamelCaseProperty in AssistantUiJsonContextTests. The pre-existing streaming, non-streaming, and Azure OpenAI integration tests are updated to assert the absence of auto-emitted request-level statuses, and the new ResponsesStreamingIntegrationTests exercises detection — including the Reasoning → ReasoningCompleted pair ordering and the non-null measured Duration — against a real reasoning-capable deployment.
Full Changelog: v0.4.0...v0.5.0