From 82677ea0c8c083e84119d06772e3353da51c0077 Mon Sep 17 00:00:00 2001 From: tsuz <6927131+tsuz@users.noreply.github.com> Date: Mon, 25 May 2026 05:30:47 +0900 Subject: [PATCH 1/3] fail if percentage sign exists --- .../io/flightdeck/think/consumer/ThinkConsumer.java | 6 +++++- .../flightdeck/think/consumer/SystemPromptTest.java | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/think/think-consumer/src/main/java/io/flightdeck/think/consumer/ThinkConsumer.java b/think/think-consumer/src/main/java/io/flightdeck/think/consumer/ThinkConsumer.java index 4ccf16a..eb87140 100644 --- a/think/think-consumer/src/main/java/io/flightdeck/think/consumer/ThinkConsumer.java +++ b/think/think-consumer/src/main/java/io/flightdeck/think/consumer/ThinkConsumer.java @@ -353,6 +353,10 @@ private void produceResponse(String sessionId, ThinkResponse response) throws Ex * Builds the system prompt, injecting memoir context if available. */ static String buildSystemPrompt(String memoirContext) { + return buildSystemPrompt(SYSTEM_PROMPT_TEMPLATE, memoirContext); + } + + static String buildSystemPrompt(String template, String memoirContext) { StringBuilder extra = new StringBuilder(); if (memoirContext != null && !memoirContext.isBlank()) { @@ -361,7 +365,7 @@ static String buildSystemPrompt(String memoirContext) { extra.append("\n\nUse the memoir to personalize your responses."); } - return String.format(SYSTEM_PROMPT_TEMPLATE, extra.toString()); + return String.format(template, extra.toString()); } /** diff --git a/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java b/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java index c1f7719..13eb52a 100644 --- a/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java +++ b/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java @@ -36,4 +36,17 @@ void missingFile_throws() { .isInstanceOf(IllegalStateException.class) .hasMessageContaining("SYSTEM_PROMPT_FILE not found"); } + + @Test + @DisplayName("System prompt containing % character is preserved literally (issue #23)") + void percentSign_inPrompt_doesNotThrow() { + // Simulates what loadSystemPromptTemplate() + "\n\n%s" produces when + // the loaded user prompt contains a literal '%' (e.g. "10% off"). + String basePrompt = "Apply a 10% discount when asked."; + String template = basePrompt + "\n\n%s"; + + String result = ThinkConsumer.buildSystemPrompt(template, null); + + assertThat(result).contains("10% discount"); + } } From c1e0eabc149497784aa1e39d5f53eedfa57991a0 Mon Sep 17 00:00:00 2001 From: tsuz <6927131+tsuz@users.noreply.github.com> Date: Mon, 25 May 2026 05:35:50 +0900 Subject: [PATCH 2/3] add fix for percentage sign --- .../flightdeck/think/consumer/ThinkConsumer.java | 16 ++++++++-------- .../think/consumer/SystemPromptTest.java | 11 ++++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/think/think-consumer/src/main/java/io/flightdeck/think/consumer/ThinkConsumer.java b/think/think-consumer/src/main/java/io/flightdeck/think/consumer/ThinkConsumer.java index eb87140..2334648 100644 --- a/think/think-consumer/src/main/java/io/flightdeck/think/consumer/ThinkConsumer.java +++ b/think/think-consumer/src/main/java/io/flightdeck/think/consumer/ThinkConsumer.java @@ -47,7 +47,7 @@ public class ThinkConsumer implements AutoCloseable { Be concise and helpful. When using tools, explain what you're doing and why."""; - private static final String SYSTEM_PROMPT_TEMPLATE = loadSystemPromptTemplate() + "\n\n%s"; + private static final String SYSTEM_PROMPT_BASE = loadSystemPromptTemplate(); private static String loadSystemPromptTemplate() { return loadSystemPromptFromFile(AppConfig.SYSTEM_PROMPT_FILE); @@ -353,19 +353,19 @@ private void produceResponse(String sessionId, ThinkResponse response) throws Ex * Builds the system prompt, injecting memoir context if available. */ static String buildSystemPrompt(String memoirContext) { - return buildSystemPrompt(SYSTEM_PROMPT_TEMPLATE, memoirContext); + return buildSystemPrompt(SYSTEM_PROMPT_BASE, memoirContext); } - static String buildSystemPrompt(String template, String memoirContext) { - StringBuilder extra = new StringBuilder(); + static String buildSystemPrompt(String basePrompt, String memoirContext) { + StringBuilder out = new StringBuilder(basePrompt).append("\n\n"); if (memoirContext != null && !memoirContext.isBlank()) { - extra.append("\n\nUser memoir (known facts about this user from previous sessions):\n"); - extra.append(memoirContext); - extra.append("\n\nUse the memoir to personalize your responses."); + out.append("\n\nUser memoir (known facts about this user from previous sessions):\n"); + out.append(memoirContext); + out.append("\n\nUse the memoir to personalize your responses."); } - return String.format(template, extra.toString()); + return out.toString(); } /** diff --git a/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java b/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java index 13eb52a..907238e 100644 --- a/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java +++ b/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java @@ -40,13 +40,14 @@ void missingFile_throws() { @Test @DisplayName("System prompt containing % character is preserved literally (issue #23)") void percentSign_inPrompt_doesNotThrow() { - // Simulates what loadSystemPromptTemplate() + "\n\n%s" produces when - // the loaded user prompt contains a literal '%' (e.g. "10% off"). String basePrompt = "Apply a 10% discount when asked."; - String template = basePrompt + "\n\n%s"; - String result = ThinkConsumer.buildSystemPrompt(template, null); + String withoutMemoir = ThinkConsumer.buildSystemPrompt(basePrompt, null); + assertThat(withoutMemoir).contains("10% discount"); - assertThat(result).contains("10% discount"); + String withMemoir = ThinkConsumer.buildSystemPrompt(basePrompt, "user likes 50% off coupons"); + assertThat(withMemoir) + .contains("10% discount") + .contains("50% off coupons"); } } From eca09d8360388f1254a704eceb26724411e00220 Mon Sep 17 00:00:00 2001 From: tsuz <6927131+tsuz@users.noreply.github.com> Date: Mon, 25 May 2026 06:16:02 +0900 Subject: [PATCH 3/3] handle other special chars --- .../think/consumer/SystemPromptTest.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java b/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java index 907238e..0446258 100644 --- a/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java +++ b/think/think-consumer/src/test/java/io/flightdeck/think/consumer/SystemPromptTest.java @@ -38,8 +38,8 @@ void missingFile_throws() { } @Test - @DisplayName("System prompt containing % character is preserved literally (issue #23)") - void percentSign_inPrompt_doesNotThrow() { + @DisplayName("System prompt containing special characters is preserved literally (issue #23)") + void specialChars_inPrompt_doesNotThrow() { String basePrompt = "Apply a 10% discount when asked."; String withoutMemoir = ThinkConsumer.buildSystemPrompt(basePrompt, null); @@ -49,5 +49,21 @@ void percentSign_inPrompt_doesNotThrow() { assertThat(withMemoir) .contains("10% discount") .contains("50% off coupons"); + + // Sequences that String.format would interpret as conversions/specifiers + // must survive unchanged now that the prompt is concatenated, not formatted. + assertThat(ThinkConsumer.buildSystemPrompt("Print %s and %d and %n literally.", null)) + .contains("%s and %d and %n literally."); + assertThat(ThinkConsumer.buildSystemPrompt("A 100%% bonus", "memoir with %s token")) + .contains("100%% bonus") + .contains("memoir with %s token"); + + // Other characters that are easy to mishandle in string plumbing. + assertThat(ThinkConsumer.buildSystemPrompt("Use a backslash \\ and a dollar $ sign.", null)) + .contains("backslash \\ and a dollar $ sign."); + assertThat(ThinkConsumer.buildSystemPrompt("Braces {0} {1} and brackets [x] stay.", null)) + .contains("Braces {0} {1} and brackets [x] stay."); + assertThat(ThinkConsumer.buildSystemPrompt("Quotes \"q\" 'a' and emoji 🚀 stay.", null)) + .contains("Quotes \"q\" 'a' and emoji 🚀 stay."); } }