Skip to main content

SAM Gateway Migration Guide: Upgrading to the A2A SDK

This guide is for developers who have built or are maintaining a custom Solace Agent Mesh (SAM) gateway. A recent architectural update has aligned SAM with the official Agent-to-Agent (A2A) protocol
specification by adopting the a2a-sdk. This migration requires some changes to your gateway code to ensure compatibility.

This document provides a high-level overview of the conceptual changes and a practical checklist to guide you through the upgrade process.

1. Why the Change?

The migration from our legacy A2A implementation to the official a2a-sdk is a foundational improvement with several key benefits:

  • Protocol Compliance: Ensures your gateway is fully interoperable with any A2A-compliant agent or system.
  • Standardization: Replaces bespoke code with a community-supported standard, reducing technical debt.
  • Improved Maintainability: Insulates your gateway from future A2A specification changes. The a2a-sdk will be updated, not your core logic.
  • Future-Proofing: Positions your gateway to easily adopt new features as the A2A protocol evolves.

2. Core Conceptual Changes

The upgrade introduces a few key changes in how you interact with A2A objects.

The a2a Helper Layer: The New Best Practice

The most significant change is the introduction of a new abstraction layer located at solace_agent_mesh.common.a2a.

You should no longer instantiate a2a.types models directly or access their properties by hand. Instead, use the provided helper functions. This layer is designed to simplify development and
protect your code from future SDK changes.

Example:

# BEFORE: Direct instantiation and property access                                                                                                                                                        
from solace_agent_mesh.common.types import TextPart, Task
my_part = TextPart(text="Hello")
task_id = my_task.id

# AFTER: Using the a2a helper layer
from solace_agent_mesh.common import a2a
my_part = a2a.create_text_part(text="Hello")
task_id = a2a.get_task_id(my_task)

Type System Migration

  • The legacy types in solace_agent_mesh.common.types (like A2APart, FileContent) are deprecated.
  • All A2A models now come from the a2a.types library.
  • The type hint for a list of message parts has changed from List[A2APart] to List[ContentPart]. ContentPart is a simple alias for the union of TextPart, DataPart, and FilePart.

Accessing Object Properties

Field names on many A2A objects have changed. Always use the a2a helper functions for safe and future-proof access.

ActionOld PatternNew Pattern (Recommended)
Get Task IDtask.ida2a.get_task_id(task)
Get Task Statustask.status.statea2a.get_task_status(task)
Get Event's Task IDevent.idevent.task_id
Get Message Partsmessage.partsa2a.get_parts_from_message(message)
Get Error Messageerror.messagea2a.get_error_message(error)
Get Error Codeerror.codea2a.get_error_code(error)
Get Error Dataerror.dataa2a.get_error_data(error)

Changes to BaseGatewayComponent

The BaseGatewayComponent has been significantly improved. It now handles more of the A2A protocol complexity, simplifying gateway implementations.

  • Artifact Handling: The base class can now automatically handle artifact URIs, converting them to embedded bytes before sending them to your gateway's _send_... methods. This is controlled by
    the resolve_artifact_uris_in_gateway parameter in the constructor.
  • Message Publishing: The base class now manages the details of preparing and publishing the A2A request message in submit_a2a_task.
  • Asynchronous Model: The underlying threading model has been removed in favor of a more direct asyncio implementation, simplifying the component lifecycle.

It is highly recommended to review the latest BaseGatewayComponent and re-base your custom gateway on it to inherit these benefits and reduce boilerplate code.

3. Practical Migration Checklist

Follow these steps to update your gateway code.

  1. Update Imports:

    • Remove imports from solace_agent_mesh.common.types.
    • Add imports from a2a.types for specific models if needed.
    • Add from solace_agent_mesh.common import a2a.
    • Add from solace_agent_mesh.common.a2a import ContentPart.
  2. Update Type Hints:

    • Find all instances of List[A2APart] and change them to List[ContentPart].
  3. Refactor Object Creation:

    • Replace direct model instantiation like TextPart(...) or FilePart(...) with their corresponding helper functions: a2a.create_text_part(...), a2a.create_file_part_from_uri(...), etc.
  4. Refactor Property Access:

    • Replace direct property access (task.id, error.message) with calls to the a2a helper functions (a2a.get_task_id(task), a2a.get_error_message(error)).
  5. Review Base Class Integration:

    • If you have a custom gateway, compare it against the latest BaseGatewayComponent. Consider refactoring to delegate more responsibility (like artifact handling and message submission) to the
      base class.
  6. Test Thoroughly:

    • Once refactored, run your integration tests to ensure the gateway correctly translates inputs and processes outputs in the new format.

4. Code Examples: Before & After

Here are some common patterns you will encounter during the migration.

Example 1: Translating External Input

This example shows how to create A2A parts from an external event.

_translate_external_input

Before:

from solace_agent_mesh.common.types import Part as A2APart, TextPart, FilePart, FileContent                                                                                                               

async def _translate_external_input(self, external_event: Any) -> Tuple[str, List[A2APart], Dict[str, Any]]:
# ...
a2a_parts: List[A2APart] = []

# Create a file part with a URI
uri = "artifact://..."
a2a_parts.append(
FilePart(
file=FileContent(name="report.pdf", uri=uri)
)
)

# Create a text part
prompt = "Summarize the attached file."
a2a_parts.append(TextPart(text=prompt))

return "summary_agent", a2a_parts, {}

After:

from solace_agent_mesh.common import a2a                                                                                                                                                                  
from solace_agent_mesh.common.a2a import ContentPart

async def _translate_external_input(self, external_event: Any) -> Tuple[str, List[ContentPart], Dict[str, Any]]:
# ...
a2a_parts: List[ContentPart] = []

# Create a file part with a URI using the helper
uri = "artifact://..."
file_part = a2a.create_file_part_from_uri(uri=uri, name="report.pdf")
a2a_parts.append(file_part)

# Create a text part using the helper
prompt = "Summarize the attached file."
text_part = a2a.create_text_part(text=prompt)
a2a_parts.append(text_part)

return "summary_agent", a2a_parts, {}

Example 2: Sending a Final Response

This example shows how to process a final Task object.

_send_final_response_to_external

Before:

from solace_agent_mesh.common.types import Task, TaskState, TextPart                                                                                                                                      

async def _send_final_response_to_external(self, context: Dict, task_data: Task) -> None:
task_id = task_data.id
final_status_text = ":checkered_flag: Task complete."

if task_data.status.state == TaskState.FAILED:
error_message_text = ""
if task_data.status.message and task_data.status.message.parts:
for part in task_data.status.message.parts:
if isinstance(part, TextPart):
error_message_text = part.text
break
final_status_text = f":x: Error: Task failed. {error_message_text}".strip()

# ... use final_status_text and task_id

After:

from solace_agent_mesh.common import a2a                                                                                                                                                                  
from a2a.types import Task, TaskState

async def _send_final_response_to_external(self, context: Dict, task_data: Task) -> None:
# Use helpers to safely access properties
task_id = a2a.get_task_id(task_data)
task_status = a2a.get_task_status(task_data)

final_status_text = ":checkered_flag: Task complete."

if task_status == TaskState.failed:
error_message_text = ""
if task_data.status and task_data.status.message:
# Use helper to extract all text from the message
error_message_text = a2a.get_text_from_message(task_data.status.message)
final_status_text = f":x: Error: Task failed. {error_message_text}".strip()

# ... use final_status_text and task_id