Skip to content

Commit 484aee4

Browse files
giulio-leoneCopilot
andcommitted
fix: restore event metadata in A2A inbound converters
The inbound converters (convert_a2a_message_to_event, convert_a2a_task_to_event, convert_a2a_status_update_to_event, convert_a2a_artifact_update_to_event) only restored 'actions' from A2A metadata. Other event metadata fields (custom_metadata, usage_metadata, error_code) serialized by the outbound converter were silently dropped. Additionally, _create_event() returned None for metadata-only messages (no parts, no actions) even when valid event metadata was present. Changes: - Add _extract_event_metadata() to restore custom_metadata, error_code, and usage_metadata from A2A metadata. - Update _create_event() to accept and set these fields, and preserve events that carry only metadata (no parts/actions). - Update all four converter functions to extract and forward metadata. - Add 9 regression tests covering metadata restoration across all converter functions and the metadata-only event case. Fixes #5185 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0fedb3b commit 484aee4

File tree

2 files changed

+333
-3
lines changed

2 files changed

+333
-3
lines changed

src/google/adk/a2a/converters/to_adk_event.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,23 @@ def _create_event(
177177
actions: Optional[EventActions] = None,
178178
long_running_function_ids: Optional[set[str]] = None,
179179
partial: bool = False,
180+
custom_metadata: Optional[dict[str, Any]] = None,
181+
error_code: Optional[str] = None,
182+
usage_metadata: Optional[
183+
genai_types.GenerateContentResponseUsageMetadata
184+
] = None,
180185
) -> Optional[Event]:
181186
"""Creates an ADK event from parts and metadata."""
182187
event_actions = actions or EventActions()
183-
if not output_parts and not event_actions.model_dump(
184-
exclude_none=True, exclude_defaults=True
185-
):
188+
has_actions = bool(
189+
event_actions.model_dump(exclude_none=True, exclude_defaults=True)
190+
)
191+
has_event_metadata = (
192+
custom_metadata is not None
193+
or error_code is not None
194+
or usage_metadata is not None
195+
)
196+
if not output_parts and not has_actions and not has_event_metadata:
186197
return None
187198

188199
event = Event(
@@ -206,6 +217,9 @@ def _create_event(
206217
else None
207218
),
208219
partial=partial,
220+
custom_metadata=custom_metadata,
221+
error_code=error_code,
222+
usage_metadata=usage_metadata,
209223
)
210224

