Summary
The agentId field is present in the JSON wire format (session-events.schema.json) and exposed on every event variant in the Node.js SDK, but it is not generated in the .NET, Go, or Python SDKs. This makes sub-agent correlation significantly harder in those languages.
Current Behavior
- Node.js: Every event variant includes
agentId?: string — works correctly
- .NET:
SessionEvent base class has Id, Timestamp, ParentId, Ephemeral, Type — no AgentId
- Go:
SessionEvent struct has ID, Timestamp, ParentID, Ephemeral, Type, Data — no AgentID
- Python:
SessionEvent dataclass has id, timestamp, parent_id, ephemeral, type — no agent_id
Additionally, the .NET SessionEvent has no [JsonExtensionData] attribute, so agentId from the wire format is silently dropped during deserialization with no way to access it.
Root Cause
The code generators for .NET (scripts/codegen/csharp.ts), Go (scripts/codegen/go.ts), and Python (scripts/codegen/python.ts) extract a shared base type for the event envelope but omit agentId from it. The Node.js generator uses a flat union type that repeats all envelope fields per variant, which naturally includes agentId.
Impact
Without agentId, .NET/Go/Python consumers must rely on:
SubagentStartedEvent.ToolCallId → SubagentCompletedEvent.ToolCallId lifecycle correlation
- Deprecated
ParentToolCallId on AssistantMessageDeltaData, ToolExecutionStartData, etc.
The ParentToolCallId field is marked [Obsolete] — when it's removed, there will be no way to identify which sub-agent emitted a streaming delta or tool execution event in .NET/Go/Python.
Code Samples
Node.js — Works Today
In Node.js, agentId is available on every event, making sub-agent attribution straightforward:
import { CopilotClient } from "@anthropic-ai/copilot-sdk";
const client = new CopilotClient();
await client.start();
const session = await client.createSession({
model: "claude-opus-4.6",
customAgents: [
{ name: "researcher", displayName: "Research Agent", instructions: "..." },
{ name: "writer", displayName: "Writer Agent", instructions: "..." },
],
});
session.on((event) => {
// agentId is directly available on the event envelope
if (event.agentId) {
console.log(`[${event.agentId}] ${event.type}`);
}
switch (event.type) {
case "assistant.message_delta":
// Attribute streaming content to the correct sub-agent
console.log(`Agent "${event.agentId}" says: ${event.data.deltaContent}`);
break;
case "tool.execution_start":
// Know which sub-agent initiated the tool call
console.log(`Agent "${event.agentId}" calling tool: ${event.data.toolName}`);
break;
}
});
await session.sendAndWait({ prompt: "Research and write a summary of topic X" });
.NET — Current Workaround (Lifecycle Correlation)
Without agentId, .NET consumers must track sub-agent boundaries manually:
var client = new CopilotClient(new CopilotClientOptions { /* ... */ });
await client.StartAsync();
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "claude-opus-4.6",
CustomAgents =
[
new CustomAgentConfig { Name = "researcher", DisplayName = "Research Agent", Instructions = "..." },
new CustomAgentConfig { Name = "writer", DisplayName = "Writer Agent", Instructions = "..." },
],
});
// Manual tracking required because agentId is not on SessionEvent
var activeAgents = new Dictionary<string, string>(); // toolCallId → agentName
session.On(evt =>
{
switch (evt)
{
case SubagentStartedEvent started:
// Must manually track which sub-agent is active
activeAgents[started.Data.ToolCallId] = started.Data.AgentDisplayName;
Console.WriteLine($"Sub-agent '{started.Data.AgentDisplayName}' started");
break;
case SubagentCompletedEvent completed:
activeAgents.Remove(completed.Data.ToolCallId);
Console.WriteLine($"Sub-agent '{completed.Data.AgentDisplayName}' completed in {completed.Data.DurationMs}ms");
break;
case ToolExecutionStartEvent toolStart:
// No agentId on the event — cannot directly attribute this tool call
// Must use deprecated ParentToolCallId or infer from ordering
#pragma warning disable CS0618
var agent = toolStart.Data.ParentToolCallId is { } parentId
&& activeAgents.TryGetValue(parentId, out var name) ? name : "unknown";
#pragma warning restore CS0618
Console.WriteLine($"Tool '{toolStart.Data.ToolName}' called by agent '{agent}'");
break;
case AssistantMessageDeltaEvent delta:
// Same problem — no agentId, must use deprecated field
#pragma warning disable CS0618
var source = delta.Data.ParentToolCallId is { } pid
&& activeAgents.TryGetValue(pid, out var agentName) ? agentName : "root";
#pragma warning restore CS0618
Console.WriteLine($"[{source}] {delta.Data.DeltaContent}");
break;
}
});
await session.SendAndWaitAsync(new MessageOptions { Prompt = "Research and write a summary of topic X" });
.NET — Desired Behavior (With agentId)
If AgentId were available on SessionEvent, the code becomes much simpler:
session.On(evt =>
{
// Direct sub-agent attribution — no manual tracking needed
var source = evt.AgentId ?? "root";
switch (evt)
{
case ToolExecutionStartEvent toolStart:
Console.WriteLine($"Agent '{source}' calling tool: {toolStart.Data.ToolName}");
break;
case AssistantMessageDeltaEvent delta:
Console.WriteLine($"[{source}] {delta.Data.DeltaContent}");
break;
case SubagentCompletedEvent completed:
Console.WriteLine($"Agent '{source}' completed in {completed.Data.DurationMs}ms");
break;
}
});
Go — Desired Behavior (With AgentID)
session.On(func(evt copilot.SessionEvent) {
source := "root"
if evt.AgentID != nil {
source = *evt.AgentID
}
switch data := evt.Data.(type) {
case *copilot.ToolExecutionStartData:
log.Printf("Agent %q calling tool: %s", source, data.ToolName)
case *copilot.AssistantMessageDeltaData:
log.Printf("[%s] %s", source, data.DeltaContent)
case *copilot.SubagentCompletedData:
log.Printf("Agent %q completed in %.0fms", source, *data.DurationMs)
}
})
Python — Desired Behavior (With agent_id)
@session.on
def handle_event(event):
source = event.agent_id or "root"
if isinstance(event.data, ToolExecutionStartData):
print(f"Agent '{source}' calling tool: {event.data.tool_name}")
elif isinstance(event.data, AssistantMessageDeltaData):
print(f"[{source}] {event.data.delta_content}")
elif isinstance(event.data, SubagentCompletedData):
print(f"Agent '{source}' completed in {event.data.duration_ms}ms")
Expected Behavior
Add agentId (or equivalent) to the base event type in all SDKs:
- .NET:
public string? AgentId { get; set; } on SessionEvent
- Go:
AgentID *string on SessionEvent
- Python:
agent_id: str | None = None on SessionEvent
Environment
- .NET SDK:
GitHub.Copilot.SDK v0.2.2
Summary
The
agentIdfield is present in the JSON wire format (session-events.schema.json) and exposed on every event variant in the Node.js SDK, but it is not generated in the .NET, Go, or Python SDKs. This makes sub-agent correlation significantly harder in those languages.Current Behavior
agentId?: string— works correctlySessionEventbase class hasId,Timestamp,ParentId,Ephemeral,Type— noAgentIdSessionEventstruct hasID,Timestamp,ParentID,Ephemeral,Type,Data— noAgentIDSessionEventdataclass hasid,timestamp,parent_id,ephemeral,type— noagent_idAdditionally, the .NET
SessionEventhas no[JsonExtensionData]attribute, soagentIdfrom the wire format is silently dropped during deserialization with no way to access it.Root Cause
The code generators for .NET (
scripts/codegen/csharp.ts), Go (scripts/codegen/go.ts), and Python (scripts/codegen/python.ts) extract a shared base type for the event envelope but omitagentIdfrom it. The Node.js generator uses a flat union type that repeats all envelope fields per variant, which naturally includesagentId.Impact
Without
agentId, .NET/Go/Python consumers must rely on:SubagentStartedEvent.ToolCallId→SubagentCompletedEvent.ToolCallIdlifecycle correlationParentToolCallIdonAssistantMessageDeltaData,ToolExecutionStartData, etc.The
ParentToolCallIdfield is marked[Obsolete]— when it's removed, there will be no way to identify which sub-agent emitted a streaming delta or tool execution event in .NET/Go/Python.Code Samples
Node.js — Works Today
In Node.js,
agentIdis available on every event, making sub-agent attribution straightforward:.NET — Current Workaround (Lifecycle Correlation)
Without
agentId, .NET consumers must track sub-agent boundaries manually:.NET — Desired Behavior (With
agentId)If
AgentIdwere available onSessionEvent, the code becomes much simpler:Go — Desired Behavior (With
AgentID)Python — Desired Behavior (With
agent_id)Expected Behavior
Add
agentId(or equivalent) to the base event type in all SDKs:public string? AgentId { get; set; }onSessionEventAgentID *stringonSessionEventagent_id: str | None = NoneonSessionEventEnvironment
GitHub.Copilot.SDKv0.2.2