diff --git a/.github/workflows/pr-commit-check.yml b/.github/workflows/pr-commit-check.yml
index ec6644311..1e31e42f3 100644
--- a/.github/workflows/pr-commit-check.yml
+++ b/.github/workflows/pr-commit-check.yml
@@ -21,7 +21,7 @@ jobs:
# Step 1: Check out the code
# This action checks out your repository under $GITHUB_WORKSPACE, so your workflow can access it.
- name: Checkout Code
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
# We need to fetch all commits to accurately count them.
# '0' means fetch all history for all branches and tags.
diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml
index d9035a579..65e66f8fd 100644
--- a/.github/workflows/validation.yml
+++ b/.github/workflows/validation.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Set up Java ${{ matrix.java-version }}
uses: actions/setup-java@v4
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index d6a5f76bd..b0f3ba770 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,4 @@
{
- ".": "0.8.0"
+ ".": "0.9.0"
}
+
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..5d33d2172
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,3 @@
+# AGENTS.md
+
+Validate changes by running `./mvnw test`.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6d5b9e5eb..ab111e90c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,61 @@
# Changelog
+## [0.9.0](https://github.com/google/adk-java/compare/v0.8.0...v0.9.0) (2026-03-13)
+
+
+### ⚠ BREAKING CHANGES
+
+* refactor ApiClient constructors hierarchy to remove Optional parameters
+* remove deprecated LlmAgent.canonicalTools method
+* remove deprecated LoadArtifactsTool.loadArtifacts method
+* update LoopAgent's maxIteration field and methods to be @Nullable instead of Optional
+* Remove Optional parameters in EventActions
+* remove deprecated url method in ComputerState.Builder
+* Remove deprecated create method in ResponseProcessor
+* remove McpAsyncToolset constructors
+* use @Nullable fields in Event class
+* remove methods with Optional params from VertexCredential.Builder
+
+### Features
+
+* add formatting to the RemoteA2A agent so it filters out the previous agent responses and updates the context of the function calls and responses ([0d6dd55](https://github.com/google/adk-java/commit/0d6dd55f4870007e79db23e21bd261879dbfba79))
+* add multiple LLM responses to LLM recordings for conformance tests ([bdfb7a7](https://github.com/google/adk-java/commit/bdfb7a72188ce6e72c12c16c0abedb824b846160))
+* add support for gemini models in VertexAiRagRetrieval ([924fb71](https://github.com/google/adk-java/commit/924fb7174855b46a58be43373c1a29284c47dfa8))
+* Fixing the spans produced by agent calls to have the right parent spans ([3c8f488](https://github.com/google/adk-java/commit/3c8f4886f0e4c76abdbeb64a348bfccd5c16120e))
+* Fixing the spans produced by agent calls to have the right parent spans ([973f887](https://github.com/google/adk-java/commit/973f88743cabebcd2e6e7a8d5f141142b596dbbb))
+* refactor ApiClient constructors hierarchy to remove Optional parameters ([910d727](https://github.com/google/adk-java/commit/910d727f1981498151dea4cb91b9e5836f91e3ba))
+* Remove deprecated create method in ResponseProcessor ([5e1e1d4](https://github.com/google/adk-java/commit/5e1e1d434fa1f3931af30194422800757de96cb6))
+* remove deprecated LlmAgent.canonicalTools method ([aabf15a](https://github.com/google/adk-java/commit/aabf15a526ba525cdb47c74c246c178eff1851d5))
+* remove deprecated LoadArtifactsTool.loadArtifacts method ([bc38558](https://github.com/google/adk-java/commit/bc385589057a6daf0209a335280bf19d20b2126b))
+* remove deprecated url method in ComputerState.Builder ([a86ede0](https://github.com/google/adk-java/commit/a86ede007c3442ed73ee08a5c6ad0e2efa12998a))
+* remove executionId method that takes Optional param from CodeExecutionUtils ([be3b3f8](https://github.com/google/adk-java/commit/be3b3f8360888ea1f13796969bb19893c32727e0))
+* remove McpAsyncToolset constructors ([82ef5ac](https://github.com/google/adk-java/commit/82ef5ac2689e01676aa95d2616e3b4d8463e573e))
+* remove methods with Optional params from VertexCredential.Builder ([0b9057c](https://github.com/google/adk-java/commit/0b9057c9ccab98ea58597ec55b8168e32ac7c9a6))
+* Remove Optional parameters in EventActions ([b8316b1](https://github.com/google/adk-java/commit/b8316b1944ce17cc9208963cc09d900c379444c6))
+* replace Optional type of version in BaseArtifactService.loadArtifact with Nullable ([5fd4c53](https://github.com/google/adk-java/commit/5fd4c53c88e977d004b9eee8fa3697625ec85f47))
+* Trigger traceCallLlm to set call_llm attributes before span ends ([d9d84ee](https://github.com/google/adk-java/commit/d9d84ee67406cce8eeb66abcf1be24fad9c58e29))
+* Update converters for task and artifact events; add long running tools ids ([9ce78d7](https://github.com/google/adk-java/commit/9ce78d7c3e1b0fb6d8d4fdce9052a572ffb9e515))
+* update LoopAgent's maxIteration field and methods to be @Nullable instead of Optional ([e0d833b](https://github.com/google/adk-java/commit/e0d833b337e958e299d0d11a03f6bfa1468731bc))
+* update return type for artifactDelta getter and setter to Map from ConcurrentMap ([d1d5539](https://github.com/google/adk-java/commit/d1d5539ef763b6bfd5057c6ea0f2591225a98535))
+* update return type for requestedToolConfirmations getter and setter to Map from ConcurrentMap ([143b656](https://github.com/google/adk-java/commit/143b656949d61363d135e0b74ef5696e78eb270a))
+* update return type for stateDelta() to Map from ConcurrentMap ([3f6504e](https://github.com/google/adk-java/commit/3f6504e9416f9f644ef431e612ec983b9a2edd9d))
+* update State constructors to accept general Map types ([c6fdb63](https://github.com/google/adk-java/commit/c6fdb63c92e2f3481a01cfeafa946b6dce728c51))
+* use @Nullable fields in Event class ([67b602f](https://github.com/google/adk-java/commit/67b602f245f564238ea22298a37bf70049e56a12))
+
+
+### Bug Fixes
+
+* Explicitly setting the otel parent spans in agents, llm flow and function calls ([20f863f](https://github.com/google/adk-java/commit/20f863f716f653979551c481d85d4e7fa56a35da))
+* Make sure that `InvocationContext.callbackContextData` remains the same instance ([14ee28b](https://github.com/google/adk-java/commit/14ee28ba593a9f6f5f7b9bb6003441539fe33a18))
+* Removing deprecated InvocationContext methods ([41f5af0](https://github.com/google/adk-java/commit/41f5af0dceb78501ca8b94e434e4d751f608a699))
+* Removing deprecated methods in Runner ([0d8e22d](https://github.com/google/adk-java/commit/0d8e22d6e9fe4e8d29c87d485915ba51a22eb350))
+* Removing deprecated methods in Runner ([b857f01](https://github.com/google/adk-java/commit/b857f010a0f51df0eb25ecdc364465ffdd9fef65))
+
+
+### Miscellaneous Chores
+
+* override new version to 0.9.0 ([a47b651](https://github.com/google/adk-java/commit/a47b651b5c4868a603fd79df164b70bc712c3a80))
+
## [0.8.0](https://github.com/google/adk-java/compare/v0.7.0...v0.8.0) (2026-03-06)
diff --git a/README.md b/README.md
index 4a5dab81f..de1cfbef7 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.8.0
+ 0.9.0
com.google.adk
google-adk-dev
- 0.8.0
+ 0.9.0
```
diff --git a/a2a/pom.xml b/a2a/pom.xml
index 5857720fd..a2f9d9456 100644
--- a/a2a/pom.xml
+++ b/a2a/pom.xml
@@ -5,7 +5,7 @@
com.google.adk
google-adk-parent
- 0.8.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
google-adk-a2a
diff --git a/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java b/a2a/src/main/java/com/google/adk/a2a/agent/RemoteA2AAgent.java
similarity index 89%
rename from a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java
rename to a2a/src/main/java/com/google/adk/a2a/agent/RemoteA2AAgent.java
index b391f2985..ccb662b7c 100644
--- a/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java
+++ b/a2a/src/main/java/com/google/adk/a2a/agent/RemoteA2AAgent.java
@@ -1,4 +1,19 @@
-package com.google.adk.a2a;
+/*
+ * Copyright 2026 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.a2a.agent;
import static com.google.common.base.Strings.nullToEmpty;
@@ -44,26 +59,21 @@
import org.slf4j.LoggerFactory;
/**
- * Agent that communicates with a remote A2A agent via A2A client.
+ * Agent that communicates with a remote A2A agent via an A2A client.
*
- *
This agent supports multiple ways to specify the remote agent:
+ *
The remote agent can be specified directly by providing an {@link AgentCard} to the builder,
+ * or it can be resolved automatically using the provided A2A client.
*
- *
- * - Direct AgentCard object
- *
- URL to agent card JSON
- *
- File path to agent card JSON
- *
- *
- * The agent handles:
+ *
Key responsibilities of this agent include:
*
*
* - Agent card resolution and validation
- *
- A2A message conversion and error handling
- *
- Session state management across requests
+ *
- Converting ADK session history events into A2A requests ({@link io.a2a.spec.Message})
+ *
- Handling streaming and non-streaming responses from the A2A client
+ *
- Buffering and aggregating streamed response chunks into ADK {@link
+ * com.google.adk.events.Event}s
+ *
- Converting A2A client responses back into ADK format
*
- *
- * **EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not
- * use in production code.
*/
public class RemoteA2AAgent extends BaseAgent {
@@ -171,6 +181,25 @@ public RemoteA2AAgent build() {
}
}
+ private Message.Builder newA2AMessage(Message.Role role, List> parts) {
+ return new Message.Builder().messageId(UUID.randomUUID().toString()).role(role).parts(parts);
+ }
+
+ private Message prepareMessage(InvocationContext invocationContext) {
+ Event userCall = EventConverter.findUserFunctionCall(invocationContext.session().events());
+ if (userCall != null) {
+ ImmutableList> parts =
+ EventConverter.contentToParts(userCall.content(), userCall.partial().orElse(false));
+ return newA2AMessage(Message.Role.USER, parts)
+ .taskId(EventConverter.taskId(userCall))
+ .contextId(EventConverter.contextId(userCall))
+ .build();
+ }
+ return newA2AMessage(
+ Message.Role.USER, EventConverter.messagePartsFromContext(invocationContext))
+ .build();
+ }
+
@Override
protected Flowable runAsyncImpl(InvocationContext invocationContext) {
// Construct A2A Message from the last ADK event
@@ -181,14 +210,7 @@ protected Flowable runAsyncImpl(InvocationContext invocationContext) {
return Flowable.empty();
}
- Optional a2aMessageOpt = EventConverter.convertEventsToA2AMessage(invocationContext);
-
- if (a2aMessageOpt.isEmpty()) {
- logger.warn("Failed to convert event to A2A message.");
- return Flowable.empty();
- }
-
- Message originalMessage = a2aMessageOpt.get();
+ Message originalMessage = prepareMessage(invocationContext);
String requestJson = serializeMessageToJson(originalMessage);
return Flowable.create(
diff --git a/a2a/src/main/java/com/google/adk/a2a/common/A2AClientError.java b/a2a/src/main/java/com/google/adk/a2a/common/A2AClientError.java
index 8e8282742..466c89223 100644
--- a/a2a/src/main/java/com/google/adk/a2a/common/A2AClientError.java
+++ b/a2a/src/main/java/com/google/adk/a2a/common/A2AClientError.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2026 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.a2a.common;
/** Exception thrown when the A2A client encounters an error. */
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
index 5c75faeac..a5faeff2a 100644
--- a/a2a/src/main/java/com/google/adk/a2a/common/A2AMetadata.java
+++ b/a2a/src/main/java/com/google/adk/a2a/common/A2AMetadata.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2026 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.a2a.common;
/** Constants and utilities for A2A metadata keys. */
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
index a5947dcb8..0ac56fc01 100644
--- a/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java
+++ b/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2026 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.a2a.common;
/** Exception thrown when the the genai class has an empty field. */
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
index b5b53c49a..e0e97c8e9 100644
--- a/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2026 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.a2a.converters;
/** Enum for the type of A2A DataPart metadata. */
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 1a49b0070..71573070e 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
@@ -1,73 +1,124 @@
+/*
+ * Copyright 2026 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.a2a.converters;
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.common.collect.Iterables;
import com.google.genai.types.Content;
-import io.a2a.spec.Message;
+import com.google.genai.types.FunctionResponse;
import io.a2a.spec.Part;
import java.util.Collection;
+import java.util.List;
import java.util.Optional;
import java.util.UUID;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.jspecify.annotations.Nullable;
-/**
- * Converter for ADK Events to A2A Messages.
- *
- * **EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not
- * use in production code.
- */
+/** Converter for ADK Events to A2A Messages. */
public final class EventConverter {
- private static final Logger logger = LoggerFactory.getLogger(EventConverter.class);
+ public static final String ADK_TASK_ID_KEY = "adk_task_id";
+ public static final String ADK_CONTEXT_ID_KEY = "adk_context_id";
private EventConverter() {}
/**
- * Converts an ADK InvocationContext to an A2A Message.
+ * Returns the task ID from the event.
*
- *
It combines all the events in the session, plus the user content, converted into A2A Parts,
- * into a single A2A Message.
+ *
Task ID is stored in the event's custom metadata with the key {@link #ADK_TASK_ID_KEY}.
*
- *
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.
+ * @param event The event to get the task ID from.
+ * @return The task ID, or an empty string if not found.
*/
- public static Optional convertEventsToA2AMessage(InvocationContext context) {
- if (context.session().events().isEmpty()) {
- logger.warn("No events in session, cannot convert to A2A message.");
- return Optional.empty();
- }
-
- ImmutableList.Builder> partsBuilder = ImmutableList.builder();
+ public static String taskId(Event event) {
+ return metadataValue(event, ADK_TASK_ID_KEY);
+ }
- context
- .session()
- .events()
- .forEach(
- event ->
- partsBuilder.addAll(
- contentToParts(event.content(), event.partial().orElse(false))));
- partsBuilder.addAll(contentToParts(context.userContent(), false));
+ /**
+ * Returns the context ID from the event.
+ *
+ * Context ID is stored in the event's custom metadata with the key {@link
+ * #ADK_CONTEXT_ID_KEY}.
+ *
+ * @param event The event to get the context ID from.
+ * @return The context ID, or an empty string if not found.
+ */
+ public static String contextId(Event event) {
+ return metadataValue(event, ADK_CONTEXT_ID_KEY);
+ }
- ImmutableList> parts = partsBuilder.build();
+ /**
+ * Returns the last user function call event from the list of events.
+ *
+ * @param events The list of events to find the user function call event from.
+ * @return The user function call event, or null if not found.
+ */
+ public static @Nullable Event findUserFunctionCall(List events) {
+ Event candidate = Iterables.getLast(events);
+ if (!candidate.author().equals("user")) {
+ return null;
+ }
+ FunctionResponse functionResponse = findUserFunctionResponse(candidate);
+ if (functionResponse == null || functionResponse.id().isEmpty()) {
+ return null;
+ }
+ for (int i = events.size() - 2; i >= 0; i--) {
+ Event event = events.get(i);
+ if (isUserFunctionCall(event, functionResponse.id().get())) {
+ return event;
+ }
+ }
+ return null;
+ }
- if (parts.isEmpty()) {
- logger.warn("No suitable content found to build A2A request message.");
- return Optional.empty();
+ private static @Nullable FunctionResponse findUserFunctionResponse(Event candidate) {
+ if (candidate.content().isEmpty() || candidate.content().get().parts().isEmpty()) {
+ return null;
}
+ return candidate.content().get().parts().get().stream()
+ .filter(part -> part.functionResponse().isPresent())
+ .findFirst()
+ .map(part -> part.functionResponse().get())
+ .orElse(null);
+ }
- return Optional.of(
- new Message.Builder()
- .messageId(UUID.randomUUID().toString())
- .parts(parts)
- .role(Message.Role.USER)
- .build());
+ private static boolean isUserFunctionCall(Event event, String functionResponseId) {
+ if (event.content().isEmpty()) {
+ return false;
+ }
+ return event.content().get().parts().get().stream()
+ .anyMatch(
+ part ->
+ part.functionCall().isPresent()
+ && part.functionCall()
+ .get()
+ .id()
+ .map(id -> id.equals(functionResponseId))
+ .orElse(false));
}
+ /**
+ * Converts a GenAI Content object to a list of A2A Parts.
+ *
+ * @param content The GenAI Content object to convert.
+ * @param isPartial Whether the content is partial.
+ * @return A list of A2A Parts.
+ */
public static ImmutableList> contentToParts(
Optional content, boolean isPartial) {
return content.flatMap(Content::parts).stream()
@@ -75,4 +126,80 @@ public static ImmutableList> contentToParts(
.map(part -> PartConverter.fromGenaiPart(part, isPartial))
.collect(toImmutableList());
}
+
+ /**
+ * Returns the parts from the context events that should be sent to the agent.
+ *
+ * All session events from the previous remote agent response (or the beginning of the session
+ * in case of the first agent invocation) are included into the A2A message. Events from other
+ * agents are presented as user messages and rephased as if a user was telling what happened in
+ * the session up to the point.
+ *
+ * @param context The invocation context to get the parts from.
+ * @return A list of A2A Parts.
+ */
+ public static ImmutableList> messagePartsFromContext(InvocationContext context) {
+ if (context.session().events().isEmpty()) {
+ return ImmutableList.of();
+ }
+ List events = context.session().events();
+ int lastResponseIndex = -1;
+ String contextId = "";
+ for (int i = events.size() - 1; i >= 0; i--) {
+ Event event = events.get(i);
+ if (event.author().equals(context.agent().name())) {
+ lastResponseIndex = i;
+ contextId = contextId(event);
+ break;
+ }
+ }
+ ImmutableList.Builder> partsBuilder = ImmutableList.builder();
+ for (int i = lastResponseIndex + 1; i < events.size(); i++) {
+ Event event = events.get(i);
+ if (!event.author().equals("user") && !event.author().equals(context.agent().name())) {
+ event = presentAsUserMessage(event, contextId);
+ }
+ contentToParts(event.content(), event.partial().orElse(false)).forEach(partsBuilder::add);
+ }
+ return partsBuilder.build();
+ }
+
+ private static Event presentAsUserMessage(Event event, String contextId) {
+ Event.Builder userEvent =
+ new Event.Builder().id(UUID.randomUUID().toString()).invocationId(contextId).author("user");
+ ImmutableList parts =
+ event.content().flatMap(Content::parts).stream()
+ .flatMap(Collection::stream)
+ // convert only non-thought parts to user message parts, skip thought parts as they are
+ // not meant to be shown to the user
+ .filter(part -> !part.thought().orElse(false))
+ .map(part -> PartConverter.remoteCallAsUserPart(event.author(), part))
+ .collect(toImmutableList());
+ if (parts.isEmpty()) {
+ return userEvent.build();
+ }
+ com.google.genai.types.Part forContext =
+ com.google.genai.types.Part.builder().text("For context:").build();
+ return userEvent
+ .content(
+ Content.builder()
+ .parts(
+ ImmutableList.builder()
+ .add(forContext)
+ .addAll(parts)
+ .build())
+ .build())
+ .build();
+ }
+
+ private static String metadataValue(Event event, String key) {
+ if (event.customMetadata().isEmpty()) {
+ return "";
+ }
+ return event.customMetadata().get().stream()
+ .filter(m -> m.key().map(k -> k.equals(key)).orElse(false))
+ .findFirst()
+ .flatMap(m -> m.stringValue())
+ .orElse("");
+ }
}
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 05125d170..61f24fa21 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
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2026 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.a2a.converters;
import static com.google.common.collect.ImmutableList.toImmutableList;
@@ -32,12 +47,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/**
- * Utility class for converting between Google GenAI Parts and A2A DataParts.
- *
- * **EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not
- * use in production code.
- */
+/** Utility class for converting between Google GenAI Parts and A2A DataParts. */
public final class PartConverter {
private static final Logger logger = LoggerFactory.getLogger(PartConverter.class);
@@ -68,13 +78,13 @@ public static Optional toTextPart(io.a2a.spec.Part> part) {
}
/** Convert an A2A JSON part into a Google GenAI part representation. */
- public static Optional toGenaiPart(io.a2a.spec.Part> a2aPart) {
+ public static com.google.genai.types.Part toGenaiPart(io.a2a.spec.Part> a2aPart) {
if (a2aPart == null) {
- return Optional.empty();
+ throw new IllegalArgumentException("A2A part cannot be null");
}
if (a2aPart instanceof TextPart textPart) {
- return Optional.of(com.google.genai.types.Part.builder().text(textPart.getText()).build());
+ return com.google.genai.types.Part.builder().text(textPart.getText()).build();
}
if (a2aPart instanceof FilePart filePart) {
@@ -85,56 +95,41 @@ public static Optional toGenaiPart(io.a2a.spec.Part
return convertDataPartToGenAiPart(dataPart);
}
- logger.warn("Unsupported A2A part type: {}", a2aPart.getClass());
- return Optional.empty();
+ throw new IllegalArgumentException("Unsupported A2A part type: " + a2aPart.getClass());
}
public static ImmutableList toGenaiParts(
List> a2aParts) {
- return a2aParts.stream()
- .map(PartConverter::toGenaiPart)
- .flatMap(Optional::stream)
- .collect(toImmutableList());
+ return a2aParts.stream().map(PartConverter::toGenaiPart).collect(toImmutableList());
}
- private static Optional convertFilePartToGenAiPart(
- FilePart filePart) {
+ private static com.google.genai.types.Part convertFilePartToGenAiPart(FilePart filePart) {
FileContent fileContent = filePart.getFile();
if (fileContent instanceof FileWithUri fileWithUri) {
- return Optional.of(
- com.google.genai.types.Part.builder()
- .fileData(
- FileData.builder()
- .fileUri(fileWithUri.uri())
- .mimeType(fileWithUri.mimeType())
- .build())
- .build());
+ return com.google.genai.types.Part.builder()
+ .fileData(
+ FileData.builder()
+ .fileUri(fileWithUri.uri())
+ .mimeType(fileWithUri.mimeType())
+ .build())
+ .build();
}
if (fileContent instanceof FileWithBytes fileWithBytes) {
String bytesString = fileWithBytes.bytes();
if (bytesString == null) {
- logger.warn("FileWithBytes missing byte content");
- return Optional.empty();
- }
- try {
- byte[] decoded = Base64.getDecoder().decode(bytesString);
- return Optional.of(
- com.google.genai.types.Part.builder()
- .inlineData(Blob.builder().data(decoded).mimeType(fileWithBytes.mimeType()).build())
- .build());
- } catch (IllegalArgumentException e) {
- logger.warn("Failed to decode base64 file content", e);
- return Optional.empty();
+ throw new GenAiFieldMissingException("FileWithBytes missing byte content");
}
+ byte[] decoded = Base64.getDecoder().decode(bytesString);
+ return com.google.genai.types.Part.builder()
+ .inlineData(Blob.builder().data(decoded).mimeType(fileWithBytes.mimeType()).build())
+ .build();
}
- logger.warn("Unsupported FilePart content: {}", fileContent.getClass());
- return Optional.empty();
+ throw new IllegalArgumentException("Unsupported FilePart content: " + fileContent.getClass());
}
- private static Optional convertDataPartToGenAiPart(
- DataPart dataPart) {
+ private static com.google.genai.types.Part convertDataPartToGenAiPart(DataPart dataPart) {
Map data =
Optional.ofNullable(dataPart.getData()).map(HashMap::new).orElseGet(HashMap::new);
Map metadata =
@@ -144,14 +139,12 @@ private static Optional convertDataPartToGenAiPart(
if ((data.containsKey(NAME_KEY) && data.containsKey(ARGS_KEY))
|| metadataType.equals(A2ADataPartMetadataType.FUNCTION_CALL.getType())) {
- String functionName = String.valueOf(data.getOrDefault(NAME_KEY, null));
- String functionId = String.valueOf(data.getOrDefault(ID_KEY, null));
+ String functionName = String.valueOf(data.getOrDefault(NAME_KEY, ""));
+ String functionId = String.valueOf(data.getOrDefault(ID_KEY, ""));
Map args = coerceToMap(data.get(ARGS_KEY));
- return Optional.of(
- com.google.genai.types.Part.builder()
- .functionCall(
- FunctionCall.builder().name(functionName).id(functionId).args(args).build())
- .build());
+ return com.google.genai.types.Part.builder()
+ .functionCall(FunctionCall.builder().name(functionName).id(functionId).args(args).build())
+ .build();
}
if ((data.containsKey(NAME_KEY) && data.containsKey(RESPONSE_KEY))
@@ -159,15 +152,14 @@ private static Optional convertDataPartToGenAiPart(
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(
- FunctionResponse.builder()
- .name(functionName)
- .id(functionId)
- .response(response)
- .build())
- .build());
+ return com.google.genai.types.Part.builder()
+ .functionResponse(
+ FunctionResponse.builder()
+ .name(functionName)
+ .id(functionId)
+ .response(response)
+ .build())
+ .build();
}
if ((data.containsKey(CODE_KEY) && data.containsKey(LANGUAGE_KEY))
@@ -175,13 +167,11 @@ private static Optional convertDataPartToGenAiPart(
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());
+ data.getOrDefault(LANGUAGE_KEY, Language.Known.LANGUAGE_UNSPECIFIED.toString()));
+ return 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))
@@ -189,22 +179,17 @@ private static Optional convertDataPartToGenAiPart(
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());
+ return 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());
+ return com.google.genai.types.Part.builder().text(json).build();
} catch (JsonProcessingException e) {
- logger.warn("Failed to serialize DataPart payload", e);
- return Optional.empty();
+ throw new IllegalArgumentException("Failed to serialize DataPart payload", e);
}
}
@@ -374,6 +359,50 @@ private static FilePart filePartToA2A(Part part, ImmutableMap.BuilderEvents are rephrased as if a user was telling what happened in the session up to the point.
+ * E.g.
+ *
+ * {@code
+ * For context:
+ * User said: Now help me with Z
+ * Agent A said: Agent B can help you with it!
+ * Agent B said: Agent C might know better.*
+ * }
+ *
+ * @param author The author of the part.
+ * @param part The part to convert.
+ * @return The converted part.
+ */
+ public static Part remoteCallAsUserPart(String author, Part part) {
+ if (part.text().isPresent()) {
+ String partText = String.format("[%s] said: %s", author, part.text().get());
+ return Part.builder().text(partText).build();
+ } else if (part.functionCall().isPresent()) {
+ FunctionCall functionCall = part.functionCall().get();
+ String partText =
+ String.format(
+ "[%s] called tool %s with parameters: %s",
+ author,
+ functionCall.name().orElse(""),
+ functionCall.args().orElse(ImmutableMap.of()));
+ return Part.builder().text(partText).build();
+ } else if (part.functionResponse().isPresent()) {
+ FunctionResponse functionResponse = part.functionResponse().get();
+ String partText =
+ String.format(
+ "[%s] %s tool returned result: %s",
+ author,
+ functionResponse.name().orElse(""),
+ functionResponse.response().orElse(ImmutableMap.of()));
+ return Part.builder().text(partText).build();
+ } else {
+ return part;
+ }
+ }
+
@SuppressWarnings("unchecked") // safe conversion from objectMapper.readValue
private static Map coerceToMap(Object value) {
if (value == 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 57a84b58f..503432a30 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
@@ -1,6 +1,23 @@
+/*
+ * Copyright 2026 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.a2a.converters;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.Streams.zip;
import com.google.adk.agents.InvocationContext;
import com.google.adk.events.Event;
@@ -14,6 +31,7 @@
import io.a2a.client.TaskEvent;
import io.a2a.client.TaskUpdateEvent;
import io.a2a.spec.Artifact;
+import io.a2a.spec.DataPart;
import io.a2a.spec.Message;
import io.a2a.spec.Task;
import io.a2a.spec.TaskArtifactUpdateEvent;
@@ -21,18 +39,14 @@
import io.a2a.spec.TaskStatusUpdateEvent;
import java.time.Instant;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/**
- * Utility for converting ADK events to A2A spec messages (and back).
- *
- * **EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not
- * use in production code.
- */
+/** Utility for converting ADK events to A2A spec messages (and back). */
public final class ResponseConverter {
private static final Logger logger = LoggerFactory.getLogger(ResponseConverter.class);
private static final ImmutableSet PENDING_STATES =
@@ -60,6 +74,14 @@ public static Optional clientEventToEvent(
throw new IllegalArgumentException("Unsupported ClientEvent type: " + event.getClass());
}
+ private static boolean isPartial(Map metadata) {
+ if (metadata == null) {
+ return false;
+ }
+ return Objects.equals(
+ metadata.getOrDefault(PartConverter.A2A_DATA_PART_METADATA_IS_PARTIAL_KEY, false), true);
+ }
+
/**
* Converts a A2A {@link TaskUpdateEvent} to an ADK {@link Event}, if applicable. Returns null if
* the event is not a final update for TaskArtifactUpdateEvent or if the message is empty for
@@ -75,7 +97,14 @@ private static Optional handleTaskUpdate(
boolean isAppend = Objects.equals(artifactEvent.isAppend(), true);
boolean isLastChunk = Objects.equals(artifactEvent.isLastChunk(), true);
+ if (isLastChunk && isPartial(artifactEvent.getMetadata())) {
+ return Optional.empty();
+ }
+
Event eventPart = artifactToEvent(artifactEvent.getArtifact(), context);
+ if (eventPart.content().flatMap(Content::parts).orElse(ImmutableList.of()).isEmpty()) {
+ return Optional.empty();
+ }
eventPart.setPartial(isAppend || !isLastChunk);
// append=true, lastChunk=false: emit as partial, update aggregation
// append=false, lastChunk=false: emit as partial, reset aggregation
@@ -105,9 +134,8 @@ private static Optional handleTaskUpdate(
.map(builder -> builder.turnComplete(true))
.map(builder -> builder.partial(false))
.map(Event.Builder::build);
- } else {
- return messageEvent;
}
+ return messageEvent;
}
throw new IllegalArgumentException(
"Unsupported TaskUpdateEvent type: " + updateEvent.getClass());
@@ -115,16 +143,12 @@ private static Optional handleTaskUpdate(
/** 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)
- .content(fromModelParts(PartConverter.toGenaiParts(message.getParts())))
- .build();
+ Event.Builder eventBuilder = remoteAgentEventBuilder(invocationContext);
+ ImmutableList genaiParts = PartConverter.toGenaiParts(artifact.parts());
+ eventBuilder
+ .content(fromModelParts(genaiParts))
+ .longRunningToolIds(getLongRunningToolIds(artifact.parts(), genaiParts));
+ return eventBuilder.build();
}
/** Converts an A2A message for a failed task to ADK event filling in the error message. */
@@ -137,6 +161,13 @@ public static Event messageToFailedEvent(Message message, InvocationContext invo
return builder.build();
}
+ /** Converts an A2A message back to ADK events. */
+ public static Event messageToEvent(Message message, InvocationContext invocationContext) {
+ return remoteAgentEventBuilder(invocationContext)
+ .content(fromModelParts(PartConverter.toGenaiParts(message.getParts())))
+ .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.
@@ -158,25 +189,71 @@ public static Event messageToEvent(
* If none of these are present, an empty event is returned.
*/
public static Event taskToEvent(Task task, InvocationContext invocationContext) {
- Message taskMessage = null;
-
- if (!task.getArtifacts().isEmpty()) {
- taskMessage =
- new Message.Builder()
- .messageId("")
- .role(Message.Role.AGENT)
- .parts(Iterables.getLast(task.getArtifacts()).parts())
- .build();
- } else if (task.getStatus().message() != null) {
- taskMessage = task.getStatus().message();
- } else if (!task.getHistory().isEmpty()) {
- taskMessage = Iterables.getLast(task.getHistory());
+ ImmutableList.Builder genaiParts = ImmutableList.builder();
+ ImmutableSet.Builder longRunningToolIds = ImmutableSet.builder();
+
+ for (Artifact artifact : task.getArtifacts()) {
+ ImmutableList converted = PartConverter.toGenaiParts(artifact.parts());
+ longRunningToolIds.addAll(getLongRunningToolIds(artifact.parts(), converted));
+ genaiParts.addAll(converted);
}
- if (taskMessage != null) {
- return messageToEvent(taskMessage, invocationContext);
+ Event.Builder eventBuilder = remoteAgentEventBuilder(invocationContext);
+
+ if (task.getStatus().message() != null) {
+ ImmutableList msgParts =
+ PartConverter.toGenaiParts(task.getStatus().message().getParts());
+ longRunningToolIds.addAll(
+ getLongRunningToolIds(task.getStatus().message().getParts(), msgParts));
+ if (task.getStatus().state() == TaskState.FAILED
+ && msgParts.size() == 1
+ && msgParts.get(0).text().isPresent()) {
+ eventBuilder.errorMessage(msgParts.get(0).text().get());
+ } else {
+ genaiParts.addAll(msgParts);
+ }
+ }
+
+ ImmutableList finalParts = genaiParts.build();
+ boolean isFinal =
+ task.getStatus().state().isFinal() || task.getStatus().state() == TaskState.INPUT_REQUIRED;
+
+ if (finalParts.isEmpty() && !isFinal) {
+ return emptyEvent(invocationContext);
}
- return emptyEvent(invocationContext);
+ if (!finalParts.isEmpty()) {
+ eventBuilder.content(fromModelParts(finalParts));
+ }
+ if (task.getStatus().state() == TaskState.INPUT_REQUIRED) {
+ eventBuilder.longRunningToolIds(longRunningToolIds.build());
+ }
+ eventBuilder.turnComplete(isFinal);
+ return eventBuilder.build();
+ }
+
+ private static ImmutableSet getLongRunningToolIds(
+ List> parts, List convertedParts) {
+ return zip(
+ parts.stream(),
+ convertedParts.stream(),
+ (part, convertedPart) -> {
+ if (!(part instanceof DataPart dataPart)) {
+ return Optional.empty();
+ }
+ Object isLongRunning =
+ dataPart
+ .getMetadata()
+ .get(PartConverter.A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY);
+ if (!Objects.equals(isLongRunning, true)) {
+ return Optional.empty();
+ }
+ if (convertedPart.functionCall().isEmpty()) {
+ return Optional.empty();
+ }
+ return convertedPart.functionCall().get().id();
+ })
+ .flatMap(Optional::stream)
+ .collect(toImmutableSet());
}
private static Event emptyEvent(InvocationContext 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 b7b4e9953..7252cdec1 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
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2026 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.a2a.executor;
import static java.util.Objects.requireNonNull;
@@ -44,12 +59,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/**
- * Implementation of the A2A AgentExecutor interface that uses ADK to execute agent tasks.
- *
- * **EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not
- * use in production code.
- */
+/** Implementation of the A2A AgentExecutor interface that uses ADK to execute agent tasks. */
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_";
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 ba0177dc4..3ee8656d2 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
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2026 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.a2a.executor;
import com.google.adk.a2a.executor.Callbacks.AfterEventCallback;
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
index 666f1d8a0..3483c527f 100644
--- a/a2a/src/main/java/com/google/adk/a2a/executor/Callbacks.java
+++ b/a2a/src/main/java/com/google/adk/a2a/executor/Callbacks.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright 2026 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.a2a.executor;
import com.google.adk.events.Event;
diff --git a/a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java b/a2a/src/test/java/com/google/adk/a2a/agent/RemoteA2AAgentTest.java
similarity index 99%
rename from a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java
rename to a2a/src/test/java/com/google/adk/a2a/agent/RemoteA2AAgentTest.java
index 87eaa2321..b1ffa248a 100644
--- a/a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java
+++ b/a2a/src/test/java/com/google/adk/a2a/agent/RemoteA2AAgentTest.java
@@ -1,4 +1,4 @@
-package com.google.adk.a2a;
+package com.google.adk.a2a.agent;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -412,10 +412,11 @@ public void runAsync_constructsRequestWithHistory() {
.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(message.getParts()).hasSize(4);
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?");
+ assertThat(((TextPart) message.getParts().get(1)).getText()).isEqualTo("For context:");
+ assertThat(((TextPart) message.getParts().get(2)).getText()).isEqualTo("[model] said: hi");
+ assertThat(((TextPart) message.getParts().get(3)).getText()).isEqualTo("how are you?");
}
@Test
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 8d460c457..207019199 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
@@ -4,23 +4,17 @@
import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.InvocationContext;
-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.FunctionCall;
import com.google.genai.types.FunctionResponse;
import com.google.genai.types.Part;
-import io.a2a.spec.DataPart;
-import io.a2a.spec.Message;
import io.a2a.spec.TextPart;
import io.reactivex.rxjava3.core.Flowable;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -30,101 +24,180 @@
public final class EventConverterTest {
@Test
- public void convertEventsToA2AMessage_preservesFunctionCallAndResponseParts() {
- // Arrange session events: user text, function call, function response.
- Part userTextPart = Part.builder().text("Roll a die").build();
- Event userEvent =
+ public void testTaskId() {
+ Event e =
+ Event.builder()
+ .customMetadata(
+ ImmutableList.of(
+ CustomMetadata.builder()
+ .key(EventConverter.ADK_TASK_ID_KEY)
+ .stringValue("task-123")
+ .build()))
+ .build();
+ assertThat(EventConverter.taskId(e)).isEqualTo("task-123");
+ }
+
+ @Test
+ public void testTaskId_empty() {
+ Event e = Event.builder().build();
+ assertThat(EventConverter.taskId(e)).isEmpty();
+ }
+
+ @Test
+ public void testContextId() {
+ Event e =
+ Event.builder()
+ .customMetadata(
+ ImmutableList.of(
+ CustomMetadata.builder()
+ .key(EventConverter.ADK_CONTEXT_ID_KEY)
+ .stringValue("context-456")
+ .build()))
+ .build();
+ assertThat(EventConverter.contextId(e)).isEqualTo("context-456");
+ }
+
+ @Test
+ public void testContextId_empty() {
+ Event e = Event.builder().build();
+ assertThat(EventConverter.contextId(e)).isEmpty();
+ }
+
+ @Test
+ public void testFindUserFunctionCall_success() {
+ Event agentEvent = Event.builder().author("agent").build();
+ FunctionCall fc = FunctionCall.builder().name("my-func").id("fc-id").build();
+ Event userEventWithCall =
Event.builder()
- .id("event-user")
.author("user")
- .content(Content.builder().role("user").parts(ImmutableList.of(userTextPart)).build())
+ .content(
+ Content.builder()
+ .parts(ImmutableList.of(Part.builder().functionCall(fc).build()))
+ .build())
.build();
- Part functionCallPart =
- Part.builder()
- .functionCall(
- FunctionCall.builder()
- .name("roll_die")
- .id("adk-call-1")
- .args(ImmutableMap.of("sides", 6))
+ FunctionResponse fr = FunctionResponse.builder().name("my-func").id("fc-id").build();
+ Event userEventWithResponse =
+ Event.builder()
+ .author("user")
+ .content(
+ Content.builder()
+ .parts(ImmutableList.of(Part.builder().functionResponse(fr).build()))
.build())
.build();
- Event callEvent =
+
+ ImmutableList events =
+ ImmutableList.of(userEventWithCall, agentEvent, userEventWithResponse);
+ assertThat(EventConverter.findUserFunctionCall(events)).isEqualTo(userEventWithCall);
+ }
+
+ @Test
+ public void testFindUserFunctionCall_noMatchingCall() {
+ Event agentEvent = Event.builder().author("agent").build();
+ FunctionCall fc = FunctionCall.builder().name("my-func").id("other-id").build();
+ Event userEventWithCall =
Event.builder()
- .id("event-call")
- .author("root_agent")
+ .author("user")
.content(
Content.builder()
- .role("assistant")
- .parts(ImmutableList.of(functionCallPart))
+ .parts(ImmutableList.of(Part.builder().functionCall(fc).build()))
.build())
.build();
- Part functionResponsePart =
- Part.builder()
- .functionResponse(
- FunctionResponse.builder()
- .name("roll_die")
- .id("adk-call-1")
- .response(ImmutableMap.of("result", 3))
+ FunctionResponse fr = FunctionResponse.builder().name("my-func").id("fc-id").build();
+ Event userEventWithResponse =
+ Event.builder()
+ .author("user")
+ .content(
+ Content.builder()
+ .parts(ImmutableList.of(Part.builder().functionResponse(fr).build()))
+ .build())
+ .build();
+
+ ImmutableList events =
+ ImmutableList.of(userEventWithCall, agentEvent, userEventWithResponse);
+ assertThat(EventConverter.findUserFunctionCall(events)).isNull();
+ }
+
+ @Test
+ public void testFindUserFunctionCall_lastEventNotUser() {
+ Event agentEvent = Event.builder().author("agent").build();
+ FunctionCall fc = FunctionCall.builder().name("my-func").id("fc-id").build();
+ Event userEventWithCall =
+ Event.builder()
+ .author("user")
+ .content(
+ Content.builder()
+ .parts(ImmutableList.of(Part.builder().functionCall(fc).build()))
.build())
.build();
- Event responseEvent =
+ FunctionResponse fr = FunctionResponse.builder().name("my-func").id("fc-id").build();
+ // Last event is not a user event, so should return null.
+ Event agentEventWithResponse =
Event.builder()
- .id("event-response")
- .author("roll_agent")
+ .author("agent")
.content(
Content.builder()
- .role("tool")
- .parts(ImmutableList.of(functionResponsePart))
+ .parts(ImmutableList.of(Part.builder().functionResponse(fr).build()))
.build())
.build();
- List events = new ArrayList<>(ImmutableList.of(userEvent, callEvent, responseEvent));
- Session session =
- Session.builder("session-1").appName("demo").userId("user").events(events).build();
+ ImmutableList events =
+ ImmutableList.of(userEventWithCall, agentEvent, agentEventWithResponse);
- InvocationContext context =
+ assertThat(EventConverter.findUserFunctionCall(events)).isNull();
+ }
+
+ @Test
+ public void testContentToParts() {
+ Part textPart = Part.builder().text("hello").build();
+ Content content = Content.builder().parts(ImmutableList.of(textPart)).build();
+ ImmutableList> list =
+ EventConverter.contentToParts(Optional.of(content), false);
+ assertThat(list).hasSize(1);
+ assertThat(((TextPart) list.get(0)).getText()).isEqualTo("hello");
+ }
+
+ @Test
+ public void testMessagePartsFromContext() {
+ Session session =
+ Session.builder("session1")
+ .events(
+ ImmutableList.of(
+ Event.builder()
+ .author("user")
+ .content(
+ Content.builder()
+ .parts(ImmutableList.of(Part.builder().text("hello").build()))
+ .build())
+ .build(),
+ Event.builder()
+ .author("test_agent")
+ .content(
+ Content.builder()
+ .parts(ImmutableList.of(Part.builder().text("hi").build()))
+ .build())
+ .build(),
+ Event.builder()
+ .author("other_agent")
+ .content(
+ Content.builder()
+ .parts(ImmutableList.of(Part.builder().text("hey").build()))
+ .build())
+ .build()))
+ .build();
+ BaseAgent agent = new TestAgent();
+ InvocationContext ctx =
InvocationContext.builder()
- .sessionService(new InMemorySessionService())
- .artifactService(new InMemoryArtifactService())
- .pluginManager(new PluginManager())
- .invocationId("invocation-1")
- .agent(new TestAgent())
.session(session)
- .userContent(
- Content.builder().role("user").parts(ImmutableList.of(userTextPart)).build())
- .endInvocation(false)
+ .sessionService(new InMemorySessionService())
+ .agent(agent)
.build();
+ ImmutableList> parts = EventConverter.messagePartsFromContext(ctx);
- // Act
- Optional maybeMessage = EventConverter.convertEventsToA2AMessage(context);
-
- // Assert
- assertThat(maybeMessage).isPresent();
- Message message = maybeMessage.get();
- 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(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(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");
+ assertThat(parts).hasSize(2);
+ assertThat(((TextPart) parts.get(0)).getText()).isEqualTo("For context:");
+ assertThat(((TextPart) parts.get(1)).getText()).isEqualTo("[other_agent] said: hey");
}
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 8e8982ffa..d93466dd2 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
@@ -18,7 +18,6 @@
import io.a2a.spec.FileWithUri;
import io.a2a.spec.TextPart;
import java.util.Base64;
-import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -27,29 +26,27 @@
public class PartConverterTest {
@Test
- public void toGenaiPart_withNullPart_returnsEmpty() {
- assertThat(PartConverter.toGenaiPart(null)).isEmpty();
+ public void toGenaiPart_withNullPart_throwsException() {
+ assertThrows(IllegalArgumentException.class, () -> PartConverter.toGenaiPart(null));
}
@Test
public void toGenaiPart_withTextPart_returnsGenaiTextPart() {
TextPart textPart = new TextPart("Hello");
- Optional result = PartConverter.toGenaiPart(textPart);
+ Part result = PartConverter.toGenaiPart(textPart);
- assertThat(result).isPresent();
- assertThat(result.get().text()).hasValue("Hello");
+ assertThat(result.text()).hasValue("Hello");
}
@Test
public void toGenaiPart_withFilePartUri_returnsGenaiFilePart() {
FilePart filePart = new FilePart(new FileWithUri("text/plain", "file.txt", "http://file.txt"));
- Optional result = PartConverter.toGenaiPart(filePart);
+ Part result = PartConverter.toGenaiPart(filePart);
- assertThat(result).isPresent();
- assertThat(result.get().fileData()).isPresent();
- FileData fileData = result.get().fileData().get();
+ assertThat(result.fileData()).isPresent();
+ FileData fileData = result.fileData().get();
assertThat(fileData.mimeType()).hasValue("text/plain");
assertThat(fileData.fileUri()).hasValue("http://file.txt");
}
@@ -60,26 +57,25 @@ public void toGenaiPart_withFilePartBytes_returnsGenaiBlobPart() {
String encoded = Base64.getEncoder().encodeToString(bytes);
FilePart filePart = new FilePart(new FileWithBytes("text/plain", "file.txt", encoded));
- Optional result = PartConverter.toGenaiPart(filePart);
+ Part result = PartConverter.toGenaiPart(filePart);
- assertThat(result).isPresent();
- assertThat(result.get().inlineData()).isPresent();
- Blob blob = result.get().inlineData().get();
+ assertThat(result.inlineData()).isPresent();
+ Blob blob = result.inlineData().get();
assertThat(blob.mimeType()).hasValue("text/plain");
assertThat(blob.data().get()).isEqualTo(bytes);
}
@Test
- public void toGenaiPart_withFilePartBytes_handlesNullBytes() {
+ public void toGenaiPart_withFilePartBytes_handlesNullBytes_throwsException() {
FilePart filePart = new FilePart(new FileWithBytes("text/plain", "file.txt", null));
- assertThat(PartConverter.toGenaiPart(filePart)).isEmpty();
+ assertThrows(GenAiFieldMissingException.class, () -> PartConverter.toGenaiPart(filePart));
}
@Test
public void toGenaiPart_withFilePartBytes_handlesInvalidBase64() {
FilePart filePart =
new FilePart(new FileWithBytes("text/plain", "file.txt", "invalid-base64!"));
- assertThat(PartConverter.toGenaiPart(filePart)).isEmpty();
+ assertThrows(IllegalArgumentException.class, () -> PartConverter.toGenaiPart(filePart));
}
@Test
@@ -93,11 +89,10 @@ public void toGenaiPart_withDataPartFunctionCall_returnsGenaiFunctionCallPart()
PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
A2ADataPartMetadataType.FUNCTION_CALL.getType()));
- Optional result = PartConverter.toGenaiPart(dataPart);
+ Part result = PartConverter.toGenaiPart(dataPart);
- assertThat(result).isPresent();
- assertThat(result.get().functionCall()).isPresent();
- FunctionCall functionCall = result.get().functionCall().get();
+ assertThat(result.functionCall()).isPresent();
+ FunctionCall functionCall = result.functionCall().get();
assertThat(functionCall.name()).hasValue("func");
assertThat(functionCall.id()).hasValue("1");
assertThat(functionCall.args()).hasValue(ImmutableMap.of());
@@ -109,11 +104,10 @@ public void toGenaiPart_withDataPartFunctionCallByNameAndArgs_returnsGenaiFuncti
ImmutableMap.of("name", "func", "id", "1", "args", ImmutableMap.of("param", "value"));
DataPart dataPart = new DataPart(data, null);
- Optional result = PartConverter.toGenaiPart(dataPart);
+ Part result = PartConverter.toGenaiPart(dataPart);
- assertThat(result).isPresent();
- assertThat(result.get().functionCall()).isPresent();
- FunctionCall functionCall = result.get().functionCall().get();
+ assertThat(result.functionCall()).isPresent();
+ FunctionCall functionCall = result.functionCall().get();
assertThat(functionCall.name()).hasValue("func");
assertThat(functionCall.id()).hasValue("1");
assertThat(functionCall.args()).hasValue(ImmutableMap.of("param", "value"));
@@ -130,11 +124,10 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons
PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
A2ADataPartMetadataType.FUNCTION_RESPONSE.getType()));
- Optional result = PartConverter.toGenaiPart(dataPart);
+ Part result = PartConverter.toGenaiPart(dataPart);
- assertThat(result).isPresent();
- assertThat(result.get().functionResponse()).isPresent();
- FunctionResponse functionResponse = result.get().functionResponse().get();
+ assertThat(result.functionResponse()).isPresent();
+ FunctionResponse functionResponse = result.functionResponse().get();
assertThat(functionResponse.name()).hasValue("func");
assertThat(functionResponse.id()).hasValue("1");
assertThat(functionResponse.response()).hasValue(ImmutableMap.of());
@@ -147,11 +140,10 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons
ImmutableMap.of("name", "func", "id", "1", "response", ImmutableMap.of("result", "value"));
DataPart dataPart = new DataPart(data, null);
- Optional result = PartConverter.toGenaiPart(dataPart);
+ Part result = PartConverter.toGenaiPart(dataPart);
- assertThat(result).isPresent();
- assertThat(result.get().functionResponse()).isPresent();
- FunctionResponse functionResponse = result.get().functionResponse().get();
+ assertThat(result.functionResponse()).isPresent();
+ FunctionResponse functionResponse = result.functionResponse().get();
assertThat(functionResponse.name()).hasValue("func");
assertThat(functionResponse.id()).hasValue("1");
assertThat(functionResponse.response()).hasValue(ImmutableMap.of("result", "value"));
@@ -162,10 +154,9 @@ public void toGenaiPart_withOtherDataPart_returnsGenaiTextPartWithJson() {
ImmutableMap data = ImmutableMap.of("key", "value");
DataPart dataPart = new DataPart(data, null);
- Optional result = PartConverter.toGenaiPart(dataPart);
+ Part result = PartConverter.toGenaiPart(dataPart);
- assertThat(result).isPresent();
- assertThat(result.get().text()).hasValue("{\"key\":\"value\"}");
+ assertThat(result.text()).hasValue("{\"key\":\"value\"}");
}
@Test
@@ -293,11 +284,10 @@ public void toGenaiPart_dataPartWithEmptyStringCoercedToEmptyMap() {
ImmutableMap data = ImmutableMap.of("name", "func", "id", "1", "args", "");
DataPart dataPart = new DataPart(data, null);
- Optional result = PartConverter.toGenaiPart(dataPart);
+ Part result = PartConverter.toGenaiPart(dataPart);
- assertThat(result).isPresent();
- assertThat(result.get().functionCall()).isPresent();
- assertThat(result.get().functionCall().get().args()).hasValue(ImmutableMap.of());
+ assertThat(result.functionCall()).isPresent();
+ assertThat(result.functionCall().get().args()).hasValue(ImmutableMap.of());
}
@Test
@@ -305,10 +295,9 @@ public void toGenaiPart_dataPartWithNonMapCoercedToMap() {
ImmutableMap data = ImmutableMap.of("name", "func", "id", "1", "args", 123);
DataPart dataPart = new DataPart(data, null);
- Optional result = PartConverter.toGenaiPart(dataPart);
+ Part result = PartConverter.toGenaiPart(dataPart);
- assertThat(result).isPresent();
- assertThat(result.get().functionCall()).isPresent();
- assertThat(result.get().functionCall().get().args()).hasValue(ImmutableMap.of("value", 123));
+ assertThat(result.functionCall()).isPresent();
+ assertThat(result.functionCall().get().args()).hasValue(ImmutableMap.of("value", 123));
}
}
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 5378bdd7b..d84dc42cd 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
@@ -11,10 +11,12 @@
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 io.a2a.client.MessageEvent;
import io.a2a.client.TaskUpdateEvent;
import io.a2a.spec.Artifact;
+import io.a2a.spec.DataPart;
import io.a2a.spec.Message;
import io.a2a.spec.Task;
import io.a2a.spec.TaskArtifactUpdateEvent;
@@ -144,6 +146,81 @@ public void taskToEvent_withNoMessage_returnsEmptyEvent() {
assertThat(event.invocationId()).isEqualTo(invocationContext.invocationId());
}
+ @Test
+ public void taskToEvent_withInputRequired_parsesLongRunningToolIds() {
+ ImmutableMap data =
+ ImmutableMap.of("name", "myTool", "id", "call_123", "args", ImmutableMap.of());
+ ImmutableMap metadata =
+ ImmutableMap.of(
+ PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
+ "function_call",
+ PartConverter.A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY,
+ true);
+ DataPart dataPart = new DataPart(data, metadata);
+ ImmutableMap statusData =
+ ImmutableMap.of("name", "messageTools", "id", "msg_123", "args", ImmutableMap.of());
+ ImmutableMap statusMetadata =
+ ImmutableMap.of(
+ PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
+ "function_call",
+ PartConverter.A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY,
+ true);
+ DataPart statusDataPart = new DataPart(statusData, statusMetadata);
+ Message statusMessage =
+ new Message.Builder()
+ .role(Message.Role.AGENT)
+ .parts(ImmutableList.of(statusDataPart))
+ .build();
+ TaskStatus status = new TaskStatus(TaskState.INPUT_REQUIRED, statusMessage, null);
+
+ Artifact artifact =
+ new Artifact.Builder().artifactId("artifact-1").parts(ImmutableList.of(dataPart)).build();
+ Task task = testTask().status(status).artifacts(ImmutableList.of(artifact)).build();
+
+ Event event = ResponseConverter.taskToEvent(task, invocationContext);
+ assertThat(event).isNotNull();
+ assertThat(event.longRunningToolIds().get()).containsExactly("call_123", "msg_123");
+ }
+
+ @Test
+ public void taskToEvent_withFailedState_setsErrorCode() {
+ 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);
+ Task task = testTask().status(status).artifacts(ImmutableList.of()).build();
+
+ Event event = ResponseConverter.taskToEvent(task, invocationContext);
+ assertThat(event).isNotNull();
+ assertThat(event.errorMessage()).hasValue("Task failed");
+ }
+
+ @Test
+ public void taskToEvent_withFinalEvent_returnsEmptyEvent() {
+ TaskStatus status = new TaskStatus(TaskState.COMPLETED);
+ Task task = testTask().status(status).artifacts(ImmutableList.of()).build();
+
+ Event event = ResponseConverter.taskToEvent(task, invocationContext);
+ assertThat(event).isNotNull();
+ assertThat(event.invocationId()).isEqualTo(invocationContext.invocationId());
+ assertThat(event.turnComplete()).hasValue(true);
+ assertThat(event.content().flatMap(Content::parts).orElse(ImmutableList.of())).isEmpty();
+ }
+
+ @Test
+ public void taskToEvent_withEmptyParts_returnsEmptyEvent() {
+ TaskStatus status = new TaskStatus(TaskState.SUBMITTED);
+ Task task = testTask().status(status).artifacts(ImmutableList.of()).build();
+
+ Event event = ResponseConverter.taskToEvent(task, invocationContext);
+ assertThat(event).isNotNull();
+ assertThat(event.invocationId()).isEqualTo(invocationContext.invocationId());
+ assertThat(event.content()).isPresent();
+ assertThat(event.content().get().parts().orElse(ImmutableList.of())).isEmpty();
+ }
+
@Test
public void clientEventToEvent_withTaskUpdateEventAndThought_returnsThoughtEvent() {
Message statusMessage =
diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml
index a62bff5b6..0079dce24 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.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
../../pom.xml
diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml
index e2ba4a7fb..c2326fa0a 100644
--- a/contrib/langchain4j/pom.xml
+++ b/contrib/langchain4j/pom.xml
@@ -20,7 +20,7 @@
com.google.adk
google-adk-parent
- 0.8.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
../../pom.xml
diff --git a/contrib/langchain4j/src/test/java/com/google/adk/models/langchain4j/RunLoop.java b/contrib/langchain4j/src/test/java/com/google/adk/models/langchain4j/RunLoop.java
index 04a2aa585..2dca5c49c 100644
--- a/contrib/langchain4j/src/test/java/com/google/adk/models/langchain4j/RunLoop.java
+++ b/contrib/langchain4j/src/test/java/com/google/adk/models/langchain4j/RunLoop.java
@@ -53,7 +53,7 @@ public static List runLoop(BaseAgent agent, boolean streaming, Object...
allEvents.addAll(
runner
.runAsync(
- session,
+ session.sessionKey(),
messageContent,
RunConfig.builder()
.setStreamingMode(
diff --git a/contrib/samples/a2a_basic/A2AAgent.java b/contrib/samples/a2a_basic/A2AAgent.java
index e4e79a4eb..e08a87a67 100644
--- a/contrib/samples/a2a_basic/A2AAgent.java
+++ b/contrib/samples/a2a_basic/A2AAgent.java
@@ -1,6 +1,6 @@
package com.example.a2a_basic;
-import com.google.adk.a2a.RemoteA2AAgent;
+import com.google.adk.a2a.agent.RemoteA2AAgent;
import com.google.adk.agents.BaseAgent;
import com.google.adk.agents.LlmAgent;
import com.google.adk.tools.FunctionTool;
diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml
index 82b11b96f..0eccb733b 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.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
..
diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml
index 84023e260..0677ad718 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.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
..
diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml
index 6f7bfff83..059bd8a38 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.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
..
diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml
index 36d12eaf0..df5d5e709 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.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
..
diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml
index 935aa6531..16b139d35 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.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
../../..
diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml
index 905f8e711..4a415113f 100644
--- a/contrib/samples/pom.xml
+++ b/contrib/samples/pom.xml
@@ -5,7 +5,7 @@
com.google.adk
google-adk-parent
- 0.8.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
../..
diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml
index f49c3faae..b24fa4b63 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.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
../../pom.xml
diff --git a/contrib/spring-ai/src/test/java/com/google/adk/models/springai/SpringAIIntegrationTest.java b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/SpringAIIntegrationTest.java
index 6843c8eaa..328df0415 100644
--- a/contrib/spring-ai/src/test/java/com/google/adk/models/springai/SpringAIIntegrationTest.java
+++ b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/SpringAIIntegrationTest.java
@@ -18,6 +18,7 @@
import static org.junit.jupiter.api.Assertions.*;
import com.google.adk.agents.LlmAgent;
+import com.google.adk.agents.RunConfig;
import com.google.adk.events.Event;
import com.google.adk.models.springai.integrations.tools.WeatherTool;
import com.google.adk.runner.InMemoryRunner;
@@ -73,14 +74,15 @@ public ChatResponse call(Prompt prompt) {
// when
Runner runner = new InMemoryRunner(agent);
- Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet();
+ Session session =
+ runner.sessionService().createSession(agent.name(), "test-user").blockingGet();
Content userMessage =
Content.builder().role("user").parts(List.of(Part.fromText("What is a qubit?"))).build();
List events =
runner
- .runAsync(session, userMessage, com.google.adk.agents.RunConfig.builder().build())
+ .runAsync(session.sessionKey(), userMessage, RunConfig.builder().build())
.toList()
.blockingGet();
@@ -149,7 +151,8 @@ public ChatResponse call(Prompt prompt) {
// when
Runner runner = new InMemoryRunner(agent);
- Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet();
+ Session session =
+ runner.sessionService().createSession(agent.name(), "test-user").blockingGet();
Content userMessage =
Content.builder()
@@ -159,7 +162,7 @@ public ChatResponse call(Prompt prompt) {
List events =
runner
- .runAsync(session, userMessage, com.google.adk.agents.RunConfig.builder().build())
+ .runAsync(session.userId(), session.id(), userMessage, RunConfig.builder().build())
.toList()
.blockingGet();
@@ -217,7 +220,8 @@ public Flux stream(Prompt prompt) {
// when
Runner runner = new InMemoryRunner(agent);
- Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet();
+ Session session =
+ runner.sessionService().createSession(agent.name(), "test-user").blockingGet();
Content userMessage =
Content.builder()
@@ -228,11 +232,10 @@ public Flux stream(Prompt prompt) {
List events =
runner
.runAsync(
- session,
+ session.userId(),
+ session.id(),
userMessage,
- com.google.adk.agents.RunConfig.builder()
- .setStreamingMode(com.google.adk.agents.RunConfig.StreamingMode.SSE)
- .build())
+ RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.SSE).build())
.toList()
.blockingGet();
diff --git a/contrib/spring-ai/src/test/java/com/google/adk/models/springai/TestUtils.java b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/TestUtils.java
index f18ded055..c23e68eae 100644
--- a/contrib/spring-ai/src/test/java/com/google/adk/models/springai/TestUtils.java
+++ b/contrib/spring-ai/src/test/java/com/google/adk/models/springai/TestUtils.java
@@ -46,7 +46,7 @@ public static List askAgent(BaseAgent agent, boolean streaming, Object...
allEvents.addAll(
runner
.runAsync(
- session,
+ session.sessionKey(),
messageContent,
RunConfig.builder()
.setStreamingMode(
@@ -67,13 +67,17 @@ public static List askBlockingAgent(BaseAgent agent, Object... messages)
}
Runner runner = new InMemoryRunner(agent);
- Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet();
+ Session session =
+ runner.sessionService().createSession(agent.name(), "test-user").blockingGet();
List events = new ArrayList<>();
for (Content content : contents) {
List batchEvents =
- runner.runAsync(session, content, RunConfig.builder().build()).toList().blockingGet();
+ runner
+ .runAsync(session.userId(), session.id(), content, RunConfig.builder().build())
+ .toList()
+ .blockingGet();
events.addAll(batchEvents);
}
@@ -88,7 +92,8 @@ public static List askAgentStreaming(BaseAgent agent, Object... messages)
}
Runner runner = new InMemoryRunner(agent);
- Session session = runner.sessionService().createSession("test-app", "test-user").blockingGet();
+ Session session =
+ runner.sessionService().createSession(agent.name(), "test-user").blockingGet();
List events = new ArrayList<>();
@@ -96,7 +101,8 @@ public static List askAgentStreaming(BaseAgent agent, Object... messages)
List batchEvents =
runner
.runAsync(
- session,
+ session.userId(),
+ session.id(),
content,
RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.SSE).build())
.toList()
diff --git a/core/pom.xml b/core/pom.xml
index a0f843f56..8c3c2069c 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -20,7 +20,7 @@
com.google.adk
google-adk-parent
- 0.8.1-SNAPSHOT
+ 0.9.1-SNAPSHOT
google-adk
@@ -92,6 +92,10 @@
com.google.errorprone
error_prone_annotations
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
com.fasterxml.jackson.core
jackson-databind
diff --git a/core/src/main/java/com/google/adk/Version.java b/core/src/main/java/com/google/adk/Version.java
index 1dc0282c3..a7aeb8b1f 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.8.0"; // x-release-please-released-version
+ public static final String JAVA_ADK_VERSION = "0.9.0"; // x-release-please-released-version
private Version() {}
}
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 d74ba9ca5..ed6631c50 100644
--- a/core/src/main/java/com/google/adk/agents/BaseAgent.java
+++ b/core/src/main/java/com/google/adk/agents/BaseAgent.java
@@ -29,10 +29,10 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.DoNotCall;
import com.google.genai.types.Content;
+import io.opentelemetry.context.Context;
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 java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -312,37 +312,41 @@ public Flowable runAsync(InvocationContext parentContext) {
private Flowable run(
InvocationContext parentContext,
Function> runImplementation) {
+ Context parentSpanContext = Context.current();
return Flowable.defer(
() -> {
InvocationContext invocationContext = createInvocationContext(parentContext);
+ Flowable mainAndAfterEvents =
+ Flowable.defer(() -> runImplementation.apply(invocationContext))
+ .concatWith(
+ Flowable.defer(
+ () ->
+ callCallback(
+ afterCallbacksToFunctions(
+ invocationContext.pluginManager(), afterAgentCallback),
+ invocationContext)
+ .toFlowable()));
+
return callCallback(
beforeCallbacksToFunctions(
invocationContext.pluginManager(), beforeAgentCallback),
invocationContext)
.flatMapPublisher(
- beforeEventOpt -> {
+ beforeEvent -> {
if (invocationContext.endInvocation()) {
- return Flowable.fromOptional(beforeEventOpt);
+ return Flowable.just(beforeEvent);
}
-
- Flowable beforeEvents = Flowable.fromOptional(beforeEventOpt);
- Flowable mainEvents =
- Flowable.defer(() -> runImplementation.apply(invocationContext));
- Flowable afterEvents =
- Flowable.defer(
- () ->
- callCallback(
- afterCallbacksToFunctions(
- invocationContext.pluginManager(), afterAgentCallback),
- invocationContext)
- .flatMapPublisher(Flowable::fromOptional));
-
- return Flowable.concat(beforeEvents, mainEvents, afterEvents);
+ return Flowable.just(beforeEvent).concatWith(mainAndAfterEvents);
})
+ .switchIfEmpty(mainAndAfterEvents)
.compose(
- Tracing.traceAgent(
- "invoke_agent " + name(), name(), description(), invocationContext));
+ Tracing.trace("invoke_agent " + name())
+ .setParent(parentSpanContext)
+ .configure(
+ span ->
+ Tracing.traceAgentInvocation(
+ span, name(), description(), invocationContext)));
});
}
@@ -383,13 +387,13 @@ private ImmutableList>> callbacksTo
*
* @param agentCallbacks Callback functions.
* @param invocationContext Current invocation context.
- * @return single emitting first event, or empty if none.
+ * @return maybe emitting first event, or empty if none.
*/
- private Single> callCallback(
+ private Maybe callCallback(
List>> agentCallbacks,
InvocationContext invocationContext) {
if (agentCallbacks.isEmpty()) {
- return Single.just(Optional.empty());
+ return Maybe.empty();
}
CallbackContext callbackContext =
@@ -404,21 +408,20 @@ private Single> callCallback(
.map(
content -> {
invocationContext.setEndInvocation(true);
- return Optional.of(
- Event.builder()
- .id(Event.generateEventId())
- .invocationId(invocationContext.invocationId())
- .author(name())
- .branch(invocationContext.branch().orElse(null))
- .actions(callbackContext.eventActions())
- .content(content)
- .build());
+ return Event.builder()
+ .id(Event.generateEventId())
+ .invocationId(invocationContext.invocationId())
+ .author(name())
+ .branch(invocationContext.branch().orElse(null))
+ .actions(callbackContext.eventActions())
+ .content(content)
+ .build();
})
.toFlowable();
})
.firstElement()
.switchIfEmpty(
- Single.defer(
+ Maybe.defer(
() -> {
if (callbackContext.state().hasDelta()) {
Event.Builder eventBuilder =
@@ -429,9 +432,9 @@ private Single> callCallback(
.branch(invocationContext.branch().orElse(null))
.actions(callbackContext.eventActions());
- return Single.just(Optional.of(eventBuilder.build()));
+ return Maybe.just(eventBuilder.build());
} else {
- return Single.just(Optional.empty());
+ return Maybe.empty();
}
}));
}
diff --git a/core/src/main/java/com/google/adk/agents/CallbackContext.java b/core/src/main/java/com/google/adk/agents/CallbackContext.java
index a29783769..da5b0d794 100644
--- a/core/src/main/java/com/google/adk/agents/CallbackContext.java
+++ b/core/src/main/java/com/google/adk/agents/CallbackContext.java
@@ -19,12 +19,12 @@
import com.google.adk.artifacts.ListArtifactsResponse;
import com.google.adk.events.EventActions;
import com.google.adk.sessions.State;
+import com.google.common.base.Preconditions;
import com.google.genai.types.Part;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import java.util.List;
-import java.util.Optional;
/** The context of various callbacks for an agent invocation. */
public class CallbackContext extends ReadonlyContext {
@@ -94,22 +94,19 @@ public Single> listArtifacts() {
/** Loads the latest version of an artifact from the service. */
public Maybe loadArtifact(String filename) {
- return loadArtifact(filename, Optional.empty());
+ checkArtifactServiceInitialized();
+ return invocationContext
+ .artifactService()
+ .loadArtifact(
+ invocationContext.appName(),
+ invocationContext.userId(),
+ invocationContext.session().id(),
+ filename);
}
/** Loads a specific version of an artifact from the service. */
public Maybe loadArtifact(String filename, int version) {
- return loadArtifact(filename, Optional.of(version));
- }
-
- /**
- * @deprecated Use {@link #loadArtifact(String)} or {@link #loadArtifact(String, int)} instead.
- */
- @Deprecated
- public Maybe loadArtifact(String filename, Optional version) {
- if (invocationContext.artifactService() == null) {
- throw new IllegalStateException("Artifact service is not initialized.");
- }
+ checkArtifactServiceInitialized();
return invocationContext
.artifactService()
.loadArtifact(
@@ -120,6 +117,11 @@ public Maybe loadArtifact(String filename, Optional version) {
version);
}
+ private void checkArtifactServiceInitialized() {
+ Preconditions.checkState(
+ invocationContext.artifactService() != null, "Artifact service is not initialized.");
+ }
+
/**
* Saves an artifact and records it as a delta for the current session.
*
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 7602ca9f2..91ce13a87 100644
--- a/core/src/main/java/com/google/adk/agents/InvocationContext.java
+++ b/core/src/main/java/com/google/adk/agents/InvocationContext.java
@@ -27,7 +27,6 @@
import com.google.adk.sessions.Session;
import com.google.adk.summarizer.EventsCompactionConfig;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
-import com.google.errorprone.annotations.InlineMe;
import com.google.genai.types.Content;
import java.util.Map;
import java.util.Objects;
@@ -75,63 +74,10 @@ protected InvocationContext(Builder builder) {
this.eventsCompactionConfig = builder.eventsCompactionConfig;
this.contextCacheConfig = builder.contextCacheConfig;
this.invocationCostManager = builder.invocationCostManager;
- this.callbackContextData = new ConcurrentHashMap<>(builder.callbackContextData);
- }
-
- /**
- * @deprecated Use {@link #builder()} instead.
- */
- @InlineMe(
- replacement =
- "InvocationContext.builder()"
- + ".sessionService(sessionService)"
- + ".artifactService(artifactService)"
- + ".invocationId(invocationId)"
- + ".agent(agent)"
- + ".session(session)"
- + ".userContent(userContent)"
- + ".runConfig(runConfig)"
- + ".build()",
- imports = {"com.google.adk.agents.InvocationContext"})
- @Deprecated(forRemoval = true)
- public static InvocationContext create(
- BaseSessionService sessionService,
- BaseArtifactService artifactService,
- String invocationId,
- BaseAgent agent,
- Session session,
- Content userContent,
- RunConfig runConfig) {
- return builder()
- .sessionService(sessionService)
- .artifactService(artifactService)
- .invocationId(invocationId)
- .agent(agent)
- .session(session)
- .userContent(userContent)
- .runConfig(runConfig)
- .build();
- }
-
- /**
- * @deprecated Use {@link #builder()} instead.
- */
- @Deprecated(forRemoval = true)
- public static InvocationContext create(
- BaseSessionService sessionService,
- BaseArtifactService artifactService,
- BaseAgent agent,
- Session session,
- LiveRequestQueue liveRequestQueue,
- RunConfig runConfig) {
- return builder()
- .sessionService(sessionService)
- .artifactService(artifactService)
- .agent(agent)
- .session(session)
- .liveRequestQueue(liveRequestQueue)
- .runConfig(runConfig)
- .build();
+ // Don't copy the callback context data. This should be the same instance for the full
+ // invocation invocation so that Plugins can access the same data it during the invocation
+ // across all types of callbacks.
+ this.callbackContextData = builder.callbackContextData;
}
/** Returns a new {@link Builder} for creating {@link InvocationContext} instances. */
@@ -345,7 +291,10 @@ private Builder(InvocationContext context) {
this.eventsCompactionConfig = context.eventsCompactionConfig;
this.contextCacheConfig = context.contextCacheConfig;
this.invocationCostManager = context.invocationCostManager;
- this.callbackContextData = new ConcurrentHashMap<>(context.callbackContextData);
+ // Don't copy the callback context data. This should be the same instance for the full
+ // invocation invocation so that Plugins can access the same data it during the invocation
+ // across all types of callbacks.
+ this.callbackContextData = context.callbackContextData;
}
private BaseSessionService sessionService;
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 bbed217f4..d326d8154 100644
--- a/core/src/main/java/com/google/adk/agents/LlmAgent.java
+++ b/core/src/main/java/com/google/adk/agents/LlmAgent.java
@@ -757,14 +757,6 @@ public Single> canonicalGlobalInstruction(ReadonlyCon
throw new IllegalStateException("Unknown Instruction subtype: " + globalInstruction.getClass());
}
- /**
- * @deprecated Use {@link #canonicalTools(ReadonlyContext)} instead.
- */
- @Deprecated
- public Flowable canonicalTools(Optional context) {
- return canonicalTools(context.orElse(null));
- }
-
/**
* Constructs the list of tools for this agent based on the {@link #tools} field.
*
diff --git a/core/src/main/java/com/google/adk/agents/LoopAgent.java b/core/src/main/java/com/google/adk/agents/LoopAgent.java
index d9d049f80..743d569b9 100644
--- a/core/src/main/java/com/google/adk/agents/LoopAgent.java
+++ b/core/src/main/java/com/google/adk/agents/LoopAgent.java
@@ -21,7 +21,7 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.reactivex.rxjava3.core.Flowable;
import java.util.List;
-import java.util.Optional;
+import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,7 +34,7 @@
public class LoopAgent extends BaseAgent {
private static final Logger logger = LoggerFactory.getLogger(LoopAgent.class);
- private final Optional maxIterations;
+ private final @Nullable Integer maxIterations;
/**
* Constructor for LoopAgent.
@@ -50,7 +50,7 @@ private LoopAgent(
String name,
String description,
List extends BaseAgent> subAgents,
- Optional maxIterations,
+ @Nullable Integer maxIterations,
List beforeAgentCallback,
List afterAgentCallback) {
@@ -60,16 +60,10 @@ private LoopAgent(
/** Builder for {@link LoopAgent}. */
public static class Builder extends BaseAgent.Builder {
- private Optional maxIterations = Optional.empty();
+ private @Nullable Integer maxIterations;
@CanIgnoreReturnValue
- public Builder maxIterations(int maxIterations) {
- this.maxIterations = Optional.of(maxIterations);
- return this;
- }
-
- @CanIgnoreReturnValue
- public Builder maxIterations(Optional maxIterations) {
+ public Builder maxIterations(@Nullable Integer maxIterations) {
this.maxIterations = maxIterations;
return this;
}
@@ -124,7 +118,7 @@ protected Flowable runAsyncImpl(InvocationContext invocationContext) {
return Flowable.fromIterable(subAgents)
.concatMap(subAgent -> subAgent.runAsync(invocationContext))
- .repeat(maxIterations.orElse(Integer.MAX_VALUE))
+ .repeat(maxIterations != null ? maxIterations : Integer.MAX_VALUE)
.takeUntil(LoopAgent::hasEscalateAction);
}
@@ -137,4 +131,8 @@ protected Flowable runLiveImpl(InvocationContext invocationContext) {
private static boolean hasEscalateAction(Event event) {
return event.actions().escalate().orElse(false);
}
+
+ public @Nullable Integer maxIterations() {
+ return maxIterations;
+ }
}
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 a9bb6ba4d..acf5979c2 100644
--- a/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java
+++ b/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java
@@ -22,7 +22,7 @@
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
-import java.util.Optional;
+import org.jspecify.annotations.Nullable;
/** Base interface for artifact services. */
public interface BaseArtifactService {
@@ -75,7 +75,7 @@ default Single saveAndReloadArtifact(
/** 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());
+ return loadArtifact(appName, userId, sessionId, filename, /* version= */ (Integer) null);
}
/** Loads the latest version of an artifact from the service. */
@@ -86,7 +86,7 @@ default Maybe loadArtifact(SessionKey sessionKey, String 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));
+ return loadArtifact(appName, userId, sessionId, filename, Integer.valueOf(version));
}
default Maybe loadArtifact(SessionKey sessionKey, String filename, int version) {
@@ -94,13 +94,8 @@ default Maybe loadArtifact(SessionKey sessionKey, String filename, int ver
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.
- */
- @Deprecated
Maybe loadArtifact(
- String appName, String userId, String sessionId, String filename, Optional version);
+ String appName, String userId, String sessionId, String filename, @Nullable Integer version);
/**
* Lists all the artifact filenames within a session.
diff --git a/core/src/main/java/com/google/adk/artifacts/GcsArtifactService.java b/core/src/main/java/com/google/adk/artifacts/GcsArtifactService.java
index e31d50327..977153828 100644
--- a/core/src/main/java/com/google/adk/artifacts/GcsArtifactService.java
+++ b/core/src/main/java/com/google/adk/artifacts/GcsArtifactService.java
@@ -38,6 +38,7 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import org.jspecify.annotations.Nullable;
/** An artifact service implementation using Google Cloud Storage (GCS). */
public final class GcsArtifactService implements BaseArtifactService {
@@ -126,8 +127,8 @@ public Single saveArtifact(
*/
@Override
public Maybe loadArtifact(
- String appName, String userId, String sessionId, String filename, Optional version) {
- return version
+ String appName, String userId, String sessionId, String filename, @Nullable Integer version) {
+ return Optional.ofNullable(version)
.map(Maybe::just)
.orElseGet(
() ->
diff --git a/core/src/main/java/com/google/adk/artifacts/InMemoryArtifactService.java b/core/src/main/java/com/google/adk/artifacts/InMemoryArtifactService.java
index 8c8ec2af8..510c96c2e 100644
--- a/core/src/main/java/com/google/adk/artifacts/InMemoryArtifactService.java
+++ b/core/src/main/java/com/google/adk/artifacts/InMemoryArtifactService.java
@@ -28,8 +28,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.stream.IntStream;
+import org.jspecify.annotations.Nullable;
/** An in-memory implementation of the {@link BaseArtifactService}. */
public final class InMemoryArtifactService implements BaseArtifactService {
@@ -61,7 +61,7 @@ public Single saveArtifact(
*/
@Override
public Maybe loadArtifact(
- String appName, String userId, String sessionId, String filename, Optional version) {
+ String appName, String userId, String sessionId, String filename, @Nullable Integer version) {
List versions =
getArtifactsMap(appName, userId, sessionId)
.computeIfAbsent(filename, unused -> new ArrayList<>());
@@ -69,10 +69,9 @@ public Maybe loadArtifact(
if (versions.isEmpty()) {
return Maybe.empty();
}
- if (version.isPresent()) {
- int v = version.get();
- if (v >= 0 && v < versions.size()) {
- return Maybe.just(versions.get(v));
+ if (version != null) {
+ if (version >= 0 && version < versions.size()) {
+ return Maybe.just(versions.get(version));
} else {
return Maybe.empty();
}
diff --git a/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java b/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java
index b9afdcaff..a4d3771c3 100644
--- a/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java
+++ b/core/src/main/java/com/google/adk/codeexecutors/CodeExecutionUtils.java
@@ -34,6 +34,7 @@
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.jspecify.annotations.Nullable;
/** Utility functions for code execution. */
public final class CodeExecutionUtils {
@@ -237,8 +238,7 @@ public abstract static class CodeExecutionInput extends JsonBaseModel {
public static Builder builder() {
return new AutoValue_CodeExecutionUtils_CodeExecutionInput.Builder()
- .inputFiles(ImmutableList.of())
- .executionId(Optional.empty());
+ .inputFiles(ImmutableList.of());
}
/** Builder for {@link CodeExecutionInput}. */
@@ -248,7 +248,7 @@ public abstract static class Builder {
public abstract Builder inputFiles(List inputFiles);
- public abstract Builder executionId(Optional executionId);
+ public abstract Builder executionId(@Nullable String executionId);
public abstract CodeExecutionInput build();
}
diff --git a/core/src/main/java/com/google/adk/codeexecutors/VertexAiCodeExecutor.java b/core/src/main/java/com/google/adk/codeexecutors/VertexAiCodeExecutor.java
index 5268edf39..af2219d18 100644
--- a/core/src/main/java/com/google/adk/codeexecutors/VertexAiCodeExecutor.java
+++ b/core/src/main/java/com/google/adk/codeexecutors/VertexAiCodeExecutor.java
@@ -36,7 +36,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
+import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -140,7 +140,7 @@ public CodeExecutionResult executeCode(
executeCodeInterpreter(
getCodeWithImports(codeExecutionInput.code()),
codeExecutionInput.inputFiles(),
- codeExecutionInput.executionId());
+ codeExecutionInput.executionId().orElse(null));
// Save output file as artifacts.
List savedFiles = new ArrayList<>();
@@ -173,7 +173,7 @@ public CodeExecutionResult executeCode(
}
private Map executeCodeInterpreter(
- String code, List inputFiles, Optional sessionId) {
+ String code, List inputFiles, @Nullable String sessionId) {
ExtensionExecutionServiceClient codeInterpreterExtension = getCodeInterpreterExtension();
if (codeInterpreterExtension == null) {
logger.warn("Vertex AI Code Interpreter execution is not available. Returning empty result.");
@@ -196,8 +196,9 @@ private Map executeCodeInterpreter(
paramsBuilder.putFields(
"files", Value.newBuilder().setListValue(listBuilder.build()).build());
}
- sessionId.ifPresent(
- s -> paramsBuilder.putFields("session_id", Value.newBuilder().setStringValue(s).build()));
+ if (sessionId != null) {
+ paramsBuilder.putFields("session_id", Value.newBuilder().setStringValue(sessionId).build());
+ }
ExecuteExtensionRequest request =
ExecuteExtensionRequest.newBuilder()
diff --git a/core/src/main/java/com/google/adk/events/EventActions.java b/core/src/main/java/com/google/adk/events/EventActions.java
index bf25acfc7..1ca856b45 100644
--- a/core/src/main/java/com/google/adk/events/EventActions.java
+++ b/core/src/main/java/com/google/adk/events/EventActions.java
@@ -28,36 +28,32 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
-import javax.annotation.Nullable;
+import org.jspecify.annotations.Nullable;
/** Represents the actions attached to an event. */
// TODO - b/414081262 make json wire camelCase
@JsonDeserialize(builder = EventActions.Builder.class)
public class EventActions extends JsonBaseModel {
- private Optional skipSummarization;
+ private @Nullable Boolean skipSummarization;
private ConcurrentMap stateDelta;
private ConcurrentMap artifactDelta;
private Set deletedArtifactIds;
- private Optional transferToAgent;
- private Optional escalate;
+ private @Nullable String transferToAgent;
+ private @Nullable Boolean escalate;
private ConcurrentMap> requestedAuthConfigs;
private ConcurrentMap requestedToolConfirmations;
private boolean endOfAgent;
- private Optional compaction;
+ private @Nullable EventCompaction compaction;
/** Default constructor for Jackson. */
public EventActions() {
- this.skipSummarization = Optional.empty();
this.stateDelta = new ConcurrentHashMap<>();
this.artifactDelta = new ConcurrentHashMap<>();
this.deletedArtifactIds = new HashSet<>();
- this.transferToAgent = Optional.empty();
- this.escalate = Optional.empty();
this.requestedAuthConfigs = new ConcurrentHashMap<>();
this.requestedToolConfirmations = new ConcurrentHashMap<>();
this.endOfAgent = false;
- this.compaction = Optional.empty();
}
private EventActions(Builder builder) {
@@ -75,27 +71,23 @@ private EventActions(Builder builder) {
@JsonProperty("skipSummarization")
public Optional skipSummarization() {
- return skipSummarization;
+ return Optional.ofNullable(skipSummarization);
}
public void setSkipSummarization(@Nullable Boolean skipSummarization) {
- this.skipSummarization = Optional.ofNullable(skipSummarization);
- }
-
- public void setSkipSummarization(Optional skipSummarization) {
this.skipSummarization = skipSummarization;
}
public void setSkipSummarization(boolean skipSummarization) {
- this.skipSummarization = Optional.of(skipSummarization);
+ this.skipSummarization = skipSummarization;
}
@JsonProperty("stateDelta")
- public ConcurrentMap stateDelta() {
+ public Map stateDelta() {
return stateDelta;
}
- @Deprecated // Use stateDelta(), addState() and removeStateByKey() instead.
+ @Deprecated // Use stateDelta() and removeStateByKey() instead.
public void setStateDelta(ConcurrentMap stateDelta) {
this.stateDelta = stateDelta;
}
@@ -110,12 +102,12 @@ public void removeStateByKey(String key) {
}
@JsonProperty("artifactDelta")
- public ConcurrentMap artifactDelta() {
+ public Map artifactDelta() {
return artifactDelta;
}
- public void setArtifactDelta(ConcurrentMap artifactDelta) {
- this.artifactDelta = artifactDelta;
+ public void setArtifactDelta(Map artifactDelta) {
+ this.artifactDelta = new ConcurrentHashMap<>(artifactDelta);
}
@JsonProperty("deletedArtifactIds")
@@ -130,30 +122,22 @@ public void setDeletedArtifactIds(Set deletedArtifactIds) {
@JsonProperty("transferToAgent")
public Optional transferToAgent() {
- return transferToAgent;
+ return Optional.ofNullable(transferToAgent);
}
- public void setTransferToAgent(Optional transferToAgent) {
+ public void setTransferToAgent(@Nullable String transferToAgent) {
this.transferToAgent = transferToAgent;
}
- public void setTransferToAgent(String transferToAgent) {
- this.transferToAgent = Optional.ofNullable(transferToAgent);
- }
-
@JsonProperty("escalate")
public Optional escalate() {
- return escalate;
+ return Optional.ofNullable(escalate);
}
- public void setEscalate(Optional escalate) {
+ public void setEscalate(@Nullable Boolean escalate) {
this.escalate = escalate;
}
- public void setEscalate(boolean escalate) {
- this.escalate = Optional.of(escalate);
- }
-
@JsonProperty("requestedAuthConfigs")
public ConcurrentMap> requestedAuthConfigs() {
return requestedAuthConfigs;
@@ -165,13 +149,20 @@ public void setRequestedAuthConfigs(
}
@JsonProperty("requestedToolConfirmations")
- public ConcurrentMap requestedToolConfirmations() {
+ public Map requestedToolConfirmations() {
return requestedToolConfirmations;
}
public void setRequestedToolConfirmations(
- ConcurrentMap requestedToolConfirmations) {
- this.requestedToolConfirmations = requestedToolConfirmations;
+ Map requestedToolConfirmations) {
+ if (requestedToolConfirmations == null) {
+ this.requestedToolConfirmations = new ConcurrentHashMap<>();
+ } else if (requestedToolConfirmations instanceof ConcurrentMap) {
+ this.requestedToolConfirmations =
+ (ConcurrentMap) requestedToolConfirmations;
+ } else {
+ this.requestedToolConfirmations = new ConcurrentHashMap<>(requestedToolConfirmations);
+ }
}
@JsonProperty("endOfAgent")
@@ -192,14 +183,6 @@ public Optional endInvocation() {
return endOfAgent ? Optional.of(true) : Optional.empty();
}
- /**
- * @deprecated Use {@link #setEndOfAgent(boolean)} instead.
- */
- @Deprecated
- public void setEndInvocation(Optional endInvocation) {
- this.endOfAgent = endInvocation.orElse(false);
- }
-
/**
* @deprecated Use {@link #setEndOfAgent(boolean)} instead.
*/
@@ -210,10 +193,10 @@ public void setEndInvocation(boolean endInvocation) {
@JsonProperty("compaction")
public Optional compaction() {
- return compaction;
+ return Optional.ofNullable(compaction);
}
- public void setCompaction(Optional compaction) {
+ public void setCompaction(@Nullable EventCompaction compaction) {
this.compaction = compaction;
}
@@ -262,47 +245,43 @@ public int hashCode() {
/** Builder for {@link EventActions}. */
public static class Builder {
- private Optional skipSummarization;
+ private @Nullable Boolean skipSummarization;
private ConcurrentMap stateDelta;
private ConcurrentMap artifactDelta;
private Set deletedArtifactIds;
- private Optional transferToAgent;
- private Optional escalate;
+ private @Nullable String transferToAgent;
+ private @Nullable Boolean escalate;
private ConcurrentMap> requestedAuthConfigs;
private ConcurrentMap requestedToolConfirmations;
private boolean endOfAgent = false;
- private Optional compaction;
+ private @Nullable EventCompaction compaction;
public Builder() {
- this.skipSummarization = Optional.empty();
this.stateDelta = new ConcurrentHashMap<>();
this.artifactDelta = new ConcurrentHashMap<>();
this.deletedArtifactIds = new HashSet<>();
- this.transferToAgent = Optional.empty();
- this.escalate = Optional.empty();
this.requestedAuthConfigs = new ConcurrentHashMap<>();
this.requestedToolConfirmations = new ConcurrentHashMap<>();
- this.compaction = Optional.empty();
}
private Builder(EventActions eventActions) {
- this.skipSummarization = eventActions.skipSummarization();
+ this.skipSummarization = eventActions.skipSummarization;
this.stateDelta = new ConcurrentHashMap<>(eventActions.stateDelta());
this.artifactDelta = new ConcurrentHashMap<>(eventActions.artifactDelta());
this.deletedArtifactIds = new HashSet<>(eventActions.deletedArtifactIds());
- this.transferToAgent = eventActions.transferToAgent();
- this.escalate = eventActions.escalate();
+ this.transferToAgent = eventActions.transferToAgent;
+ this.escalate = eventActions.escalate;
this.requestedAuthConfigs = new ConcurrentHashMap<>(eventActions.requestedAuthConfigs());
this.requestedToolConfirmations =
new ConcurrentHashMap<>(eventActions.requestedToolConfirmations());
- this.endOfAgent = eventActions.endOfAgent();
- this.compaction = eventActions.compaction();
+ this.endOfAgent = eventActions.endOfAgent;
+ this.compaction = eventActions.compaction;
}
@CanIgnoreReturnValue
@JsonProperty("skipSummarization")
public Builder skipSummarization(boolean skipSummarization) {
- this.skipSummarization = Optional.of(skipSummarization);
+ this.skipSummarization = skipSummarization;
return this;
}
@@ -315,8 +294,8 @@ public Builder stateDelta(ConcurrentMap value) {
@CanIgnoreReturnValue
@JsonProperty("artifactDelta")
- public Builder artifactDelta(ConcurrentMap value) {
- this.artifactDelta = value;
+ public Builder artifactDelta(Map value) {
+ this.artifactDelta = new ConcurrentHashMap<>(value);
return this;
}
@@ -329,15 +308,15 @@ public Builder deletedArtifactIds(Set value) {
@CanIgnoreReturnValue
@JsonProperty("transferToAgent")
- public Builder transferToAgent(String agentId) {
- this.transferToAgent = Optional.ofNullable(agentId);
+ public Builder transferToAgent(@Nullable String agentId) {
+ this.transferToAgent = agentId;
return this;
}
@CanIgnoreReturnValue
@JsonProperty("escalate")
public Builder escalate(boolean escalate) {
- this.escalate = Optional.of(escalate);
+ this.escalate = escalate;
return this;
}
@@ -351,8 +330,16 @@ public Builder requestedAuthConfigs(
@CanIgnoreReturnValue
@JsonProperty("requestedToolConfirmations")
- public Builder requestedToolConfirmations(ConcurrentMap value) {
- this.requestedToolConfirmations = value;
+ public Builder requestedToolConfirmations(@Nullable Map value) {
+ if (value == null) {
+ this.requestedToolConfirmations = new ConcurrentHashMap<>();
+ return this;
+ }
+ if (value instanceof ConcurrentMap) {
+ this.requestedToolConfirmations = (ConcurrentMap) value;
+ } else {
+ this.requestedToolConfirmations = new ConcurrentHashMap<>(value);
+ }
return this;
}
@@ -376,8 +363,8 @@ public Builder endInvocation(boolean endInvocation) {
@CanIgnoreReturnValue
@JsonProperty("compaction")
- public Builder compaction(EventCompaction value) {
- this.compaction = Optional.ofNullable(value);
+ public Builder compaction(@Nullable EventCompaction value) {
+ this.compaction = value;
return this;
}
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 6ed9ccaa3..ab5f6567a 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
@@ -164,58 +164,63 @@ protected Flowable postprocess(
* callbacks. Callbacks should not rely on its ID if they create their own separate events.
*/
private Flowable callLlm(
- InvocationContext context, LlmRequest llmRequest, Event eventForCallbackUsage) {
+ Context spanContext,
+ InvocationContext context,
+ LlmRequest llmRequest,
+ Event eventForCallbackUsage) {
LlmAgent agent = (LlmAgent) context.agent();
LlmRequest.Builder llmRequestBuilder = llmRequest.toBuilder();
return handleBeforeModelCallback(context, llmRequestBuilder, eventForCallbackUsage)
- .flatMapPublisher(
- beforeResponse -> {
- if (beforeResponse.isPresent()) {
- return Flowable.just(beforeResponse.get());
- }
- BaseLlm llm =
- agent.resolvedModel().model().isPresent()
- ? agent.resolvedModel().model().get()
- : LlmRegistry.getLlm(agent.resolvedModel().modelName().get());
- return llm.generateContent(
- llmRequestBuilder.build(),
- context.runConfig().streamingMode() == StreamingMode.SSE)
- .onErrorResumeNext(
- exception ->
- handleOnModelErrorCallback(
- context, llmRequestBuilder, eventForCallbackUsage, exception)
- .switchIfEmpty(Single.error(exception))
- .toFlowable())
- .doOnNext(
- llmResp ->
- Tracing.traceCallLlm(
- context,
- eventForCallbackUsage.id(),
- llmRequestBuilder.build(),
- llmResp))
- .doOnError(
- error -> {
- Span span = Span.current();
- span.setStatus(StatusCode.ERROR, error.getMessage());
- span.recordException(error);
- })
- .compose(Tracing.trace("call_llm"))
- .concatMap(
- llmResp ->
- handleAfterModelCallback(context, llmResp, eventForCallbackUsage)
- .toFlowable());
- });
+ .toFlowable()
+ .switchIfEmpty(
+ Flowable.defer(
+ () -> {
+ BaseLlm llm =
+ agent.resolvedModel().model().isPresent()
+ ? agent.resolvedModel().model().get()
+ : LlmRegistry.getLlm(agent.resolvedModel().modelName().get());
+ return llm.generateContent(
+ llmRequestBuilder.build(),
+ context.runConfig().streamingMode() == StreamingMode.SSE)
+ .onErrorResumeNext(
+ exception ->
+ handleOnModelErrorCallback(
+ context, llmRequestBuilder, eventForCallbackUsage, exception)
+ .switchIfEmpty(Single.error(exception))
+ .toFlowable())
+ .doOnError(
+ error -> {
+ Span span = Span.current();
+ span.setStatus(StatusCode.ERROR, error.getMessage());
+ span.recordException(error);
+ })
+ .compose(
+ Tracing.trace("call_llm")
+ .setParent(spanContext)
+ .onSuccess(
+ (span, llmResp) ->
+ Tracing.traceCallLlm(
+ span,
+ context,
+ eventForCallbackUsage.id(),
+ llmRequestBuilder.build(),
+ llmResp)))
+ .concatMap(
+ llmResp ->
+ handleAfterModelCallback(context, llmResp, eventForCallbackUsage)
+ .toFlowable());
+ }));
}
/**
* Invokes {@link BeforeModelCallback}s. If any returns a response, it's used instead of calling
* the LLM.
*
- * @return A {@link Single} with the callback result or {@link Optional#empty()}.
+ * @return A {@link Maybe} with the callback result.
*/
- private Single> handleBeforeModelCallback(
+ private Maybe handleBeforeModelCallback(
InvocationContext context, LlmRequest.Builder llmRequestBuilder, Event modelResponseEvent) {
Event callbackEvent = modelResponseEvent.toBuilder().build();
CallbackContext callbackContext =
@@ -228,7 +233,7 @@ private Single> handleBeforeModelCallback(
List extends BeforeModelCallback> callbacks = agent.canonicalBeforeModelCallbacks();
if (callbacks.isEmpty()) {
- return pluginResult.map(Optional::of).defaultIfEmpty(Optional.empty());
+ return pluginResult;
}
Maybe callbackResult =
@@ -238,10 +243,7 @@ private Single> handleBeforeModelCallback(
.concatMapMaybe(callback -> callback.call(callbackContext, llmRequestBuilder))
.firstElement());
- return pluginResult
- .switchIfEmpty(callbackResult)
- .map(Optional::of)
- .defaultIfEmpty(Optional.empty());
+ return pluginResult.switchIfEmpty(callbackResult);
}
/**
@@ -323,7 +325,7 @@ private Single handleAfterModelCallback(
* @throws LlmCallsLimitExceededException if the agent exceeds allowed LLM invocations.
* @throws IllegalStateException if a transfer agent is specified but not found.
*/
- private Flowable runOneStep(InvocationContext context) {
+ private Flowable runOneStep(Context spanContext, InvocationContext context) {
AtomicReference llmRequestRef = new AtomicReference<>(LlmRequest.builder().build());
return Flowable.defer(
@@ -355,7 +357,11 @@ private Flowable runOneStep(InvocationContext context) {
.build();
mutableEventTemplate.setTimestamp(0L);
- return callLlm(context, llmRequestAfterPreprocess, mutableEventTemplate)
+ return callLlm(
+ spanContext,
+ context,
+ llmRequestAfterPreprocess,
+ mutableEventTemplate)
.concatMap(
llmResponse -> {
try (Scope postScope = currentContext.makeCurrent()) {
@@ -407,11 +413,12 @@ private Flowable runOneStep(InvocationContext context) {
*/
@Override
public Flowable run(InvocationContext invocationContext) {
- return run(invocationContext, 0);
+ return run(Context.current(), invocationContext, 0);
}
- private Flowable run(InvocationContext invocationContext, int stepsCompleted) {
- Flowable currentStepEvents = runOneStep(invocationContext).cache();
+ private Flowable run(
+ Context spanContext, InvocationContext invocationContext, int stepsCompleted) {
+ Flowable currentStepEvents = runOneStep(spanContext, invocationContext).cache();
if (stepsCompleted + 1 >= maxSteps) {
logger.debug("Ending flow execution because max steps reached.");
return currentStepEvents;
@@ -431,7 +438,7 @@ private Flowable run(InvocationContext invocationContext, int stepsComple
return Flowable.empty();
} else {
logger.debug("Continuing to next step of the flow.");
- return run(invocationContext, stepsCompleted + 1);
+ return run(spanContext, invocationContext, stepsCompleted + 1);
}
}));
}
@@ -448,6 +455,7 @@ private Flowable run(InvocationContext invocationContext, int stepsComple
public Flowable runLive(InvocationContext invocationContext) {
AtomicReference llmRequestRef = new AtomicReference<>(LlmRequest.builder().build());
Flowable preprocessEvents = preprocess(invocationContext, llmRequestRef);
+ Context spanContext = Context.current();
return preprocessEvents.concatWith(
Flowable.defer(
@@ -485,7 +493,7 @@ public Flowable runLive(InvocationContext invocationContext) {
eventIdForSendData,
llmRequestAfterPreprocess.contents());
})
- .compose(Tracing.trace("send_data"));
+ .compose(Tracing.trace("send_data").setParent(spanContext));
Flowable liveRequests =
invocationContext
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 f7c3c51ef..d76cd1a04 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
@@ -159,8 +159,7 @@ public Single processResponse(
InvocationContext invocationContext, LlmResponse llmResponse) {
if (llmResponse.partial().orElse(false)) {
return Single.just(
- ResponseProcessor.ResponseProcessingResult.create(
- llmResponse, ImmutableList.of(), Optional.empty()));
+ ResponseProcessor.ResponseProcessingResult.create(llmResponse, ImmutableList.of()));
}
var llmResponseBuilder = llmResponse.toBuilder();
return runPostProcessor(invocationContext, llmResponseBuilder)
@@ -168,7 +167,7 @@ public Single processResponse(
.map(
events ->
ResponseProcessor.ResponseProcessingResult.create(
- llmResponseBuilder.build(), events, Optional.empty()));
+ llmResponseBuilder.build(), events));
}
}
@@ -241,7 +240,8 @@ private static Flowable runPreProcessor(
.code(codeStr)
.inputFiles(ImmutableList.of(file))
.executionId(
- getOrSetExecutionId(invocationContext, codeExecutorContext))
+ getOrSetExecutionId(invocationContext, codeExecutorContext)
+ .orElse(null))
.build());
codeExecutorContext.updateCodeExecutionResult(
@@ -321,7 +321,9 @@ private static Flowable runPostProcessor(
CodeExecutionInput.builder()
.code(codeStr)
.inputFiles(codeExecutorContext.getInputFiles())
- .executionId(getOrSetExecutionId(invocationContext, codeExecutorContext))
+ .executionId(
+ getOrSetExecutionId(invocationContext, codeExecutorContext)
+ .orElse(null))
.build());
codeExecutorContext.updateCodeExecutionResult(
invocationContext.invocationId(),
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 ca8e0a051..840a370c6 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
@@ -25,6 +25,7 @@
import com.google.adk.events.Event;
import com.google.adk.events.EventCompaction;
import com.google.adk.models.LlmRequest;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -41,6 +42,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import javax.annotation.Nullable;
/** {@link RequestProcessor} that populates content in request for LLM flows. */
public final class Contents implements RequestProcessor {
@@ -62,14 +64,19 @@ public Single processRequest(
modelName = "";
}
+ ImmutableList sessionEvents;
+ synchronized (context.session().events()) {
+ sessionEvents = ImmutableList.copyOf(context.session().events());
+ }
+
if (llmAgent.includeContents() == LlmAgent.IncludeContents.NONE) {
return Single.just(
RequestProcessor.RequestProcessingResult.create(
request.toBuilder()
.contents(
getCurrentTurnContents(
- context.branch(),
- context.session().events(),
+ context.branch().orElse(null),
+ sessionEvents,
context.agent().name(),
modelName))
.build(),
@@ -78,7 +85,7 @@ public Single processRequest(
ImmutableList contents =
getContents(
- context.branch(), context.session().events(), context.agent().name(), modelName);
+ context.branch().orElse(null), sessionEvents, context.agent().name(), modelName);
return Single.just(
RequestProcessor.RequestProcessingResult.create(
@@ -87,7 +94,7 @@ public Single processRequest(
/** Gets contents for the current turn only (no conversation history). */
private ImmutableList getCurrentTurnContents(
- Optional currentBranch, List events, String agentName, String modelName) {
+ @Nullable String currentBranch, List events, String agentName, String modelName) {
// Find the latest event that starts the current turn and process from there.
for (int i = events.size() - 1; i >= 0; i--) {
Event event = events.get(i);
@@ -99,7 +106,7 @@ private ImmutableList getCurrentTurnContents(
}
private ImmutableList getContents(
- Optional currentBranch, List events, String agentName, String modelName) {
+ @Nullable String currentBranch, List events, String agentName, String modelName) {
List filteredEvents = new ArrayList<>();
boolean hasCompactEvent = false;
@@ -414,16 +421,12 @@ private static String convertMapToJson(Map