Skip to content

VDurocher/Swift-AI-Agent-Core

Repository files navigation

Swift AI Agent Core

Production-grade Swift package for integrating LLM agents into iOS, macOS, watchOS, and tvOS apps.

Swift 6.0 Platforms License SPM Compatible CI

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.


Features

  • 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 AIError enum covering all failure scenarios
  • Swift 6.0 concurrency — fully Sendable types, actor-based implementation
  • Zero dependencies — pure Swift, no external packages

Supported Models

OpenAI

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

Anthropic

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

Requirements

  • 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.


Installation

Swift Package Manager

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:

  1. File > Add Package Dependencies
  2. Enter: https://github.com/VDurocher/Swift-AI-Agent-Core
  3. Select version and add to your target

Quick Start

Basic Usage

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)

Streaming Responses

for try await chunk in agent.stream(message: "Write a haiku about coding") {
    print(chunk, terminator: "")
}

Multi-Turn Conversations

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)

Anthropic (Claude)

let claudeAgent = try AIAgentImplementation.claudeSonnet46(apiKey: "your-anthropic-api-key")
let response = try await claudeAgent.send(message: "Summarize the Swift concurrency model.")
print(response)

SwiftUI Integration

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
        }
    }
}

Local Persistence

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+.

Setup

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!")

SwiftUI History View

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)
        }
    }
}

Resume Previous Context

// 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)

Function Calling (Tool Use)

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)
    }
}

API Reference

AIAgent Protocol

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.

AIConfiguration

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)

Convenience Initializers

// 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 4

Retry Policies

RetryPolicy.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)

Token Management

// 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)

Error Handling

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
    }
}

Architecture

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.


Testing

swift test

Mocking

AIAgent 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"))
    }
}

Examples

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

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/your-feature)
  3. Commit your changes (git commit -m 'feat: add your feature')
  4. Push to the branch (git push origin feature/your-feature)
  5. Open a Pull Request
git clone https://github.com/VDurocher/Swift-AI-Agent-Core.git
cd Swift-AI-Agent-Core
swift build
swift test

License

PolyForm Noncommercial 1.0 — see LICENSE. Commercial use requires a separate license (contact durochervictor@gmail.com).


@VDurocher

About

Professional Swift package for integrating LLM agents into iOS apps with production-grade reliability. Features streaming support, retry mechanisms, and token management.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages