SwiftAgent is a powerful Swift framework for building AI agents using a declarative SwiftUI-like syntax. It provides a type-safe, composable way to create complex agent workflows while maintaining Swift's expressiveness.
graph TB
subgraph "Core Protocols"
Step["Step<Input, Output>"]
Agent["Agent: Step"]
Model["Model: Step"]
Step --> Agent
Step --> Model
end
subgraph "OpenFoundationModels Integration"
Tool["Tool Protocol"]
LMS["LanguageModelSession"]
Generable["@Generable"]
Prompt["@PromptBuilder"]
Instructions["@InstructionsBuilder"]
end
subgraph "Built-in Steps"
subgraph "Transform Steps"
Transform["Transform"]
Map["Map"]
Reduce["Reduce"]
Join["Join"]
end
subgraph "Control Flow"
Loop["Loop"]
Parallel["Parallel"]
Race["Race"]
WaitForInput["WaitForInput"]
end
subgraph "AI Generation"
Generate["Generate<T>"]
GenerateText["GenerateText"]
end
end
subgraph "State Management"
Memory["@Memory"]
Relay["Relay<T>"]
Session["@Session"]
end
subgraph "Safety & Monitoring"
Guardrails["Guardrails"]
Monitor["Monitor"]
Tracing["Distributed Tracing"]
end
subgraph "Tools"
ReadTool["ReadTool"]
WriteTool["WriteTool"]
EditTool["EditTool"]
MultiEditTool["MultiEditTool"]
GrepTool["GrepTool"]
GlobTool["GlobTool"]
GitTool["GitTool"]
ExecuteCommandTool["ExecuteCommandTool"]
URLFetchTool["URLFetchTool"]
end
Step --> Transform
Step --> Map
Step --> Loop
Step --> Parallel
Step --> Generate
Step --> GenerateText
Agent --> Guardrails
Step --> Monitor
Step --> Tracing
Generate --> LMS
GenerateText --> LMS
Session --> LMS
Tool --> ReadTool
Tool --> WriteTool
Tool --> EditTool
Tool --> ExecuteCommandTool
- 🎯 Declarative Syntax: Build agents using familiar SwiftUI-like syntax
- 🔄 Composable Steps: Chain multiple steps together seamlessly with StepBuilder
- 🛠️ Type-Safe Tools: Define and use tools with compile-time type checking
- 🤖 Model-Agnostic: Works with any AI model through OpenFoundationModels
- 📦 Modular Design: Create reusable agent components
- 🔄 Async/Await Support: Built for modern Swift concurrency
- 🎭 Protocol-Based: Flexible and extensible architecture
- 📊 State Management: Memory and Relay for state handling
- 🎨 @Session: Elegant session management with property wrapper
- 🏗️ Builder APIs: Dynamic Instructions and Prompt construction with result builders
- 🔍 Monitoring: Built-in monitoring and distributed tracing support
- 📡 OpenTelemetry: Industry-standard distributed tracing with swift-distributed-tracing
- 🛡️ Guardrails: Safety checks for input/output validation
- ⚙️ Chain Support: Chain up to 8 steps with type-safe composition
Steps are the fundamental building blocks in SwiftAgent. They process input and produce output in a type-safe manner:
public protocol Step<Input, Output> {
associatedtype Input: Sendable
associatedtype Output: Sendable
func run(_ input: Input) async throws -> Output
}
Agents are high-level abstractions that combine steps to create complex workflows:
public protocol Agent: Step {
associatedtype Body: Step
@StepBuilder var body: Self.Body { get }
var maxTurns: Int { get }
var guardrails: [any Guardrail] { get }
}
SwiftAgent provides elegant session management through the @Session
property wrapper, enabling reusable and shared language model sessions across your agents.
The @Session
wrapper simplifies session creation and management:
struct MyAgent {
// With explicit initialization
@Session
var session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey),
instructions: Instructions("You are a helpful assistant")
)
// With custom configuration
@Session
var customSession = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey)
) {
Instructions("You are an expert")
}
}
Pass sessions to Generate steps using the $
prefix for Relay access:
struct ContentAgent {
@Session
var session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey)
) {
Instructions("You are a content creator")
}
var body: some Step {
GenerateText(session: $session) { input in
Prompt {
"Create content about: \(input)"
if detailed {
"Include comprehensive details"
}
}
}
}
}
SwiftAgent uses OpenFoundationModels for AI model integration, providing a unified interface for multiple AI providers:
// Build instructions dynamically with conditions and loops
let session = LanguageModelSession {
"You are an AI assistant"
if userPreferences.verbose {
"Provide detailed explanations"
}
for expertise in userExpertiseAreas {
"You have expertise in \(expertise)"
}
"Always be helpful and accurate"
}
// Build prompts dynamically
GenerateText(session: $session) { input in
Prompt {
"User request: \(input)"
if includeContext {
"Context: \(contextInfo)"
}
for example in relevantExamples {
"Example: \(example)"
}
"Please provide a comprehensive response"
}
}
SwiftAgent works with any AI provider through OpenFoundationModels. Configure your preferred provider:
import SwiftAgent
import OpenFoundationModels
// Import your chosen provider package
// e.g., OpenFoundationModelsOpenAI, OpenFoundationModelsOllama, etc.
// Create a session with your chosen model
@Session
var session = LanguageModelSession(
model: YourModelFactory.create(apiKey: "your-api-key")
) {
Instructions("You are a helpful assistant.")
}
SwiftAgent is provider-agnostic. Choose from available OpenFoundationModels provider packages:
- OpenFoundationModels-OpenAI - OpenAI models (GPT-4o, etc.)
- OpenFoundationModels-Ollama - Local models via Ollama
- OpenFoundationModels-Anthropic - Anthropic Claude models
- Additional providers available through the OpenFoundationModels ecosystem
Convert data from one type to another:
Transform<String, Int> { input in
Int(input) ?? 0
}
Generate structured output using AI models with Builder APIs:
@Generable
struct Story {
@Guide(description: "The story title")
let title: String
@Guide(description: "The story content")
let content: String
}
// Using @Session and PromptBuilder
struct StoryGenerator {
@Session
var session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey)
) {
Instructions("You are a creative writer")
}
var generator: some Step {
Generate<String, Story>(session: $session) { input in
Prompt {
"Write a story about: \(input)"
"Include vivid descriptions"
if includeDialogue {
"Add realistic dialogue"
}
}
}
}
}
Generate string output using AI models with dynamic builders:
// Using @Session with shared configuration
struct TextGenerator {
@Session
var session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey)
) {
Instructions("You are a creative writer")
}
func generate(_ topic: String) -> some Step {
GenerateText(session: $session) { input in
Prompt {
"Write about: \(topic)"
"Input: \(input)"
}
}
}
}
Iterate with a condition:
Loop(max: 5) { input in
ProcessingStep()
} until: { output in
output.meetsQualityCriteria
}
Process collections:
Map<[String], [Int]> { item, index in
Transform { str in
str.count
}
}
Execute steps concurrently:
Parallel<String, Int> {
CountWordsStep()
CountCharactersStep()
CountLinesStep()
}
SwiftAgent includes a comprehensive suite of tools for file operations, searching, command execution, and more. See the Tool Reference section below for detailed information about each tool.
// Reading files
let readTool = ReadTool()
let content = try await readTool.call(ReadInput(path: "config.json", startLine: 1, endLine: 50))
// Writing files
let writeTool = WriteTool()
let result = try await writeTool.call(WriteInput(path: "output.txt", content: "Hello, World!"))
// Editing files
let editTool = EditTool()
let edited = try await editTool.call(EditInput(
path: "main.swift",
oldString: "print(\"old\")",
newString: "print(\"new\")",
replaceAll: "true"
))
// Search with grep
let grepTool = GrepTool()
let matches = try await grepTool.call(GrepInput(
pattern: "TODO:",
filePattern: "*.swift",
basePath: "./src",
ignoreCase: "false",
contextBefore: 2,
contextAfter: 2
))
// Find files with glob
let globTool = GlobTool()
let files = try await globTool.call(GlobInput(
pattern: "**/*.md",
basePath: ".",
fileType: "file"
))
// Execute commands
let executeTool = ExecuteCommandTool()
let output = try await executeTool.call(ExecuteCommandInput(
command: "swift build",
timeout: 30
))
// Git operations
let gitTool = GitTool()
let status = try await gitTool.call(GitInput(
command: "status",
args: "--short"
))
// Fetch URLs
let urlTool = URLFetchTool()
let webpage = try await urlTool.call(URLInput(url: "https://api.example.com/data"))
Tool Name | Purpose | Main Features | Common Use Cases |
---|---|---|---|
ReadTool | Read file contents with line numbers | • Line number formatting (123→content) • Range selection (startLine, endLine) • UTF-8 text file support • Max file size: 1MB |
• View source code • Read configuration files • Inspect specific line ranges • Debug file contents |
WriteTool | Write content to files | • Create new files or overwrite existing • Automatic parent directory creation • Atomic write operations • UTF-8 encoding only |
• Save generated content • Create configuration files • Export data • Write logs or reports |
EditTool | Find and replace text in files | • Single or all occurrences replacement • Preview changes before applying • Atomic operations • Validates old/new strings |
• Fix bugs in code • Update configuration values • Refactor variable names • Correct typos |
MultiEditTool | Apply multiple edits in one transaction | • Batch find/replace operations • Transactional (all or nothing) • Order-preserving execution • JSON-based edit specification |
• Complex refactoring • Multiple related changes • Atomic updates • Code migrations |
GrepTool | Search file contents using regex | • Regular expression patterns • Case-insensitive option • Context lines (before/after) • Multi-file search |
• Find TODO comments • Locate function usage • Search error messages • Code analysis |
GlobTool | Find files using glob patterns | • Wildcard patterns (*, **, ?) • File type filtering • Recursive directory traversal • Sorted results |
• Find files by extension • List directory contents • Discover project structure • Batch file operations |
ExecuteCommandTool | Execute shell commands | • Configurable timeout • stdout/stderr capture • Working directory support • Process control |
• Build projects • Run tests • System operations • Script execution |
GitTool | Perform Git operations | • All git commands supported • Argument passing • Repository operations • Output capture |
• Version control • Commit changes • Branch management • Check status |
URLFetchTool | Fetch content from URLs | • HTTP/HTTPS support • Content type handling • Error handling • Response parsing |
• API calls • Web scraping • Data fetching • External integrations |
SwiftAgent allows you to use any Step as a Tool when it meets certain requirements. This enables seamless integration of your custom Steps with AI models, promoting code reuse and maintainability.
A Step can be used directly as a Tool when it:
- Conforms to both Step and Tool protocols
- Input conforms to
ConvertibleFromGeneratedContent
&Generable
- Output conforms to
PromptRepresentable
- Conforms to
Sendable
for thread safety - Defines
name
anddescription
properties for Tool identification
import SwiftAgent
import OpenFoundationModels
// Define a Step that also conforms to Tool
struct UppercaseStep: Step, Tool, Sendable {
// Required Tool properties
let name = "uppercase"
let description = "Converts text to uppercase"
// Step implementation
func run(_ input: String) async throws -> String {
input.uppercased()
}
}
// The Step can now be used as a Tool
let session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey),
tools: [UppercaseStep()] // Using Step as Tool
) {
Instructions("You are a text processing assistant")
}
import SwiftAgent
import OpenFoundationModels
// Define structured input using @Generable
@Generable
struct CalculationInput: Sendable {
@Guide(description: "First number for calculation")
let a: Double
@Guide(description: "Second number for calculation")
let b: Double
@Guide(description: "Operation: add, subtract, multiply, divide")
let operation: String
}
// Define a calculation Step that can be used as a Tool
struct CalculatorStep: Step, Tool, Sendable {
// Tool identification
let name = "calculator"
let description = "Performs basic arithmetic operations"
// Step implementation
func run(_ input: CalculationInput) async throws -> String {
let result: Double = switch input.operation {
case "add": input.a + input.b
case "subtract": input.a - input.b
case "multiply": input.a * input.b
case "divide": input.b != 0 ? input.a / input.b : 0
default: 0
}
return "Result: \(result)"
}
}
// Use in an AI session
let session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey),
tools: [CalculatorStep()]
) {
Instructions("You are a math assistant. Use the calculator tool for computations.")
}
The Tool+Step
extension automatically provides:
Arguments
type alias pointing to your Step's Input typeparameters
property derived from Input's GenerationSchemacall(arguments:)
method that delegates to the Step'srun()
method- Automatic name inference from the Step type (can be overridden)
This means you write your Step logic once and can use it both:
- As a Step in agent workflows for composable processing
- As a Tool for AI models to invoke during generation
- Keep Steps focused: Each Step should do one thing well
- Use @Generable for complex inputs: This ensures proper schema generation
- Provide clear descriptions: Help the AI understand when to use your tool
- Make outputs PromptRepresentable: Return strings or implement the protocol
- Consider thread safety: Ensure your Step is Sendable for concurrent use
All tools implement the OpenFoundationModels.Tool
protocol and can be used with AI models:
let session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey),
tools: [
// Built-in tools
ReadTool(),
WriteTool(),
EditTool(),
GrepTool(),
ExecuteCommandTool(),
// Your custom Steps as Tools
UppercaseStep(),
CalculatorStep()
]
) {
Instructions("You are a code assistant with file system access.")
}
All tools use @Generable
structs for type-safe input and provide structured output:
// Example: ReadTool
@Generable
struct ReadInput {
@Guide(description: "File path to read")
let path: String
@Guide(description: "Starting line number (1-based, 0 for beginning)")
let startLine: Int
@Guide(description: "Ending line number (0 for end of file)")
let endLine: Int
}
// Output includes metadata
struct ReadOutput {
let content: String // Formatted with line numbers
let totalLines: Int // Total lines in file
let linesRead: Int // Lines actually read
let path: String // Normalized path
let startLine: Int // Actual start line
let endLine: Int // Actual end line
}
import SwiftAgent
import OpenFoundationModels
public struct Writer: Agent {
public typealias Input = String
public typealias Output = String
@Session
var session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey)
) {
Instructions("You are a creative writer")
}
public init() {}
public var body: some Step<Input, Output> {
GenerateText(session: $session) { input in
Prompt {
"Request: \(input)"
"Create an engaging narrative"
}
}
}
}
// Usage
let writer = Writer()
let story = try await writer.run("Write a story about a time-traveling scientist")
import SwiftAgent
import OpenFoundationModels
import AgentTools
struct CodeAnalyzer: Agent {
typealias Input = String
typealias Output = AnalysisResult
@Session
var session: LanguageModelSession
init(model: any LanguageModel) {
self._session = Session(wrappedValue: LanguageModelSession(
model: model,
tools: [ReadTool(), GrepTool(), GitTool()]
) {
Instructions {
"You are a code analysis expert"
"Analyze the codebase and provide insights"
"Focus on code quality and best practices"
}
})
}
var body: some Step<Input, Output> {
Generate<String, AnalysisResult>(session: $session) { request in
Prompt {
"Analyze the following: \(request)"
"Use available tools to examine the code"
"Provide actionable recommendations"
}
}
}
}
@Generable
struct AnalysisResult {
@Guide(description: "Summary of findings")
let summary: String
@Guide(description: "List of issues found")
let issues: String // Space-separated list
@Guide(description: "Recommendations")
let recommendations: String
}
struct ResearchAgent: Agent {
typealias Input = String
typealias Output = ResearchReport
@Session
var session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey)
) {
Instructions("You are a research expert")
}
var body: some Step<Input, Output> {
// Step 1: Generate search queries
Transform<String, SearchQueries> { topic in
SearchQueries(topic: topic)
}
// Step 2: Search in parallel
Map<SearchQueries, [SearchResult]> { query, _ in
URLFetchTool().call(URLInput(url: query.url))
.map { SearchResult(content: $0) }
}
// Step 3: Analyze results
Generate<[SearchResult], ResearchReport>(session: $session) { results in
Prompt {
"Synthesize these search results:"
for result in results {
"- \(result.content)"
}
"Create a comprehensive report with key findings"
}
}
}
var guardrails: [any Guardrail] {
[ContentSafetyGuardrail(), TokenLimitGuardrail(maxTokens: 4000)]
}
}
struct ChatAgent: Agent {
typealias Input = String
typealias Output = String
@Memory var conversationHistory: [String] = []
@Session
var session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: apiKey)
) {
Instructions("You are a helpful conversational assistant")
}
var body: some Step<Input, Output> {
Transform<String, String> { input in
// Add to conversation history
conversationHistory.append("User: \(input)")
return input
}
GenerateText(session: $session) { input in
Prompt {
"Conversation history:"
for message in conversationHistory.suffix(10) {
message
}
""
"Current message: \(input)"
"Respond naturally and helpfully"
}
}
Transform<String, String> { response in
// Save assistant response
conversationHistory.append("Assistant: \(response)")
return response
}
}
}
- Swift 6.0+
- iOS 18.0+ / macOS 15.0+
- Xcode 15.0+
dependencies: [
.package(url: "https://github.com/1amageek/SwiftAgent.git", branch: "main")
]
SwiftAgent requires a model provider package from the OpenFoundationModels ecosystem. Choose based on your needs:
// For OpenAI models
.package(url: "https://github.com/1amageek/OpenFoundationModels-OpenAI.git", branch: "main")
// For local models via Ollama
.package(url: "https://github.com/1amageek/OpenFoundationModels-Ollama.git", branch: "main")
// For Anthropic Claude models
.package(url: "https://github.com/1amageek/OpenFoundationModels-Anthropic.git", branch: "main")
// Additional providers available - check OpenFoundationModels documentation
// Complete Package.swift example
import PackageDescription
let package = Package(
name: "MyAgentProject",
platforms: [.iOS(.v18), .macOS(.v15)],
products: [
.executable(name: "MyAgent", targets: ["MyAgent"])
],
dependencies: [
.package(url: "https://github.com/1amageek/SwiftAgent.git", branch: "main"),
// Choose your AI provider - example with OpenAI
.package(url: "https://github.com/1amageek/OpenFoundationModels-OpenAI.git", branch: "main")
],
targets: [
.executableTarget(
name: "MyAgent",
dependencies: [
.product(name: "SwiftAgent", package: "SwiftAgent"),
.product(name: "AgentTools", package: "SwiftAgent"), // Optional: for built-in tools
.product(name: "OpenFoundationModelsOpenAI", package: "OpenFoundationModels-OpenAI")
]
)
]
)
import PackageDescription
let package = Package(
name: "MyAgentApp",
platforms: [.iOS(.v18), .macOS(.v15)],
dependencies: [
// Core SwiftAgent framework
.package(url: "https://github.com/1amageek/SwiftAgent.git", branch: "main"),
// Add your chosen AI provider package
// Example: OpenAI
.package(url: "https://github.com/1amageek/OpenFoundationModels-OpenAI.git", branch: "main")
],
targets: [
.target(
name: "MyAgentApp",
dependencies: [
.product(name: "SwiftAgent", package: "SwiftAgent"),
.product(name: "AgentTools", package: "SwiftAgent"), // Optional: for built-in tools
.product(name: "OpenFoundationModelsOpenAI", package: "OpenFoundationModels-OpenAI")
]
)
]
)
Configure your chosen AI provider according to their documentation. For example:
# For OpenAI
export OPENAI_API_KEY="your-api-key"
# For Anthropic
export ANTHROPIC_API_KEY="your-api-key"
# For local models (Ollama)
# Install and configure Ollama according to their documentation
import SwiftAgent
import OpenFoundationModels
import OpenFoundationModelsOpenAI // Your chosen provider
struct MyAgent: Agent {
typealias Input = String
typealias Output = String
@Session
var session = LanguageModelSession(
model: OpenAIModelFactory.gpt4o(apiKey: ProcessInfo.processInfo.environment["OPENAI_API_KEY"] ?? "")
) {
Instructions {
"You are a helpful assistant"
"Be concise and informative"
}
}
var body: some Step<Input, Output> {
GenerateText(session: $session) { input in
Prompt {
"User request: \(input)"
"Provide a helpful response"
}
}
}
}
@main
struct MyApp {
static func main() async throws {
let agent = MyAgent()
let result = try await agent.run("Hello, world!")
print(result)
}
}
Add safety checks to your agents:
struct ContentGuardrail: Guardrail {
func validate(_ content: String) throws {
if content.contains("inappropriate") {
throw GuardrailError.contentViolation
}
}
}
struct MyAgent: Agent {
var guardrails: [any Guardrail] {
[ContentGuardrail()]
}
var body: some Step<String, String> {
// Agent implementation
}
}
SwiftAgent provides two complementary ways to observe step execution:
Use Monitor
for simple debugging and logging during development:
GenerateText { input in
"Process: \(input)"
}
.onInput { input in
print("Starting with: \(input)")
}
.onOutput { output in
print("Generated: \(output)")
}
.onError { error in
print("Failed: \(error)")
}
.onComplete { duration in
print("Took \(duration) seconds")
}
Use trace()
for production-grade distributed tracing with OpenTelemetry support:
GenerateText { input in
"Process: \(input)"
}
.trace("TextGeneration", kind: .client)
// Combine monitoring and tracing
GenerateText { input in
"Process: \(input)"
}
.onError { error in
logger.error("Generation failed: \(error)")
}
.trace("TextGeneration")
SwiftAgent automatically integrates with Vapor's distributed tracing:
app.post("analyze") { req async throws -> Response in
// SwiftAgent traces will be nested under Vapor's request trace
let agent = MyAgent()
let result = try await agent.run(input)
return result
}
The trace hierarchy will look like:
[Trace] Request abc-123
├─ [Span] POST /analyze (Vapor) - 2.5s
│ ├─ [Span] MyAgent - 2.3s
│ │ ├─ [Span] ContentSafetyGuardrail - 0.1s ✓
│ │ ├─ [Span] GenerateText - 2.0s
│ │ └─ [Span] OutputGuardrail - 0.2s ✓
│ └─ [Span] Response serialization - 0.2s
Maintain state across agent runs:
struct StatefulAgent: Agent {
@Memory var conversationHistory: [String] = []
var body: some Step<String, String> {
Transform { input in
conversationHistory.append(input)
return conversationHistory.joined(separator: "\n")
}
}
}
SwiftAgent is available under the MIT license.
@1amageek