diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 0124008f4..668d090f5 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -497,6 +497,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance hasHooks ? true : null, config.WorkingDirectory, config.Streaming is true ? true : null, + config.IncludeSubAgentStreamingEvents, config.McpServers, "direct", config.CustomAgents, @@ -622,6 +623,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes config.EnableConfigDiscovery, config.DisableResume is true ? true : null, config.Streaming is true ? true : null, + config.IncludeSubAgentStreamingEvents, config.McpServers, "direct", config.CustomAgents, @@ -1636,6 +1638,7 @@ internal record CreateSessionRequest( bool? Hooks, string? WorkingDirectory, bool? Streaming, + bool? IncludeSubAgentStreamingEvents, IDictionary? McpServers, string? EnvValueMode, IList? CustomAgents, @@ -1691,6 +1694,7 @@ internal record ResumeSessionRequest( bool? EnableConfigDiscovery, bool? DisableResume, bool? Streaming, + bool? IncludeSubAgentStreamingEvents, IDictionary? McpServers, string? EnvValueMode, IList? CustomAgents, diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 1fd8afa39..fd42d0c27 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1732,6 +1732,7 @@ protected SessionConfig(SessionConfig? other) SessionId = other.SessionId; SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null; Streaming = other.Streaming; + IncludeSubAgentStreamingEvents = other.IncludeSubAgentStreamingEvents; SystemMessage = other.SystemMessage; Tools = other.Tools is not null ? [.. other.Tools] : null; WorkingDirectory = other.WorkingDirectory; @@ -1848,6 +1849,17 @@ protected SessionConfig(SessionConfig? other) /// public bool Streaming { get; set; } + /// + /// Include sub-agent streaming events in the event stream. When true, streaming + /// delta events from sub-agents (e.g., assistant.message_delta, + /// assistant.reasoning_delta, assistant.streaming_delta with + /// agentId set) are forwarded to this connection. When false, only + /// non-streaming sub-agent events and subagent.* lifecycle events are + /// forwarded; streaming deltas from sub-agents are suppressed. + /// Default: true. + /// + public bool IncludeSubAgentStreamingEvents { get; set; } = true; + /// /// MCP server configurations for the session. /// Keys are server names, values are server configurations ( or ). @@ -1961,6 +1973,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) CreateSessionFsHandler = other.CreateSessionFsHandler; SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null; Streaming = other.Streaming; + IncludeSubAgentStreamingEvents = other.IncludeSubAgentStreamingEvents; SystemMessage = other.SystemMessage; Tools = other.Tools is not null ? [.. other.Tools] : null; WorkingDirectory = other.WorkingDirectory; @@ -2082,6 +2095,17 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// public bool Streaming { get; set; } + /// + /// Include sub-agent streaming events in the event stream. When true, streaming + /// delta events from sub-agents (e.g., assistant.message_delta, + /// assistant.reasoning_delta, assistant.streaming_delta with + /// agentId set) are forwarded to this connection. When false, only + /// non-streaming sub-agent events and subagent.* lifecycle events are + /// forwarded; streaming deltas from sub-agents are suppressed. + /// Default: true. + /// + public bool IncludeSubAgentStreamingEvents { get; set; } = true; + /// /// MCP server configurations for the session. /// Keys are server names, values are server configurations ( or ). diff --git a/dotnet/test/CloneTests.cs b/dotnet/test/CloneTests.cs index 39c42fb25..cc36162ff 100644 --- a/dotnet/test/CloneTests.cs +++ b/dotnet/test/CloneTests.cs @@ -86,6 +86,7 @@ public void SessionConfig_Clone_CopiesAllProperties() ExcludedTools = ["tool3"], WorkingDirectory = "/workspace", Streaming = true, + IncludeSubAgentStreamingEvents = false, McpServers = new Dictionary { ["server1"] = new McpStdioServerConfig { Command = "echo" } }, CustomAgents = [new CustomAgentConfig { Name = "agent1" }], Agent = "agent1", @@ -104,6 +105,7 @@ public void SessionConfig_Clone_CopiesAllProperties() Assert.Equal(original.ExcludedTools, clone.ExcludedTools); Assert.Equal(original.WorkingDirectory, clone.WorkingDirectory); Assert.Equal(original.Streaming, clone.Streaming); + Assert.Equal(original.IncludeSubAgentStreamingEvents, clone.IncludeSubAgentStreamingEvents); Assert.Equal(original.McpServers.Count, clone.McpServers!.Count); Assert.Equal(original.CustomAgents.Count, clone.CustomAgents!.Count); Assert.Equal(original.Agent, clone.Agent); @@ -243,6 +245,7 @@ public void Clone_WithNullCollections_ReturnsNullCollections() Assert.Null(clone.SkillDirectories); Assert.Null(clone.DisabledSkills); Assert.Null(clone.Tools); + Assert.True(clone.IncludeSubAgentStreamingEvents); } [Fact] @@ -272,4 +275,27 @@ public void ResumeSessionConfig_Clone_CopiesAgentProperty() Assert.Equal("test-agent", clone.Agent); } + + [Fact] + public void ResumeSessionConfig_Clone_CopiesIncludeSubAgentStreamingEvents() + { + var original = new ResumeSessionConfig + { + IncludeSubAgentStreamingEvents = false, + }; + + var clone = original.Clone(); + + Assert.False(clone.IncludeSubAgentStreamingEvents); + } + + [Fact] + public void ResumeSessionConfig_Clone_PreservesIncludeSubAgentStreamingEventsDefault() + { + var original = new ResumeSessionConfig(); + + var clone = original.Clone(); + + Assert.True(clone.IncludeSubAgentStreamingEvents); + } } diff --git a/go/client.go b/go/client.go index db8438041..37e572dc8 100644 --- a/go/client.go +++ b/go/client.go @@ -611,6 +611,11 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if config.Streaming { req.Streaming = Bool(true) } + if config.IncludeSubAgentStreamingEvents != nil { + req.IncludeSubAgentStreamingEvents = config.IncludeSubAgentStreamingEvents + } else { + req.IncludeSubAgentStreamingEvents = Bool(true) + } if config.OnUserInputRequest != nil { req.RequestUserInput = Bool(true) } @@ -744,6 +749,11 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, if config.Streaming { req.Streaming = Bool(true) } + if config.IncludeSubAgentStreamingEvents != nil { + req.IncludeSubAgentStreamingEvents = config.IncludeSubAgentStreamingEvents + } else { + req.IncludeSubAgentStreamingEvents = Bool(true) + } if config.OnUserInputRequest != nil { req.RequestUserInput = Bool(true) } diff --git a/go/client_test.go b/go/client_test.go index 091c31726..8840e8269 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -861,6 +861,80 @@ func TestResumeSessionRequest_RequestElicitation(t *testing.T) { }) } +func TestCreateSessionRequest_IncludeSubAgentStreamingEvents(t *testing.T) { + t.Run("defaults to true when nil", func(t *testing.T) { + req := createSessionRequest{ + IncludeSubAgentStreamingEvents: Bool(true), + } + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["includeSubAgentStreamingEvents"] != true { + t.Errorf("Expected includeSubAgentStreamingEvents to be true, got %v", m["includeSubAgentStreamingEvents"]) + } + }) + + t.Run("preserves explicit false", func(t *testing.T) { + req := createSessionRequest{ + IncludeSubAgentStreamingEvents: Bool(false), + } + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["includeSubAgentStreamingEvents"] != false { + t.Errorf("Expected includeSubAgentStreamingEvents to be false, got %v", m["includeSubAgentStreamingEvents"]) + } + }) +} + +func TestResumeSessionRequest_IncludeSubAgentStreamingEvents(t *testing.T) { + t.Run("defaults to true when nil", func(t *testing.T) { + req := resumeSessionRequest{ + SessionID: "s1", + IncludeSubAgentStreamingEvents: Bool(true), + } + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["includeSubAgentStreamingEvents"] != true { + t.Errorf("Expected includeSubAgentStreamingEvents to be true, got %v", m["includeSubAgentStreamingEvents"]) + } + }) + + t.Run("preserves explicit false", func(t *testing.T) { + req := resumeSessionRequest{ + SessionID: "s1", + IncludeSubAgentStreamingEvents: Bool(false), + } + data, err := json.Marshal(req) + if err != nil { + t.Fatalf("Failed to marshal: %v", err) + } + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + t.Fatalf("Failed to unmarshal: %v", err) + } + if m["includeSubAgentStreamingEvents"] != false { + t.Errorf("Expected includeSubAgentStreamingEvents to be false, got %v", m["includeSubAgentStreamingEvents"]) + } + }) +} + func TestCreateSessionResponse_Capabilities(t *testing.T) { t.Run("reads capabilities from session.create response", func(t *testing.T) { responseJSON := `{"sessionId":"s1","workspacePath":"/tmp","capabilities":{"ui":{"elicitation":true}}}` diff --git a/go/types.go b/go/types.go index 15c62cec0..aa4fafc94 100644 --- a/go/types.go +++ b/go/types.go @@ -527,6 +527,13 @@ type SessionConfig struct { // When true, assistant.message_delta and assistant.reasoning_delta events // with deltaContent are sent as the response is generated. Streaming bool + // IncludeSubAgentStreamingEvents includes sub-agent streaming events in the + // event stream. When true, streaming delta events from sub-agents (e.g., + // assistant.message_delta, assistant.reasoning_delta, assistant.streaming_delta + // with agentId set) are forwarded to this connection. When false, only + // non-streaming sub-agent events and subagent.* lifecycle events are forwarded; + // streaming deltas from sub-agents are suppressed. When nil, defaults to true. + IncludeSubAgentStreamingEvents *bool // Provider configures a custom model provider (BYOK) Provider *ProviderConfig // ModelCapabilities overrides individual model capabilities resolved by the runtime. @@ -740,6 +747,13 @@ type ResumeSessionConfig struct { // When true, assistant.message_delta and assistant.reasoning_delta events // with deltaContent are sent as the response is generated. Streaming bool + // IncludeSubAgentStreamingEvents includes sub-agent streaming events in the + // event stream. When true, streaming delta events from sub-agents (e.g., + // assistant.message_delta, assistant.reasoning_delta, assistant.streaming_delta + // with agentId set) are forwarded to this connection. When false, only + // non-streaming sub-agent events and subagent.* lifecycle events are forwarded; + // streaming deltas from sub-agents are suppressed. When nil, defaults to true. + IncludeSubAgentStreamingEvents *bool // MCPServers configures MCP servers for the session MCPServers map[string]MCPServerConfig // CustomAgents configures custom agents for the session @@ -937,34 +951,35 @@ type SessionLifecycleHandler func(event SessionLifecycleEvent) // createSessionRequest is the request for session.create type createSessionRequest struct { - Model string `json:"model,omitempty"` - SessionID string `json:"sessionId,omitempty"` - ClientName string `json:"clientName,omitempty"` - ReasoningEffort string `json:"reasoningEffort,omitempty"` - Tools []Tool `json:"tools,omitempty"` - SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` - AvailableTools []string `json:"availableTools"` - ExcludedTools []string `json:"excludedTools,omitempty"` - Provider *ProviderConfig `json:"provider,omitempty"` - ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` - RequestPermission *bool `json:"requestPermission,omitempty"` - RequestUserInput *bool `json:"requestUserInput,omitempty"` - Hooks *bool `json:"hooks,omitempty"` - WorkingDirectory string `json:"workingDirectory,omitempty"` - Streaming *bool `json:"streaming,omitempty"` - MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` - EnvValueMode string `json:"envValueMode,omitempty"` - CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` - Agent string `json:"agent,omitempty"` - ConfigDir string `json:"configDir,omitempty"` - EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` - SkillDirectories []string `json:"skillDirectories,omitempty"` - DisabledSkills []string `json:"disabledSkills,omitempty"` - InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` - Commands []wireCommand `json:"commands,omitempty"` - RequestElicitation *bool `json:"requestElicitation,omitempty"` - Traceparent string `json:"traceparent,omitempty"` - Tracestate string `json:"tracestate,omitempty"` + Model string `json:"model,omitempty"` + SessionID string `json:"sessionId,omitempty"` + ClientName string `json:"clientName,omitempty"` + ReasoningEffort string `json:"reasoningEffort,omitempty"` + Tools []Tool `json:"tools,omitempty"` + SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` + AvailableTools []string `json:"availableTools"` + ExcludedTools []string `json:"excludedTools,omitempty"` + Provider *ProviderConfig `json:"provider,omitempty"` + ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` + RequestPermission *bool `json:"requestPermission,omitempty"` + RequestUserInput *bool `json:"requestUserInput,omitempty"` + Hooks *bool `json:"hooks,omitempty"` + WorkingDirectory string `json:"workingDirectory,omitempty"` + Streaming *bool `json:"streaming,omitempty"` + IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` + MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` + EnvValueMode string `json:"envValueMode,omitempty"` + CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` + Agent string `json:"agent,omitempty"` + ConfigDir string `json:"configDir,omitempty"` + EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` + SkillDirectories []string `json:"skillDirectories,omitempty"` + DisabledSkills []string `json:"disabledSkills,omitempty"` + InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` + Commands []wireCommand `json:"commands,omitempty"` + RequestElicitation *bool `json:"requestElicitation,omitempty"` + Traceparent string `json:"traceparent,omitempty"` + Tracestate string `json:"tracestate,omitempty"` } // wireCommand is the wire representation of a command (name + description only, no handler). @@ -982,35 +997,36 @@ type createSessionResponse struct { // resumeSessionRequest is the request for session.resume type resumeSessionRequest struct { - SessionID string `json:"sessionId"` - ClientName string `json:"clientName,omitempty"` - Model string `json:"model,omitempty"` - ReasoningEffort string `json:"reasoningEffort,omitempty"` - Tools []Tool `json:"tools,omitempty"` - SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` - AvailableTools []string `json:"availableTools"` - ExcludedTools []string `json:"excludedTools,omitempty"` - Provider *ProviderConfig `json:"provider,omitempty"` - ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` - RequestPermission *bool `json:"requestPermission,omitempty"` - RequestUserInput *bool `json:"requestUserInput,omitempty"` - Hooks *bool `json:"hooks,omitempty"` - WorkingDirectory string `json:"workingDirectory,omitempty"` - ConfigDir string `json:"configDir,omitempty"` - EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` - DisableResume *bool `json:"disableResume,omitempty"` - Streaming *bool `json:"streaming,omitempty"` - MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` - EnvValueMode string `json:"envValueMode,omitempty"` - CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` - Agent string `json:"agent,omitempty"` - SkillDirectories []string `json:"skillDirectories,omitempty"` - DisabledSkills []string `json:"disabledSkills,omitempty"` - InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` - Commands []wireCommand `json:"commands,omitempty"` - RequestElicitation *bool `json:"requestElicitation,omitempty"` - Traceparent string `json:"traceparent,omitempty"` - Tracestate string `json:"tracestate,omitempty"` + SessionID string `json:"sessionId"` + ClientName string `json:"clientName,omitempty"` + Model string `json:"model,omitempty"` + ReasoningEffort string `json:"reasoningEffort,omitempty"` + Tools []Tool `json:"tools,omitempty"` + SystemMessage *SystemMessageConfig `json:"systemMessage,omitempty"` + AvailableTools []string `json:"availableTools"` + ExcludedTools []string `json:"excludedTools,omitempty"` + Provider *ProviderConfig `json:"provider,omitempty"` + ModelCapabilities *rpc.ModelCapabilitiesOverride `json:"modelCapabilities,omitempty"` + RequestPermission *bool `json:"requestPermission,omitempty"` + RequestUserInput *bool `json:"requestUserInput,omitempty"` + Hooks *bool `json:"hooks,omitempty"` + WorkingDirectory string `json:"workingDirectory,omitempty"` + ConfigDir string `json:"configDir,omitempty"` + EnableConfigDiscovery *bool `json:"enableConfigDiscovery,omitempty"` + DisableResume *bool `json:"disableResume,omitempty"` + Streaming *bool `json:"streaming,omitempty"` + IncludeSubAgentStreamingEvents *bool `json:"includeSubAgentStreamingEvents,omitempty"` + MCPServers map[string]MCPServerConfig `json:"mcpServers,omitempty"` + EnvValueMode string `json:"envValueMode,omitempty"` + CustomAgents []CustomAgentConfig `json:"customAgents,omitempty"` + Agent string `json:"agent,omitempty"` + SkillDirectories []string `json:"skillDirectories,omitempty"` + DisabledSkills []string `json:"disabledSkills,omitempty"` + InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` + Commands []wireCommand `json:"commands,omitempty"` + RequestElicitation *bool `json:"requestElicitation,omitempty"` + Traceparent string `json:"traceparent,omitempty"` + Tracestate string `json:"tracestate,omitempty"` } // resumeSessionResponse is the response from session.resume diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index a3d50d5ff..c8c137c3d 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -748,6 +748,7 @@ export class CopilotClient { hooks: !!(config.hooks && Object.values(config.hooks).some(Boolean)), workingDirectory: config.workingDirectory, streaming: config.streaming, + includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true, mcpServers: config.mcpServers, envValueMode: "direct", customAgents: config.customAgents, @@ -888,6 +889,7 @@ export class CopilotClient { configDir: config.configDir, enableConfigDiscovery: config.enableConfigDiscovery, streaming: config.streaming, + includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true, mcpServers: config.mcpServers, envValueMode: "direct", customAgents: config.customAgents, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 0c901f989..9b2df4193 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1270,6 +1270,17 @@ export interface SessionConfig { */ streaming?: boolean; + /** + * Include sub-agent streaming events in the event stream. When true, streaming + * delta events from sub-agents (e.g., `assistant.message_delta`, + * `assistant.reasoning_delta`, `assistant.streaming_delta` with `agentId` set) + * are forwarded to this connection. When false, only non-streaming sub-agent + * events and `subagent.*` lifecycle events are forwarded; streaming deltas from + * sub-agents are suppressed. + * @default true + */ + includeSubAgentStreamingEvents?: boolean; + /** * MCP server configurations for the session. * Keys are server names, values are server configurations. @@ -1338,6 +1349,7 @@ export type ResumeSessionConfig = Pick< | "provider" | "modelCapabilities" | "streaming" + | "includeSubAgentStreamingEvents" | "reasoningEffort" | "onPermissionRequest" | "onUserInputRequest" diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 870ccb1ed..1c0eceb65 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -98,6 +98,74 @@ describe("CopilotClient", () => { spy.mockRestore(); }); + it("defaults includeSubAgentStreamingEvents to true in session.create when not specified", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi.spyOn((client as any).connection!, "sendRequest"); + await client.createSession({ onPermissionRequest: approveAll }); + + const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any; + expect(payload.includeSubAgentStreamingEvents).toBe(true); + }); + + it("forwards explicit false for includeSubAgentStreamingEvents in session.create", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi.spyOn((client as any).connection!, "sendRequest"); + await client.createSession({ + onPermissionRequest: approveAll, + includeSubAgentStreamingEvents: false, + }); + + const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any; + expect(payload.includeSubAgentStreamingEvents).toBe(false); + }); + + it("defaults includeSubAgentStreamingEvents to true in session.resume when not specified", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + await client.resumeSession(session.sessionId, { onPermissionRequest: approveAll }); + + const payload = spy.mock.calls.find((c) => c[0] === "session.resume")![1] as any; + expect(payload.includeSubAgentStreamingEvents).toBe(true); + spy.mockRestore(); + }); + + it("forwards explicit false for includeSubAgentStreamingEvents in session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + includeSubAgentStreamingEvents: false, + }); + + const payload = spy.mock.calls.find((c) => c[0] === "session.resume")![1] as any; + expect(payload.includeSubAgentStreamingEvents).toBe(false); + spy.mockRestore(); + }); + it("forwards provider headers in session.create request", async () => { const client = new CopilotClient(); await client.start(); diff --git a/python/copilot/client.py b/python/copilot/client.py index 5d62db301..4c1186f23 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -1196,6 +1196,7 @@ async def create_session( provider: ProviderConfig | None = None, model_capabilities: ModelCapabilitiesOverride | None = None, streaming: bool | None = None, + include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, custom_agents: list[CustomAgentConfig] | None = None, agent: str | None = None, @@ -1233,6 +1234,11 @@ async def create_session( provider: Provider configuration for Azure or custom endpoints. model_capabilities: Override individual model capabilities resolved by the runtime. streaming: Whether to enable streaming responses. + include_sub_agent_streaming_events: Whether to include sub-agent streaming + delta events (e.g., ``assistant.message_delta``, + ``assistant.reasoning_delta``, ``assistant.streaming_delta`` with + ``agentId`` set). When False, only non-streaming sub-agent events and + ``subagent.*`` lifecycle events are forwarded. Defaults to True. mcp_servers: MCP server configurations. custom_agents: Custom agent configurations. agent: Agent to use for the session. @@ -1341,6 +1347,13 @@ async def create_session( if streaming is not None: payload["streaming"] = streaming + # Include sub-agent streaming events (defaults to True) + payload["includeSubAgentStreamingEvents"] = ( + include_sub_agent_streaming_events + if include_sub_agent_streaming_events is not None + else True + ) + # Add provider configuration if provided if provider: payload["provider"] = self._convert_provider_to_wire_format(provider) @@ -1461,6 +1474,7 @@ async def resume_session( provider: ProviderConfig | None = None, model_capabilities: ModelCapabilitiesOverride | None = None, streaming: bool | None = None, + include_sub_agent_streaming_events: bool | None = None, mcp_servers: dict[str, MCPServerConfig] | None = None, custom_agents: list[CustomAgentConfig] | None = None, agent: str | None = None, @@ -1498,6 +1512,11 @@ async def resume_session( provider: Provider configuration for Azure or custom endpoints. model_capabilities: Override individual model capabilities resolved by the runtime. streaming: Whether to enable streaming responses. + include_sub_agent_streaming_events: Whether to include sub-agent streaming + delta events (e.g., ``assistant.message_delta``, + ``assistant.reasoning_delta``, ``assistant.streaming_delta`` with + ``agentId`` set). When False, only non-streaming sub-agent events and + ``subagent.*`` lifecycle events are forwarded. Defaults to True. mcp_servers: MCP server configurations. custom_agents: Custom agent configurations. agent: Agent to use for the session. @@ -1584,6 +1603,13 @@ async def resume_session( if streaming is not None: payload["streaming"] = streaming + # Include sub-agent streaming events (defaults to True) + payload["includeSubAgentStreamingEvents"] = ( + include_sub_agent_streaming_events + if include_sub_agent_streaming_events is not None + else True + ) + # Always enable permission request callback payload["requestPermission"] = True diff --git a/python/copilot/session.py b/python/copilot/session.py index 43a1c4c5a..ac771923a 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -859,6 +859,13 @@ class SessionConfig(TypedDict, total=False): # When True, assistant.message_delta and assistant.reasoning_delta events # with delta_content are sent as the response is generated streaming: bool + # Include sub-agent streaming events in the event stream. When True, streaming + # delta events from sub-agents (e.g., assistant.message_delta, + # assistant.reasoning_delta, assistant.streaming_delta with agentId set) are + # forwarded to this connection. When False, only non-streaming sub-agent events + # and subagent.* lifecycle events are forwarded; streaming deltas from sub-agents + # are suppressed. Defaults to True. + include_sub_agent_streaming_events: bool # MCP server configurations for the session mcp_servers: dict[str, MCPServerConfig] # Custom agent configurations for the session @@ -920,6 +927,13 @@ class ResumeSessionConfig(TypedDict, total=False): config_dir: str # Enable streaming of assistant message chunks streaming: bool + # Include sub-agent streaming events in the event stream. When True, streaming + # delta events from sub-agents (e.g., assistant.message_delta, + # assistant.reasoning_delta, assistant.streaming_delta with agentId set) are + # forwarded to this connection. When False, only non-streaming sub-agent events + # and subagent.* lifecycle events are forwarded; streaming deltas from sub-agents + # are suppressed. Defaults to True. + include_sub_agent_streaming_events: bool # MCP server configurations for the session mcp_servers: dict[str, MCPServerConfig] # Custom agent configurations for the session diff --git a/python/test_client.py b/python/test_client.py index 0896b54e2..eb132cd0d 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -594,6 +594,110 @@ async def mock_request(method, params): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_session_defaults_include_sub_agent_streaming_events_to_true(self): + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + await client.start() + + try: + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + return await original_request(method, params) + + client._client.request = mock_request + await client.create_session( + on_permission_request=PermissionHandler.approve_all, + ) + assert captured["session.create"]["includeSubAgentStreamingEvents"] is True + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_create_session_preserves_explicit_false_include_sub_agent_streaming_events( + self, + ): + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + await client.start() + + try: + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + return await original_request(method, params) + + client._client.request = mock_request + await client.create_session( + on_permission_request=PermissionHandler.approve_all, + include_sub_agent_streaming_events=False, + ) + assert captured["session.create"]["includeSubAgentStreamingEvents"] is False + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_resume_session_defaults_include_sub_agent_streaming_events_to_true(self): + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + if method == "session.resume": + return {"sessionId": session.session_id} + return await original_request(method, params) + + client._client.request = mock_request + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + ) + assert captured["session.resume"]["includeSubAgentStreamingEvents"] is True + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_resume_session_preserves_explicit_false_include_sub_agent_streaming_events( + self, + ): + client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params): + captured[method] = params + if method == "session.resume": + return {"sessionId": session.session_id} + return await original_request(method, params) + + client._client.request = mock_request + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + include_sub_agent_streaming_events=False, + ) + assert captured["session.resume"]["includeSubAgentStreamingEvents"] is False + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_set_model_sends_correct_rpc(self): client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH))