Skip to content

Commit dd80e87

Browse files
Add preamble section and preserve action to SDKs (#1713)
* Add preamble section and preserve action to SDKs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add E2E test for replaced preamble section across SDKs Adds a 'should_use_replaced_preamble_section_in_response' E2E test to all six SDKs (Node, Go, Python, .NET, Java, Rust) exercising the new 'preamble' section ID end-to-end, with a shared replay snapshot. Updates the Java SystemMessageSections constant-count assertion from 11 to 12. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align getting-started unknown-section docs with SDK READMEs Remove the unsupported 'a warning is emitted' claim from the system message customization section so it matches the silent-fallback behavior documented in the language READMEs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix Ruff E501 line-length in preamble E2E test docstring The new preamble test docstring exceeded the 100-char limit, failing 'ruff check' in the Python SDK Tests CI job across all platforms. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add SectionPreamble to go/README SystemMessage keys list The SystemMessage parameter-reference table's inline section-constant list still started at SectionIdentity. Add SectionPreamble so it matches the dedicated 'Available section constants' list and the other SDKs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 245a640 commit dd80e87

18 files changed

Lines changed: 308 additions & 17 deletions

File tree

docs/getting-started.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1911,9 +1911,11 @@ const session = await client.createSession({
19111911
});
19121912
```
19131913

1914-
Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `runtime_instructions`, `last_instructions`.
1914+
Available section IDs: `preamble`, `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `runtime_instructions`, `last_instructions`.
19151915

1916-
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.
1916+
`identity` and `tool_instructions` are section *groups*: they target a collection of related sub-sections as a unit. Use `preamble` to target just the identity preamble without affecting its sibling sub-sections.
1917+
1918+
Each override supports five actions: `replace`, `remove`, `append`, `prepend`, and `preserve`. The `preserve` action is a no-op that opts an individually-addressable section out of a group-level `remove` (for example, keep `tone` when removing the `identity` group). Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored.
19171919

19181920
See the language-specific SDK READMEs for examples in [TypeScript](../nodejs/README.md), [Python](../python/README.md), [Go](../go/README.md), [Rust](../rust/README.md), [Java](../java/README.md), and [C#](../dotnet/README.md).
19191921

dotnet/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,9 +677,9 @@ var session = await client.CreateSessionAsync(new SessionConfig
677677
});
678678
```
679679

680-
Available section IDs are defined as static properties on the `SystemMessageSection` struct: `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `RuntimeInstructions`, `LastInstructions`.
680+
Available section IDs are defined as static properties on the `SystemMessageSection` struct: `Preamble`, `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `RuntimeInstructions`, `LastInstructions`. `Identity` and `ToolInstructions` are section groups that target a collection of related sub-sections as a unit; use `Preamble` to target just the identity preamble.
681681

682-
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.
682+
Each section override supports five actions: `Replace`, `Remove`, `Append`, `Prepend`, and `Preserve` (a no-op that opts an individually-addressable section out of a group-level `Remove`). Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored.
683683

684684
#### Replace Mode
685685

dotnet/src/Types.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,6 +1840,13 @@ public enum SectionOverrideAction
18401840
/// <summary>Prepend content before the existing section.</summary>
18411841
[JsonStringEnumMemberName("prepend")]
18421842
Prepend,
1843+
/// <summary>
1844+
/// No-op marker that opts an individually-addressable section out of a group-level
1845+
/// remove (e.g. keep <see cref="SystemMessageSection.Tone"/> when removing the
1846+
/// <see cref="SystemMessageSection.Identity"/> group).
1847+
/// </summary>
1848+
[JsonStringEnumMemberName("preserve")]
1849+
Preserve,
18431850
/// <summary>Transform the section content via a callback.</summary>
18441851
[JsonStringEnumMemberName("transform")]
18451852
Transform
@@ -1878,6 +1885,8 @@ public sealed class SectionOverride
18781885
public readonly struct SystemMessageSection : IEquatable<SystemMessageSection>
18791886
{
18801887
/// <summary>Agent identity preamble and mode statement.</summary>
1888+
public static SystemMessageSection Preamble { get; } = new("preamble");
1889+
/// <summary>Section group covering the identity preamble and its sibling sub-sections (tone, tool efficiency, etc.).</summary>
18811890
public static SystemMessageSection Identity { get; } = new("identity");
18821891
/// <summary>Response style, conciseness rules, output formatting preferences.</summary>
18831892
public static SystemMessageSection Tone { get; } = new("tone");

dotnet/test/E2E/SystemMessageSectionsE2ETests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,34 @@ public async Task Should_Use_Replaced_Identity_Section_In_Response()
4040
content.Contains("botanica") || content.Contains("garden") || content.Contains("plant"),
4141
$"Expected response to reflect the replaced identity section, but got: {response.Data.Content}");
4242
}
43+
44+
[Fact]
45+
public async Task Should_Use_Replaced_Preamble_Section_In_Response()
46+
{
47+
var session = await CreateSessionAsync(new SessionConfig
48+
{
49+
OnPermissionRequest = PermissionHandler.ApproveAll,
50+
SystemMessage = new SystemMessageConfig
51+
{
52+
Mode = SystemMessageMode.Customize,
53+
Sections = new Dictionary<SystemMessageSection, SectionOverride>
54+
{
55+
[SystemMessageSection.Preamble] = new SectionOverride
56+
{
57+
Action = SectionOverrideAction.Replace,
58+
Content = "You are a helpful gardening assistant called Botanica. You only answer questions about plants and gardening."
59+
}
60+
}
61+
}
62+
});
63+
64+
await session.SendAsync(new MessageOptions { Prompt = "Who are you?" });
65+
var response = await TestHelper.GetFinalAssistantMessageAsync(session);
66+
67+
Assert.NotNull(response);
68+
var content = response.Data.Content.ToLowerInvariant();
69+
Assert.True(
70+
content.Contains("botanica") || content.Contains("garden") || content.Contains("plant"),
71+
$"Expected response to reflect the replaced preamble section, but got: {response.Data.Content}");
72+
}
4373
}

go/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ Event types: `SessionLifecycleCreated`, `SessionLifecycleDeleted`, `SessionLifec
165165
- `SystemMessage` (\*SystemMessageConfig): System message configuration. Supports three modes:
166166
- **append** (default): Appends `Content` after the SDK-managed prompt
167167
- **replace**: Replaces the entire prompt with `Content`
168-
- **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`)
168+
- **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`)
169169
- `Provider` (\*ProviderConfig): Custom API provider configuration (BYOK). See [Custom Providers](#custom-providers) section.
170170
- `Streaming` (*bool): Enable streaming delta events (nil = runtime default)
171171
- `InfiniteSessions` (\*InfiniteSessionConfig): Automatic context compaction configuration
@@ -238,14 +238,17 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{
238238
})
239239
```
240240

241-
Available section constants: `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionRuntimeInstructions`, `SectionLastInstructions`.
241+
Available section constants: `SectionPreamble`, `SectionIdentity`, `SectionTone`, `SectionToolEfficiency`, `SectionEnvironmentContext`, `SectionCodeChangeRules`, `SectionGuidelines`, `SectionSafety`, `SectionToolInstructions`, `SectionCustomInstructions`, `SectionRuntimeInstructions`, `SectionLastInstructions`.
242242

243-
Each section override supports four actions:
243+
`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.
244+
245+
Each section override supports five actions:
244246

