diff --git a/docs/auth/byok.md b/docs/auth/byok.md
index 8d9650280..4099f212e 100644
--- a/docs/auth/byok.md
+++ b/docs/auth/byok.md
@@ -335,7 +335,7 @@ const client = new CopilotClient({
```python
from copilot import CopilotClient
-from copilot.types import ModelInfo, ModelCapabilities, ModelSupports, ModelLimits
+from copilot.client import ModelInfo, ModelCapabilities, ModelSupports, ModelLimits
client = CopilotClient({
"on_list_models": lambda: [
diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md
index 47712d9cf..60cbebef1 100644
--- a/docs/features/custom-agents.md
+++ b/docs/features/custom-agents.md
@@ -65,7 +65,7 @@ const session = await client.createSession({
```python
from copilot import CopilotClient
-from copilot.types import PermissionRequestResult
+from copilot.session import PermissionRequestResult
client = CopilotClient()
await client.start()
diff --git a/docs/features/image-input.md b/docs/features/image-input.md
index acec80d4a..44a9f57d9 100644
--- a/docs/features/image-input.md
+++ b/docs/features/image-input.md
@@ -69,7 +69,7 @@ await session.send({
```python
from copilot import CopilotClient
-from copilot.types import PermissionRequestResult
+from copilot.session import PermissionRequestResult
client = CopilotClient()
await client.start()
@@ -121,7 +121,7 @@ func main() {
Prompt: "Describe what you see in this image",
Attachments: []copilot.Attachment{
{
- Type: copilot.File,
+ Type: copilot.AttachmentTypeFile,
Path: &path,
},
},
@@ -147,7 +147,7 @@ session.Send(ctx, copilot.MessageOptions{
Prompt: "Describe what you see in this image",
Attachments: []copilot.Attachment{
{
- Type: copilot.File,
+ Type: copilot.AttachmentTypeFile,
Path: &path,
},
},
@@ -315,7 +315,7 @@ func main() {
Prompt: "Describe what you see in this image",
Attachments: []copilot.Attachment{
{
- Type: copilot.Blob,
+ Type: copilot.AttachmentTypeBlob,
Data: &base64ImageData,
MIMEType: &mimeType,
DisplayName: &displayName,
@@ -333,7 +333,7 @@ session.Send(ctx, copilot.MessageOptions{
Prompt: "Describe what you see in this image",
Attachments: []copilot.Attachment{
{
- Type: copilot.Blob,
+ Type: copilot.AttachmentTypeBlob,
Data: &base64ImageData, // base64-encoded string
MIMEType: &mimeType,
DisplayName: &displayName,
diff --git a/docs/features/skills.md b/docs/features/skills.md
index 466c637ff..9065697c5 100644
--- a/docs/features/skills.md
+++ b/docs/features/skills.md
@@ -43,7 +43,7 @@ await session.sendAndWait({ prompt: "Review this code for security issues" });
```python
from copilot import CopilotClient
-from copilot.types import PermissionRequestResult
+from copilot.session import PermissionRequestResult
async def main():
client = CopilotClient()
diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md
index 7da349e1c..a3e1b6d2b 100644
--- a/docs/features/steering-and-queueing.md
+++ b/docs/features/steering-and-queueing.md
@@ -70,7 +70,7 @@ await session.send({
```python
from copilot import CopilotClient
-from copilot.types import PermissionRequestResult
+from copilot.session import PermissionRequestResult
async def main():
client = CopilotClient()
@@ -229,7 +229,7 @@ await session.send({
```python
from copilot import CopilotClient
-from copilot.types import PermissionRequestResult
+from copilot.session import PermissionRequestResult
async def main():
client = CopilotClient()
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 6c0aee72e..14fd8babf 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -129,7 +129,8 @@ Create `main.py`:
```python
import asyncio
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient
+from copilot.session import PermissionHandler
async def main():
client = CopilotClient()
@@ -275,7 +276,8 @@ Update `main.py`:
```python
import asyncio
import sys
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient
+from copilot.session import PermissionHandler
from copilot.generated.session_events import SessionEventType
async def main():
@@ -651,7 +653,8 @@ Update `main.py`:
import asyncio
import random
import sys
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient
+from copilot.session import PermissionHandler
from copilot.tools import define_tool
from copilot.generated.session_events import SessionEventType
from pydantic import BaseModel, Field
@@ -919,7 +922,8 @@ Create `weather_assistant.py`:
import asyncio
import random
import sys
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient
+from copilot.session import PermissionHandler
from copilot.tools import define_tool
from copilot.generated.session_events import SessionEventType
from pydantic import BaseModel, Field
@@ -1235,7 +1239,7 @@ const session = await client.createSession({
### Customize the System Message
-Control the AI's behavior and personality:
+Control the AI's behavior and personality by appending instructions:
```typescript
const session = await client.createSession({
@@ -1245,6 +1249,28 @@ const session = await client.createSession({
});
```
+For more fine-grained control, use `mode: "customize"` to override individual sections of the system prompt while preserving the rest:
+
+```typescript
+const session = await client.createSession({
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations." },
+ code_change_rules: { action: "remove" },
+ guidelines: { action: "append", content: "\n* Always cite data sources" },
+ },
+ content: "Focus on financial analysis and reporting.",
+ },
+});
+```
+
+Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`.
+
+Each override supports four actions: `replace`, `remove`, `append`, and `prepend`. Unknown section IDs are handled gracefully — content is appended to additional instructions and a warning is emitted; `remove` on unknown sections is silently ignored.
+
+See the language-specific SDK READMEs for examples in [TypeScript](../nodejs/README.md), [Python](../python/README.md), [Go](../go/README.md), and [C#](../dotnet/README.md).
+
---
## Connecting to an External CLI Server
@@ -1290,7 +1316,8 @@ const session = await client.createSession({ onPermissionRequest: approveAll });
Python
```python
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient
+from copilot.session import PermissionHandler
client = CopilotClient({
"cli_url": "localhost:4321"
diff --git a/docs/hooks/error-handling.md b/docs/hooks/error-handling.md
index a67906ac9..da97d5107 100644
--- a/docs/hooks/error-handling.md
+++ b/docs/hooks/error-handling.md
@@ -35,7 +35,7 @@ type ErrorOccurredHandler = (
```python
-from copilot.types import ErrorOccurredHookInput, HookInvocation, ErrorOccurredHookOutput
+from copilot.session import ErrorOccurredHookInput, ErrorOccurredHookOutput
from typing import Callable, Awaitable
ErrorOccurredHandler = Callable[
diff --git a/docs/hooks/post-tool-use.md b/docs/hooks/post-tool-use.md
index 029e9eb2f..fed1af727 100644
--- a/docs/hooks/post-tool-use.md
+++ b/docs/hooks/post-tool-use.md
@@ -35,7 +35,7 @@ type PostToolUseHandler = (
```python
-from copilot.types import PostToolUseHookInput, HookInvocation, PostToolUseHookOutput
+from copilot.session import PostToolUseHookInput, PostToolUseHookOutput
from typing import Callable, Awaitable
PostToolUseHandler = Callable[
diff --git a/docs/hooks/pre-tool-use.md b/docs/hooks/pre-tool-use.md
index e1bb97495..df23bbd55 100644
--- a/docs/hooks/pre-tool-use.md
+++ b/docs/hooks/pre-tool-use.md
@@ -35,7 +35,7 @@ type PreToolUseHandler = (
```python
-from copilot.types import PreToolUseHookInput, HookInvocation, PreToolUseHookOutput
+from copilot.session import PreToolUseHookInput, PreToolUseHookOutput
from typing import Callable, Awaitable
PreToolUseHandler = Callable[
diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md
index 4efd33ccc..e86f5d7a9 100644
--- a/docs/hooks/session-lifecycle.md
+++ b/docs/hooks/session-lifecycle.md
@@ -39,7 +39,7 @@ type SessionStartHandler = (
```python
-from copilot.types import SessionStartHookInput, HookInvocation, SessionStartHookOutput
+from copilot.session import SessionStartHookInput, SessionStartHookOutput
from typing import Callable, Awaitable
SessionStartHandler = Callable[
@@ -249,7 +249,7 @@ type SessionEndHandler = (
```python
-from copilot.types import SessionEndHookInput, HookInvocation
+from copilot.session import SessionEndHookInput
from typing import Callable, Awaitable
SessionEndHandler = Callable[
diff --git a/docs/hooks/user-prompt-submitted.md b/docs/hooks/user-prompt-submitted.md
index 2aca7f1ce..89831e34b 100644
--- a/docs/hooks/user-prompt-submitted.md
+++ b/docs/hooks/user-prompt-submitted.md
@@ -35,7 +35,7 @@ type UserPromptSubmittedHandler = (
```python
-from copilot.types import UserPromptSubmittedHookInput, HookInvocation, UserPromptSubmittedHookOutput
+from copilot.session import UserPromptSubmittedHookInput, UserPromptSubmittedHookOutput
from typing import Callable, Awaitable
UserPromptSubmittedHandler = Callable[
diff --git a/docs/setup/azure-managed-identity.md b/docs/setup/azure-managed-identity.md
index 40d87c5ba..b92b63b18 100644
--- a/docs/setup/azure-managed-identity.md
+++ b/docs/setup/azure-managed-identity.md
@@ -42,7 +42,8 @@ import asyncio
import os
from azure.identity import DefaultAzureCredential
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient
+from copilot.session import ProviderConfig, SessionConfig
COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default"
@@ -58,14 +59,15 @@ async def main():
await client.start()
session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-4.1",
- provider={
- "type": "openai",
- "base_url": f"{foundry_url.rstrip('/')}/openai/v1/",
- "bearer_token": token, # Short-lived bearer token
- "wire_api": "responses",
- },
+ SessionConfig(
+ model="gpt-4.1",
+ provider=ProviderConfig(
+ type="openai",
+ base_url=f"{foundry_url.rstrip('/')}/openai/v1/",
+ bearer_token=token, # Short-lived bearer token
+ wire_api="responses",
+ ),
+ )
)
response = await session.send_and_wait({"prompt": "Hello from Managed Identity!"})
@@ -83,7 +85,8 @@ Bearer tokens expire (typically after ~1 hour). For servers or long-running agen
```python
from azure.identity import DefaultAzureCredential
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient
+from copilot.session import ProviderConfig, SessionConfig
COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default"
@@ -97,21 +100,24 @@ class ManagedIdentityCopilotAgent:
self.credential = DefaultAzureCredential()
self.client = CopilotClient()
- def _get_provider_config(self) -> dict:
- """Build a provider config dict with a fresh bearer token."""
+ def _get_session_config(self) -> SessionConfig:
+ """Build a SessionConfig with a fresh bearer token."""
token = self.credential.get_token(COGNITIVE_SERVICES_SCOPE).token
- return {
- "type": "openai",
- "base_url": f"{self.foundry_url}/openai/v1/",
- "bearer_token": token,
- "wire_api": "responses",
- }
+ return SessionConfig(
+ model=self.model,
+ provider=ProviderConfig(
+ type="openai",
+ base_url=f"{self.foundry_url}/openai/v1/",
+ bearer_token=token,
+ wire_api="responses",
+ ),
+ )
async def chat(self, prompt: str) -> str:
"""Send a prompt and return the response text."""
# Fresh token for each session
- provider = self._get_provider_config()
- session = await self.client.create_session(on_permission_request=PermissionHandler.approve_all, model=self.model, provider=provider)
+ config = self._get_session_config()
+ session = await self.client.create_session(config)
response = await session.send_and_wait({"prompt": prompt})
await session.disconnect()
diff --git a/dotnet/README.md b/dotnet/README.md
index cb7dbba18..cab1cf068 100644
--- a/dotnet/README.md
+++ b/dotnet/README.md
@@ -509,6 +509,34 @@ var session = await client.CreateSessionAsync(new SessionConfig
});
```
+#### Customize Mode
+
+Use `Mode = SystemMessageMode.Customize` to selectively override individual sections of the prompt while preserving the rest:
+
+```csharp
+var session = await client.CreateSessionAsync(new SessionConfig
+{
+ Model = "gpt-5",
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ [SystemPromptSections.Tone] = new() { Action = SectionOverrideAction.Replace, Content = "Respond in a warm, professional tone. Be thorough in explanations." },
+ [SystemPromptSections.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove },
+ [SystemPromptSections.Guidelines] = new() { Action = SectionOverrideAction.Append, Content = "\n* Always cite data sources" },
+ },
+ Content = "Focus on financial analysis and reporting."
+ }
+});
+```
+
+Available section IDs are defined as constants on `SystemPromptSections`: `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `LastInstructions`.
+
+Each section override supports four actions: `Replace`, `Remove`, `Append`, and `Prepend`. Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored.
+
+#### Replace Mode
+
For full control (removes all guardrails), use `Mode = SystemMessageMode.Replace`:
```csharp
diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs
index a9ad1fccd..99c0eff00 100644
--- a/dotnet/src/Client.cs
+++ b/dotnet/src/Client.cs
@@ -365,6 +365,44 @@ private async Task CleanupConnectionAsync(List? errors)
}
}
+ private static (SystemMessageConfig? wireConfig, Dictionary>>? callbacks) ExtractTransformCallbacks(SystemMessageConfig? systemMessage)
+ {
+ if (systemMessage?.Mode != SystemMessageMode.Customize || systemMessage.Sections == null)
+ {
+ return (systemMessage, null);
+ }
+
+ var callbacks = new Dictionary>>();
+ var wireSections = new Dictionary();
+
+ foreach (var (sectionId, sectionOverride) in systemMessage.Sections)
+ {
+ if (sectionOverride.Transform != null)
+ {
+ callbacks[sectionId] = sectionOverride.Transform;
+ wireSections[sectionId] = new SectionOverride { Action = SectionOverrideAction.Transform };
+ }
+ else
+ {
+ wireSections[sectionId] = sectionOverride;
+ }
+ }
+
+ if (callbacks.Count == 0)
+ {
+ return (systemMessage, null);
+ }
+
+ var wireConfig = new SystemMessageConfig
+ {
+ Mode = systemMessage.Mode,
+ Content = systemMessage.Content,
+ Sections = wireSections
+ };
+
+ return (wireConfig, callbacks);
+ }
+
///
/// Creates a new Copilot session with the specified configuration.
///
@@ -409,6 +447,8 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
config.Hooks.OnSessionEnd != null ||
config.Hooks.OnErrorOccurred != null);
+ var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage);
+
var sessionId = config.SessionId ?? Guid.NewGuid().ToString();
// Create and register the session before issuing the RPC so that
@@ -424,6 +464,10 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
{
session.RegisterHooks(config.Hooks);
}
+ if (transformCallbacks != null)
+ {
+ session.RegisterTransformCallbacks(transformCallbacks);
+ }
if (config.OnEvent != null)
{
session.On(config.OnEvent);
@@ -440,7 +484,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance
config.ClientName,
config.ReasoningEffort,
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
- config.SystemMessage,
+ wireSystemMessage,
config.AvailableTools,
config.ExcludedTools,
config.Provider,
@@ -519,6 +563,8 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
config.Hooks.OnSessionEnd != null ||
config.Hooks.OnErrorOccurred != null);
+ var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage);
+
// Create and register the session before issuing the RPC so that
// events emitted by the CLI (e.g. session.start) are not dropped.
var session = new CopilotSession(sessionId, connection.Rpc, _logger);
@@ -532,6 +578,10 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
{
session.RegisterHooks(config.Hooks);
}
+ if (transformCallbacks != null)
+ {
+ session.RegisterTransformCallbacks(transformCallbacks);
+ }
if (config.OnEvent != null)
{
session.On(config.OnEvent);
@@ -548,7 +598,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes
config.Model,
config.ReasoningEffort,
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
- config.SystemMessage,
+ wireSystemMessage,
config.AvailableTools,
config.ExcludedTools,
config.Provider,
@@ -1222,6 +1272,7 @@ private async Task ConnectToServerAsync(Process? cliProcess, string?
rpc.AddLocalRpcMethod("permission.request", handler.OnPermissionRequestV2);
rpc.AddLocalRpcMethod("userInput.request", handler.OnUserInputRequest);
rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke);
+ rpc.AddLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform);
rpc.StartListening();
// Transition state to Disconnected if the JSON-RPC connection drops
@@ -1350,6 +1401,12 @@ public async Task OnHooksInvoke(string sessionId, string ho
return new HooksInvokeResponse(output);
}
+ public async Task OnSystemMessageTransform(string sessionId, JsonElement sections)
+ {
+ var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
+ return await session.HandleSystemMessageTransformAsync(sections);
+ }
+
// Protocol v2 backward-compatibility adapters
public async Task OnToolCallV2(string sessionId,
@@ -1685,6 +1742,7 @@ private static LogLevel MapLevel(TraceEventType eventType)
[JsonSerializable(typeof(ResumeSessionResponse))]
[JsonSerializable(typeof(SessionMetadata))]
[JsonSerializable(typeof(SystemMessageConfig))]
+ [JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
[JsonSerializable(typeof(ToolCallResponseV2))]
[JsonSerializable(typeof(ToolDefinition))]
[JsonSerializable(typeof(ToolResultAIContent))]
diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs
index 6fc593c12..fabe4817e 100644
--- a/dotnet/src/Generated/Rpc.cs
+++ b/dotnet/src/Generated/Rpc.cs
@@ -245,6 +245,10 @@ internal class SessionLogRequest
/// When true, the message is transient and not persisted to the session event log on disk.
[JsonPropertyName("ephemeral")]
public bool? Ephemeral { get; set; }
+
+ /// Optional URL the user can open in their browser for more details.
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
/// RPC data type for SessionModelGetCurrent operations.
@@ -577,6 +581,347 @@ internal class SessionAgentDeselectRequest
public string SessionId { get; set; } = string.Empty;
}
+/// RPC data type for SessionAgentReload operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionAgentReloadResult
+{
+ /// Reloaded custom agents.
+ [JsonPropertyName("agents")]
+ public List Agents { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionAgentReload operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionAgentReloadRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for Skill operations.
+public class Skill
+{
+ /// Unique identifier for the skill.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Description of what the skill does.
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = string.Empty;
+
+ /// Source location type (e.g., project, personal, plugin).
+ [JsonPropertyName("source")]
+ public string Source { get; set; } = string.Empty;
+
+ /// Whether the skill can be invoked by the user as a slash command.
+ [JsonPropertyName("userInvocable")]
+ public bool UserInvocable { get; set; }
+
+ /// Whether the skill is currently enabled.
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+
+ /// Absolute path to the skill file.
+ [JsonPropertyName("path")]
+ public string? Path { get; set; }
+}
+
+/// RPC data type for SessionSkillsList operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionSkillsListResult
+{
+ /// Available skills.
+ [JsonPropertyName("skills")]
+ public List Skills { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionSkillsList operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionSkillsListRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionSkillsEnable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionSkillsEnableResult
+{
+}
+
+/// RPC data type for SessionSkillsEnable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionSkillsEnableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Name of the skill to enable.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionSkillsDisable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionSkillsDisableResult
+{
+}
+
+/// RPC data type for SessionSkillsDisable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionSkillsDisableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Name of the skill to disable.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionSkillsReload operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionSkillsReloadResult
+{
+}
+
+/// RPC data type for SessionSkillsReload operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionSkillsReloadRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for Server operations.
+public class Server
+{
+ /// Server name (config key).
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Connection status: connected, failed, pending, disabled, or not_configured.
+ [JsonPropertyName("status")]
+ public ServerStatus Status { get; set; }
+
+ /// Configuration source: user, workspace, plugin, or builtin.
+ [JsonPropertyName("source")]
+ public string? Source { get; set; }
+
+ /// Error message if the server failed to connect.
+ [JsonPropertyName("error")]
+ public string? Error { get; set; }
+}
+
+/// RPC data type for SessionMcpList operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionMcpListResult
+{
+ /// Configured MCP servers.
+ [JsonPropertyName("servers")]
+ public List Servers { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionMcpList operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionMcpListRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionMcpEnable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionMcpEnableResult
+{
+}
+
+/// RPC data type for SessionMcpEnable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionMcpEnableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Name of the MCP server to enable.
+ [JsonPropertyName("serverName")]
+ public string ServerName { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionMcpDisable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionMcpDisableResult
+{
+}
+
+/// RPC data type for SessionMcpDisable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionMcpDisableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Name of the MCP server to disable.
+ [JsonPropertyName("serverName")]
+ public string ServerName { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionMcpReload operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionMcpReloadResult
+{
+}
+
+/// RPC data type for SessionMcpReload operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionMcpReloadRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for Plugin operations.
+public class Plugin
+{
+ /// Plugin name.
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Marketplace the plugin came from.
+ [JsonPropertyName("marketplace")]
+ public string Marketplace { get; set; } = string.Empty;
+
+ /// Installed version.
+ [JsonPropertyName("version")]
+ public string? Version { get; set; }
+
+ /// Whether the plugin is currently enabled.
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+}
+
+/// RPC data type for SessionPluginsList operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionPluginsListResult
+{
+ /// Installed plugins.
+ [JsonPropertyName("plugins")]
+ public List Plugins { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionPluginsList operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionPluginsListRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for Extension operations.
+public class Extension
+{
+ /// Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper').
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+
+ /// Extension name (directory name).
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ /// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/).
+ [JsonPropertyName("source")]
+ public ExtensionSource Source { get; set; }
+
+ /// Current status: running, disabled, failed, or starting.
+ [JsonPropertyName("status")]
+ public ExtensionStatus Status { get; set; }
+
+ /// Process ID if the extension is running.
+ [JsonPropertyName("pid")]
+ public double? Pid { get; set; }
+}
+
+/// RPC data type for SessionExtensionsList operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionExtensionsListResult
+{
+ /// Discovered extensions and their current status.
+ [JsonPropertyName("extensions")]
+ public List Extensions { get => field ??= []; set; }
+}
+
+/// RPC data type for SessionExtensionsList operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionExtensionsListRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionExtensionsEnable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionExtensionsEnableResult
+{
+}
+
+/// RPC data type for SessionExtensionsEnable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionExtensionsEnableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Source-qualified extension ID to enable.
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionExtensionsDisable operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionExtensionsDisableResult
+{
+}
+
+/// RPC data type for SessionExtensionsDisable operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionExtensionsDisableRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Source-qualified extension ID to disable.
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = string.Empty;
+}
+
+/// RPC data type for SessionExtensionsReload operations.
+[Experimental(Diagnostics.Experimental)]
+public class SessionExtensionsReloadResult
+{
+}
+
+/// RPC data type for SessionExtensionsReload operations.
+[Experimental(Diagnostics.Experimental)]
+internal class SessionExtensionsReloadRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+}
+
/// RPC data type for SessionCompactionCompact operations.
[Experimental(Diagnostics.Experimental)]
public class SessionCompactionCompactResult
@@ -631,6 +976,74 @@ internal class SessionToolsHandlePendingToolCallRequest
public string? Error { get; set; }
}
+/// RPC data type for SessionCommandsHandlePendingCommand operations.
+public class SessionCommandsHandlePendingCommandResult
+{
+ /// Gets or sets the success value.
+ [JsonPropertyName("success")]
+ public bool Success { get; set; }
+}
+
+/// RPC data type for SessionCommandsHandlePendingCommand operations.
+internal class SessionCommandsHandlePendingCommandRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Request ID from the command invocation event.
+ [JsonPropertyName("requestId")]
+ public string RequestId { get; set; } = string.Empty;
+
+ /// Error message if the command handler failed.
+ [JsonPropertyName("error")]
+ public string? Error { get; set; }
+}
+
+/// RPC data type for SessionUiElicitation operations.
+public class SessionUiElicitationResult
+{
+ /// The user's response: accept (submitted), decline (rejected), or cancel (dismissed).
+ [JsonPropertyName("action")]
+ public SessionUiElicitationResultAction Action { get; set; }
+
+ /// The form values submitted by the user (present when action is 'accept').
+ [JsonPropertyName("content")]
+ public Dictionary? Content { get; set; }
+}
+
+/// JSON Schema describing the form fields to present to the user.
+public class SessionUiElicitationRequestRequestedSchema
+{
+ /// Schema type indicator (always 'object').
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = string.Empty;
+
+ /// Form field definitions, keyed by field name.
+ [JsonPropertyName("properties")]
+ public Dictionary Properties { get => field ??= []; set; }
+
+ /// List of required field names.
+ [JsonPropertyName("required")]
+ public List? Required { get; set; }
+}
+
+/// RPC data type for SessionUiElicitation operations.
+internal class SessionUiElicitationRequest
+{
+ /// Target session identifier.
+ [JsonPropertyName("sessionId")]
+ public string SessionId { get; set; } = string.Empty;
+
+ /// Message describing what information is needed from the user.
+ [JsonPropertyName("message")]
+ public string Message { get; set; } = string.Empty;
+
+ /// JSON Schema describing the form fields to present to the user.
+ [JsonPropertyName("requestedSchema")]
+ public SessionUiElicitationRequestRequestedSchema RequestedSchema { get => field ??= new(); set; }
+}
+
/// RPC data type for SessionPermissionsHandlePendingPermissionRequest operations.
public class SessionPermissionsHandlePendingPermissionRequestResult
{
@@ -739,6 +1152,76 @@ public enum SessionModeGetResultMode
}
+/// Connection status: connected, failed, pending, disabled, or not_configured.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum ServerStatus
+{
+ /// The connected variant.
+ [JsonStringEnumMemberName("connected")]
+ Connected,
+ /// The failed variant.
+ [JsonStringEnumMemberName("failed")]
+ Failed,
+ /// The pending variant.
+ [JsonStringEnumMemberName("pending")]
+ Pending,
+ /// The disabled variant.
+ [JsonStringEnumMemberName("disabled")]
+ Disabled,
+ /// The not_configured variant.
+ [JsonStringEnumMemberName("not_configured")]
+ NotConfigured,
+}
+
+
+/// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/).
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum ExtensionSource
+{
+ /// The project variant.
+ [JsonStringEnumMemberName("project")]
+ Project,
+ /// The user variant.
+ [JsonStringEnumMemberName("user")]
+ User,
+}
+
+
+/// Current status: running, disabled, failed, or starting.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum ExtensionStatus
+{
+ /// The running variant.
+ [JsonStringEnumMemberName("running")]
+ Running,
+ /// The disabled variant.
+ [JsonStringEnumMemberName("disabled")]
+ Disabled,
+ /// The failed variant.
+ [JsonStringEnumMemberName("failed")]
+ Failed,
+ /// The starting variant.
+ [JsonStringEnumMemberName("starting")]
+ Starting,
+}
+
+
+/// The user's response: accept (submitted), decline (rejected), or cancel (dismissed).
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionUiElicitationResultAction
+{
+ /// The accept variant.
+ [JsonStringEnumMemberName("accept")]
+ Accept,
+ /// The decline variant.
+ [JsonStringEnumMemberName("decline")]
+ Decline,
+ /// The cancel variant.
+ [JsonStringEnumMemberName("cancel")]
+ Cancel,
+}
+
+
/// Signal to send (default: SIGTERM).
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SessionShellKillRequestSignal
@@ -853,8 +1336,14 @@ internal SessionRpc(JsonRpc rpc, string sessionId)
Workspace = new WorkspaceApi(rpc, sessionId);
Fleet = new FleetApi(rpc, sessionId);
Agent = new AgentApi(rpc, sessionId);
+ Skills = new SkillsApi(rpc, sessionId);
+ Mcp = new McpApi(rpc, sessionId);
+ Plugins = new PluginsApi(rpc, sessionId);
+ Extensions = new ExtensionsApi(rpc, sessionId);
Compaction = new CompactionApi(rpc, sessionId);
Tools = new ToolsApi(rpc, sessionId);
+ Commands = new CommandsApi(rpc, sessionId);
+ Ui = new UiApi(rpc, sessionId);
Permissions = new PermissionsApi(rpc, sessionId);
Shell = new ShellApi(rpc, sessionId);
}
@@ -877,12 +1366,30 @@ internal SessionRpc(JsonRpc rpc, string sessionId)
/// Agent APIs.
public AgentApi Agent { get; }
+ /// Skills APIs.
+ public SkillsApi Skills { get; }
+
+ /// Mcp APIs.
+ public McpApi Mcp { get; }
+
+ /// Plugins APIs.
+ public PluginsApi Plugins { get; }
+
+ /// Extensions APIs.
+ public ExtensionsApi Extensions { get; }
+
/// Compaction APIs.
public CompactionApi Compaction { get; }
/// Tools APIs.
public ToolsApi Tools { get; }
+ /// Commands APIs.
+ public CommandsApi Commands { get; }
+
+ /// Ui APIs.
+ public UiApi Ui { get; }
+
/// Permissions APIs.
public PermissionsApi Permissions { get; }
@@ -890,9 +1397,9 @@ internal SessionRpc(JsonRpc rpc, string sessionId)
public ShellApi Shell { get; }
/// Calls "session.log".
- public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default)
+ public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, string? url = null, CancellationToken cancellationToken = default)
{
- var request = new SessionLogRequest { SessionId = _sessionId, Message = message, Level = level, Ephemeral = ephemeral };
+ var request = new SessionLogRequest { SessionId = _sessionId, Message = message, Level = level, Ephemeral = ephemeral, Url = url };
return await CopilotClient.InvokeRpcAsync(_rpc, "session.log", [request], cancellationToken);
}
}
@@ -1080,6 +1587,160 @@ public async Task DeselectAsync(CancellationToken ca
var request = new SessionAgentDeselectRequest { SessionId = _sessionId };
return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.deselect", [request], cancellationToken);
}
+
+ /// Calls "session.agent.reload".
+ public async Task ReloadAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionAgentReloadRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.reload", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Skills APIs.
+[Experimental(Diagnostics.Experimental)]
+public class SkillsApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal SkillsApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.skills.list".
+ public async Task ListAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionSkillsListRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.list", [request], cancellationToken);
+ }
+
+ /// Calls "session.skills.enable".
+ public async Task EnableAsync(string name, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionSkillsEnableRequest { SessionId = _sessionId, Name = name };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.enable", [request], cancellationToken);
+ }
+
+ /// Calls "session.skills.disable".
+ public async Task DisableAsync(string name, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionSkillsDisableRequest { SessionId = _sessionId, Name = name };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.disable", [request], cancellationToken);
+ }
+
+ /// Calls "session.skills.reload".
+ public async Task ReloadAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionSkillsReloadRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.skills.reload", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Mcp APIs.
+[Experimental(Diagnostics.Experimental)]
+public class McpApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal McpApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.mcp.list".
+ public async Task ListAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionMcpListRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.list", [request], cancellationToken);
+ }
+
+ /// Calls "session.mcp.enable".
+ public async Task EnableAsync(string serverName, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionMcpEnableRequest { SessionId = _sessionId, ServerName = serverName };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.enable", [request], cancellationToken);
+ }
+
+ /// Calls "session.mcp.disable".
+ public async Task DisableAsync(string serverName, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionMcpDisableRequest { SessionId = _sessionId, ServerName = serverName };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.disable", [request], cancellationToken);
+ }
+
+ /// Calls "session.mcp.reload".
+ public async Task ReloadAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionMcpReloadRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.reload", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Plugins APIs.
+[Experimental(Diagnostics.Experimental)]
+public class PluginsApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal PluginsApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.plugins.list".
+ public async Task ListAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionPluginsListRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.plugins.list", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Extensions APIs.
+[Experimental(Diagnostics.Experimental)]
+public class ExtensionsApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal ExtensionsApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.extensions.list".
+ public async Task ListAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionExtensionsListRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.list", [request], cancellationToken);
+ }
+
+ /// Calls "session.extensions.enable".
+ public async Task EnableAsync(string id, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionExtensionsEnableRequest { SessionId = _sessionId, Id = id };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.enable", [request], cancellationToken);
+ }
+
+ /// Calls "session.extensions.disable".
+ public async Task DisableAsync(string id, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionExtensionsDisableRequest { SessionId = _sessionId, Id = id };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.disable", [request], cancellationToken);
+ }
+
+ /// Calls "session.extensions.reload".
+ public async Task ReloadAsync(CancellationToken cancellationToken = default)
+ {
+ var request = new SessionExtensionsReloadRequest { SessionId = _sessionId };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.extensions.reload", [request], cancellationToken);
+ }
}
/// Provides session-scoped Compaction APIs.
@@ -1123,6 +1784,46 @@ public async Task HandlePendingToolCall
}
}
+/// Provides session-scoped Commands APIs.
+public class CommandsApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal CommandsApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.commands.handlePendingCommand".
+ public async Task HandlePendingCommandAsync(string requestId, string? error = null, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionCommandsHandlePendingCommandRequest { SessionId = _sessionId, RequestId = requestId, Error = error };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.commands.handlePendingCommand", [request], cancellationToken);
+ }
+}
+
+/// Provides session-scoped Ui APIs.
+public class UiApi
+{
+ private readonly JsonRpc _rpc;
+ private readonly string _sessionId;
+
+ internal UiApi(JsonRpc rpc, string sessionId)
+ {
+ _rpc = rpc;
+ _sessionId = sessionId;
+ }
+
+ /// Calls "session.ui.elicitation".
+ public async Task ElicitationAsync(string message, SessionUiElicitationRequestRequestedSchema requestedSchema, CancellationToken cancellationToken = default)
+ {
+ var request = new SessionUiElicitationRequest { SessionId = _sessionId, Message = message, RequestedSchema = requestedSchema };
+ return await CopilotClient.InvokeRpcAsync(_rpc, "session.ui.elicitation", [request], cancellationToken);
+ }
+}
+
/// Provides session-scoped Permissions APIs.
public class PermissionsApi
{
@@ -1177,6 +1878,7 @@ public async Task KillAsync(string processId, SessionShe
[JsonSerializable(typeof(AccountGetQuotaResult))]
[JsonSerializable(typeof(AccountGetQuotaResultQuotaSnapshotsValue))]
[JsonSerializable(typeof(Agent))]
+[JsonSerializable(typeof(Extension))]
[JsonSerializable(typeof(Model))]
[JsonSerializable(typeof(ModelBilling))]
[JsonSerializable(typeof(ModelCapabilities))]
@@ -1186,6 +1888,8 @@ public async Task KillAsync(string processId, SessionShe
[JsonSerializable(typeof(ModelsListResult))]
[JsonSerializable(typeof(PingRequest))]
[JsonSerializable(typeof(PingResult))]
+[JsonSerializable(typeof(Plugin))]
+[JsonSerializable(typeof(Server))]
[JsonSerializable(typeof(SessionAgentDeselectRequest))]
[JsonSerializable(typeof(SessionAgentDeselectResult))]
[JsonSerializable(typeof(SessionAgentGetCurrentRequest))]
@@ -1193,15 +1897,35 @@ public async Task KillAsync(string processId, SessionShe
[JsonSerializable(typeof(SessionAgentGetCurrentResultAgent))]
[JsonSerializable(typeof(SessionAgentListRequest))]
[JsonSerializable(typeof(SessionAgentListResult))]
+[JsonSerializable(typeof(SessionAgentReloadRequest))]
+[JsonSerializable(typeof(SessionAgentReloadResult))]
[JsonSerializable(typeof(SessionAgentSelectRequest))]
[JsonSerializable(typeof(SessionAgentSelectResult))]
[JsonSerializable(typeof(SessionAgentSelectResultAgent))]
+[JsonSerializable(typeof(SessionCommandsHandlePendingCommandRequest))]
+[JsonSerializable(typeof(SessionCommandsHandlePendingCommandResult))]
[JsonSerializable(typeof(SessionCompactionCompactRequest))]
[JsonSerializable(typeof(SessionCompactionCompactResult))]
+[JsonSerializable(typeof(SessionExtensionsDisableRequest))]
+[JsonSerializable(typeof(SessionExtensionsDisableResult))]
+[JsonSerializable(typeof(SessionExtensionsEnableRequest))]
+[JsonSerializable(typeof(SessionExtensionsEnableResult))]
+[JsonSerializable(typeof(SessionExtensionsListRequest))]
+[JsonSerializable(typeof(SessionExtensionsListResult))]
+[JsonSerializable(typeof(SessionExtensionsReloadRequest))]
+[JsonSerializable(typeof(SessionExtensionsReloadResult))]
[JsonSerializable(typeof(SessionFleetStartRequest))]
[JsonSerializable(typeof(SessionFleetStartResult))]
[JsonSerializable(typeof(SessionLogRequest))]
[JsonSerializable(typeof(SessionLogResult))]
+[JsonSerializable(typeof(SessionMcpDisableRequest))]
+[JsonSerializable(typeof(SessionMcpDisableResult))]
+[JsonSerializable(typeof(SessionMcpEnableRequest))]
+[JsonSerializable(typeof(SessionMcpEnableResult))]
+[JsonSerializable(typeof(SessionMcpListRequest))]
+[JsonSerializable(typeof(SessionMcpListResult))]
+[JsonSerializable(typeof(SessionMcpReloadRequest))]
+[JsonSerializable(typeof(SessionMcpReloadResult))]
[JsonSerializable(typeof(SessionModeGetRequest))]
[JsonSerializable(typeof(SessionModeGetResult))]
[JsonSerializable(typeof(SessionModeSetRequest))]
@@ -1218,18 +1942,32 @@ public async Task KillAsync(string processId, SessionShe
[JsonSerializable(typeof(SessionPlanReadResult))]
[JsonSerializable(typeof(SessionPlanUpdateRequest))]
[JsonSerializable(typeof(SessionPlanUpdateResult))]
+[JsonSerializable(typeof(SessionPluginsListRequest))]
+[JsonSerializable(typeof(SessionPluginsListResult))]
[JsonSerializable(typeof(SessionShellExecRequest))]
[JsonSerializable(typeof(SessionShellExecResult))]
[JsonSerializable(typeof(SessionShellKillRequest))]
[JsonSerializable(typeof(SessionShellKillResult))]
+[JsonSerializable(typeof(SessionSkillsDisableRequest))]
+[JsonSerializable(typeof(SessionSkillsDisableResult))]
+[JsonSerializable(typeof(SessionSkillsEnableRequest))]
+[JsonSerializable(typeof(SessionSkillsEnableResult))]
+[JsonSerializable(typeof(SessionSkillsListRequest))]
+[JsonSerializable(typeof(SessionSkillsListResult))]
+[JsonSerializable(typeof(SessionSkillsReloadRequest))]
+[JsonSerializable(typeof(SessionSkillsReloadResult))]
[JsonSerializable(typeof(SessionToolsHandlePendingToolCallRequest))]
[JsonSerializable(typeof(SessionToolsHandlePendingToolCallResult))]
+[JsonSerializable(typeof(SessionUiElicitationRequest))]
+[JsonSerializable(typeof(SessionUiElicitationRequestRequestedSchema))]
+[JsonSerializable(typeof(SessionUiElicitationResult))]
[JsonSerializable(typeof(SessionWorkspaceCreateFileRequest))]
[JsonSerializable(typeof(SessionWorkspaceCreateFileResult))]
[JsonSerializable(typeof(SessionWorkspaceListFilesRequest))]
[JsonSerializable(typeof(SessionWorkspaceListFilesResult))]
[JsonSerializable(typeof(SessionWorkspaceReadFileRequest))]
[JsonSerializable(typeof(SessionWorkspaceReadFileResult))]
+[JsonSerializable(typeof(Skill))]
[JsonSerializable(typeof(Tool))]
[JsonSerializable(typeof(ToolsListRequest))]
[JsonSerializable(typeof(ToolsListResult))]
diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs
index 08c6bf5e0..2821052d0 100644
--- a/dotnet/src/Generated/SessionEvents.cs
+++ b/dotnet/src/Generated/SessionEvents.cs
@@ -17,7 +17,7 @@ namespace GitHub.Copilot.SDK;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "type",
- UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]
+ IgnoreUnrecognizedTypeDiscriminators = true)]
[JsonDerivedType(typeof(AbortEvent), "abort")]
[JsonDerivedType(typeof(AssistantIntentEvent), "assistant.intent")]
[JsonDerivedType(typeof(AssistantMessageEvent), "assistant.message")]
@@ -29,7 +29,9 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(AssistantTurnStartEvent), "assistant.turn_start")]
[JsonDerivedType(typeof(AssistantUsageEvent), "assistant.usage")]
[JsonDerivedType(typeof(CommandCompletedEvent), "command.completed")]
+[JsonDerivedType(typeof(CommandExecuteEvent), "command.execute")]
[JsonDerivedType(typeof(CommandQueuedEvent), "command.queued")]
+[JsonDerivedType(typeof(CommandsChangedEvent), "commands.changed")]
[JsonDerivedType(typeof(ElicitationCompletedEvent), "elicitation.completed")]
[JsonDerivedType(typeof(ElicitationRequestedEvent), "elicitation.requested")]
[JsonDerivedType(typeof(ExitPlanModeCompletedEvent), "exit_plan_mode.completed")]
@@ -38,6 +40,8 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(ExternalToolRequestedEvent), "external_tool.requested")]
[JsonDerivedType(typeof(HookEndEvent), "hook.end")]
[JsonDerivedType(typeof(HookStartEvent), "hook.start")]
+[JsonDerivedType(typeof(McpOauthCompletedEvent), "mcp.oauth_completed")]
+[JsonDerivedType(typeof(McpOauthRequiredEvent), "mcp.oauth_required")]
[JsonDerivedType(typeof(PendingMessagesModifiedEvent), "pending_messages.modified")]
[JsonDerivedType(typeof(PermissionCompletedEvent), "permission.completed")]
[JsonDerivedType(typeof(PermissionRequestedEvent), "permission.requested")]
@@ -46,14 +50,18 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(SessionCompactionStartEvent), "session.compaction_start")]
[JsonDerivedType(typeof(SessionContextChangedEvent), "session.context_changed")]
[JsonDerivedType(typeof(SessionErrorEvent), "session.error")]
+[JsonDerivedType(typeof(SessionExtensionsLoadedEvent), "session.extensions_loaded")]
[JsonDerivedType(typeof(SessionHandoffEvent), "session.handoff")]
[JsonDerivedType(typeof(SessionIdleEvent), "session.idle")]
[JsonDerivedType(typeof(SessionInfoEvent), "session.info")]
+[JsonDerivedType(typeof(SessionMcpServerStatusChangedEvent), "session.mcp_server_status_changed")]
+[JsonDerivedType(typeof(SessionMcpServersLoadedEvent), "session.mcp_servers_loaded")]
[JsonDerivedType(typeof(SessionModeChangedEvent), "session.mode_changed")]
[JsonDerivedType(typeof(SessionModelChangeEvent), "session.model_change")]
[JsonDerivedType(typeof(SessionPlanChangedEvent), "session.plan_changed")]
[JsonDerivedType(typeof(SessionResumeEvent), "session.resume")]
[JsonDerivedType(typeof(SessionShutdownEvent), "session.shutdown")]
+[JsonDerivedType(typeof(SessionSkillsLoadedEvent), "session.skills_loaded")]
[JsonDerivedType(typeof(SessionSnapshotRewindEvent), "session.snapshot_rewind")]
[JsonDerivedType(typeof(SessionStartEvent), "session.start")]
[JsonDerivedType(typeof(SessionTaskCompleteEvent), "session.task_complete")]
@@ -79,7 +87,7 @@ namespace GitHub.Copilot.SDK;
[JsonDerivedType(typeof(UserInputCompletedEvent), "user_input.completed")]
[JsonDerivedType(typeof(UserInputRequestedEvent), "user_input.requested")]
[JsonDerivedType(typeof(UserMessageEvent), "user.message")]
-public abstract partial class SessionEvent
+public partial class SessionEvent
{
/// Unique event identifier (UUID v4), generated when the event is emitted.
[JsonPropertyName("id")]
@@ -102,7 +110,7 @@ public abstract partial class SessionEvent
/// The event type discriminator.
///
[JsonIgnore]
- public abstract string Type { get; }
+ public virtual string Type => "unknown";
/// Deserializes a JSON string into a .
public static SessionEvent FromJson(string json) =>
@@ -337,7 +345,7 @@ public partial class SessionUsageInfoEvent : SessionEvent
public required SessionUsageInfoData Data { get; set; }
}
-/// Empty payload; the event signals that LLM-powered conversation compaction has begun.
+/// Context window breakdown at the start of LLM-powered conversation compaction.
/// Represents the session.compaction_start event.
public partial class SessionCompactionStartEvent : SessionEvent
{
@@ -363,7 +371,7 @@ public partial class SessionCompactionCompleteEvent : SessionEvent
public required SessionCompactionCompleteData Data { get; set; }
}
-/// Task completion notification with optional summary from the agent.
+/// Task completion notification with summary from the agent.
/// Represents the session.task_complete event.
public partial class SessionTaskCompleteEvent : SessionEvent
{
@@ -376,8 +384,7 @@ public partial class SessionTaskCompleteEvent : SessionEvent
public required SessionTaskCompleteData Data { get; set; }
}
-/// User message content with optional attachments, source information, and interaction metadata.
-/// Represents the user.message event.
+/// Represents the user.message event.
public partial class UserMessageEvent : SessionEvent
{
///
@@ -779,7 +786,7 @@ public partial class UserInputCompletedEvent : SessionEvent
public required UserInputCompletedData Data { get; set; }
}
-/// Structured form elicitation request with JSON schema definition for form fields.
+/// Elicitation request; may be form-based (structured input) or URL-based (browser redirect).
/// Represents the elicitation.requested event.
public partial class ElicitationRequestedEvent : SessionEvent
{
@@ -805,6 +812,32 @@ public partial class ElicitationCompletedEvent : SessionEvent
public required ElicitationCompletedData Data { get; set; }
}
+/// OAuth authentication request for an MCP server.
+/// Represents the mcp.oauth_required event.
+public partial class McpOauthRequiredEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "mcp.oauth_required";
+
+ /// The mcp.oauth_required event payload.
+ [JsonPropertyName("data")]
+ public required McpOauthRequiredData Data { get; set; }
+}
+
+/// MCP OAuth request completion notification.
+/// Represents the mcp.oauth_completed event.
+public partial class McpOauthCompletedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "mcp.oauth_completed";
+
+ /// The mcp.oauth_completed event payload.
+ [JsonPropertyName("data")]
+ public required McpOauthCompletedData Data { get; set; }
+}
+
/// External tool invocation request for client-side tool execution.
/// Represents the external_tool.requested event.
public partial class ExternalToolRequestedEvent : SessionEvent
@@ -844,6 +877,19 @@ public partial class CommandQueuedEvent : SessionEvent
public required CommandQueuedData Data { get; set; }
}
+/// Registered command dispatch request routed to the owning client.
+/// Represents the command.execute event.
+public partial class CommandExecuteEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "command.execute";
+
+ /// The command.execute event payload.
+ [JsonPropertyName("data")]
+ public required CommandExecuteData Data { get; set; }
+}
+
/// Queued command completion notification signaling UI dismissal.
/// Represents the command.completed event.
public partial class CommandCompletedEvent : SessionEvent
@@ -857,6 +903,19 @@ public partial class CommandCompletedEvent : SessionEvent
public required CommandCompletedData Data { get; set; }
}
+/// SDK command registration change notification.
+/// Represents the commands.changed event.
+public partial class CommandsChangedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "commands.changed";
+
+ /// The commands.changed event payload.
+ [JsonPropertyName("data")]
+ public required CommandsChangedData Data { get; set; }
+}
+
/// Plan approval request with plan content and available user actions.
/// Represents the exit_plan_mode.requested event.
public partial class ExitPlanModeRequestedEvent : SessionEvent
@@ -907,6 +966,54 @@ public partial class SessionBackgroundTasksChangedEvent : SessionEvent
public required SessionBackgroundTasksChangedData Data { get; set; }
}
+/// Represents the session.skills_loaded event.
+public partial class SessionSkillsLoadedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.skills_loaded";
+
+ /// The session.skills_loaded event payload.
+ [JsonPropertyName("data")]
+ public required SessionSkillsLoadedData Data { get; set; }
+}
+
+/// Represents the session.mcp_servers_loaded event.
+public partial class SessionMcpServersLoadedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.mcp_servers_loaded";
+
+ /// The session.mcp_servers_loaded event payload.
+ [JsonPropertyName("data")]
+ public required SessionMcpServersLoadedData Data { get; set; }
+}
+
+/// Represents the session.mcp_server_status_changed event.
+public partial class SessionMcpServerStatusChangedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.mcp_server_status_changed";
+
+ /// The session.mcp_server_status_changed event payload.
+ [JsonPropertyName("data")]
+ public required SessionMcpServerStatusChangedData Data { get; set; }
+}
+
+/// Represents the session.extensions_loaded event.
+public partial class SessionExtensionsLoadedEvent : SessionEvent
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "session.extensions_loaded";
+
+ /// The session.extensions_loaded event payload.
+ [JsonPropertyName("data")]
+ public required SessionExtensionsLoadedData Data { get; set; }
+}
+
/// Session initialization metadata including context and configuration.
public partial class SessionStartData
{
@@ -1008,6 +1115,11 @@ public partial class SessionErrorData
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("providerCallId")]
public string? ProviderCallId { get; set; }
+
+ /// Optional URL associated with this error that the user can open in a browser.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
/// Payload indicating the agent is idle; includes any background tasks still in flight.
@@ -1037,6 +1149,11 @@ public partial class SessionInfoData
/// Human-readable informational message for display in the timeline.
[JsonPropertyName("message")]
public required string Message { get; set; }
+
+ /// Optional URL associated with this message that the user can open in a browser.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
/// Warning message for timeline display with categorization.
@@ -1049,6 +1166,11 @@ public partial class SessionWarningData
/// Human-readable warning message for display in the timeline.
[JsonPropertyName("message")]
public required string Message { get; set; }
+
+ /// Optional URL associated with this warning that the user can open in a browser.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
/// Model change details including previous and new model identifiers.
@@ -1222,6 +1344,26 @@ public partial class SessionShutdownData
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("currentModel")]
public string? CurrentModel { get; set; }
+
+ /// Total tokens in context window at shutdown.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("currentTokens")]
+ public double? CurrentTokens { get; set; }
+
+ /// System message token count at shutdown.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("systemTokens")]
+ public double? SystemTokens { get; set; }
+
+ /// Non-system message token count at shutdown.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("conversationTokens")]
+ public double? ConversationTokens { get; set; }
+
+ /// Tool definitions token count at shutdown.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolDefinitionsTokens")]
+ public double? ToolDefinitionsTokens { get; set; }
}
/// Updated working directory and git context after the change.
@@ -1276,11 +1418,45 @@ public partial class SessionUsageInfoData
/// Current number of messages in the conversation.
[JsonPropertyName("messagesLength")]
public required double MessagesLength { get; set; }
+
+ /// Token count from system message(s).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("systemTokens")]
+ public double? SystemTokens { get; set; }
+
+ /// Token count from non-system messages (user, assistant, tool).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("conversationTokens")]
+ public double? ConversationTokens { get; set; }
+
+ /// Token count from tool definitions.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolDefinitionsTokens")]
+ public double? ToolDefinitionsTokens { get; set; }
+
+ /// Whether this is the first usage_info event emitted in this session.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("isInitial")]
+ public bool? IsInitial { get; set; }
}
-/// Empty payload; the event signals that LLM-powered conversation compaction has begun.
+/// Context window breakdown at the start of LLM-powered conversation compaction.
public partial class SessionCompactionStartData
{
+ /// Token count from system message(s) at compaction start.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("systemTokens")]
+ public double? SystemTokens { get; set; }
+
+ /// Token count from non-system messages (user, assistant, tool) at compaction start.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("conversationTokens")]
+ public double? ConversationTokens { get; set; }
+
+ /// Token count from tool definitions at compaction start.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolDefinitionsTokens")]
+ public double? ToolDefinitionsTokens { get; set; }
}
/// Conversation compaction results including success status, metrics, and optional error details.
@@ -1344,18 +1520,38 @@ public partial class SessionCompactionCompleteData
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("requestId")]
public string? RequestId { get; set; }
+
+ /// Token count from system message(s) after compaction.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("systemTokens")]
+ public double? SystemTokens { get; set; }
+
+ /// Token count from non-system messages (user, assistant, tool) after compaction.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("conversationTokens")]
+ public double? ConversationTokens { get; set; }
+
+ /// Token count from tool definitions after compaction.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolDefinitionsTokens")]
+ public double? ToolDefinitionsTokens { get; set; }
}
-/// Task completion notification with optional summary from the agent.
+/// Task completion notification with summary from the agent.
public partial class SessionTaskCompleteData
{
- /// Optional summary of the completed task, provided by the agent.
+ /// Summary of the completed task, provided by the agent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("summary")]
public string? Summary { get; set; }
+
+ /// Whether the tool call succeeded. False when validation failed (e.g., invalid arguments).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("success")]
+ public bool? Success { get; set; }
}
-/// User message content with optional attachments, source information, and interaction metadata.
+/// Event payload for .
public partial class UserMessageData
{
/// The user's message text as displayed in the timeline.
@@ -1372,10 +1568,10 @@ public partial class UserMessageData
[JsonPropertyName("attachments")]
public UserMessageDataAttachmentsItem[]? Attachments { get; set; }
- /// Origin of this message, used for timeline filtering and telemetry (e.g., "user", "autopilot", "skill", or "command").
+ /// Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user).
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("source")]
- public UserMessageDataSource? Source { get; set; }
+ public string? Source { get; set; }
/// The agent mode that was active when this message was sent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
@@ -1953,6 +2149,11 @@ public partial class UserInputRequestedData
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("allowFreeform")]
public bool? AllowFreeform { get; set; }
+
+ /// The LLM-assigned tool call ID that triggered this request; used by remote UIs to correlate responses.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
}
/// User input request completion notification signaling UI dismissal.
@@ -1963,25 +2164,41 @@ public partial class UserInputCompletedData
public required string RequestId { get; set; }
}
-/// Structured form elicitation request with JSON schema definition for form fields.
+/// Elicitation request; may be form-based (structured input) or URL-based (browser redirect).
public partial class ElicitationRequestedData
{
/// Unique identifier for this elicitation request; used to respond via session.respondToElicitation().
[JsonPropertyName("requestId")]
public required string RequestId { get; set; }
+ /// Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id for remote UIs.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("toolCallId")]
+ public string? ToolCallId { get; set; }
+
+ /// The source that initiated the request (MCP server name, or absent for agent-initiated).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("elicitationSource")]
+ public string? ElicitationSource { get; set; }
+
/// Message describing what information is needed from the user.
[JsonPropertyName("message")]
public required string Message { get; set; }
- /// Elicitation mode; currently only "form" is supported. Defaults to "form" when absent.
+ /// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("mode")]
- public string? Mode { get; set; }
+ public ElicitationRequestedDataMode? Mode { get; set; }
- /// JSON Schema describing the form fields to present to the user.
+ /// JSON Schema describing the form fields to present to the user (form mode only).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("requestedSchema")]
- public required ElicitationRequestedDataRequestedSchema RequestedSchema { get; set; }
+ public ElicitationRequestedDataRequestedSchema? RequestedSchema { get; set; }
+
+ /// URL to open in the user's browser (url mode only).
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("url")]
+ public string? Url { get; set; }
}
/// Elicitation request completion notification signaling UI dismissal.
@@ -1992,6 +2209,35 @@ public partial class ElicitationCompletedData
public required string RequestId { get; set; }
}
+/// OAuth authentication request for an MCP server.
+public partial class McpOauthRequiredData
+{
+ /// Unique identifier for this OAuth request; used to respond via session.respondToMcpOAuth().
+ [JsonPropertyName("requestId")]
+ public required string RequestId { get; set; }
+
+ /// Display name of the MCP server that requires OAuth.
+ [JsonPropertyName("serverName")]
+ public required string ServerName { get; set; }
+
+ /// URL of the MCP server that requires OAuth.
+ [JsonPropertyName("serverUrl")]
+ public required string ServerUrl { get; set; }
+
+ /// Static OAuth client configuration, if the server specifies one.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("staticClientConfig")]
+ public McpOauthRequiredDataStaticClientConfig? StaticClientConfig { get; set; }
+}
+
+/// MCP OAuth request completion notification.
+public partial class McpOauthCompletedData
+{
+ /// Request ID of the resolved OAuth request.
+ [JsonPropertyName("requestId")]
+ public required string RequestId { get; set; }
+}
+
/// External tool invocation request for client-side tool execution.
public partial class ExternalToolRequestedData
{
@@ -2047,6 +2293,26 @@ public partial class CommandQueuedData
public required string Command { get; set; }
}
+/// Registered command dispatch request routed to the owning client.
+public partial class CommandExecuteData
+{
+ /// Unique identifier; used to respond via session.commands.handlePendingCommand().
+ [JsonPropertyName("requestId")]
+ public required string RequestId { get; set; }
+
+ /// The full command text (e.g., /deploy production).
+ [JsonPropertyName("command")]
+ public required string Command { get; set; }
+
+ /// Command name without leading /.
+ [JsonPropertyName("commandName")]
+ public required string CommandName { get; set; }
+
+ /// Raw argument string after the command name.
+ [JsonPropertyName("args")]
+ public required string Args { get; set; }
+}
+
/// Queued command completion notification signaling UI dismissal.
public partial class CommandCompletedData
{
@@ -2055,6 +2321,14 @@ public partial class CommandCompletedData
public required string RequestId { get; set; }
}
+/// SDK command registration change notification.
+public partial class CommandsChangedData
+{
+ /// Current list of registered SDK commands.
+ [JsonPropertyName("commands")]
+ public required CommandsChangedDataCommandsItem[] Commands { get; set; }
+}
+
/// Plan approval request with plan content and available user actions.
public partial class ExitPlanModeRequestedData
{
@@ -2100,6 +2374,42 @@ public partial class SessionBackgroundTasksChangedData
{
}
+/// Event payload for .
+public partial class SessionSkillsLoadedData
+{
+ /// Array of resolved skill metadata.
+ [JsonPropertyName("skills")]
+ public required SessionSkillsLoadedDataSkillsItem[] Skills { get; set; }
+}
+
+/// Event payload for .
+public partial class SessionMcpServersLoadedData
+{
+ /// Array of MCP server status summaries.
+ [JsonPropertyName("servers")]
+ public required SessionMcpServersLoadedDataServersItem[] Servers { get; set; }
+}
+
+/// Event payload for .
+public partial class SessionMcpServerStatusChangedData
+{
+ /// Name of the MCP server whose status changed.
+ [JsonPropertyName("serverName")]
+ public required string ServerName { get; set; }
+
+ /// New connection status: connected, failed, pending, disabled, or not_configured.
+ [JsonPropertyName("status")]
+ public required SessionMcpServersLoadedDataServersItemStatus Status { get; set; }
+}
+
+/// Event payload for .
+public partial class SessionExtensionsLoadedData
+{
+ /// Array of discovered extensions and their status.
+ [JsonPropertyName("extensions")]
+ public required SessionExtensionsLoadedDataExtensionsItem[] Extensions { get; set; }
+}
+
/// Working directory and git context at session start.
/// Nested data type for SessionStartDataContext.
public partial class SessionStartDataContext
@@ -2787,6 +3097,27 @@ public partial class SystemNotificationDataKindAgentCompleted : SystemNotificati
public string? Prompt { get; set; }
}
+/// The agent_idle variant of .
+public partial class SystemNotificationDataKindAgentIdle : SystemNotificationDataKind
+{
+ ///
+ [JsonIgnore]
+ public override string Type => "agent_idle";
+
+ /// Unique identifier of the background agent.
+ [JsonPropertyName("agentId")]
+ public required string AgentId { get; set; }
+
+ /// Type of the agent (e.g., explore, task, general-purpose).
+ [JsonPropertyName("agentType")]
+ public required string AgentType { get; set; }
+
+ /// Human-readable description of the agent task.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+}
+
/// The shell_completed variant of .
public partial class SystemNotificationDataKindShellCompleted : SystemNotificationDataKind
{
@@ -2832,6 +3163,7 @@ public partial class SystemNotificationDataKindShellDetachedCompleted : SystemNo
TypeDiscriminatorPropertyName = "type",
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
[JsonDerivedType(typeof(SystemNotificationDataKindAgentCompleted), "agent_completed")]
+[JsonDerivedType(typeof(SystemNotificationDataKindAgentIdle), "agent_idle")]
[JsonDerivedType(typeof(SystemNotificationDataKindShellCompleted), "shell_completed")]
[JsonDerivedType(typeof(SystemNotificationDataKindShellDetachedCompleted), "shell_detached_completed")]
public partial class SystemNotificationDataKind
@@ -3130,7 +3462,7 @@ public partial class PermissionCompletedDataResult
public required PermissionCompletedDataResultKind Kind { get; set; }
}
-/// JSON Schema describing the form fields to present to the user.
+/// JSON Schema describing the form fields to present to the user (form mode only).
/// Nested data type for ElicitationRequestedDataRequestedSchema.
public partial class ElicitationRequestedDataRequestedSchema
{
@@ -3148,6 +3480,104 @@ public partial class ElicitationRequestedDataRequestedSchema
public string[]? Required { get; set; }
}
+/// Static OAuth client configuration, if the server specifies one.
+/// Nested data type for McpOauthRequiredDataStaticClientConfig.
+public partial class McpOauthRequiredDataStaticClientConfig
+{
+ /// OAuth client ID for the server.
+ [JsonPropertyName("clientId")]
+ public required string ClientId { get; set; }
+
+ /// Whether this is a public OAuth client.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("publicClient")]
+ public bool? PublicClient { get; set; }
+}
+
+/// Nested data type for CommandsChangedDataCommandsItem.
+public partial class CommandsChangedDataCommandsItem
+{
+ /// Gets or sets the name value.
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ /// Gets or sets the description value.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("description")]
+ public string? Description { get; set; }
+}
+
+/// Nested data type for SessionSkillsLoadedDataSkillsItem.
+public partial class SessionSkillsLoadedDataSkillsItem
+{
+ /// Unique identifier for the skill.
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ /// Description of what the skill does.
+ [JsonPropertyName("description")]
+ public required string Description { get; set; }
+
+ /// Source location type of the skill (e.g., project, personal, plugin).
+ [JsonPropertyName("source")]
+ public required string Source { get; set; }
+
+ /// Whether the skill can be invoked by the user as a slash command.
+ [JsonPropertyName("userInvocable")]
+ public required bool UserInvocable { get; set; }
+
+ /// Whether the skill is currently enabled.
+ [JsonPropertyName("enabled")]
+ public required bool Enabled { get; set; }
+
+ /// Absolute path to the skill file, if available.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("path")]
+ public string? Path { get; set; }
+}
+
+/// Nested data type for SessionMcpServersLoadedDataServersItem.
+public partial class SessionMcpServersLoadedDataServersItem
+{
+ /// Server name (config key).
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ /// Connection status: connected, failed, pending, disabled, or not_configured.
+ [JsonPropertyName("status")]
+ public required SessionMcpServersLoadedDataServersItemStatus Status { get; set; }
+
+ /// Configuration source: user, workspace, plugin, or builtin.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("source")]
+ public string? Source { get; set; }
+
+ /// Error message if the server failed to connect.
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonPropertyName("error")]
+ public string? Error { get; set; }
+}
+
+/// Nested data type for SessionExtensionsLoadedDataExtensionsItem.
+public partial class SessionExtensionsLoadedDataExtensionsItem
+{
+ /// Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper').
+ [JsonPropertyName("id")]
+ public required string Id { get; set; }
+
+ /// Extension name (directory name).
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ /// Discovery source.
+ [JsonPropertyName("source")]
+ public required SessionExtensionsLoadedDataExtensionsItemSource Source { get; set; }
+
+ /// Current status: running, disabled, failed, or starting.
+ [JsonPropertyName("status")]
+ public required SessionExtensionsLoadedDataExtensionsItemStatus Status { get; set; }
+}
+
/// Hosting platform type of the repository (github or ado).
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum SessionStartDataContextHostType
@@ -3226,42 +3656,6 @@ public enum UserMessageDataAttachmentsItemGithubReferenceReferenceType
Discussion,
}
-/// Origin of this message, used for timeline filtering and telemetry (e.g., "user", "autopilot", "skill", or "command").
-[JsonConverter(typeof(JsonStringEnumConverter))]
-public enum UserMessageDataSource
-{
- /// The user variant.
- [JsonStringEnumMemberName("user")]
- User,
- /// The autopilot variant.
- [JsonStringEnumMemberName("autopilot")]
- Autopilot,
- /// The skill variant.
- [JsonStringEnumMemberName("skill")]
- Skill,
- /// The system variant.
- [JsonStringEnumMemberName("system")]
- System,
- /// The command variant.
- [JsonStringEnumMemberName("command")]
- Command,
- /// The immediate-prompt variant.
- [JsonStringEnumMemberName("immediate-prompt")]
- ImmediatePrompt,
- /// The jit-instruction variant.
- [JsonStringEnumMemberName("jit-instruction")]
- JitInstruction,
- /// The snippy-blocking variant.
- [JsonStringEnumMemberName("snippy-blocking")]
- SnippyBlocking,
- /// The thinking-exhausted-continuation variant.
- [JsonStringEnumMemberName("thinking-exhausted-continuation")]
- ThinkingExhaustedContinuation,
- /// The other variant.
- [JsonStringEnumMemberName("other")]
- Other,
-}
-
/// The agent mode that was active when this message was sent.
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum UserMessageDataAgentMode
@@ -3349,6 +3743,69 @@ public enum PermissionCompletedDataResultKind
DeniedByContentExclusionPolicy,
}
+/// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum ElicitationRequestedDataMode
+{
+ /// The form variant.
+ [JsonStringEnumMemberName("form")]
+ Form,
+ /// The url variant.
+ [JsonStringEnumMemberName("url")]
+ Url,
+}
+
+/// Connection status: connected, failed, pending, disabled, or not_configured.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionMcpServersLoadedDataServersItemStatus
+{
+ /// The connected variant.
+ [JsonStringEnumMemberName("connected")]
+ Connected,
+ /// The failed variant.
+ [JsonStringEnumMemberName("failed")]
+ Failed,
+ /// The pending variant.
+ [JsonStringEnumMemberName("pending")]
+ Pending,
+ /// The disabled variant.
+ [JsonStringEnumMemberName("disabled")]
+ Disabled,
+ /// The not_configured variant.
+ [JsonStringEnumMemberName("not_configured")]
+ NotConfigured,
+}
+
+/// Discovery source.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionExtensionsLoadedDataExtensionsItemSource
+{
+ /// The project variant.
+ [JsonStringEnumMemberName("project")]
+ Project,
+ /// The user variant.
+ [JsonStringEnumMemberName("user")]
+ User,
+}
+
+/// Current status: running, disabled, failed, or starting.
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SessionExtensionsLoadedDataExtensionsItemStatus
+{
+ /// The running variant.
+ [JsonStringEnumMemberName("running")]
+ Running,
+ /// The disabled variant.
+ [JsonStringEnumMemberName("disabled")]
+ Disabled,
+ /// The failed variant.
+ [JsonStringEnumMemberName("failed")]
+ Failed,
+ /// The starting variant.
+ [JsonStringEnumMemberName("starting")]
+ Starting,
+}
+
[JsonSourceGenerationOptions(
JsonSerializerDefaults.Web,
AllowOutOfOrderMetadataProperties = true,
@@ -3379,8 +3836,13 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(AssistantUsageEvent))]
[JsonSerializable(typeof(CommandCompletedData))]
[JsonSerializable(typeof(CommandCompletedEvent))]
+[JsonSerializable(typeof(CommandExecuteData))]
+[JsonSerializable(typeof(CommandExecuteEvent))]
[JsonSerializable(typeof(CommandQueuedData))]
[JsonSerializable(typeof(CommandQueuedEvent))]
+[JsonSerializable(typeof(CommandsChangedData))]
+[JsonSerializable(typeof(CommandsChangedDataCommandsItem))]
+[JsonSerializable(typeof(CommandsChangedEvent))]
[JsonSerializable(typeof(ElicitationCompletedData))]
[JsonSerializable(typeof(ElicitationCompletedEvent))]
[JsonSerializable(typeof(ElicitationRequestedData))]
@@ -3399,6 +3861,11 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(HookEndEvent))]
[JsonSerializable(typeof(HookStartData))]
[JsonSerializable(typeof(HookStartEvent))]
+[JsonSerializable(typeof(McpOauthCompletedData))]
+[JsonSerializable(typeof(McpOauthCompletedEvent))]
+[JsonSerializable(typeof(McpOauthRequiredData))]
+[JsonSerializable(typeof(McpOauthRequiredDataStaticClientConfig))]
+[JsonSerializable(typeof(McpOauthRequiredEvent))]
[JsonSerializable(typeof(PendingMessagesModifiedData))]
[JsonSerializable(typeof(PendingMessagesModifiedEvent))]
[JsonSerializable(typeof(PermissionCompletedData))]
@@ -3429,6 +3896,9 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(SessionErrorData))]
[JsonSerializable(typeof(SessionErrorEvent))]
[JsonSerializable(typeof(SessionEvent))]
+[JsonSerializable(typeof(SessionExtensionsLoadedData))]
+[JsonSerializable(typeof(SessionExtensionsLoadedDataExtensionsItem))]
+[JsonSerializable(typeof(SessionExtensionsLoadedEvent))]
[JsonSerializable(typeof(SessionHandoffData))]
[JsonSerializable(typeof(SessionHandoffDataRepository))]
[JsonSerializable(typeof(SessionHandoffEvent))]
@@ -3439,6 +3909,11 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(SessionIdleEvent))]
[JsonSerializable(typeof(SessionInfoData))]
[JsonSerializable(typeof(SessionInfoEvent))]
+[JsonSerializable(typeof(SessionMcpServerStatusChangedData))]
+[JsonSerializable(typeof(SessionMcpServerStatusChangedEvent))]
+[JsonSerializable(typeof(SessionMcpServersLoadedData))]
+[JsonSerializable(typeof(SessionMcpServersLoadedDataServersItem))]
+[JsonSerializable(typeof(SessionMcpServersLoadedEvent))]
[JsonSerializable(typeof(SessionModeChangedData))]
[JsonSerializable(typeof(SessionModeChangedEvent))]
[JsonSerializable(typeof(SessionModelChangeData))]
@@ -3451,6 +3926,9 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(SessionShutdownData))]
[JsonSerializable(typeof(SessionShutdownDataCodeChanges))]
[JsonSerializable(typeof(SessionShutdownEvent))]
+[JsonSerializable(typeof(SessionSkillsLoadedData))]
+[JsonSerializable(typeof(SessionSkillsLoadedDataSkillsItem))]
+[JsonSerializable(typeof(SessionSkillsLoadedEvent))]
[JsonSerializable(typeof(SessionSnapshotRewindData))]
[JsonSerializable(typeof(SessionSnapshotRewindEvent))]
[JsonSerializable(typeof(SessionStartData))]
@@ -3488,6 +3966,7 @@ public enum PermissionCompletedDataResultKind
[JsonSerializable(typeof(SystemNotificationData))]
[JsonSerializable(typeof(SystemNotificationDataKind))]
[JsonSerializable(typeof(SystemNotificationDataKindAgentCompleted))]
+[JsonSerializable(typeof(SystemNotificationDataKindAgentIdle))]
[JsonSerializable(typeof(SystemNotificationDataKindShellCompleted))]
[JsonSerializable(typeof(SystemNotificationDataKindShellDetachedCompleted))]
[JsonSerializable(typeof(SystemNotificationEvent))]
diff --git a/dotnet/src/SdkProtocolVersion.cs b/dotnet/src/SdkProtocolVersion.cs
index f3d8f04c5..889af460b 100644
--- a/dotnet/src/SdkProtocolVersion.cs
+++ b/dotnet/src/SdkProtocolVersion.cs
@@ -16,8 +16,5 @@ internal static class SdkProtocolVersion
///
/// Gets the SDK protocol version.
///
- public static int GetVersion()
- {
- return Version;
- }
+ public static int GetVersion() => Version;
}
diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs
index 606c0b052..675a3e0c0 100644
--- a/dotnet/src/Session.cs
+++ b/dotnet/src/Session.cs
@@ -65,6 +65,8 @@ public sealed partial class CopilotSession : IAsyncDisposable
private SessionHooks? _hooks;
private readonly SemaphoreSlim _hooksLock = new(1, 1);
+ private Dictionary>>? _transformCallbacks;
+ private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1);
private SessionRpc? _sessionRpc;
private int _isDisposed;
@@ -653,6 +655,72 @@ internal void RegisterHooks(SessionHooks hooks)
};
}
+ ///
+ /// Registers transform callbacks for system message sections.
+ ///
+ /// The transform callbacks keyed by section identifier.
+ internal void RegisterTransformCallbacks(Dictionary>>? callbacks)
+ {
+ _transformCallbacksLock.Wait();
+ try
+ {
+ _transformCallbacks = callbacks;
+ }
+ finally
+ {
+ _transformCallbacksLock.Release();
+ }
+ }
+
+ ///
+ /// Handles a systemMessage.transform RPC call from the Copilot CLI.
+ ///
+ /// The raw JSON element containing sections to transform.
+ /// A task that resolves with the transformed sections.
+ internal async Task HandleSystemMessageTransformAsync(JsonElement sections)
+ {
+ Dictionary>>? callbacks;
+ await _transformCallbacksLock.WaitAsync();
+ try
+ {
+ callbacks = _transformCallbacks;
+ }
+ finally
+ {
+ _transformCallbacksLock.Release();
+ }
+
+ var parsed = JsonSerializer.Deserialize(
+ sections.GetRawText(),
+ SessionJsonContext.Default.DictionaryStringSystemMessageTransformSection) ?? new();
+
+ var result = new Dictionary();
+ foreach (var (sectionId, data) in parsed)
+ {
+ Func>? callback = null;
+ callbacks?.TryGetValue(sectionId, out callback);
+
+ if (callback != null)
+ {
+ try
+ {
+ var transformed = await callback(data.Content ?? "");
+ result[sectionId] = new SystemMessageTransformSection { Content = transformed };
+ }
+ catch
+ {
+ result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" };
+ }
+ }
+ else
+ {
+ result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" };
+ }
+ }
+
+ return new SystemMessageTransformRpcResponse { Sections = result };
+ }
+
///
/// Gets the complete list of messages and events in the session.
///
@@ -749,6 +817,7 @@ public Task SetModelAsync(string model, CancellationToken cancellationToken = de
/// The message to log.
/// Log level (default: info).
/// When true, the message is not persisted to disk.
+ /// Optional URL to associate with the log entry.
/// Optional cancellation token.
///
///
@@ -758,9 +827,9 @@ public Task SetModelAsync(string model, CancellationToken cancellationToken = de
/// await session.LogAsync("Temporary status", ephemeral: true);
///
///
- public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, CancellationToken cancellationToken = default)
+ public async Task LogAsync(string message, SessionLogRequestLevel? level = null, bool? ephemeral = null, string? url = null, CancellationToken cancellationToken = default)
{
- await Rpc.LogAsync(message, level, ephemeral, cancellationToken);
+ await Rpc.LogAsync(message, level, ephemeral, url, cancellationToken);
}
///
@@ -890,5 +959,8 @@ internal record SessionDestroyRequest
[JsonSerializable(typeof(SessionEndHookOutput))]
[JsonSerializable(typeof(ErrorOccurredHookInput))]
[JsonSerializable(typeof(ErrorOccurredHookOutput))]
+ [JsonSerializable(typeof(SystemMessageTransformSection))]
+ [JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
+ [JsonSerializable(typeof(Dictionary))]
internal partial class SessionJsonContext : JsonSerializerContext;
}
diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs
index 84e7feaed..d6530f9c7 100644
--- a/dotnet/src/Types.cs
+++ b/dotnet/src/Types.cs
@@ -968,7 +968,86 @@ public enum SystemMessageMode
Append,
/// Replace the default system message entirely.
[JsonStringEnumMemberName("replace")]
- Replace
+ Replace,
+ /// Override individual sections of the system prompt.
+ [JsonStringEnumMemberName("customize")]
+ Customize
+}
+
+///
+/// Specifies the operation to perform on a system prompt section.
+///
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum SectionOverrideAction
+{
+ /// Replace the section content entirely.
+ [JsonStringEnumMemberName("replace")]
+ Replace,
+ /// Remove the section from the prompt.
+ [JsonStringEnumMemberName("remove")]
+ Remove,
+ /// Append content after the existing section.
+ [JsonStringEnumMemberName("append")]
+ Append,
+ /// Prepend content before the existing section.
+ [JsonStringEnumMemberName("prepend")]
+ Prepend,
+ /// Transform the section content via a callback.
+ [JsonStringEnumMemberName("transform")]
+ Transform
+}
+
+///
+/// Override operation for a single system prompt section.
+///
+public class SectionOverride
+{
+ ///
+ /// The operation to perform on this section. Ignored when Transform is set.
+ ///
+ [JsonPropertyName("action")]
+ public SectionOverrideAction? Action { get; set; }
+
+ ///
+ /// Content for the override. Optional for all actions. Ignored for remove.
+ ///
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+
+ ///
+ /// Transform callback. When set, takes precedence over Action.
+ /// Receives current section content, returns transformed content.
+ /// Not serialized — the SDK handles this locally.
+ ///
+ [JsonIgnore]
+ public Func>? Transform { get; set; }
+}
+
+///
+/// Known system prompt section identifiers for the "customize" mode.
+///
+public static class SystemPromptSections
+{
+ /// Agent identity preamble and mode statement.
+ public const string Identity = "identity";
+ /// Response style, conciseness rules, output formatting preferences.
+ public const string Tone = "tone";
+ /// Tool usage patterns, parallel calling, batching guidelines.
+ public const string ToolEfficiency = "tool_efficiency";
+ /// CWD, OS, git root, directory listing, available tools.
+ public const string EnvironmentContext = "environment_context";
+ /// Coding rules, linting/testing, ecosystem tools, style.
+ public const string CodeChangeRules = "code_change_rules";
+ /// Tips, behavioral best practices, behavioral guidelines.
+ public const string Guidelines = "guidelines";
+ /// Environment limitations, prohibited actions, security policies.
+ public const string Safety = "safety";
+ /// Per-tool usage instructions.
+ public const string ToolInstructions = "tool_instructions";
+ /// Repository and organization custom instructions.
+ public const string CustomInstructions = "custom_instructions";
+ /// End-of-prompt instructions: parallel tool calling, persistence, task completion.
+ public const string LastInstructions = "last_instructions";
}
///
@@ -977,13 +1056,21 @@ public enum SystemMessageMode
public class SystemMessageConfig
{
///
- /// How the system message is applied (append or replace).
+ /// How the system message is applied (append, replace, or customize).
///
public SystemMessageMode? Mode { get; set; }
+
///
- /// Content of the system message.
+ /// Content of the system message. Used by append and replace modes.
+ /// In customize mode, additional content appended after all sections.
///
public string? Content { get; set; }
+
+ ///
+ /// Section-level overrides for customize mode.
+ /// Keys are section identifiers (see ).
+ ///
+ public Dictionary? Sections { get; set; }
}
///
@@ -2032,6 +2119,30 @@ public class SetForegroundSessionResponse
public string? Error { get; set; }
}
+///
+/// Content data for a single system prompt section in a transform RPC call.
+///
+public class SystemMessageTransformSection
+{
+ ///
+ /// The content of the section.
+ ///
+ [JsonPropertyName("content")]
+ public string? Content { get; set; }
+}
+
+///
+/// Response to a systemMessage.transform RPC call.
+///
+public class SystemMessageTransformRpcResponse
+{
+ ///
+ /// The transformed sections keyed by section identifier.
+ ///
+ [JsonPropertyName("sections")]
+ public Dictionary? Sections { get; set; }
+}
+
[JsonSourceGenerationOptions(
JsonSerializerDefaults.Web,
AllowOutOfOrderMetadataProperties = true,
@@ -2061,6 +2172,7 @@ public class SetForegroundSessionResponse
[JsonSerializable(typeof(SessionLifecycleEvent))]
[JsonSerializable(typeof(SessionLifecycleEventMetadata))]
[JsonSerializable(typeof(SessionListFilter))]
+[JsonSerializable(typeof(SectionOverride))]
[JsonSerializable(typeof(SessionMetadata))]
[JsonSerializable(typeof(SetForegroundSessionResponse))]
[JsonSerializable(typeof(SystemMessageConfig))]
diff --git a/dotnet/test/ForwardCompatibilityTests.cs b/dotnet/test/ForwardCompatibilityTests.cs
new file mode 100644
index 000000000..d3f5b7785
--- /dev/null
+++ b/dotnet/test/ForwardCompatibilityTests.cs
@@ -0,0 +1,100 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using Xunit;
+
+namespace GitHub.Copilot.SDK.Test;
+
+///
+/// Tests for forward-compatible handling of unknown session event types.
+/// Verifies that the SDK gracefully handles event types introduced by newer CLI versions.
+///
+public class ForwardCompatibilityTests
+{
+ [Fact]
+ public void FromJson_KnownEventType_DeserializesNormally()
+ {
+ var json = """
+ {
+ "id": "00000000-0000-0000-0000-000000000001",
+ "timestamp": "2026-01-01T00:00:00Z",
+ "parentId": null,
+ "type": "user.message",
+ "data": {
+ "content": "Hello"
+ }
+ }
+ """;
+
+ var result = SessionEvent.FromJson(json);
+
+ Assert.IsType(result);
+ Assert.Equal("user.message", result.Type);
+ }
+
+ [Fact]
+ public void FromJson_UnknownEventType_ReturnsBaseSessionEvent()
+ {
+ var json = """
+ {
+ "id": "12345678-1234-1234-1234-123456789abc",
+ "timestamp": "2026-06-15T10:30:00Z",
+ "parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef",
+ "type": "future.feature_from_server",
+ "data": { "key": "value" }
+ }
+ """;
+
+ var result = SessionEvent.FromJson(json);
+
+ Assert.IsType(result);
+ Assert.Equal("unknown", result.Type);
+ }
+
+ [Fact]
+ public void FromJson_UnknownEventType_PreservesBaseMetadata()
+ {
+ var json = """
+ {
+ "id": "12345678-1234-1234-1234-123456789abc",
+ "timestamp": "2026-06-15T10:30:00Z",
+ "parentId": "abcdefab-abcd-abcd-abcd-abcdefabcdef",
+ "type": "future.feature_from_server",
+ "data": {}
+ }
+ """;
+
+ var result = SessionEvent.FromJson(json);
+
+ Assert.Equal(Guid.Parse("12345678-1234-1234-1234-123456789abc"), result.Id);
+ Assert.Equal(DateTimeOffset.Parse("2026-06-15T10:30:00Z"), result.Timestamp);
+ Assert.Equal(Guid.Parse("abcdefab-abcd-abcd-abcd-abcdefabcdef"), result.ParentId);
+ }
+
+ [Fact]
+ public void FromJson_MultipleEvents_MixedKnownAndUnknown()
+ {
+ var events = new[]
+ {
+ """{"id":"00000000-0000-0000-0000-000000000001","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Hi"}}""",
+ """{"id":"00000000-0000-0000-0000-000000000002","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"future.unknown_type","data":{}}""",
+ """{"id":"00000000-0000-0000-0000-000000000003","timestamp":"2026-01-01T00:00:00Z","parentId":null,"type":"user.message","data":{"content":"Bye"}}""",
+ };
+
+ var results = events.Select(SessionEvent.FromJson).ToList();
+
+ Assert.Equal(3, results.Count);
+ Assert.IsType(results[0]);
+ Assert.IsType(results[1]);
+ Assert.IsType(results[2]);
+ }
+
+ [Fact]
+ public void SessionEvent_Type_DefaultsToUnknown()
+ {
+ var evt = new SessionEvent();
+
+ Assert.Equal("unknown", evt.Type);
+ }
+}
diff --git a/dotnet/test/SessionTests.cs b/dotnet/test/SessionTests.cs
index 30a9135a5..5aecaccba 100644
--- a/dotnet/test/SessionTests.cs
+++ b/dotnet/test/SessionTests.cs
@@ -91,6 +91,37 @@ public async Task Should_Create_A_Session_With_Replaced_SystemMessage_Config()
Assert.Equal(testSystemMessage, GetSystemMessage(traffic[0]));
}
+ [Fact]
+ public async Task Should_Create_A_Session_With_Customized_SystemMessage_Config()
+ {
+ var customTone = "Respond in a warm, professional tone. Be thorough in explanations.";
+ var appendedContent = "Always mention quarterly earnings.";
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ [SystemPromptSections.Tone] = new() { Action = SectionOverrideAction.Replace, Content = customTone },
+ [SystemPromptSections.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove },
+ },
+ Content = appendedContent
+ }
+ });
+
+ await session.SendAsync(new MessageOptions { Prompt = "Who are you?" });
+ var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
+ Assert.NotNull(assistantMessage);
+
+ var traffic = await Ctx.GetExchangesAsync();
+ Assert.NotEmpty(traffic);
+ var systemMessage = GetSystemMessage(traffic[0]);
+ Assert.Contains(customTone, systemMessage);
+ Assert.Contains(appendedContent, systemMessage);
+ Assert.DoesNotContain("", systemMessage);
+ }
+
[Fact]
public async Task Should_Create_A_Session_With_AvailableTools()
{
diff --git a/dotnet/test/SystemMessageTransformTests.cs b/dotnet/test/SystemMessageTransformTests.cs
new file mode 100644
index 000000000..cdddc5a79
--- /dev/null
+++ b/dotnet/test/SystemMessageTransformTests.cs
@@ -0,0 +1,140 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+using GitHub.Copilot.SDK.Test.Harness;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace GitHub.Copilot.SDK.Test;
+
+public class SystemMessageTransformTests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "system_message_transform", output)
+{
+ [Fact]
+ public async Task Should_Invoke_Transform_Callbacks_With_Section_Content()
+ {
+ var identityCallbackInvoked = false;
+ var toneCallbackInvoked = false;
+
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ ["identity"] = new SectionOverride
+ {
+ Transform = async (content) =>
+ {
+ Assert.False(string.IsNullOrEmpty(content));
+ identityCallbackInvoked = true;
+ return content;
+ }
+ },
+ ["tone"] = new SectionOverride
+ {
+ Transform = async (content) =>
+ {
+ Assert.False(string.IsNullOrEmpty(content));
+ toneCallbackInvoked = true;
+ return content;
+ }
+ }
+ }
+ }
+ });
+
+ await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "test.txt"), "Hello transform!");
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Read the contents of test.txt and tell me what it says"
+ });
+
+ await TestHelper.GetFinalAssistantMessageAsync(session);
+
+ Assert.True(identityCallbackInvoked, "Expected identity transform callback to be invoked");
+ Assert.True(toneCallbackInvoked, "Expected tone transform callback to be invoked");
+ }
+
+ [Fact]
+ public async Task Should_Apply_Transform_Modifications_To_Section_Content()
+ {
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ ["identity"] = new SectionOverride
+ {
+ Transform = async (content) =>
+ {
+ return content + "\nAlways end your reply with TRANSFORM_MARKER";
+ }
+ }
+ }
+ }
+ });
+
+ await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "hello.txt"), "Hello!");
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Read the contents of hello.txt"
+ });
+
+ await TestHelper.GetFinalAssistantMessageAsync(session);
+
+ // Verify the transform result was actually applied to the system message
+ var traffic = await Ctx.GetExchangesAsync();
+ Assert.NotEmpty(traffic);
+ var systemMessage = GetSystemMessage(traffic[0]);
+ Assert.Contains("TRANSFORM_MARKER", systemMessage);
+ }
+
+ [Fact]
+ public async Task Should_Work_With_Static_Overrides_And_Transforms_Together()
+ {
+ var transformCallbackInvoked = false;
+
+ var session = await CreateSessionAsync(new SessionConfig
+ {
+ OnPermissionRequest = PermissionHandler.ApproveAll,
+ SystemMessage = new SystemMessageConfig
+ {
+ Mode = SystemMessageMode.Customize,
+ Sections = new Dictionary
+ {
+ ["safety"] = new SectionOverride
+ {
+ Action = SectionOverrideAction.Remove
+ },
+ ["identity"] = new SectionOverride
+ {
+ Transform = async (content) =>
+ {
+ transformCallbackInvoked = true;
+ return content;
+ }
+ }
+ }
+ }
+ });
+
+ await File.WriteAllTextAsync(Path.Combine(Ctx.WorkDir, "combo.txt"), "Combo test!");
+
+ await session.SendAsync(new MessageOptions
+ {
+ Prompt = "Read the contents of combo.txt and tell me what it says"
+ });
+
+ await TestHelper.GetFinalAssistantMessageAsync(session);
+
+ Assert.True(transformCallbackInvoked, "Expected identity transform callback to be invoked");
+ }
+}
diff --git a/go/README.md b/go/README.md
index 1d0665130..f29ef9fb7 100644
--- a/go/README.md
+++ b/go/README.md
@@ -150,7 +150,10 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `ReasoningEffort` (string): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `ListModels()` to check which models support this option.
- `SessionID` (string): Custom session ID
- `Tools` ([]Tool): Custom tools exposed to the CLI
-- `SystemMessage` (\*SystemMessageConfig): System message configuration
+- `SystemMessage` (\*SystemMessageConfig): System message configuration. Supports three modes:
+ - **append** (default): Appends `Content` after the SDK-managed prompt
+ - **replace**: Replaces the entire prompt with `Content`
+ - **customize**: Selectively override individual sections via `Sections` map (keys: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionLastInstructions`; values: `SectionOverride` with `Action` and optional `Content`)
- `Provider` (\*ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section.
- `Streaming` (bool): Enable streaming delta events
- `InfiniteSessions` (\*InfiniteSessionConfig): Automatic context compaction configuration
@@ -179,6 +182,52 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `Bool(v bool) *bool` - Helper to create bool pointers for `AutoStart` option
+### System Message Customization
+
+Control the system prompt using `SystemMessage` in session config:
+
+```go
+session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ SystemMessage: &copilot.SystemMessageConfig{
+ Content: "Always check for security vulnerabilities before suggesting changes.",
+ },
+})
+```
+
+The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `Content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `Mode: "replace"` or `Mode: "customize"`.
+
+#### Customize Mode
+
+Use `Mode: "customize"` to selectively override individual sections of the prompt while preserving the rest:
+
+```go
+session, err := client.CreateSession(ctx, &copilot.SessionConfig{
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ // Replace the tone/style section
+ copilot.SectionTone: {Action: "replace", Content: "Respond in a warm, professional tone. Be thorough in explanations."},
+ // Remove coding-specific rules
+ copilot.SectionCodeChangeRules: {Action: "remove"},
+ // Append to existing guidelines
+ copilot.SectionGuidelines: {Action: "append", Content: "\n* Always cite data sources"},
+ },
+ // Additional instructions appended after all sections
+ Content: "Focus on financial analysis and reporting.",
+ },
+})
+```
+
+Available section constants: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionLastInstructions`.
+
+Each section override supports four actions:
+- **`replace`** — Replace the section content entirely
+- **`remove`** — Remove the section from the prompt
+- **`append`** — Add content after the existing section
+- **`prepend`** — Add content before the existing section
+
+Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored.
+
## Image Support
The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment:
@@ -201,7 +250,7 @@ _, err = session.Send(context.Background(), copilot.MessageOptions{
Prompt: "What's in this image?",
Attachments: []copilot.Attachment{
{
- Type: copilot.Blob,
+ Type: copilot.AttachmentTypeBlob,
Data: &base64ImageData,
MIMEType: &mimeType,
},
diff --git a/go/client.go b/go/client.go
index a2431ad39..22be47ec6 100644
--- a/go/client.go
+++ b/go/client.go
@@ -482,6 +482,37 @@ func (c *Client) ensureConnected(ctx context.Context) error {
// },
// },
// })
+//
+// extractTransformCallbacks separates transform callbacks from a SystemMessageConfig,
+// returning a wire-safe config and a map of callbacks (nil if none).
+func extractTransformCallbacks(config *SystemMessageConfig) (*SystemMessageConfig, map[string]SectionTransformFn) {
+ if config == nil || config.Mode != "customize" || len(config.Sections) == 0 {
+ return config, nil
+ }
+
+ callbacks := make(map[string]SectionTransformFn)
+ wireSections := make(map[string]SectionOverride)
+ for id, override := range config.Sections {
+ if override.Transform != nil {
+ callbacks[id] = override.Transform
+ wireSections[id] = SectionOverride{Action: "transform"}
+ } else {
+ wireSections[id] = override
+ }
+ }
+
+ if len(callbacks) == 0 {
+ return config, nil
+ }
+
+ wireConfig := &SystemMessageConfig{
+ Mode: config.Mode,
+ Content: config.Content,
+ Sections: wireSections,
+ }
+ return wireConfig, callbacks
+}
+
func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Session, error) {
if config == nil || config.OnPermissionRequest == nil {
return nil, fmt.Errorf("an OnPermissionRequest handler is required when creating a session. For example, to allow all permissions, use &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll}")
@@ -497,7 +528,8 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
req.ReasoningEffort = config.ReasoningEffort
req.ConfigDir = config.ConfigDir
req.Tools = config.Tools
- req.SystemMessage = config.SystemMessage
+ wireSystemMessage, transformCallbacks := extractTransformCallbacks(config.SystemMessage)
+ req.SystemMessage = wireSystemMessage
req.AvailableTools = config.AvailableTools
req.ExcludedTools = config.ExcludedTools
req.Provider = config.Provider
@@ -548,6 +580,9 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
if config.Hooks != nil {
session.registerHooks(config.Hooks)
}
+ if transformCallbacks != nil {
+ session.registerTransformCallbacks(transformCallbacks)
+ }
if config.OnEvent != nil {
session.On(config.OnEvent)
}
@@ -616,7 +651,8 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
req.ClientName = config.ClientName
req.Model = config.Model
req.ReasoningEffort = config.ReasoningEffort
- req.SystemMessage = config.SystemMessage
+ wireSystemMessage, transformCallbacks := extractTransformCallbacks(config.SystemMessage)
+ req.SystemMessage = wireSystemMessage
req.Tools = config.Tools
req.Provider = config.Provider
req.AvailableTools = config.AvailableTools
@@ -665,6 +701,9 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
if config.Hooks != nil {
session.registerHooks(config.Hooks)
}
+ if transformCallbacks != nil {
+ session.registerTransformCallbacks(transformCallbacks)
+ }
if config.OnEvent != nil {
session.On(config.OnEvent)
}
@@ -1402,6 +1441,7 @@ func (c *Client) setupNotificationHandler() {
c.client.SetRequestHandler("permission.request", jsonrpc2.RequestHandlerFor(c.handlePermissionRequestV2))
c.client.SetRequestHandler("userInput.request", jsonrpc2.RequestHandlerFor(c.handleUserInputRequest))
c.client.SetRequestHandler("hooks.invoke", jsonrpc2.RequestHandlerFor(c.handleHooksInvoke))
+ c.client.SetRequestHandler("systemMessage.transform", jsonrpc2.RequestHandlerFor(c.handleSystemMessageTransform))
}
func (c *Client) handleSessionEvent(req sessionEventRequest) {
@@ -1468,6 +1508,26 @@ func (c *Client) handleHooksInvoke(req hooksInvokeRequest) (map[string]any, *jso
return result, nil
}
+// handleSystemMessageTransform handles a system message transform request from the CLI server.
+func (c *Client) handleSystemMessageTransform(req systemMessageTransformRequest) (systemMessageTransformResponse, *jsonrpc2.Error) {
+ if req.SessionID == "" {
+ return systemMessageTransformResponse{}, &jsonrpc2.Error{Code: -32602, Message: "invalid system message transform payload"}
+ }
+
+ c.sessionsMux.Lock()
+ session, ok := c.sessions[req.SessionID]
+ c.sessionsMux.Unlock()
+ if !ok {
+ return systemMessageTransformResponse{}, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("unknown session %s", req.SessionID)}
+ }
+
+ resp, err := session.handleSystemMessageTransform(req.Sections)
+ if err != nil {
+ return systemMessageTransformResponse{}, &jsonrpc2.Error{Code: -32603, Message: err.Error()}
+ }
+ return resp, nil
+}
+
// ========================================================================
// Protocol v2 backward-compatibility adapters
// ========================================================================
diff --git a/go/generated_session_events.go b/go/generated_session_events.go
index 55eea011e..fbdb1597f 100644
--- a/go/generated_session_events.go
+++ b/go/generated_session_events.go
@@ -61,15 +61,12 @@ type SessionEvent struct {
//
// Current context window usage statistics including token and message counts
//
- // Empty payload; the event signals that LLM-powered conversation compaction has begun
+ // Context window breakdown at the start of LLM-powered conversation compaction
//
// Conversation compaction results including success status, metrics, and optional error
// details
//
- // Task completion notification with optional summary from the agent
- //
- // User message content with optional attachments, source information, and interaction
- // metadata
+ // Task completion notification with summary from the agent
//
// Empty payload; the event signals that the pending message queue has changed
//
@@ -135,18 +132,27 @@ type SessionEvent struct {
//
// User input request completion notification signaling UI dismissal
//
- // Structured form elicitation request with JSON schema definition for form fields
+ // Elicitation request; may be form-based (structured input) or URL-based (browser
+ // redirect)
//
// Elicitation request completion notification signaling UI dismissal
//
+ // OAuth authentication request for an MCP server
+ //
+ // MCP OAuth request completion notification
+ //
// External tool invocation request for client-side tool execution
//
// External tool completion notification signaling UI dismissal
//
// Queued slash command dispatch request for client execution
//
+ // Registered command dispatch request routed to the owning client
+ //
// Queued command completion notification signaling UI dismissal
//
+ // SDK command registration change notification
+ //
// Plan approval request with plan content and available user actions
//
// Plan mode exit completion notification signaling UI dismissal
@@ -198,15 +204,12 @@ type SessionEvent struct {
//
// # Current context window usage statistics including token and message counts
//
-// Empty payload; the event signals that LLM-powered conversation compaction has begun
+// # Context window breakdown at the start of LLM-powered conversation compaction
//
// Conversation compaction results including success status, metrics, and optional error
// details
//
-// # Task completion notification with optional summary from the agent
-//
-// User message content with optional attachments, source information, and interaction
-// metadata
+// # Task completion notification with summary from the agent
//
// Empty payload; the event signals that the pending message queue has changed
//
@@ -272,18 +275,27 @@ type SessionEvent struct {
//
// # User input request completion notification signaling UI dismissal
//
-// # Structured form elicitation request with JSON schema definition for form fields
+// Elicitation request; may be form-based (structured input) or URL-based (browser
+// redirect)
//
// # Elicitation request completion notification signaling UI dismissal
//
+// # OAuth authentication request for an MCP server
+//
+// # MCP OAuth request completion notification
+//
// # External tool invocation request for client-side tool execution
//
// # External tool completion notification signaling UI dismissal
//
// # Queued slash command dispatch request for client execution
//
+// # Registered command dispatch request routed to the owning client
+//
// # Queued command completion notification signaling UI dismissal
//
+// # SDK command registration change notification
+//
// # Plan approval request with plan content and available user actions
//
// Plan mode exit completion notification signaling UI dismissal
@@ -343,6 +355,14 @@ type Data struct {
Stack *string `json:"stack,omitempty"`
// HTTP status code from the upstream request, if applicable
StatusCode *int64 `json:"statusCode,omitempty"`
+ // Optional URL associated with this error that the user can open in a browser
+ //
+ // Optional URL associated with this message that the user can open in a browser
+ //
+ // Optional URL associated with this warning that the user can open in a browser
+ //
+ // URL to open in the user's browser (url mode only)
+ URL *string `json:"url,omitempty"`
// Background tasks still running when the agent became idle
BackgroundTasks *BackgroundTasks `json:"backgroundTasks,omitempty"`
// The new display title for the session
@@ -383,7 +403,7 @@ type Data struct {
SourceType *SourceType `json:"sourceType,omitempty"`
// Summary of the work done in the source session
//
- // Optional summary of the completed task, provided by the agent
+ // Summary of the completed task, provided by the agent
//
// Summary of the plan that was created
Summary *string `json:"summary,omitempty"`
@@ -409,8 +429,20 @@ type Data struct {
UpToEventID *string `json:"upToEventId,omitempty"`
// Aggregate code change metrics for the session
CodeChanges *CodeChanges `json:"codeChanges,omitempty"`
+ // Non-system message token count at shutdown
+ //
+ // Token count from non-system messages (user, assistant, tool)
+ //
+ // Token count from non-system messages (user, assistant, tool) at compaction start
+ //
+ // Token count from non-system messages (user, assistant, tool) after compaction
+ ConversationTokens *float64 `json:"conversationTokens,omitempty"`
// Model that was selected at the time of shutdown
CurrentModel *string `json:"currentModel,omitempty"`
+ // Total tokens in context window at shutdown
+ //
+ // Current number of tokens in the context window
+ CurrentTokens *float64 `json:"currentTokens,omitempty"`
// Error description when shutdownType is "error"
ErrorReason *string `json:"errorReason,omitempty"`
// Per-model usage breakdown, keyed by model identifier
@@ -419,6 +451,22 @@ type Data struct {
SessionStartTime *float64 `json:"sessionStartTime,omitempty"`
// Whether the session ended normally ("routine") or due to a crash/fatal error ("error")
ShutdownType *ShutdownType `json:"shutdownType,omitempty"`
+ // System message token count at shutdown
+ //
+ // Token count from system message(s)
+ //
+ // Token count from system message(s) at compaction start
+ //
+ // Token count from system message(s) after compaction
+ SystemTokens *float64 `json:"systemTokens,omitempty"`
+ // Tool definitions token count at shutdown
+ //
+ // Token count from tool definitions
+ //
+ // Token count from tool definitions at compaction start
+ //
+ // Token count from tool definitions after compaction
+ ToolDefinitionsTokens *float64 `json:"toolDefinitionsTokens,omitempty"`
// Cumulative time spent in API calls during the session, in milliseconds
TotalAPIDurationMS *float64 `json:"totalApiDurationMs,omitempty"`
// Total number of premium API requests used during the session
@@ -435,8 +483,8 @@ type Data struct {
HeadCommit *string `json:"headCommit,omitempty"`
// Hosting platform type of the repository (github or ado)
HostType *HostType `json:"hostType,omitempty"`
- // Current number of tokens in the context window
- CurrentTokens *float64 `json:"currentTokens,omitempty"`
+ // Whether this is the first usage_info event emitted in this session
+ IsInitial *bool `json:"isInitial,omitempty"`
// Current number of messages in the conversation
MessagesLength *float64 `json:"messagesLength,omitempty"`
// Checkpoint snapshot number created for recovery
@@ -481,6 +529,11 @@ type Data struct {
// Request ID of the resolved elicitation request; clients should dismiss any UI for this
// request
//
+ // Unique identifier for this OAuth request; used to respond via
+ // session.respondToMcpOAuth()
+ //
+ // Request ID of the resolved OAuth request
+ //
// Unique identifier for this request; used to respond via session.respondToExternalTool()
//
// Request ID of the resolved external tool request; clients should dismiss any UI for this
@@ -488,6 +541,8 @@ type Data struct {
//
// Unique identifier for this request; used to respond via session.respondToQueuedCommand()
//
+ // Unique identifier; used to respond via session.commands.handlePendingCommand()
+ //
// Request ID of the resolved command request; clients should dismiss any UI for this
// request
//
@@ -498,6 +553,8 @@ type Data struct {
RequestID *string `json:"requestId,omitempty"`
// Whether compaction completed successfully
//
+ // Whether the tool call succeeded. False when validation failed (e.g., invalid arguments)
+ //
// Whether the tool execution completed successfully
//
// Whether the hook completed successfully
@@ -530,9 +587,9 @@ type Data struct {
//
// CAPI interaction ID for correlating this tool execution with upstream telemetry
InteractionID *string `json:"interactionId,omitempty"`
- // Origin of this message, used for timeline filtering and telemetry (e.g., "user",
- // "autopilot", "skill", or "command")
- Source *Source `json:"source,omitempty"`
+ // Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected
+ // messages that should be hidden from the user)
+ Source *string `json:"source,omitempty"`
// Transformed version of the message sent to the model, with XML wrapping, timestamps, and
// other augmentations for prompt caching
TransformedContent *string `json:"transformedContent,omitempty"`
@@ -618,6 +675,12 @@ type Data struct {
//
// Tool call ID of the parent tool invocation that spawned this sub-agent
//
+ // The LLM-assigned tool call ID that triggered this request; used by remote UIs to
+ // correlate responses
+ //
+ // Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id
+ // for remote UIs
+ //
// Tool call ID assigned to this external tool invocation
ToolCallID *string `json:"toolCallId,omitempty"`
// Name of the tool the user wants to invoke
@@ -690,22 +753,49 @@ type Data struct {
Choices []string `json:"choices,omitempty"`
// The question or prompt to present to the user
Question *string `json:"question,omitempty"`
- // Elicitation mode; currently only "form" is supported. Defaults to "form" when absent.
+ // The source that initiated the request (MCP server name, or absent for agent-initiated)
+ ElicitationSource *string `json:"elicitationSource,omitempty"`
+ // Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to
+ // "form" when absent.
Mode *Mode `json:"mode,omitempty"`
- // JSON Schema describing the form fields to present to the user
+ // JSON Schema describing the form fields to present to the user (form mode only)
RequestedSchema *RequestedSchema `json:"requestedSchema,omitempty"`
+ // Display name of the MCP server that requires OAuth
+ //
+ // Name of the MCP server whose status changed
+ ServerName *string `json:"serverName,omitempty"`
+ // URL of the MCP server that requires OAuth
+ ServerURL *string `json:"serverUrl,omitempty"`
+ // Static OAuth client configuration, if the server specifies one
+ StaticClientConfig *StaticClientConfig `json:"staticClientConfig,omitempty"`
// W3C Trace Context traceparent header for the execute_tool span
Traceparent *string `json:"traceparent,omitempty"`
// W3C Trace Context tracestate header for the execute_tool span
Tracestate *string `json:"tracestate,omitempty"`
// The slash command text to be executed (e.g., /help, /clear)
+ //
+ // The full command text (e.g., /deploy production)
Command *string `json:"command,omitempty"`
+ // Raw argument string after the command name
+ Args *string `json:"args,omitempty"`
+ // Command name without leading /
+ CommandName *string `json:"commandName,omitempty"`
+ // Current list of registered SDK commands
+ Commands []DataCommand `json:"commands,omitempty"`
// Available actions the user can take (e.g., approve, edit, reject)
Actions []string `json:"actions,omitempty"`
// Full content of the plan file
PlanContent *string `json:"planContent,omitempty"`
// The recommended action for the user to take
RecommendedAction *string `json:"recommendedAction,omitempty"`
+ // Array of resolved skill metadata
+ Skills []Skill `json:"skills,omitempty"`
+ // Array of MCP server status summaries
+ Servers []Server `json:"servers,omitempty"`
+ // New connection status: connected, failed, pending, disabled, or not_configured
+ Status *ServerStatus `json:"status,omitempty"`
+ // Array of discovered extensions and their status
+ Extensions []Extension `json:"extensions,omitempty"`
}
// A user message attachment — a file, directory, code selection, blob, or GitHub reference
@@ -822,6 +912,11 @@ type CodeChanges struct {
LinesRemoved float64 `json:"linesRemoved"`
}
+type DataCommand struct {
+ Description *string `json:"description,omitempty"`
+ Name string `json:"name"`
+}
+
// Token usage breakdown for the compaction LLM call
type CompactionTokensUsed struct {
// Cached input tokens reused in the compaction LLM call
@@ -885,6 +980,17 @@ type ErrorClass struct {
Stack *string `json:"stack,omitempty"`
}
+type Extension struct {
+ // Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper')
+ ID string `json:"id"`
+ // Extension name (directory name)
+ Name string `json:"name"`
+ // Discovery source
+ Source Source `json:"source"`
+ // Current status: running, disabled, failed, or starting
+ Status ExtensionStatus `json:"status"`
+}
+
// Structured metadata identifying what triggered this notification
type KindClass struct {
// Unique identifier of the background agent
@@ -898,8 +1004,8 @@ type KindClass struct {
// The full prompt given to the background agent
Prompt *string `json:"prompt,omitempty"`
// Whether the agent completed successfully or failed
- Status *Status `json:"status,omitempty"`
- Type KindType `json:"type"`
+ Status *KindStatus `json:"status,omitempty"`
+ Type KindType `json:"type"`
// Exit code of the shell command, if available
ExitCode *float64 `json:"exitCode,omitempty"`
// Unique identifier of the shell session
@@ -964,7 +1070,7 @@ type PermissionRequest struct {
// Whether the UI can offer session-wide approval for this command pattern
CanOfferSessionApproval *bool `json:"canOfferSessionApproval,omitempty"`
// Parsed command identifiers found in the command text
- Commands []CommandElement `json:"commands,omitempty"`
+ Commands []PermissionRequestCommand `json:"commands,omitempty"`
// The complete shell command text to be executed
FullCommandText *string `json:"fullCommandText,omitempty"`
// Whether the command includes a file write redirection (e.g., > or >>)
@@ -1027,7 +1133,7 @@ type PermissionRequest struct {
ToolArgs interface{} `json:"toolArgs"`
}
-type CommandElement struct {
+type PermissionRequestCommand struct {
// Command identifier (e.g., executable name)
Identifier string `json:"identifier"`
// Whether this command is read-only (no side effects)
@@ -1068,7 +1174,7 @@ type RepositoryClass struct {
Owner string `json:"owner"`
}
-// JSON Schema describing the form fields to present to the user
+// JSON Schema describing the form fields to present to the user (form mode only)
type RequestedSchema struct {
// Form field definitions, keyed by field name
Properties map[string]interface{} `json:"properties"`
@@ -1172,6 +1278,40 @@ type ResourceClass struct {
Blob *string `json:"blob,omitempty"`
}
+type Server struct {
+ // Error message if the server failed to connect
+ Error *string `json:"error,omitempty"`
+ // Server name (config key)
+ Name string `json:"name"`
+ // Configuration source: user, workspace, plugin, or builtin
+ Source *string `json:"source,omitempty"`
+ // Connection status: connected, failed, pending, disabled, or not_configured
+ Status ServerStatus `json:"status"`
+}
+
+type Skill struct {
+ // Description of what the skill does
+ Description string `json:"description"`
+ // Whether the skill is currently enabled
+ Enabled bool `json:"enabled"`
+ // Unique identifier for the skill
+ Name string `json:"name"`
+ // Absolute path to the skill file, if available
+ Path *string `json:"path,omitempty"`
+ // Source location type of the skill (e.g., project, personal, plugin)
+ Source string `json:"source"`
+ // Whether the skill can be invoked by the user as a slash command
+ UserInvocable bool `json:"userInvocable"`
+}
+
+// Static OAuth client configuration, if the server specifies one
+type StaticClientConfig struct {
+ // OAuth client ID for the server
+ ClientID string `json:"clientId"`
+ // Whether this is a public OAuth client
+ PublicClient *bool `json:"publicClient,omitempty"`
+}
+
// A tool invocation request from the assistant
type ToolRequest struct {
// Arguments to pass to the tool, format depends on the tool
@@ -1193,59 +1333,81 @@ type ToolRequest struct {
type AgentMode string
const (
- AgentModeAutopilot AgentMode = "autopilot"
- AgentModeShell AgentMode = "shell"
- Interactive AgentMode = "interactive"
- Plan AgentMode = "plan"
+ AgentModeShell AgentMode = "shell"
+ AgentModeAutopilot AgentMode = "autopilot"
+ AgentModeInteractive AgentMode = "interactive"
+ AgentModePlan AgentMode = "plan"
)
// Type of GitHub reference
type ReferenceType string
const (
- Discussion ReferenceType = "discussion"
- Issue ReferenceType = "issue"
- PR ReferenceType = "pr"
+ ReferenceTypeDiscussion ReferenceType = "discussion"
+ ReferenceTypeIssue ReferenceType = "issue"
+ ReferenceTypePr ReferenceType = "pr"
)
type AttachmentType string
const (
- Blob AttachmentType = "blob"
- Directory AttachmentType = "directory"
- File AttachmentType = "file"
- GithubReference AttachmentType = "github_reference"
- Selection AttachmentType = "selection"
+ AttachmentTypeBlob AttachmentType = "blob"
+ AttachmentTypeDirectory AttachmentType = "directory"
+ AttachmentTypeFile AttachmentType = "file"
+ AttachmentTypeGithubReference AttachmentType = "github_reference"
+ AttachmentTypeSelection AttachmentType = "selection"
)
// Hosting platform type of the repository (github or ado)
type HostType string
const (
- ADO HostType = "ado"
- Github HostType = "github"
+ HostTypeAdo HostType = "ado"
+ HostTypeGithub HostType = "github"
+)
+
+// Discovery source
+type Source string
+
+const (
+ SourceProject Source = "project"
+ SourceUser Source = "user"
+)
+
+// Current status: running, disabled, failed, or starting
+type ExtensionStatus string
+
+const (
+ ExtensionStatusDisabled ExtensionStatus = "disabled"
+ ExtensionStatusFailed ExtensionStatus = "failed"
+ ExtensionStatusRunning ExtensionStatus = "running"
+ ExtensionStatusStarting ExtensionStatus = "starting"
)
// Whether the agent completed successfully or failed
-type Status string
+type KindStatus string
const (
- Completed Status = "completed"
- Failed Status = "failed"
+ KindStatusCompleted KindStatus = "completed"
+ KindStatusFailed KindStatus = "failed"
)
type KindType string
const (
- AgentCompleted KindType = "agent_completed"
- ShellCompleted KindType = "shell_completed"
- ShellDetachedCompleted KindType = "shell_detached_completed"
+ KindTypeAgentCompleted KindType = "agent_completed"
+ KindTypeAgentIdle KindType = "agent_idle"
+ KindTypeShellCompleted KindType = "shell_completed"
+ KindTypeShellDetachedCompleted KindType = "shell_detached_completed"
)
+// Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to
+// "form" when absent.
type Mode string
const (
- Form Mode = "form"
+ ModeForm Mode = "form"
+ ModeURL Mode = "url"
)
// The type of operation performed on the plan file
@@ -1254,99 +1416,95 @@ const (
type Operation string
const (
- Create Operation = "create"
- Delete Operation = "delete"
- Update Operation = "update"
+ OperationCreate Operation = "create"
+ OperationDelete Operation = "delete"
+ OperationUpdate Operation = "update"
)
type PermissionRequestKind string
const (
- CustomTool PermissionRequestKind = "custom-tool"
- Hook PermissionRequestKind = "hook"
- KindShell PermissionRequestKind = "shell"
- MCP PermissionRequestKind = "mcp"
- Memory PermissionRequestKind = "memory"
- Read PermissionRequestKind = "read"
- URL PermissionRequestKind = "url"
- Write PermissionRequestKind = "write"
+ PermissionRequestKindCustomTool PermissionRequestKind = "custom-tool"
+ PermissionRequestKindHook PermissionRequestKind = "hook"
+ PermissionRequestKindShell PermissionRequestKind = "shell"
+ PermissionRequestKindURL PermissionRequestKind = "url"
+ PermissionRequestKindMcp PermissionRequestKind = "mcp"
+ PermissionRequestKindMemory PermissionRequestKind = "memory"
+ PermissionRequestKindRead PermissionRequestKind = "read"
+ PermissionRequestKindWrite PermissionRequestKind = "write"
)
type RequestedSchemaType string
const (
- Object RequestedSchemaType = "object"
+ RequestedSchemaTypeObject RequestedSchemaType = "object"
)
// Theme variant this icon is intended for
type Theme string
const (
- Dark Theme = "dark"
- Light Theme = "light"
+ ThemeDark Theme = "dark"
+ ThemeLight Theme = "light"
)
type ContentType string
const (
- Audio ContentType = "audio"
- Image ContentType = "image"
- Resource ContentType = "resource"
- ResourceLink ContentType = "resource_link"
- Terminal ContentType = "terminal"
- Text ContentType = "text"
+ ContentTypeAudio ContentType = "audio"
+ ContentTypeImage ContentType = "image"
+ ContentTypeResource ContentType = "resource"
+ ContentTypeResourceLink ContentType = "resource_link"
+ ContentTypeTerminal ContentType = "terminal"
+ ContentTypeText ContentType = "text"
)
// The outcome of the permission request
type ResultKind string
const (
- Approved ResultKind = "approved"
- DeniedByContentExclusionPolicy ResultKind = "denied-by-content-exclusion-policy"
- DeniedByRules ResultKind = "denied-by-rules"
- DeniedInteractivelyByUser ResultKind = "denied-interactively-by-user"
- DeniedNoApprovalRuleAndCouldNotRequestFromUser ResultKind = "denied-no-approval-rule-and-could-not-request-from-user"
+ ResultKindApproved ResultKind = "approved"
+ ResultKindDeniedByContentExclusionPolicy ResultKind = "denied-by-content-exclusion-policy"
+ ResultKindDeniedByRules ResultKind = "denied-by-rules"
+ ResultKindDeniedInteractivelyByUser ResultKind = "denied-interactively-by-user"
+ ResultKindDeniedNoApprovalRuleAndCouldNotRequestFromUser ResultKind = "denied-no-approval-rule-and-could-not-request-from-user"
)
// Message role: "system" for system prompts, "developer" for developer-injected instructions
type Role string
const (
- Developer Role = "developer"
- RoleSystem Role = "system"
+ RoleDeveloper Role = "developer"
+ RoleSystem Role = "system"
)
-// Whether the session ended normally ("routine") or due to a crash/fatal error ("error")
-type ShutdownType string
+// Connection status: connected, failed, pending, disabled, or not_configured
+//
+// New connection status: connected, failed, pending, disabled, or not_configured
+type ServerStatus string
const (
- Error ShutdownType = "error"
- Routine ShutdownType = "routine"
+ ServerStatusConnected ServerStatus = "connected"
+ ServerStatusDisabled ServerStatus = "disabled"
+ ServerStatusNotConfigured ServerStatus = "not_configured"
+ ServerStatusPending ServerStatus = "pending"
+ ServerStatusFailed ServerStatus = "failed"
)
-// Origin of this message, used for timeline filtering and telemetry (e.g., "user",
-// "autopilot", "skill", or "command")
-type Source string
+// Whether the session ended normally ("routine") or due to a crash/fatal error ("error")
+type ShutdownType string
const (
- Command Source = "command"
- ImmediatePrompt Source = "immediate-prompt"
- JITInstruction Source = "jit-instruction"
- Other Source = "other"
- Skill Source = "skill"
- SnippyBlocking Source = "snippy-blocking"
- SourceAutopilot Source = "autopilot"
- SourceSystem Source = "system"
- ThinkingExhaustedContinuation Source = "thinking-exhausted-continuation"
- User Source = "user"
+ ShutdownTypeError ShutdownType = "error"
+ ShutdownTypeRoutine ShutdownType = "routine"
)
// Origin type of the session being handed off
type SourceType string
const (
- Local SourceType = "local"
- Remote SourceType = "remote"
+ SourceTypeLocal SourceType = "local"
+ SourceTypeRemote SourceType = "remote"
)
// Tool call type: "function" for standard tool calls, "custom" for grammar-based tool
@@ -1354,74 +1512,82 @@ const (
type ToolRequestType string
const (
- Custom ToolRequestType = "custom"
- Function ToolRequestType = "function"
+ ToolRequestTypeCustom ToolRequestType = "custom"
+ ToolRequestTypeFunction ToolRequestType = "function"
)
type SessionEventType string
const (
- Abort SessionEventType = "abort"
- AssistantIntent SessionEventType = "assistant.intent"
- AssistantMessage SessionEventType = "assistant.message"
- AssistantMessageDelta SessionEventType = "assistant.message_delta"
- AssistantReasoning SessionEventType = "assistant.reasoning"
- AssistantReasoningDelta SessionEventType = "assistant.reasoning_delta"
- AssistantStreamingDelta SessionEventType = "assistant.streaming_delta"
- AssistantTurnEnd SessionEventType = "assistant.turn_end"
- AssistantTurnStart SessionEventType = "assistant.turn_start"
- AssistantUsage SessionEventType = "assistant.usage"
- CommandCompleted SessionEventType = "command.completed"
- CommandQueued SessionEventType = "command.queued"
- ElicitationCompleted SessionEventType = "elicitation.completed"
- ElicitationRequested SessionEventType = "elicitation.requested"
- ExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed"
- ExitPlanModeRequested SessionEventType = "exit_plan_mode.requested"
- ExternalToolCompleted SessionEventType = "external_tool.completed"
- ExternalToolRequested SessionEventType = "external_tool.requested"
- HookEnd SessionEventType = "hook.end"
- HookStart SessionEventType = "hook.start"
- PendingMessagesModified SessionEventType = "pending_messages.modified"
- PermissionCompleted SessionEventType = "permission.completed"
- PermissionRequested SessionEventType = "permission.requested"
- SessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed"
- SessionCompactionComplete SessionEventType = "session.compaction_complete"
- SessionCompactionStart SessionEventType = "session.compaction_start"
- SessionContextChanged SessionEventType = "session.context_changed"
- SessionError SessionEventType = "session.error"
- SessionHandoff SessionEventType = "session.handoff"
- SessionIdle SessionEventType = "session.idle"
- SessionInfo SessionEventType = "session.info"
- SessionModeChanged SessionEventType = "session.mode_changed"
- SessionModelChange SessionEventType = "session.model_change"
- SessionPlanChanged SessionEventType = "session.plan_changed"
- SessionResume SessionEventType = "session.resume"
- SessionShutdown SessionEventType = "session.shutdown"
- SessionSnapshotRewind SessionEventType = "session.snapshot_rewind"
- SessionStart SessionEventType = "session.start"
- SessionTaskComplete SessionEventType = "session.task_complete"
- SessionTitleChanged SessionEventType = "session.title_changed"
- SessionToolsUpdated SessionEventType = "session.tools_updated"
- SessionTruncation SessionEventType = "session.truncation"
- SessionUsageInfo SessionEventType = "session.usage_info"
- SessionWarning SessionEventType = "session.warning"
- SessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed"
- SkillInvoked SessionEventType = "skill.invoked"
- SubagentCompleted SessionEventType = "subagent.completed"
- SubagentDeselected SessionEventType = "subagent.deselected"
- SubagentFailed SessionEventType = "subagent.failed"
- SubagentSelected SessionEventType = "subagent.selected"
- SubagentStarted SessionEventType = "subagent.started"
- SystemMessage SessionEventType = "system.message"
- SystemNotification SessionEventType = "system.notification"
- ToolExecutionComplete SessionEventType = "tool.execution_complete"
- ToolExecutionPartialResult SessionEventType = "tool.execution_partial_result"
- ToolExecutionProgress SessionEventType = "tool.execution_progress"
- ToolExecutionStart SessionEventType = "tool.execution_start"
- ToolUserRequested SessionEventType = "tool.user_requested"
- UserInputCompleted SessionEventType = "user_input.completed"
- UserInputRequested SessionEventType = "user_input.requested"
- UserMessage SessionEventType = "user.message"
+ SessionEventTypeAbort SessionEventType = "abort"
+ SessionEventTypeAssistantIntent SessionEventType = "assistant.intent"
+ SessionEventTypeAssistantMessage SessionEventType = "assistant.message"
+ SessionEventTypeAssistantMessageDelta SessionEventType = "assistant.message_delta"
+ SessionEventTypeAssistantReasoning SessionEventType = "assistant.reasoning"
+ SessionEventTypeAssistantReasoningDelta SessionEventType = "assistant.reasoning_delta"
+ SessionEventTypeAssistantStreamingDelta SessionEventType = "assistant.streaming_delta"
+ SessionEventTypeAssistantTurnEnd SessionEventType = "assistant.turn_end"
+ SessionEventTypeAssistantTurnStart SessionEventType = "assistant.turn_start"
+ SessionEventTypeAssistantUsage SessionEventType = "assistant.usage"
+ SessionEventTypeCommandCompleted SessionEventType = "command.completed"
+ SessionEventTypeCommandExecute SessionEventType = "command.execute"
+ SessionEventTypeCommandQueued SessionEventType = "command.queued"
+ SessionEventTypeCommandsChanged SessionEventType = "commands.changed"
+ SessionEventTypeElicitationCompleted SessionEventType = "elicitation.completed"
+ SessionEventTypeElicitationRequested SessionEventType = "elicitation.requested"
+ SessionEventTypeExitPlanModeCompleted SessionEventType = "exit_plan_mode.completed"
+ SessionEventTypeExitPlanModeRequested SessionEventType = "exit_plan_mode.requested"
+ SessionEventTypeExternalToolCompleted SessionEventType = "external_tool.completed"
+ SessionEventTypeExternalToolRequested SessionEventType = "external_tool.requested"
+ SessionEventTypeHookEnd SessionEventType = "hook.end"
+ SessionEventTypeHookStart SessionEventType = "hook.start"
+ SessionEventTypeMcpOauthCompleted SessionEventType = "mcp.oauth_completed"
+ SessionEventTypeMcpOauthRequired SessionEventType = "mcp.oauth_required"
+ SessionEventTypePendingMessagesModified SessionEventType = "pending_messages.modified"
+ SessionEventTypePermissionCompleted SessionEventType = "permission.completed"
+ SessionEventTypePermissionRequested SessionEventType = "permission.requested"
+ SessionEventTypeSessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed"
+ SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete"
+ SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start"
+ SessionEventTypeSessionContextChanged SessionEventType = "session.context_changed"
+ SessionEventTypeSessionError SessionEventType = "session.error"
+ SessionEventTypeSessionExtensionsLoaded SessionEventType = "session.extensions_loaded"
+ SessionEventTypeSessionHandoff SessionEventType = "session.handoff"
+ SessionEventTypeSessionIdle SessionEventType = "session.idle"
+ SessionEventTypeSessionInfo SessionEventType = "session.info"
+ SessionEventTypeSessionMcpServerStatusChanged SessionEventType = "session.mcp_server_status_changed"
+ SessionEventTypeSessionMcpServersLoaded SessionEventType = "session.mcp_servers_loaded"
+ SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed"
+ SessionEventTypeSessionModelChange SessionEventType = "session.model_change"
+ SessionEventTypeSessionPlanChanged SessionEventType = "session.plan_changed"
+ SessionEventTypeSessionResume SessionEventType = "session.resume"
+ SessionEventTypeSessionShutdown SessionEventType = "session.shutdown"
+ SessionEventTypeSessionSkillsLoaded SessionEventType = "session.skills_loaded"
+ SessionEventTypeSessionSnapshotRewind SessionEventType = "session.snapshot_rewind"
+ SessionEventTypeSessionStart SessionEventType = "session.start"
+ SessionEventTypeSessionTaskComplete SessionEventType = "session.task_complete"
+ SessionEventTypeSessionTitleChanged SessionEventType = "session.title_changed"
+ SessionEventTypeSessionToolsUpdated SessionEventType = "session.tools_updated"
+ SessionEventTypeSessionTruncation SessionEventType = "session.truncation"
+ SessionEventTypeSessionUsageInfo SessionEventType = "session.usage_info"
+ SessionEventTypeSessionWarning SessionEventType = "session.warning"
+ SessionEventTypeSessionWorkspaceFileChanged SessionEventType = "session.workspace_file_changed"
+ SessionEventTypeSkillInvoked SessionEventType = "skill.invoked"
+ SessionEventTypeSubagentCompleted SessionEventType = "subagent.completed"
+ SessionEventTypeSubagentDeselected SessionEventType = "subagent.deselected"
+ SessionEventTypeSubagentFailed SessionEventType = "subagent.failed"
+ SessionEventTypeSubagentSelected SessionEventType = "subagent.selected"
+ SessionEventTypeSubagentStarted SessionEventType = "subagent.started"
+ SessionEventTypeSystemMessage SessionEventType = "system.message"
+ SessionEventTypeSystemNotification SessionEventType = "system.notification"
+ SessionEventTypeToolExecutionComplete SessionEventType = "tool.execution_complete"
+ SessionEventTypeToolExecutionPartialResult SessionEventType = "tool.execution_partial_result"
+ SessionEventTypeToolExecutionProgress SessionEventType = "tool.execution_progress"
+ SessionEventTypeToolExecutionStart SessionEventType = "tool.execution_start"
+ SessionEventTypeToolUserRequested SessionEventType = "tool.user_requested"
+ SessionEventTypeUserInputCompleted SessionEventType = "user_input.completed"
+ SessionEventTypeUserInputRequested SessionEventType = "user_input.requested"
+ SessionEventTypeUserMessage SessionEventType = "user.message"
)
type ContextUnion struct {
diff --git a/go/internal/e2e/agent_and_compact_rpc_test.go b/go/internal/e2e/agent_and_compact_rpc_test.go
index 338f4da67..cbd52a326 100644
--- a/go/internal/e2e/agent_and_compact_rpc_test.go
+++ b/go/internal/e2e/agent_and_compact_rpc_test.go
@@ -215,7 +215,7 @@ func TestAgentSelectionRpc(t *testing.T) {
}
})
- t.Run("should return empty list when no custom agents configured", func(t *testing.T) {
+ t.Run("should return no custom agents when none configured", func(t *testing.T) {
client := copilot.NewClient(&copilot.ClientOptions{
CLIPath: cliPath,
UseStdio: copilot.Bool(true),
@@ -238,8 +238,13 @@ func TestAgentSelectionRpc(t *testing.T) {
t.Fatalf("Failed to list agents: %v", err)
}
- if len(result.Agents) != 0 {
- t.Errorf("Expected empty agent list, got %d agents", len(result.Agents))
+ // The CLI may return built-in/default agents even when no custom agents
+ // are configured, so just verify none of the known custom agent names appear.
+ customNames := map[string]bool{"test-agent": true, "another-agent": true}
+ for _, agent := range result.Agents {
+ if customNames[agent.Name] {
+ t.Errorf("Expected no custom agents, but found %q", agent.Name)
+ }
}
if err := client.Stop(); err != nil {
diff --git a/go/internal/e2e/compaction_test.go b/go/internal/e2e/compaction_test.go
index aee80704d..888ab2aa9 100644
--- a/go/internal/e2e/compaction_test.go
+++ b/go/internal/e2e/compaction_test.go
@@ -36,10 +36,10 @@ func TestCompaction(t *testing.T) {
var compactionCompleteEvents []copilot.SessionEvent
session.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.SessionCompactionStart {
+ if event.Type == copilot.SessionEventTypeSessionCompactionStart {
compactionStartEvents = append(compactionStartEvents, event)
}
- if event.Type == copilot.SessionCompactionComplete {
+ if event.Type == copilot.SessionEventTypeSessionCompactionComplete {
compactionCompleteEvents = append(compactionCompleteEvents, event)
}
})
@@ -105,7 +105,7 @@ func TestCompaction(t *testing.T) {
var compactionEvents []copilot.SessionEvent
session.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.SessionCompactionStart || event.Type == copilot.SessionCompactionComplete {
+ if event.Type == copilot.SessionEventTypeSessionCompactionStart || event.Type == copilot.SessionEventTypeSessionCompactionComplete {
compactionEvents = append(compactionEvents, event)
}
})
diff --git a/go/internal/e2e/multi_client_test.go b/go/internal/e2e/multi_client_test.go
index 9571ab58e..3c7dc34c3 100644
--- a/go/internal/e2e/multi_client_test.go
+++ b/go/internal/e2e/multi_client_test.go
@@ -79,13 +79,13 @@ func TestMultiClient(t *testing.T) {
client2Completed := make(chan struct{}, 1)
session1.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.ExternalToolRequested {
+ if event.Type == copilot.SessionEventTypeExternalToolRequested {
select {
case client1Requested <- struct{}{}:
default:
}
}
- if event.Type == copilot.ExternalToolCompleted {
+ if event.Type == copilot.SessionEventTypeExternalToolCompleted {
select {
case client1Completed <- struct{}{}:
default:
@@ -93,13 +93,13 @@ func TestMultiClient(t *testing.T) {
}
})
session2.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.ExternalToolRequested {
+ if event.Type == copilot.SessionEventTypeExternalToolRequested {
select {
case client2Requested <- struct{}{}:
default:
}
}
- if event.Type == copilot.ExternalToolCompleted {
+ if event.Type == copilot.SessionEventTypeExternalToolCompleted {
select {
case client2Completed <- struct{}{}:
default:
@@ -120,7 +120,7 @@ func TestMultiClient(t *testing.T) {
}
// Wait for all broadcast events to arrive on both clients
- timeout := time.After(10 * time.Second)
+ timeout := time.After(30 * time.Second)
for _, ch := range []chan struct{}{client1Requested, client2Requested, client1Completed, client2Completed} {
select {
case <-ch:
@@ -197,10 +197,10 @@ func TestMultiClient(t *testing.T) {
// Both clients should have seen permission.requested events
mu1.Lock()
- c1PermRequested := filterEventsByType(client1Events, copilot.PermissionRequested)
+ c1PermRequested := filterEventsByType(client1Events, copilot.SessionEventTypePermissionRequested)
mu1.Unlock()
mu2.Lock()
- c2PermRequested := filterEventsByType(client2Events, copilot.PermissionRequested)
+ c2PermRequested := filterEventsByType(client2Events, copilot.SessionEventTypePermissionRequested)
mu2.Unlock()
if len(c1PermRequested) == 0 {
@@ -212,10 +212,10 @@ func TestMultiClient(t *testing.T) {
// Both clients should have seen permission.completed events with approved result
mu1.Lock()
- c1PermCompleted := filterEventsByType(client1Events, copilot.PermissionCompleted)
+ c1PermCompleted := filterEventsByType(client1Events, copilot.SessionEventTypePermissionCompleted)
mu1.Unlock()
mu2.Lock()
- c2PermCompleted := filterEventsByType(client2Events, copilot.PermissionCompleted)
+ c2PermCompleted := filterEventsByType(client2Events, copilot.SessionEventTypePermissionCompleted)
mu2.Unlock()
if len(c1PermCompleted) == 0 {
@@ -293,10 +293,10 @@ func TestMultiClient(t *testing.T) {
// Both clients should have seen permission.requested events
mu1.Lock()
- c1PermRequested := filterEventsByType(client1Events, copilot.PermissionRequested)
+ c1PermRequested := filterEventsByType(client1Events, copilot.SessionEventTypePermissionRequested)
mu1.Unlock()
mu2.Lock()
- c2PermRequested := filterEventsByType(client2Events, copilot.PermissionRequested)
+ c2PermRequested := filterEventsByType(client2Events, copilot.SessionEventTypePermissionRequested)
mu2.Unlock()
if len(c1PermRequested) == 0 {
@@ -308,10 +308,10 @@ func TestMultiClient(t *testing.T) {
// Both clients should see the denial in the completed event
mu1.Lock()
- c1PermCompleted := filterEventsByType(client1Events, copilot.PermissionCompleted)
+ c1PermCompleted := filterEventsByType(client1Events, copilot.SessionEventTypePermissionCompleted)
mu1.Unlock()
mu2.Lock()
- c2PermCompleted := filterEventsByType(client2Events, copilot.PermissionCompleted)
+ c2PermCompleted := filterEventsByType(client2Events, copilot.SessionEventTypePermissionCompleted)
mu2.Unlock()
if len(c1PermCompleted) == 0 {
diff --git a/go/internal/e2e/permissions_test.go b/go/internal/e2e/permissions_test.go
index 328e7e788..98f620043 100644
--- a/go/internal/e2e/permissions_test.go
+++ b/go/internal/e2e/permissions_test.go
@@ -173,7 +173,7 @@ func TestPermissions(t *testing.T) {
permissionDenied := false
session.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.ToolExecutionComplete &&
+ if event.Type == copilot.SessionEventTypeToolExecutionComplete &&
event.Data.Success != nil && !*event.Data.Success &&
event.Data.Error != nil && event.Data.Error.ErrorClass != nil &&
strings.Contains(event.Data.Error.ErrorClass.Message, "Permission denied") {
@@ -223,7 +223,7 @@ func TestPermissions(t *testing.T) {
permissionDenied := false
session2.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.ToolExecutionComplete &&
+ if event.Type == copilot.SessionEventTypeToolExecutionComplete &&
event.Data.Success != nil && !*event.Data.Success &&
event.Data.Error != nil && event.Data.Error.ErrorClass != nil &&
strings.Contains(event.Data.Error.ErrorClass.Message, "Permission denied") {
diff --git a/go/internal/e2e/rpc_test.go b/go/internal/e2e/rpc_test.go
index ebcbe1130..3d69b97ad 100644
--- a/go/internal/e2e/rpc_test.go
+++ b/go/internal/e2e/rpc_test.go
@@ -219,16 +219,16 @@ func TestSessionRpc(t *testing.T) {
if err != nil {
t.Fatalf("Failed to get mode: %v", err)
}
- if initial.Mode != rpc.Interactive {
+ if initial.Mode != rpc.ModeInteractive {
t.Errorf("Expected initial mode 'interactive', got %q", initial.Mode)
}
// Switch to plan mode
- planResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Plan})
+ planResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.ModePlan})
if err != nil {
t.Fatalf("Failed to set mode to plan: %v", err)
}
- if planResult.Mode != rpc.Plan {
+ if planResult.Mode != rpc.ModePlan {
t.Errorf("Expected mode 'plan', got %q", planResult.Mode)
}
@@ -237,16 +237,16 @@ func TestSessionRpc(t *testing.T) {
if err != nil {
t.Fatalf("Failed to get mode after plan: %v", err)
}
- if afterPlan.Mode != rpc.Plan {
+ if afterPlan.Mode != rpc.ModePlan {
t.Errorf("Expected mode 'plan' after set, got %q", afterPlan.Mode)
}
// Switch back to interactive
- interactiveResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Interactive})
+ interactiveResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.ModeInteractive})
if err != nil {
t.Fatalf("Failed to set mode to interactive: %v", err)
}
- if interactiveResult.Mode != rpc.Interactive {
+ if interactiveResult.Mode != rpc.ModeInteractive {
t.Errorf("Expected mode 'interactive', got %q", interactiveResult.Mode)
}
})
diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go
index 052ae1580..7f1817da9 100644
--- a/go/internal/e2e/session_test.go
+++ b/go/internal/e2e/session_test.go
@@ -184,6 +184,51 @@ func TestSession(t *testing.T) {
}
})
+ t.Run("should create a session with customized systemMessage config", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ customTone := "Respond in a warm, professional tone. Be thorough in explanations."
+ appendedContent := "Always mention quarterly earnings."
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ copilot.SectionTone: {Action: "replace", Content: customTone},
+ copilot.SectionCodeChangeRules: {Action: "remove"},
+ },
+ Content: appendedContent,
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Who are you?"})
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ // Validate the system message sent to the model
+ traffic, err := ctx.GetExchanges()
+ if err != nil {
+ t.Fatalf("Failed to get exchanges: %v", err)
+ }
+ if len(traffic) == 0 {
+ t.Fatal("Expected at least one exchange")
+ }
+ systemMessage := getSystemMessage(traffic[0])
+ if !strings.Contains(systemMessage, customTone) {
+ t.Errorf("Expected system message to contain custom tone, got %q", systemMessage)
+ }
+ if !strings.Contains(systemMessage, appendedContent) {
+ t.Errorf("Expected system message to contain appended content, got %q", systemMessage)
+ }
+ if strings.Contains(systemMessage, "") {
+ t.Error("Expected system message to NOT contain code_change_instructions (it was removed)")
+ }
+ })
+
t.Run("should create a session with availableTools", func(t *testing.T) {
ctx.ConfigureForTest(t)
@@ -506,7 +551,7 @@ func TestSession(t *testing.T) {
toolStartCh := make(chan *copilot.SessionEvent, 1)
toolStartErrCh := make(chan error, 1)
go func() {
- evt, err := testharness.GetNextEventOfType(session, copilot.ToolExecutionStart, 60*time.Second)
+ evt, err := testharness.GetNextEventOfType(session, copilot.SessionEventTypeToolExecutionStart, 60*time.Second)
if err != nil {
toolStartErrCh <- err
} else {
@@ -517,7 +562,7 @@ func TestSession(t *testing.T) {
sessionIdleCh := make(chan *copilot.SessionEvent, 1)
sessionIdleErrCh := make(chan error, 1)
go func() {
- evt, err := testharness.GetNextEventOfType(session, copilot.SessionIdle, 60*time.Second)
+ evt, err := testharness.GetNextEventOfType(session, copilot.SessionEventTypeSessionIdle, 60*time.Second)
if err != nil {
sessionIdleErrCh <- err
} else {
@@ -565,7 +610,7 @@ func TestSession(t *testing.T) {
// Verify messages contain an abort event
hasAbortEvent := false
for _, msg := range messages {
- if msg.Type == copilot.Abort {
+ if msg.Type == copilot.SessionEventTypeAbort {
hasAbortEvent = true
break
}
@@ -913,7 +958,7 @@ func TestSetModelWithReasoningEffort(t *testing.T) {
modelChanged := make(chan copilot.SessionEvent, 1)
session.On(func(event copilot.SessionEvent) {
- if event.Type == copilot.SessionModelChange {
+ if event.Type == copilot.SessionEventTypeSessionModelChange {
select {
case modelChanged <- event:
default:
@@ -964,7 +1009,7 @@ func TestSessionBlobAttachment(t *testing.T) {
Prompt: "Describe this image",
Attachments: []copilot.Attachment{
{
- Type: copilot.Blob,
+ Type: copilot.AttachmentTypeBlob,
Data: &data,
MIMEType: &mimeType,
DisplayName: &displayName,
@@ -1028,7 +1073,7 @@ func TestSessionLog(t *testing.T) {
t.Fatalf("Log failed: %v", err)
}
- evt := waitForEvent(t, &mu, &events, copilot.SessionInfo, "Info message", 5*time.Second)
+ evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionInfo, "Info message", 5*time.Second)
if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" {
t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType)
}
@@ -1038,11 +1083,11 @@ func TestSessionLog(t *testing.T) {
})
t.Run("should log warning message", func(t *testing.T) {
- if err := session.Log(t.Context(), "Warning message", &copilot.LogOptions{Level: rpc.Warning}); err != nil {
+ if err := session.Log(t.Context(), "Warning message", &copilot.LogOptions{Level: rpc.LevelWarning}); err != nil {
t.Fatalf("Log failed: %v", err)
}
- evt := waitForEvent(t, &mu, &events, copilot.SessionWarning, "Warning message", 5*time.Second)
+ evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionWarning, "Warning message", 5*time.Second)
if evt.Data.WarningType == nil || *evt.Data.WarningType != "notification" {
t.Errorf("Expected warningType 'notification', got %v", evt.Data.WarningType)
}
@@ -1052,11 +1097,11 @@ func TestSessionLog(t *testing.T) {
})
t.Run("should log error message", func(t *testing.T) {
- if err := session.Log(t.Context(), "Error message", &copilot.LogOptions{Level: rpc.Error}); err != nil {
+ if err := session.Log(t.Context(), "Error message", &copilot.LogOptions{Level: rpc.LevelError}); err != nil {
t.Fatalf("Log failed: %v", err)
}
- evt := waitForEvent(t, &mu, &events, copilot.SessionError, "Error message", 5*time.Second)
+ evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionError, "Error message", 5*time.Second)
if evt.Data.ErrorType == nil || *evt.Data.ErrorType != "notification" {
t.Errorf("Expected errorType 'notification', got %v", evt.Data.ErrorType)
}
@@ -1070,7 +1115,7 @@ func TestSessionLog(t *testing.T) {
t.Fatalf("Log failed: %v", err)
}
- evt := waitForEvent(t, &mu, &events, copilot.SessionInfo, "Ephemeral message", 5*time.Second)
+ evt := waitForEvent(t, &mu, &events, copilot.SessionEventTypeSessionInfo, "Ephemeral message", 5*time.Second)
if evt.Data.InfoType == nil || *evt.Data.InfoType != "notification" {
t.Errorf("Expected infoType 'notification', got %v", evt.Data.InfoType)
}
diff --git a/go/internal/e2e/system_message_transform_test.go b/go/internal/e2e/system_message_transform_test.go
new file mode 100644
index 000000000..2d62b01cf
--- /dev/null
+++ b/go/internal/e2e/system_message_transform_test.go
@@ -0,0 +1,189 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+package e2e
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+
+ copilot "github.com/github/copilot-sdk/go"
+ "github.com/github/copilot-sdk/go/internal/e2e/testharness"
+)
+
+func TestSystemMessageTransform(t *testing.T) {
+ ctx := testharness.NewTestContext(t)
+ client := ctx.NewClient()
+ t.Cleanup(func() { client.ForceStop() })
+
+ t.Run("should_invoke_transform_callbacks_with_section_content", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ var identityContent string
+ var toneContent string
+ var mu sync.Mutex
+ identityCalled := false
+ toneCalled := false
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ "identity": {
+ Transform: func(currentContent string) (string, error) {
+ mu.Lock()
+ identityCalled = true
+ identityContent = currentContent
+ mu.Unlock()
+ return currentContent, nil
+ },
+ },
+ "tone": {
+ Transform: func(currentContent string) (string, error) {
+ mu.Lock()
+ toneCalled = true
+ toneContent = currentContent
+ mu.Unlock()
+ return currentContent, nil
+ },
+ },
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ testFile := filepath.Join(ctx.WorkDir, "test.txt")
+ err = os.WriteFile(testFile, []byte("Hello transform!"), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write test file: %v", err)
+ }
+
+ _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{
+ Prompt: "Read the contents of test.txt and tell me what it says",
+ })
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+
+ if !identityCalled {
+ t.Error("Expected identity transform callback to be invoked")
+ }
+ if !toneCalled {
+ t.Error("Expected tone transform callback to be invoked")
+ }
+ if identityContent == "" {
+ t.Error("Expected identity transform to receive non-empty content")
+ }
+ if toneContent == "" {
+ t.Error("Expected tone transform to receive non-empty content")
+ }
+ })
+
+ t.Run("should_apply_transform_modifications_to_section_content", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ "identity": {
+ Transform: func(currentContent string) (string, error) {
+ return currentContent + "\nAlways end your reply with TRANSFORM_MARKER", nil
+ },
+ },
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ testFile := filepath.Join(ctx.WorkDir, "hello.txt")
+ err = os.WriteFile(testFile, []byte("Hello!"), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write test file: %v", err)
+ }
+
+ assistantMessage, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
+ Prompt: "Read the contents of hello.txt",
+ })
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ // Verify the transform result was actually applied to the system message
+ traffic, err := ctx.GetExchanges()
+ if err != nil {
+ t.Fatalf("Failed to get exchanges: %v", err)
+ }
+ if len(traffic) == 0 {
+ t.Fatal("Expected at least one exchange")
+ }
+ systemMessage := getSystemMessage(traffic[0])
+ if !strings.Contains(systemMessage, "TRANSFORM_MARKER") {
+ t.Errorf("Expected system message to contain TRANSFORM_MARKER, got %q", systemMessage)
+ }
+
+ _ = assistantMessage
+ })
+
+ t.Run("should_work_with_static_overrides_and_transforms_together", func(t *testing.T) {
+ ctx.ConfigureForTest(t)
+
+ var mu sync.Mutex
+ transformCalled := false
+
+ session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
+ OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
+ SystemMessage: &copilot.SystemMessageConfig{
+ Mode: "customize",
+ Sections: map[string]copilot.SectionOverride{
+ "safety": {
+ Action: copilot.SectionActionRemove,
+ },
+ "identity": {
+ Transform: func(currentContent string) (string, error) {
+ mu.Lock()
+ transformCalled = true
+ mu.Unlock()
+ return currentContent, nil
+ },
+ },
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ testFile := filepath.Join(ctx.WorkDir, "combo.txt")
+ err = os.WriteFile(testFile, []byte("Combo test!"), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write test file: %v", err)
+ }
+
+ _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{
+ Prompt: "Read the contents of combo.txt and tell me what it says",
+ })
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+
+ mu.Lock()
+ defer mu.Unlock()
+
+ if !transformCalled {
+ t.Error("Expected identity transform callback to be invoked")
+ }
+ })
+}
diff --git a/go/internal/e2e/testharness/helper.go b/go/internal/e2e/testharness/helper.go
index 05947c806..3b521f330 100644
--- a/go/internal/e2e/testharness/helper.go
+++ b/go/internal/e2e/testharness/helper.go
@@ -67,7 +67,7 @@ func GetNextEventOfType(session *copilot.Session, eventType copilot.SessionEvent
case result <- &event:
default:
}
- case copilot.SessionError:
+ case copilot.SessionEventTypeSessionError:
msg := "session error"
if event.Data.Message != nil {
msg = *event.Data.Message
diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go
index 401f38305..b9ba408b5 100644
--- a/go/rpc/generated_rpc.go
+++ b/go/rpc/generated_rpc.go
@@ -223,10 +223,10 @@ type SessionFleetStartParams struct {
// Experimental: SessionAgentListResult is part of an experimental API and may change or be removed.
type SessionAgentListResult struct {
// Available custom agents
- Agents []AgentElement `json:"agents"`
+ Agents []SessionAgentListResultAgent `json:"agents"`
}
-type AgentElement struct {
+type SessionAgentListResultAgent struct {
// Description of the agent's purpose
Description string `json:"description"`
// Human-readable display name
@@ -276,6 +276,161 @@ type SessionAgentSelectParams struct {
type SessionAgentDeselectResult struct {
}
+// Experimental: SessionAgentReloadResult is part of an experimental API and may change or be removed.
+type SessionAgentReloadResult struct {
+ // Reloaded custom agents
+ Agents []SessionAgentReloadResultAgent `json:"agents"`
+}
+
+type SessionAgentReloadResultAgent struct {
+ // Description of the agent's purpose
+ Description string `json:"description"`
+ // Human-readable display name
+ DisplayName string `json:"displayName"`
+ // Unique identifier of the custom agent
+ Name string `json:"name"`
+}
+
+// Experimental: SessionSkillsListResult is part of an experimental API and may change or be removed.
+type SessionSkillsListResult struct {
+ // Available skills
+ Skills []Skill `json:"skills"`
+}
+
+type Skill struct {
+ // Description of what the skill does
+ Description string `json:"description"`
+ // Whether the skill is currently enabled
+ Enabled bool `json:"enabled"`
+ // Unique identifier for the skill
+ Name string `json:"name"`
+ // Absolute path to the skill file
+ Path *string `json:"path,omitempty"`
+ // Source location type (e.g., project, personal, plugin)
+ Source string `json:"source"`
+ // Whether the skill can be invoked by the user as a slash command
+ UserInvocable bool `json:"userInvocable"`
+}
+
+// Experimental: SessionSkillsEnableResult is part of an experimental API and may change or be removed.
+type SessionSkillsEnableResult struct {
+}
+
+// Experimental: SessionSkillsEnableParams is part of an experimental API and may change or be removed.
+type SessionSkillsEnableParams struct {
+ // Name of the skill to enable
+ Name string `json:"name"`
+}
+
+// Experimental: SessionSkillsDisableResult is part of an experimental API and may change or be removed.
+type SessionSkillsDisableResult struct {
+}
+
+// Experimental: SessionSkillsDisableParams is part of an experimental API and may change or be removed.
+type SessionSkillsDisableParams struct {
+ // Name of the skill to disable
+ Name string `json:"name"`
+}
+
+// Experimental: SessionSkillsReloadResult is part of an experimental API and may change or be removed.
+type SessionSkillsReloadResult struct {
+}
+
+type SessionMCPListResult struct {
+ // Configured MCP servers
+ Servers []Server `json:"servers"`
+}
+
+type Server struct {
+ // Error message if the server failed to connect
+ Error *string `json:"error,omitempty"`
+ // Server name (config key)
+ Name string `json:"name"`
+ // Configuration source: user, workspace, plugin, or builtin
+ Source *string `json:"source,omitempty"`
+ // Connection status: connected, failed, pending, disabled, or not_configured
+ Status ServerStatus `json:"status"`
+}
+
+type SessionMCPEnableResult struct {
+}
+
+type SessionMCPEnableParams struct {
+ // Name of the MCP server to enable
+ ServerName string `json:"serverName"`
+}
+
+type SessionMCPDisableResult struct {
+}
+
+type SessionMCPDisableParams struct {
+ // Name of the MCP server to disable
+ ServerName string `json:"serverName"`
+}
+
+type SessionMCPReloadResult struct {
+}
+
+// Experimental: SessionPluginsListResult is part of an experimental API and may change or be removed.
+type SessionPluginsListResult struct {
+ // Installed plugins
+ Plugins []Plugin `json:"plugins"`
+}
+
+type Plugin struct {
+ // Whether the plugin is currently enabled
+ Enabled bool `json:"enabled"`
+ // Marketplace the plugin came from
+ Marketplace string `json:"marketplace"`
+ // Plugin name
+ Name string `json:"name"`
+ // Installed version
+ Version *string `json:"version,omitempty"`
+}
+
+// Experimental: SessionExtensionsListResult is part of an experimental API and may change or be removed.
+type SessionExtensionsListResult struct {
+ // Discovered extensions and their current status
+ Extensions []Extension `json:"extensions"`
+}
+
+type Extension struct {
+ // Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper')
+ ID string `json:"id"`
+ // Extension name (directory name)
+ Name string `json:"name"`
+ // Process ID if the extension is running
+ PID *int64 `json:"pid,omitempty"`
+ // Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)
+ Source Source `json:"source"`
+ // Current status: running, disabled, failed, or starting
+ Status ExtensionStatus `json:"status"`
+}
+
+// Experimental: SessionExtensionsEnableResult is part of an experimental API and may change or be removed.
+type SessionExtensionsEnableResult struct {
+}
+
+// Experimental: SessionExtensionsEnableParams is part of an experimental API and may change or be removed.
+type SessionExtensionsEnableParams struct {
+ // Source-qualified extension ID to enable
+ ID string `json:"id"`
+}
+
+// Experimental: SessionExtensionsDisableResult is part of an experimental API and may change or be removed.
+type SessionExtensionsDisableResult struct {
+}
+
+// Experimental: SessionExtensionsDisableParams is part of an experimental API and may change or be removed.
+type SessionExtensionsDisableParams struct {
+ // Source-qualified extension ID to disable
+ ID string `json:"id"`
+}
+
+// Experimental: SessionExtensionsReloadResult is part of an experimental API and may change or be removed.
+type SessionExtensionsReloadResult struct {
+}
+
// Experimental: SessionCompactionCompactResult is part of an experimental API and may change or be removed.
type SessionCompactionCompactResult struct {
// Number of messages removed during compaction
@@ -304,6 +459,75 @@ type ResultResult struct {
ToolTelemetry map[string]interface{} `json:"toolTelemetry,omitempty"`
}
+type SessionCommandsHandlePendingCommandResult struct {
+ Success bool `json:"success"`
+}
+
+type SessionCommandsHandlePendingCommandParams struct {
+ // Error message if the command handler failed
+ Error *string `json:"error,omitempty"`
+ // Request ID from the command invocation event
+ RequestID string `json:"requestId"`
+}
+
+type SessionUIElicitationResult struct {
+ // The user's response: accept (submitted), decline (rejected), or cancel (dismissed)
+ Action Action `json:"action"`
+ // The form values submitted by the user (present when action is 'accept')
+ Content map[string]*Content `json:"content,omitempty"`
+}
+
+type SessionUIElicitationParams struct {
+ // Message describing what information is needed from the user
+ Message string `json:"message"`
+ // JSON Schema describing the form fields to present to the user
+ RequestedSchema RequestedSchema `json:"requestedSchema"`
+}
+
+// JSON Schema describing the form fields to present to the user
+type RequestedSchema struct {
+ // Form field definitions, keyed by field name
+ Properties map[string]Property `json:"properties"`
+ // List of required field names
+ Required []string `json:"required,omitempty"`
+ // Schema type indicator (always 'object')
+ Type RequestedSchemaType `json:"type"`
+}
+
+type Property struct {
+ Default *Content `json:"default"`
+ Description *string `json:"description,omitempty"`
+ Enum []string `json:"enum,omitempty"`
+ EnumNames []string `json:"enumNames,omitempty"`
+ Title *string `json:"title,omitempty"`
+ Type PropertyType `json:"type"`
+ OneOf []OneOf `json:"oneOf,omitempty"`
+ Items *Items `json:"items,omitempty"`
+ MaxItems *float64 `json:"maxItems,omitempty"`
+ MinItems *float64 `json:"minItems,omitempty"`
+ Format *Format `json:"format,omitempty"`
+ MaxLength *float64 `json:"maxLength,omitempty"`
+ MinLength *float64 `json:"minLength,omitempty"`
+ Maximum *float64 `json:"maximum,omitempty"`
+ Minimum *float64 `json:"minimum,omitempty"`
+}
+
+type Items struct {
+ Enum []string `json:"enum,omitempty"`
+ Type *ItemsType `json:"type,omitempty"`
+ AnyOf []AnyOf `json:"anyOf,omitempty"`
+}
+
+type AnyOf struct {
+ Const string `json:"const"`
+ Title string `json:"title"`
+}
+
+type OneOf struct {
+ Const string `json:"const"`
+ Title string `json:"title"`
+}
+
type SessionPermissionsHandlePendingPermissionRequestResult struct {
// Whether the permission request was handled successfully
Success bool `json:"success"`
@@ -335,6 +559,8 @@ type SessionLogParams struct {
Level *Level `json:"level,omitempty"`
// Human-readable message
Message string `json:"message"`
+ // Optional URL the user can open in their browser for more details
+ URL *string `json:"url,omitempty"`
}
type SessionShellExecResult struct {
@@ -371,19 +597,88 @@ type SessionShellKillParams struct {
type Mode string
const (
- Autopilot Mode = "autopilot"
- Interactive Mode = "interactive"
- Plan Mode = "plan"
+ ModeAutopilot Mode = "autopilot"
+ ModeInteractive Mode = "interactive"
+ ModePlan Mode = "plan"
+)
+
+// Connection status: connected, failed, pending, disabled, or not_configured
+type ServerStatus string
+
+const (
+ ServerStatusConnected ServerStatus = "connected"
+ ServerStatusNotConfigured ServerStatus = "not_configured"
+ ServerStatusPending ServerStatus = "pending"
+ ServerStatusDisabled ServerStatus = "disabled"
+ ServerStatusFailed ServerStatus = "failed"
+)
+
+// Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)
+type Source string
+
+const (
+ SourceProject Source = "project"
+ SourceUser Source = "user"
+)
+
+// Current status: running, disabled, failed, or starting
+type ExtensionStatus string
+
+const (
+ ExtensionStatusDisabled ExtensionStatus = "disabled"
+ ExtensionStatusFailed ExtensionStatus = "failed"
+ ExtensionStatusRunning ExtensionStatus = "running"
+ ExtensionStatusStarting ExtensionStatus = "starting"
+)
+
+// The user's response: accept (submitted), decline (rejected), or cancel (dismissed)
+type Action string
+
+const (
+ ActionAccept Action = "accept"
+ ActionCancel Action = "cancel"
+ ActionDecline Action = "decline"
+)
+
+type Format string
+
+const (
+ FormatDate Format = "date"
+ FormatDateTime Format = "date-time"
+ FormatEmail Format = "email"
+ FormatUri Format = "uri"
+)
+
+type ItemsType string
+
+const (
+ ItemsTypeString ItemsType = "string"
+)
+
+type PropertyType string
+
+const (
+ PropertyTypeArray PropertyType = "array"
+ PropertyTypeBoolean PropertyType = "boolean"
+ PropertyTypeString PropertyType = "string"
+ PropertyTypeInteger PropertyType = "integer"
+ PropertyTypeNumber PropertyType = "number"
+)
+
+type RequestedSchemaType string
+
+const (
+ RequestedSchemaTypeObject RequestedSchemaType = "object"
)
type Kind string
const (
- Approved Kind = "approved"
- DeniedByContentExclusionPolicy Kind = "denied-by-content-exclusion-policy"
- DeniedByRules Kind = "denied-by-rules"
- DeniedInteractivelyByUser Kind = "denied-interactively-by-user"
- DeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user"
+ KindApproved Kind = "approved"
+ KindDeniedByContentExclusionPolicy Kind = "denied-by-content-exclusion-policy"
+ KindDeniedByRules Kind = "denied-by-rules"
+ KindDeniedInteractivelyByUser Kind = "denied-interactively-by-user"
+ KindDeniedNoApprovalRuleAndCouldNotRequestFromUser Kind = "denied-no-approval-rule-and-could-not-request-from-user"
)
// Log severity level. Determines how the message is displayed in the timeline. Defaults to
@@ -391,18 +686,18 @@ const (
type Level string
const (
- Error Level = "error"
- Info Level = "info"
- Warning Level = "warning"
+ LevelError Level = "error"
+ LevelInfo Level = "info"
+ LevelWarning Level = "warning"
)
// Signal to send (default: SIGTERM)
type Signal string
const (
- Sigint Signal = "SIGINT"
- Sigkill Signal = "SIGKILL"
- Sigterm Signal = "SIGTERM"
+ SignalSIGINT Signal = "SIGINT"
+ SignalSIGKILL Signal = "SIGKILL"
+ SignalSIGTERM Signal = "SIGTERM"
)
type ResultUnion struct {
@@ -410,6 +705,13 @@ type ResultUnion struct {
String *string
}
+type Content struct {
+ Bool *bool
+ Double *float64
+ String *string
+ StringArray []string
+}
+
type ServerModelsRpcApi struct {
client *jsonrpc2.Client
}
@@ -740,6 +1042,230 @@ func (a *AgentRpcApi) Deselect(ctx context.Context) (*SessionAgentDeselectResult
return &result, nil
}
+func (a *AgentRpcApi) Reload(ctx context.Context) (*SessionAgentReloadResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.agent.reload", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionAgentReloadResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+// Experimental: SkillsRpcApi contains experimental APIs that may change or be removed.
+type SkillsRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *SkillsRpcApi) List(ctx context.Context) (*SessionSkillsListResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.skills.list", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionSkillsListResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *SkillsRpcApi) Enable(ctx context.Context, params *SessionSkillsEnableParams) (*SessionSkillsEnableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["name"] = params.Name
+ }
+ raw, err := a.client.Request("session.skills.enable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionSkillsEnableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *SkillsRpcApi) Disable(ctx context.Context, params *SessionSkillsDisableParams) (*SessionSkillsDisableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["name"] = params.Name
+ }
+ raw, err := a.client.Request("session.skills.disable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionSkillsDisableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *SkillsRpcApi) Reload(ctx context.Context) (*SessionSkillsReloadResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.skills.reload", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionSkillsReloadResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+// Experimental: McpRpcApi contains experimental APIs that may change or be removed.
+type McpRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *McpRpcApi) List(ctx context.Context) (*SessionMCPListResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.mcp.list", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionMCPListResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *McpRpcApi) Enable(ctx context.Context, params *SessionMCPEnableParams) (*SessionMCPEnableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["serverName"] = params.ServerName
+ }
+ raw, err := a.client.Request("session.mcp.enable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionMCPEnableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *McpRpcApi) Disable(ctx context.Context, params *SessionMCPDisableParams) (*SessionMCPDisableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["serverName"] = params.ServerName
+ }
+ raw, err := a.client.Request("session.mcp.disable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionMCPDisableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *McpRpcApi) Reload(ctx context.Context) (*SessionMCPReloadResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.mcp.reload", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionMCPReloadResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+// Experimental: PluginsRpcApi contains experimental APIs that may change or be removed.
+type PluginsRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *PluginsRpcApi) List(ctx context.Context) (*SessionPluginsListResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.plugins.list", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionPluginsListResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+// Experimental: ExtensionsRpcApi contains experimental APIs that may change or be removed.
+type ExtensionsRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *ExtensionsRpcApi) List(ctx context.Context) (*SessionExtensionsListResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.extensions.list", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionExtensionsListResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *ExtensionsRpcApi) Enable(ctx context.Context, params *SessionExtensionsEnableParams) (*SessionExtensionsEnableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["id"] = params.ID
+ }
+ raw, err := a.client.Request("session.extensions.enable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionExtensionsEnableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *ExtensionsRpcApi) Disable(ctx context.Context, params *SessionExtensionsDisableParams) (*SessionExtensionsDisableResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["id"] = params.ID
+ }
+ raw, err := a.client.Request("session.extensions.disable", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionExtensionsDisableResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func (a *ExtensionsRpcApi) Reload(ctx context.Context) (*SessionExtensionsReloadResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ raw, err := a.client.Request("session.extensions.reload", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionExtensionsReloadResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
// Experimental: CompactionRpcApi contains experimental APIs that may change or be removed.
type CompactionRpcApi struct {
client *jsonrpc2.Client
@@ -786,6 +1312,52 @@ func (a *ToolsRpcApi) HandlePendingToolCall(ctx context.Context, params *Session
return &result, nil
}
+type CommandsRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *CommandsRpcApi) HandlePendingCommand(ctx context.Context, params *SessionCommandsHandlePendingCommandParams) (*SessionCommandsHandlePendingCommandResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["requestId"] = params.RequestID
+ if params.Error != nil {
+ req["error"] = *params.Error
+ }
+ }
+ raw, err := a.client.Request("session.commands.handlePendingCommand", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionCommandsHandlePendingCommandResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+type UiRpcApi struct {
+ client *jsonrpc2.Client
+ sessionID string
+}
+
+func (a *UiRpcApi) Elicitation(ctx context.Context, params *SessionUIElicitationParams) (*SessionUIElicitationResult, error) {
+ req := map[string]interface{}{"sessionId": a.sessionID}
+ if params != nil {
+ req["message"] = params.Message
+ req["requestedSchema"] = params.RequestedSchema
+ }
+ raw, err := a.client.Request("session.ui.elicitation", req)
+ if err != nil {
+ return nil, err
+ }
+ var result SessionUIElicitationResult
+ if err := json.Unmarshal(raw, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
type PermissionsRpcApi struct {
client *jsonrpc2.Client
sessionID string
@@ -864,8 +1436,14 @@ type SessionRpc struct {
Workspace *WorkspaceRpcApi
Fleet *FleetRpcApi
Agent *AgentRpcApi
+ Skills *SkillsRpcApi
+ Mcp *McpRpcApi
+ Plugins *PluginsRpcApi
+ Extensions *ExtensionsRpcApi
Compaction *CompactionRpcApi
Tools *ToolsRpcApi
+ Commands *CommandsRpcApi
+ Ui *UiRpcApi
Permissions *PermissionsRpcApi
Shell *ShellRpcApi
}
@@ -880,6 +1458,9 @@ func (a *SessionRpc) Log(ctx context.Context, params *SessionLogParams) (*Sessio
if params.Ephemeral != nil {
req["ephemeral"] = *params.Ephemeral
}
+ if params.URL != nil {
+ req["url"] = *params.URL
+ }
}
raw, err := a.client.Request("session.log", req)
if err != nil {
@@ -900,8 +1481,14 @@ func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc {
Workspace: &WorkspaceRpcApi{client: client, sessionID: sessionID},
Fleet: &FleetRpcApi{client: client, sessionID: sessionID},
Agent: &AgentRpcApi{client: client, sessionID: sessionID},
+ Skills: &SkillsRpcApi{client: client, sessionID: sessionID},
+ Mcp: &McpRpcApi{client: client, sessionID: sessionID},
+ Plugins: &PluginsRpcApi{client: client, sessionID: sessionID},
+ Extensions: &ExtensionsRpcApi{client: client, sessionID: sessionID},
Compaction: &CompactionRpcApi{client: client, sessionID: sessionID},
Tools: &ToolsRpcApi{client: client, sessionID: sessionID},
+ Commands: &CommandsRpcApi{client: client, sessionID: sessionID},
+ Ui: &UiRpcApi{client: client, sessionID: sessionID},
Permissions: &PermissionsRpcApi{client: client, sessionID: sessionID},
Shell: &ShellRpcApi{client: client, sessionID: sessionID},
}
diff --git a/go/samples/chat.go b/go/samples/chat.go
index f984f758a..4d5e98d7d 100644
--- a/go/samples/chat.go
+++ b/go/samples/chat.go
@@ -35,11 +35,11 @@ func main() {
session.On(func(event copilot.SessionEvent) {
var output string
switch event.Type {
- case copilot.AssistantReasoning:
+ case copilot.SessionEventTypeAssistantReasoning:
if event.Data.Content != nil {
output = fmt.Sprintf("[reasoning: %s]", *event.Data.Content)
}
- case copilot.ToolExecutionStart:
+ case copilot.SessionEventTypeToolExecutionStart:
if event.Data.ToolName != nil {
output = fmt.Sprintf("[tool: %s]", *event.Data.ToolName)
}
diff --git a/go/session.go b/go/session.go
index d2a5785be..3a94a818e 100644
--- a/go/session.go
+++ b/go/session.go
@@ -50,20 +50,22 @@ type sessionHandler struct {
// })
type Session struct {
// SessionID is the unique identifier for this session.
- SessionID string
- workspacePath string
- client *jsonrpc2.Client
- handlers []sessionHandler
- nextHandlerID uint64
- handlerMutex sync.RWMutex
- toolHandlers map[string]ToolHandler
- toolHandlersM sync.RWMutex
- permissionHandler PermissionHandlerFunc
- permissionMux sync.RWMutex
- userInputHandler UserInputHandler
- userInputMux sync.RWMutex
- hooks *SessionHooks
- hooksMux sync.RWMutex
+ SessionID string
+ workspacePath string
+ client *jsonrpc2.Client
+ handlers []sessionHandler
+ nextHandlerID uint64
+ handlerMutex sync.RWMutex
+ toolHandlers map[string]ToolHandler
+ toolHandlersM sync.RWMutex
+ permissionHandler PermissionHandlerFunc
+ permissionMux sync.RWMutex
+ userInputHandler UserInputHandler
+ userInputMux sync.RWMutex
+ hooks *SessionHooks
+ hooksMux sync.RWMutex
+ transformCallbacks map[string]SectionTransformFn
+ transformMu sync.Mutex
// eventCh serializes user event handler dispatch. dispatchEvent enqueues;
// a single goroutine (processEvents) dequeues and invokes handlers in FIFO order.
@@ -182,17 +184,17 @@ func (s *Session) SendAndWait(ctx context.Context, options MessageOptions) (*Ses
unsubscribe := s.On(func(event SessionEvent) {
switch event.Type {
- case AssistantMessage:
+ case SessionEventTypeAssistantMessage:
mu.Lock()
eventCopy := event
lastAssistantMessage = &eventCopy
mu.Unlock()
- case SessionIdle:
+ case SessionEventTypeSessionIdle:
select {
case idleCh <- struct{}{}:
default:
}
- case SessionError:
+ case SessionEventTypeSessionError:
errMsg := "session error"
if event.Data.Message != nil {
errMsg = *event.Data.Message
@@ -446,6 +448,56 @@ func (s *Session) handleHooksInvoke(hookType string, rawInput json.RawMessage) (
}
}
+// registerTransformCallbacks registers transform callbacks for this session.
+//
+// Transform callbacks are invoked when the CLI requests system message section
+// transforms. This method is internal and typically called when creating a session.
+func (s *Session) registerTransformCallbacks(callbacks map[string]SectionTransformFn) {
+ s.transformMu.Lock()
+ defer s.transformMu.Unlock()
+ s.transformCallbacks = callbacks
+}
+
+type systemMessageTransformSection struct {
+ Content string `json:"content"`
+}
+
+type systemMessageTransformRequest struct {
+ SessionID string `json:"sessionId"`
+ Sections map[string]systemMessageTransformSection `json:"sections"`
+}
+
+type systemMessageTransformResponse struct {
+ Sections map[string]systemMessageTransformSection `json:"sections"`
+}
+
+// handleSystemMessageTransform handles a system message transform request from the Copilot CLI.
+// This is an internal method called by the SDK when the CLI requests section transforms.
+func (s *Session) handleSystemMessageTransform(sections map[string]systemMessageTransformSection) (systemMessageTransformResponse, error) {
+ s.transformMu.Lock()
+ callbacks := s.transformCallbacks
+ s.transformMu.Unlock()
+
+ result := make(map[string]systemMessageTransformSection)
+ for sectionID, data := range sections {
+ var callback SectionTransformFn
+ if callbacks != nil {
+ callback = callbacks[sectionID]
+ }
+ if callback != nil {
+ transformed, err := callback(data.Content)
+ if err != nil {
+ result[sectionID] = systemMessageTransformSection{Content: data.Content}
+ } else {
+ result[sectionID] = systemMessageTransformSection{Content: transformed}
+ }
+ } else {
+ result[sectionID] = systemMessageTransformSection{Content: data.Content}
+ }
+ }
+ return systemMessageTransformResponse{Sections: result}, nil
+}
+
// dispatchEvent enqueues an event for delivery to user handlers and fires
// broadcast handlers concurrently.
//
@@ -501,7 +553,7 @@ func (s *Session) processEvents() {
// cause RPC deadlocks.
func (s *Session) handleBroadcastEvent(event SessionEvent) {
switch event.Type {
- case ExternalToolRequested:
+ case SessionEventTypeExternalToolRequested:
requestID := event.Data.RequestID
toolName := event.Data.ToolName
if requestID == nil || toolName == nil {
@@ -524,7 +576,7 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) {
}
s.executeToolAndRespond(*requestID, *toolName, toolCallID, event.Data.Arguments, handler, tp, ts)
- case PermissionRequested:
+ case SessionEventTypePermissionRequested:
requestID := event.Data.RequestID
if requestID == nil || event.Data.PermissionRequest == nil {
return
@@ -585,7 +637,7 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques
s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{
RequestID: requestID,
Result: rpc.SessionPermissionsHandlePendingPermissionRequestParamsResult{
- Kind: rpc.DeniedNoApprovalRuleAndCouldNotRequestFromUser,
+ Kind: rpc.KindDeniedNoApprovalRuleAndCouldNotRequestFromUser,
},
})
}
@@ -600,7 +652,7 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques
s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.SessionPermissionsHandlePendingPermissionRequestParams{
RequestID: requestID,
Result: rpc.SessionPermissionsHandlePendingPermissionRequestParamsResult{
- Kind: rpc.DeniedNoApprovalRuleAndCouldNotRequestFromUser,
+ Kind: rpc.KindDeniedNoApprovalRuleAndCouldNotRequestFromUser,
},
})
return
@@ -770,8 +822,8 @@ func (s *Session) SetModel(ctx context.Context, model string, opts ...SetModelOp
// LogOptions configures optional parameters for [Session.Log].
type LogOptions struct {
- // Level sets the log severity. Valid values are [rpc.Info] (default),
- // [rpc.Warning], and [rpc.Error].
+ // Level sets the log severity. Valid values are [rpc.LevelInfo] (default),
+ // [rpc.LevelWarning], and [rpc.LevelError].
Level rpc.Level
// Ephemeral marks the message as transient so it is not persisted
// to the session event log on disk. When nil the server decides the
@@ -791,7 +843,7 @@ type LogOptions struct {
// session.Log(ctx, "Processing started")
//
// // Warning with options
-// session.Log(ctx, "Rate limit approaching", &copilot.LogOptions{Level: rpc.Warning})
+// session.Log(ctx, "Rate limit approaching", &copilot.LogOptions{Level: rpc.LevelWarning})
//
// // Ephemeral message (not persisted)
// session.Log(ctx, "Working...", &copilot.LogOptions{Ephemeral: copilot.Bool(true)})
diff --git a/go/types.go b/go/types.go
index fd9968e3e..502d61c1c 100644
--- a/go/types.go
+++ b/go/types.go
@@ -111,6 +111,51 @@ func Float64(v float64) *float64 {
return &v
}
+// Known system prompt section identifiers for the "customize" mode.
+const (
+ SectionIdentity = "identity"
+ SectionTone = "tone"
+ SectionToolEfficiency = "tool_efficiency"
+ SectionEnvironmentContext = "environment_context"
+ SectionCodeChangeRules = "code_change_rules"
+ SectionGuidelines = "guidelines"
+ SectionSafety = "safety"
+ SectionToolInstructions = "tool_instructions"
+ SectionCustomInstructions = "custom_instructions"
+ SectionLastInstructions = "last_instructions"
+)
+
+// SectionOverrideAction represents the action to perform on a system prompt section.
+type SectionOverrideAction string
+
+const (
+ // SectionActionReplace replaces section content entirely.
+ SectionActionReplace SectionOverrideAction = "replace"
+ // SectionActionRemove removes the section.
+ SectionActionRemove SectionOverrideAction = "remove"
+ // SectionActionAppend appends to existing section content.
+ SectionActionAppend SectionOverrideAction = "append"
+ // SectionActionPrepend prepends to existing section content.
+ SectionActionPrepend SectionOverrideAction = "prepend"
+)
+
+// SectionTransformFn is a callback that receives the current content of a system prompt section
+// and returns the transformed content. Used with the "transform" action to read-then-write
+// modify sections at runtime.
+type SectionTransformFn func(currentContent string) (string, error)
+
+// SectionOverride defines an override operation for a single system prompt section.
+type SectionOverride struct {
+ // Action is the operation to perform: "replace", "remove", "append", "prepend", or "transform".
+ Action SectionOverrideAction `json:"action,omitempty"`
+ // Content for the override. Optional for all actions. Ignored for "remove".
+ Content string `json:"content,omitempty"`
+ // Transform is a callback invoked when Action is "transform".
+ // The runtime calls this with the current section content and uses the returned string.
+ // Excluded from JSON serialization; the SDK registers it as an RPC callback internally.
+ Transform SectionTransformFn `json:"-"`
+}
+
// SystemMessageAppendConfig is append mode: use CLI foundation with optional appended content.
type SystemMessageAppendConfig struct {
// Mode is optional, defaults to "append"
@@ -129,11 +174,15 @@ type SystemMessageReplaceConfig struct {
}
// SystemMessageConfig represents system message configuration for session creation.
-// Use SystemMessageAppendConfig for default behavior, SystemMessageReplaceConfig for full control.
-// In Go, use one struct or the other based on your needs.
+// - Append mode (default): SDK foundation + optional custom content
+// - Replace mode: Full control, caller provides entire system message
+// - Customize mode: Section-level overrides with graceful fallback
+//
+// In Go, use one struct and set fields appropriate for the desired mode.
type SystemMessageConfig struct {
- Mode string `json:"mode,omitempty"`
- Content string `json:"content,omitempty"`
+ Mode string `json:"mode,omitempty"`
+ Content string `json:"content,omitempty"`
+ Sections map[string]SectionOverride `json:"sections,omitempty"`
}
// PermissionRequestResultKind represents the kind of a permission request result.
diff --git a/nodejs/README.md b/nodejs/README.md
index e9d23c529..cc5d62416 100644
--- a/nodejs/README.md
+++ b/nodejs/README.md
@@ -473,7 +473,45 @@ const session = await client.createSession({
});
```
-The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `mode: "replace"`.
+The SDK auto-injects environment context, tool instructions, and security guardrails. The default CLI persona is preserved, and your `content` is appended after SDK-managed sections. To change the persona or fully redefine the prompt, use `mode: "replace"` or `mode: "customize"`.
+
+#### Customize Mode
+
+Use `mode: "customize"` to selectively override individual sections of the prompt while preserving the rest:
+
+```typescript
+import { SYSTEM_PROMPT_SECTIONS } from "@github/copilot-sdk";
+import type { SectionOverride, SystemPromptSection } from "@github/copilot-sdk";
+
+const session = await client.createSession({
+ model: "gpt-5",
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ // Replace the tone/style section
+ tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations." },
+ // Remove coding-specific rules
+ code_change_rules: { action: "remove" },
+ // Append to existing guidelines
+ guidelines: { action: "append", content: "\n* Always cite data sources" },
+ },
+ // Additional instructions appended after all sections
+ content: "Focus on financial analysis and reporting.",
+ },
+});
+```
+
+Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`. Use the `SYSTEM_PROMPT_SECTIONS` constant for descriptions of each section.
+
+Each section override supports four actions:
+- **`replace`** — Replace the section content entirely
+- **`remove`** — Remove the section from the prompt
+- **`append`** — Add content after the existing section
+- **`prepend`** — Add content before the existing section
+
+Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored.
+
+#### Replace Mode
For full control (removes all guardrails), use `mode: "replace"`:
diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json
index 0952122f0..fd56aa84b 100644
--- a/nodejs/package-lock.json
+++ b/nodejs/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.1.8",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^1.0.4",
+ "@github/copilot": "^1.0.10-0",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
@@ -662,26 +662,26 @@
}
},
"node_modules/@github/copilot": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.4.tgz",
- "integrity": "sha512-IpPg+zYplLu4F4lmatEDdR/1Y/jJ9cGWt89m3K3H4YSfYrZ5Go4UlM28llulYCG7sVdQeIGauQN1/KiBI/Rocg==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.10-0.tgz",
+ "integrity": "sha512-LmVe3yVDamZc4cbZeyprZ6WjTME9Z4UcB5YWnEagtXJ19KP5PBKbBZVG7pZnQHL2/IHZ/dqcZW3IHMgYDoqDvg==",
"license": "SEE LICENSE IN LICENSE.md",
"bin": {
"copilot": "npm-loader.js"
},
"optionalDependencies": {
- "@github/copilot-darwin-arm64": "1.0.4",
- "@github/copilot-darwin-x64": "1.0.4",
- "@github/copilot-linux-arm64": "1.0.4",
- "@github/copilot-linux-x64": "1.0.4",
- "@github/copilot-win32-arm64": "1.0.4",
- "@github/copilot-win32-x64": "1.0.4"
+ "@github/copilot-darwin-arm64": "1.0.10-0",
+ "@github/copilot-darwin-x64": "1.0.10-0",
+ "@github/copilot-linux-arm64": "1.0.10-0",
+ "@github/copilot-linux-x64": "1.0.10-0",
+ "@github/copilot-win32-arm64": "1.0.10-0",
+ "@github/copilot-win32-x64": "1.0.10-0"
}
},
"node_modules/@github/copilot-darwin-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.4.tgz",
- "integrity": "sha512-/YGGhv6cp0ItolsF0HsLq2KmesA4atn0IEYApBs770fzJ8OP2pkOEzrxo3gWU3wc7fHF2uDB1RrJEZ7QSFLdEQ==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.10-0.tgz",
+ "integrity": "sha512-u5CbflcTpvc4E48E0jrqbN3Y5hWzValMs21RR6L+GDjQpPI2pvDeUWAJZ03Y7qQ2Uk3KZ+hOIJWJvje9VHxrDQ==",
"cpu": [
"arm64"
],
@@ -695,9 +695,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.4.tgz",
- "integrity": "sha512-gwn2QjZbc1SqPVSAtDMesU1NopyHZT8Qsn37xPfznpV9s94KVyX4TTiDZaUwfnI0wr8kVHBL46RPLNz6I8kR9A==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.10-0.tgz",
+ "integrity": "sha512-4y5OXhAfWX+il9slhrq7v8ONzq+Hpw46ktnz7l1fAZKdmn+dzmFVCvr6pJPr5Az78cAKBuN+Gt4eeSNaxuKCmA==",
"cpu": [
"x64"
],
@@ -711,9 +711,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.4.tgz",
- "integrity": "sha512-92vzHKxN55BpI76sP/5fXIXfat1gzAhsq4bNLqLENGfZyMP/25OiVihCZuQHnvxzXaHBITFGUvtxfdll2kbcng==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.10-0.tgz",
+ "integrity": "sha512-j+Z/ZahEIT5SCblUqOJ2+2glWeIIUPKXXFS5bbu5kFZ9Xyag37FBvTjyxDeB02dpSKKDD4xbMVjcijFbtyr1PA==",
"cpu": [
"arm64"
],
@@ -727,9 +727,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.4.tgz",
- "integrity": "sha512-wQvpwf4/VMTnSmWyYzq07Xg18Vxg7aZ5NVkkXqlLTuXRASW0kvCCb5USEtXHHzR7E6rJztkhCjFRE1bZW8jAGw==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.10-0.tgz",
+ "integrity": "sha512-S8IfuiMZWwnFW1v0vOGHalPIXq/75kL/RpZCYd1sleQA/yztCNNjxH9tNpXsdZnhYrAgU/3hqseWq5hbz8xjxA==",
"cpu": [
"x64"
],
@@ -743,9 +743,9 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.4.tgz",
- "integrity": "sha512-zOvD/5GVxDf0ZdlTkK+m55Vs55xuHNmACX50ZO2N23ZGG2dmkdS4mkruL59XB5ISgrOfeqvnqrwTFHbmPZtLfw==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.10-0.tgz",
+ "integrity": "sha512-6HJErp91fLrwIkoXegLK8SXjHzLgbl9GF+QdOtUGqZ915UUfXcchef0tQjN8u35yNLEW82VnAmft/PJ9Ok2UhQ==",
"cpu": [
"arm64"
],
@@ -759,9 +759,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.4.tgz",
- "integrity": "sha512-yQenHMdkV0b77mF6aLM60TuwtNZ592TluptVDF+80Sj2zPfCpLyvrRh2FCIHRtuwTy4BfxETh2hCFHef8E6IOw==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.10-0.tgz",
+ "integrity": "sha512-AQwZYHoarRACbmPUPmH7gPOEomTAtDusCn65ancI3BoWGj9fzAgZEZ5JSaR3N/VUoXWoEbSe+PcH380ZYwsPag==",
"cpu": [
"x64"
],
diff --git a/nodejs/package.json b/nodejs/package.json
index 6b0d30f2c..7d1822a9c 100644
--- a/nodejs/package.json
+++ b/nodejs/package.json
@@ -56,7 +56,7 @@
"author": "GitHub",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^1.0.4",
+ "@github/copilot": "^1.0.10-0",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json
index 4f93a271c..77daced15 100644
--- a/nodejs/samples/package-lock.json
+++ b/nodejs/samples/package-lock.json
@@ -18,7 +18,7 @@
"version": "0.1.8",
"license": "MIT",
"dependencies": {
- "@github/copilot": "^1.0.4",
+ "@github/copilot": "^1.0.10-0",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index 46d932242..9b8af3dd1 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -36,6 +36,7 @@ import type {
GetStatusResponse,
ModelInfo,
ResumeSessionConfig,
+ SectionTransformFn,
SessionConfig,
SessionContext,
SessionEvent,
@@ -44,6 +45,7 @@ import type {
SessionLifecycleHandler,
SessionListFilter,
SessionMetadata,
+ SystemMessageCustomizeConfig,
TelemetryConfig,
Tool,
ToolCallRequestPayload,
@@ -82,6 +84,45 @@ function toJsonSchema(parameters: Tool["parameters"]): Record |
return parameters;
}
+/**
+ * Extract transform callbacks from a system message config and prepare the wire payload.
+ * Function-valued actions are replaced with `{ action: "transform" }` for serialization,
+ * and the original callbacks are returned in a separate map.
+ */
+function extractTransformCallbacks(systemMessage: SessionConfig["systemMessage"]): {
+ wirePayload: SessionConfig["systemMessage"];
+ transformCallbacks: Map | undefined;
+} {
+ if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) {
+ return { wirePayload: systemMessage, transformCallbacks: undefined };
+ }
+
+ const transformCallbacks = new Map();
+ const wireSections: Record = {};
+
+ for (const [sectionId, override] of Object.entries(systemMessage.sections)) {
+ if (!override) continue;
+
+ if (typeof override.action === "function") {
+ transformCallbacks.set(sectionId, override.action);
+ wireSections[sectionId] = { action: "transform" };
+ } else {
+ wireSections[sectionId] = { action: override.action, content: override.content };
+ }
+ }
+
+ if (transformCallbacks.size === 0) {
+ return { wirePayload: systemMessage, transformCallbacks: undefined };
+ }
+
+ const wirePayload: SystemMessageCustomizeConfig = {
+ ...systemMessage,
+ sections: wireSections as SystemMessageCustomizeConfig["sections"],
+ };
+
+ return { wirePayload, transformCallbacks };
+}
+
function getNodeExecPath(): string {
if (process.versions.bun) {
return "node";
@@ -605,6 +646,15 @@ export class CopilotClient {
if (config.hooks) {
session.registerHooks(config.hooks);
}
+
+ // Extract transform callbacks from system message config before serialization.
+ const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
+ config.systemMessage
+ );
+ if (transformCallbacks) {
+ session.registerTransformCallbacks(transformCallbacks);
+ }
+
if (config.onEvent) {
session.on(config.onEvent);
}
@@ -624,7 +674,7 @@ export class CopilotClient {
overridesBuiltInTool: tool.overridesBuiltInTool,
skipPermission: tool.skipPermission,
})),
- systemMessage: config.systemMessage,
+ systemMessage: wireSystemMessage,
availableTools: config.availableTools,
excludedTools: config.excludedTools,
provider: config.provider,
@@ -711,6 +761,15 @@ export class CopilotClient {
if (config.hooks) {
session.registerHooks(config.hooks);
}
+
+ // Extract transform callbacks from system message config before serialization.
+ const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
+ config.systemMessage
+ );
+ if (transformCallbacks) {
+ session.registerTransformCallbacks(transformCallbacks);
+ }
+
if (config.onEvent) {
session.on(config.onEvent);
}
@@ -723,7 +782,7 @@ export class CopilotClient {
clientName: config.clientName,
model: config.model,
reasoningEffort: config.reasoningEffort,
- systemMessage: config.systemMessage,
+ systemMessage: wireSystemMessage,
availableTools: config.availableTools,
excludedTools: config.excludedTools,
tools: config.tools?.map((tool) => ({
@@ -1477,6 +1536,15 @@ export class CopilotClient {
}): Promise<{ output?: unknown }> => await this.handleHooksInvoke(params)
);
+ this.connection.onRequest(
+ "systemMessage.transform",
+ async (params: {
+ sessionId: string;
+ sections: Record;
+ }): Promise<{ sections: Record }> =>
+ await this.handleSystemMessageTransform(params)
+ );
+
this.connection.onClose(() => {
this.state = "disconnected";
});
@@ -1588,6 +1656,27 @@ export class CopilotClient {
return { output };
}
+ private async handleSystemMessageTransform(params: {
+ sessionId: string;
+ sections: Record;
+ }): Promise<{ sections: Record }> {
+ if (
+ !params ||
+ typeof params.sessionId !== "string" ||
+ !params.sections ||
+ typeof params.sections !== "object"
+ ) {
+ throw new Error("Invalid systemMessage.transform payload");
+ }
+
+ const session = this.sessions.get(params.sessionId);
+ if (!session) {
+ throw new Error(`Session not found: ${params.sessionId}`);
+ }
+
+ return await session._handleSystemMessageTransform(params.sections);
+ }
+
// ========================================================================
// Protocol v2 backward-compatibility adapters
// ========================================================================
diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts
index 16907fdba..dadb9e79d 100644
--- a/nodejs/src/generated/rpc.ts
+++ b/nodejs/src/generated/rpc.ts
@@ -462,6 +462,302 @@ export interface SessionAgentDeselectParams {
sessionId: string;
}
+/** @experimental */
+export interface SessionAgentReloadResult {
+ /**
+ * Reloaded custom agents
+ */
+ agents: {
+ /**
+ * Unique identifier of the custom agent
+ */
+ name: string;
+ /**
+ * Human-readable display name
+ */
+ displayName: string;
+ /**
+ * Description of the agent's purpose
+ */
+ description: string;
+ }[];
+}
+
+/** @experimental */
+export interface SessionAgentReloadParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionSkillsListResult {
+ /**
+ * Available skills
+ */
+ skills: {
+ /**
+ * Unique identifier for the skill
+ */
+ name: string;
+ /**
+ * Description of what the skill does
+ */
+ description: string;
+ /**
+ * Source location type (e.g., project, personal, plugin)
+ */
+ source: string;
+ /**
+ * Whether the skill can be invoked by the user as a slash command
+ */
+ userInvocable: boolean;
+ /**
+ * Whether the skill is currently enabled
+ */
+ enabled: boolean;
+ /**
+ * Absolute path to the skill file
+ */
+ path?: string;
+ }[];
+}
+
+/** @experimental */
+export interface SessionSkillsListParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionSkillsEnableResult {}
+
+/** @experimental */
+export interface SessionSkillsEnableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Name of the skill to enable
+ */
+ name: string;
+}
+
+/** @experimental */
+export interface SessionSkillsDisableResult {}
+
+/** @experimental */
+export interface SessionSkillsDisableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Name of the skill to disable
+ */
+ name: string;
+}
+
+/** @experimental */
+export interface SessionSkillsReloadResult {}
+
+/** @experimental */
+export interface SessionSkillsReloadParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionMcpListResult {
+ /**
+ * Configured MCP servers
+ */
+ servers: {
+ /**
+ * Server name (config key)
+ */
+ name: string;
+ /**
+ * Connection status: connected, failed, pending, disabled, or not_configured
+ */
+ status: "connected" | "failed" | "pending" | "disabled" | "not_configured";
+ /**
+ * Configuration source: user, workspace, plugin, or builtin
+ */
+ source?: string;
+ /**
+ * Error message if the server failed to connect
+ */
+ error?: string;
+ }[];
+}
+
+/** @experimental */
+export interface SessionMcpListParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionMcpEnableResult {}
+
+/** @experimental */
+export interface SessionMcpEnableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Name of the MCP server to enable
+ */
+ serverName: string;
+}
+
+/** @experimental */
+export interface SessionMcpDisableResult {}
+
+/** @experimental */
+export interface SessionMcpDisableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Name of the MCP server to disable
+ */
+ serverName: string;
+}
+
+/** @experimental */
+export interface SessionMcpReloadResult {}
+
+/** @experimental */
+export interface SessionMcpReloadParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionPluginsListResult {
+ /**
+ * Installed plugins
+ */
+ plugins: {
+ /**
+ * Plugin name
+ */
+ name: string;
+ /**
+ * Marketplace the plugin came from
+ */
+ marketplace: string;
+ /**
+ * Installed version
+ */
+ version?: string;
+ /**
+ * Whether the plugin is currently enabled
+ */
+ enabled: boolean;
+ }[];
+}
+
+/** @experimental */
+export interface SessionPluginsListParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionExtensionsListResult {
+ /**
+ * Discovered extensions and their current status
+ */
+ extensions: {
+ /**
+ * Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper')
+ */
+ id: string;
+ /**
+ * Extension name (directory name)
+ */
+ name: string;
+ /**
+ * Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)
+ */
+ source: "project" | "user";
+ /**
+ * Current status: running, disabled, failed, or starting
+ */
+ status: "running" | "disabled" | "failed" | "starting";
+ /**
+ * Process ID if the extension is running
+ */
+ pid?: number;
+ }[];
+}
+
+/** @experimental */
+export interface SessionExtensionsListParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
+/** @experimental */
+export interface SessionExtensionsEnableResult {}
+
+/** @experimental */
+export interface SessionExtensionsEnableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Source-qualified extension ID to enable
+ */
+ id: string;
+}
+
+/** @experimental */
+export interface SessionExtensionsDisableResult {}
+
+/** @experimental */
+export interface SessionExtensionsDisableParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Source-qualified extension ID to disable
+ */
+ id: string;
+}
+
+/** @experimental */
+export interface SessionExtensionsReloadResult {}
+
+/** @experimental */
+export interface SessionExtensionsReloadParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+}
+
/** @experimental */
export interface SessionCompactionCompactResult {
/**
@@ -512,6 +808,135 @@ export interface SessionToolsHandlePendingToolCallParams {
error?: string;
}
+export interface SessionCommandsHandlePendingCommandResult {
+ success: boolean;
+}
+
+export interface SessionCommandsHandlePendingCommandParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Request ID from the command invocation event
+ */
+ requestId: string;
+ /**
+ * Error message if the command handler failed
+ */
+ error?: string;
+}
+
+export interface SessionUiElicitationResult {
+ /**
+ * The user's response: accept (submitted), decline (rejected), or cancel (dismissed)
+ */
+ action: "accept" | "decline" | "cancel";
+ /**
+ * The form values submitted by the user (present when action is 'accept')
+ */
+ content?: {
+ [k: string]: string | number | boolean | string[];
+ };
+}
+
+export interface SessionUiElicitationParams {
+ /**
+ * Target session identifier
+ */
+ sessionId: string;
+ /**
+ * Message describing what information is needed from the user
+ */
+ message: string;
+ /**
+ * JSON Schema describing the form fields to present to the user
+ */
+ requestedSchema: {
+ /**
+ * Schema type indicator (always 'object')
+ */
+ type: "object";
+ /**
+ * Form field definitions, keyed by field name
+ */
+ properties: {
+ [k: string]:
+ | {
+ type: "string";
+ title?: string;
+ description?: string;
+ enum: string[];
+ enumNames?: string[];
+ default?: string;
+ }
+ | {
+ type: "string";
+ title?: string;
+ description?: string;
+ oneOf: {
+ const: string;
+ title: string;
+ }[];
+ default?: string;
+ }
+ | {
+ type: "array";
+ title?: string;
+ description?: string;
+ minItems?: number;
+ maxItems?: number;
+ items: {
+ type: "string";
+ enum: string[];
+ };
+ default?: string[];
+ }
+ | {
+ type: "array";
+ title?: string;
+ description?: string;
+ minItems?: number;
+ maxItems?: number;
+ items: {
+ anyOf: {
+ const: string;
+ title: string;
+ }[];
+ };
+ default?: string[];
+ }
+ | {
+ type: "boolean";
+ title?: string;
+ description?: string;
+ default?: boolean;
+ }
+ | {
+ type: "string";
+ title?: string;
+ description?: string;
+ minLength?: number;
+ maxLength?: number;
+ format?: "email" | "uri" | "date" | "date-time";
+ default?: string;
+ }
+ | {
+ type: "number" | "integer";
+ title?: string;
+ description?: string;
+ minimum?: number;
+ maximum?: number;
+ default?: number;
+ };
+ };
+ /**
+ * List of required field names
+ */
+ required?: string[];
+ };
+}
+
export interface SessionPermissionsHandlePendingPermissionRequestResult {
/**
* Whether the permission request was handled successfully
@@ -571,6 +996,10 @@ export interface SessionLogParams {
* When true, the message is transient and not persisted to the session event log on disk
*/
ephemeral?: boolean;
+ /**
+ * Optional URL the user can open in their browser for more details
+ */
+ url?: string;
}
export interface SessionShellExecResult {
@@ -687,6 +1116,46 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
connection.sendRequest("session.agent.select", { sessionId, ...params }),
deselect: async (): Promise =>
connection.sendRequest("session.agent.deselect", { sessionId }),
+ reload: async (): Promise =>
+ connection.sendRequest("session.agent.reload", { sessionId }),
+ },
+ /** @experimental */
+ skills: {
+ list: async (): Promise =>
+ connection.sendRequest("session.skills.list", { sessionId }),
+ enable: async (params: Omit): Promise =>
+ connection.sendRequest("session.skills.enable", { sessionId, ...params }),
+ disable: async (params: Omit): Promise =>
+ connection.sendRequest("session.skills.disable", { sessionId, ...params }),
+ reload: async (): Promise =>
+ connection.sendRequest("session.skills.reload", { sessionId }),
+ },
+ /** @experimental */
+ mcp: {
+ list: async (): Promise =>
+ connection.sendRequest("session.mcp.list", { sessionId }),
+ enable: async (params: Omit): Promise =>
+ connection.sendRequest("session.mcp.enable", { sessionId, ...params }),
+ disable: async (params: Omit): Promise =>
+ connection.sendRequest("session.mcp.disable", { sessionId, ...params }),
+ reload: async (): Promise =>
+ connection.sendRequest("session.mcp.reload", { sessionId }),
+ },
+ /** @experimental */
+ plugins: {
+ list: async (): Promise =>
+ connection.sendRequest("session.plugins.list", { sessionId }),
+ },
+ /** @experimental */
+ extensions: {
+ list: async (): Promise =>
+ connection.sendRequest("session.extensions.list", { sessionId }),
+ enable: async (params: Omit): Promise =>
+ connection.sendRequest("session.extensions.enable", { sessionId, ...params }),
+ disable: async (params: Omit): Promise =>
+ connection.sendRequest("session.extensions.disable", { sessionId, ...params }),
+ reload: async (): Promise =>
+ connection.sendRequest("session.extensions.reload", { sessionId }),
},
/** @experimental */
compaction: {
@@ -697,6 +1166,14 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
handlePendingToolCall: async (params: Omit): Promise =>
connection.sendRequest("session.tools.handlePendingToolCall", { sessionId, ...params }),
},
+ commands: {
+ handlePendingCommand: async (params: Omit): Promise =>
+ connection.sendRequest("session.commands.handlePendingCommand", { sessionId, ...params }),
+ },
+ ui: {
+ elicitation: async (params: Omit): Promise =>
+ connection.sendRequest("session.ui.elicitation", { sessionId, ...params }),
+ },
permissions: {
handlePendingPermissionRequest: async (params: Omit): Promise =>
connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }),
diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts
index e9d48bc57..9ad6d3c02 100644
--- a/nodejs/src/generated/session-events.ts
+++ b/nodejs/src/generated/session-events.ts
@@ -212,6 +212,10 @@ export type SessionEvent =
* GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs
*/
providerCallId?: string;
+ /**
+ * Optional URL associated with this error that the user can open in a browser
+ */
+ url?: string;
};
}
| {
@@ -325,6 +329,10 @@ export type SessionEvent =
* Human-readable informational message for display in the timeline
*/
message: string;
+ /**
+ * Optional URL associated with this message that the user can open in a browser
+ */
+ url?: string;
};
}
| {
@@ -357,6 +365,10 @@ export type SessionEvent =
* Human-readable warning message for display in the timeline
*/
message: string;
+ /**
+ * Optional URL associated with this warning that the user can open in a browser
+ */
+ url?: string;
};
}
| {
@@ -741,6 +753,22 @@ export type SessionEvent =
* Model that was selected at the time of shutdown
*/
currentModel?: string;
+ /**
+ * Total tokens in context window at shutdown
+ */
+ currentTokens?: number;
+ /**
+ * System message token count at shutdown
+ */
+ systemTokens?: number;
+ /**
+ * Non-system message token count at shutdown
+ */
+ conversationTokens?: number;
+ /**
+ * Tool definitions token count at shutdown
+ */
+ toolDefinitionsTokens?: number;
};
}
| {
@@ -826,6 +854,22 @@ export type SessionEvent =
* Current number of messages in the conversation
*/
messagesLength: number;
+ /**
+ * Token count from system message(s)
+ */
+ systemTokens?: number;
+ /**
+ * Token count from non-system messages (user, assistant, tool)
+ */
+ conversationTokens?: number;
+ /**
+ * Token count from tool definitions
+ */
+ toolDefinitionsTokens?: number;
+ /**
+ * Whether this is the first usage_info event emitted in this session
+ */
+ isInitial?: boolean;
};
}
| {
@@ -847,9 +891,22 @@ export type SessionEvent =
ephemeral?: boolean;
type: "session.compaction_start";
/**
- * Empty payload; the event signals that LLM-powered conversation compaction has begun
+ * Context window breakdown at the start of LLM-powered conversation compaction
*/
- data: {};
+ data: {
+ /**
+ * Token count from system message(s) at compaction start
+ */
+ systemTokens?: number;
+ /**
+ * Token count from non-system messages (user, assistant, tool) at compaction start
+ */
+ conversationTokens?: number;
+ /**
+ * Token count from tool definitions at compaction start
+ */
+ toolDefinitionsTokens?: number;
+ };
}
| {
/**
@@ -934,6 +991,18 @@ export type SessionEvent =
* GitHub request tracing ID (x-github-request-id header) for the compaction LLM call
*/
requestId?: string;
+ /**
+ * Token count from system message(s) after compaction
+ */
+ systemTokens?: number;
+ /**
+ * Token count from non-system messages (user, assistant, tool) after compaction
+ */
+ conversationTokens?: number;
+ /**
+ * Token count from tool definitions after compaction
+ */
+ toolDefinitionsTokens?: number;
};
}
| {
@@ -955,13 +1024,17 @@ export type SessionEvent =
ephemeral?: boolean;
type: "session.task_complete";
/**
- * Task completion notification with optional summary from the agent
+ * Task completion notification with summary from the agent
*/
data: {
/**
- * Optional summary of the completed task, provided by the agent
+ * Summary of the completed task, provided by the agent
*/
summary?: string;
+ /**
+ * Whether the tool call succeeded. False when validation failed (e.g., invalid arguments)
+ */
+ success?: boolean;
};
}
| {
@@ -982,9 +1055,6 @@ export type SessionEvent =
*/
ephemeral?: boolean;
type: "user.message";
- /**
- * User message content with optional attachments, source information, and interaction metadata
- */
data: {
/**
* The user's message text as displayed in the timeline
@@ -1134,19 +1204,9 @@ export type SessionEvent =
}
)[];
/**
- * Origin of this message, used for timeline filtering and telemetry (e.g., "user", "autopilot", "skill", or "command")
+ * Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user)
*/
- source?:
- | "user"
- | "autopilot"
- | "skill"
- | "system"
- | "command"
- | "immediate-prompt"
- | "jit-instruction"
- | "snippy-blocking"
- | "thinking-exhausted-continuation"
- | "other";
+ source?: string;
/**
* The agent mode that was active when this message was sent
*/
@@ -2434,6 +2494,21 @@ export type SessionEvent =
*/
prompt?: string;
}
+ | {
+ type: "agent_idle";
+ /**
+ * Unique identifier of the background agent
+ */
+ agentId: string;
+ /**
+ * Type of the agent (e.g., explore, task, general-purpose)
+ */
+ agentType: string;
+ /**
+ * Human-readable description of the agent task
+ */
+ description?: string;
+ }
| {
type: "shell_completed";
/**
@@ -2785,6 +2860,10 @@ export type SessionEvent =
* Whether the user can provide a free-form text response in addition to predefined choices
*/
allowFreeform?: boolean;
+ /**
+ * The LLM-assigned tool call ID that triggered this request; used by remote UIs to correlate responses
+ */
+ toolCallId?: string;
};
}
| {
@@ -2828,25 +2907,33 @@ export type SessionEvent =
ephemeral: true;
type: "elicitation.requested";
/**
- * Structured form elicitation request with JSON schema definition for form fields
+ * Elicitation request; may be form-based (structured input) or URL-based (browser redirect)
*/
data: {
/**
* Unique identifier for this elicitation request; used to respond via session.respondToElicitation()
*/
requestId: string;
+ /**
+ * Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id for remote UIs
+ */
+ toolCallId?: string;
+ /**
+ * The source that initiated the request (MCP server name, or absent for agent-initiated)
+ */
+ elicitationSource?: string;
/**
* Message describing what information is needed from the user
*/
message: string;
/**
- * Elicitation mode; currently only "form" is supported. Defaults to "form" when absent.
+ * Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to "form" when absent.
*/
- mode?: "form";
+ mode?: "form" | "url";
/**
- * JSON Schema describing the form fields to present to the user
+ * JSON Schema describing the form fields to present to the user (form mode only)
*/
- requestedSchema: {
+ requestedSchema?: {
/**
* Schema type indicator (always 'object')
*/
@@ -2862,6 +2949,10 @@ export type SessionEvent =
*/
required?: string[];
};
+ /**
+ * URL to open in the user's browser (url mode only)
+ */
+ url?: string;
[k: string]: unknown;
};
}
@@ -2890,6 +2981,77 @@ export type SessionEvent =
requestId: string;
};
}
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "mcp.oauth_required";
+ /**
+ * OAuth authentication request for an MCP server
+ */
+ data: {
+ /**
+ * Unique identifier for this OAuth request; used to respond via session.respondToMcpOAuth()
+ */
+ requestId: string;
+ /**
+ * Display name of the MCP server that requires OAuth
+ */
+ serverName: string;
+ /**
+ * URL of the MCP server that requires OAuth
+ */
+ serverUrl: string;
+ /**
+ * Static OAuth client configuration, if the server specifies one
+ */
+ staticClientConfig?: {
+ /**
+ * OAuth client ID for the server
+ */
+ clientId: string;
+ /**
+ * Whether this is a public OAuth client
+ */
+ publicClient?: boolean;
+ };
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "mcp.oauth_completed";
+ /**
+ * MCP OAuth request completion notification
+ */
+ data: {
+ /**
+ * Request ID of the resolved OAuth request
+ */
+ requestId: string;
+ };
+ }
| {
/**
* Unique event identifier (UUID v4), generated when the event is emitted
@@ -2995,6 +3157,43 @@ export type SessionEvent =
command: string;
};
}
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "command.execute";
+ /**
+ * Registered command dispatch request routed to the owning client
+ */
+ data: {
+ /**
+ * Unique identifier; used to respond via session.commands.handlePendingCommand()
+ */
+ requestId: string;
+ /**
+ * The full command text (e.g., /deploy production)
+ */
+ command: string;
+ /**
+ * Command name without leading /
+ */
+ commandName: string;
+ /**
+ * Raw argument string after the command name
+ */
+ args: string;
+ };
+ }
| {
/**
* Unique event identifier (UUID v4), generated when the event is emitted
@@ -3020,6 +3219,34 @@ export type SessionEvent =
requestId: string;
};
}
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "commands.changed";
+ /**
+ * SDK command registration change notification
+ */
+ data: {
+ /**
+ * Current list of registered SDK commands
+ */
+ commands: {
+ name: string;
+ description?: string;
+ }[];
+ };
+ }
| {
/**
* Unique event identifier (UUID v4), generated when the event is emitted
@@ -3121,4 +3348,155 @@ export type SessionEvent =
ephemeral: true;
type: "session.background_tasks_changed";
data: {};
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.skills_loaded";
+ data: {
+ /**
+ * Array of resolved skill metadata
+ */
+ skills: {
+ /**
+ * Unique identifier for the skill
+ */
+ name: string;
+ /**
+ * Description of what the skill does
+ */
+ description: string;
+ /**
+ * Source location type of the skill (e.g., project, personal, plugin)
+ */
+ source: string;
+ /**
+ * Whether the skill can be invoked by the user as a slash command
+ */
+ userInvocable: boolean;
+ /**
+ * Whether the skill is currently enabled
+ */
+ enabled: boolean;
+ /**
+ * Absolute path to the skill file, if available
+ */
+ path?: string;
+ }[];
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.mcp_servers_loaded";
+ data: {
+ /**
+ * Array of MCP server status summaries
+ */
+ servers: {
+ /**
+ * Server name (config key)
+ */
+ name: string;
+ /**
+ * Connection status: connected, failed, pending, disabled, or not_configured
+ */
+ status: "connected" | "failed" | "pending" | "disabled" | "not_configured";
+ /**
+ * Configuration source: user, workspace, plugin, or builtin
+ */
+ source?: string;
+ /**
+ * Error message if the server failed to connect
+ */
+ error?: string;
+ }[];
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.mcp_server_status_changed";
+ data: {
+ /**
+ * Name of the MCP server whose status changed
+ */
+ serverName: string;
+ /**
+ * New connection status: connected, failed, pending, disabled, or not_configured
+ */
+ status: "connected" | "failed" | "pending" | "disabled" | "not_configured";
+ };
+ }
+ | {
+ /**
+ * Unique event identifier (UUID v4), generated when the event is emitted
+ */
+ id: string;
+ /**
+ * ISO 8601 timestamp when the event was created
+ */
+ timestamp: string;
+ /**
+ * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event.
+ */
+ parentId: string | null;
+ ephemeral: true;
+ type: "session.extensions_loaded";
+ data: {
+ /**
+ * Array of discovered extensions and their status
+ */
+ extensions: {
+ /**
+ * Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper')
+ */
+ id: string;
+ /**
+ * Extension name (directory name)
+ */
+ name: string;
+ /**
+ * Discovery source
+ */
+ source: "project" | "user";
+ /**
+ * Current status: running, disabled, failed, or starting
+ */
+ status: "running" | "disabled" | "failed" | "starting";
+ }[];
+ };
};
diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts
index 214b80050..f3788e168 100644
--- a/nodejs/src/index.ts
+++ b/nodejs/src/index.ts
@@ -10,7 +10,7 @@
export { CopilotClient } from "./client.js";
export { CopilotSession, type AssistantMessageEvent } from "./session.js";
-export { defineTool, approveAll } from "./types.js";
+export { defineTool, approveAll, SYSTEM_PROMPT_SECTIONS } from "./types.js";
export type {
ConnectionState,
CopilotClientOptions,
@@ -31,6 +31,9 @@ export type {
PermissionRequest,
PermissionRequestResult,
ResumeSessionConfig,
+ SectionOverride,
+ SectionOverrideAction,
+ SectionTransformFn,
SessionConfig,
SessionEvent,
SessionEventHandler,
@@ -44,7 +47,9 @@ export type {
SessionMetadata,
SystemMessageAppendConfig,
SystemMessageConfig,
+ SystemMessageCustomizeConfig,
SystemMessageReplaceConfig,
+ SystemPromptSection,
TelemetryConfig,
TraceContext,
TraceContextProvider,
diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts
index 674526764..122f4ece8 100644
--- a/nodejs/src/session.ts
+++ b/nodejs/src/session.ts
@@ -17,6 +17,7 @@ import type {
PermissionRequest,
PermissionRequestResult,
ReasoningEffort,
+ SectionTransformFn,
SessionEvent,
SessionEventHandler,
SessionEventPayload,
@@ -70,6 +71,7 @@ export class CopilotSession {
private permissionHandler?: PermissionHandler;
private userInputHandler?: UserInputHandler;
private hooks?: SessionHooks;
+ private transformCallbacks?: Map;
private _rpc: ReturnType | null = null;
private traceContextProvider?: TraceContextProvider;
@@ -517,6 +519,48 @@ export class CopilotSession {
this.hooks = hooks;
}
+ /**
+ * Registers transform callbacks for system message sections.
+ *
+ * @param callbacks - Map of section ID to transform callback, or undefined to clear
+ * @internal This method is typically called internally when creating a session.
+ */
+ registerTransformCallbacks(callbacks?: Map): void {
+ this.transformCallbacks = callbacks;
+ }
+
+ /**
+ * Handles a systemMessage.transform request from the runtime.
+ * Dispatches each section to its registered transform callback.
+ *
+ * @param sections - Map of section IDs to their current rendered content
+ * @returns A promise that resolves with the transformed sections
+ * @internal This method is for internal use by the SDK.
+ */
+ async _handleSystemMessageTransform(
+ sections: Record
+ ): Promise<{ sections: Record }> {
+ const result: Record = {};
+
+ for (const [sectionId, { content }] of Object.entries(sections)) {
+ const callback = this.transformCallbacks?.get(sectionId);
+ if (callback) {
+ try {
+ const transformed = await callback(content);
+ result[sectionId] = { content: transformed };
+ } catch (_error) {
+ // Callback failed — return original content
+ result[sectionId] = { content };
+ }
+ } else {
+ // No callback for this section — pass through unchanged
+ result[sectionId] = { content };
+ }
+ }
+
+ return { sections: result };
+ }
+
/**
* Handles a permission request in the v2 protocol format (synchronous RPC).
* Used as a back-compat adapter when connected to a v2 server.
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index 9052bde52..992dbdb9d 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -272,6 +272,79 @@ export interface ToolCallResponsePayload {
result: ToolResult;
}
+/**
+ * Known system prompt section identifiers for the "customize" mode.
+ * Each section corresponds to a distinct part of the system prompt.
+ */
+export type SystemPromptSection =
+ | "identity"
+ | "tone"
+ | "tool_efficiency"
+ | "environment_context"
+ | "code_change_rules"
+ | "guidelines"
+ | "safety"
+ | "tool_instructions"
+ | "custom_instructions"
+ | "last_instructions";
+
+/** Section metadata for documentation and tooling. */
+export const SYSTEM_PROMPT_SECTIONS: Record = {
+ identity: { description: "Agent identity preamble and mode statement" },
+ tone: { description: "Response style, conciseness rules, output formatting preferences" },
+ tool_efficiency: { description: "Tool usage patterns, parallel calling, batching guidelines" },
+ environment_context: { description: "CWD, OS, git root, directory listing, available tools" },
+ code_change_rules: { description: "Coding rules, linting/testing, ecosystem tools, style" },
+ guidelines: { description: "Tips, behavioral best practices, behavioral guidelines" },
+ safety: { description: "Environment limitations, prohibited actions, security policies" },
+ tool_instructions: { description: "Per-tool usage instructions" },
+ custom_instructions: { description: "Repository and organization custom instructions" },
+ last_instructions: {
+ description:
+ "End-of-prompt instructions: parallel tool calling, persistence, task completion",
+ },
+};
+
+/**
+ * Transform callback for a single section: receives current content, returns new content.
+ */
+export type SectionTransformFn = (currentContent: string) => string | Promise;
+
+/**
+ * Override action: a string literal for static overrides, or a callback for transforms.
+ *
+ * - `"replace"`: Replace section content entirely
+ * - `"remove"`: Remove the section
+ * - `"append"`: Append to existing section content
+ * - `"prepend"`: Prepend to existing section content
+ * - `function`: Transform callback — receives current section content, returns new content
+ */
+export type SectionOverrideAction =
+ | "replace"
+ | "remove"
+ | "append"
+ | "prepend"
+ | SectionTransformFn;
+
+/**
+ * Override operation for a single system prompt section.
+ */
+export interface SectionOverride {
+ /**
+ * The operation to perform on this section.
+ * Can be a string action or a transform callback function.
+ */
+ action: SectionOverrideAction;
+
+ /**
+ * Content for the override. Optional for all actions.
+ * - For replace, omitting content replaces with an empty string.
+ * - For append/prepend, content is added before/after the existing section.
+ * - Ignored for the remove action.
+ */
+ content?: string;
+}
+
/**
* Append mode: Use CLI foundation with optional appended content (default).
*/
@@ -298,12 +371,37 @@ export interface SystemMessageReplaceConfig {
content: string;
}
+/**
+ * Customize mode: Override individual sections of the system prompt.
+ * Keeps the SDK-managed prompt structure while allowing targeted modifications.
+ */
+export interface SystemMessageCustomizeConfig {
+ mode: "customize";
+
+ /**
+ * Override specific sections of the system prompt by section ID.
+ * Unknown section IDs gracefully fall back: content-bearing overrides are appended
+ * to additional instructions, and "remove" on unknown sections is a silent no-op.
+ */
+ sections?: Partial>;
+
+ /**
+ * Additional content appended after all sections.
+ * Equivalent to append mode's content field — provided for convenience.
+ */
+ content?: string;
+}
+
/**
* System message configuration for session creation.
* - Append mode (default): SDK foundation + optional custom content
* - Replace mode: Full control, caller provides entire system message
+ * - Customize mode: Section-level overrides with graceful fallback
*/
-export type SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig;
+export type SystemMessageConfig =
+ | SystemMessageAppendConfig
+ | SystemMessageReplaceConfig
+ | SystemMessageCustomizeConfig;
/**
* Permission request types from the server
diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts
index 1eb8a175d..dbcbed8bb 100644
--- a/nodejs/test/e2e/session.test.ts
+++ b/nodejs/test/e2e/session.test.ts
@@ -96,6 +96,33 @@ describe("Sessions", async () => {
expect(systemMessage).toEqual(testSystemMessage); // Exact match
});
+ it("should create a session with customized systemMessage config", async () => {
+ const customTone = "Respond in a warm, professional tone. Be thorough in explanations.";
+ const appendedContent = "Always mention quarterly earnings.";
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ tone: { action: "replace", content: customTone },
+ code_change_rules: { action: "remove" },
+ },
+ content: appendedContent,
+ },
+ });
+
+ const assistantMessage = await session.sendAndWait({ prompt: "Who are you?" });
+ expect(assistantMessage?.data.content).toBeDefined();
+
+ // Validate the system message sent to the model
+ const traffic = await openAiEndpoint.getExchanges();
+ const systemMessage = getSystemMessage(traffic[0]);
+ expect(systemMessage).toContain(customTone);
+ expect(systemMessage).toContain(appendedContent);
+ // The code_change_rules section should have been removed
+ expect(systemMessage).not.toContain("");
+ });
+
it("should create a session with availableTools", async () => {
const session = await client.createSession({
onPermissionRequest: approveAll,
diff --git a/nodejs/test/e2e/system_message_transform.test.ts b/nodejs/test/e2e/system_message_transform.test.ts
new file mode 100644
index 000000000..ef37c39e9
--- /dev/null
+++ b/nodejs/test/e2e/system_message_transform.test.ts
@@ -0,0 +1,125 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+import { writeFile } from "fs/promises";
+import { join } from "path";
+import { describe, expect, it } from "vitest";
+import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js";
+import { approveAll } from "../../src/index.js";
+import { createSdkTestContext } from "./harness/sdkTestContext.js";
+
+describe("System message transform", async () => {
+ const { copilotClient: client, openAiEndpoint, workDir } = await createSdkTestContext();
+
+ it("should invoke transform callbacks with section content", async () => {
+ const transformedSections: Record = {};
+
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ identity: {
+ action: (content: string) => {
+ transformedSections["identity"] = content;
+ // Pass through unchanged
+ return content;
+ },
+ },
+ tone: {
+ action: (content: string) => {
+ transformedSections["tone"] = content;
+ return content;
+ },
+ },
+ },
+ },
+ });
+
+ await writeFile(join(workDir, "test.txt"), "Hello transform!");
+
+ await session.sendAndWait({
+ prompt: "Read the contents of test.txt and tell me what it says",
+ });
+
+ // Transform callbacks should have been invoked with real section content
+ expect(Object.keys(transformedSections).length).toBe(2);
+ expect(transformedSections["identity"]).toBeDefined();
+ expect(transformedSections["identity"]!.length).toBeGreaterThan(0);
+ expect(transformedSections["tone"]).toBeDefined();
+ expect(transformedSections["tone"]!.length).toBeGreaterThan(0);
+
+ await session.disconnect();
+ });
+
+ it("should apply transform modifications to section content", async () => {
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ identity: {
+ action: (content: string) => {
+ return content + "\nTRANSFORM_MARKER";
+ },
+ },
+ },
+ },
+ });
+
+ await writeFile(join(workDir, "hello.txt"), "Hello!");
+
+ await session.sendAndWait({
+ prompt: "Read the contents of hello.txt",
+ });
+
+ // Verify the transform result was actually applied to the system message
+ const traffic = await openAiEndpoint.getExchanges();
+ const systemMessage = getSystemMessage(traffic[0]);
+ expect(systemMessage).toContain("TRANSFORM_MARKER");
+
+ await session.disconnect();
+ });
+
+ it("should work with static overrides and transforms together", async () => {
+ const transformedSections: Record = {};
+
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ // Static override
+ safety: { action: "remove" },
+ // Transform
+ identity: {
+ action: (content: string) => {
+ transformedSections["identity"] = content;
+ return content;
+ },
+ },
+ },
+ },
+ });
+
+ await writeFile(join(workDir, "combo.txt"), "Combo test!");
+
+ await session.sendAndWait({
+ prompt: "Read the contents of combo.txt and tell me what it says",
+ });
+
+ // Transform should have been invoked
+ expect(transformedSections["identity"]).toBeDefined();
+ expect(transformedSections["identity"]!.length).toBeGreaterThan(0);
+
+ await session.disconnect();
+ });
+});
+
+function getSystemMessage(exchange: ParsedHttpExchange): string | undefined {
+ const systemMessage = exchange.request.messages.find((m) => m.role === "system") as
+ | { role: "system"; content: string }
+ | undefined;
+ return systemMessage?.content;
+}
diff --git a/python/README.md b/python/README.md
index 2394c351a..57bb78cab 100644
--- a/python/README.md
+++ b/python/README.md
@@ -33,7 +33,10 @@ async def main():
await client.start()
# Create a session (on_permission_request is required)
- session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5")
+ session = await client.create_session({
+ "model": "gpt-5",
+ "on_permission_request": PermissionHandler.approve_all,
+ })
# Wait for response using session.idle event
done = asyncio.Event()
@@ -60,7 +63,10 @@ asyncio.run(main())
Sessions also support the `async with` context manager pattern for automatic cleanup:
```python
-async with await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5") as session:
+async with await client.create_session({
+ "model": "gpt-5",
+ "on_permission_request": PermissionHandler.approve_all,
+}) as session:
await session.send("What is 2+2?")
# session is automatically disconnected when leaving the block
```
@@ -85,7 +91,7 @@ from copilot import CopilotClient, SubprocessConfig
client = CopilotClient() # uses bundled CLI, stdio transport
await client.start()
-session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5")
+session = await client.create_session({"model": "gpt-5"})
def on_event(event):
print(f"Event: {event['type']}")
@@ -134,59 +140,19 @@ CopilotClient(
- `url` (str): Server URL (e.g., `"localhost:8080"`, `"http://127.0.0.1:9000"`, or just `"8080"`).
-**`create_session` Parameters:**
-
-All parameters are keyword-only:
+**SessionConfig Options (for `create_session`):**
-- `on_permission_request` (callable): **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
-- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.).
-- `session_id` (str): Custom session ID for resuming or identifying sessions.
-- `client_name` (str): Client name to identify the application using the SDK. Included in the User-Agent header for API requests.
+- `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.**
- `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh"). Use `list_models()` to check which models support this option.
-- `tools` (list): Custom tools exposed to the CLI.
-- `system_message` (dict): System message configuration.
-- `available_tools` (list[str]): List of tool names to allow. Takes precedence over `excluded_tools`.
-- `excluded_tools` (list[str]): List of tool names to disable. Ignored if `available_tools` is set.
-- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
-- `hooks` (dict): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
-- `working_directory` (str): Working directory for the session. Tool operations will be relative to this directory.
+- `session_id` (str): Custom session ID
+- `tools` (list): Custom tools exposed to the CLI
+- `system_message` (dict): System message configuration
+- `streaming` (bool): Enable streaming delta events
- `provider` (dict): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section.
-- `streaming` (bool): Enable streaming delta events.
-- `mcp_servers` (dict): MCP server configurations for the session.
-- `custom_agents` (list): Custom agent configurations for the session.
-- `config_dir` (str): Override the default configuration directory location.
-- `skill_directories` (list[str]): Directories to load skills from.
-- `disabled_skills` (list[str]): List of skill names to disable.
-- `infinite_sessions` (dict): Automatic context compaction configuration.
-
-**`resume_session` Parameters:**
-
-- `session_id` (str): **Required.** The ID of the session to resume.
-
-The parameters below are keyword-only:
-
+- `infinite_sessions` (dict): Automatic context compaction configuration
- `on_permission_request` (callable): **Required.** Handler called before each tool execution to approve or deny it. Use `PermissionHandler.approve_all` to allow everything, or provide a custom function for fine-grained control. See [Permission Handling](#permission-handling) section.
-- `model` (str): Model to use (can change the model when resuming).
-- `client_name` (str): Client name to identify the application using the SDK.
-- `reasoning_effort` (str): Reasoning effort level ("low", "medium", "high", "xhigh").
-- `tools` (list): Custom tools exposed to the CLI.
-- `system_message` (dict): System message configuration.
-- `available_tools` (list[str]): List of tool names to allow. Takes precedence over `excluded_tools`.
-- `excluded_tools` (list[str]): List of tool names to disable. Ignored if `available_tools` is set.
-- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool).
-- `hooks` (dict): Hook handlers for session lifecycle events.
-- `working_directory` (str): Working directory for the session.
-- `provider` (dict): Custom API provider configuration (BYOK).
-- `streaming` (bool): Enable streaming delta events.
-- `mcp_servers` (dict): MCP server configurations for the session.
-- `custom_agents` (list): Custom agent configurations for the session.
-- `agent` (str): Name of the custom agent to activate when the session starts.
-- `config_dir` (str): Override the default configuration directory location.
-- `skill_directories` (list[str]): Directories to load skills from.
-- `disabled_skills` (list[str]): List of skill names to disable.
-- `infinite_sessions` (dict): Automatic context compaction configuration.
-- `disable_resume` (bool): Skip emitting the session.resume event (default: False).
-- `on_event` (callable): Event handler registered before the session.resume RPC.
+- `on_user_input_request` (callable): Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
+- `hooks` (dict): Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
**Session Lifecycle Methods:**
@@ -223,7 +189,7 @@ Define tools with automatic JSON schema generation using the `@define_tool` deco
```python
from pydantic import BaseModel, Field
-from copilot import CopilotClient, define_tool, PermissionHandler
+from copilot import CopilotClient, define_tool
class LookupIssueParams(BaseModel):
id: str = Field(description="Issue identifier")
@@ -233,11 +199,10 @@ async def lookup_issue(params: LookupIssueParams) -> str:
issue = await fetch_issue(params.id)
return issue.summary
-session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-5",
- tools=[lookup_issue],
-)
+session = await client.create_session({
+ "model": "gpt-5",
+ "tools": [lookup_issue],
+})
```
> **Note:** When using `from __future__ import annotations`, define Pydantic models at module level (not inside functions).
@@ -247,7 +212,8 @@ session = await client.create_session(
For users who prefer manual schema definition:
```python
-from copilot import CopilotClient, Tool, PermissionHandler
+from copilot import CopilotClient
+from copilot.tools import Tool
async def lookup_issue(invocation):
issue_id = invocation["arguments"]["id"]
@@ -258,10 +224,9 @@ async def lookup_issue(invocation):
"sessionLog": f"Fetched issue {issue_id}",
}
-session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-5",
- tools=[
+session = await client.create_session({
+ "model": "gpt-5",
+ "tools": [
Tool(
name="lookup_issue",
description="Fetch issue details from our tracker",
@@ -275,7 +240,7 @@ session = await client.create_session(
handler=lookup_issue,
)
],
-)
+})
```
The SDK automatically handles `tool.call`, executes your handler (sync or async), and responds with the final result when the tool completes.
@@ -345,17 +310,16 @@ Enable streaming to receive assistant response chunks as they're generated:
```python
import asyncio
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient
async def main():
client = CopilotClient()
await client.start()
- session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-5",
- streaming=True,
- )
+ session = await client.create_session({
+ "model": "gpt-5",
+ "streaming": True
+ })
# Use asyncio.Event to wait for completion
done = asyncio.Event()
@@ -406,29 +370,27 @@ By default, sessions use **infinite sessions** which automatically manage contex
```python
# Default: infinite sessions enabled with default thresholds
-session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="gpt-5")
+session = await client.create_session({"model": "gpt-5"})
# Access the workspace path for checkpoints and files
print(session.workspace_path)
# => ~/.copilot/session-state/{session_id}/
# Custom thresholds
-session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-5",
- infinite_sessions={
+session = await client.create_session({
+ "model": "gpt-5",
+ "infinite_sessions": {
"enabled": True,
"background_compaction_threshold": 0.80, # Start compacting at 80% context usage
"buffer_exhaustion_threshold": 0.95, # Block at 95% until compaction completes
},
-)
+})
# Disable infinite sessions
-session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-5",
- infinite_sessions={"enabled": False},
-)
+session = await client.create_session({
+ "model": "gpt-5",
+ "infinite_sessions": {"enabled": False},
+})
```
When enabled, sessions emit compaction events:
@@ -452,15 +414,14 @@ The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own K
**Example with Ollama:**
```python
-session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="deepseek-coder-v2:16b", # Model to use with the custom provider
- provider={
+session = await client.create_session({
+ "model": "deepseek-coder-v2:16b", # Required when using custom provider
+ "provider": {
"type": "openai",
"base_url": "http://localhost:11434/v1", # Ollama endpoint
# api_key not required for Ollama
},
-)
+})
await session.send("Hello!")
```
@@ -470,15 +431,14 @@ await session.send("Hello!")
```python
import os
-session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-4",
- provider={
+session = await client.create_session({
+ "model": "gpt-4",
+ "provider": {
"type": "openai",
"base_url": "https://my-api.example.com/v1",
"api_key": os.environ["MY_API_KEY"],
},
-)
+})
```
**Example with Azure OpenAI:**
@@ -486,10 +446,9 @@ session = await client.create_session(
```python
import os
-session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-4",
- provider={
+session = await client.create_session({
+ "model": "gpt-4",
+ "provider": {
"type": "azure", # Must be "azure" for Azure endpoints, NOT "openai"
"base_url": "https://my-resource.openai.azure.com", # Just the host, no path
"api_key": os.environ["AZURE_OPENAI_KEY"],
@@ -497,10 +456,11 @@ session = await client.create_session(
"api_version": "2024-10-21",
},
},
-)
+})
```
> **Important notes:**
+> - When using a custom provider, the `model` parameter is **required**. The SDK will throw an error if no model is specified.
> - For Azure OpenAI endpoints (`*.openai.azure.com`), you **must** use `type: "azure"`, not `type: "openai"`.
> - The `base_url` should be just the host (e.g., `https://my-resource.openai.azure.com`). Do **not** include `/openai/v1` in the URL - the SDK handles path construction automatically.
@@ -635,11 +595,10 @@ async def handle_user_input(request, invocation):
"wasFreeform": True, # Whether the answer was freeform (not from choices)
}
-session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-5",
- on_user_input_request=handle_user_input,
-)
+session = await client.create_session({
+ "model": "gpt-5",
+ "on_user_input_request": handle_user_input,
+})
```
## Session Hooks
@@ -683,10 +642,9 @@ async def on_error_occurred(input, invocation):
"errorHandling": "retry", # "retry", "skip", or "abort"
}
-session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="gpt-5",
- hooks={
+session = await client.create_session({
+ "model": "gpt-5",
+ "hooks": {
"on_pre_tool_use": on_pre_tool_use,
"on_post_tool_use": on_post_tool_use,
"on_user_prompt_submitted": on_user_prompt_submitted,
@@ -694,7 +652,7 @@ session = await client.create_session(
"on_session_end": on_session_end,
"on_error_occurred": on_error_occurred,
},
-)
+})
```
**Available hooks:**
diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py
index e1fdf9253..92764c0e8 100644
--- a/python/copilot/__init__.py
+++ b/python/copilot/__init__.py
@@ -4,84 +4,16 @@
JSON-RPC based SDK for programmatic control of GitHub Copilot CLI
"""
-from .client import CopilotClient
+from .client import CopilotClient, ExternalServerConfig, SubprocessConfig
from .session import CopilotSession
from .tools import define_tool
-from .types import (
- Attachment,
- AzureProviderOptions,
- BlobAttachment,
- ConnectionState,
- CustomAgentConfig,
- DirectoryAttachment,
- ExternalServerConfig,
- FileAttachment,
- GetAuthStatusResponse,
- GetStatusResponse,
- MCPLocalServerConfig,
- MCPRemoteServerConfig,
- MCPServerConfig,
- ModelBilling,
- ModelCapabilities,
- ModelInfo,
- ModelPolicy,
- PermissionHandler,
- PermissionRequest,
- PermissionRequestResult,
- PingResponse,
- ProviderConfig,
- SelectionAttachment,
- SessionContext,
- SessionEvent,
- SessionListFilter,
- SessionMetadata,
- StopError,
- SubprocessConfig,
- TelemetryConfig,
- Tool,
- ToolHandler,
- ToolInvocation,
- ToolResult,
-)
__version__ = "0.1.0"
__all__ = [
- "Attachment",
- "AzureProviderOptions",
- "BlobAttachment",
"CopilotClient",
"CopilotSession",
- "ConnectionState",
- "CustomAgentConfig",
- "DirectoryAttachment",
"ExternalServerConfig",
- "FileAttachment",
- "GetAuthStatusResponse",
- "GetStatusResponse",
- "MCPLocalServerConfig",
- "MCPRemoteServerConfig",
- "MCPServerConfig",
- "ModelBilling",
- "ModelCapabilities",
- "ModelInfo",
- "ModelPolicy",
- "PermissionHandler",
- "PermissionRequest",
- "PermissionRequestResult",
- "PingResponse",
- "ProviderConfig",
- "SelectionAttachment",
- "SessionContext",
- "SessionEvent",
- "SessionListFilter",
- "SessionMetadata",
- "StopError",
"SubprocessConfig",
- "TelemetryConfig",
- "Tool",
- "ToolHandler",
- "ToolInvocation",
- "ToolResult",
"define_tool",
]
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 28050088e..4a5632e5e 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -5,15 +5,15 @@
to the Copilot CLI server and provides session management capabilities.
Example:
- >>> from copilot import CopilotClient, PermissionHandler
+ >>> from copilot import CopilotClient
>>>
>>> async with CopilotClient() as client:
- ... session = await client.create_session(
- ... on_permission_request=PermissionHandler.approve_all
- ... )
+ ... session = await client.create_session()
... await session.send("Hello!")
"""
+from __future__ import annotations
+
import asyncio
import inspect
import os
@@ -24,43 +24,619 @@
import threading
import uuid
from collections.abc import Awaitable, Callable
+from dataclasses import KW_ONLY, dataclass, field
from pathlib import Path
-from typing import Any, cast, overload
+from typing import Any, Literal, TypedDict, cast, overload
from ._jsonrpc import JsonRpcClient, ProcessExitedError
from ._sdk_protocol_version import get_sdk_protocol_version
from ._telemetry import get_trace_context, trace_context
from .generated.rpc import ServerRpc
from .generated.session_events import PermissionRequest, session_event_from_dict
-from .session import CopilotSession
-from .types import (
- ConnectionState,
+from .session import (
+ CopilotSession,
CustomAgentConfig,
- ExternalServerConfig,
- GetAuthStatusResponse,
- GetStatusResponse,
- InfiniteSessionConfig,
- MCPServerConfig,
- ModelInfo,
- PingResponse,
ProviderConfig,
- ReasoningEffort,
- SessionEvent,
- SessionHooks,
- SessionLifecycleEvent,
- SessionLifecycleEventType,
- SessionLifecycleHandler,
- SessionListFilter,
- SessionMetadata,
- StopError,
- SubprocessConfig,
- SystemMessageConfig,
- Tool,
- ToolInvocation,
- ToolResult,
- UserInputHandler,
- _PermissionHandlerFn,
+ ResumeSessionConfig,
+ SessionConfig,
)
+from .tools import ToolInvocation, ToolResult
+
+# ============================================================================
+# Connection Types
+# ============================================================================
+
+ConnectionState = Literal["disconnected", "connecting", "connected", "error"]
+
+LogLevel = Literal["none", "error", "warning", "info", "debug", "all"]
+
+
+class TelemetryConfig(TypedDict, total=False):
+ """Configuration for OpenTelemetry integration with the Copilot CLI."""
+
+ otlp_endpoint: str
+ """OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT."""
+ file_path: str
+ """File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH."""
+ exporter_type: str
+ """Exporter backend type: "otlp-http" or "file". Sets COPILOT_OTEL_EXPORTER_TYPE."""
+ source_name: str
+ """Instrumentation scope name. Sets COPILOT_OTEL_SOURCE_NAME."""
+ capture_content: bool
+ """Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501
+
+
+@dataclass
+class SubprocessConfig:
+ """Config for spawning a local Copilot CLI subprocess.
+
+ Example:
+ >>> config = SubprocessConfig(github_token="ghp_...")
+ >>> client = CopilotClient(config)
+
+ >>> # Custom CLI path with TCP transport
+ >>> config = SubprocessConfig(
+ ... cli_path="/usr/local/bin/copilot",
+ ... use_stdio=False,
+ ... log_level="debug",
+ ... )
+ """
+
+ cli_path: str | None = None
+ """Path to the Copilot CLI executable. ``None`` uses the bundled binary."""
+
+ cli_args: list[str] = field(default_factory=list)
+ """Extra arguments passed to the CLI executable (inserted before SDK-managed args)."""
+
+ _: KW_ONLY
+
+ cwd: str | None = None
+ """Working directory for the CLI process. ``None`` uses the current directory."""
+
+ use_stdio: bool = True
+ """Use stdio transport (``True``, default) or TCP (``False``)."""
+
+ port: int = 0
+ """TCP port for the CLI server (only when ``use_stdio=False``). 0 means random."""
+
+ log_level: LogLevel = "info"
+ """Log level for the CLI process."""
+
+ env: dict[str, str] | None = None
+ """Environment variables for the CLI process. ``None`` inherits the current env."""
+
+ github_token: str | None = None
+ """GitHub token for authentication. Takes priority over other auth methods."""
+
+ use_logged_in_user: bool | None = None
+ """Use the logged-in user for authentication.
+
+ ``None`` (default) resolves to ``True`` unless ``github_token`` is set.
+ """
+
+ telemetry: TelemetryConfig | None = None
+ """OpenTelemetry configuration. Providing this enables telemetry — no separate flag needed."""
+
+
+@dataclass
+class ExternalServerConfig:
+ """Config for connecting to an existing Copilot CLI server over TCP.
+
+ Example:
+ >>> config = ExternalServerConfig(url="localhost:3000")
+ >>> client = CopilotClient(config)
+ """
+
+ url: str
+ """Server URL. Supports ``"host:port"``, ``"http://host:port"``, or just ``"port"``."""
+
+
+# ============================================================================
+# Response Types
+# ============================================================================
+
+
+@dataclass
+class PingResponse:
+ """Response from ping"""
+
+ message: str # Echo message with "pong: " prefix
+ timestamp: int # Server timestamp in milliseconds
+ protocolVersion: int # Protocol version for SDK compatibility
+
+ @staticmethod
+ def from_dict(obj: Any) -> PingResponse:
+ assert isinstance(obj, dict)
+ message = obj.get("message")
+ timestamp = obj.get("timestamp")
+ protocolVersion = obj.get("protocolVersion")
+ if message is None or timestamp is None or protocolVersion is None:
+ raise ValueError(
+ f"Missing required fields in PingResponse: message={message}, "
+ f"timestamp={timestamp}, protocolVersion={protocolVersion}"
+ )
+ return PingResponse(str(message), int(timestamp), int(protocolVersion))
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["message"] = self.message
+ result["timestamp"] = self.timestamp
+ result["protocolVersion"] = self.protocolVersion
+ return result
+
+
+@dataclass
+class StopError(Exception):
+ """Error that occurred during client stop cleanup."""
+
+ message: str # Error message describing what failed during cleanup
+
+ def __post_init__(self) -> None:
+ Exception.__init__(self, self.message)
+
+ @staticmethod
+ def from_dict(obj: Any) -> StopError:
+ assert isinstance(obj, dict)
+ message = obj.get("message")
+ if message is None:
+ raise ValueError("Missing required field 'message' in StopError")
+ return StopError(str(message))
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["message"] = self.message
+ return result
+
+
+@dataclass
+class GetStatusResponse:
+ """Response from status.get"""
+
+ version: str # Package version (e.g., "1.0.0")
+ protocolVersion: int # Protocol version for SDK compatibility
+
+ @staticmethod
+ def from_dict(obj: Any) -> GetStatusResponse:
+ assert isinstance(obj, dict)
+ version = obj.get("version")
+ protocolVersion = obj.get("protocolVersion")
+ if version is None or protocolVersion is None:
+ raise ValueError(
+ f"Missing required fields in GetStatusResponse: version={version}, "
+ f"protocolVersion={protocolVersion}"
+ )
+ return GetStatusResponse(str(version), int(protocolVersion))
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["version"] = self.version
+ result["protocolVersion"] = self.protocolVersion
+ return result
+
+
+@dataclass
+class GetAuthStatusResponse:
+ """Response from auth.getStatus"""
+
+ isAuthenticated: bool # Whether the user is authenticated
+ authType: str | None = None # Authentication type
+ host: str | None = None # GitHub host URL
+ login: str | None = None # User login name
+ statusMessage: str | None = None # Human-readable status message
+
+ @staticmethod
+ def from_dict(obj: Any) -> GetAuthStatusResponse:
+ assert isinstance(obj, dict)
+ isAuthenticated = obj.get("isAuthenticated")
+ if isAuthenticated is None:
+ raise ValueError("Missing required field 'isAuthenticated' in GetAuthStatusResponse")
+ authType = obj.get("authType")
+ host = obj.get("host")
+ login = obj.get("login")
+ statusMessage = obj.get("statusMessage")
+ return GetAuthStatusResponse(
+ isAuthenticated=bool(isAuthenticated),
+ authType=authType,
+ host=host,
+ login=login,
+ statusMessage=statusMessage,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["isAuthenticated"] = self.isAuthenticated
+ if self.authType is not None:
+ result["authType"] = self.authType
+ if self.host is not None:
+ result["host"] = self.host
+ if self.login is not None:
+ result["login"] = self.login
+ if self.statusMessage is not None:
+ result["statusMessage"] = self.statusMessage
+ return result
+
+
+# ============================================================================
+# Model Types
+# ============================================================================
+
+
+@dataclass
+class ModelVisionLimits:
+ """Vision-specific limits"""
+
+ supported_media_types: list[str] | None = None
+ max_prompt_images: int | None = None
+ max_prompt_image_size: int | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> ModelVisionLimits:
+ assert isinstance(obj, dict)
+ supported_media_types = obj.get("supported_media_types")
+ max_prompt_images = obj.get("max_prompt_images")
+ max_prompt_image_size = obj.get("max_prompt_image_size")
+ return ModelVisionLimits(
+ supported_media_types=supported_media_types,
+ max_prompt_images=max_prompt_images,
+ max_prompt_image_size=max_prompt_image_size,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ if self.supported_media_types is not None:
+ result["supported_media_types"] = self.supported_media_types
+ if self.max_prompt_images is not None:
+ result["max_prompt_images"] = self.max_prompt_images
+ if self.max_prompt_image_size is not None:
+ result["max_prompt_image_size"] = self.max_prompt_image_size
+ return result
+
+
+@dataclass
+class ModelLimits:
+ """Model limits"""
+
+ max_prompt_tokens: int | None = None
+ max_context_window_tokens: int | None = None
+ vision: ModelVisionLimits | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> ModelLimits:
+ assert isinstance(obj, dict)
+ max_prompt_tokens = obj.get("max_prompt_tokens")
+ max_context_window_tokens = obj.get("max_context_window_tokens")
+ vision_dict = obj.get("vision")
+ vision = ModelVisionLimits.from_dict(vision_dict) if vision_dict else None
+ return ModelLimits(
+ max_prompt_tokens=max_prompt_tokens,
+ max_context_window_tokens=max_context_window_tokens,
+ vision=vision,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ if self.max_prompt_tokens is not None:
+ result["max_prompt_tokens"] = self.max_prompt_tokens
+ if self.max_context_window_tokens is not None:
+ result["max_context_window_tokens"] = self.max_context_window_tokens
+ if self.vision is not None:
+ result["vision"] = self.vision.to_dict()
+ return result
+
+
+@dataclass
+class ModelSupports:
+ """Model support flags"""
+
+ vision: bool
+ reasoning_effort: bool = False # Whether this model supports reasoning effort
+
+ @staticmethod
+ def from_dict(obj: Any) -> ModelSupports:
+ assert isinstance(obj, dict)
+ vision = obj.get("vision")
+ if vision is None:
+ raise ValueError("Missing required field 'vision' in ModelSupports")
+ reasoning_effort = obj.get("reasoningEffort", False)
+ return ModelSupports(vision=bool(vision), reasoning_effort=bool(reasoning_effort))
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["vision"] = self.vision
+ result["reasoningEffort"] = self.reasoning_effort
+ return result
+
+
+@dataclass
+class ModelCapabilities:
+ """Model capabilities and limits"""
+
+ supports: ModelSupports
+ limits: ModelLimits
+
+ @staticmethod
+ def from_dict(obj: Any) -> ModelCapabilities:
+ assert isinstance(obj, dict)
+ supports_dict = obj.get("supports")
+ limits_dict = obj.get("limits")
+ if supports_dict is None or limits_dict is None:
+ raise ValueError(
+ f"Missing required fields in ModelCapabilities: supports={supports_dict}, "
+ f"limits={limits_dict}"
+ )
+ supports = ModelSupports.from_dict(supports_dict)
+ limits = ModelLimits.from_dict(limits_dict)
+ return ModelCapabilities(supports=supports, limits=limits)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["supports"] = self.supports.to_dict()
+ result["limits"] = self.limits.to_dict()
+ return result
+
+
+@dataclass
+class ModelPolicy:
+ """Model policy state"""
+
+ state: str # "enabled", "disabled", or "unconfigured"
+ terms: str
+
+ @staticmethod
+ def from_dict(obj: Any) -> ModelPolicy:
+ assert isinstance(obj, dict)
+ state = obj.get("state")
+ terms = obj.get("terms")
+ if state is None or terms is None:
+ raise ValueError(
+ f"Missing required fields in ModelPolicy: state={state}, terms={terms}"
+ )
+ return ModelPolicy(state=str(state), terms=str(terms))
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["state"] = self.state
+ result["terms"] = self.terms
+ return result
+
+
+@dataclass
+class ModelBilling:
+ """Model billing information"""
+
+ multiplier: float
+
+ @staticmethod
+ def from_dict(obj: Any) -> ModelBilling:
+ assert isinstance(obj, dict)
+ multiplier = obj.get("multiplier")
+ if multiplier is None:
+ raise ValueError("Missing required field 'multiplier' in ModelBilling")
+ return ModelBilling(multiplier=float(multiplier))
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["multiplier"] = self.multiplier
+ return result
+
+
+@dataclass
+class ModelInfo:
+ """Information about an available model"""
+
+ id: str # Model identifier (e.g., "claude-sonnet-4.5")
+ name: str # Display name
+ capabilities: ModelCapabilities # Model capabilities and limits
+ policy: ModelPolicy | None = None # Policy state
+ billing: ModelBilling | None = None # Billing information
+ # Supported reasoning effort levels (only present if model supports reasoning effort)
+ supported_reasoning_efforts: list[str] | None = None
+ # Default reasoning effort level (only present if model supports reasoning effort)
+ default_reasoning_effort: str | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> ModelInfo:
+ assert isinstance(obj, dict)
+ id = obj.get("id")
+ name = obj.get("name")
+ capabilities_dict = obj.get("capabilities")
+ if id is None or name is None or capabilities_dict is None:
+ raise ValueError(
+ f"Missing required fields in ModelInfo: id={id}, name={name}, "
+ f"capabilities={capabilities_dict}"
+ )
+ capabilities = ModelCapabilities.from_dict(capabilities_dict)
+ policy_dict = obj.get("policy")
+ policy = ModelPolicy.from_dict(policy_dict) if policy_dict else None
+ billing_dict = obj.get("billing")
+ billing = ModelBilling.from_dict(billing_dict) if billing_dict else None
+ supported_reasoning_efforts = obj.get("supportedReasoningEfforts")
+ default_reasoning_effort = obj.get("defaultReasoningEffort")
+ return ModelInfo(
+ id=str(id),
+ name=str(name),
+ capabilities=capabilities,
+ policy=policy,
+ billing=billing,
+ supported_reasoning_efforts=supported_reasoning_efforts,
+ default_reasoning_effort=default_reasoning_effort,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["id"] = self.id
+ result["name"] = self.name
+ result["capabilities"] = self.capabilities.to_dict()
+ if self.policy is not None:
+ result["policy"] = self.policy.to_dict()
+ if self.billing is not None:
+ result["billing"] = self.billing.to_dict()
+ if self.supported_reasoning_efforts is not None:
+ result["supportedReasoningEfforts"] = self.supported_reasoning_efforts
+ if self.default_reasoning_effort is not None:
+ result["defaultReasoningEffort"] = self.default_reasoning_effort
+ return result
+
+
+# ============================================================================
+# Session Metadata Types
+# ============================================================================
+
+
+@dataclass
+class SessionContext:
+ """Working directory context for a session"""
+
+ cwd: str # Working directory where the session was created
+ gitRoot: str | None = None # Git repository root (if in a git repo)
+ repository: str | None = None # GitHub repository in "owner/repo" format
+ branch: str | None = None # Current git branch
+
+ @staticmethod
+ def from_dict(obj: Any) -> SessionContext:
+ assert isinstance(obj, dict)
+ cwd = obj.get("cwd")
+ if cwd is None:
+ raise ValueError("Missing required field 'cwd' in SessionContext")
+ return SessionContext(
+ cwd=str(cwd),
+ gitRoot=obj.get("gitRoot"),
+ repository=obj.get("repository"),
+ branch=obj.get("branch"),
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {"cwd": self.cwd}
+ if self.gitRoot is not None:
+ result["gitRoot"] = self.gitRoot
+ if self.repository is not None:
+ result["repository"] = self.repository
+ if self.branch is not None:
+ result["branch"] = self.branch
+ return result
+
+
+@dataclass
+class SessionListFilter:
+ """Filter options for listing sessions"""
+
+ cwd: str | None = None # Filter by exact cwd match
+ gitRoot: str | None = None # Filter by git root
+ repository: str | None = None # Filter by repository (owner/repo format)
+ branch: str | None = None # Filter by branch
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ if self.cwd is not None:
+ result["cwd"] = self.cwd
+ if self.gitRoot is not None:
+ result["gitRoot"] = self.gitRoot
+ if self.repository is not None:
+ result["repository"] = self.repository
+ if self.branch is not None:
+ result["branch"] = self.branch
+ return result
+
+
+@dataclass
+class SessionMetadata:
+ """Metadata about a session"""
+
+ sessionId: str # Session identifier
+ startTime: str # ISO 8601 timestamp when session was created
+ modifiedTime: str # ISO 8601 timestamp when session was last modified
+ isRemote: bool # Whether the session is remote
+ summary: str | None = None # Optional summary of the session
+ context: SessionContext | None = None # Working directory context
+
+ @staticmethod
+ def from_dict(obj: Any) -> SessionMetadata:
+ assert isinstance(obj, dict)
+ sessionId = obj.get("sessionId")
+ startTime = obj.get("startTime")
+ modifiedTime = obj.get("modifiedTime")
+ isRemote = obj.get("isRemote")
+ if sessionId is None or startTime is None or modifiedTime is None or isRemote is None:
+ raise ValueError(
+ f"Missing required fields in SessionMetadata: sessionId={sessionId}, "
+ f"startTime={startTime}, modifiedTime={modifiedTime}, isRemote={isRemote}"
+ )
+ summary = obj.get("summary")
+ context_dict = obj.get("context")
+ context = SessionContext.from_dict(context_dict) if context_dict else None
+ return SessionMetadata(
+ sessionId=str(sessionId),
+ startTime=str(startTime),
+ modifiedTime=str(modifiedTime),
+ isRemote=bool(isRemote),
+ summary=summary,
+ context=context,
+ )
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["sessionId"] = self.sessionId
+ result["startTime"] = self.startTime
+ result["modifiedTime"] = self.modifiedTime
+ result["isRemote"] = self.isRemote
+ if self.summary is not None:
+ result["summary"] = self.summary
+ if self.context is not None:
+ result["context"] = self.context.to_dict()
+ return result
+
+
+# ============================================================================
+# Session Lifecycle Types (for TUI+server mode)
+# ============================================================================
+
+SessionLifecycleEventType = Literal[
+ "session.created",
+ "session.deleted",
+ "session.updated",
+ "session.foreground",
+ "session.background",
+]
+
+
+@dataclass
+class SessionLifecycleEventMetadata:
+ """Metadata for session lifecycle events."""
+
+ startTime: str
+ modifiedTime: str
+ summary: str | None = None
+
+ @staticmethod
+ def from_dict(data: dict) -> SessionLifecycleEventMetadata:
+ return SessionLifecycleEventMetadata(
+ startTime=data.get("startTime", ""),
+ modifiedTime=data.get("modifiedTime", ""),
+ summary=data.get("summary"),
+ )
+
+
+@dataclass
+class SessionLifecycleEvent:
+ """Session lifecycle event notification."""
+
+ type: SessionLifecycleEventType
+ sessionId: str
+ metadata: SessionLifecycleEventMetadata | None = None
+
+ @staticmethod
+ def from_dict(data: dict) -> SessionLifecycleEvent:
+ metadata = None
+ if "metadata" in data and data["metadata"]:
+ metadata = SessionLifecycleEventMetadata.from_dict(data["metadata"])
+ return SessionLifecycleEvent(
+ type=data.get("type", "session.updated"),
+ sessionId=data.get("sessionId", ""),
+ metadata=metadata,
+ )
+
+
+SessionLifecycleHandler = Callable[[SessionLifecycleEvent], None]
HandlerUnsubcribe = Callable[[], None]
@@ -110,10 +686,10 @@ class CopilotClient:
>>> await client.start()
>>>
>>> # Create a session and send a message
- >>> session = await client.create_session(
- ... PermissionHandler.approve_all,
- ... "gpt-4",
- ... )
+ >>> session = await client.create_session({
+ ... "on_permission_request": PermissionHandler.approve_all,
+ ... "model": "gpt-4",
+ ... })
>>> session.on(lambda event: print(event.type))
>>> await session.send("Hello!")
>>>
@@ -152,12 +728,10 @@ def __init__(
>>> client = CopilotClient(ExternalServerConfig(url="localhost:3000"))
>>>
>>> # Custom CLI path with specific log level
- >>> client = CopilotClient(
- ... SubprocessConfig(
- ... cli_path="/usr/local/bin/copilot",
- ... log_level="debug",
- ... )
- ... )
+ >>> client = CopilotClient(SubprocessConfig(
+ ... cli_path="/usr/local/bin/copilot",
+ ... log_level="debug",
+ ... ))
"""
if config is None:
config = SubprocessConfig()
@@ -435,32 +1009,7 @@ async def force_stop(self) -> None:
if not self._is_external_server:
self._actual_port = None
- async def create_session(
- self,
- *,
- on_permission_request: _PermissionHandlerFn,
- model: str | None = None,
- session_id: str | None = None,
- client_name: str | None = None,
- reasoning_effort: ReasoningEffort | None = None,
- tools: list[Tool] | None = None,
- system_message: SystemMessageConfig | None = None,
- available_tools: list[str] | None = None,
- excluded_tools: list[str] | None = None,
- on_user_input_request: UserInputHandler | None = None,
- hooks: SessionHooks | None = None,
- working_directory: str | None = None,
- provider: ProviderConfig | None = None,
- streaming: bool | None = None,
- mcp_servers: dict[str, MCPServerConfig] | None = None,
- custom_agents: list[CustomAgentConfig] | None = None,
- agent: str | None = None,
- config_dir: str | None = None,
- skill_directories: list[str] | None = None,
- disabled_skills: list[str] | None = None,
- infinite_sessions: InfiniteSessionConfig | None = None,
- on_event: Callable[[SessionEvent], None] | None = None,
- ) -> CopilotSession:
+ async def create_session(self, config: SessionConfig) -> CopilotSession:
"""
Create a new conversation session with the Copilot CLI.
@@ -469,29 +1018,8 @@ async def create_session(
automatically start the connection.
Args:
- on_permission_request: Handler for permission requests from the server.
- model: Model to use for this session.
- session_id: Custom session ID.
- client_name: Client name to identify the application using the SDK.
- reasoning_effort: Reasoning effort level ("low", "medium", "high", "xhigh").
- tools: Custom tools exposed to the CLI.
- system_message: System message configuration.
- available_tools: List of tool names to allow (takes precedence over excluded_tools).
- excluded_tools: List of tool names to disable (ignored if available_tools is set).
- on_user_input_request: Handler for user input requests (enables ask_user tool).
- hooks: Hook handlers for intercepting session lifecycle events.
- working_directory: Working directory for the session.
- provider: Custom provider configuration (BYOK - Bring Your Own Key).
- streaming: Enable streaming of assistant message and reasoning chunks.
- mcp_servers: MCP server configurations for the session.
- custom_agents: Custom agent configurations for the session.
- agent: Name of the custom agent to activate when the session starts.
- config_dir: Override the default configuration directory location.
- skill_directories: Directories to load skills from.
- disabled_skills: List of skill names to disable.
- infinite_sessions: Infinite session configuration for persistent workspaces.
- on_event: Event handler registered before the session.create RPC, ensuring
- early events (e.g. session.start) are not missed.
+ config: Optional configuration for the session, including model selection,
+ custom tools, system messages, and more.
Returns:
A :class:`CopilotSession` instance for the new session.
@@ -500,30 +1028,34 @@ async def create_session(
RuntimeError: If the client is not connected and auto_start is disabled.
Example:
- >>> session = await client.create_session(
- ... on_permission_request=PermissionHandler.approve_all,
- ... )
+ >>> # Basic session
+ >>> config = {"on_permission_request": PermissionHandler.approve_all}
+ >>> session = await client.create_session(config)
>>>
>>> # Session with model and streaming
- >>> session = await client.create_session(
- ... on_permission_request=PermissionHandler.approve_all,
- ... model="gpt-4",
- ... streaming=True,
- ... )
+ >>> session = await client.create_session({
+ ... "on_permission_request": PermissionHandler.approve_all,
+ ... "model": "gpt-4",
+ ... "streaming": True
+ ... })
"""
- if not on_permission_request or not callable(on_permission_request):
- raise ValueError(
- "A valid on_permission_request handler is required. "
- "Use PermissionHandler.approve_all or provide a custom handler."
- )
-
if not self._client:
if self._auto_start:
await self.start()
else:
raise RuntimeError("Client not connected. Call start() first.")
+ cfg = config
+
+ if not cfg.get("on_permission_request"):
+ raise ValueError(
+ "An on_permission_request handler is required when creating a session. "
+ "For example, to allow all permissions, use "
+ '{"on_permission_request": PermissionHandler.approve_all}.'
+ )
+
tool_defs = []
+ tools = cfg.get("tools")
if tools:
for tool in tools:
definition: dict[str, Any] = {
@@ -539,61 +1071,92 @@ async def create_session(
tool_defs.append(definition)
payload: dict[str, Any] = {}
- if model:
- payload["model"] = model
- if client_name:
- payload["clientName"] = client_name
- if reasoning_effort:
- payload["reasoningEffort"] = reasoning_effort
+ if cfg.get("model"):
+ payload["model"] = cfg["model"]
+ if cfg.get("client_name"):
+ payload["clientName"] = cfg["client_name"]
+ if cfg.get("reasoning_effort"):
+ payload["reasoningEffort"] = cfg["reasoning_effort"]
if tool_defs:
payload["tools"] = tool_defs
+ # Add system message configuration if provided
+ system_message = cfg.get("system_message")
if system_message:
payload["systemMessage"] = system_message
+ # Add tool filtering options
+ available_tools = cfg.get("available_tools")
if available_tools is not None:
payload["availableTools"] = available_tools
+ excluded_tools = cfg.get("excluded_tools")
if excluded_tools is not None:
payload["excludedTools"] = excluded_tools
+ # Always enable permission request callback (deny by default if no handler provided)
+ on_permission_request = cfg.get("on_permission_request")
payload["requestPermission"] = True
+ # Enable user input request callback if handler provided
+ on_user_input_request = cfg.get("on_user_input_request")
if on_user_input_request:
payload["requestUserInput"] = True
+ # Enable hooks callback if any hook handler provided
+ hooks = cfg.get("hooks")
if hooks and any(hooks.values()):
payload["hooks"] = True
+ # Add working directory if provided
+ working_directory = cfg.get("working_directory")
if working_directory:
payload["workingDirectory"] = working_directory
+ # Add streaming option if provided
+ streaming = cfg.get("streaming")
if streaming is not None:
payload["streaming"] = streaming
+ # Add provider configuration if provided
+ provider = cfg.get("provider")
if provider:
payload["provider"] = self._convert_provider_to_wire_format(provider)
+ # Add MCP servers configuration if provided
+ mcp_servers = cfg.get("mcp_servers")
if mcp_servers:
payload["mcpServers"] = mcp_servers
payload["envValueMode"] = "direct"
+ # Add custom agents configuration if provided
+ custom_agents = cfg.get("custom_agents")
if custom_agents:
payload["customAgents"] = [
- self._convert_custom_agent_to_wire_format(ca) for ca in custom_agents
+ self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents
]
+ # Add agent selection if provided
+ agent = cfg.get("agent")
if agent:
payload["agent"] = agent
+ # Add config directory override if provided
+ config_dir = cfg.get("config_dir")
if config_dir:
payload["configDir"] = config_dir
+ # Add skill directories configuration if provided
+ skill_directories = cfg.get("skill_directories")
if skill_directories:
payload["skillDirectories"] = skill_directories
+ # Add disabled skills configuration if provided
+ disabled_skills = cfg.get("disabled_skills")
if disabled_skills:
payload["disabledSkills"] = disabled_skills
+ # Add infinite sessions configuration if provided
+ infinite_sessions = cfg.get("infinite_sessions")
if infinite_sessions:
wire_config: dict[str, Any] = {}
if "enabled" in infinite_sessions:
@@ -611,7 +1174,7 @@ async def create_session(
if not self._client:
raise RuntimeError("Client not connected")
- session_id = session_id or str(uuid.uuid4())
+ session_id = cfg.get("session_id") or str(uuid.uuid4())
payload["sessionId"] = session_id
# Propagate W3C Trace Context to CLI if OpenTelemetry is active
@@ -627,6 +1190,7 @@ async def create_session(
session._register_user_input_handler(on_user_input_request)
if hooks:
session._register_hooks(hooks)
+ on_event = cfg.get("on_event")
if on_event:
session.on(on_event)
with self._sessions_lock:
@@ -642,33 +1206,7 @@ async def create_session(
return session
- async def resume_session(
- self,
- session_id: str,
- *,
- on_permission_request: _PermissionHandlerFn,
- model: str | None = None,
- client_name: str | None = None,
- reasoning_effort: ReasoningEffort | None = None,
- tools: list[Tool] | None = None,
- system_message: SystemMessageConfig | None = None,
- available_tools: list[str] | None = None,
- excluded_tools: list[str] | None = None,
- on_user_input_request: UserInputHandler | None = None,
- hooks: SessionHooks | None = None,
- working_directory: str | None = None,
- provider: ProviderConfig | None = None,
- streaming: bool | None = None,
- mcp_servers: dict[str, MCPServerConfig] | None = None,
- custom_agents: list[CustomAgentConfig] | None = None,
- agent: str | None = None,
- config_dir: str | None = None,
- skill_directories: list[str] | None = None,
- disabled_skills: list[str] | None = None,
- infinite_sessions: InfiniteSessionConfig | None = None,
- disable_resume: bool = False,
- on_event: Callable[[SessionEvent], None] | None = None,
- ) -> CopilotSession:
+ async def resume_session(self, session_id: str, config: ResumeSessionConfig) -> CopilotSession:
"""
Resume an existing conversation session by its ID.
@@ -678,30 +1216,7 @@ async def resume_session(
Args:
session_id: The ID of the session to resume.
- on_permission_request: Handler for permission requests from the server.
- model: Model to use for this session. Can change the model when resuming.
- client_name: Client name to identify the application using the SDK.
- reasoning_effort: Reasoning effort level ("low", "medium", "high", "xhigh").
- tools: Custom tools exposed to the CLI.
- system_message: System message configuration.
- available_tools: List of tool names to allow (takes precedence over excluded_tools).
- excluded_tools: List of tool names to disable (ignored if available_tools is set).
- on_user_input_request: Handler for user input requests (enables ask_user tool).
- hooks: Hook handlers for intercepting session lifecycle events.
- working_directory: Working directory for the session.
- provider: Custom provider configuration (BYOK - Bring Your Own Key).
- streaming: Enable streaming of assistant message and reasoning chunks.
- mcp_servers: MCP server configurations for the session.
- custom_agents: Custom agent configurations for the session.
- agent: Name of the custom agent to activate when the session starts.
- config_dir: Override the default configuration directory location.
- skill_directories: Directories to load skills from.
- disabled_skills: List of skill names to disable.
- infinite_sessions: Infinite session configuration for persistent workspaces.
- disable_resume: When True, skips emitting the session.resume event.
- Useful for reconnecting without triggering resume-related side effects.
- on_event: Event handler registered before the session.resume RPC, ensuring
- early events (e.g. session.start) are not missed.
+ config: Optional configuration for the resumed session.
Returns:
A :class:`CopilotSession` instance for the resumed session.
@@ -710,32 +1225,33 @@ async def resume_session(
RuntimeError: If the session does not exist or the client is not connected.
Example:
- >>> session = await client.resume_session(
- ... "session-123",
- ... on_permission_request=PermissionHandler.approve_all,
- ... )
+ >>> # Resume a previous session
+ >>> config = {"on_permission_request": PermissionHandler.approve_all}
+ >>> session = await client.resume_session("session-123", config)
>>>
- >>> # Resume with model and streaming
- >>> session = await client.resume_session(
- ... "session-123",
- ... on_permission_request=PermissionHandler.approve_all,
- ... model="gpt-4",
- ... streaming=True,
- ... )
+ >>> # Resume with new tools
+ >>> session = await client.resume_session("session-123", {
+ ... "on_permission_request": PermissionHandler.approve_all,
+ ... "tools": [my_new_tool]
+ ... })
"""
- if not on_permission_request or not callable(on_permission_request):
- raise ValueError(
- "A valid on_permission_request handler is required. "
- "Use PermissionHandler.approve_all or provide a custom handler."
- )
-
if not self._client:
if self._auto_start:
await self.start()
else:
raise RuntimeError("Client not connected. Call start() first.")
+ cfg = config
+
+ if not cfg.get("on_permission_request"):
+ raise ValueError(
+ "An on_permission_request handler is required when resuming a session. "
+ "For example, to allow all permissions, use "
+ '{"on_permission_request": PermissionHandler.approve_all}.'
+ )
+
tool_defs = []
+ tools = cfg.get("tools")
if tools:
for tool in tools:
definition: dict[str, Any] = {
@@ -751,64 +1267,104 @@ async def resume_session(
tool_defs.append(definition)
payload: dict[str, Any] = {"sessionId": session_id}
- if model:
- payload["model"] = model
+
+ # Add client name if provided
+ client_name = cfg.get("client_name")
if client_name:
payload["clientName"] = client_name
- if reasoning_effort:
- payload["reasoningEffort"] = reasoning_effort
+
+ # Add model if provided
+ model = cfg.get("model")
+ if model:
+ payload["model"] = model
+
+ if cfg.get("reasoning_effort"):
+ payload["reasoningEffort"] = cfg["reasoning_effort"]
if tool_defs:
payload["tools"] = tool_defs
+ # Add system message configuration if provided
+ system_message = cfg.get("system_message")
if system_message:
payload["systemMessage"] = system_message
+ # Add available/excluded tools if provided
+ available_tools = cfg.get("available_tools")
if available_tools is not None:
payload["availableTools"] = available_tools
+
+ excluded_tools = cfg.get("excluded_tools")
if excluded_tools is not None:
payload["excludedTools"] = excluded_tools
+ provider = cfg.get("provider")
+ if provider:
+ payload["provider"] = self._convert_provider_to_wire_format(provider)
+
+ # Add streaming option if provided
+ streaming = cfg.get("streaming")
+ if streaming is not None:
+ payload["streaming"] = streaming
+
+ # Always enable permission request callback (deny by default if no handler provided)
+ on_permission_request = cfg.get("on_permission_request")
payload["requestPermission"] = True
+ # Enable user input request callback if handler provided
+ on_user_input_request = cfg.get("on_user_input_request")
if on_user_input_request:
payload["requestUserInput"] = True
+ # Enable hooks callback if any hook handler provided
+ hooks = cfg.get("hooks")
if hooks and any(hooks.values()):
payload["hooks"] = True
+ # Add working directory if provided
+ working_directory = cfg.get("working_directory")
if working_directory:
payload["workingDirectory"] = working_directory
- if streaming is not None:
- payload["streaming"] = streaming
+ # Add config directory if provided
+ config_dir = cfg.get("config_dir")
+ if config_dir:
+ payload["configDir"] = config_dir
- if provider:
- payload["provider"] = self._convert_provider_to_wire_format(provider)
+ # Add disable resume flag if provided
+ disable_resume = cfg.get("disable_resume")
+ if disable_resume:
+ payload["disableResume"] = True
+ # Add MCP servers configuration if provided
+ mcp_servers = cfg.get("mcp_servers")
if mcp_servers:
payload["mcpServers"] = mcp_servers
payload["envValueMode"] = "direct"
+ # Add custom agents configuration if provided
+ custom_agents = cfg.get("custom_agents")
if custom_agents:
payload["customAgents"] = [
- self._convert_custom_agent_to_wire_format(ca) for ca in custom_agents
+ self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents
]
+ # Add agent selection if provided
+ agent = cfg.get("agent")
if agent:
payload["agent"] = agent
- if config_dir:
- payload["configDir"] = config_dir
-
- if disable_resume:
- payload["disableResume"] = True
-
+ # Add skill directories configuration if provided
+ skill_directories = cfg.get("skill_directories")
if skill_directories:
payload["skillDirectories"] = skill_directories
+ # Add disabled skills configuration if provided
+ disabled_skills = cfg.get("disabled_skills")
if disabled_skills:
payload["disabledSkills"] = disabled_skills
+ # Add infinite sessions configuration if provided
+ infinite_sessions = cfg.get("infinite_sessions")
if infinite_sessions:
wire_config: dict[str, Any] = {}
if "enabled" in infinite_sessions:
@@ -833,12 +1389,13 @@ async def resume_session(
# Create and register the session before issuing the RPC so that
# events emitted by the CLI (e.g. session.start) are not dropped.
session = CopilotSession(session_id, self._client, None)
- session._register_tools(tools)
+ session._register_tools(cfg.get("tools"))
session._register_permission_handler(on_permission_request)
if on_user_input_request:
session._register_user_input_handler(on_user_input_request)
if hooks:
session._register_hooks(hooks)
+ on_event = cfg.get("on_event")
if on_event:
session.on(on_event)
with self._sessions_lock:
@@ -868,7 +1425,7 @@ def get_state(self) -> ConnectionState:
"""
return self._state
- async def ping(self, message: str | None = None) -> "PingResponse":
+ async def ping(self, message: str | None = None) -> PingResponse:
"""
Send a ping request to the server to verify connectivity.
@@ -891,7 +1448,7 @@ async def ping(self, message: str | None = None) -> "PingResponse":
result = await self._client.request("ping", {"message": message})
return PingResponse.from_dict(result)
- async def get_status(self) -> "GetStatusResponse":
+ async def get_status(self) -> GetStatusResponse:
"""
Get CLI status including version and protocol information.
@@ -911,7 +1468,7 @@ async def get_status(self) -> "GetStatusResponse":
result = await self._client.request("status.get", {})
return GetStatusResponse.from_dict(result)
- async def get_auth_status(self) -> "GetAuthStatusResponse":
+ async def get_auth_status(self) -> GetAuthStatusResponse:
"""
Get current authentication status.
@@ -932,7 +1489,7 @@ async def get_auth_status(self) -> "GetAuthStatusResponse":
result = await self._client.request("auth.getStatus", {})
return GetAuthStatusResponse.from_dict(result)
- async def list_models(self) -> list["ModelInfo"]:
+ async def list_models(self) -> list[ModelInfo]:
"""
List available models with their metadata.
@@ -982,9 +1539,7 @@ async def list_models(self) -> list["ModelInfo"]:
return list(models) # Return a copy to prevent cache mutation
- async def list_sessions(
- self, filter: "SessionListFilter | None" = None
- ) -> list["SessionMetadata"]:
+ async def list_sessions(self, filter: SessionListFilter | None = None) -> list[SessionMetadata]:
"""
List all available sessions known to the server.
@@ -1005,7 +1560,7 @@ async def list_sessions(
>>> for session in sessions:
... print(f"Session: {session.sessionId}")
>>> # Filter sessions by repository
- >>> from copilot import SessionListFilter
+ >>> from copilot.client import SessionListFilter
>>> filtered = await client.list_sessions(SessionListFilter(repository="owner/repo"))
"""
if not self._client:
@@ -1068,7 +1623,8 @@ async def get_last_session_id(self) -> str | None:
Example:
>>> last_id = await client.get_last_session_id()
>>> if last_id:
- ... session = await client.resume_session(last_id, PermissionHandler.approve_all)
+ ... config = {"on_permission_request": PermissionHandler.approve_all}
+ ... session = await client.resume_session(last_id, config)
"""
if not self._client:
raise RuntimeError("Client not connected")
diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py
index da6748d79..14ae307d7 100644
--- a/python/copilot/generated/rpc.py
+++ b/python/copilot/generated/rpc.py
@@ -74,6 +74,11 @@ def to_enum(c: type[EnumT], x: Any) -> EnumT:
return x.value
+def from_int(x: Any) -> int:
+ assert isinstance(x, int) and not isinstance(x, bool)
+ return x
+
+
@dataclass
class PingResult:
message: str
@@ -762,7 +767,7 @@ def to_dict(self) -> dict:
@dataclass
-class AgentElement:
+class SessionAgentListResultAgent:
description: str
"""Description of the agent's purpose"""
@@ -773,12 +778,12 @@ class AgentElement:
"""Unique identifier of the custom agent"""
@staticmethod
- def from_dict(obj: Any) -> 'AgentElement':
+ def from_dict(obj: Any) -> 'SessionAgentListResultAgent':
assert isinstance(obj, dict)
description = from_str(obj.get("description"))
display_name = from_str(obj.get("displayName"))
name = from_str(obj.get("name"))
- return AgentElement(description, display_name, name)
+ return SessionAgentListResultAgent(description, display_name, name)
def to_dict(self) -> dict:
result: dict = {}
@@ -791,18 +796,18 @@ def to_dict(self) -> dict:
# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
class SessionAgentListResult:
- agents: list[AgentElement]
+ agents: list[SessionAgentListResultAgent]
"""Available custom agents"""
@staticmethod
def from_dict(obj: Any) -> 'SessionAgentListResult':
assert isinstance(obj, dict)
- agents = from_list(AgentElement.from_dict, obj.get("agents"))
+ agents = from_list(SessionAgentListResultAgent.from_dict, obj.get("agents"))
return SessionAgentListResult(agents)
def to_dict(self) -> dict:
result: dict = {}
- result["agents"] = from_list(lambda x: to_class(AgentElement, x), self.agents)
+ result["agents"] = from_list(lambda x: to_class(SessionAgentListResultAgent, x), self.agents)
return result
@@ -929,334 +934,1129 @@ def to_dict(self) -> dict:
return result
-# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionCompactionCompactResult:
- messages_removed: float
- """Number of messages removed during compaction"""
+class SessionAgentReloadResultAgent:
+ description: str
+ """Description of the agent's purpose"""
- success: bool
- """Whether compaction completed successfully"""
+ display_name: str
+ """Human-readable display name"""
- tokens_removed: float
- """Number of tokens freed by compaction"""
+ name: str
+ """Unique identifier of the custom agent"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionCompactionCompactResult':
+ def from_dict(obj: Any) -> 'SessionAgentReloadResultAgent':
assert isinstance(obj, dict)
- messages_removed = from_float(obj.get("messagesRemoved"))
- success = from_bool(obj.get("success"))
- tokens_removed = from_float(obj.get("tokensRemoved"))
- return SessionCompactionCompactResult(messages_removed, success, tokens_removed)
+ description = from_str(obj.get("description"))
+ display_name = from_str(obj.get("displayName"))
+ name = from_str(obj.get("name"))
+ return SessionAgentReloadResultAgent(description, display_name, name)
def to_dict(self) -> dict:
result: dict = {}
- result["messagesRemoved"] = to_float(self.messages_removed)
- result["success"] = from_bool(self.success)
- result["tokensRemoved"] = to_float(self.tokens_removed)
+ result["description"] = from_str(self.description)
+ result["displayName"] = from_str(self.display_name)
+ result["name"] = from_str(self.name)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionToolsHandlePendingToolCallResult:
- success: bool
- """Whether the tool call result was handled successfully"""
+class SessionAgentReloadResult:
+ agents: list[SessionAgentReloadResultAgent]
+ """Reloaded custom agents"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallResult':
+ def from_dict(obj: Any) -> 'SessionAgentReloadResult':
assert isinstance(obj, dict)
- success = from_bool(obj.get("success"))
- return SessionToolsHandlePendingToolCallResult(success)
+ agents = from_list(SessionAgentReloadResultAgent.from_dict, obj.get("agents"))
+ return SessionAgentReloadResult(agents)
def to_dict(self) -> dict:
result: dict = {}
- result["success"] = from_bool(self.success)
+ result["agents"] = from_list(lambda x: to_class(SessionAgentReloadResultAgent, x), self.agents)
return result
@dataclass
-class ResultResult:
- text_result_for_llm: str
- error: str | None = None
- result_type: str | None = None
- tool_telemetry: dict[str, Any] | None = None
+class Skill:
+ description: str
+ """Description of what the skill does"""
+
+ enabled: bool
+ """Whether the skill is currently enabled"""
+
+ name: str
+ """Unique identifier for the skill"""
+
+ source: str
+ """Source location type (e.g., project, personal, plugin)"""
+
+ user_invocable: bool
+ """Whether the skill can be invoked by the user as a slash command"""
+
+ path: str | None = None
+ """Absolute path to the skill file"""
@staticmethod
- def from_dict(obj: Any) -> 'ResultResult':
+ def from_dict(obj: Any) -> 'Skill':
assert isinstance(obj, dict)
- text_result_for_llm = from_str(obj.get("textResultForLlm"))
- error = from_union([from_str, from_none], obj.get("error"))
- result_type = from_union([from_str, from_none], obj.get("resultType"))
- tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry"))
- return ResultResult(text_result_for_llm, error, result_type, tool_telemetry)
+ description = from_str(obj.get("description"))
+ enabled = from_bool(obj.get("enabled"))
+ name = from_str(obj.get("name"))
+ source = from_str(obj.get("source"))
+ user_invocable = from_bool(obj.get("userInvocable"))
+ path = from_union([from_str, from_none], obj.get("path"))
+ return Skill(description, enabled, name, source, user_invocable, path)
def to_dict(self) -> dict:
result: dict = {}
- result["textResultForLlm"] = from_str(self.text_result_for_llm)
- if self.error is not None:
- result["error"] = from_union([from_str, from_none], self.error)
- if self.result_type is not None:
- result["resultType"] = from_union([from_str, from_none], self.result_type)
- if self.tool_telemetry is not None:
- result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry)
+ result["description"] = from_str(self.description)
+ result["enabled"] = from_bool(self.enabled)
+ result["name"] = from_str(self.name)
+ result["source"] = from_str(self.source)
+ result["userInvocable"] = from_bool(self.user_invocable)
+ if self.path is not None:
+ result["path"] = from_union([from_str, from_none], self.path)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionToolsHandlePendingToolCallParams:
- request_id: str
- error: str | None = None
- result: ResultResult | str | None = None
+class SessionSkillsListResult:
+ skills: list[Skill]
+ """Available skills"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallParams':
+ def from_dict(obj: Any) -> 'SessionSkillsListResult':
assert isinstance(obj, dict)
- request_id = from_str(obj.get("requestId"))
- error = from_union([from_str, from_none], obj.get("error"))
- result = from_union([ResultResult.from_dict, from_str, from_none], obj.get("result"))
- return SessionToolsHandlePendingToolCallParams(request_id, error, result)
+ skills = from_list(Skill.from_dict, obj.get("skills"))
+ return SessionSkillsListResult(skills)
def to_dict(self) -> dict:
result: dict = {}
- result["requestId"] = from_str(self.request_id)
- if self.error is not None:
- result["error"] = from_union([from_str, from_none], self.error)
- if self.result is not None:
- result["result"] = from_union([lambda x: to_class(ResultResult, x), from_str, from_none], self.result)
+ result["skills"] = from_list(lambda x: to_class(Skill, x), self.skills)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionPermissionsHandlePendingPermissionRequestResult:
- success: bool
- """Whether the permission request was handled successfully"""
-
+class SessionSkillsEnableResult:
@staticmethod
- def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestResult':
+ def from_dict(obj: Any) -> 'SessionSkillsEnableResult':
assert isinstance(obj, dict)
- success = from_bool(obj.get("success"))
- return SessionPermissionsHandlePendingPermissionRequestResult(success)
+ return SessionSkillsEnableResult()
def to_dict(self) -> dict:
result: dict = {}
- result["success"] = from_bool(self.success)
return result
-class Kind(Enum):
- APPROVED = "approved"
- DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy"
- DENIED_BY_RULES = "denied-by-rules"
- DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user"
- DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user"
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionSkillsEnableParams:
+ name: str
+ """Name of the skill to enable"""
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionSkillsEnableParams':
+ assert isinstance(obj, dict)
+ name = from_str(obj.get("name"))
+ return SessionSkillsEnableParams(name)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["name"] = from_str(self.name)
+ return result
-@dataclass
-class SessionPermissionsHandlePendingPermissionRequestParamsResult:
- kind: Kind
- rules: list[Any] | None = None
- feedback: str | None = None
- message: str | None = None
- path: str | None = None
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionSkillsDisableResult:
@staticmethod
- def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParamsResult':
+ def from_dict(obj: Any) -> 'SessionSkillsDisableResult':
assert isinstance(obj, dict)
- kind = Kind(obj.get("kind"))
- rules = from_union([lambda x: from_list(lambda x: x, x), from_none], obj.get("rules"))
- feedback = from_union([from_str, from_none], obj.get("feedback"))
- message = from_union([from_str, from_none], obj.get("message"))
- path = from_union([from_str, from_none], obj.get("path"))
- return SessionPermissionsHandlePendingPermissionRequestParamsResult(kind, rules, feedback, message, path)
+ return SessionSkillsDisableResult()
def to_dict(self) -> dict:
result: dict = {}
- result["kind"] = to_enum(Kind, self.kind)
- if self.rules is not None:
- result["rules"] = from_union([lambda x: from_list(lambda x: x, x), from_none], self.rules)
- if self.feedback is not None:
- result["feedback"] = from_union([from_str, from_none], self.feedback)
- if self.message is not None:
- result["message"] = from_union([from_str, from_none], self.message)
- if self.path is not None:
- result["path"] = from_union([from_str, from_none], self.path)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionPermissionsHandlePendingPermissionRequestParams:
- request_id: str
- result: SessionPermissionsHandlePendingPermissionRequestParamsResult
+class SessionSkillsDisableParams:
+ name: str
+ """Name of the skill to disable"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParams':
+ def from_dict(obj: Any) -> 'SessionSkillsDisableParams':
assert isinstance(obj, dict)
- request_id = from_str(obj.get("requestId"))
- result = SessionPermissionsHandlePendingPermissionRequestParamsResult.from_dict(obj.get("result"))
- return SessionPermissionsHandlePendingPermissionRequestParams(request_id, result)
+ name = from_str(obj.get("name"))
+ return SessionSkillsDisableParams(name)
def to_dict(self) -> dict:
result: dict = {}
- result["requestId"] = from_str(self.request_id)
- result["result"] = to_class(SessionPermissionsHandlePendingPermissionRequestParamsResult, self.result)
+ result["name"] = from_str(self.name)
return result
+# Experimental: this type is part of an experimental API and may change or be removed.
@dataclass
-class SessionLogResult:
- event_id: UUID
- """The unique identifier of the emitted session event"""
-
+class SessionSkillsReloadResult:
@staticmethod
- def from_dict(obj: Any) -> 'SessionLogResult':
+ def from_dict(obj: Any) -> 'SessionSkillsReloadResult':
assert isinstance(obj, dict)
- event_id = UUID(obj.get("eventId"))
- return SessionLogResult(event_id)
+ return SessionSkillsReloadResult()
def to_dict(self) -> dict:
result: dict = {}
- result["eventId"] = str(self.event_id)
return result
-class Level(Enum):
- """Log severity level. Determines how the message is displayed in the timeline. Defaults to
- "info".
- """
- ERROR = "error"
- INFO = "info"
- WARNING = "warning"
+class ServerStatus(Enum):
+ """Connection status: connected, failed, pending, disabled, or not_configured"""
+
+ CONNECTED = "connected"
+ DISABLED = "disabled"
+ FAILED = "failed"
+ NOT_CONFIGURED = "not_configured"
+ PENDING = "pending"
@dataclass
-class SessionLogParams:
- message: str
- """Human-readable message"""
+class Server:
+ name: str
+ """Server name (config key)"""
- ephemeral: bool | None = None
- """When true, the message is transient and not persisted to the session event log on disk"""
+ status: ServerStatus
+ """Connection status: connected, failed, pending, disabled, or not_configured"""
- level: Level | None = None
- """Log severity level. Determines how the message is displayed in the timeline. Defaults to
- "info".
- """
+ error: str | None = None
+ """Error message if the server failed to connect"""
+
+ source: str | None = None
+ """Configuration source: user, workspace, plugin, or builtin"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionLogParams':
+ def from_dict(obj: Any) -> 'Server':
assert isinstance(obj, dict)
- message = from_str(obj.get("message"))
- ephemeral = from_union([from_bool, from_none], obj.get("ephemeral"))
- level = from_union([Level, from_none], obj.get("level"))
- return SessionLogParams(message, ephemeral, level)
+ name = from_str(obj.get("name"))
+ status = ServerStatus(obj.get("status"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ source = from_union([from_str, from_none], obj.get("source"))
+ return Server(name, status, error, source)
def to_dict(self) -> dict:
result: dict = {}
- result["message"] = from_str(self.message)
- if self.ephemeral is not None:
- result["ephemeral"] = from_union([from_bool, from_none], self.ephemeral)
- if self.level is not None:
- result["level"] = from_union([lambda x: to_enum(Level, x), from_none], self.level)
+ result["name"] = from_str(self.name)
+ result["status"] = to_enum(ServerStatus, self.status)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ if self.source is not None:
+ result["source"] = from_union([from_str, from_none], self.source)
return result
@dataclass
-class SessionShellExecResult:
- process_id: str
- """Unique identifier for tracking streamed output"""
+class SessionMCPListResult:
+ servers: list[Server]
+ """Configured MCP servers"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionShellExecResult':
+ def from_dict(obj: Any) -> 'SessionMCPListResult':
assert isinstance(obj, dict)
- process_id = from_str(obj.get("processId"))
- return SessionShellExecResult(process_id)
+ servers = from_list(Server.from_dict, obj.get("servers"))
+ return SessionMCPListResult(servers)
def to_dict(self) -> dict:
result: dict = {}
- result["processId"] = from_str(self.process_id)
+ result["servers"] = from_list(lambda x: to_class(Server, x), self.servers)
return result
@dataclass
-class SessionShellExecParams:
- command: str
- """Shell command to execute"""
-
- cwd: str | None = None
- """Working directory (defaults to session working directory)"""
-
- timeout: float | None = None
- """Timeout in milliseconds (default: 30000)"""
-
+class SessionMCPEnableResult:
@staticmethod
- def from_dict(obj: Any) -> 'SessionShellExecParams':
+ def from_dict(obj: Any) -> 'SessionMCPEnableResult':
assert isinstance(obj, dict)
- command = from_str(obj.get("command"))
- cwd = from_union([from_str, from_none], obj.get("cwd"))
- timeout = from_union([from_float, from_none], obj.get("timeout"))
- return SessionShellExecParams(command, cwd, timeout)
+ return SessionMCPEnableResult()
def to_dict(self) -> dict:
result: dict = {}
- result["command"] = from_str(self.command)
- if self.cwd is not None:
- result["cwd"] = from_union([from_str, from_none], self.cwd)
- if self.timeout is not None:
- result["timeout"] = from_union([to_float, from_none], self.timeout)
return result
@dataclass
-class SessionShellKillResult:
- killed: bool
- """Whether the signal was sent successfully"""
+class SessionMCPEnableParams:
+ server_name: str
+ """Name of the MCP server to enable"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionShellKillResult':
+ def from_dict(obj: Any) -> 'SessionMCPEnableParams':
assert isinstance(obj, dict)
- killed = from_bool(obj.get("killed"))
- return SessionShellKillResult(killed)
+ server_name = from_str(obj.get("serverName"))
+ return SessionMCPEnableParams(server_name)
def to_dict(self) -> dict:
result: dict = {}
- result["killed"] = from_bool(self.killed)
+ result["serverName"] = from_str(self.server_name)
return result
-class Signal(Enum):
- """Signal to send (default: SIGTERM)"""
+@dataclass
+class SessionMCPDisableResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionMCPDisableResult':
+ assert isinstance(obj, dict)
+ return SessionMCPDisableResult()
- SIGINT = "SIGINT"
- SIGKILL = "SIGKILL"
- SIGTERM = "SIGTERM"
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
@dataclass
-class SessionShellKillParams:
- process_id: str
- """Process identifier returned by shell.exec"""
-
- signal: Signal | None = None
- """Signal to send (default: SIGTERM)"""
+class SessionMCPDisableParams:
+ server_name: str
+ """Name of the MCP server to disable"""
@staticmethod
- def from_dict(obj: Any) -> 'SessionShellKillParams':
+ def from_dict(obj: Any) -> 'SessionMCPDisableParams':
assert isinstance(obj, dict)
- process_id = from_str(obj.get("processId"))
- signal = from_union([Signal, from_none], obj.get("signal"))
- return SessionShellKillParams(process_id, signal)
+ server_name = from_str(obj.get("serverName"))
+ return SessionMCPDisableParams(server_name)
def to_dict(self) -> dict:
result: dict = {}
- result["processId"] = from_str(self.process_id)
- if self.signal is not None:
- result["signal"] = from_union([lambda x: to_enum(Signal, x), from_none], self.signal)
+ result["serverName"] = from_str(self.server_name)
return result
-def ping_result_from_dict(s: Any) -> PingResult:
- return PingResult.from_dict(s)
-
-
+@dataclass
+class SessionMCPReloadResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionMCPReloadResult':
+ assert isinstance(obj, dict)
+ return SessionMCPReloadResult()
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
+
+
+@dataclass
+class Plugin:
+ enabled: bool
+ """Whether the plugin is currently enabled"""
+
+ marketplace: str
+ """Marketplace the plugin came from"""
+
+ name: str
+ """Plugin name"""
+
+ version: str | None = None
+ """Installed version"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Plugin':
+ assert isinstance(obj, dict)
+ enabled = from_bool(obj.get("enabled"))
+ marketplace = from_str(obj.get("marketplace"))
+ name = from_str(obj.get("name"))
+ version = from_union([from_str, from_none], obj.get("version"))
+ return Plugin(enabled, marketplace, name, version)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["enabled"] = from_bool(self.enabled)
+ result["marketplace"] = from_str(self.marketplace)
+ result["name"] = from_str(self.name)
+ if self.version is not None:
+ result["version"] = from_union([from_str, from_none], self.version)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionPluginsListResult:
+ plugins: list[Plugin]
+ """Installed plugins"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionPluginsListResult':
+ assert isinstance(obj, dict)
+ plugins = from_list(Plugin.from_dict, obj.get("plugins"))
+ return SessionPluginsListResult(plugins)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["plugins"] = from_list(lambda x: to_class(Plugin, x), self.plugins)
+ return result
+
+
+class Source(Enum):
+ """Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)"""
+
+ PROJECT = "project"
+ USER = "user"
+
+
+class ExtensionStatus(Enum):
+ """Current status: running, disabled, failed, or starting"""
+
+ DISABLED = "disabled"
+ FAILED = "failed"
+ RUNNING = "running"
+ STARTING = "starting"
+
+
+@dataclass
+class Extension:
+ id: str
+ """Source-qualified ID (e.g., 'project:my-ext', 'user:auth-helper')"""
+
+ name: str
+ """Extension name (directory name)"""
+
+ source: Source
+ """Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/)"""
+
+ status: ExtensionStatus
+ """Current status: running, disabled, failed, or starting"""
+
+ pid: int | None = None
+ """Process ID if the extension is running"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Extension':
+ assert isinstance(obj, dict)
+ id = from_str(obj.get("id"))
+ name = from_str(obj.get("name"))
+ source = Source(obj.get("source"))
+ status = ExtensionStatus(obj.get("status"))
+ pid = from_union([from_int, from_none], obj.get("pid"))
+ return Extension(id, name, source, status, pid)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["id"] = from_str(self.id)
+ result["name"] = from_str(self.name)
+ result["source"] = to_enum(Source, self.source)
+ result["status"] = to_enum(ExtensionStatus, self.status)
+ if self.pid is not None:
+ result["pid"] = from_union([from_int, from_none], self.pid)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsListResult:
+ extensions: list[Extension]
+ """Discovered extensions and their current status"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsListResult':
+ assert isinstance(obj, dict)
+ extensions = from_list(Extension.from_dict, obj.get("extensions"))
+ return SessionExtensionsListResult(extensions)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["extensions"] = from_list(lambda x: to_class(Extension, x), self.extensions)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsEnableResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsEnableResult':
+ assert isinstance(obj, dict)
+ return SessionExtensionsEnableResult()
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsEnableParams:
+ id: str
+ """Source-qualified extension ID to enable"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsEnableParams':
+ assert isinstance(obj, dict)
+ id = from_str(obj.get("id"))
+ return SessionExtensionsEnableParams(id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["id"] = from_str(self.id)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsDisableResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsDisableResult':
+ assert isinstance(obj, dict)
+ return SessionExtensionsDisableResult()
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsDisableParams:
+ id: str
+ """Source-qualified extension ID to disable"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsDisableParams':
+ assert isinstance(obj, dict)
+ id = from_str(obj.get("id"))
+ return SessionExtensionsDisableParams(id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["id"] = from_str(self.id)
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionExtensionsReloadResult:
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionExtensionsReloadResult':
+ assert isinstance(obj, dict)
+ return SessionExtensionsReloadResult()
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ return result
+
+
+# Experimental: this type is part of an experimental API and may change or be removed.
+@dataclass
+class SessionCompactionCompactResult:
+ messages_removed: float
+ """Number of messages removed during compaction"""
+
+ success: bool
+ """Whether compaction completed successfully"""
+
+ tokens_removed: float
+ """Number of tokens freed by compaction"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionCompactionCompactResult':
+ assert isinstance(obj, dict)
+ messages_removed = from_float(obj.get("messagesRemoved"))
+ success = from_bool(obj.get("success"))
+ tokens_removed = from_float(obj.get("tokensRemoved"))
+ return SessionCompactionCompactResult(messages_removed, success, tokens_removed)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["messagesRemoved"] = to_float(self.messages_removed)
+ result["success"] = from_bool(self.success)
+ result["tokensRemoved"] = to_float(self.tokens_removed)
+ return result
+
+
+@dataclass
+class SessionToolsHandlePendingToolCallResult:
+ success: bool
+ """Whether the tool call result was handled successfully"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallResult':
+ assert isinstance(obj, dict)
+ success = from_bool(obj.get("success"))
+ return SessionToolsHandlePendingToolCallResult(success)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["success"] = from_bool(self.success)
+ return result
+
+
+@dataclass
+class ResultResult:
+ text_result_for_llm: str
+ error: str | None = None
+ result_type: str | None = None
+ tool_telemetry: dict[str, Any] | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'ResultResult':
+ assert isinstance(obj, dict)
+ text_result_for_llm = from_str(obj.get("textResultForLlm"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ result_type = from_union([from_str, from_none], obj.get("resultType"))
+ tool_telemetry = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("toolTelemetry"))
+ return ResultResult(text_result_for_llm, error, result_type, tool_telemetry)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["textResultForLlm"] = from_str(self.text_result_for_llm)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ if self.result_type is not None:
+ result["resultType"] = from_union([from_str, from_none], self.result_type)
+ if self.tool_telemetry is not None:
+ result["toolTelemetry"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.tool_telemetry)
+ return result
+
+
+@dataclass
+class SessionToolsHandlePendingToolCallParams:
+ request_id: str
+ error: str | None = None
+ result: ResultResult | str | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionToolsHandlePendingToolCallParams':
+ assert isinstance(obj, dict)
+ request_id = from_str(obj.get("requestId"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ result = from_union([ResultResult.from_dict, from_str, from_none], obj.get("result"))
+ return SessionToolsHandlePendingToolCallParams(request_id, error, result)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["requestId"] = from_str(self.request_id)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ if self.result is not None:
+ result["result"] = from_union([lambda x: to_class(ResultResult, x), from_str, from_none], self.result)
+ return result
+
+
+@dataclass
+class SessionCommandsHandlePendingCommandResult:
+ success: bool
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionCommandsHandlePendingCommandResult':
+ assert isinstance(obj, dict)
+ success = from_bool(obj.get("success"))
+ return SessionCommandsHandlePendingCommandResult(success)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["success"] = from_bool(self.success)
+ return result
+
+
+@dataclass
+class SessionCommandsHandlePendingCommandParams:
+ request_id: str
+ """Request ID from the command invocation event"""
+
+ error: str | None = None
+ """Error message if the command handler failed"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionCommandsHandlePendingCommandParams':
+ assert isinstance(obj, dict)
+ request_id = from_str(obj.get("requestId"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ return SessionCommandsHandlePendingCommandParams(request_id, error)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["requestId"] = from_str(self.request_id)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ return result
+
+
+class Action(Enum):
+ """The user's response: accept (submitted), decline (rejected), or cancel (dismissed)"""
+
+ ACCEPT = "accept"
+ CANCEL = "cancel"
+ DECLINE = "decline"
+
+
+@dataclass
+class SessionUIElicitationResult:
+ action: Action
+ """The user's response: accept (submitted), decline (rejected), or cancel (dismissed)"""
+
+ content: dict[str, float | bool | list[str] | str] | None = None
+ """The form values submitted by the user (present when action is 'accept')"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionUIElicitationResult':
+ assert isinstance(obj, dict)
+ action = Action(obj.get("action"))
+ content = from_union([lambda x: from_dict(lambda x: from_union([from_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], obj.get("content"))
+ return SessionUIElicitationResult(action, content)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["action"] = to_enum(Action, self.action)
+ if self.content is not None:
+ result["content"] = from_union([lambda x: from_dict(lambda x: from_union([to_float, from_bool, lambda x: from_list(from_str, x), from_str], x), x), from_none], self.content)
+ return result
+
+
+class Format(Enum):
+ DATE = "date"
+ DATE_TIME = "date-time"
+ EMAIL = "email"
+ URI = "uri"
+
+
+@dataclass
+class AnyOf:
+ const: str
+ title: str
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'AnyOf':
+ assert isinstance(obj, dict)
+ const = from_str(obj.get("const"))
+ title = from_str(obj.get("title"))
+ return AnyOf(const, title)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["const"] = from_str(self.const)
+ result["title"] = from_str(self.title)
+ return result
+
+
+class ItemsType(Enum):
+ STRING = "string"
+
+
+@dataclass
+class Items:
+ enum: list[str] | None = None
+ type: ItemsType | None = None
+ any_of: list[AnyOf] | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Items':
+ assert isinstance(obj, dict)
+ enum = from_union([lambda x: from_list(from_str, x), from_none], obj.get("enum"))
+ type = from_union([ItemsType, from_none], obj.get("type"))
+ any_of = from_union([lambda x: from_list(AnyOf.from_dict, x), from_none], obj.get("anyOf"))
+ return Items(enum, type, any_of)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ if self.enum is not None:
+ result["enum"] = from_union([lambda x: from_list(from_str, x), from_none], self.enum)
+ if self.type is not None:
+ result["type"] = from_union([lambda x: to_enum(ItemsType, x), from_none], self.type)
+ if self.any_of is not None:
+ result["anyOf"] = from_union([lambda x: from_list(lambda x: to_class(AnyOf, x), x), from_none], self.any_of)
+ return result
+
+
+@dataclass
+class OneOf:
+ const: str
+ title: str
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'OneOf':
+ assert isinstance(obj, dict)
+ const = from_str(obj.get("const"))
+ title = from_str(obj.get("title"))
+ return OneOf(const, title)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["const"] = from_str(self.const)
+ result["title"] = from_str(self.title)
+ return result
+
+
+class PropertyType(Enum):
+ ARRAY = "array"
+ BOOLEAN = "boolean"
+ INTEGER = "integer"
+ NUMBER = "number"
+ STRING = "string"
+
+
+@dataclass
+class Property:
+ type: PropertyType
+ default: float | bool | list[str] | str | None = None
+ description: str | None = None
+ enum: list[str] | None = None
+ enum_names: list[str] | None = None
+ title: str | None = None
+ one_of: list[OneOf] | None = None
+ items: Items | None = None
+ max_items: float | None = None
+ min_items: float | None = None
+ format: Format | None = None
+ max_length: float | None = None
+ min_length: float | None = None
+ maximum: float | None = None
+ minimum: float | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Property':
+ assert isinstance(obj, dict)
+ type = PropertyType(obj.get("type"))
+ default = from_union([from_float, from_bool, lambda x: from_list(from_str, x), from_str, from_none], obj.get("default"))
+ description = from_union([from_str, from_none], obj.get("description"))
+ enum = from_union([lambda x: from_list(from_str, x), from_none], obj.get("enum"))
+ enum_names = from_union([lambda x: from_list(from_str, x), from_none], obj.get("enumNames"))
+ title = from_union([from_str, from_none], obj.get("title"))
+ one_of = from_union([lambda x: from_list(OneOf.from_dict, x), from_none], obj.get("oneOf"))
+ items = from_union([Items.from_dict, from_none], obj.get("items"))
+ max_items = from_union([from_float, from_none], obj.get("maxItems"))
+ min_items = from_union([from_float, from_none], obj.get("minItems"))
+ format = from_union([Format, from_none], obj.get("format"))
+ max_length = from_union([from_float, from_none], obj.get("maxLength"))
+ min_length = from_union([from_float, from_none], obj.get("minLength"))
+ maximum = from_union([from_float, from_none], obj.get("maximum"))
+ minimum = from_union([from_float, from_none], obj.get("minimum"))
+ return Property(type, default, description, enum, enum_names, title, one_of, items, max_items, min_items, format, max_length, min_length, maximum, minimum)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["type"] = to_enum(PropertyType, self.type)
+ if self.default is not None:
+ result["default"] = from_union([to_float, from_bool, lambda x: from_list(from_str, x), from_str, from_none], self.default)
+ if self.description is not None:
+ result["description"] = from_union([from_str, from_none], self.description)
+ if self.enum is not None:
+ result["enum"] = from_union([lambda x: from_list(from_str, x), from_none], self.enum)
+ if self.enum_names is not None:
+ result["enumNames"] = from_union([lambda x: from_list(from_str, x), from_none], self.enum_names)
+ if self.title is not None:
+ result["title"] = from_union([from_str, from_none], self.title)
+ if self.one_of is not None:
+ result["oneOf"] = from_union([lambda x: from_list(lambda x: to_class(OneOf, x), x), from_none], self.one_of)
+ if self.items is not None:
+ result["items"] = from_union([lambda x: to_class(Items, x), from_none], self.items)
+ if self.max_items is not None:
+ result["maxItems"] = from_union([to_float, from_none], self.max_items)
+ if self.min_items is not None:
+ result["minItems"] = from_union([to_float, from_none], self.min_items)
+ if self.format is not None:
+ result["format"] = from_union([lambda x: to_enum(Format, x), from_none], self.format)
+ if self.max_length is not None:
+ result["maxLength"] = from_union([to_float, from_none], self.max_length)
+ if self.min_length is not None:
+ result["minLength"] = from_union([to_float, from_none], self.min_length)
+ if self.maximum is not None:
+ result["maximum"] = from_union([to_float, from_none], self.maximum)
+ if self.minimum is not None:
+ result["minimum"] = from_union([to_float, from_none], self.minimum)
+ return result
+
+
+class RequestedSchemaType(Enum):
+ OBJECT = "object"
+
+
+@dataclass
+class RequestedSchema:
+ """JSON Schema describing the form fields to present to the user"""
+
+ properties: dict[str, Property]
+ """Form field definitions, keyed by field name"""
+
+ type: RequestedSchemaType
+ """Schema type indicator (always 'object')"""
+
+ required: list[str] | None = None
+ """List of required field names"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'RequestedSchema':
+ assert isinstance(obj, dict)
+ properties = from_dict(Property.from_dict, obj.get("properties"))
+ type = RequestedSchemaType(obj.get("type"))
+ required = from_union([lambda x: from_list(from_str, x), from_none], obj.get("required"))
+ return RequestedSchema(properties, type, required)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["properties"] = from_dict(lambda x: to_class(Property, x), self.properties)
+ result["type"] = to_enum(RequestedSchemaType, self.type)
+ if self.required is not None:
+ result["required"] = from_union([lambda x: from_list(from_str, x), from_none], self.required)
+ return result
+
+
+@dataclass
+class SessionUIElicitationParams:
+ message: str
+ """Message describing what information is needed from the user"""
+
+ requested_schema: RequestedSchema
+ """JSON Schema describing the form fields to present to the user"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionUIElicitationParams':
+ assert isinstance(obj, dict)
+ message = from_str(obj.get("message"))
+ requested_schema = RequestedSchema.from_dict(obj.get("requestedSchema"))
+ return SessionUIElicitationParams(message, requested_schema)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["message"] = from_str(self.message)
+ result["requestedSchema"] = to_class(RequestedSchema, self.requested_schema)
+ return result
+
+
+@dataclass
+class SessionPermissionsHandlePendingPermissionRequestResult:
+ success: bool
+ """Whether the permission request was handled successfully"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestResult':
+ assert isinstance(obj, dict)
+ success = from_bool(obj.get("success"))
+ return SessionPermissionsHandlePendingPermissionRequestResult(success)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["success"] = from_bool(self.success)
+ return result
+
+
+class Kind(Enum):
+ APPROVED = "approved"
+ DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy"
+ DENIED_BY_RULES = "denied-by-rules"
+ DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user"
+ DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user"
+
+
+@dataclass
+class SessionPermissionsHandlePendingPermissionRequestParamsResult:
+ kind: Kind
+ rules: list[Any] | None = None
+ feedback: str | None = None
+ message: str | None = None
+ path: str | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParamsResult':
+ assert isinstance(obj, dict)
+ kind = Kind(obj.get("kind"))
+ rules = from_union([lambda x: from_list(lambda x: x, x), from_none], obj.get("rules"))
+ feedback = from_union([from_str, from_none], obj.get("feedback"))
+ message = from_union([from_str, from_none], obj.get("message"))
+ path = from_union([from_str, from_none], obj.get("path"))
+ return SessionPermissionsHandlePendingPermissionRequestParamsResult(kind, rules, feedback, message, path)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["kind"] = to_enum(Kind, self.kind)
+ if self.rules is not None:
+ result["rules"] = from_union([lambda x: from_list(lambda x: x, x), from_none], self.rules)
+ if self.feedback is not None:
+ result["feedback"] = from_union([from_str, from_none], self.feedback)
+ if self.message is not None:
+ result["message"] = from_union([from_str, from_none], self.message)
+ if self.path is not None:
+ result["path"] = from_union([from_str, from_none], self.path)
+ return result
+
+
+@dataclass
+class SessionPermissionsHandlePendingPermissionRequestParams:
+ request_id: str
+ result: SessionPermissionsHandlePendingPermissionRequestParamsResult
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionPermissionsHandlePendingPermissionRequestParams':
+ assert isinstance(obj, dict)
+ request_id = from_str(obj.get("requestId"))
+ result = SessionPermissionsHandlePendingPermissionRequestParamsResult.from_dict(obj.get("result"))
+ return SessionPermissionsHandlePendingPermissionRequestParams(request_id, result)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["requestId"] = from_str(self.request_id)
+ result["result"] = to_class(SessionPermissionsHandlePendingPermissionRequestParamsResult, self.result)
+ return result
+
+
+@dataclass
+class SessionLogResult:
+ event_id: UUID
+ """The unique identifier of the emitted session event"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionLogResult':
+ assert isinstance(obj, dict)
+ event_id = UUID(obj.get("eventId"))
+ return SessionLogResult(event_id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["eventId"] = str(self.event_id)
+ return result
+
+
+class Level(Enum):
+ """Log severity level. Determines how the message is displayed in the timeline. Defaults to
+ "info".
+ """
+ ERROR = "error"
+ INFO = "info"
+ WARNING = "warning"
+
+
+@dataclass
+class SessionLogParams:
+ message: str
+ """Human-readable message"""
+
+ ephemeral: bool | None = None
+ """When true, the message is transient and not persisted to the session event log on disk"""
+
+ level: Level | None = None
+ """Log severity level. Determines how the message is displayed in the timeline. Defaults to
+ "info".
+ """
+ url: str | None = None
+ """Optional URL the user can open in their browser for more details"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionLogParams':
+ assert isinstance(obj, dict)
+ message = from_str(obj.get("message"))
+ ephemeral = from_union([from_bool, from_none], obj.get("ephemeral"))
+ level = from_union([Level, from_none], obj.get("level"))
+ url = from_union([from_str, from_none], obj.get("url"))
+ return SessionLogParams(message, ephemeral, level, url)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["message"] = from_str(self.message)
+ if self.ephemeral is not None:
+ result["ephemeral"] = from_union([from_bool, from_none], self.ephemeral)
+ if self.level is not None:
+ result["level"] = from_union([lambda x: to_enum(Level, x), from_none], self.level)
+ if self.url is not None:
+ result["url"] = from_union([from_str, from_none], self.url)
+ return result
+
+
+@dataclass
+class SessionShellExecResult:
+ process_id: str
+ """Unique identifier for tracking streamed output"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionShellExecResult':
+ assert isinstance(obj, dict)
+ process_id = from_str(obj.get("processId"))
+ return SessionShellExecResult(process_id)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["processId"] = from_str(self.process_id)
+ return result
+
+
+@dataclass
+class SessionShellExecParams:
+ command: str
+ """Shell command to execute"""
+
+ cwd: str | None = None
+ """Working directory (defaults to session working directory)"""
+
+ timeout: float | None = None
+ """Timeout in milliseconds (default: 30000)"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionShellExecParams':
+ assert isinstance(obj, dict)
+ command = from_str(obj.get("command"))
+ cwd = from_union([from_str, from_none], obj.get("cwd"))
+ timeout = from_union([from_float, from_none], obj.get("timeout"))
+ return SessionShellExecParams(command, cwd, timeout)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["command"] = from_str(self.command)
+ if self.cwd is not None:
+ result["cwd"] = from_union([from_str, from_none], self.cwd)
+ if self.timeout is not None:
+ result["timeout"] = from_union([to_float, from_none], self.timeout)
+ return result
+
+
+@dataclass
+class SessionShellKillResult:
+ killed: bool
+ """Whether the signal was sent successfully"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionShellKillResult':
+ assert isinstance(obj, dict)
+ killed = from_bool(obj.get("killed"))
+ return SessionShellKillResult(killed)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["killed"] = from_bool(self.killed)
+ return result
+
+
+class Signal(Enum):
+ """Signal to send (default: SIGTERM)"""
+
+ SIGINT = "SIGINT"
+ SIGKILL = "SIGKILL"
+ SIGTERM = "SIGTERM"
+
+
+@dataclass
+class SessionShellKillParams:
+ process_id: str
+ """Process identifier returned by shell.exec"""
+
+ signal: Signal | None = None
+ """Signal to send (default: SIGTERM)"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'SessionShellKillParams':
+ assert isinstance(obj, dict)
+ process_id = from_str(obj.get("processId"))
+ signal = from_union([Signal, from_none], obj.get("signal"))
+ return SessionShellKillParams(process_id, signal)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["processId"] = from_str(self.process_id)
+ if self.signal is not None:
+ result["signal"] = from_union([lambda x: to_enum(Signal, x), from_none], self.signal)
+ return result
+
+
+def ping_result_from_dict(s: Any) -> PingResult:
+ return PingResult.from_dict(s)
+
+
def ping_result_to_dict(x: PingResult) -> Any:
return to_class(PingResult, x)
@@ -1477,6 +2277,166 @@ def session_agent_deselect_result_to_dict(x: SessionAgentDeselectResult) -> Any:
return to_class(SessionAgentDeselectResult, x)
+def session_agent_reload_result_from_dict(s: Any) -> SessionAgentReloadResult:
+ return SessionAgentReloadResult.from_dict(s)
+
+
+def session_agent_reload_result_to_dict(x: SessionAgentReloadResult) -> Any:
+ return to_class(SessionAgentReloadResult, x)
+
+
+def session_skills_list_result_from_dict(s: Any) -> SessionSkillsListResult:
+ return SessionSkillsListResult.from_dict(s)
+
+
+def session_skills_list_result_to_dict(x: SessionSkillsListResult) -> Any:
+ return to_class(SessionSkillsListResult, x)
+
+
+def session_skills_enable_result_from_dict(s: Any) -> SessionSkillsEnableResult:
+ return SessionSkillsEnableResult.from_dict(s)
+
+
+def session_skills_enable_result_to_dict(x: SessionSkillsEnableResult) -> Any:
+ return to_class(SessionSkillsEnableResult, x)
+
+
+def session_skills_enable_params_from_dict(s: Any) -> SessionSkillsEnableParams:
+ return SessionSkillsEnableParams.from_dict(s)
+
+
+def session_skills_enable_params_to_dict(x: SessionSkillsEnableParams) -> Any:
+ return to_class(SessionSkillsEnableParams, x)
+
+
+def session_skills_disable_result_from_dict(s: Any) -> SessionSkillsDisableResult:
+ return SessionSkillsDisableResult.from_dict(s)
+
+
+def session_skills_disable_result_to_dict(x: SessionSkillsDisableResult) -> Any:
+ return to_class(SessionSkillsDisableResult, x)
+
+
+def session_skills_disable_params_from_dict(s: Any) -> SessionSkillsDisableParams:
+ return SessionSkillsDisableParams.from_dict(s)
+
+
+def session_skills_disable_params_to_dict(x: SessionSkillsDisableParams) -> Any:
+ return to_class(SessionSkillsDisableParams, x)
+
+
+def session_skills_reload_result_from_dict(s: Any) -> SessionSkillsReloadResult:
+ return SessionSkillsReloadResult.from_dict(s)
+
+
+def session_skills_reload_result_to_dict(x: SessionSkillsReloadResult) -> Any:
+ return to_class(SessionSkillsReloadResult, x)
+
+
+def session_mcp_list_result_from_dict(s: Any) -> SessionMCPListResult:
+ return SessionMCPListResult.from_dict(s)
+
+
+def session_mcp_list_result_to_dict(x: SessionMCPListResult) -> Any:
+ return to_class(SessionMCPListResult, x)
+
+
+def session_mcp_enable_result_from_dict(s: Any) -> SessionMCPEnableResult:
+ return SessionMCPEnableResult.from_dict(s)
+
+
+def session_mcp_enable_result_to_dict(x: SessionMCPEnableResult) -> Any:
+ return to_class(SessionMCPEnableResult, x)
+
+
+def session_mcp_enable_params_from_dict(s: Any) -> SessionMCPEnableParams:
+ return SessionMCPEnableParams.from_dict(s)
+
+
+def session_mcp_enable_params_to_dict(x: SessionMCPEnableParams) -> Any:
+ return to_class(SessionMCPEnableParams, x)
+
+
+def session_mcp_disable_result_from_dict(s: Any) -> SessionMCPDisableResult:
+ return SessionMCPDisableResult.from_dict(s)
+
+
+def session_mcp_disable_result_to_dict(x: SessionMCPDisableResult) -> Any:
+ return to_class(SessionMCPDisableResult, x)
+
+
+def session_mcp_disable_params_from_dict(s: Any) -> SessionMCPDisableParams:
+ return SessionMCPDisableParams.from_dict(s)
+
+
+def session_mcp_disable_params_to_dict(x: SessionMCPDisableParams) -> Any:
+ return to_class(SessionMCPDisableParams, x)
+
+
+def session_mcp_reload_result_from_dict(s: Any) -> SessionMCPReloadResult:
+ return SessionMCPReloadResult.from_dict(s)
+
+
+def session_mcp_reload_result_to_dict(x: SessionMCPReloadResult) -> Any:
+ return to_class(SessionMCPReloadResult, x)
+
+
+def session_plugins_list_result_from_dict(s: Any) -> SessionPluginsListResult:
+ return SessionPluginsListResult.from_dict(s)
+
+
+def session_plugins_list_result_to_dict(x: SessionPluginsListResult) -> Any:
+ return to_class(SessionPluginsListResult, x)
+
+
+def session_extensions_list_result_from_dict(s: Any) -> SessionExtensionsListResult:
+ return SessionExtensionsListResult.from_dict(s)
+
+
+def session_extensions_list_result_to_dict(x: SessionExtensionsListResult) -> Any:
+ return to_class(SessionExtensionsListResult, x)
+
+
+def session_extensions_enable_result_from_dict(s: Any) -> SessionExtensionsEnableResult:
+ return SessionExtensionsEnableResult.from_dict(s)
+
+
+def session_extensions_enable_result_to_dict(x: SessionExtensionsEnableResult) -> Any:
+ return to_class(SessionExtensionsEnableResult, x)
+
+
+def session_extensions_enable_params_from_dict(s: Any) -> SessionExtensionsEnableParams:
+ return SessionExtensionsEnableParams.from_dict(s)
+
+
+def session_extensions_enable_params_to_dict(x: SessionExtensionsEnableParams) -> Any:
+ return to_class(SessionExtensionsEnableParams, x)
+
+
+def session_extensions_disable_result_from_dict(s: Any) -> SessionExtensionsDisableResult:
+ return SessionExtensionsDisableResult.from_dict(s)
+
+
+def session_extensions_disable_result_to_dict(x: SessionExtensionsDisableResult) -> Any:
+ return to_class(SessionExtensionsDisableResult, x)
+
+
+def session_extensions_disable_params_from_dict(s: Any) -> SessionExtensionsDisableParams:
+ return SessionExtensionsDisableParams.from_dict(s)
+
+
+def session_extensions_disable_params_to_dict(x: SessionExtensionsDisableParams) -> Any:
+ return to_class(SessionExtensionsDisableParams, x)
+
+
+def session_extensions_reload_result_from_dict(s: Any) -> SessionExtensionsReloadResult:
+ return SessionExtensionsReloadResult.from_dict(s)
+
+
+def session_extensions_reload_result_to_dict(x: SessionExtensionsReloadResult) -> Any:
+ return to_class(SessionExtensionsReloadResult, x)
+
+
def session_compaction_compact_result_from_dict(s: Any) -> SessionCompactionCompactResult:
return SessionCompactionCompactResult.from_dict(s)
@@ -1501,6 +2461,38 @@ def session_tools_handle_pending_tool_call_params_to_dict(x: SessionToolsHandleP
return to_class(SessionToolsHandlePendingToolCallParams, x)
+def session_commands_handle_pending_command_result_from_dict(s: Any) -> SessionCommandsHandlePendingCommandResult:
+ return SessionCommandsHandlePendingCommandResult.from_dict(s)
+
+
+def session_commands_handle_pending_command_result_to_dict(x: SessionCommandsHandlePendingCommandResult) -> Any:
+ return to_class(SessionCommandsHandlePendingCommandResult, x)
+
+
+def session_commands_handle_pending_command_params_from_dict(s: Any) -> SessionCommandsHandlePendingCommandParams:
+ return SessionCommandsHandlePendingCommandParams.from_dict(s)
+
+
+def session_commands_handle_pending_command_params_to_dict(x: SessionCommandsHandlePendingCommandParams) -> Any:
+ return to_class(SessionCommandsHandlePendingCommandParams, x)
+
+
+def session_ui_elicitation_result_from_dict(s: Any) -> SessionUIElicitationResult:
+ return SessionUIElicitationResult.from_dict(s)
+
+
+def session_ui_elicitation_result_to_dict(x: SessionUIElicitationResult) -> Any:
+ return to_class(SessionUIElicitationResult, x)
+
+
+def session_ui_elicitation_params_from_dict(s: Any) -> SessionUIElicitationParams:
+ return SessionUIElicitationParams.from_dict(s)
+
+
+def session_ui_elicitation_params_to_dict(x: SessionUIElicitationParams) -> Any:
+ return to_class(SessionUIElicitationParams, x)
+
+
def session_permissions_handle_pending_permission_request_result_from_dict(s: Any) -> SessionPermissionsHandlePendingPermissionRequestResult:
return SessionPermissionsHandlePendingPermissionRequestResult.from_dict(s)
@@ -1706,6 +2698,88 @@ async def select(self, params: SessionAgentSelectParams, *, timeout: float | Non
async def deselect(self, *, timeout: float | None = None) -> SessionAgentDeselectResult:
return SessionAgentDeselectResult.from_dict(await self._client.request("session.agent.deselect", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+ async def reload(self, *, timeout: float | None = None) -> SessionAgentReloadResult:
+ return SessionAgentReloadResult.from_dict(await self._client.request("session.agent.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+
+# Experimental: this API group is experimental and may change or be removed.
+class SkillsApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def list(self, *, timeout: float | None = None) -> SessionSkillsListResult:
+ return SessionSkillsListResult.from_dict(await self._client.request("session.skills.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+ async def enable(self, params: SessionSkillsEnableParams, *, timeout: float | None = None) -> SessionSkillsEnableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionSkillsEnableResult.from_dict(await self._client.request("session.skills.enable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def disable(self, params: SessionSkillsDisableParams, *, timeout: float | None = None) -> SessionSkillsDisableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionSkillsDisableResult.from_dict(await self._client.request("session.skills.disable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def reload(self, *, timeout: float | None = None) -> SessionSkillsReloadResult:
+ return SessionSkillsReloadResult.from_dict(await self._client.request("session.skills.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+
+# Experimental: this API group is experimental and may change or be removed.
+class McpApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def list(self, *, timeout: float | None = None) -> SessionMCPListResult:
+ return SessionMCPListResult.from_dict(await self._client.request("session.mcp.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+ async def enable(self, params: SessionMCPEnableParams, *, timeout: float | None = None) -> SessionMCPEnableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionMCPEnableResult.from_dict(await self._client.request("session.mcp.enable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def disable(self, params: SessionMCPDisableParams, *, timeout: float | None = None) -> SessionMCPDisableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionMCPDisableResult.from_dict(await self._client.request("session.mcp.disable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def reload(self, *, timeout: float | None = None) -> SessionMCPReloadResult:
+ return SessionMCPReloadResult.from_dict(await self._client.request("session.mcp.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+
+# Experimental: this API group is experimental and may change or be removed.
+class PluginsApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def list(self, *, timeout: float | None = None) -> SessionPluginsListResult:
+ return SessionPluginsListResult.from_dict(await self._client.request("session.plugins.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+
+# Experimental: this API group is experimental and may change or be removed.
+class ExtensionsApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def list(self, *, timeout: float | None = None) -> SessionExtensionsListResult:
+ return SessionExtensionsListResult.from_dict(await self._client.request("session.extensions.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
+ async def enable(self, params: SessionExtensionsEnableParams, *, timeout: float | None = None) -> SessionExtensionsEnableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionExtensionsEnableResult.from_dict(await self._client.request("session.extensions.enable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def disable(self, params: SessionExtensionsDisableParams, *, timeout: float | None = None) -> SessionExtensionsDisableResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionExtensionsDisableResult.from_dict(await self._client.request("session.extensions.disable", params_dict, **_timeout_kwargs(timeout)))
+
+ async def reload(self, *, timeout: float | None = None) -> SessionExtensionsReloadResult:
+ return SessionExtensionsReloadResult.from_dict(await self._client.request("session.extensions.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)))
+
# Experimental: this API group is experimental and may change or be removed.
class CompactionApi:
@@ -1728,6 +2802,28 @@ async def handle_pending_tool_call(self, params: SessionToolsHandlePendingToolCa
return SessionToolsHandlePendingToolCallResult.from_dict(await self._client.request("session.tools.handlePendingToolCall", params_dict, **_timeout_kwargs(timeout)))
+class CommandsApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def handle_pending_command(self, params: SessionCommandsHandlePendingCommandParams, *, timeout: float | None = None) -> SessionCommandsHandlePendingCommandResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionCommandsHandlePendingCommandResult.from_dict(await self._client.request("session.commands.handlePendingCommand", params_dict, **_timeout_kwargs(timeout)))
+
+
+class UiApi:
+ def __init__(self, client: "JsonRpcClient", session_id: str):
+ self._client = client
+ self._session_id = session_id
+
+ async def elicitation(self, params: SessionUIElicitationParams, *, timeout: float | None = None) -> SessionUIElicitationResult:
+ params_dict = {k: v for k, v in params.to_dict().items() if v is not None}
+ params_dict["sessionId"] = self._session_id
+ return SessionUIElicitationResult.from_dict(await self._client.request("session.ui.elicitation", params_dict, **_timeout_kwargs(timeout)))
+
+
class PermissionsApi:
def __init__(self, client: "JsonRpcClient", session_id: str):
self._client = client
@@ -1766,8 +2862,14 @@ def __init__(self, client: "JsonRpcClient", session_id: str):
self.workspace = WorkspaceApi(client, session_id)
self.fleet = FleetApi(client, session_id)
self.agent = AgentApi(client, session_id)
+ self.skills = SkillsApi(client, session_id)
+ self.mcp = McpApi(client, session_id)
+ self.plugins = PluginsApi(client, session_id)
+ self.extensions = ExtensionsApi(client, session_id)
self.compaction = CompactionApi(client, session_id)
self.tools = ToolsApi(client, session_id)
+ self.commands = CommandsApi(client, session_id)
+ self.ui = UiApi(client, session_id)
self.permissions = PermissionsApi(client, session_id)
self.shell = ShellApi(client, session_id)
diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py
index 3fc313399..f3970b815 100644
--- a/python/copilot/generated/session_events.py
+++ b/python/copilot/generated/session_events.py
@@ -419,6 +419,26 @@ def to_dict(self) -> dict:
return result
+@dataclass
+class DataCommand:
+ name: str
+ description: str | None = None
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'DataCommand':
+ assert isinstance(obj, dict)
+ name = from_str(obj.get("name"))
+ description = from_union([from_str, from_none], obj.get("description"))
+ return DataCommand(name, description)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["name"] = from_str(self.name)
+ if self.description is not None:
+ result["description"] = from_union([from_str, from_none], self.description)
+ return result
+
+
@dataclass
class CompactionTokensUsed:
"""Token usage breakdown for the compaction LLM call"""
@@ -605,7 +625,55 @@ def to_dict(self) -> dict:
return result
-class Status(Enum):
+class Source(Enum):
+ """Discovery source"""
+
+ PROJECT = "project"
+ USER = "user"
+
+
+class ExtensionStatus(Enum):
+ """Current status: running, disabled, failed, or starting"""
+
+ DISABLED = "disabled"
+ FAILED = "failed"
+ RUNNING = "running"
+ STARTING = "starting"
+
+
+@dataclass
+class Extension:
+ id: str
+ """Source-qualified extension ID (e.g., 'project:my-ext', 'user:auth-helper')"""
+
+ name: str
+ """Extension name (directory name)"""
+
+ source: Source
+ """Discovery source"""
+
+ status: ExtensionStatus
+ """Current status: running, disabled, failed, or starting"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Extension':
+ assert isinstance(obj, dict)
+ id = from_str(obj.get("id"))
+ name = from_str(obj.get("name"))
+ source = Source(obj.get("source"))
+ status = ExtensionStatus(obj.get("status"))
+ return Extension(id, name, source, status)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["id"] = from_str(self.id)
+ result["name"] = from_str(self.name)
+ result["source"] = to_enum(Source, self.source)
+ result["status"] = to_enum(ExtensionStatus, self.status)
+ return result
+
+
+class KindStatus(Enum):
"""Whether the agent completed successfully or failed"""
COMPLETED = "completed"
@@ -614,6 +682,7 @@ class Status(Enum):
class KindType(Enum):
AGENT_COMPLETED = "agent_completed"
+ AGENT_IDLE = "agent_idle"
SHELL_COMPLETED = "shell_completed"
SHELL_DETACHED_COMPLETED = "shell_detached_completed"
@@ -637,7 +706,7 @@ class KindClass:
prompt: str | None = None
"""The full prompt given to the background agent"""
- status: Status | None = None
+ status: KindStatus | None = None
"""Whether the agent completed successfully or failed"""
exit_code: float | None = None
@@ -657,7 +726,7 @@ def from_dict(obj: Any) -> 'KindClass':
agent_type = from_union([from_str, from_none], obj.get("agentType"))
description = from_union([from_str, from_none], obj.get("description"))
prompt = from_union([from_str, from_none], obj.get("prompt"))
- status = from_union([Status, from_none], obj.get("status"))
+ status = from_union([KindStatus, from_none], obj.get("status"))
exit_code = from_union([from_float, from_none], obj.get("exitCode"))
shell_id = from_union([from_str, from_none], obj.get("shellId"))
return KindClass(type, agent_id, agent_type, description, prompt, status, exit_code, shell_id)
@@ -674,7 +743,7 @@ def to_dict(self) -> dict:
if self.prompt is not None:
result["prompt"] = from_union([from_str, from_none], self.prompt)
if self.status is not None:
- result["status"] = from_union([lambda x: to_enum(Status, x), from_none], self.status)
+ result["status"] = from_union([lambda x: to_enum(KindStatus, x), from_none], self.status)
if self.exit_code is not None:
result["exitCode"] = from_union([to_float, from_none], self.exit_code)
if self.shell_id is not None:
@@ -709,7 +778,11 @@ def to_dict(self) -> dict:
class Mode(Enum):
+ """Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to
+ "form" when absent.
+ """
FORM = "form"
+ URL = "url"
@dataclass
@@ -803,7 +876,7 @@ class Operation(Enum):
@dataclass
-class Command:
+class PermissionRequestCommand:
identifier: str
"""Command identifier (e.g., executable name)"""
@@ -811,11 +884,11 @@ class Command:
"""Whether this command is read-only (no side effects)"""
@staticmethod
- def from_dict(obj: Any) -> 'Command':
+ def from_dict(obj: Any) -> 'PermissionRequestCommand':
assert isinstance(obj, dict)
identifier = from_str(obj.get("identifier"))
read_only = from_bool(obj.get("readOnly"))
- return Command(identifier, read_only)
+ return PermissionRequestCommand(identifier, read_only)
def to_dict(self) -> dict:
result: dict = {}
@@ -878,7 +951,7 @@ class PermissionRequest:
can_offer_session_approval: bool | None = None
"""Whether the UI can offer session-wide approval for this command pattern"""
- commands: list[Command] | None = None
+ commands: list[PermissionRequestCommand] | None = None
"""Parsed command identifiers found in the command text"""
full_command_text: str | None = None
@@ -967,7 +1040,7 @@ def from_dict(obj: Any) -> 'PermissionRequest':
assert isinstance(obj, dict)
kind = PermissionRequestKind(obj.get("kind"))
can_offer_session_approval = from_union([from_bool, from_none], obj.get("canOfferSessionApproval"))
- commands = from_union([lambda x: from_list(Command.from_dict, x), from_none], obj.get("commands"))
+ commands = from_union([lambda x: from_list(PermissionRequestCommand.from_dict, x), from_none], obj.get("commands"))
full_command_text = from_union([from_str, from_none], obj.get("fullCommandText"))
has_write_file_redirection = from_union([from_bool, from_none], obj.get("hasWriteFileRedirection"))
intention = from_union([from_str, from_none], obj.get("intention"))
@@ -999,7 +1072,7 @@ def to_dict(self) -> dict:
if self.can_offer_session_approval is not None:
result["canOfferSessionApproval"] = from_union([from_bool, from_none], self.can_offer_session_approval)
if self.commands is not None:
- result["commands"] = from_union([lambda x: from_list(lambda x: to_class(Command, x), x), from_none], self.commands)
+ result["commands"] = from_union([lambda x: from_list(lambda x: to_class(PermissionRequestCommand, x), x), from_none], self.commands)
if self.full_command_text is not None:
result["fullCommandText"] = from_union([from_str, from_none], self.full_command_text)
if self.has_write_file_redirection is not None:
@@ -1138,7 +1211,7 @@ class RequestedSchemaType(Enum):
@dataclass
class RequestedSchema:
- """JSON Schema describing the form fields to present to the user"""
+ """JSON Schema describing the form fields to present to the user (form mode only)"""
properties: dict[str, Any]
"""Form field definitions, keyed by field name"""
@@ -1430,6 +1503,52 @@ class Role(Enum):
SYSTEM = "system"
+class ServerStatus(Enum):
+ """Connection status: connected, failed, pending, disabled, or not_configured
+
+ New connection status: connected, failed, pending, disabled, or not_configured
+ """
+ CONNECTED = "connected"
+ DISABLED = "disabled"
+ FAILED = "failed"
+ NOT_CONFIGURED = "not_configured"
+ PENDING = "pending"
+
+
+@dataclass
+class Server:
+ name: str
+ """Server name (config key)"""
+
+ status: ServerStatus
+ """Connection status: connected, failed, pending, disabled, or not_configured"""
+
+ error: str | None = None
+ """Error message if the server failed to connect"""
+
+ source: str | None = None
+ """Configuration source: user, workspace, plugin, or builtin"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Server':
+ assert isinstance(obj, dict)
+ name = from_str(obj.get("name"))
+ status = ServerStatus(obj.get("status"))
+ error = from_union([from_str, from_none], obj.get("error"))
+ source = from_union([from_str, from_none], obj.get("source"))
+ return Server(name, status, error, source)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["name"] = from_str(self.name)
+ result["status"] = to_enum(ServerStatus, self.status)
+ if self.error is not None:
+ result["error"] = from_union([from_str, from_none], self.error)
+ if self.source is not None:
+ result["source"] = from_union([from_str, from_none], self.source)
+ return result
+
+
class ShutdownType(Enum):
"""Whether the session ended normally ("routine") or due to a crash/fatal error ("error")"""
@@ -1437,20 +1556,47 @@ class ShutdownType(Enum):
ROUTINE = "routine"
-class Source(Enum):
- """Origin of this message, used for timeline filtering and telemetry (e.g., "user",
- "autopilot", "skill", or "command")
- """
- AUTOPILOT = "autopilot"
- COMMAND = "command"
- IMMEDIATE_PROMPT = "immediate-prompt"
- JIT_INSTRUCTION = "jit-instruction"
- OTHER = "other"
- SKILL = "skill"
- SNIPPY_BLOCKING = "snippy-blocking"
- SYSTEM = "system"
- THINKING_EXHAUSTED_CONTINUATION = "thinking-exhausted-continuation"
- USER = "user"
+@dataclass
+class Skill:
+ description: str
+ """Description of what the skill does"""
+
+ enabled: bool
+ """Whether the skill is currently enabled"""
+
+ name: str
+ """Unique identifier for the skill"""
+
+ source: str
+ """Source location type of the skill (e.g., project, personal, plugin)"""
+
+ user_invocable: bool
+ """Whether the skill can be invoked by the user as a slash command"""
+
+ path: str | None = None
+ """Absolute path to the skill file, if available"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'Skill':
+ assert isinstance(obj, dict)
+ description = from_str(obj.get("description"))
+ enabled = from_bool(obj.get("enabled"))
+ name = from_str(obj.get("name"))
+ source = from_str(obj.get("source"))
+ user_invocable = from_bool(obj.get("userInvocable"))
+ path = from_union([from_str, from_none], obj.get("path"))
+ return Skill(description, enabled, name, source, user_invocable, path)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["description"] = from_str(self.description)
+ result["enabled"] = from_bool(self.enabled)
+ result["name"] = from_str(self.name)
+ result["source"] = from_str(self.source)
+ result["userInvocable"] = from_bool(self.user_invocable)
+ if self.path is not None:
+ result["path"] = from_union([from_str, from_none], self.path)
+ return result
class SourceType(Enum):
@@ -1460,6 +1606,31 @@ class SourceType(Enum):
REMOTE = "remote"
+@dataclass
+class StaticClientConfig:
+ """Static OAuth client configuration, if the server specifies one"""
+
+ client_id: str
+ """OAuth client ID for the server"""
+
+ public_client: bool | None = None
+ """Whether this is a public OAuth client"""
+
+ @staticmethod
+ def from_dict(obj: Any) -> 'StaticClientConfig':
+ assert isinstance(obj, dict)
+ client_id = from_str(obj.get("clientId"))
+ public_client = from_union([from_bool, from_none], obj.get("publicClient"))
+ return StaticClientConfig(client_id, public_client)
+
+ def to_dict(self) -> dict:
+ result: dict = {}
+ result["clientId"] = from_str(self.client_id)
+ if self.public_client is not None:
+ result["publicClient"] = from_union([from_bool, from_none], self.public_client)
+ return result
+
+
class ToolRequestType(Enum):
"""Tool call type: "function" for standard tool calls, "custom" for grammar-based tool
calls. Defaults to "function" when absent.
@@ -1555,15 +1726,12 @@ class Data:
Current context window usage statistics including token and message counts
- Empty payload; the event signals that LLM-powered conversation compaction has begun
+ Context window breakdown at the start of LLM-powered conversation compaction
Conversation compaction results including success status, metrics, and optional error
details
- Task completion notification with optional summary from the agent
-
- User message content with optional attachments, source information, and interaction
- metadata
+ Task completion notification with summary from the agent
Empty payload; the event signals that the pending message queue has changed
@@ -1629,18 +1797,27 @@ class Data:
User input request completion notification signaling UI dismissal
- Structured form elicitation request with JSON schema definition for form fields
+ Elicitation request; may be form-based (structured input) or URL-based (browser
+ redirect)
Elicitation request completion notification signaling UI dismissal
+ OAuth authentication request for an MCP server
+
+ MCP OAuth request completion notification
+
External tool invocation request for client-side tool execution
External tool completion notification signaling UI dismissal
Queued slash command dispatch request for client execution
+ Registered command dispatch request routed to the owning client
+
Queued command completion notification signaling UI dismissal
+ SDK command registration change notification
+
Plan approval request with plan content and available user actions
Plan mode exit completion notification signaling UI dismissal
@@ -1716,6 +1893,15 @@ class Data:
status_code: int | None = None
"""HTTP status code from the upstream request, if applicable"""
+ url: str | None = None
+ """Optional URL associated with this error that the user can open in a browser
+
+ Optional URL associated with this message that the user can open in a browser
+
+ Optional URL associated with this warning that the user can open in a browser
+
+ URL to open in the user's browser (url mode only)
+ """
background_tasks: BackgroundTasks | None = None
"""Background tasks still running when the agent became idle"""
@@ -1772,7 +1958,7 @@ class Data:
summary: str | None = None
"""Summary of the work done in the source session
- Optional summary of the completed task, provided by the agent
+ Summary of the completed task, provided by the agent
Summary of the plan that was created
"""
@@ -1809,9 +1995,23 @@ class Data:
code_changes: CodeChanges | None = None
"""Aggregate code change metrics for the session"""
+ conversation_tokens: float | None = None
+ """Non-system message token count at shutdown
+
+ Token count from non-system messages (user, assistant, tool)
+
+ Token count from non-system messages (user, assistant, tool) at compaction start
+
+ Token count from non-system messages (user, assistant, tool) after compaction
+ """
current_model: str | None = None
"""Model that was selected at the time of shutdown"""
+ current_tokens: float | None = None
+ """Total tokens in context window at shutdown
+
+ Current number of tokens in the context window
+ """
error_reason: str | None = None
"""Error description when shutdownType is "error\""""
@@ -1824,6 +2024,24 @@ class Data:
shutdown_type: ShutdownType | None = None
"""Whether the session ended normally ("routine") or due to a crash/fatal error ("error")"""
+ system_tokens: float | None = None
+ """System message token count at shutdown
+
+ Token count from system message(s)
+
+ Token count from system message(s) at compaction start
+
+ Token count from system message(s) after compaction
+ """
+ tool_definitions_tokens: float | None = None
+ """Tool definitions token count at shutdown
+
+ Token count from tool definitions
+
+ Token count from tool definitions at compaction start
+
+ Token count from tool definitions after compaction
+ """
total_api_duration_ms: float | None = None
"""Cumulative time spent in API calls during the session, in milliseconds"""
@@ -1848,8 +2066,8 @@ class Data:
host_type: HostType | None = None
"""Hosting platform type of the repository (github or ado)"""
- current_tokens: float | None = None
- """Current number of tokens in the context window"""
+ is_initial: bool | None = None
+ """Whether this is the first usage_info event emitted in this session"""
messages_length: float | None = None
"""Current number of messages in the conversation"""
@@ -1905,6 +2123,11 @@ class Data:
Request ID of the resolved elicitation request; clients should dismiss any UI for this
request
+ Unique identifier for this OAuth request; used to respond via
+ session.respondToMcpOAuth()
+
+ Request ID of the resolved OAuth request
+
Unique identifier for this request; used to respond via session.respondToExternalTool()
Request ID of the resolved external tool request; clients should dismiss any UI for this
@@ -1912,6 +2135,8 @@ class Data:
Unique identifier for this request; used to respond via session.respondToQueuedCommand()
+ Unique identifier; used to respond via session.commands.handlePendingCommand()
+
Request ID of the resolved command request; clients should dismiss any UI for this
request
@@ -1923,6 +2148,8 @@ class Data:
success: bool | None = None
"""Whether compaction completed successfully
+ Whether the tool call succeeded. False when validation failed (e.g., invalid arguments)
+
Whether the tool execution completed successfully
Whether the hook completed successfully
@@ -1961,9 +2188,9 @@ class Data:
CAPI interaction ID for correlating this tool execution with upstream telemetry
"""
- source: Source | None = None
- """Origin of this message, used for timeline filtering and telemetry (e.g., "user",
- "autopilot", "skill", or "command")
+ source: str | None = None
+ """Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected
+ messages that should be hidden from the user)
"""
transformed_content: str | None = None
"""Transformed version of the message sent to the model, with XML wrapping, timestamps, and
@@ -2077,6 +2304,12 @@ class Data:
Tool call ID of the parent tool invocation that spawned this sub-agent
+ The LLM-assigned tool call ID that triggered this request; used by remote UIs to
+ correlate responses
+
+ Tool call ID from the LLM completion; used to correlate with CompletionChunk.toolCall.id
+ for remote UIs
+
Tool call ID assigned to this external tool invocation
"""
tool_name: str | None = None
@@ -2176,11 +2409,26 @@ class Data:
question: str | None = None
"""The question or prompt to present to the user"""
- mode: Mode | None = None
- """Elicitation mode; currently only "form" is supported. Defaults to "form" when absent."""
+ elicitation_source: str | None = None
+ """The source that initiated the request (MCP server name, or absent for agent-initiated)"""
+ mode: Mode | None = None
+ """Elicitation mode; "form" for structured input, "url" for browser-based. Defaults to
+ "form" when absent.
+ """
requested_schema: RequestedSchema | None = None
- """JSON Schema describing the form fields to present to the user"""
+ """JSON Schema describing the form fields to present to the user (form mode only)"""
+
+ server_name: str | None = None
+ """Display name of the MCP server that requires OAuth
+
+ Name of the MCP server whose status changed
+ """
+ server_url: str | None = None
+ """URL of the MCP server that requires OAuth"""
+
+ static_client_config: StaticClientConfig | None = None
+ """Static OAuth client configuration, if the server specifies one"""
traceparent: str | None = None
"""W3C Trace Context traceparent header for the execute_tool span"""
@@ -2189,7 +2437,18 @@ class Data:
"""W3C Trace Context tracestate header for the execute_tool span"""
command: str | None = None
- """The slash command text to be executed (e.g., /help, /clear)"""
+ """The slash command text to be executed (e.g., /help, /clear)
+
+ The full command text (e.g., /deploy production)
+ """
+ args: str | None = None
+ """Raw argument string after the command name"""
+
+ command_name: str | None = None
+ """Command name without leading /"""
+
+ commands: list[DataCommand] | None = None
+ """Current list of registered SDK commands"""
actions: list[str] | None = None
"""Available actions the user can take (e.g., approve, edit, reject)"""
@@ -2200,6 +2459,18 @@ class Data:
recommended_action: str | None = None
"""The recommended action for the user to take"""
+ skills: list[Skill] | None = None
+ """Array of resolved skill metadata"""
+
+ servers: list[Server] | None = None
+ """Array of MCP server status summaries"""
+
+ status: ServerStatus | None = None
+ """New connection status: connected, failed, pending, disabled, or not_configured"""
+
+ extensions: list[Extension] | None = None
+ """Array of discovered extensions and their status"""
+
@staticmethod
def from_dict(obj: Any) -> 'Data':
assert isinstance(obj, dict)
@@ -2219,6 +2490,7 @@ def from_dict(obj: Any) -> 'Data':
provider_call_id = from_union([from_str, from_none], obj.get("providerCallId"))
stack = from_union([from_str, from_none], obj.get("stack"))
status_code = from_union([from_int, from_none], obj.get("statusCode"))
+ url = from_union([from_str, from_none], obj.get("url"))
background_tasks = from_union([BackgroundTasks.from_dict, from_none], obj.get("backgroundTasks"))
title = from_union([from_str, from_none], obj.get("title"))
info_type = from_union([from_str, from_none], obj.get("infoType"))
@@ -2246,11 +2518,15 @@ def from_dict(obj: Any) -> 'Data':
events_removed = from_union([from_float, from_none], obj.get("eventsRemoved"))
up_to_event_id = from_union([from_str, from_none], obj.get("upToEventId"))
code_changes = from_union([CodeChanges.from_dict, from_none], obj.get("codeChanges"))
+ conversation_tokens = from_union([from_float, from_none], obj.get("conversationTokens"))
current_model = from_union([from_str, from_none], obj.get("currentModel"))
+ current_tokens = from_union([from_float, from_none], obj.get("currentTokens"))
error_reason = from_union([from_str, from_none], obj.get("errorReason"))
model_metrics = from_union([lambda x: from_dict(ModelMetric.from_dict, x), from_none], obj.get("modelMetrics"))
session_start_time = from_union([from_float, from_none], obj.get("sessionStartTime"))
shutdown_type = from_union([ShutdownType, from_none], obj.get("shutdownType"))
+ system_tokens = from_union([from_float, from_none], obj.get("systemTokens"))
+ tool_definitions_tokens = from_union([from_float, from_none], obj.get("toolDefinitionsTokens"))
total_api_duration_ms = from_union([from_float, from_none], obj.get("totalApiDurationMs"))
total_premium_requests = from_union([from_float, from_none], obj.get("totalPremiumRequests"))
base_commit = from_union([from_str, from_none], obj.get("baseCommit"))
@@ -2259,7 +2535,7 @@ def from_dict(obj: Any) -> 'Data':
git_root = from_union([from_str, from_none], obj.get("gitRoot"))
head_commit = from_union([from_str, from_none], obj.get("headCommit"))
host_type = from_union([HostType, from_none], obj.get("hostType"))
- current_tokens = from_union([from_float, from_none], obj.get("currentTokens"))
+ is_initial = from_union([from_bool, from_none], obj.get("isInitial"))
messages_length = from_union([from_float, from_none], obj.get("messagesLength"))
checkpoint_number = from_union([from_float, from_none], obj.get("checkpointNumber"))
checkpoint_path = from_union([from_str, from_none], obj.get("checkpointPath"))
@@ -2277,7 +2553,7 @@ def from_dict(obj: Any) -> 'Data':
attachments = from_union([lambda x: from_list(Attachment.from_dict, x), from_none], obj.get("attachments"))
content = from_union([from_str, from_none], obj.get("content"))
interaction_id = from_union([from_str, from_none], obj.get("interactionId"))
- source = from_union([Source, from_none], obj.get("source"))
+ source = from_union([from_str, from_none], obj.get("source"))
transformed_content = from_union([from_str, from_none], obj.get("transformedContent"))
turn_id = from_union([from_str, from_none], obj.get("turnId"))
intent = from_union([from_str, from_none], obj.get("intent"))
@@ -2332,15 +2608,26 @@ def from_dict(obj: Any) -> 'Data':
allow_freeform = from_union([from_bool, from_none], obj.get("allowFreeform"))
choices = from_union([lambda x: from_list(from_str, x), from_none], obj.get("choices"))
question = from_union([from_str, from_none], obj.get("question"))
+ elicitation_source = from_union([from_str, from_none], obj.get("elicitationSource"))
mode = from_union([Mode, from_none], obj.get("mode"))
requested_schema = from_union([RequestedSchema.from_dict, from_none], obj.get("requestedSchema"))
+ server_name = from_union([from_str, from_none], obj.get("serverName"))
+ server_url = from_union([from_str, from_none], obj.get("serverUrl"))
+ static_client_config = from_union([StaticClientConfig.from_dict, from_none], obj.get("staticClientConfig"))
traceparent = from_union([from_str, from_none], obj.get("traceparent"))
tracestate = from_union([from_str, from_none], obj.get("tracestate"))
command = from_union([from_str, from_none], obj.get("command"))
+ args = from_union([from_str, from_none], obj.get("args"))
+ command_name = from_union([from_str, from_none], obj.get("commandName"))
+ commands = from_union([lambda x: from_list(DataCommand.from_dict, x), from_none], obj.get("commands"))
actions = from_union([lambda x: from_list(from_str, x), from_none], obj.get("actions"))
plan_content = from_union([from_str, from_none], obj.get("planContent"))
recommended_action = from_union([from_str, from_none], obj.get("recommendedAction"))
- return Data(already_in_use, context, copilot_version, producer, reasoning_effort, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, mode, requested_schema, traceparent, tracestate, command, actions, plan_content, recommended_action)
+ skills = from_union([lambda x: from_list(Skill.from_dict, x), from_none], obj.get("skills"))
+ servers = from_union([lambda x: from_list(Server.from_dict, x), from_none], obj.get("servers"))
+ status = from_union([ServerStatus, from_none], obj.get("status"))
+ extensions = from_union([lambda x: from_list(Extension.from_dict, x), from_none], obj.get("extensions"))
+ return Data(already_in_use, context, copilot_version, producer, reasoning_effort, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, url, background_tasks, title, info_type, warning_type, new_model, previous_model, previous_reasoning_effort, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, conversation_tokens, current_model, current_tokens, error_reason, model_metrics, session_start_time, shutdown_type, system_tokens, tool_definitions_tokens, total_api_duration_ms, total_premium_requests, base_commit, branch, cwd, git_root, head_commit, host_type, is_initial, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, interaction_id, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, output_tokens, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, copilot_usage, cost, duration, initiator, input_tokens, model, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, plugin_name, plugin_version, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role, kind, permission_request, allow_freeform, choices, question, elicitation_source, mode, requested_schema, server_name, server_url, static_client_config, traceparent, tracestate, command, args, command_name, commands, actions, plan_content, recommended_action, skills, servers, status, extensions)
def to_dict(self) -> dict:
result: dict = {}
@@ -2376,6 +2663,8 @@ def to_dict(self) -> dict:
result["stack"] = from_union([from_str, from_none], self.stack)
if self.status_code is not None:
result["statusCode"] = from_union([from_int, from_none], self.status_code)
+ if self.url is not None:
+ result["url"] = from_union([from_str, from_none], self.url)
if self.background_tasks is not None:
result["backgroundTasks"] = from_union([lambda x: to_class(BackgroundTasks, x), from_none], self.background_tasks)
if self.title is not None:
@@ -2430,8 +2719,12 @@ def to_dict(self) -> dict:
result["upToEventId"] = from_union([from_str, from_none], self.up_to_event_id)
if self.code_changes is not None:
result["codeChanges"] = from_union([lambda x: to_class(CodeChanges, x), from_none], self.code_changes)
+ if self.conversation_tokens is not None:
+ result["conversationTokens"] = from_union([to_float, from_none], self.conversation_tokens)
if self.current_model is not None:
result["currentModel"] = from_union([from_str, from_none], self.current_model)
+ if self.current_tokens is not None:
+ result["currentTokens"] = from_union([to_float, from_none], self.current_tokens)
if self.error_reason is not None:
result["errorReason"] = from_union([from_str, from_none], self.error_reason)
if self.model_metrics is not None:
@@ -2440,6 +2733,10 @@ def to_dict(self) -> dict:
result["sessionStartTime"] = from_union([to_float, from_none], self.session_start_time)
if self.shutdown_type is not None:
result["shutdownType"] = from_union([lambda x: to_enum(ShutdownType, x), from_none], self.shutdown_type)
+ if self.system_tokens is not None:
+ result["systemTokens"] = from_union([to_float, from_none], self.system_tokens)
+ if self.tool_definitions_tokens is not None:
+ result["toolDefinitionsTokens"] = from_union([to_float, from_none], self.tool_definitions_tokens)
if self.total_api_duration_ms is not None:
result["totalApiDurationMs"] = from_union([to_float, from_none], self.total_api_duration_ms)
if self.total_premium_requests is not None:
@@ -2456,8 +2753,8 @@ def to_dict(self) -> dict:
result["headCommit"] = from_union([from_str, from_none], self.head_commit)
if self.host_type is not None:
result["hostType"] = from_union([lambda x: to_enum(HostType, x), from_none], self.host_type)
- if self.current_tokens is not None:
- result["currentTokens"] = from_union([to_float, from_none], self.current_tokens)
+ if self.is_initial is not None:
+ result["isInitial"] = from_union([from_bool, from_none], self.is_initial)
if self.messages_length is not None:
result["messagesLength"] = from_union([to_float, from_none], self.messages_length)
if self.checkpoint_number is not None:
@@ -2493,7 +2790,7 @@ def to_dict(self) -> dict:
if self.interaction_id is not None:
result["interactionId"] = from_union([from_str, from_none], self.interaction_id)
if self.source is not None:
- result["source"] = from_union([lambda x: to_enum(Source, x), from_none], self.source)
+ result["source"] = from_union([from_str, from_none], self.source)
if self.transformed_content is not None:
result["transformedContent"] = from_union([from_str, from_none], self.transformed_content)
if self.turn_id is not None:
@@ -2602,22 +2899,44 @@ def to_dict(self) -> dict:
result["choices"] = from_union([lambda x: from_list(from_str, x), from_none], self.choices)
if self.question is not None:
result["question"] = from_union([from_str, from_none], self.question)
+ if self.elicitation_source is not None:
+ result["elicitationSource"] = from_union([from_str, from_none], self.elicitation_source)
if self.mode is not None:
result["mode"] = from_union([lambda x: to_enum(Mode, x), from_none], self.mode)
if self.requested_schema is not None:
result["requestedSchema"] = from_union([lambda x: to_class(RequestedSchema, x), from_none], self.requested_schema)
+ if self.server_name is not None:
+ result["serverName"] = from_union([from_str, from_none], self.server_name)
+ if self.server_url is not None:
+ result["serverUrl"] = from_union([from_str, from_none], self.server_url)
+ if self.static_client_config is not None:
+ result["staticClientConfig"] = from_union([lambda x: to_class(StaticClientConfig, x), from_none], self.static_client_config)
if self.traceparent is not None:
result["traceparent"] = from_union([from_str, from_none], self.traceparent)
if self.tracestate is not None:
result["tracestate"] = from_union([from_str, from_none], self.tracestate)
if self.command is not None:
result["command"] = from_union([from_str, from_none], self.command)
+ if self.args is not None:
+ result["args"] = from_union([from_str, from_none], self.args)
+ if self.command_name is not None:
+ result["commandName"] = from_union([from_str, from_none], self.command_name)
+ if self.commands is not None:
+ result["commands"] = from_union([lambda x: from_list(lambda x: to_class(DataCommand, x), x), from_none], self.commands)
if self.actions is not None:
result["actions"] = from_union([lambda x: from_list(from_str, x), from_none], self.actions)
if self.plan_content is not None:
result["planContent"] = from_union([from_str, from_none], self.plan_content)
if self.recommended_action is not None:
result["recommendedAction"] = from_union([from_str, from_none], self.recommended_action)
+ if self.skills is not None:
+ result["skills"] = from_union([lambda x: from_list(lambda x: to_class(Skill, x), x), from_none], self.skills)
+ if self.servers is not None:
+ result["servers"] = from_union([lambda x: from_list(lambda x: to_class(Server, x), x), from_none], self.servers)
+ if self.status is not None:
+ result["status"] = from_union([lambda x: to_enum(ServerStatus, x), from_none], self.status)
+ if self.extensions is not None:
+ result["extensions"] = from_union([lambda x: from_list(lambda x: to_class(Extension, x), x), from_none], self.extensions)
return result
@@ -2632,7 +2951,9 @@ class SessionEventType(Enum):
ASSISTANT_TURN_END = "assistant.turn_end"
ASSISTANT_TURN_START = "assistant.turn_start"
ASSISTANT_USAGE = "assistant.usage"
+ COMMANDS_CHANGED = "commands.changed"
COMMAND_COMPLETED = "command.completed"
+ COMMAND_EXECUTE = "command.execute"
COMMAND_QUEUED = "command.queued"
ELICITATION_COMPLETED = "elicitation.completed"
ELICITATION_REQUESTED = "elicitation.requested"
@@ -2642,6 +2963,8 @@ class SessionEventType(Enum):
EXTERNAL_TOOL_REQUESTED = "external_tool.requested"
HOOK_END = "hook.end"
HOOK_START = "hook.start"
+ MCP_OAUTH_COMPLETED = "mcp.oauth_completed"
+ MCP_OAUTH_REQUIRED = "mcp.oauth_required"
PENDING_MESSAGES_MODIFIED = "pending_messages.modified"
PERMISSION_COMPLETED = "permission.completed"
PERMISSION_REQUESTED = "permission.requested"
@@ -2650,14 +2973,18 @@ class SessionEventType(Enum):
SESSION_COMPACTION_START = "session.compaction_start"
SESSION_CONTEXT_CHANGED = "session.context_changed"
SESSION_ERROR = "session.error"
+ SESSION_EXTENSIONS_LOADED = "session.extensions_loaded"
SESSION_HANDOFF = "session.handoff"
SESSION_IDLE = "session.idle"
SESSION_INFO = "session.info"
+ SESSION_MCP_SERVERS_LOADED = "session.mcp_servers_loaded"
+ SESSION_MCP_SERVER_STATUS_CHANGED = "session.mcp_server_status_changed"
SESSION_MODEL_CHANGE = "session.model_change"
SESSION_MODE_CHANGED = "session.mode_changed"
SESSION_PLAN_CHANGED = "session.plan_changed"
SESSION_RESUME = "session.resume"
SESSION_SHUTDOWN = "session.shutdown"
+ SESSION_SKILLS_LOADED = "session.skills_loaded"
SESSION_SNAPSHOT_REWIND = "session.snapshot_rewind"
SESSION_START = "session.start"
SESSION_TASK_COMPLETE = "session.task_complete"
@@ -2731,15 +3058,12 @@ class SessionEvent:
Current context window usage statistics including token and message counts
- Empty payload; the event signals that LLM-powered conversation compaction has begun
+ Context window breakdown at the start of LLM-powered conversation compaction
Conversation compaction results including success status, metrics, and optional error
details
- Task completion notification with optional summary from the agent
-
- User message content with optional attachments, source information, and interaction
- metadata
+ Task completion notification with summary from the agent
Empty payload; the event signals that the pending message queue has changed
@@ -2805,18 +3129,27 @@ class SessionEvent:
User input request completion notification signaling UI dismissal
- Structured form elicitation request with JSON schema definition for form fields
+ Elicitation request; may be form-based (structured input) or URL-based (browser
+ redirect)
Elicitation request completion notification signaling UI dismissal
+ OAuth authentication request for an MCP server
+
+ MCP OAuth request completion notification
+
External tool invocation request for client-side tool execution
External tool completion notification signaling UI dismissal
Queued slash command dispatch request for client execution
+ Registered command dispatch request routed to the owning client
+
Queued command completion notification signaling UI dismissal
+ SDK command registration change notification
+
Plan approval request with plan content and available user actions
Plan mode exit completion notification signaling UI dismissal
diff --git a/python/copilot/session.py b/python/copilot/session.py
index 7a8b9f05d..936c07d6c 100644
--- a/python/copilot/session.py
+++ b/python/copilot/session.py
@@ -2,14 +2,18 @@
Copilot Session - represents a single conversation session with the Copilot CLI.
This module provides the CopilotSession class for managing individual
-conversation sessions with the Copilot CLI.
+conversation sessions with the Copilot CLI, along with all session-related
+configuration and handler types.
"""
+from __future__ import annotations
+
import asyncio
import inspect
import threading
-from collections.abc import Callable
-from typing import Any, Literal, cast
+from collections.abc import Awaitable, Callable
+from dataclasses import dataclass
+from typing import Any, Literal, NotRequired, TypedDict, cast
from ._jsonrpc import JsonRpcError, ProcessExitedError
from ._telemetry import get_trace_context, trace_context
@@ -24,24 +28,532 @@
SessionRpc,
SessionToolsHandlePendingToolCallParams,
)
-from .generated.session_events import SessionEvent, SessionEventType, session_event_from_dict
-from .types import (
- Attachment,
+from .generated.session_events import (
PermissionRequest,
- PermissionRequestResult,
- SessionHooks,
- Tool,
- ToolHandler,
- ToolInvocation,
- ToolResult,
- UserInputHandler,
- UserInputRequest,
- UserInputResponse,
- _PermissionHandlerFn,
-)
-from .types import (
- SessionEvent as SessionEventTypeAlias,
+ SessionEvent,
+ SessionEventType,
+ session_event_from_dict,
)
+from .tools import Tool, ToolHandler, ToolInvocation, ToolResult
+
+# Re-export SessionEvent under an alias used internally
+SessionEventTypeAlias = SessionEvent
+
+# ============================================================================
+# Reasoning Effort
+# ============================================================================
+
+ReasoningEffort = Literal["low", "medium", "high", "xhigh"]
+
+# ============================================================================
+# Attachment Types
+# ============================================================================
+
+
+class SelectionRange(TypedDict):
+ line: int
+ character: int
+
+
+class Selection(TypedDict):
+ start: SelectionRange
+ end: SelectionRange
+
+
+class FileAttachment(TypedDict):
+ """File attachment."""
+
+ type: Literal["file"]
+ path: str
+ displayName: NotRequired[str]
+
+
+class DirectoryAttachment(TypedDict):
+ """Directory attachment."""
+
+ type: Literal["directory"]
+ path: str
+ displayName: NotRequired[str]
+
+
+class SelectionAttachment(TypedDict):
+ """Selection attachment with text from a file."""
+
+ type: Literal["selection"]
+ filePath: str
+ displayName: str
+ selection: NotRequired[Selection]
+ text: NotRequired[str]
+
+
+class BlobAttachment(TypedDict):
+ """Inline base64-encoded content attachment (e.g. images)."""
+
+ type: Literal["blob"]
+ data: str
+ """Base64-encoded content"""
+ mimeType: str
+ """MIME type of the inline data"""
+ displayName: NotRequired[str]
+
+
+Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment | BlobAttachment
+
+# ============================================================================
+# System Message Configuration
+# ============================================================================
+
+
+class SystemMessageAppendConfig(TypedDict, total=False):
+ """
+ Append mode: Use CLI foundation with optional appended content.
+ """
+
+ mode: NotRequired[Literal["append"]]
+ content: NotRequired[str]
+
+
+class SystemMessageReplaceConfig(TypedDict):
+ """
+ Replace mode: Use caller-provided system message entirely.
+ Removes all SDK guardrails including security restrictions.
+ """
+
+ mode: Literal["replace"]
+ content: str
+
+
+SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig
+
+# ============================================================================
+# Permission Types
+# ============================================================================
+
+PermissionRequestResultKind = Literal[
+ "approved",
+ "denied-by-rules",
+ "denied-by-content-exclusion-policy",
+ "denied-no-approval-rule-and-could-not-request-from-user",
+ "denied-interactively-by-user",
+ "no-result",
+]
+
+
+@dataclass
+class PermissionRequestResult:
+ """Result of a permission request."""
+
+ kind: PermissionRequestResultKind = "denied-no-approval-rule-and-could-not-request-from-user"
+ rules: list[Any] | None = None
+ feedback: str | None = None
+ message: str | None = None
+ path: str | None = None
+
+
+_PermissionHandlerFn = Callable[
+ [PermissionRequest, dict[str, str]],
+ PermissionRequestResult | Awaitable[PermissionRequestResult],
+]
+
+
+class PermissionHandler:
+ @staticmethod
+ def approve_all(
+ request: PermissionRequest, invocation: dict[str, str]
+ ) -> PermissionRequestResult:
+ return PermissionRequestResult(kind="approved")
+
+
+# ============================================================================
+# User Input Request Types
+# ============================================================================
+
+
+class UserInputRequest(TypedDict, total=False):
+ """Request for user input from the agent (enables ask_user tool)"""
+
+ question: str
+ choices: list[str]
+ allowFreeform: bool
+
+
+class UserInputResponse(TypedDict):
+ """Response to a user input request"""
+
+ answer: str
+ wasFreeform: bool
+
+
+UserInputHandler = Callable[
+ [UserInputRequest, dict[str, str]],
+ UserInputResponse | Awaitable[UserInputResponse],
+]
+
+# ============================================================================
+# Hook Types
+# ============================================================================
+
+
+class BaseHookInput(TypedDict):
+ """Base interface for all hook inputs"""
+
+ timestamp: int
+ cwd: str
+
+
+class PreToolUseHookInput(TypedDict):
+ """Input for pre-tool-use hook"""
+
+ timestamp: int
+ cwd: str
+ toolName: str
+ toolArgs: Any
+
+
+class PreToolUseHookOutput(TypedDict, total=False):
+ """Output for pre-tool-use hook"""
+
+ permissionDecision: Literal["allow", "deny", "ask"]
+ permissionDecisionReason: str
+ modifiedArgs: Any
+ additionalContext: str
+ suppressOutput: bool
+
+
+PreToolUseHandler = Callable[
+ [PreToolUseHookInput, dict[str, str]],
+ PreToolUseHookOutput | None | Awaitable[PreToolUseHookOutput | None],
+]
+
+
+class PostToolUseHookInput(TypedDict):
+ """Input for post-tool-use hook"""
+
+ timestamp: int
+ cwd: str
+ toolName: str
+ toolArgs: Any
+ toolResult: Any
+
+
+class PostToolUseHookOutput(TypedDict, total=False):
+ """Output for post-tool-use hook"""
+
+ modifiedResult: Any
+ additionalContext: str
+ suppressOutput: bool
+
+
+PostToolUseHandler = Callable[
+ [PostToolUseHookInput, dict[str, str]],
+ PostToolUseHookOutput | None | Awaitable[PostToolUseHookOutput | None],
+]
+
+
+class UserPromptSubmittedHookInput(TypedDict):
+ """Input for user-prompt-submitted hook"""
+
+ timestamp: int
+ cwd: str
+ prompt: str
+
+
+class UserPromptSubmittedHookOutput(TypedDict, total=False):
+ """Output for user-prompt-submitted hook"""
+
+ modifiedPrompt: str
+ additionalContext: str
+ suppressOutput: bool
+
+
+UserPromptSubmittedHandler = Callable[
+ [UserPromptSubmittedHookInput, dict[str, str]],
+ UserPromptSubmittedHookOutput | None | Awaitable[UserPromptSubmittedHookOutput | None],
+]
+
+
+class SessionStartHookInput(TypedDict):
+ """Input for session-start hook"""
+
+ timestamp: int
+ cwd: str
+ source: Literal["startup", "resume", "new"]
+ initialPrompt: NotRequired[str]
+
+
+class SessionStartHookOutput(TypedDict, total=False):
+ """Output for session-start hook"""
+
+ additionalContext: str
+ modifiedConfig: dict[str, Any]
+
+
+SessionStartHandler = Callable[
+ [SessionStartHookInput, dict[str, str]],
+ SessionStartHookOutput | None | Awaitable[SessionStartHookOutput | None],
+]
+
+
+class SessionEndHookInput(TypedDict):
+ """Input for session-end hook"""
+
+ timestamp: int
+ cwd: str
+ reason: Literal["complete", "error", "abort", "timeout", "user_exit"]
+ finalMessage: NotRequired[str]
+ error: NotRequired[str]
+
+
+class SessionEndHookOutput(TypedDict, total=False):
+ """Output for session-end hook"""
+
+ suppressOutput: bool
+ cleanupActions: list[str]
+ sessionSummary: str
+
+
+SessionEndHandler = Callable[
+ [SessionEndHookInput, dict[str, str]],
+ SessionEndHookOutput | None | Awaitable[SessionEndHookOutput | None],
+]
+
+
+class ErrorOccurredHookInput(TypedDict):
+ """Input for error-occurred hook"""
+
+ timestamp: int
+ cwd: str
+ error: str
+ errorContext: Literal["model_call", "tool_execution", "system", "user_input"]
+ recoverable: bool
+
+
+class ErrorOccurredHookOutput(TypedDict, total=False):
+ """Output for error-occurred hook"""
+
+ suppressOutput: bool
+ errorHandling: Literal["retry", "skip", "abort"]
+ retryCount: int
+ userNotification: str
+
+
+ErrorOccurredHandler = Callable[
+ [ErrorOccurredHookInput, dict[str, str]],
+ ErrorOccurredHookOutput | None | Awaitable[ErrorOccurredHookOutput | None],
+]
+
+
+class SessionHooks(TypedDict, total=False):
+ """Configuration for session hooks"""
+
+ on_pre_tool_use: PreToolUseHandler
+ on_post_tool_use: PostToolUseHandler
+ on_user_prompt_submitted: UserPromptSubmittedHandler
+ on_session_start: SessionStartHandler
+ on_session_end: SessionEndHandler
+ on_error_occurred: ErrorOccurredHandler
+
+
+# ============================================================================
+# MCP Server Configuration Types
+# ============================================================================
+
+
+class MCPLocalServerConfig(TypedDict, total=False):
+ """Configuration for a local/stdio MCP server."""
+
+ tools: list[str] # List of tools to include. [] means none. "*" means all.
+ type: NotRequired[Literal["local", "stdio"]] # Server type
+ timeout: NotRequired[int] # Timeout in milliseconds
+ command: str # Command to run
+ args: list[str] # Command arguments
+ env: NotRequired[dict[str, str]] # Environment variables
+ cwd: NotRequired[str] # Working directory
+
+
+class MCPRemoteServerConfig(TypedDict, total=False):
+ """Configuration for a remote MCP server (HTTP or SSE)."""
+
+ tools: list[str] # List of tools to include. [] means none. "*" means all.
+ type: Literal["http", "sse"] # Server type
+ timeout: NotRequired[int] # Timeout in milliseconds
+ url: str # URL of the remote server
+ headers: NotRequired[dict[str, str]] # HTTP headers
+
+
+MCPServerConfig = MCPLocalServerConfig | MCPRemoteServerConfig
+
+# ============================================================================
+# Custom Agent Configuration Types
+# ============================================================================
+
+
+class CustomAgentConfig(TypedDict, total=False):
+ """Configuration for a custom agent."""
+
+ name: str # Unique name of the custom agent
+ display_name: NotRequired[str] # Display name for UI purposes
+ description: NotRequired[str] # Description of what the agent does
+ # List of tool names the agent can use
+ tools: NotRequired[list[str] | None]
+ prompt: str # The prompt content for the agent
+ # MCP servers specific to agent
+ mcp_servers: NotRequired[dict[str, MCPServerConfig]]
+ infer: NotRequired[bool] # Whether agent is available for model inference
+
+
+class InfiniteSessionConfig(TypedDict, total=False):
+ """
+ Configuration for infinite sessions with automatic context compaction
+ and workspace persistence.
+
+ When enabled, sessions automatically manage context window limits through
+ background compaction and persist state to a workspace directory.
+ """
+
+ # Whether infinite sessions are enabled (default: True)
+ enabled: bool
+ # Context utilization threshold (0.0-1.0) at which background compaction starts.
+ # Compaction runs asynchronously, allowing the session to continue processing.
+ # Default: 0.80
+ background_compaction_threshold: float
+ # Context utilization threshold (0.0-1.0) at which the session blocks until
+ # compaction completes. This prevents context overflow when compaction hasn't
+ # finished in time. Default: 0.95
+ buffer_exhaustion_threshold: float
+
+
+# ============================================================================
+# Session Configuration
+# ============================================================================
+
+
+class AzureProviderOptions(TypedDict, total=False):
+ """Azure-specific provider configuration"""
+
+ api_version: str # Azure API version. Defaults to "2024-10-21".
+
+
+class ProviderConfig(TypedDict, total=False):
+ """Configuration for a custom API provider"""
+
+ type: Literal["openai", "azure", "anthropic"]
+ wire_api: Literal["completions", "responses"]
+ base_url: str
+ api_key: str
+ # Bearer token for authentication. Sets the Authorization header directly.
+ # Use this for services requiring bearer token auth instead of API key.
+ # Takes precedence over api_key when both are set.
+ bearer_token: str
+ azure: AzureProviderOptions # Azure-specific options
+
+
+class SessionConfig(TypedDict, total=False):
+ """Configuration for creating a session"""
+
+ session_id: str # Optional custom session ID
+ # Client name to identify the application using the SDK.
+ # Included in the User-Agent header for API requests.
+ client_name: str
+ model: str # Model to use for this session. Use client.list_models() to see available models.
+ # Reasoning effort level for models that support it.
+ # Only valid for models where capabilities.supports.reasoning_effort is True.
+ reasoning_effort: ReasoningEffort
+ tools: list[Tool]
+ system_message: SystemMessageConfig # System message configuration
+ # List of tool names to allow (takes precedence over excluded_tools)
+ available_tools: list[str]
+ # List of tool names to disable (ignored if available_tools is set)
+ excluded_tools: list[str]
+ # Handler for permission requests from the server
+ on_permission_request: _PermissionHandlerFn
+ # Handler for user input requests from the agent (enables ask_user tool)
+ on_user_input_request: UserInputHandler
+ # Hook handlers for intercepting session lifecycle events
+ hooks: SessionHooks
+ # Working directory for the session. Tool operations will be relative to this directory.
+ working_directory: str
+ # Custom provider configuration (BYOK - Bring Your Own Key)
+ provider: ProviderConfig
+ # Enable streaming of assistant message and reasoning chunks
+ # When True, assistant.message_delta and assistant.reasoning_delta events
+ # with delta_content are sent as the response is generated
+ streaming: bool
+ # MCP server configurations for the session
+ mcp_servers: dict[str, MCPServerConfig]
+ # Custom agent configurations for the session
+ custom_agents: list[CustomAgentConfig]
+ # Name of the custom agent to activate when the session starts.
+ # Must match the name of one of the agents in custom_agents.
+ agent: str
+ # Override the default configuration directory location.
+ # When specified, the session will use this directory for storing config and state.
+ config_dir: str
+ # Directories to load skills from
+ skill_directories: list[str]
+ # List of skill names to disable
+ disabled_skills: list[str]
+ # Infinite session configuration for persistent workspaces and automatic compaction.
+ # When enabled (default), sessions automatically manage context limits and persist state.
+ # Set to {"enabled": False} to disable.
+ infinite_sessions: InfiniteSessionConfig
+ # Optional event handler that is registered on the session before the
+ # session.create RPC is issued, ensuring early events (e.g. session.start)
+ # are delivered. Equivalent to calling session.on(handler) immediately
+ # after creation, but executes earlier in the lifecycle so no events are missed.
+ on_event: Callable[[SessionEvent], None]
+
+
+class ResumeSessionConfig(TypedDict, total=False):
+ """Configuration for resuming a session"""
+
+ # Client name to identify the application using the SDK.
+ # Included in the User-Agent header for API requests.
+ client_name: str
+ # Model to use for this session. Can change the model when resuming.
+ model: str
+ tools: list[Tool]
+ system_message: SystemMessageConfig # System message configuration
+ # List of tool names to allow (takes precedence over excluded_tools)
+ available_tools: list[str]
+ # List of tool names to disable (ignored if available_tools is set)
+ excluded_tools: list[str]
+ provider: ProviderConfig
+ # Reasoning effort level for models that support it.
+ reasoning_effort: ReasoningEffort
+ on_permission_request: _PermissionHandlerFn
+ # Handler for user input requestsfrom the agent (enables ask_user tool)
+ on_user_input_request: UserInputHandler
+ # Hook handlers for intercepting session lifecycle events
+ hooks: SessionHooks
+ # Working directory for the session. Tool operations will be relative to this directory.
+ working_directory: str
+ # Override the default configuration directory location.
+ config_dir: str
+ # Enable streaming of assistant message chunks
+ streaming: bool
+ # MCP server configurations for the session
+ mcp_servers: dict[str, MCPServerConfig]
+ # Custom agent configurations for the session
+ custom_agents: list[CustomAgentConfig]
+ # Name of the custom agent to activate when the session starts.
+ # Must match the name of one of the agents in custom_agents.
+ agent: str
+ # Directories to load skills from
+ skill_directories: list[str]
+ # List of skill names to disable
+ disabled_skills: list[str]
+ # Infinite session configuration for persistent workspaces and automatic compaction.
+ infinite_sessions: InfiniteSessionConfig
+ # When True, skips emitting the session.resume event.
+ # Useful for reconnecting to a session without triggering resume-related side effects.
+ disable_resume: bool
+ # Optional event handler registered before the session.resume RPC is issued,
+ # ensuring early events are delivered. See SessionConfig.on_event.
+ on_event: Callable[[SessionEvent], None]
+
+
+SessionEventHandler = Callable[[SessionEvent], None]
class CopilotSession:
@@ -245,7 +757,9 @@ def on(self, handler: Callable[[SessionEvent], None]) -> Callable[[], None]:
... print(f"Assistant: {event.data.content}")
... elif event.type == "session.error":
... print(f"Error: {event.data.message}")
+ ...
>>> unsubscribe = session.on(handle_event)
+ ...
>>> # Later, to stop receiving events:
>>> unsubscribe()
"""
@@ -706,7 +1220,7 @@ async def destroy(self) -> None:
)
await self.disconnect()
- async def __aenter__(self) -> "CopilotSession":
+ async def __aenter__(self) -> CopilotSession:
"""Enable use as an async context manager."""
return self
@@ -728,7 +1242,9 @@ async def abort(self) -> None:
>>> import asyncio
>>>
>>> # Start a long-running request
- >>> task = asyncio.create_task(session.send("Write a very long story..."))
+ >>> task = asyncio.create_task(
+ ... session.send("Write a very long story...")
+ ... )
>>>
>>> # Abort after 5 seconds
>>> await asyncio.sleep(5)
diff --git a/python/copilot/tools.py b/python/copilot/tools.py
index 58e58d97e..f559cfefe 100644
--- a/python/copilot/tools.py
+++ b/python/copilot/tools.py
@@ -9,12 +9,59 @@
import inspect
import json
-from collections.abc import Callable
-from typing import Any, TypeVar, get_type_hints, overload
+from collections.abc import Awaitable, Callable
+from dataclasses import dataclass
+from typing import Any, Literal, TypeVar, get_type_hints, overload
from pydantic import BaseModel
-from .types import Tool, ToolInvocation, ToolResult
+ToolResultType = Literal["success", "failure", "rejected", "denied"]
+
+
+@dataclass
+class ToolBinaryResult:
+ """Binary content returned by a tool."""
+
+ data: str = ""
+ mime_type: str = ""
+ type: str = ""
+ description: str = ""
+
+
+@dataclass
+class ToolResult:
+ """Result of a tool invocation."""
+
+ text_result_for_llm: str = ""
+ result_type: ToolResultType = "success"
+ error: str | None = None
+ binary_results_for_llm: list[ToolBinaryResult] | None = None
+ session_log: str | None = None
+ tool_telemetry: dict[str, Any] | None = None
+
+
+@dataclass
+class ToolInvocation:
+ """Context passed to a tool handler when invoked."""
+
+ session_id: str = ""
+ tool_call_id: str = ""
+ tool_name: str = ""
+ arguments: Any = None
+
+
+ToolHandler = Callable[[ToolInvocation], ToolResult | Awaitable[ToolResult]]
+
+
+@dataclass
+class Tool:
+ name: str
+ description: str
+ handler: ToolHandler
+ parameters: dict[str, Any] | None = None
+ overrides_built_in_tool: bool = False
+ skip_permission: bool = False
+
T = TypeVar("T", bound=BaseModel)
R = TypeVar("R")
diff --git a/python/copilot/types.py b/python/copilot/types.py
deleted file mode 100644
index 17be065bc..000000000
--- a/python/copilot/types.py
+++ /dev/null
@@ -1,1060 +0,0 @@
-"""
-Type definitions for the Copilot SDK
-"""
-
-from __future__ import annotations
-
-from collections.abc import Awaitable, Callable
-from dataclasses import KW_ONLY, dataclass, field
-from typing import Any, Literal, NotRequired, TypedDict
-
-# Import generated SessionEvent types
-from .generated.session_events import (
- PermissionRequest,
- SessionEvent,
-)
-
-# SessionEvent is now imported from generated types
-# It provides proper type discrimination for all event types
-
-# Valid reasoning effort levels for models that support it
-ReasoningEffort = Literal["low", "medium", "high", "xhigh"]
-
-# Connection state
-ConnectionState = Literal["disconnected", "connecting", "connected", "error"]
-
-# Log level type
-LogLevel = Literal["none", "error", "warning", "info", "debug", "all"]
-
-
-# Selection range for text attachments
-class SelectionRange(TypedDict):
- line: int
- character: int
-
-
-class Selection(TypedDict):
- start: SelectionRange
- end: SelectionRange
-
-
-# Attachment types - discriminated union based on 'type' field
-class FileAttachment(TypedDict):
- """File attachment."""
-
- type: Literal["file"]
- path: str
- displayName: NotRequired[str]
-
-
-class DirectoryAttachment(TypedDict):
- """Directory attachment."""
-
- type: Literal["directory"]
- path: str
- displayName: NotRequired[str]
-
-
-class SelectionAttachment(TypedDict):
- """Selection attachment with text from a file."""
-
- type: Literal["selection"]
- filePath: str
- displayName: str
- selection: NotRequired[Selection]
- text: NotRequired[str]
-
-
-class BlobAttachment(TypedDict):
- """Inline base64-encoded content attachment (e.g. images)."""
-
- type: Literal["blob"]
- data: str
- """Base64-encoded content"""
- mimeType: str
- """MIME type of the inline data"""
- displayName: NotRequired[str]
-
-
-# Attachment type - union of all attachment types
-Attachment = FileAttachment | DirectoryAttachment | SelectionAttachment | BlobAttachment
-
-
-# Configuration for OpenTelemetry integration with the Copilot CLI.
-class TelemetryConfig(TypedDict, total=False):
- """Configuration for OpenTelemetry integration with the Copilot CLI."""
-
- otlp_endpoint: str
- """OTLP HTTP endpoint URL for trace/metric export. Sets OTEL_EXPORTER_OTLP_ENDPOINT."""
- file_path: str
- """File path for JSON-lines trace output. Sets COPILOT_OTEL_FILE_EXPORTER_PATH."""
- exporter_type: str
- """Exporter backend type: "otlp-http" or "file". Sets COPILOT_OTEL_EXPORTER_TYPE."""
- source_name: str
- """Instrumentation scope name. Sets COPILOT_OTEL_SOURCE_NAME."""
- capture_content: bool
- """Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501
-
-
-# Configuration for CopilotClient connection modes
-
-
-@dataclass
-class SubprocessConfig:
- """Config for spawning a local Copilot CLI subprocess.
-
- Example:
- >>> config = SubprocessConfig(github_token="ghp_...")
- >>> client = CopilotClient(config)
-
- >>> # Custom CLI path with TCP transport
- >>> config = SubprocessConfig(
- ... cli_path="/usr/local/bin/copilot",
- ... use_stdio=False,
- ... log_level="debug",
- ... )
- """
-
- cli_path: str | None = None
- """Path to the Copilot CLI executable. ``None`` uses the bundled binary."""
-
- cli_args: list[str] = field(default_factory=list)
- """Extra arguments passed to the CLI executable (inserted before SDK-managed args)."""
-
- _: KW_ONLY
-
- cwd: str | None = None
- """Working directory for the CLI process. ``None`` uses the current directory."""
-
- use_stdio: bool = True
- """Use stdio transport (``True``, default) or TCP (``False``)."""
-
- port: int = 0
- """TCP port for the CLI server (only when ``use_stdio=False``). 0 means random."""
-
- log_level: LogLevel = "info"
- """Log level for the CLI process."""
-
- env: dict[str, str] | None = None
- """Environment variables for the CLI process. ``None`` inherits the current env."""
-
- github_token: str | None = None
- """GitHub token for authentication. Takes priority over other auth methods."""
-
- use_logged_in_user: bool | None = None
- """Use the logged-in user for authentication.
-
- ``None`` (default) resolves to ``True`` unless ``github_token`` is set.
- """
-
- telemetry: TelemetryConfig | None = None
- """OpenTelemetry configuration. Providing this enables telemetry — no separate flag needed."""
-
-
-@dataclass
-class ExternalServerConfig:
- """Config for connecting to an existing Copilot CLI server over TCP.
-
- Example:
- >>> config = ExternalServerConfig(url="localhost:3000")
- >>> client = CopilotClient(config)
- """
-
- url: str
- """Server URL. Supports ``"host:port"``, ``"http://host:port"``, or just ``"port"``."""
-
-
-ToolResultType = Literal["success", "failure", "rejected", "denied"]
-
-
-@dataclass
-class ToolBinaryResult:
- """Binary content returned by a tool."""
-
- data: str = ""
- mime_type: str = ""
- type: str = ""
- description: str = ""
-
-
-@dataclass
-class ToolResult:
- """Result of a tool invocation."""
-
- text_result_for_llm: str = ""
- result_type: ToolResultType = "success"
- error: str | None = None
- binary_results_for_llm: list[ToolBinaryResult] | None = None
- session_log: str | None = None
- tool_telemetry: dict[str, Any] | None = None
-
-
-@dataclass
-class ToolInvocation:
- """Context passed to a tool handler when invoked."""
-
- session_id: str = ""
- tool_call_id: str = ""
- tool_name: str = ""
- arguments: Any = None
-
-
-ToolHandler = Callable[[ToolInvocation], ToolResult | Awaitable[ToolResult]]
-
-
-@dataclass
-class Tool:
- name: str
- description: str
- handler: ToolHandler
- parameters: dict[str, Any] | None = None
- overrides_built_in_tool: bool = False
- skip_permission: bool = False
-
-
-# System message configuration (discriminated union)
-# Use SystemMessageAppendConfig for default behavior, SystemMessageReplaceConfig for full control
-
-
-class SystemMessageAppendConfig(TypedDict, total=False):
- """
- Append mode: Use CLI foundation with optional appended content.
- """
-
- mode: NotRequired[Literal["append"]]
- content: NotRequired[str]
-
-
-class SystemMessageReplaceConfig(TypedDict):
- """
- Replace mode: Use caller-provided system message entirely.
- Removes all SDK guardrails including security restrictions.
- """
-
- mode: Literal["replace"]
- content: str
-
-
-# Union type - use one or the other
-SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig
-
-
-# Permission result types
-
-PermissionRequestResultKind = Literal[
- "approved",
- "denied-by-rules",
- "denied-by-content-exclusion-policy",
- "denied-no-approval-rule-and-could-not-request-from-user",
- "denied-interactively-by-user",
- "no-result",
-]
-
-
-@dataclass
-class PermissionRequestResult:
- """Result of a permission request."""
-
- kind: PermissionRequestResultKind = "denied-no-approval-rule-and-could-not-request-from-user"
- rules: list[Any] | None = None
- feedback: str | None = None
- message: str | None = None
- path: str | None = None
-
-
-_PermissionHandlerFn = Callable[
- [PermissionRequest, dict[str, str]],
- PermissionRequestResult | Awaitable[PermissionRequestResult],
-]
-
-
-class PermissionHandler:
- @staticmethod
- def approve_all(
- request: PermissionRequest, invocation: dict[str, str]
- ) -> PermissionRequestResult:
- return PermissionRequestResult(kind="approved")
-
-
-# ============================================================================
-# User Input Request Types
-# ============================================================================
-
-
-class UserInputRequest(TypedDict, total=False):
- """Request for user input from the agent (enables ask_user tool)"""
-
- question: str
- choices: list[str]
- allowFreeform: bool
-
-
-class UserInputResponse(TypedDict):
- """Response to a user input request"""
-
- answer: str
- wasFreeform: bool
-
-
-UserInputHandler = Callable[
- [UserInputRequest, dict[str, str]],
- UserInputResponse | Awaitable[UserInputResponse],
-]
-
-
-# ============================================================================
-# Hook Types
-# ============================================================================
-
-
-class BaseHookInput(TypedDict):
- """Base interface for all hook inputs"""
-
- timestamp: int
- cwd: str
-
-
-class PreToolUseHookInput(TypedDict):
- """Input for pre-tool-use hook"""
-
- timestamp: int
- cwd: str
- toolName: str
- toolArgs: Any
-
-
-class PreToolUseHookOutput(TypedDict, total=False):
- """Output for pre-tool-use hook"""
-
- permissionDecision: Literal["allow", "deny", "ask"]
- permissionDecisionReason: str
- modifiedArgs: Any
- additionalContext: str
- suppressOutput: bool
-
-
-PreToolUseHandler = Callable[
- [PreToolUseHookInput, dict[str, str]],
- PreToolUseHookOutput | None | Awaitable[PreToolUseHookOutput | None],
-]
-
-
-class PostToolUseHookInput(TypedDict):
- """Input for post-tool-use hook"""
-
- timestamp: int
- cwd: str
- toolName: str
- toolArgs: Any
- toolResult: Any
-
-
-class PostToolUseHookOutput(TypedDict, total=False):
- """Output for post-tool-use hook"""
-
- modifiedResult: Any
- additionalContext: str
- suppressOutput: bool
-
-
-PostToolUseHandler = Callable[
- [PostToolUseHookInput, dict[str, str]],
- PostToolUseHookOutput | None | Awaitable[PostToolUseHookOutput | None],
-]
-
-
-class UserPromptSubmittedHookInput(TypedDict):
- """Input for user-prompt-submitted hook"""
-
- timestamp: int
- cwd: str
- prompt: str
-
-
-class UserPromptSubmittedHookOutput(TypedDict, total=False):
- """Output for user-prompt-submitted hook"""
-
- modifiedPrompt: str
- additionalContext: str
- suppressOutput: bool
-
-
-UserPromptSubmittedHandler = Callable[
- [UserPromptSubmittedHookInput, dict[str, str]],
- UserPromptSubmittedHookOutput | None | Awaitable[UserPromptSubmittedHookOutput | None],
-]
-
-
-class SessionStartHookInput(TypedDict):
- """Input for session-start hook"""
-
- timestamp: int
- cwd: str
- source: Literal["startup", "resume", "new"]
- initialPrompt: NotRequired[str]
-
-
-class SessionStartHookOutput(TypedDict, total=False):
- """Output for session-start hook"""
-
- additionalContext: str
- modifiedConfig: dict[str, Any]
-
-
-SessionStartHandler = Callable[
- [SessionStartHookInput, dict[str, str]],
- SessionStartHookOutput | None | Awaitable[SessionStartHookOutput | None],
-]
-
-
-class SessionEndHookInput(TypedDict):
- """Input for session-end hook"""
-
- timestamp: int
- cwd: str
- reason: Literal["complete", "error", "abort", "timeout", "user_exit"]
- finalMessage: NotRequired[str]
- error: NotRequired[str]
-
-
-class SessionEndHookOutput(TypedDict, total=False):
- """Output for session-end hook"""
-
- suppressOutput: bool
- cleanupActions: list[str]
- sessionSummary: str
-
-
-SessionEndHandler = Callable[
- [SessionEndHookInput, dict[str, str]],
- SessionEndHookOutput | None | Awaitable[SessionEndHookOutput | None],
-]
-
-
-class ErrorOccurredHookInput(TypedDict):
- """Input for error-occurred hook"""
-
- timestamp: int
- cwd: str
- error: str
- errorContext: Literal["model_call", "tool_execution", "system", "user_input"]
- recoverable: bool
-
-
-class ErrorOccurredHookOutput(TypedDict, total=False):
- """Output for error-occurred hook"""
-
- suppressOutput: bool
- errorHandling: Literal["retry", "skip", "abort"]
- retryCount: int
- userNotification: str
-
-
-ErrorOccurredHandler = Callable[
- [ErrorOccurredHookInput, dict[str, str]],
- ErrorOccurredHookOutput | None | Awaitable[ErrorOccurredHookOutput | None],
-]
-
-
-class SessionHooks(TypedDict, total=False):
- """Configuration for session hooks"""
-
- on_pre_tool_use: PreToolUseHandler
- on_post_tool_use: PostToolUseHandler
- on_user_prompt_submitted: UserPromptSubmittedHandler
- on_session_start: SessionStartHandler
- on_session_end: SessionEndHandler
- on_error_occurred: ErrorOccurredHandler
-
-
-# ============================================================================
-# MCP Server Configuration Types
-# ============================================================================
-
-
-class MCPLocalServerConfig(TypedDict, total=False):
- """Configuration for a local/stdio MCP server."""
-
- tools: list[str] # List of tools to include. [] means none. "*" means all.
- type: NotRequired[Literal["local", "stdio"]] # Server type
- timeout: NotRequired[int] # Timeout in milliseconds
- command: str # Command to run
- args: list[str] # Command arguments
- env: NotRequired[dict[str, str]] # Environment variables
- cwd: NotRequired[str] # Working directory
-
-
-class MCPRemoteServerConfig(TypedDict, total=False):
- """Configuration for a remote MCP server (HTTP or SSE)."""
-
- tools: list[str] # List of tools to include. [] means none. "*" means all.
- type: Literal["http", "sse"] # Server type
- timeout: NotRequired[int] # Timeout in milliseconds
- url: str # URL of the remote server
- headers: NotRequired[dict[str, str]] # HTTP headers
-
-
-MCPServerConfig = MCPLocalServerConfig | MCPRemoteServerConfig
-
-
-# ============================================================================
-# Custom Agent Configuration Types
-# ============================================================================
-
-
-class CustomAgentConfig(TypedDict, total=False):
- """Configuration for a custom agent."""
-
- name: str # Unique name of the custom agent
- display_name: NotRequired[str] # Display name for UI purposes
- description: NotRequired[str] # Description of what the agent does
- # List of tool names the agent can use
- tools: NotRequired[list[str] | None]
- prompt: str # The prompt content for the agent
- # MCP servers specific to agent
- mcp_servers: NotRequired[dict[str, MCPServerConfig]]
- infer: NotRequired[bool] # Whether agent is available for model inference
-
-
-class InfiniteSessionConfig(TypedDict, total=False):
- """
- Configuration for infinite sessions with automatic context compaction
- and workspace persistence.
-
- When enabled, sessions automatically manage context window limits through
- background compaction and persist state to a workspace directory.
- """
-
- # Whether infinite sessions are enabled (default: True)
- enabled: bool
- # Context utilization threshold (0.0-1.0) at which background compaction starts.
- # Compaction runs asynchronously, allowing the session to continue processing.
- # Default: 0.80
- background_compaction_threshold: float
- # Context utilization threshold (0.0-1.0) at which the session blocks until
- # compaction completes. This prevents context overflow when compaction hasn't
- # finished in time. Default: 0.95
- buffer_exhaustion_threshold: float
-
-
-# Azure-specific provider options
-class AzureProviderOptions(TypedDict, total=False):
- """Azure-specific provider configuration"""
-
- api_version: str # Azure API version. Defaults to "2024-10-21".
-
-
-# Configuration for a custom API provider
-class ProviderConfig(TypedDict, total=False):
- """Configuration for a custom API provider"""
-
- type: Literal["openai", "azure", "anthropic"]
- wire_api: Literal["completions", "responses"]
- base_url: str
- api_key: str
- # Bearer token for authentication. Sets the Authorization header directly.
- # Use this for services requiring bearer token auth instead of API key.
- # Takes precedence over api_key when both are set.
- bearer_token: str
- azure: AzureProviderOptions # Azure-specific options
-
-
-# Event handler type
-SessionEventHandler = Callable[[SessionEvent], None]
-
-
-# Response from ping
-@dataclass
-class PingResponse:
- """Response from ping"""
-
- message: str # Echo message with "pong: " prefix
- timestamp: int # Server timestamp in milliseconds
- protocolVersion: int # Protocol version for SDK compatibility
-
- @staticmethod
- def from_dict(obj: Any) -> PingResponse:
- assert isinstance(obj, dict)
- message = obj.get("message")
- timestamp = obj.get("timestamp")
- protocolVersion = obj.get("protocolVersion")
- if message is None or timestamp is None or protocolVersion is None:
- raise ValueError(
- f"Missing required fields in PingResponse: message={message}, "
- f"timestamp={timestamp}, protocolVersion={protocolVersion}"
- )
- return PingResponse(str(message), int(timestamp), int(protocolVersion))
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["message"] = self.message
- result["timestamp"] = self.timestamp
- result["protocolVersion"] = self.protocolVersion
- return result
-
-
-# Error information from client stop
-@dataclass
-class StopError(Exception):
- """Error that occurred during client stop cleanup."""
-
- message: str # Error message describing what failed during cleanup
-
- def __post_init__(self) -> None:
- Exception.__init__(self, self.message)
-
- @staticmethod
- def from_dict(obj: Any) -> StopError:
- assert isinstance(obj, dict)
- message = obj.get("message")
- if message is None:
- raise ValueError("Missing required field 'message' in StopError")
- return StopError(str(message))
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["message"] = self.message
- return result
-
-
-# Response from status.get
-@dataclass
-class GetStatusResponse:
- """Response from status.get"""
-
- version: str # Package version (e.g., "1.0.0")
- protocolVersion: int # Protocol version for SDK compatibility
-
- @staticmethod
- def from_dict(obj: Any) -> GetStatusResponse:
- assert isinstance(obj, dict)
- version = obj.get("version")
- protocolVersion = obj.get("protocolVersion")
- if version is None or protocolVersion is None:
- raise ValueError(
- f"Missing required fields in GetStatusResponse: version={version}, "
- f"protocolVersion={protocolVersion}"
- )
- return GetStatusResponse(str(version), int(protocolVersion))
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["version"] = self.version
- result["protocolVersion"] = self.protocolVersion
- return result
-
-
-# Response from auth.getStatus
-@dataclass
-class GetAuthStatusResponse:
- """Response from auth.getStatus"""
-
- isAuthenticated: bool # Whether the user is authenticated
- authType: str | None = None # Authentication type
- host: str | None = None # GitHub host URL
- login: str | None = None # User login name
- statusMessage: str | None = None # Human-readable status message
-
- @staticmethod
- def from_dict(obj: Any) -> GetAuthStatusResponse:
- assert isinstance(obj, dict)
- isAuthenticated = obj.get("isAuthenticated")
- if isAuthenticated is None:
- raise ValueError("Missing required field 'isAuthenticated' in GetAuthStatusResponse")
- authType = obj.get("authType")
- host = obj.get("host")
- login = obj.get("login")
- statusMessage = obj.get("statusMessage")
- return GetAuthStatusResponse(
- isAuthenticated=bool(isAuthenticated),
- authType=authType,
- host=host,
- login=login,
- statusMessage=statusMessage,
- )
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["isAuthenticated"] = self.isAuthenticated
- if self.authType is not None:
- result["authType"] = self.authType
- if self.host is not None:
- result["host"] = self.host
- if self.login is not None:
- result["login"] = self.login
- if self.statusMessage is not None:
- result["statusMessage"] = self.statusMessage
- return result
-
-
-# Model capabilities
-@dataclass
-class ModelVisionLimits:
- """Vision-specific limits"""
-
- supported_media_types: list[str] | None = None
- max_prompt_images: int | None = None
- max_prompt_image_size: int | None = None
-
- @staticmethod
- def from_dict(obj: Any) -> ModelVisionLimits:
- assert isinstance(obj, dict)
- supported_media_types = obj.get("supported_media_types")
- max_prompt_images = obj.get("max_prompt_images")
- max_prompt_image_size = obj.get("max_prompt_image_size")
- return ModelVisionLimits(
- supported_media_types=supported_media_types,
- max_prompt_images=max_prompt_images,
- max_prompt_image_size=max_prompt_image_size,
- )
-
- def to_dict(self) -> dict:
- result: dict = {}
- if self.supported_media_types is not None:
- result["supported_media_types"] = self.supported_media_types
- if self.max_prompt_images is not None:
- result["max_prompt_images"] = self.max_prompt_images
- if self.max_prompt_image_size is not None:
- result["max_prompt_image_size"] = self.max_prompt_image_size
- return result
-
-
-@dataclass
-class ModelLimits:
- """Model limits"""
-
- max_prompt_tokens: int | None = None
- max_context_window_tokens: int | None = None
- vision: ModelVisionLimits | None = None
-
- @staticmethod
- def from_dict(obj: Any) -> ModelLimits:
- assert isinstance(obj, dict)
- max_prompt_tokens = obj.get("max_prompt_tokens")
- max_context_window_tokens = obj.get("max_context_window_tokens")
- vision_dict = obj.get("vision")
- vision = ModelVisionLimits.from_dict(vision_dict) if vision_dict else None
- return ModelLimits(
- max_prompt_tokens=max_prompt_tokens,
- max_context_window_tokens=max_context_window_tokens,
- vision=vision,
- )
-
- def to_dict(self) -> dict:
- result: dict = {}
- if self.max_prompt_tokens is not None:
- result["max_prompt_tokens"] = self.max_prompt_tokens
- if self.max_context_window_tokens is not None:
- result["max_context_window_tokens"] = self.max_context_window_tokens
- if self.vision is not None:
- result["vision"] = self.vision.to_dict()
- return result
-
-
-@dataclass
-class ModelSupports:
- """Model support flags"""
-
- vision: bool
- reasoning_effort: bool = False # Whether this model supports reasoning effort
-
- @staticmethod
- def from_dict(obj: Any) -> ModelSupports:
- assert isinstance(obj, dict)
- vision = obj.get("vision")
- if vision is None:
- raise ValueError("Missing required field 'vision' in ModelSupports")
- reasoning_effort = obj.get("reasoningEffort", False)
- return ModelSupports(vision=bool(vision), reasoning_effort=bool(reasoning_effort))
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["vision"] = self.vision
- result["reasoningEffort"] = self.reasoning_effort
- return result
-
-
-@dataclass
-class ModelCapabilities:
- """Model capabilities and limits"""
-
- supports: ModelSupports
- limits: ModelLimits
-
- @staticmethod
- def from_dict(obj: Any) -> ModelCapabilities:
- assert isinstance(obj, dict)
- supports_dict = obj.get("supports")
- limits_dict = obj.get("limits")
- if supports_dict is None or limits_dict is None:
- raise ValueError(
- f"Missing required fields in ModelCapabilities: supports={supports_dict}, "
- f"limits={limits_dict}"
- )
- supports = ModelSupports.from_dict(supports_dict)
- limits = ModelLimits.from_dict(limits_dict)
- return ModelCapabilities(supports=supports, limits=limits)
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["supports"] = self.supports.to_dict()
- result["limits"] = self.limits.to_dict()
- return result
-
-
-@dataclass
-class ModelPolicy:
- """Model policy state"""
-
- state: str # "enabled", "disabled", or "unconfigured"
- terms: str
-
- @staticmethod
- def from_dict(obj: Any) -> ModelPolicy:
- assert isinstance(obj, dict)
- state = obj.get("state")
- terms = obj.get("terms")
- if state is None or terms is None:
- raise ValueError(
- f"Missing required fields in ModelPolicy: state={state}, terms={terms}"
- )
- return ModelPolicy(state=str(state), terms=str(terms))
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["state"] = self.state
- result["terms"] = self.terms
- return result
-
-
-@dataclass
-class ModelBilling:
- """Model billing information"""
-
- multiplier: float
-
- @staticmethod
- def from_dict(obj: Any) -> ModelBilling:
- assert isinstance(obj, dict)
- multiplier = obj.get("multiplier")
- if multiplier is None:
- raise ValueError("Missing required field 'multiplier' in ModelBilling")
- return ModelBilling(multiplier=float(multiplier))
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["multiplier"] = self.multiplier
- return result
-
-
-@dataclass
-class ModelInfo:
- """Information about an available model"""
-
- id: str # Model identifier (e.g., "claude-sonnet-4.5")
- name: str # Display name
- capabilities: ModelCapabilities # Model capabilities and limits
- policy: ModelPolicy | None = None # Policy state
- billing: ModelBilling | None = None # Billing information
- # Supported reasoning effort levels (only present if model supports reasoning effort)
- supported_reasoning_efforts: list[str] | None = None
- # Default reasoning effort level (only present if model supports reasoning effort)
- default_reasoning_effort: str | None = None
-
- @staticmethod
- def from_dict(obj: Any) -> ModelInfo:
- assert isinstance(obj, dict)
- id = obj.get("id")
- name = obj.get("name")
- capabilities_dict = obj.get("capabilities")
- if id is None or name is None or capabilities_dict is None:
- raise ValueError(
- f"Missing required fields in ModelInfo: id={id}, name={name}, "
- f"capabilities={capabilities_dict}"
- )
- capabilities = ModelCapabilities.from_dict(capabilities_dict)
- policy_dict = obj.get("policy")
- policy = ModelPolicy.from_dict(policy_dict) if policy_dict else None
- billing_dict = obj.get("billing")
- billing = ModelBilling.from_dict(billing_dict) if billing_dict else None
- supported_reasoning_efforts = obj.get("supportedReasoningEfforts")
- default_reasoning_effort = obj.get("defaultReasoningEffort")
- return ModelInfo(
- id=str(id),
- name=str(name),
- capabilities=capabilities,
- policy=policy,
- billing=billing,
- supported_reasoning_efforts=supported_reasoning_efforts,
- default_reasoning_effort=default_reasoning_effort,
- )
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["id"] = self.id
- result["name"] = self.name
- result["capabilities"] = self.capabilities.to_dict()
- if self.policy is not None:
- result["policy"] = self.policy.to_dict()
- if self.billing is not None:
- result["billing"] = self.billing.to_dict()
- if self.supported_reasoning_efforts is not None:
- result["supportedReasoningEfforts"] = self.supported_reasoning_efforts
- if self.default_reasoning_effort is not None:
- result["defaultReasoningEffort"] = self.default_reasoning_effort
- return result
-
-
-@dataclass
-class SessionContext:
- """Working directory context for a session"""
-
- cwd: str # Working directory where the session was created
- gitRoot: str | None = None # Git repository root (if in a git repo)
- repository: str | None = None # GitHub repository in "owner/repo" format
- branch: str | None = None # Current git branch
-
- @staticmethod
- def from_dict(obj: Any) -> SessionContext:
- assert isinstance(obj, dict)
- cwd = obj.get("cwd")
- if cwd is None:
- raise ValueError("Missing required field 'cwd' in SessionContext")
- return SessionContext(
- cwd=str(cwd),
- gitRoot=obj.get("gitRoot"),
- repository=obj.get("repository"),
- branch=obj.get("branch"),
- )
-
- def to_dict(self) -> dict:
- result: dict = {"cwd": self.cwd}
- if self.gitRoot is not None:
- result["gitRoot"] = self.gitRoot
- if self.repository is not None:
- result["repository"] = self.repository
- if self.branch is not None:
- result["branch"] = self.branch
- return result
-
-
-@dataclass
-class SessionListFilter:
- """Filter options for listing sessions"""
-
- cwd: str | None = None # Filter by exact cwd match
- gitRoot: str | None = None # Filter by git root
- repository: str | None = None # Filter by repository (owner/repo format)
- branch: str | None = None # Filter by branch
-
- def to_dict(self) -> dict:
- result: dict = {}
- if self.cwd is not None:
- result["cwd"] = self.cwd
- if self.gitRoot is not None:
- result["gitRoot"] = self.gitRoot
- if self.repository is not None:
- result["repository"] = self.repository
- if self.branch is not None:
- result["branch"] = self.branch
- return result
-
-
-@dataclass
-class SessionMetadata:
- """Metadata about a session"""
-
- sessionId: str # Session identifier
- startTime: str # ISO 8601 timestamp when session was created
- modifiedTime: str # ISO 8601 timestamp when session was last modified
- isRemote: bool # Whether the session is remote
- summary: str | None = None # Optional summary of the session
- context: SessionContext | None = None # Working directory context
-
- @staticmethod
- def from_dict(obj: Any) -> SessionMetadata:
- assert isinstance(obj, dict)
- sessionId = obj.get("sessionId")
- startTime = obj.get("startTime")
- modifiedTime = obj.get("modifiedTime")
- isRemote = obj.get("isRemote")
- if sessionId is None or startTime is None or modifiedTime is None or isRemote is None:
- raise ValueError(
- f"Missing required fields in SessionMetadata: sessionId={sessionId}, "
- f"startTime={startTime}, modifiedTime={modifiedTime}, isRemote={isRemote}"
- )
- summary = obj.get("summary")
- context_dict = obj.get("context")
- context = SessionContext.from_dict(context_dict) if context_dict else None
- return SessionMetadata(
- sessionId=str(sessionId),
- startTime=str(startTime),
- modifiedTime=str(modifiedTime),
- isRemote=bool(isRemote),
- summary=summary,
- context=context,
- )
-
- def to_dict(self) -> dict:
- result: dict = {}
- result["sessionId"] = self.sessionId
- result["startTime"] = self.startTime
- result["modifiedTime"] = self.modifiedTime
- result["isRemote"] = self.isRemote
- if self.summary is not None:
- result["summary"] = self.summary
- if self.context is not None:
- result["context"] = self.context.to_dict()
- return result
-
-
-# Session Lifecycle Types (for TUI+server mode)
-
-SessionLifecycleEventType = Literal[
- "session.created",
- "session.deleted",
- "session.updated",
- "session.foreground",
- "session.background",
-]
-
-
-@dataclass
-class SessionLifecycleEventMetadata:
- """Metadata for session lifecycle events."""
-
- startTime: str
- modifiedTime: str
- summary: str | None = None
-
- @staticmethod
- def from_dict(data: dict) -> SessionLifecycleEventMetadata:
- return SessionLifecycleEventMetadata(
- startTime=data.get("startTime", ""),
- modifiedTime=data.get("modifiedTime", ""),
- summary=data.get("summary"),
- )
-
-
-@dataclass
-class SessionLifecycleEvent:
- """Session lifecycle event notification."""
-
- type: SessionLifecycleEventType
- sessionId: str
- metadata: SessionLifecycleEventMetadata | None = None
-
- @staticmethod
- def from_dict(data: dict) -> SessionLifecycleEvent:
- metadata = None
- if "metadata" in data and data["metadata"]:
- metadata = SessionLifecycleEventMetadata.from_dict(data["metadata"])
- return SessionLifecycleEvent(
- type=data.get("type", "session.updated"),
- sessionId=data.get("sessionId", ""),
- metadata=metadata,
- )
-
-
-# Handler types for session lifecycle events
-SessionLifecycleHandler = Callable[[SessionLifecycleEvent], None]
diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py
index 63d3e7322..ce946d2f3 100644
--- a/python/e2e/test_agent_and_compact_rpc.py
+++ b/python/e2e/test_agent_and_compact_rpc.py
@@ -2,8 +2,10 @@
import pytest
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
from copilot.generated.rpc import SessionAgentSelectParams
+from copilot.session import PermissionHandler
from .testharness import CLI_PATH, E2ETestContext
@@ -147,7 +149,7 @@ async def test_should_deselect_current_agent(self):
@pytest.mark.asyncio
async def test_should_return_empty_list_when_no_custom_agents_configured(self):
- """Test listing agents returns empty when none configured."""
+ """Test listing agents returns no custom agents when none configured."""
client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
try:
@@ -157,7 +159,13 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self):
)
result = await session.rpc.agent.list()
- assert result.agents == []
+ # The CLI may return built-in/default agents even when no custom agents
+ # are configured. Verify no custom test agents appear in the list.
+ custom_names = {"test-agent", "another-agent"}
+ for agent in result.agents:
+ assert agent.name not in custom_names, (
+ f"Expected no custom agents, but found {agent.name!r}"
+ )
await session.disconnect()
await client.stop()
diff --git a/python/e2e/test_ask_user.py b/python/e2e/test_ask_user.py
index fc4cc60b5..0a764029c 100644
--- a/python/e2e/test_ask_user.py
+++ b/python/e2e/test_ask_user.py
@@ -4,7 +4,7 @@
import pytest
-from copilot import PermissionHandler
+from copilot.session import PermissionHandler
from .testharness import E2ETestContext
diff --git a/python/e2e/test_client.py b/python/e2e/test_client.py
index d266991f7..4ea3fc843 100644
--- a/python/e2e/test_client.py
+++ b/python/e2e/test_client.py
@@ -2,7 +2,9 @@
import pytest
-from copilot import CopilotClient, PermissionHandler, StopError, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import StopError, SubprocessConfig
+from copilot.session import PermissionHandler
from .testharness import CLI_PATH
diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py
index beb51e74b..c6df2bffa 100644
--- a/python/e2e/test_compaction.py
+++ b/python/e2e/test_compaction.py
@@ -2,8 +2,8 @@
import pytest
-from copilot import PermissionHandler
from copilot.generated.session_events import SessionEventType
+from copilot.session import PermissionHandler
from .testharness import E2ETestContext
diff --git a/python/e2e/test_hooks.py b/python/e2e/test_hooks.py
index 2858d40f2..e355f3a80 100644
--- a/python/e2e/test_hooks.py
+++ b/python/e2e/test_hooks.py
@@ -4,7 +4,7 @@
import pytest
-from copilot import PermissionHandler
+from copilot.session import PermissionHandler
from .testharness import E2ETestContext
from .testharness.helper import write_file
diff --git a/python/e2e/test_mcp_and_agents.py b/python/e2e/test_mcp_and_agents.py
index c4bd89414..c6a590d6c 100644
--- a/python/e2e/test_mcp_and_agents.py
+++ b/python/e2e/test_mcp_and_agents.py
@@ -6,7 +6,7 @@
import pytest
-from copilot import CustomAgentConfig, MCPServerConfig, PermissionHandler
+from copilot.session import CustomAgentConfig, MCPServerConfig, PermissionHandler
from .testharness import E2ETestContext, get_final_assistant_message
diff --git a/python/e2e/test_multi_client.py b/python/e2e/test_multi_client.py
index c77ae86e1..2d866e8aa 100644
--- a/python/e2e/test_multi_client.py
+++ b/python/e2e/test_multi_client.py
@@ -13,15 +13,10 @@
import pytest_asyncio
from pydantic import BaseModel, Field
-from copilot import (
- CopilotClient,
- ExternalServerConfig,
- PermissionHandler,
- PermissionRequestResult,
- SubprocessConfig,
- ToolInvocation,
- define_tool,
-)
+from copilot import CopilotClient, define_tool
+from copilot.client import ExternalServerConfig, SubprocessConfig
+from copilot.session import PermissionHandler, PermissionRequestResult
+from copilot.tools import ToolInvocation
from .testharness import get_final_assistant_message
from .testharness.proxy import CapiProxy
diff --git a/python/e2e/test_permissions.py b/python/e2e/test_permissions.py
index a673d63b5..692c600e0 100644
--- a/python/e2e/test_permissions.py
+++ b/python/e2e/test_permissions.py
@@ -6,7 +6,7 @@
import pytest
-from copilot import PermissionHandler, PermissionRequest, PermissionRequestResult
+from copilot.session import PermissionHandler, PermissionRequest, PermissionRequestResult
from .testharness import E2ETestContext
from .testharness.helper import read_file, write_file
diff --git a/python/e2e/test_rpc.py b/python/e2e/test_rpc.py
index 814da067d..a86f874db 100644
--- a/python/e2e/test_rpc.py
+++ b/python/e2e/test_rpc.py
@@ -2,8 +2,10 @@
import pytest
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
from copilot.generated.rpc import PingParams
+from copilot.session import PermissionHandler
from .testharness import CLI_PATH, E2ETestContext
diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py
index ffb0cd2bc..c1a65e494 100644
--- a/python/e2e/test_session.py
+++ b/python/e2e/test_session.py
@@ -4,8 +4,10 @@
import pytest
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
-from copilot.types import Tool, ToolResult
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
+from copilot.session import PermissionHandler
+from copilot.tools import Tool, ToolResult
from .testharness import E2ETestContext, get_final_assistant_message, get_next_event_of_type
@@ -82,6 +84,33 @@ async def test_should_create_a_session_with_replaced_systemMessage_config(
system_message = _get_system_message(traffic[0])
assert system_message == test_system_message # Exact match
+ async def test_should_create_a_session_with_customized_systemMessage_config(
+ self, ctx: E2ETestContext
+ ):
+ custom_tone = "Respond in a warm, professional tone. Be thorough in explanations."
+ appended_content = "Always mention quarterly earnings."
+ session = await ctx.client.create_session(
+ on_permission_request=PermissionHandler.approve_all,
+ system_message={
+ "mode": "customize",
+ "sections": {
+ "tone": {"action": "replace", "content": custom_tone},
+ "code_change_rules": {"action": "remove"},
+ },
+ "content": appended_content,
+ },
+ )
+
+ assistant_message = await session.send_and_wait("Who are you?")
+ assert assistant_message is not None
+
+ # Validate the system message sent to the model
+ traffic = await ctx.get_exchanges()
+ system_message = _get_system_message(traffic[0])
+ assert custom_tone in system_message
+ assert appended_content in system_message
+ assert "" not in system_message
+
async def test_should_create_a_session_with_availableTools(self, ctx: E2ETestContext):
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
diff --git a/python/e2e/test_skills.py b/python/e2e/test_skills.py
index 9b0599975..feacae73b 100644
--- a/python/e2e/test_skills.py
+++ b/python/e2e/test_skills.py
@@ -7,7 +7,7 @@
import pytest
-from copilot import PermissionHandler
+from copilot.session import PermissionHandler
from .testharness import E2ETestContext
diff --git a/python/e2e/test_streaming_fidelity.py b/python/e2e/test_streaming_fidelity.py
index 05e977e12..c2e79814a 100644
--- a/python/e2e/test_streaming_fidelity.py
+++ b/python/e2e/test_streaming_fidelity.py
@@ -4,7 +4,9 @@
import pytest
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
+from copilot.session import PermissionHandler
from .testharness import E2ETestContext
diff --git a/python/e2e/test_system_message_transform.py b/python/e2e/test_system_message_transform.py
new file mode 100644
index 000000000..9ae170637
--- /dev/null
+++ b/python/e2e/test_system_message_transform.py
@@ -0,0 +1,123 @@
+"""
+Copyright (c) Microsoft Corporation.
+
+Tests for system message transform functionality
+"""
+
+import pytest
+
+from copilot import PermissionHandler
+
+from .testharness import E2ETestContext
+from .testharness.helper import write_file
+
+pytestmark = pytest.mark.asyncio(loop_scope="module")
+
+
+class TestSystemMessageTransform:
+ async def test_should_invoke_transform_callbacks_with_section_content(
+ self, ctx: E2ETestContext
+ ):
+ """Test that transform callbacks are invoked with the section content"""
+ identity_contents = []
+ tone_contents = []
+
+ async def identity_transform(content: str) -> str:
+ identity_contents.append(content)
+ return content
+
+ async def tone_transform(content: str) -> str:
+ tone_contents.append(content)
+ return content
+
+ session = await ctx.client.create_session(
+ system_message={
+ "mode": "customize",
+ "sections": {
+ "identity": {"action": identity_transform},
+ "tone": {"action": tone_transform},
+ },
+ },
+ on_permission_request=PermissionHandler.approve_all,
+ )
+
+ write_file(ctx.work_dir, "test.txt", "Hello transform!")
+
+ await session.send_and_wait("Read the contents of test.txt and tell me what it says")
+
+ # Both transform callbacks should have been invoked
+ assert len(identity_contents) > 0
+ assert len(tone_contents) > 0
+
+ # Callbacks should have received non-empty content
+ assert all(len(c) > 0 for c in identity_contents)
+ assert all(len(c) > 0 for c in tone_contents)
+
+ await session.disconnect()
+
+ async def test_should_apply_transform_modifications_to_section_content(
+ self, ctx: E2ETestContext
+ ):
+ """Test that transform modifications are applied to the section content"""
+
+ async def identity_transform(content: str) -> str:
+ return content + "\nTRANSFORM_MARKER"
+
+ session = await ctx.client.create_session(
+ system_message={
+ "mode": "customize",
+ "sections": {
+ "identity": {"action": identity_transform},
+ },
+ },
+ on_permission_request=PermissionHandler.approve_all,
+ )
+
+ write_file(ctx.work_dir, "hello.txt", "Hello!")
+
+ await session.send_and_wait("Read the contents of hello.txt")
+
+ # Verify the transform result was actually applied to the system message
+ traffic = await ctx.get_exchanges()
+ system_message = _get_system_message(traffic[0])
+ assert "TRANSFORM_MARKER" in system_message
+
+ await session.disconnect()
+
+ async def test_should_work_with_static_overrides_and_transforms_together(
+ self, ctx: E2ETestContext
+ ):
+ """Test that static overrides and transforms work together"""
+ identity_contents = []
+
+ async def identity_transform(content: str) -> str:
+ identity_contents.append(content)
+ return content
+
+ session = await ctx.client.create_session(
+ system_message={
+ "mode": "customize",
+ "sections": {
+ "safety": {"action": "remove"},
+ "identity": {"action": identity_transform},
+ },
+ },
+ on_permission_request=PermissionHandler.approve_all,
+ )
+
+ write_file(ctx.work_dir, "combo.txt", "Combo test!")
+
+ await session.send_and_wait("Read the contents of combo.txt and tell me what it says")
+
+ # The transform callback should have been invoked
+ assert len(identity_contents) > 0
+
+ await session.disconnect()
+
+
+def _get_system_message(exchange: dict) -> str:
+ messages = exchange.get("request", {}).get("messages", [])
+ for msg in messages:
+ if msg.get("role") == "system":
+ return msg.get("content", "")
+ return ""
diff --git a/python/e2e/test_tools.py b/python/e2e/test_tools.py
index 458897d49..4bb853976 100644
--- a/python/e2e/test_tools.py
+++ b/python/e2e/test_tools.py
@@ -5,12 +5,9 @@
import pytest
from pydantic import BaseModel, Field
-from copilot import (
- PermissionHandler,
- PermissionRequestResult,
- ToolInvocation,
- define_tool,
-)
+from copilot import define_tool
+from copilot.session import PermissionHandler, PermissionRequestResult
+from copilot.tools import ToolInvocation
from .testharness import E2ETestContext, get_final_assistant_message
diff --git a/python/e2e/test_tools_unit.py b/python/e2e/test_tools_unit.py
index c1a9163e1..c9c996f0e 100644
--- a/python/e2e/test_tools_unit.py
+++ b/python/e2e/test_tools_unit.py
@@ -5,8 +5,8 @@
import pytest
from pydantic import BaseModel, Field
-from copilot import ToolInvocation, ToolResult, define_tool
-from copilot.tools import _normalize_result
+from copilot import define_tool
+from copilot.tools import ToolInvocation, ToolResult, _normalize_result
class TestDefineTool:
diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py
index 27dce38a1..6a4bac6d2 100644
--- a/python/e2e/testharness/context.py
+++ b/python/e2e/testharness/context.py
@@ -10,7 +10,8 @@
import tempfile
from pathlib import Path
-from copilot import CopilotClient, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
from .proxy import CapiProxy
diff --git a/python/samples/chat.py b/python/samples/chat.py
index ee94c21fe..890191b19 100644
--- a/python/samples/chat.py
+++ b/python/samples/chat.py
@@ -1,6 +1,7 @@
import asyncio
-from copilot import CopilotClient, PermissionHandler
+from copilot import CopilotClient
+from copilot.session import PermissionHandler
BLUE = "\033[34m"
RESET = "\033[0m"
diff --git a/python/test_client.py b/python/test_client.py
index 9f8f38423..41f536d28 100644
--- a/python/test_client.py
+++ b/python/test_client.py
@@ -6,15 +6,16 @@
import pytest
-from copilot import (
- CopilotClient,
+from copilot import CopilotClient, define_tool
+from copilot.client import (
ExternalServerConfig,
- PermissionHandler,
- PermissionRequestResult,
+ ModelCapabilities,
+ ModelInfo,
+ ModelLimits,
+ ModelSupports,
SubprocessConfig,
- define_tool,
)
-from copilot.types import ModelCapabilities, ModelInfo, ModelLimits, ModelSupports
+from copilot.session import PermissionHandler, PermissionRequestResult
from e2e.testharness import CLI_PATH
diff --git a/python/test_telemetry.py b/python/test_telemetry.py
index aec38f816..d10ffeb9f 100644
--- a/python/test_telemetry.py
+++ b/python/test_telemetry.py
@@ -5,7 +5,7 @@
from unittest.mock import patch
from copilot._telemetry import get_trace_context, trace_context
-from copilot.types import SubprocessConfig, TelemetryConfig
+from copilot.client import SubprocessConfig, TelemetryConfig
class TestGetTraceContext:
diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts
index c44973fb1..a48ed47b6 100644
--- a/scripts/codegen/csharp.ts
+++ b/scripts/codegen/csharp.ts
@@ -522,11 +522,11 @@ namespace GitHub.Copilot.SDK;
lines.push(`/// Provides the base class from which all session events derive.`);
lines.push(`/// `);
lines.push(`[DebuggerDisplay("{DebuggerDisplay,nq}")]`);
- lines.push(`[JsonPolymorphic(`, ` TypeDiscriminatorPropertyName = "type",`, ` UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization)]`);
+ lines.push(`[JsonPolymorphic(`, ` TypeDiscriminatorPropertyName = "type",`, ` IgnoreUnrecognizedTypeDiscriminators = true)]`);
for (const variant of [...variants].sort((a, b) => a.typeName.localeCompare(b.typeName))) {
lines.push(`[JsonDerivedType(typeof(${variant.className}), "${variant.typeName}")]`);
}
- lines.push(`public abstract partial class SessionEvent`, `{`);
+ lines.push(`public partial class SessionEvent`, `{`);
lines.push(...xmlDocComment(baseDesc("id"), " "));
lines.push(` [JsonPropertyName("id")]`, ` public Guid Id { get; set; }`, "");
lines.push(...xmlDocComment(baseDesc("timestamp"), " "));
@@ -536,7 +536,7 @@ namespace GitHub.Copilot.SDK;
lines.push(...xmlDocComment(baseDesc("ephemeral"), " "));
lines.push(` [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]`, ` [JsonPropertyName("ephemeral")]`, ` public bool? Ephemeral { get; set; }`, "");
lines.push(` /// `, ` /// The event type discriminator.`, ` /// `);
- lines.push(` [JsonIgnore]`, ` public abstract string Type { get; }`, "");
+ lines.push(` [JsonIgnore]`, ` public virtual string Type => "unknown";`, "");
lines.push(` /// Deserializes a JSON string into a .`);
lines.push(` public static SessionEvent FromJson(string json) =>`, ` JsonSerializer.Deserialize(json, SessionEventsJsonContext.Default.SessionEvent)!;`, "");
lines.push(` /// Serializes this event to a JSON string.`);
diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts
index c467761d0..59abee298 100644
--- a/scripts/codegen/go.ts
+++ b/scripts/codegen/go.ts
@@ -45,6 +45,77 @@ function toGoFieldName(jsonName: string): string {
.join("");
}
+/**
+ * Post-process Go enum constants so every constant follows the canonical
+ * Go `TypeNameValue` convention. quicktype disambiguates collisions with
+ * whimsical prefixes (Purple, Fluffy, …) that we replace.
+ */
+function postProcessEnumConstants(code: string): string {
+ const renames = new Map();
+
+ // Match constant declarations inside const ( … ) blocks.
+ const constLineRe = /^\s+(\w+)\s+(\w+)\s*=\s*"([^"]+)"/gm;
+ let m;
+ while ((m = constLineRe.exec(code)) !== null) {
+ const [, constName, typeName, value] = m;
+ if (constName.startsWith(typeName)) continue;
+
+ // Use the same initialism logic as toPascalCase so "url" → "URL", "mcp" → "MCP", etc.
+ const valuePascal = value
+ .split(/[._-]/)
+ .map((w) => goInitialisms.has(w.toLowerCase()) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1))
+ .join("");
+ const desired = typeName + valuePascal;
+ if (constName !== desired) {
+ renames.set(constName, desired);
+ }
+ }
+
+ // Replace each const block in place, then fix switch-case references
+ // in marshal/unmarshal functions. This avoids renaming struct fields.
+
+ // Phase 1: Rename inside const ( … ) blocks
+ code = code.replace(/^(const \([\s\S]*?\n\))/gm, (block) => {
+ let b = block;
+ for (const [oldName, newName] of renames) {
+ b = b.replace(new RegExp(`\\b${oldName}\\b`, "g"), newName);
+ }
+ return b;
+ });
+
+ // Phase 2: Rename inside func bodies (marshal/unmarshal helpers use case statements)
+ code = code.replace(/^(func \([\s\S]*?\n\})/gm, (funcBlock) => {
+ let b = funcBlock;
+ for (const [oldName, newName] of renames) {
+ b = b.replace(new RegExp(`\\b${oldName}\\b`, "g"), newName);
+ }
+ return b;
+ });
+
+ return code;
+}
+
+/**
+ * Extract a mapping from (structName, jsonFieldName) → goFieldName
+ * so the wrapper code references the actual quicktype-generated field names.
+ */
+function extractFieldNames(qtCode: string): Map> {
+ const result = new Map>();
+ const structRe = /^type\s+(\w+)\s+struct\s*\{([^}]*)\}/gm;
+ let sm;
+ while ((sm = structRe.exec(qtCode)) !== null) {
+ const [, structName, body] = sm;
+ const fields = new Map();
+ const fieldRe = /^\s+(\w+)\s+[^`\n]+`json:"([^",]+)/gm;
+ let fm;
+ while ((fm = fieldRe.exec(body)) !== null) {
+ fields.set(fm[2], fm[1]);
+ }
+ result.set(structName, fields);
+ }
+ return result;
+}
+
async function formatGoFile(filePath: string): Promise {
try {
await execFileAsync("go", ["fmt", filePath]);
@@ -93,7 +164,7 @@ async function generateSessionEvents(schemaPath?: string): Promise {
`;
- const outPath = await writeGeneratedFile("go/generated_session_events.go", banner + result.lines.join("\n"));
+ const outPath = await writeGeneratedFile("go/generated_session_events.go", banner + postProcessEnumConstants(result.lines.join("\n")));
console.log(` ✓ ${outPath}`);
await formatGoFile(outPath);
@@ -154,22 +225,25 @@ async function generateRpc(schemaPath?: string): Promise {
rendererOptions: { package: "copilot", "just-types": "true" },
});
- // Build method wrappers
- const lines: string[] = [];
- lines.push(`// AUTO-GENERATED FILE - DO NOT EDIT`);
- lines.push(`// Generated from: api.schema.json`);
- lines.push(``);
- lines.push(`package rpc`);
- lines.push(``);
- lines.push(`import (`);
- lines.push(`\t"context"`);
- lines.push(`\t"encoding/json"`);
- lines.push(``);
- lines.push(`\t"github.com/github/copilot-sdk/go/internal/jsonrpc2"`);
- lines.push(`)`);
- lines.push(``);
+ // Post-process quicktype output: fix enum constant names
+ let qtCode = qtResult.lines.filter((l) => !l.startsWith("package ")).join("\n");
+ qtCode = postProcessEnumConstants(qtCode);
+ // Strip trailing whitespace from quicktype output (gofmt requirement)
+ qtCode = qtCode.replace(/[ \t]+$/gm, "");
- // Add quicktype-generated types (skip package line), annotating experimental types
+ // Extract actual type names generated by quicktype (may differ from toPascalCase)
+ const actualTypeNames = new Map();
+ const structRe = /^type\s+(\w+)\s+struct\b/gm;
+ let sm;
+ while ((sm = structRe.exec(qtCode)) !== null) {
+ actualTypeNames.set(sm[1].toLowerCase(), sm[1]);
+ }
+ const resolveType = (name: string): string => actualTypeNames.get(name.toLowerCase()) ?? name;
+
+ // Extract field name mappings (quicktype may rename fields to avoid Go keyword conflicts)
+ const fieldNames = extractFieldNames(qtCode);
+
+ // Annotate experimental data types
const experimentalTypeNames = new Set();
for (const method of allMethods) {
if (method.stability !== "experimental") continue;
@@ -179,9 +253,6 @@ async function generateRpc(schemaPath?: string): Promise {
experimentalTypeNames.add(baseName + "Params");
}
}
- let qtCode = qtResult.lines.filter((l) => !l.startsWith("package ")).join("\n");
- // Strip trailing whitespace from quicktype output (gofmt requirement)
- qtCode = qtCode.replace(/[ \t]+$/gm, "");
for (const typeName of experimentalTypeNames) {
qtCode = qtCode.replace(
new RegExp(`^(type ${typeName} struct)`, "m"),
@@ -190,17 +261,33 @@ async function generateRpc(schemaPath?: string): Promise {
}
// Remove trailing blank lines from quicktype output before appending
qtCode = qtCode.replace(/\n+$/, "");
+
+ // Build method wrappers
+ const lines: string[] = [];
+ lines.push(`// AUTO-GENERATED FILE - DO NOT EDIT`);
+ lines.push(`// Generated from: api.schema.json`);
+ lines.push(``);
+ lines.push(`package rpc`);
+ lines.push(``);
+ lines.push(`import (`);
+ lines.push(`\t"context"`);
+ lines.push(`\t"encoding/json"`);
+ lines.push(``);
+ lines.push(`\t"github.com/github/copilot-sdk/go/internal/jsonrpc2"`);
+ lines.push(`)`);
+ lines.push(``);
+
lines.push(qtCode);
lines.push(``);
// Emit ServerRpc
if (schema.server) {
- emitRpcWrapper(lines, schema.server, false);
+ emitRpcWrapper(lines, schema.server, false, resolveType, fieldNames);
}
// Emit SessionRpc
if (schema.session) {
- emitRpcWrapper(lines, schema.session, true);
+ emitRpcWrapper(lines, schema.session, true, resolveType, fieldNames);
}
const outPath = await writeGeneratedFile("go/rpc/generated_rpc.go", lines.join("\n"));
@@ -209,7 +296,7 @@ async function generateRpc(schemaPath?: string): Promise {
await formatGoFile(outPath);
}
-function emitRpcWrapper(lines: string[], node: Record, isSession: boolean): void {
+function emitRpcWrapper(lines: string[], node: Record, isSession: boolean, resolveType: (name: string) => string, fieldNames: Map>): void {
const groups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v));
const topLevelMethods = Object.entries(node).filter(([, v]) => isRpcMethod(v));
@@ -235,7 +322,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio
lines.push(``);
for (const [key, value] of Object.entries(groupNode as Record)) {
if (!isRpcMethod(value)) continue;
- emitMethod(lines, apiName, key, value, isSession, groupExperimental);
+ emitMethod(lines, apiName, key, value, isSession, resolveType, fieldNames, groupExperimental);
}
}
@@ -260,7 +347,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio
// Top-level methods (server only)
for (const [key, value] of topLevelMethods) {
if (!isRpcMethod(value)) continue;
- emitMethod(lines, wrapperName, key, value, isSession, false);
+ emitMethod(lines, wrapperName, key, value, isSession, resolveType, fieldNames, false);
}
// Compute key alignment for constructor composite literal (gofmt aligns key: value)
@@ -284,15 +371,15 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio
lines.push(``);
}
-function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean, groupExperimental = false): void {
+function emitMethod(lines: string[], receiver: string, name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, fieldNames: Map>, groupExperimental = false): void {
const methodName = toPascalCase(name);
- const resultType = toPascalCase(method.rpcMethod) + "Result";
+ const resultType = resolveType(toPascalCase(method.rpcMethod) + "Result");
const paramProps = method.params?.properties || {};
const requiredParams = new Set(method.params?.required || []);
const nonSessionParams = Object.keys(paramProps).filter((k) => k !== "sessionId");
const hasParams = isSession ? nonSessionParams.length > 0 : Object.keys(paramProps).length > 0;
- const paramsType = hasParams ? toPascalCase(method.rpcMethod) + "Params" : "";
+ const paramsType = hasParams ? resolveType(toPascalCase(method.rpcMethod) + "Params") : "";
if (method.stability === "experimental" && !groupExperimental) {
lines.push(`// Experimental: ${methodName} is an experimental API and may change or be removed in future versions.`);
@@ -308,7 +395,7 @@ function emitMethod(lines: string[], receiver: string, name: string, method: Rpc
if (hasParams) {
lines.push(`\tif params != nil {`);
for (const pName of nonSessionParams) {
- const goField = toGoFieldName(pName);
+ const goField = fieldNames.get(paramsType)?.get(pName) ?? toGoFieldName(pName);
const isOptional = !requiredParams.has(pName);
if (isOptional) {
// Optional fields are pointers - only add when non-nil and dereference
diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts
index cbbc3df38..0340cf1f1 100644
--- a/scripts/codegen/python.ts
+++ b/scripts/codegen/python.ts
@@ -32,12 +32,37 @@ import {
* - Callable from collections.abc instead of typing
* - Clean up unused typing imports
*/
+function replaceBalancedBrackets(code: string, prefix: string, replacer: (inner: string) => string): string {
+ let result = "";
+ let i = 0;
+ while (i < code.length) {
+ const idx = code.indexOf(prefix + "[", i);
+ if (idx === -1) {
+ result += code.slice(i);
+ break;
+ }
+ result += code.slice(i, idx);
+ const start = idx + prefix.length + 1; // after '['
+ let depth = 1;
+ let j = start;
+ while (j < code.length && depth > 0) {
+ if (code[j] === "[") depth++;
+ else if (code[j] === "]") depth--;
+ j++;
+ }
+ const inner = code.slice(start, j - 1);
+ result += replacer(inner);
+ i = j;
+ }
+ return result;
+}
+
function modernizePython(code: string): string {
- // Replace Optional[X] with X | None (handles nested brackets)
- code = code.replace(/Optional\[([^\[\]]*(?:\[[^\[\]]*\])*[^\[\]]*)\]/g, "$1 | None");
+ // Replace Optional[X] with X | None (handles arbitrarily nested brackets)
+ code = replaceBalancedBrackets(code, "Optional", (inner) => `${inner} | None`);
// Replace Union[X, Y] with X | Y
- code = code.replace(/Union\[([^\[\]]*(?:\[[^\[\]]*\])*[^\[\]]*)\]/g, (_match, inner: string) => {
+ code = replaceBalancedBrackets(code, "Union", (inner) => {
return inner.split(",").map((s: string) => s.trim()).join(" | ");
});
@@ -234,6 +259,16 @@ async function generateRpc(schemaPath?: string): Promise {
);
}
+ // Extract actual class names generated by quicktype (may differ from toPascalCase,
+ // e.g. quicktype produces "SessionMCPList" not "SessionMcpList")
+ const actualTypeNames = new Map();
+ const classRe = /^class\s+(\w+)\b/gm;
+ let cm;
+ while ((cm = classRe.exec(typesCode)) !== null) {
+ actualTypeNames.set(cm[1].toLowerCase(), cm[1]);
+ }
+ const resolveType = (name: string): string => actualTypeNames.get(name.toLowerCase()) ?? name;
+
const lines: string[] = [];
lines.push(`"""
AUTO-GENERATED FILE - DO NOT EDIT
@@ -258,17 +293,17 @@ def _timeout_kwargs(timeout: float | None) -> dict:
// Emit RPC wrapper classes
if (schema.server) {
- emitRpcWrapper(lines, schema.server, false);
+ emitRpcWrapper(lines, schema.server, false, resolveType);
}
if (schema.session) {
- emitRpcWrapper(lines, schema.session, true);
+ emitRpcWrapper(lines, schema.session, true, resolveType);
}
const outPath = await writeGeneratedFile("python/copilot/generated/rpc.py", lines.join("\n"));
console.log(` ✓ ${outPath}`);
}
-function emitRpcWrapper(lines: string[], node: Record, isSession: boolean): void {
+function emitRpcWrapper(lines: string[], node: Record, isSession: boolean, resolveType: (name: string) => string): void {
const groups = Object.entries(node).filter(([, v]) => typeof v === "object" && v !== null && !isRpcMethod(v));
const topLevelMethods = Object.entries(node).filter(([, v]) => isRpcMethod(v));
@@ -298,7 +333,7 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio
lines.push(``);
for (const [key, value] of Object.entries(groupNode as Record)) {
if (!isRpcMethod(value)) continue;
- emitMethod(lines, key, value, isSession, groupExperimental);
+ emitMethod(lines, key, value, isSession, resolveType, groupExperimental);
}
lines.push(``);
}
@@ -327,19 +362,19 @@ function emitRpcWrapper(lines: string[], node: Record, isSessio
// Top-level methods
for (const [key, value] of topLevelMethods) {
if (!isRpcMethod(value)) continue;
- emitMethod(lines, key, value, isSession, false);
+ emitMethod(lines, key, value, isSession, resolveType, false);
}
lines.push(``);
}
-function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, groupExperimental = false): void {
+function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: boolean, resolveType: (name: string) => string, groupExperimental = false): void {
const methodName = toSnakeCase(name);
- const resultType = toPascalCase(method.rpcMethod) + "Result";
+ const resultType = resolveType(toPascalCase(method.rpcMethod) + "Result");
const paramProps = method.params?.properties || {};
const nonSessionParams = Object.keys(paramProps).filter((k) => k !== "sessionId");
const hasParams = isSession ? nonSessionParams.length > 0 : Object.keys(paramProps).length > 0;
- const paramsType = toPascalCase(method.rpcMethod) + "Params";
+ const paramsType = resolveType(toPascalCase(method.rpcMethod) + "Params");
// Build signature with typed params + optional timeout
const sig = hasParams
diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json
index bf9564d9a..c8ec038fb 100644
--- a/test/harness/package-lock.json
+++ b/test/harness/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
- "@github/copilot": "^1.0.4",
+ "@github/copilot": "^1.0.10-0",
"@modelcontextprotocol/sdk": "^1.26.0",
"@types/node": "^25.3.3",
"openai": "^6.17.0",
@@ -462,27 +462,27 @@
}
},
"node_modules/@github/copilot": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.4.tgz",
- "integrity": "sha512-IpPg+zYplLu4F4lmatEDdR/1Y/jJ9cGWt89m3K3H4YSfYrZ5Go4UlM28llulYCG7sVdQeIGauQN1/KiBI/Rocg==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.10-0.tgz",
+ "integrity": "sha512-LmVe3yVDamZc4cbZeyprZ6WjTME9Z4UcB5YWnEagtXJ19KP5PBKbBZVG7pZnQHL2/IHZ/dqcZW3IHMgYDoqDvg==",
"dev": true,
"license": "SEE LICENSE IN LICENSE.md",
"bin": {
"copilot": "npm-loader.js"
},
"optionalDependencies": {
- "@github/copilot-darwin-arm64": "1.0.4",
- "@github/copilot-darwin-x64": "1.0.4",
- "@github/copilot-linux-arm64": "1.0.4",
- "@github/copilot-linux-x64": "1.0.4",
- "@github/copilot-win32-arm64": "1.0.4",
- "@github/copilot-win32-x64": "1.0.4"
+ "@github/copilot-darwin-arm64": "1.0.10-0",
+ "@github/copilot-darwin-x64": "1.0.10-0",
+ "@github/copilot-linux-arm64": "1.0.10-0",
+ "@github/copilot-linux-x64": "1.0.10-0",
+ "@github/copilot-win32-arm64": "1.0.10-0",
+ "@github/copilot-win32-x64": "1.0.10-0"
}
},
"node_modules/@github/copilot-darwin-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.4.tgz",
- "integrity": "sha512-/YGGhv6cp0ItolsF0HsLq2KmesA4atn0IEYApBs770fzJ8OP2pkOEzrxo3gWU3wc7fHF2uDB1RrJEZ7QSFLdEQ==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.10-0.tgz",
+ "integrity": "sha512-u5CbflcTpvc4E48E0jrqbN3Y5hWzValMs21RR6L+GDjQpPI2pvDeUWAJZ03Y7qQ2Uk3KZ+hOIJWJvje9VHxrDQ==",
"cpu": [
"arm64"
],
@@ -497,9 +497,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.4.tgz",
- "integrity": "sha512-gwn2QjZbc1SqPVSAtDMesU1NopyHZT8Qsn37xPfznpV9s94KVyX4TTiDZaUwfnI0wr8kVHBL46RPLNz6I8kR9A==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.10-0.tgz",
+ "integrity": "sha512-4y5OXhAfWX+il9slhrq7v8ONzq+Hpw46ktnz7l1fAZKdmn+dzmFVCvr6pJPr5Az78cAKBuN+Gt4eeSNaxuKCmA==",
"cpu": [
"x64"
],
@@ -514,9 +514,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.4.tgz",
- "integrity": "sha512-92vzHKxN55BpI76sP/5fXIXfat1gzAhsq4bNLqLENGfZyMP/25OiVihCZuQHnvxzXaHBITFGUvtxfdll2kbcng==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.10-0.tgz",
+ "integrity": "sha512-j+Z/ZahEIT5SCblUqOJ2+2glWeIIUPKXXFS5bbu5kFZ9Xyag37FBvTjyxDeB02dpSKKDD4xbMVjcijFbtyr1PA==",
"cpu": [
"arm64"
],
@@ -531,9 +531,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.4.tgz",
- "integrity": "sha512-wQvpwf4/VMTnSmWyYzq07Xg18Vxg7aZ5NVkkXqlLTuXRASW0kvCCb5USEtXHHzR7E6rJztkhCjFRE1bZW8jAGw==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.10-0.tgz",
+ "integrity": "sha512-S8IfuiMZWwnFW1v0vOGHalPIXq/75kL/RpZCYd1sleQA/yztCNNjxH9tNpXsdZnhYrAgU/3hqseWq5hbz8xjxA==",
"cpu": [
"x64"
],
@@ -548,9 +548,9 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.4.tgz",
- "integrity": "sha512-zOvD/5GVxDf0ZdlTkK+m55Vs55xuHNmACX50ZO2N23ZGG2dmkdS4mkruL59XB5ISgrOfeqvnqrwTFHbmPZtLfw==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.10-0.tgz",
+ "integrity": "sha512-6HJErp91fLrwIkoXegLK8SXjHzLgbl9GF+QdOtUGqZ915UUfXcchef0tQjN8u35yNLEW82VnAmft/PJ9Ok2UhQ==",
"cpu": [
"arm64"
],
@@ -565,9 +565,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.4.tgz",
- "integrity": "sha512-yQenHMdkV0b77mF6aLM60TuwtNZ592TluptVDF+80Sj2zPfCpLyvrRh2FCIHRtuwTy4BfxETh2hCFHef8E6IOw==",
+ "version": "1.0.10-0",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.10-0.tgz",
+ "integrity": "sha512-AQwZYHoarRACbmPUPmH7gPOEomTAtDusCn65ancI3BoWGj9fzAgZEZ5JSaR3N/VUoXWoEbSe+PcH380ZYwsPag==",
"cpu": [
"x64"
],
diff --git a/test/harness/package.json b/test/harness/package.json
index 9f336dfd4..25117cac9 100644
--- a/test/harness/package.json
+++ b/test/harness/package.json
@@ -11,7 +11,7 @@
"test": "vitest run"
},
"devDependencies": {
- "@github/copilot": "^1.0.4",
+ "@github/copilot": "^1.0.10-0",
"@modelcontextprotocol/sdk": "^1.26.0",
"@types/node": "^25.3.3",
"openai": "^6.17.0",
diff --git a/test/scenarios/auth/byok-anthropic/python/main.py b/test/scenarios/auth/byok-anthropic/python/main.py
index 995002070..3ad893ba5 100644
--- a/test/scenarios/auth/byok-anthropic/python/main.py
+++ b/test/scenarios/auth/byok-anthropic/python/main.py
@@ -1,7 +1,8 @@
import asyncio
import os
import sys
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-sonnet-4-20250514")
@@ -18,20 +19,19 @@ async def main():
))
try:
- session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model=ANTHROPIC_MODEL,
- provider={
+ session = await client.create_session({
+ "model": ANTHROPIC_MODEL,
+ "provider": {
"type": "anthropic",
"base_url": ANTHROPIC_BASE_URL,
"api_key": ANTHROPIC_API_KEY,
},
- available_tools=[],
- system_message={
+ "available_tools": [],
+ "system_message": {
"mode": "replace",
"content": "You are a helpful assistant. Answer concisely.",
},
- )
+ })
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/auth/byok-azure/python/main.py b/test/scenarios/auth/byok-azure/python/main.py
index 57a49f2a5..1ae214261 100644
--- a/test/scenarios/auth/byok-azure/python/main.py
+++ b/test/scenarios/auth/byok-azure/python/main.py
@@ -1,7 +1,8 @@
import asyncio
import os
import sys
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
AZURE_OPENAI_ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT")
AZURE_OPENAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY")
@@ -19,10 +20,9 @@ async def main():
))
try:
- session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model=AZURE_OPENAI_MODEL,
- provider={
+ session = await client.create_session({
+ "model": AZURE_OPENAI_MODEL,
+ "provider": {
"type": "azure",
"base_url": AZURE_OPENAI_ENDPOINT,
"api_key": AZURE_OPENAI_API_KEY,
@@ -30,12 +30,12 @@ async def main():
"api_version": AZURE_API_VERSION,
},
},
- available_tools=[],
- system_message={
+ "available_tools": [],
+ "system_message": {
"mode": "replace",
"content": "You are a helpful assistant. Answer concisely.",
},
- )
+ })
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/auth/byok-ollama/python/main.py b/test/scenarios/auth/byok-ollama/python/main.py
index 87dad5866..78019acd7 100644
--- a/test/scenarios/auth/byok-ollama/python/main.py
+++ b/test/scenarios/auth/byok-ollama/python/main.py
@@ -1,7 +1,8 @@
import asyncio
import os
import sys
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434/v1")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2:3b")
@@ -17,19 +18,18 @@ async def main():
))
try:
- session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model=OLLAMA_MODEL,
- provider={
+ session = await client.create_session({
+ "model": OLLAMA_MODEL,
+ "provider": {
"type": "openai",
"base_url": OLLAMA_BASE_URL,
},
- available_tools=[],
- system_message={
+ "available_tools": [],
+ "system_message": {
"mode": "replace",
"content": COMPACT_SYSTEM_PROMPT,
},
- )
+ })
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/auth/byok-openai/python/main.py b/test/scenarios/auth/byok-openai/python/main.py
index fadd1c79d..8362963b2 100644
--- a/test/scenarios/auth/byok-openai/python/main.py
+++ b/test/scenarios/auth/byok-openai/python/main.py
@@ -1,7 +1,8 @@
import asyncio
import os
import sys
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "claude-haiku-4.5")
@@ -18,15 +19,14 @@ async def main():
))
try:
- session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model=OPENAI_MODEL,
- provider={
+ session = await client.create_session({
+ "model": OPENAI_MODEL,
+ "provider": {
"type": "openai",
"base_url": OPENAI_BASE_URL,
"api_key": OPENAI_API_KEY,
},
- )
+ })
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/auth/gh-app/python/main.py b/test/scenarios/auth/gh-app/python/main.py
index e7f640ae9..afba29254 100644
--- a/test/scenarios/auth/gh-app/python/main.py
+++ b/test/scenarios/auth/gh-app/python/main.py
@@ -4,7 +4,8 @@
import time
import urllib.request
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
DEVICE_CODE_URL = "https://github.com/login/device/code"
@@ -84,7 +85,7 @@ async def main():
))
try:
- session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session = await client.create_session({"model": "claude-haiku-4.5"})
response = await session.send_and_wait("What is the capital of France?")
if response:
print(response.data.content)
diff --git a/test/scenarios/bundling/app-backend-to-server/python/main.py b/test/scenarios/bundling/app-backend-to-server/python/main.py
index c9ab35bce..2684a30b8 100644
--- a/test/scenarios/bundling/app-backend-to-server/python/main.py
+++ b/test/scenarios/bundling/app-backend-to-server/python/main.py
@@ -5,7 +5,8 @@
import urllib.request
from flask import Flask, request, jsonify
-from copilot import CopilotClient, PermissionHandler, ExternalServerConfig
+from copilot import CopilotClient
+from copilot.client import ExternalServerConfig
app = Flask(__name__)
@@ -16,7 +17,7 @@ async def ask_copilot(prompt: str) -> str:
client = CopilotClient(ExternalServerConfig(url=CLI_URL))
try:
- session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session = await client.create_session({"model": "claude-haiku-4.5"})
response = await session.send_and_wait(prompt)
diff --git a/test/scenarios/bundling/app-direct-server/python/main.py b/test/scenarios/bundling/app-direct-server/python/main.py
index 07eb74e20..b441bec51 100644
--- a/test/scenarios/bundling/app-direct-server/python/main.py
+++ b/test/scenarios/bundling/app-direct-server/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, ExternalServerConfig
+from copilot import CopilotClient
+from copilot.client import ExternalServerConfig
async def main():
@@ -9,7 +10,7 @@ async def main():
))
try:
- session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session = await client.create_session({"model": "claude-haiku-4.5"})
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/bundling/container-proxy/python/main.py b/test/scenarios/bundling/container-proxy/python/main.py
index 07eb74e20..b441bec51 100644
--- a/test/scenarios/bundling/container-proxy/python/main.py
+++ b/test/scenarios/bundling/container-proxy/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, ExternalServerConfig
+from copilot import CopilotClient
+from copilot.client import ExternalServerConfig
async def main():
@@ -9,7 +10,7 @@ async def main():
))
try:
- session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session = await client.create_session({"model": "claude-haiku-4.5"})
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py
index 382f9c4f9..39ce2bb81 100644
--- a/test/scenarios/bundling/fully-bundled/python/main.py
+++ b/test/scenarios/bundling/fully-bundled/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -10,7 +11,7 @@ async def main():
))
try:
- session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session = await client.create_session({"model": "claude-haiku-4.5"})
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py
index bc9782b6b..dbfceb22a 100644
--- a/test/scenarios/callbacks/hooks/python/main.py
+++ b/test/scenarios/callbacks/hooks/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
hook_log: list[str] = []
@@ -47,16 +48,18 @@ async def main():
try:
session = await client.create_session(
- on_permission_request=auto_approve_permission,
- model="claude-haiku-4.5",
- hooks={
- "on_session_start": on_session_start,
- "on_session_end": on_session_end,
- "on_pre_tool_use": on_pre_tool_use,
- "on_post_tool_use": on_post_tool_use,
- "on_user_prompt_submitted": on_user_prompt_submitted,
- "on_error_occurred": on_error_occurred,
- },
+ {
+ "model": "claude-haiku-4.5",
+ "on_permission_request": auto_approve_permission,
+ "hooks": {
+ "on_session_start": on_session_start,
+ "on_session_end": on_session_end,
+ "on_pre_tool_use": on_pre_tool_use,
+ "on_post_tool_use": on_post_tool_use,
+ "on_user_prompt_submitted": on_user_prompt_submitted,
+ "on_error_occurred": on_error_occurred,
+ },
+ }
)
response = await session.send_and_wait(
diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py
index e4de98a9a..de788e5fb 100644
--- a/test/scenarios/callbacks/permissions/python/main.py
+++ b/test/scenarios/callbacks/permissions/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
# Track which tools requested permission
permission_log: list[str] = []
@@ -23,9 +24,11 @@ async def main():
try:
session = await client.create_session(
- on_permission_request=log_permission,
- model="claude-haiku-4.5",
- hooks={"on_pre_tool_use": auto_approve_tool},
+ {
+ "model": "claude-haiku-4.5",
+ "on_permission_request": log_permission,
+ "hooks": {"on_pre_tool_use": auto_approve_tool},
+ }
)
response = await session.send_and_wait(
diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py
index 92981861d..0c23e6b15 100644
--- a/test/scenarios/callbacks/user-input/python/main.py
+++ b/test/scenarios/callbacks/user-input/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
input_log: list[str] = []
@@ -27,10 +28,12 @@ async def main():
try:
session = await client.create_session(
- on_permission_request=auto_approve_permission,
- model="claude-haiku-4.5",
- on_user_input_request=handle_user_input,
- hooks={"on_pre_tool_use": auto_approve_tool},
+ {
+ "model": "claude-haiku-4.5",
+ "on_permission_request": auto_approve_permission,
+ "on_user_input_request": handle_user_input,
+ "hooks": {"on_pre_tool_use": auto_approve_tool},
+ }
)
response = await session.send_and_wait(
diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py
index 55f1cb394..ece50a662 100644
--- a/test/scenarios/modes/default/python/main.py
+++ b/test/scenarios/modes/default/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -10,7 +11,9 @@ async def main():
))
try:
- session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session = await client.create_session({
+ "model": "claude-haiku-4.5",
+ })
response = await session.send_and_wait("Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.")
if response:
diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py
index 22f321b22..722c1e5e1 100644
--- a/test/scenarios/modes/minimal/python/main.py
+++ b/test/scenarios/modes/minimal/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -10,15 +11,14 @@ async def main():
))
try:
- session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- available_tools=[],
- system_message={
+ session = await client.create_session({
+ "model": "claude-haiku-4.5",
+ "available_tools": [],
+ "system_message": {
"mode": "replace",
"content": "You have no tools. Respond with text only.",
},
- )
+ })
response = await session.send_and_wait("Use the grep tool to search for 'SDK' in README.md.")
if response:
diff --git a/test/scenarios/prompts/attachments/README.md b/test/scenarios/prompts/attachments/README.md
index d61a26e57..76b76751d 100644
--- a/test/scenarios/prompts/attachments/README.md
+++ b/test/scenarios/prompts/attachments/README.md
@@ -39,7 +39,7 @@ Demonstrates sending **file attachments** alongside a prompt using the Copilot S
|----------|------------------------|
| TypeScript | `attachments: [{ type: "blob", data: base64Data, mimeType: "image/png" }]` |
| Python | `"attachments": [{"type": "blob", "data": base64_data, "mimeType": "image/png"}]` |
-| Go | `Attachments: []copilot.Attachment{{Type: copilot.Blob, Data: &data, MIMEType: &mime}}` |
+| Go | `Attachments: []copilot.Attachment{{Type: copilot.AttachmentTypeBlob, Data: &data, MIMEType: &mime}}` |
## Sample Data
diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py
index 37654e269..fdf259c6a 100644
--- a/test/scenarios/prompts/attachments/python/main.py
+++ b/test/scenarios/prompts/attachments/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
SYSTEM_PROMPT = """You are a helpful assistant. Answer questions about attached files concisely."""
@@ -13,10 +14,11 @@ async def main():
try:
session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- system_message={"mode": "replace", "content": SYSTEM_PROMPT},
- available_tools=[],
+ {
+ "model": "claude-haiku-4.5",
+ "system_message": {"mode": "replace", "content": SYSTEM_PROMPT},
+ "available_tools": [],
+ }
)
sample_file = os.path.join(os.path.dirname(__file__), "..", "sample-data.txt")
diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py
index 8baed649d..122f44895 100644
--- a/test/scenarios/prompts/reasoning-effort/python/main.py
+++ b/test/scenarios/prompts/reasoning-effort/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -10,16 +11,15 @@ async def main():
))
try:
- session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-opus-4.6",
- reasoning_effort="low",
- available_tools=[],
- system_message={
+ session = await client.create_session({
+ "model": "claude-opus-4.6",
+ "reasoning_effort": "low",
+ "available_tools": [],
+ "system_message": {
"mode": "replace",
"content": "You are a helpful assistant. Answer concisely.",
},
- )
+ })
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py
index 15d354258..b77c1e4a1 100644
--- a/test/scenarios/prompts/system-message/python/main.py
+++ b/test/scenarios/prompts/system-message/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
PIRATE_PROMPT = """You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout."""
@@ -13,10 +14,11 @@ async def main():
try:
session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- system_message={"mode": "replace", "content": PIRATE_PROMPT},
- available_tools=[],
+ {
+ "model": "claude-haiku-4.5",
+ "system_message": {"mode": "replace", "content": PIRATE_PROMPT},
+ "available_tools": [],
+ }
)
response = await session.send_and_wait(
diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py
index 5c3994c4c..a32dc5e10 100644
--- a/test/scenarios/sessions/concurrent-sessions/python/main.py
+++ b/test/scenarios/sessions/concurrent-sessions/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
PIRATE_PROMPT = "You are a pirate. Always say Arrr!"
ROBOT_PROMPT = "You are a robot. Always say BEEP BOOP!"
@@ -15,16 +16,18 @@ async def main():
try:
session1, session2 = await asyncio.gather(
client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- system_message={"mode": "replace", "content": PIRATE_PROMPT},
- available_tools=[],
+ {
+ "model": "claude-haiku-4.5",
+ "system_message": {"mode": "replace", "content": PIRATE_PROMPT},
+ "available_tools": [],
+ }
),
client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- system_message={"mode": "replace", "content": ROBOT_PROMPT},
- available_tools=[],
+ {
+ "model": "claude-haiku-4.5",
+ "system_message": {"mode": "replace", "content": ROBOT_PROMPT},
+ "available_tools": [],
+ }
),
)
diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py
index 30aa40cd1..724dc155d 100644
--- a/test/scenarios/sessions/infinite-sessions/python/main.py
+++ b/test/scenarios/sessions/infinite-sessions/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -10,20 +11,19 @@ async def main():
))
try:
- session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- available_tools=[],
- system_message={
+ session = await client.create_session({
+ "model": "claude-haiku-4.5",
+ "available_tools": [],
+ "system_message": {
"mode": "replace",
"content": "You are a helpful assistant. Answer concisely in one sentence.",
},
- infinite_sessions={
+ "infinite_sessions": {
"enabled": True,
"background_compaction_threshold": 0.80,
"buffer_exhaustion_threshold": 0.95,
},
- )
+ })
prompts = [
"What is the capital of France?",
diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py
index 049ca1f83..ccb9c69f0 100644
--- a/test/scenarios/sessions/session-resume/python/main.py
+++ b/test/scenarios/sessions/session-resume/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -12,9 +13,10 @@ async def main():
try:
# 1. Create a session
session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- available_tools=[],
+ {
+ "model": "claude-haiku-4.5",
+ "available_tools": [],
+ }
)
# 2. Send the secret word
@@ -26,7 +28,7 @@ async def main():
session_id = session.session_id
# 4. Resume the session with the same ID
- resumed = await client.resume_session(session_id, on_permission_request=PermissionHandler.approve_all)
+ resumed = await client.resume_session(session_id)
print("Session resumed")
# 5. Ask for the secret word
diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py
index 20fd4902e..e2312cd14 100644
--- a/test/scenarios/sessions/streaming/python/main.py
+++ b/test/scenarios/sessions/streaming/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -11,9 +12,10 @@ async def main():
try:
session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- streaming=True,
+ {
+ "model": "claude-haiku-4.5",
+ "streaming": True,
+ }
)
chunk_count = 0
diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py
index c30107a2f..d4c45950f 100644
--- a/test/scenarios/tools/custom-agents/python/main.py
+++ b/test/scenarios/tools/custom-agents/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -11,17 +12,18 @@ async def main():
try:
session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- custom_agents=[
- {
- "name": "researcher",
- "display_name": "Research Agent",
- "description": "A research agent that can only read and search files, not modify them",
- "tools": ["grep", "glob", "view"],
- "prompt": "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.",
- },
- ],
+ {
+ "model": "claude-haiku-4.5",
+ "custom_agents": [
+ {
+ "name": "researcher",
+ "display_name": "Research Agent",
+ "description": "A research agent that can only read and search files, not modify them",
+ "tools": ["grep", "glob", "view"],
+ "prompt": "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.",
+ },
+ ],
+ }
)
response = await session.send_and_wait(
diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py
index 9edd04115..2fa81b82d 100644
--- a/test/scenarios/tools/mcp-servers/python/main.py
+++ b/test/scenarios/tools/mcp-servers/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -22,7 +23,8 @@ async def main():
"args": args,
}
- session_kwargs = {
+ session_config = {
+ "model": "claude-haiku-4.5",
"available_tools": [],
"system_message": {
"mode": "replace",
@@ -30,11 +32,9 @@ async def main():
},
}
if mcp_servers:
- session_kwargs["mcp_servers"] = mcp_servers
+ session_config["mcp_servers"] = mcp_servers
- session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5", **session_kwargs
- )
+ session = await client.create_session(session_config)
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py
index c9a8047ec..c3eeb6a17 100644
--- a/test/scenarios/tools/no-tools/python/main.py
+++ b/test/scenarios/tools/no-tools/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
SYSTEM_PROMPT = """You are a minimal assistant with no tools available.
You cannot execute code, read files, edit files, search, or perform any actions.
@@ -16,10 +17,11 @@ async def main():
try:
session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- system_message={"mode": "replace", "content": SYSTEM_PROMPT},
- available_tools=[],
+ {
+ "model": "claude-haiku-4.5",
+ "system_message": {"mode": "replace", "content": SYSTEM_PROMPT},
+ "available_tools": [],
+ }
)
response = await session.send_and_wait(
diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py
index afa871d83..3ec9fb2ee 100644
--- a/test/scenarios/tools/skills/python/main.py
+++ b/test/scenarios/tools/skills/python/main.py
@@ -2,7 +2,8 @@
import os
from pathlib import Path
-from copilot import CopilotClient, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py
index 668bca197..9da4ca571 100644
--- a/test/scenarios/tools/tool-filtering/python/main.py
+++ b/test/scenarios/tools/tool-filtering/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
SYSTEM_PROMPT = """You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available."""
@@ -13,10 +14,11 @@ async def main():
try:
session = await client.create_session(
- on_permission_request=PermissionHandler.approve_all,
- model="claude-haiku-4.5",
- system_message={"mode": "replace", "content": SYSTEM_PROMPT},
- available_tools=["grep", "glob", "view"],
+ {
+ "model": "claude-haiku-4.5",
+ "system_message": {"mode": "replace", "content": SYSTEM_PROMPT},
+ "available_tools": ["grep", "glob", "view"],
+ }
)
response = await session.send_and_wait(
diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py
index 73c539fe1..687933973 100644
--- a/test/scenarios/tools/tool-overrides/python/main.py
+++ b/test/scenarios/tools/tool-overrides/python/main.py
@@ -3,7 +3,9 @@
from pydantic import BaseModel, Field
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig, define_tool
+from copilot import CopilotClient, define_tool
+from copilot.client import SubprocessConfig
+from copilot.session import PermissionHandler
class GrepParams(BaseModel):
diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py
index 92f2593a6..f7635c6c6 100644
--- a/test/scenarios/tools/virtual-filesystem/python/main.py
+++ b/test/scenarios/tools/virtual-filesystem/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, SubprocessConfig, define_tool
+from copilot import CopilotClient, define_tool
+from copilot.client import SubprocessConfig
from pydantic import BaseModel, Field
# In-memory virtual filesystem
diff --git a/test/scenarios/transport/reconnect/python/main.py b/test/scenarios/transport/reconnect/python/main.py
index d1b8a5696..d1d4505a8 100644
--- a/test/scenarios/transport/reconnect/python/main.py
+++ b/test/scenarios/transport/reconnect/python/main.py
@@ -1,7 +1,8 @@
import asyncio
import os
import sys
-from copilot import CopilotClient, PermissionHandler, ExternalServerConfig
+from copilot import CopilotClient
+from copilot.client import ExternalServerConfig
async def main():
@@ -12,7 +13,7 @@ async def main():
try:
# First session
print("--- Session 1 ---")
- session1 = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session1 = await client.create_session({"model": "claude-haiku-4.5"})
response1 = await session1.send_and_wait(
"What is the capital of France?"
@@ -29,7 +30,7 @@ async def main():
# Second session — tests that the server accepts new sessions
print("--- Session 2 ---")
- session2 = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session2 = await client.create_session({"model": "claude-haiku-4.5"})
response2 = await session2.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py
index 382f9c4f9..39ce2bb81 100644
--- a/test/scenarios/transport/stdio/python/main.py
+++ b/test/scenarios/transport/stdio/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, SubprocessConfig
+from copilot import CopilotClient
+from copilot.client import SubprocessConfig
async def main():
@@ -10,7 +11,7 @@ async def main():
))
try:
- session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session = await client.create_session({"model": "claude-haiku-4.5"})
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/scenarios/transport/tcp/python/main.py b/test/scenarios/transport/tcp/python/main.py
index 07eb74e20..b441bec51 100644
--- a/test/scenarios/transport/tcp/python/main.py
+++ b/test/scenarios/transport/tcp/python/main.py
@@ -1,6 +1,7 @@
import asyncio
import os
-from copilot import CopilotClient, PermissionHandler, ExternalServerConfig
+from copilot import CopilotClient
+from copilot.client import ExternalServerConfig
async def main():
@@ -9,7 +10,7 @@ async def main():
))
try:
- session = await client.create_session(on_permission_request=PermissionHandler.approve_all, model="claude-haiku-4.5")
+ session = await client.create_session({"model": "claude-haiku-4.5"})
response = await session.send_and_wait(
"What is the capital of France?"
diff --git a/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml
new file mode 100644
index 000000000..f3ce077a6
--- /dev/null
+++ b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml
@@ -0,0 +1,35 @@
+models:
+ - claude-sonnet-4.5
+conversations:
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: Who are you?
+ - role: assistant
+ content: >-
+ I'm **GitHub Copilot CLI**, a terminal assistant built by GitHub. I'm powered by claude-sonnet-4.5 (model ID:
+ claude-sonnet-4.5).
+
+
+ I'm here to help you with software engineering tasks, including:
+
+ - Writing, debugging, and refactoring code
+
+ - Running commands and managing development workflows
+
+ - Exploring codebases and understanding how things work
+
+ - Setting up projects, installing dependencies, and configuring tools
+
+ - Working with Git, testing, and deployment tasks
+
+ - Planning and implementing features
+
+
+ I have access to a variety of tools including file operations, shell commands, code search, and specialized
+ sub-agents for specific tasks. I can work with multiple languages and frameworks, and I'm designed to be
+ efficient by running tasks in parallel when possible.
+
+
+ How can I help you today?
diff --git a/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml
new file mode 100644
index 000000000..98004f2b0
--- /dev/null
+++ b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml
@@ -0,0 +1,33 @@
+models:
+ - claude-sonnet-4.5
+conversations:
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: Read the contents of hello.txt
+ - role: assistant
+ content: I'll read the hello.txt file for you.
+ tool_calls:
+ - id: toolcall_0
+ type: function
+ function:
+ name: report_intent
+ arguments: '{"intent":"Reading hello.txt file"}'
+ - id: toolcall_1
+ type: function
+ function:
+ name: view
+ arguments: '{"path":"${workdir}/hello.txt"}'
+ - role: tool
+ tool_call_id: toolcall_0
+ content: Intent logged
+ - role: tool
+ tool_call_id: toolcall_1
+ content: 1. Hello!
+ - role: assistant
+ content: |-
+ The file hello.txt contains:
+ ```
+ Hello!
+ ```
diff --git a/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml
new file mode 100644
index 000000000..631a8eef8
--- /dev/null
+++ b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml
@@ -0,0 +1,54 @@
+models:
+ - claude-sonnet-4.5
+conversations:
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: Read the contents of test.txt and tell me what it says
+ - role: assistant
+ content: I'll read the test.txt file for you.
+ - role: assistant
+ tool_calls:
+ - id: toolcall_0
+ type: function
+ function:
+ name: report_intent
+ arguments: '{"intent":"Reading test.txt file"}'
+ - role: assistant
+ tool_calls:
+ - id: toolcall_1
+ type: function
+ function:
+ name: view
+ arguments: '{"path":"${workdir}/test.txt"}'
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: Read the contents of test.txt and tell me what it says
+ - role: assistant
+ content: I'll read the test.txt file for you.
+ tool_calls:
+ - id: toolcall_0
+ type: function
+ function:
+ name: report_intent
+ arguments: '{"intent":"Reading test.txt file"}'
+ - id: toolcall_1
+ type: function
+ function:
+ name: view
+ arguments: '{"path":"${workdir}/test.txt"}'
+ - role: tool
+ tool_call_id: toolcall_0
+ content: Intent logged
+ - role: tool
+ tool_call_id: toolcall_1
+ content: 1. Hello transform!
+ - role: assistant
+ content: |-
+ The file test.txt contains:
+ ```
+ Hello transform!
+ ```
diff --git a/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml
new file mode 100644
index 000000000..9cb515694
--- /dev/null
+++ b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml
@@ -0,0 +1,50 @@
+models:
+ - claude-sonnet-4.5
+conversations:
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: Read the contents of combo.txt and tell me what it says
+ - role: assistant
+ content: I'll read the contents of combo.txt for you.
+ - role: assistant
+ tool_calls:
+ - id: toolcall_0
+ type: function
+ function:
+ name: report_intent
+ arguments: '{"intent":"Reading file contents"}'
+ - role: assistant
+ tool_calls:
+ - id: toolcall_1
+ type: function
+ function:
+ name: view
+ arguments: '{"path":"${workdir}/combo.txt"}'
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: Read the contents of combo.txt and tell me what it says
+ - role: assistant
+ content: I'll read the contents of combo.txt for you.
+ tool_calls:
+ - id: toolcall_0
+ type: function
+ function:
+ name: report_intent
+ arguments: '{"intent":"Reading file contents"}'
+ - id: toolcall_1
+ type: function
+ function:
+ name: view
+ arguments: '{"path":"${workdir}/combo.txt"}'
+ - role: tool
+ tool_call_id: toolcall_0
+ content: Intent logged
+ - role: tool
+ tool_call_id: toolcall_1
+ content: 1. Combo test!
+ - role: assistant
+ content: The file combo.txt contains a single line that says "Combo test!"