diff --git a/docs/getting-started.md b/docs/getting-started.md index 24e6c5b8..4575a7bf 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1251,7 +1251,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({ @@ -1261,6 +1261,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`. + +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 diff --git a/dotnet/README.md b/dotnet/README.md index 712323c0..f881e292 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -488,6 +488,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`. + +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 a9ad1fcc..49f95324 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -1683,6 +1683,7 @@ private static LogLevel MapLevel(TraceEventType eventType) [JsonSerializable(typeof(ProviderConfig))] [JsonSerializable(typeof(ResumeSessionRequest))] [JsonSerializable(typeof(ResumeSessionResponse))] + [JsonSerializable(typeof(SectionOverride))] [JsonSerializable(typeof(SessionMetadata))] [JsonSerializable(typeof(SystemMessageConfig))] [JsonSerializable(typeof(ToolCallResponseV2))] diff --git a/dotnet/src/SdkProtocolVersion.cs b/dotnet/src/SdkProtocolVersion.cs index f3d8f04c..889af460 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/Types.cs b/dotnet/src/Types.cs index 84e7feae..b63663f9 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -968,7 +968,71 @@ 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 +} + +/// +/// Override operation for a single system prompt section. +/// +public class SectionOverride +{ + /// + /// The operation to perform on this section. + /// + public SectionOverrideAction Action { get; set; } + + /// + /// Content for the override. Optional for all actions. Ignored for remove. + /// + public string? Content { 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"; } /// @@ -977,13 +1041,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; } } /// @@ -2061,6 +2133,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/SessionTests.cs b/dotnet/test/SessionTests.cs index 8cd4c84e..cf5313ef 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/go/README.md b/go/README.md index f87c3d1b..b58bef58 100644 --- a/go/README.md +++ b/go/README.md @@ -149,7 +149,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`; 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 @@ -176,6 +179,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`. + +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: diff --git a/go/internal/e2e/session_test.go b/go/internal/e2e/session_test.go index c3c9cc00..7a2f1d20 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) diff --git a/go/types.go b/go/types.go index fd9968e3..c5ac33fb 100644 --- a/go/types.go +++ b/go/types.go @@ -111,6 +111,27 @@ 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" +) + +// SectionOverride defines an override operation for a single system prompt section. +type SectionOverride struct { + // Action is the operation to perform: "replace", "remove", "append", or "prepend". + Action string `json:"action"` + // Content for the override. Optional for all actions. Ignored for "remove". + Content string `json:"content,omitempty"` +} + // SystemMessageAppendConfig is append mode: use CLI foundation with optional appended content. type SystemMessageAppendConfig struct { // Mode is optional, defaults to "append" @@ -129,11 +150,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 af37b27b..48a60c9a 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -458,7 +458,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`. 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/src/index.ts b/nodejs/src/index.ts index 214b8005..a717d883 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,7 @@ export type { PermissionRequest, PermissionRequestResult, ResumeSessionConfig, + SectionOverride, SessionConfig, SessionEvent, SessionEventHandler, @@ -44,7 +45,9 @@ export type { SessionMetadata, SystemMessageAppendConfig, SystemMessageConfig, + SystemMessageCustomizeConfig, SystemMessageReplaceConfig, + SystemPromptSection, TelemetryConfig, TraceContext, TraceContextProvider, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 9576b692..8a6bcf8d 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -272,6 +272,50 @@ 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"; + +/** 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" }, +}; + +/** + * Override operation for a single system prompt section. + */ +export interface SectionOverride { + /** The operation to perform on this section. */ + action: "replace" | "remove" | "append" | "prepend"; + + /** + * 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 +342,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 1eb8a175..dbcbed8b 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/python/README.md b/python/README.md index 9d83ae65..b050476b 100644 --- a/python/README.md +++ b/python/README.md @@ -140,7 +140,10 @@ CopilotClient( - `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. - `session_id` (str): Custom session ID - `tools` (list): Custom tools exposed to the CLI -- `system_message` (dict): System message configuration +- `system_message` (dict): 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` dict (keys: `"identity"`, `"tone"`, `"tool_efficiency"`, `"environment_context"`, `"code_change_rules"`, `"guidelines"`, `"safety"`, `"tool_instructions"`, `"custom_instructions"`; values: `SectionOverride` with `action` and optional `content`) - `streaming` (bool): Enable streaming delta events - `provider` (dict): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section. - `infinite_sessions` (dict): Automatic context compaction configuration @@ -176,6 +179,54 @@ unsubscribe() - `session.foreground` - A session became the foreground session in TUI - `session.background` - A session is no longer the foreground session +### System Message Customization + +Control the system prompt using `system_message` in session config: + +```python +session = await client.create_session( + system_message={ + "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: + +```python +from copilot import SYSTEM_PROMPT_SECTIONS + +session = await client.create_session( + system_message={ + "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"`. Use the `SYSTEM_PROMPT_SECTIONS` dict 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. + ### Tools Define tools with automatic JSON schema generation using the `@define_tool` decorator and Pydantic models: diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index e0f627c7..9124537f 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -28,12 +28,19 @@ PingResponse, ProviderConfig, ResumeSessionConfig, + SectionOverride, SessionConfig, SessionContext, SessionEvent, SessionListFilter, SessionMetadata, StopError, + SystemMessageAppendConfig, + SystemMessageConfig, + SystemMessageCustomizeConfig, + SystemMessageReplaceConfig, + SystemPromptSection, + SYSTEM_PROMPT_SECTIONS, SubprocessConfig, TelemetryConfig, Tool, @@ -67,12 +74,19 @@ "PingResponse", "ProviderConfig", "ResumeSessionConfig", + "SectionOverride", "SessionConfig", "SessionContext", "SessionEvent", "SessionListFilter", "SessionMetadata", "StopError", + "SystemMessageAppendConfig", + "SystemMessageConfig", + "SystemMessageCustomizeConfig", + "SystemMessageReplaceConfig", + "SystemPromptSection", + "SYSTEM_PROMPT_SECTIONS", "SubprocessConfig", "TelemetryConfig", "Tool", diff --git a/python/copilot/types.py b/python/copilot/types.py index e572e751..e62dc657 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable from dataclasses import KW_ONLY, dataclass, field -from typing import Any, Literal, NotRequired, TypedDict +from typing import Any, Literal, NotRequired, Required, TypedDict # Import generated SessionEvent types from .generated.session_events import ( @@ -202,7 +202,41 @@ class Tool: # System message configuration (discriminated union) -# Use SystemMessageAppendConfig for default behavior, SystemMessageReplaceConfig for full control +# Use SystemMessageAppendConfig for default behavior, +# SystemMessageReplaceConfig for full control, +# or SystemMessageCustomizeConfig for section-level overrides. + +# Known system prompt section identifiers for the "customize" mode. +SystemPromptSection = Literal[ + "identity", + "tone", + "tool_efficiency", + "environment_context", + "code_change_rules", + "guidelines", + "safety", + "tool_instructions", + "custom_instructions", +] + +SYSTEM_PROMPT_SECTIONS: dict[SystemPromptSection, str] = { + "identity": "Agent identity preamble and mode statement", + "tone": "Response style, conciseness rules, output formatting preferences", + "tool_efficiency": "Tool usage patterns, parallel calling, batching guidelines", + "environment_context": "CWD, OS, git root, directory listing, available tools", + "code_change_rules": "Coding rules, linting/testing, ecosystem tools, style", + "guidelines": "Tips, behavioral best practices, behavioral guidelines", + "safety": "Environment limitations, prohibited actions, security policies", + "tool_instructions": "Per-tool usage instructions", + "custom_instructions": "Repository and organization custom instructions", +} + + +class SectionOverride(TypedDict, total=False): + """Override operation for a single system prompt section.""" + + action: Required[Literal["replace", "remove", "append", "prepend"]] + content: NotRequired[str] class SystemMessageAppendConfig(TypedDict, total=False): @@ -224,8 +258,21 @@ class SystemMessageReplaceConfig(TypedDict): content: str -# Union type - use one or the other -SystemMessageConfig = SystemMessageAppendConfig | SystemMessageReplaceConfig +class SystemMessageCustomizeConfig(TypedDict, total=False): + """ + Customize mode: Override individual sections of the system prompt. + Keeps the SDK-managed prompt structure while allowing targeted modifications. + """ + + mode: Required[Literal["customize"]] + sections: NotRequired[dict[SystemPromptSection, SectionOverride]] + content: NotRequired[str] + + +# Union type - use one based on your needs +SystemMessageConfig = ( + SystemMessageAppendConfig | SystemMessageReplaceConfig | SystemMessageCustomizeConfig +) # Permission result types diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index 9e663fcc..6df7adbe 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -88,6 +88,35 @@ 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( + { + "system_message": { + "mode": "customize", + "sections": { + "tone": {"action": "replace", "content": custom_tone}, + "code_change_rules": {"action": "remove"}, + }, + "content": appended_content, + }, + "on_permission_request": PermissionHandler.approve_all, + } + ) + + assistant_message = await session.send_and_wait({"prompt": "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( {