A2A for Swift

March 19, 2026 · View on GitHub

 █████╗  ██████╗   █████╗    ███████╗ ██╗    ██╗ ██╗ ███████╗ ████████╗
██╔══██╗ ╚════██╗ ██╔══██╗   ██╔════╝ ██║    ██║ ██║ ██╔════╝ ╚══██╔══╝
███████║  █████╔╝ ███████║   ███████╗ ██║ █╗ ██║ ██║ █████╗      ██║   
██╔══██║ ██╔═══╝  ██╔══██║   ╚════██║ ██║███╗██║ ██║ ██╔══╝      ██║   
██║  ██║ ███████╗ ██║  ██║   ███████║ ╚███╔███╔╝ ██║ ██║         ██║   
╚═╝  ╚═╝ ╚══════╝ ╚═╝  ╚═╝   ╚══════╝  ╚══╝╚══╝  ╚═╝ ╚═╝         ╚═╝   

⚡ Agent-to-Agent Protocol 1.0 ⚡

Swift 6.0+ Platforms SPM Compatible License: MIT

A2A for Swift

The complete Swift SDK for Google's Agent-to-Agent (A2A) protocol — build, connect, and orchestrate AI agents that talk to each other.

Zero dependencies. Native Swift concurrency. Production-ready.


Why A2A?

AI agents are everywhere — but they can't talk to each other. Google's A2A protocol fixes that by defining a standard way for agents to discover capabilities, exchange messages, stream results, and collaborate on tasks — regardless of framework, language, or vendor.

a2a-swift brings this to the Apple ecosystem and Swift on Linux, so your agents can interoperate with any A2A-compatible agent in Python, JavaScript, Java, .NET, or Go.

Why this SDK?

  • Just implement execute() — the SDK handles task lifecycle, event processing, SSE streaming, and push notifications automatically
  • Native Swift concurrencyasync/await, AsyncSequence, actors. No Combine, no callbacks
  • Zero dependencies — pure Foundation. No framework lock-in
  • Framework-agnostic server — plug into Vapor, Hummingbird, or any HTTP stack with a single router
  • Full protocol coverage — every A2A v1.0 method, type, and error code

Installation

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/Victory-Apps/a2a-swift.git", from: "0.2.0")
]

Then add the dependency to your target:

.target(name: "MyApp", dependencies: ["A2A"])

Quick Start

Build an agent in ~30 lines

import A2A

// 1. Implement your agent logic
struct TranslationAgent: AgentExecutor {
    func execute(context: RequestContext, updater: TaskUpdater) async throws {
        updater.startWork(message: "Translating...")

        let translated = try await translate(context.userText)

        updater.addArtifact(
            name: "translation",
            parts: [.text(translated)]
        )
        updater.complete()
    }
}

// 2. Define your agent card
let card = AgentCard(
    name: "Translator",
    description: "Translates text between languages",
    supportedInterfaces: [
        AgentInterface(url: "https://my-agent.com", protocolVersion: "1.0")
    ],
    version: "1.0.0",
    capabilities: AgentCapabilities(streaming: true),
    defaultInputModes: ["text/plain"],
    defaultOutputModes: ["text/plain"],
    skills: [
        AgentSkill(
            id: "translate",
            name: "Translate",
            description: "Translates text between languages",
            tags: ["translation", "language"],
            examples: ["Translate 'hello' to French"]
        )
    ]
)

// 3. Create the handler and router — done!
let handler = DefaultRequestHandler(executor: TranslationAgent(), card: card)
let router = A2ARouter(handler: handler)

The DefaultRequestHandler automatically manages:

  • Task creation and lifecycle
  • Event processing and store updates
  • SSE streaming for real-time responses
  • Push notification delivery
  • Error handling and JSON-RPC formatting

Talk to any A2A agent

import A2A

let client = A2AClient(baseURL: URL(string: "https://agent.example.com")!)

// Discover what the agent can do
let card = try await client.fetchAgentCard()
print("Agent: \(card.name) — \(card.description)")
print("Skills: \(card.skills.map(\.name).joined(separator: ", "))")

// Send a message
let response = try await client.sendMessage(SendMessageRequest(
    message: Message(role: .user, parts: [.text("Translate 'hello' to French")])
))

// Stream responses in real-time
let stream = try await client.sendStreamingMessage(SendMessageRequest(
    message: Message(role: .user, parts: [.text("Write a detailed report")])
))

for try await event in stream {
    switch event {
    case .statusUpdate(let update):
        print("Status: \(update.status.state)")
    case .artifactUpdate(let update):
        print(update.artifact.parts.compactMap(\.text).joined())
    case .task(let task):
        print("Task \(task.id): \(task.status.state)")
    case .message(let message):
        print(message.parts.compactMap(\.text).joined())
    }
}

