From d7bd3e8e0e916deafdbea7d207b0b87b8f230d00 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:55:16 +0000 Subject: [PATCH 01/61] chore(deps): bump org.assertj:assertj-core Bumps the maven group with 1 update in the / directory: [org.assertj:assertj-core](https://github.com/assertj/assertj). Updates `org.assertj:assertj-core` from 3.27.3 to 3.27.7 - [Release notes](https://github.com/assertj/assertj/releases) - [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.3...assertj-build-3.27.7) --- updated-dependencies: - dependency-name: org.assertj:assertj-core dependency-version: 3.27.7 dependency-type: direct:production dependency-group: maven ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6a1aa5af5..0ba20838e 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ 3.1.5 3.7.0 2.35.1 - 3.27.3 + 3.27.7 1.4.0 3.9.0 5.4.3 From 15255b48285819c7d3aedb4470e91f37d1bcfaf4 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Wed, 25 Feb 2026 03:21:54 -0800 Subject: [PATCH 02/61] feat: drop gemini-1 support in GoogleSearchTool PiperOrigin-RevId: 875065099 --- .../google/adk/tools/GoogleSearchTool.java | 16 +-------------- .../com/google/adk/tools/BaseToolTest.java | 20 ------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/core/src/main/java/com/google/adk/tools/GoogleSearchTool.java b/core/src/main/java/com/google/adk/tools/GoogleSearchTool.java index 6f89754cf..b4f298c21 100644 --- a/core/src/main/java/com/google/adk/tools/GoogleSearchTool.java +++ b/core/src/main/java/com/google/adk/tools/GoogleSearchTool.java @@ -20,12 +20,9 @@ import com.google.common.collect.ImmutableList; import com.google.genai.types.GenerateContentConfig; import com.google.genai.types.GoogleSearch; -import com.google.genai.types.GoogleSearchRetrieval; import com.google.genai.types.Tool; import io.reactivex.rxjava3.core.Completable; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A built-in tool that is automatically invoked by Gemini 2 and 3 models to retrieve search results @@ -43,7 +40,6 @@ * } */ public final class GoogleSearchTool extends BaseTool { - private static final Logger logger = LoggerFactory.getLogger(GoogleSearchTool.class); public static final GoogleSearchTool INSTANCE = new GoogleSearchTool(); public GoogleSearchTool() { @@ -66,17 +62,7 @@ public Completable processLlmRequest( updatedToolsBuilder.addAll(existingTools); String model = llmRequestBuilder.build().model().get(); - if (model != null && model.startsWith("gemini-1")) { - if (!updatedToolsBuilder.build().isEmpty()) { - logger.error("Tools already present: {}", configBuilder.build().tools().get()); - return Completable.error( - new IllegalArgumentException( - "Google search tool cannot be used with other tools in Gemini 1.x.")); - } - updatedToolsBuilder.add( - Tool.builder().googleSearchRetrieval(GoogleSearchRetrieval.builder().build()).build()); - configBuilder.tools(updatedToolsBuilder.build()); - } else if (model != null && (model.startsWith("gemini-2") || model.startsWith("gemini-3"))) { + if (model != null && (model.startsWith("gemini-2") || model.startsWith("gemini-3"))) { updatedToolsBuilder.add(Tool.builder().googleSearch(GoogleSearch.builder().build()).build()); configBuilder.tools(updatedToolsBuilder.build()); diff --git a/core/src/test/java/com/google/adk/tools/BaseToolTest.java b/core/src/test/java/com/google/adk/tools/BaseToolTest.java index 16418657d..e8d3887a4 100644 --- a/core/src/test/java/com/google/adk/tools/BaseToolTest.java +++ b/core/src/test/java/com/google/adk/tools/BaseToolTest.java @@ -8,7 +8,6 @@ import com.google.genai.types.GenerateContentConfig; import com.google.genai.types.GoogleMaps; import com.google.genai.types.GoogleSearch; -import com.google.genai.types.GoogleSearchRetrieval; import com.google.genai.types.Tool; import com.google.genai.types.ToolCodeExecution; import com.google.genai.types.UrlContext; @@ -143,25 +142,6 @@ public void processLlmRequestWithGoogleSearchToolAddsToolToConfig() { Tool.builder().googleSearch(GoogleSearch.builder().build()).build()); } - @Test - public void processLlmRequestWithGoogleSearchRetrievalToolAddsToolToConfig() { - GoogleSearchTool googleSearchTool = new GoogleSearchTool(); - LlmRequest llmRequest = - LlmRequest.builder() - .config(GenerateContentConfig.builder().build()) - .model("gemini-1") - .build(); - LlmRequest.Builder llmRequestBuilder = llmRequest.toBuilder(); - Completable unused = - googleSearchTool.processLlmRequest(llmRequestBuilder, /* toolContext= */ null); - LlmRequest updatedLlmRequest = llmRequestBuilder.build(); - assertThat(updatedLlmRequest.config()).isPresent(); - assertThat(updatedLlmRequest.config().get().tools()).isPresent(); - assertThat(updatedLlmRequest.config().get().tools().get()) - .containsExactly( - Tool.builder().googleSearchRetrieval(GoogleSearchRetrieval.builder().build()).build()); - } - @Test public void processLlmRequestWithUrlContextToolAddsToolToConfig() { FunctionDeclaration functionDeclaration = From 746e857d97c6f356ffe5c20be0ccae85d5a8f989 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Wed, 25 Feb 2026 07:41:00 -0800 Subject: [PATCH 03/61] feat: Handle final and error TaskStatusUpdateEvents PiperOrigin-RevId: 875158844 --- .../adk/a2a/converters/PartConverter.java | 7 ++ .../adk/a2a/converters/ResponseConverter.java | 39 +++++++++-- .../a2a/converters/ResponseConverterTest.java | 70 +++++++++++++++++++ 3 files changed, 109 insertions(+), 7 deletions(-) diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java index c6ef06400..bc0620f83 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java @@ -47,6 +47,13 @@ public final class PartConverter { "code_execution_result"; public static final String A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = "executable_code"; + public static Optional toTextPart(io.a2a.spec.Part part) { + if (part instanceof TextPart textPart) { + return Optional.of(textPart); + } + return Optional.empty(); + } + /** Convert an A2A JSON part into a Google GenAI part representation. */ public static Optional toGenaiPart(io.a2a.spec.Part a2aPart) { if (a2aPart == null) { diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java index 61ab84c90..0a272b72d 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java @@ -197,13 +197,28 @@ private static Optional handleTaskUpdate( } if (updateEvent instanceof TaskStatusUpdateEvent statusEvent) { var status = statusEvent.getStatus(); - return Optional.ofNullable(status.message()) - .map( - value -> - messageToEvent( - value, - context, - PENDING_STATES.contains(event.getTask().getStatus().state()))); + var taskState = event.getTask().getStatus().state(); + + Optional messageEvent = + Optional.ofNullable(status.message()) + .map( + value -> { + if (taskState == TaskState.FAILED) { + return messageToFailedEvent(value, context); + } + return messageToEvent(value, context, PENDING_STATES.contains(taskState)); + }); + + if (statusEvent.isFinal()) { + return messageEvent + .map(Event::toBuilder) + .or(() -> Optional.of(remoteAgentEventBuilder(context))) + .map(builder -> builder.turnComplete(true)) + .map(builder -> builder.partial(false)) + .map(Event.Builder::build); + } else { + return messageEvent; + } } throw new IllegalArgumentException( "Unsupported TaskUpdateEvent type: " + updateEvent.getClass()); @@ -216,6 +231,16 @@ public static Event messageToEvent(Message message, InvocationContext invocation .build(); } + /** Converts an A2A message for a failed task to ADK event filling in the error message. */ + public static Event messageToFailedEvent(Message message, InvocationContext invocationContext) { + Event.Builder builder = remoteAgentEventBuilder(invocationContext); + Optional.ofNullable(Iterables.getFirst(message.getParts(), null)) + .flatMap(PartConverter::toTextPart) + .ifPresent(textPart -> builder.errorMessage(textPart.getText())); + + return builder.build(); + } + /** * Converts an A2A message back to ADK events. For streaming task in pending state it sets the * thought field to true, to mark them as thought updates. diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java index 1a4873a85..d196d2f6d 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java @@ -62,6 +62,10 @@ private Task.Builder testTask() { return new Task.Builder().id("task-1").contextId("context-1"); } + private static TaskStatusUpdateEvent.Builder testTaskStatusUpdateEvent() { + return new TaskStatusUpdateEvent.Builder().taskId("task-1").contextId("context-1"); + } + @Test public void eventsToMessage_withNullEvents_returnsEmptyAgentMessage() { Message message = ResponseConverter.eventsToMessage(null, "context-1", "task-1"); @@ -330,6 +334,72 @@ public void clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkFalse_re assertThat(optionalEvent).isEmpty(); } + @Test + public void clientEventToEvent_withFinalTaskStatusUpdateEvent_withMessage_returnsEvent() { + Message statusMessage = + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("Final status message"))) + .build(); + TaskStatus status = new TaskStatus(TaskState.COMPLETED, statusMessage, null); + TaskStatusUpdateEvent updateEvent = + testTaskStatusUpdateEvent().isFinal(true).status(status).build(); + + TaskUpdateEvent event = new TaskUpdateEvent(testTask().status(status).build(), updateEvent); + + Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); + assertThat(optionalEvent).isPresent(); + Event resultEvent = optionalEvent.get(); + assertThat(resultEvent.content().get().parts().get().get(0).text()) + .hasValue("Final status message"); + assertThat(resultEvent.content().get().parts().get().get(0).thought()).hasValue(false); + assertThat(resultEvent.partial().orElse(false)).isFalse(); + assertThat(resultEvent.turnComplete()).hasValue(true); + } + + @Test + public void clientEventToEvent_withFinalTaskStatusUpdateEvent_withoutMessage_returnsEvent() { + TaskStatus status = new TaskStatus(TaskState.COMPLETED, null, null); + TaskStatusUpdateEvent updateEvent = + new TaskStatusUpdateEvent("task-id-1", status, "context-1", true, null); + TaskUpdateEvent event = new TaskUpdateEvent(testTask().status(status).build(), updateEvent); + + Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); + assertThat(optionalEvent).isPresent(); + Event resultEvent = optionalEvent.get(); + assertThat(resultEvent.turnComplete()).hasValue(true); + } + + @Test + public void clientEventToEvent_withNonFinalTaskStatusUpdateEvent_withoutMessage_returnsEmpty() { + TaskStatus status = new TaskStatus(TaskState.WORKING, null, null); + TaskStatusUpdateEvent updateEvent = + new TaskStatusUpdateEvent("task-id-1", status, "context-1", false, null); + TaskUpdateEvent event = new TaskUpdateEvent(testTask().status(status).build(), updateEvent); + + Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); + assertThat(optionalEvent).isEmpty(); + } + + @Test + public void clientEventToEvent_withFailedTaskStatusUpdateEvent_returnsErrorEvent() { + Message statusMessage = + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("Task failed"))) + .build(); + TaskStatus status = new TaskStatus(TaskState.FAILED, statusMessage, null); + TaskStatusUpdateEvent updateEvent = + new TaskStatusUpdateEvent("task-id-1", status, "context-1", true, null); + TaskUpdateEvent event = new TaskUpdateEvent(testTask().status(status).build(), updateEvent); + + Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); + assertThat(optionalEvent).isPresent(); + Event resultEvent = optionalEvent.get(); + assertThat(resultEvent.errorMessage()).hasValue("Task failed"); + assertThat(resultEvent.turnComplete()).hasValue(true); + } + private static final class TestAgent extends BaseAgent { TestAgent() { super("test_agent", "test", ImmutableList.of(), null, null); From 1a593a996607904eed24b64bc63eecd7708710af Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Wed, 25 Feb 2026 10:57:18 -0800 Subject: [PATCH 04/61] feat: remove model restrictions in BuiltInCodeExecutionTool PiperOrigin-RevId: 875242195 --- .../adk/tools/BuiltInCodeExecutionTool.java | 33 +++++- .../com/google/adk/utils/ModelNameUtils.java | 31 ++++++ .../com/google/adk/tools/BaseToolTest.java | 45 +++++++- .../google/adk/utils/ModelNameUtilsTest.java | 100 ++++++++++++++++++ 4 files changed, 203 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/com/google/adk/tools/BuiltInCodeExecutionTool.java b/core/src/main/java/com/google/adk/tools/BuiltInCodeExecutionTool.java index 060b3ffb8..ad97b96a6 100644 --- a/core/src/main/java/com/google/adk/tools/BuiltInCodeExecutionTool.java +++ b/core/src/main/java/com/google/adk/tools/BuiltInCodeExecutionTool.java @@ -16,13 +16,19 @@ package com.google.adk.tools; +import com.google.adk.agents.LlmAgent; +import com.google.adk.models.BaseLlm; import com.google.adk.models.LlmRequest; +import com.google.adk.utils.ModelNameUtils; import com.google.common.collect.ImmutableList; import com.google.genai.types.GenerateContentConfig; import com.google.genai.types.Tool; import com.google.genai.types.ToolCodeExecution; import io.reactivex.rxjava3.core.Completable; import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A built-in code execution tool that is automatically invoked by Gemini 2 models. @@ -32,6 +38,7 @@ */ public final class BuiltInCodeExecutionTool extends BaseTool { public static final BuiltInCodeExecutionTool INSTANCE = new BuiltInCodeExecutionTool(); + private static final Logger LOG = LoggerFactory.getLogger(BuiltInCodeExecutionTool.class); public BuiltInCodeExecutionTool() { super("code_execution", "code_execution"); @@ -41,10 +48,28 @@ public BuiltInCodeExecutionTool() { public Completable processLlmRequest( LlmRequest.Builder llmRequestBuilder, ToolContext toolContext) { - String model = llmRequestBuilder.build().model().get(); - if (model.isEmpty() || !model.startsWith("gemini-2")) { - return Completable.error( - new IllegalArgumentException("Code execution tool is not supported for model " + model)); + Optional model = + Optional.ofNullable(toolContext) + .flatMap(tCtx -> Optional.ofNullable(tCtx.invocationContext())) + .flatMap( + iCtx -> { + if (iCtx.agent() instanceof LlmAgent llmAgent) { + return Optional.of(llmAgent); + } else { + return Optional.empty(); + } + }) + .flatMap(llmAgent -> llmAgent.resolvedModel().model()); + + String modelName = llmRequestBuilder.build().model().get(); + if (!ModelNameUtils.isGeminiModel(modelName) + || model.filter(ModelNameUtils::isInstanceOfGemini).isEmpty()) { + // model name is not a gemini model, or the model isn't an instance of Gemini class (eg. + // LangChain case). + LOG.warn( + "Code execution tool is not supported for model: {} ({}).", + modelName, + model.map(Object::getClass).map(Class::toString).orElse("")); } GenerateContentConfig.Builder configBuilder = llmRequestBuilder diff --git a/core/src/main/java/com/google/adk/utils/ModelNameUtils.java b/core/src/main/java/com/google/adk/utils/ModelNameUtils.java index 9995f18b2..c46f6e3a8 100644 --- a/core/src/main/java/com/google/adk/utils/ModelNameUtils.java +++ b/core/src/main/java/com/google/adk/utils/ModelNameUtils.java @@ -16,16 +16,24 @@ package com.google.adk.utils; +import com.google.common.base.Strings; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class ModelNameUtils { + private static final String GEMINI_PREFIX = "gemini-"; private static final Pattern GEMINI_2_PATTERN = Pattern.compile("^gemini-2\\..*"); + private static final String GEMINI_CLASS = "com.google.adk.models.Gemini"; private static final Pattern PATH_PATTERN = Pattern.compile("^projects/[^/]+/locations/[^/]+/publishers/[^/]+/models/(.+)$"); private static final Pattern APIGEE_PATTERN = Pattern.compile("^apigee/(?:[^/]+/)?(?:[^/]+/)?(.+)$"); + public static boolean isGeminiModel(String modelString) { + return extractModelName(Strings.nullToEmpty(modelString)).startsWith(GEMINI_PREFIX); + } + public static boolean isGemini2Model(String modelString) { if (modelString == null) { return false; @@ -34,6 +42,29 @@ public static boolean isGemini2Model(String modelString) { return GEMINI_2_PATTERN.matcher(modelName).matches(); } + /** + * Checks whether an object is an instance of {@link com.google.adk.models.Gemini}, by searching + * through its class hierarchy for a class whose name equals the hardcoded String name of Gemini + * class. + * + *

This method can be used where the "real" instanceof check is not possible because the Gemini + * type is not known at compile time. + * + * @param o The object to check. + * @return true if object's class is {@link com.google.adk.models.Gemini}, false otherwise. + */ + public static boolean isInstanceOfGemini(Object o) { + if (o == null) { + return false; + } + for (Class clazz = o.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + if (Objects.equals(clazz.getName(), GEMINI_CLASS)) { + return true; + } + } + return false; + } + /** * Extract the actual model name from either simple or path-based format. * diff --git a/core/src/test/java/com/google/adk/tools/BaseToolTest.java b/core/src/test/java/com/google/adk/tools/BaseToolTest.java index e8d3887a4..2a07e7a44 100644 --- a/core/src/test/java/com/google/adk/tools/BaseToolTest.java +++ b/core/src/test/java/com/google/adk/tools/BaseToolTest.java @@ -2,7 +2,11 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.adk.agents.InvocationContext; +import com.google.adk.agents.LlmAgent; +import com.google.adk.models.Gemini; import com.google.adk.models.LlmRequest; +import com.google.adk.sessions.InMemorySessionService; import com.google.common.collect.ImmutableList; import com.google.genai.types.FunctionDeclaration; import com.google.genai.types.GenerateContentConfig; @@ -171,13 +175,27 @@ public void processLlmRequestWithUrlContextToolAddsToolToConfig() { Tool.builder().urlContext(UrlContext.builder().build()).build()); } + private static InvocationContext.Builder testInvocationContext() { + InvocationContext.Builder builder = InvocationContext.builder(); + builder.agent(testAgent().build()); + InMemorySessionService inMemorySessionService = new InMemorySessionService(); + builder.sessionService(inMemorySessionService); + builder.session(inMemorySessionService.createSession("test-app", "test-user-id").blockingGet()); + return builder; + } + + private static LlmAgent.Builder testAgent() { + return LlmAgent.builder().name("test-agent"); + } + @Test - public void processLlmRequestWithBuiltInCodeExecutionToolAddsToolToConfig() { + public void + processLlmRequestWithBuiltInCodeExecutionToolAndNonGeminiModelAndNullContextAddsToolToConfig() { BuiltInCodeExecutionTool builtInCodeExecutionTool = new BuiltInCodeExecutionTool(); LlmRequest llmRequest = LlmRequest.builder() .config(GenerateContentConfig.builder().build()) - .model("gemini-2") + .model("text-bison") .build(); LlmRequest.Builder llmRequestBuilder = llmRequest.toBuilder(); Completable unused = @@ -189,6 +207,29 @@ public void processLlmRequestWithBuiltInCodeExecutionToolAddsToolToConfig() { .containsExactly(Tool.builder().codeExecution(ToolCodeExecution.builder().build()).build()); } + @Test + public void processLlmRequestWithBuiltInCodeExecutionToolAndGemini2ModelAddsToolToConfig() { + BuiltInCodeExecutionTool builtInCodeExecutionTool = new BuiltInCodeExecutionTool(); + LlmRequest llmRequest = + LlmRequest.builder() + .config(GenerateContentConfig.builder().build()) + .model("gemini-2") + .build(); + LlmRequest.Builder llmRequestBuilder = llmRequest.toBuilder(); + ToolContext toolContext = + ToolContext.builder( + testInvocationContext() + .agent(testAgent().model(new Gemini("gemini-2", "")).build()) + .build()) + .build(); + Completable unused = builtInCodeExecutionTool.processLlmRequest(llmRequestBuilder, toolContext); + LlmRequest updatedLlmRequest = llmRequestBuilder.build(); + assertThat(updatedLlmRequest.config()).isPresent(); + assertThat(updatedLlmRequest.config().get().tools()).isPresent(); + assertThat(updatedLlmRequest.config().get().tools().get()) + .containsExactly(Tool.builder().codeExecution(ToolCodeExecution.builder().build()).build()); + } + @Test public void processLlmRequestWithGoogleMapsToolAddsToolToConfig() { GoogleMapsTool googleMapsTool = new GoogleMapsTool(); diff --git a/core/src/test/java/com/google/adk/utils/ModelNameUtilsTest.java b/core/src/test/java/com/google/adk/utils/ModelNameUtilsTest.java index 37853c477..20dda7034 100644 --- a/core/src/test/java/com/google/adk/utils/ModelNameUtilsTest.java +++ b/core/src/test/java/com/google/adk/utils/ModelNameUtilsTest.java @@ -2,6 +2,7 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.adk.models.Gemini; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -69,4 +70,103 @@ public void isGemini2Model_withApigeeProviderV1BetaGemini2Model_returnsTrue() { public void isGemini2Model_withNullModel_returnsFalse() { assertThat(ModelNameUtils.isGemini2Model(null)).isFalse(); } + + @Test + public void isGeminiModel_withGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withNonGeminiModel_returnsFalse() { + assertThat(ModelNameUtils.isGeminiModel("text-bison")).isFalse(); + } + + @Test + public void isGeminiModel_withPathBasedGeminiModel_returnsTrue() { + assertThat( + ModelNameUtils.isGeminiModel( + "projects/test-project/locations/us-central1/publishers/google/models/gemini-1.5-pro")) + .isTrue(); + } + + @Test + public void isGeminiModel_withPathBasedNonGeminiModel_returnsFalse() { + assertThat( + ModelNameUtils.isGeminiModel( + "projects/test-project/locations/us-central1/publishers/google/models/text-bison")) + .isFalse(); + } + + @Test + public void isGeminiModel_withApigeeGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeV1GeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/v1/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeProviderGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/gemini/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeProviderVertexGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/vertex_ai/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeProviderV1GeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/gemini/v1/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeProviderV1BetaGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/vertex_ai/v1beta/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withNullModel_returnsFalse() { + assertThat(ModelNameUtils.isGeminiModel(null)).isFalse(); + } + + @Test + public void isGeminiModel_withEmptyModel_returnsFalse() { + assertThat(ModelNameUtils.isGeminiModel("")).isFalse(); + } + + @Test + public void isInstanceOfGemini_withGeminiInstance_returnsTrue() { + assertThat(ModelNameUtils.isInstanceOfGemini(new Gemini("", ""))).isTrue(); + } + + @Test + public void isInstanceOfGemini_withNonGeminiInstance_returnsFalse() { + assertThat(ModelNameUtils.isInstanceOfGemini(new Object())).isFalse(); + } + + @Test + public void isInstanceOfGemini_withNullInstance_returnsFalse() { + assertThat(ModelNameUtils.isInstanceOfGemini(null)).isFalse(); + } + + private static class GeminiSubclass extends Gemini { + GeminiSubclass() { + super("test-model", "test-api-key"); + } + } + + private static class GeminiSubclassSubclass extends GeminiSubclass {} + + @Test + public void isInstanceOfGemini_withGeminiSubclassInstance_returnsTrue() { + assertThat(ModelNameUtils.isInstanceOfGemini(new GeminiSubclass())).isTrue(); + } + + @Test + public void isInstanceOfGemini_withSubclassOfGeminiSubclassInstance_returnsTrue() { + assertThat(ModelNameUtils.isInstanceOfGemini(new GeminiSubclassSubclass())).isTrue(); + } } From 94aacc8ce403c69447c6726c07d02f0b176f28b0 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Wed, 25 Feb 2026 11:03:12 -0800 Subject: [PATCH 05/61] chore: update errorprone to 2.47.0 PiperOrigin-RevId: 875245205 --- a2a/pom.xml | 2 -- pom.xml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/a2a/pom.xml b/a2a/pom.xml index e4bd841a1..08b2cf228 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -23,7 +23,6 @@ 3.1.5 1.0.0 2.0.17 - 2.38.0 1.4.4 4.13.2 @@ -42,7 +41,6 @@ com.google.errorprone error_prone_annotations - ${errorprone.version} com.fasterxml.jackson.core diff --git a/pom.xml b/pom.xml index 3e97f54b5..6aeafc63a 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 3.4.1 1.49.0 0.14.0 - 2.38.0 + 2.47.0 1.32.0 4.32.0 5.11.4 From 2d2903c2700ac62f2d3a10c856e1880e59256f2a Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Wed, 25 Feb 2026 14:29:11 -0800 Subject: [PATCH 06/61] refactor: Replacing Optional in BaseToolSet with Nullable PiperOrigin-RevId: 875337870 --- .../com/google/adk/tools/BaseToolset.java | 32 ++++++++++++------- .../com/google/adk/tools/ToolPredicate.java | 14 ++++++++ .../google/adk/tools/mcp/McpAsyncToolset.java | 4 +-- .../com/google/adk/tools/mcp/McpToolset.java | 4 +-- 4 files changed, 36 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/com/google/adk/tools/BaseToolset.java b/core/src/main/java/com/google/adk/tools/BaseToolset.java index 4d3482c57..c8ed6df4e 100644 --- a/core/src/main/java/com/google/adk/tools/BaseToolset.java +++ b/core/src/main/java/com/google/adk/tools/BaseToolset.java @@ -20,6 +20,7 @@ import io.reactivex.rxjava3.core.Flowable; import java.util.List; import java.util.Optional; +import javax.annotation.Nullable; /** Base interface for toolsets. */ public interface BaseToolset extends AutoCloseable { @@ -43,28 +44,35 @@ public interface BaseToolset extends AutoCloseable { void close() throws Exception; /** - * Helper method to be used by implementers that returns true if the given tool is in the provided - * list of tools of if testing against the given ToolPredicate returns true (otherwise false). + * Checks if a tool should be selected based on a filter. * * @param tool The tool to check. - * @param toolFilter An Optional containing either a ToolPredicate or a List of tool names. - * @param readonlyContext The current context. - * @return true if the tool is selected. + * @param toolFilter A ToolPredicate, a List of tool names, or null. + * @param readonlyContext The context for checking the tool, or null. */ default boolean isToolSelected( - BaseTool tool, Optional toolFilter, Optional readonlyContext) { - if (toolFilter.isEmpty()) { + BaseTool tool, @Nullable Object toolFilter, @Nullable ReadonlyContext readonlyContext) { + if (toolFilter == null) { return true; } - Object filter = toolFilter.get(); - if (filter instanceof ToolPredicate toolPredicate) { + + if (toolFilter instanceof ToolPredicate toolPredicate) { return toolPredicate.test(tool, readonlyContext); } - if (filter instanceof List) { - @SuppressWarnings("unchecked") - List toolNames = (List) filter; + + if (toolFilter instanceof List toolNames) { return toolNames.contains(tool.name()); } + return false; } + + /** + * @deprecated Use {@link #isToolSelected(BaseTool, Object, ReadonlyContext)} instead. + */ + @Deprecated + default boolean isToolSelected( + BaseTool tool, Optional toolFilter, Optional readonlyContext) { + return isToolSelected(tool, toolFilter.orElse(null), readonlyContext.orElse(null)); + } } diff --git a/core/src/main/java/com/google/adk/tools/ToolPredicate.java b/core/src/main/java/com/google/adk/tools/ToolPredicate.java index 86d739e70..6adf53c18 100644 --- a/core/src/main/java/com/google/adk/tools/ToolPredicate.java +++ b/core/src/main/java/com/google/adk/tools/ToolPredicate.java @@ -18,6 +18,7 @@ import com.google.adk.agents.ReadonlyContext; import java.util.Optional; +import javax.annotation.Nullable; /** * Functional interface to decide whether a tool should be exposed to the LLM based on the current @@ -31,6 +32,19 @@ public interface ToolPredicate { * @param tool The tool to check. * @param readonlyContext The current context. * @return true if the tool should be selected, false otherwise. + * @deprecated Use {@link #test(BaseTool, ReadonlyContext)} instead. */ + @Deprecated boolean test(BaseTool tool, Optional readonlyContext); + + /** + * Decides if the given tool is selected. + * + * @param tool The tool to check. + * @param readonlyContext The current context. + * @return true if the tool should be selected, false otherwise. + */ + default boolean test(BaseTool tool, @Nullable ReadonlyContext readonlyContext) { + return test(tool, Optional.ofNullable(readonlyContext)); + } } diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java b/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java index 45a2fe333..73af9cc6a 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java +++ b/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java @@ -170,9 +170,7 @@ public Flowable getTools(ReadonlyContext readonlyContext) { .map( tools -> tools.stream() - .filter( - tool -> - isToolSelected(tool, toolFilter, Optional.ofNullable(readonlyContext))) + .filter(tool -> isToolSelected(tool, toolFilter.orElse(null), readonlyContext)) .toList()) .onErrorResumeNext( err -> { diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java b/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java index 3bf8f39d0..207243ceb 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java +++ b/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java @@ -216,9 +216,7 @@ public Flowable getTools(ReadonlyContext readonlyContext) { new McpTool( tool, this.mcpSession, this.mcpSessionManager, this.objectMapper)) .filter( - tool -> - isToolSelected( - tool, toolFilter, Optional.ofNullable(readonlyContext)))); + tool -> isToolSelected(tool, toolFilter.orElse(null), readonlyContext))); }) .retryWhen( errorObservable -> From e162a6ba38180563061d3cc32fa4bff7394e26c2 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Thu, 26 Feb 2026 02:16:02 -0800 Subject: [PATCH 07/61] chore: bump auto-value version to 1.11.1 PiperOrigin-RevId: 875605247 --- contrib/samples/helloworld/pom.xml | 2 +- contrib/samples/mcpfilesystem/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index cf2389fd0..cd8ca880d 100644 --- a/contrib/samples/helloworld/pom.xml +++ b/contrib/samples/helloworld/pom.xml @@ -36,7 +36,7 @@ UTF-8 17 - 1.11.0 + 1.11.1 com.example.helloworld.HelloWorldRun ${project.version} diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index e46ce994f..3ed9dd8e2 100644 --- a/contrib/samples/mcpfilesystem/pom.xml +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -36,7 +36,7 @@ UTF-8 17 - 1.11.0 + 1.11.1 com.example.mcpfilesystem.McpFilesystemRun ${project.parent.version} diff --git a/pom.xml b/pom.xml index 6aeafc63a..c770637bc 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ ${java.version} UTF-8 - 1.11.0 + 1.11.1 3.4.1 1.49.0 0.14.0 From 70041fe2cc5449e445d08faa545d45b5a205b46e Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Thu, 26 Feb 2026 05:33:36 -0800 Subject: [PATCH 08/61] refactor: Adding a new BaseSessionService.createSession() that takes in map PiperOrigin-RevId: 875673726 --- .../adk/sessions/FirestoreSessionService.java | 16 ++++++- .../adk/sessions/BaseSessionService.java | 45 ++++++++++++++++++- .../adk/sessions/InMemorySessionService.java | 11 ++++- .../google/adk/sessions/VertexAiClient.java | 8 ++-- .../adk/sessions/VertexAiSessionService.java | 9 ++++ .../sessions/InMemorySessionServiceTest.java | 15 +++---- .../sessions/VertexAiSessionServiceTest.java | 13 +++--- 7 files changed, 90 insertions(+), 27 deletions(-) diff --git a/contrib/firestore-session-service/src/main/java/com/google/adk/sessions/FirestoreSessionService.java b/contrib/firestore-session-service/src/main/java/com/google/adk/sessions/FirestoreSessionService.java index d3295a6ef..db236fc88 100644 --- a/contrib/firestore-session-service/src/main/java/com/google/adk/sessions/FirestoreSessionService.java +++ b/contrib/firestore-session-service/src/main/java/com/google/adk/sessions/FirestoreSessionService.java @@ -50,6 +50,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,7 +89,20 @@ private CollectionReference getSessionsCollection(String userId) { /** Creates a new session in Firestore. */ @Override public Single createSession( - String appName, String userId, ConcurrentMap state, String sessionId) { + String appName, + String userId, + @Nullable ConcurrentMap state, + @Nullable String sessionId) { + return createSession(appName, userId, (Map) state, sessionId); + } + + /** Creates a new session in Firestore. */ + @Override + public Single createSession( + String appName, + String userId, + @Nullable Map state, + @Nullable String sessionId) { return Single.fromCallable( () -> { Objects.requireNonNull(appName, "appName cannot be null"); diff --git a/core/src/main/java/com/google/adk/sessions/BaseSessionService.java b/core/src/main/java/com/google/adk/sessions/BaseSessionService.java index 540153460..94e8cd7ba 100644 --- a/core/src/main/java/com/google/adk/sessions/BaseSessionService.java +++ b/core/src/main/java/com/google/adk/sessions/BaseSessionService.java @@ -23,8 +23,10 @@ import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.annotation.Nullable; @@ -47,13 +49,35 @@ public interface BaseSessionService { * service should generate a unique ID. * @return The newly created {@link Session} instance. * @throws SessionException if creation fails. + * @deprecated Use {@link #createSession(String, String, Map, String)} instead. */ + @Deprecated Single createSession( String appName, String userId, @Nullable ConcurrentMap state, @Nullable String sessionId); + /** + * Creates a new session with the specified parameters. + * + * @param appName The name of the application associated with the session. + * @param userId The identifier for the user associated with the session. + * @param state An optional map representing the initial state of the session. Can be null or + * empty. + * @param sessionId An optional client-provided identifier for the session. If empty or null, the + * service should generate a unique ID. + * @return The newly created {@link Session} instance. + * @throws SessionException if creation fails. + */ + default Single createSession( + String appName, + String userId, + @Nullable Map state, + @Nullable String sessionId) { + return createSession(appName, userId, ensureConcurrentMap(state), sessionId); + } + /** * Creates a new session with the specified application name and user ID, using a default state * (null) and allowing the service to generate a unique session ID. @@ -165,9 +189,9 @@ default Single appendEvent(Session session, Event event) { EventActions actions = event.actions(); if (actions != null) { - ConcurrentMap stateDelta = actions.stateDelta(); + Map stateDelta = actions.stateDelta(); if (stateDelta != null && !stateDelta.isEmpty()) { - ConcurrentMap sessionState = session.state(); + Map sessionState = session.state(); if (sessionState != null) { stateDelta.forEach( (key, value) -> { @@ -190,4 +214,21 @@ default Single appendEvent(Session session, Event event) { return Single.just(event); } + + /** + * Ensures the given {@link Map} is a {@link ConcurrentMap}. If the input is null, returns null. + * If the input is already a {@link ConcurrentMap}, it is cast and returned. Otherwise, a new + * {@link ConcurrentHashMap} is created from the input map. + */ + @Nullable + private static ConcurrentMap ensureConcurrentMap( + @Nullable Map state) { + if (state == null) { + return null; + } + if (state instanceof ConcurrentMap concurrentMap) { + return concurrentMap; + } + return new ConcurrentHashMap<>(state); + } } diff --git a/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java b/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java index 060fcaf60..b2a584b11 100644 --- a/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java +++ b/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java @@ -71,6 +71,15 @@ public Single createSession( String userId, @Nullable ConcurrentMap state, @Nullable String sessionId) { + return createSession(appName, userId, (Map) state, sessionId); + } + + @Override + public Single createSession( + String appName, + String userId, + @Nullable Map state, + @Nullable String sessionId) { Objects.requireNonNull(appName, "appName cannot be null"); Objects.requireNonNull(userId, "userId cannot be null"); @@ -83,7 +92,6 @@ public Single createSession( // Ensure state map and events list are mutable for the new session ConcurrentMap initialState = (state == null) ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(state); - List initialEvents = new ArrayList<>(); // Assuming Session constructor or setters allow setting these mutable collections Session newSession = @@ -91,7 +99,6 @@ public Single createSession( .appName(appName) .userId(userId) .state(initialState) - .events(initialEvents) .lastUpdateTime(Instant.now()) .build(); diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java index d35bbccae..718738b92 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java @@ -14,10 +14,10 @@ import io.reactivex.rxjava3.core.Single; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; import okhttp3.ResponseBody; @@ -51,8 +51,8 @@ final class VertexAiClient { } Maybe createSession( - String reasoningEngineId, String userId, ConcurrentMap state) { - ConcurrentHashMap sessionJsonMap = new ConcurrentHashMap<>(); + String reasoningEngineId, String userId, Map state) { + Map sessionJsonMap = new HashMap<>(); sessionJsonMap.put("userId", userId); if (state != null) { sessionJsonMap.put("sessionState", state); diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java index 7878daf22..2fff7a752 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java @@ -76,6 +76,15 @@ public Single createSession( String userId, @Nullable ConcurrentMap state, @Nullable String sessionId) { + return createSession(appName, userId, (Map) state, sessionId); + } + + @Override + public Single createSession( + String appName, + String userId, + @Nullable Map state, + @Nullable String sessionId) { String reasoningEngineId = parseReasoningEngineId(appName); return client diff --git a/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java b/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java index 6223dd2f0..41e156ffd 100644 --- a/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java +++ b/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java @@ -20,6 +20,7 @@ import com.google.adk.events.Event; import com.google.adk.events.EventActions; import io.reactivex.rxjava3.core.Single; +import java.util.HashMap; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -84,7 +85,7 @@ public void lifecycle_listSessions() { Session session = sessionService - .createSession("app-name", "user-id", new ConcurrentHashMap<>(), "session-1") + .createSession("app-name", "user-id", new HashMap<>(), "session-1") .blockingGet(); ConcurrentMap stateDelta = new ConcurrentHashMap<>(); @@ -130,9 +131,7 @@ public void lifecycle_deleteSession() { public void appendEvent_updatesSessionState() { InMemorySessionService sessionService = new InMemorySessionService(); Session session = - sessionService - .createSession("app", "user", new ConcurrentHashMap<>(), "session1") - .blockingGet(); + sessionService.createSession("app", "user", new HashMap<>(), "session1").blockingGet(); ConcurrentMap stateDelta = new ConcurrentHashMap<>(); stateDelta.put("sessionKey", "sessionValue"); @@ -167,9 +166,7 @@ public void appendEvent_updatesSessionState() { public void appendEvent_removesState() { InMemorySessionService sessionService = new InMemorySessionService(); Session session = - sessionService - .createSession("app", "user", new ConcurrentHashMap<>(), "session1") - .blockingGet(); + sessionService.createSession("app", "user", new HashMap<>(), "session1").blockingGet(); ConcurrentMap stateDeltaAdd = new ConcurrentHashMap<>(); stateDeltaAdd.put("sessionKey", "sessionValue"); @@ -221,9 +218,7 @@ public void appendEvent_removesState() { public void sequentialAgents_shareTempState() { InMemorySessionService sessionService = new InMemorySessionService(); Session session = - sessionService - .createSession("app", "user", new ConcurrentHashMap<>(), "session1") - .blockingGet(); + sessionService.createSession("app", "user", new HashMap<>(), "session1").blockingGet(); // Agent 1 writes to temp state ConcurrentMap stateDelta1 = new ConcurrentHashMap<>(); diff --git a/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java b/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java index 36eab1d16..def4faf4c 100644 --- a/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java +++ b/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java @@ -167,8 +167,7 @@ public void setUp() throws Exception { @Test public void createSession_success() throws Exception { - ConcurrentMap sessionStateMap = - new ConcurrentHashMap<>(ImmutableMap.of("new_key", "new_value")); + Map sessionStateMap = new HashMap<>(ImmutableMap.of("new_key", "new_value")); Single sessionSingle = vertexAiSessionService.createSession("123", "test_user", sessionStateMap, null); Session createdSession = sessionSingle.blockingGet(); @@ -190,8 +189,7 @@ public void createSession_success() throws Exception { @Test public void createSession_getSession_success() throws Exception { - ConcurrentMap sessionStateMap = - new ConcurrentHashMap<>(ImmutableMap.of("new_key", "new_value")); + Map sessionStateMap = new HashMap<>(ImmutableMap.of("new_key", "new_value")); Single sessionSingle = vertexAiSessionService.createSession("789", "test_user", sessionStateMap, null); Session createdSession = sessionSingle.blockingGet(); @@ -252,8 +250,7 @@ public void getAndDeleteSession_success() throws Exception { @Test public void createSessionAndGetSession_success() throws Exception { - ConcurrentMap sessionStateMap = - new ConcurrentHashMap<>(ImmutableMap.of("key", "value")); + Map sessionStateMap = new HashMap<>(ImmutableMap.of("key", "value")); Single sessionSingle = vertexAiSessionService.createSession("123", "user", sessionStateMap, null); Session createdSession = sessionSingle.blockingGet(); @@ -341,8 +338,8 @@ public void listEmptySession_success() { @Test public void appendEvent_withStateRemoved_updatesSessionState() { String userId = "userB"; - ConcurrentMap initialState = - new ConcurrentHashMap<>(ImmutableMap.of("key1", "value1", "key2", "value2")); + Map initialState = + new HashMap<>(ImmutableMap.of("key1", "value1", "key2", "value2")); Session session = vertexAiSessionService.createSession("987", userId, initialState, null).blockingGet(); From 63fbc056aa3d6a6b7388082b8de2954be6981ecd Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Thu, 26 Feb 2026 05:38:54 -0800 Subject: [PATCH 09/61] chore: bump protobuf version to 4.33.5 PiperOrigin-RevId: 875675369 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c770637bc..dfc96c034 100644 --- a/pom.xml +++ b/pom.xml @@ -48,7 +48,7 @@ 0.14.0 2.47.0 1.32.0 - 4.32.0 + 4.33.5 5.11.4 5.20.0 1.6.0 From a738bb4f020479e0191bcb057aee8acf79e31a1c Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Thu, 26 Feb 2026 05:58:16 -0800 Subject: [PATCH 10/61] chore: bump truth version to 1.4.5 PiperOrigin-RevId: 875681229 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dfc96c034..ef5389d86 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ 3.9.0 1.8.0 2.0.17 - 1.4.4 + 1.4.5 1.0.0 3.1.5 3.7.0 From 25fde824505a3f0b76ca724bf33a63fb8cd0068e Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Thu, 26 Feb 2026 06:30:13 -0800 Subject: [PATCH 11/61] chore: bump com.google.cloud:libraries-pom version to 26.76.0 PiperOrigin-RevId: 875693121 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ef5389d86..a6b47d776 100644 --- a/pom.xml +++ b/pom.xml @@ -77,7 +77,7 @@ com.google.cloud libraries-bom - 26.53.0 + 26.76.0 pom import From b6ebfc537e06e4a0c9e7b59279f35ad81e5e7255 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Thu, 26 Feb 2026 14:40:56 +0000 Subject: [PATCH 12/61] chore: Add a suitable devcontainer.json Primarily so that "it just works" (which it does not, without this, because of a Java version mismatch). --- .devcontainer/devcontainer.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..e1ff3efcc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "Java 17", + "image": "mcr.microsoft.com/devcontainers/java:1-17-bullseye", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/git-lfs:1": {} + }, + "customizations": { + "vscode": { + "extensions": [ + "extension-pack-for-java", + "redhat.vscode-xml", + "rangav.vscode-thunder-client" + ], + "settings": { + "java.jdt.download.server": "latest", + "java.help.firstView": "none", + "java.showBuildStatusOnStart": "notification", + "java.configuration.updateBuildConfiguration": "interactive", + "java.autobuild.enabled": true, + "terminal.integrated.focusOnOutput": false + } + } + }, + "remoteUser": "vscode", + "forwardPorts": [8000, 8080, 8081, 8082], + "postCreateCommand": "git config --global credential.helper '!gh auth git-credential' && git config --global lfs.locksverify false" +} From 411b6d9eaec5c09e553addd5d9d1d69b05cd22b7 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Tue, 19 Aug 2025 02:03:08 +0200 Subject: [PATCH 13/61] fix: Add missing logging for Async MCP Servers --- .../google/adk/tools/mcp/McpServerLogConsumer.java | 5 +++-- .../com/google/adk/tools/mcp/McpSessionManager.java | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpServerLogConsumer.java b/core/src/main/java/com/google/adk/tools/mcp/McpServerLogConsumer.java index 6fe16b923..921197061 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/McpServerLogConsumer.java +++ b/core/src/main/java/com/google/adk/tools/mcp/McpServerLogConsumer.java @@ -24,10 +24,11 @@ class McpServerLogConsumer implements Consumer { + private static final Logger LOG = LoggerFactory.getLogger(McpServerLogConsumer.class); + @Override public void accept(LoggingMessageNotification notif) { - Logger log = LoggerFactory.getLogger(notif.logger()); - log.atLevel(convert(notif.level())).log("{}", notif.data()); + LOG.atLevel(convert(notif.level())).log("{}", notif.data()); } private Level convert(McpSchema.LoggingLevel level) { diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpSessionManager.java b/core/src/main/java/com/google/adk/tools/mcp/McpSessionManager.java index eabed9c6a..65eb59fe9 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/McpSessionManager.java +++ b/core/src/main/java/com/google/adk/tools/mcp/McpSessionManager.java @@ -20,12 +20,15 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.spec.McpClientTransport; +import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.InitializeResult; import java.time.Duration; import java.util.Optional; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; /** * Manages MCP client sessions. @@ -113,6 +116,16 @@ public static McpAsyncClient initializeAsyncSession( initializationTimeout == null ? Duration.ofMinutes(5) : initializationTimeout) .requestTimeout(requestTimeout == null ? Duration.ofMinutes(5) : requestTimeout) .capabilities(ClientCapabilities.builder().build()) + .loggingConsumer(asyncMcpServerLogConsumer()) .build(); } + + private static Function> + asyncMcpServerLogConsumer() { + var syncConsumer = new McpServerLogConsumer(); + return message -> { + syncConsumer.accept(message); + return Mono.empty(); + }; + } } From ed0916f667f1657c730e1e4e4d2160ea2b1c119e Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Thu, 26 Feb 2026 11:37:31 -0800 Subject: [PATCH 14/61] chore: bump langchain4j version to 1.11.0 PiperOrigin-RevId: 875816627 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a6b47d776..301427e7a 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,7 @@ 0.18.1 3.41.0 3.9.0 - 1.8.0 + 1.11.0 2.0.17 1.4.5 1.0.0 From 313ce8590982346bb8ac631b4bf88da76fb849a4 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Thu, 26 Feb 2026 14:48:18 -0800 Subject: [PATCH 15/61] fix: LlmAgent model name resolution and improve Gemini-3 model detection logic PiperOrigin-RevId: 875900582 --- .../src/main/java/com/google/adk/agents/LlmAgent.java | 5 ++++- .../java/com/google/adk/flows/llmflows/Contents.java | 2 +- .../test/java/com/google/adk/agents/LlmAgentTest.java | 11 +++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/adk/agents/LlmAgent.java b/core/src/main/java/com/google/adk/agents/LlmAgent.java index ee24cae4d..ee4e6ab4c 100644 --- a/core/src/main/java/com/google/adk/agents/LlmAgent.java +++ b/core/src/main/java/com/google/adk/agents/LlmAgent.java @@ -970,7 +970,10 @@ private Model resolveModelInternal() { Model currentModel = this.model.get(); if (currentModel.model().isPresent()) { - return currentModel; + String modelName = currentModel.model().get().model(); + BaseLlm resolvedLlm = currentModel.model().get(); + + return Model.builder().modelName(modelName).model(resolvedLlm).build(); } if (currentModel.modelName().isPresent()) { diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java index f45461626..0f2e2d166 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java @@ -559,7 +559,7 @@ private static List rearrangeEventsForAsyncFunctionResponsesInHistory( // Gemini 3 requires function calls to be grouped first and only then function responses: // FC1 FC2 FR1 FR2 - boolean shouldBufferResponseEvents = modelName.startsWith("gemini-3-"); + boolean shouldBufferResponseEvents = modelName.contains("gemini-3-"); for (int i = 0; i < events.size(); i++) { Event event = events.get(i); diff --git a/core/src/test/java/com/google/adk/agents/LlmAgentTest.java b/core/src/test/java/com/google/adk/agents/LlmAgentTest.java index ce8be8dfb..594e47fd8 100644 --- a/core/src/test/java/com/google/adk/agents/LlmAgentTest.java +++ b/core/src/test/java/com/google/adk/agents/LlmAgentTest.java @@ -376,6 +376,17 @@ public void resolveModel_withModelName_resolvesFromRegistry() { assertThat(resolvedModel.model()).hasValue(testLlm); } + @Test + public void resolveModel_withModel_usesProvidedModel() { + TestLlm testLlm = createTestLlm(LlmResponse.builder().build()); + LlmAgent testAgent = createTestAgent(testLlm); + + Model resolvedModel = testAgent.resolvedModel(); + + assertThat(resolvedModel.model()).hasValue(testLlm); + assertThat(resolvedModel.modelName()).hasValue(testLlm.model()); + } + @Test public void canonicalCallbacks_returnsEmptyListWhenNull() { TestLlm testLlm = createTestLlm(LlmResponse.builder().build()); From 0b5ac9214926200c3d65d64d8c10489847c29291 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Thu, 26 Feb 2026 15:20:55 -0800 Subject: [PATCH 16/61] fix: change Session events list to a threadsafe implementation by default Session appends to event, so any readers of events will get a ConcurrentModificationException. PiperOrigin-RevId: 875914576 --- core/src/main/java/com/google/adk/sessions/Session.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/google/adk/sessions/Session.java b/core/src/main/java/com/google/adk/sessions/Session.java index 3bf27b55e..877a95220 100644 --- a/core/src/main/java/com/google/adk/sessions/Session.java +++ b/core/src/main/java/com/google/adk/sessions/Session.java @@ -25,6 +25,7 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -54,7 +55,7 @@ public static final class Builder { private String appName; private String userId; private State state = new State(new ConcurrentHashMap<>()); - private List events = new ArrayList<>(); + private List events = Collections.synchronizedList(new ArrayList<>()); private Instant lastUpdateTime = Instant.EPOCH; public Builder(String id) { @@ -101,7 +102,7 @@ public Builder userId(String userId) { @CanIgnoreReturnValue @JsonProperty("events") public Builder events(List events) { - this.events = events; + this.events = Collections.synchronizedList(events); return this; } From 0f9600cba9fc3687c04a19ec2a177523c8fe76ba Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 27 Feb 2026 00:30:59 -0800 Subject: [PATCH 17/61] chore: bump otel version to 1.59.0 PiperOrigin-RevId: 876103916 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 301427e7a..1eba36133 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 1.11.1 3.4.1 - 1.49.0 + 1.59.0 0.14.0 2.47.0 1.32.0 From f422b4764d7b82e610b3edc99af6a52e062789b0 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 27 Feb 2026 02:52:22 -0800 Subject: [PATCH 18/61] chore: bump httpclient5 version to 5.6 PiperOrigin-RevId: 876152853 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1eba36133..b32b49e2e 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ 3.27.7 1.4.0 3.9.0 - 5.4.3 + 5.6 From e0f7137253c9bd929fe3ea899e32f4b61f994986 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 27 Feb 2026 03:02:45 -0800 Subject: [PATCH 19/61] feat: add the AgentExecutor config PiperOrigin-RevId: 876156542 --- .../adk/a2a/{ => executor}/AgentExecutor.java | 41 ++++++----- .../adk/a2a/executor/AgentExecutorConfig.java | 53 +++++++++++++++ .../google/adk/a2a/executor/Callbacks.java | 68 +++++++++++++++++++ .../a2a/{ => executor}/AgentExecutorTest.java | 18 ++++- 4 files changed, 163 insertions(+), 17 deletions(-) rename a2a/src/main/java/com/google/adk/a2a/{ => executor}/AgentExecutor.java (91%) create mode 100644 a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java create mode 100644 a2a/src/main/java/com/google/adk/a2a/executor/Callbacks.java rename a2a/src/test/java/com/google/adk/a2a/{ => executor}/AgentExecutorTest.java (77%) diff --git a/a2a/src/main/java/com/google/adk/a2a/AgentExecutor.java b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java similarity index 91% rename from a2a/src/main/java/com/google/adk/a2a/AgentExecutor.java rename to a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java index 0fbeb0a72..0c12727aa 100644 --- a/a2a/src/main/java/com/google/adk/a2a/AgentExecutor.java +++ b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java @@ -1,9 +1,10 @@ -package com.google.adk.a2a; +package com.google.adk.a2a.executor; + +import static java.util.Objects.requireNonNull; import com.google.adk.a2a.converters.EventConverter; import com.google.adk.a2a.converters.PartConverter; import com.google.adk.agents.BaseAgent; -import com.google.adk.agents.RunConfig; import com.google.adk.apps.App; import com.google.adk.artifacts.BaseArtifactService; import com.google.adk.events.Event; @@ -44,12 +45,10 @@ public class AgentExecutor implements io.a2a.server.agentexecution.AgentExecutor private static final Logger logger = LoggerFactory.getLogger(AgentExecutor.class); private static final String USER_ID_PREFIX = "A2A_USER_"; - private static final RunConfig DEFAULT_RUN_CONFIG = - RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.NONE).setMaxLlmCalls(20).build(); private final Map activeTasks = new ConcurrentHashMap<>(); private final Runner.Builder runnerBuilder; - private final RunConfig runConfig; + private final AgentExecutorConfig agentExecutorConfig; private AgentExecutor( App app, @@ -59,7 +58,10 @@ private AgentExecutor( BaseSessionService sessionService, BaseMemoryService memoryService, List plugins, - RunConfig runConfig) { + AgentExecutorConfig agentExecutorConfig) { + requireNonNull(agentExecutorConfig); + this.agentExecutorConfig = agentExecutorConfig; + this.runnerBuilder = Runner.builder() .agent(agent) @@ -73,7 +75,6 @@ private AgentExecutor( } // Check that the runner is configured correctly and can be built. var unused = runnerBuilder.build(); - this.runConfig = runConfig == null ? DEFAULT_RUN_CONFIG : runConfig; } /** Builder for {@link AgentExecutor}. */ @@ -85,7 +86,13 @@ public static class Builder { private BaseSessionService sessionService; private BaseMemoryService memoryService; private List plugins = ImmutableList.of(); - private RunConfig runConfig; + private AgentExecutorConfig agentExecutorConfig; + + @CanIgnoreReturnValue + public Builder agentExecutorConfig(AgentExecutorConfig agentExecutorConfig) { + this.agentExecutorConfig = agentExecutorConfig; + return this; + } @CanIgnoreReturnValue public Builder app(App app) { @@ -129,16 +136,17 @@ public Builder plugins(List plugins) { return this; } - @CanIgnoreReturnValue - public Builder runConfig(RunConfig runConfig) { - this.runConfig = runConfig; - return this; - } - @CanIgnoreReturnValue public AgentExecutor build() { return new AgentExecutor( - app, agent, appName, artifactService, sessionService, memoryService, plugins, runConfig); + app, + agent, + appName, + artifactService, + sessionService, + memoryService, + plugins, + agentExecutorConfig); } } @@ -178,7 +186,8 @@ public void execute(RequestContext ctx, EventQueue eventQueue) { .flatMapPublisher( session -> { updater.startWork(); - return runner.runAsync(getUserId(ctx), session.id(), content, runConfig); + return runner.runAsync( + getUserId(ctx), session.id(), content, agentExecutorConfig.runConfig()); }) .subscribe( event -> { diff --git a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java new file mode 100644 index 000000000..9b1ed808b --- /dev/null +++ b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java @@ -0,0 +1,53 @@ +package com.google.adk.a2a.executor; + +import com.google.adk.a2a.executor.Callbacks.AfterEventCallback; +import com.google.adk.a2a.executor.Callbacks.AfterExecuteCallback; +import com.google.adk.a2a.executor.Callbacks.BeforeExecuteCallback; +import com.google.adk.agents.RunConfig; +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import org.jspecify.annotations.Nullable; + +/** Configuration for the {@link AgentExecutor}. */ +@AutoValue +public abstract class AgentExecutorConfig { + + private static final RunConfig DEFAULT_RUN_CONFIG = + RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.NONE).setMaxLlmCalls(20).build(); + + public abstract RunConfig runConfig(); + + public abstract @Nullable BeforeExecuteCallback beforeExecuteCallback(); + + public abstract @Nullable AfterExecuteCallback afterExecuteCallback(); + + public abstract @Nullable AfterEventCallback afterEventCallback(); + + public abstract Builder toBuilder(); + + public static Builder builder() { + return new AutoValue_AgentExecutorConfig.Builder().runConfig(DEFAULT_RUN_CONFIG); + } + + /** Builder for {@link AgentExecutorConfig}. */ + @AutoValue.Builder + public abstract static class Builder { + @CanIgnoreReturnValue + public abstract Builder runConfig(RunConfig runConfig); + + @CanIgnoreReturnValue + public abstract Builder beforeExecuteCallback(BeforeExecuteCallback beforeExecuteCallback); + + @CanIgnoreReturnValue + public abstract Builder afterExecuteCallback(AfterExecuteCallback afterExecuteCallback); + + @CanIgnoreReturnValue + public abstract Builder afterEventCallback(AfterEventCallback afterEventCallback); + + abstract AgentExecutorConfig autoBuild(); + + public AgentExecutorConfig build() { + return autoBuild(); + } + } +} diff --git a/a2a/src/main/java/com/google/adk/a2a/executor/Callbacks.java b/a2a/src/main/java/com/google/adk/a2a/executor/Callbacks.java new file mode 100644 index 000000000..666f1d8a0 --- /dev/null +++ b/a2a/src/main/java/com/google/adk/a2a/executor/Callbacks.java @@ -0,0 +1,68 @@ +package com.google.adk.a2a.executor; + +import com.google.adk.events.Event; +import io.a2a.server.agentexecution.RequestContext; +import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.TaskStatusUpdateEvent; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; + +/** Functional interfaces for agent executor lifecycle callbacks. */ +public final class Callbacks { + + private Callbacks() {} + + interface BeforeExecuteCallbackBase {} + + /** Async callback interface for actions to be performed before an execution is started. */ + @FunctionalInterface + public interface BeforeExecuteCallback extends BeforeExecuteCallbackBase { + /** + * Callback which will be called before an execution is started. It can be used to instrument a + * context or prevent the execution by returning an error. + * + * @param ctx the request context + * @return a {@link Single} that completes with a boolean indicating whether the execution + * should be prevented + */ + Single call(RequestContext ctx); + } + + interface AfterExecuteCallbackBase {} + + /** + * Async callback interface for actions to be performed after an execution is completed or failed. + */ + @FunctionalInterface + public interface AfterExecuteCallback extends AfterExecuteCallbackBase { + /** + * Callback which will be called after an execution resolved into a completed or failed task. + * This gives an opportunity to enrich the event with additional metadata or log it. + * + * @param ctx the request context + * @param finalUpdateEvent the final update event + * @return a {@link Maybe} that completes when the callback is done + */ + Maybe call(RequestContext ctx, TaskStatusUpdateEvent finalUpdateEvent); + } + + interface AfterEventCallbackBase {} + + /** Async callback interface for actions to be performed after an event is processed. */ + @FunctionalInterface + public interface AfterEventCallback extends AfterEventCallbackBase { + /** + * Callback which will be called after an ADK event is successfully converted to an A2A event. + * This gives an opportunity to enrich the event with additional metadata or abort the execution + * by returning an error. The callback is not invoked for errors originating from ADK or event + * processing. + * + * @param ctx the request context + * @param processedEvent the processed task artifact update event + * @param event the ADK event + * @return a {@link Maybe} that completes when the callback is done + */ + Maybe call( + RequestContext ctx, TaskArtifactUpdateEvent processedEvent, Event event); + } +} diff --git a/a2a/src/test/java/com/google/adk/a2a/AgentExecutorTest.java b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java similarity index 77% rename from a2a/src/test/java/com/google/adk/a2a/AgentExecutorTest.java rename to a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java index 44daf13d1..350bd6f16 100644 --- a/a2a/src/test/java/com/google/adk/a2a/AgentExecutorTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java @@ -1,4 +1,4 @@ -package com.google.adk.a2a; +package com.google.adk.a2a.executor; import static org.junit.Assert.assertThrows; @@ -32,6 +32,7 @@ public void createAgentExecutor_noAgent_succeeds() { .app(App.builder().name("test_app").rootAgent(testAgent).build()) .sessionService(new InMemorySessionService()) .artifactService(new InMemoryArtifactService()) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) .build(); } @@ -44,6 +45,7 @@ public void createAgentExecutor_withAgentAndApp_throwsException() { .agent(testAgent) .app(App.builder().name("test_app").rootAgent(testAgent).build()) .sessionService(new InMemorySessionService()) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) .artifactService(new InMemoryArtifactService()) .build(); }); @@ -55,6 +57,20 @@ public void createAgentExecutor_withEmptyAgentAndApp_throwsException() { IllegalStateException.class, () -> { new AgentExecutor.Builder() + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .build(); + }); + } + + @Test + public void createAgentExecutor_noAgentExecutorConfig_throwsException() { + assertThrows( + NullPointerException.class, + () -> { + new AgentExecutor.Builder() + .app(App.builder().name("test_app").rootAgent(testAgent).build()) .sessionService(new InMemorySessionService()) .artifactService(new InMemoryArtifactService()) .build(); From 482fc76dd8b8911eae5ead5475fb36e72e7b3446 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 27 Feb 2026 04:05:15 -0800 Subject: [PATCH 20/61] chore: bump google genai version to 1.41.0 PiperOrigin-RevId: 876177029 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b32b49e2e..75a007f50 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ 1.59.0 0.14.0 2.47.0 - 1.32.0 + 1.41.0 4.33.5 5.11.4 5.20.0 From 64d3a775d68610d20c084678ffdc559cd467e627 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 27 Feb 2026 04:52:00 -0800 Subject: [PATCH 21/61] fix: make a mutable copy of function args for the beforeToolCallback invocations PiperOrigin-RevId: 876191407 --- .../google/adk/flows/llmflows/Functions.java | 12 ++---- .../com/google/adk/agents/CallbacksTest.java | 43 ++++++++++++++++++- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Functions.java b/core/src/main/java/com/google/adk/flows/llmflows/Functions.java index 82813defa..269764046 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Functions.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Functions.java @@ -253,7 +253,8 @@ private static Function> getFunctionCallMapper( functionCall.id().map(toolConfirmations::get).orElse(null)) .build(); - Map functionArgs = functionCall.args().orElse(new HashMap<>()); + Map functionArgs = + functionCall.args().map(HashMap::new).orElse(new HashMap<>()); Maybe> maybeFunctionResult = maybeInvokeBeforeToolCall(invocationContext, tool, functionArgs, toolContext) @@ -482,12 +483,8 @@ private static Maybe> maybeInvokeBeforeToolCall( if (invocationContext.agent() instanceof LlmAgent) { LlmAgent agent = (LlmAgent) invocationContext.agent(); - HashMap mutableFunctionArgs = new HashMap<>(functionArgs); - Maybe> pluginResult = - invocationContext - .pluginManager() - .beforeToolCallback(tool, mutableFunctionArgs, toolContext); + invocationContext.pluginManager().beforeToolCallback(tool, functionArgs, toolContext); List callbacks = agent.canonicalBeforeToolCallbacks(); if (callbacks.isEmpty()) { @@ -500,8 +497,7 @@ private static Maybe> maybeInvokeBeforeToolCall( Flowable.fromIterable(callbacks) .concatMapMaybe( callback -> - callback.call( - invocationContext, tool, mutableFunctionArgs, toolContext)) + callback.call(invocationContext, tool, functionArgs, toolContext)) .firstElement()); return pluginResult.switchIfEmpty(callbackResult); diff --git a/core/src/test/java/com/google/adk/agents/CallbacksTest.java b/core/src/test/java/com/google/adk/agents/CallbacksTest.java index 11087e6d6..8325d346e 100644 --- a/core/src/test/java/com/google/adk/agents/CallbacksTest.java +++ b/core/src/test/java/com/google/adk/agents/CallbacksTest.java @@ -1172,10 +1172,51 @@ public Maybe> beforeToolCallback( event, ImmutableMap.of("echo_tool", new TestUtils.FailingEchoTool())) .blockingGet(); - assertThat(getFunctionResponse(functionResponseEvent)).isEqualTo(responseFromAgentCb); } + @Test + public void handleFunctionCalls_withBeforeToolCallback_modifiesArgs() { + ImmutableMap originalArgs = ImmutableMap.of("arg1", "val1"); + ImmutableMap modifiedArgs = ImmutableMap.of("arg1", "val1", "arg2", "val2"); + + Callbacks.BeforeToolCallbackSync cb1 = + (invocationContext, tool, input, toolContext) -> { + input.put("arg2", "val2"); + return Optional.empty(); + }; + + TestUtils.EchoTool echoTool = new TestUtils.EchoTool(); + + InvocationContext invocationContext = + createInvocationContext( + createTestAgentBuilder(createTestLlm(LlmResponse.builder().build())) + .beforeToolCallbackSync(cb1) + .build()); + + Event event = + createEvent("event").toBuilder() + .content( + Content.fromParts( + Part.fromText("..."), + Part.builder() + .functionCall( + FunctionCall.builder() + .id("fc_id") + .name("echo_tool") + .args(originalArgs) + .build()) + .build())) + .build(); + + Event functionResponseEvent = + Functions.handleFunctionCalls( + invocationContext, event, ImmutableMap.of("echo_tool", echoTool)) + .blockingGet(); + + assertThat(getFunctionResponse(functionResponseEvent)).containsExactly("result", modifiedArgs); + } + @Test public void agentRunAsync_withToolCallbacks_inspectsArgsAndReturnsResponse() { TestUtils.EchoTool echoTool = new TestUtils.EchoTool(); From 571d162de716406629cc877959dbd1ae355dc63c Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 27 Feb 2026 12:53:13 +0000 Subject: [PATCH 22/61] chore(main): release 0.7.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 28 +++++++++++++++++++ README.md | 4 +-- a2a/pom.xml | 2 +- contrib/firestore-session-service/pom.xml | 2 +- contrib/langchain4j/pom.xml | 2 +- contrib/samples/a2a_basic/pom.xml | 2 +- contrib/samples/configagent/pom.xml | 2 +- contrib/samples/helloworld/pom.xml | 2 +- contrib/samples/mcpfilesystem/pom.xml | 2 +- contrib/samples/pom.xml | 2 +- contrib/spring-ai/pom.xml | 2 +- core/pom.xml | 2 +- .../src/main/java/com/google/adk/Version.java | 2 +- dev/pom.xml | 2 +- maven_plugin/examples/custom_tools/pom.xml | 2 +- maven_plugin/examples/simple-agent/pom.xml | 2 +- maven_plugin/pom.xml | 2 +- pom.xml | 2 +- tutorials/city-time-weather/pom.xml | 2 +- tutorials/live-audio-single-agent/pom.xml | 2 +- 21 files changed, 49 insertions(+), 21 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b5b966fb1..77c0d292c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.0" + ".": "0.7.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index f360e9399..b10bad3ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## [0.7.0](https://github.com/google/adk-java/compare/v0.6.0...v0.7.0) (2026-02-27) + + +### Features + +* Add ComputerUse tool ([d733a48](https://github.com/google/adk-java/commit/d733a480a7a787cb7c32fd3470ab978ca3eb574c)) +* add the AgentExecutor config ([e0f7137](https://github.com/google/adk-java/commit/e0f7137253c9bd929fe3ea899e32f4b61f994986)) +* drop gemini-1 support in GoogleSearchTool ([15255b4](https://github.com/google/adk-java/commit/15255b48285819c7d3aedb4470e91f37d1bcfaf4)) +* Extend url_context support to Gemini 3 in Java ADK ([2c9d4dd](https://github.com/google/adk-java/commit/2c9d4dd5eafe8efe3a2fb099b58e2d0f1d9cad98)) +* Extend url_context support to Gemini 3 in Java ADK ([5f5869f](https://github.com/google/adk-java/commit/5f5869f67200831dcbb7ac10ad0d7f44410bc096)) +* Handle final and error TaskStatusUpdateEvents ([746e857](https://github.com/google/adk-java/commit/746e857d97c6f356ffe5c20be0ccae85d5a8f989)) +* remove model restrictions in BuiltInCodeExecutionTool ([1a593a9](https://github.com/google/adk-java/commit/1a593a996607904eed24b64bc63eecd7708710af)) +* Update AgentExecutor so it builds new runner on execute and there is no need to pass the runner instance ([7218295](https://github.com/google/adk-java/commit/72182958586e59ccb3d7490cd207ec2837c5b577)) + + +### Bug Fixes + +* change Session events list to a threadsafe implementation by default ([0b5ac92](https://github.com/google/adk-java/commit/0b5ac9214926200c3d65d64d8c10489847c29291)) +* deep-merge stateDelta maps when merging EventActions ([ff07474](https://github.com/google/adk-java/commit/ff07474035baec910f0c3fa83b7b1646d8409ffd)) +* drop explicit gemini-1 model version check in GoogleMapsTool ([7953503](https://github.com/google/adk-java/commit/7953503e61c547e40a1e1abbece73a99910766c1)) +* LlmAgent model name resolution and improve Gemini-3 model detection logic ([313ce85](https://github.com/google/adk-java/commit/313ce8590982346bb8ac631b4bf88da76fb849a4)) +* make a mutable copy of function args for the beforeToolCallback invocations ([64d3a77](https://github.com/google/adk-java/commit/64d3a775d68610d20c084678ffdc559cd467e627)) + + +### Documentation + +* Update a parameter name in a comment ([5262d4a](https://github.com/google/adk-java/commit/5262d4ae3eca533e1a695e6e2e71c5845055ed5d)) + ## [0.6.0](https://github.com/google/adk-java/compare/v0.5.0...v0.6.0) (2026-02-19) diff --git a/README.md b/README.md index 691b62f5f..d0471c1bf 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,13 @@ If you're using Maven, add the following to your dependencies: com.google.adk google-adk - 0.6.0 + 0.7.0 com.google.adk google-adk-dev - 0.6.0 + 0.7.0 ``` diff --git a/a2a/pom.xml b/a2a/pom.xml index 08b2cf228..dc6d9b8f7 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 google-adk-a2a diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml index 5f9906ab7..461c15db9 100644 --- a/contrib/firestore-session-service/pom.xml +++ b/contrib/firestore-session-service/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 ../../pom.xml diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml index 4713c0999..fd029e19d 100644 --- a/contrib/langchain4j/pom.xml +++ b/contrib/langchain4j/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 ../../pom.xml diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml index d6a763161..698b56372 100644 --- a/contrib/samples/a2a_basic/pom.xml +++ b/contrib/samples/a2a_basic/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.6.1-SNAPSHOT + 0.7.0 .. diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml index 35605fe75..a267c9df1 100644 --- a/contrib/samples/configagent/pom.xml +++ b/contrib/samples/configagent/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.6.1-SNAPSHOT + 0.7.0 .. diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index cd8ca880d..a14d91fbe 100644 --- a/contrib/samples/helloworld/pom.xml +++ b/contrib/samples/helloworld/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-samples - 0.6.1-SNAPSHOT + 0.7.0 .. diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index 3ed9dd8e2..5cda3dc6f 100644 --- a/contrib/samples/mcpfilesystem/pom.xml +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 ../../.. diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index 18a234a66..0f1aed85c 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 ../.. diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index 55747138f..5481a0458 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 ../../pom.xml diff --git a/core/pom.xml b/core/pom.xml index d31a2691b..822722e82 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 google-adk diff --git a/core/src/main/java/com/google/adk/Version.java b/core/src/main/java/com/google/adk/Version.java index 26577f792..10219a31b 100644 --- a/core/src/main/java/com/google/adk/Version.java +++ b/core/src/main/java/com/google/adk/Version.java @@ -22,7 +22,7 @@ */ public final class Version { // Don't touch this, release-please should keep it up to date. - public static final String JAVA_ADK_VERSION = "0.6.0"; // x-release-please-released-version + public static final String JAVA_ADK_VERSION = "0.7.0"; // x-release-please-released-version private Version() {} } diff --git a/dev/pom.xml b/dev/pom.xml index 5b1ebf2ad..cd8d0c80a 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -18,7 +18,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 google-adk-dev diff --git a/maven_plugin/examples/custom_tools/pom.xml b/maven_plugin/examples/custom_tools/pom.xml index eb35b2b9f..22a3b353a 100644 --- a/maven_plugin/examples/custom_tools/pom.xml +++ b/maven_plugin/examples/custom_tools/pom.xml @@ -4,7 +4,7 @@ com.example custom-tools-example - 0.6.1-SNAPSHOT + 0.7.0 jar ADK Custom Tools Example diff --git a/maven_plugin/examples/simple-agent/pom.xml b/maven_plugin/examples/simple-agent/pom.xml index 253e17258..d1b62b667 100644 --- a/maven_plugin/examples/simple-agent/pom.xml +++ b/maven_plugin/examples/simple-agent/pom.xml @@ -4,7 +4,7 @@ com.example simple-adk-agent - 0.6.1-SNAPSHOT + 0.7.0 jar Simple ADK Agent Example diff --git a/maven_plugin/pom.xml b/maven_plugin/pom.xml index a513fd149..2800041e6 100644 --- a/maven_plugin/pom.xml +++ b/maven_plugin/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 ../pom.xml diff --git a/pom.xml b/pom.xml index 75a007f50..3ef6fd8f1 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 pom Google Agent Development Kit Maven Parent POM diff --git a/tutorials/city-time-weather/pom.xml b/tutorials/city-time-weather/pom.xml index d116858ca..c7edd9b37 100644 --- a/tutorials/city-time-weather/pom.xml +++ b/tutorials/city-time-weather/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 ../../pom.xml diff --git a/tutorials/live-audio-single-agent/pom.xml b/tutorials/live-audio-single-agent/pom.xml index 6127a0492..4efba4525 100644 --- a/tutorials/live-audio-single-agent/pom.xml +++ b/tutorials/live-audio-single-agent/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.6.1-SNAPSHOT + 0.7.0 ../../pom.xml From 39936b36d56685ae298dcc8c7e302832810c49f8 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 27 Feb 2026 13:32:30 +0000 Subject: [PATCH 23/61] chore(main): release 0.7.1-SNAPSHOT --- a2a/pom.xml | 2 +- contrib/firestore-session-service/pom.xml | 2 +- contrib/langchain4j/pom.xml | 2 +- contrib/samples/a2a_basic/pom.xml | 2 +- contrib/samples/configagent/pom.xml | 2 +- contrib/samples/helloworld/pom.xml | 2 +- contrib/samples/mcpfilesystem/pom.xml | 2 +- contrib/samples/pom.xml | 2 +- contrib/spring-ai/pom.xml | 2 +- core/pom.xml | 2 +- dev/pom.xml | 2 +- maven_plugin/examples/custom_tools/pom.xml | 2 +- maven_plugin/examples/simple-agent/pom.xml | 2 +- maven_plugin/pom.xml | 2 +- pom.xml | 2 +- tutorials/city-time-weather/pom.xml | 2 +- tutorials/live-audio-single-agent/pom.xml | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/a2a/pom.xml b/a2a/pom.xml index dc6d9b8f7..d98064ec8 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT google-adk-a2a diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml index 461c15db9..d3d49e824 100644 --- a/contrib/firestore-session-service/pom.xml +++ b/contrib/firestore-session-service/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT ../../pom.xml diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml index fd029e19d..330339440 100644 --- a/contrib/langchain4j/pom.xml +++ b/contrib/langchain4j/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT ../../pom.xml diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml index 698b56372..fc666cd71 100644 --- a/contrib/samples/a2a_basic/pom.xml +++ b/contrib/samples/a2a_basic/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.7.0 + 0.7.1-SNAPSHOT .. diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml index a267c9df1..552a6d826 100644 --- a/contrib/samples/configagent/pom.xml +++ b/contrib/samples/configagent/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.7.0 + 0.7.1-SNAPSHOT .. diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index a14d91fbe..bff6b86a6 100644 --- a/contrib/samples/helloworld/pom.xml +++ b/contrib/samples/helloworld/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-samples - 0.7.0 + 0.7.1-SNAPSHOT .. diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index 5cda3dc6f..3e221f5f6 100644 --- a/contrib/samples/mcpfilesystem/pom.xml +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT ../../.. diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index 0f1aed85c..84a4898d6 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT ../.. diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index 5481a0458..64346b1c3 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT ../../pom.xml diff --git a/core/pom.xml b/core/pom.xml index 822722e82..eefbcda79 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT google-adk diff --git a/dev/pom.xml b/dev/pom.xml index cd8d0c80a..f0211dc2b 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -18,7 +18,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT google-adk-dev diff --git a/maven_plugin/examples/custom_tools/pom.xml b/maven_plugin/examples/custom_tools/pom.xml index 22a3b353a..73e53e480 100644 --- a/maven_plugin/examples/custom_tools/pom.xml +++ b/maven_plugin/examples/custom_tools/pom.xml @@ -4,7 +4,7 @@ com.example custom-tools-example - 0.7.0 + 0.7.1-SNAPSHOT jar ADK Custom Tools Example diff --git a/maven_plugin/examples/simple-agent/pom.xml b/maven_plugin/examples/simple-agent/pom.xml index d1b62b667..8b6c14c26 100644 --- a/maven_plugin/examples/simple-agent/pom.xml +++ b/maven_plugin/examples/simple-agent/pom.xml @@ -4,7 +4,7 @@ com.example simple-adk-agent - 0.7.0 + 0.7.1-SNAPSHOT jar Simple ADK Agent Example diff --git a/maven_plugin/pom.xml b/maven_plugin/pom.xml index 2800041e6..373c6775d 100644 --- a/maven_plugin/pom.xml +++ b/maven_plugin/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 3ef6fd8f1..fac45cf94 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT pom Google Agent Development Kit Maven Parent POM diff --git a/tutorials/city-time-weather/pom.xml b/tutorials/city-time-weather/pom.xml index c7edd9b37..ef0c8f991 100644 --- a/tutorials/city-time-weather/pom.xml +++ b/tutorials/city-time-weather/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT ../../pom.xml diff --git a/tutorials/live-audio-single-agent/pom.xml b/tutorials/live-audio-single-agent/pom.xml index 4efba4525..77a3f3b55 100644 --- a/tutorials/live-audio-single-agent/pom.xml +++ b/tutorials/live-audio-single-agent/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.0 + 0.7.1-SNAPSHOT ../../pom.xml From e3ea378051e5c4e5e5031657467145779e42db55 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 27 Feb 2026 08:52:36 -0800 Subject: [PATCH 24/61] feat: add example on how to expose agent via A2A protocol PiperOrigin-RevId: 876276519 --- a2a/pom.xml | 5 + contrib/samples/a2a_server/README.md | 70 +++++++++ contrib/samples/a2a_server/pom.xml | 142 ++++++++++++++++++ .../samples/a2aagent/AgentCardProducer.java | 33 ++++ .../a2aagent/AgentExecutorProducer.java | 28 ++++ .../adk/samples/a2aagent/StartupConfig.java | 17 +++ .../adk/samples/a2aagent/agent/Agent.java | 107 +++++++++++++ .../src/main/resources/agent/agent.json | 18 +++ .../src/main/resources/application.properties | 10 ++ contrib/samples/pom.xml | 1 + 10 files changed, 431 insertions(+) create mode 100644 contrib/samples/a2a_server/README.md create mode 100644 contrib/samples/a2a_server/pom.xml create mode 100644 contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java create mode 100644 contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java create mode 100644 contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java create mode 100644 contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java create mode 100644 contrib/samples/a2a_server/src/main/resources/agent/agent.json create mode 100644 contrib/samples/a2a_server/src/main/resources/application.properties diff --git a/a2a/pom.xml b/a2a/pom.xml index d98064ec8..d7034f36c 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -104,6 +104,11 @@ ${truth.version} test + + org.mockito + mockito-core + test + diff --git a/contrib/samples/a2a_server/README.md b/contrib/samples/a2a_server/README.md new file mode 100644 index 000000000..5351e32c0 --- /dev/null +++ b/contrib/samples/a2a_server/README.md @@ -0,0 +1,70 @@ +# Google ADK A2A Agent Server Sample + +This sample demonstrates how to expose a Google ADK (Agent Development Kit) +agent via the A2A (Agent-to-Agent) protocol using A2A SDK and Quarkus service. + +## Overview + +The application implements a simple conversational agent that checks whether +given numbers are prime numbers. It uses the `LlmAgent` from the Google ADK and +exposes it via an A2A server. + +### Key Components + +* **`Agent.java`**: Defines the `LlmAgent` instance (`check_prime_agent`) and + the `checkPrime` tool function it uses to verify numbers. +* **`AgentCardProducer.java`**: Loads and provides the `AgentCard` metadata + (from `agent.json`) which defines the agent's identity and capabilities in + the A2A network. +* **`AgentExecutorProducer.java`**: Configures and provides the A2A + `AgentExecutor`, implemented by the ADK library to wire ADK-owned agents + automatically. +* **`StartupConfig.java`**: Contains initialization logic, such as registering + JSON modules for the Vert.x/Quarkus runtime. +* **`application.properties`**: Contains a configuration for the Quarkus + service and A2A, such as port where application will be exposed, application + name and event processing timeouts. + +## Building the Project + +You can build the project using Maven: + +```shell +mvn clean install +``` + +The Java server can be started using `mvn` as follow (don't forget to set your +GOOGLE_API_KEY before running the service): + +```bash +export GOOGLE_API_KEY= + +cd contrib/samples/a2a_server +mvn quarkus:dev +``` + +## Sample request + +```bash +curl -X POST http://localhost:9090 \ + -H 'Content-Type: application/json' \ + -d '{ + "jsonrpc": "2.0", + "id": "cli-check-2", + "method": "message/stream", + "params": { + "message": { + "kind": "message", + "contextId": "cli-demo-context", + "messageId": "cli-check-2", + "role": "user", + "parts": [ + { + "kind": "text", + "text": "Is 2 prime?" + } + ] + } + } + }' +``` diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml new file mode 100644 index 000000000..d47a0c984 --- /dev/null +++ b/contrib/samples/a2a_server/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + + com.google.adk + google-adk-samples + 0.7.1-SNAPSHOT + .. + + + google-adk-sample-a2a-agent + jar + + Google ADK - Sample - A2A Agent Server + Demonstrates exposing ADK agent via A2A. + + + UTF-8 + 17 + ${project.version} + ${project.version} + 0.3.0.Beta1 + 3.30.6 + 0.8 + + + + + + io.quarkus + quarkus-bom + ${quarkus.platform.version} + pom + import + + + + + + + io.github.a2asdk + a2a-java-sdk-reference-jsonrpc + ${a2a.sdk.version} + + + io.quarkus + quarkus-resteasy-jackson + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.google.adk + google-adk + ${google-adk.version} + + + com.google.adk + google-adk-a2a + ${google-adk-a2a.version} + + + + io.github.a2asdk + a2a-java-sdk-spec + ${a2a.sdk.version} + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-reactive-routes + + + io.quarkus + quarkus-jackson + + + + io.github.a2asdk + a2a-java-sdk-client + ${a2a.sdk.version} + + + + com.google.flogger + flogger + ${flogger.version} + + + + com.google.flogger + google-extensions + ${flogger.version} + + + + com.google.flogger + flogger-system-backend + ${flogger.version} + + + + + + + + src/main/java + + **/*.json + + + + src/main/resources + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + + diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java new file mode 100644 index 000000000..0937e2512 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java @@ -0,0 +1,33 @@ +package com.google.adk.samples.a2aagent; + +import io.a2a.server.PublicAgentCard; +import io.a2a.spec.AgentCard; +import io.a2a.util.Utils; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** Produces the {@link AgentCard} from the bundled JSON resources. */ +@ApplicationScoped +public class AgentCardProducer { + + @Produces + @PublicAgentCard + public AgentCard agentCard() { + try (InputStream is = getClass().getResourceAsStream("/agent/agent.json")) { + if (is == null) { + throw new RuntimeException("agent.json not found in resources"); + } + + // Read the JSON file content + String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); + + // Use the SDK's built-in mapper to convert JSON string to AgentCard record + return Utils.OBJECT_MAPPER.readValue(json, AgentCard.class); + + } catch (Exception e) { + throw new RuntimeException("Failed to load AgentCard from JSON", e); + } + } +} diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java new file mode 100644 index 000000000..4ecd2517d --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java @@ -0,0 +1,28 @@ +package com.google.adk.samples.a2aagent; + +import com.google.adk.a2a.executor.AgentExecutorConfig; +import com.google.adk.samples.a2aagent.agent.Agent; +import com.google.adk.sessions.InMemorySessionService; +import io.a2a.server.agentexecution.AgentExecutor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** Produces the {@link AgentExecutor} instance that handles agent interactions. */ +@ApplicationScoped +public class AgentExecutorProducer { + + @ConfigProperty(name = "my.adk.app.name", defaultValue = "default-app") + String appName; + + @Produces + public AgentExecutor agentExecutor() { + InMemorySessionService sessionService = new InMemorySessionService(); + return new com.google.adk.a2a.executor.AgentExecutor.Builder() + .agent(Agent.ROOT_AGENT) + .appName(appName) + .sessionService(sessionService) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .build(); + } +} diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java new file mode 100644 index 000000000..0da70b086 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java @@ -0,0 +1,17 @@ +package com.google.adk.samples.a2aagent; + +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.quarkus.runtime.StartupEvent; +import io.vertx.core.json.jackson.DatabindCodec; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +/** Configuration applied on startup, such as Jackson module registrations. */ +@ApplicationScoped +public class StartupConfig { + + void onStart(@Observes StartupEvent ev) { + // Register globally for Vert.x's internal JSON handling + DatabindCodec.mapper().registerModule(new JavaTimeModule()); + } +} diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java new file mode 100644 index 000000000..a415f618f --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java @@ -0,0 +1,107 @@ +package com.google.adk.samples.a2aagent.agent; + +import static java.util.stream.Collectors.joining; + +import com.google.adk.agents.LlmAgent; +import com.google.adk.tools.FunctionTool; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.flogger.GoogleLogger; +import io.reactivex.rxjava3.core.Maybe; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Agent that can check whether numbers are prime. */ +public final class Agent { + + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + + /** + * Checks if a list of numbers are prime. + * + * @param nums The list of numbers to check + * @return A map containing the result message + */ + public static ImmutableMap checkPrime(List nums) { + logger.atInfo().log("checkPrime called with nums=%s", nums); + Set primes = new HashSet<>(); + for (int num : nums) { + if (num <= 1) { + continue; + } + boolean isPrime = true; + for (int i = 2; i <= Math.sqrt(num); i++) { + if (num % i == 0) { + isPrime = false; + break; + } + } + if (isPrime) { + primes.add(num); + } + } + String result; + if (primes.isEmpty()) { + result = "No prime numbers found."; + } else if (primes.size() == 1) { + int only = primes.iterator().next(); + // Per request: singular phrasing without article + result = only + " is prime number."; + } else { + result = primes.stream().map(String::valueOf).collect(joining(", ")) + " are prime numbers."; + } + logger.atInfo().log("checkPrime result=%s", result); + return ImmutableMap.of("result", result); + } + + public static final LlmAgent ROOT_AGENT = + LlmAgent.builder() + .model("gemini-2.5-pro") + .name("check_prime_agent") + .description("check prime agent that can check whether numbers are prime.") + .instruction( + """ + You check whether numbers are prime. + + If the last user message contains numbers, call checkPrime exactly once with exactly + those integers as a list (e.g., [2]). Never add other numbers. Do not ask for + clarification. Return only the tool's result. + + Always pass a list of integers to the tool (use a single-element list for one + number). Never pass strings. + """) + // Log the exact contents passed to the LLM request for verification + .beforeModelCallback( + (callbackContext, llmRequest) -> { + try { + logger.atInfo().log( + "Invocation events (count=%d): %s", + callbackContext.events().size(), callbackContext.events()); + } catch (Throwable t) { + logger.atWarning().withCause(t).log("BeforeModel logging error"); + } + return Maybe.empty(); + }) + .afterModelCallback( + (callbackContext, llmResponse) -> { + try { + String content = + llmResponse.content().map(Object::toString).orElse(""); + logger.atInfo().log("AfterModel content=%s", content); + llmResponse + .errorMessage() + .ifPresent( + error -> + logger.atInfo().log( + "AfterModel errorMessage=%s", error.replace("\n", "\\n"))); + } catch (Throwable t) { + logger.atWarning().withCause(t).log("AfterModel logging error"); + } + return Maybe.empty(); + }) + .tools(ImmutableList.of(FunctionTool.create(Agent.class, "checkPrime"))) + .build(); + + private Agent() {} +} diff --git a/contrib/samples/a2a_server/src/main/resources/agent/agent.json b/contrib/samples/a2a_server/src/main/resources/agent/agent.json new file mode 100644 index 000000000..4a0848282 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/resources/agent/agent.json @@ -0,0 +1,18 @@ +{ + "capabilities": {"streaming": true}, + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["application/json"], + "description": "An agent specialized in checking whether numbers are prime. It can efficiently determine the primality of individual numbers or lists of numbers.", + "name": "check_prime_agent", + "skills": [ + { + "id": "prime_checking", + "name": "Prime Number Checking", + "description": "Check if numbers in a list are prime using efficient mathematical algorithms", + "tags": ["mathematical", "computation", "prime", "numbers"] + } + ], + "preferredTransport": "JSONRPC", + "url": "http://localhost:9090", + "version": "1.0.0" +} diff --git a/contrib/samples/a2a_server/src/main/resources/application.properties b/contrib/samples/a2a_server/src/main/resources/application.properties new file mode 100644 index 000000000..ba7a5f2b0 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/resources/application.properties @@ -0,0 +1,10 @@ +# Timeout for the agent to complete execution (default 30s) +a2a.blocking.agent.timeout.seconds=30 + +# Timeout for final event processing (default 5s) +a2a.blocking.consumption.timeout.seconds=5 + +# Custom application name for the ADK Runner +my.adk.app.name=My-JSONRPC-Agent + +quarkus.http.port=9090 \ No newline at end of file diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index 84a4898d6..f3c8359b8 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -17,6 +17,7 @@ a2a_basic + a2a_server configagent helloworld mcpfilesystem From 117fedf672bb67c4b078ac75ee81a7710452c5b5 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Mon, 2 Mar 2026 05:07:01 -0800 Subject: [PATCH 25/61] fix: downgrade otel.version to 1.51.0 PiperOrigin-RevId: 877344087 --- dev/pom.xml | 4 ++++ pom.xml | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/dev/pom.xml b/dev/pom.xml index f0211dc2b..8595995ed 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -104,6 +104,10 @@ io.opentelemetry opentelemetry-sdk-trace + + io.opentelemetry + opentelemetry-sdk-metrics + com.flipkart.zjsonpatch zjsonpatch diff --git a/pom.xml b/pom.xml index fac45cf94..3b9c2bb2d 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,10 @@ 1.11.1 3.4.1 - 1.59.0 + + 1.51.0 0.14.0 2.47.0 1.41.0 From 6a2669b1fba9ec441d7526da7adcef8abb3a90a2 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Mon, 2 Mar 2026 05:16:57 -0800 Subject: [PATCH 26/61] refactor: Replace Optional builder methods with @Nullable PiperOrigin-RevId: 877347079 --- .../google/adk/agents/InvocationContext.java | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/com/google/adk/agents/InvocationContext.java b/core/src/main/java/com/google/adk/agents/InvocationContext.java index 6457a8ca4..89c90b9fe 100644 --- a/core/src/main/java/com/google/adk/agents/InvocationContext.java +++ b/core/src/main/java/com/google/adk/agents/InvocationContext.java @@ -43,18 +43,18 @@ public class InvocationContext { private final BaseArtifactService artifactService; private final BaseMemoryService memoryService; private final Plugin pluginManager; - private final Optional liveRequestQueue; + @Nullable private final LiveRequestQueue liveRequestQueue; private final Map activeStreamingTools; private final String invocationId; private final Session session; - private final Optional userContent; + @Nullable private final Content userContent; private final RunConfig runConfig; @Nullable private final EventsCompactionConfig eventsCompactionConfig; @Nullable private final ContextCacheConfig contextCacheConfig; private final InvocationCostManager invocationCostManager; private final Map callbackContextData; - private Optional branch; + @Nullable private String branch; private BaseAgent agent; private boolean endInvocation; @@ -153,10 +153,10 @@ public InvocationContext( + ".invocationId(invocationId)" + ".agent(agent)" + ".session(session)" - + ".userContent(Optional.ofNullable(userContent))" + + ".userContent(userContent)" + ".runConfig(runConfig)" + ".build()", - imports = {"com.google.adk.agents.InvocationContext", "java.util.Optional"}) + imports = {"com.google.adk.agents.InvocationContext"}) @Deprecated(forRemoval = true) public static InvocationContext create( BaseSessionService sessionService, @@ -172,7 +172,7 @@ public static InvocationContext create( .invocationId(invocationId) .agent(agent) .session(session) - .userContent(Optional.ofNullable(userContent)) + .userContent(userContent) .runConfig(runConfig) .build(); } @@ -245,7 +245,7 @@ public Map activeStreamingTools() { /** Returns the queue for managing live requests, if available for this invocation. */ public Optional liveRequestQueue() { - return liveRequestQueue; + return Optional.ofNullable(liveRequestQueue); } /** Returns the unique ID for this invocation. */ @@ -258,7 +258,7 @@ public String invocationId() { * history. */ public void branch(@Nullable String branch) { - this.branch = Optional.ofNullable(branch); + this.branch = branch; } /** @@ -266,7 +266,7 @@ public void branch(@Nullable String branch) { * the conversation history. */ public Optional branch() { - return branch; + return Optional.ofNullable(branch); } /** Returns the agent being invoked. */ @@ -291,7 +291,7 @@ public Session session() { /** Returns the user content that triggered this invocation, if any. */ public Optional userContent() { - return userContent; + return Optional.ofNullable(userContent); } /** Returns the configuration for the current agent run. */ @@ -416,13 +416,13 @@ private Builder(InvocationContext context) { private BaseArtifactService artifactService; private BaseMemoryService memoryService; private Plugin pluginManager = new PluginManager(); - private Optional liveRequestQueue = Optional.empty(); + @Nullable private LiveRequestQueue liveRequestQueue = null; private Map activeStreamingTools = new ConcurrentHashMap<>(); - private Optional branch = Optional.empty(); + @Nullable private String branch = null; private String invocationId = newInvocationContextId(); private BaseAgent agent; private Session session; - private Optional userContent = Optional.empty(); + @Nullable private Content userContent = null; private RunConfig runConfig = RunConfig.builder().build(); private boolean endInvocation = false; @Nullable private EventsCompactionConfig eventsCompactionConfig; @@ -489,7 +489,7 @@ public Builder pluginManager(Plugin pluginManager) { @Deprecated(forRemoval = true) @CanIgnoreReturnValue public Builder liveRequestQueue(Optional liveRequestQueue) { - this.liveRequestQueue = liveRequestQueue; + this.liveRequestQueue = liveRequestQueue.orElse(null); return this; } @@ -501,7 +501,7 @@ public Builder liveRequestQueue(Optional liveRequestQueue) { */ @CanIgnoreReturnValue public Builder liveRequestQueue(@Nullable LiveRequestQueue liveRequestQueue) { - this.liveRequestQueue = Optional.ofNullable(liveRequestQueue); + this.liveRequestQueue = liveRequestQueue; return this; } @@ -516,7 +516,7 @@ public Builder liveRequestQueue(@Nullable LiveRequestQueue liveRequestQueue) { @Deprecated(forRemoval = true) @CanIgnoreReturnValue public Builder branch(Optional branch) { - this.branch = branch; + this.branch = branch.orElse(null); return this; } @@ -527,8 +527,8 @@ public Builder branch(Optional branch) { * @return this builder instance for chaining. */ @CanIgnoreReturnValue - public Builder branch(String branch) { - this.branch = Optional.of(branch); + public Builder branch(@Nullable String branch) { + this.branch = branch; return this; } @@ -569,14 +569,12 @@ public Builder session(Session session) { } /** - * Sets the user content that triggered this invocation. - * - * @param userContent the user content that triggered this invocation. - * @return this builder instance for chaining. + * @deprecated Use {@link #userContent(Content)} instead. */ @CanIgnoreReturnValue + @Deprecated public Builder userContent(Optional userContent) { - this.userContent = userContent; + this.userContent = userContent.orElse(null); return this; } @@ -587,8 +585,8 @@ public Builder userContent(Optional userContent) { * @return this builder instance for chaining. */ @CanIgnoreReturnValue - public Builder userContent(Content userContent) { - this.userContent = Optional.of(userContent); + public Builder userContent(@Nullable Content userContent) { + this.userContent = userContent; return this; } From d899f6f4ad52c84cb4ac8c90d0dc88c22487029c Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Mon, 2 Mar 2026 06:39:45 -0800 Subject: [PATCH 27/61] feat: Adding a SessionKey for typeSafety PiperOrigin-RevId: 877372854 --- .../adk/artifacts/BaseArtifactService.java | 36 ++++++ .../java/com/google/adk/runner/Runner.java | 31 ++++++ .../adk/sessions/BaseSessionService.java | 65 ++++++++--- .../java/com/google/adk/sessions/Session.java | 26 +++++ .../com/google/adk/sessions/SessionKey.java | 82 ++++++++++++++ .../com/google/adk/runner/RunnerTest.java | 103 ++++++++++++++++++ 6 files changed, 330 insertions(+), 13 deletions(-) create mode 100644 core/src/main/java/com/google/adk/sessions/SessionKey.java diff --git a/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java b/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java index 32ef9ff4d..a9bb6ba4d 100644 --- a/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java +++ b/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java @@ -16,6 +16,7 @@ package com.google.adk.artifacts; +import com.google.adk.sessions.SessionKey; import com.google.common.collect.ImmutableList; import com.google.genai.types.Part; import io.reactivex.rxjava3.core.Completable; @@ -39,6 +40,12 @@ public interface BaseArtifactService { Single saveArtifact( String appName, String userId, String sessionId, String filename, Part artifact); + /** Saves an artifact. */ + default Single saveArtifact(SessionKey sessionKey, String filename, Part artifact) { + return saveArtifact( + sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename, artifact); + } + /** * Saves an artifact and returns it with fileData if available. * @@ -58,18 +65,35 @@ default Single saveAndReloadArtifact( .flatMap(version -> loadArtifact(appName, userId, sessionId, filename, version).toSingle()); } + /** Saves an artifact and returns it with fileData if available. */ + default Single saveAndReloadArtifact( + SessionKey sessionKey, String filename, Part artifact) { + return saveAndReloadArtifact( + sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename, artifact); + } + /** Loads the latest version of an artifact from the service. */ default Maybe loadArtifact( String appName, String userId, String sessionId, String filename) { return loadArtifact(appName, userId, sessionId, filename, Optional.empty()); } + /** Loads the latest version of an artifact from the service. */ + default Maybe loadArtifact(SessionKey sessionKey, String filename) { + return loadArtifact(sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename); + } + /** Loads a specific version of an artifact from the service. */ default Maybe loadArtifact( String appName, String userId, String sessionId, String filename, int version) { return loadArtifact(appName, userId, sessionId, filename, Optional.of(version)); } + default Maybe loadArtifact(SessionKey sessionKey, String filename, int version) { + return loadArtifact( + sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename, version); + } + /** * @deprecated Use {@link #loadArtifact(String, String, String, String)} or {@link * #loadArtifact(String, String, String, String, int)} instead. @@ -88,6 +112,10 @@ Maybe loadArtifact( */ Single listArtifactKeys(String appName, String userId, String sessionId); + default Single listArtifactKeys(SessionKey sessionKey) { + return listArtifactKeys(sessionKey.appName(), sessionKey.userId(), sessionKey.id()); + } + /** * Deletes an artifact. * @@ -98,6 +126,10 @@ Maybe loadArtifact( */ Completable deleteArtifact(String appName, String userId, String sessionId, String filename); + default Completable deleteArtifact(SessionKey sessionKey, String filename) { + return deleteArtifact(sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename); + } + /** * Lists all the versions (as revision IDs) of an artifact. * @@ -109,4 +141,8 @@ Maybe loadArtifact( */ Single> listVersions( String appName, String userId, String sessionId, String filename); + + default Single> listVersions(SessionKey sessionKey, String filename) { + return listVersions(sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename); + } } diff --git a/core/src/main/java/com/google/adk/runner/Runner.java b/core/src/main/java/com/google/adk/runner/Runner.java index 0ddfdaea1..ea7cb80f6 100644 --- a/core/src/main/java/com/google/adk/runner/Runner.java +++ b/core/src/main/java/com/google/adk/runner/Runner.java @@ -35,6 +35,7 @@ import com.google.adk.sessions.BaseSessionService; import com.google.adk.sessions.InMemorySessionService; import com.google.adk.sessions.Session; +import com.google.adk.sessions.SessionKey; import com.google.adk.summarizer.EventsCompactionConfig; import com.google.adk.summarizer.LlmEventSummarizer; import com.google.adk.summarizer.SlidingWindowEventCompactor; @@ -383,6 +384,25 @@ public Flowable runAsync( .flatMapPublisher(session -> this.runAsyncImpl(session, newMessage, runConfig, stateDelta)); } + /** See {@link #runAsync(String, String, Content, RunConfig, Map)}. */ + public Flowable runAsync( + SessionKey sessionKey, + Content newMessage, + RunConfig runConfig, + @Nullable Map stateDelta) { + return runAsync(sessionKey.userId(), sessionKey.id(), newMessage, runConfig, stateDelta); + } + + /** See {@link #runAsync(String, String, Content, RunConfig, Map)}. */ + public Flowable runAsync(SessionKey sessionKey, Content newMessage, RunConfig runConfig) { + return runAsync(sessionKey, newMessage, runConfig, /* stateDelta= */ null); + } + + /** See {@link #runAsync(String, String, Content, RunConfig, Map)}. */ + public Flowable runAsync(SessionKey sessionKey, Content newMessage) { + return runAsync(sessionKey, newMessage, RunConfig.builder().build()); + } + /** See {@link #runAsync(String, String, Content, RunConfig, Map)}. */ public Flowable runAsync(String userId, String sessionId, Content newMessage) { return runAsync(userId, sessionId, newMessage, RunConfig.builder().build()); @@ -671,6 +691,17 @@ public Flowable runLive( .flatMapPublisher(session -> this.runLive(session, liveRequestQueue, runConfig)); } + /** + * Retrieves the session and runs the agent in live mode. + * + * @return stream of events from the agent. + * @throws IllegalArgumentException if the session is not found. + */ + public Flowable runLive( + SessionKey sessionKey, LiveRequestQueue liveRequestQueue, RunConfig runConfig) { + return runLive(sessionKey.userId(), sessionKey.id(), liveRequestQueue, runConfig); + } + /** * Runs the agent asynchronously with a default user ID. * diff --git a/core/src/main/java/com/google/adk/sessions/BaseSessionService.java b/core/src/main/java/com/google/adk/sessions/BaseSessionService.java index 94e8cd7ba..7a0885544 100644 --- a/core/src/main/java/com/google/adk/sessions/BaseSessionService.java +++ b/core/src/main/java/com/google/adk/sessions/BaseSessionService.java @@ -78,6 +78,18 @@ default Single createSession( return createSession(appName, userId, ensureConcurrentMap(state), sessionId); } + /** + * Creates a new session with the specified parameters. + * + * @param sessionKey The session key containing appName, userId and sessionId. + * @param state An optional map representing the initial state of the session. Can be null or + * empty. + */ + default Single createSession( + SessionKey sessionKey, @Nullable Map state) { + return createSession(sessionKey.appName(), sessionKey.userId(), state, sessionKey.id()); + } + /** * Creates a new session with the specified application name and user ID, using a default state * (null) and allowing the service to generate a unique session ID. @@ -94,6 +106,14 @@ default Single createSession(String appName, String userId) { return createSession(appName, userId, null, null); } + /** + * Creates a new session with the specified application name and user ID, using a default state + * (null) and allowing the service to generate a unique session ID. + */ + default Single createSession(SessionKey sessionKey) { + return createSession(sessionKey.appName(), sessionKey.userId(), null, sessionKey.id()); + } + /** * Retrieves a specific session, optionally filtering the events included. * @@ -110,6 +130,12 @@ default Single createSession(String appName, String userId) { Maybe getSession( String appName, String userId, String sessionId, Optional config); + /** Retrieves a specific session, optionally filtering the events included. */ + default Maybe getSession(SessionKey sessionKey, @Nullable GetSessionConfig config) { + return getSession( + sessionKey.appName(), sessionKey.userId(), sessionKey.id(), Optional.ofNullable(config)); + } + /** * Lists sessions associated with a specific application and user. * @@ -123,6 +149,11 @@ Maybe getSession( */ Single listSessions(String appName, String userId); + /** Lists sessions associated with a specific application and user. */ + default Single listSessions(SessionKey sessionKey) { + return listSessions(sessionKey.appName(), sessionKey.userId()); + } + /** * Deletes a specific session. * @@ -134,6 +165,11 @@ Maybe getSession( */ Completable deleteSession(String appName, String userId, String sessionId); + /** Deletes a specific session. */ + default Completable deleteSession(SessionKey sessionKey) { + return deleteSession(sessionKey.appName(), sessionKey.userId(), sessionKey.id()); + } + /** * Lists the events within a specific session. Supports pagination via the response object. * @@ -147,6 +183,11 @@ Maybe getSession( */ Single listEvents(String appName, String userId, String sessionId); + /** Lists the events within a specific session. */ + default Single listEvents(SessionKey sessionKey) { + return listEvents(sessionKey.appName(), sessionKey.userId(), sessionKey.id()); + } + /** * Closes a session. This is currently a placeholder and may involve finalizing session state or * performing cleanup actions in future implementations. The default implementation does nothing. @@ -190,20 +231,18 @@ default Single appendEvent(Session session, Event event) { EventActions actions = event.actions(); if (actions != null) { Map stateDelta = actions.stateDelta(); - if (stateDelta != null && !stateDelta.isEmpty()) { - Map sessionState = session.state(); - if (sessionState != null) { - stateDelta.forEach( - (key, value) -> { - if (!key.startsWith(State.TEMP_PREFIX)) { - if (value == State.REMOVED) { - sessionState.remove(key); - } else { - sessionState.put(key, value); - } + Map sessionState = session.state(); + if (stateDelta != null && !stateDelta.isEmpty() && sessionState != null) { + stateDelta.forEach( + (key, value) -> { + if (!key.startsWith(State.TEMP_PREFIX)) { + if (value == State.REMOVED) { + sessionState.remove(key); + } else { + sessionState.put(key, value); } - }); - } + } + }); } } diff --git a/core/src/main/java/com/google/adk/sessions/Session.java b/core/src/main/java/com/google/adk/sessions/Session.java index 877a95220..f8376589a 100644 --- a/core/src/main/java/com/google/adk/sessions/Session.java +++ b/core/src/main/java/com/google/adk/sessions/Session.java @@ -49,6 +49,11 @@ public static Builder builder(String id) { return new Builder(id); } + /** Creates a new {@link Builder} with the given session key. */ + public static Builder builder(SessionKey sessionKey) { + return new Builder(sessionKey); + } + /** Builder for {@link Session}. */ public static final class Builder { private String id; @@ -62,6 +67,13 @@ public Builder(String id) { this.id = id; } + /** Creates a new {@link Builder} with the given session key. */ + public Builder(SessionKey sessionKey) { + this.id = sessionKey.id(); + this.appName = sessionKey.appName(); + this.userId = sessionKey.userId(); + } + @JsonCreator private Builder() {} @@ -72,6 +84,15 @@ public Builder id(String id) { return this; } + /** Sets the session key. */ + @CanIgnoreReturnValue + public Builder sessionKey(SessionKey sessionKey) { + this.id = sessionKey.id(); + this.appName = sessionKey.appName(); + this.userId = sessionKey.userId(); + return this; + } + @CanIgnoreReturnValue public Builder state(State state) { this.state = state; @@ -130,6 +151,11 @@ public Session build() { } } + /** Returns the session key. */ + public SessionKey sessionKey() { + return new SessionKey(appName, userId, id); + } + @JsonProperty("id") public String id() { return id; diff --git a/core/src/main/java/com/google/adk/sessions/SessionKey.java b/core/src/main/java/com/google/adk/sessions/SessionKey.java new file mode 100644 index 000000000..db26b5a3a --- /dev/null +++ b/core/src/main/java/com/google/adk/sessions/SessionKey.java @@ -0,0 +1,82 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.sessions; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.adk.JsonBaseModel; +import java.util.Objects; + +/** Key for a session, composed of appName, userId and session id. */ +public final class SessionKey extends JsonBaseModel { + private final String appName; + private final String userId; + private final String id; + + @JsonCreator + public SessionKey( + @JsonProperty("appName") String appName, + @JsonProperty("userId") String userId, + @JsonProperty("id") String id) { + this.appName = appName; + this.userId = userId; + this.id = id; + } + + @JsonProperty("appName") + public String appName() { + return appName; + } + + @JsonProperty("userId") + public String userId() { + return userId; + } + + @JsonProperty("id") + public String id() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SessionKey that = (SessionKey) o; + return Objects.equals(appName, that.appName) + && Objects.equals(userId, that.userId) + && Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(appName, userId, id); + } + + @Override + public String toString() { + return toJson(); + } + + public static SessionKey fromJson(String json) { + return fromJsonString(json, SessionKey.class); + } +} diff --git a/core/src/test/java/com/google/adk/runner/RunnerTest.java b/core/src/test/java/com/google/adk/runner/RunnerTest.java index 421b79abb..42452c6a0 100644 --- a/core/src/test/java/com/google/adk/runner/RunnerTest.java +++ b/core/src/test/java/com/google/adk/runner/RunnerTest.java @@ -41,6 +41,7 @@ import com.google.adk.models.LlmResponse; import com.google.adk.plugins.BasePlugin; import com.google.adk.sessions.Session; +import com.google.adk.sessions.SessionKey; import com.google.adk.summarizer.EventsCompactionConfig; import com.google.adk.telemetry.Tracing; import com.google.adk.testing.TestLlm; @@ -578,6 +579,14 @@ public void onEventCallback_success() { verify(plugin).onEventCallback(any(), any()); } + @Test + public void runAsync_withSessionKey_success() { + var events = + runner.runAsync(session.sessionKey(), createContent("from user")).toList().blockingGet(); + + assertThat(simplifyEvents(events)).containsExactly("test agent: from llm"); + } + @Test public void runAsync_withStateDelta_mergesStateIntoSession() { ImmutableMap stateDelta = ImmutableMap.of("key1", "value1", "key2", 42); @@ -605,6 +614,32 @@ public void runAsync_withStateDelta_mergesStateIntoSession() { assertThat(finalSession.state()).containsAtLeastEntriesIn(stateDelta); } + @Test + public void runAsync_withSessionKeyAndStateDelta_mergesStateIntoSession() { + ImmutableMap stateDelta = ImmutableMap.of("key1", "value1", "key2", 42); + + var events = + runner + .runAsync( + session.sessionKey(), + createContent("test message"), + RunConfig.builder().build(), + stateDelta) + .toList() + .blockingGet(); + + // Verify agent runs successfully + assertThat(simplifyEvents(events)).containsExactly("test agent: from llm"); + + // Verify state was merged into session + Session finalSession = + runner + .sessionService() + .getSession("test", "user", session.id(), Optional.empty()) + .blockingGet(); + assertThat(finalSession.state()).containsAtLeastEntriesIn(stateDelta); + } + @Test public void runAsync_withEmptyStateDelta_doesNotModifySession() { ImmutableMap emptyStateDelta = ImmutableMap.of(); @@ -840,6 +875,20 @@ public void runLive_success() throws Exception { assertThat(simplifyEvents(testSubscriber.values())).containsExactly("test agent: from llm"); } + @Test + public void runLive_withSessionKey_success() throws Exception { + LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); + TestSubscriber testSubscriber = + runner.runLive(session.sessionKey(), liveRequestQueue, RunConfig.builder().build()).test(); + + liveRequestQueue.content(createContent("from user")); + liveRequestQueue.close(); + + testSubscriber.await(); + testSubscriber.assertComplete(); + assertThat(simplifyEvents(testSubscriber.values())).containsExactly("test agent: from llm"); + } + @Test public void runLive_withToolExecution() throws Exception { LlmAgent agentWithTool = @@ -948,6 +997,18 @@ public void runAsync_withoutSessionAndAutoCreateSessionTrue_createsSession() { .isNotNull(); } + @Test + public void runAsync_withoutSessionAndAutoCreateSessionTrue_withSessionKey_createsSession() { + RunConfig runConfig = RunConfig.builder().setAutoCreateSession(true).build(); + SessionKey sessionKey = new SessionKey("test", "user", UUID.randomUUID().toString()); + + var events = + runner.runAsync(sessionKey, createContent("from user"), runConfig).toList().blockingGet(); + + assertThat(simplifyEvents(events)).containsExactly("test agent: from llm"); + assertThat(runner.sessionService().getSession(sessionKey, null).blockingGet()).isNotNull(); + } + @Test public void runAsync_withoutSessionAndAutoCreateSessionFalse_throwsException() { RunConfig runConfig = RunConfig.builder().setAutoCreateSession(false).build(); @@ -959,6 +1020,17 @@ public void runAsync_withoutSessionAndAutoCreateSessionFalse_throwsException() { .assertError(IllegalArgumentException.class); } + @Test + public void runAsync_withoutSessionAndAutoCreateSessionFalse_withSessionKey_throwsException() { + RunConfig runConfig = RunConfig.builder().setAutoCreateSession(false).build(); + SessionKey sessionKey = new SessionKey("test", "user", UUID.randomUUID().toString()); + + runner + .runAsync(sessionKey, createContent("from user"), runConfig) + .test() + .assertError(IllegalArgumentException.class); + } + @Test public void runLive_withoutSessionAndAutoCreateSessionTrue_createsSession() throws Exception { RunConfig runConfig = RunConfig.builder().setAutoCreateSession(true).build(); @@ -982,6 +1054,25 @@ public void runLive_withoutSessionAndAutoCreateSessionTrue_createsSession() thro .isNotNull(); } + @Test + public void runLive_withoutSessionAndAutoCreateSessionTrue_withSessionKey_createsSession() + throws Exception { + RunConfig runConfig = RunConfig.builder().setAutoCreateSession(true).build(); + SessionKey sessionKey = new SessionKey("test", "user", UUID.randomUUID().toString()); + LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); + + TestSubscriber testSubscriber = + runner.runLive(sessionKey, liveRequestQueue, runConfig).test(); + + liveRequestQueue.content(createContent("from user")); + liveRequestQueue.close(); + + testSubscriber.await(); + testSubscriber.assertComplete(); + assertThat(simplifyEvents(testSubscriber.values())).containsExactly("test agent: from llm"); + assertThat(runner.sessionService().getSession(sessionKey, null).blockingGet()).isNotNull(); + } + @Test public void runLive_withoutSessionAndAutoCreateSessionFalse_throwsException() { RunConfig runConfig = RunConfig.builder().setAutoCreateSession(false).build(); @@ -994,6 +1085,18 @@ public void runLive_withoutSessionAndAutoCreateSessionFalse_throwsException() { .assertError(IllegalArgumentException.class); } + @Test + public void runLive_withoutSessionAndAutoCreateSessionFalse_withSessionKey_throwsException() { + RunConfig runConfig = RunConfig.builder().setAutoCreateSession(false).build(); + SessionKey sessionKey = new SessionKey("test", "user", UUID.randomUUID().toString()); + LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); + + runner + .runLive(sessionKey, liveRequestQueue, runConfig) + .test() + .assertError(IllegalArgumentException.class); + } + @Test public void runAsync_withToolConfirmation() { TestLlm testLlm = From e0640673d212b9849d312953f192f8da51fae85b Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Tue, 3 Mar 2026 01:32:46 -0800 Subject: [PATCH 28/61] feat: implement partial event aggregation in RemoteA2AAgent PiperOrigin-RevId: 877812730 --- a2a/pom.xml | 5 + .../com/google/adk/a2a/RemoteA2AAgent.java | 336 ++++++- .../google/adk/a2a/common/A2AMetadata.java | 24 + .../adk/a2a/converters/PartConverter.java | 114 ++- .../adk/a2a/converters/ResponseConverter.java | 24 +- .../adk/a2a/executor/AgentExecutor.java | 51 +- .../adk/a2a/executor/AgentExecutorConfig.java | 21 +- .../google/adk/a2a/RemoteA2AAgentTest.java | 872 ++++++++++++++++++ .../a2a/converters/ResponseConverterTest.java | 8 +- .../adk/a2a/executor/AgentExecutorTest.java | 106 ++- .../java/com/google/adk/events/Event.java | 28 + .../com/google/adk/models/LlmResponse.java | 12 + 12 files changed, 1532 insertions(+), 69 deletions(-) create mode 100644 a2a/src/main/java/com/google/adk/a2a/common/A2AMetadata.java create mode 100644 a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java diff --git a/a2a/pom.xml b/a2a/pom.xml index d7034f36c..9fa36e087 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -52,6 +52,11 @@ jackson-databind ${jackson.version} + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + com.fasterxml.jackson.module jackson-module-parameter-names diff --git a/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java b/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java index 5e6e341d7..b8ff39808 100644 --- a/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java +++ b/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java @@ -2,7 +2,11 @@ import static com.google.common.base.Strings.nullToEmpty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.adk.a2a.common.A2AClientError; +import com.google.adk.a2a.common.A2AMetadata; import com.google.adk.a2a.converters.EventConverter; import com.google.adk.a2a.converters.ResponseConverter; import com.google.adk.agents.BaseAgent; @@ -11,21 +15,31 @@ import com.google.adk.events.Event; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.genai.types.Content; +import com.google.genai.types.CustomMetadata; +import com.google.genai.types.Part; import io.a2a.client.Client; import io.a2a.client.ClientEvent; +import io.a2a.client.MessageEvent; import io.a2a.client.TaskEvent; import io.a2a.client.TaskUpdateEvent; import io.a2a.spec.A2AClientException; import io.a2a.spec.AgentCard; import io.a2a.spec.Message; +import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskState; +import io.a2a.spec.TaskStatusUpdateEvent; import io.reactivex.rxjava3.core.BackpressureStrategy; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.FlowableEmitter; +import java.time.Instant; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.UUID; import java.util.function.BiConsumer; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,6 +68,8 @@ public class RemoteA2AAgent extends BaseAgent { private static final Logger logger = LoggerFactory.getLogger(RemoteA2AAgent.class); + private static final ObjectMapper objectMapper = + new ObjectMapper().registerModule(new JavaTimeModule()); private final AgentCard agentCard; private final Client a2aClient; @@ -173,61 +189,303 @@ protected Flowable runAsyncImpl(InvocationContext invocationContext) { } Message originalMessage = a2aMessageOpt.get(); + String requestJson = serializeMessageToJson(originalMessage); return Flowable.create( emitter -> { - FlowableEmitter flowableEmitter = emitter.serialize(); - AtomicBoolean done = new AtomicBoolean(false); + StreamHandler handler = + new StreamHandler( + emitter.serialize(), invocationContext, requestJson, streaming, name()); ImmutableList> consumers = - ImmutableList.of( - (event, unused) -> - handleClientEvent(event, flowableEmitter, invocationContext, done)); - a2aClient.sendMessage( - originalMessage, consumers, e -> handleClientError(e, flowableEmitter, done), null); + ImmutableList.of(handler::handleEvent); + a2aClient.sendMessage(originalMessage, consumers, handler::handleError, null); }, BackpressureStrategy.BUFFER); } - private void handleClientError(Throwable e, FlowableEmitter emitter, AtomicBoolean done) { - // Mark the flow as done if it is already cancelled. - done.compareAndSet(false, emitter.isCancelled()); + private @Nullable String serializeMessageToJson(Message message) { + try { + return objectMapper.writeValueAsString(message); + } catch (JsonProcessingException e) { + logger.warn("Failed to serialize request", e); + return null; + } + } - // If the flow is already done, stop processing and exit the consumer. - if (done.get()) { - return; + private static class StreamHandler { + private final FlowableEmitter emitter; + private final InvocationContext invocationContext; + private final String requestJson; + private final boolean streaming; + private final String agentName; + private boolean done = false; + private final StringBuilder textBuffer = new StringBuilder(); + private final StringBuilder thoughtsBuffer = new StringBuilder(); + + StreamHandler( + FlowableEmitter emitter, + InvocationContext invocationContext, + String requestJson, + boolean streaming, + String agentName) { + this.emitter = emitter; + this.invocationContext = invocationContext; + this.requestJson = requestJson; + this.streaming = streaming; + this.agentName = agentName; } - // If the error is raised, complete the flow with an error. - if (!done.getAndSet(true)) { + + synchronized void handleError(Throwable e) { + // Mark the flow as done if it is already cancelled. + if (!done) { + done = emitter.isCancelled(); + } + + // If the flow is already done, stop processing. + if (done) { + return; + } + // If the error is raised, complete the flow with an error. + done = true; emitter.tryOnError(new A2AClientError("Failed to communicate with the remote agent", e)); } - } - private void handleClientEvent( - ClientEvent clientEvent, - FlowableEmitter emitter, - InvocationContext invocationContext, - AtomicBoolean done) { - // Mark the flow as done if it is already cancelled. - done.compareAndSet(false, emitter.isCancelled()); - - // If the flow is already done, stop processing and exit the consumer. - if (done.get()) { - return; + // TODO: b/483038527 - The synchronized block might block the thread, we should optimize for + // performance in the future. + synchronized void handleEvent(ClientEvent clientEvent, AgentCard unused) { + // Mark the flow as done if it is already cancelled. + if (!done) { + done = emitter.isCancelled(); + } + + // If the flow is already done, stop processing. + if (done) { + return; + } + + Optional eventOpt = + ResponseConverter.clientEventToEvent(clientEvent, invocationContext); + eventOpt.ifPresent( + event -> { + addMetadata(event, clientEvent); + + if (isCompleted(clientEvent)) { + // Terminal event, check if we can merge. + boolean mergeResult = mergeAggregatedContentIntoEvent(event); + if (!mergeResult) { + emitAggregatedEventAndClearBuffer(null); + } + } else { + boolean isPartial = event.partial().orElse(false); + if (isPartial) { + if (shouldResetBuffer(clientEvent)) { + clearBuffer(); + } + boolean addedToBuffer = bufferContent(event, clientEvent); + if (!addedToBuffer) { + // Partial event with no content to buffer (e.g. tool call). + // Flush buffer before emitting this event. + emitAggregatedEventAndClearBuffer(null); + } + } else { + // Intermediate non-partial. + emitAggregatedEventAndClearBuffer(null); + } + } + emitter.onNext(event); + }); + + // For non-streaming communication, complete the flow; for streaming, wait until the client + // marks the completion. + if (isCompleted(clientEvent) || !streaming) { + // Only complete the flow once. + if (!done) { + emitAggregatedEventAndClearBuffer(clientEvent); + done = true; + emitter.onComplete(); + } + } + } + + private void addMetadata(Event event, ClientEvent clientEvent) { + ImmutableList.Builder eventMetadataBuilder = ImmutableList.builder(); + event.customMetadata().ifPresent(eventMetadataBuilder::addAll); + if (requestJson != null) { + eventMetadataBuilder.add( + CustomMetadata.builder() + .key(A2AMetadata.Key.REQUEST.getValue()) + .stringValue(requestJson) + .build()); + } + try { + if (clientEvent != null) { + eventMetadataBuilder.add( + CustomMetadata.builder() + .key(A2AMetadata.Key.RESPONSE.getValue()) + .stringValue(objectMapper.writeValueAsString(clientEvent)) + .build()); + } + } catch (JsonProcessingException e) { + // metadata serialization is not critical for agent execution, so we just log and continue. + logger.warn("Failed to serialize response metadata", e); + } + event.setCustomMetadata(eventMetadataBuilder.build()); + } + + /** + * Buffers the content from the event into the text and thoughts buffers. + * + * @return true if the event has content that was added to the buffer, false otherwise. + */ + private boolean bufferContent(Event event, ClientEvent clientEvent) { + if (!shouldBuffer(clientEvent)) { + return false; + } + + boolean updated = false; + for (Part part : eventParts(event)) { + if (part.text().isPresent()) { + String t = part.text().get(); + if (part.thought().orElse(false)) { + thoughtsBuffer.append(t); + updated = true; + } else { + textBuffer.append(t); + updated = true; + } + } + } + return updated; + } + + /** + * Determines if the event should be buffered. + * + *

Buffering is used to aggregate content from partial events. We buffer events that can + * contain content which is streamed in chunks, like {@link MessageEvent} or {@link + * TaskArtifactUpdateEvent}. Events that do not contain content to be aggregated, like {@link + * TaskStatusUpdateEvent} or {@link TaskEvent} without artifacts, should not be buffered. + */ + private boolean shouldBuffer(ClientEvent event) { + if (event instanceof TaskUpdateEvent taskUpdateEvent) { + Object innerEvent = taskUpdateEvent.getUpdateEvent(); + return !(innerEvent instanceof TaskStatusUpdateEvent); + } + if (event instanceof TaskEvent taskEvent) { + return !taskEvent.getTask().getArtifacts().isEmpty(); + } + return true; + } + + /** + * Determines if text buffers should be reset before processing new content. + * + *

When receiving artifact updates via {@link TaskArtifactUpdateEvent}, if {@code append} is + * false, it indicates the new content should replace any prior chunks. If this is not the + * {@code last_chunk}, it means we are at the beginning of receiving a new set of chunks, so we + * need to reset buffers to avoid appending to stale content from a prior update. + */ + private boolean shouldResetBuffer(ClientEvent event) { + if (event instanceof TaskUpdateEvent taskUpdateEvent) { + Object innerEvent = taskUpdateEvent.getUpdateEvent(); + if (innerEvent instanceof TaskArtifactUpdateEvent artifactEvent) { + return Objects.equals(artifactEvent.isAppend(), false) + && Objects.equals(artifactEvent.isLastChunk(), false); + } + } + return false; } - Optional event = ResponseConverter.clientEventToEvent(clientEvent, invocationContext); - if (event.isPresent()) { - emitter.onNext(event.get()); + private void clearBuffer() { + thoughtsBuffer.setLength(0); + textBuffer.setLength(0); } - // For non-streaming communication, complete the flow; for streaming, wait until the client - // marks the completion. - if (isCompleted(clientEvent) || !streaming) { - // Only complete the flow once. - if (!done.getAndSet(true)) { - emitter.onComplete(); + private void emitAggregatedEventAndClearBuffer(@Nullable ClientEvent triggerEvent) { + if (thoughtsBuffer.length() > 0 || textBuffer.length() > 0) { + List parts = new ArrayList<>(); + if (thoughtsBuffer.length() > 0) { + parts.add(Part.builder().thought(true).text(thoughtsBuffer.toString()).build()); + } + if (textBuffer.length() > 0) { + parts.add(Part.builder().text(textBuffer.toString()).build()); + } + Content aggregatedContent = Content.builder().role("model").parts(parts).build(); + emitter.onNext(createAggregatedEvent(aggregatedContent, triggerEvent)); + clearBuffer(); } } + + private boolean mergeAggregatedContentIntoEvent(Event event) { + if (thoughtsBuffer.isEmpty() && textBuffer.isEmpty()) { + return false; + } + boolean hasContent = + event.content().isPresent() + && !event.content().get().parts().orElse(ImmutableList.of()).isEmpty(); + if (hasContent) { + return false; + } + + List parts = new ArrayList<>(); + if (thoughtsBuffer.length() > 0) { + parts.add(Part.builder().thought(true).text(thoughtsBuffer.toString()).build()); + } + if (textBuffer.length() > 0) { + parts.add(Part.builder().text(textBuffer.toString()).build()); + } + Content aggregatedContent = Content.builder().role("model").parts(parts).build(); + + event.setContent(Optional.of(aggregatedContent)); + + ImmutableList.Builder newMetadata = ImmutableList.builder(); + event.customMetadata().ifPresent(newMetadata::addAll); + newMetadata.add( + CustomMetadata.builder() + .key(A2AMetadata.Key.AGGREGATED.getValue()) + .stringValue("true") + .build()); + event.setCustomMetadata(newMetadata.build()); + + clearBuffer(); + return true; + } + + private Event createAggregatedEvent(Content content, @Nullable ClientEvent triggerEvent) { + ImmutableList.Builder aggMetadataBuilder = ImmutableList.builder(); + aggMetadataBuilder.add( + CustomMetadata.builder() + .key(A2AMetadata.Key.AGGREGATED.getValue()) + .stringValue("true") + .build()); + if (requestJson != null) { + aggMetadataBuilder.add( + CustomMetadata.builder() + .key(A2AMetadata.Key.REQUEST.getValue()) + .stringValue(requestJson) + .build()); + } + if (triggerEvent != null) { + try { + aggMetadataBuilder.add( + CustomMetadata.builder() + .key(A2AMetadata.Key.RESPONSE.getValue()) + .stringValue(objectMapper.writeValueAsString(triggerEvent)) + .build()); + } catch (JsonProcessingException e) { + logger.warn("Failed to serialize response metadata for aggregated event", e); + } + } + + return Event.builder() + .id(UUID.randomUUID().toString()) + .invocationId(invocationContext.invocationId()) + .author(agentName) + .content(content) + .timestamp(Instant.now().toEpochMilli()) + .customMetadata(aggMetadataBuilder.build()) + .build(); + } } private static boolean isCompleted(ClientEvent event) { @@ -240,6 +498,10 @@ private static boolean isCompleted(ClientEvent event) { return executionState.equals(TaskState.COMPLETED); } + private static ImmutableList eventParts(Event event) { + return ImmutableList.copyOf(event.content().flatMap(Content::parts).orElse(ImmutableList.of())); + } + @Override protected Flowable runLiveImpl(InvocationContext invocationContext) { throw new UnsupportedOperationException( diff --git a/a2a/src/main/java/com/google/adk/a2a/common/A2AMetadata.java b/a2a/src/main/java/com/google/adk/a2a/common/A2AMetadata.java new file mode 100644 index 000000000..5c75faeac --- /dev/null +++ b/a2a/src/main/java/com/google/adk/a2a/common/A2AMetadata.java @@ -0,0 +1,24 @@ +package com.google.adk.a2a.common; + +/** Constants and utilities for A2A metadata keys. */ +public final class A2AMetadata { + + /** Enum for A2A custom metadata keys. */ + public enum Key { + REQUEST("a2a:request"), + RESPONSE("a2a:response"), + AGGREGATED("a2a:aggregated"); + + private final String value; + + Key(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + private A2AMetadata() {} +} diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java index bc0620f83..8e407406f 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java @@ -7,10 +7,14 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.genai.types.Blob; +import com.google.genai.types.CodeExecutionResult; import com.google.genai.types.Content; +import com.google.genai.types.ExecutableCode; import com.google.genai.types.FileData; import com.google.genai.types.FunctionCall; import com.google.genai.types.FunctionResponse; +import com.google.genai.types.Language; +import com.google.genai.types.Outcome; import com.google.genai.types.Part; import io.a2a.spec.DataPart; import io.a2a.spec.FileContent; @@ -46,6 +50,14 @@ public final class PartConverter { public static final String A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT = "code_execution_result"; public static final String A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = "executable_code"; + public static final String LANGUAGE_KEY = "language"; + public static final String OUTCOME_KEY = "outcome"; + public static final String CODE_KEY = "code"; + public static final String OUTPUT_KEY = "output"; + public static final String NAME_KEY = "name"; + public static final String ARGS_KEY = "args"; + public static final String RESPONSE_KEY = "response"; + public static final String ID_KEY = "id"; public static Optional toTextPart(io.a2a.spec.Part part) { if (part instanceof TextPart textPart) { @@ -102,6 +114,10 @@ public static Optional convertGenaiPartToA2aPart(Part part) { return createDataPartFromFunctionCall(part.functionCall().get()); } else if (part.functionResponse().isPresent()) { return createDataPartFromFunctionResponse(part.functionResponse().get()); + } else if (part.executableCode().isPresent()) { + return createDataPartFromExecutableCode(part.executableCode().get()); + } else if (part.codeExecutionResult().isPresent()) { + return createDataPartFromCodeExecutionResult(part.codeExecutionResult().get()); } logger.warn("Cannot convert unsupported part for Google GenAI part: " + part); @@ -153,11 +169,11 @@ private static Optional convertDataPartToGenAiPart( String metadataType = metadata.getOrDefault(A2A_DATA_PART_METADATA_TYPE_KEY, "").toString(); - if ((data.containsKey("name") && data.containsKey("args")) + if ((data.containsKey(NAME_KEY) && data.containsKey(ARGS_KEY)) || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)) { - String functionName = String.valueOf(data.getOrDefault("name", "")); - String functionId = String.valueOf(data.getOrDefault("id", "")); - Map args = coerceToMap(data.get("args")); + String functionName = String.valueOf(data.getOrDefault(NAME_KEY, null)); + String functionId = String.valueOf(data.getOrDefault(ID_KEY, null)); + Map args = coerceToMap(data.get(ARGS_KEY)); return Optional.of( com.google.genai.types.Part.builder() .functionCall( @@ -165,11 +181,11 @@ private static Optional convertDataPartToGenAiPart( .build()); } - if ((data.containsKey("name") && data.containsKey("response")) + if ((data.containsKey(NAME_KEY) && data.containsKey(RESPONSE_KEY)) || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)) { - String functionName = String.valueOf(data.getOrDefault("name", "")); - String functionId = String.valueOf(data.getOrDefault("id", "")); - Map response = coerceToMap(data.get("response")); + String functionName = String.valueOf(data.getOrDefault(NAME_KEY, "")); + String functionId = String.valueOf(data.getOrDefault(ID_KEY, "")); + Map response = coerceToMap(data.get(RESPONSE_KEY)); return Optional.of( com.google.genai.types.Part.builder() .functionResponse( @@ -181,6 +197,35 @@ private static Optional convertDataPartToGenAiPart( .build()); } + if ((data.containsKey(CODE_KEY) && data.containsKey(LANGUAGE_KEY)) + || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE)) { + String code = String.valueOf(data.getOrDefault(CODE_KEY, "")); + String language = + String.valueOf( + data.getOrDefault(LANGUAGE_KEY, Language.Known.LANGUAGE_UNSPECIFIED.toString()) + .toString()); + return Optional.of( + com.google.genai.types.Part.builder() + .executableCode( + ExecutableCode.builder().code(code).language(new Language(language)).build()) + .build()); + } + + if ((data.containsKey(OUTCOME_KEY) && data.containsKey(OUTPUT_KEY)) + || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT)) { + String outcome = + String.valueOf(data.getOrDefault(OUTCOME_KEY, Outcome.Known.OUTCOME_OK).toString()); + String output = String.valueOf(data.getOrDefault(OUTPUT_KEY, "")); + return Optional.of( + com.google.genai.types.Part.builder() + .codeExecutionResult( + CodeExecutionResult.builder() + .outcome(new Outcome(outcome)) + .output(output) + .build()) + .build()); + } + try { String json = objectMapper.writeValueAsString(data); return Optional.of(com.google.genai.types.Part.builder().text(json).build()); @@ -208,9 +253,9 @@ public static Content messageToContent(Message message) { */ private static Optional createDataPartFromFunctionCall(FunctionCall functionCall) { Map data = new HashMap<>(); - data.put("name", functionCall.name().orElse("")); - data.put("id", functionCall.id().orElse("")); - data.put("args", functionCall.args().orElse(ImmutableMap.of())); + data.put(NAME_KEY, functionCall.name().orElse("")); + data.put(ID_KEY, functionCall.id().orElse("")); + data.put(ARGS_KEY, functionCall.args().orElse(ImmutableMap.of())); ImmutableMap metadata = ImmutableMap.of(A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); @@ -227,9 +272,9 @@ private static Optional createDataPartFromFunctionCall(FunctionCall fu private static Optional createDataPartFromFunctionResponse( FunctionResponse functionResponse) { Map data = new HashMap<>(); - data.put("name", functionResponse.name().orElse("")); - data.put("id", functionResponse.id().orElse("")); - data.put("response", functionResponse.response().orElse(ImmutableMap.of())); + data.put(NAME_KEY, functionResponse.name().orElse("")); + data.put(ID_KEY, functionResponse.id().orElse("")); + data.put(RESPONSE_KEY, functionResponse.response().orElse(ImmutableMap.of())); ImmutableMap metadata = ImmutableMap.of( @@ -238,6 +283,42 @@ private static Optional createDataPartFromFunctionResponse( return Optional.of(new DataPart(data, metadata)); } + private static Optional createDataPartFromExecutableCode( + ExecutableCode executableCode) { + Map data = new HashMap<>(); + data.put(CODE_KEY, executableCode.code().orElse("")); + data.put( + LANGUAGE_KEY, + executableCode + .language() + .map(Language::toString) + .orElse(Language.Known.LANGUAGE_UNSPECIFIED.toString())); + + ImmutableMap metadata = + ImmutableMap.of( + A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE); + + return Optional.of(new DataPart(data, metadata)); + } + + private static Optional createDataPartFromCodeExecutionResult( + CodeExecutionResult result) { + Map data = new HashMap<>(); + data.put( + OUTCOME_KEY, + result + .outcome() + .map(Outcome::toString) + .orElse(new Outcome(Outcome.Known.OUTCOME_UNSPECIFIED).toString())); + data.put(OUTPUT_KEY, result.output().orElse(null)); + + ImmutableMap metadata = + ImmutableMap.of( + A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT); + + return Optional.of(new DataPart(data, metadata)); + } + private PartConverter() {} /** Convert a GenAI part into the A2A JSON representation. */ @@ -267,7 +348,10 @@ public static Optional> fromGenaiPart(Part part) { return Optional.of(new FilePart(new FileWithBytes(mime, name, encoded), new HashMap<>())); } - if (part.functionCall().isPresent() || part.functionResponse().isPresent()) { + if (part.functionCall().isPresent() + || part.functionResponse().isPresent() + || part.executableCode().isPresent() + || part.codeExecutionResult().isPresent()) { return convertGenaiPartToA2aPart(part).map(data -> data); } diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java index 0a272b72d..2e32b4c8c 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java @@ -13,6 +13,7 @@ import io.a2a.client.MessageEvent; import io.a2a.client.TaskEvent; import io.a2a.client.TaskUpdateEvent; +import io.a2a.spec.Artifact; import io.a2a.spec.EventKind; import io.a2a.spec.JSONRPCError; import io.a2a.spec.Message; @@ -189,12 +190,18 @@ private static Optional handleTaskUpdate( var updateEvent = event.getUpdateEvent(); if (updateEvent instanceof TaskArtifactUpdateEvent artifactEvent) { - if (Objects.equals(artifactEvent.isAppend(), false) - || Objects.equals(artifactEvent.isLastChunk(), true)) { - return Optional.of(taskToEvent(event.getTask(), context)); - } - return Optional.empty(); + boolean isAppend = Objects.equals(artifactEvent.isAppend(), true); + boolean isLastChunk = Objects.equals(artifactEvent.isLastChunk(), true); + + Event eventPart = artifactToEvent(artifactEvent.getArtifact(), context); + eventPart.setPartial(Optional.of(isAppend || !isLastChunk)); + // append=true, lastChunk=false: emit as partial, update aggregation + // append=false, lastChunk=false: emit as partial, reset aggregation + // append=true, lastChunk=true: emit as partial, update aggregation and emit as non-partial + // append=false, lastChunk=true: emit as non-partial, drop aggregation + return Optional.of(eventPart); } + if (updateEvent instanceof TaskStatusUpdateEvent statusEvent) { var status = statusEvent.getStatus(); var taskState = event.getTask().getStatus().state(); @@ -224,6 +231,13 @@ private static Optional handleTaskUpdate( "Unsupported TaskUpdateEvent type: " + updateEvent.getClass()); } + /** Converts an artifact to an ADK event. */ + public static Event artifactToEvent(Artifact artifact, InvocationContext invocationContext) { + Message message = + new Message.Builder().role(Message.Role.AGENT).parts(artifact.parts()).build(); + return messageToEvent(message, invocationContext); + } + /** Converts an A2A message back to ADK events. */ public static Event messageToEvent(Message message, InvocationContext invocationContext) { return remoteAgentEventBuilder(invocationContext) diff --git a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java index 0c12727aa..94a54aa67 100644 --- a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java +++ b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java @@ -14,9 +14,9 @@ import com.google.adk.sessions.BaseSessionService; import com.google.adk.sessions.Session; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.genai.types.Content; +import com.google.genai.types.CustomMetadata; import io.a2a.server.agentexecution.RequestContext; import io.a2a.server.events.EventQueue; import io.a2a.server.tasks.TaskUpdater; @@ -27,6 +27,7 @@ import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -177,7 +178,7 @@ public void execute(RequestContext ctx, EventQueue eventQueue) { throw new IllegalStateException(String.format("Task %s already running", ctx.getTaskId())); } - EventProcessor p = new EventProcessor(); + EventProcessor p = new EventProcessor(agentExecutorConfig.outputMode()); Content content = PartConverter.messageToContent(message); Runner runner = runnerBuilder.build(); @@ -238,14 +239,16 @@ private static Message failedMessage(RequestContext context, Throwable e) { // Processor that will process all events related to the one runner invocation. private static class EventProcessor { + private final String runArtifactId; + private final AgentExecutorConfig.OutputMode outputMode; + private final Map lastAgentPartialArtifact = new ConcurrentHashMap<>(); // All artifacts related to the invocation should have the same artifact id. - private EventProcessor() { - artifactId = UUID.randomUUID().toString(); + private EventProcessor(AgentExecutorConfig.OutputMode outputMode) { + this.runArtifactId = UUID.randomUUID().toString(); + this.outputMode = outputMode; } - private final String artifactId; - private void process(Event event, TaskUpdater updater) { if (event.errorCode().isPresent()) { throw new InvalidAgentResponseError( @@ -265,7 +268,41 @@ private void process(Event event, TaskUpdater updater) { }); } - updater.addArtifact(parts, artifactId, null, ImmutableMap.of()); + Map metadata = new HashMap<>(); + if (event.customMetadata().isPresent()) { + for (CustomMetadata cm : event.customMetadata().get()) { + if (cm.key().isPresent() && cm.stringValue().isPresent()) { + metadata.put(cm.key().get(), cm.stringValue().get()); + } + } + } + + Boolean append = true; + Boolean lastChunk = false; + String artifactId = runArtifactId; + + if (outputMode == AgentExecutorConfig.OutputMode.ARTIFACT_PER_EVENT) { + String author = event.author(); + boolean isPartial = event.partial().orElse(false); + + if (lastAgentPartialArtifact.containsKey(author)) { + artifactId = lastAgentPartialArtifact.get(author); + append = isPartial; + } else { + artifactId = UUID.randomUUID().toString(); + append = isPartial; + } + + lastChunk = !isPartial; + + if (isPartial) { + lastAgentPartialArtifact.put(author, artifactId); + } else { + lastAgentPartialArtifact.remove(author); + } + } + + updater.addArtifact(parts, artifactId, null, metadata, append, lastChunk); } } } diff --git a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java index 9b1ed808b..ba0177dc4 100644 --- a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java +++ b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java @@ -12,11 +12,25 @@ @AutoValue public abstract class AgentExecutorConfig { + /** + * Output mode for the agent executor. + * + *

ARTIFACT_PER_RUN: The agent executor will return one artifact per run. + * + *

ARTIFACT_PER_EVENT: The agent executor will return one artifact per event. + */ + public enum OutputMode { + ARTIFACT_PER_RUN, + ARTIFACT_PER_EVENT + } + private static final RunConfig DEFAULT_RUN_CONFIG = RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.NONE).setMaxLlmCalls(20).build(); public abstract RunConfig runConfig(); + public abstract OutputMode outputMode(); + public abstract @Nullable BeforeExecuteCallback beforeExecuteCallback(); public abstract @Nullable AfterExecuteCallback afterExecuteCallback(); @@ -26,7 +40,9 @@ public abstract class AgentExecutorConfig { public abstract Builder toBuilder(); public static Builder builder() { - return new AutoValue_AgentExecutorConfig.Builder().runConfig(DEFAULT_RUN_CONFIG); + return new AutoValue_AgentExecutorConfig.Builder() + .runConfig(DEFAULT_RUN_CONFIG) + .outputMode(OutputMode.ARTIFACT_PER_RUN); } /** Builder for {@link AgentExecutorConfig}. */ @@ -35,6 +51,9 @@ public abstract static class Builder { @CanIgnoreReturnValue public abstract Builder runConfig(RunConfig runConfig); + @CanIgnoreReturnValue + public abstract Builder outputMode(OutputMode outputMode); + @CanIgnoreReturnValue public abstract Builder beforeExecuteCallback(BeforeExecuteCallback beforeExecuteCallback); diff --git a/a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java b/a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java new file mode 100644 index 000000000..87eaa2321 --- /dev/null +++ b/a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java @@ -0,0 +1,872 @@ +package com.google.adk.a2a; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.adk.a2a.common.A2AMetadata; +import com.google.adk.agents.BaseAgent; +import com.google.adk.agents.CallbackContext; +import com.google.adk.agents.Callbacks; +import com.google.adk.agents.InvocationContext; +import com.google.adk.agents.RunConfig; +import com.google.adk.artifacts.InMemoryArtifactService; +import com.google.adk.events.Event; +import com.google.adk.plugins.PluginManager; +import com.google.adk.sessions.InMemorySessionService; +import com.google.adk.sessions.Session; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.genai.types.Content; +import com.google.genai.types.CustomMetadata; +import com.google.genai.types.FunctionResponse; +import com.google.genai.types.Part; +import io.a2a.client.Client; +import io.a2a.client.ClientEvent; +import io.a2a.client.TaskEvent; +import io.a2a.client.TaskUpdateEvent; +import io.a2a.spec.AgentCapabilities; +import io.a2a.spec.AgentCard; +import io.a2a.spec.Artifact; +import io.a2a.spec.DataPart; +import io.a2a.spec.FilePart; +import io.a2a.spec.FileWithUri; +import io.a2a.spec.Message; +import io.a2a.spec.Task; +import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.TaskState; +import io.a2a.spec.TaskStatus; +import io.a2a.spec.TaskStatusUpdateEvent; +import io.a2a.spec.TextPart; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +@RunWith(JUnit4.class) +public final class RemoteA2AAgentTest { + + private Client mockClient; + private AgentCard agentCard; + private InvocationContext invocationContext; + private Session session; + + @Before + public void setUp() { + mockClient = mock(Client.class); + agentCard = + new AgentCard.Builder() + .name("remote-agent") + .description("Remote Agent") + .version("1.0.0") + .url("http://example.com") + .capabilities(new AgentCapabilities.Builder().streaming(true).build()) + .defaultInputModes(ImmutableList.of("text")) + .defaultOutputModes(ImmutableList.of("text")) + .skills(ImmutableList.of()) + .build(); + + when(mockClient.getAgentCard()).thenReturn(agentCard); + + session = + Session.builder("session-1") + .appName("demo") + .userId("user") + .events( + ImmutableList.of( + Event.builder() + .id("event-1") + .author("user") + .content( + Content.builder() + .role("user") + .parts(ImmutableList.of(Part.builder().text("Hello").build())) + .build()) + .build())) + .build(); + + invocationContext = + InvocationContext.builder() + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .pluginManager(new PluginManager()) + .invocationId("invocation-1") + .agent(new TestAgent()) + .session(session) + .runConfig(RunConfig.builder().build()) + .endInvocation(false) + .build(); + } + + @Test + public void runAsync_aggregatesPartialEvents() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("Hello ", true, false), agentCard); + consumer.accept(createPartialEvent("World!", true, false), agentCard); + consumer.accept(createFinalEvent("Final artifact content"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(4); + assertText(events.get(0), "Hello "); + assertNotAggregated(events.get(0)); + assertText(events.get(1), "World!"); + assertNotAggregated(events.get(1)); + Event aggregatedEvent = events.get(2); + assertThat(aggregatedEvent.content().get().parts().get()).hasSize(1); + assertThought(aggregatedEvent, false); + assertText(aggregatedEvent, "Hello World!"); + assertAggregated(aggregatedEvent); + Event finalEvent = events.get(3); + assertText(finalEvent, "Final artifact content"); + assertRequestMetadata(finalEvent); + assertResponseMetadata(finalEvent); + } + + @Test + public void runAsync_aggregatesInterleavedFunctionCalls() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("Hello ", true, false), agentCard); + consumer.accept(createPartialFunctionCallEvent("get_weather", "call_1"), agentCard); + consumer.accept(createPartialEvent("World!", true, false), agentCard); + consumer.accept(createFinalEvent("Final"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(6); + assertText(events.get(0), "Hello "); + assertNotAggregated(events.get(0)); + assertAggregated(events.get(1)); // Flushed Aggregation + assertText(events.get(1), "Hello "); + assertThat(events.get(2).content().get().parts().get().get(0).functionCall()).isPresent(); + assertThat( + events + .get(2) + .content() + .get() + .parts() + .get() + .get(0) + .functionCall() + .get() + .name() + .orElse("")) + .isEqualTo("get_weather"); + assertText(events.get(3), "World!"); + assertNotAggregated(events.get(3)); + assertText(events.get(5), "Final"); + assertNotAggregated(events.get(5)); + assertRequestMetadata(events.get(5)); + assertResponseMetadata(events.get(5)); + } + + @Test + public void runAsync_aggregatesFiles() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("Here is a file: ", true, false), agentCard); + consumer.accept( + createPartialFileEvent("http://example.com/file.txt", "text/plain"), agentCard); + consumer.accept(createFinalEvent("Done"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(4); + assertText(events.get(0), "Here is a file: "); + assertNotAggregated(events.get(0)); + assertAggregated(events.get(1)); // Flushed Aggregation + assertText(events.get(1), "Here is a file: "); + Part filePart = events.get(2).content().get().parts().get().get(0); + assertThat(filePart.fileData()).isPresent(); + assertThat(filePart.fileData().get().fileUri().orElse("")) + .isEqualTo("http://example.com/file.txt"); + assertRequestMetadata(events.get(2)); + assertResponseMetadata(events.get(2)); + + assertText(events.get(3), "Done"); + assertRequestMetadata(events.get(3)); + assertResponseMetadata(events.get(3)); + } + + @Test + public void runAsync_handlesTasksWithStatusMessage() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Task task = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status( + new TaskStatus( + TaskState.COMPLETED, + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("hello"))) + .build(), + null)) + .build(); + consumer.accept(new TaskEvent(task), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(1); + assertText(events.get(0), "hello"); + assertRequestMetadata(events.get(0)); + assertResponseMetadata(events.get(0)); + } + + @Test + public void runAsync_handlesTasksWithMultipartArtifact() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Artifact artifact = + new Artifact.Builder() + .artifactId("artifact-1") + .parts(ImmutableList.of(new TextPart("hello"), new TextPart("world"))) + .build(); + Task task = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(ImmutableList.of(artifact)) + .build(); + consumer.accept(new TaskEvent(task), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(1); + assertThat(events.get(0).content().get().parts().get()).hasSize(2); + assertText(events.get(0), 0, "hello"); + assertText(events.get(0), 1, "world"); + assertRequestMetadata(events.get(0)); + assertResponseMetadata(events.get(0)); + } + + @Test + public void runAsync_handlesNonFinalStatusUpdatesAsThoughts() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Task task1 = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.SUBMITTED)) + .build(); + consumer.accept( + new TaskUpdateEvent( + task1, + new TaskStatusUpdateEvent.Builder() + .taskId("task-1") + .contextId("context-1") + .status( + new TaskStatus( + TaskState.SUBMITTED, + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("submitted..."))) + .build(), + null)) + .build()), + agentCard); + Task task2 = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + consumer.accept( + new TaskUpdateEvent( + task2, + new TaskStatusUpdateEvent.Builder() + .taskId("task-1") + .contextId("context-1") + .status( + new TaskStatus( + TaskState.WORKING, + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("working..."))) + .build(), + null)) + .build()), + agentCard); + Task task3 = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts( + ImmutableList.of( + new Artifact.Builder() + .artifactId("a1") + .parts(ImmutableList.of(new TextPart("done"))) + .build())) + .build(); + consumer.accept(new TaskEvent(task3), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(3); + assertText(events.get(0), "submitted..."); + assertThought(events.get(0), true); + assertRequestMetadata(events.get(0)); + assertResponseMetadata(events.get(0)); + assertText(events.get(1), "working..."); + assertThought(events.get(1), true); + assertRequestMetadata(events.get(1)); + assertResponseMetadata(events.get(1)); + assertText(events.get(2), "done"); + assertThought(events.get(2), false); + assertRequestMetadata(events.get(2)); + assertResponseMetadata(events.get(2)); + } + + @Test + @SuppressWarnings("unchecked") // cast for Mockito + public void runAsync_constructsRequestWithHistory() { + RemoteA2AAgent agent = createAgent(); + Session historySession = + Session.builder("session-2") + .appName("demo") + .userId("user") + .events( + ImmutableList.of( + Event.builder() + .id("e1") + .author("user") + .content( + Content.builder() + .role("user") + .parts(ImmutableList.of(Part.builder().text("hello").build())) + .build()) + .build(), + Event.builder() + .id("e2") + .author("model") + .content( + Content.builder() + .role("model") + .parts(ImmutableList.of(Part.builder().text("hi").build())) + .build()) + .build(), + Event.builder() + .id("e3") + .author("user") + .content( + Content.builder() + .role("user") + .parts( + ImmutableList.of(Part.builder().text("how are you?").build())) + .build()) + .build())) + .build(); + InvocationContext context = + InvocationContext.builder() + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .pluginManager(new PluginManager()) + .invocationId("invocation-2") + .agent(new TestAgent()) + .session(historySession) + .runConfig(RunConfig.builder().build()) + .build(); + mockStreamResponse( + consumer -> { + consumer.accept(createFinalEvent("fine"), agentCard); + }); + + var unused = agent.runAsync(context).toList().blockingGet(); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mockClient) + .sendMessage(messageCaptor.capture(), any(List.class), any(Consumer.class), any()); + Message message = messageCaptor.getValue(); + assertThat(message.getRole()).isEqualTo(Message.Role.USER); + assertThat(message.getParts()).hasSize(3); + assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("hello"); + assertThat(((TextPart) message.getParts().get(1)).getText()).isEqualTo("hi"); + assertThat(((TextPart) message.getParts().get(2)).getText()).isEqualTo("how are you?"); + } + + @Test + @SuppressWarnings("unchecked") // cast for Mockito + public void runAsync_constructsRequestWithFunctionResponse() { + RemoteA2AAgent agent = createAgent(); + Session session = + Session.builder("session-3") + .appName("demo") + .userId("user") + .events( + ImmutableList.of( + Event.builder() + .id("e1") + .author("user") + .content( + Content.builder() + .role("user") + .parts( + ImmutableList.of( + Part.builder() + .functionResponse( + FunctionResponse.builder() + .name("fn") + .id("call-1") + .response(ImmutableMap.of("status", "ok")) + .build()) + .build())) + .build()) + .build())) + .build(); + InvocationContext context = + InvocationContext.builder() + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .pluginManager(new PluginManager()) + .invocationId("invocation-3") + .agent(new TestAgent()) + .session(session) + .runConfig(RunConfig.builder().build()) + .build(); + mockStreamResponse( + consumer -> { + consumer.accept(createFinalEvent("ok"), agentCard); + }); + + var unused = agent.runAsync(context).toList().blockingGet(); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mockClient) + .sendMessage(messageCaptor.capture(), any(List.class), any(Consumer.class), any()); + Message message = messageCaptor.getValue(); + + assertThat(message.getParts()).hasSize(1); + io.a2a.spec.Part part = message.getParts().get(0); + assertThat(part).isInstanceOf(DataPart.class); + DataPart dataPart = (DataPart) part; + assertThat(dataPart.getData().get("name")).isEqualTo("fn"); + assertThat(dataPart.getData().get("id")).isEqualTo("call-1"); + assertThat(dataPart.getMetadata().get("adk_type")).isEqualTo("function_response"); + } + + @Test + public void runAsync_invokesBeforeAndAfterCallbacks() { + AtomicBoolean beforeCalled = new AtomicBoolean(false); + AtomicBoolean afterCalled = new AtomicBoolean(false); + RemoteA2AAgent agent = + getAgentBuilder() + .beforeAgentCallback( + ImmutableList.of( + (CallbackContext unused) -> { + beforeCalled.set(true); + return Maybe.empty(); + })) + .afterAgentCallback( + ImmutableList.of( + (CallbackContext unused) -> { + afterCalled.set(true); + return Maybe.empty(); + })) + .build(); + mockStreamResponse( + consumer -> { + consumer.accept(createFinalEvent("done"), agentCard); + }); + + var unused = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(beforeCalled.get()).isTrue(); + assertThat(afterCalled.get()).isTrue(); + } + + @Test + public void runAsync_aggregatesCodeExecution() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialCodeEvent("print('hello')", "java"), agentCard); + consumer.accept(createPartialCodeResultEvent("hello\n", "ok"), agentCard); + consumer.accept(createFinalEvent("Done"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(3); + Part codePart = events.get(0).content().get().parts().get().get(0); + assertThat(codePart.executableCode()).isPresent(); + assertThat(codePart.executableCode().get().code()).hasValue("print('hello')"); + assertThat(codePart.executableCode().get().language().get().toString()).isEqualTo("java"); + Part resultPart = events.get(1).content().get().parts().get().get(0); + assertThat(resultPart.codeExecutionResult()).isPresent(); + assertThat(resultPart.codeExecutionResult().get().output()).hasValue("hello\n"); + assertText(events.get(2), "Done"); + assertRequestMetadata(events.get(2)); + assertResponseMetadata(events.get(2)); + } + + @Test + public void runAsync_aggregatesCodeExecution_defaultsToEmptyLanguage() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Map data = new HashMap<>(); + data.put("code", "print('hello')"); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "executable_code"); + consumer.accept( + createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false), + agentCard); + consumer.accept(createFinalEvent("Done"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(2); + Part codePart = events.get(0).content().get().parts().get().get(0); + assertThat(codePart.executableCode()).isPresent(); + assertThat(codePart.executableCode().get().code()).hasValue("print('hello')"); + assertThat(codePart.executableCode().get().language().get().toString()) + .isEqualTo("LANGUAGE_UNSPECIFIED"); + } + + @Test + public void runAsync_aggregatesCodeExecutionResult_withOnlyMetadata() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Map data = new HashMap<>(); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "code_execution_result"); + consumer.accept( + createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false), + agentCard); + consumer.accept(createFinalEvent("Done"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(2); + Part resultPart = events.get(0).content().get().parts().get().get(0); + assertThat(resultPart.codeExecutionResult()).isPresent(); + assertThat(resultPart.codeExecutionResult().get().outcome().get().toString()) + .isEqualTo("OUTCOME_OK"); + assertThat(resultPart.codeExecutionResult().get().output()).hasValue(""); + } + + @Test + public void runAsync_aggregatesPartialEvents_emptyFinalEvent() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("Hello ", true, false), agentCard); + consumer.accept(createPartialEvent("World!", true, false), agentCard); + Task task = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .build(); + consumer.accept(new TaskEvent(task), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(3); + assertText(events.get(0), "Hello "); + assertNotAggregated(events.get(0)); + assertText(events.get(1), "World!"); + assertNotAggregated(events.get(1)); + Event finalEvent = events.get(2); + assertText(finalEvent, "Hello World!"); + assertAggregated(finalEvent); + } + + @Test + public void runAsync_aggregatesPartialButNotNonPartialEvents() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("1", true, false), agentCard); + consumer.accept(createPartialEvent("2", true, false), agentCard); + consumer.accept(createPartialEvent("3", false, false), agentCard); + consumer.accept(createPartialEvent("4", true, false), agentCard); + consumer.accept(createFinalEvent("5"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(6); + assertText(events.get(0), "1"); + assertRequestMetadata(events.get(0)); + assertResponseMetadata(events.get(0)); + assertNotAggregated(events.get(0)); + assertText(events.get(1), "2"); + assertRequestMetadata(events.get(1)); + assertResponseMetadata(events.get(1)); + assertNotAggregated(events.get(1)); + assertText(events.get(2), "3"); + assertRequestMetadata(events.get(2)); + assertResponseMetadata(events.get(2)); + assertNotAggregated(events.get(2)); + assertText(events.get(3), "4"); + assertRequestMetadata(events.get(3)); + assertResponseMetadata(events.get(3)); + assertNotAggregated(events.get(3)); + assertText(events.get(4), "34"); + assertRequestMetadata(events.get(4)); + // Aggregated events do not carry response metadata + assertAggregated(events.get(4)); + assertText(events.get(5), "5"); + assertRequestMetadata(events.get(5)); + assertResponseMetadata(events.get(5)); + assertNotAggregated(events.get(5)); + } + + @Test + public void runAsync_beforeCallbackCanShortCircuit() { + Content shortCircuitContent = + Content.builder() + .role("model") + .parts(ImmutableList.of(Part.builder().text("short circuit").build())) + .build(); + RemoteA2AAgent agent = + getAgentBuilder() + .beforeAgentCallback( + ImmutableList.of( + (CallbackContext unused) -> Maybe.just(shortCircuitContent))) + .build(); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(1); + assertText(events.get(0), "short circuit"); + verifyNoInteractions(mockClient); + } + + @Test + public void runAsync_handlesClientError() { + RemoteA2AAgent agent = createAgent(); + mockStreamError(new RuntimeException("Connection failed")); + + agent + .runAsync(invocationContext) + .test() + .awaitDone(5, SECONDS) + .assertError(RuntimeException.class) + .assertError( + e -> e.getCause() != null && e.getCause().getMessage().contains("Connection failed")); + } + + private ClientEvent createPartialEvent(String text, boolean append, boolean lastChunk) { + return createTestEvent(new TextPart(text), TaskState.WORKING, append, lastChunk); + } + + private ClientEvent createPartialFunctionCallEvent(String name, String id) { + Map data = new HashMap<>(); + data.put("name", name); + data.put("id", id); + data.put("args", new HashMap<>()); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "function_call"); + + return createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false); + } + + private ClientEvent createPartialCodeEvent(String code, String language) { + Map data = new HashMap<>(); + data.put("code", code); + data.put("language", language); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "executable_code"); + + return createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false); + } + + private ClientEvent createPartialCodeResultEvent(String output, String outcome) { + Map data = new HashMap<>(); + data.put("output", output); + data.put("outcome", outcome); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "code_execution_result"); + + return createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false); + } + + private ClientEvent createPartialFileEvent(String uri, String mimeType) { + return createTestEvent( + new FilePart(new FileWithUri(mimeType, "file", uri)), TaskState.WORKING, true, false); + } + + private ClientEvent createFinalEvent(String text) { + return createTestEvent(new TextPart(text), TaskState.COMPLETED, false, false); + } + + private ClientEvent createTestEvent( + io.a2a.spec.Part part, TaskState state, boolean append, boolean lastChunk) { + Artifact artifact = + new Artifact.Builder().artifactId("artifact-1").parts(ImmutableList.of(part)).build(); + Task task = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(state)) + .artifacts(ImmutableList.of(artifact)) + .build(); + + if (state == TaskState.COMPLETED && !append && !lastChunk) { + return new TaskEvent(task); + } + + TaskArtifactUpdateEvent updateEvent = + new TaskArtifactUpdateEvent.Builder() + .lastChunk(lastChunk) + .append(append) + .contextId("context-1") + .artifact(artifact) + .taskId("task-id-1") + .build(); + return new TaskUpdateEvent(task, updateEvent); + } + + private RemoteA2AAgent.Builder getAgentBuilder() { + return RemoteA2AAgent.builder().name("remote-agent").a2aClient(mockClient).agentCard(agentCard); + } + + private RemoteA2AAgent createAgent() { + return getAgentBuilder().build(); + } + + @SuppressWarnings("unchecked") // cast for Mockito + private void mockStreamResponse(Consumer> responseProducer) { + doAnswer( + invocation -> { + List> consumers = invocation.getArgument(1); + BiConsumer consumer = consumers.get(0); + responseProducer.accept(consumer); + return null; + }) + .when(mockClient) + .sendMessage(any(Message.class), any(List.class), any(Consumer.class), any()); + } + + @SuppressWarnings("unchecked") // cast for Mockito + private void mockStreamError(Throwable error) { + doAnswer( + invocation -> { + Consumer errorConsumer = invocation.getArgument(2); + errorConsumer.accept(error); + return null; + }) + .when(mockClient) + .sendMessage(any(Message.class), any(List.class), any(Consumer.class), any()); + } + + private void assertText(Event event, String expectedText) { + assertText(event, 0, expectedText); + } + + private void assertText(Event event, int partIndex, String expectedText) { + assertThat(event.content().get().parts().get().get(partIndex).text().orElse("")) + .isEqualTo(expectedText); + } + + private void assertThought(Event event, boolean expected) { + assertThat(event.content().get().parts().get().get(0).thought().orElse(false)) + .isEqualTo(expected); + } + + private void assertAggregated(Event event) { + assertThat(event.customMetadata()).isPresent(); + List metadata = event.customMetadata().get(); + + boolean hasAggregated = + metadata.stream() + .anyMatch( + m -> + A2AMetadata.Key.AGGREGATED.getValue().equals(m.key().orElse("")) + && Objects.equals(m.stringValue().orElse(""), "true")); + boolean hasRequest = + metadata.stream() + .anyMatch(m -> A2AMetadata.Key.REQUEST.getValue().equals(m.key().orElse(""))); + + assertThat(hasAggregated).isTrue(); + assertThat(hasRequest).isTrue(); + } + + private void assertNotAggregated(Event event) { + if (event.customMetadata().isEmpty()) { + return; + } + List metadata = event.customMetadata().get(); + boolean hasAggregated = + metadata.stream() + .anyMatch( + m -> + A2AMetadata.Key.AGGREGATED.getValue().equals(m.key().orElse("")) + && Objects.equals(m.stringValue().orElse(""), "true")); + assertThat(hasAggregated).isFalse(); + } + + private void assertRequestMetadata(Event event) { + assertThat(event.customMetadata()).isPresent(); + List metadata = event.customMetadata().get(); + boolean hasRequest = + metadata.stream() + .anyMatch(m -> A2AMetadata.Key.REQUEST.getValue().equals(m.key().orElse(""))); + assertThat(hasRequest).isTrue(); + } + + private void assertResponseMetadata(Event event) { + assertThat(event.customMetadata()).isPresent(); + List metadata = event.customMetadata().get(); + boolean hasResponse = + metadata.stream() + .anyMatch(m -> A2AMetadata.Key.RESPONSE.getValue().equals(m.key().orElse(""))); + assertThat(hasResponse).isTrue(); + } + + private static final class TestAgent extends BaseAgent { + TestAgent() { + super("test_agent", "test", ImmutableList.of(), null, null); + } + + @Override + protected Flowable runAsyncImpl(InvocationContext invocationContext) { + return Flowable.empty(); + } + + @Override + protected Flowable runLiveImpl(InvocationContext invocationContext) { + return Flowable.empty(); + } + } +} diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java index d196d2f6d..8dc70ca2a 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java @@ -312,7 +312,8 @@ public void clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkTrue_ret } @Test - public void clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkFalse_returnsNull() { + public void + clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkFalse_returnsHandlingPartialEvent() { io.a2a.spec.Part a2aPart = new TextPart("Artifact content"); Artifact artifact = new Artifact.Builder().artifactId("artifact-1").parts(ImmutableList.of(a2aPart)).build(); @@ -324,6 +325,7 @@ public void clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkFalse_re TaskArtifactUpdateEvent updateEvent = new TaskArtifactUpdateEvent.Builder() .lastChunk(false) + .append(false) .contextId("context-1") .artifact(artifact) .taskId("task-id-1") @@ -331,7 +333,9 @@ public void clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkFalse_re TaskUpdateEvent event = new TaskUpdateEvent(task, updateEvent); Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); - assertThat(optionalEvent).isEmpty(); + assertThat(optionalEvent).isPresent(); + Event resultEvent = optionalEvent.get(); + assertThat(resultEvent.partial().orElse(false)).isTrue(); } @Test diff --git a/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java index 350bd6f16..d9c7c25ab 100644 --- a/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java @@ -1,6 +1,12 @@ package com.google.adk.a2a.executor; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.google.adk.agents.BaseAgent; import com.google.adk.agents.InvocationContext; @@ -9,11 +15,20 @@ import com.google.adk.events.Event; import com.google.adk.sessions.InMemorySessionService; import com.google.common.collect.ImmutableList; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import io.a2a.server.agentexecution.RequestContext; +import io.a2a.server.events.EventQueue; +import io.a2a.spec.Message; +import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.TextPart; import io.reactivex.rxjava3.core.Flowable; +import java.util.Optional; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; @RunWith(JUnit4.class) public final class AgentExecutorTest { @@ -45,8 +60,8 @@ public void createAgentExecutor_withAgentAndApp_throwsException() { .agent(testAgent) .app(App.builder().name("test_app").rootAgent(testAgent).build()) .sessionService(new InMemorySessionService()) - .agentExecutorConfig(AgentExecutorConfig.builder().build()) .artifactService(new InMemoryArtifactService()) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) .build(); }); } @@ -77,12 +92,99 @@ public void createAgentExecutor_noAgentExecutorConfig_throwsException() { }); } + @Test + public void process_statefulAggregation_tracksArtifactIdAndAppendForAuthor() { + Event partial1 = + Event.builder() + .partial(Optional.of(true)) + .author("agent_author") + .content( + Content.builder() + .parts(ImmutableList.of(Part.builder().text("chunk1").build())) + .build()) + .build(); + Event partial2 = + Event.builder() + .partial(Optional.of(true)) + .author("agent_author") + .content( + Content.builder() + .parts(ImmutableList.of(Part.builder().text("chunk2").build())) + .build()) + .build(); + Event finalEvent = + Event.builder() + .partial(Optional.of(false)) + .author("agent_author") + .content( + Content.builder() + .parts(ImmutableList.of(Part.builder().text("chunk1chunk2").build())) + .build()) + .build(); + TestAgent agent = new TestAgent(Flowable.just(partial1, partial2, finalEvent)); + AgentExecutor executor = + new AgentExecutor.Builder() + .app(App.builder().name("test_app").rootAgent(agent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .agentExecutorConfig( + AgentExecutorConfig.builder() + .outputMode(AgentExecutorConfig.OutputMode.ARTIFACT_PER_EVENT) + .build()) + .build(); + RequestContext requestContext = mock(RequestContext.class); + Message message = + new Message.Builder() + .messageId("msg-id") + .taskId("task-id") + .contextId("context-id") + .role(Message.Role.USER) + .parts(ImmutableList.of(new TextPart("test"))) + .build(); + when(requestContext.getMessage()).thenReturn(message); + when(requestContext.getTaskId()).thenReturn("task-id"); + when(requestContext.getContextId()).thenReturn("context-id"); + EventQueue eventQueue = mock(EventQueue.class); + + executor.execute(requestContext, eventQueue); + + ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(io.a2a.spec.Event.class); + verify(eventQueue, atLeastOnce()).enqueueEvent(eventCaptor.capture()); + ImmutableList artifactEvents = + eventCaptor.getAllValues().stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .map(e -> (TaskArtifactUpdateEvent) e) + .collect(toImmutableList()); + TaskArtifactUpdateEvent ev1 = artifactEvents.get(0); + TaskArtifactUpdateEvent ev2 = artifactEvents.get(1); + TaskArtifactUpdateEvent ev3 = artifactEvents.get(2); + String firstArtifactId = ev1.getArtifact().artifactId(); + // Event 1 (Partial) + assertThat(artifactEvents).hasSize(3); + assertThat(ev1.isAppend()).isTrue(); + assertThat(ev1.isLastChunk()).isFalse(); + // Event 2 (Partial) + assertThat(ev2.isAppend()).isTrue(); + assertThat(ev2.isLastChunk()).isFalse(); + assertThat(ev2.getArtifact().artifactId()).isEqualTo(firstArtifactId); + // Event 3 (Non-partial, final) + assertThat(ev3.isAppend()).isFalse(); + assertThat(ev3.isLastChunk()).isTrue(); + assertThat(ev3.getArtifact().artifactId()).isEqualTo(firstArtifactId); + } + private static final class TestAgent extends BaseAgent { - private final Flowable eventsToEmit = Flowable.empty(); + private final Flowable eventsToEmit; TestAgent() { + this(Flowable.empty()); + } + + TestAgent(Flowable eventsToEmit) { // BaseAgent constructor: name, description, examples, tools, model super("test_agent", "test", ImmutableList.of(), null, null); + this.eventsToEmit = eventsToEmit; } @Override 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 d968efa53..91dc79a56 100644 --- a/core/src/main/java/com/google/adk/events/Event.java +++ b/core/src/main/java/com/google/adk/events/Event.java @@ -27,6 +27,7 @@ import com.google.common.collect.Iterables; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.genai.types.Content; +import com.google.genai.types.CustomMetadata; import com.google.genai.types.FinishReason; import com.google.genai.types.FunctionCall; import com.google.genai.types.FunctionResponse; @@ -61,6 +62,7 @@ public class Event extends JsonBaseModel { private Optional interrupted = Optional.empty(); private Optional branch = Optional.empty(); private Optional groundingMetadata = Optional.empty(); + private Optional> customMetadata = Optional.empty(); private Optional modelVersion = Optional.empty(); private long timestamp; @@ -242,6 +244,16 @@ public void setGroundingMetadata(Optional groundingMetadata) this.groundingMetadata = groundingMetadata; } + /** The custom metadata of the event. */ + @JsonProperty("customMetadata") + public Optional> customMetadata() { + return customMetadata; + } + + public void setCustomMetadata(@Nullable List customMetadata) { + this.customMetadata = Optional.ofNullable(customMetadata); + } + /** The model version used to generate the response. */ @JsonProperty("modelVersion") public Optional modelVersion() { @@ -346,6 +358,7 @@ public static class Builder { private Optional interrupted = Optional.empty(); private Optional branch = Optional.empty(); private Optional groundingMetadata = Optional.empty(); + private Optional> customMetadata = Optional.empty(); private Optional modelVersion = Optional.empty(); private Optional timestamp = Optional.empty(); @@ -569,6 +582,17 @@ Optional groundingMetadata() { return groundingMetadata; } + @CanIgnoreReturnValue + @JsonProperty("customMetadata") + public Builder customMetadata(@Nullable List value) { + this.customMetadata = Optional.ofNullable(value); + return this; + } + + Optional> customMetadata() { + return customMetadata; + } + @CanIgnoreReturnValue @JsonProperty("modelVersion") public Builder modelVersion(@Nullable String value) { @@ -603,6 +627,7 @@ public Event build() { event.setInterrupted(interrupted); event.branch(branch); event.setGroundingMetadata(groundingMetadata); + event.setCustomMetadata(customMetadata.orElse(null)); event.setModelVersion(modelVersion); event.setActions(actions().orElseGet(() -> EventActions.builder().build())); event.setTimestamp(timestamp().orElseGet(() -> Instant.now().toEpochMilli())); @@ -639,6 +664,7 @@ public Builder toBuilder() { .interrupted(this.interrupted) .branch(this.branch) .groundingMetadata(this.groundingMetadata) + .customMetadata(this.customMetadata.orElse(null)) .modelVersion(this.modelVersion); if (this.timestamp != 0) { builder.timestamp(this.timestamp); @@ -671,6 +697,7 @@ public boolean equals(Object obj) { && Objects.equals(interrupted, other.interrupted) && Objects.equals(branch, other.branch) && Objects.equals(groundingMetadata, other.groundingMetadata) + && Objects.equals(customMetadata, other.customMetadata) && Objects.equals(modelVersion, other.modelVersion); } @@ -698,6 +725,7 @@ public int hashCode() { interrupted, branch, groundingMetadata, + customMetadata, modelVersion, timestamp); } diff --git a/core/src/main/java/com/google/adk/models/LlmResponse.java b/core/src/main/java/com/google/adk/models/LlmResponse.java index 6f8f3d785..78791acfa 100644 --- a/core/src/main/java/com/google/adk/models/LlmResponse.java +++ b/core/src/main/java/com/google/adk/models/LlmResponse.java @@ -25,6 +25,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.genai.types.Candidate; import com.google.genai.types.Content; +import com.google.genai.types.CustomMetadata; import com.google.genai.types.FinishReason; import com.google.genai.types.GenerateContentResponse; import com.google.genai.types.GenerateContentResponsePromptFeedback; @@ -59,6 +60,14 @@ public abstract class LlmResponse extends JsonBaseModel { @JsonProperty("groundingMetadata") public abstract Optional groundingMetadata(); + /** + * Returns the custom metadata of the response, if available. + * + * @return An {@link Optional} containing a list of {@link CustomMetadata} or empty. + */ + @JsonProperty("customMetadata") + public abstract Optional> customMetadata(); + /** * Indicates whether the text content is part of a unfinished text stream. * @@ -133,6 +142,9 @@ static LlmResponse.Builder jacksonBuilder() { public abstract Builder groundingMetadata(Optional groundingMetadata); + @JsonProperty("customMetadata") + public abstract Builder customMetadata(@Nullable List customMetadata); + @JsonProperty("partial") public abstract Builder partial(@Nullable Boolean partial); From 71b10701e753bddaa96d5e6579b759d2b9bb3e92 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Tue, 7 Oct 2025 21:08:18 +0200 Subject: [PATCH 29/61] fix: Allow injecting ObjectMapper in FunctionTool, default to ObjectMapper (re. #473) --- .../adk/tools/FunctionCallingUtils.java | 27 +++++++++++++++---- .../com/google/adk/tools/FunctionTool.java | 24 ++++++++++++++--- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/com/google/adk/tools/FunctionCallingUtils.java b/core/src/main/java/com/google/adk/tools/FunctionCallingUtils.java index eec115127..3244b40e9 100644 --- a/core/src/main/java/com/google/adk/tools/FunctionCallingUtils.java +++ b/core/src/main/java/com/google/adk/tools/FunctionCallingUtils.java @@ -45,7 +45,7 @@ public final class FunctionCallingUtils { private static final Logger logger = LoggerFactory.getLogger(FunctionCallingUtils.class); - private static final ObjectMapper objectMapper = JsonBaseModel.getMapper(); + private static final ObjectMapper defaultObjectMapper = JsonBaseModel.getMapper(); /** Holds the state during a single schema generation process to handle caching and recursion. */ private static class SchemaGenerationContext { @@ -162,7 +162,20 @@ private static Schema buildSchemaFromParameter(Parameter param) { * @throws IllegalArgumentException if a type is encountered that cannot be serialized by Jackson. */ public static Schema buildSchemaFromType(Type type) { - return buildSchemaRecursive(objectMapper.constructType(type), new SchemaGenerationContext()); + return buildSchemaFromType(type, defaultObjectMapper); + } + + /** + * Builds a Schema from a Java Type, creating a new context for the generation process. + * + * @param type The Java {@link Type} to convert into a Schema. + * @param objectMapper The {@link ObjectMapper} to use for introspecting types. + * @return The generated {@link Schema}. + * @throws IllegalArgumentException if a type is encountered that cannot be serialized by Jackson. + */ + public static Schema buildSchemaFromType(Type type, ObjectMapper objectMapper) { + return buildSchemaRecursive( + objectMapper.constructType(type), new SchemaGenerationContext(), objectMapper); } /** @@ -173,7 +186,8 @@ public static Schema buildSchemaFromType(Type type) { * @return The generated {@link Schema}. * @throws IllegalArgumentException if a type is encountered that cannot be serialized by Jackson. */ - private static Schema buildSchemaRecursive(JavaType javaType, SchemaGenerationContext context) { + private static Schema buildSchemaRecursive( + JavaType javaType, SchemaGenerationContext context, ObjectMapper objectMapper) { if (context.isProcessing(javaType)) { logger.warn("Type {} is recursive. Omitting from schema.", javaType.toCanonical()); return Schema.builder() @@ -194,7 +208,9 @@ private static Schema buildSchemaRecursive(JavaType javaType, SchemaGenerationCo Class rawClass = javaType.getRawClass(); if (javaType.isCollectionLikeType() && List.class.isAssignableFrom(rawClass)) { - builder.type("ARRAY").items(buildSchemaRecursive(javaType.getContentType(), context)); + builder + .type("ARRAY") + .items(buildSchemaRecursive(javaType.getContentType(), context, objectMapper)); } else if (javaType.isMapLikeType()) { builder.type("OBJECT"); } else if (String.class.equals(rawClass)) { @@ -232,7 +248,8 @@ private static Schema buildSchemaRecursive(JavaType javaType, SchemaGenerationCo for (BeanPropertyDefinition property : beanDescription.findProperties()) { AnnotatedMember member = property.getPrimaryMember(); if (member != null) { - properties.put(property.getName(), buildSchemaRecursive(member.getType(), context)); + properties.put( + property.getName(), buildSchemaRecursive(member.getType(), context, objectMapper)); if (property.isRequired()) { required.add(property.getName()); } diff --git a/core/src/main/java/com/google/adk/tools/FunctionTool.java b/core/src/main/java/com/google/adk/tools/FunctionTool.java index a6167ee46..4323b4569 100644 --- a/core/src/main/java/com/google/adk/tools/FunctionTool.java +++ b/core/src/main/java/com/google/adk/tools/FunctionTool.java @@ -44,12 +44,12 @@ public class FunctionTool extends BaseTool { private static final Logger logger = LoggerFactory.getLogger(FunctionTool.class); - private static final ObjectMapper objectMapper = JsonBaseModel.getMapper(); private final @Nullable Object instance; private final Method func; private final FunctionDeclaration funcDeclaration; private final boolean requireConfirmation; + private final ObjectMapper objectMapper; public static FunctionTool create(Object instance, Method func) { return create(instance, func, /* requireConfirmation= */ false); @@ -166,11 +166,26 @@ private static boolean wasCompiledWithDefaultParameterNames(Method func) { } protected FunctionTool(@Nullable Object instance, Method func, boolean isLongRunning) { - this(instance, func, isLongRunning, /* requireConfirmation= */ false); + this( + instance, func, isLongRunning, /* requireConfirmation= */ false, JsonBaseModel.getMapper()); } protected FunctionTool( @Nullable Object instance, Method func, boolean isLongRunning, boolean requireConfirmation) { + this(instance, func, isLongRunning, requireConfirmation, JsonBaseModel.getMapper()); + } + + protected FunctionTool( + @Nullable Object instance, Method func, boolean isLongRunning, ObjectMapper objectMapper) { + this(instance, func, isLongRunning, /* requireConfirmation= */ false, objectMapper); + } + + protected FunctionTool( + @Nullable Object instance, + Method func, + boolean isLongRunning, + boolean requireConfirmation, + ObjectMapper objectMapper) { super( func.isAnnotationPresent(Annotations.Schema.class) && !func.getAnnotation(Annotations.Schema.class).name().isEmpty() @@ -193,6 +208,7 @@ protected FunctionTool( FunctionCallingUtils.buildFunctionDeclaration( this.func, ImmutableList.of("toolContext", "inputStream")); this.requireConfirmation = requireConfirmation; + this.objectMapper = objectMapper; } @Override @@ -365,7 +381,7 @@ private static Class getTypeClass(Type type, String paramName) { } } - private static List createList(List values, Class type) { + private List createList(List values, Class type) { List list = new ArrayList<>(); // List of parameterized type is not supported. if (type == null) { @@ -387,7 +403,7 @@ private static List createList(List values, Class type) { return list; } - private static Object castValue(Object value, Class type) { + private Object castValue(Object value, Class type) { if (type.equals(Integer.class) || type.equals(int.class)) { if (value instanceof Integer) { return value; From 5bc3ef89e62eb3f32ba7e45657c9e40c88c3a5e9 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Tue, 3 Mar 2026 03:00:06 -0800 Subject: [PATCH 30/61] fix: Exit from rearrangeEventsForLatestFunctionResponse if size of events is less than 2 PiperOrigin-RevId: 877845164 --- .../google/adk/flows/llmflows/Contents.java | 5 ++ .../adk/flows/llmflows/ContentsTest.java | 52 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java index 0f2e2d166..040b14c05 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java @@ -406,6 +406,11 @@ private static boolean isEventBelongsToBranch(Optional invocationBranchO * @return A new list of events with the appropriate rearrangement. */ private static List rearrangeEventsForLatestFunctionResponse(List events) { + if (events.size() < 2) { + // No need to process, since there is no function_call. + return events; + } + // TODO: b/412663475 - Handle parallel function calls within the same event. Currently, this // throws an error. if (events.isEmpty() || Iterables.getLast(events).functionResponses().isEmpty()) { diff --git a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java index b5df658ba..d555525f4 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java @@ -142,7 +142,9 @@ public void rearrangeLatest_multipleFRsForSameFCAsync_returnsMergedFR() { @Test public void rearrangeLatest_missingFCEvent_throwsException() { Event frEvent = createFunctionResponseEvent("fr1", "tool1", "call1"); - ImmutableList events = ImmutableList.of(createUserEvent("u1", "Query"), frEvent); + Event frEvent2 = createFunctionResponseEvent("fr2", "tool1", "call1"); + ImmutableList events = + ImmutableList.of(createUserEvent("u1", "Query"), frEvent, frEvent2); assertThrows(IllegalStateException.class, () -> runContentsProcessor(events)); } @@ -473,10 +475,12 @@ public void processRequest_includeContentsNone_asyncFRAcrossTurns_throwsExceptio Event fc1 = createFunctionCallEvent("fc1", "tool1", "call1"); Event u2 = createUserEvent("u2", "Query 2"); Event fr1 = createFunctionResponseEvent("fr1", "tool1", "call1"); // FR for fc1 + Event fr2 = createFunctionResponseEvent("fr2", "tool2", "call1"); // FR for fc2 - ImmutableList events = ImmutableList.of(u1, fc1, u2, fr1); + ImmutableList events = ImmutableList.of(u1, fc1, u2, fr1, fr2); - // The current turn starts from u2. fc1 is not in the sublist [u2, fr1], so rearrangement fails. + // The current turn starts from u2. fc1 is not in the sublist [u2, fr1, fr2], so rearrangement + // fails. IllegalStateException e = assertThrows( IllegalStateException.class, @@ -486,6 +490,19 @@ public void processRequest_includeContentsNone_asyncFRAcrossTurns_throwsExceptio .contains("No function call event found for function response IDs: [call1]"); } + @Test + public void processRequest_notEnoughEvents_returnsOriginalList() { + Event fr1 = + createFunctionCallAndResponseEvent( + "fr1", "tool1", "call1", ImmutableMap.of("result", "ok"), "user"); + + ImmutableList events = ImmutableList.of(fr1); + + List result = + runContentsProcessorWithIncludeContents(events, LlmAgent.IncludeContents.NONE, "A2A-agent"); + assertThat(result).isEmpty(); + } + @Test public void processRequest_includeContentsNone_asyncFRWithinTurn() { Event u1 = createUserEvent("u1", "Query 1"); @@ -883,13 +900,40 @@ private static Event createFunctionResponseEvent( .build(); } + private static Event createFunctionCallAndResponseEvent( + String id, String toolName, String callId, Map response, String author) { + return Event.builder() + .id(id) + .author(author) + .invocationId("invocationId") + .content( + Content.fromParts( + Part.builder() + .functionCall(FunctionCall.builder().name(toolName).id(callId).build()) + .build(), + Part.builder() + .functionResponse( + FunctionResponse.builder() + .name(toolName) + .id(callId) + .response(response) + .build()) + .build())) + .build(); + } + private List runContentsProcessor(List events) { return runContentsProcessorWithIncludeContents(events, LlmAgent.IncludeContents.DEFAULT); } private List runContentsProcessorWithIncludeContents( List events, LlmAgent.IncludeContents includeContents) { - LlmAgent agent = LlmAgent.builder().name(AGENT).includeContents(includeContents).build(); + return runContentsProcessorWithIncludeContents(events, includeContents, AGENT); + } + + private List runContentsProcessorWithIncludeContents( + List events, LlmAgent.IncludeContents includeContents, String agentName) { + LlmAgent agent = LlmAgent.builder().name(agentName).includeContents(includeContents).build(); Session session = sessionService.createSession("test-app", "test-user", null, "test-session").blockingGet(); session.events().addAll(events); From 89c49c72006b35bf4d5a1fbcb4bf924b90aa9ff5 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Tue, 3 Mar 2026 10:59:18 +0000 Subject: [PATCH 31/61] dev: Increase DevContainer 2 to 4 CPUs, and rm extension GitHub default 2 CPU machines for Codespace are unbearably slow to work on with Maven Java projects, so bump to 4 CPUs requirement. Also remove the "rangav.vscode-thunder-client" VSC extension (https://www.thunderclient.com) which was originally automatically added, but is not required. --- .devcontainer/devcontainer.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e1ff3efcc..cbde1a76f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,8 +9,7 @@ "vscode": { "extensions": [ "extension-pack-for-java", - "redhat.vscode-xml", - "rangav.vscode-thunder-client" + "redhat.vscode-xml" ], "settings": { "java.jdt.download.server": "latest", @@ -24,5 +23,8 @@ }, "remoteUser": "vscode", "forwardPorts": [8000, 8080, 8081, 8082], - "postCreateCommand": "git config --global credential.helper '!gh auth git-credential' && git config --global lfs.locksverify false" + "postCreateCommand": "git config --global credential.helper '!gh auth git-credential' && git config --global lfs.locksverify false", + "hostRequirements": { + "cpus": 4 + } } From c0f9f9511c3f237458dc8fa07d18bc041382364b Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Tue, 3 Mar 2026 04:21:43 -0800 Subject: [PATCH 32/61] chore: bump rxjava version to 3.1.12 PiperOrigin-RevId: 877872293 --- a2a/pom.xml | 2 -- pom.xml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/a2a/pom.xml b/a2a/pom.xml index 9fa36e087..05c740067 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -20,7 +20,6 @@ ${project.version} 33.0.0-jre 2.19.0 - 3.1.5 1.0.0 2.0.17 1.4.4 @@ -65,7 +64,6 @@ io.reactivex.rxjava3 rxjava - ${rxjava.version} org.jspecify diff --git a/pom.xml b/pom.xml index 3b9c2bb2d..d3f2ba432 100644 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 2.0.17 1.4.5 1.0.0 - 3.1.5 + 3.1.12 3.7.0 2.35.1 3.27.7 From 69599096cb6f8ae3338353c84416aa846b6b477e Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Wed, 4 Mar 2026 01:39:55 -0800 Subject: [PATCH 33/61] chore: bump docker-java version to 3.7.0 PiperOrigin-RevId: 878360494 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d3f2ba432..feb117f6c 100644 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 1.6.0 2.19.0 4.12.0 - 3.3.6 + 3.7.0 0.18.1 3.41.0 3.9.0 From 840118ffd1bc1dd4fc1261776700743480227838 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Wed, 4 Mar 2026 01:58:50 -0800 Subject: [PATCH 34/61] chore: bump anthropic dep version to 2.15.0 PiperOrigin-RevId: 878368486 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index feb117f6c..7eaa11f84 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ 3.7.0 2.35.1 3.27.7 - 1.4.0 + 2.15.0 3.9.0 5.6 From c1ccb2e9d375fedcd7dbb594300e66a1a0488a91 Mon Sep 17 00:00:00 2001 From: Mateusz Krawiec Date: Wed, 4 Mar 2026 01:59:51 -0800 Subject: [PATCH 35/61] refactor!: remove support for legacy `transferToAgent`, superseded by `transfer_to_agent` PiperOrigin-RevId: 878368864 --- .../adk/flows/llmflows/AgentTransfer.java | 25 +--- .../adk/flows/llmflows/BaseLlmFlow.java | 8 +- .../adk/flows/llmflows/AgentTransferTest.java | 127 +++++++----------- 3 files changed, 51 insertions(+), 109 deletions(-) diff --git a/core/src/main/java/com/google/adk/flows/llmflows/AgentTransfer.java b/core/src/main/java/com/google/adk/flows/llmflows/AgentTransfer.java index 402f71c9d..0a0da8761 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/AgentTransfer.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/AgentTransfer.java @@ -55,22 +55,17 @@ public Single processRequest( .appendInstructions( ImmutableList.of(buildTargetAgentsInstructions(agent, transferTargets))); - // Note: this tool is not exposed to the LLM in GenerateContent request. It is there only to - // serve as a backwards-compatible instance for users who depend on the exact name of - // "transferToAgent". - builder.appendTools(ImmutableList.of(createTransferToAgentTool("legacyTransferToAgent"))); - - FunctionTool agentTransferTool = createTransferToAgentTool("transferToAgent"); + FunctionTool agentTransferTool = createTransferToAgentTool(); agentTransferTool.processLlmRequest(builder, ToolContext.builder(context).build()); return Single.just( RequestProcessor.RequestProcessingResult.create(builder.build(), ImmutableList.of())); } - private FunctionTool createTransferToAgentTool(String methodName) { + private FunctionTool createTransferToAgentTool() { Method transferToAgentMethod; try { transferToAgentMethod = - AgentTransfer.class.getMethod(methodName, String.class, ToolContext.class); + AgentTransfer.class.getMethod("transferToAgent", String.class, ToolContext.class); } catch (NoSuchMethodException e) { throw new IllegalStateException(e); } @@ -169,18 +164,4 @@ public static void transferToAgent( EventActions eventActions = toolContext.eventActions(); toolContext.setActions(eventActions.toBuilder().transferToAgent(agentName).build()); } - - /** - * Backwards compatible transferToAgent that uses camel-case naming instead of the ADK's - * snake_case convention. - * - *

It exists only to support users who already use literal "transferToAgent" function call to - * instruct ADK to transfer the question to another agent. - */ - @Schema(name = "transferToAgent") - public static void legacyTransferToAgent( - @Schema(name = "agentName") String agentName, - @Schema(optional = true) ToolContext toolContext) { - transferToAgent(agentName, toolContext); - } } diff --git a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java index 549652e86..1249728d8 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java @@ -577,13 +577,7 @@ public void onError(Throwable e) { .get() .content(event.content().get()); } - if (functionResponses.stream() - .anyMatch( - functionResponse -> - functionResponse - .name() - .orElse("") - .equals("transferToAgent")) + if (event.actions().transferToAgent().isPresent() || event.actions().endInvocation().orElse(false)) { sendTask.dispose(); connection.close(); diff --git a/core/src/test/java/com/google/adk/flows/llmflows/AgentTransferTest.java b/core/src/test/java/com/google/adk/flows/llmflows/AgentTransferTest.java index 6e6e99640..79552520b 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/AgentTransferTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/AgentTransferTest.java @@ -24,12 +24,13 @@ import static com.google.common.truth.Truth.assertThat; import com.google.adk.agents.InvocationContext; +import com.google.adk.agents.LiveRequest; +import com.google.adk.agents.LiveRequestQueue; import com.google.adk.agents.LlmAgent; import com.google.adk.agents.LoopAgent; import com.google.adk.agents.RunConfig; import com.google.adk.agents.SequentialAgent; import com.google.adk.events.Event; -import com.google.adk.models.LlmRequest; import com.google.adk.runner.InMemoryRunner; import com.google.adk.runner.Runner; import com.google.adk.sessions.Session; @@ -44,6 +45,7 @@ import com.google.genai.types.Schema; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.subscribers.TestSubscriber; import java.util.List; import java.util.Map; import java.util.Optional; @@ -97,6 +99,50 @@ public void exitLoopTool_exitsLoop() { // TODO: b/413488103 - complete when LoopAgent is implemented. } + @Test + public void runLive_transferToAgent_closesConnection() throws Exception { + // Arrange + Content transferCallContent = Content.fromParts(createTransferCallPart("sub_agent_1")); + Content response1 = Content.fromParts(Part.fromText("response1")); + + TestLlm testLlm = + createTestLlm( + Flowable.just(createLlmResponse(transferCallContent)), + Flowable.just(createLlmResponse(response1))); + + LlmAgent subAgent1 = createTestAgentBuilder(testLlm).name("sub_agent_1").build(); + LlmAgent rootAgent = + createTestAgentBuilder(testLlm) + .name("root_agent") + .subAgents(ImmutableList.of(subAgent1)) + .build(); + InvocationContext invocationContext = createInvocationContext(rootAgent); + + Runner runner = getRunnerAndCreateSession(rootAgent, invocationContext.session()); + LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); + + // Act + TestSubscriber testSubscriber = + runner + .runLive(invocationContext.session(), liveRequestQueue, RunConfig.builder().build()) + .test(); + liveRequestQueue.content(Content.fromParts(Part.fromText("hi"))); + testSubscriber.await(); + + // Assert + testSubscriber.assertComplete(); + assertThat(simplifyEvents(testSubscriber.values())) + .containsExactly( + "root_agent: FunctionCall(name=transfer_to_agent, args={agent_name=sub_agent_1})", + "root_agent: FunctionResponse(name=transfer_to_agent, response={})", + "sub_agent_1: response1") + .inOrder(); + + long closedConnectionsCount = + testLlm.getLiveRequestHistory().stream().filter(LiveRequest::shouldClose).count(); + assertThat(closedConnectionsCount).isEqualTo(1); + } + @Test public void testAutoToAuto() { Content transferCallContent = Content.fromParts(createTransferCallPart("sub_agent_1")); @@ -412,85 +458,6 @@ public void testAutoToLoop() { assertThat(simplifyEvents(actualEvents)).containsExactly("root_agent: response5"); } - @Test - public void testLegacyTransferToAgent() { - Content transferCallContent = - Content.fromParts( - Part.fromFunctionCall("transferToAgent", ImmutableMap.of("agentName", "sub_agent_1"))); - Content response1 = Content.fromParts(Part.fromText("response1")); - Content response2 = Content.fromParts(Part.fromText("response2")); - - TestLlm testLlm = - createTestLlm( - Flowable.just(createLlmResponse(transferCallContent)), - Flowable.just(createLlmResponse(response1)), - Flowable.just(createLlmResponse(response2))); - - LlmAgent subAgent1 = createTestAgentBuilder(testLlm).name("sub_agent_1").build(); - LlmAgent rootAgent = - createTestAgentBuilder(testLlm) - .name("root_agent") - .subAgents(ImmutableList.of(subAgent1)) - .build(); - InvocationContext invocationContext = createInvocationContext(rootAgent); - - Runner runner = getRunnerAndCreateSession(rootAgent, invocationContext.session()); - List actualEvents = runRunner(runner, invocationContext); - - assertThat(simplifyEvents(actualEvents)) - .containsExactly( - "root_agent: FunctionCall(name=transferToAgent, args={agentName=sub_agent_1})", - "root_agent: FunctionResponse(name=transferToAgent, response={})", - "sub_agent_1: response1") - .inOrder(); - - actualEvents = runRunner(runner, invocationContext); - - assertThat(simplifyEvents(actualEvents)).containsExactly("sub_agent_1: response2"); - } - - @Test - public void testAgentTransferDoesNotExposeLegacyTransferToAgent() { - Content transferCallContent = - Content.fromParts( - Part.fromFunctionCall("transferToAgent", ImmutableMap.of("agentName", "sub_agent_1"))); - Content response1 = Content.fromParts(Part.fromText("response1")); - Content response2 = Content.fromParts(Part.fromText("response2")); - TestLlm testLlm = - createTestLlm( - Flowable.just(createLlmResponse(transferCallContent)), - Flowable.just(createLlmResponse(response1)), - Flowable.just(createLlmResponse(response2))); - LlmAgent subAgent1 = createTestAgentBuilder(testLlm).name("sub_agent_1").build(); - LlmAgent rootAgent = - createTestAgentBuilder(testLlm) - .name("root_agent") - .subAgents(ImmutableList.of(subAgent1)) - .build(); - InvocationContext invocationContext = createInvocationContext(rootAgent); - AgentTransfer processor = new AgentTransfer(); - LlmRequest request = LlmRequest.builder().build(); - - var processed = processor.processRequest(invocationContext, request); - - assertThat(processed.blockingGet().updatedRequest().config().get().tools()).isPresent(); - assertThat(processed.blockingGet().updatedRequest().config().get().tools().get()).hasSize(1); - assertThat( - processed - .blockingGet() - .updatedRequest() - .config() - .get() - .tools() - .get() - .get(0) - .functionDeclarations() - .get() - .get(0) - .name()) - .hasValue("transfer_to_agent"); - } - private Runner getRunnerAndCreateSession(LlmAgent agent, Session session) { Runner runner = new InMemoryRunner(agent, session.appName()); // Ensure the session exists before running the agent. From 6e30af3425f3851bf9ff32466217d0a3593dcca6 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Wed, 4 Mar 2026 02:01:29 -0800 Subject: [PATCH 36/61] refactor: Relax type constraints on callback lists in LlmAgent.Builder Update builder methods to accept List instead of List. This allows passing lists containing subtypes of the base callback interfaces, improving flexibility, especially all the *CallbackBase are package private interfaces. PiperOrigin-RevId: 878369566 --- .../main/java/com/google/adk/agents/LlmAgent.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/com/google/adk/agents/LlmAgent.java b/core/src/main/java/com/google/adk/agents/LlmAgent.java index ee4e6ab4c..bbed217f4 100644 --- a/core/src/main/java/com/google/adk/agents/LlmAgent.java +++ b/core/src/main/java/com/google/adk/agents/LlmAgent.java @@ -318,7 +318,7 @@ public Builder beforeModelCallback(BeforeModelCallback beforeModelCallback) { @CanIgnoreReturnValue public Builder beforeModelCallback( - @Nullable List beforeModelCallbacks) { + @Nullable List beforeModelCallbacks) { this.beforeModelCallback = convertCallbacks( beforeModelCallbacks, @@ -355,7 +355,8 @@ public Builder afterModelCallback(AfterModelCallback afterModelCallback) { } @CanIgnoreReturnValue - public Builder afterModelCallback(@Nullable List afterModelCallbacks) { + public Builder afterModelCallback( + @Nullable List afterModelCallbacks) { this.afterModelCallback = convertCallbacks( afterModelCallbacks, @@ -392,7 +393,7 @@ public Builder onModelErrorCallback(OnModelErrorCallback onModelErrorCallback) { @CanIgnoreReturnValue public Builder onModelErrorCallback( - @Nullable List onModelErrorCallbacks) { + @Nullable List onModelErrorCallbacks) { this.onModelErrorCallback = convertCallbacks( onModelErrorCallbacks, @@ -488,7 +489,8 @@ public Builder afterToolCallback(AfterToolCallback afterToolCallback) { } @CanIgnoreReturnValue - public Builder afterToolCallback(@Nullable List afterToolCallbacks) { + public Builder afterToolCallback( + @Nullable List afterToolCallbacks) { this.afterToolCallback = convertCallbacks( afterToolCallbacks, @@ -528,7 +530,7 @@ public Builder onToolErrorCallback(OnToolErrorCallback onToolErrorCallback) { @CanIgnoreReturnValue public Builder onToolErrorCallback( - @Nullable List onToolErrorCallbacks) { + @Nullable List onToolErrorCallbacks) { this.onToolErrorCallback = convertCallbacks( onToolErrorCallbacks, From a0cba25d691f4be72bea22b0649ecf2d2c110736 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Wed, 4 Mar 2026 02:05:08 -0800 Subject: [PATCH 37/61] fix: Fixed issue where events were marked empty if the first part had an empty text; now checks all parts for meaningful content PiperOrigin-RevId: 878371150 --- .../google/adk/flows/llmflows/Contents.java | 31 ++++++++++++++++++- .../adk/flows/llmflows/ContentsTest.java | 26 ++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java index 040b14c05..f98a35f0b 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java @@ -169,7 +169,36 @@ private boolean isEmptyContent(Event event) { || content.role().get().isEmpty() || content.parts().isEmpty() || content.parts().get().isEmpty() - || content.parts().get().get(0).text().map(String::isEmpty).orElse(false)); + || content.parts().get().stream().allMatch(this::isPartInvisible)); + } + + /** + * Returns whether a part is invisible for LLM context. + * + *

A part is invisible if: + * + *

    + *
  • It has no meaningful content (text, inline_data, file_data, function_call, + * function_response, executable_code, or code_execution_result), OR + *
  • It is marked as a thought AND does not contain function_call or function_response + *
+ * + *

Function calls and responses are never invisible, even if marked as thought, because they + * represent actions that need to be executed or results that need to be processed. + * + * @param part the part to check. + * @return {@code true} if the part is invisible, {@code false} otherwise. + */ + private boolean isPartInvisible(Part part) { + if (part.functionCall().isPresent() || part.functionResponse().isPresent()) { + return false; + } + return part.thought().orElse(false) + || !(part.text().isPresent() + || part.inlineData().isPresent() + || part.fileData().isPresent() + || part.codeExecutionResult().isPresent() + || part.executableCode().isPresent()); } /** diff --git a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java index d555525f4..3041a855b 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java @@ -754,6 +754,32 @@ public void processRequest_slidingWindow_preservesOverlappingCompactions() { .containsExactly("C1", "C2", "E4", "E5"); } + @Test + public void processRequest_notEmptyContent() { + Event e = + Event.builder() + .id("e1") + .author(AGENT) + .content( + Content.builder() + .role("model") + .parts( + ImmutableList.of( + Part.builder().text("").thought(true).build(), + Part.builder() + .functionCall( + FunctionCall.builder() + .name("test-tool") + .id("test-call-id") + .build()) + .thought(false) + .build())) + .build()) + .build(); + List contents = runContentsProcessor(ImmutableList.of(e)); + assertThat(contents).containsExactly(e.content().get()); + } + private static Event createUserEvent(String id, String text) { return Event.builder() .id(id) From db0983ba2f89ed26af35ff043e555510e86b7f33 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Wed, 4 Mar 2026 02:16:14 -0800 Subject: [PATCH 38/61] chore: bump okhttp version to 5.3.2 PiperOrigin-RevId: 878375304 --- core/pom.xml | 4 ++++ pom.xml | 14 ++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index eefbcda79..93c72e745 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -74,6 +74,10 @@ com.squareup.okhttp3 okhttp + + com.squareup.okhttp3 + okhttp-jvm + com.google.auto.value auto-value-annotations diff --git a/pom.xml b/pom.xml index 7eaa11f84..b35a38451 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ 5.20.0 1.6.0 2.19.0 - 4.12.0 + 5.3.2 3.7.0 0.18.1 3.41.0 @@ -112,6 +112,13 @@ pom import + + com.squareup.okhttp3 + okhttp-bom + ${okhttp.version} + pom + import + @@ -144,11 +151,6 @@ google-genai ${google.genai.version} - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - com.google.auto.value auto-value-annotations From 6001b448f31fb2cf52e56670297224cfa81864c2 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Wed, 4 Mar 2026 02:44:13 -0800 Subject: [PATCH 39/61] chore: add illegal optional checkstyle check PiperOrigin-RevId: 878384678 --- pom.xml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pom.xml b/pom.xml index b35a38451..af8a1f2b1 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,7 @@ 17 ${java.version} UTF-8 + 3.6.0 1.11.1 3.4.1 @@ -289,6 +290,11 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven.checkstyle.plugin.version} + maven-clean-plugin 3.1.0 @@ -464,6 +470,40 @@ + + illegal-optional-check + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + illegal-optional-check + + check + + compile + + + + + + + + + + + + + + + + + + + + release-sonatype From 05fbcfc933923ae711cd12e7fc9e587fd8e2685c Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Wed, 4 Mar 2026 05:05:13 -0800 Subject: [PATCH 40/61] feat: Adding a Builder for EventsCompactionConfig PiperOrigin-RevId: 878430680 --- .../summarizer/EventsCompactionConfig.java | 31 +++++++++++ .../EventsCompactionConfigTest.java | 55 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 core/src/test/java/com/google/adk/summarizer/EventsCompactionConfigTest.java diff --git a/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java b/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java index b61cd2008..39698c3db 100644 --- a/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java +++ b/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java @@ -16,6 +16,8 @@ package com.google.adk.summarizer; +import com.google.auto.value.AutoBuilder; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import javax.annotation.Nullable; /** @@ -39,6 +41,35 @@ public record EventsCompactionConfig( @Nullable Integer tokenThreshold, @Nullable Integer eventRetentionSize) { + public static Builder builder() { + return new AutoBuilder_EventsCompactionConfig_Builder(); + } + + public Builder toBuilder() { + return new AutoBuilder_EventsCompactionConfig_Builder(this); + } + + /** Builder for {@link EventsCompactionConfig}. */ + @AutoBuilder + public abstract static class Builder { + @CanIgnoreReturnValue + public abstract Builder compactionInterval(@Nullable Integer compactionInterval); + + @CanIgnoreReturnValue + public abstract Builder overlapSize(@Nullable Integer overlapSize); + + @CanIgnoreReturnValue + public abstract Builder summarizer(@Nullable BaseEventSummarizer summarizer); + + @CanIgnoreReturnValue + public abstract Builder tokenThreshold(@Nullable Integer tokenThreshold); + + @CanIgnoreReturnValue + public abstract Builder eventRetentionSize(@Nullable Integer eventRetentionSize); + + public abstract EventsCompactionConfig build(); + } + public EventsCompactionConfig(int compactionInterval, int overlapSize) { this(compactionInterval, overlapSize, null, null, null); } diff --git a/core/src/test/java/com/google/adk/summarizer/EventsCompactionConfigTest.java b/core/src/test/java/com/google/adk/summarizer/EventsCompactionConfigTest.java new file mode 100644 index 000000000..01f59d37a --- /dev/null +++ b/core/src/test/java/com/google/adk/summarizer/EventsCompactionConfigTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.summarizer; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class EventsCompactionConfigTest { + + @Test + public void builder_buildsConfig() { + EventsCompactionConfig config = + EventsCompactionConfig.builder() + .compactionInterval(10) + .overlapSize(2) + .tokenThreshold(100) + .eventRetentionSize(5) + .build(); + + assertThat(config.compactionInterval()).isEqualTo(10); + assertThat(config.overlapSize()).isEqualTo(2); + assertThat(config.tokenThreshold()).isEqualTo(100); + assertThat(config.eventRetentionSize()).isEqualTo(5); + assertThat(config.summarizer()).isNull(); + } + + @Test + public void toBuilder_rebuildsConfig() { + EventsCompactionConfig config = + EventsCompactionConfig.builder().compactionInterval(10).overlapSize(2).build(); + + EventsCompactionConfig rebuilt = config.toBuilder().compactionInterval(20).build(); + + assertThat(rebuilt.compactionInterval()).isEqualTo(20); + assertThat(rebuilt.overlapSize()).isEqualTo(2); + } +} From 7e8f9dcf82fe7e62aee625fbfaa8673d238ff184 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Wed, 4 Mar 2026 05:37:00 -0800 Subject: [PATCH 41/61] feat: add callbacks functionality to the agent executor PiperOrigin-RevId: 878441462 --- .../adk/a2a/executor/AgentExecutor.java | 129 +++++++-- .../adk/a2a/executor/AgentExecutorTest.java | 271 +++++++++++++++++- 2 files changed, 369 insertions(+), 31 deletions(-) diff --git a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java index 94a54aa67..3d66a4e07 100644 --- a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java +++ b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java @@ -20,11 +20,19 @@ import io.a2a.server.agentexecution.RequestContext; import io.a2a.server.events.EventQueue; import io.a2a.server.tasks.TaskUpdater; +import io.a2a.spec.Artifact; import io.a2a.spec.InvalidAgentResponseError; import io.a2a.spec.Message; import io.a2a.spec.Part; +import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.TaskState; +import io.a2a.spec.TaskStatus; +import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.spec.TextPart; +import io.reactivex.rxjava3.core.Completable; +import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import java.util.HashMap; @@ -43,10 +51,8 @@ * use in production code. */ public class AgentExecutor implements io.a2a.server.agentexecution.AgentExecutor { - private static final Logger logger = LoggerFactory.getLogger(AgentExecutor.class); private static final String USER_ID_PREFIX = "A2A_USER_"; - private final Map activeTasks = new ConcurrentHashMap<>(); private final Runner.Builder runnerBuilder; private final AgentExecutorConfig agentExecutorConfig; @@ -137,7 +143,6 @@ public Builder plugins(List plugins) { return this; } - @CanIgnoreReturnValue public AgentExecutor build() { return new AgentExecutor( app, @@ -165,46 +170,88 @@ public void execute(RequestContext ctx, EventQueue eventQueue) { if (message == null) { throw new IllegalArgumentException("Message cannot be null"); } - // Submits a new task if there is no active task. if (ctx.getTask() == null) { updater.submit(); } - // Group all reactive work for this task into one container CompositeDisposable taskDisposables = new CompositeDisposable(); // Check if the task with the task id is already running, put if absent. if (activeTasks.putIfAbsent(ctx.getTaskId(), taskDisposables) != null) { throw new IllegalStateException(String.format("Task %s already running", ctx.getTaskId())); } - EventProcessor p = new EventProcessor(agentExecutorConfig.outputMode()); Content content = PartConverter.messageToContent(message); - Runner runner = runnerBuilder.build(); + Single skipExecution = + agentExecutorConfig.beforeExecuteCallback() != null + ? agentExecutorConfig.beforeExecuteCallback().call(ctx) + : Single.just(false); + Runner runner = runnerBuilder.build(); taskDisposables.add( - prepareSession(ctx, runner.appName(), runner.sessionService()) + skipExecution .flatMapPublisher( - session -> { - updater.startWork(); - return runner.runAsync( - getUserId(ctx), session.id(), content, agentExecutorConfig.runConfig()); + skip -> { + if (skip) { + cancel(ctx, eventQueue); + return Flowable.empty(); + } + return Maybe.defer( + () -> { + return prepareSession(ctx, runner.appName(), runner.sessionService()); + }) + .flatMapPublisher( + session -> { + updater.startWork(); + return runner.runAsync( + getUserId(ctx), + session.id(), + content, + agentExecutorConfig.runConfig()); + }); }) - .subscribe( + .concatMap( event -> { - p.process(event, updater); - }, + return p.process(event, ctx, agentExecutorConfig.afterEventCallback(), eventQueue) + .toFlowable(); + }) + // Ignore all events from the runner, since they are already processed. + .ignoreElements() + .materialize() + .flatMapCompletable( + notification -> { + Throwable error = notification.getError(); + if (error != null) { + logger.error("Runner failed to execute", error); + } + return handleExecutionEnd(ctx, error, eventQueue); + }) + .doFinally(() -> cleanupTask(ctx.getTaskId())) + .subscribe( + () -> {}, error -> { - logger.error("Runner failed with {}", error); - updater.fail(failedMessage(ctx, error)); - cleanupTask(ctx.getTaskId()); - }, - () -> { - updater.complete(); - cleanupTask(ctx.getTaskId()); + logger.error("Failed to handle execution end", error); })); } + private Completable handleExecutionEnd( + RequestContext ctx, Throwable error, EventQueue eventQueue) { + TaskState state = error != null ? TaskState.FAILED : TaskState.COMPLETED; + Message message = error != null ? failedMessage(ctx, error) : null; + TaskStatusUpdateEvent initialEvent = + new TaskStatusUpdateEvent.Builder() + .taskId(ctx.getTaskId()) + .contextId(ctx.getContextId()) + .isFinal(true) + .status(new TaskStatus(state, message, null)) + .build(); + Maybe afterExecute = + agentExecutorConfig.afterExecuteCallback() != null + ? agentExecutorConfig.afterExecuteCallback().call(ctx, initialEvent) + : Maybe.just(initialEvent); + return afterExecute.doOnSuccess(event -> eventQueue.enqueueEvent(event)).ignoreElement(); + } + private void cleanupTask(String taskId) { Disposable d = activeTasks.remove(taskId); if (d != null) { @@ -249,16 +296,19 @@ private EventProcessor(AgentExecutorConfig.OutputMode outputMode) { this.outputMode = outputMode; } - private void process(Event event, TaskUpdater updater) { + private Maybe process( + Event event, + RequestContext ctx, + Callbacks.AfterEventCallback callback, + EventQueue eventQueue) { if (event.errorCode().isPresent()) { - throw new InvalidAgentResponseError( - null, // Uses default code -32006 - "Agent returned an error: " + event.errorCode().get(), - null); + return Maybe.error( + new InvalidAgentResponseError( + null, // Uses default code -32006 + "Agent returned an error: " + event.errorCode().get(), + null)); } - ImmutableList> parts = EventConverter.contentToParts(event.content()); - // Mark all parts as partial if the event is partial. if (event.partial().orElse(false)) { parts.forEach( @@ -302,7 +352,26 @@ private void process(Event event, TaskUpdater updater) { } } - updater.addArtifact(parts, artifactId, null, metadata, append, lastChunk); + TaskArtifactUpdateEvent initialEvent = + new TaskArtifactUpdateEvent.Builder() + .taskId(ctx.getTaskId()) + .contextId(ctx.getContextId()) + .lastChunk(lastChunk) + .append(append) + .artifact( + new Artifact.Builder() + .artifactId(artifactId) + .parts(parts) + .metadata(metadata) + .build()) + .build(); + + Maybe afterEvent = + callback != null ? callback.call(ctx, initialEvent, event) : Maybe.just(initialEvent); + return afterEvent.doOnSuccess( + finalEvent -> { + eventQueue.enqueueEvent(finalEvent); + }); } } } diff --git a/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java index d9c7c25ab..5570f40d0 100644 --- a/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java @@ -3,7 +3,9 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -15,15 +17,25 @@ import com.google.adk.events.Event; import com.google.adk.sessions.InMemorySessionService; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.genai.types.Content; import com.google.genai.types.Part; import io.a2a.server.agentexecution.RequestContext; import io.a2a.server.events.EventQueue; import io.a2a.spec.Message; import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.TaskState; +import io.a2a.spec.TaskStatus; +import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.spec.TextPart; import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.UUID; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,10 +45,21 @@ @RunWith(JUnit4.class) public final class AgentExecutorTest { + private EventQueue eventQueue; + private List enqueuedEvents; private TestAgent testAgent; @Before public void setUp() { + enqueuedEvents = new ArrayList<>(); + eventQueue = mock(EventQueue.class); + doAnswer( + invocation -> { + enqueuedEvents.add(invocation.getArgument(0)); + return null; + }) + .when(eventQueue) + .enqueueEvent(any()); testAgent = new TestAgent(); } @@ -92,6 +115,248 @@ public void createAgentExecutor_noAgentExecutorConfig_throwsException() { }); } + @Test + public void execute_withBeforeExecuteCallback_cancelsExecutionOnError() { + // If callback returns error, execution should stop/fail. + Callbacks.BeforeExecuteCallback callback = + ctx -> Single.error(new RuntimeException("Cancelled")); + + AgentExecutorConfig config = + AgentExecutorConfig.builder().beforeExecuteCallback(callback).build(); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(config) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Verify error handling triggered cleanup and fail event + // The executor catches the error and emits failed event. + assertThat(enqueuedEvents).isNotEmpty(); + Object lastEvent = Iterables.getLast(enqueuedEvents); + assertThat(lastEvent).isInstanceOf(TaskStatusUpdateEvent.class); + TaskStatusUpdateEvent statusEvent = (TaskStatusUpdateEvent) lastEvent; + assertThat(statusEvent.getStatus().state().toString()).isEqualTo("FAILED"); + assertThat(statusEvent.getStatus().message().getParts().get(0)).isInstanceOf(TextPart.class); + TextPart textPart = (TextPart) statusEvent.getStatus().message().getParts().get(0); + assertThat(textPart.getText()).contains("Cancelled"); + } + + @Test + public void execute_withBeforeExecuteCallback_skipsExecutionIfTrue() { + Callbacks.BeforeExecuteCallback callback = ctx -> Single.just(true); + + AgentExecutorConfig config = + AgentExecutorConfig.builder().beforeExecuteCallback(callback).build(); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(config) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Filter for artifact events + Optional artifactEvent = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .map(e -> (TaskArtifactUpdateEvent) e) + .findFirst(); + + assertThat(artifactEvent).isEmpty(); + } + + @Test + public void execute_withAfterEventCallback_modifiesEvent() { + // Agent emits an event. Callback intercepts and modifies it. + Part textPart = Part.builder().text("Hello world").build(); + Event agentEvent = + Event.builder() + .id("event-1") + .author("agent") + .content(Content.builder().role("model").parts(ImmutableList.of(textPart)).build()) + .build(); + testAgent.setEventsToEmit(Flowable.just(agentEvent)); + + Callbacks.AfterEventCallback callback = + (ctx, event, sourceEvent) -> { + // Modify event by adding metadata + return Maybe.just( + new TaskArtifactUpdateEvent.Builder(event) + .metadata(ImmutableMap.of("modified", true)) + .build()); + }; + + AgentExecutorConfig config = AgentExecutorConfig.builder().afterEventCallback(callback).build(); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(config) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Filter for artifact events + Optional artifactEvent = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .map(e -> (TaskArtifactUpdateEvent) e) + .findFirst(); + + assertThat(artifactEvent).isPresent(); + assertThat(artifactEvent.get().getMetadata()).containsEntry("modified", true); + } + + @Test + public void execute_withAfterExecuteCallback_modifiesStatus() { + testAgent.setEventsToEmit(Flowable.empty()); // Just complete + + Callbacks.AfterExecuteCallback callback = + (ctx, event) -> { + // Modify status to have different message + Message newMessage = + new Message.Builder() + .messageId(UUID.randomUUID().toString()) + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("Modified completion"))) + .build(); + + return Maybe.just( + new TaskStatusUpdateEvent.Builder(event) + .status(new TaskStatus(event.getStatus().state(), newMessage, null)) + .build()); + }; + + AgentExecutorConfig config = + AgentExecutorConfig.builder().afterExecuteCallback(callback).build(); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(config) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Verify status event + Optional statusEvent = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent) + .map(e -> (TaskStatusUpdateEvent) e) + .filter(TaskStatusUpdateEvent::isFinal) + .findFirst(); + + assertThat(statusEvent).isPresent(); + assertThat(statusEvent.get().getStatus().message().getParts().get(0)) + .isInstanceOf(TextPart.class); + TextPart textPart = (TextPart) statusEvent.get().getStatus().message().getParts().get(0); + assertThat(textPart.getText()).isEqualTo("Modified completion"); + } + + @Test + public void execute_runnerFails_registersFailedEvent() { + testAgent.setEventsToEmit(Flowable.error(new RuntimeException("Runner error"))); + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + ImmutableList finalEvents = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent) + .map(e -> (TaskStatusUpdateEvent) e) + // final events could be COMPLETED, FAILED, CANCELED, REJECTED or UNKNOWN + // as per io.a2a.spec.TaskState + .filter(TaskStatusUpdateEvent::isFinal) + .collect(toImmutableList()); + + assertThat(finalEvents).hasSize(1); + + TaskStatusUpdateEvent statusEvent = finalEvents.get(0); + assertThat(statusEvent.getStatus().state()).isEqualTo(TaskState.FAILED); + assertThat(statusEvent.getStatus().message().getParts().get(0)).isInstanceOf(TextPart.class); + TextPart textPart = (TextPart) statusEvent.getStatus().message().getParts().get(0); + assertThat(textPart.getText()).isEqualTo("Runner error"); + } + + @Test + public void execute_runnerSucceeds_registerCompletedTaskFails_noFailedTaskRegistered() { + testAgent.setEventsToEmit(Flowable.empty()); + + // Configure eventQueue to throw exception when TaskStatusUpdateEvent is enqueued + doAnswer( + invocation -> { + Object event = invocation.getArgument(0); + if (event instanceof TaskStatusUpdateEvent statusUpdate) { + if (statusUpdate.getStatus().state() == TaskState.COMPLETED) { + throw new RuntimeException("Enqueue failed"); + } + } + return null; + }) + .when(eventQueue) + .enqueueEvent(any()); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Verify status events in the tracked enqueuedEvents + ImmutableList statusEvents = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent) + .map(e -> (TaskStatusUpdateEvent) e) + .filter(TaskStatusUpdateEvent::isFinal) + .collect(toImmutableList()); + + // There should be no final status events. + assertThat(statusEvents).isEmpty(); + } + + private RequestContext createRequestContext() { + Message message = + new Message.Builder() + .messageId("msg-1") + .role(Message.Role.USER) + .parts(ImmutableList.of(new TextPart("trigger"))) + .build(); + + RequestContext ctx = mock(RequestContext.class); + when(ctx.getMessage()).thenReturn(message); + when(ctx.getTaskId()).thenReturn("task-" + UUID.randomUUID()); + when(ctx.getContextId()).thenReturn("ctx-" + UUID.randomUUID()); + return ctx; + } + @Test public void process_statefulAggregation_tracksArtifactIdAndAppendForAuthor() { Event partial1 = @@ -175,7 +440,7 @@ public void process_statefulAggregation_tracksArtifactIdAndAppendForAuthor() { } private static final class TestAgent extends BaseAgent { - private final Flowable eventsToEmit; + private Flowable eventsToEmit; TestAgent() { this(Flowable.empty()); @@ -187,6 +452,10 @@ private static final class TestAgent extends BaseAgent { this.eventsToEmit = eventsToEmit; } + void setEventsToEmit(Flowable events) { + this.eventsToEmit = events; + } + @Override protected Flowable runAsyncImpl(InvocationContext invocationContext) { return eventsToEmit; From acffdb96bcd8133af99cb0b9426665ba73a83bbc Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Wed, 4 Mar 2026 07:32:58 -0800 Subject: [PATCH 42/61] fix: Ensure Gemini 3.1 models have events correctly buffered PiperOrigin-RevId: 878483852 --- core/src/main/java/com/google/adk/flows/llmflows/Contents.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java index f98a35f0b..a770808d4 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java @@ -593,7 +593,7 @@ private static List rearrangeEventsForAsyncFunctionResponsesInHistory( // Gemini 3 requires function calls to be grouped first and only then function responses: // FC1 FC2 FR1 FR2 - boolean shouldBufferResponseEvents = modelName.contains("gemini-3-"); + boolean shouldBufferResponseEvents = modelName.contains("gemini-3"); for (int i = 0; i < events.size(); i++) { Event event = events.get(i); From a3ac436bcfa241e90c07485e5da918ec8dbc2b4a Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Thu, 5 Mar 2026 02:41:47 -0800 Subject: [PATCH 43/61] feat!: remove Optional parameters from LlmResponse.Builder's methods PiperOrigin-RevId: 878941018 --- .../adk/flows/llmflows/CodeExecution.java | 2 +- .../adk/models/GeminiLlmConnection.java | 2 +- .../com/google/adk/models/LlmResponse.java | 33 +++---------------- .../google/adk/models/LlmResponseTest.java | 16 ++------- 4 files changed, 10 insertions(+), 43 deletions(-) diff --git a/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java b/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java index 1f99cf4a2..be0504dd4 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java @@ -303,7 +303,7 @@ private static Flowable runPostProcessor( } String codeStr = codeStrOptional.get(); responseContent = responseContentBuilder.build(); - llmResponseBuilder.content(Optional.empty()); + llmResponseBuilder.content((Content) null); Event codeEvent = Event.builder() diff --git a/core/src/main/java/com/google/adk/models/GeminiLlmConnection.java b/core/src/main/java/com/google/adk/models/GeminiLlmConnection.java index 2e1229d0b..7585d3a7a 100644 --- a/core/src/main/java/com/google/adk/models/GeminiLlmConnection.java +++ b/core/src/main/java/com/google/adk/models/GeminiLlmConnection.java @@ -191,7 +191,7 @@ private static LlmResponse createServerContentResponse(LiveServerContent serverC return builder .partial(serverContent.turnComplete().map(completed -> !completed).orElse(false)) .turnComplete(serverContent.turnComplete().orElse(false)) - .interrupted(serverContent.interrupted()) + .interrupted(serverContent.interrupted().orElse(null)) .build(); } diff --git a/core/src/main/java/com/google/adk/models/LlmResponse.java b/core/src/main/java/com/google/adk/models/LlmResponse.java index 78791acfa..1ca381e23 100644 --- a/core/src/main/java/com/google/adk/models/LlmResponse.java +++ b/core/src/main/java/com/google/adk/models/LlmResponse.java @@ -128,74 +128,51 @@ static LlmResponse.Builder jacksonBuilder() { } @JsonProperty("content") - public abstract Builder content(Content content); - - public abstract Builder content(Optional content); + public abstract Builder content(@Nullable Content content); @JsonProperty("interrupted") public abstract Builder interrupted(@Nullable Boolean interrupted); - public abstract Builder interrupted(Optional interrupted); - @JsonProperty("groundingMetadata") public abstract Builder groundingMetadata(@Nullable GroundingMetadata groundingMetadata); - public abstract Builder groundingMetadata(Optional groundingMetadata); - @JsonProperty("customMetadata") public abstract Builder customMetadata(@Nullable List customMetadata); @JsonProperty("partial") public abstract Builder partial(@Nullable Boolean partial); - public abstract Builder partial(Optional partial); - @JsonProperty("turnComplete") public abstract Builder turnComplete(@Nullable Boolean turnComplete); - public abstract Builder turnComplete(Optional turnComplete); - @JsonProperty("errorCode") public abstract Builder errorCode(@Nullable FinishReason errorCode); - public abstract Builder errorCode(Optional errorCode); - @JsonProperty("finishReason") public abstract Builder finishReason(@Nullable FinishReason finishReason); - public abstract Builder finishReason(Optional finishReason); - @JsonProperty("avgLogprobs") public abstract Builder avgLogprobs(@Nullable Double avgLogprobs); - public abstract Builder avgLogprobs(Optional avgLogprobs); - @JsonProperty("errorMessage") public abstract Builder errorMessage(@Nullable String errorMessage); - public abstract Builder errorMessage(Optional errorMessage); - @JsonProperty("usageMetadata") public abstract Builder usageMetadata( @Nullable GenerateContentResponseUsageMetadata usageMetadata); - public abstract Builder usageMetadata( - Optional usageMetadata); - @JsonProperty("modelVersion") public abstract Builder modelVersion(@Nullable String modelVersion); - public abstract Builder modelVersion(Optional modelVersion); - @CanIgnoreReturnValue public final Builder response(GenerateContentResponse response) { Optional> candidatesOpt = response.candidates(); if (candidatesOpt.isPresent() && !candidatesOpt.get().isEmpty()) { Candidate candidate = candidatesOpt.get().get(0); - this.finishReason(candidate.finishReason()); + this.finishReason(candidate.finishReason().orElse(null)); if (candidate.content().isPresent()) { this.content(candidate.content().get()); - this.groundingMetadata(candidate.groundingMetadata()); + this.groundingMetadata(candidate.groundingMetadata().orElse(null)); } else { candidate.finishReason().ifPresent(this::errorCode); candidate.finishMessage().ifPresent(this::errorMessage); @@ -214,8 +191,8 @@ public final Builder response(GenerateContentResponse response) { this.errorMessage("Unknown error."); } } - this.usageMetadata(response.usageMetadata()); - this.modelVersion(response.modelVersion()); + this.usageMetadata(response.usageMetadata().orElse(null)); + this.modelVersion(response.modelVersion().orElse(null)); return this; } diff --git a/core/src/test/java/com/google/adk/models/LlmResponseTest.java b/core/src/test/java/com/google/adk/models/LlmResponseTest.java index 65e00413f..646b89602 100644 --- a/core/src/test/java/com/google/adk/models/LlmResponseTest.java +++ b/core/src/test/java/com/google/adk/models/LlmResponseTest.java @@ -28,7 +28,6 @@ import com.google.genai.types.FunctionCall; import com.google.genai.types.GenerateContentResponseUsageMetadata; import com.google.genai.types.Part; -import java.util.Optional; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -74,8 +73,8 @@ public void testSerializationAndDeserialization_allFieldsPresent() .partial(true) .turnComplete(false) .errorCode(new FinishReason("ERR_123")) - .errorMessage(Optional.of("An error occurred.")) - .interrupted(Optional.of(true)) + .errorMessage("An error occurred.") + .interrupted(true) .usageMetadata(usageMetadata) .build(); @@ -113,16 +112,7 @@ public void testSerializationAndDeserialization_optionalFieldsEmpty() throws JsonProcessingException { Content sampleContent = createSampleFunctionCallContent("tool_abc"); LlmResponse originalResponse = - LlmResponse.builder() - .content(sampleContent) - .groundingMetadata(Optional.empty()) - .partial(Optional.empty()) - .turnComplete(false) - .errorCode(Optional.empty()) - .errorMessage(Optional.empty()) - .interrupted(Optional.empty()) - .usageMetadata(Optional.empty()) - .build(); + LlmResponse.builder().content(sampleContent).turnComplete(false).build(); String json = originalResponse.toJson(); assertThat(json).isNotNull(); From 8c6591bc4ad86c376cdd70e1bb64f359fbf22fe9 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Thu, 5 Mar 2026 05:44:52 -0800 Subject: [PATCH 44/61] fix: prepare JSON serialization for Jackson 2.20.2 and Spring Boot 4.0.2 upgrades PiperOrigin-RevId: 879016658 --- .../java/com/google/adk/web/AdkWebServer.java | 17 +++++++++++++++++ .../adk/web/controller/ExecutionController.java | 10 +++++++--- .../com/google/adk/web/dto/AgentRunRequest.java | 15 +++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/dev/src/main/java/com/google/adk/web/AdkWebServer.java b/dev/src/main/java/com/google/adk/web/AdkWebServer.java index 0d7c7d0e1..b321cef27 100644 --- a/dev/src/main/java/com/google/adk/web/AdkWebServer.java +++ b/dev/src/main/java/com/google/adk/web/AdkWebServer.java @@ -35,6 +35,8 @@ import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -87,10 +89,25 @@ public BaseMemoryService memoryService() { * @return Configured ObjectMapper instance */ @Bean + @Primary public ObjectMapper objectMapper() { return JsonBaseModel.getMapper(); } + /** + * Configures the message converter to use the custom ADK ObjectMapper. This ensures that Spring + * Web uses the correct JSON serialization settings (like omitting absent optional fields) and + * prevents double-serialization issues, particularly for Server-Sent Events (SSE). + * + * @param objectMapper The primary ObjectMapper configured for the ADK. + * @return A configured MappingJackson2HttpMessageConverter. + */ + @Bean + public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( + ObjectMapper objectMapper) { + return new MappingJackson2HttpMessageConverter(objectMapper); + } + /** * Configures resource handlers for serving static content (like the Dev UI). Maps requests * starting with "/dev-ui/" to the directory specified by the 'adk.web.ui.dir' system property. diff --git a/dev/src/main/java/com/google/adk/web/controller/ExecutionController.java b/dev/src/main/java/com/google/adk/web/controller/ExecutionController.java index 6d5a2764c..e88a83cef 100644 --- a/dev/src/main/java/com/google/adk/web/controller/ExecutionController.java +++ b/dev/src/main/java/com/google/adk/web/controller/ExecutionController.java @@ -81,7 +81,11 @@ public List agentRun(@RequestBody AgentRunRequest request) { RunConfig runConfig = RunConfig.builder().setStreamingMode(StreamingMode.NONE).build(); Flowable eventStream = runner.runAsync( - request.userId, request.sessionId, request.newMessage, runConfig, request.stateDelta); + request.userId, + request.sessionId, + request.getNewMessage(), + runConfig, + request.stateDelta); List events = Lists.newArrayList(eventStream.blockingIterable()); log.info("Agent run for session {} generated {} events.", request.sessionId, events.size()); @@ -155,7 +159,7 @@ public SseEmitter agentRunSse(@RequestBody AgentRunRequest request) { runner.runAsync( request.userId, request.sessionId, - request.newMessage, + request.getNewMessage(), runConfig, request.stateDelta); @@ -167,7 +171,7 @@ public SseEmitter agentRunSse(@RequestBody AgentRunRequest request) { try { log.debug( "SseEmitter: Sending event {} for session {}", event.id(), sessionId); - emitter.send(SseEmitter.event().data(event.toJson())); + emitter.send(SseEmitter.event().data(event)); } catch (IOException e) { log.error( "SseEmitter: IOException sending event for session {}: {}", diff --git a/dev/src/main/java/com/google/adk/web/dto/AgentRunRequest.java b/dev/src/main/java/com/google/adk/web/dto/AgentRunRequest.java index 652de5aac..025aa64eb 100644 --- a/dev/src/main/java/com/google/adk/web/dto/AgentRunRequest.java +++ b/dev/src/main/java/com/google/adk/web/dto/AgentRunRequest.java @@ -17,6 +17,7 @@ package com.google.adk.web.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.adk.JsonBaseModel; import com.google.genai.types.Content; import java.util.Map; import javax.annotation.Nullable; @@ -36,7 +37,7 @@ public class AgentRunRequest { public String sessionId; @JsonProperty("newMessage") - public Content newMessage; + public Object newMessage; @JsonProperty("streaming") public boolean streaming = false; @@ -65,7 +66,17 @@ public String getSessionId() { } public Content getNewMessage() { - return newMessage; + if (newMessage instanceof Content) { + return (Content) newMessage; + } + if (newMessage != null) { + try { + return JsonBaseModel.getMapper().convertValue(newMessage, Content.class); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Failed to parse newMessage into Content", e); + } + } + return null; } public boolean getStreaming() { From dc1a192a81a92870aa5a4af27a9dc90e81cdaf67 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Thu, 5 Mar 2026 06:06:56 -0800 Subject: [PATCH 45/61] feat: Adding plugin(Plugin... p) helper methods on App and Runner builders PiperOrigin-RevId: 879024972 --- core/src/main/java/com/google/adk/apps/App.java | 6 ++++++ .../com/google/adk/plugins/PluginManager.java | 17 +++++++++++++++++ .../main/java/com/google/adk/runner/Runner.java | 7 +++++++ .../java/com/google/adk/runner/RunnerTest.java | 8 ++++++++ 4 files changed, 38 insertions(+) diff --git a/core/src/main/java/com/google/adk/apps/App.java b/core/src/main/java/com/google/adk/apps/App.java index 18e8753c7..897e24490 100644 --- a/core/src/main/java/com/google/adk/apps/App.java +++ b/core/src/main/java/com/google/adk/apps/App.java @@ -104,6 +104,12 @@ public Builder plugins(List plugins) { return this; } + @CanIgnoreReturnValue + public Builder plugins(Plugin... plugins) { + this.plugins = ImmutableList.copyOf(plugins); + return this; + } + @CanIgnoreReturnValue public Builder eventsCompactionConfig(EventsCompactionConfig eventsCompactionConfig) { this.eventsCompactionConfig = eventsCompactionConfig; diff --git a/core/src/main/java/com/google/adk/plugins/PluginManager.java b/core/src/main/java/com/google/adk/plugins/PluginManager.java index a63d9a402..56dea936a 100644 --- a/core/src/main/java/com/google/adk/plugins/PluginManager.java +++ b/core/src/main/java/com/google/adk/plugins/PluginManager.java @@ -23,6 +23,8 @@ import com.google.adk.models.LlmResponse; import com.google.adk.tools.BaseTool; import com.google.adk.tools.ToolContext; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import com.google.genai.types.Content; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Flowable; @@ -81,6 +83,21 @@ public Optional getPlugin(String pluginName) { return plugins.stream().filter(p -> p.getName().equals(pluginName)).findFirst(); } + /** + * Returns the list of registered plugins. + * + *

This method is intended for testing purposes only. + * + *

Note that it returns a copy of the plugins list to prevent modification of the original + * list. + * + * @return The list of registered plugins. + */ + @VisibleForTesting + public List getPlugins() { + return ImmutableList.copyOf(plugins); + } + // --- Callback Runners --- public Maybe runOnUserMessageCallback( diff --git a/core/src/main/java/com/google/adk/runner/Runner.java b/core/src/main/java/com/google/adk/runner/Runner.java index ea7cb80f6..58fc27f47 100644 --- a/core/src/main/java/com/google/adk/runner/Runner.java +++ b/core/src/main/java/com/google/adk/runner/Runner.java @@ -132,6 +132,13 @@ public Builder plugins(List plugins) { return this; } + @CanIgnoreReturnValue + public Builder plugins(Plugin... plugins) { + Preconditions.checkState(this.app == null, "plugins() cannot be called when app is set."); + this.plugins = ImmutableList.copyOf(plugins); + return this; + } + public Runner build() { BaseAgent buildAgent; String buildAppName; diff --git a/core/src/test/java/com/google/adk/runner/RunnerTest.java b/core/src/test/java/com/google/adk/runner/RunnerTest.java index 42452c6a0..8a0a84b08 100644 --- a/core/src/test/java/com/google/adk/runner/RunnerTest.java +++ b/core/src/test/java/com/google/adk/runner/RunnerTest.java @@ -1188,6 +1188,14 @@ public void close_closesPluginsAndCodeExecutors() { verify(plugin).close(); } + @Test + public void buildRunnerWithPlugins_success() { + BasePlugin plugin1 = mockPlugin("test1"); + BasePlugin plugin2 = mockPlugin("test2"); + Runner runner = Runner.builder().agent(agent).appName("test").plugins(plugin1, plugin2).build(); + assertThat(runner.pluginManager().getPlugins()).containsExactly(plugin1, plugin2); + } + public static class Tools { private Tools() {} From 401c25d50705a1b263c785308966f8eed54c8310 Mon Sep 17 00:00:00 2001 From: ddobrin Date: Thu, 5 Mar 2026 10:41:55 -0500 Subject: [PATCH 46/61] refactor:Fixes #947 Update Spring Boot version , Spring AI version, organize imports --- contrib/spring-ai/pom.xml | 2 +- dev/pom.xml | 5 ++ .../com/google/adk/web/AdkWebServerTest.java | 2 +- .../google/adk/web/AdkWebServerUITest.java | 2 +- pom.xml | 71 ++++++++++--------- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index 64346b1c3..e07deec9e 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -29,7 +29,7 @@ Spring AI integration for the Agent Development Kit. - 1.1.0 + 2.0.0-M2 1.21.3 diff --git a/dev/pom.xml b/dev/pom.xml index 8595995ed..6a0a03467 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -63,6 +63,11 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test + com.google.truth truth diff --git a/dev/src/test/java/com/google/adk/web/AdkWebServerTest.java b/dev/src/test/java/com/google/adk/web/AdkWebServerTest.java index 71aaca180..275001200 100644 --- a/dev/src/test/java/com/google/adk/web/AdkWebServerTest.java +++ b/dev/src/test/java/com/google/adk/web/AdkWebServerTest.java @@ -26,8 +26,8 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; diff --git a/dev/src/test/java/com/google/adk/web/AdkWebServerUITest.java b/dev/src/test/java/com/google/adk/web/AdkWebServerUITest.java index f4287f786..cfb162db2 100644 --- a/dev/src/test/java/com/google/adk/web/AdkWebServerUITest.java +++ b/dev/src/test/java/com/google/adk/web/AdkWebServerUITest.java @@ -24,8 +24,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.test.web.servlet.MockMvc; /** diff --git a/pom.xml b/pom.xml index af8a1f2b1..34bef7755 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 3.6.0 1.11.1 - 3.4.1 + 4.0.2 @@ -56,7 +56,7 @@ 5.11.4 5.20.0 1.6.0 - 2.19.0 + 2.20.2 5.3.2 3.7.0 0.18.1 @@ -78,6 +78,13 @@ + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + com.google.cloud libraries-bom @@ -162,36 +169,36 @@ error_prone_annotations ${errorprone.version} - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - ${jackson.version} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.google.protobuf protobuf-java From d2f11456c3a99edd43b3dc0d04743ae7e9390ded Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Thu, 5 Mar 2026 08:00:32 -0800 Subject: [PATCH 47/61] feat!: remove deprecated BaseToolset.isToolSelected method PiperOrigin-RevId: 879069821 --- .../main/java/com/google/adk/tools/BaseToolset.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/src/main/java/com/google/adk/tools/BaseToolset.java b/core/src/main/java/com/google/adk/tools/BaseToolset.java index c8ed6df4e..40167aa75 100644 --- a/core/src/main/java/com/google/adk/tools/BaseToolset.java +++ b/core/src/main/java/com/google/adk/tools/BaseToolset.java @@ -19,7 +19,6 @@ import com.google.adk.agents.ReadonlyContext; import io.reactivex.rxjava3.core.Flowable; import java.util.List; -import java.util.Optional; import javax.annotation.Nullable; /** Base interface for toolsets. */ @@ -66,13 +65,4 @@ default boolean isToolSelected( return false; } - - /** - * @deprecated Use {@link #isToolSelected(BaseTool, Object, ReadonlyContext)} instead. - */ - @Deprecated - default boolean isToolSelected( - BaseTool tool, Optional toolFilter, Optional readonlyContext) { - return isToolSelected(tool, toolFilter.orElse(null), readonlyContext.orElse(null)); - } } From 88153c833697a9b9c6ec735a69f48a92cbdfc54b Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Thu, 5 Mar 2026 08:08:17 -0800 Subject: [PATCH 48/61] feat!: remove deprecated methods accepting Optional params in InvocationContext PiperOrigin-RevId: 879073419 --- .../google/adk/agents/InvocationContext.java | 104 ----------------- .../adk/agents/InvocationContextTest.java | 108 +----------------- 2 files changed, 3 insertions(+), 209 deletions(-) diff --git a/core/src/main/java/com/google/adk/agents/InvocationContext.java b/core/src/main/java/com/google/adk/agents/InvocationContext.java index 89c90b9fe..7602ca9f2 100644 --- a/core/src/main/java/com/google/adk/agents/InvocationContext.java +++ b/core/src/main/java/com/google/adk/agents/InvocationContext.java @@ -78,70 +78,6 @@ protected InvocationContext(Builder builder) { this.callbackContextData = new ConcurrentHashMap<>(builder.callbackContextData); } - /** - * @deprecated Use {@link #builder()} instead. - */ - @Deprecated(forRemoval = true) - public InvocationContext( - BaseSessionService sessionService, - BaseArtifactService artifactService, - BaseMemoryService memoryService, - Plugin pluginManager, - Optional liveRequestQueue, - Optional branch, - String invocationId, - BaseAgent agent, - Session session, - Optional userContent, - RunConfig runConfig, - boolean endInvocation) { - this( - builder() - .sessionService(sessionService) - .artifactService(artifactService) - .memoryService(memoryService) - .pluginManager(pluginManager) - .liveRequestQueue(liveRequestQueue) - .branch(branch) - .invocationId(invocationId) - .agent(agent) - .session(session) - .userContent(userContent) - .runConfig(runConfig) - .endInvocation(endInvocation)); - } - - /** - * @deprecated Use {@link #builder()} instead. - */ - @Deprecated(forRemoval = true) - public InvocationContext( - BaseSessionService sessionService, - BaseArtifactService artifactService, - BaseMemoryService memoryService, - Optional liveRequestQueue, - Optional branch, - String invocationId, - BaseAgent agent, - Session session, - Optional userContent, - RunConfig runConfig, - boolean endInvocation) { - this( - builder() - .sessionService(sessionService) - .artifactService(artifactService) - .memoryService(memoryService) - .liveRequestQueue(liveRequestQueue) - .branch(branch) - .invocationId(invocationId) - .agent(agent) - .session(session) - .userContent(userContent) - .runConfig(runConfig) - .endInvocation(endInvocation)); - } - /** * @deprecated Use {@link #builder()} instead. */ @@ -478,21 +414,6 @@ public Builder pluginManager(Plugin pluginManager) { return this; } - /** - * Sets the queue for managing live requests. - * - * @param liveRequestQueue the queue for managing live requests. - * @return this builder instance for chaining. - * @deprecated Use {@link #liveRequestQueue(LiveRequestQueue)} instead. - */ - // TODO: b/462140921 - Builders should not accept Optional parameters. - @Deprecated(forRemoval = true) - @CanIgnoreReturnValue - public Builder liveRequestQueue(Optional liveRequestQueue) { - this.liveRequestQueue = liveRequestQueue.orElse(null); - return this; - } - /** * Sets the queue for managing live requests. * @@ -505,21 +426,6 @@ public Builder liveRequestQueue(@Nullable LiveRequestQueue liveRequestQueue) { return this; } - /** - * Sets the branch ID for the invocation. - * - * @param branch the branch ID for the invocation. - * @return this builder instance for chaining. - * @deprecated Use {@link #branch(String)} instead. - */ - // TODO: b/462140921 - Builders should not accept Optional parameters. - @Deprecated(forRemoval = true) - @CanIgnoreReturnValue - public Builder branch(Optional branch) { - this.branch = branch.orElse(null); - return this; - } - /** * Sets the branch ID for the invocation. * @@ -568,16 +474,6 @@ public Builder session(Session session) { return this; } - /** - * @deprecated Use {@link #userContent(Content)} instead. - */ - @CanIgnoreReturnValue - @Deprecated - public Builder userContent(Optional userContent) { - this.userContent = userContent.orElse(null); - return this; - } - /** * Sets the user content that triggered this invocation. * diff --git a/core/src/test/java/com/google/adk/agents/InvocationContextTest.java b/core/src/test/java/com/google/adk/agents/InvocationContextTest.java index 0237261c5..e588a38ca 100644 --- a/core/src/test/java/com/google/adk/agents/InvocationContextTest.java +++ b/core/src/test/java/com/google/adk/agents/InvocationContextTest.java @@ -31,7 +31,6 @@ import com.google.genai.types.Content; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import org.junit.Assert; import org.junit.Before; @@ -607,45 +606,6 @@ public void testBranch() { assertThat(context.branch()).isEmpty(); } - @Test - // Testing deprecated methods. - public void testDeprecatedCreateMethods() { - InvocationContext context1 = - InvocationContext.builder() - .sessionService(mockSessionService) - .artifactService(mockArtifactService) - .invocationId(testInvocationId) - .agent(mockAgent) - .session(session) - .userContent(Optional.ofNullable(userContent)) - .runConfig(runConfig) - .build(); - - assertThat(context1.sessionService()).isEqualTo(mockSessionService); - assertThat(context1.artifactService()).isEqualTo(mockArtifactService); - assertThat(context1.invocationId()).isEqualTo(testInvocationId); - assertThat(context1.agent()).isEqualTo(mockAgent); - assertThat(context1.session()).isEqualTo(session); - assertThat(context1.userContent()).hasValue(userContent); - assertThat(context1.runConfig()).isEqualTo(runConfig); - - InvocationContext context2 = - InvocationContext.create( - mockSessionService, - mockArtifactService, - mockAgent, - session, - liveRequestQueue, - runConfig); - - assertThat(context2.sessionService()).isEqualTo(mockSessionService); - assertThat(context2.artifactService()).isEqualTo(mockArtifactService); - assertThat(context2.agent()).isEqualTo(mockAgent); - assertThat(context2.session()).isEqualTo(session); - assertThat(context2.liveRequestQueue()).hasValue(liveRequestQueue); - assertThat(context2.runConfig()).isEqualTo(runConfig); - } - @Test public void testActiveStreamingTools() { InvocationContext context = @@ -686,9 +646,9 @@ public void testBuilderOptionalParameters() { .artifactService(mockArtifactService) .agent(mockAgent) .session(session) - .liveRequestQueue(Optional.of(liveRequestQueue)) - .branch(Optional.of("test-branch")) - .userContent(Optional.of(userContent)) + .liveRequestQueue(liveRequestQueue) + .branch("test-branch") + .userContent(userContent) .build(); assertThat(context.liveRequestQueue()).hasValue(liveRequestQueue); @@ -696,68 +656,6 @@ public void testBuilderOptionalParameters() { assertThat(context.userContent()).hasValue(userContent); } - @Test - // Testing deprecated methods. - public void testDeprecatedConstructor() { - InvocationContext context = - new InvocationContext( - mockSessionService, - mockArtifactService, - mockMemoryService, - pluginManager, - Optional.of(liveRequestQueue), - Optional.of("test-branch"), - testInvocationId, - mockAgent, - session, - Optional.of(userContent), - runConfig, - true); - - assertThat(context.sessionService()).isEqualTo(mockSessionService); - assertThat(context.artifactService()).isEqualTo(mockArtifactService); - assertThat(context.memoryService()).isEqualTo(mockMemoryService); - assertThat(context.pluginManager()).isEqualTo(pluginManager); - assertThat(context.liveRequestQueue()).hasValue(liveRequestQueue); - assertThat(context.branch()).hasValue("test-branch"); - assertThat(context.invocationId()).isEqualTo(testInvocationId); - assertThat(context.agent()).isEqualTo(mockAgent); - assertThat(context.session()).isEqualTo(session); - assertThat(context.userContent()).hasValue(userContent); - assertThat(context.runConfig()).isEqualTo(runConfig); - assertThat(context.endInvocation()).isTrue(); - } - - @Test - // Testing deprecated methods. - public void testDeprecatedConstructor_11params() { - InvocationContext context = - new InvocationContext( - mockSessionService, - mockArtifactService, - mockMemoryService, - Optional.of(liveRequestQueue), - Optional.of("test-branch"), - testInvocationId, - mockAgent, - session, - Optional.of(userContent), - runConfig, - true); - - assertThat(context.sessionService()).isEqualTo(mockSessionService); - assertThat(context.artifactService()).isEqualTo(mockArtifactService); - assertThat(context.memoryService()).isEqualTo(mockMemoryService); - assertThat(context.liveRequestQueue()).hasValue(liveRequestQueue); - assertThat(context.branch()).hasValue("test-branch"); - assertThat(context.invocationId()).isEqualTo(testInvocationId); - assertThat(context.agent()).isEqualTo(mockAgent); - assertThat(context.session()).isEqualTo(session); - assertThat(context.userContent()).hasValue(userContent); - assertThat(context.runConfig()).isEqualTo(runConfig); - assertThat(context.endInvocation()).isTrue(); - } - @Test public void build_missingInvocationId_null_throwsException() { InvocationContext.Builder builder = From c7d2a41f812e96cfe8b68f6cf2b0bff6fa892770 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 00:15:30 -0800 Subject: [PATCH 49/61] chore: switch release please secret to use adk-java-releases-bot's token PiperOrigin-RevId: 879472602 --- .github/workflows/release-please.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml index 6d3142907..258cf90db 100644 --- a/.github/workflows/release-please.yaml +++ b/.github/workflows/release-please.yaml @@ -14,4 +14,4 @@ jobs: steps: - uses: googleapis/release-please-action@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} From b8eef9e5d8323bd833bf3faf18b9b901fc6c5fe7 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 02:26:16 -0800 Subject: [PATCH 50/61] refactor: convert Optional to @Nullable param in Runner PiperOrigin-RevId: 879517819 --- core/src/main/java/com/google/adk/runner/Runner.java | 11 ++++++----- .../adk/runner/InputAudioTranscriptionTest.java | 6 ++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/com/google/adk/runner/Runner.java b/core/src/main/java/com/google/adk/runner/Runner.java index 58fc27f47..f2cb5b9d5 100644 --- a/core/src/main/java/com/google/adk/runner/Runner.java +++ b/core/src/main/java/com/google/adk/runner/Runner.java @@ -591,9 +591,9 @@ private void copySessionStates(Session source, Session target) { * @return invocation context configured for a live run. */ private InvocationContext newInvocationContextForLive( - Session session, Optional liveRequestQueue, RunConfig runConfig) { + Session session, @Nullable LiveRequestQueue liveRequestQueue, RunConfig runConfig) { RunConfig.Builder runConfigBuilder = RunConfig.builder(runConfig); - if (liveRequestQueue.isPresent()) { + if (liveRequestQueue != null) { // Default to AUDIO modality if not specified. if (CollectionUtils.isNullOrEmpty(runConfig.responseModalities())) { runConfigBuilder.setResponseModalities( @@ -614,8 +614,9 @@ private InvocationContext newInvocationContextForLive( InvocationContext.Builder builder = newInvocationContextBuilder(session) .runConfig(runConfigBuilder.build()) - .userContent(Content.fromParts()); - liveRequestQueue.ifPresent(builder::liveRequestQueue); + .userContent(Content.fromParts()) + .liveRequestQueue(liveRequestQueue); + return builder.build(); } @@ -643,7 +644,7 @@ public Flowable runLive( return Flowable.defer( () -> { InvocationContext invocationContext = - newInvocationContextForLive(session, Optional.of(liveRequestQueue), runConfig); + newInvocationContextForLive(session, liveRequestQueue, runConfig); Single invocationContextSingle; if (invocationContext.agent() instanceof LlmAgent agent) { diff --git a/core/src/test/java/com/google/adk/runner/InputAudioTranscriptionTest.java b/core/src/test/java/com/google/adk/runner/InputAudioTranscriptionTest.java index 55d41916f..95a016e34 100644 --- a/core/src/test/java/com/google/adk/runner/InputAudioTranscriptionTest.java +++ b/core/src/test/java/com/google/adk/runner/InputAudioTranscriptionTest.java @@ -33,7 +33,6 @@ import com.google.genai.types.Modality; import com.google.genai.types.Part; import java.lang.reflect.Method; -import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -50,10 +49,9 @@ private InvocationContext invokeNewInvocationContextForLive( throws Exception { Method method = Runner.class.getDeclaredMethod( - "newInvocationContextForLive", Session.class, Optional.class, RunConfig.class); + "newInvocationContextForLive", Session.class, LiveRequestQueue.class, RunConfig.class); method.setAccessible(true); - return (InvocationContext) - method.invoke(runner, session, Optional.of(liveRequestQueue), runConfig); + return (InvocationContext) method.invoke(runner, session, liveRequestQueue, runConfig); } @Test From 84c62a48ef7b62641722824fe5ba1200606b7b17 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 02:27:41 -0800 Subject: [PATCH 51/61] feat!: remove methods with Optional params from LiveRequest.Builder PiperOrigin-RevId: 879518204 --- core/src/main/java/com/google/adk/agents/LiveRequest.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/src/main/java/com/google/adk/agents/LiveRequest.java b/core/src/main/java/com/google/adk/agents/LiveRequest.java index 9c66f038b..df1ada6a5 100644 --- a/core/src/main/java/com/google/adk/agents/LiveRequest.java +++ b/core/src/main/java/com/google/adk/agents/LiveRequest.java @@ -77,18 +77,12 @@ public abstract static class Builder { @JsonProperty("content") public abstract Builder content(@Nullable Content content); - public abstract Builder content(Optional content); - @JsonProperty("blob") public abstract Builder blob(@Nullable Blob blob); - public abstract Builder blob(Optional blob); - @JsonProperty("close") public abstract Builder close(@Nullable Boolean close); - public abstract Builder close(Optional close); - abstract LiveRequest autoBuild(); public final LiveRequest build() { From e25e3357cd711e4e14deba4a870d076c976a8bfb Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 6 Mar 2026 03:36:26 -0800 Subject: [PATCH 52/61] refactor: remove unused converters code in the A2A; reduce Optional usage in A2A converters PiperOrigin-RevId: 879543755 --- .../common/GenAiFieldMissingException.java | 12 + .../converters/A2ADataPartMetadataType.java | 19 ++ .../converters/ConversationPreprocessor.java | 108 --------- .../adk/a2a/converters/EventConverter.java | 93 ++----- .../adk/a2a/converters/PartConverter.java | 229 ++++++++++-------- .../adk/a2a/converters/RequestConverter.java | 198 --------------- .../adk/a2a/converters/ResponseConverter.java | 150 +----------- .../adk/a2a/executor/AgentExecutor.java | 12 +- .../a2a/converters/EventConverterTest.java | 12 +- .../adk/a2a/converters/PartConverterTest.java | 130 ++++------ .../a2a/converters/ResponseConverterTest.java | 120 --------- 11 files changed, 236 insertions(+), 847 deletions(-) create mode 100644 a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java create mode 100644 a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java delete mode 100644 a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java delete mode 100644 a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java diff --git a/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java b/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java new file mode 100644 index 000000000..a5947dcb8 --- /dev/null +++ b/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java @@ -0,0 +1,12 @@ +package com.google.adk.a2a.common; + +/** Exception thrown when the the genai class has an empty field. */ +public class GenAiFieldMissingException extends RuntimeException { + public GenAiFieldMissingException(String message) { + super(message); + } + + public GenAiFieldMissingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java b/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java new file mode 100644 index 000000000..b5b53c49a --- /dev/null +++ b/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java @@ -0,0 +1,19 @@ +package com.google.adk.a2a.converters; + +/** Enum for the type of A2A DataPart metadata. */ +public enum A2ADataPartMetadataType { + FUNCTION_RESPONSE("function_response"), + FUNCTION_CALL("function_call"), + CODE_EXECUTION_RESULT("code_execution_result"), + EXECUTABLE_CODE("executable_code"); + + private final String type; + + private A2ADataPartMetadataType(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java b/a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java deleted file mode 100644 index 11bbbd326..000000000 --- a/a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.google.adk.a2a.converters; - -import com.google.adk.events.Event; -import com.google.common.collect.ImmutableList; -import com.google.genai.types.Content; -import com.google.genai.types.Part; -import java.util.List; -import java.util.Optional; - -/** - * Preprocesses a batch of ADK events prior to invoking a remote A2A agent. - * - *

The class splits the conversation into two logical buckets: - * - *

    - *
  • The historical session events that should be preserved as-is when relayed over the wire. - *
  • The most recent user-authored text event, surfaced separately so it can be supplied as the - * pending user input on the {@link com.google.adk.agents.InvocationContext}. - *
- * - *

This mirrors the Python A2A implementation where the in-flight user message is maintained - * separately from the persisted transcript. - * - *

**EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not - * use in production code. - */ -public final class ConversationPreprocessor { - - /** - * Immutable value that surfaces the results of preprocessing. - * - *

All fields are deliberately exposed to avoid additional AutoValue dependencies in this - * internal module. - */ - public static final class PreparedInput { - /** Historical events that should remain in the session transcript. */ - public final ImmutableList historyEvents; - - /** Extracted user message content, if a qualifying text event was found. */ - public final Optional userContent; - - /** The concrete event that supplied {@link #userContent}, for callers needing metadata. */ - public final Optional userEvent; - - /** - * Creates a new instance. - * - * @param historyEvents ordered historical events retained in the session stream - * @param userContent optional content to place on the pending user message - * @param userEvent optional original event that contained {@code userContent} - */ - public PreparedInput( - ImmutableList historyEvents, - Optional userContent, - Optional userEvent) { - this.historyEvents = historyEvents; - this.userContent = userContent; - this.userEvent = userEvent; - } - } - - private ConversationPreprocessor() {} - - /** - * Splits the provided event list into history and the latest user-authored text message. - * - * @param inputEvents ordered session events, oldest to newest; may be {@code null} - * @return container encapsulating the derived history, optional user content, and the original - * user event when present - */ - public static PreparedInput extractHistoryAndUserContent(List inputEvents) { - if (inputEvents == null || inputEvents.isEmpty()) { - return new PreparedInput(ImmutableList.of(), Optional.empty(), Optional.empty()); - } - - Content userContent = null; - int lastTextIndex = -1; - Event userEvent = null; - for (int i = inputEvents.size() - 1; i >= 0; i--) { - Event ev = inputEvents.get(i); - if (ev.content().isPresent() && ev.content().get().parts().isPresent()) { - boolean hasText = false; - for (Part p : ev.content().get().parts().get()) { - if (p.text().isPresent()) { - hasText = true; - break; - } - } - if (hasText) { - userContent = ev.content().get(); - lastTextIndex = i; - userEvent = ev; - break; - } - } - } - - ImmutableList.Builder historyBuilder = ImmutableList.builder(); - for (int i = 0; i < inputEvents.size(); i++) { - if (i != lastTextIndex) { - historyBuilder.add(inputEvents.get(i)); - } - } - - return new PreparedInput( - historyBuilder.build(), Optional.ofNullable(userContent), Optional.ofNullable(userEvent)); - } -} diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java index f5b1178c0..1a49b0070 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java @@ -3,14 +3,11 @@ import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.adk.agents.InvocationContext; -import com.google.adk.events.Event; import com.google.common.collect.ImmutableList; import com.google.genai.types.Content; -import com.google.genai.types.Part; import io.a2a.spec.Message; -import io.a2a.spec.TextPart; -import java.util.ArrayList; -import java.util.List; +import io.a2a.spec.Part; +import java.util.Collection; import java.util.Optional; import java.util.UUID; import org.slf4j.Logger; @@ -28,47 +25,35 @@ public final class EventConverter { private EventConverter() {} /** - * Aggregation mode for converting events to A2A messages. + * Converts an ADK InvocationContext to an A2A Message. * - *

AS_IS: Parts are aggregated as-is. + *

It combines all the events in the session, plus the user content, converted into A2A Parts, + * into a single A2A Message. * - *

EXTERNAL_HANDOFF: Parts are aggregated as-is, except for function responses, which are - * converted to text parts with the function name and response map. + *

If the context has no events, or no suitable content to build the message, an empty optional + * is returned. + * + * @param context The ADK InvocationContext to convert. + * @return The converted A2A Message. */ - public enum AggregationMode { - AS_IS, - EXTERNAL_HANDOFF - } - - public static ImmutableList> contentToParts(Optional content) { - if (content.isPresent() && content.get().parts().isPresent()) { - return content.get().parts().get().stream() - .map(PartConverter::fromGenaiPart) - .flatMap(Optional::stream) - .collect(toImmutableList()); - } - return ImmutableList.of(); - } - public static Optional convertEventsToA2AMessage(InvocationContext context) { - return convertEventsToA2AMessage(context, AggregationMode.AS_IS); - } - - public static Optional convertEventsToA2AMessage( - InvocationContext context, AggregationMode mode) { if (context.session().events().isEmpty()) { logger.warn("No events in session, cannot convert to A2A message."); return Optional.empty(); } - List> parts = new ArrayList<>(); - for (Event event : context.session().events()) { - appendContentParts(event.content(), mode, parts); - } + ImmutableList.Builder> partsBuilder = ImmutableList.builder(); context - .userContent() - .ifPresent(content -> appendContentParts(Optional.of(content), mode, parts)); + .session() + .events() + .forEach( + event -> + partsBuilder.addAll( + contentToParts(event.content(), event.partial().orElse(false)))); + partsBuilder.addAll(contentToParts(context.userContent(), false)); + + ImmutableList> parts = partsBuilder.build(); if (parts.isEmpty()) { logger.warn("No suitable content found to build A2A request message."); @@ -83,37 +68,11 @@ public static Optional convertEventsToA2AMessage( .build()); } - private static void appendContentParts( - Optional contentOpt, AggregationMode mode, List> target) { - if (contentOpt.isEmpty() || contentOpt.get().parts().isEmpty()) { - return; - } - - for (Part part : contentOpt.get().parts().get()) { - if (part.text().isPresent()) { - target.add(new TextPart(part.text().get())); - continue; - } - - if (part.functionCall().isPresent()) { - if (mode == AggregationMode.AS_IS) { - PartConverter.convertGenaiPartToA2aPart(part).ifPresent(target::add); - } - continue; - } - - if (part.functionResponse().isPresent()) { - if (mode == AggregationMode.AS_IS) { - PartConverter.convertGenaiPartToA2aPart(part).ifPresent(target::add); - } else { - String name = part.functionResponse().get().name().orElse(""); - String mapStr = String.valueOf(part.functionResponse().get().response().orElse(null)); - target.add(new TextPart(String.format("%s response: %s", name, mapStr))); - } - continue; - } - - PartConverter.fromGenaiPart(part).ifPresent(target::add); - } + public static ImmutableList> contentToParts( + Optional content, boolean isPartial) { + return content.flatMap(Content::parts).stream() + .flatMap(Collection::stream) + .map(part -> PartConverter.fromGenaiPart(part, isPartial)) + .collect(toImmutableList()); } } diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java index 8e407406f..05125d170 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.adk.a2a.common.GenAiFieldMissingException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.genai.types.Blob; @@ -38,18 +39,14 @@ * use in production code. */ public final class PartConverter { + private static final Logger logger = LoggerFactory.getLogger(PartConverter.class); private static final ObjectMapper objectMapper = new ObjectMapper(); - // Constants for metadata types. By convention metadata keys are prefixed with "adk_" to align // with the Python and Golang libraries. public static final String A2A_DATA_PART_METADATA_TYPE_KEY = "adk_type"; public static final String A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY = "adk_is_long_running"; - public static final String A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL = "function_call"; - public static final String A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE = "function_response"; - public static final String A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT = - "code_execution_result"; - public static final String A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = "executable_code"; + public static final String A2A_DATA_PART_METADATA_IS_PARTIAL_KEY = "adk_partial"; public static final String LANGUAGE_KEY = "language"; public static final String OUTCOME_KEY = "outcome"; public static final String CODE_KEY = "code"; @@ -58,6 +55,10 @@ public final class PartConverter { public static final String ARGS_KEY = "args"; public static final String RESPONSE_KEY = "response"; public static final String ID_KEY = "id"; + public static final String WILL_CONTINUE_KEY = "willContinue"; + public static final String PARTIAL_ARGS_KEY = "partialArgs"; + public static final String SCHEDULING_KEY = "scheduling"; + public static final String PARTS_KEY = "parts"; public static Optional toTextPart(io.a2a.spec.Part part) { if (part instanceof TextPart textPart) { @@ -96,34 +97,6 @@ public static ImmutableList toGenaiParts( .collect(toImmutableList()); } - /** - * Convert a Google GenAI Part to an A2A Part. - * - * @param part The GenAI part to convert. - * @return Optional containing the converted A2A Part, or empty if conversion fails. - */ - public static Optional convertGenaiPartToA2aPart(Part part) { - if (part == null) { - return Optional.empty(); - } - - if (part.text().isPresent()) { - // Text parts are handled directly in the Message content, not as DataPart - return Optional.empty(); - } else if (part.functionCall().isPresent()) { - return createDataPartFromFunctionCall(part.functionCall().get()); - } else if (part.functionResponse().isPresent()) { - return createDataPartFromFunctionResponse(part.functionResponse().get()); - } else if (part.executableCode().isPresent()) { - return createDataPartFromExecutableCode(part.executableCode().get()); - } else if (part.codeExecutionResult().isPresent()) { - return createDataPartFromCodeExecutionResult(part.codeExecutionResult().get()); - } - - logger.warn("Cannot convert unsupported part for Google GenAI part: " + part); - return Optional.empty(); - } - private static Optional convertFilePartToGenAiPart( FilePart filePart) { FileContent fileContent = filePart.getFile(); @@ -170,7 +143,7 @@ private static Optional convertDataPartToGenAiPart( String metadataType = metadata.getOrDefault(A2A_DATA_PART_METADATA_TYPE_KEY, "").toString(); if ((data.containsKey(NAME_KEY) && data.containsKey(ARGS_KEY)) - || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)) { + || metadataType.equals(A2ADataPartMetadataType.FUNCTION_CALL.getType())) { String functionName = String.valueOf(data.getOrDefault(NAME_KEY, null)); String functionId = String.valueOf(data.getOrDefault(ID_KEY, null)); Map args = coerceToMap(data.get(ARGS_KEY)); @@ -182,7 +155,7 @@ private static Optional convertDataPartToGenAiPart( } if ((data.containsKey(NAME_KEY) && data.containsKey(RESPONSE_KEY)) - || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)) { + || metadataType.equals(A2ADataPartMetadataType.FUNCTION_RESPONSE.getType())) { String functionName = String.valueOf(data.getOrDefault(NAME_KEY, "")); String functionId = String.valueOf(data.getOrDefault(ID_KEY, "")); Map response = coerceToMap(data.get(RESPONSE_KEY)); @@ -198,7 +171,7 @@ private static Optional convertDataPartToGenAiPart( } if ((data.containsKey(CODE_KEY) && data.containsKey(LANGUAGE_KEY)) - || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE)) { + || metadataType.equals(A2ADataPartMetadataType.EXECUTABLE_CODE.getType())) { String code = String.valueOf(data.getOrDefault(CODE_KEY, "")); String language = String.valueOf( @@ -212,7 +185,7 @@ private static Optional convertDataPartToGenAiPart( } if ((data.containsKey(OUTCOME_KEY) && data.containsKey(OUTPUT_KEY)) - || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT)) { + || metadataType.equals(A2ADataPartMetadataType.CODE_EXECUTION_RESULT.getType())) { String outcome = String.valueOf(data.getOrDefault(OUTCOME_KEY, Outcome.Known.OUTCOME_OK).toString()); String output = String.valueOf(data.getOrDefault(OUTPUT_KEY, "")); @@ -251,112 +224,154 @@ public static Content messageToContent(Message message) { * * @return Optional containing the converted A2A Part, or empty if conversion fails. */ - private static Optional createDataPartFromFunctionCall(FunctionCall functionCall) { - Map data = new HashMap<>(); - data.put(NAME_KEY, functionCall.name().orElse("")); - data.put(ID_KEY, functionCall.id().orElse("")); - data.put(ARGS_KEY, functionCall.args().orElse(ImmutableMap.of())); - - ImmutableMap metadata = - ImmutableMap.of(A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); + private static DataPart createDataPartFromFunctionCall( + FunctionCall functionCall, ImmutableMap.Builder metadata) { + ImmutableMap.Builder data = ImmutableMap.builder(); + addValueIfPresent(data, NAME_KEY, functionCall.name()); + addValueIfPresent(data, ID_KEY, functionCall.id()); + addValueIfPresent(data, ARGS_KEY, functionCall.args()); + addValueIfPresent(data, WILL_CONTINUE_KEY, functionCall.willContinue()); + addValueIfPresent(data, PARTIAL_ARGS_KEY, functionCall.partialArgs()); + + metadata.put(A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.FUNCTION_CALL.getType()); + + return new DataPart(data.buildOrThrow(), metadata.buildOrThrow()); + } - return Optional.of(new DataPart(data, metadata)); + private static void addValueIfPresent( + ImmutableMap.Builder data, String key, Optional value) { + value.ifPresent(v -> data.put(key, v)); } /** * Creates an A2A DataPart from a Google GenAI FunctionResponse. * * @param functionResponse The GenAI FunctionResponse to convert. - * @return Optional containing the converted A2A Part, or empty if conversion fails. + * @return The converted A2A Part. */ - private static Optional createDataPartFromFunctionResponse( - FunctionResponse functionResponse) { - Map data = new HashMap<>(); - data.put(NAME_KEY, functionResponse.name().orElse("")); - data.put(ID_KEY, functionResponse.id().orElse("")); - data.put(RESPONSE_KEY, functionResponse.response().orElse(ImmutableMap.of())); - - ImmutableMap metadata = - ImmutableMap.of( - A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE); - - return Optional.of(new DataPart(data, metadata)); + private static DataPart createDataPartFromFunctionResponse( + FunctionResponse functionResponse, ImmutableMap.Builder metadata) { + ImmutableMap.Builder data = ImmutableMap.builder(); + addValueIfPresent(data, NAME_KEY, functionResponse.name()); + addValueIfPresent(data, ID_KEY, functionResponse.id()); + addValueIfPresent(data, RESPONSE_KEY, functionResponse.response()); + addValueIfPresent(data, WILL_CONTINUE_KEY, functionResponse.willContinue()); + addValueIfPresent(data, SCHEDULING_KEY, functionResponse.scheduling()); + addValueIfPresent(data, PARTS_KEY, functionResponse.parts()); + + metadata.put( + A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.FUNCTION_RESPONSE.getType()); + + return new DataPart(data.buildOrThrow(), metadata.buildOrThrow()); } - private static Optional createDataPartFromExecutableCode( - ExecutableCode executableCode) { - Map data = new HashMap<>(); - data.put(CODE_KEY, executableCode.code().orElse("")); + /** + * Creates an A2A DataPart from a Google GenAI CodeExecutionResult. + * + * @param codeExecutionResult The GenAI CodeExecutionResult to convert. + * @return The converted A2A Part. + */ + private static DataPart createDataPartFromCodeExecutionResult( + CodeExecutionResult codeExecutionResult, ImmutableMap.Builder metadata) { + ImmutableMap.Builder data = ImmutableMap.builder(); data.put( - LANGUAGE_KEY, - executableCode - .language() - .map(Language::toString) - .orElse(Language.Known.LANGUAGE_UNSPECIFIED.toString())); + OUTCOME_KEY, + codeExecutionResult + .outcome() + .map(Outcome::toString) + .orElse(new Outcome(Outcome.Known.OUTCOME_UNSPECIFIED).toString())); + addValueIfPresent(data, OUTPUT_KEY, codeExecutionResult.output()); - ImmutableMap metadata = - ImmutableMap.of( - A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE); + metadata.put( + A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.CODE_EXECUTION_RESULT.getType()); - return Optional.of(new DataPart(data, metadata)); + return new DataPart(data.buildOrThrow(), metadata.buildOrThrow()); } - private static Optional createDataPartFromCodeExecutionResult( - CodeExecutionResult result) { - Map data = new HashMap<>(); + /** + * Creates an A2A DataPart from a Google GenAI ExecutableCode. + * + * @param executableCode The GenAI ExecutableCode to convert. + * @return The converted A2A Part. + */ + private static DataPart createDataPartFromExecutableCode( + ExecutableCode executableCode, ImmutableMap.Builder metadata) { + ImmutableMap.Builder data = ImmutableMap.builder(); data.put( - OUTCOME_KEY, - result - .outcome() - .map(Outcome::toString) - .orElse(new Outcome(Outcome.Known.OUTCOME_UNSPECIFIED).toString())); - data.put(OUTPUT_KEY, result.output().orElse(null)); + LANGUAGE_KEY, + executableCode + .language() + .map(Language::toString) + .orElse(Language.Known.LANGUAGE_UNSPECIFIED.toString())); + addValueIfPresent(data, CODE_KEY, executableCode.code()); - ImmutableMap metadata = - ImmutableMap.of( - A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT); + metadata.put( + A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.EXECUTABLE_CODE.getType()); - return Optional.of(new DataPart(data, metadata)); + return new DataPart(data.buildOrThrow(), metadata.buildOrThrow()); } private PartConverter() {} /** Convert a GenAI part into the A2A JSON representation. */ - public static Optional> fromGenaiPart(Part part) { + public static io.a2a.spec.Part fromGenaiPart(Part part, boolean isPartial) { if (part == null) { - return Optional.empty(); + throw new GenAiFieldMissingException("GenAI part cannot be null"); } - - if (part.text().isPresent()) { - return Optional.of(new TextPart(part.text().get(), new HashMap<>())); + ImmutableMap.Builder metadata = ImmutableMap.builder(); + if (isPartial) { + metadata.put(A2A_DATA_PART_METADATA_IS_PARTIAL_KEY, true); } - if (part.fileData().isPresent()) { - FileData fileData = part.fileData().get(); - String uri = fileData.fileUri().orElse(null); - String mime = fileData.mimeType().orElse(null); - String name = fileData.displayName().orElse(null); - return Optional.of(new FilePart(new FileWithUri(mime, name, uri), new HashMap<>())); + if (part.text().isPresent()) { + addValueIfPresent(metadata, "thought", part.thought()); + return new TextPart(part.text().get(), metadata.buildOrThrow()); } - if (part.inlineData().isPresent()) { - Blob blob = part.inlineData().get(); - byte[] bytes = blob.data().orElse(null); - String encoded = bytes != null ? Base64.getEncoder().encodeToString(bytes) : null; - String mime = blob.mimeType().orElse(null); - String name = blob.displayName().orElse(null); - return Optional.of(new FilePart(new FileWithBytes(mime, name, encoded), new HashMap<>())); + if (part.fileData().isPresent() || part.inlineData().isPresent()) { + return filePartToA2A(part, metadata); } if (part.functionCall().isPresent() || part.functionResponse().isPresent() || part.executableCode().isPresent() || part.codeExecutionResult().isPresent()) { - return convertGenaiPartToA2aPart(part).map(data -> data); + return dataPartToA2A(part, metadata); } - logger.warn("Unsupported GenAI part type for JSON export: {}", part); - return Optional.empty(); + throw new IllegalArgumentException("Unsupported GenAI part type: " + part); + } + + private static DataPart dataPartToA2A(Part part, ImmutableMap.Builder metadata) { + + if (part.functionCall().isPresent()) { + return createDataPartFromFunctionCall(part.functionCall().get(), metadata); + } else if (part.functionResponse().isPresent()) { + return createDataPartFromFunctionResponse(part.functionResponse().get(), metadata); + } else if (part.codeExecutionResult().isPresent()) { + return createDataPartFromCodeExecutionResult(part.codeExecutionResult().get(), metadata); + } else if (part.executableCode().isPresent()) { + return createDataPartFromExecutableCode(part.executableCode().get(), metadata); + } + + throw new IllegalArgumentException("Unsupported GenAI data part type: " + part); + } + + private static FilePart filePartToA2A(Part part, ImmutableMap.Builder metadata) { + if (part.fileData().isPresent()) { + FileData fileData = part.fileData().get(); + String uri = fileData.fileUri().orElse(null); + String mime = fileData.mimeType().orElse(null); + String name = fileData.displayName().orElse(null); + return new FilePart(new FileWithUri(mime, name, uri), metadata.buildOrThrow()); + } + Blob blob = part.inlineData().get(); + byte[] bytes = blob.data().orElse(null); + String encoded = bytes != null ? Base64.getEncoder().encodeToString(bytes) : null; + addValueIfPresent(metadata, "video_metadata", part.videoMetadata()); + return new FilePart( + new FileWithBytes(blob.mimeType().orElse(null), blob.displayName().orElse(null), encoded), + metadata.buildOrThrow()); } @SuppressWarnings("unchecked") // safe conversion from objectMapper.readValue diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java deleted file mode 100644 index 57f7aeffd..000000000 --- a/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.google.adk.a2a.converters; - -import com.google.adk.events.Event; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.genai.types.Content; -import io.a2a.spec.DataPart; -import io.a2a.spec.Message; -import io.a2a.spec.Part; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * rfe Converter for A2A Messages to ADK Events. This is used on the A2A service side to convert - * incoming A2A requests to ADK Events. - * - *

**EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not - * use in production code. - */ -public final class RequestConverter { - private static final Logger logger = LoggerFactory.getLogger(RequestConverter.class); - - private RequestConverter() {} - - /** - * Convert an A2A Message to an ADK Event. This is used when the A2A service receives a request - * and needs to process it with ADK. - * - * @param message The A2A message to convert. - * @param invocationId The invocation ID for the event. - * @return Optional containing the converted ADK Event, or empty if conversion fails. - */ - public static Optional convertA2aMessageToAdkEvent(Message message, String invocationId) { - if (message == null) { - // Create an empty user message event - logger.info("Null message received, creating empty user event"); - Event event = - Event.builder() - .id(UUID.randomUUID().toString()) - .invocationId(invocationId != null ? invocationId : UUID.randomUUID().toString()) - .author("user") - .content( - Content.builder() - .role("user") - .parts( - ImmutableList.of(com.google.genai.types.Part.builder().text("").build())) - .build()) - .timestamp(Instant.now().toEpochMilli()) - .build(); - return Optional.of(event); - } - - List genaiParts = new ArrayList<>(); - - // Convert each A2A Part to GenAI Part - if (message.getParts() != null) { - for (Part a2aPart : message.getParts()) { - Optional genaiPart = PartConverter.toGenaiPart(a2aPart); - genaiPart.ifPresent(genaiParts::add); - } - } - - if (genaiParts.isEmpty()) { - logger.warn("No convertible parts found in A2A message"); - return Optional.empty(); - } - - // Treat inbound A2A requests as user input for the ADK agent. - String author = "user"; - - // Build the Content object - Content content = Content.builder().role("user").parts(genaiParts).build(); - - // Build the Event - Event event = - Event.builder() - .id( - !message.getMessageId().isEmpty() - ? message.getMessageId() - : UUID.randomUUID().toString()) - .invocationId(invocationId != null ? invocationId : UUID.randomUUID().toString()) - .author(author) - .content(content) - .timestamp(Instant.now().toEpochMilli()) - .build(); - - return Optional.of(event); - } - - /** - * Convert an aggregated A2A Message to multiple ADK Events. This reconstructs the original event - * sequence from an aggregated message. - * - * @param message The aggregated A2A message to convert. - * @param invocationId The invocation ID for the events. - * @return List of ADK Events representing the conversation history. - */ - public static ImmutableList convertAggregatedA2aMessageToAdkEvents( - Message message, String invocationId) { - if (message == null || message.getParts() == null || message.getParts().isEmpty()) { - logger.info("Null or empty message received, creating empty user event"); - Event event = - Event.builder() - .id(UUID.randomUUID().toString()) - .invocationId(invocationId != null ? invocationId : UUID.randomUUID().toString()) - .author("user") - .content( - Content.builder() - .role("user") - .parts( - ImmutableList.of(com.google.genai.types.Part.builder().text("").build())) - .build()) - .timestamp(Instant.now().toEpochMilli()) - .build(); - return ImmutableList.of(event); - } - - List events = new ArrayList<>(); - - // Emit exactly one ADK Event per A2A Part, preserving order. - for (Part a2aPart : message.getParts()) { - Optional genaiPart = PartConverter.toGenaiPart(a2aPart); - if (genaiPart.isEmpty()) { - continue; - } - - String author = extractAuthorFromMetadata(a2aPart); - String role = determineRoleFromAuthor(author); - - events.add(createEvent(ImmutableList.of(genaiPart.get()), author, role, invocationId)); - } - - if (events.isEmpty()) { - logger.warn("No events created from aggregated message; returning single empty user event"); - Event event = - Event.builder() - .id(UUID.randomUUID().toString()) - .invocationId(invocationId) - .author("user") - .content( - Content.builder() - .role("user") - .parts( - ImmutableList.of(com.google.genai.types.Part.builder().text("").build())) - .build()) - .timestamp(Instant.now().toEpochMilli()) - .build(); - events.add(event); - } - - logger.info("Converted aggregated A2A message to {} ADK events", events.size()); - return ImmutableList.copyOf(events); - } - - private static String extractAuthorFromMetadata(Part a2aPart) { - if (a2aPart instanceof DataPart dataPart) { - Map metadata = - Optional.ofNullable(dataPart.getMetadata()).orElse(ImmutableMap.of()); - String type = - metadata.getOrDefault(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, "").toString(); - if (type.equals(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)) { - return "model"; - } - if (type.equals(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)) { - return "user"; - } - Map data = Optional.ofNullable(dataPart.getData()).orElse(ImmutableMap.of()); - if (data.containsKey("args")) { - return "model"; - } - if (data.containsKey("response")) { - return "user"; - } - } - return "user"; - } - - private static String determineRoleFromAuthor(String author) { - return author.equals("model") ? "model" : "user"; - } - - private static Event createEvent( - List parts, String author, String role, String invocationId) { - return Event.builder() - .id(UUID.randomUUID().toString()) - .invocationId(invocationId) - .author(author) - .content(Content.builder().role(role).parts(new ArrayList<>(parts)).build()) - .timestamp(Instant.now().toEpochMilli()) - .build(); - } -} diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java index 2e32b4c8c..ccbb1b9cf 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java @@ -2,34 +2,28 @@ import static com.google.common.collect.ImmutableList.toImmutableList; -import com.google.adk.a2a.common.A2AClientError; import com.google.adk.agents.InvocationContext; import com.google.adk.events.Event; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.genai.types.Content; +import com.google.genai.types.Part; import io.a2a.client.ClientEvent; import io.a2a.client.MessageEvent; import io.a2a.client.TaskEvent; import io.a2a.client.TaskUpdateEvent; import io.a2a.spec.Artifact; -import io.a2a.spec.EventKind; -import io.a2a.spec.JSONRPCError; import io.a2a.spec.Message; -import io.a2a.spec.SendMessageResponse; import io.a2a.spec.Task; import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskState; import io.a2a.spec.TaskStatusUpdateEvent; -import io.a2a.spec.TextPart; import java.time.Instant; -import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.UUID; -import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,119 +40,6 @@ public final class ResponseConverter { private ResponseConverter() {} - /** - * Converts a {@link SendMessageResponse} containing a {@link Message} result into ADK events. - * - *

Non-message results are ignored in the message-only integration and logged for awareness. - */ - public static List sendMessageResponseToEvents( - SendMessageResponse response, String invocationId, String branch) { - if (response == null) { - logger.warn("SendMessageResponse was null; returning no events."); - return ImmutableList.of(); - } - - EventKind result = response.getResult(); - if (result == null) { - - JSONRPCError error = response.getError(); - if (error != null) { - throw new A2AClientError( - String.format("SendMessageResponse error for invocation %s", invocationId), error); - } - - throw new A2AClientError( - String.format("SendMessageResponse result was null for invocation %s", invocationId)); - } - - if (result instanceof Message message) { - return messageToEvents(message, invocationId, branch); - } - - throw new IllegalArgumentException( - String.format( - "SendMessageResponse result was neither a Message nor an error for invocation %s", - invocationId)); - } - - /** Converts an A2A message back to ADK events. */ - public static List messageToEvents(Message message, String invocationId, String branch) { - List events = new ArrayList<>(); - - for (io.a2a.spec.Part part : message.getParts()) { - PartConverter.toGenaiPart(part) - .ifPresent( - genaiPart -> - events.add( - Event.builder() - .id(UUID.randomUUID().toString()) - .invocationId(invocationId) - .author(message.getRole() == Message.Role.AGENT ? "agent" : "user") - .branch(branch) - .content( - Content.builder() - .role(message.getRole() == Message.Role.AGENT ? "model" : "user") - .parts(ImmutableList.of(genaiPart)) - .build()) - .timestamp(Instant.now().toEpochMilli()) - .build())); - } - return events; - } - - private static Message emptyAgentMessage(String contextId) { - Message.Builder builder = - new Message.Builder() - .messageId(UUID.randomUUID().toString()) - .role(Message.Role.AGENT) - .parts(ImmutableList.of(new TextPart(""))); - if (contextId != null) { - builder.contextId(contextId); - } - return builder.build(); - } - - /** Converts a list of ADK events into a single aggregated A2A message. */ - public static Message eventsToMessage(List events, String contextId, String taskId) { - if (events == null || events.isEmpty()) { - return emptyAgentMessage(contextId); - } - - if (events.size() == 1) { - return eventToMessage(events.get(0), contextId); - } - - List> parts = new ArrayList<>(); - for (Event event : events) { - parts.addAll(eventParts(event)); - } - - Message.Builder builder = - new Message.Builder() - .messageId(taskId != null ? taskId : UUID.randomUUID().toString()) - .role(Message.Role.AGENT) - .parts(parts); - if (contextId != null) { - builder.contextId(contextId); - } - return builder.build(); - } - - /** Converts a single ADK event into an A2A message. */ - public static Message eventToMessage(Event event, String contextId) { - List> parts = eventParts(event); - - Message.Builder builder = - new Message.Builder() - .messageId(event.id() != null ? event.id() : UUID.randomUUID().toString()) - .role(event.author().equalsIgnoreCase("user") ? Message.Role.USER : Message.Role.AGENT) - .parts(parts); - if (contextId != null) { - builder.contextId(contextId); - } - return builder.build(); - } - /** * Converts a A2A {@link ClientEvent} to an ADK {@link Event}, based on the event type. Returns an * empty optional if the event should be ignored (e.g. if the event is not a final update for @@ -175,6 +56,7 @@ public static Optional clientEventToEvent( } else if (event instanceof TaskUpdateEvent updateEvent) { return handleTaskUpdate(updateEvent, invocationContext); } + logger.warn("Unsupported ClientEvent type: {}", event.getClass()); throw new IllegalArgumentException("Unsupported ClientEvent type: " + event.getClass()); } @@ -262,7 +144,7 @@ public static Event messageToFailedEvent(Message message, InvocationContext invo public static Event messageToEvent( Message message, InvocationContext invocationContext, boolean isPending) { - ImmutableList genaiParts = + ImmutableList genaiParts = PartConverter.toGenaiParts(message.getParts()).stream() .map(part -> part.toBuilder().thought(isPending).build()) .collect(toImmutableList()); @@ -297,19 +179,6 @@ public static Event taskToEvent(Task task, InvocationContext invocationContext) return emptyEvent(invocationContext); } - private static List> eventParts(Event event) { - List> parts = new ArrayList<>(); - Optional content = event.content(); - if (content.isEmpty() || content.get().parts().isEmpty()) { - return parts; - } - - for (com.google.genai.types.Part genaiPart : content.get().parts().get()) { - PartConverter.fromGenaiPart(genaiPart).ifPresent(parts::add); - } - return parts; - } - private static Event emptyEvent(InvocationContext invocationContext) { Event.Builder builder = Event.builder() @@ -322,7 +191,7 @@ private static Event emptyEvent(InvocationContext invocationContext) { return builder.build(); } - private static Content fromModelParts(List parts) { + private static Content fromModelParts(List parts) { return Content.builder().role("model").parts(parts).build(); } @@ -334,15 +203,4 @@ private static Event.Builder remoteAgentEventBuilder(InvocationContext invocatio .branch(invocationContext.branch().orElse(null)) .timestamp(Instant.now().toEpochMilli()); } - - /** Simple REST-friendly wrapper to carry either a message result or a task result. */ - public record MessageSendResult(@Nullable Message message, @Nullable Task task) { - public static MessageSendResult fromMessage(Message message) { - return new MessageSendResult(message, null); - } - - public static MessageSendResult fromTask(Task task) { - return new MessageSendResult(null, task); - } - } } diff --git a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java index 3d66a4e07..b7b4e9953 100644 --- a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java +++ b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java @@ -308,16 +308,8 @@ private Maybe process( "Agent returned an error: " + event.errorCode().get(), null)); } - ImmutableList> parts = EventConverter.contentToParts(event.content()); - // Mark all parts as partial if the event is partial. - if (event.partial().orElse(false)) { - parts.forEach( - part -> { - Map metadata = part.getMetadata(); - metadata.put("adk_partial", true); - }); - } - + ImmutableList> parts = + EventConverter.contentToParts(event.content(), event.partial().orElse(false)); Map metadata = new HashMap<>(); if (event.customMetadata().isPresent()) { for (CustomMetadata cm : event.customMetadata().get()) { diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java index f9d34bf3f..8d460c457 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java @@ -92,6 +92,8 @@ public void convertEventsToA2AMessage_preservesFunctionCallAndResponseParts() { .invocationId("invocation-1") .agent(new TestAgent()) .session(session) + .userContent( + Content.builder().role("user").parts(ImmutableList.of(userTextPart)).build()) .endInvocation(false) .build(); @@ -101,24 +103,28 @@ public void convertEventsToA2AMessage_preservesFunctionCallAndResponseParts() { // Assert assertThat(maybeMessage).isPresent(); Message message = maybeMessage.get(); - assertThat(message.getParts()).hasSize(3); + assertThat(message.getParts()).hasSize(4); assertThat(message.getParts().get(0)).isInstanceOf(TextPart.class); assertThat(message.getParts().get(1)).isInstanceOf(DataPart.class); assertThat(message.getParts().get(2)).isInstanceOf(DataPart.class); + assertThat(message.getParts().get(3)).isInstanceOf(TextPart.class); DataPart callDataPart = (DataPart) message.getParts().get(1); assertThat(callDataPart.getMetadata().get(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY)) - .isEqualTo(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); + .isEqualTo(A2ADataPartMetadataType.FUNCTION_CALL.getType()); assertThat(callDataPart.getData()).containsEntry("name", "roll_die"); assertThat(callDataPart.getData()).containsEntry("id", "adk-call-1"); assertThat(callDataPart.getData()).containsEntry("args", ImmutableMap.of("sides", 6)); DataPart responseDataPart = (DataPart) message.getParts().get(2); assertThat(responseDataPart.getMetadata().get(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY)) - .isEqualTo(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE); + .isEqualTo(A2ADataPartMetadataType.FUNCTION_RESPONSE.getType()); assertThat(responseDataPart.getData()).containsEntry("name", "roll_die"); assertThat(responseDataPart.getData()).containsEntry("id", "adk-call-1"); assertThat(responseDataPart.getData()).containsEntry("response", ImmutableMap.of("result", 3)); + + TextPart lastTextPart = (TextPart) message.getParts().get(3); + assertThat(lastTextPart.getText()).isEqualTo("Roll a die"); } private static final class TestAgent extends BaseAgent { diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java index 6ccfd9566..8e8982ffa 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java @@ -2,7 +2,9 @@ import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertThrows; +import com.google.adk.a2a.common.GenAiFieldMissingException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.genai.types.Blob; @@ -89,7 +91,7 @@ public void toGenaiPart_withDataPartFunctionCall_returnsGenaiFunctionCallPart() data, ImmutableMap.of( PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)); + A2ADataPartMetadataType.FUNCTION_CALL.getType())); Optional result = PartConverter.toGenaiPart(dataPart); @@ -126,7 +128,7 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons data, ImmutableMap.of( PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)); + A2ADataPartMetadataType.FUNCTION_RESPONSE.getType())); Optional result = PartConverter.toGenaiPart(dataPart); @@ -181,78 +183,21 @@ public void toGenaiParts_convertsAllSupportedParts() { } @Test - public void convertGenaiPartToA2aPart_withNullPart_returnsEmpty() { - assertThat(PartConverter.convertGenaiPartToA2aPart(null)).isEmpty(); - } - - @Test - public void convertGenaiPartToA2aPart_withTextPart_returnsEmpty() { - Part part = Part.builder().text("text").build(); - assertThat(PartConverter.convertGenaiPartToA2aPart(part)).isEmpty(); - } - - @Test - public void convertGenaiPartToA2aPart_withFunctionCallPart_returnsDataPart() { - Part part = - Part.builder() - .functionCall( - FunctionCall.builder() - .name("func") - .id("1") - .args(ImmutableMap.of("param", "value")) - .build()) - .build(); - - Optional result = PartConverter.convertGenaiPartToA2aPart(part); - - assertThat(result).isPresent(); - DataPart dataPart = result.get(); - assertThat(dataPart.getData()) - .containsExactly("name", "func", "id", "1", "args", ImmutableMap.of("param", "value")); - assertThat(dataPart.getMetadata()) - .containsEntry( - PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); - } - - @Test - public void convertGenaiPartToA2aPart_withFunctionResponsePart_returnsDataPart() { - Part part = - Part.builder() - .functionResponse( - FunctionResponse.builder() - .name("func") - .id("1") - .response(ImmutableMap.of("result", "value")) - .build()) - .build(); - - Optional result = PartConverter.convertGenaiPartToA2aPart(part); - - assertThat(result).isPresent(); - DataPart dataPart = result.get(); - assertThat(dataPart.getData()) - .containsExactly("name", "func", "id", "1", "response", ImmutableMap.of("result", "value")); - assertThat(dataPart.getMetadata()) - .containsEntry( - PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE); - } - - @Test - public void fromGenaiPart_withNullPart_returnsEmpty() { - assertThat(PartConverter.fromGenaiPart(null)).isEmpty(); + public void fromGenaiPart_withNullPart_throwsException() { + assertThrows(GenAiFieldMissingException.class, () -> PartConverter.fromGenaiPart(null, false)); } @Test public void fromGenaiPart_withTextPart_returnsTextPart() { - Part part = Part.builder().text("text").build(); + Part part = Part.builder().text("text").thought(true).build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, true); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(TextPart.class); - assertThat(((TextPart) result.get()).getText()).isEqualTo("text"); + assertThat(result).isInstanceOf(TextPart.class); + assertThat(((TextPart) result).getText()).isEqualTo("text"); + assertThat(((TextPart) result).getMetadata()).containsEntry("thought", true); + assertThat(((TextPart) result).getMetadata()) + .containsEntry(PartConverter.A2A_DATA_PART_METADATA_IS_PARTIAL_KEY, true); } @Test @@ -262,11 +207,10 @@ public void fromGenaiPart_withFileDataPart_returnsFilePartWithUri() { .fileData(FileData.builder().mimeType("text/plain").fileUri("http://file.txt").build()) .build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, false); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) result.get(); + assertThat(result).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) result; assertThat(filePart.getFile()).isInstanceOf(FileWithUri.class); FileWithUri fileWithUri = (FileWithUri) filePart.getFile(); assertThat(fileWithUri.mimeType()).isEqualTo("text/plain"); @@ -281,11 +225,10 @@ public void fromGenaiPart_withInlineDataPart_returnsFilePartWithBytes() { .inlineData(Blob.builder().mimeType("text/plain").data(bytes).build()) .build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, false); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) result.get(); + assertThat(result).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) result; assertThat(filePart.getFile()).isInstanceOf(FileWithBytes.class); FileWithBytes fileWithBytes = (FileWithBytes) filePart.getFile(); assertThat(fileWithBytes.mimeType()).isEqualTo("text/plain"); @@ -297,20 +240,32 @@ public void fromGenaiPart_withFunctionCallPart_returnsDataPart() { Part part = Part.builder() .functionCall( - FunctionCall.builder().name("func").id("1").args(ImmutableMap.of()).build()) + FunctionCall.builder() + .name("func") + .id("1") + .willContinue(true) + .args(ImmutableMap.of()) + .build()) .build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, false); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(DataPart.class); - DataPart dataPart = (DataPart) result.get(); + assertThat(result).isInstanceOf(DataPart.class); + DataPart dataPart = (DataPart) result; assertThat(dataPart.getData()) - .containsExactly("name", "func", "id", "1", "args", ImmutableMap.of()); + .containsExactly( + "name", + "func", + "id", + "1", + "args", + ImmutableMap.of(), + PartConverter.WILL_CONTINUE_KEY, + true); assertThat(dataPart.getMetadata()) .containsEntry( PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); + A2ADataPartMetadataType.FUNCTION_CALL.getType()); } @Test @@ -321,17 +276,16 @@ public void fromGenaiPart_withFunctionResponsePart_returnsDataPart() { FunctionResponse.builder().name("func").id("1").response(ImmutableMap.of()).build()) .build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, false); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(DataPart.class); - DataPart dataPart = (DataPart) result.get(); + assertThat(result).isInstanceOf(DataPart.class); + DataPart dataPart = (DataPart) result; assertThat(dataPart.getData()) .containsExactly("name", "func", "id", "1", "response", ImmutableMap.of()); assertThat(dataPart.getMetadata()) .containsEntry( PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE); + A2ADataPartMetadataType.FUNCTION_RESPONSE.getType()); } @Test diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java index 8dc70ca2a..5378bdd7b 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java @@ -12,7 +12,6 @@ import com.google.adk.sessions.Session; import com.google.common.collect.ImmutableList; import com.google.genai.types.Content; -import com.google.genai.types.Part; import io.a2a.client.MessageEvent; import io.a2a.client.TaskUpdateEvent; import io.a2a.spec.Artifact; @@ -25,7 +24,6 @@ import io.a2a.spec.TextPart; import io.reactivex.rxjava3.core.Flowable; import java.util.Optional; -import java.util.UUID; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -66,124 +64,6 @@ private static TaskStatusUpdateEvent.Builder testTaskStatusUpdateEvent() { return new TaskStatusUpdateEvent.Builder().taskId("task-1").contextId("context-1"); } - @Test - public void eventsToMessage_withNullEvents_returnsEmptyAgentMessage() { - Message message = ResponseConverter.eventsToMessage(null, "context-1", "task-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.AGENT); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEmpty(); - } - - @Test - public void eventsToMessage_withEmptyEvents_returnsEmptyAgentMessage() { - Message message = ResponseConverter.eventsToMessage(ImmutableList.of(), "context-1", "task-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.AGENT); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEmpty(); - } - - @Test - public void eventsToMessage_withSingleEvent_returnsMessage() { - Event event = - Event.builder() - .id(UUID.randomUUID().toString()) - .author("user") - .content( - Content.builder() - .role("user") - .parts(ImmutableList.of(Part.builder().text("Hello").build())) - .build()) - .build(); - - Message message = - ResponseConverter.eventsToMessage(ImmutableList.of(event), "context-1", "task-1"); - - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.USER); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Hello"); - } - - @Test - public void eventsToMessage_withMultipleEvents_returnsAggregatedMessage() { - Event event1 = - Event.builder() - .id(UUID.randomUUID().toString()) - .author("agent") - .content( - Content.builder() - .role("model") - .parts(ImmutableList.of(Part.builder().text("Hello ").build())) - .build()) - .build(); - Event event2 = - Event.builder() - .id(UUID.randomUUID().toString()) - .author("agent") - .content( - Content.builder() - .role("model") - .parts(ImmutableList.of(Part.builder().text("World").build())) - .build()) - .build(); - - Message message = - ResponseConverter.eventsToMessage(ImmutableList.of(event1, event2), "context-1", "task-1"); - - assertThat(message.getMessageId()).isEqualTo("task-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.AGENT); - assertThat(message.getParts()).hasSize(2); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Hello "); - assertThat(((TextPart) message.getParts().get(1)).getText()).isEqualTo("World"); - } - - @Test - public void eventToMessage_convertsUserEvent() { - Event event = - Event.builder() - .id("event-1") - .author("user") - .content( - Content.builder() - .role("user") - .parts(ImmutableList.of(Part.builder().text("Test").build())) - .build()) - .build(); - - Message message = ResponseConverter.eventToMessage(event, "context-1"); - - assertThat(message.getMessageId()).isEqualTo("event-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.USER); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Test"); - } - - @Test - public void eventToMessage_convertsAgentEvent() { - Event event = - Event.builder() - .id("event-1") - .author("agent") - .content( - Content.builder() - .role("model") - .parts(ImmutableList.of(Part.builder().text("Test").build())) - .build()) - .build(); - - Message message = ResponseConverter.eventToMessage(event, "context-1"); - - assertThat(message.getMessageId()).isEqualTo("event-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.AGENT); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Test"); - } - @Test public void clientEventToEvent_withMessageEvent_returnsEvent() { Message a2aMessage = From 7eafd1bd9b16e9ed83dfbc3d0983cfc415c0aaec Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 04:11:07 -0800 Subject: [PATCH 53/61] chore: revert: switch release please secret to use adk-java-releases-bot's token Release-As: 0.8.0 PiperOrigin-RevId: 879555202 --- .github/workflows/release-please.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml index 258cf90db..6d3142907 100644 --- a/.github/workflows/release-please.yaml +++ b/.github/workflows/release-please.yaml @@ -14,4 +14,4 @@ jobs: steps: - uses: googleapis/release-please-action@v4 with: - token: ${{ secrets.RELEASE_PLEASE_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} From d6e6d1532f1b7ea5d3ebf480e061f7b644184362 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 12:13:17 +0000 Subject: [PATCH 54/61] chore(main): release 0.8.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 44 +++++++++++++++++++ README.md | 4 +- a2a/pom.xml | 2 +- contrib/firestore-session-service/pom.xml | 2 +- contrib/langchain4j/pom.xml | 2 +- contrib/samples/a2a_basic/pom.xml | 2 +- contrib/samples/a2a_server/pom.xml | 6 +-- contrib/samples/configagent/pom.xml | 2 +- contrib/samples/helloworld/pom.xml | 2 +- contrib/samples/mcpfilesystem/pom.xml | 2 +- contrib/samples/pom.xml | 2 +- contrib/spring-ai/pom.xml | 2 +- core/pom.xml | 2 +- .../src/main/java/com/google/adk/Version.java | 2 +- dev/pom.xml | 2 +- maven_plugin/examples/custom_tools/pom.xml | 2 +- maven_plugin/examples/simple-agent/pom.xml | 2 +- maven_plugin/pom.xml | 2 +- pom.xml | 10 ++--- tutorials/city-time-weather/pom.xml | 2 +- tutorials/live-audio-single-agent/pom.xml | 2 +- 22 files changed, 71 insertions(+), 29 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 77c0d292c..d6a5f76bd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.0" + ".": "0.8.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index b10bad3ce..6d5b9e5eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## [0.8.0](https://github.com/google/adk-java/compare/v0.7.0...v0.8.0) (2026-03-06) + + +### âš  BREAKING CHANGES + +* remove methods with Optional params from LiveRequest.Builder +* remove deprecated methods accepting Optional params in InvocationContext +* remove deprecated BaseToolset.isToolSelected method +* remove Optional parameters from LlmResponse.Builder's methods +* remove support for legacy `transferToAgent`, superseded by `transfer_to_agent` + +### Features + +* add callbacks functionality to the agent executor ([7e8f9dc](https://github.com/google/adk-java/commit/7e8f9dcf82fe7e62aee625fbfaa8673d238ff184)) +* add example on how to expose agent via A2A protocol ([e3ea378](https://github.com/google/adk-java/commit/e3ea378051e5c4e5e5031657467145779e42db55)) +* Adding a Builder for EventsCompactionConfig ([05fbcfc](https://github.com/google/adk-java/commit/05fbcfc933923ae711cd12e7fc9e587fd8e2685c)) +* Adding a SessionKey for typeSafety ([d899f6f](https://github.com/google/adk-java/commit/d899f6f4ad52c84cb4ac8c90d0dc88c22487029c)) +* Adding plugin(Plugin... p) helper methods on App and Runner builders ([dc1a192](https://github.com/google/adk-java/commit/dc1a192a81a92870aa5a4af27a9dc90e81cdaf67)) +* implement partial event aggregation in RemoteA2AAgent ([e064067](https://github.com/google/adk-java/commit/e0640673d212b9849d312953f192f8da51fae85b)) +* remove deprecated BaseToolset.isToolSelected method ([d2f1145](https://github.com/google/adk-java/commit/d2f11456c3a99edd43b3dc0d04743ae7e9390ded)) +* remove deprecated methods accepting Optional params in InvocationContext ([88153c8](https://github.com/google/adk-java/commit/88153c833697a9b9c6ec735a69f48a92cbdfc54b)) +* remove methods with Optional params from LiveRequest.Builder ([84c62a4](https://github.com/google/adk-java/commit/84c62a48ef7b62641722824fe5ba1200606b7b17)) +* remove Optional parameters from LlmResponse.Builder's methods ([a3ac436](https://github.com/google/adk-java/commit/a3ac436bcfa241e90c07485e5da918ec8dbc2b4a)) + + +### Bug Fixes + +* Allow injecting ObjectMapper in FunctionTool, default to ObjectMapper (re. [#473](https://github.com/google/adk-java/issues/473)) ([71b1070](https://github.com/google/adk-java/commit/71b10701e753bddaa96d5e6579b759d2b9bb3e92)) +* downgrade otel.version to 1.51.0 ([117fedf](https://github.com/google/adk-java/commit/117fedf672bb67c4b078ac75ee81a7710452c5b5)) +* Ensure Gemini 3.1 models have events correctly buffered ([acffdb9](https://github.com/google/adk-java/commit/acffdb96bcd8133af99cb0b9426665ba73a83bbc)) +* Exit from rearrangeEventsForLatestFunctionResponse if size of events is less than 2 ([5bc3ef8](https://github.com/google/adk-java/commit/5bc3ef89e62eb3f32ba7e45657c9e40c88c3a5e9)) +* Fixed issue where events were marked empty if the first part had an empty text; now checks all parts for meaningful content ([a0cba25](https://github.com/google/adk-java/commit/a0cba25d691f4be72bea22b0649ecf2d2c110736)) +* prepare JSON serialization for Jackson 2.20.2 and Spring Boot 4.0.2 upgrades ([8c6591b](https://github.com/google/adk-java/commit/8c6591bc4ad86c376cdd70e1bb64f359fbf22fe9)) + + +### Miscellaneous Chores + +* revert: switch release please secret to use adk-java-releases-bot's token ([7eafd1b](https://github.com/google/adk-java/commit/7eafd1bd9b16e9ed83dfbc3d0983cfc415c0aaec)) + + +### Code Refactoring + +* remove support for legacy `transferToAgent`, superseded by `transfer_to_agent` ([c1ccb2e](https://github.com/google/adk-java/commit/c1ccb2e9d375fedcd7dbb594300e66a1a0488a91)) + ## [0.7.0](https://github.com/google/adk-java/compare/v0.6.0...v0.7.0) (2026-02-27) diff --git a/README.md b/README.md index d0471c1bf..4a5dab81f 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,13 @@ If you're using Maven, add the following to your dependencies: com.google.adk google-adk - 0.7.0 + 0.8.0 com.google.adk google-adk-dev - 0.7.0 + 0.8.0 ``` diff --git a/a2a/pom.xml b/a2a/pom.xml index 05c740067..0e969600e 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 google-adk-a2a diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml index d3d49e824..0b421e361 100644 --- a/contrib/firestore-session-service/pom.xml +++ b/contrib/firestore-session-service/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 ../../pom.xml diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml index 330339440..75dc8b58e 100644 --- a/contrib/langchain4j/pom.xml +++ b/contrib/langchain4j/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 ../../pom.xml diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml index fc666cd71..13c8e50b2 100644 --- a/contrib/samples/a2a_basic/pom.xml +++ b/contrib/samples/a2a_basic/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.7.1-SNAPSHOT + 0.8.0 .. diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml index d47a0c984..83a29727f 100644 --- a/contrib/samples/a2a_server/pom.xml +++ b/contrib/samples/a2a_server/pom.xml @@ -1,13 +1,11 @@ - + 4.0.0 com.google.adk google-adk-samples - 0.7.1-SNAPSHOT + 0.8.0 .. diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml index 552a6d826..b9e90a0e0 100644 --- a/contrib/samples/configagent/pom.xml +++ b/contrib/samples/configagent/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.7.1-SNAPSHOT + 0.8.0 .. diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index bff6b86a6..8dd9b3a85 100644 --- a/contrib/samples/helloworld/pom.xml +++ b/contrib/samples/helloworld/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-samples - 0.7.1-SNAPSHOT + 0.8.0 .. diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index 3e221f5f6..f1514a0ee 100644 --- a/contrib/samples/mcpfilesystem/pom.xml +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 ../../.. diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index f3c8359b8..413bd9722 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 ../.. diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index e07deec9e..c80428650 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 ../../pom.xml diff --git a/core/pom.xml b/core/pom.xml index 93c72e745..b455a9fc8 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 google-adk diff --git a/core/src/main/java/com/google/adk/Version.java b/core/src/main/java/com/google/adk/Version.java index 10219a31b..1dc0282c3 100644 --- a/core/src/main/java/com/google/adk/Version.java +++ b/core/src/main/java/com/google/adk/Version.java @@ -22,7 +22,7 @@ */ public final class Version { // Don't touch this, release-please should keep it up to date. - public static final String JAVA_ADK_VERSION = "0.7.0"; // x-release-please-released-version + public static final String JAVA_ADK_VERSION = "0.8.0"; // x-release-please-released-version private Version() {} } diff --git a/dev/pom.xml b/dev/pom.xml index 6a0a03467..825d4b1cf 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -18,7 +18,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 google-adk-dev diff --git a/maven_plugin/examples/custom_tools/pom.xml b/maven_plugin/examples/custom_tools/pom.xml index 73e53e480..03978de9a 100644 --- a/maven_plugin/examples/custom_tools/pom.xml +++ b/maven_plugin/examples/custom_tools/pom.xml @@ -4,7 +4,7 @@ com.example custom-tools-example - 0.7.1-SNAPSHOT + 0.8.0 jar ADK Custom Tools Example diff --git a/maven_plugin/examples/simple-agent/pom.xml b/maven_plugin/examples/simple-agent/pom.xml index 8b6c14c26..eaf4be09b 100644 --- a/maven_plugin/examples/simple-agent/pom.xml +++ b/maven_plugin/examples/simple-agent/pom.xml @@ -4,7 +4,7 @@ com.example simple-adk-agent - 0.7.1-SNAPSHOT + 0.8.0 jar Simple ADK Agent Example diff --git a/maven_plugin/pom.xml b/maven_plugin/pom.xml index 373c6775d..3e09e863a 100644 --- a/maven_plugin/pom.xml +++ b/maven_plugin/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 ../pom.xml diff --git a/pom.xml b/pom.xml index 34bef7755..d985cf33f 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 pom Google Agent Development Kit Maven Parent POM @@ -495,13 +495,13 @@ - + - - + + - + diff --git a/tutorials/city-time-weather/pom.xml b/tutorials/city-time-weather/pom.xml index ef0c8f991..5e641b44c 100644 --- a/tutorials/city-time-weather/pom.xml +++ b/tutorials/city-time-weather/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 ../../pom.xml diff --git a/tutorials/live-audio-single-agent/pom.xml b/tutorials/live-audio-single-agent/pom.xml index 77a3f3b55..9efd6489a 100644 --- a/tutorials/live-audio-single-agent/pom.xml +++ b/tutorials/live-audio-single-agent/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.7.1-SNAPSHOT + 0.8.0 ../../pom.xml From 47d0a199d7816e32ea5310d43616ad82932b5170 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 12:45:39 +0000 Subject: [PATCH 55/61] chore(main): release 0.8.1-SNAPSHOT --- a2a/pom.xml | 2 +- contrib/firestore-session-service/pom.xml | 2 +- contrib/langchain4j/pom.xml | 2 +- contrib/samples/a2a_basic/pom.xml | 2 +- contrib/samples/a2a_server/pom.xml | 2 +- contrib/samples/configagent/pom.xml | 2 +- contrib/samples/helloworld/pom.xml | 2 +- contrib/samples/mcpfilesystem/pom.xml | 2 +- contrib/samples/pom.xml | 2 +- contrib/spring-ai/pom.xml | 2 +- core/pom.xml | 2 +- dev/pom.xml | 2 +- maven_plugin/examples/custom_tools/pom.xml | 2 +- maven_plugin/examples/simple-agent/pom.xml | 2 +- maven_plugin/pom.xml | 2 +- pom.xml | 2 +- tutorials/city-time-weather/pom.xml | 2 +- tutorials/live-audio-single-agent/pom.xml | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/a2a/pom.xml b/a2a/pom.xml index 0e969600e..5857720fd 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT google-adk-a2a diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml index 0b421e361..a62bff5b6 100644 --- a/contrib/firestore-session-service/pom.xml +++ b/contrib/firestore-session-service/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT ../../pom.xml diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml index 75dc8b58e..e2ba4a7fb 100644 --- a/contrib/langchain4j/pom.xml +++ b/contrib/langchain4j/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT ../../pom.xml diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml index 13c8e50b2..82b11b96f 100644 --- a/contrib/samples/a2a_basic/pom.xml +++ b/contrib/samples/a2a_basic/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.8.0 + 0.8.1-SNAPSHOT .. diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml index 83a29727f..84023e260 100644 --- a/contrib/samples/a2a_server/pom.xml +++ b/contrib/samples/a2a_server/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.8.0 + 0.8.1-SNAPSHOT .. diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml index b9e90a0e0..6f7bfff83 100644 --- a/contrib/samples/configagent/pom.xml +++ b/contrib/samples/configagent/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.8.0 + 0.8.1-SNAPSHOT .. diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index 8dd9b3a85..36d12eaf0 100644 --- a/contrib/samples/helloworld/pom.xml +++ b/contrib/samples/helloworld/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-samples - 0.8.0 + 0.8.1-SNAPSHOT .. diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index f1514a0ee..935aa6531 100644 --- a/contrib/samples/mcpfilesystem/pom.xml +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT ../../.. diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index 413bd9722..905f8e711 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT ../.. diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index c80428650..f49c3faae 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT ../../pom.xml diff --git a/core/pom.xml b/core/pom.xml index b455a9fc8..ba7185927 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT google-adk diff --git a/dev/pom.xml b/dev/pom.xml index 825d4b1cf..57aa808c2 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -18,7 +18,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT google-adk-dev diff --git a/maven_plugin/examples/custom_tools/pom.xml b/maven_plugin/examples/custom_tools/pom.xml index 03978de9a..abd3c60f2 100644 --- a/maven_plugin/examples/custom_tools/pom.xml +++ b/maven_plugin/examples/custom_tools/pom.xml @@ -4,7 +4,7 @@ com.example custom-tools-example - 0.8.0 + 0.8.1-SNAPSHOT jar ADK Custom Tools Example diff --git a/maven_plugin/examples/simple-agent/pom.xml b/maven_plugin/examples/simple-agent/pom.xml index eaf4be09b..309fe9364 100644 --- a/maven_plugin/examples/simple-agent/pom.xml +++ b/maven_plugin/examples/simple-agent/pom.xml @@ -4,7 +4,7 @@ com.example simple-adk-agent - 0.8.0 + 0.8.1-SNAPSHOT jar Simple ADK Agent Example diff --git a/maven_plugin/pom.xml b/maven_plugin/pom.xml index 3e09e863a..6ff3404f3 100644 --- a/maven_plugin/pom.xml +++ b/maven_plugin/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index d985cf33f..ffe904d74 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT pom Google Agent Development Kit Maven Parent POM diff --git a/tutorials/city-time-weather/pom.xml b/tutorials/city-time-weather/pom.xml index 5e641b44c..f4e8bdb52 100644 --- a/tutorials/city-time-weather/pom.xml +++ b/tutorials/city-time-weather/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT ../../pom.xml diff --git a/tutorials/live-audio-single-agent/pom.xml b/tutorials/live-audio-single-agent/pom.xml index 9efd6489a..b6e649222 100644 --- a/tutorials/live-audio-single-agent/pom.xml +++ b/tutorials/live-audio-single-agent/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.8.0 + 0.8.1-SNAPSHOT ../../pom.xml From 0b9057c9ccab98ea58597ec55b8168e32ac7c9a6 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 05:34:43 -0800 Subject: [PATCH 56/61] feat!: remove methods with Optional params from VertexCredential.Builder PiperOrigin-RevId: 879582516 --- .../main/java/com/google/adk/models/VertexCredentials.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/src/main/java/com/google/adk/models/VertexCredentials.java b/core/src/main/java/com/google/adk/models/VertexCredentials.java index 5e36cbe95..93dc05ec1 100644 --- a/core/src/main/java/com/google/adk/models/VertexCredentials.java +++ b/core/src/main/java/com/google/adk/models/VertexCredentials.java @@ -38,16 +38,11 @@ public static Builder builder() { /** Builder for {@link VertexCredentials}. */ @AutoValue.Builder public abstract static class Builder { - public abstract Builder setProject(Optional value); public abstract Builder setProject(@Nullable String value); - public abstract Builder setLocation(Optional value); - public abstract Builder setLocation(@Nullable String value); - public abstract Builder setCredentials(Optional value); - public abstract Builder setCredentials(@Nullable GoogleCredentials value); public abstract VertexCredentials build(); From 924fb7174855b46a58be43373c1a29284c47dfa8 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 05:42:58 -0800 Subject: [PATCH 57/61] feat: add support for gemini models in VertexAiRagRetrieval PiperOrigin-RevId: 879585211 --- core/pom.xml | 19 +++++++++++++++++++ .../tools/retrieval/VertexAiRagRetrieval.java | 6 +++--- .../retrieval/VertexAiRagRetrievalTest.java | 2 +- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index ba7185927..a0f843f56 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -209,10 +209,29 @@ maven-surefire-plugin + basic test + + + false + + + + + vertex-ai-rag-retrieval + + test + + + + true + + + VertexAiRagRetrievalTest#processLlmRequest_gemini2Model_addVertexRagStoreToConfig, VertexAiRagRetrievalTest#processLlmRequest_otherModel_doNotAddVertexRagStoreToConfig + apigee-llm diff --git a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java index b36a05d10..16f11a1f8 100644 --- a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java +++ b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java @@ -20,6 +20,7 @@ import com.google.adk.models.LlmRequest; import com.google.adk.tools.ToolContext; +import com.google.adk.utils.ModelNameUtils; import com.google.cloud.aiplatform.v1.RagContexts; import com.google.cloud.aiplatform.v1.RagQuery; import com.google.cloud.aiplatform.v1.RetrieveContextsRequest; @@ -105,10 +106,9 @@ public VertexAiRagRetrieval( public Completable processLlmRequest( LlmRequest.Builder llmRequestBuilder, ToolContext toolContext) { LlmRequest llmRequest = llmRequestBuilder.build(); - // Use Gemini built-in Vertex AI RAG tool for Gemini 2 models or when using Vertex AI API Model + // Use Gemini built-in Vertex AI RAG tool for Gemini models when using Vertex AI API Model boolean useVertexAi = Boolean.parseBoolean(System.getenv("GOOGLE_GENAI_USE_VERTEXAI")); - if (useVertexAi - && (llmRequest.model().isPresent() && llmRequest.model().get().startsWith("gemini-2"))) { + if (useVertexAi && llmRequest.model().filter(ModelNameUtils::isGeminiModel).isPresent()) { GenerateContentConfig config = llmRequest.config().orElseGet(() -> GenerateContentConfig.builder().build()); ImmutableList.Builder toolsBuilder = ImmutableList.builder(); diff --git a/core/src/test/java/com/google/adk/tools/retrieval/VertexAiRagRetrievalTest.java b/core/src/test/java/com/google/adk/tools/retrieval/VertexAiRagRetrievalTest.java index 6f04a7ef8..8246751b9 100644 --- a/core/src/test/java/com/google/adk/tools/retrieval/VertexAiRagRetrievalTest.java +++ b/core/src/test/java/com/google/adk/tools/retrieval/VertexAiRagRetrievalTest.java @@ -208,7 +208,7 @@ public void processLlmRequest_otherModel_doNotAddVertexRagStoreToConfig() { "projects/test-project/locations/us-central1", ragResources, vectorDistanceThreshold); - LlmRequest.Builder llmRequestBuilder = LlmRequest.builder().model("gemini-1-pro"); + LlmRequest.Builder llmRequestBuilder = LlmRequest.builder().model("other-model"); ToolContext toolContext = buildToolContext(); GenerateContentConfig initialConfig = GenerateContentConfig.builder().build(); llmRequestBuilder.config(initialConfig); From 11ce49ec456b7eba81b7a7a29461e9716d4e5260 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 10:01:08 -0800 Subject: [PATCH 58/61] chore: switch release please secret to use adk-java-releases-bot's token PiperOrigin-RevId: 879687277 --- .github/workflows/release-please.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml index 6d3142907..258cf90db 100644 --- a/.github/workflows/release-please.yaml +++ b/.github/workflows/release-please.yaml @@ -14,4 +14,4 @@ jobs: steps: - uses: googleapis/release-please-action@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} From 67b602f245f564238ea22298a37bf70049e56a12 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 6 Mar 2026 11:01:57 -0800 Subject: [PATCH 59/61] feat!: use @Nullable fields in Event class PiperOrigin-RevId: 879717644 --- .../com/google/adk/a2a/RemoteA2AAgent.java | 2 +- .../adk/a2a/converters/ResponseConverter.java | 2 +- .../adk/a2a/executor/AgentExecutorTest.java | 6 +- .../java/com/google/adk/agents/BaseAgent.java | 4 +- .../java/com/google/adk/events/Event.java | 224 ++++++------------ .../adk/flows/llmflows/BaseLlmFlow.java | 28 +-- .../adk/flows/llmflows/CodeExecution.java | 6 +- .../google/adk/flows/llmflows/Contents.java | 4 +- .../google/adk/flows/llmflows/Functions.java | 10 +- .../java/com/google/adk/runner/Runner.java | 4 +- .../adk/sessions/SessionJsonConverter.java | 9 +- .../java/com/google/adk/events/EventTest.java | 2 + .../adk/flows/llmflows/ContentsTest.java | 4 +- .../adk/flows/llmflows/FunctionsTest.java | 8 +- .../google/adk/plugins/LoggingPluginTest.java | 2 - .../sessions/SessionJsonConverterTest.java | 7 +- 16 files changed, 113 insertions(+), 209 deletions(-) diff --git a/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java b/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java index b8ff39808..b391f2985 100644 --- a/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java +++ b/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java @@ -436,7 +436,7 @@ private boolean mergeAggregatedContentIntoEvent(Event event) { } Content aggregatedContent = Content.builder().role("model").parts(parts).build(); - event.setContent(Optional.of(aggregatedContent)); + event.setContent(aggregatedContent); ImmutableList.Builder newMetadata = ImmutableList.builder(); event.customMetadata().ifPresent(newMetadata::addAll); diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java index ccbb1b9cf..57a84b58f 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java @@ -76,7 +76,7 @@ private static Optional handleTaskUpdate( boolean isLastChunk = Objects.equals(artifactEvent.isLastChunk(), true); Event eventPart = artifactToEvent(artifactEvent.getArtifact(), context); - eventPart.setPartial(Optional.of(isAppend || !isLastChunk)); + eventPart.setPartial(isAppend || !isLastChunk); // append=true, lastChunk=false: emit as partial, update aggregation // append=false, lastChunk=false: emit as partial, reset aggregation // append=true, lastChunk=true: emit as partial, update aggregation and emit as non-partial diff --git a/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java index 5570f40d0..647aaf21f 100644 --- a/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java @@ -361,7 +361,7 @@ private RequestContext createRequestContext() { public void process_statefulAggregation_tracksArtifactIdAndAppendForAuthor() { Event partial1 = Event.builder() - .partial(Optional.of(true)) + .partial(true) .author("agent_author") .content( Content.builder() @@ -370,7 +370,7 @@ public void process_statefulAggregation_tracksArtifactIdAndAppendForAuthor() { .build(); Event partial2 = Event.builder() - .partial(Optional.of(true)) + .partial(true) .author("agent_author") .content( Content.builder() @@ -379,7 +379,7 @@ public void process_statefulAggregation_tracksArtifactIdAndAppendForAuthor() { .build(); Event finalEvent = Event.builder() - .partial(Optional.of(false)) + .partial(false) .author("agent_author") .content( Content.builder() diff --git a/core/src/main/java/com/google/adk/agents/BaseAgent.java b/core/src/main/java/com/google/adk/agents/BaseAgent.java index 226e61abe..d74ba9ca5 100644 --- a/core/src/main/java/com/google/adk/agents/BaseAgent.java +++ b/core/src/main/java/com/google/adk/agents/BaseAgent.java @@ -409,7 +409,7 @@ private Single> callCallback( .id(Event.generateEventId()) .invocationId(invocationContext.invocationId()) .author(name()) - .branch(invocationContext.branch()) + .branch(invocationContext.branch().orElse(null)) .actions(callbackContext.eventActions()) .content(content) .build()); @@ -426,7 +426,7 @@ private Single> callCallback( .id(Event.generateEventId()) .invocationId(invocationContext.invocationId()) .author(name()) - .branch(invocationContext.branch()) + .branch(invocationContext.branch().orElse(null)) .actions(callbackContext.eventActions()); return Single.just(Optional.of(eventBuilder.build())); 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 91dc79a56..2677b635d 100644 --- a/core/src/main/java/com/google/adk/events/Event.java +++ b/core/src/main/java/com/google/adk/events/Event.java @@ -49,21 +49,21 @@ public class Event extends JsonBaseModel { private String id; private String invocationId; private String author; - private Optional content = Optional.empty(); + private @Nullable Content content; private EventActions actions; - private Optional> longRunningToolIds = Optional.empty(); - private Optional partial = Optional.empty(); - private Optional turnComplete = Optional.empty(); - private Optional errorCode = Optional.empty(); - private Optional errorMessage = Optional.empty(); - private Optional finishReason = Optional.empty(); - private Optional usageMetadata = Optional.empty(); - private Optional avgLogprobs = Optional.empty(); - private Optional interrupted = Optional.empty(); - private Optional branch = Optional.empty(); - private Optional groundingMetadata = Optional.empty(); - private Optional> customMetadata = Optional.empty(); - private Optional modelVersion = Optional.empty(); + private @Nullable Set longRunningToolIds; + private @Nullable Boolean partial; + private @Nullable Boolean turnComplete; + private @Nullable FinishReason errorCode; + private @Nullable String errorMessage; + private @Nullable FinishReason finishReason; + private @Nullable GenerateContentResponseUsageMetadata usageMetadata; + private @Nullable Double avgLogprobs; + private @Nullable Boolean interrupted; + private @Nullable String branch; + private @Nullable GroundingMetadata groundingMetadata; + private @Nullable List customMetadata; + private @Nullable String modelVersion; private long timestamp; private Event() {} @@ -104,10 +104,10 @@ public void setAuthor(String author) { @JsonProperty("content") public Optional content() { - return content; + return Optional.ofNullable(content); } - public void setContent(Optional content) { + public void setContent(@Nullable Content content) { this.content = content; } @@ -126,10 +126,10 @@ public void setActions(EventActions actions) { */ @JsonProperty("longRunningToolIds") public Optional> longRunningToolIds() { - return longRunningToolIds; + return Optional.ofNullable(longRunningToolIds); } - public void setLongRunningToolIds(Optional> longRunningToolIds) { + public void setLongRunningToolIds(@Nullable Set longRunningToolIds) { this.longRunningToolIds = longRunningToolIds; } @@ -139,73 +139,79 @@ public void setLongRunningToolIds(Optional> longRunningToolIds) { */ @JsonProperty("partial") public Optional partial() { - return partial; + return Optional.ofNullable(partial); } - public void setPartial(Optional partial) { + public void setPartial(@Nullable Boolean partial) { this.partial = partial; } @JsonProperty("turnComplete") public Optional turnComplete() { - return turnComplete; + return Optional.ofNullable(turnComplete); } - public void setTurnComplete(Optional turnComplete) { + public void setTurnComplete(@Nullable Boolean turnComplete) { this.turnComplete = turnComplete; } @JsonProperty("errorCode") public Optional errorCode() { - return errorCode; + return Optional.ofNullable(errorCode); } @JsonProperty("finishReason") public Optional finishReason() { - return finishReason; + return Optional.ofNullable(finishReason); } - public void setErrorCode(Optional errorCode) { + public void setErrorCode(@Nullable FinishReason errorCode) { this.errorCode = errorCode; } + @Deprecated + @SuppressWarnings("checkstyle:IllegalType") public void setFinishReason(Optional finishReason) { + this.finishReason = finishReason.orElse(null); + } + + public void setFinishReason(@Nullable FinishReason finishReason) { this.finishReason = finishReason; } @JsonProperty("errorMessage") public Optional errorMessage() { - return errorMessage; + return Optional.ofNullable(errorMessage); } - public void setErrorMessage(Optional errorMessage) { + public void setErrorMessage(@Nullable String errorMessage) { this.errorMessage = errorMessage; } @JsonProperty("usageMetadata") public Optional usageMetadata() { - return usageMetadata; + return Optional.ofNullable(usageMetadata); } - public void setUsageMetadata(Optional usageMetadata) { + public void setUsageMetadata(@Nullable GenerateContentResponseUsageMetadata usageMetadata) { this.usageMetadata = usageMetadata; } @JsonProperty("avgLogprobs") public Optional avgLogprobs() { - return avgLogprobs; + return Optional.ofNullable(avgLogprobs); } - public void setAvgLogprobs(Optional avgLogprobs) { + public void setAvgLogprobs(@Nullable Double avgLogprobs) { this.avgLogprobs = avgLogprobs; } @JsonProperty("interrupted") public Optional interrupted() { - return interrupted; + return Optional.ofNullable(interrupted); } - public void setInterrupted(Optional interrupted) { + public void setInterrupted(@Nullable Boolean interrupted) { this.interrupted = interrupted; } @@ -216,7 +222,7 @@ public void setInterrupted(Optional interrupted) { */ @JsonProperty("branch") public Optional branch() { - return branch; + return Optional.ofNullable(branch); } /** @@ -227,40 +233,36 @@ public Optional branch() { * @param branch Branch identifier. */ public void branch(@Nullable String branch) { - this.branch = Optional.ofNullable(branch); - } - - public void branch(Optional branch) { this.branch = branch; } /** The grounding metadata of the event. */ @JsonProperty("groundingMetadata") public Optional groundingMetadata() { - return groundingMetadata; + return Optional.ofNullable(groundingMetadata); } - public void setGroundingMetadata(Optional groundingMetadata) { + public void setGroundingMetadata(@Nullable GroundingMetadata groundingMetadata) { this.groundingMetadata = groundingMetadata; } /** The custom metadata of the event. */ @JsonProperty("customMetadata") public Optional> customMetadata() { - return customMetadata; + return Optional.ofNullable(customMetadata); } public void setCustomMetadata(@Nullable List customMetadata) { - this.customMetadata = Optional.ofNullable(customMetadata); + this.customMetadata = customMetadata; } /** The model version used to generate the response. */ @JsonProperty("modelVersion") public Optional modelVersion() { - return modelVersion; + return Optional.ofNullable(modelVersion); } - public void setModelVersion(Optional modelVersion) { + public void setModelVersion(@Nullable String modelVersion) { this.modelVersion = modelVersion; } @@ -345,22 +347,22 @@ public static class Builder { private String id; private String invocationId; private String author; - private Optional content = Optional.empty(); + private @Nullable Content content; private EventActions actions; - private Optional> longRunningToolIds = Optional.empty(); - private Optional partial = Optional.empty(); - private Optional turnComplete = Optional.empty(); - private Optional errorCode = Optional.empty(); - private Optional errorMessage = Optional.empty(); - private Optional finishReason = Optional.empty(); - private Optional usageMetadata = Optional.empty(); - private Optional avgLogprobs = Optional.empty(); - private Optional interrupted = Optional.empty(); - private Optional branch = Optional.empty(); - private Optional groundingMetadata = Optional.empty(); - private Optional> customMetadata = Optional.empty(); - private Optional modelVersion = Optional.empty(); - private Optional timestamp = Optional.empty(); + private @Nullable Set longRunningToolIds; + private @Nullable Boolean partial; + private @Nullable Boolean turnComplete; + private @Nullable FinishReason errorCode; + private @Nullable String errorMessage; + private @Nullable FinishReason finishReason; + private @Nullable GenerateContentResponseUsageMetadata usageMetadata; + private @Nullable Double avgLogprobs; + private @Nullable Boolean interrupted; + private @Nullable String branch; + private @Nullable GroundingMetadata groundingMetadata; + private @Nullable List customMetadata; + private @Nullable String modelVersion; + private @Nullable Long timestamp; @JsonCreator private static Builder create() { @@ -391,12 +393,6 @@ public Builder author(String value) { @CanIgnoreReturnValue @JsonProperty("content") public Builder content(@Nullable Content value) { - this.content = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder content(Optional value) { this.content = value; return this; } @@ -415,12 +411,6 @@ Optional actions() { @CanIgnoreReturnValue @JsonProperty("longRunningToolIds") public Builder longRunningToolIds(@Nullable Set value) { - this.longRunningToolIds = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder longRunningToolIds(Optional> value) { this.longRunningToolIds = value; return this; } @@ -428,12 +418,6 @@ public Builder longRunningToolIds(Optional> value) { @CanIgnoreReturnValue @JsonProperty("partial") public Builder partial(@Nullable Boolean value) { - this.partial = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder partial(Optional value) { this.partial = value; return this; } @@ -441,12 +425,6 @@ public Builder partial(Optional value) { @CanIgnoreReturnValue @JsonProperty("turnComplete") public Builder turnComplete(@Nullable Boolean value) { - this.turnComplete = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder turnComplete(Optional value) { this.turnComplete = value; return this; } @@ -454,12 +432,6 @@ public Builder turnComplete(Optional value) { @CanIgnoreReturnValue @JsonProperty("errorCode") public Builder errorCode(@Nullable FinishReason value) { - this.errorCode = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder errorCode(Optional value) { this.errorCode = value; return this; } @@ -467,12 +439,6 @@ public Builder errorCode(Optional value) { @CanIgnoreReturnValue @JsonProperty("errorMessage") public Builder errorMessage(@Nullable String value) { - this.errorMessage = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder errorMessage(Optional value) { this.errorMessage = value; return this; } @@ -480,12 +446,6 @@ public Builder errorMessage(Optional value) { @CanIgnoreReturnValue @JsonProperty("finishReason") public Builder finishReason(@Nullable FinishReason value) { - this.finishReason = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder finishReason(Optional value) { this.finishReason = value; return this; } @@ -493,12 +453,6 @@ public Builder finishReason(Optional value) { @CanIgnoreReturnValue @JsonProperty("usageMetadata") public Builder usageMetadata(@Nullable GenerateContentResponseUsageMetadata value) { - this.usageMetadata = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder usageMetadata(Optional value) { this.usageMetadata = value; return this; } @@ -506,12 +460,6 @@ public Builder usageMetadata(Optional valu @CanIgnoreReturnValue @JsonProperty("avgLogprobs") public Builder avgLogprobs(@Nullable Double value) { - this.avgLogprobs = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder avgLogprobs(Optional value) { this.avgLogprobs = value; return this; } @@ -519,12 +467,6 @@ public Builder avgLogprobs(Optional value) { @CanIgnoreReturnValue @JsonProperty("interrupted") public Builder interrupted(@Nullable Boolean value) { - this.interrupted = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder interrupted(Optional value) { this.interrupted = value; return this; } @@ -532,84 +474,52 @@ public Builder interrupted(Optional value) { @CanIgnoreReturnValue @JsonProperty("timestamp") public Builder timestamp(long value) { - this.timestamp = Optional.of(value); - return this; - } - - @CanIgnoreReturnValue - public Builder timestamp(Optional value) { this.timestamp = value; return this; } // Getter for builder's timestamp, used in build() Optional timestamp() { - return timestamp; + return Optional.ofNullable(timestamp); } @CanIgnoreReturnValue @JsonProperty("branch") public Builder branch(@Nullable String value) { - this.branch = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder branch(Optional value) { this.branch = value; return this; } // Getter for builder's branch, used in build() Optional branch() { - return branch; + return Optional.ofNullable(branch); } @CanIgnoreReturnValue @JsonProperty("groundingMetadata") public Builder groundingMetadata(@Nullable GroundingMetadata value) { - this.groundingMetadata = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder groundingMetadata(Optional value) { this.groundingMetadata = value; return this; } Optional groundingMetadata() { - return groundingMetadata; + return Optional.ofNullable(groundingMetadata); } @CanIgnoreReturnValue @JsonProperty("customMetadata") public Builder customMetadata(@Nullable List value) { - this.customMetadata = Optional.ofNullable(value); + this.customMetadata = value; return this; } - Optional> customMetadata() { - return customMetadata; - } - @CanIgnoreReturnValue @JsonProperty("modelVersion") public Builder modelVersion(@Nullable String value) { - this.modelVersion = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder modelVersion(Optional value) { this.modelVersion = value; return this; } - Optional modelVersion() { - return modelVersion; - } - public Event build() { Event event = new Event(); event.setId(id); @@ -627,7 +537,7 @@ public Event build() { event.setInterrupted(interrupted); event.branch(branch); event.setGroundingMetadata(groundingMetadata); - event.setCustomMetadata(customMetadata.orElse(null)); + event.setCustomMetadata(customMetadata); event.setModelVersion(modelVersion); event.setActions(actions().orElseGet(() -> EventActions.builder().build())); event.setTimestamp(timestamp().orElseGet(() -> Instant.now().toEpochMilli())); @@ -664,7 +574,7 @@ public Builder toBuilder() { .interrupted(this.interrupted) .branch(this.branch) .groundingMetadata(this.groundingMetadata) - .customMetadata(this.customMetadata.orElse(null)) + .customMetadata(this.customMetadata) .modelVersion(this.modelVersion); if (this.timestamp != 0) { builder.timestamp(this.timestamp); diff --git a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java index 1249728d8..6ed9ccaa3 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java @@ -351,7 +351,7 @@ private Flowable runOneStep(InvocationContext context) { .id(Event.generateEventId()) .invocationId(context.invocationId()) .author(context.agent().name()) - .branch(context.branch()) + .branch(context.branch().orElse(null)) .build(); mutableEventTemplate.setTimestamp(0L); @@ -535,7 +535,7 @@ public void onError(Throwable e) { Event.builder() .invocationId(invocationContext.invocationId()) .author(invocationContext.agent().name()) - .branch(invocationContext.branch()); + .branch(invocationContext.branch().orElse(null)); Flowable receiveFlow = connection @@ -639,17 +639,17 @@ private Event buildModelResponseEvent( Event baseEventForLlmResponse, LlmRequest llmRequest, LlmResponse llmResponse) { Event.Builder eventBuilder = baseEventForLlmResponse.toBuilder() - .content(llmResponse.content()) - .partial(llmResponse.partial()) - .errorCode(llmResponse.errorCode()) - .errorMessage(llmResponse.errorMessage()) - .interrupted(llmResponse.interrupted()) - .turnComplete(llmResponse.turnComplete()) - .groundingMetadata(llmResponse.groundingMetadata()) - .avgLogprobs(llmResponse.avgLogprobs()) - .finishReason(llmResponse.finishReason()) - .usageMetadata(llmResponse.usageMetadata()) - .modelVersion(llmResponse.modelVersion()); + .content(llmResponse.content().orElse(null)) + .partial(llmResponse.partial().orElse(null)) + .errorCode(llmResponse.errorCode().orElse(null)) + .errorMessage(llmResponse.errorMessage().orElse(null)) + .interrupted(llmResponse.interrupted().orElse(null)) + .turnComplete(llmResponse.turnComplete().orElse(null)) + .groundingMetadata(llmResponse.groundingMetadata().orElse(null)) + .avgLogprobs(llmResponse.avgLogprobs().orElse(null)) + .finishReason(llmResponse.finishReason().orElse(null)) + .usageMetadata(llmResponse.usageMetadata().orElse(null)) + .modelVersion(llmResponse.modelVersion().orElse(null)); Event event = eventBuilder.build(); @@ -661,7 +661,7 @@ private Event buildModelResponseEvent( Functions.getLongRunningFunctionCalls(event.functionCalls(), llmRequest.tools()); logger.debug("longRunningToolIds: {}", longRunningToolIds); if (!longRunningToolIds.isEmpty()) { - event.setLongRunningToolIds(Optional.of(longRunningToolIds)); + event.setLongRunningToolIds(longRunningToolIds); } } return event; diff --git a/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java b/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java index be0504dd4..f7c3c51ef 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java @@ -229,7 +229,7 @@ private static Flowable runPreProcessor( Event.builder() .invocationId(invocationContext.invocationId()) .author(llmAgent.name()) - .content(Optional.of(codeContent)) + .content(codeContent) .build(); return Flowable.defer( @@ -309,7 +309,7 @@ private static Flowable runPostProcessor( Event.builder() .invocationId(invocationContext.invocationId()) .author(llmAgent.name()) - .content(Optional.of(responseContent)) + .content(responseContent) .actions(EventActions.builder().build()) .build(); @@ -456,7 +456,7 @@ private static Single postProcessCodeExecutionResult( return Event.builder() .invocationId(invocationContext.invocationId()) .author(invocationContext.agent().name()) - .content(Optional.of(resultContent)) + .content(resultContent) .actions(eventActionsBuilder.build()) .build(); }); diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java index a770808d4..ca8e0a051 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java @@ -760,9 +760,7 @@ private static Event mergeFunctionResponseEvents(List functionResponseEve } return baseEvent.toBuilder() - .content( - Optional.of( - Content.builder().role(baseContent.role().get()).parts(partsInMergedEvent).build())) + .content(Content.builder().role(baseContent.role().get()).parts(partsInMergedEvent).build()) .build(); } diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Functions.java b/core/src/main/java/com/google/adk/flows/llmflows/Functions.java index 269764046..ecc2bb412 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Functions.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Functions.java @@ -122,7 +122,7 @@ public static void populateClientFunctionCallId(Event modelResponseEvent) { new IllegalStateException( "Content role is missing in event: " + modelResponseEvent.id())); Content newContent = Content.builder().role(role).parts(newParts).build(); - modelResponseEvent.setContent(Optional.of(newContent)); + modelResponseEvent.setContent(newContent); } } @@ -468,8 +468,8 @@ private static Optional mergeParallelFunctionResponseEvents( .id(Event.generateEventId()) .invocationId(baseEvent.invocationId()) .author(baseEvent.author()) - .branch(baseEvent.branch()) - .content(Optional.of(Content.builder().role("user").parts(mergedParts).build())) + .branch(baseEvent.branch().orElse(null)) + .content(Content.builder().role("user").parts(mergedParts).build()) .actions(mergedActionsBuilder.build()) .timestamp(baseEvent.timestamp()) .build()); @@ -624,7 +624,7 @@ private static Event buildResponseEvent( .id(Event.generateEventId()) .invocationId(invocationContext.invocationId()) .author(invocationContext.agent().name()) - .branch(invocationContext.branch()) + .branch(invocationContext.branch().orElse(null)) .content(Content.builder().role("user").parts(partFunctionResponse).build()) .actions(toolContext.eventActions()) .build(); @@ -684,7 +684,7 @@ public static Optional generateRequestConfirmationEvent( Event.builder() .invocationId(invocationContext.invocationId()) .author(invocationContext.agent().name()) - .branch(invocationContext.branch()) + .branch(invocationContext.branch().orElse(null)) .content(contentBuilder.build()) .longRunningToolIds(longRunningToolIds) .build()); diff --git a/core/src/main/java/com/google/adk/runner/Runner.java b/core/src/main/java/com/google/adk/runner/Runner.java index f2cb5b9d5..4371300fb 100644 --- a/core/src/main/java/com/google/adk/runner/Runner.java +++ b/core/src/main/java/com/google/adk/runner/Runner.java @@ -340,7 +340,7 @@ private Single appendNewMessageToSession( .id(Event.generateEventId()) .invocationId(invocationContext.invocationId()) .author("user") - .content(Optional.of(newMessage)); + .content(newMessage); // Add state delta if provided if (stateDelta != null && !stateDelta.isEmpty()) { @@ -540,7 +540,7 @@ private Flowable runAgentWithFreshSession( .id(Event.generateEventId()) .invocationId(contextWithUpdatedSession.invocationId()) .author("model") - .content(Optional.of(content)) + .content(content) .build()); // Agent execution diff --git a/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java b/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java index 71b072695..97cc0f56d 100644 --- a/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java +++ b/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java @@ -208,9 +208,12 @@ static Event fromApiEvent(Map apiEvent) { .timestamp(convertToInstant(apiEvent.get("timestamp")).toEpochMilli()) .errorCode( Optional.ofNullable(apiEvent.get("errorCode")) - .map(value -> new FinishReason((String) value))) + .map(value -> new FinishReason((String) value)) + .orElse(null)) .errorMessage( - Optional.ofNullable(apiEvent.get("errorMessage")).map(value -> (String) value)) + Optional.ofNullable(apiEvent.get("errorMessage")) + .map(value -> (String) value) + .orElse(null)) .build(); Map eventMetadata = (Map) apiEvent.get("eventMetadata"); if (eventMetadata != null) { @@ -236,7 +239,7 @@ static Event fromApiEvent(Map apiEvent) { Optional.ofNullable((Boolean) eventMetadata.get("turnComplete")).orElse(false)) .interrupted( Optional.ofNullable((Boolean) eventMetadata.get("interrupted")).orElse(false)) - .branch(Optional.ofNullable((String) eventMetadata.get("branch"))) + .branch((String) eventMetadata.get("branch")) .groundingMetadata(groundingMetadata) .usageMetadata(usageMetadata) .longRunningToolIds( 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 cbfb6ef0b..a4feab5c1 100644 --- a/core/src/test/java/com/google/adk/events/EventTest.java +++ b/core/src/test/java/com/google/adk/events/EventTest.java @@ -76,6 +76,7 @@ public final class EventTest { .avgLogprobs(0.5) .interrupted(true) .timestamp(123456789L) + .modelVersion("model_version") .build(); @Test @@ -99,6 +100,7 @@ public void event_builder_works() { assertThat(EVENT.interrupted()).hasValue(true); assertThat(EVENT.timestamp()).isEqualTo(123456789L); assertThat(EVENT.actions()).isEqualTo(EVENT_ACTIONS); + assertThat(EVENT.modelVersion()).hasValue("model_version"); } @Test diff --git a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java index 3041a855b..85e78666d 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java @@ -784,7 +784,7 @@ private static Event createUserEvent(String id, String text) { return Event.builder() .id(id) .author(USER) - .content(Optional.of(Content.fromParts(Part.fromText(text)))) + .content(Content.fromParts(Part.fromText(text))) .invocationId("invocationId") .build(); } @@ -794,7 +794,7 @@ private static Event createUserEvent( return Event.builder() .id(id) .author(USER) - .content(Optional.of(Content.fromParts(Part.fromText(text)))) + .content(Content.fromParts(Part.fromText(text))) .invocationId(invocationId) .timestamp(timestamp) .build(); diff --git a/core/src/test/java/com/google/adk/flows/llmflows/FunctionsTest.java b/core/src/test/java/com/google/adk/flows/llmflows/FunctionsTest.java index 97092f68c..d5db4d4b3 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/FunctionsTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/FunctionsTest.java @@ -33,7 +33,6 @@ import com.google.genai.types.FunctionCall; import com.google.genai.types.FunctionResponse; import com.google.genai.types.Part; -import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -43,12 +42,7 @@ public final class FunctionsTest { private static final Event EVENT_WITH_NO_CONTENT = - Event.builder() - .id("event1") - .invocationId("invocation1") - .author("agent") - .content(Optional.empty()) - .build(); + Event.builder().id("event1").invocationId("invocation1").author("agent").build(); private static final Event EVENT_WITH_NO_PARTS = Event.builder() diff --git a/core/src/test/java/com/google/adk/plugins/LoggingPluginTest.java b/core/src/test/java/com/google/adk/plugins/LoggingPluginTest.java index 764525ff0..a08599c9a 100644 --- a/core/src/test/java/com/google/adk/plugins/LoggingPluginTest.java +++ b/core/src/test/java/com/google/adk/plugins/LoggingPluginTest.java @@ -65,9 +65,7 @@ public class LoggingPluginTest { Event.builder() .id("event_id") .author("author") - .content(Optional.empty()) .actions(EventActions.builder().build()) - .longRunningToolIds(Optional.empty()) .build(); private final LlmRequest.Builder llmRequestBuilder = LlmRequest.builder().model("default").contents(ImmutableList.of()); diff --git a/core/src/test/java/com/google/adk/sessions/SessionJsonConverterTest.java b/core/src/test/java/com/google/adk/sessions/SessionJsonConverterTest.java index f6120cf08..335f7f1d0 100644 --- a/core/src/test/java/com/google/adk/sessions/SessionJsonConverterTest.java +++ b/core/src/test/java/com/google/adk/sessions/SessionJsonConverterTest.java @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.junit.Test; @@ -49,12 +48,12 @@ public void convertEventToJson_fullEvent_success() throws JsonProcessingExceptio .author("user") .invocationId("inv-123") .timestamp(Instant.parse("2023-01-01T00:00:00Z").toEpochMilli()) - .errorCode(Optional.of(new FinishReason("OTHER"))) - .errorMessage(Optional.of("Something was not found")) + .errorCode(new FinishReason("OTHER")) + .errorMessage("Something was not found") .partial(true) .turnComplete(true) .interrupted(false) - .branch(Optional.of("branch-1")) + .branch("branch-1") .content(Content.fromParts(Part.fromText("Hello"))) .actions(actions) .build(); From 2291ad7f329e31aa56c3277d5aa2f68db63c598b Mon Sep 17 00:00:00 2001 From: Sandeep Belgavi Date: Sat, 7 Mar 2026 13:38:32 +0530 Subject: [PATCH 60/61] Fix build after upstream merge: restore custom dependencies and adapt to API changes - Restore custom dependencies in core/pom.xml (org.json, mapdb, cassandra, mongodb, redis, kafka, HikariCP, anthropic-bedrock, etc.) - Restore gRPC dependencies and protobuf build plugins in a2a/pom.xml - Restore deleted ConversationPreprocessor.java and RequestConverter.java - Add missing eventsToMessage/eventToMessage methods to ResponseConverter - Fix RequestConverter to use A2ADataPartMetadataType enum instead of removed PartConverter constants - Fix MediaSupportTest for new fromGenaiPart(Part, boolean) signature - Fix HttpServerSseController to use getNewMessage() instead of newMessage Made-with: Cursor --- a2a/pom.xml | 107 ++++++++++ .../converters/ConversationPreprocessor.java | 108 ++++++++++ .../adk/a2a/converters/RequestConverter.java | 198 ++++++++++++++++++ .../adk/a2a/converters/ResponseConverter.java | 68 ++++++ .../google/adk/a2a/grpc/MediaSupportTest.java | 32 +-- core/pom.xml | 88 ++++++++ .../httpserver/HttpServerSseController.java | 6 +- 7 files changed, 590 insertions(+), 17 deletions(-) create mode 100644 a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java create mode 100644 a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java diff --git a/a2a/pom.xml b/a2a/pom.xml index 5857720fd..a3a7096ea 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -19,8 +19,11 @@ 0.3.2.Final ${project.version} 33.0.0-jre + 3.1.5 2.19.0 1.0.0 + 2.38.0 + 1.62.2 2.0.17 1.4.4 4.13.2 @@ -32,6 +35,27 @@ google-adk ${google.adk.version} + + io.grpc + grpc-netty-shaded + ${grpc.version} + runtime + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + com.google.code.gson + gson + 2.10.1 + com.google.guava guava @@ -40,6 +64,7 @@ com.google.errorprone error_prone_annotations + ${errorprone.version} com.fasterxml.jackson.core @@ -64,6 +89,7 @@ io.reactivex.rxjava3 rxjava + ${rxjava.version} org.jspecify @@ -112,8 +138,30 @@ mockito-core test + + org.junit.jupiter + junit-jupiter-api + test + + + org.mockito + mockito-junit-jupiter + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + kr.motd.maven + os-maven-plugin + 1.7.0 + + org.apache.maven.plugins @@ -123,6 +171,65 @@ ${java.version} + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + + + integration-test + verify + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + + com.google.protobuf:protoc:3.21.7:exe:${os.detected.classifier} + + grpc-java + + io.grpc:protoc-gen-grpc-java:1.48.1:exe:${os.detected.classifier} + + + + + + compile + compile-custom + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + generate-sources + + add-source + + + + target/generated-sources/protobuf/java + target/generated-sources/protobuf/grpc-java + + + + + diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java b/a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java new file mode 100644 index 000000000..11bbbd326 --- /dev/null +++ b/a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java @@ -0,0 +1,108 @@ +package com.google.adk.a2a.converters; + +import com.google.adk.events.Event; +import com.google.common.collect.ImmutableList; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import java.util.List; +import java.util.Optional; + +/** + * Preprocesses a batch of ADK events prior to invoking a remote A2A agent. + * + *

The class splits the conversation into two logical buckets: + * + *

    + *
  • The historical session events that should be preserved as-is when relayed over the wire. + *
  • The most recent user-authored text event, surfaced separately so it can be supplied as the + * pending user input on the {@link com.google.adk.agents.InvocationContext}. + *
+ * + *

This mirrors the Python A2A implementation where the in-flight user message is maintained + * separately from the persisted transcript. + * + *

**EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not + * use in production code. + */ +public final class ConversationPreprocessor { + + /** + * Immutable value that surfaces the results of preprocessing. + * + *

All fields are deliberately exposed to avoid additional AutoValue dependencies in this + * internal module. + */ + public static final class PreparedInput { + /** Historical events that should remain in the session transcript. */ + public final ImmutableList historyEvents; + + /** Extracted user message content, if a qualifying text event was found. */ + public final Optional userContent; + + /** The concrete event that supplied {@link #userContent}, for callers needing metadata. */ + public final Optional userEvent; + + /** + * Creates a new instance. + * + * @param historyEvents ordered historical events retained in the session stream + * @param userContent optional content to place on the pending user message + * @param userEvent optional original event that contained {@code userContent} + */ + public PreparedInput( + ImmutableList historyEvents, + Optional userContent, + Optional userEvent) { + this.historyEvents = historyEvents; + this.userContent = userContent; + this.userEvent = userEvent; + } + } + + private ConversationPreprocessor() {} + + /** + * Splits the provided event list into history and the latest user-authored text message. + * + * @param inputEvents ordered session events, oldest to newest; may be {@code null} + * @return container encapsulating the derived history, optional user content, and the original + * user event when present + */ + public static PreparedInput extractHistoryAndUserContent(List inputEvents) { + if (inputEvents == null || inputEvents.isEmpty()) { + return new PreparedInput(ImmutableList.of(), Optional.empty(), Optional.empty()); + } + + Content userContent = null; + int lastTextIndex = -1; + Event userEvent = null; + for (int i = inputEvents.size() - 1; i >= 0; i--) { + Event ev = inputEvents.get(i); + if (ev.content().isPresent() && ev.content().get().parts().isPresent()) { + boolean hasText = false; + for (Part p : ev.content().get().parts().get()) { + if (p.text().isPresent()) { + hasText = true; + break; + } + } + if (hasText) { + userContent = ev.content().get(); + lastTextIndex = i; + userEvent = ev; + break; + } + } + } + + ImmutableList.Builder historyBuilder = ImmutableList.builder(); + for (int i = 0; i < inputEvents.size(); i++) { + if (i != lastTextIndex) { + historyBuilder.add(inputEvents.get(i)); + } + } + + return new PreparedInput( + historyBuilder.build(), Optional.ofNullable(userContent), Optional.ofNullable(userEvent)); + } +} diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java new file mode 100644 index 000000000..b289ced6c --- /dev/null +++ b/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java @@ -0,0 +1,198 @@ +package com.google.adk.a2a.converters; + +import com.google.adk.events.Event; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.genai.types.Content; +import io.a2a.spec.DataPart; +import io.a2a.spec.Message; +import io.a2a.spec.Part; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * rfe Converter for A2A Messages to ADK Events. This is used on the A2A service side to convert + * incoming A2A requests to ADK Events. + * + *

**EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not + * use in production code. + */ +public final class RequestConverter { + private static final Logger logger = LoggerFactory.getLogger(RequestConverter.class); + + private RequestConverter() {} + + /** + * Convert an A2A Message to an ADK Event. This is used when the A2A service receives a request + * and needs to process it with ADK. + * + * @param message The A2A message to convert. + * @param invocationId The invocation ID for the event. + * @return Optional containing the converted ADK Event, or empty if conversion fails. + */ + public static Optional convertA2aMessageToAdkEvent(Message message, String invocationId) { + if (message == null) { + // Create an empty user message event + logger.info("Null message received, creating empty user event"); + Event event = + Event.builder() + .id(UUID.randomUUID().toString()) + .invocationId(invocationId != null ? invocationId : UUID.randomUUID().toString()) + .author("user") + .content( + Content.builder() + .role("user") + .parts( + ImmutableList.of(com.google.genai.types.Part.builder().text("").build())) + .build()) + .timestamp(Instant.now().toEpochMilli()) + .build(); + return Optional.of(event); + } + + List genaiParts = new ArrayList<>(); + + // Convert each A2A Part to GenAI Part + if (message.getParts() != null) { + for (Part a2aPart : message.getParts()) { + Optional genaiPart = PartConverter.toGenaiPart(a2aPart); + genaiPart.ifPresent(genaiParts::add); + } + } + + if (genaiParts.isEmpty()) { + logger.warn("No convertible parts found in A2A message"); + return Optional.empty(); + } + + // Treat inbound A2A requests as user input for the ADK agent. + String author = "user"; + + // Build the Content object + Content content = Content.builder().role("user").parts(genaiParts).build(); + + // Build the Event + Event event = + Event.builder() + .id( + !message.getMessageId().isEmpty() + ? message.getMessageId() + : UUID.randomUUID().toString()) + .invocationId(invocationId != null ? invocationId : UUID.randomUUID().toString()) + .author(author) + .content(content) + .timestamp(Instant.now().toEpochMilli()) + .build(); + + return Optional.of(event); + } + + /** + * Convert an aggregated A2A Message to multiple ADK Events. This reconstructs the original event + * sequence from an aggregated message. + * + * @param message The aggregated A2A message to convert. + * @param invocationId The invocation ID for the events. + * @return List of ADK Events representing the conversation history. + */ + public static ImmutableList convertAggregatedA2aMessageToAdkEvents( + Message message, String invocationId) { + if (message == null || message.getParts() == null || message.getParts().isEmpty()) { + logger.info("Null or empty message received, creating empty user event"); + Event event = + Event.builder() + .id(UUID.randomUUID().toString()) + .invocationId(invocationId != null ? invocationId : UUID.randomUUID().toString()) + .author("user") + .content( + Content.builder() + .role("user") + .parts( + ImmutableList.of(com.google.genai.types.Part.builder().text("").build())) + .build()) + .timestamp(Instant.now().toEpochMilli()) + .build(); + return ImmutableList.of(event); + } + + List events = new ArrayList<>(); + + // Emit exactly one ADK Event per A2A Part, preserving order. + for (Part a2aPart : message.getParts()) { + Optional genaiPart = PartConverter.toGenaiPart(a2aPart); + if (genaiPart.isEmpty()) { + continue; + } + + String author = extractAuthorFromMetadata(a2aPart); + String role = determineRoleFromAuthor(author); + + events.add(createEvent(ImmutableList.of(genaiPart.get()), author, role, invocationId)); + } + + if (events.isEmpty()) { + logger.warn("No events created from aggregated message; returning single empty user event"); + Event event = + Event.builder() + .id(UUID.randomUUID().toString()) + .invocationId(invocationId) + .author("user") + .content( + Content.builder() + .role("user") + .parts( + ImmutableList.of(com.google.genai.types.Part.builder().text("").build())) + .build()) + .timestamp(Instant.now().toEpochMilli()) + .build(); + events.add(event); + } + + logger.info("Converted aggregated A2A message to {} ADK events", events.size()); + return ImmutableList.copyOf(events); + } + + private static String extractAuthorFromMetadata(Part a2aPart) { + if (a2aPart instanceof DataPart dataPart) { + Map metadata = + Optional.ofNullable(dataPart.getMetadata()).orElse(ImmutableMap.of()); + String type = + metadata.getOrDefault(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, "").toString(); + if (type.equals(A2ADataPartMetadataType.FUNCTION_CALL.getType())) { + return "model"; + } + if (type.equals(A2ADataPartMetadataType.FUNCTION_RESPONSE.getType())) { + return "user"; + } + Map data = Optional.ofNullable(dataPart.getData()).orElse(ImmutableMap.of()); + if (data.containsKey("args")) { + return "model"; + } + if (data.containsKey("response")) { + return "user"; + } + } + return "user"; + } + + private static String determineRoleFromAuthor(String author) { + return author.equals("model") ? "model" : "user"; + } + + private static Event createEvent( + List parts, String author, String role, String invocationId) { + return Event.builder() + .id(UUID.randomUUID().toString()) + .invocationId(invocationId) + .author(author) + .content(Content.builder().role(role).parts(new ArrayList<>(parts)).build()) + .timestamp(Instant.now().toEpochMilli()) + .build(); + } +} diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java index 57a84b58f..6d73a4482 100644 --- a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java +++ b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java @@ -19,7 +19,9 @@ import io.a2a.spec.TaskArtifactUpdateEvent; import io.a2a.spec.TaskState; import io.a2a.spec.TaskStatusUpdateEvent; +import io.a2a.spec.TextPart; import java.time.Instant; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -203,4 +205,70 @@ private static Event.Builder remoteAgentEventBuilder(InvocationContext invocatio .branch(invocationContext.branch().orElse(null)) .timestamp(Instant.now().toEpochMilli()); } + + private static Message emptyAgentMessage(String contextId) { + Message.Builder builder = + new Message.Builder() + .messageId(UUID.randomUUID().toString()) + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart(""))); + if (contextId != null) { + builder.contextId(contextId); + } + return builder.build(); + } + + /** Converts a list of ADK events into a single aggregated A2A message. */ + public static Message eventsToMessage(List events, String contextId, String taskId) { + if (events == null || events.isEmpty()) { + return emptyAgentMessage(contextId); + } + + if (events.size() == 1) { + return eventToMessage(events.get(0), contextId); + } + + List> parts = new ArrayList<>(); + for (Event event : events) { + parts.addAll(eventParts(event)); + } + + Message.Builder builder = + new Message.Builder() + .messageId(taskId != null ? taskId : UUID.randomUUID().toString()) + .role(Message.Role.AGENT) + .parts(parts); + if (contextId != null) { + builder.contextId(contextId); + } + return builder.build(); + } + + /** Converts a single ADK event into an A2A message. */ + public static Message eventToMessage(Event event, String contextId) { + List> parts = eventParts(event); + + Message.Builder builder = + new Message.Builder() + .messageId(event.id() != null ? event.id() : UUID.randomUUID().toString()) + .role(event.author().equalsIgnoreCase("user") ? Message.Role.USER : Message.Role.AGENT) + .parts(parts); + if (contextId != null) { + builder.contextId(contextId); + } + return builder.build(); + } + + private static List> eventParts(Event event) { + List> parts = new ArrayList<>(); + Optional content = event.content(); + if (content.isEmpty() || content.get().parts().isEmpty()) { + return parts; + } + + for (com.google.genai.types.Part genaiPart : content.get().parts().get()) { + parts.add(PartConverter.fromGenaiPart(genaiPart, false)); + } + return parts; + } } diff --git a/a2a/src/test/java/com/google/adk/a2a/grpc/MediaSupportTest.java b/a2a/src/test/java/com/google/adk/a2a/grpc/MediaSupportTest.java index b9fa148d3..a8019be30 100644 --- a/a2a/src/test/java/com/google/adk/a2a/grpc/MediaSupportTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/grpc/MediaSupportTest.java @@ -44,11 +44,11 @@ void testTextPart_conversion() { // GenAI Part to A2A TextPart Part genaiTextPart = Part.builder().text("Hello, world!").build(); - Optional> a2aPart = PartConverter.fromGenaiPart(genaiTextPart); + io.a2a.spec.Part a2aPart = PartConverter.fromGenaiPart(genaiTextPart, false); - assertThat(a2aPart).isPresent(); - assertThat(a2aPart.get()).isInstanceOf(TextPart.class); - assertThat(((TextPart) a2aPart.get()).getText()).isEqualTo("Hello, world!"); + assertThat(a2aPart).isNotNull(); + assertThat(a2aPart).isInstanceOf(TextPart.class); + assertThat(((TextPart) a2aPart).getText()).isEqualTo("Hello, world!"); } @Test @@ -158,11 +158,11 @@ void testGenAIImagePart_toA2A() { .build()) .build(); - Optional> a2aPart = PartConverter.fromGenaiPart(genaiImagePart); + io.a2a.spec.Part a2aPart = PartConverter.fromGenaiPart(genaiImagePart, false); - assertThat(a2aPart).isPresent(); - assertThat(a2aPart.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) a2aPart.get(); + assertThat(a2aPart).isNotNull(); + assertThat(a2aPart).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) a2aPart; assertThat(filePart.getFile()).isInstanceOf(FileWithUri.class); FileWithUri fileWithUri = (FileWithUri) filePart.getFile(); assertThat(fileWithUri.uri()).isEqualTo("https://example.com/image.jpg"); @@ -183,11 +183,11 @@ void testGenAIAudioPart_toA2A() { .build()) .build(); - Optional> a2aPart = PartConverter.fromGenaiPart(genaiAudioPart); + io.a2a.spec.Part a2aPart = PartConverter.fromGenaiPart(genaiAudioPart, false); - assertThat(a2aPart).isPresent(); - assertThat(a2aPart.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) a2aPart.get(); + assertThat(a2aPart).isNotNull(); + assertThat(a2aPart).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) a2aPart; assertThat(filePart.getFile()).isInstanceOf(FileWithBytes.class); FileWithBytes fileWithBytes = (FileWithBytes) filePart.getFile(); assertThat(fileWithBytes.mimeType()).isEqualTo("audio/wav"); @@ -207,11 +207,11 @@ void testGenAIVideoPart_toA2A() { .build()) .build(); - Optional> a2aPart = PartConverter.fromGenaiPart(genaiVideoPart); + io.a2a.spec.Part a2aPart = PartConverter.fromGenaiPart(genaiVideoPart, false); - assertThat(a2aPart).isPresent(); - assertThat(a2aPart.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) a2aPart.get(); + assertThat(a2aPart).isNotNull(); + assertThat(a2aPart).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) a2aPart; assertThat(filePart.getFile()).isInstanceOf(FileWithUri.class); FileWithUri fileWithUri = (FileWithUri) filePart.getFile(); assertThat(fileWithUri.mimeType()).isEqualTo("video/mp4"); diff --git a/core/pom.xml b/core/pom.xml index a0f843f56..1fa1b3689 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -159,6 +159,11 @@ io.reactivex.rxjava3 rxjava + + org.json + json + 20240303 + io.projectreactor reactor-core @@ -193,6 +198,89 @@ opentelemetry-sdk-testing test + + com.zaxxer + HikariCP + 5.1.0 + + + org.postgresql + postgresql + 42.7.3 + + + org.mongodb + mongodb-driver-sync + 4.10.2 + + + org.mapdb + mapdb + 3.0.8 + + + com.datastax.oss + java-driver-core + 4.17.0 + + + com.fasterxml.jackson.datatype + jackson-datatype-guava + ${jackson.version} + + + com.anthropic + anthropic-java-bedrock + 1.4.0 + + + org.testcontainers + cassandra + 1.19.7 + test + + + org.testcontainers + junit-jupiter + 1.19.7 + test + + + io.lettuce + lettuce-core + 6.3.2.RELEASE + + + io.projectreactor.addons + reactor-adapter + 3.5.1 + + + org.ini4j + ini4j + 0.5.4 + + + redis.clients + jedis + 6.0.0 + + + org.apache.kafka + kafka-clients + 3.3.1 + + + net.javacrumbs.future-converter + future-converter-java8-guava + 1.2.0 + + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + diff --git a/dev/src/main/java/com/google/adk/web/controller/httpserver/HttpServerSseController.java b/dev/src/main/java/com/google/adk/web/controller/httpserver/HttpServerSseController.java index 7777dfb95..767590da9 100644 --- a/dev/src/main/java/com/google/adk/web/controller/httpserver/HttpServerSseController.java +++ b/dev/src/main/java/com/google/adk/web/controller/httpserver/HttpServerSseController.java @@ -172,7 +172,11 @@ private void streamEvents( // Get event stream io.reactivex.rxjava3.core.Flowable eventFlowable = runner.runAsync( - request.userId, request.sessionId, request.newMessage, runConfig, request.stateDelta); + request.userId, + request.sessionId, + request.getNewMessage(), + runConfig, + request.stateDelta); // Use CountDownLatch to wait for stream completion java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1); From a2edac0c8c290ea1a11a9cf8c1fc053aa7969a27 Mon Sep 17 00:00:00 2001 From: Sandeep Belgavi Date: Sat, 7 Mar 2026 13:56:25 +0530 Subject: [PATCH 61/61] Fix gRPC version mismatch causing A2A integration test failures Upgraded grpc.version from 1.62.2 to 1.76.2 to match the version pulled transitively by google-cloud-speech. The old version caused NoSuchMethodError in grpc-netty-shaded due to incompatible Http2ClientStreamTransportState constructor signature. Also aligned protoc-gen-grpc-java plugin version to use the same property. Made-with: Cursor --- a2a/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/a2a/pom.xml b/a2a/pom.xml index a3a7096ea..9e498250a 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -23,7 +23,7 @@ 2.19.0 1.0.0 2.38.0 - 1.62.2 + 1.76.2 2.0.17 1.4.4 4.13.2 @@ -199,7 +199,7 @@ grpc-java - io.grpc:protoc-gen-grpc-java:1.48.1:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}