Skip to content

v0.5.0

Choose a tag to compare

@RorroRojas3 RorroRojas3 released this 06 Aug 05:33
· 4 commits to main since this release
616fc51

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.Thinking is renamed to ChatProgressKind.Reasoning (Andes.Extensions.AI/Progress/ChatProgressKind.cs; the underlying value 1 is preserved). Consumers switching on the enum rename ThinkingReasoning; persisted numeric values are unaffected.
  • ChatProgressKind.RequestStarted is renamed to ChatProgressKind.Custom (same file; the underlying value 0 is preserved): a developer-constructed status carrying any application-supplied message, never emitted by the middleware. Its factory is ChatProgressUpdate.CreateCustom(string message) — the message is required (ArgumentException.ThrowIfNullOrEmpty: ArgumentNullException on null, ArgumentException on empty), replacing the interim CreateRequestStarted(string? message = null) from earlier drafts of this release. The interim CreateReasoning(...) factory is removed without replacement: Reasoning is 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/EmitThinking are deleted, and AdvanceIteration now only advances the turn counter and re-arms reasoning detection — so the first in-band event of a tool-calling request is now the ToolInvoking header.
    • 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's AssistantStatusSnapshot.AssistantStatus now stays null until the first request-level event arrives).

Added

  • Detection-driven Reasoning status. ToolTrackingChatClient.Inspect watches the stream for Microsoft.Extensions.AI.TextReasoningContent and calls the new RequestTracker.OnReasoningDetected(), which emits one Reasoning event 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 produces TextReasoningContent today, while plain Chat Completions never streams reasoning — chat pipelines simply never see the status. Non-streaming GetResponseAsync mirrors this post-hoc: if any response message contains TextReasoningContent, at most one Reasoning event 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.ReasoningCompleted closes each detected reasoning turn. New enum member (ReasoningCompleted = 8, Andes.Extensions.AI/Progress/ChatProgressKind.cs), raised at most once per model turn by a RequestTracker.OnReasoningCompleted() latch — re-armed alongside detection by AdvanceIteration() after each tool round-trip — when the first answer text (non-empty TextContent) or FunctionCallContent follows detected reasoning, or when the stream ends on a reasoning-only final turn. The function-call hook fires before OnFunctionCall, so the close precedes any ToolInvoking header; the stream-end close happens in PumpAsync before the channel completes, so the event stays in-band ahead of the trailing RequestCompleted. Message "Reasoning completed", root scope, depth 0. ChatProgressUpdate.Duration carries the elapsed reasoning time — first detection to the first completion trigger — when streaming; the non-streaming post-hoc mirror passes measured: false, so Duration stays null there while observers now receive a balanced Reasoning + ReasoningCompleted pair per request. Like Reasoning, 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 raises RequestFailed with no reasoning completion. The UI package needed zero code changes: MapKind collapses the new kind to AssistantUiEventKind.Status (status line "Reasoning completed") and ToUiEvent already maps DurationDurationSeconds generically, so UIs receive the reasoning duration on that Status event — the shipped TypeScript contract is untouched (ChatProgressKind never 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-level Custom update carrying any application-supplied message, stamped with the new public constant ChatProgressUpdate.ExternalScopeId ("scope-external" — never collides with the middleware's per-request scope identifiers), depth 0, and the current UTC time. The new extension ChatProgressUpdateExtensions.ToResponseUpdate() (Andes.Extensions.AI/Progress/ChatProgressUpdateExtensions.cs) wraps an update into a role-less ChatResponseUpdate carrying a single ChatProgressContent — 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 of ToStatusSnapshotsAsync()). Both sample apps demonstrate the prepend pattern with CreateCustom("Starting request") in their StreamTurn local function.
  • Andes.Extensions.AI.UI: the model's reasoning summary text now propagates through the UI contract. New AssistantUiEventKind.ReasoningDelta (between TextDelta and Finished): ChatResponseUiExtensions.ToUiEventsAsync emits one per in-band TextReasoningContent with non-empty text — encrypted-only reasoning items (empty text) carry nothing renderable and are skipped — with the chunk in AssistantUiEvent.Text. AssistantStatusReducer accumulates the deltas into the new AssistantStatusSnapshot.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 matching foldAssistantEvents case. 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 plain OpenAIClient (stable OpenAI 2.12.0) targets the OpenAI-v1-compatible endpoint ({endpoint}/openai/v1) — the stable Azure.AI.OpenAI 2.1.0 has no Responses surface, which is why the plain-client route is used — and GetResponsesClient().AsIChatClient(deployment) adapts it to the tracked pipeline. ChatOptions.Reasoning = new ReasoningOptions { Output = ReasoningOutput.Full } makes reasoning summaries stream back as TextReasoningContent, lighting up the live Reasoning status (see Fixed for why Summary does not work on gpt-5-series deployments), and the demo's StatusRenderer shows the accumulating AssistantStatusSnapshot.ReasoningText in 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 recorded ReasoningCompleted statuses' Duration. Requires a reasoning-capable deployment (gpt-5 family / o-series); builds with NoWarn OPENAI001 because the Responses surface is still [Experimental] in OpenAI 2.12.0. Registered in Andes.Extensions.slnx; IsPackable=false like the existing sample — it never ships to NuGet.
  • New optional integration setting AzureOpenAI:ResponsesDeployment, gating the new [SkippableFact] suite tests/Andes.Extensions.AI.Integration.Test/ResponsesStreamingIntegrationTests.cs: end to end against the Responses API, it asserts that no Custom status 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 single ReasoningCompleted with a non-null Duration follows 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.AI 1.16.0 → 1.17.0 (the Agent satellite's new floor) and ModelContextProtocol/ModelContextProtocol.Core 2.0.0 → 2.1.0 (the MCP satellite now floors Core >= 2.1.0; the full ModelContextProtocol package remains test-and-demo-only). A new explicit pin OpenAI 2.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) prepends ChatProgressUpdate.CreateCustom("Starting request").ToResponseUpdate() in its StreamTurn tee — 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 existing Status contract — request-level kinds still collapse to AssistantUiEventKind.Status with the message passed through, so the middleware's detected Reasoning status, its closing ReasoningCompleted (whose reasoning duration arrives as the Status event's DurationSeconds — see Added), and a developer-prepended Custom update all drive AssistantStatusSnapshot.AssistantStatus unchanged; the reasoning text travels separately on the new ReasoningDelta surface (see Added). Doc-comment example strings switch "Thinking…" → "Reasoning…" in AssistantStatusSnapshot, AssistantUiEventKind, and the shipped typescript/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, not Summary. Earlier drafts of this release requested Output = 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 the Reasoning status never fired. Full maps to "detailed", which streams. No Effort override is set either: ExtraHigh maps to "xhigh", accepted only by gpt-5.1+. Applied in samples/Andes.Extensions.AI.Demo.Responses/Program.cs and tests/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 ReasoningReasoningCompleted pair ordering and the non-null measured Duration — against a real reasoning-capable deployment.

Full Changelog: v0.4.0...v0.5.0