Apple Intelligence Integration

Build A2A agents powered by Apple's on-device language model — zero cloud dependency, complete privacy:

#if canImport(FoundationModels)
import A2A
import FoundationModels

struct AppleIntelligenceAgent: AgentExecutor {
    func execute(context: RequestContext, updater: TaskUpdater) async throws {
        guard SystemLanguageModel.default.isAvailable else {
            updater.fail(message: "Apple Intelligence not available")
            return
        }

        let session = LanguageModelSession(instructions: "You are a helpful assistant.")
        updater.startWork(message: "Thinking...")

        let artifactId = UUID().uuidString
        let stream = session.streamResponse(to: context.userText)

        var lastContent = ""
        var isFirst = true
        for try await partial in stream {
            let delta = String(partial.content.dropFirst(lastContent.count))
            lastContent = partial.content
            guard !delta.isEmpty else { continue }

            updater.streamText(delta, artifactId: artifactId, append: !isFirst)
            isFirst = false
        }

        updater.streamText("", artifactId: artifactId, append: true, lastChunk: true)
        updater.complete()
    }
}
#endif

Requires iOS 26+ / macOS 26+ with Apple Intelligence enabled. See Examples/OnDeviceLLMAgent.swift for the full example including structured output, and Examples/A2AClientApp.swift for a SwiftUI chat client.


Samples & Examples

Sample Apps

Complete, runnable applications in Samples/:

A2A Chat Client — macOS SwiftUI app connected to the product catalog agent

SampleDescriptionStack
A2AServerDockerized product catalog agent with Ollama LLM, streaming responses, and conversation memoryVapor · Docker · Ollama
A2AChatClientmacOS chat client with multi-agent connectivity, Apple Intelligence routing, and streaming UISwiftUI · Foundation Models
# Start the server (requires Docker Desktop — or run locally with swift run)
cd Samples/A2AServer && docker compose up --build

# Open the client in Xcode (requires Xcode 26+)
cd Samples/A2AChatClient && open Package.swift
# Build & Run (⌘R), then connect to http://localhost:8080

Code Examples

Single-file reference snippets in Examples/:

FileWhat it shows
EchoAgent.swiftAgentExecutor patterns — echo, streaming, multi-turn
A2AClientApp.swiftSwiftUI client with streaming & agent card discovery
OnDeviceLLMAgent.swiftApple Intelligence on-device agent

Agent Patterns

Streaming agent

Stream results token-by-token using AsyncSequence-based event delivery:

struct StreamingAgent: AgentExecutor {
    func execute(context: RequestContext, updater: TaskUpdater) async throws {
        updater.startWork()
        let artifactId = UUID().uuidString

        for chunk in generateChunks(context.userText) {
            updater.streamText(chunk, artifactId: artifactId, append: true)
        }
        updater.streamText("", artifactId: artifactId, append: true, lastChunk: true)
        updater.complete()
    }
}

Multi-turn conversation

Keep the task alive with requireInput() to build interactive flows:

struct ChatAgent: AgentExecutor {
    func execute(context: RequestContext, updater: TaskUpdater) async throws {
        updater.startWork()

        if context.isNewTask {
            updater.addArtifact(parts: [.text("Hello! What would you like to discuss?")])
            updater.requireInput(message: "Waiting for your response...")
        } else {
            let reply = try await generateReply(context.userText)
            updater.addArtifact(parts: [.text(reply)])
            updater.requireInput(message: "Anything else?")
        }
    }
}

Client with authentication

let client = A2AClient(
    baseURL: agentURL,
    interceptors: [
        BearerAuthInterceptor(token: "your-api-key")
    ]
)

// Or use a custom interceptor
struct MyAuthInterceptor: A2AClientInterceptor {
    func before(request: inout URLRequest, method: A2AMethod) async throws {
        let token = try await fetchToken()
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
    }
}

HTTP Framework Integration

The A2ARouter is framework-agnostic. Here's how to plug it in:

Vapor

import Vapor
import A2A

func routes(_ app: Application, router: A2ARouter) {
    app.get(".well-known", "agent-card.json") { req async throws -> Response in
        let data = try await router.handleAgentCardRequest()
        var headers = HTTPHeaders()
        headers.add(name: .contentType, value: "application/json")
        return Response(status: .ok, headers: headers, body: .init(data: data))
    }

    app.post { req async throws -> Response in
        let body = Data(buffer: req.body.data ?? ByteBuffer())
        let result = try await router.route(body: body)
        switch result {
        case .response(let data):
            var headers = HTTPHeaders()
            headers.add(name: .contentType, value: "application/json")
            return Response(status: .ok, headers: headers, body: .init(data: data))
        case .stream(let stream):
            // Return SSE stream
            var headers = HTTPHeaders()
            headers.add(name: .contentType, value: "text/event-stream")
            let responseBody = Response.Body(asyncStream: { writer in
                for try await chunk in stream {
                    try await writer.write(.buffer(.init(data: chunk)))
                }
            })
            return Response(status: .ok, headers: headers, body: responseBody)
        case .agentCard(let data):
            return Response(status: .ok, body: .init(data: data))
        }
    }
}

