Skip to content

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

A2AClient

CI Swift 6.0 Platforms License

A Swift client library for the Agent-to-Agent (A2A) Protocol, enabling seamless communication between applications and AI agents.

Quick Links

Overview

A2AClient provides a fully-featured implementation of the A2A Protocol for Apple platforms. It supports message-based communication, long-running task management, streaming responses, push notifications, and multiple authentication schemes.

Platform Support

  • iOS 15.0+
  • macOS 12.0+
  • watchOS 8.0+
  • tvOS 15.0+

Protocol Version

  • A2A Protocol Version: 1.0

Installation

Swift Package Manager

Add A2AClient to your Package.swift:

dependencies: [
    .package(url: "https://github.com/tolgaki/a2a-swift-client.git", from: "1.0.5")
]

Then add it to your target:

targets: [
    .target(
        name: "YourApp",
        dependencies: ["A2AClient"]
    )
]

Xcode

  1. Go to File > Add Package Dependencies
  2. Enter the repository URL: https://github.com/tolgaki/a2a-swift-client
  3. Select the version and add the package

Quick Start

Basic Usage

import A2AClient

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

// Send a message
let response = try await client.sendMessage("Hello, agent!")

// Handle the response
switch response {
case .message(let message):
    print("Response: \(message.textContent)")
case .task(let task):
    print("Task created: \(task.id)")
}

Using the Convenience API

import A2AClient

// One-liner message sending
let response = try await sendMessage("Hello!", to: URL(string: "https://agent.example.com")!)

// Or use the A2A namespace
let client = A2A.client(url: URL(string: "https://agent.example.com")!)
let response = try await client.sendMessage("Hello!")

Agent Discovery

// Discover an agent from a domain
let (agentCard, client) = try await A2A.discover(domain: "example.com")
print("Found agent: \(agentCard.name)")
print("Capabilities: \(agentCard.capabilities)")

// Send a message to the discovered agent
let response = try await client.sendMessage("Hello!")

Core Concepts

Messages

Messages are the primary unit of communication. Each message has a role (user or agent) and contains one or more parts:

// Simple text message
let message = Message.user("Hello, world!")

// Message with multiple parts
let message = Message.user(parts: [
    .text("Please analyze this image:"),
    .file(data: imageData, name: "photo.jpg", mediaType: "image/jpeg")
])

// Message with context for multi-turn conversations
let message = Message.user("Follow-up question", contextId: conversationId)

Parts

Parts represent different types of content. The Part model uses a "oneof" pattern where exactly one content field should be set:

// Text content
Part.text("Hello, world!")

// File with raw data (base64 encoded in JSON)
Part.file(data: pdfData, name: "document.pdf", mediaType: "application/pdf")

// File by URL reference
Part.file(uri: "https://example.com/file.pdf", name: "document.pdf", mediaType: "application/pdf")

// URL reference without metadata
Part.url("https://example.com/resource")

// Raw bytes
Part.raw(binaryData)

// Structured data
Part.data(["key": "value", "count": 42])

// Check content type
let part = Part.text("Hello")
if part.isText {
    print(part.text!)  // "Hello"
}

Tasks

Long-running operations return tasks that track progress:

// Send a message that creates a task
let response = try await client.sendMessage("Generate a report")

if let task = response.task {
    // Check task status
    print("Status: \(task.status.state)")

    // Wait for completion by polling
    while !task.isComplete {
        let updated = try await client.getTask(task.id)
        // Check updated.status.state
    }

    // Or subscribe to updates
    let stream = try await client.subscribeToTask(task.id)
    for try await event in stream {
        switch event {
        case .taskStatusUpdate(let update):
            print("Status: \(update.status.state)")
        case .taskArtifactUpdate(let update):
            print("New artifact: \(update.artifact.name ?? "unnamed")")
        }
    }
}

Task States

State Description
submitted Task has been received
working Task is being processed
inputRequired Agent needs additional input
completed Task finished successfully
failed Task encountered an error
cancelled Task was cancelled
authRequired Authentication needed
rejected Task was rejected

Streaming

For real-time updates, use streaming:

let stream = try await client.sendStreamingMessage("Generate a story")

for try await event in stream {
    switch event {
    case .taskStatusUpdate(let update):
        print("Status: \(update.status.state)")
        if let message = update.status.message {
            print("Message: \(message.textContent ?? "")")
        }

    case .taskArtifactUpdate(let update):
        print("Artifact: \(update.artifact.name ?? "unnamed")")
        for part in update.artifact.parts {
            if let text = part.text {
                print(text)
            }
        }
        
    case .task(let task):
        print("Task update: \(task.state)")
        
    case .message(let message):
        print("Message: \(message.textContent ?? "")")
    }
}

Authentication

A2AClient supports multiple authentication methods:

API Key

let config = A2AClientConfiguration(baseURL: url)
    .withAPIKey("your-api-key", name: "X-API-Key", location: .header)
let client = A2AClient(configuration: config)

Bearer Token

let config = A2AClientConfiguration(baseURL: url)
    .withBearerToken("your-jwt-token")
let client = A2AClient(configuration: config)

Basic Authentication

let config = A2AClientConfiguration(baseURL: url)
    .withBasicAuth(username: "user", password: "password")
let client = A2AClient(configuration: config)

OAuth 2.0

let oauth = OAuth2Authentication(
    configuration: .init(
        tokenUrl: "https://auth.example.com/token",
        clientId: "your-client-id",
        clientSecret: "your-client-secret"
    )
)

let config = A2AClientConfiguration(baseURL: url)
    .with(authenticationProvider: oauth)
let client = A2AClient(configuration: config)

Custom Authentication

Implement the AuthenticationProvider protocol for custom auth:

struct CustomAuth: AuthenticationProvider {
    func authenticate(request: URLRequest) async throws -> URLRequest {
        var request = request
        request.addValue("Custom credentials", forHTTPHeaderField: "Authorization")
        return request
    }
}

Push Notifications

Configure webhooks to receive task updates:

// Create a push notification configuration
let pushConfig = PushNotificationConfig(
    url: "https://your-app.example.com/webhook",
    token: "secret-verification-token"
)

// Create the configuration for a task
try await client.createPushNotificationConfig(taskId: task.id, config: pushConfig)

// List existing configurations
let configs = try await client.listPushNotificationConfigs(taskId: task.id)

// Delete a configuration
try await client.deletePushNotificationConfig(taskId: task.id, configId: config.id)

Configuration

Client Configuration

let config = A2AClientConfiguration(
    baseURL: URL(string: "https://agent.example.com")!,
    transportBinding: .httpREST,  // or .jsonRPC
    protocolVersion: "1.0",
    timeoutInterval: 60
)

let client = A2AClient(configuration: config)

Transport Bindings

The client supports two transport bindings:

  • HTTP/REST (default): Standard RESTful HTTP methods
  • JSON-RPC 2.0: Wrapped JSON-RPC protocol
// Use JSON-RPC transport
let config = A2AClientConfiguration(baseURL: url, transportBinding: .jsonRPC)
let client = A2AClient(configuration: config)

JSON Key Casing

Some servers (like the Microsoft Graph RP) use camelCase JSON keys, while others follow the A2A spec's snake_case convention. The library defaults to camelCase and supports configurable casing:

// Default: camelCase (messageId, contextId, taskId)
let config = A2AClientConfiguration(baseURL: url)

// For servers using snake_case (message_id, context_id, task_id)
let config = A2AClientConfiguration(baseURL: url, jsonKeyCasing: .snakeCase)

Custom URLSession

The custom URLSession is used for non-streaming requests. Streaming always uses URLSession.shared to ensure bytes are delivered incrementally (custom sessions can buffer the entire SSE response).

let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 30
sessionConfig.httpAdditionalHeaders = ["Custom-Header": "Value"]

let config = A2AClientConfiguration(
    baseURL: url,
    sessionConfiguration: sessionConfig
)
let client = A2AClient(configuration: config)

Multi-turn Conversations

Use context IDs to group related messages:

let contextId = UUID().uuidString

// First message
let response1 = try await client.sendMessage(
    "What's the weather like?",
    contextId: contextId
)

// Follow-up in the same context
let response2 = try await client.sendMessage(
    "What about tomorrow?",
    contextId: contextId
)

// List all tasks in this context
let tasks = try await client.listTasks(contextId: contextId)

Agent-to-Agent Orchestration

The library's core value proposition is agents talking to agents. The Examples/TravelPlannerAgent.swift example shows an on-device orchestrator that discovers remote agents, routes by skill, and aggregates results — covering every major API pattern:

import A2AClient

// Discover agents via well-known URLs
let (card, client) = try await A2A.discover(domain: "weather.example.com")

// Route by inspecting skills and capabilities
let weatherAgent = agents.first { agent in
    agent.card.skills.contains { !Set(["weather"]).isDisjoint(with: $0.tags) }
}

// Synchronous request (waits for completion)
let config = MessageSendConfiguration(returnImmediately: false)
let response = try await client.sendMessage("Weather in Tokyo?", configuration: config)

