diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md
index e43aca1b80..f563c7f47b 100644
--- a/docs/features/custom-agents.md
+++ b/docs/features/custom-agents.md
@@ -434,6 +434,8 @@ By default, all custom agents are available for automatic selection (`infer: tru
When a sub-agent runs, the parent session emits lifecycle events. Subscribe to these events to build UIs that visualize agent activity.
+Sub-agent-originated session events share the parent session stream and include envelope-level `agentId`. Root/main agent events and session-level events omit `agentId`, so renderers can keep the parent response separate from sub-agent traces by checking the event envelope.
+
### Event types
| Event | Emitted when | Data |
diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md
index 0c2dafdefb..f359dd606e 100644
--- a/docs/features/streaming-events.md
+++ b/docs/features/streaming-events.md
@@ -56,6 +56,7 @@ Every session event, regardless of type, includes these fields:
| `id` | `string` (UUID v4) | Unique event identifier |
| `timestamp` | `string` (ISO 8601) | When the event was created |
| `parentId` | `string \| null` | ID of the previous event in the chain; `null` for the first event |
+| `agentId` | `string?` | Sub-agent instance ID for sub-agent-originated events; absent for root/main agent and session-level events |
| `ephemeral` | `boolean?` | `true` for transient events; absent or `false` for persisted events |
| `type` | `string` | Event type discriminator (see tables below) |
| `data` | `object` | Event-specific payload |
@@ -217,6 +218,139 @@ session.on(AssistantMessageDeltaEvent.class, event ->
> [!TIP]
> **(TypeScript)** The TypeScript SDK uses a discriminated union—when you match on `event.type`, the `data` payload is automatically narrowed to the correct shape.
+## Render only the parent agent response
+
+Sub-agent events share the parent session stream and include envelope-level `agentId`. Root/main agent events and session-level events omit `agentId`, so main-chat renderers can ignore assistant events where `agentId` is set and route those events to traces or progress UI instead.
+
+
+TypeScript
+
+```typescript
+import type { CopilotSession } from "@github/copilot-sdk";
+
+export function subscribeParentResponse(session: CopilotSession): void {
+ session.on("assistant.message_delta", (event) => {
+ if (!event.agentId) {
+ process.stdout.write(event.data.deltaContent);
+ }
+ });
+}
+```
+
+
+
+
+Python
+
+```python
+from copilot import CopilotSession, SessionEvent, SessionEventType
+from copilot.session_events import AssistantMessageDeltaData
+
+
+def subscribe_parent_response(session: CopilotSession) -> None:
+ def handle(event: SessionEvent) -> None:
+ if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA and event.agent_id is None:
+ data = event.data
+ if isinstance(data, AssistantMessageDeltaData):
+ print(data.delta_content, end="", flush=True)
+
+ session.on(handle)
+```
+
+
+
+
+Go
+
+```go
+package example
+
+import (
+ "fmt"
+
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+func subscribeParentResponse(session *copilot.Session) {
+ session.On(func(event copilot.SessionEvent) {
+ if event.AgentID != nil {
+ return
+ }
+
+ if d, ok := event.Data.(*copilot.AssistantMessageDeltaData); ok {
+ fmt.Print(d.DeltaContent)
+ }
+ })
+}
+```
+
+
+
+
+.NET
+
+```csharp
+using System;
+using GitHub.Copilot;
+
+static class ParentAgentResponseExample
+{
+ public static void SubscribeParentResponse(CopilotSession session)
+ {
+ session.On(evt =>
+ {
+ if (evt.AgentId is null)
+ {
+ Console.Write(evt.Data.DeltaContent);
+ }
+ });
+ }
+}
+```
+
+
+
+
+Java
+
+```java
+import com.github.copilot.CopilotSession;
+import com.github.copilot.generated.AssistantMessageDeltaEvent;
+
+final class ParentAgentResponseExample {
+ static void subscribeParentResponse(CopilotSession session) {
+ session.on(AssistantMessageDeltaEvent.class, event -> {
+ if (event.getAgentId() == null) {
+ System.out.print(event.getData().deltaContent());
+ }
+ });
+ }
+}
+```
+
+
+
+
+Rust
+
+```rust
+use github_copilot_sdk::session::Session;
+
+async fn subscribe_parent_response(session: &Session) {
+ let mut events = session.subscribe();
+
+ while let Ok(event) = events.recv().await {
+ if event.event_type == "assistant.message_delta" && event.agent_id.is_none() {
+ if let Some(delta) = event.data.get("deltaContent").and_then(|v| v.as_str()) {
+ print!("{delta}");
+ }
+ }
+ }
+}
+```
+
+
+
## Assistant events
These events track the agent's response lifecycle—from turn start through streaming chunks to the final message.
@@ -271,7 +405,7 @@ The assistant's complete response for this LLM call. May include tool invocation
| `phase` | `string` | | Generation phase (e.g., `"thinking"` vs `"response"`) |
| `outputTokens` | `number` | | Actual output token count from the API response |
| `interactionId` | `string` | | CAPI interaction ID for telemetry |
-| `parentToolCallId` | `string` | | Set when this message originates from a sub-agent |
+| `parentToolCallId` | `string` | | Deprecated. Use envelope-level `agentId` for sub-agent attribution |
**`ToolRequest` fields:**
@@ -290,7 +424,7 @@ Ephemeral. Incremental chunk of the assistant's text response, streamed in real
|------------|------|----------|-------------|
| `messageId` | `string` | ✅ | Matches the corresponding `assistant.message` event |
| `deltaContent` | `string` | ✅ | Text chunk to append to the message |
-| `parentToolCallId` | `string` | | Set when originating from a sub-agent |
+| `parentToolCallId` | `string` | | Deprecated. Use envelope-level `agentId` for sub-agent attribution |
### `assistant.turn_end`
@@ -317,7 +451,7 @@ Ephemeral. Token usage and cost information for an individual API call.
| `apiCallId` | `string` | | Completion ID from the provider (e.g., `chatcmpl-abc123`) |
| `apiEndpoint` | `"/chat/completions" \| "/v1/messages" \| "/responses" \| "ws:/responses"` | | API endpoint used for the model call; useful for observability and cost attribution. `ws:/responses` is the websocket variant of the responses API |
| `providerCallId` | `string` | | GitHub request tracing ID (`x-github-request-id`) |
-| `parentToolCallId` | `string` | | Set when usage originates from a sub-agent |
+| `parentToolCallId` | `string` | | Deprecated. Use envelope-level `agentId` for sub-agent attribution |
| `quotaSnapshots` | `Record` | | Per-quota resource usage, keyed by quota identifier |
| `copilotUsage` | `CopilotUsage` | | Itemized token cost breakdown from the API |
@@ -344,7 +478,7 @@ Emitted when a tool begins executing.
| `arguments` | `object` | | Parsed arguments passed to the tool |
| `mcpServerName` | `string` | | MCP server name, when the tool is provided by an MCP server |
| `mcpToolName` | `string` | | Original tool name on the MCP server |
-| `parentToolCallId` | `string` | | Set when invoked by a sub-agent |
+| `parentToolCallId` | `string` | | Deprecated. Use envelope-level `agentId` for sub-agent attribution |
### `tool.execution_partial_result`
@@ -378,7 +512,7 @@ Emitted when a tool finishes executing—successfully or with an error.
| `result` | `Result` | | Present on success (see below) |
| `error` | `{ message, code? }` | | Present on failure |
| `toolTelemetry` | `object` | | Tool-specific telemetry (e.g., CodeQL check counts) |
-| `parentToolCallId` | `string` | | Set when invoked by a sub-agent |
+| `parentToolCallId` | `string` | | Deprecated. Use envelope-level `agentId` for sub-agent attribution |
**`Result` fields:**
@@ -789,6 +923,8 @@ session.idle → Ready for next message (ephemeral)
## All event types at a glance
+This table lists key `data` payload fields. Common envelope fields are documented above.
+
| Event Type | Ephemeral | Category | Key Data Fields |
|------------|-----------|----------|-----------------|
| `assistant.turn_start` | | Assistant | `turnId`, `interactionId?` |
@@ -797,7 +933,7 @@ session.idle → Ready for next message (ephemeral)
| `assistant.reasoning_delta` | ✅ | Assistant | `reasoningId`, `deltaContent` |
| `assistant.streaming_delta` | ✅ | Assistant | `totalResponseSizeBytes` |
| `assistant.message` | | Assistant | `messageId`, `content`, `toolRequests?`, `outputTokens?`, `phase?` |
-| `assistant.message_delta` | ✅ | Assistant | `messageId`, `deltaContent`, `parentToolCallId?` |
+| `assistant.message_delta` | ✅ | Assistant | `messageId`, `deltaContent` |
| `assistant.turn_end` | | Assistant | `turnId` |
| `assistant.usage` | ✅ | Assistant | `model`, `apiEndpoint?`, `inputTokens?`, `outputTokens?`, `cost?`, `duration?` |
| `tool.user_requested` | | Tool | `toolCallId`, `toolName`, `arguments?` |
diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts
index 7c2a5cebea..376d29634b 100644
--- a/java/scripts/codegen/java.ts
+++ b/java/scripts/codegen/java.ts
@@ -814,6 +814,10 @@ async function generateSessionEventBaseClass(
lines.push(` @JsonProperty("parentId")`);
lines.push(` private UUID parentId;`);
lines.push("");
+ lines.push(` /** Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. */`);
+ lines.push(` @JsonProperty("agentId")`);
+ lines.push(` private String agentId;`);
+ lines.push("");
lines.push(` /** When true, the event is transient and not persisted to the session event log on disk. */`);
lines.push(` @JsonProperty("ephemeral")`);
lines.push(` private Boolean ephemeral;`);
@@ -834,6 +838,9 @@ async function generateSessionEventBaseClass(
lines.push(` public UUID getParentId() { return parentId; }`);
lines.push(` public void setParentId(UUID parentId) { this.parentId = parentId; }`);
lines.push("");
+ lines.push(` public String getAgentId() { return agentId; }`);
+ lines.push(` public void setAgentId(String agentId) { this.agentId = agentId; }`);
+ lines.push("");
lines.push(` public Boolean getEphemeral() { return ephemeral; }`);
lines.push(` public void setEphemeral(Boolean ephemeral) { this.ephemeral = ephemeral; }`);
lines.push(`}`);
diff --git a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java
index b6fdc56e9e..2d7ffa362c 100644
--- a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java
+++ b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java
@@ -256,6 +256,10 @@ public abstract sealed class SessionEvent permits
@JsonProperty("parentId")
private UUID parentId;
+ /** Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. */
+ @JsonProperty("agentId")
+ private String agentId;
+
/** When true, the event is transient and not persisted to the session event log on disk. */
@JsonProperty("ephemeral")
private Boolean ephemeral;
@@ -276,6 +280,9 @@ public abstract sealed class SessionEvent permits
public UUID getParentId() { return parentId; }
public void setParentId(UUID parentId) { this.parentId = parentId; }
+ public String getAgentId() { return agentId; }
+ public void setAgentId(String agentId) { this.agentId = agentId; }
+
public Boolean getEphemeral() { return ephemeral; }
public void setEphemeral(Boolean ephemeral) { this.ephemeral = ephemeral; }
}
diff --git a/java/src/test/java/com/github/copilot/SessionEventDeserializationTest.java b/java/src/test/java/com/github/copilot/SessionEventDeserializationTest.java
index 07b4c2eed2..516d1ecf4a 100644
--- a/java/src/test/java/com/github/copilot/SessionEventDeserializationTest.java
+++ b/java/src/test/java/com/github/copilot/SessionEventDeserializationTest.java
@@ -956,6 +956,23 @@ void testParseBaseFieldsParentId() throws Exception {
assertEquals(UUID.fromString(parentUuid), event.getParentId());
}
+ @Test
+ void testParseBaseFieldsAgentId() throws Exception {
+ String json = """
+ {
+ "type": "assistant.message",
+ "agentId": "subagent-1",
+ "data": {
+ "content": "Hello"
+ }
+ }
+ """;
+
+ SessionEvent event = parseJson(json);
+ assertNotNull(event);
+ assertEquals("subagent-1", event.getAgentId());
+ }
+
@Test
void testParseBaseFieldsEphemeral() throws Exception {
String json = """
@@ -995,6 +1012,7 @@ void testParseBaseFieldsAllTogether() throws Exception {
"type": "assistant.message",
"id": "%s",
"parentId": "%s",
+ "agentId": "subagent-1",
"ephemeral": false,
"timestamp": "2025-06-15T12:00:00+02:00",
"data": {
@@ -1007,6 +1025,7 @@ void testParseBaseFieldsAllTogether() throws Exception {
assertNotNull(event);
assertEquals(UUID.fromString(uuid), event.getId());
assertEquals(UUID.fromString(parentUuid), event.getParentId());
+ assertEquals("subagent-1", event.getAgentId());
assertFalse(event.getEphemeral());
assertNotNull(event.getTimestamp());
assertInstanceOf(AssistantMessageEvent.class, event);
@@ -1026,6 +1045,7 @@ void testParseBaseFieldsNullWhenAbsent() throws Exception {
assertNotNull(event);
assertNull(event.getId());
assertNull(event.getParentId());
+ assertNull(event.getAgentId());
assertNull(event.getEphemeral());
assertNull(event.getTimestamp());
}