Copilot SDK sub-agents fail with Requested function "task" not found — hosted sub-agent chat pipeline installs tool-running middleware that the Copilot SDK should own
Summary
When a Copilot-SDK-backed parent agent invokes the task tool to spawn a sub-agent, the child sub-agent's transcript renders:
Error: Requested function "task" not found.
The Copilot SDK owns tool running end-to-end for Copilot chats (the CLI drives the agentic loop and executes tools itself). Our parent Copilot SDK chat client (CopilotSdkChatClient) advertises this by implementing the marker ISelfInvokingToolChatClient, which causes the agent framework to use it as-is and skip its FunctionInvokingChatClient tool-running middleware. The hosted sub-agent chat client (CopilotSubAgentChatClient) does not implement or expose this marker. As a result, the agent framework wraps every hosted sub-agent chat with FunctionInvokingChatClient middleware — a tool-running layer that the Copilot SDK should own but the sub-agent stub does not opt out of. When CopilotSubAgentRouter.InjectToolCallPrompt pushes the parent's FunctionCallContent(Name="task", …) into the child's channel, that content flows through FunctionInvokingChatClient, which attempts to resolve task against the child's (empty) ChatOptions.Tools and throws Requested function "task" not found. The fix is to stop installing tool-running middleware on Copilot subagent SDK chats — mark CopilotSubAgentChatClient as ISelfInvokingToolChatClient so the agent framework uses it as-is, exactly as it already does for the parent CopilotSdkChatClient.
Root Cause
1. The error string is emitted by the SDK / MEAI, not by our code
A recursive text search of features\ for Requested function and not found yields no matches in Phantom.Workspaces.*. The string is emitted by the FunctionInvokingChatClient middleware in Microsoft.Extensions.AI 10.5.2 when it encounters a streamed FunctionCallContent whose Name does not resolve against the current ChatOptions.Tools.
SDK versions (from Directory.Packages.props:30,33):
Microsoft.Extensions.AI 10.5.2
GitHub.Copilot.SDK 1.0.0-beta.2
2. The agent framework wraps chat clients with FunctionInvokingChatClient unless they opt out via ISelfInvokingToolChatClient
Phantom.Workspaces.Llm.Core/ISelfInvokingToolChatClient.cs defines the opt-out marker:
// Marker for IChatClient implementations that invoke their own tools (for example the
// GitHub Copilot SDK, where the copilot CLI drives the agentic loop and executes tools itself).
//
// Such clients must be used by the agent framework as-is, without the framework adding its
// function-invoking middleware …
public interface ISelfInvokingToolChatClient { }
AgentChat.ResolveUseProvidedChatClientAsIs (AgentChat.cs:2106-2112) is the decision point:
internal static bool ResolveUseProvidedChatClientAsIs(bool hasClientOverride, IChatClient resolvedClient)
{
ArgumentNullException.ThrowIfNull(resolvedClient);
return hasClientOverride
|| resolvedClient is ISelfInvokingToolChatClient
|| resolvedClient.GetService(typeof(ISelfInvokingToolChatClient)) is not null;
}
Its result is fed into ChatClientAgentOptions.UseProvidedChatClientAsIs at AgentChat.cs:246-306:
var useProvidedChatClientAsIs = this.request.OverrideUseProvidedChatClientAsIs
?? ResolveUseProvidedChatClientAsIs(
this.request.ClientOverride is not null,
resolvedClient);
…
this.chatOptions = new ChatClientAgentOptions
{
ChatOptions = new ChatOptions(),
ChatHistoryProvider = this.chatHistoryProvider,
UseProvidedChatClientAsIs = useProvidedChatClientAsIs,
RequirePerServiceCallChatHistoryPersistence = !useProvidedChatClientAsIs,
};
…
this.chatClientAgent = new ChatClientAgent(streamingMiddleware, this.chatOptions);
ChatClientAgent (Microsoft.Extensions.AI.Agents) treats UseProvidedChatClientAsIs = false as an instruction to add its own tool-running middleware (FunctionInvokingChatClient) around the resolved client. UseProvidedChatClientAsIs = true skips that wrapping and passes the client through as-is.
3. CopilotSdkChatClient (parent) opts out — CopilotSubAgentChatClient (child) does not
The parent Copilot SDK client opts out:
// CopilotSdkChatClient.cs:32
public sealed class CopilotSdkChatClient : IChatClient, IAsyncDisposable,
ISelfInvokingToolChatClient, SlashCommands.IModelSlashCommandClient
The hosted sub-agent client does not:
// CopilotSubAgentChatClient.cs:16
internal sealed class CopilotSubAgentChatClient
: IChatClient, ICopilotSubAgentReceiver, IHostedAgentChatClient
{
…
// CopilotSubAgentChatClient.cs:57-58 — only exposes the sub-agent receiver marker
public object? GetService(Type serviceType, object? key = null) =>
serviceType == typeof(ICopilotSubAgentReceiver) ? this : null;
}
The sub-agent chat client is constructed in AgentFactory.cs:226-229 without any wrapping:
if (provider == "github-copilot-subagent")
{
return new ChatClientResult(new CopilotSubAgentChatClient(), "GitHub Copilot Sub-Agent");
}
Because the returned client is not ISelfInvokingToolChatClient and returns null for that service, ResolveUseProvidedChatClientAsIs returns false for every hosted sub-agent AgentChat, and ChatClientAgent therefore installs FunctionInvokingChatClient around it. That is the tool-running middleware the owner is calling out: the Copilot SDK does tool running; this middleware must not be present for Copilot subagent SDK chats.
4. Causal chain from injection to throw
The routing pipeline for a task invocation is (Phantom.Workspaces.Llm.Core/CopilotSubAgentRouter.cs):
- Parent turn emits
FunctionCallContent(CallId="call_task_1", Name="task", …) on the root stream.
BufferRootToolStart (CopilotSubAgentRouter.cs:172-195) stores it in bufferedToolStarts[…].
- The SDK emits
SubagentStartedEvent for the child, carrying ParentToolCallId="call_task_1".
HandleSubAgentStartedAsync (CopilotSubAgentRouter.cs:197-277) creates the child sink and calls InjectToolCallPrompt(entry, buffered) (L275).
InjectToolCallPrompt (CopilotSubAgentRouter.cs:466-472) pushes the parent's FunctionCallContent into the child sink as a ChatRole.User update:
private static void InjectToolCallPrompt(ChildRoutingEntry entry, FunctionCallContent toolStart)
{
entry.Push(new ChatResponseUpdate
{
Role = ChatRole.User,
Contents = [toolStart],
});
}
Because the hosted sub-agent's chat pipeline includes FunctionInvokingChatClient (step 3 above), that streamed FunctionCallContent(Name="task") is intercepted by the middleware, which performs a name-based lookup against the child's ChatOptions.Tools. That collection is empty for hosted sub-agents (AgentFactory.cs:226-229 constructs the stub without tools, matching the SubAgentDefinition at CopilotSubAgentRouter.cs:29-31), so the lookup fails and the middleware throws Requested function "task" not found. The error surfaces in the child's transcript because it is generated inside the child AgentChat's ChatClientAgent invocation.
The middleware — not the injection per se — is the defect: with the middleware absent (as it correctly is for the parent CopilotSdkChatClient), the injected FunctionCallContent would flow through the receive-only CopilotSubAgentChatClient channel and be persisted to the child's history/transcript without any function-registry lookup or throw.
Affected Files
| File |
Role in bug / what changes |
Phantom.Workspaces.Llm.Core/CopilotSubAgentChatClient.cs (L16, L57-58) |
Hosted sub-agent chat client stub. Change: implement ISelfInvokingToolChatClient (add the marker interface to the class declaration and optionally return this from GetService(typeof(ISelfInvokingToolChatClient))) so the agent framework recognises it as self-invoking and skips FunctionInvokingChatClient wrapping. |
Phantom.Workspaces.Llm.Core/AgentChat.cs (L246-306, L2098-2112) |
ResolveUseProvidedChatClientAsIs + ChatClientAgentOptions.UseProvidedChatClientAsIs — the decision point that, once CopilotSubAgentChatClient is marked, will return true and cause ChatClientAgent to omit FunctionInvokingChatClient. No source change required here, but tests must assert the new outcome. |
Phantom.Workspaces.Llm.Core/AgentFactory.cs (L226-229) |
github-copilot-subagent provider constructs the hosted stub; no wrapping change required. The stub's opt-out marker (above) is sufficient — the framework, not this factory, is what installs the middleware. |
Phantom.Workspaces.Llm.Core/CopilotSubAgentRouter.cs (L172-195, L197-277, L466-472) |
BufferRootToolStart / HandleSubAgentStartedAsync / InjectToolCallPrompt push the parent's FunctionCallContent into the child sink. With middleware removed the injection no longer throws; no change required (see "Injection is now moot" below). |
Phantom.Workspaces.Llm.Core/ISelfInvokingToolChatClient.cs |
Marker consumed by ResolveUseProvidedChatClientAsIs; no change. |
Phantom.Workspaces.Llm.Core/CopilotSdkChatClient.cs (L32) |
Parent Copilot SDK client already implements ISelfInvokingToolChatClient — the pattern being extended to the sub-agent stub. |
Directory.Packages.props (L30, L33) |
Pins Microsoft.Extensions.AI 10.5.2 (source of FunctionInvokingChatClient and its "Requested function … not found" throw) and GitHub.Copilot.SDK 1.0.0-beta.2. |
Design / Fix
Chosen fix — mark CopilotSubAgentChatClient as ISelfInvokingToolChatClient so the agent framework does not wrap it with tool-running middleware
The GitHub Copilot SDK owns tool running for Copilot chats. The Microsoft.Extensions.AI.Agents framework's ChatClientAgent adds FunctionInvokingChatClient (tool-running middleware) to the pipeline whenever ChatClientAgentOptions.UseProvidedChatClientAsIs is false. Our parent CopilotSdkChatClient opts out of that middleware by implementing ISelfInvokingToolChatClient; the hosted sub-agent CopilotSubAgentChatClient currently does not, so it silently receives the middleware even though the Copilot SDK — driving the parent — is the actual tool runner for the sub-agent's stream.
Change the sub-agent chat client to advertise the same opt-out:
// CopilotSubAgentChatClient.cs
internal sealed class CopilotSubAgentChatClient
: IChatClient,
ICopilotSubAgentReceiver,
IHostedAgentChatClient,
ISelfInvokingToolChatClient // NEW — the Copilot SDK owns tool running for this stream
{
…
public object? GetService(Type serviceType, object? key = null)
{
if (serviceType == typeof(ICopilotSubAgentReceiver)) return this;
if (serviceType == typeof(ISelfInvokingToolChatClient)) return this;
return null;
}
}
Concretely, the pipeline changes for hosted sub-agents from:
before: ChatClientAgent → FunctionInvokingChatClient → StreamingPersistenceMiddleware → CopilotSubAgentChatClient
^^^^^^^^^^^^^^^^^^^^^^^^^^
throws Requested function "task" not found on the injected FunctionCallContent
to:
after: ChatClientAgent → StreamingPersistenceMiddleware → CopilotSubAgentChatClient
(UseProvidedChatClientAsIs = true; no tool-running middleware)
which matches the existing Copilot SDK parent pipeline (also UseProvidedChatClientAsIs = true).
InjectToolCallPrompt's FunctionCallContent injection is now moot for this bug
With FunctionInvokingChatClient removed from the child pipeline, the parent-sourced FunctionCallContent(Name="task", …) pushed into the child sink by CopilotSubAgentRouter.InjectToolCallPrompt (CopilotSubAgentRouter.cs:466-472) is never subjected to a function-registry lookup: the child's chat client (CopilotSubAgentChatClient) is a receive-only stub that only forwards updates to persistence/display, and no other component in the child's pipeline resolves function names against ChatOptions.Tools. The injection therefore no longer causes the Requested function "task" not found error and does not need to be rewritten as TextContent. It is preserved as-is so the child transcript continues to record the spawning tool call attribution for downstream consumers, matching current behaviour on the parent side.
Considered / Background — superseded
Considered — propagate the parent's tool set into the child's ChatOptions.Tools (original diagnosis). The idea was that the child's SDK session had no task registered, so populate its tool set with a trust-profile-filtered copy of the parent's tools. Superseded because the child sub-agent's model never emitted a task call — that FunctionCallContent originates on the parent — and because the Copilot SDK, not our framework, owns tool running. Populating the child's tools would only mask the middleware defect and would misleadingly attribute the parent's task execution to the child. Nested sub-agent spawning (the concern this option addresses) is a separate feature that should be tracked with an explicit trust-profile design (see docs/design/subagent-toolset.md).
Considered — convert the injected FunctionCallContent into TextContent in CopilotSubAgentRouter.InjectToolCallPrompt (previous chosen fix). The idea was to synthesise a plain user-text turn describing the spawn context so no FunctionCallContent referencing an unregistered function would enter the child's history. Superseded because it fixes the symptom at the wrong layer: the injection is legitimate (parent-tool-call attribution belongs in the child transcript); the actual defect is that the child's chat pipeline includes tool-running middleware the Copilot SDK should own. Once that middleware is removed (chosen fix above), the raw FunctionCallContent flows through the receive-only stub without any lookup or throw, and the injection can stay in its current form. This option is also rejected because rewriting the content type would degrade downstream consumers that rely on structured tool-call attribution in the child transcript.
Considered — strip unknown-function FunctionCallContent inside CopilotSubAgentChatClient before it reaches the middleware. A downstream filter that drops any FunctionCallContent whose Name is not in ChatOptions.Tools. Superseded because it hides a real semantic error (tool-running middleware is running where it should not be) and could accidentally drop legitimate content in future scenarios; the correct call-site is the middleware selection, not a downstream stripper.
Expected Tests
Follow the existing Subject_Scenario_ExpectedOutcome convention (e.g. AgentChatHostedSubAgentTests.AgentChat_HostedSubAgent_ProcessLoop_ConsumesChannelData_WithoutUserInput at Phantom.Workspaces.Llm.Core.Tests/AgentChatHostedSubAgentTests.cs:125, AgentChatTests.ResolveUseProvidedChatClientAsIs_TrueForOverride_SelfInvoking_OrServiceDiscovered at AgentChatTests.cs:995).
| Test Name |
Class |
What It Verifies |
CopilotSubAgentChatClient_ImplementsISelfInvokingToolChatClient |
AgentChatTests |
typeof(ISelfInvokingToolChatClient).IsAssignableFrom(typeof(CopilotSubAgentChatClient)) — mirrors the existing CopilotSdkChatClient_IsSelfInvokingToolChatClient assertion at AgentChatTests.cs:1012. |
CopilotSubAgentChatClient_GetService_ReturnsSelfInvokingToolChatClientMarker |
AgentChatTests |
new CopilotSubAgentChatClient().GetService(typeof(ISelfInvokingToolChatClient)) is non-null so ResolveUseProvidedChatClientAsIs selects the as-is path even through DelegatingChatClient.GetService propagation. |
ResolveUseProvidedChatClientAsIs_HostedSubAgentClient_ReturnsTrue |
AgentChatTests |
ResolveUseProvidedChatClientAsIs(hasClientOverride: false, new CopilotSubAgentChatClient()) returns true, i.e. the agent framework is instructed to use the sub-agent stub as-is (no FunctionInvokingChatClient wrapping). |
AgentChat_HostedSubAgent_PipelineDoesNotWrapWithFunctionInvokingChatClient |
AgentChatHostedSubAgentTests |
Constructs a hosted sub-agent AgentChat and asserts that chatOptions.UseProvidedChatClientAsIs is true for the resolved CopilotSubAgentChatClient, so ChatClientAgent will not install a tool-running middleware layer around it. |
AgentChat_HostedSubAgent_ParentTaskFunctionCallInjected_ChildTranscriptDoesNotThrowFunctionNotFound |
AgentChatHostedSubAgentTests |
End-to-end: parent turn buffers a FunctionCallContent(Name="task"), router injects it into the hosted sub-agent sink; the child AgentChat completes with no Requested function "task" not found error surfaced in its history or completion state. |
AgentChat_HostedSubAgent_ParentTaskInvocation_TaskToolCallAttributedToParentTranscriptOnly |
AgentChatHostedSubAgentTests |
The parent's task FunctionCallContent / FunctionResultContent remain attributed to the parent transcript; the injected copy in the child sink is preserved in the child history but never triggers a function-registry lookup. |
Related
This bug is a middleware-selection defect for hosted Copilot subagent chats and does not overlap with trust-profile-propagation work those related issues might imply.
Copilot SDK sub-agents fail with
Requested function "task" not found— hosted sub-agent chat pipeline installs tool-running middleware that the Copilot SDK should ownSummary
When a Copilot-SDK-backed parent agent invokes the
tasktool to spawn a sub-agent, the child sub-agent's transcript renders:The Copilot SDK owns tool running end-to-end for Copilot chats (the CLI drives the agentic loop and executes tools itself). Our parent Copilot SDK chat client (
CopilotSdkChatClient) advertises this by implementing the markerISelfInvokingToolChatClient, which causes the agent framework to use it as-is and skip itsFunctionInvokingChatClienttool-running middleware. The hosted sub-agent chat client (CopilotSubAgentChatClient) does not implement or expose this marker. As a result, the agent framework wraps every hosted sub-agent chat withFunctionInvokingChatClientmiddleware — a tool-running layer that the Copilot SDK should own but the sub-agent stub does not opt out of. WhenCopilotSubAgentRouter.InjectToolCallPromptpushes the parent'sFunctionCallContent(Name="task", …)into the child's channel, that content flows throughFunctionInvokingChatClient, which attempts to resolvetaskagainst the child's (empty)ChatOptions.Toolsand throwsRequested function "task" not found. The fix is to stop installing tool-running middleware on Copilot subagent SDK chats — markCopilotSubAgentChatClientasISelfInvokingToolChatClientso the agent framework uses it as-is, exactly as it already does for the parentCopilotSdkChatClient.Root Cause
1. The error string is emitted by the SDK / MEAI, not by our code
A recursive text search of
features\forRequested functionandnot foundyields no matches inPhantom.Workspaces.*. The string is emitted by theFunctionInvokingChatClientmiddleware inMicrosoft.Extensions.AI 10.5.2when it encounters a streamedFunctionCallContentwhoseNamedoes not resolve against the currentChatOptions.Tools.SDK versions (from
Directory.Packages.props:30,33):Microsoft.Extensions.AI10.5.2GitHub.Copilot.SDK1.0.0-beta.22. The agent framework wraps chat clients with
FunctionInvokingChatClientunless they opt out viaISelfInvokingToolChatClientPhantom.Workspaces.Llm.Core/ISelfInvokingToolChatClient.csdefines the opt-out marker:AgentChat.ResolveUseProvidedChatClientAsIs(AgentChat.cs:2106-2112) is the decision point:Its result is fed into
ChatClientAgentOptions.UseProvidedChatClientAsIsatAgentChat.cs:246-306:ChatClientAgent(Microsoft.Extensions.AI.Agents) treatsUseProvidedChatClientAsIs = falseas an instruction to add its own tool-running middleware (FunctionInvokingChatClient) around the resolved client.UseProvidedChatClientAsIs = trueskips that wrapping and passes the client through as-is.3.
CopilotSdkChatClient(parent) opts out —CopilotSubAgentChatClient(child) does notThe parent Copilot SDK client opts out:
The hosted sub-agent client does not:
The sub-agent chat client is constructed in
AgentFactory.cs:226-229without any wrapping:Because the returned client is not
ISelfInvokingToolChatClientand returnsnullfor that service,ResolveUseProvidedChatClientAsIsreturnsfalsefor every hosted sub-agent AgentChat, andChatClientAgenttherefore installsFunctionInvokingChatClientaround it. That is the tool-running middleware the owner is calling out: the Copilot SDK does tool running; this middleware must not be present for Copilot subagent SDK chats.4. Causal chain from injection to throw
The routing pipeline for a
taskinvocation is (Phantom.Workspaces.Llm.Core/CopilotSubAgentRouter.cs):FunctionCallContent(CallId="call_task_1", Name="task", …)on the root stream.BufferRootToolStart(CopilotSubAgentRouter.cs:172-195) stores it inbufferedToolStarts[…].SubagentStartedEventfor the child, carryingParentToolCallId="call_task_1".HandleSubAgentStartedAsync(CopilotSubAgentRouter.cs:197-277) creates the child sink and callsInjectToolCallPrompt(entry, buffered)(L275).InjectToolCallPrompt(CopilotSubAgentRouter.cs:466-472) pushes the parent'sFunctionCallContentinto the child sink as aChatRole.Userupdate:Because the hosted sub-agent's chat pipeline includes
FunctionInvokingChatClient(step 3 above), that streamedFunctionCallContent(Name="task")is intercepted by the middleware, which performs a name-based lookup against the child'sChatOptions.Tools. That collection is empty for hosted sub-agents (AgentFactory.cs:226-229constructs the stub without tools, matching theSubAgentDefinitionatCopilotSubAgentRouter.cs:29-31), so the lookup fails and the middleware throwsRequested function "task" not found. The error surfaces in the child's transcript because it is generated inside the child AgentChat'sChatClientAgentinvocation.The middleware — not the injection per se — is the defect: with the middleware absent (as it correctly is for the parent
CopilotSdkChatClient), the injectedFunctionCallContentwould flow through the receive-onlyCopilotSubAgentChatClientchannel and be persisted to the child's history/transcript without any function-registry lookup or throw.Affected Files
Phantom.Workspaces.Llm.Core/CopilotSubAgentChatClient.cs(L16, L57-58)ISelfInvokingToolChatClient(add the marker interface to the class declaration and optionally returnthisfromGetService(typeof(ISelfInvokingToolChatClient))) so the agent framework recognises it as self-invoking and skipsFunctionInvokingChatClientwrapping.Phantom.Workspaces.Llm.Core/AgentChat.cs(L246-306, L2098-2112)ResolveUseProvidedChatClientAsIs+ChatClientAgentOptions.UseProvidedChatClientAsIs— the decision point that, onceCopilotSubAgentChatClientis marked, will returntrueand causeChatClientAgentto omitFunctionInvokingChatClient. No source change required here, but tests must assert the new outcome.Phantom.Workspaces.Llm.Core/AgentFactory.cs(L226-229)github-copilot-subagentprovider constructs the hosted stub; no wrapping change required. The stub's opt-out marker (above) is sufficient — the framework, not this factory, is what installs the middleware.Phantom.Workspaces.Llm.Core/CopilotSubAgentRouter.cs(L172-195, L197-277, L466-472)BufferRootToolStart/HandleSubAgentStartedAsync/InjectToolCallPromptpush the parent'sFunctionCallContentinto the child sink. With middleware removed the injection no longer throws; no change required (see "Injection is now moot" below).Phantom.Workspaces.Llm.Core/ISelfInvokingToolChatClient.csResolveUseProvidedChatClientAsIs; no change.Phantom.Workspaces.Llm.Core/CopilotSdkChatClient.cs(L32)ISelfInvokingToolChatClient— the pattern being extended to the sub-agent stub.Directory.Packages.props(L30, L33)Microsoft.Extensions.AI 10.5.2(source ofFunctionInvokingChatClientand its "Requested function … not found" throw) andGitHub.Copilot.SDK 1.0.0-beta.2.Design / Fix
Chosen fix — mark
CopilotSubAgentChatClientasISelfInvokingToolChatClientso the agent framework does not wrap it with tool-running middlewareThe GitHub Copilot SDK owns tool running for Copilot chats. The
Microsoft.Extensions.AI.Agentsframework'sChatClientAgentaddsFunctionInvokingChatClient(tool-running middleware) to the pipeline wheneverChatClientAgentOptions.UseProvidedChatClientAsIsisfalse. Our parentCopilotSdkChatClientopts out of that middleware by implementingISelfInvokingToolChatClient; the hosted sub-agentCopilotSubAgentChatClientcurrently does not, so it silently receives the middleware even though the Copilot SDK — driving the parent — is the actual tool runner for the sub-agent's stream.Change the sub-agent chat client to advertise the same opt-out:
Concretely, the pipeline changes for hosted sub-agents from:
to:
which matches the existing Copilot SDK parent pipeline (also
UseProvidedChatClientAsIs = true).InjectToolCallPrompt'sFunctionCallContentinjection is now moot for this bugWith
FunctionInvokingChatClientremoved from the child pipeline, the parent-sourcedFunctionCallContent(Name="task", …)pushed into the child sink byCopilotSubAgentRouter.InjectToolCallPrompt(CopilotSubAgentRouter.cs:466-472) is never subjected to a function-registry lookup: the child's chat client (CopilotSubAgentChatClient) is a receive-only stub that only forwards updates to persistence/display, and no other component in the child's pipeline resolves function names againstChatOptions.Tools. The injection therefore no longer causes theRequested function "task" not founderror and does not need to be rewritten asTextContent. It is preserved as-is so the child transcript continues to record the spawning tool call attribution for downstream consumers, matching current behaviour on the parent side.Considered / Background — superseded
Considered — propagate the parent's tool set into the child's
ChatOptions.Tools(original diagnosis). The idea was that the child's SDK session had notaskregistered, so populate its tool set with a trust-profile-filtered copy of the parent's tools. Superseded because the child sub-agent's model never emitted ataskcall — thatFunctionCallContentoriginates on the parent — and because the Copilot SDK, not our framework, owns tool running. Populating the child's tools would only mask the middleware defect and would misleadingly attribute the parent'staskexecution to the child. Nested sub-agent spawning (the concern this option addresses) is a separate feature that should be tracked with an explicit trust-profile design (seedocs/design/subagent-toolset.md).Considered — convert the injected
FunctionCallContentintoTextContentinCopilotSubAgentRouter.InjectToolCallPrompt(previous chosen fix). The idea was to synthesise a plain user-text turn describing the spawn context so noFunctionCallContentreferencing an unregistered function would enter the child's history. Superseded because it fixes the symptom at the wrong layer: the injection is legitimate (parent-tool-call attribution belongs in the child transcript); the actual defect is that the child's chat pipeline includes tool-running middleware the Copilot SDK should own. Once that middleware is removed (chosen fix above), the rawFunctionCallContentflows through the receive-only stub without any lookup or throw, and the injection can stay in its current form. This option is also rejected because rewriting the content type would degrade downstream consumers that rely on structured tool-call attribution in the child transcript.Considered — strip unknown-function
FunctionCallContentinsideCopilotSubAgentChatClientbefore it reaches the middleware. A downstream filter that drops anyFunctionCallContentwhoseNameis not inChatOptions.Tools. Superseded because it hides a real semantic error (tool-running middleware is running where it should not be) and could accidentally drop legitimate content in future scenarios; the correct call-site is the middleware selection, not a downstream stripper.Expected Tests
Follow the existing
Subject_Scenario_ExpectedOutcomeconvention (e.g.AgentChatHostedSubAgentTests.AgentChat_HostedSubAgent_ProcessLoop_ConsumesChannelData_WithoutUserInputatPhantom.Workspaces.Llm.Core.Tests/AgentChatHostedSubAgentTests.cs:125,AgentChatTests.ResolveUseProvidedChatClientAsIs_TrueForOverride_SelfInvoking_OrServiceDiscoveredatAgentChatTests.cs:995).CopilotSubAgentChatClient_ImplementsISelfInvokingToolChatClientAgentChatTeststypeof(ISelfInvokingToolChatClient).IsAssignableFrom(typeof(CopilotSubAgentChatClient))— mirrors the existingCopilotSdkChatClient_IsSelfInvokingToolChatClientassertion atAgentChatTests.cs:1012.CopilotSubAgentChatClient_GetService_ReturnsSelfInvokingToolChatClientMarkerAgentChatTestsnew CopilotSubAgentChatClient().GetService(typeof(ISelfInvokingToolChatClient))is non-null soResolveUseProvidedChatClientAsIsselects the as-is path even throughDelegatingChatClient.GetServicepropagation.ResolveUseProvidedChatClientAsIs_HostedSubAgentClient_ReturnsTrueAgentChatTestsResolveUseProvidedChatClientAsIs(hasClientOverride: false, new CopilotSubAgentChatClient())returnstrue, i.e. the agent framework is instructed to use the sub-agent stub as-is (noFunctionInvokingChatClientwrapping).AgentChat_HostedSubAgent_PipelineDoesNotWrapWithFunctionInvokingChatClientAgentChatHostedSubAgentTestsAgentChatand asserts thatchatOptions.UseProvidedChatClientAsIsistruefor the resolvedCopilotSubAgentChatClient, soChatClientAgentwill not install a tool-running middleware layer around it.AgentChat_HostedSubAgent_ParentTaskFunctionCallInjected_ChildTranscriptDoesNotThrowFunctionNotFoundAgentChatHostedSubAgentTestsFunctionCallContent(Name="task"), router injects it into the hosted sub-agent sink; the child AgentChat completes with noRequested function "task" not founderror surfaced in its history or completion state.AgentChat_HostedSubAgent_ParentTaskInvocation_TaskToolCallAttributedToParentTranscriptOnlyAgentChatHostedSubAgentTeststaskFunctionCallContent/FunctionResultContentremain attributed to the parent transcript; the injected copy in the child sink is preserved in the child history but never triggers a function-registry lookup.Related
HandleSubAgentStartedAsync).This bug is a middleware-selection defect for hosted Copilot subagent chats and does not overlap with trust-profile-propagation work those related issues might imply.