Production-grade Swift package for integrating LLM agents into iOS, macOS, watchOS, and tvOS apps.
SwiftAIAgentCore integrates OpenAI and Anthropic language models into Apple platform apps. Built with Swift 6.0 strict concurrency, clean architecture, and local persistence via SwiftData.
- Streaming responses — real-time chunks via
AsyncThrowingStream - Automatic retry — exponential backoff with configurable policies
- Token management — estimate and validate token usage before sending requests
- Function calling — tool use API compatible with both OpenAI and Anthropic formats
- Local persistence — conversation history via SwiftData (iOS 17+ / macOS 14+)
- Typed errors — exhaustive
AIErrorenum covering all failure scenarios - Swift 6.0 concurrency — fully
Sendabletypes,actor-based implementation - Zero dependencies — pure Swift, no external packages
| Model | Constant | Context |
|---|---|---|
| GPT-4 | .gpt4 |
8k |
| GPT-4 Turbo | .gpt4Turbo |
128k |
| GPT-3.5 Turbo | .gpt35Turbo |
16k |
| GPT-4o | .gpt4o |
128k |
| GPT-4o Mini | .gpt4oMini |
128k |
| GPT-4.1 | .gpt41 |
1M |
| GPT-4.1 Mini | .gpt41Mini |
1M |
| Model | Constant | Context |
|---|---|---|
| Claude 3 Haiku | .claude3Haiku |
200k |
| Claude 3 Sonnet | .claude3Sonnet |
200k |
| Claude 3 Opus | .claude3Opus |
200k |
| Claude 3.5 Haiku | .claude35Haiku |
200k |
| Claude 3.5 Sonnet | .claude35Sonnet |
200k |
| Claude Haiku 4.5 | .claudeHaiku45 |
200k |
| Claude Sonnet 4.6 | .claudeSonnet46 |
200k |
| Claude 3.7 Sonnet | .claude37Sonnet |
200k |
| Claude Opus 4.6 | .claudeOpus46 |
200k |
- iOS 16.0+ / macOS 13.0+ / watchOS 9.0+ / tvOS 16.0+
- Swift 6.0+
- Xcode 16.0+
The persistence layer (
HistoryManager,HistoryView) requires iOS 17.0+ / macOS 14.0+ / watchOS 10.0+ / tvOS 17.0+. All other functionality works from the base deployment targets.
Add SwiftAIAgentCore to your Package.swift:
dependencies: [
.package(url: "https://github.com/VDurocher/Swift-AI-Agent-Core.git", from: "1.0.0")
]Or add it via Xcode:
- File > Add Package Dependencies
- Enter:
https://github.com/VDurocher/Swift-AI-Agent-Core - Select version and add to your target
import SwiftAIAgentCore
// Create an agent with a convenience initializer
let agent = try AIAgentImplementation.gpt4o(apiKey: "your-openai-api-key")
// Send a single message — returns the response as a plain String
let response = try await agent.send(message: "Explain Swift concurrency in one sentence.")
print(response)for try await chunk in agent.stream(message: "Write a haiku about coding") {
print(chunk, terminator: "")
}let conversation: [AIMessage] = [
.system("You are a Swift expert."),
.user("What is a protocol?"),
]
// Returns an AIMessage with role .assistant
let response = try await agent.send(messages: conversation)
print(response.content)let claudeAgent = try AIAgentImplementation.claudeSonnet46(apiKey: "your-anthropic-api-key")
let response = try await claudeAgent.send(message: "Summarize the Swift concurrency model.")
print(response)AIMessage conforms to Identifiable, so it works directly with ForEach:
import SwiftUI
import SwiftAIAgentCore
struct ChatView: View {
@State private var messages: [AIMessage] = []
@State private var input = ""
let agent: any AIAgent
var body: some View {
VStack {
ScrollView {
ForEach(messages) { message in
HStack {
if message.role == .user { Spacer() }
Text(message.content)
.padding()
.background(message.role == .user ? Color.blue : Color.gray)
.cornerRadius(10)
if message.role == .assistant { Spacer() }
}
}
}
HStack {
TextField("Message...", text: $input)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Send") {
Task { await sendMessage() }
}
}
.padding()
}
}
func sendMessage() async {
guard !input.isEmpty else { return }
let userMessage = AIMessage.user(input)
messages.append(userMessage)
input = ""
do {
let response = try await agent.send(messages: messages)
messages.append(response)
} catch {
// Handle AIError cases
}
}
}The persistence layer is powered by SwiftData and stores all data on-device. It requires iOS 17.0+ / macOS 14.0+ / watchOS 10.0+ / tvOS 17.0+.
import SwiftUI
import SwiftData
import SwiftAIAgentCore
// 1. Create the ModelContainer once at app startup
let schema = Schema([ConversationRecord.self, MessageRecord.self])
let container = try ModelContainer(for: schema)
// 2. Create the HistoryManager
let historyManager = HistoryManager(modelContainer: container)
// 3. Initialize the agent with history persistence
let agent = try AIAgentImplementation(
configuration: AIConfiguration(model: .gpt4Turbo, apiKey: "your-api-key"),
historyManager: historyManager
)
// Every call to send() now auto-saves the conversation locally
let response = try await agent.send(message: "Hello!")The package provides a ready-to-use HistoryView that lists conversations with delete support:
@main
struct MyApp: App {
let container = try! ModelContainer(for: ConversationRecord.self, MessageRecord.self)
var body: some Scene {
WindowGroup {
HistoryView()
.modelContainer(container)
}
}
}// Load the last 20 messages from the most recent conversation
let previousMessages = try await agent.loadPreviousContext(limit: 20)
// Append new user input and continue
let messages = previousMessages + [.user("Continue from where we left off")]
let response = try await agent.send(messages: messages)SwiftAIAgentCore supports the tool use API for both OpenAI and Anthropic. Define tools with a JSON Schema description, send them alongside messages, and handle the model's tool calls in your app:
// 1. Define a tool
let weatherTool = AITool(
name: "get_weather",
description: "Returns the current weather for a city",
parameters: AIToolParameters(
properties: [
"city": AIToolProperty(type: "string", description: "City name"),
"unit": AIToolProperty(
type: "string",
description: "Temperature unit",
enumValues: ["celsius", "fahrenheit"]
)
],
required: ["city"]
)
)
// 2. Send with tools — the model may request a tool call
let result = try await agent.send(messages: conversation, tools: [weatherTool])
// 3. Check if the model wants to call a tool
if result.requiresToolExecution {
for toolCall in result.toolCalls {
let args = toolCall.decodeArguments() // [String: Any]?
// Execute the tool in your app, then send the result back
let toolResult = AIToolResult(toolCallId: toolCall.id, content: "22°C, sunny")
let finalResponse = try await agent.send(messages: conversation, toolResults: [toolResult])
print(finalResponse.message.content)
}
}public protocol AIAgent: Sendable {
var configuration: AIConfiguration { get }
func send(message: String) async throws -> String
func send(messages: [AIMessage]) async throws -> AIMessage
func stream(message: String) -> AsyncThrowingStream<String, Error>
func stream(messages: [AIMessage]) -> AsyncThrowingStream<String, Error>
func estimateTokens(for messages: [AIMessage]) -> Int
func send(messages: [AIMessage], tools: [AITool]) async throws -> AIMessageWithTools
func send(messages: [AIMessage], toolResults: [AIToolResult]) async throws -> AIMessageWithTools
}Default implementations are provided for send(message:), stream(message:), estimateTokens(for:), and both tool-related methods.
let config = AIConfiguration(
model: .claudeSonnet46,
apiKey: "your-api-key",
temperature: 0.7, // 0.0–2.0
maxResponseTokens: 2000,
timeout: 30,
retryPolicy: .default
)
let agent = try AIAgentImplementation(configuration: config)// OpenAI
AIAgentImplementation.gpt4(apiKey:) // GPT-4, 8k context
AIAgentImplementation.gpt4Turbo(apiKey:) // GPT-4 Turbo, 128k context
AIAgentImplementation.gpt35Turbo(apiKey:) // GPT-3.5 Turbo, 16k context
AIAgentImplementation.gpt4o(apiKey:) // GPT-4o, 128k context
AIAgentImplementation.gpt4oMini(apiKey:) // GPT-4o Mini, 128k context
// Anthropic — Claude 3
AIAgentImplementation.claude3Haiku(apiKey:) // fastest Claude 3
AIAgentImplementation.claude3Sonnet(apiKey:)
AIAgentImplementation.claude3Opus(apiKey:) // most capable Claude 3
// Anthropic — Claude 3.5
AIAgentImplementation.claude35Haiku(apiKey:) // fast and cost-efficient
AIAgentImplementation.claude35Sonnet(apiKey:) // high performance
// Anthropic — Claude 4
AIAgentImplementation.claudeHaiku45(apiKey:) // fastest Claude 4
AIAgentImplementation.claudeSonnet46(apiKey:)
AIAgentImplementation.claudeOpus46(apiKey:) // most capable Claude 4RetryPolicy.default // 3 retries, exponential backoff (1s → 60s)
RetryPolicy.none // No retries
RetryPolicy.aggressive // 5 retries, shorter delays (0.5s → 30s)
// Custom
RetryPolicy(maxRetries: 3, initialDelay: 1.0, maxDelay: 60.0, multiplier: 2.0)// Estimate token count for a message array
let tokens = TokenEstimator.estimate(messages: messages)
// Validate against model limits (throws AIError.tokenLimitExceeded if over limit)
try TokenEstimator.validate(messages: messages, model: .gpt4, maxResponseTokens: 1000)
// Truncate to fit within a limit, preserving system messages
let truncated = TokenEstimator.truncate(messages: messages, limit: 2000, keepSystemMessages: true)do {
let response = try await agent.send(message: "Hello")
} catch let error as AIError {
switch error {
case .invalidAPIKey:
// Invalid or missing API key
case .rateLimit(let retryAfter):
// Rate limited — retryAfter is TimeInterval? (seconds to wait)
case .tokenLimitExceeded(let current, let max):
// current and max are Int (token counts)
case .networkError(let underlying):
// Underlying URLSession or transport error
case .timeout:
// Request exceeded the configured timeout
case .invalidResponse(let statusCode, let message):
// Non-2xx HTTP response
case .streamingError(let reason):
// Error during streaming (including model not supporting streaming)
case .cancelled:
// Task was cancelled
case .decodingError, .invalidContext, .unknown:
break
}
if error.isRecoverable {
// rateLimit, networkError, and timeout are recoverable
// Retry is handled automatically by the configured RetryPolicy
}
}Sources/SwiftAIAgentCore/
├── Core/
│ ├── AIAgentProtocol.swift — AIAgent protocol
│ ├── AIMessage.swift — Message model (user / assistant / system)
│ ├── AIRole.swift — Role enumeration
│ ├── AIModel.swift — Model configurations (GPT-4, Claude, etc.)
│ ├── AIConfiguration.swift — Agent configuration and retry policies
│ ├── AIError.swift — Typed error enum
│ ├── AITool.swift — Tool (function) definition for tool use
│ └── AIToolCall.swift — Tool call / result / response types
│
├── Network/
│ ├── NetworkClient.swift — Base HTTP client with retry logic
│ ├── OpenAIClient.swift — OpenAI Chat Completions API
│ ├── AnthropicClient.swift — Anthropic Messages API
│ └── AIAgentImplementation.swift — Concrete actor conforming to AIAgent
│
├── Persistence/ — iOS 17+ / macOS 14+ only
│ ├── ConversationRecord.swift — SwiftData model for conversations
│ ├── MessageRecord.swift — SwiftData model for messages
│ └── HistoryManager.swift — @ModelActor — thread-safe history operations
│
├── UI/ — iOS 17+ / macOS 14+ only
│ ├── HistoryView.swift — Conversation list with delete support
│ └── ConversationDetailView.swift — Message-level view
│
└── Utils/
└── TokenEstimator.swift — Token counting and truncation
AIAgentImplementation is declared as actor, guaranteeing thread-safe access to its internal clients. All calls to its methods require await.
swift testAIAgent is a protocol — implement it to create test doubles. Because AIAgentImplementation is an actor, mocks must be Sendable. The protocol includes two tool-use methods; provide at least stub implementations:
struct MockAIAgent: AIAgent {
let configuration: AIConfiguration
func send(messages: [AIMessage]) async throws -> AIMessage {
.assistant("Mocked response")
}
func stream(messages: [AIMessage]) -> AsyncThrowingStream<String, Error> {
AsyncThrowingStream { continuation in
continuation.yield("Mocked stream")
continuation.finish()
}
}
func send(messages: [AIMessage], tools: [AITool]) async throws -> AIMessageWithTools {
AIMessageWithTools(message: .assistant("Mocked tool response"))
}
func send(messages: [AIMessage], toolResults: [AIToolResult]) async throws -> AIMessageWithTools {
AIMessageWithTools(message: .assistant("Mocked tool result response"))
}
}The Examples directory contains:
- BasicExample.swift — single message, streaming, and multi-turn conversation
- AdvancedExample.swift — token management, retry handling, production patterns
- ChatApp/ — a complete SwiftUI macOS chat application using the package
Run the basic example:
cd Examples
export OPENAI_API_KEY="your-key"
swift BasicExample.swift- Fork the repository
- Create your feature branch (
git checkout -b feature/your-feature) - Commit your changes (
git commit -m 'feat: add your feature') - Push to the branch (
git push origin feature/your-feature) - Open a Pull Request
git clone https://github.com/VDurocher/Swift-AI-Agent-Core.git
cd Swift-AI-Agent-Core
swift build
swift testPolyForm Noncommercial 1.0 — see LICENSE. Commercial use requires a separate license (contact durochervictor@gmail.com).