245247
- **`replace`** — Replace the section content entirely
246248
- **`remove`** — Remove the section from the prompt
247249
- **`append`** — Add content after the existing section
248250
- **`prepend`** — Add content before the existing section
251+
- **`preserve`** — No-op that opts an individually-addressable section out of a group-level `remove`
249252

250253
Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored.
251254

go/internal/e2e/system_message_sections_e2e_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,43 @@ func TestSystemMessageSectionsE2E(t *testing.T) {
5454
t.Errorf("Expected response to reflect the replaced identity section, but got: %s", ad.Content)
5555
}
5656
})
57+
58+
t.Run("should_use_replaced_preamble_section_in_response", func(t *testing.T) {
59+
ctx.ConfigureForTest(t)
60+
61+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
62+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
63+
SystemMessage: &copilot.SystemMessageConfig{
64+
Mode: "customize",
65+
Sections: map[string]copilot.SectionOverride{
66+
copilot.SectionPreamble: {
67+
Action: copilot.SectionActionReplace,
68+
Content: "You are a helpful gardening assistant called Botanica. You only answer questions about plants and gardening.",
69+
},
70+
},
71+
},
72+
})
73+
if err != nil {
74+
t.Fatalf("Failed to create session: %v", err)
75+
}
76+
77+
response, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
78+
Prompt: "Who are you?",
79+
})
80+
if err != nil {
81+
t.Fatalf("Failed to send message: %v", err)
82+
}
83+
if response == nil {
84+
t.Fatal("Expected a response from the assistant")
85+
}
86+
87+
ad, ok := response.Data.(*copilot.AssistantMessageData)
88+
if !ok {
89+
t.Fatalf("Expected AssistantMessageData, got %T", response.Data)
90+
}
91+
content := strings.ToLower(ad.Content)
92+
if !strings.Contains(content, "botanica") && !strings.Contains(content, "garden") && !strings.Contains(content, "plant") {
93+
t.Errorf("Expected response to reflect the replaced preamble section, but got: %s", ad.Content)
94+
}
95+
})
5796
}

