From ab67dc5015cc3554214d7919682613e200c9e5f1 Mon Sep 17 00:00:00 2001 From: Laurianti Date: Mon, 28 Sep 2026 21:27:47 +0200 Subject: [PATCH] fix(events): treat an error event without function calls as a final response Matches adk-python: a complete event carrying an error code ends the turn unless the model still asked for tools, even when it holds a function response. --- .../java/com/google/adk/events/Event.java | 4 + .../java/com/google/adk/events/EventTest.java | 110 ++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/core/src/main/java/com/google/adk/events/Event.java b/core/src/main/java/com/google/adk/events/Event.java index eb266f459..29cc20a20 100644 --- a/core/src/main/java/com/google/adk/events/Event.java +++ b/core/src/main/java/com/google/adk/events/Event.java @@ -413,6 +413,10 @@ public final boolean finalResponse() { if (actions().skipSummarization().orElse(false) || hasPendingLongRunningToolCall()) { return true; } + // An error ends the turn unless the model still asked for tools; mirrors Python ADK. + if (errorCode().isPresent() && !partial().orElse(false) && functionCalls().isEmpty()) { + return true; + } return functionCalls().isEmpty() && functionResponses().isEmpty() && !partial().orElse(false) diff --git a/core/src/test/java/com/google/adk/events/EventTest.java b/core/src/test/java/com/google/adk/events/EventTest.java index 7eb3f82e0..cadfb7258 100644 --- a/core/src/test/java/com/google/adk/events/EventTest.java +++ b/core/src/test/java/com/google/adk/events/EventTest.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.genai.types.CodeExecutionResult; import com.google.genai.types.Content; import com.google.genai.types.FinishReason; import com.google.genai.types.FunctionCall; @@ -439,6 +440,115 @@ public void finalResponse_returnsTrueIfSkipSummarization() { assertThat(event.finalResponse()).isTrue(); } + @Test + public void finalResponse_isTrueForErrorEvent() { + Event event = + Event.builder() + .id("e1") + .invocationId("i1") + .author("agent") + .content(Content.fromParts(Part.fromText("hello"))) + .errorCode(new FinishReason("SOME_ERROR")) + .errorMessage("Something went wrong") + .build(); + assertThat(event.finalResponse()).isTrue(); + } + + @Test + public void finalResponse_isTrueForErrorEventWithFunctionResponse() { + Event event = + Event.builder() + .id("e1") + .invocationId("i1") + .author("agent") + .content( + Content.fromParts(Part.fromFunctionResponse("tool", ImmutableMap.of("k", "v")))) + .errorCode(new FinishReason("SOME_ERROR")) + .errorMessage("Something went wrong") + .build(); + assertThat(event.finalResponse()).isTrue(); + } + + @Test + public void finalResponse_isFalseForErrorEventWithFunctionCall() { + Event event = + Event.builder() + .id("e1") + .invocationId("i1") + .author("agent") + .content(Content.fromParts(Part.fromFunctionCall("tool", ImmutableMap.of("k", "v")))) + .errorCode(new FinishReason("SOME_ERROR")) + .errorMessage("Something went wrong") + .build(); + assertThat(event.finalResponse()).isFalse(); + } + + @Test + public void finalResponse_isFalseForPartialErrorEvent() { + Event event = + Event.builder() + .id("e1") + .invocationId("i1") + .author("agent") + .content(Content.fromParts(Part.fromText("hello"))) + .errorCode(new FinishReason("SOME_ERROR")) + .errorMessage("Something went wrong") + .partial(true) + .build(); + assertThat(event.finalResponse()).isFalse(); + } + + @Test + public void finalResponse_isFalseForFunctionResponse() { + Event event = + Event.builder() + .id("e1") + .invocationId("i1") + .author("agent") + .content( + Content.fromParts(Part.fromFunctionResponse("tool", ImmutableMap.of("k", "v")))) + .build(); + assertThat(event.finalResponse()).isFalse(); + } + + @Test + public void finalResponse_isFalseForTrailingCodeExecutionResult() { + Event event = + Event.builder() + .id("e1") + .invocationId("i1") + .author("agent") + .content( + Content.fromParts( + Part.fromText("hello"), + Part.builder() + .codeExecutionResult( + CodeExecutionResult.builder().outcome("OK").output("42").build()) + .build())) + .build(); + assertThat(event.finalResponse()).isFalse(); + } + + @Test + public void finalResponse_isTrueForErrorEventWithTrailingCodeExecutionResult() { + Event event = + Event.builder() + .id("e1") + .invocationId("i1") + .author("agent") + .content( + Content.fromParts( + Part.fromText("hello"), + Part.builder() + .codeExecutionResult( + CodeExecutionResult.builder().outcome("OK").output("42").build()) + .build())) + .errorCode(new FinishReason("SOME_ERROR")) + .errorMessage("Something went wrong") + .build(); + assertThat(event.finalResponse()).isTrue(); + } + private static String eventJsonWithNodeInfo(String nodeInfoJson) { return "{\"id\":\"event_id\",\"author\":\"agent\",\"nodeInfo\":" + nodeInfoJson + "}"; }