211225
return event
@@ -248,6 +262,59 @@ def _extract_event_actions(
248262
return EventActions()
249263

250264

265+
def _extract_event_metadata(
266+
metadata: Optional[dict[str, Any]],
267+
) -> dict[str, Any]:
268+
"""Extracts ADK event metadata fields from A2A metadata.
269+
270+
Restores custom_metadata, error_code, and usage_metadata that were
271+
serialized by the outbound converter.
272+
273+
Args:
274+
metadata: The A2A metadata dictionary.
275+
276+
Returns:
277+
A dict of keyword arguments suitable for passing to _create_event().
278+
"""
279+
if not metadata:
280+
return {}
281+
282+
result: dict[str, Any] = {}
283+
284+
raw_custom = metadata.get(_get_adk_metadata_key("custom_metadata"))
285+
if raw_custom is not None:
286+
parsed = _parse_adk_metadata_value(raw_custom)
287+
if isinstance(parsed, dict):
288+
result["custom_metadata"] = parsed
289+
else:
290+
logger.warning(
291+
"Ignoring invalid ADK custom_metadata of type %s",
292+
type(parsed).__name__,
293+
)
294+
295+
raw_error_code = metadata.get(_get_adk_metadata_key("error_code"))
296+
if raw_error_code is not None:
297+
result["error_code"] = str(raw_error_code)
298+
299+
raw_usage = metadata.get(_get_adk_metadata_key("usage_metadata"))
300+
if raw_usage is not None:
301+
parsed = _parse_adk_metadata_value(raw_usage)
302+
if isinstance(parsed, dict):
303+
try:
304+
result["usage_metadata"] = (
305+
genai_types.GenerateContentResponseUsageMetadata(**parsed)
306+
)
307+
except Exception as e:
308+
logger.warning("Ignoring invalid ADK usage_metadata: %s", e)
309+
else:
310+
logger.warning(
311+
"Ignoring invalid ADK usage_metadata of type %s",
312+
type(parsed).__name__,
313+
)
314+
315+
return result
316+
317+
251318
def _merge_top_level_dicts(
252319
base: dict[str, Any], new_values: dict[str, Any]
253320
) -> dict[str, Any]:
@@ -304,6 +371,7 @@ def convert_a2a_task_to_event(
304371

305372
try:
306373
event_actions = EventActions()
374+
event_metadata: dict[str, Any] = {}
307375
output_parts = []
308376
long_running_function_ids = set()
309377
if a2a_task.artifacts:
@@ -314,6 +382,7 @@ def convert_a2a_task_to_event(
314382
event_actions = _merge_event_actions(
315383
event_actions, _extract_event_actions(artifact.metadata)
316384
)
385+
event_metadata.update(_extract_event_metadata(artifact.metadata))
317386
output_parts, _ = _convert_a2a_parts_to_adk_parts(
318387
artifact_parts, part_converter
319388
)
@@ -325,6 +394,9 @@ def convert_a2a_task_to_event(
325394
event_actions,
326395
_extract_event_actions(a2a_task.status.message.metadata),
327396
)
397+
event_metadata.update(
398+
_extract_event_metadata(a2a_task.status.message.metadata)
399+
)
328400
parts, ids = _convert_a2a_parts_to_adk_parts(
329401
a2a_task.status.message.parts, part_converter
330402
)
@@ -337,6 +409,7 @@ def convert_a2a_task_to_event(
337409
author,
338410
event_actions,
339411
long_running_function_ids,
412+
**event_metadata,
340413
)
341414

342415
except Exception as e:
@@ -375,11 +448,13 @@ def convert_a2a_message_to_event(
375448
output_parts, _ = _convert_a2a_parts_to_adk_parts(
376449
a2a_message.parts, part_converter
377450
)
451+
event_metadata = _extract_event_metadata(a2a_message.metadata)
378452
return _create_event(
379453
output_parts,
380454
invocation_context,
381455
author,
382456
_extract_event_actions(a2a_message.metadata),
457+
**event_metadata,
383458
)
384459

385460
except Exception as e:
@@ -412,10 +487,14 @@ def convert_a2a_status_update_to_event(
412487
output_parts = []
413488
long_running_function_ids = set()
414489
event_actions = EventActions()
490+
event_metadata: dict[str, Any] = {}
415491
if a2a_status_update.status.message:
416492
event_actions = _extract_event_actions(
417493
a2a_status_update.status.message.metadata
418494
)
495+
event_metadata = _extract_event_metadata(
496+
a2a_status_update.status.message.metadata
497+
)
419498
parts, ids = _convert_a2a_parts_to_adk_parts(
420499
a2a_status_update.status.message.parts, part_converter
421500
)
@@ -428,6 +507,7 @@ def convert_a2a_status_update_to_event(
428507
author,
429508
event_actions,
430509
long_running_function_ids,
510+
**event_metadata,
431511
)
432512
except Exception as e:
433513
logger.error("Failed to convert A2A status update to event: %s", e)
@@ -460,12 +540,16 @@ def convert_a2a_artifact_update_to_event(
460540
output_parts, _ = _convert_a2a_parts_to_adk_parts(
461541
a2a_artifact_update.artifact.parts, part_converter
462542
)
543+
event_metadata = _extract_event_metadata(
544+
a2a_artifact_update.artifact.metadata
545+
)
463546
return _create_event(
464547
output_parts,
465548
invocation_context,
466549
author,
467550
_extract_event_actions(a2a_artifact_update.artifact.metadata),
468551
partial=not a2a_artifact_update.last_chunk,
552+
**event_metadata,
469553
)
470554
except Exception as e:
471555
logger.error("Failed to convert A2A artifact update to event: %s", e)

0 commit comments

Comments
 (0)