Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions src/google/adk/a2a/converters/to_adk_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,9 +507,10 @@ def convert_a2a_task_to_event(
)
if not metadata_fields:
metadata_fields = _extract_all_metadata_fields(artifact.metadata)
output_parts, _ = _convert_a2a_parts_to_adk_parts(
output_parts, ids = _convert_a2a_parts_to_adk_parts(
artifact_parts, part_converter
)
long_running_function_ids.update(ids)
if status_message and (
a2a_task.status.state == _compat.TS_INPUT_REQUIRED
or a2a_task.status.state == _compat.TS_AUTH_REQUIRED
Expand Down Expand Up @@ -576,7 +577,7 @@ def convert_a2a_message_to_event(
raise ValueError("A2A message cannot be None")

try:
output_parts, _ = _convert_a2a_parts_to_adk_parts(
output_parts, long_running_function_ids = _convert_a2a_parts_to_adk_parts(
a2a_message.parts, part_converter
)
content_role = _a2a_role_to_content_role(getattr(a2a_message, "role", None))
Expand All @@ -586,6 +587,7 @@ def convert_a2a_message_to_event(
invocation_context,
author,
_extract_event_actions(a2a_message.metadata),
long_running_function_ids,
content_role=content_role,
**metadata_fields,
)
Expand Down Expand Up @@ -675,7 +677,7 @@ def convert_a2a_artifact_update_to_event(
raise ValueError("A2A artifact update cannot be None")

try:
output_parts, _ = _convert_a2a_parts_to_adk_parts(
output_parts, long_running_function_ids = _convert_a2a_parts_to_adk_parts(
a2a_artifact_update.artifact.parts, part_converter
)
metadata_fields = _extract_all_metadata_fields(
Expand All @@ -686,6 +688,7 @@ def convert_a2a_artifact_update_to_event(
invocation_context,
author,
_extract_event_actions(a2a_artifact_update.artifact.metadata),
long_running_function_ids,
partial=not a2a_artifact_update.last_chunk,
**metadata_fields,
)
Expand Down
135 changes: 135 additions & 0 deletions tests/unittests/a2a/converters/test_to_adk.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,64 @@ def _make_a2a_part_for_test(metadata=None):
return m


# One wire input per inbound converter, built from a shared A2A message.
_LONG_RUNNING_INBOUND_CONVERTERS = {
"task": (
lambda message: _compat.make_task(
id="task-1",
context_id="context-1",
kind="task",
status=_compat.make_task_status(
_compat.TS_INPUT_REQUIRED, timestamp="now", message=message
),
),
convert_a2a_task_to_event,
),
"status_update": (
lambda message: _compat.make_task_status_update_event(
task_id="task-1",
context_id="context-1",
final=False,
status=_compat.make_task_status(
_compat.TS_INPUT_REQUIRED, timestamp="now", message=message
),
),
convert_a2a_status_update_to_event,
),
"task_artifact": (
lambda message: _compat.make_task(
id="task-1",
context_id="context-1",
kind="task",
status=_compat.make_task_status(
_compat.TS_INPUT_REQUIRED, timestamp="now"
),
artifacts=[
_compat.make_artifact(
artifact_id="art-1", parts=list(message.parts)
)
],
),
convert_a2a_task_to_event,
),
"message": (lambda message: message, convert_a2a_message_to_event),
"artifact_update": (
lambda message: TaskArtifactUpdateEvent(
task_id="task-1",
context_id="context-1",
artifact=_compat.make_artifact(
artifact_id="art-1",
artifact_type="message",
parts=list(message.parts),
),
append=True,
last_chunk=True,
),
convert_a2a_artifact_update_to_event,
),
}


class TestToAdk:
"""Test suite for to_adk functions."""

Expand Down Expand Up @@ -770,6 +828,83 @@ def test_convert_a2a_message_to_event_agent_role(self) -> None:

assert event.content.role == "model"

@pytest.mark.parametrize(
"converter_key", list(_LONG_RUNNING_INBOUND_CONVERTERS)
)
def test_long_running_tool_ids_survive_every_inbound_converter(
self, converter_key
):
"""Every inbound converter must surface the ids it recovers."""
build_input, convert = _LONG_RUNNING_INBOUND_CONVERTERS[converter_key]
a2a_part = _make_a2a_part_for_test({
_get_adk_metadata_key(A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY): True
})
message = Message(
message_id="m1", role=_compat.ROLE_AGENT, parts=[a2a_part]
)
mock_part_converter = Mock(
return_value=[
genai_types.Part(
function_call=genai_types.FunctionCall(
name="wait_for_human_approval", args={}, id="call-1"
)
)
]
)

event = convert(
build_input(message),
author="test-author",
invocation_context=self.mock_context,
part_converter=mock_part_converter,
)

assert event is not None
assert event.long_running_tool_ids == {"call-1"}

def test_input_required_task_keeps_its_own_long_running_call(self):
"""A pending call must not be replaced by a synthesised one.

`_create_mock_function_call_for_required_user_input` synthesises a call
under a fresh uuid only when no ids survived. Dropping a real id here
would hand the caller an id that answers nothing.
"""
a2a_part = _make_a2a_part_for_test({
_get_adk_metadata_key(A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY): True
})
task = _compat.make_task(
id="task-1",
context_id="context-1",
kind="task",
status=_compat.make_task_status(
_compat.TS_INPUT_REQUIRED, timestamp="now"
),
artifacts=[
_compat.make_artifact(artifact_id="art-1", parts=[a2a_part])
],
)
mock_part_converter = Mock(
return_value=[
genai_types.Part(
function_call=genai_types.FunctionCall(
name="wait_for_human_approval", args={}, id="call-1"
)
)
]
)

event = convert_a2a_task_to_event(
task,
author="test-author",
invocation_context=self.mock_context,
part_converter=mock_part_converter,
)

assert event.long_running_tool_ids == {"call-1"}
assert event.content.parts[0].function_call.name == (
"wait_for_human_approval"
)


class TestExtractGenaiMetadata:

Expand Down
Loading