A Swift client library for the Agent-to-Agent (A2A) Protocol, enabling seamless communication between applications and AI agents.
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.
- iOS 15.0+
- macOS 12.0+
- watchOS 8.0+
- tvOS 15.0+
- A2A Protocol Version: 1.0
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"]
)
]- Go to File > Add Package Dependencies
- Enter the repository URL:
https://github.com/tolgaki/a2a-swift-client - Select the version and add the package
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)")
}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!")// 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!")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 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"
}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")")
}
}
}| 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 |
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 ?? "")")
}
}A2AClient supports multiple authentication methods:
let config = A2AClientConfiguration(baseURL: url)
.withAPIKey("your-api-key", name: "X-API-Key", location: .header)
let client = A2AClient(configuration: config)let config = A2AClientConfiguration(baseURL: url)
.withBearerToken("your-jwt-token")
let client = A2AClient(configuration: config)let config = A2AClientConfiguration(baseURL: url)
.withBasicAuth(username: "user", password: "password")
let client = A2AClient(configuration: config)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)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
}
}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)let config = A2AClientConfiguration(
baseURL: URL(string: "https://agent.example.com")!,
transportBinding: .httpREST, // or .jsonRPC
protocolVersion: "1.0",
timeoutInterval: 60
)
let client = A2AClient(configuration: config)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)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)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)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)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.
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.
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)")
}
}| 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 |
| Method | Description |
|---|---|
A2AClient.discoverAgent(domain:) |
Discover an agent from a domain |
A2AClient.fetchAgentCard(from:) |
Fetch an agent card from a URL |
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.
- Swift 6.0+
- Xcode 16.0+
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
See CONTRIBUTING.md for contribution guidelines.
For security concerns, please see our Security Policy.
- DESIGN.md - Architecture and design documentation
- CHANGELOG.md - Version history
- A2A Protocol Specification - Official protocol specification