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 ;
1921
2022import com .google .adk .agents .InvocationContext ;
2123import com .google .adk .events .Event ;
2931import io .a2a .client .TaskEvent ;
3032import io .a2a .client .TaskUpdateEvent ;
3133import io .a2a .spec .Artifact ;
34+ import io .a2a .spec .DataPart ;
3235import io .a2a .spec .Message ;
3336import io .a2a .spec .Task ;
3437import io .a2a .spec .TaskArtifactUpdateEvent ;
3538import io .a2a .spec .TaskState ;
3639import io .a2a .spec .TaskStatusUpdateEvent ;
3740import java .time .Instant ;
3841import java .util .List ;
42+ import java .util .Map ;
3943import java .util .Objects ;
4044import java .util .Optional ;
4145import java .util .UUID ;
@@ -70,6 +74,14 @@ public static Optional<Event> clientEventToEvent(
7074 throw new IllegalArgumentException ("Unsupported ClientEvent type: " + event .getClass ());
7175 }
7276
77+ private static boolean isPartial (Map <String , Object > metadata ) {
78+ if (metadata == null ) {
79+ return false ;
80+ }
81+ return Objects .equals (
82+ metadata .getOrDefault (PartConverter .A2A_DATA_PART_METADATA_IS_PARTIAL_KEY , false ), true );
83+ }
84+
7385 /**
7486 * Converts a A2A {@link TaskUpdateEvent} to an ADK {@link Event}, if applicable. Returns null if
7587 * the event is not a final update for TaskArtifactUpdateEvent or if the message is empty for
@@ -85,7 +97,14 @@ private static Optional<Event> handleTaskUpdate(
8597 boolean isAppend = Objects .equals (artifactEvent .isAppend (), true );
8698 boolean isLastChunk = Objects .equals (artifactEvent .isLastChunk (), true );
8799
100+ if (isLastChunk && isPartial (artifactEvent .getMetadata ())) {
101+ return Optional .empty ();
102+ }
103+
88104 Event eventPart = artifactToEvent (artifactEvent .getArtifact (), context );
105+ if (eventPart .content ().flatMap (Content ::parts ).orElse (ImmutableList .of ()).isEmpty ()) {
106+ return Optional .empty ();
107+ }
89108 eventPart .setPartial (isAppend || !isLastChunk );
90109 // append=true, lastChunk=false: emit as partial, update aggregation
91110 // append=false, lastChunk=false: emit as partial, reset aggregation
@@ -115,26 +134,21 @@ private static Optional<Event> handleTaskUpdate(
115134 .map (builder -> builder .turnComplete (true ))
116135 .map (builder -> builder .partial (false ))
117136 .map (Event .Builder ::build );
118- } else {
119- return messageEvent ;
120137 }
138+ return messageEvent ;
121139 }
122140 throw new IllegalArgumentException (
123141 "Unsupported TaskUpdateEvent type: " + updateEvent .getClass ());
124142 }
125143
126144 /** Converts an artifact to an ADK event. */
127145 public static Event artifactToEvent (Artifact artifact , InvocationContext invocationContext ) {
128- Message message =
129- new Message .Builder ().role (Message .Role .AGENT ).parts (artifact .parts ()).build ();
130- return messageToEvent (message , invocationContext );
131- }
132-
133- /** Converts an A2A message back to ADK events. */
134- public static Event messageToEvent (Message message , InvocationContext invocationContext ) {
135- return remoteAgentEventBuilder (invocationContext )
136- .content (fromModelParts (PartConverter .toGenaiParts (message .getParts ())))
137- .build ();
146+ Event .Builder eventBuilder = remoteAgentEventBuilder (invocationContext );
147+ ImmutableList <Part > genaiParts = PartConverter .toGenaiParts (artifact .parts ());
148+ eventBuilder
149+ .content (fromModelParts (genaiParts ))
150+ .longRunningToolIds (getLongRunningToolIds (artifact .parts (), genaiParts ));
151+ return eventBuilder .build ();
138152 }
139153
140154 /** Converts an A2A message for a failed task to ADK event filling in the error message. */
@@ -147,6 +161,13 @@ public static Event messageToFailedEvent(Message message, InvocationContext invo
147161 return builder .build ();
148162 }
149163
164+ /** Converts an A2A message back to ADK events. */
165+ public static Event messageToEvent (Message message , InvocationContext invocationContext ) {
166+ return remoteAgentEventBuilder (invocationContext )
167+ .content (fromModelParts (PartConverter .toGenaiParts (message .getParts ())))
168+ .build ();
169+ }
170+
150171 /**
151172 * Converts an A2A message back to ADK events. For streaming task in pending state it sets the
152173 * thought field to true, to mark them as thought updates.
@@ -168,25 +189,71 @@ public static Event messageToEvent(
168189 * If none of these are present, an empty event is returned.
169190 */
170191 public static Event taskToEvent (Task task , InvocationContext invocationContext ) {
171- Message taskMessage = null ;
172-
173- if (!task .getArtifacts ().isEmpty ()) {
174- taskMessage =
175- new Message .Builder ()
176- .messageId ("" )
177- .role (Message .Role .AGENT )
178- .parts (Iterables .getLast (task .getArtifacts ()).parts ())
179- .build ();
180- } else if (task .getStatus ().message () != null ) {
181- taskMessage = task .getStatus ().message ();
182- } else if (!task .getHistory ().isEmpty ()) {
183- taskMessage = Iterables .getLast (task .getHistory ());
192+ ImmutableList .Builder <Part > genaiParts = ImmutableList .builder ();
193+ ImmutableSet .Builder <String > longRunningToolIds = ImmutableSet .builder ();
194+
195+ for (Artifact artifact : task .getArtifacts ()) {
196+ ImmutableList <Part > converted = PartConverter .toGenaiParts (artifact .parts ());
197+ longRunningToolIds .addAll (getLongRunningToolIds (artifact .parts (), converted ));
198+ genaiParts .addAll (converted );
199+ }
200+
201+ Event .Builder eventBuilder = remoteAgentEventBuilder (invocationContext );
202+
203+ if (task .getStatus ().message () != null ) {
204+ ImmutableList <Part > msgParts =
205+ PartConverter .toGenaiParts (task .getStatus ().message ().getParts ());
206+ longRunningToolIds .addAll (
207+ getLongRunningToolIds (task .getStatus ().message ().getParts (), msgParts ));
208+ if (task .getStatus ().state () == TaskState .FAILED
209+ && msgParts .size () == 1
210+ && msgParts .get (0 ).text ().isPresent ()) {
211+ eventBuilder .errorMessage (msgParts .get (0 ).text ().get ());
212+ } else {
213+ genaiParts .addAll (msgParts );
214+ }
184215 }
185216
186- if (taskMessage != null ) {
187- return messageToEvent (taskMessage , invocationContext );
217+ ImmutableList <Part > finalParts = genaiParts .build ();
218+ boolean isFinal =
219+ task .getStatus ().state ().isFinal () || task .getStatus ().state () == TaskState .INPUT_REQUIRED ;
220+
221+ if (finalParts .isEmpty () && !isFinal ) {
222+ return emptyEvent (invocationContext );
188223 }
189- return emptyEvent (invocationContext );
224+ if (!finalParts .isEmpty ()) {
225+ eventBuilder .content (fromModelParts (finalParts ));
226+ }
227+ if (task .getStatus ().state () == TaskState .INPUT_REQUIRED ) {
228+ eventBuilder .longRunningToolIds (longRunningToolIds .build ());
229+ }
230+ eventBuilder .turnComplete (isFinal );
231+ return eventBuilder .build ();
232+ }
233+
234+ private static ImmutableSet <String > getLongRunningToolIds (
235+ List <io .a2a .spec .Part <?>> parts , List <Part > convertedParts ) {
236+ return zip (
237+ parts .stream (),
238+ convertedParts .stream (),
239+ (part , convertedPart ) -> {
240+ if (!(part instanceof DataPart dataPart )) {
241+ return Optional .<String >empty ();
242+ }
243+ Object isLongRunning =
244+ dataPart
245+ .getMetadata ()
246+ .get (PartConverter .A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY );
247+ if (!Objects .equals (isLongRunning , true )) {
248+ return Optional .<String >empty ();
249+ }
250+ if (convertedPart .functionCall ().isEmpty ()) {
251+ return Optional .<String >empty ();
252+ }
253+ return convertedPart .functionCall ().get ().id ();
254+ })
255+ .flatMap (Optional ::stream )
256+ .collect (toImmutableSet ());
190257 }
191258
192259 private static Event emptyEvent (InvocationContext invocationContext ) {
0 commit comments