From 34ebb52f46f887e04475018324d89706594274bd Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Wed, 1 Jul 2026 16:02:33 -0400 Subject: [PATCH 1/8] docs: document sub-agent event attribution Document envelope-level agentId in streaming events and clarify parent-agent rendering guidance. Mark parentToolCallId as deprecated for sub-agent attribution. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/features/custom-agents.md | 2 ++ docs/features/streaming-events.md | 44 ++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index fb2f81fd1c..1bb09c8300 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 3703f871d3..fd22477845 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,35 @@ 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. + +
+Node.js / TypeScript + +```typescript +session.on("assistant.message_delta", (event) => { + if (!event.agentId) process.stdout.write(event.data.deltaContent); +}); +``` + +
+
+Python + +```python +from copilot.session_events import SessionEventType + +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 +301,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 +320,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 +347,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 +374,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 +408,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 +819,8 @@ session.idle → Ready for next message (ephemeral) ## All event types at a glance +This table lists key `data` payload fields. For sub-agent attribution, use the envelope-level `agentId`. + | Event Type | Ephemeral | Category | Key Data Fields | |------------|-----------|----------|-----------------| | `assistant.turn_start` | | Assistant | `turnId`, `interactionId?` | @@ -797,7 +829,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?` | From a3c4106a94d3ca83eb6756fd729f77caa3b86bc5 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Wed, 1 Jul 2026 16:05:26 -0400 Subject: [PATCH 2/8] docs: fix Python streaming event snippet validation Add a hidden validation-only Python block for the parent-agent streaming example so docs validation has a typed session placeholder while keeping the visible snippet concise. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/streaming-events.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index fd22477845..63cb63160e 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -235,6 +235,22 @@ session.on("assistant.message_delta", (event) => {
Python + +```python +from typing import Any + +from copilot.session_events import SessionEventType + +session: Any = None # assume session is created elsewhere + +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) +``` + + ```python from copilot.session_events import SessionEventType From ca26dad0d1c6b87ed0285d244c9a94b9c41afd27 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Wed, 1 Jul 2026 16:09:43 -0400 Subject: [PATCH 3/8] docs: avoid Any in Python streaming example Use a typed CopilotSession helper in the hidden validation snippet instead of typing the session as Any. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/features/streaming-events.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 63cb63160e..5d04203341 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -237,17 +237,15 @@ session.on("assistant.message_delta", (event) => { ```python -from typing import Any - +from copilot import CopilotSession from copilot.session_events import SessionEventType -session: Any = None # assume session is created elsewhere - -def handle(event): - if event.type == SessionEventType.ASSISTANT_MESSAGE_DELTA and event.agent_id is None: - print(event.data.delta_content, end="", flush=True) +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) + session.on(handle) ``` From 650947c593dcc2355824380be802a1538662df1e Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Wed, 1 Jul 2026 16:10:53 -0400 Subject: [PATCH 4/8] docs: use supported TypeScript tab label Use the docs pipeline's supported TypeScript summary label for the new streaming-events tabbed example. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/features/streaming-events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 5d04203341..5090c3e476 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -223,7 +223,7 @@ session.on(AssistantMessageDeltaEvent.class, event -> 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.
-Node.js / TypeScript +TypeScript ```typescript session.on("assistant.message_delta", (event) => { From ee7936293c14c61d2b4a877f686c3c6db69f127e Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Wed, 1 Jul 2026 16:18:48 -0400 Subject: [PATCH 5/8] docs: generalize event quick reference note Keep the quick-reference table note focused on payload fields rather than repeating sub-agent attribution guidance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/features/streaming-events.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 5090c3e476..095570ef17 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -833,7 +833,7 @@ session.idle → Ready for next message (ephemeral) ## All event types at a glance -This table lists key `data` payload fields. For sub-agent attribution, use the envelope-level `agentId`. +This table lists key `data` payload fields. Common envelope fields are documented above. | Event Type | Ephemeral | Category | Key Data Fields | |------------|-----------|----------|-----------------| From ba0413cda323ed2b87c309dbe24237433d35cf5e Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Wed, 1 Jul 2026 16:20:10 -0400 Subject: [PATCH 6/8] docs: collapse Python streaming example Use a single visible Python snippet for parent-response filtering instead of a hidden validation block plus duplicate visible example. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/features/streaming-events.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 095570ef17..322a3dc1a1 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -235,7 +235,6 @@ session.on("assistant.message_delta", (event) => {
Python - ```python from copilot import CopilotSession from copilot.session_events import SessionEventType @@ -247,17 +246,6 @@ def subscribe_parent_response(session: CopilotSession): session.on(handle) ``` - - -```python -from copilot.session_events import SessionEventType - -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) -```
From c964d3994abb69cef3d96a785772e23f0c1096ae Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Tue, 14 Jul 2026 16:26:19 -0400 Subject: [PATCH 7/8] docs: avoid partial language examples Remove the two-language parent-response snippet so the streaming events guide does not present uneven language coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/features/streaming-events.md | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index fba100bffd..755fb74207 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -222,33 +222,6 @@ session.on(AssistantMessageDeltaEvent.class, event -> 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. From 6bb4f20a08727057dd2c5b43c381c127447ec804 Mon Sep 17 00:00:00 2001 From: Logan Rosen Date: Tue, 14 Jul 2026 16:42:43 -0400 Subject: [PATCH 8/8] java: expose session event agent id Add the generated Java session event envelope field for sub-agent attribution and restore equivalent parent-response streaming examples across all SDK languages. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f1fa1c7f-9fdc-4cd4-9279-586191a6a646 --- docs/features/streaming-events.md | 129 ++++++++++++++++++ java/scripts/codegen/java.ts | 7 + .../copilot/generated/SessionEvent.java | 7 + .../SessionEventDeserializationTest.java | 20 +++ 4 files changed, 163 insertions(+) diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 755fb74207..f359dd606e 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -222,6 +222,135 @@ session.on(AssistantMessageDeltaEvent.class, event -> 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. 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()); }