go/types.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,10 @@ func Int(v int) *int {
215215

216216
// Known system message section identifiers for the "customize" mode.
217217
const (
218-
// SectionIdentity is the agent identity preamble and mode statement.
218+
// SectionPreamble is the agent identity preamble and mode statement.
219+
SectionPreamble = "preamble"
220+
// SectionIdentity is the section group covering the identity preamble and its
221+
// sibling sub-sections (tone, tool efficiency, etc.).
219222
SectionIdentity = "identity"
220223
// SectionTone covers response style, conciseness rules, and output formatting preferences.
221224
SectionTone = "tone"
@@ -254,6 +257,10 @@ const (
254257
SectionActionAppend SectionOverrideAction = "append"
255258
// SectionActionPrepend prepends to existing section content.
256259
SectionActionPrepend SectionOverrideAction = "prepend"
260+
// SectionActionPreserve is a no-op marker that opts an individually-addressable
261+
// section out of a group-level "remove" (e.g. keep "tone" when removing the
262+
// "identity" group).
263+
SectionActionPreserve SectionOverrideAction = "preserve"
257264
)
258265

259266
// SectionTransformFn is a callback that receives the current content of a system message section

java/src/main/java/com/github/copilot/rpc/SectionOverrideAction.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ public enum SectionOverrideAction {
2828
/** Prepend content before the existing section. */
2929
PREPEND("prepend"),
3030

31+
/**
32+
* No-op marker that opts an individually-addressable section out of a
33+
* group-level {@link #REMOVE} (e.g. keep {@link SystemMessageSections#TONE}
34+
* when removing the {@link SystemMessageSections#IDENTITY} group).
35+
*/
36+
PRESERVE("preserve"),
37+
3138
/**
3239
* Transform the section content via a callback.
3340
* <p>

java/src/main/java/com/github/copilot/rpc/SystemMessageSections.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
public abstract sealed class SystemMessageSections permits SystemPromptSections {
2929

3030
/** Agent identity preamble and mode statement. */
31+
public static final String PREAMBLE = "preamble";
32+
33+
/**
34+
* Section group covering the identity preamble and its sibling sub-sections
35+
* (tone, tool efficiency, etc.).
36+
*/
3137
public static final String IDENTITY = "identity";
3238

3339
/** Response style, conciseness rules, output formatting preferences. */

java/src/test/java/com/github/copilot/SystemMessageSectionsIT.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ void transformOnIdentitySectionReceivesNonEmptyContent() throws Exception {
118118
*/
119119
@Test
120120
void deprecatedSystemPromptSectionsMatchesSystemMessageSections() {
121+
assertEquals(SystemMessageSections.PREAMBLE, SystemPromptSections.PREAMBLE);
121122
assertEquals(SystemMessageSections.IDENTITY, SystemPromptSections.IDENTITY);
122123
assertEquals(SystemMessageSections.TONE, SystemPromptSections.TONE);
123124
assertEquals(SystemMessageSections.TOOL_EFFICIENCY, SystemPromptSections.TOOL_EFFICIENCY);
@@ -143,7 +144,7 @@ void allConstantsInheritedByDeprecatedClass() throws Exception {
143144
&& Modifier.isFinal(f.getModifiers()) && f.getType() == String.class)
144145
.map(Field::getName).collect(Collectors.toSet());
145146

146-
assertEquals(11, parentConstants.size(), "Expected 11 section constants in SystemMessageSections");
147+
assertEquals(12, parentConstants.size(), "Expected 12 section constants in SystemMessageSections");
147148

148149
for (String constantName : parentConstants) {
149150
Field parentField = SystemMessageSections.class.getDeclaredField(constantName);
@@ -189,4 +190,41 @@ void shouldUseReplacedIdentitySectionInResponse() throws Exception {
189190
}
190191
}
191192
}
193+
194+
/**
195+
* Verifies that replacing the {@link SystemMessageSections#PREAMBLE} section
196+
* via {@link SectionOverrideAction#REPLACE} causes the assistant to adopt the
197+
* custom identity in its response without affecting sibling sections.
198+
*
199+
* @see Snapshot:
200+
* system_message_sections/should_use_replaced_preamble_section_in_response
201+
*/
202+
@Test
203+
void shouldUseReplacedPreambleSectionInResponse() throws Exception {
204+
ctx.configureForTest("system_message_sections", "should_use_replaced_preamble_section_in_response");
205+
206+
var systemMessage = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE)
207+
.setSections(Map.of(SystemMessageSections.PREAMBLE,
208+
new SectionOverride().setAction(SectionOverrideAction.REPLACE)
209+
.setContent("You are a helpful gardening assistant called Botanica. "
210+
+ "You only answer questions about plants and gardening.")));
211+
212+
try (CopilotClient client = ctx.createClient()) {
213+
CopilotSession session = client.createSession(new SessionConfig().setSystemMessage(systemMessage)
214+
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(30, TimeUnit.SECONDS);
215+
216+
try {
217+
AssistantMessageEvent response = session
218+
.sendAndWait(new MessageOptions().setPrompt("Who are you?"), 60_000).get(90, TimeUnit.SECONDS);
219+
220+
assertNotNull(response, "Expected a response from the assistant");
221+
String content = response.getData().content().toLowerCase();
222+
assertTrue(content.contains("botanica") || content.contains("garden") || content.contains("plant"),
223+
"Expected response to reflect the replaced preamble section, but got: "
224+
+ response.getData().content());
225+
} finally {
226+
session.close();
227+
}
228+
}
229+
}
192230
}

0 commit comments

Comments
 (0)