Hummingbird

import Hummingbird
import A2A

let app = Application()
let router = A2ARouter(handler: DefaultRequestHandler(executor: MyAgent(), card: myCard))

app.router.get(".well-known/agent-card.json") { _, _ in
    let data = try await router.handleAgentCardRequest()
    return Response(status: .ok, headers: [.contentType: "application/json"], body: .init(byteBuffer: ByteBuffer(data: data)))
}

app.router.post("/") { request, _ in
    let body = try await request.body.collect(upTo: 1_048_576)
    let result = try await router.route(body: Data(buffer: body))
    // handle .response / .stream / .agentCard
}

Architecture

Sources/A2A/
├── Models/
│   ├── JSONValue.swift             Type-safe arbitrary JSON
│   ├── Part.swift                  Content: text, binary, URL, structured data
│   ├── Message.swift               Messages with role (user/agent) and parts
│   ├── Artifact.swift              Task output artifacts
│   ├── Task.swift                  Task, TaskState, TaskStatus
│   ├── Events.swift                Stream events and response unions
│   ├── Requests.swift              All JSON-RPC request/response types
│   ├── AgentCard.swift             Agent discovery and capabilities
│   ├── Security.swift              API key, OAuth2, OpenID, mTLS schemes
│   ├── PushNotification.swift      Webhook push notification config
│   └── Errors.swift                A2A error codes (-32001 to -32009)

├── JSONRPC/
│   └── JSONRPCMessage.swift        JSON-RPC 2.0 request/response/error layer

├── Client/
│   ├── A2AClient.swift             Full client with SSE streaming
│   └── ClientInterceptor.swift     Middleware: BearerAuth, APIKey, custom

└── Server/
    ├── AgentExecutor.swift          ← You implement this
    ├── RequestContext.swift         Rich context + TaskUpdater helpers
    ├── EventQueue.swift             AsyncSequence pub/sub, multi-subscriber
    ├── TaskStore.swift              Protocol for custom storage backends
    ├── InMemoryTaskStore.swift      Actor-based reference implementation
    ├── TaskManager.swift            Event processing + store updates
    ├── PushNotificationSender.swift Webhook delivery
    ├── DefaultRequestHandler.swift  Orchestrates everything automatically
    └── A2AServer.swift              Low-level handler protocol + router

Two ways to build agents:

ApproachBest forYou implement
AgentExecutor + DefaultRequestHandlerMost agentsJust execute() — SDK handles the rest
A2AAgentHandler + A2ARouterFull controlAll handler methods manually

Protocol Coverage

Full implementation of the A2A v1.0 specification.

CategoryFeatures
CoreSendMessage, GetTask, ListTasks, CancelTask
StreamingSendStreamingMessage (SSE), SubscribeToTask (SSE)
DiscoveryAgent Card (.well-known/agent-card.json), Extended Agent Card
SecurityAPI Key, HTTP Bearer, OAuth 2.0 (auth code, client credentials, device code), OpenID Connect, Mutual TLS
PushCreate/Get/List/Delete push notification configs, webhook delivery
TransportJSON-RPC 2.0 over HTTP(S)
ErrorsAll 9 A2A error codes + standard JSON-RPC errors

Comparison with official SDKs

FeaturePythonJS/TSJava.NETSwift
Protocol typesYesYesYesYesYes
Client + SSEYesYesYesYesYes
Client interceptorsYesYesYesYes
AgentExecutor patternYesYesYesYesYes
EventQueue (AsyncSequence)YesYesYesYes
TaskManagerYesYesYesYesYes
TaskStore protocolYesYesYesYesYes
Push notification senderYesYesYesYesYes
DefaultRequestHandlerYesYesYesYesYes
Zero dependenciesYes

Requirements

PlatformMinimum Version
Swift6.0+
macOS13.0+
iOS16.0+
tvOS16.0+
watchOS9.0+
LinuxSwift 6.0+

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

git clone https://github.com/Victory-Apps/a2a-swift.git
cd a2a-swift
swift build
swift test   # 60 tests across 8 suites

License

MIT License. See LICENSE for details.


Built with Swift concurrency. No dependencies. No compromises.