Summary
<system_notification> content from the Copilot SDK appears under an [assistant] label with the XML tags visible. The data model is correct end-to-end; the bug is entirely in the rendering layer and consists of two separate root causes that must both be present to produce the observed symptom.
This bug is CLI-only. The GUI WebView path correctly renders [system] because ChatHistoryItemViewModel uses item.Role.Value for the label. The CLI path ignores item.Role entirely.
Root Cause 1 — CLI OnTurnCompleted Always Labels Items "assistant"
File: Phantom.Workspaces.Agent.Cli/Program.cs, lines 355–376
private void OnTurnCompleted(object? sender, AgentChatHistoryItem item)
{
var itemText = string.Concat(item.Contents.OfType<TextContent>().Select(static c => c.Text));
...
if (!this.supportsInteractiveRendering)
{
Console.WriteLine($"assistant > {itemText}"); // ignores item.Role
return;
}
this.FinalizeAssistantTurn(); // also always the assistant visual context
}
AgentChat.TurnCompleted fires for every AgentChatHistoryItem promoted to history — including {Role=System} items. OnTurnCompleted (subscribed at line 92) never inspects item.Role. A system-role item is rendered identically to an assistant message.
Fix: Branch on item.Role — for ChatRole.System, either render with a system > prefix, route to a separate display path, or suppress CLI output (system notifications are informational metadata, not assistant replies).
Root Cause 2 — StripSystemNotificationTags Exact-Match Failure
File: Phantom.Workspaces.Llm.Core/CopilotSdkStreamAdapter.cs, lines 272–285
private static string StripSystemNotificationTags(string content)
{
const string openTag = "<system_notification>";
const string closeTag = "</system_notification>";
var trimmed = content.Trim();
if (trimmed.StartsWith(openTag, StringComparison.Ordinal)
&& trimmed.EndsWith(closeTag, StringComparison.Ordinal))
{
return trimmed[openTag.Length..^closeTag.Length].Trim();
}
return content; // raw XML returned if pattern doesn't match exactly
}
The SDK documentation states the notification content is "typically" wrapped in <system_notification> tags — not always. If the content has text outside the tags, uses different casing, or has no tags at all, StripSystemNotificationTags returns the content verbatim including the XML markup.
Fix: Use a regex or IndexOf-based approach to extract the inner content regardless of surrounding text, or accept that tag-less content is also valid and strip tags wherever they appear in the string.
Data Pipeline (verified correct)
The SystemNotificationEvent carries the correct ChatRole.System through every layer:
| Step |
Location |
Role preserved |
SystemNotificationEvent received |
CopilotSdkStreamAdapter.cs:168–178 |
✅ ChatRole.System set |
| Routed to root channel |
CopilotSubAgentRouter.cs:83 |
✅ |
| Passed through middleware |
StreamingPersistenceMiddleware.cs |
✅ |
| Coalesced to history item |
AgentChat.cs CoalesceSegmentAsync:1285–1292 |
✅ AgentChatHistoryItem { Role=System } |
| CLI rendering |
Program.cs:369 |
❌ Role ignored — labeled assistant > |
| GUI rendering |
ChatHistoryItemViewModel.cs:19 |
✅ Shows [system] correctly |
Affected Files
| File |
Root Cause |
What to change |
Phantom.Workspaces.Agent.Cli/Program.cs |
#1 |
Inspect item.Role in OnTurnCompleted; render system items differently |
Phantom.Workspaces.Llm.Core/CopilotSdkStreamAdapter.cs |
#2 |
Robustify StripSystemNotificationTags to handle non-exact-match content |
Expected Tests
| Test Name |
Class |
What It Verifies |
OnTurnCompleted_SystemRoleItem_DoesNotRenderAsAssistant |
CliRenderingTests (new) |
A history item with Role=System is not output with the assistant > prefix |
OnTurnCompleted_SystemRoleItem_RendersWithSystemPrefix |
CliRenderingTests (new) |
A history item with Role=System renders with system > prefix (or is suppressed) |
StripSystemNotificationTags_ContentWithSurroundingText_StillStripped |
CopilotSdkStreamAdapterTests |
Tags are stripped even when non-tag text precedes or follows them |
StripSystemNotificationTags_ContentWithNoTags_ReturnsContentUnchanged |
CopilotSdkStreamAdapterTests |
Content with no tags is returned as-is without throwing |
SystemNotificationEvent_AppearsInHistory_WithSystemRole |
CopilotSdkStreamAdapterTests |
End-to-end: SystemNotificationEvent produces AgentChatHistoryItem with Role=System |
Summary
<system_notification>content from the Copilot SDK appears under an[assistant]label with the XML tags visible. The data model is correct end-to-end; the bug is entirely in the rendering layer and consists of two separate root causes that must both be present to produce the observed symptom.This bug is CLI-only. The GUI WebView path correctly renders
[system]becauseChatHistoryItemViewModelusesitem.Role.Valuefor the label. The CLI path ignoresitem.Roleentirely.Root Cause 1 — CLI
OnTurnCompletedAlways Labels Items"assistant"File:
Phantom.Workspaces.Agent.Cli/Program.cs, lines 355–376AgentChat.TurnCompletedfires for everyAgentChatHistoryItempromoted to history — including{Role=System}items.OnTurnCompleted(subscribed at line 92) never inspectsitem.Role. A system-role item is rendered identically to an assistant message.Fix: Branch on
item.Role— forChatRole.System, either render with asystem >prefix, route to a separate display path, or suppress CLI output (system notifications are informational metadata, not assistant replies).Root Cause 2 —
StripSystemNotificationTagsExact-Match FailureFile:
Phantom.Workspaces.Llm.Core/CopilotSdkStreamAdapter.cs, lines 272–285The SDK documentation states the notification content is "typically" wrapped in
<system_notification>tags — not always. If the content has text outside the tags, uses different casing, or has no tags at all,StripSystemNotificationTagsreturns the content verbatim including the XML markup.Fix: Use a regex or
IndexOf-based approach to extract the inner content regardless of surrounding text, or accept that tag-less content is also valid and strip tags wherever they appear in the string.Data Pipeline (verified correct)
The
SystemNotificationEventcarries the correctChatRole.Systemthrough every layer:SystemNotificationEventreceivedCopilotSdkStreamAdapter.cs:168–178ChatRole.SystemsetCopilotSubAgentRouter.cs:83StreamingPersistenceMiddleware.csAgentChat.cs CoalesceSegmentAsync:1285–1292AgentChatHistoryItem { Role=System }Program.cs:369assistant >ChatHistoryItemViewModel.cs:19[system]correctlyAffected Files
Phantom.Workspaces.Agent.Cli/Program.csitem.RoleinOnTurnCompleted; render system items differentlyPhantom.Workspaces.Llm.Core/CopilotSdkStreamAdapter.csStripSystemNotificationTagsto handle non-exact-match contentExpected Tests
OnTurnCompleted_SystemRoleItem_DoesNotRenderAsAssistantCliRenderingTests(new)Role=Systemis not output with theassistant >prefixOnTurnCompleted_SystemRoleItem_RendersWithSystemPrefixCliRenderingTests(new)Role=Systemrenders withsystem >prefix (or is suppressed)StripSystemNotificationTags_ContentWithSurroundingText_StillStrippedCopilotSdkStreamAdapterTestsStripSystemNotificationTags_ContentWithNoTags_ReturnsContentUnchangedCopilotSdkStreamAdapterTestsSystemNotificationEvent_AppearsInHistory_WithSystemRoleCopilotSdkStreamAdapterTestsSystemNotificationEventproducesAgentChatHistoryItemwithRole=System