You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copilot-SDK-backed chats silently drop any GitHub.Copilot.SDK session-event kind that is not one of the ~11 cases handled by CopilotSdkStreamAdapter.TranslateCopilotSdkSessionEvents. The switch statement has no default: arm, and the adapter's own doc-comment states that "Unrecognised event types are dropped." Because CopilotSdkChatClient implements ISelfInvokingToolChatClient, AgentFactory.WrapWithMiddleware deliberately bypasses both ToolResultSteeringMiddleware and the framework FunctionInvokingChatClient — so the adapter's translation is the only path by which any item can reach the transcript, history, or UI. Any SDK event kind the adapter does not explicitly translate is permanently invisible: it is never persisted, never rendered, and never surfaced to the user. The user-visible symptom is missing tool-call / tool-result / reasoning / background-task items that the model clearly acted on (e.g. an agent references a read_powershell call and the result of a running background task, yet neither the tool call, its result, nor the background-task lifecycle appear in the transcript).
Root Cause
Two mechanisms combine to produce silent, unrecoverable event loss for the Copilot-SDK provider:
Phantom.Workspaces.Llm.Core\AgentFactory.cs lines 410–424 — WrapWithMiddleware short-circuits for self-invoking clients:
// Wraps the inner client with ToolResultSteeringMiddleware when a queue manager is provided.// Never wraps self-invoking clients — they drive their own tool loop and GetStreamingResponseAsync// is never re-called with FunctionResultContent, so the middleware would never inject anything;// worse, delegating GetService would make the framework suppress its FunctionInvocationMiddleware.privatestaticChatClientResultWrapWithMiddleware((IChatClientclient,stringdisplayName)inner,AgentInputQueueManager?queueManager){if(queueManagerisnull||inner.clientisISelfInvokingToolChatClient||inner.client.GetService(typeof(ISelfInvokingToolChatClient))is not null){returnnewChatClientResult(inner.client,inner.displayName);}returnnewChatClientResult(newToolResultSteeringMiddleware(inner.client,queueManager),inner.displayName);}
AgentChat.cs correspondingly sets UseProvidedChatClientAsIs = true for self-invoking clients, so the framework does not attach its own FunctionInvokingChatClient wrapper either.
Consequence: the SDK adapter's translator is the sole producer of transcript items for Copilot-SDK-backed chats. Anything it fails to translate never reaches persistence or the UI.
2. The adapter switch has no default: arm
Phantom.Workspaces.Llm.Core\CopilotSdkStreamAdapter.cs lines 77–221 — the doc-comment at lines 77–84 explicitly states the drop policy:
/// <summary>/// Translates raw Copilot SDK session events into <see cref="ChatResponseUpdate"/> items./// The stream completes normally on <see cref="SessionIdleEvent"/> and faults with/// <see cref="InvalidOperationException"/> on <see cref="SessionErrorEvent"/>. Unrecognised/// event types are dropped. Accepting a <see cref="ChannelReader{T}"/> keeps the method/// testable without a live Copilot SDK session: tests write mock events to a channel and/// observe the translated output directly./// </summary>publicstaticasyncIAsyncEnumerable<ChatResponseUpdate>TranslateCopilotSdkSessionEvents(ChannelReader<SessionEvent>events,[EnumeratorCancellation]CancellationTokencancellationToken=default){ArgumentNullException.ThrowIfNull(events);awaitforeach(varsessionEventinevents.ReadAllAsync(cancellationToken).ConfigureAwait(false)){switch(sessionEvent){caseAssistantMessageDeltaEventdeltawhen ...:// line 95caseAssistantReasoningDeltaEventreasoningDeltawhen ...:// line 103caseToolExecutionStartEventtoolStart:// line 113caseToolExecutionCompleteEventtoolComplete:// line 121caseSubagentStartedEventstartedwhen ...:// line 136caseSubagentCompletedEventcompletedwhen ...:// line 156caseSubagentFailedEventfailedwhen ...:// line 168caseAssistantUsageEventusagewhen ...:// line 184caseSystemNotificationEventnotificationwhen ...:// line 192caseSessionErrorEventerror:// line 204caseSessionIdleEvent:// line 208// NO default arm — anything else falls through silently.}}}
That is the entire set: 11 event kinds handled, everything else silently dropped with no log, no telemetry, no diagnostic. Tool start/complete are mapped via CopilotToolEventMapper.MapToolStart / MapToolComplete into FunctionCallContent / FunctionResultContent, but any other tool-lifecycle event (background/async/deferred completions, cancellations, progress, etc.) has nowhere to go.
3. The non-streaming path mirrors the same limited set
Phantom.Workspaces.Llm.Core\CopilotSdkChatClient.cs lines 512–567 — GetResponseAsync subscribes to the session with a switch at lines 531–547 that only handles ToolExecutionStartEvent and ToolExecutionCompleteEvent, so any fix in the streaming adapter must be mirrored here or the non-streaming path will keep losing events:
Providers whose clients do NOT implement ISelfInvokingToolChatClient (echo, github-models, ollama, openai, azure-openai) fall through to the second branch of WrapWithMiddleware and are wrapped with ToolResultSteeringMiddleware and the framework FunctionInvokingChatClient. That framework layer surfaces tool-call / tool-result content as first-class AIContent, which is why those providers reliably show tool items even when the underlying model output is terse. The Copilot-SDK provider has no equivalent safety net — it depends entirely on the adapter's exhaustive translation.
Maps ToolExecutionStart/Complete → FunctionCallContent/FunctionResultContent; would need extension for any additional tool-lifecycle event kinds we start translating.
docs/design/copilot-sdk-session-events.md
Catalog of Copilot SDK session events that the adapter must be reconciled against.
docs/design/copilot-sdk-tool-events.md
Catalog of Copilot SDK tool-lifecycle events that the adapter must be reconciled against.
Design / Fix
The fix has four parts. The exact enumeration of event kinds that need explicit mapping is intentionally deferred to a follow-up investigation that reconciles the design docs against the live GitHub.Copilot.SDK event set the CLI actually emits; the design docs here list the shape of the fix, not the final list of case arms.
(a) Add explicit case arms for currently-dropped SDK event kinds
Prioritise, at minimum:
Complete assistant-reasoning blocks — the AssistantReasoningDeltaEvent delta is translated, but the completed reasoning block event (e.g. AssistantReasoningEvent or equivalent) is not, so reasoning content that arrives as a single non-delta payload disappears. Map to TextReasoningContent.
Background / async / deferred tool-lifecycle events — anything the SDK emits for a tool call whose completion is deferred (e.g. background PowerShell shells) that is NOT exactly ToolExecutionStartEvent / ToolExecutionCompleteEvent. These almost certainly explain the user-visible symptom that motivated this bug (an agent references a read_powershell result and a running background task, but nothing appears in the transcript). Map to FunctionCallContent / FunctionResultContent via an extended CopilotToolEventMapper.
Session / turn lifecycle events the model or user might want visibility into: e.g. SessionStart / SessionResume, AssistantIntent, AssistantTurnStart, SessionTitleChanged, SessionModelChange, SessionModeChanged, SessionCompactionStart / SessionCompactionComplete, SessionTaskComplete, SessionInfo, SessionWarning, SessionScheduleCreated / SessionScheduleCancelled, SessionPlanChanged, SessionWorkspaceFileChanged, PendingMessagesModified, etc. Most should map to a system/informational TextContent (possibly tagged with a distinct ContentTypePropertyName) so they persist and render; a subset (e.g. compaction) may deserve their own AIContent type. The exact list is subject to the follow-up investigation.
(b) Add a default: arm that never silently drops
Even after (a), the SDK may add new event kinds in future versions. The default: arm must at minimum log the unrecognised event (with kind, agent id, and a truncated payload) at Warning level, and ideally surface it as a generic informational content update tagged with a "unknown-copilot-sdk-event" content type so nothing is invisibly lost. The current behaviour — silent, unlogged drop — must not be reachable.
(c) Mirror the additions in the non-streaming GetResponseAsync path
CopilotSdkChatClient.GetResponseAsync (lines 512–567) has its own tool-event switch (lines 531–547) that is independent of the streaming adapter. Any event kind added in (a) or (b) must be handled there too, or the non-streaming path will continue to drop the same events.
(d) Reconcile design docs against the live SDK event set
docs/design/copilot-sdk-session-events.md and docs/design/copilot-sdk-tool-events.md must be reconciled against the concrete GitHub.Copilot.SDK event types the CLI emits at runtime. The follow-up investigation should update both docs and use them as the checklist that drives (a) and (c).
Relationship to other issues
Related to Toolbar 'Running agents' brain pulsates unconditionally; should only pulsate when an agent is actively working #1305 (toolbar pulsating brain should pulsate only when an agent is actively working) and to the separately-diagnosed gap where a background/async task does not mark the chat as running. The running-state is scoped to the synchronous model-turn loop in AgentChat.cs (running item created ~line 1726, cleared in the finally at ~line 1858 via CompleteRunningItem → runningItemOperations.Remove). A dropped background-tool lifecycle event is plausibly a shared trigger: the same unmapped async-tool event that hides transcript items would also fail to keep the chat "running." This bug is scoped to the dropped-items problem; the running-state issue may be filed separately.
Expected Tests
Add to Phantom.Workspaces.Llm.Core.Tests\CopilotSdkStreamAdapterTests.cs (and, where applicable, non-streaming tests against CopilotSdkChatClient.GetResponseAsync). Naming follows the existing TranslateCopilotSdkSessionEvents_<Scenario>_<Outcome> PascalCase convention already used throughout that file.
A SessionEvent subtype that is not in the current 11 handled cases is written to the channel.
The event is not silently dropped: it is either logged at Warning level and/or emitted as a generic informational ChatResponseUpdate tagged with an "unknown-copilot-sdk-event" content type. Regression guard against the switch losing a default: arm.
A complete (non-delta) assistant-reasoning event is written to the channel.
A single ChatResponseUpdate with role Assistant is emitted whose Contents contains a TextReasoningContent carrying the full reasoning payload, tagged with the originating AgentId.
A background / async / deferred tool-lifecycle event (exact SDK type TBD in follow-up) is written to the channel.
A ChatResponseUpdate is emitted whose Contents contains an appropriate FunctionCallContent or FunctionResultContent (via an extended CopilotToolEventMapper) so the background tool call and its eventual result appear in the transcript.
A SessionEvent subtype that is not in the current 11 handled cases is written to the channel with a non-null AgentId.
The logger receives a Warning-level entry that includes both the runtime event type name and the AgentId, so future SDK additions are diagnosable from logs.
Non-streaming path: the session raises an event kind not handled by the switch at lines 531–547.
The event is logged and/or captured into the response rather than being silently discarded, matching the streaming adapter's default-arm behaviour.
The exact SDK event types referenced by these tests will be finalised as part of the follow-up investigation that reconciles copilot-sdk-session-events.md and copilot-sdk-tool-events.md against the live GitHub.Copilot.SDK event catalog.
Summary
Copilot-SDK-backed chats silently drop any
GitHub.Copilot.SDKsession-event kind that is not one of the ~11 cases handled byCopilotSdkStreamAdapter.TranslateCopilotSdkSessionEvents. The switch statement has nodefault:arm, and the adapter's own doc-comment states that "Unrecognised event types are dropped." BecauseCopilotSdkChatClientimplementsISelfInvokingToolChatClient,AgentFactory.WrapWithMiddlewaredeliberately bypasses bothToolResultSteeringMiddlewareand the frameworkFunctionInvokingChatClient— so the adapter's translation is the only path by which any item can reach the transcript, history, or UI. Any SDK event kind the adapter does not explicitly translate is permanently invisible: it is never persisted, never rendered, and never surfaced to the user. The user-visible symptom is missing tool-call / tool-result / reasoning / background-task items that the model clearly acted on (e.g. an agent references aread_powershellcall and the result of a running background task, yet neither the tool call, its result, nor the background-task lifecycle appear in the transcript).Root Cause
Two mechanisms combine to produce silent, unrecoverable event loss for the Copilot-SDK provider:
1. Self-invoking clients bypass framework middleware
Phantom.Workspaces.Llm.Core\CopilotSdkChatClient.csline 32 — the class implementsISelfInvokingToolChatClient:Phantom.Workspaces.Llm.Core\AgentFactory.cslines 410–424 —WrapWithMiddlewareshort-circuits for self-invoking clients:AgentChat.cscorrespondingly setsUseProvidedChatClientAsIs = truefor self-invoking clients, so the framework does not attach its ownFunctionInvokingChatClientwrapper either.Consequence: the SDK adapter's translator is the sole producer of transcript items for Copilot-SDK-backed chats. Anything it fails to translate never reaches persistence or the UI.
2. The adapter switch has no
default:armPhantom.Workspaces.Llm.Core\CopilotSdkStreamAdapter.cslines 77–221 — the doc-comment at lines 77–84 explicitly states the drop policy:That is the entire set: 11 event kinds handled, everything else silently dropped with no log, no telemetry, no diagnostic. Tool start/complete are mapped via
CopilotToolEventMapper.MapToolStart/MapToolCompleteintoFunctionCallContent/FunctionResultContent, but any other tool-lifecycle event (background/async/deferred completions, cancellations, progress, etc.) has nowhere to go.3. The non-streaming path mirrors the same limited set
Phantom.Workspaces.Llm.Core\CopilotSdkChatClient.cslines 512–567 —GetResponseAsyncsubscribes to the session with aswitchat lines 531–547 that only handlesToolExecutionStartEventandToolExecutionCompleteEvent, so any fix in the streaming adapter must be mirrored here or the non-streaming path will keep losing events:Contrast with non-Copilot providers
Providers whose clients do NOT implement
ISelfInvokingToolChatClient(echo, github-models, ollama, openai, azure-openai) fall through to the second branch ofWrapWithMiddlewareand are wrapped withToolResultSteeringMiddlewareand the frameworkFunctionInvokingChatClient. That framework layer surfaces tool-call / tool-result content as first-classAIContent, which is why those providers reliably show tool items even when the underlying model output is terse. The Copilot-SDK provider has no equivalent safety net — it depends entirely on the adapter's exhaustive translation.Affected Files
Phantom.Workspaces.Llm.Core/CopilotSdkStreamAdapter.csswitch-with-no-default that silently drops unrecognised SDK events.Phantom.Workspaces.Llm.Core/CopilotSdkChatClient.csISelfInvokingToolChatClient(line 32) and duplicates the limited event switch in the non-streamingGetResponseAsyncpath (lines 531–547).Phantom.Workspaces.Llm.Core/AgentFactory.csWrapWithMiddleware(lines 410–424) short-circuits for self-invoking clients, so no framework tool-content middleware compensates for adapter gaps.Phantom.Workspaces.Llm.Core/CopilotToolEventMapper.csToolExecutionStart/Complete→FunctionCallContent/FunctionResultContent; would need extension for any additional tool-lifecycle event kinds we start translating.docs/design/copilot-sdk-session-events.mddocs/design/copilot-sdk-tool-events.mdDesign / Fix
The fix has four parts. The exact enumeration of event kinds that need explicit mapping is intentionally deferred to a follow-up investigation that reconciles the design docs against the live
GitHub.Copilot.SDKevent set the CLI actually emits; the design docs here list the shape of the fix, not the final list of case arms.(a) Add explicit
casearms for currently-dropped SDK event kindsPrioritise, at minimum:
AssistantReasoningDeltaEventdelta is translated, but the completed reasoning block event (e.g.AssistantReasoningEventor equivalent) is not, so reasoning content that arrives as a single non-delta payload disappears. Map toTextReasoningContent.ToolExecutionStartEvent/ToolExecutionCompleteEvent. These almost certainly explain the user-visible symptom that motivated this bug (an agent references aread_powershellresult and a running background task, but nothing appears in the transcript). Map toFunctionCallContent/FunctionResultContentvia an extendedCopilotToolEventMapper.SessionStart/SessionResume,AssistantIntent,AssistantTurnStart,SessionTitleChanged,SessionModelChange,SessionModeChanged,SessionCompactionStart/SessionCompactionComplete,SessionTaskComplete,SessionInfo,SessionWarning,SessionScheduleCreated/SessionScheduleCancelled,SessionPlanChanged,SessionWorkspaceFileChanged,PendingMessagesModified, etc. Most should map to a system/informationalTextContent(possibly tagged with a distinctContentTypePropertyName) so they persist and render; a subset (e.g. compaction) may deserve their ownAIContenttype. The exact list is subject to the follow-up investigation.(b) Add a
default:arm that never silently dropsEven after (a), the SDK may add new event kinds in future versions. The
default:arm must at minimum log the unrecognised event (with kind, agent id, and a truncated payload) at Warning level, and ideally surface it as a generic informational content update tagged with a "unknown-copilot-sdk-event" content type so nothing is invisibly lost. The current behaviour — silent, unlogged drop — must not be reachable.(c) Mirror the additions in the non-streaming
GetResponseAsyncpathCopilotSdkChatClient.GetResponseAsync(lines 512–567) has its own tool-eventswitch(lines 531–547) that is independent of the streaming adapter. Any event kind added in (a) or (b) must be handled there too, or the non-streaming path will continue to drop the same events.(d) Reconcile design docs against the live SDK event set
docs/design/copilot-sdk-session-events.mdanddocs/design/copilot-sdk-tool-events.mdmust be reconciled against the concreteGitHub.Copilot.SDKevent types the CLI emits at runtime. The follow-up investigation should update both docs and use them as the checklist that drives (a) and (c).Relationship to other issues
AgentChat.cs(running item created ~line 1726, cleared in thefinallyat ~line 1858 viaCompleteRunningItem→runningItemOperations.Remove). A dropped background-tool lifecycle event is plausibly a shared trigger: the same unmapped async-tool event that hides transcript items would also fail to keep the chat "running." This bug is scoped to the dropped-items problem; the running-state issue may be filed separately.Expected Tests
Add to
Phantom.Workspaces.Llm.Core.Tests\CopilotSdkStreamAdapterTests.cs(and, where applicable, non-streaming tests againstCopilotSdkChatClient.GetResponseAsync). Naming follows the existingTranslateCopilotSdkSessionEvents_<Scenario>_<Outcome>PascalCase convention already used throughout that file.TranslateCopilotSdkSessionEvents_UnknownEventKind_IsSurfacedNotDroppedSessionEventsubtype that is not in the current 11 handled cases is written to the channel.ChatResponseUpdatetagged with an "unknown-copilot-sdk-event" content type. Regression guard against the switch losing adefault:arm.TranslateCopilotSdkSessionEvents_ReasoningBlockEvent_EmitsReasoningContentChatResponseUpdatewith roleAssistantis emitted whoseContentscontains aTextReasoningContentcarrying the full reasoning payload, tagged with the originatingAgentId.TranslateCopilotSdkSessionEvents_BackgroundToolEvent_EmitsToolItemChatResponseUpdateis emitted whoseContentscontains an appropriateFunctionCallContentorFunctionResultContent(via an extendedCopilotToolEventMapper) so the background tool call and its eventual result appear in the transcript.TranslateCopilotSdkSessionEvents_UnknownEventKind_IsLoggedWithKindAndAgentIdSessionEventsubtype that is not in the current 11 handled cases is written to the channel with a non-nullAgentId.AgentId, so future SDK additions are diagnosable from logs.CopilotSdkChatClient_GetResponseAsync_BackgroundToolEvent_IncludedInResponseChatResponseincludes the correspondingFunctionCallContent/FunctionResultContentitems, mirroring the streaming adapter's behaviour.CopilotSdkChatClient_GetResponseAsync_UnknownEventKind_IsSurfacedNotDroppedThe exact SDK event types referenced by these tests will be finalised as part of the follow-up investigation that reconciles
copilot-sdk-session-events.mdandcopilot-sdk-tool-events.mdagainst the liveGitHub.Copilot.SDKevent catalog.