diff --git a/docs/getting-started.md b/docs/getting-started.md index ad401894f..3eaa8d2ba 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1911,9 +1911,11 @@ 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`. +Available section IDs: `preamble`, `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `runtime_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. +`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. + +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. 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). diff --git a/dotnet/README.md b/dotnet/README.md index 567b1c33b..370d9f82e 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -676,9 +676,9 @@ var session = await client.CreateSessionAsync(new SessionConfig }); ``` -Available section IDs are defined as static properties on the `SystemMessageSection` struct: `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `RuntimeInstructions`, `LastInstructions`. +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. -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. +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. #### Replace Mode diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 706a1ec6b..5365c0915 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1828,6 +1828,13 @@ public enum SectionOverrideAction /// Prepend content before the existing section. [JsonStringEnumMemberName("prepend")] Prepend, + /// + /// No-op marker that opts an individually-addressable section out of a group-level + /// remove (e.g. keep when removing the + /// group). + /// + [JsonStringEnumMemberName("preserve")] + Preserve, /// Transform the section content via a callback. [JsonStringEnumMemberName("transform")] Transform @@ -1866,6 +1873,8 @@ public sealed class SectionOverride public readonly struct SystemMessageSection : IEquatable { /// Agent identity preamble and mode statement. + public static SystemMessageSection Preamble { get; } = new("preamble"); + /// Section group covering the identity preamble and its sibling sub-sections (tone, tool efficiency, etc.). public static SystemMessageSection Identity { get; } = new("identity"); /// Response style, conciseness rules, output formatting preferences. public static SystemMessageSection Tone { get; } = new("tone"); diff --git a/dotnet/test/E2E/SystemMessageSectionsE2ETests.cs b/dotnet/test/E2E/SystemMessageSectionsE2ETests.cs index 210fbcd21..41c46d3b9 100644 --- a/dotnet/test/E2E/SystemMessageSectionsE2ETests.cs +++ b/dotnet/test/E2E/SystemMessageSectionsE2ETests.cs @@ -40,4 +40,34 @@ public async Task Should_Use_Replaced_Identity_Section_In_Response() content.Contains("botanica") || content.Contains("garden") || content.Contains("plant"), $"Expected response to reflect the replaced identity section, but got: {response.Data.Content}"); } + + [Fact] + public async Task Should_Use_Replaced_Preamble_Section_In_Response() + { + var session = await CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + SystemMessage = new SystemMessageConfig + { + Mode = SystemMessageMode.Customize, + Sections = new Dictionary + { + [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?