1616package com .google .adk .a2a .converters ;
1717
1818import static com .google .common .collect .ImmutableList .toImmutableList ;
19- import static com .google .common .collect .ImmutableSet .toImmutableSet ;
20- import static com .google .common .collect .Streams .zip ;
2119
2220import com .fasterxml .jackson .core .JsonProcessingException ;
2321import com .fasterxml .jackson .databind .JavaType ;
3129import com .google .genai .types .Content ;
3230import com .google .genai .types .CustomMetadata ;
3331import com .google .genai .types .FinishReason ;
32+ import com .google .genai .types .FunctionCall ;
3433import com .google .genai .types .GenerateContentResponseUsageMetadata ;
3534import com .google .genai .types .GroundingMetadata ;
3635import com .google .genai .types .Part ;
5554import org .slf4j .Logger ;
5655import org .slf4j .LoggerFactory ;
5756
58- /** Utility for converting ADK events to A2A spec messages (and back). */
57+ /**
58+ * Utility for converting ADK events to A2A spec messages (and back).
59+ *
60+ * <p>Unparseable ADK metadata and unconvertible parts from a remote agent are logged and dropped;
61+ * the rest of the event is still converted.
62+ */
5963public final class ResponseConverter {
6064 private static final ObjectMapper objectMapper = new ObjectMapper ();
6165 private static final Logger logger = LoggerFactory .getLogger (ResponseConverter .class );
@@ -69,16 +73,16 @@ private ResponseConverter() {}
6973 /**
7074 * Converts a A2A {@link ClientEvent} to an ADK {@link Event}, based on the event type. Returns an
7175 * empty optional if the event should be ignored (e.g. if the event is not a final update for
72- * TaskArtifactUpdateEvent or if the message is empty for TaskStatusUpdateEvent).
73- *
74- * <p>Unparseable ADK metadata is logged and dropped; the rest of the event is still converted.
76+ * TaskArtifactUpdateEvent, if the message is empty for TaskStatusUpdateEvent, or if no part could
77+ * be converted from a message or from the message of a status update that does not end the turn).
7578 *
7679 * @throws IllegalArgumentException if the event type is not supported.
7780 */
7881 public static Optional <Event > clientEventToEvent (
7982 ClientEvent event , InvocationContext invocationContext ) {
8083 if (event instanceof MessageEvent messageEvent ) {
81- return Optional .of (messageToEvent (messageEvent .getMessage (), invocationContext ));
84+ return Optional .of (messageToEvent (messageEvent .getMessage (), invocationContext ))
85+ .filter (ResponseConverter ::hasParts );
8286 } else if (event instanceof TaskEvent taskEvent ) {
8387 return Optional .of (taskToEvent (taskEvent .getTask (), invocationContext ));
8488 } else if (event instanceof TaskUpdateEvent updateEvent ) {
@@ -101,9 +105,10 @@ private static boolean isLongRunning(@Nullable Map<String, Object> metadata) {
101105 }
102106
103107 /**
104- * Converts a A2A {@link TaskUpdateEvent} to an ADK {@link Event}, if applicable. Returns null if
105- * the event is not a final update for TaskArtifactUpdateEvent or if the message is empty for
106- * TaskStatusUpdateEvent.
108+ * Converts a A2A {@link TaskUpdateEvent} to an ADK {@link Event}, if applicable. Returns empty if
109+ * the event is not a final update for TaskArtifactUpdateEvent, if the message is empty for
110+ * TaskStatusUpdateEvent, or if no part could be converted from the message of a status update
111+ * that does not end the turn.
107112 *
108113 * @throws IllegalArgumentException if the task update type is not supported.
109114 */
@@ -120,7 +125,7 @@ private static Optional<Event> handleTaskUpdate(
120125 }
121126
122127 Event eventPart = artifactToEvent (artifactEvent .getArtifact (), context );
123- if (eventPart . content (). flatMap ( Content :: parts ). orElse ( ImmutableList . of ()). isEmpty ( )) {
128+ if (! hasParts ( eventPart )) {
124129 return Optional .empty ();
125130 }
126131 eventPart .setPartial (isAppend || !isLastChunk );
@@ -148,7 +153,8 @@ private static Optional<Event> handleTaskUpdate(
148153 return messageToFailedEvent (value , context );
149154 }
150155 return messageToEvent (value , context , PENDING_STATES .contains (taskState ));
151- });
156+ })
157+ .filter (converted -> taskState == TaskState .FAILED || hasParts (converted ));
152158
153159 if (statusEvent .isFinal ()
154160 || taskState == TaskState .INPUT_REQUIRED
@@ -176,10 +182,10 @@ private static Optional<Event> handleTaskUpdate(
176182 /** Converts an artifact to an ADK event. */
177183 public static Event artifactToEvent (Artifact artifact , InvocationContext invocationContext ) {
178184 Event .Builder eventBuilder = remoteAgentEventBuilder (invocationContext );
179- ImmutableList < Part > genaiParts = PartConverter . toGenaiParts (artifact .parts ());
185+ ConvertedParts converted = convertParts (artifact .parts ());
180186 eventBuilder
181- .content (fromModelParts (genaiParts ))
182- .longRunningToolIds (getLongRunningToolIds ( artifact . parts (), genaiParts ));
187+ .content (fromModelParts (converted . parts () ))
188+ .longRunningToolIds (converted . longRunningToolIds ( ));
183189 return eventBuilder .build ();
184190 }
185191
@@ -193,15 +199,11 @@ public static Event messageToFailedEvent(Message message, InvocationContext invo
193199 return builder .build ();
194200 }
195201
196- /**
197- * Converts an A2A message back to ADK events.
198- *
199- * <p>Unparseable ADK metadata is logged and dropped; the rest of the event is still converted.
200- */
202+ /** Converts an A2A message back to ADK events. */
201203 public static Event messageToEvent (Message message , InvocationContext invocationContext ) {
202204 return updateEventMetadata (
203205 remoteAgentEventBuilder (invocationContext )
204- .content (fromModelParts (PartConverter . toGenaiParts (message .getParts ())))
206+ .content (fromModelParts (convertParts (message .getParts ()). parts ( )))
205207 .build (),
206208 message .getMetadata (),
207209 message .getTaskId (),
@@ -216,7 +218,7 @@ public static Event messageToEvent(
216218 Message message , InvocationContext invocationContext , boolean isPending ) {
217219
218220 ImmutableList <Part > genaiParts =
219- PartConverter . toGenaiParts (message .getParts ()).stream ()
221+ convertParts (message .getParts ()). parts ( ).stream ()
220222 .map (part -> part .toBuilder ().thought (isPending ).build ())
221223 .collect (toImmutableList ());
222224
@@ -227,26 +229,23 @@ public static Event messageToEvent(
227229 * Converts an A2A {@link Task} to an ADK {@link Event}. If the artifacts are present, the last
228230 * artifact is used. If not, the status message is used. If not, the last history message is used.
229231 * If none of these are present, an empty event is returned.
230- *
231- * <p>Unparseable ADK metadata is logged and dropped; the rest of the event is still converted.
232232 */
233233 public static Event taskToEvent (Task task , InvocationContext invocationContext ) {
234234 ImmutableList .Builder <Part > genaiParts = ImmutableList .builder ();
235235 ImmutableSet .Builder <String > longRunningToolIds = ImmutableSet .builder ();
236236
237237 for (Artifact artifact : task .getArtifacts ()) {
238- ImmutableList < Part > converted = PartConverter . toGenaiParts (artifact .parts ());
239- longRunningToolIds .addAll (getLongRunningToolIds ( artifact . parts (), converted ));
240- genaiParts .addAll (converted );
238+ ConvertedParts converted = convertParts (artifact .parts ());
239+ longRunningToolIds .addAll (converted . longRunningToolIds ( ));
240+ genaiParts .addAll (converted . parts () );
241241 }
242242
243243 Event .Builder eventBuilder = remoteAgentEventBuilder (invocationContext );
244244
245245 if (task .getStatus ().message () != null ) {
246- ImmutableList <Part > msgParts =
247- PartConverter .toGenaiParts (task .getStatus ().message ().getParts ());
248- longRunningToolIds .addAll (
249- getLongRunningToolIds (task .getStatus ().message ().getParts (), msgParts ));
246+ ConvertedParts convertedMessage = convertParts (task .getStatus ().message ().getParts ());
247+ ImmutableList <Part > msgParts = convertedMessage .parts ();
248+ longRunningToolIds .addAll (convertedMessage .longRunningToolIds ());
250249 if (task .getStatus ().state () == TaskState .FAILED
251250 && msgParts .size () == 1
252251 && msgParts .get (0 ).text ().isPresent ()) {
@@ -277,26 +276,36 @@ public static Event taskToEvent(Task task, InvocationContext invocationContext)
277276 eventBuilder .build (), task .getMetadata (), task .getId (), task .getContextId ());
278277 }
279278
280- private static ImmutableSet <String > getLongRunningToolIds (
281- List <io .a2a .spec .Part <?>> parts , List <Part > convertedParts ) {
282- return zip (
283- parts .stream (),
284- convertedParts .stream (),
285- (part , convertedPart ) -> {
286- if (!(part instanceof DataPart dataPart )) {
287- return Optional .<String >empty ();
288- }
289- // A2A peers may omit metadata entirely, which deserializes to null.
290- if (!isLongRunning (dataPart .getMetadata ())) {
291- return Optional .<String >empty ();
292- }
293- if (convertedPart .functionCall ().isEmpty ()) {
294- return Optional .<String >empty ();
295- }
296- return convertedPart .functionCall ().get ().id ();
297- })
298- .flatMap (Optional ::stream )
299- .collect (toImmutableSet ());
279+ /** GenAI parts converted from A2A parts, and the ids of the long-running calls among them. */
280+ private record ConvertedParts (
281+ ImmutableList <Part > parts , ImmutableSet <String > longRunningToolIds ) {}
282+
283+ /**
284+ * Converts peer-supplied parts, skipping any part that cannot be converted so that one malformed
285+ * part does not fail the whole event.
286+ */
287+ private static ConvertedParts convertParts (List <io .a2a .spec .Part <?>> a2aParts ) {
288+ ImmutableList .Builder <Part > parts = ImmutableList .builder ();
289+ ImmutableSet .Builder <String > longRunningToolIds = ImmutableSet .builder ();
290+ for (io .a2a .spec .Part <?> a2aPart : a2aParts ) {
291+ Part part ;
292+ try {
293+ part = PartConverter .toGenaiPart (a2aPart );
294+ } catch (RuntimeException e ) {
295+ // The exception message can quote the peer's bytes, so log only the types.
296+ logger .warn (
297+ "Skipping A2A part that could not be converted: {} ({})" ,
298+ a2aPart == null ? "null" : a2aPart .getClass ().getSimpleName (),
299+ e .getClass ().getSimpleName ());
300+ continue ;
301+ }
302+ parts .add (part );
303+ // A2A peers may omit metadata entirely, which deserializes to null.
304+ if (a2aPart instanceof DataPart dataPart && isLongRunning (dataPart .getMetadata ())) {
305+ part .functionCall ().flatMap (FunctionCall ::id ).ifPresent (longRunningToolIds ::add );
306+ }
307+ }
308+ return new ConvertedParts (parts .build (), longRunningToolIds .build ());
300309 }
301310
302311 private static Event updateEventMetadata (
@@ -406,6 +415,10 @@ private static Content fromModelParts(List<Part> parts) {
406415 return Content .builder ().role ("model" ).parts (parts ).build ();
407416 }
408417
418+ private static boolean hasParts (Event event ) {
419+ return !event .content ().flatMap (Content ::parts ).orElse (ImmutableList .of ()).isEmpty ();
420+ }
421+
409422 private static Event .Builder remoteAgentEventBuilder (InvocationContext invocationContext ) {
410423 return Event .builder ()
411424 .id (UUID .randomUUID ().toString ())
0 commit comments