// Streaming for real-time updates
let stream = try await client.sendStreamingMessage("Find flights to Tokyo")
for try await event in stream {
    switch event {
    case .taskStatusUpdate(let update):
        print("Status: \(update.status.state.rawValue)")
    case .taskArtifactUpdate(let update):
        print("Result: \(update.artifact.textContent)")
    case .task(let task):
        print("Done: \(task.state.rawValue)")
    case .message(let message):
        print("Reply: \(message.textContent)")
    }
}

// Multi-turn conversation with contextId
let contextId = UUID().uuidString
var bookingResponse = try await client.sendMessage("Book a hotel in Tokyo", contextId: contextId)
if let task = bookingResponse.task, task.needsInput {
    bookingResponse = try await client.sendMessage("$200-300/night, 4-star", contextId: contextId, taskId: task.id)
}

// Concurrent orchestration with TaskGroup
let results = try await withThrowingTaskGroup(of: (String, String).self) { group in
    group.addTask { ("weather", try await checkWeather(using: weatherAgent)) }
    group.addTask { ("flights", try await searchFlights(using: flightsAgent)) }
    var collected: [String: String] = [:]
    for try await (key, value) in group { collected[key] = value }
    return collected
}

See the full example for the complete orchestrator with error handling and polling fallback.

For a more advanced pattern, the SmartTravelPlanner example adds an IntentRouter protocol that classifies user intent, selects agents, and crafts per-agent prompts — showing how an on-device LLM (e.g. Apple Intelligence / Foundation Models) can drive agent orchestration from natural language queries.

Examples & A2A 1.0 Sample Tour

The Examples/ directory contains a guided tour of every major surface of the A2A protocol 1.0, with each sample focused on one concept and documented inline. Pick the one that matches what you're trying to do:

Sample Focus Run
HelloAgent Bare-minimum send-and-print quickstart swift run HelloAgent
AgentInspector Discovery + every AgentCard field swift run AgentInspector
MultimodalMessenger Multi-part messages (text / file / URL / data) swift run MultimodalMessenger
StreamingNarrator All four StreamingEvent cases with chunked artifacts swift run StreamingNarrator
TaskLifecycleDemo Submit, poll, list, inspect, cancel swift run TaskLifecycleDemo
PushNotificationDemo Webhook CRUD on a task swift run PushNotificationDemo
AuthShowcase Every built-in auth provider (offline-safe) swift run AuthShowcase
TravelPlannerAgent Multi-agent orchestration with skill routing swift run TravelPlannerAgent
SmartTravelPlanner LLM-style intent routing on top of the orchestrator swift run SmartTravelPlanner

See Examples/README.md for an extended SDK quick reference, troubleshooting tips, and per-sample walk-throughs.

Error Handling

do {
    let response = try await client.sendMessage("Hello")
} catch let error as A2AError {
    switch error {
    case .taskNotFound(let taskId, _):
        print("Task not found: \(taskId)")
    case .authenticationRequired:
        print("Please provide credentials")
    case .networkError(let underlying):
        print("Network error: \(underlying?.localizedDescription ?? "unknown")")
    case .invalidResponse(let message):
        print("Invalid response: \(message)")
    default:
        print("Error: \(error.localizedDescription)")
    }
}

API Reference

A2AClient Methods

Method Description
sendMessage(_:) Send a message to the agent
sendStreamingMessage(_:) Send a message with streaming response
getTask(_:) Retrieve a task by ID
listTasks(_:) List tasks with optional filters
cancelTask(_:) Cancel a task
subscribeToTask(_:) Subscribe to task updates
createPushNotificationConfig(taskId:config:) Configure push notifications
getPushNotificationConfig(taskId:configId:) Get push notification config
listPushNotificationConfigs(taskId:) List push notification configs
deletePushNotificationConfig(taskId:configId:) Delete push notification config
getExtendedAgentCard() Get extended agent metadata

Static Methods

Method Description
A2AClient.discoverAgent(domain:) Discover an agent from a domain
A2AClient.fetchAgentCard(from:) Fetch an agent card from a URL

Thread Safety

All public types in A2AClient conform to Sendable and are safe to use across concurrency domains. The library uses Swift's modern concurrency features (async/await, actors) for thread-safe operation.

Requirements

  • Swift 6.0+
  • Xcode 16.0+

License

This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

Contributing

See CONTRIBUTING.md for contribution guidelines.

Security

For security concerns, please see our Security Policy.

See Also

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages