+ {
+ [SystemMessageSection.Preamble] = new SectionOverride
+ {
+ Action = SectionOverrideAction.Replace,
+ Content = "You are a helpful gardening assistant called Botanica. You only answer questions about plants and gardening."
+ }
+ }
+ }
+ });
+
+ await session.SendAsync(new MessageOptions { Prompt = "Who are you?" });
+ var response = await TestHelper.GetFinalAssistantMessageAsync(session);
+
+ Assert.NotNull(response);
+ var content = response.Data.Content.ToLowerInvariant();
+ Assert.True(
+ content.Contains("botanica") || content.Contains("garden") || content.Contains("plant"),
+ $"Expected response to reflect the replaced preamble section, but got: {response.Data.Content}");
+ }
}
diff --git a/go/README.md b/go/README.md
index b89a76318..9bd3fac61 100644
--- a/go/README.md
+++ b/go/README.md
@@ -165,7 +165,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
- `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`, `SectionRuntimeInstructions`, `SectionLastInstructions`; values: `SectionOverride` with `Action` and optional `Content`)
+ - **customize**: Selectively override individual sections via `Sections` map (keys: `SectionPreamble`, `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionRuntimeInstructions`, `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 (nil = runtime default)
- `InfiniteSessions` (\*InfiniteSessionConfig): Automatic context compaction configuration
@@ -238,14 +238,17 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{
})
```
-Available section constants: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionRuntimeInstructions`, `SectionLastInstructions`.
+Available section constants: `SectionPreamble`, `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionRuntimeInstructions`, `SectionLastInstructions`.
-Each section override supports four actions:
+`SectionIdentity` and `SectionToolInstructions` are section _groups_ that target a collection of related sub-sections as a unit. Use `SectionPreamble` to target just the identity preamble without affecting its sibling sub-sections.
+
+Each section override supports five 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
+- **`preserve`** — No-op that opts an individually-addressable section out of a group-level `remove`
Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored.
diff --git a/go/internal/e2e/system_message_sections_e2e_test.go b/go/internal/e2e/system_message_sections_e2e_test.go
index dfdb466be..61493c812 100644
--- a/go/internal/e2e/system_message_sections_e2e_test.go
+++ b/go/internal/e2e/system_message_sections_e2e_test.go
@@ -54,4 +54,43 @@ func TestSystemMessageSectionsE2E(t *testing.T) {
t.Errorf("Expected response to reflect the replaced identity section, but got: %s", ad.Content)
}
})
+
+ t.Run("should_use_replaced_preamble_section_in_response", 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{
+ copilot.SectionPreamble: {
+ Action: copilot.SectionActionReplace,
+ Content: "You are a helpful gardening assistant called Botanica. You only answer questions about plants and gardening.",
+ },
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("Failed to create session: %v", err)
+ }
+
+ response, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
+ Prompt: "Who are you?",
+ })
+ if err != nil {
+ t.Fatalf("Failed to send message: %v", err)
+ }
+ if response == nil {
+ t.Fatal("Expected a response from the assistant")
+ }
+
+ ad, ok := response.Data.(*copilot.AssistantMessageData)
+ if !ok {
+ t.Fatalf("Expected AssistantMessageData, got %T", response.Data)
+ }
+ content := strings.ToLower(ad.Content)
+ if !strings.Contains(content, "botanica") && !strings.Contains(content, "garden") && !strings.Contains(content, "plant") {
+ t.Errorf("Expected response to reflect the replaced preamble section, but got: %s", ad.Content)
+ }
+ })
}
diff --git a/go/types.go b/go/types.go
index 5ed0b6931..b421e4954 100644
--- a/go/types.go
+++ b/go/types.go
@@ -209,7 +209,10 @@ func Int(v int) *int {
// Known system message section identifiers for the "customize" mode.
const (
- // SectionIdentity is the agent identity preamble and mode statement.
+ // SectionPreamble is the agent identity preamble and mode statement.
+ SectionPreamble = "preamble"
+ // SectionIdentity is the section group covering the identity preamble and its
+ // sibling sub-sections (tone, tool efficiency, etc.).
SectionIdentity = "identity"
// SectionTone covers response style, conciseness rules, and output formatting preferences.
SectionTone = "tone"
@@ -248,6 +251,10 @@ const (
SectionActionAppend SectionOverrideAction = "append"
// SectionActionPrepend prepends to existing section content.
SectionActionPrepend SectionOverrideAction = "prepend"
+ // SectionActionPreserve is a no-op marker that opts an individually-addressable
+ // section out of a group-level "remove" (e.g. keep "tone" when removing the
+ // "identity" group).
+ SectionActionPreserve SectionOverrideAction = "preserve"
)
// SectionTransformFn is a callback that receives the current content of a system message section
diff --git a/java/src/main/java/com/github/copilot/rpc/SectionOverrideAction.java b/java/src/main/java/com/github/copilot/rpc/SectionOverrideAction.java
index bee462d58..a00958fdb 100644
--- a/java/src/main/java/com/github/copilot/rpc/SectionOverrideAction.java
+++ b/java/src/main/java/com/github/copilot/rpc/SectionOverrideAction.java
@@ -28,6 +28,13 @@ public enum SectionOverrideAction {
/** Prepend content before the existing section. */
PREPEND("prepend"),
+ /**
+ * No-op marker that opts an individually-addressable section out of a
+ * group-level {@link #REMOVE} (e.g. keep {@link SystemMessageSections#TONE}
+ * when removing the {@link SystemMessageSections#IDENTITY} group).
+ */
+ PRESERVE("preserve"),
+
/**
* Transform the section content via a callback.
*
diff --git a/java/src/main/java/com/github/copilot/rpc/SystemMessageSections.java b/java/src/main/java/com/github/copilot/rpc/SystemMessageSections.java
index a896be70d..ca410e497 100644
--- a/java/src/main/java/com/github/copilot/rpc/SystemMessageSections.java
+++ b/java/src/main/java/com/github/copilot/rpc/SystemMessageSections.java
@@ -28,6 +28,12 @@
public abstract sealed class SystemMessageSections permits SystemPromptSections {
/** Agent identity preamble and mode statement. */
+ public static final String PREAMBLE = "preamble";
+
+ /**
+ * Section group covering the identity preamble and its sibling sub-sections
+ * (tone, tool efficiency, etc.).
+ */
public static final String IDENTITY = "identity";
/** Response style, conciseness rules, output formatting preferences. */
diff --git a/java/src/test/java/com/github/copilot/SystemMessageSectionsIT.java b/java/src/test/java/com/github/copilot/SystemMessageSectionsIT.java
index bdab3ede5..1541af279 100644
--- a/java/src/test/java/com/github/copilot/SystemMessageSectionsIT.java
+++ b/java/src/test/java/com/github/copilot/SystemMessageSectionsIT.java
@@ -118,6 +118,7 @@ void transformOnIdentitySectionReceivesNonEmptyContent() throws Exception {
*/
@Test
void deprecatedSystemPromptSectionsMatchesSystemMessageSections() {
+ assertEquals(SystemMessageSections.PREAMBLE, SystemPromptSections.PREAMBLE);
assertEquals(SystemMessageSections.IDENTITY, SystemPromptSections.IDENTITY);
assertEquals(SystemMessageSections.TONE, SystemPromptSections.TONE);
assertEquals(SystemMessageSections.TOOL_EFFICIENCY, SystemPromptSections.TOOL_EFFICIENCY);
@@ -143,7 +144,7 @@ void allConstantsInheritedByDeprecatedClass() throws Exception {
&& Modifier.isFinal(f.getModifiers()) && f.getType() == String.class)
.map(Field::getName).collect(Collectors.toSet());
- assertEquals(11, parentConstants.size(), "Expected 11 section constants in SystemMessageSections");
+ assertEquals(12, parentConstants.size(), "Expected 12 section constants in SystemMessageSections");
for (String constantName : parentConstants) {
Field parentField = SystemMessageSections.class.getDeclaredField(constantName);
@@ -189,4 +190,41 @@ void shouldUseReplacedIdentitySectionInResponse() throws Exception {
}
}
}
+
+ /**
+ * Verifies that replacing the {@link SystemMessageSections#PREAMBLE} section
+ * via {@link SectionOverrideAction#REPLACE} causes the assistant to adopt the
+ * custom identity in its response without affecting sibling sections.
+ *
+ * @see Snapshot:
+ * system_message_sections/should_use_replaced_preamble_section_in_response
+ */
+ @Test
+ void shouldUseReplacedPreambleSectionInResponse() throws Exception {
+ ctx.configureForTest("system_message_sections", "should_use_replaced_preamble_section_in_response");
+
+ var systemMessage = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE)
+ .setSections(Map.of(SystemMessageSections.PREAMBLE,
+ new SectionOverride().setAction(SectionOverrideAction.REPLACE)
+ .setContent("You are a helpful gardening assistant called Botanica. "
+ + "You only answer questions about plants and gardening.")));
+
+ try (CopilotClient client = ctx.createClient()) {
+ CopilotSession session = client.createSession(new SessionConfig().setSystemMessage(systemMessage)
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(30, TimeUnit.SECONDS);
+
+ try {
+ AssistantMessageEvent response = session
+ .sendAndWait(new MessageOptions().setPrompt("Who are you?"), 60_000).get(90, TimeUnit.SECONDS);
+
+ assertNotNull(response, "Expected a response from the assistant");
+ String content = response.getData().content().toLowerCase();
+ assertTrue(content.contains("botanica") || content.contains("garden") || content.contains("plant"),
+ "Expected response to reflect the replaced preamble section, but got: "
+ + response.getData().content());
+ } finally {
+ session.close();
+ }
+ }
+ }
}
diff --git a/nodejs/README.md b/nodejs/README.md
index bc91cd793..a8194cc49 100644
--- a/nodejs/README.md
+++ b/nodejs/README.md
@@ -618,14 +618,17 @@ const session = await client.createSession({
});
```
-Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `runtime_instructions`, `last_instructions`. Use the `SYSTEM_MESSAGE_SECTIONS` constant for descriptions of each section.
+Available section IDs: `preamble`, `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `runtime_instructions`, `last_instructions`. Use the `SYSTEM_MESSAGE_SECTIONS` constant for descriptions of each section.
-Each section override supports four actions:
+`identity` and `tool_instructions` are section _groups_ that target a collection of related sub-sections as a unit. Use `preamble` to target just the identity preamble without affecting its sibling sub-sections.
+
+Each section override supports five 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
+- **`preserve`** — No-op that opts an individually-addressable section out of a group-level `remove`
Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored.
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index bad1c33ad..1ca1301d3 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -802,6 +802,7 @@ export interface ToolCallResponsePayload {
* Each section corresponds to a distinct part of the system prompt.
*/
export type SystemMessageSection =
+ | "preamble"
| "identity"
| "tone"
| "tool_efficiency"
@@ -816,7 +817,11 @@ export type SystemMessageSection =
/** Section metadata for documentation and tooling. */
export const SYSTEM_MESSAGE_SECTIONS: Record = {
- identity: { description: "Agent identity preamble and mode statement" },
+ preamble: { description: "Agent identity preamble and mode statement" },
+ identity: {
+ description:
+ "Section group covering the identity preamble and its sibling sub-sections (tone, tool efficiency, etc.)",
+ },
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" },
@@ -847,6 +852,8 @@ export type SectionTransformFn = (currentContent: string) => string | Promise {
await session.disconnect();
});
+
+ it("should_use_replaced_preamble_section_in_response", async () => {
+ const session = await client.createSession({
+ onPermissionRequest: approveAll,
+ systemMessage: {
+ mode: "customize",
+ sections: {
+ preamble: {
+ action: "replace",
+ content:
+ "You are a helpful gardening assistant called Botanica. You only answer questions about plants and gardening.",
+ },
+ },
+ },
+ });
+
+ const response = await session.sendAndWait({ prompt: "Who are you?" });
+
+ expect(response).not.toBeNull();
+ const content = response!.data.content.toLowerCase();
+ expect(
+ content.includes("botanica") || content.includes("garden") || content.includes("plant"),
+ `Expected response to reflect the replaced preamble section, but got: ${response!.data.content}`
+ ).toBe(true);
+
+ await session.disconnect();
+ });
});
diff --git a/python/copilot/session.py b/python/copilot/session.py
index 3720af05d..7bd4e9f02 100644
--- a/python/copilot/session.py
+++ b/python/copilot/session.py
@@ -253,10 +253,17 @@ class SystemMessageReplaceConfig(TypedDict):
SectionTransformFn = Callable[[str], str | Awaitable[str]]
"""Transform callback: receives current section content, returns new content."""
-SectionOverrideAction = Literal["replace", "remove", "append", "prepend"] | SectionTransformFn
-"""Override action: a string literal for static overrides, or a callback for transforms."""
+SectionOverrideAction = (
+ Literal["replace", "remove", "append", "prepend", "preserve"] | SectionTransformFn
+)
+"""Override action: a string literal for static overrides, or a callback for transforms.
+
+``"preserve"`` is a no-op marker that opts an individually-addressable section out of a
+group-level ``"remove"`` (e.g. keep ``tone`` when removing the ``identity`` group).
+"""
SystemMessageSection = Literal[
+ "preamble",
"identity",
"tone",
"tool_efficiency",
@@ -271,7 +278,11 @@ class SystemMessageReplaceConfig(TypedDict):
]
SYSTEM_MESSAGE_SECTIONS: dict[SystemMessageSection, str] = {
- "identity": "Agent identity preamble and mode statement",
+ "preamble": "Agent identity preamble and mode statement",
+ "identity": (
+ "Section group covering the identity preamble and its sibling sub-sections"
+ " (tone, tool efficiency, etc.)"
+ ),
"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",
diff --git a/python/e2e/test_system_message_sections_e2e.py b/python/e2e/test_system_message_sections_e2e.py
index a8f2345fe..d6017dba6 100644
--- a/python/e2e/test_system_message_sections_e2e.py
+++ b/python/e2e/test_system_message_sections_e2e.py
@@ -42,3 +42,32 @@ async def test_should_use_replaced_identity_section_in_response(self, ctx: E2ETe
)
await session.disconnect()
+
+ async def test_should_use_replaced_preamble_section_in_response(self, ctx: E2ETestContext):
+ """Test that replacing only the preamble section changes the assistant persona"""
+ session = await ctx.client.create_session(
+ system_message={
+ "mode": "customize",
+ "sections": {
+ "preamble": {
+ "action": "replace",
+ "content": (
+ "You are a helpful gardening assistant called Botanica."
+ " You only answer questions about plants and gardening."
+ ),
+ },
+ },
+ },
+ on_permission_request=PermissionHandler.approve_all,
+ )
+
+ response = await session.send_and_wait("Who are you?")
+
+ assert response is not None, "Expected a response from the assistant"
+ content = response.data.content.lower()
+ assert "botanica" in content or "garden" in content or "plant" in content, (
+ f"Expected response to reflect the replaced preamble section,"
+ f" but got: {response.data.content}"
+ )
+
+ await session.disconnect()
diff --git a/rust/src/types.rs b/rust/src/types.rs
index c0643ec66..3295f6dd7 100644
--- a/rust/src/types.rs
+++ b/rust/src/types.rs
@@ -3245,11 +3245,12 @@ impl SystemMessageConfig {
///
/// Used within [`SystemMessageConfig::sections`] when `mode` is `"customize"`.
/// The `action` field determines the operation: `"replace"`, `"remove"`,
-/// `"append"`, `"prepend"`, or `"transform"`.
+/// `"append"`, `"prepend"`, `"preserve"`, or `"transform"`.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SectionOverride {
- /// Override action: `"replace"`, `"remove"`, `"append"`, `"prepend"`, or `"transform"`.
+ /// Override action: `"replace"`, `"remove"`, `"append"`, `"prepend"`,
+ /// `"preserve"`, or `"transform"`.
#[serde(skip_serializing_if = "Option::is_none")]
pub action: Option,
/// Content for the override operation.
diff --git a/rust/tests/e2e/system_message_sections.rs b/rust/tests/e2e/system_message_sections.rs
index 945b8b8ea..e582d3846 100644
--- a/rust/tests/e2e/system_message_sections.rs
+++ b/rust/tests/e2e/system_message_sections.rs
@@ -57,3 +57,57 @@ async fn should_use_replaced_identity_section_in_response() {
)
.await;
}
+
+#[tokio::test]
+async fn should_use_replaced_preamble_section_in_response() {
+ with_e2e_context(
+ "system_message_sections",
+ "should_use_replaced_preamble_section_in_response",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let mut sections = HashMap::new();
+ sections.insert(
+ "preamble".to_string(),
+ SectionOverride {
+ action: Some("replace".to_string()),
+ content: Some(
+ "You are a helpful gardening assistant called Botanica. \
+ You only answer questions about plants and gardening."
+ .to_string(),
+ ),
+ },
+ );
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(
+ ctx.approve_all_session_config().with_system_message(
+ SystemMessageConfig::new()
+ .with_mode("customize")
+ .with_sections(sections),
+ ),
+ )
+ .await
+ .expect("create session");
+
+ let answer = session
+ .send_and_wait("Who are you?")
+ .await
+ .expect("send")
+ .expect("assistant message");
+ let content = assistant_message_content(&answer).to_lowercase();
+ assert!(
+ content.contains("botanica")
+ || content.contains("garden")
+ || content.contains("plant"),
+ "Expected response to reflect the replaced preamble section, but got: {}",
+ assistant_message_content(&answer)
+ );
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
diff --git a/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml b/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml
new file mode 100644
index 000000000..9d2c688c1
--- /dev/null
+++ b/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml
@@ -0,0 +1,17 @@
+models:
+ - claude-sonnet-4.5
+conversations:
+ - messages:
+ - role: system
+ content: ${system}
+ - role: user
+ content: Who are you?
+ - role: assistant
+ content: >-
+ I'm **Botanica**, your helpful gardening assistant! 🌱 I'm here to answer questions about plants, gardening,
+ horticulture, and everything related to growing and caring for greenery. Whether you need advice on soil,
+ watering, pests, plant identification, or growing tips, I'm here to help!
+
+
+ I'm powered by claude-sonnet-4.5, but I focus specifically on gardening topics. What plant or gardening
+ question can I help you with today?