A2A .NET SDK

repository·main·Indexed 42 days ago

https://github.com/a2aproject/a2a-dotnet

A library for building and consuming agentic applications following the Agent2Agent (A2A) protocol. It provides a core library for protocol implementation and ASP.NET Core extensions for hosting agents. Key features include support for message-based and task-based communication, agent discovery via AgentCard, and an Interoperability Test Kit (ITK) for cross-SDK compatibility testing across JSON-RPC, HTTP+JSON, and gRPC.

Tokens
16K
Snippets
31
Records
50
Agent score
54%

What's inside A2A .NET SDK

  1. Core A2A Library components

    main

    The A2A library provides the fundamental building blocks for the Agent2Agent protocol.

    Client-side

    • A2AClient: Primary client for making A2A requests. Supports streaming, task management, and push notifications.
    • A2ACardResolver: Resolves AgentCard information from endpoints to discover capabilities.

    Server-side

    • A2AServer: Handles JSON-RPC requests, manages task lifecycles via TaskProjection, and coordinates responses.
    • IAgentHandler: The interface you implement to define agent behavior. Requires ExecuteAsync() and CancelAsync().
    • TaskUpdater: API for emitting lifecycle events: Submit, StartWork, AddArtifact, Complete, Fail, Cancel, RequireInput.
    • MessageResponder: API for stateless, immediate message replies.
    • RequestContext: Provides context to the agent, including the incoming Message, TaskId, ContextId, and properties like UserText and IsContinuation.
    • AgentEventQueue: A channel-backed queue where agents write events for the SDK to process.
    • ITaskStore: Interface for task persistence. Use InMemoryTaskStore for development/testing.

    Core Models

    • AgentTask: Represents task status, history, artifacts, and metadata.
    • AgentCard: Contains agent metadata, capabilities, and endpoints.
    • Message: The data format for messages exchanged between agents.
  2. How the .NET ITK agent works

    main

    The .NET ITK agent (ItkAgent.cs) implements the ITK instruction protocol to verify agent behavior. It operates through a four-step lifecycle:

    1. Receives: A protobuf-encoded instruction embedded within an A2A message.
    2. Parses: The instruction, which can be one of: CallAgent, ReturnResponse, or SeriesOfSteps.
    3. Executes:
      • CallAgent: Resolves the target's agent card, creates an A2A client, and forwards the nested instruction.
      • ReturnResponse: Returns the specified text.
      • SeriesOfSteps: Executes each step sequentially and concatenates the results.
    4. Returns: The collected trace as the task response.

    Supported behaviors tested include send_message, push_notification, and resubscribe (streaming with disconnect/reconnect).

  3. How Response Types and Streaming changed in V1

    main

    V0.3 used a single A2AResponse with a kind discriminator. V1 splits responses into specific types based on the operation.

    SendMessage Responses

    SendMessageAsync now returns a SendMessageResponse. Use PayloadCase to distinguish between a Task or a Message.

    Streaming Responses

    Streaming operations (like SendStreamingMessageAsync) return an IAsyncEnumerable<StreamResponse>. Use StreamResponseCase to handle different event types such as StatusUpdate, ArtifactUpdate, Task, or Message.

    // V1: SendMessage returns SendMessageResponse with named fields
    SendMessageResponse response = await client.SendMessageAsync(request);
    switch (response.PayloadCase)
    {
        case SendMessageResponseCase.Task:
            Console.WriteLine($"Task: {response.Task!.Id}, State: {response.Task.Status.State}");
            break;
        case SendMessageResponseCase.Message:
            Console.WriteLine($"Message from agent: {response.Message!.Parts[0].Text}");
            break;
    }
    
    // V1: Streaming returns IAsyncEnumerable<StreamResponse>
    await foreach (StreamResponse evt in client.SendStreamingMessageAsync(request))
    {
        switch (evt.PayloadCase)
        {
            case StreamResponseCase.StatusUpdate:
                Console.WriteLine(evt.StatusUpdate!.Status.State);
                break;
            case StreamResponseCase.ArtifactUpdate:
                Console.WriteLine(evt.ArtifactUpdate!.Artifact.ArtifactId);
                break;
            case StreamResponseCase.Task:
                Console.WriteLine(evt.Task!.Id);
                break;
            case StreamResponseCase.Message:
                Console.WriteLine(evt.Message!.Parts[0].Text);
                break;
        }
    }
  4. How the Part model changed in V1

    main

    In V0.3, parts were represented by a hierarchy of subclasses (TextPart, FilePart, DataPart). In V1, these are unified into a single sealed Part class.

    Creating Parts in V1

    Use the factory methods provided by the Part class:

    • Part.FromText(string text)
    • Part.FromUrl(string url, string mediaType, string filename)
    • Part.FromRaw(byte[] data)
    • Part.FromData(JsonElement data)

    Consuming Parts in V1

    Instead of type casting, use the ContentCase computed enum to identify the part type:

    foreach (Part part in message.Parts)
    {
        switch (part.ContentCase)
        {
            case PartContentCase.Text:
                Console.WriteLine(part.Text);
                break;
            case PartContentCase.Url:
                Console.WriteLine(part.Url);
                break;
            case PartContentCase.Raw:
                // part.Raw is byte[]
                break;
            case PartContentCase.Data:
                Console.WriteLine(part.Data);
                break;
        }
    }
    // Creating parts — use factory methods
    var textPart = Part.FromText("Hello, world!");
    var filePart = Part.FromUrl("https://example.com/report.pdf",
        mediaType: "application/pdf", filename: "report.pdf");
    var dataPart = Part.FromData(
        JsonDocument.Parse("{\"key\": \"value\"}").RootElement);
    
    // Consuming parts — use ContentCase enum
    foreach (Part part in message.Parts)
    {
        switch (part.ContentCase)
        {
            case PartContentCase.Text:
                Console.WriteLine(part.Text);
                break;
            case PartContentCase.Url:
                Console.WriteLine(part.Url);
                break;
            case PartContentCase.Raw:
                // part.Raw is byte[] (base64 in JSON)
                break;
            case PartContentCase.Data:
                Console.WriteLine(part.Data);
                break;
        }
    }
  5. Security responsibility boundaries in A2A .NET SDK

    main

    The SDK handles protocol mechanics (serialization, JSON-RPC dispatch, task lifecycle), but the application developer is responsible for the following security layers:

    BoundarySDK ResponsibilityApplication Responsibility
    NetworkRoutes/dispatches JSON-RPC; maps errors to ProblemDetailsAuthentication, Authorization, HTTPS, Rate-limiting, Size-limiting, Cookie isolation
    Streaming (SSE)Writes incremental events to the response streamConcurrency limits, Idle timeouts, Backpressure, Content redaction
    WebhooksStores PushNotificationConfigURL validation (SSRF), Token verification, Retry caps
    Tenant IsolationNoneScoping every task/config operation to an authenticated identity
    StorageProvides ITaskStore interfaceProduction storage, Encryption, Quotas, TTL cleanup
    File HandlingTreats FilePart as opaque dataSize limits, MIME allowlists, Malware scanning, URI validation

    Non-Goals: The SDK does not provide protection against prompt injection, cross-agent instruction smuggling, or webhook egress safety (SSRF).

  6. Update DI registration and endpoint wiring in v1

    main

    In v1, you no longer manually instantiate TaskManager and attach it to an agent. Instead, use the ASP.NET Core extension methods to register services in the DI container.

    • Use builder.Services.AddA2AAgent<THandler>(agentCard) to register IAgentHandler, AgentCard, A2AServerOptions, ChannelEventNotifier, ITaskStore (defaults to InMemoryTaskStore), and IA2ARequestHandler (as A2AServer).
    • Use app.MapA2A(path) to resolve the handler from DI automatically.
    • Use app.MapWellKnownAgentCard(card) to serve the agent card at .well-known/agent-card.json for discovery.
    • Custom Task Store: To use a custom ITaskStore, register it in the DI container before calling AddA2AAgent<T>() using TryAddSingleton.
  7. Maintain Backward Compatibility with A2A.V0_3

    main

    During a migration period, you can use the A2A.V0_3 NuGet package to maintain compatibility with legacy code. This package is a standalone snapshot of the v0.3 SDK and has no dependencies on the v1 package. You can use both namespaces in the same project by using fully qualified names when they conflict.

    // V0.3 namespace
    using A2A.V0_3;
    
    // V1 namespace
    using A2A;
    
    // Use fully qualified names to avoid ambiguity
    var v03Part = new A2A.V0_3.TextPart { Text = "hello" };
    var v1Part = A2A.Part.FromText("hello");
  8. How to build an Agent Server with ASP.NET Core

    main

    To host an A2A agent in an ASP.NET Core application, follow these steps:

    1. Implement IAgentHandler: Create a class that defines the agent's logic in ExecuteAsync.
    2. Register the Agent: Use AddA2AAgent<THandler>(AgentCard) in your service collection to register the handler, its card, and necessary A2A services.
    3. Map Endpoints: Use MapA2A(path) to expose JSON-RPC endpoints and MapWellKnownAgentCard(card) to enable agent discovery.

    Example implementation of an Echo Agent:

    using A2A;
    using A2A.AspNetCore;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Register the agent with DI
    builder.Services.AddA2AAgent<EchoAgent>(EchoAgent.GetAgentCard("http://localhost:5000/echo"));
    
    var app = builder.Build();
    
    // Map JSON-RPC endpoint
    app.MapA2A("/echo");
    
    // Map well-known agent card for discovery
    var card = app.Services.GetRequiredService<AgentCard>();
    app.MapWellKnownAgentCard(card);
    
    app.Run();
    
    public sealed class EchoAgent : IAgentHandler
    {
        public async Task ExecuteAsync(
            RequestContext context,
            AgentEventQueue eventQueue,
            CancellationToken cancellationToken)
        {
            var responder = new MessageResponder(eventQueue, context.ContextId);
            await responder.ReplyAsync($"Echo: {context.UserText}", cancellationToken);
        }
    
        public static AgentCard GetAgentCard(string url) => new()
        {
            Name = "Echo Agent",
            Description = "Echoes messages back to the user",
            Version = "1.0.0",
            SupportedInterfaces = [new AgentInterface
            {
                Url = url,
                ProtocolBinding = "JSONRPC",
                ProtocolVersion = "1.0"
            }],
            DefaultInputModes = ["text/plain"],
            DefaultOutputModes = ["text/plain"],
            Capabilities = new AgentCapabilities { Streaming = false },
            Skills = [new AgentSkill
            {
                Id = "echo",
                Name = "Echo",
                Description = "Echoes back user messages",
                Tags = ["echo"]
            }],
        };
    }
  9. Run ITK compatibility tests locally

    main

    The Interoperability Test Kit (ITK) allows you to run cross-SDK compatibility tests against the A2A .NET SDK. It verifies interoperability by routing messages through a cluster of agents built with different SDKs (Python, Go, .NET) across multiple transport protocols (JSON-RPC, HTTP+JSON, gRPC).

    Prerequisites

    • Docker (or Podman with docker compatibility)
    • .NET 8.0 SDK (required to build the .NET agent)

    Execution Steps

    1. Set the revision environment variable:
      export A2A_ITK_REVISION=main
    2. Navigate to the itk directory and run the test script:
      cd itk
      ./run_itk.sh

    When executed, the script clones a2a-itk, builds the ITK service Docker image, mounts the current repository as the agent under test, and runs the test scenarios.

    export A2A_ITK_REVISION=main
    cd itk
    ./run_itk.sh
  10. Install the A2A .NET SDK

    main

    To use the A2A protocol in your .NET applications, install the core library or the ASP.NET Core extensions depending on your needs.

    Core A2A Library (for general protocol implementation):

    dotnet add package A2A

    ASP.NET Core Extensions (for hosting agents in web applications):

    dotnet add package A2A.AspNetCore

    Note for v0.3 users: If you are upgrading from the v0.3 SDK, use the backward-compatible package:

    dotnet add package A2A.V0_3
    dotnet add package A2A
    dotnet add package A2A.AspNetCore
  11. Migrate TaskManager from v0.3 to v1

    main

    This guide outlines the migration steps for moving from TaskManager v0.3 to v1. The primary shift is from a callback-based model (attaching delegates to TaskManager) to a Dependency Injection (DI) and interface-based model where agents implement IAgentHandler.

    // v1 Setup Example
    using A2A;
    using A2A.AspNetCore;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Register the agent, its card, and all A2A services via DI
    builder.Services.AddA2AAgent<EchoAgent>(EchoAgent.GetAgentCard("http://localhost:5000/echo"));
    
    var app = builder.Build();
    
    // Map JSON-RPC endpoint
    app.MapA2A("/echo");
    
    // Map well-known agent card for discovery
    var card = app.Services.GetRequiredService<AgentCard>();
    app.MapWellKnownAgentCard(card);
    
    app.Run();