From 899b871b53253e73c46b370bc0d11e37ce009f98 Mon Sep 17 00:00:00 2001 From: Jaehee Shin Date: Thu, 8 Oct 2026 09:18:07 +0900 Subject: [PATCH] fix(core): align Gemini placeholder user turns with ADK Python GeminiUtil.ensureModelResponse appends a user turn when a request has no contents or does not end with a user turn. In Java this turn is "Continue output. DO NOT look at this line. ONLY look at the content before this line and system instruction." ADK Python, TypeScript and Go use different, neutral wording, and the Java sentence reads like an instruction-override prompt. A request whose only user-role content was this sentence was flagged by Model Armor prompt injection and jailbreak detection (confidence HIGH), which blocks agents that are driven only by their system instruction. Use the same wording as ADK Python and ADK TypeScript: - no contents: "Handle the requests as specified in the System Instruction." - last turn not from the user: "Continue processing previous requests as instructed. Exit or provide a summary if no more outputs are needed." --- .../com/google/adk/models/GeminiUtil.java | 29 +++++++++++++------ .../com/google/adk/models/GeminiUtilTest.java | 19 ++++++++---- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/com/google/adk/models/GeminiUtil.java b/core/src/main/java/com/google/adk/models/GeminiUtil.java index 5986c3b5e..d0c8f0546 100644 --- a/core/src/main/java/com/google/adk/models/GeminiUtil.java +++ b/core/src/main/java/com/google/adk/models/GeminiUtil.java @@ -35,9 +35,20 @@ /** Request / Response utilities for {@link Gemini}. */ public final class GeminiUtil { + /** + * Text of the user turn appended when a request has no contents, so that the model acts on the + * system instruction. Same wording as ADK Python and ADK TypeScript. + */ + public static final String HANDLE_SYSTEM_INSTRUCTION_MESSAGE = + "Handle the requests as specified in the System Instruction."; + + /** + * Text of the user turn appended when the last content is not from the user, so that the model + * keeps producing output. Same wording as ADK Python, ADK TypeScript and ADK Go. + */ public static final String CONTINUE_OUTPUT_MESSAGE = - "Continue output. DO NOT look at this line. ONLY look at the content before this line and" - + " system instruction."; + "Continue processing previous requests as instructed. Exit or provide a summary if no more" + + " outputs are needed."; private GeminiUtil() {} @@ -193,9 +204,10 @@ private static Part removeClientFunctionCallIdFromPart(Part part) { * Ensures that the content is conducive to prompting a model response by ensuring the last * content part is from the user. * - *

If the list is empty or the last message is not from the user, a new "user" content part - * with a {@link #CONTINUE_OUTPUT_MESSAGE} is appended to the list. This is necessary to prompt - * the model to generate a response. + *

If the list is empty, a new "user" content part with {@link + * #HANDLE_SYSTEM_INSTRUCTION_MESSAGE} is appended. If the last message is not from the user, a + * new "user" content part with {@link #CONTINUE_OUTPUT_MESSAGE} is appended. This is necessary to + * prompt the model to generate a response. * * @param contents The original list of {@link Content}. * @return A list of {@link Content} where the last element is guaranteed to be from the "user". @@ -204,11 +216,10 @@ static List ensureModelResponse(List contents) { // Last content must be from the user, otherwise the model won't respond. if (contents.isEmpty() || !Ascii.equalsIgnoreCase(Iterables.getLast(contents).role().orElse(""), Role.USER)) { + String text = + contents.isEmpty() ? HANDLE_SYSTEM_INSTRUCTION_MESSAGE : CONTINUE_OUTPUT_MESSAGE; Content userContent = - Content.builder() - .parts(ImmutableList.of(Part.fromText(CONTINUE_OUTPUT_MESSAGE))) - .role(Role.USER) - .build(); + Content.builder().parts(ImmutableList.of(Part.fromText(text))).role(Role.USER).build(); return Stream.concat(contents.stream(), Stream.of(userContent)).collect(toImmutableList()); } return contents; diff --git a/core/src/test/java/com/google/adk/models/GeminiUtilTest.java b/core/src/test/java/com/google/adk/models/GeminiUtilTest.java index b0943aa50..dca88890e 100644 --- a/core/src/test/java/com/google/adk/models/GeminiUtilTest.java +++ b/core/src/test/java/com/google/adk/models/GeminiUtilTest.java @@ -37,8 +37,16 @@ @RunWith(JUnit4.class) public final class GeminiUtilTest { + // Same wording as the user turns that ADK Python appends in + // BaseLlm._maybe_append_user_content. + private static final Content SYSTEM_INSTRUCTION_CONTENT = + Content.fromParts( + Part.fromText("Handle the requests as specified in the System Instruction.")); private static final Content CONTINUE_CONTENT = - Content.fromParts(Part.fromText(GeminiUtil.CONTINUE_OUTPUT_MESSAGE)); + Content.fromParts( + Part.fromText( + "Continue processing previous requests as instructed. Exit or provide a summary if" + + " no more outputs are needed.")); @Test public void getPart0FromLlmResponse_noContent_returnsEmpty() { @@ -334,12 +342,12 @@ public void sanitizeRequestForGeminiApi_multipleContents_sanitizesAll() { } @Test - public void ensureModelResponse_emptyList_appendsContinueMessage() { + public void ensureModelResponse_emptyList_appendsSystemInstructionMessage() { ImmutableList contents = ImmutableList.of(); List result = GeminiUtil.ensureModelResponse(contents); - assertThat(result).containsExactly(CONTINUE_CONTENT); + assertThat(result).containsExactly(SYSTEM_INSTRUCTION_CONTENT); } @Test @@ -405,12 +413,13 @@ public void ensureModelResponse_lastContentIsNotUser_appendsContinueMessage() { } @Test - public void prepareGenenerateContentRequest_emptyRequest_returnsRequestWithContinueContent() { + public void + prepareGenenerateContentRequest_emptyRequest_returnsRequestWithSystemInstructionContent() { LlmRequest request = LlmRequest.builder().build(); LlmRequest result = GeminiUtil.prepareGenenerateContentRequest(request, true); - assertThat(result.contents()).containsExactly(CONTINUE_CONTENT); + assertThat(result.contents()).containsExactly(SYSTEM_INSTRUCTION_CONTENT); assertThat(result.config()).isEmpty(); }