Add Andes.Extensions.AI.UI status contract and de-dup progress headers - #6
Conversation
Adds a fourth satellite package, Andes.Extensions.AI.UI, providing a serializable, cross-language contract an API streams to a UI to render the assistant's live activity hierarchy before its final answer: the assistant's own functions, MCP tools, and agents, each with streaming sub-statuses and nested children, plus per-activity and total token usage. - DTO records (AssistantUiEvent flat delta + AssistantStatusSnapshot folded tree, AssistantActivity, SubStatus, UsageSummary) and enums. - ChatResponseUiExtensions maps the tracked ChatProgressContent/ UsageReportContent stream into the contract; AssistantStatusReducer folds events into snapshots; AssistantUiJsonContext gives camelCase, string-enum, null-omitting JSON. - A 1:1 TypeScript file (interfaces + foldAssistantEvents) ships as package content so a SPA reconstructs the same tree a Blazor/console client does. - Unit tests for the mapper, reducer, and serialization. Each activity carries a clean DisplayName plus a separate Kind badge instead of a pre-composed "Calling ... MCP/Agent/Tool" header, so the kind word is never repeated. Relatedly, the core default header formatter now omits the kind word when the name already ends with it (case-insensitive), so a server named "Andes Test MCP" or an agent named "Research Agent" is no longer doubled. Updated the affected header assertions and added a de-dup theory. Docs: new docs/ui.md and docs/examples/ui-contract.md (minimal-API, Blazor WASM, and TypeScript SPA consumers); architecture and README updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019byxhMbTJYVwCtvwWUGZtC
There was a problem hiding this comment.
Pull request overview
This pull request introduces a new satellite package, Andes.Extensions.AI.UI, that provides a serializable C# + TypeScript contract for streaming an assistant’s live activity hierarchy (tools, MCP servers, agents), including sub-statuses and usage, while also de-duplicating the kind word in the core default progress headers.
Changes:
- Added Andes.Extensions.AI.UI package (DTOs, JSON source-gen context, mapper/reducer, and shipped TypeScript reducer/types).
- Updated core default header formatting to avoid repeating kind words (e.g., “MCP MCP”, “Agent Agent”) and updated affected tests/docs.
- Added documentation and examples for consuming the UI contract via SSE in .NET and TypeScript.
Reviewed changes
Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Andes.Extensions.AI.Unit.Test/ToolClassificationTests.cs | Adds theory coverage for header de-dup behavior across tool kinds. |
| tests/Andes.Extensions.AI.UI.Unit.Test/ChatResponseUiExtensionsTests.cs | Verifies UI event mapping and snapshot folding behavior for UI contract. |
| tests/Andes.Extensions.AI.UI.Unit.Test/AssistantUiJsonContextTests.cs | Tests JSON shape (camelCase, string enums, null omission) for UI contract. |
| tests/Andes.Extensions.AI.UI.Unit.Test/AssistantStatusReducerTests.cs | Tests folding logic for nested activities, failure, finished usage, and text accumulation. |
| tests/Andes.Extensions.AI.UI.Unit.Test/Andes.Extensions.AI.UI.Unit.Test.csproj | New unit test project for the UI satellite package. |
| tests/Andes.Extensions.AI.Mcp.Unit.Test/McpToolClassificationTests.cs | Updates MCP header assertion to reflect de-dup behavior. |
| tests/Andes.Extensions.AI.Mcp.Integration.Test/McpToolTrackingIntegrationTests.cs | Updates integration assertion for MCP header de-dup behavior. |
| tests/Andes.Extensions.AI.Agent.Unit.Test/AgentToolClassificationTests.cs | Updates agent header assertion to reflect de-dup behavior. |
| tests/Andes.Extensions.AI.Agent.Unit.Test/AgentFunctionCallReportingTests.cs | Updates agent header assertion for de-dup behavior in nested progress. |
| tests/Andes.Extensions.AI.Agent.Integration.Test/AgentToolTrackingIntegrationTests.cs | Updates integration assertion for agent header de-dup behavior. |
| README.md | Adds top-level README section introducing the new UI package and usage. |
| docs/ui.md | New documentation describing the UI contract, mapper/reducer, TS file, and examples. |
| docs/getting-started.md | Updates header formatter docs to describe kind-word de-dup behavior. |
| docs/examples/ui-contract.md | New end-to-end example: minimal API SSE producer + Blazor WASM + TypeScript SPA consumer. |
| docs/examples/progress-board.md | Updates example output/docs to reflect de-duplicated headers. |
| docs/architecture.md | Documents the new UI satellite package and its relationship to the scope tree. |
| docs/agents.md | Updates agent docs to describe name-ending de-dup behavior. |
| Andes.Extensions.slnx | Adds the new UI project and its unit test project to the solution. |
| Andes.Extensions.AI/ToolTrackingOptions.cs | Updates XML docs to describe kind-word de-dup behavior. |
| Andes.Extensions.AI/Internal/RequestTracker.cs | Implements kind-word de-dup logic in the default header formatter. |
| Andes.Extensions.AI.UI/UsageSummary.cs | Adds flattened, serialization-friendly usage DTO. |
| Andes.Extensions.AI.UI/typescript/andes-assistant-ui.ts | Adds shipped TypeScript contract + folding reducer to mirror C# behavior. |
| Andes.Extensions.AI.UI/SubStatus.cs | Adds sub-status DTO for activity progress lines. |
| Andes.Extensions.AI.UI/README.md | Adds package README describing the contract and how to consume it. |
| Andes.Extensions.AI.UI/ChatResponseUiExtensions.cs | Adds stream-to-contract mapper and snapshot folding pipeline extensions. |
| Andes.Extensions.AI.UI/AssistantUiJsonContext.cs | Adds source-generated System.Text.Json context for the UI contract. |
| Andes.Extensions.AI.UI/AssistantUiEventKind.cs | Adds discriminator enum for UI events. |
| Andes.Extensions.AI.UI/AssistantUiEvent.cs | Adds flat event DTO for streaming deltas. |
| Andes.Extensions.AI.UI/AssistantStatusSnapshot.cs | Adds folded snapshot DTO for UI rendering. |
| Andes.Extensions.AI.UI/AssistantStatusReducer.cs | Adds reducer that folds events into immutable snapshots. |
| Andes.Extensions.AI.UI/AssistantActivity.cs | Adds activity DTO used by folded snapshots (tree structure). |
| Andes.Extensions.AI.UI/Andes.Extensions.AI.UI.csproj | New UI satellite package project + packing the TS file. |
| Andes.Extensions.AI.UI/ActivityState.cs | Adds activity/request lifecycle enum for UI state. |
| Andes.Extensions.AI.Agent/README.md | Updates agent README example header to reflect de-dup behavior. |
Comments suppressed due to low confidence (1)
Andes.Extensions.AI.UI/ChatResponseUiExtensions.cs:172
Finishedevents are emitted withoutTimestamp, so (likeTextDelta) the serialized JSON will include a defaulttimestampvalue (year 0001) rather than something meaningful. If clients display timestamps, this looks like bad data. Consider settingTimestamp(and explicitlyDepth/ToolKind) when building the finished event.
private static AssistantUiEvent ToFinishedEvent(ChatUsageReport report)
{
return new AssistantUiEvent
{
Kind = AssistantUiEventKind.Finished,
Usage = report.TotalUsage.ToUsageSummary(),
DurationSeconds = report.Duration.TotalSeconds,
};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Introduced interactive console chat in `samples/Andes.Extensions.AI.Demo` - Implemented `DemoTools`, `DemoAgents`, and `DemoMcpServer` for functionality - Added `StatusRenderer` for rendering assistant status snapshots - Included sample configuration file `appsettings.sample.json` - Updated `.gitignore` and `Directory.Build.props` for demo project settings
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 47 out of 48 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (4)
Andes.Extensions.AI/Internal/RequestTracker.cs:356
- DefaultHeader.Compose trims trailing whitespace only for the EndsWith check, but still emits the original untrimmed name. If a tool/server name has trailing spaces, the header will include those spaces (and may still omit the suffix), producing odd UI/log output.
{
return name.TrimEnd().EndsWith(suffix, StringComparison.OrdinalIgnoreCase)
? $"Calling {name}"
: $"Calling {name} {suffix}";
}
Andes.Extensions.AI.UI/ChatResponseUiExtensions.cs:114
- ToUiEvent currently copies ChatProgressUpdate.Message for all event kinds. For ActivityStarted/Completed/Failed this reintroduces the composed English header/status strings (e.g., "Calling … MCP"), which the UI contract docs say should be represented via DisplayName + ToolKind badge (and message reserved for request status / sub-status lines). Consider nulling Message for non-Status and non-ActivityProgress events.
return new AssistantUiEvent
{
Kind = MapKind(update.Kind),
Message = update.Message,
ScopeId = update.ScopeId,
Andes.Extensions.AI.UI/ChatResponseUiExtensions.cs:67
- TextDelta and Finished events are created without setting Timestamp. Because AssistantUiJsonContext omits only nulls (not default value types), these events will serialize a default timestamp ("0001-01-01…"), which is noisy and can confuse consumers (TypeScript contract also treats timestamp as required). Consider explicitly setting a meaningful timestamp (e.g., UtcNow) for these synthetic events.
if (update.Text is { Length: > 0 } text)
{
yield return new AssistantUiEvent
{
Kind = AssistantUiEventKind.TextDelta,
Text = text,
};
}
Andes.Extensions.AI.UI/ChatResponseUiExtensions.cs:173
- Finished events also omit Timestamp, so they will serialize a default "0001-01-01…" value under the current JsonSourceGenerationOptions. Setting a real timestamp avoids consumers having to special-case the default value.
private static AssistantUiEvent ToFinishedEvent(ChatUsageReport report)
{
return new AssistantUiEvent
{
Kind = AssistantUiEventKind.Finished,
Usage = report.TotalUsage.ToUsageSummary(),
DurationSeconds = report.Duration.TotalSeconds,
};
}
- Introduced `ChatProgressToolScope` to manage nested tool-call scopes and track usage. - Enhanced `AgentToolTrackingIntegrationTests` to validate nested agent tool invocation and usage attribution. - Added `FinalSnapshot` class to merge activity snapshots with usage reports. - Created unit tests for nested agent scopes, ensuring correct tracking and reporting behavior. - Updated existing tests to cover new functionality and edge cases for nested scopes. - Implemented `LiveNestedActivityTests` to verify live updates for nested activities. - Ensured proper handling of tool invocation and usage reporting in various scenarios.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 65 out of 66 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
Andes.Extensions.AI/Internal/RequestTracker.cs:357
- DefaultHeader's Compose() trims the name only for the EndsWith check, but returns the original (potentially trailing-whitespace) name. That can produce headers with unexpected trailing spaces (and suppress the suffix) if a DisplayName/Source accidentally includes whitespace. Trimming once and using the trimmed value for both the check and the output avoids this edge case.
static string Compose(string name, string suffix)
{
return name.TrimEnd().EndsWith(suffix, StringComparison.OrdinalIgnoreCase)
? $"Calling {name}"
: $"Calling {name} {suffix}";
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 69 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
Andes.Extensions.AI.UI/ChatResponseUiExtensions.cs:68
ToUiEventsAsynccurrently forwards the in-bandRequestCompletedprogress update as aStatusevent (viaToUiEvent/MapKind), which overwrites the UI's last meaningfulAssistantStatuswith the literal "Request completed" right before emitting the finalFinishedevent. Also,TextDeltaevents leaveTimestampat its default, which will serialize as an invalid-looking year-0001 value.
Consider skipping RequestCompleted progress events here (the Finished event already represents completion) and setting an explicit timestamp for text deltas.
foreach (AIContent content in update.Contents)
{
switch (content)
{
case ChatProgressContent progress:
Andes.Extensions.AI.UI/ChatResponseUiExtensions.cs:173
ToFinishedEventleavesDepth,ToolKind, and especiallyTimestampat their defaults. BecauseAssistantUiJsonContextonly omitsnulls (not default values), this will serializetimestampas the year-0001 default, which is surprising for consumers.
Set explicit values so the wire contract stays consistent and doesn't emit sentinel timestamps.
private static AssistantUiEvent ToFinishedEvent(ChatUsageReport report)
{
return new AssistantUiEvent
{
Kind = AssistantUiEventKind.Finished,
Usage = report.TotalUsage.ToUsageSummary(),
DurationSeconds = report.Duration.TotalSeconds,
};
}
Andes.Extensions.AI.UI/ChatResponseUiExtensions.cs:189
ToActivityusesScopeId = call.CallId ?? call.ToolName. WhenCallIdis missing (it can benullfor nested scopes opened outside a function-invocation loop, or for providers that don't surface call ids), multiple invocations of the same tool will collide on the sameScopeId. That can break UI diffing/keying and any later event association.
Use a guaranteed-unique fallback ID when CallId is null.
private static AssistantActivity ToActivity(ToolCallUsage call)
{
return new AssistantActivity
{
ScopeId = call.CallId ?? call.ToolName,
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 67 out of 69 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
Andes.Extensions.AI.UI/ChatResponseUiExtensions.cs:179
ChatUsageReport.ToSnapshot()usesScopeId = call.CallId ?? call.ToolName, which can collide when the same tool is invoked multiple times with a nullCallId(e.g., direct invocation from inside a tool body). Colliding scope IDs can break UI keying and any consumer that expects scope IDs to be unique within a snapshot.
ScopeId = call.CallId ?? call.ToolName,
Andes.Extensions.AI.UI/AssistantUiEvent.cs:94
Timestampis non-nullable, butTextDeltaandFinishedevents are created without setting it, so serialization will emit the default0001-01-01...value. Consumers may misinterpret that as a real event time; consider makingTimestampnullable and omitting it when unavailable (and updating the TypeScript contract accordingly), or explicitly setting a meaningful timestamp for those event kinds.
/// status and activity events; <see cref="AssistantUiEventKind.TextDelta"/> and
/// <see cref="AssistantUiEventKind.Finished"/> events, which have no source progress event,
/// leave it at its default. Consume events in stream order rather than sorting by this value.
/// </summary>
public DateTimeOffset Timestamp { get; init; }
This pull request adds support for the new
Andes.Extensions.AI.UIpackage across the codebase, introduces per-release documentation requirements, and implements nested tool tracking for agents. It also bumps all package versions to0.3.0, updates the NuGet publishing workflow to handle four packages, and improves documentation and packaging metadata.Support for Andes.Extensions.AI.UI and Release Process
Andes.Extensions.AI.UIas a fourth shipping package throughout documentation and the.github/workflows/nuget.ymlworkflow, including packaging, validation, and publishing steps. [1] [2] [3] [4] [5]releases/directory for per-release notes and updated project conventions to require a release changelog for every version bump. [1] [2]Nested Tool Tracking for Agents
AgentTrackingAIFunctionand updated XML docs and README. [1] [2] [3] [4]Version and Packaging Updates
Andes.Extensions.AI,.Mcp,.Agent,.UI) to0.3.0and addedicon.pngto the.Agentand.Mcppackages for improved NuGet presentation. [1] [2] [3] [4] [5]Documentation Improvements
Summary of Most Important Changes
1. Support for Andes.Extensions.AI.UI and Release Process
Andes.Extensions.AI.UIas a fourth package in all documentation and the NuGet publishing workflow, including validation for four packages and updating related scripts and comments. [1] [2] [3] [4] [5]releases/directory for per-release notes and updated project standards to require a changelog for every version bump. [1] [2]2. Nested Tool Tracking for Agents
AgentTrackingAIFunction, so agent tools invoked within other tools/agents open their own child tracking scopes, with clear usage attribution and activity rendering. [1] [2] [3] [4]3. Version and Packaging Updates
0.3.0and addedicon.pngto.Agentand.Mcppackages for improved NuGet presentation. [1] [2] [3] [4] [5]4. Documentation Improvements
Andes.Extensions.AI.UIpackage, updates the project and workflow to handle four shipping packages (instead of three), and introduces nested tool tracking scopes for both Agent and MCP tool invocations. This ensures that when agents or MCP tools are invoked as tools of other agents or tools, their activities and usage are tracked as child activities, improving attribution and reporting. Documentation and versioning are updated to reflect these changes.Support for Andes.Extensions.AI.UI and Release Process Updates:
Andes.Extensions.AI.UIpackage to the project, workflow, and documentation, including version lockstep and release notes requirements. (.claude/CLAUDE.md,.github/workflows/nuget.yml) [1] [2] [3] [4] [5] [6] [7] [8]Andes.Extensions.AI,Andes.Extensions.AI.Mcp,Andes.Extensions.AI.Agent,Andes.Extensions.AI.UI). [1] [2] [3]Nested Tool Tracking Scopes:
Andes.Extensions.AI.Agent/Internal/AgentTrackingAIFunction.cs,Andes.Extensions.AI.Mcp/Internal/McpTrackingAIFunction.cs) [1] [2] [3] [4]Andes.Extensions.AI.Agent/AgentToolTrackingExtensions.cs,Andes.Extensions.AI.Agent/README.md) [1] [2]Version Bumps:
Bumped package versions to
0.3.0forAndes.Extensions.AI.AgentandAndes.Extensions.AI.Mcpto reflect new features and lockstep versioning. (Andes.Extensions.AI.Agent/Andes.Extensions.AI.Agent.csproj,Andes.Extensions.AI.Mcp/Andes.Extensions.AI.Mcp.csproj) [1] [2]Adds a fourth satellite package, Andes.Extensions.AI.UI, providing aserializable, cross-language contract an API streams to a UI to render the
assistant's live activity hierarchy before its final answer: the assistant's
own functions, MCP tools, and agents, each with streaming sub-statuses and
nested children, plus per-activity and total token usage.
DTO records (AssistantUiEvent flat delta + AssistantStatusSnapshot folded
tree, AssistantActivity, SubStatus, UsageSummary) and enums.
ChatResponseUiExtensions maps the tracked ChatProgressContent/
UsageReportContent stream into the contract; AssistantStatusReducer folds
events into snapshots; AssistantUiJsonContext gives camelCase, string-enum,
null-omitting JSON.
A 1:1 TypeScript file (interfaces + foldAssistantEvents) ships as package
content so a SPA reconstructs the same tree a Blazor/console client does.
Unit tests for the mapper, reducer, and serialization.
Each activity carries a clean DisplayName plus a separate Kind badge instead
of a pre-composed "Calling ... MCP/Agent/Tool" header, so the kind word is
never repeated. Relatedly, the core default header formatter now omits the
kind word when the name already ends with it (case-insensitive), so a server
named "Andes Test MCP" or an agent named "Research Agent" is no longer
doubled. Updated the affected header assertions and added a de-dup theory.
Docs: new docs/ui.md and docs/examples/ui-contract.md (minimal-API, Blazor
WASM, and TypeScript SPA consumers); architecture and README updated.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_019byxhMbTJYVwCtvwWUGZtC