diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md
index fb2f81fd1..1bb09c830 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 3703f871d..322a3dc1a 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,37 @@ 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
+session.on("assistant.message_delta", (event) => {
+ if (!event.agentId) process.stdout.write(event.data.deltaContent);
+});
+```
+
+
+
+Python
+
+```python
+from copilot import CopilotSession
+from copilot.session_events import SessionEventType
+
+def subscribe_parent_response(session: CopilotSession):
+ def handle(event):
+ if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA and event.agent_id is None:
+ print(event.data.delta_content, end="", flush=True)
+
+ session.on(handle)
+```
+
+
+
## Assistant events
These events track the agent's response lifecycle—from turn start through streaming chunks to the final message.
@@ -271,7 +303,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 +322,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 +349,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 +376,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 +410,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 +821,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 +831,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?` |