ra2a Rust SDK

repository·main·Indexed 41 days ago

https://github.com/qntx/ra2a

A Rust implementation of the Agent2Agent (A2A) Protocol SDK, part of the QuantX open-source ecosystem. It provides tools for agent-to-agent communication, including a ClientFactory for transport selection (JSON-RPC, HTTP+JSON), CallInterceptor for request/response middleware, and extension propagation via ServerPropagator and ClientPropagator to preserve metadata across agent chains.

Tokens
14.7K
Snippets
51
Records
65
Agent score
51%

What's inside ra2a

  1. Use the A2A Rust SDK

    main

    The ra2a crate is a Rust implementation of the Agent2Agent (A2A) Protocol v1.0. It is designed for building agentic applications that require secure, type-safe communication between agents.

    Key features include:

    • A2A Protocol v1.0 Compliance: Full support for the latest specification.
    • Async/Await: Built on tokio for high-performance asynchronous operations.
    • Type-Safe Models: Uses strongly typed models with newtype IDs and serde serialization.
    • Extensibility: Modular design supporting optional features like grpc, telemetry (OpenTelemetry), and server/client modes.
  2. Automatic Tenant ID injection via TenantTransportDecorator

    main

    The ClientFactory automatically handles multi-tenancy. If an AgentInterface within an AgentCard contains a non-empty tenant field, the factory wraps the selected transport in a TenantTransportDecorator.

    This decorator ensures that the tenant ID is automatically injected into the tenant field of every request (e.g., SendMessageRequest, GetTaskRequest, etc.) before it is passed to the underlying transport, preventing the need for manual injection in your application logic.

  3. Use CallContext to access request scope

    main

    A CallContext is created by the transport layer for every incoming request. It is passed to CallInterceptors and handlers, providing access to:

    • The current handler method name.
    • The RequestMeta (headers/metadata).
    • The authenticated User.
    • Activated and requested extension URIs.

    Extensions can be checked via is_extension_active(uri) or is_extension_requested(uri).

    // Inside a handler or interceptor
    fn handle(ctx: &CallContext) {
        let method = ctx.method();
        let user = &ctx.user;
        let is_ext = ctx.is_extension_requested("ext://my-plugin");
    }
  4. How extension propagation works in agent-to-agent chains

    main

    When an agent acts as both a server and a client in a chain (e.g., A → B → C), extension metadata and headers must be passed through. This is achieved using two interceptors:

    1. ServerPropagator: Extracts extension-related metadata and headers from the incoming request (A → B) and stores them in a PropagatorContext.
    2. ClientPropagator: Injects that stored context into the outgoing request (B → C).

    Crucial Requirement: For this to work, the agent's request handler must be wrapped in PropagatorContext::scope() or the task must be initialized with init_propagation(). This allows the ClientPropagator to access the extracted data via task_local storage.

    A → [HTTP] → B (ServerPropagator.before extracts → PropagatorContext)
                      → handler wraps executor in PropagatorContext::scope()
                        → B calls C (ClientPropagator.before injects from task_local)
  5. Configure AgentCapabilities

    main

    The AgentCapabilities struct defines high-level features supported by the agent. Key capabilities include:

    • streaming: Indicates if the agent supports streaming responses.
    • push_notifications: Indicates if the agent supports push notifications.
    • extended_agent_card: Indicates if the agent provides an extended card when authenticated.
    • extensions: A list of AgentExtension objects for protocol-specific features.
  6. Propagate extensions across agent chains using ServerPropagator and ClientPropagator

    main

    To enable agent-to-agent chaining (e.g., Agent A $\rightarrow$ Agent B $\rightarrow$ Agent C) where extension metadata is preserved, use the propagation components:

    1. ServerPropagator: A server-side interceptor that extracts extension-related metadata and headers from incoming requests.
    2. ClientPropagator: A client-side interceptor that injects that extracted metadata into outgoing requests to the next agent in the chain.

    Use init_propagation to set up the propagation logic.

  7. How A2A handles streaming via SSE

    main

    The A2A REST API supports Server-Sent Events (SSE) for real-time updates through two specific endpoints:

    1. POST /message:stream: Streams messages using SSE.
    2. GET /tasks/{id}:subscribe: Subscribes to updates for a specific task using SSE.

    Both endpoints return an event stream where each event contains a JSON-serialized payload. If an error occurs within the stream, the error is serialized into the JSON data of the event.

  8. Use TaskVersion for optimistic concurrency control

    main

    The TaskVersion struct is used to track the version of a task stored on the server. This is critical for preventing lost updates in concurrent environments.

    When calling TaskStore::save, you should provide the version you last read. If the version in the store has changed since your last read, the operation should fail with a concurrency error (e.g., A2AError::ConcurrentTaskModification).

    • Use TaskVersion::MISSING (value 0) when version tracking is not active or for initial creation where no previous version exists.
    pub struct TaskVersion(pub i64);
    
    impl TaskVersion {
        pub const MISSING: Self = Self(0);
    }
  9. JSON serialization format for A2A streaming events

    main

    The A2A protocol (v1.0) uses JSON member names as discriminators for streaming events instead of a dedicated kind field. When consuming or producing events via the StreamResponse enum, expect the following JSON structures:

    • {"task": {...}} (Full task snapshot)
    • {"message": {...}} (Direct message)
    • {"statusUpdate": {...}} (Task status change)
    • {"artifactUpdate": {...}} (Artifact creation or update)
  10. Initialize extension propagation with init_propagation

    main

    To enable the ServerPropagator to store extracted data and allow the ClientPropagator to read it later, you must wrap your request handler in init_propagation. This sets up the necessary task-local storage scope.

    let result = ra2a_ext::init_propagation(async {
        // ServerPropagator.before() will store data here
        // Your handler runs
        // ClientPropagator.before() will be able to read data here
        handle_request(req).await
    }).await;
    let result = ra2a_ext::init_propagation(async {
        // ServerPropagator.before() stores data here
        // handler runs
        // ClientPropagator.before() reads data here
        handle_request(req).await
    }).await;
  11. Use SQL-backed TaskStores (SQLite, PostgreSQL, MySQL)

    main

    For production environments requiring persistent storage, use the SQL-backed implementations provided via the sql feature. These implementations use SQLx and support different database backends.

    Available Implementations

    • SqliteTaskStore: For SQLite databases.
    • PostgresTaskStore: For PostgreSQL databases.
    • MySqlTaskStore: For MySQL databases.

    Initialization

    Before using a SQL store, you must call .initialize() to create the necessary tables (e.g., a2a_tasks).

    Schema Details

    • SQLite: Uses standard text types for history, artifacts, and metadata.
    • PostgreSQL: Uses JSONB for optimized storage of history, artifacts, and metadata.
    // Example pattern for initializing a SQL store
    let pool = sqlx::SqlitePool::connect("sqlite:tasks.db").await?;
    let store = SqliteTaskStore::new(pool);
    store.initialize().await?;
  12. Create an A2A Client using ClientFactory

    main

    The ClientFactory is used to automatically select and connect to the best available transport for an agent based on its AgentCard. It inspects the supported_interfaces in the card and matches them against registered protocol builders (like JSON-RPC or REST) and your client's preferred transport order.

    You can create a client in two ways:

    1. From an AgentCard object: Use create_from_card if you already have the card in memory.
    2. From a base URL: Use create_from_url to automatically fetch the agent card from the well-known URL at that base address and then establish a connection.

    By default, the factory supports:

    • JSONRPC (Primary/Fallback)
    • HTTP+JSON (REST)
    // Create a client from an existing AgentCard
    let client = ClientFactory::new()
        .create_from_card(&card)
        .await?;
    
    // OR create a client by fetching the card from a URL
    let client = ClientFactory::new()
        .create_from_url("https://api.example.com")
        .await?;