SwiftAgent
Native Swift SDK for building autonomous AI agents with Apple's FoundationModels design philosophy
Install / Use
/learn @SwiftedMind/SwiftAgentREADME
SwiftAgent
Native Swift SDK for building autonomous AI agents with Apple's FoundationModels design philosophy
SwiftAgent simplifies AI agent development by providing a clean, intuitive API that handles all the complexity of agent loops, tool execution, and adapter communication. Inspired by Apple's FoundationModels framework, it brings the same elegant, declarative approach to cross-platform AI agent development.
SwiftAgent in Action
import OpenAISession
@SessionSchema
struct CityExplorerSchema {
@Tool var cityFacts = CityFactsTool()
@Tool var reservation = ReservationTool()
@Grounding(Date.self) var travelDate
@Grounding([String].self) var mustVisitIdeas
@StructuredOutput(ItinerarySummary.self) var itinerary
}
@MainActor
func planCopenhagenWeekend() async throws {
let schema = CityExplorerSchema()
let session = OpenAISession(
schema: schema,
instructions: "Design cinematic weekends. Call tools for local intel and reservations.",
apiKey: "sk-..."
)
let response = try await session.respond(
to: "Coffee, design, and dinner plans for two days in Copenhagen.",
groundingWith: [
.travelDate(Date(timeIntervalSinceNow: 86_400)),
.mustVisitIdeas([
"Coffee Collective, Nørrebro",
"Designmuseum Denmark",
"Kødbyens Fiskebar"
])
],
generating: \.itinerary
)
print(response.content.headline)
print(response.content.mustTry.joined(separator: " → "))
for entry in try schema.resolve(session.transcript) {
if case let .toolRun(.cityFacts(run)) = entry, let output = run.output {
print("Local picks:", output)
}
if case let .prompt(prompt) = entry {
print("Groundings:", prompt.sources)
}
}
}
Table of Contents
- Features
- Quick Start
- Session Schema
- Streaming Responses
- Streaming State Helpers
- Proxy Servers
- Simulated Session
- Logging
- Recording HTTP Fixtures
- Development Status
- Example App
- License
- Acknowledgments
Features
- Zero-Setup Agent Loops — Handle autonomous agent execution with just a few lines of code
- Native Tool Integration — Use
@Generablestructs from FoundationModels as agent tools seamlessly - Adapter Agnostic — Abstract interface supports multiple AI adapters (OpenAI + Anthropic included, more coming)
- Apple-Native Design — API inspired by FoundationModels for familiar, intuitive development
- Modern Swift — Built with Swift 6, async/await, and latest concurrency features
- Rich Logging — Comprehensive, human-readable logging for debugging and monitoring
- Flexible Configuration — Fine-tune generation options, tools, and adapter settings
Quick Start
Installation
Add SwiftAgent to your Swift project:
// Package.swift
dependencies: [
.package(url: "https://github.com/SwiftedMind/SwiftAgent.git", branch: "main")
]
// OpenAI target
.product(name: "OpenAISession", package: "SwiftAgent")
// Anthropic target
.product(name: "AnthropicSession", package: "SwiftAgent")
Then import the target you need:
// For OpenAI
import OpenAISession
// For Anthropic
import AnthropicSession
Basic Usage
Create an OpenAISession with your default instructions and call respond whenever you need a single-turn answer. The session tracks conversation state for you, so you can start simple and layer on additional features later.
import OpenAISession
let session = OpenAISession(
instructions: "You are a helpful assistant.",
apiKey: "sk-...",
)
// Create a response
let response = try await session.respond(to: "What's the weather like in San Francisco?")
// Process response
print(response.content)
Or use Anthropic:
import AnthropicSession
let session = AnthropicSession(
instructions: "You are a helpful assistant.",
apiKey: "sk-ant-...",
)
let response = try await session.respond(to: "What's the weather like in San Francisco?")
print(response.content)
[!NOTE] Using an API key directly is great for prototyping, but do not ship it in production apps. For shipping apps, use a secure proxy with per‑turn tokens. See Proxy Servers for more information.
Building Tools
Create tools using Apple's @Generable macro for type-safe, schema-free tool definitions. Tools expose argument and output types that SwiftAgent validates for you, so the model can call into Swift code and receive strongly typed results without manual JSON parsing.
import FoundationModels
import OpenAISession
struct WeatherTool: Tool {
let name = "get_weather"
let description = "Get current weather for a location"
@Generable
struct Arguments {
@Guide(description: "City name")
let city: String
@Guide(description: "Temperature unit")
let unit: String
}
@Generable
struct Output {
let temperature: Double
let condition: String
let humidity: Int
}
func call(arguments: Arguments) async throws -> Output {
return Output(
temperature: 22.5,
condition: "sunny",
humidity: 65
)
}
}
let session = OpenAISession(
tools: WeatherTool(),
instructions: "You are a helpful assistant.",
apiKey: "sk-...",
)
let response = try await session.respond(to: "What's the weather like in San Francisco?")
print(response.content)
[!NOTE] Unlike Apple's
LanguageModelSessionobject,OpenAISessiontakes thetoolsparameter as variadic arguments. So instead of passing an array liketools: [WeatherTool(), OtherTool()], you pass the tools as a list of argumentstools: WeatherTool(), OtherTool().
Recoverable Tool Rejections
If a tool call fails in a way the agent can correct (such as an unknown identifier or other validation issue), throw a ToolRunRejection. SwiftAgent forwards the structured content you provide to the model without aborting the loop so the agent can adjust its next action.
SwiftAgent always wraps your payload in a standardized envelope that includes error: true and the reason string so the agent can reliably detect recoverable rejections.
For quick cases, attach string-keyed details with the convenience initializer:
struct CustomerLookupTool: Tool {
func call(arguments: Arguments) async throws -> Output {
guard let customer = try await directory.loadCustomer(id: arguments.customerId) else {
throw ToolRunRejection(
reason: "Customer not found",
details: [
"issue": "customerNotFound",
"customerId": arguments.customerId
]
)
}
return Output(summary: customer.summary)
}
}
For richer payloads, pass any @Generable type via the content: initializer to return structured data:
@Generable
struct CustomerLookupRejectionDetails {
var issue: String
var customerId: String
var suggestions: [String]
}
throw ToolRunRejection(
reason: "Customer not found",
content: CustomerLookupRejectionDetails(
issue: "customerNotFound",
customerId: arguments.customerId,
suggestions: ["Ask the user to confirm the identifier"]
)
)
Structured Responses
You can force the response to be structured by defining a type conforming to StructuredOutput and passing it to the session.respond method:
import FoundationModels
import OpenAISession
struct WeatherReport: StructuredOutput {
static let name: String = "weatherReport"
@Generable
struct Schema {
let temperature: Double
let condition: String
let humidity: Int
}
}
let session = OpenAISession(
tools: WeatherTool(),
instructions: "You are a helpful assistant.",
apiKey: "sk-...",
)
let response = try await session.respond(
to: "What's the weather like in San Francisco?",
generating: WeatherReport.self,
)
// Fully typed response content
print(response.content.temperature)
print(response.content.condition)
print(response.content.humidity)
The response body is now a fully typed WeatherReport. SwiftAgent validates the payload against your schema, so you can use the data immediately in UI or unit tests without defensive decoding.
Access Transcripts
Every OpenAISession maintains a running transcript that records prompts, reasoning steps, tool calls, and responses. Iterate over it to drive custom analytics, persistence, or UI updates:
import OpenAISession
let session = OpenAISession(
instructions: "You are a helpful assistant.",
apiKey: "sk-...",
)
for entry in session.transcript {
switch entry {
case let .prompt(prompt):
print("Prompt: ", prompt)
case let .reasoning(reasoning):
print("Reasoning: ", reasoning)
case let .toolCalls(toolCalls):
print("Tool Calls: ", toolCalls)
case let .toolOutput(toolOutpu
