diff --git a/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java b/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java
new file mode 100644
index 000000000..a5947dcb8
--- /dev/null
+++ b/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java
@@ -0,0 +1,12 @@
+package com.google.adk.a2a.common;
+
+/** Exception thrown when the the genai class has an empty field. */
+public class GenAiFieldMissingException extends RuntimeException {
+ public GenAiFieldMissingException(String message) {
+ super(message);
+ }
+
+ public GenAiFieldMissingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java b/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java
new file mode 100644
index 000000000..b5b53c49a
--- /dev/null
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java
@@ -0,0 +1,19 @@
+package com.google.adk.a2a.converters;
+
+/** Enum for the type of A2A DataPart metadata. */
+public enum A2ADataPartMetadataType {
+ FUNCTION_RESPONSE("function_response"),
+ FUNCTION_CALL("function_call"),
+ CODE_EXECUTION_RESULT("code_execution_result"),
+ EXECUTABLE_CODE("executable_code");
+
+ private final String type;
+
+ private A2ADataPartMetadataType(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+}
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java b/a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java
deleted file mode 100644
index 11bbbd326..000000000
--- a/a2a/src/main/java/com/google/adk/a2a/converters/ConversationPreprocessor.java
+++ /dev/null
@@ -1,108 +0,0 @@
-package com.google.adk.a2a.converters;
-
-import com.google.adk.events.Event;
-import com.google.common.collect.ImmutableList;
-import com.google.genai.types.Content;
-import com.google.genai.types.Part;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Preprocesses a batch of ADK events prior to invoking a remote A2A agent.
- *
- *
The class splits the conversation into two logical buckets:
- *
- *
- * - The historical session events that should be preserved as-is when relayed over the wire.
- *
- The most recent user-authored text event, surfaced separately so it can be supplied as the
- * pending user input on the {@link com.google.adk.agents.InvocationContext}.
- *
- *
- * This mirrors the Python A2A implementation where the in-flight user message is maintained
- * separately from the persisted transcript.
- *
- *
**EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not
- * use in production code.
- */
-public final class ConversationPreprocessor {
-
- /**
- * Immutable value that surfaces the results of preprocessing.
- *
- *
All fields are deliberately exposed to avoid additional AutoValue dependencies in this
- * internal module.
- */
- public static final class PreparedInput {
- /** Historical events that should remain in the session transcript. */
- public final ImmutableList historyEvents;
-
- /** Extracted user message content, if a qualifying text event was found. */
- public final Optional userContent;
-
- /** The concrete event that supplied {@link #userContent}, for callers needing metadata. */
- public final Optional userEvent;
-
- /**
- * Creates a new instance.
- *
- * @param historyEvents ordered historical events retained in the session stream
- * @param userContent optional content to place on the pending user message
- * @param userEvent optional original event that contained {@code userContent}
- */
- public PreparedInput(
- ImmutableList historyEvents,
- Optional userContent,
- Optional userEvent) {
- this.historyEvents = historyEvents;
- this.userContent = userContent;
- this.userEvent = userEvent;
- }
- }
-
- private ConversationPreprocessor() {}
-
- /**
- * Splits the provided event list into history and the latest user-authored text message.
- *
- * @param inputEvents ordered session events, oldest to newest; may be {@code null}
- * @return container encapsulating the derived history, optional user content, and the original
- * user event when present
- */
- public static PreparedInput extractHistoryAndUserContent(List inputEvents) {
- if (inputEvents == null || inputEvents.isEmpty()) {
- return new PreparedInput(ImmutableList.of(), Optional.empty(), Optional.empty());
- }
-
- Content userContent = null;
- int lastTextIndex = -1;
- Event userEvent = null;
- for (int i = inputEvents.size() - 1; i >= 0; i--) {
- Event ev = inputEvents.get(i);
- if (ev.content().isPresent() && ev.content().get().parts().isPresent()) {
- boolean hasText = false;
- for (Part p : ev.content().get().parts().get()) {
- if (p.text().isPresent()) {
- hasText = true;
- break;
- }
- }
- if (hasText) {
- userContent = ev.content().get();
- lastTextIndex = i;
- userEvent = ev;
- break;
- }
- }
- }
-
- ImmutableList.Builder historyBuilder = ImmutableList.builder();
- for (int i = 0; i < inputEvents.size(); i++) {
- if (i != lastTextIndex) {
- historyBuilder.add(inputEvents.get(i));
- }
- }
-
- return new PreparedInput(
- historyBuilder.build(), Optional.ofNullable(userContent), Optional.ofNullable(userEvent));
- }
-}
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java
index f5b1178c0..1a49b0070 100644
--- a/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java
@@ -3,14 +3,11 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.adk.agents.InvocationContext;
-import com.google.adk.events.Event;
import com.google.common.collect.ImmutableList;
import com.google.genai.types.Content;
-import com.google.genai.types.Part;
import io.a2a.spec.Message;
-import io.a2a.spec.TextPart;
-import java.util.ArrayList;
-import java.util.List;
+import io.a2a.spec.Part;
+import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
import org.slf4j.Logger;
@@ -28,47 +25,35 @@ public final class EventConverter {
private EventConverter() {}
/**
- * Aggregation mode for converting events to A2A messages.
+ * Converts an ADK InvocationContext to an A2A Message.
*
- * AS_IS: Parts are aggregated as-is.
+ *
It combines all the events in the session, plus the user content, converted into A2A Parts,
+ * into a single A2A Message.
*
- *
EXTERNAL_HANDOFF: Parts are aggregated as-is, except for function responses, which are
- * converted to text parts with the function name and response map.
+ *
If the context has no events, or no suitable content to build the message, an empty optional
+ * is returned.
+ *
+ * @param context The ADK InvocationContext to convert.
+ * @return The converted A2A Message.
*/
- public enum AggregationMode {
- AS_IS,
- EXTERNAL_HANDOFF
- }
-
- public static ImmutableList> contentToParts(Optional content) {
- if (content.isPresent() && content.get().parts().isPresent()) {
- return content.get().parts().get().stream()
- .map(PartConverter::fromGenaiPart)
- .flatMap(Optional::stream)
- .collect(toImmutableList());
- }
- return ImmutableList.of();
- }
-
public static Optional convertEventsToA2AMessage(InvocationContext context) {
- return convertEventsToA2AMessage(context, AggregationMode.AS_IS);
- }
-
- public static Optional convertEventsToA2AMessage(
- InvocationContext context, AggregationMode mode) {
if (context.session().events().isEmpty()) {
logger.warn("No events in session, cannot convert to A2A message.");
return Optional.empty();
}
- List> parts = new ArrayList<>();
- for (Event event : context.session().events()) {
- appendContentParts(event.content(), mode, parts);
- }
+ ImmutableList.Builder> partsBuilder = ImmutableList.builder();
context
- .userContent()
- .ifPresent(content -> appendContentParts(Optional.of(content), mode, parts));
+ .session()
+ .events()
+ .forEach(
+ event ->
+ partsBuilder.addAll(
+ contentToParts(event.content(), event.partial().orElse(false))));
+ partsBuilder.addAll(contentToParts(context.userContent(), false));
+
+ ImmutableList> parts = partsBuilder.build();
if (parts.isEmpty()) {
logger.warn("No suitable content found to build A2A request message.");
@@ -83,37 +68,11 @@ public static Optional convertEventsToA2AMessage(
.build());
}
- private static void appendContentParts(
- Optional contentOpt, AggregationMode mode, List> target) {
- if (contentOpt.isEmpty() || contentOpt.get().parts().isEmpty()) {
- return;
- }
-
- for (Part part : contentOpt.get().parts().get()) {
- if (part.text().isPresent()) {
- target.add(new TextPart(part.text().get()));
- continue;
- }
-
- if (part.functionCall().isPresent()) {
- if (mode == AggregationMode.AS_IS) {
- PartConverter.convertGenaiPartToA2aPart(part).ifPresent(target::add);
- }
- continue;
- }
-
- if (part.functionResponse().isPresent()) {
- if (mode == AggregationMode.AS_IS) {
- PartConverter.convertGenaiPartToA2aPart(part).ifPresent(target::add);
- } else {
- String name = part.functionResponse().get().name().orElse("");
- String mapStr = String.valueOf(part.functionResponse().get().response().orElse(null));
- target.add(new TextPart(String.format("%s response: %s", name, mapStr)));
- }
- continue;
- }
-
- PartConverter.fromGenaiPart(part).ifPresent(target::add);
- }
+ public static ImmutableList> contentToParts(
+ Optional content, boolean isPartial) {
+ return content.flatMap(Content::parts).stream()
+ .flatMap(Collection::stream)
+ .map(part -> PartConverter.fromGenaiPart(part, isPartial))
+ .collect(toImmutableList());
}
}
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java
index 8e407406f..05125d170 100644
--- a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java
@@ -4,6 +4,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.adk.a2a.common.GenAiFieldMissingException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.types.Blob;
@@ -38,18 +39,14 @@
* use in production code.
*/
public final class PartConverter {
+
private static final Logger logger = LoggerFactory.getLogger(PartConverter.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
-
// Constants for metadata types. By convention metadata keys are prefixed with "adk_" to align
// with the Python and Golang libraries.
public static final String A2A_DATA_PART_METADATA_TYPE_KEY = "adk_type";
public static final String A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY = "adk_is_long_running";
- public static final String A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL = "function_call";
- public static final String A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE = "function_response";
- public static final String A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT =
- "code_execution_result";
- public static final String A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = "executable_code";
+ public static final String A2A_DATA_PART_METADATA_IS_PARTIAL_KEY = "adk_partial";
public static final String LANGUAGE_KEY = "language";
public static final String OUTCOME_KEY = "outcome";
public static final String CODE_KEY = "code";
@@ -58,6 +55,10 @@ public final class PartConverter {
public static final String ARGS_KEY = "args";
public static final String RESPONSE_KEY = "response";
public static final String ID_KEY = "id";
+ public static final String WILL_CONTINUE_KEY = "willContinue";
+ public static final String PARTIAL_ARGS_KEY = "partialArgs";
+ public static final String SCHEDULING_KEY = "scheduling";
+ public static final String PARTS_KEY = "parts";
public static Optional toTextPart(io.a2a.spec.Part> part) {
if (part instanceof TextPart textPart) {
@@ -96,34 +97,6 @@ public static ImmutableList toGenaiParts(
.collect(toImmutableList());
}
- /**
- * Convert a Google GenAI Part to an A2A Part.
- *
- * @param part The GenAI part to convert.
- * @return Optional containing the converted A2A Part, or empty if conversion fails.
- */
- public static Optional convertGenaiPartToA2aPart(Part part) {
- if (part == null) {
- return Optional.empty();
- }
-
- if (part.text().isPresent()) {
- // Text parts are handled directly in the Message content, not as DataPart
- return Optional.empty();
- } else if (part.functionCall().isPresent()) {
- return createDataPartFromFunctionCall(part.functionCall().get());
- } else if (part.functionResponse().isPresent()) {
- return createDataPartFromFunctionResponse(part.functionResponse().get());
- } else if (part.executableCode().isPresent()) {
- return createDataPartFromExecutableCode(part.executableCode().get());
- } else if (part.codeExecutionResult().isPresent()) {
- return createDataPartFromCodeExecutionResult(part.codeExecutionResult().get());
- }
-
- logger.warn("Cannot convert unsupported part for Google GenAI part: " + part);
- return Optional.empty();
- }
-
private static Optional convertFilePartToGenAiPart(
FilePart filePart) {
FileContent fileContent = filePart.getFile();
@@ -170,7 +143,7 @@ private static Optional convertDataPartToGenAiPart(
String metadataType = metadata.getOrDefault(A2A_DATA_PART_METADATA_TYPE_KEY, "").toString();
if ((data.containsKey(NAME_KEY) && data.containsKey(ARGS_KEY))
- || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)) {
+ || metadataType.equals(A2ADataPartMetadataType.FUNCTION_CALL.getType())) {
String functionName = String.valueOf(data.getOrDefault(NAME_KEY, null));
String functionId = String.valueOf(data.getOrDefault(ID_KEY, null));
Map args = coerceToMap(data.get(ARGS_KEY));
@@ -182,7 +155,7 @@ private static Optional convertDataPartToGenAiPart(
}
if ((data.containsKey(NAME_KEY) && data.containsKey(RESPONSE_KEY))
- || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)) {
+ || metadataType.equals(A2ADataPartMetadataType.FUNCTION_RESPONSE.getType())) {
String functionName = String.valueOf(data.getOrDefault(NAME_KEY, ""));
String functionId = String.valueOf(data.getOrDefault(ID_KEY, ""));
Map response = coerceToMap(data.get(RESPONSE_KEY));
@@ -198,7 +171,7 @@ private static Optional convertDataPartToGenAiPart(
}
if ((data.containsKey(CODE_KEY) && data.containsKey(LANGUAGE_KEY))
- || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE)) {
+ || metadataType.equals(A2ADataPartMetadataType.EXECUTABLE_CODE.getType())) {
String code = String.valueOf(data.getOrDefault(CODE_KEY, ""));
String language =
String.valueOf(
@@ -212,7 +185,7 @@ private static Optional convertDataPartToGenAiPart(
}
if ((data.containsKey(OUTCOME_KEY) && data.containsKey(OUTPUT_KEY))
- || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT)) {
+ || metadataType.equals(A2ADataPartMetadataType.CODE_EXECUTION_RESULT.getType())) {
String outcome =
String.valueOf(data.getOrDefault(OUTCOME_KEY, Outcome.Known.OUTCOME_OK).toString());
String output = String.valueOf(data.getOrDefault(OUTPUT_KEY, ""));
@@ -251,112 +224,154 @@ public static Content messageToContent(Message message) {
*
* @return Optional containing the converted A2A Part, or empty if conversion fails.
*/
- private static Optional createDataPartFromFunctionCall(FunctionCall functionCall) {
- Map data = new HashMap<>();
- data.put(NAME_KEY, functionCall.name().orElse(""));
- data.put(ID_KEY, functionCall.id().orElse(""));
- data.put(ARGS_KEY, functionCall.args().orElse(ImmutableMap.of()));
-
- ImmutableMap metadata =
- ImmutableMap.of(A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL);
+ private static DataPart createDataPartFromFunctionCall(
+ FunctionCall functionCall, ImmutableMap.Builder metadata) {
+ ImmutableMap.Builder data = ImmutableMap.builder();
+ addValueIfPresent(data, NAME_KEY, functionCall.name());
+ addValueIfPresent(data, ID_KEY, functionCall.id());
+ addValueIfPresent(data, ARGS_KEY, functionCall.args());
+ addValueIfPresent(data, WILL_CONTINUE_KEY, functionCall.willContinue());
+ addValueIfPresent(data, PARTIAL_ARGS_KEY, functionCall.partialArgs());
+
+ metadata.put(A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.FUNCTION_CALL.getType());
+
+ return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
+ }
- return Optional.of(new DataPart(data, metadata));
+ private static void addValueIfPresent(
+ ImmutableMap.Builder data, String key, Optional> value) {
+ value.ifPresent(v -> data.put(key, v));
}
/**
* Creates an A2A DataPart from a Google GenAI FunctionResponse.
*
* @param functionResponse The GenAI FunctionResponse to convert.
- * @return Optional containing the converted A2A Part, or empty if conversion fails.
+ * @return The converted A2A Part.
*/
- private static Optional createDataPartFromFunctionResponse(
- FunctionResponse functionResponse) {
- Map data = new HashMap<>();
- data.put(NAME_KEY, functionResponse.name().orElse(""));
- data.put(ID_KEY, functionResponse.id().orElse(""));
- data.put(RESPONSE_KEY, functionResponse.response().orElse(ImmutableMap.of()));
-
- ImmutableMap metadata =
- ImmutableMap.of(
- A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE);
-
- return Optional.of(new DataPart(data, metadata));
+ private static DataPart createDataPartFromFunctionResponse(
+ FunctionResponse functionResponse, ImmutableMap.Builder metadata) {
+ ImmutableMap.Builder data = ImmutableMap.builder();
+ addValueIfPresent(data, NAME_KEY, functionResponse.name());
+ addValueIfPresent(data, ID_KEY, functionResponse.id());
+ addValueIfPresent(data, RESPONSE_KEY, functionResponse.response());
+ addValueIfPresent(data, WILL_CONTINUE_KEY, functionResponse.willContinue());
+ addValueIfPresent(data, SCHEDULING_KEY, functionResponse.scheduling());
+ addValueIfPresent(data, PARTS_KEY, functionResponse.parts());
+
+ metadata.put(
+ A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.FUNCTION_RESPONSE.getType());
+
+ return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
}
- private static Optional createDataPartFromExecutableCode(
- ExecutableCode executableCode) {
- Map data = new HashMap<>();
- data.put(CODE_KEY, executableCode.code().orElse(""));
+ /**
+ * Creates an A2A DataPart from a Google GenAI CodeExecutionResult.
+ *
+ * @param codeExecutionResult The GenAI CodeExecutionResult to convert.
+ * @return The converted A2A Part.
+ */
+ private static DataPart createDataPartFromCodeExecutionResult(
+ CodeExecutionResult codeExecutionResult, ImmutableMap.Builder metadata) {
+ ImmutableMap.Builder data = ImmutableMap.builder();
data.put(
- LANGUAGE_KEY,
- executableCode
- .language()
- .map(Language::toString)
- .orElse(Language.Known.LANGUAGE_UNSPECIFIED.toString()));
+ OUTCOME_KEY,
+ codeExecutionResult
+ .outcome()
+ .map(Outcome::toString)
+ .orElse(new Outcome(Outcome.Known.OUTCOME_UNSPECIFIED).toString()));
+ addValueIfPresent(data, OUTPUT_KEY, codeExecutionResult.output());
- ImmutableMap metadata =
- ImmutableMap.of(
- A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE);
+ metadata.put(
+ A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.CODE_EXECUTION_RESULT.getType());
- return Optional.of(new DataPart(data, metadata));
+ return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
}
- private static Optional createDataPartFromCodeExecutionResult(
- CodeExecutionResult result) {
- Map data = new HashMap<>();
+ /**
+ * Creates an A2A DataPart from a Google GenAI ExecutableCode.
+ *
+ * @param executableCode The GenAI ExecutableCode to convert.
+ * @return The converted A2A Part.
+ */
+ private static DataPart createDataPartFromExecutableCode(
+ ExecutableCode executableCode, ImmutableMap.Builder metadata) {
+ ImmutableMap.Builder data = ImmutableMap.builder();
data.put(
- OUTCOME_KEY,
- result
- .outcome()
- .map(Outcome::toString)
- .orElse(new Outcome(Outcome.Known.OUTCOME_UNSPECIFIED).toString()));
- data.put(OUTPUT_KEY, result.output().orElse(null));
+ LANGUAGE_KEY,
+ executableCode
+ .language()
+ .map(Language::toString)
+ .orElse(Language.Known.LANGUAGE_UNSPECIFIED.toString()));
+ addValueIfPresent(data, CODE_KEY, executableCode.code());
- ImmutableMap metadata =
- ImmutableMap.of(
- A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT);
+ metadata.put(
+ A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.EXECUTABLE_CODE.getType());
- return Optional.of(new DataPart(data, metadata));
+ return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
}
private PartConverter() {}
/** Convert a GenAI part into the A2A JSON representation. */
- public static Optional> fromGenaiPart(Part part) {
+ public static io.a2a.spec.Part> fromGenaiPart(Part part, boolean isPartial) {
if (part == null) {
- return Optional.empty();
+ throw new GenAiFieldMissingException("GenAI part cannot be null");
}
-
- if (part.text().isPresent()) {
- return Optional.of(new TextPart(part.text().get(), new HashMap<>()));
+ ImmutableMap.Builder metadata = ImmutableMap.builder();
+ if (isPartial) {
+ metadata.put(A2A_DATA_PART_METADATA_IS_PARTIAL_KEY, true);
}
- if (part.fileData().isPresent()) {
- FileData fileData = part.fileData().get();
- String uri = fileData.fileUri().orElse(null);
- String mime = fileData.mimeType().orElse(null);
- String name = fileData.displayName().orElse(null);
- return Optional.of(new FilePart(new FileWithUri(mime, name, uri), new HashMap<>()));
+ if (part.text().isPresent()) {
+ addValueIfPresent(metadata, "thought", part.thought());
+ return new TextPart(part.text().get(), metadata.buildOrThrow());
}
- if (part.inlineData().isPresent()) {
- Blob blob = part.inlineData().get();
- byte[] bytes = blob.data().orElse(null);
- String encoded = bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
- String mime = blob.mimeType().orElse(null);
- String name = blob.displayName().orElse(null);
- return Optional.of(new FilePart(new FileWithBytes(mime, name, encoded), new HashMap<>()));
+ if (part.fileData().isPresent() || part.inlineData().isPresent()) {
+ return filePartToA2A(part, metadata);
}
if (part.functionCall().isPresent()
|| part.functionResponse().isPresent()
|| part.executableCode().isPresent()
|| part.codeExecutionResult().isPresent()) {
- return convertGenaiPartToA2aPart(part).map(data -> data);
+ return dataPartToA2A(part, metadata);
}
- logger.warn("Unsupported GenAI part type for JSON export: {}", part);
- return Optional.empty();
+ throw new IllegalArgumentException("Unsupported GenAI part type: " + part);
+ }
+
+ private static DataPart dataPartToA2A(Part part, ImmutableMap.Builder metadata) {
+
+ if (part.functionCall().isPresent()) {
+ return createDataPartFromFunctionCall(part.functionCall().get(), metadata);
+ } else if (part.functionResponse().isPresent()) {
+ return createDataPartFromFunctionResponse(part.functionResponse().get(), metadata);
+ } else if (part.codeExecutionResult().isPresent()) {
+ return createDataPartFromCodeExecutionResult(part.codeExecutionResult().get(), metadata);
+ } else if (part.executableCode().isPresent()) {
+ return createDataPartFromExecutableCode(part.executableCode().get(), metadata);
+ }
+
+ throw new IllegalArgumentException("Unsupported GenAI data part type: " + part);
+ }
+
+ private static FilePart filePartToA2A(Part part, ImmutableMap.Builder metadata) {
+ if (part.fileData().isPresent()) {
+ FileData fileData = part.fileData().get();
+ String uri = fileData.fileUri().orElse(null);
+ String mime = fileData.mimeType().orElse(null);
+ String name = fileData.displayName().orElse(null);
+ return new FilePart(new FileWithUri(mime, name, uri), metadata.buildOrThrow());
+ }
+ Blob blob = part.inlineData().get();
+ byte[] bytes = blob.data().orElse(null);
+ String encoded = bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
+ addValueIfPresent(metadata, "video_metadata", part.videoMetadata());
+ return new FilePart(
+ new FileWithBytes(blob.mimeType().orElse(null), blob.displayName().orElse(null), encoded),
+ metadata.buildOrThrow());
}
@SuppressWarnings("unchecked") // safe conversion from objectMapper.readValue
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java
deleted file mode 100644
index 57f7aeffd..000000000
--- a/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java
+++ /dev/null
@@ -1,198 +0,0 @@
-package com.google.adk.a2a.converters;
-
-import com.google.adk.events.Event;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.genai.types.Content;
-import io.a2a.spec.DataPart;
-import io.a2a.spec.Message;
-import io.a2a.spec.Part;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * rfe Converter for A2A Messages to ADK Events. This is used on the A2A service side to convert
- * incoming A2A requests to ADK Events.
- *
- * **EXPERIMENTAL:** Subject to change, rename, or removal in any future patch release. Do not
- * use in production code.
- */
-public final class RequestConverter {
- private static final Logger logger = LoggerFactory.getLogger(RequestConverter.class);
-
- private RequestConverter() {}
-
- /**
- * Convert an A2A Message to an ADK Event. This is used when the A2A service receives a request
- * and needs to process it with ADK.
- *
- * @param message The A2A message to convert.
- * @param invocationId The invocation ID for the event.
- * @return Optional containing the converted ADK Event, or empty if conversion fails.
- */
- public static Optional convertA2aMessageToAdkEvent(Message message, String invocationId) {
- if (message == null) {
- // Create an empty user message event
- logger.info("Null message received, creating empty user event");
- Event event =
- Event.builder()
- .id(UUID.randomUUID().toString())
- .invocationId(invocationId != null ? invocationId : UUID.randomUUID().toString())
- .author("user")
- .content(
- Content.builder()
- .role("user")
- .parts(
- ImmutableList.of(com.google.genai.types.Part.builder().text("").build()))
- .build())
- .timestamp(Instant.now().toEpochMilli())
- .build();
- return Optional.of(event);
- }
-
- List genaiParts = new ArrayList<>();
-
- // Convert each A2A Part to GenAI Part
- if (message.getParts() != null) {
- for (Part> a2aPart : message.getParts()) {
- Optional genaiPart = PartConverter.toGenaiPart(a2aPart);
- genaiPart.ifPresent(genaiParts::add);
- }
- }
-
- if (genaiParts.isEmpty()) {
- logger.warn("No convertible parts found in A2A message");
- return Optional.empty();
- }
-
- // Treat inbound A2A requests as user input for the ADK agent.
- String author = "user";
-
- // Build the Content object
- Content content = Content.builder().role("user").parts(genaiParts).build();
-
- // Build the Event
- Event event =
- Event.builder()
- .id(
- !message.getMessageId().isEmpty()
- ? message.getMessageId()
- : UUID.randomUUID().toString())
- .invocationId(invocationId != null ? invocationId : UUID.randomUUID().toString())
- .author(author)
- .content(content)
- .timestamp(Instant.now().toEpochMilli())
- .build();
-
- return Optional.of(event);
- }
-
- /**
- * Convert an aggregated A2A Message to multiple ADK Events. This reconstructs the original event
- * sequence from an aggregated message.
- *
- * @param message The aggregated A2A message to convert.
- * @param invocationId The invocation ID for the events.
- * @return List of ADK Events representing the conversation history.
- */
- public static ImmutableList convertAggregatedA2aMessageToAdkEvents(
- Message message, String invocationId) {
- if (message == null || message.getParts() == null || message.getParts().isEmpty()) {
- logger.info("Null or empty message received, creating empty user event");
- Event event =
- Event.builder()
- .id(UUID.randomUUID().toString())
- .invocationId(invocationId != null ? invocationId : UUID.randomUUID().toString())
- .author("user")
- .content(
- Content.builder()
- .role("user")
- .parts(
- ImmutableList.of(com.google.genai.types.Part.builder().text("").build()))
- .build())
- .timestamp(Instant.now().toEpochMilli())
- .build();
- return ImmutableList.of(event);
- }
-
- List events = new ArrayList<>();
-
- // Emit exactly one ADK Event per A2A Part, preserving order.
- for (Part> a2aPart : message.getParts()) {
- Optional genaiPart = PartConverter.toGenaiPart(a2aPart);
- if (genaiPart.isEmpty()) {
- continue;
- }
-
- String author = extractAuthorFromMetadata(a2aPart);
- String role = determineRoleFromAuthor(author);
-
- events.add(createEvent(ImmutableList.of(genaiPart.get()), author, role, invocationId));
- }
-
- if (events.isEmpty()) {
- logger.warn("No events created from aggregated message; returning single empty user event");
- Event event =
- Event.builder()
- .id(UUID.randomUUID().toString())
- .invocationId(invocationId)
- .author("user")
- .content(
- Content.builder()
- .role("user")
- .parts(
- ImmutableList.of(com.google.genai.types.Part.builder().text("").build()))
- .build())
- .timestamp(Instant.now().toEpochMilli())
- .build();
- events.add(event);
- }
-
- logger.info("Converted aggregated A2A message to {} ADK events", events.size());
- return ImmutableList.copyOf(events);
- }
-
- private static String extractAuthorFromMetadata(Part> a2aPart) {
- if (a2aPart instanceof DataPart dataPart) {
- Map metadata =
- Optional.ofNullable(dataPart.getMetadata()).orElse(ImmutableMap.of());
- String type =
- metadata.getOrDefault(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, "").toString();
- if (type.equals(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)) {
- return "model";
- }
- if (type.equals(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)) {
- return "user";
- }
- Map data = Optional.ofNullable(dataPart.getData()).orElse(ImmutableMap.of());
- if (data.containsKey("args")) {
- return "model";
- }
- if (data.containsKey("response")) {
- return "user";
- }
- }
- return "user";
- }
-
- private static String determineRoleFromAuthor(String author) {
- return author.equals("model") ? "model" : "user";
- }
-
- private static Event createEvent(
- List parts, String author, String role, String invocationId) {
- return Event.builder()
- .id(UUID.randomUUID().toString())
- .invocationId(invocationId)
- .author(author)
- .content(Content.builder().role(role).parts(new ArrayList<>(parts)).build())
- .timestamp(Instant.now().toEpochMilli())
- .build();
- }
-}
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java
index 2e32b4c8c..ccbb1b9cf 100644
--- a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java
@@ -2,34 +2,28 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
-import com.google.adk.a2a.common.A2AClientError;
import com.google.adk.agents.InvocationContext;
import com.google.adk.events.Event;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.genai.types.Content;
+import com.google.genai.types.Part;
import io.a2a.client.ClientEvent;
import io.a2a.client.MessageEvent;
import io.a2a.client.TaskEvent;
import io.a2a.client.TaskUpdateEvent;
import io.a2a.spec.Artifact;
-import io.a2a.spec.EventKind;
-import io.a2a.spec.JSONRPCError;
import io.a2a.spec.Message;
-import io.a2a.spec.SendMessageResponse;
import io.a2a.spec.Task;
import io.a2a.spec.TaskArtifactUpdateEvent;
import io.a2a.spec.TaskState;
import io.a2a.spec.TaskStatusUpdateEvent;
-import io.a2a.spec.TextPart;
import java.time.Instant;
-import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
-import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,119 +40,6 @@ public final class ResponseConverter {
private ResponseConverter() {}
- /**
- * Converts a {@link SendMessageResponse} containing a {@link Message} result into ADK events.
- *
- * Non-message results are ignored in the message-only integration and logged for awareness.
- */
- public static List sendMessageResponseToEvents(
- SendMessageResponse response, String invocationId, String branch) {
- if (response == null) {
- logger.warn("SendMessageResponse was null; returning no events.");
- return ImmutableList.of();
- }
-
- EventKind result = response.getResult();
- if (result == null) {
-
- JSONRPCError error = response.getError();
- if (error != null) {
- throw new A2AClientError(
- String.format("SendMessageResponse error for invocation %s", invocationId), error);
- }
-
- throw new A2AClientError(
- String.format("SendMessageResponse result was null for invocation %s", invocationId));
- }
-
- if (result instanceof Message message) {
- return messageToEvents(message, invocationId, branch);
- }
-
- throw new IllegalArgumentException(
- String.format(
- "SendMessageResponse result was neither a Message nor an error for invocation %s",
- invocationId));
- }
-
- /** Converts an A2A message back to ADK events. */
- public static List messageToEvents(Message message, String invocationId, String branch) {
- List events = new ArrayList<>();
-
- for (io.a2a.spec.Part> part : message.getParts()) {
- PartConverter.toGenaiPart(part)
- .ifPresent(
- genaiPart ->
- events.add(
- Event.builder()
- .id(UUID.randomUUID().toString())
- .invocationId(invocationId)
- .author(message.getRole() == Message.Role.AGENT ? "agent" : "user")
- .branch(branch)
- .content(
- Content.builder()
- .role(message.getRole() == Message.Role.AGENT ? "model" : "user")
- .parts(ImmutableList.of(genaiPart))
- .build())
- .timestamp(Instant.now().toEpochMilli())
- .build()));
- }
- return events;
- }
-
- private static Message emptyAgentMessage(String contextId) {
- Message.Builder builder =
- new Message.Builder()
- .messageId(UUID.randomUUID().toString())
- .role(Message.Role.AGENT)
- .parts(ImmutableList.of(new TextPart("")));
- if (contextId != null) {
- builder.contextId(contextId);
- }
- return builder.build();
- }
-
- /** Converts a list of ADK events into a single aggregated A2A message. */
- public static Message eventsToMessage(List events, String contextId, String taskId) {
- if (events == null || events.isEmpty()) {
- return emptyAgentMessage(contextId);
- }
-
- if (events.size() == 1) {
- return eventToMessage(events.get(0), contextId);
- }
-
- List> parts = new ArrayList<>();
- for (Event event : events) {
- parts.addAll(eventParts(event));
- }
-
- Message.Builder builder =
- new Message.Builder()
- .messageId(taskId != null ? taskId : UUID.randomUUID().toString())
- .role(Message.Role.AGENT)
- .parts(parts);
- if (contextId != null) {
- builder.contextId(contextId);
- }
- return builder.build();
- }
-
- /** Converts a single ADK event into an A2A message. */
- public static Message eventToMessage(Event event, String contextId) {
- List> parts = eventParts(event);
-
- Message.Builder builder =
- new Message.Builder()
- .messageId(event.id() != null ? event.id() : UUID.randomUUID().toString())
- .role(event.author().equalsIgnoreCase("user") ? Message.Role.USER : Message.Role.AGENT)
- .parts(parts);
- if (contextId != null) {
- builder.contextId(contextId);
- }
- return builder.build();
- }
-
/**
* Converts a A2A {@link ClientEvent} to an ADK {@link Event}, based on the event type. Returns an
* empty optional if the event should be ignored (e.g. if the event is not a final update for
@@ -175,6 +56,7 @@ public static Optional clientEventToEvent(
} else if (event instanceof TaskUpdateEvent updateEvent) {
return handleTaskUpdate(updateEvent, invocationContext);
}
+ logger.warn("Unsupported ClientEvent type: {}", event.getClass());
throw new IllegalArgumentException("Unsupported ClientEvent type: " + event.getClass());
}
@@ -262,7 +144,7 @@ public static Event messageToFailedEvent(Message message, InvocationContext invo
public static Event messageToEvent(
Message message, InvocationContext invocationContext, boolean isPending) {
- ImmutableList genaiParts =
+ ImmutableList genaiParts =
PartConverter.toGenaiParts(message.getParts()).stream()
.map(part -> part.toBuilder().thought(isPending).build())
.collect(toImmutableList());
@@ -297,19 +179,6 @@ public static Event taskToEvent(Task task, InvocationContext invocationContext)
return emptyEvent(invocationContext);
}
- private static List> eventParts(Event event) {
- List> parts = new ArrayList<>();
- Optional content = event.content();
- if (content.isEmpty() || content.get().parts().isEmpty()) {
- return parts;
- }
-
- for (com.google.genai.types.Part genaiPart : content.get().parts().get()) {
- PartConverter.fromGenaiPart(genaiPart).ifPresent(parts::add);
- }
- return parts;
- }
-
private static Event emptyEvent(InvocationContext invocationContext) {
Event.Builder builder =
Event.builder()
@@ -322,7 +191,7 @@ private static Event emptyEvent(InvocationContext invocationContext) {
return builder.build();
}
- private static Content fromModelParts(List parts) {
+ private static Content fromModelParts(List parts) {
return Content.builder().role("model").parts(parts).build();
}
@@ -334,15 +203,4 @@ private static Event.Builder remoteAgentEventBuilder(InvocationContext invocatio
.branch(invocationContext.branch().orElse(null))
.timestamp(Instant.now().toEpochMilli());
}
-
- /** Simple REST-friendly wrapper to carry either a message result or a task result. */
- public record MessageSendResult(@Nullable Message message, @Nullable Task task) {
- public static MessageSendResult fromMessage(Message message) {
- return new MessageSendResult(message, null);
- }
-
- public static MessageSendResult fromTask(Task task) {
- return new MessageSendResult(null, task);
- }
- }
}
diff --git a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java
index 3d66a4e07..b7b4e9953 100644
--- a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java
+++ b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java
@@ -308,16 +308,8 @@ private Maybe process(
"Agent returned an error: " + event.errorCode().get(),
null));
}
- ImmutableList> parts = EventConverter.contentToParts(event.content());
- // Mark all parts as partial if the event is partial.
- if (event.partial().orElse(false)) {
- parts.forEach(
- part -> {
- Map metadata = part.getMetadata();
- metadata.put("adk_partial", true);
- });
- }
-
+ ImmutableList> parts =
+ EventConverter.contentToParts(event.content(), event.partial().orElse(false));
Map metadata = new HashMap<>();
if (event.customMetadata().isPresent()) {
for (CustomMetadata cm : event.customMetadata().get()) {
diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java
index f9d34bf3f..8d460c457 100644
--- a/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java
+++ b/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java
@@ -92,6 +92,8 @@ public void convertEventsToA2AMessage_preservesFunctionCallAndResponseParts() {
.invocationId("invocation-1")
.agent(new TestAgent())
.session(session)
+ .userContent(
+ Content.builder().role("user").parts(ImmutableList.of(userTextPart)).build())
.endInvocation(false)
.build();
@@ -101,24 +103,28 @@ public void convertEventsToA2AMessage_preservesFunctionCallAndResponseParts() {
// Assert
assertThat(maybeMessage).isPresent();
Message message = maybeMessage.get();
- assertThat(message.getParts()).hasSize(3);
+ assertThat(message.getParts()).hasSize(4);
assertThat(message.getParts().get(0)).isInstanceOf(TextPart.class);
assertThat(message.getParts().get(1)).isInstanceOf(DataPart.class);
assertThat(message.getParts().get(2)).isInstanceOf(DataPart.class);
+ assertThat(message.getParts().get(3)).isInstanceOf(TextPart.class);
DataPart callDataPart = (DataPart) message.getParts().get(1);
assertThat(callDataPart.getMetadata().get(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY))
- .isEqualTo(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL);
+ .isEqualTo(A2ADataPartMetadataType.FUNCTION_CALL.getType());
assertThat(callDataPart.getData()).containsEntry("name", "roll_die");
assertThat(callDataPart.getData()).containsEntry("id", "adk-call-1");
assertThat(callDataPart.getData()).containsEntry("args", ImmutableMap.of("sides", 6));
DataPart responseDataPart = (DataPart) message.getParts().get(2);
assertThat(responseDataPart.getMetadata().get(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY))
- .isEqualTo(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE);
+ .isEqualTo(A2ADataPartMetadataType.FUNCTION_RESPONSE.getType());
assertThat(responseDataPart.getData()).containsEntry("name", "roll_die");
assertThat(responseDataPart.getData()).containsEntry("id", "adk-call-1");
assertThat(responseDataPart.getData()).containsEntry("response", ImmutableMap.of("result", 3));
+
+ TextPart lastTextPart = (TextPart) message.getParts().get(3);
+ assertThat(lastTextPart.getText()).isEqualTo("Roll a die");
}
private static final class TestAgent extends BaseAgent {
diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java
index 6ccfd9566..8e8982ffa 100644
--- a/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java
+++ b/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java
@@ -2,7 +2,9 @@
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertThrows;
+import com.google.adk.a2a.common.GenAiFieldMissingException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.types.Blob;
@@ -89,7 +91,7 @@ public void toGenaiPart_withDataPartFunctionCall_returnsGenaiFunctionCallPart()
data,
ImmutableMap.of(
PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
- PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL));
+ A2ADataPartMetadataType.FUNCTION_CALL.getType()));
Optional result = PartConverter.toGenaiPart(dataPart);
@@ -126,7 +128,7 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons
data,
ImmutableMap.of(
PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
- PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE));
+ A2ADataPartMetadataType.FUNCTION_RESPONSE.getType()));
Optional result = PartConverter.toGenaiPart(dataPart);
@@ -181,78 +183,21 @@ public void toGenaiParts_convertsAllSupportedParts() {
}
@Test
- public void convertGenaiPartToA2aPart_withNullPart_returnsEmpty() {
- assertThat(PartConverter.convertGenaiPartToA2aPart(null)).isEmpty();
- }
-
- @Test
- public void convertGenaiPartToA2aPart_withTextPart_returnsEmpty() {
- Part part = Part.builder().text("text").build();
- assertThat(PartConverter.convertGenaiPartToA2aPart(part)).isEmpty();
- }
-
- @Test
- public void convertGenaiPartToA2aPart_withFunctionCallPart_returnsDataPart() {
- Part part =
- Part.builder()
- .functionCall(
- FunctionCall.builder()
- .name("func")
- .id("1")
- .args(ImmutableMap.of("param", "value"))
- .build())
- .build();
-
- Optional result = PartConverter.convertGenaiPartToA2aPart(part);
-
- assertThat(result).isPresent();
- DataPart dataPart = result.get();
- assertThat(dataPart.getData())
- .containsExactly("name", "func", "id", "1", "args", ImmutableMap.of("param", "value"));
- assertThat(dataPart.getMetadata())
- .containsEntry(
- PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
- PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL);
- }
-
- @Test
- public void convertGenaiPartToA2aPart_withFunctionResponsePart_returnsDataPart() {
- Part part =
- Part.builder()
- .functionResponse(
- FunctionResponse.builder()
- .name("func")
- .id("1")
- .response(ImmutableMap.of("result", "value"))
- .build())
- .build();
-
- Optional result = PartConverter.convertGenaiPartToA2aPart(part);
-
- assertThat(result).isPresent();
- DataPart dataPart = result.get();
- assertThat(dataPart.getData())
- .containsExactly("name", "func", "id", "1", "response", ImmutableMap.of("result", "value"));
- assertThat(dataPart.getMetadata())
- .containsEntry(
- PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
- PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE);
- }
-
- @Test
- public void fromGenaiPart_withNullPart_returnsEmpty() {
- assertThat(PartConverter.fromGenaiPart(null)).isEmpty();
+ public void fromGenaiPart_withNullPart_throwsException() {
+ assertThrows(GenAiFieldMissingException.class, () -> PartConverter.fromGenaiPart(null, false));
}
@Test
public void fromGenaiPart_withTextPart_returnsTextPart() {
- Part part = Part.builder().text("text").build();
+ Part part = Part.builder().text("text").thought(true).build();
- Optional> result = PartConverter.fromGenaiPart(part);
+ io.a2a.spec.Part> result = PartConverter.fromGenaiPart(part, true);
- assertThat(result).isPresent();
- assertThat(result.get()).isInstanceOf(TextPart.class);
- assertThat(((TextPart) result.get()).getText()).isEqualTo("text");
+ assertThat(result).isInstanceOf(TextPart.class);
+ assertThat(((TextPart) result).getText()).isEqualTo("text");
+ assertThat(((TextPart) result).getMetadata()).containsEntry("thought", true);
+ assertThat(((TextPart) result).getMetadata())
+ .containsEntry(PartConverter.A2A_DATA_PART_METADATA_IS_PARTIAL_KEY, true);
}
@Test
@@ -262,11 +207,10 @@ public void fromGenaiPart_withFileDataPart_returnsFilePartWithUri() {
.fileData(FileData.builder().mimeType("text/plain").fileUri("http://file.txt").build())
.build();
- Optional> result = PartConverter.fromGenaiPart(part);
+ io.a2a.spec.Part> result = PartConverter.fromGenaiPart(part, false);
- assertThat(result).isPresent();
- assertThat(result.get()).isInstanceOf(FilePart.class);
- FilePart filePart = (FilePart) result.get();
+ assertThat(result).isInstanceOf(FilePart.class);
+ FilePart filePart = (FilePart) result;
assertThat(filePart.getFile()).isInstanceOf(FileWithUri.class);
FileWithUri fileWithUri = (FileWithUri) filePart.getFile();
assertThat(fileWithUri.mimeType()).isEqualTo("text/plain");
@@ -281,11 +225,10 @@ public void fromGenaiPart_withInlineDataPart_returnsFilePartWithBytes() {
.inlineData(Blob.builder().mimeType("text/plain").data(bytes).build())
.build();
- Optional> result = PartConverter.fromGenaiPart(part);
+ io.a2a.spec.Part> result = PartConverter.fromGenaiPart(part, false);
- assertThat(result).isPresent();
- assertThat(result.get()).isInstanceOf(FilePart.class);
- FilePart filePart = (FilePart) result.get();
+ assertThat(result).isInstanceOf(FilePart.class);
+ FilePart filePart = (FilePart) result;
assertThat(filePart.getFile()).isInstanceOf(FileWithBytes.class);
FileWithBytes fileWithBytes = (FileWithBytes) filePart.getFile();
assertThat(fileWithBytes.mimeType()).isEqualTo("text/plain");
@@ -297,20 +240,32 @@ public void fromGenaiPart_withFunctionCallPart_returnsDataPart() {
Part part =
Part.builder()
.functionCall(
- FunctionCall.builder().name("func").id("1").args(ImmutableMap.of()).build())
+ FunctionCall.builder()
+ .name("func")
+ .id("1")
+ .willContinue(true)
+ .args(ImmutableMap.of())
+ .build())
.build();
- Optional> result = PartConverter.fromGenaiPart(part);
+ io.a2a.spec.Part> result = PartConverter.fromGenaiPart(part, false);
- assertThat(result).isPresent();
- assertThat(result.get()).isInstanceOf(DataPart.class);
- DataPart dataPart = (DataPart) result.get();
+ assertThat(result).isInstanceOf(DataPart.class);
+ DataPart dataPart = (DataPart) result;
assertThat(dataPart.getData())
- .containsExactly("name", "func", "id", "1", "args", ImmutableMap.of());
+ .containsExactly(
+ "name",
+ "func",
+ "id",
+ "1",
+ "args",
+ ImmutableMap.of(),
+ PartConverter.WILL_CONTINUE_KEY,
+ true);
assertThat(dataPart.getMetadata())
.containsEntry(
PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
- PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL);
+ A2ADataPartMetadataType.FUNCTION_CALL.getType());
}
@Test
@@ -321,17 +276,16 @@ public void fromGenaiPart_withFunctionResponsePart_returnsDataPart() {
FunctionResponse.builder().name("func").id("1").response(ImmutableMap.of()).build())
.build();
- Optional> result = PartConverter.fromGenaiPart(part);
+ io.a2a.spec.Part> result = PartConverter.fromGenaiPart(part, false);
- assertThat(result).isPresent();
- assertThat(result.get()).isInstanceOf(DataPart.class);
- DataPart dataPart = (DataPart) result.get();
+ assertThat(result).isInstanceOf(DataPart.class);
+ DataPart dataPart = (DataPart) result;
assertThat(dataPart.getData())
.containsExactly("name", "func", "id", "1", "response", ImmutableMap.of());
assertThat(dataPart.getMetadata())
.containsEntry(
PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY,
- PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE);
+ A2ADataPartMetadataType.FUNCTION_RESPONSE.getType());
}
@Test
diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java
index 8dc70ca2a..5378bdd7b 100644
--- a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java
+++ b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java
@@ -12,7 +12,6 @@
import com.google.adk.sessions.Session;
import com.google.common.collect.ImmutableList;
import com.google.genai.types.Content;
-import com.google.genai.types.Part;
import io.a2a.client.MessageEvent;
import io.a2a.client.TaskUpdateEvent;
import io.a2a.spec.Artifact;
@@ -25,7 +24,6 @@
import io.a2a.spec.TextPart;
import io.reactivex.rxjava3.core.Flowable;
import java.util.Optional;
-import java.util.UUID;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -66,124 +64,6 @@ private static TaskStatusUpdateEvent.Builder testTaskStatusUpdateEvent() {
return new TaskStatusUpdateEvent.Builder().taskId("task-1").contextId("context-1");
}
- @Test
- public void eventsToMessage_withNullEvents_returnsEmptyAgentMessage() {
- Message message = ResponseConverter.eventsToMessage(null, "context-1", "task-1");
- assertThat(message.getContextId()).isEqualTo("context-1");
- assertThat(message.getRole()).isEqualTo(Message.Role.AGENT);
- assertThat(message.getParts()).hasSize(1);
- assertThat(((TextPart) message.getParts().get(0)).getText()).isEmpty();
- }
-
- @Test
- public void eventsToMessage_withEmptyEvents_returnsEmptyAgentMessage() {
- Message message = ResponseConverter.eventsToMessage(ImmutableList.of(), "context-1", "task-1");
- assertThat(message.getContextId()).isEqualTo("context-1");
- assertThat(message.getRole()).isEqualTo(Message.Role.AGENT);
- assertThat(message.getParts()).hasSize(1);
- assertThat(((TextPart) message.getParts().get(0)).getText()).isEmpty();
- }
-
- @Test
- public void eventsToMessage_withSingleEvent_returnsMessage() {
- Event event =
- Event.builder()
- .id(UUID.randomUUID().toString())
- .author("user")
- .content(
- Content.builder()
- .role("user")
- .parts(ImmutableList.of(Part.builder().text("Hello").build()))
- .build())
- .build();
-
- Message message =
- ResponseConverter.eventsToMessage(ImmutableList.of(event), "context-1", "task-1");
-
- assertThat(message.getContextId()).isEqualTo("context-1");
- assertThat(message.getRole()).isEqualTo(Message.Role.USER);
- assertThat(message.getParts()).hasSize(1);
- assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Hello");
- }
-
- @Test
- public void eventsToMessage_withMultipleEvents_returnsAggregatedMessage() {
- Event event1 =
- Event.builder()
- .id(UUID.randomUUID().toString())
- .author("agent")
- .content(
- Content.builder()
- .role("model")
- .parts(ImmutableList.of(Part.builder().text("Hello ").build()))
- .build())
- .build();
- Event event2 =
- Event.builder()
- .id(UUID.randomUUID().toString())
- .author("agent")
- .content(
- Content.builder()
- .role("model")
- .parts(ImmutableList.of(Part.builder().text("World").build()))
- .build())
- .build();
-
- Message message =
- ResponseConverter.eventsToMessage(ImmutableList.of(event1, event2), "context-1", "task-1");
-
- assertThat(message.getMessageId()).isEqualTo("task-1");
- assertThat(message.getContextId()).isEqualTo("context-1");
- assertThat(message.getRole()).isEqualTo(Message.Role.AGENT);
- assertThat(message.getParts()).hasSize(2);
- assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Hello ");
- assertThat(((TextPart) message.getParts().get(1)).getText()).isEqualTo("World");
- }
-
- @Test
- public void eventToMessage_convertsUserEvent() {
- Event event =
- Event.builder()
- .id("event-1")
- .author("user")
- .content(
- Content.builder()
- .role("user")
- .parts(ImmutableList.of(Part.builder().text("Test").build()))
- .build())
- .build();
-
- Message message = ResponseConverter.eventToMessage(event, "context-1");
-
- assertThat(message.getMessageId()).isEqualTo("event-1");
- assertThat(message.getContextId()).isEqualTo("context-1");
- assertThat(message.getRole()).isEqualTo(Message.Role.USER);
- assertThat(message.getParts()).hasSize(1);
- assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Test");
- }
-
- @Test
- public void eventToMessage_convertsAgentEvent() {
- Event event =
- Event.builder()
- .id("event-1")
- .author("agent")
- .content(
- Content.builder()
- .role("model")
- .parts(ImmutableList.of(Part.builder().text("Test").build()))
- .build())
- .build();
-
- Message message = ResponseConverter.eventToMessage(event, "context-1");
-
- assertThat(message.getMessageId()).isEqualTo("event-1");
- assertThat(message.getContextId()).isEqualTo("context-1");
- assertThat(message.getRole()).isEqualTo(Message.Role.AGENT);
- assertThat(message.getParts()).hasSize(1);
- assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Test");
- }
-
@Test
public void clientEventToEvent_withMessageEvent_returnsEvent() {
Message a2aMessage =