Description
When GitHubCopilotAgent runs against MCP HTTP tools, the underlying Copilot SDK successfully invokes the tool and returns the result, but the result is wrapped in an opaque AIContent { RawRepresentation = ... } instead of being projected into FunctionResultContent. The LLM receives the tool's output bytes embedded in its conversation context but with no semantic marker that this is the result of its tool call.
Every model tested (gpt-4.1, gpt-5-mini, claude-sonnet-4.6) interprets this as "tool call never completed" and emits a fabricated timeout error in its final response (commonly MCP error -32001: Request timed out, which is not a real protocol code).
The Python equivalent of this bug has been fixed in #4734, #4814, #4828. The .NET equivalent is unfixed in current main (= 1.6.1-preview.260514.1).
Reproduction
- Configure a
GitHubCopilotAgent with one or more MCP HTTP servers exposing read-only tools (e.g. msgraph-admin, sn-query-table).
- Set
SessionConfig.Hooks.OnPreToolUse and OnPostToolUse to increment counters.
- Issue a request that requires a single tool call returning a JSON payload of any size.
- Inspect the response stream's content types and the hook counters.
Expected behavior
OnPostToolUse fires for every successful tool return (matches Python test test_hooks_e2e.py::test that postToolUse hook is invoked after model runs a tool).
RunAsync response stream contains a FunctionResultContent (or equivalent) item per tool call.
- The LLM recognizes the tool result and proceeds with the task.
Actual behavior
OnPreToolUse fires correctly (one fire per tool invocation).
OnPostToolUse fires only for tool returns that have a structured failure body (e.g. {success:false, ...}). It does NOT fire for successful tool returns.
RunAsync response stream contains zero FunctionCallContent and zero FunctionResultContent items — only TextContent. Counted via response.Messages.SelectMany(m => m.Contents).Where(c => c is X).
- The tool's bytes ARE present in the LLM's input on the next turn (input token counts jump from ~6 KB to ~70-110 KB after the call), proving data flow is intact.
- The LLM cannot interpret the bytes as a tool result and fabricates a timeout error in its final response.
Empirical evidence (3 runs, 3 different model families, identical pattern)
Across runs against MCP HTTP tools (msgraph-admin, sn-query-table, pnp-admin, exchange-admin):
| Run |
Executor model |
OnPreToolUse fires |
OnPostToolUse fires |
API server confirmed successes |
| 1 |
gpt-4.1 |
7 |
0 |
5 |
| 2 |
gpt-5-mini |
4 |
0 |
4 |
| 3 |
claude-sonnet-4.6 |
8 |
0 |
6 |
OnPostToolUse fired exactly once across these 19 invocations, and only for pnp-admin's structured-failure response. All 15 successful MCP returns failed to fire OnPostToolUse.
In each case the LLM's final output (persisted via our journey messages table) contained text along the lines of:
"Request to Microsoft Graph timed out while retrieving users" (gpt-4.1)
"MCP error -32001: Request timed out... Retries also timed out" (gpt-5-mini)
"Microsoft Graph API calls are timing out consistently (MCP error -32001). Three successive Graph API calls with progressively smaller \$top values all timed out, indicating a backend connectivity issue rather than a query error" (claude-sonnet-4.6 — note the inferential reasoning about a non-existent timeout)
Root cause (proposed)
In dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs on main, the event-dispatch switch in RunCoreStreamingAsync (around line 167) has no case for tool-execution-complete events. They fall through to default: and are wrapped via ConvertToAgentResponseUpdate(SessionEvent) as new AIContent { RawRepresentation = sessionEvent }.
Specifically:
case AssistantMessageDeltaEvent: → TextContent
case AssistantMessageEvent: → AIContent { RawRepresentation }
case AssistantUsageEvent: → UsageContent
case SessionIdleEvent / SessionErrorEvent: → handled
default: (includes tool execution events) → opaque AIContent
There is no projection of tool execution events into FunctionCallContent or FunctionResultContent.
Related: #4633 documents the broader "AIContent { RawRepresentation } serializes as empty objects" issue for the same code paths. #4732 is also relevant — CopySessionConfig() hardcodes Streaming = true, forcing every consumer through the streaming path that has this bug.
Proposed fix
Add explicit cases in the event-dispatch switch for tool execution events, projecting them into FunctionCallContent (on ToolExecutionStartEvent / equivalent) and FunctionResultContent (on ToolExecutionCompleteEvent / equivalent). Mirror the Python fixes in #4734, #4814, #4828.
Package versions
Microsoft.Agents.AI.GitHub.Copilot 1.3.0-preview.260423.1
GitHub.Copilot.SDK 0.2.2
Microsoft.Agents.AI.Workflows 1.3.0
- .NET 10.0
- Bug also confirmed in current
main (= 1.6.1-preview.260514.1) via source inspection of GitHubCopilotAgent.cs.
Description
When
GitHubCopilotAgentruns against MCP HTTP tools, the underlying Copilot SDK successfully invokes the tool and returns the result, but the result is wrapped in an opaqueAIContent { RawRepresentation = ... }instead of being projected intoFunctionResultContent. The LLM receives the tool's output bytes embedded in its conversation context but with no semantic marker that this is the result of its tool call.Every model tested (gpt-4.1, gpt-5-mini, claude-sonnet-4.6) interprets this as "tool call never completed" and emits a fabricated timeout error in its final response (commonly
MCP error -32001: Request timed out, which is not a real protocol code).The Python equivalent of this bug has been fixed in #4734, #4814, #4828. The .NET equivalent is unfixed in current
main(=1.6.1-preview.260514.1).Reproduction
GitHubCopilotAgentwith one or more MCP HTTP servers exposing read-only tools (e.g.msgraph-admin,sn-query-table).SessionConfig.Hooks.OnPreToolUseandOnPostToolUseto increment counters.Expected behavior
OnPostToolUsefires for every successful tool return (matches Python testtest_hooks_e2e.py::test that postToolUse hook is invoked after model runs a tool).RunAsyncresponse stream contains aFunctionResultContent(or equivalent) item per tool call.Actual behavior
OnPreToolUsefires correctly (one fire per tool invocation).OnPostToolUsefires only for tool returns that have a structured failure body (e.g.{success:false, ...}). It does NOT fire for successful tool returns.RunAsyncresponse stream contains zeroFunctionCallContentand zeroFunctionResultContentitems — onlyTextContent. Counted viaresponse.Messages.SelectMany(m => m.Contents).Where(c => c is X).Empirical evidence (3 runs, 3 different model families, identical pattern)
Across runs against MCP HTTP tools (
msgraph-admin,sn-query-table,pnp-admin,exchange-admin):OnPostToolUse fired exactly once across these 19 invocations, and only for
pnp-admin's structured-failure response. All 15 successful MCP returns failed to fire OnPostToolUse.In each case the LLM's final output (persisted via our journey messages table) contained text along the lines of:
"Request to Microsoft Graph timed out while retrieving users"(gpt-4.1)"MCP error -32001: Request timed out... Retries also timed out"(gpt-5-mini)"Microsoft Graph API calls are timing out consistently (MCP error -32001). Three successive Graph API calls with progressively smaller \$top values all timed out, indicating a backend connectivity issue rather than a query error"(claude-sonnet-4.6 — note the inferential reasoning about a non-existent timeout)Root cause (proposed)
In
dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cson main, the event-dispatch switch inRunCoreStreamingAsync(around line 167) has no case for tool-execution-complete events. They fall through todefault:and are wrapped viaConvertToAgentResponseUpdate(SessionEvent)asnew AIContent { RawRepresentation = sessionEvent }.Specifically:
case AssistantMessageDeltaEvent:→TextContentcase AssistantMessageEvent:→AIContent { RawRepresentation }case AssistantUsageEvent:→UsageContentcase SessionIdleEvent / SessionErrorEvent:→ handleddefault:(includes tool execution events) → opaqueAIContentThere is no projection of tool execution events into
FunctionCallContentorFunctionResultContent.Related: #4633 documents the broader "AIContent { RawRepresentation } serializes as empty objects" issue for the same code paths. #4732 is also relevant —
CopySessionConfig()hardcodesStreaming = true, forcing every consumer through the streaming path that has this bug.Proposed fix
Add explicit cases in the event-dispatch switch for tool execution events, projecting them into
FunctionCallContent(onToolExecutionStartEvent/ equivalent) andFunctionResultContent(onToolExecutionCompleteEvent/ equivalent). Mirror the Python fixes in #4734, #4814, #4828.Package versions
Microsoft.Agents.AI.GitHub.Copilot 1.3.0-preview.260423.1GitHub.Copilot.SDK 0.2.2Microsoft.Agents.AI.Workflows 1.3.0main(=1.6.1-preview.260514.1) via source inspection ofGitHubCopilotAgent.cs.