SwiftAgent SDK
repository·main·Indexed 42 days ago
https://github.com/swiftedmind/swiftagentA native Swift 6 SDK for building autonomous AI agents, inspired by Apple's FoundationModels design philosophy. It provides a declarative API for managing agent loops, tool execution, and multi-adapter communication with support for OpenAI and Anthropic. Key features include type-safe tools via @Generable, structured outputs, session transcripts, and a @PromptBuilder DSL for complex prompt construction.
What's inside SwiftAgent
- SwiftAgent is a native Swift SDK designed for building autonomous AI agents. It follows Apple's FoundationModels design philosophy, providing a declarative and intuitive API for managing agent loops, tool execution, and adapter communication. It is built with Swift 6 and utilizes modern concurrency (async/await).
Build type-safe tools using @Generable
mainCreate tools by conforming to the
Toolprotocol. Use Apple's@Generablemacro on nestedArgumentsandOutputstructs to provide type-safe, schema-free definitions. SwiftAgent validates these types so the model can interact with Swift code without manual JSON parsing.Note: When initializing a session with tools, pass them as variadic arguments (e.g.,
tools: ToolA(), ToolB()) rather than an array.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)Define a SessionSchema to decode transcripts
mainTo avoid working with raw
GeneratedContentevents, use the@SessionSchemamacro to define a strongly typed schema for your session. This allows SwiftAgent to decode transcript entries into typed cases for tools, groundings, and structured outputs.Key macros:
@Tool: Links a tool implementation to decoded entries, providing typed arguments, outputs, and errors.@Grounding: Registers values injected into prompts (e.g., dates, search results) so they can be replayed alongside prompt text.@StructuredOutput: Binds a guided generation schema to its decoded result, supporting both partial streaming updates and final values.
Pass the schema instance to your session object (e.g.,
OpenAISession) during initialization.@SessionSchema struct SessionSchema { @Tool var weatherTool = WeatherTool() @Tool var calculatorTool = CalculatorTool() @Grounding(Date.self) var currentDate @Grounding(VectorSearchResult.self) var searchResults @StructuredOutput(WeatherReport.self) var weatherReport @StructuredOutput(CalculatorOutput .self) var calculatorOutput } let sessionSchema = SessionSchema() let session = OpenAISession( schema: sessionSchema, instructions: "You are a helpful assistant.", apiKey: "sk-...", )Use Streaming State Helpers for SwiftUI
mainSwiftAgent provides helpers to keep SwiftUI views stable during streaming.
For
ToolRunobjects:currentArguments: Returns a partially generated variant of the argument type with anisFinalflag. This is ideal for stable UI projections.finalArguments: Returns the fully validated payload once the model finalizes arguments.argumentsPhase: An enum (.partial,.final, or.none) used to branch logic based on streaming progress.
For structured outputs, use
snapshot.currentContentfor partial projections, andsnapshot.contentPhaseto check the streaming status.struct WeatherToolRunView: View { let run: ToolRun<WeatherTool> var body: some View { // 1. Stable UI projection if let currentArguments = run.currentArguments { Text("City: \(currentArguments.city ?? "-")") if currentArguments.isFinal { Text("Locked in") } } // 2. Fully validated payload if let finalArguments = run.finalArguments { Text("Resolved: \(finalArguments.city)") } // 3. Phase-based branching switch run.argumentsPhase { case let .partial(partialArguments): Text("Awaiting... \(partialArguments.city ?? "-")") case let .final(finalArguments): Text("Final: \(finalArguments.city)") case .none: EmptyView() } } }How to use SwiftAgent in a session
mainTo build an agent, you define a
@SessionSchemastruct that declares your tools, grounding data, and structured outputs. You then initialize a session (e.g.,OpenAISession) with this schema and instructions. You interact with the agent using the.respond(to:groundingWith:generating:)method.Key components of a schema include:
@Tool: Declares tools the agent can call.@Grounding: Provides context or data to the agent.@StructuredOutput: Defines the shape of the final response.
After a response, you can use
schema.resolve(session.transcript)to inspect the execution history, including tool runs and prompt sources.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"]) ], generating: \.itinerary ) print(response.content.headline) // Accessing the transcript to see what happened for entry in try schema.resolve(session.transcript) { if case let .toolRun(.cityFacts(run)) = entry, let output = run.output { print("Local picks:", output) } } }Use SimulatedSession for Prototyping and Testing
mainYou can develop and test agents without making live API calls using
SimulatedSession. This requires defining a@SessionSchemaand providing aSimulationConfigurationthat contains mock generations (reasoning, tool runs, and responses).To mock tools, implement the
MockableToolprotocol, which allows you to definemockArguments()andmockOutput()to simulate tool behavior.import OpenAISession import SimulatedSession // 1. Create mockable tool wrappers struct WeatherToolMock: MockableTool { var tool: WeatherTool func mockArguments() -> WeatherTool.Arguments { .init(city: "San Fransico", unit: "Celsius") } func mockOutput() async throws -> WeatherTool.Output { .init( temperature: 22.5, condition: "sunny", humidity: 65 ) } } // 2. Define the session schema @SessionSchema struct SessionSchema { @Tool var weatherTool = WeatherTool() } // 3. Configure the simulation let sessionSchema = SessionSchema() let configuration = SimulationConfiguration(defaultGenerations: [ .reasoning(summary: "Simulated Reasoning"), .toolRun(tool: WeatherToolMock(tool: WeatherTool())), .response(text: "It's a beautiful sunny day in San Francisco with 22.5°C!"), ]) // 4. Initialize and use the simulated session let session = SimulatedSession( schema: sessionSchema, instructions: "You are a helpful assistant.", configuration: configuration, ) let response = try await session.respond(to: "What's the weather like in San Francisco?") print(response.content) // "It's a beautiful sunny day in San Francisco with 22.5°C!"Basic Usage with OpenAISession and AnthropicSession
mainTo get started, initialize a session with
instructionsand yourapiKey. Use therespond(to:)method to send a single-turn prompt and receive anAgentResponse. The session automatically tracks conversation state.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)Install SwiftAgent via Swift Package Manager
mainAdd SwiftAgent to your
Package.swiftdependencies. Depending on which LLM provider you intend to use, you must select the corresponding product:OpenAISessionfor OpenAI orAnthropicSessionfor Anthropic.// 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")Run the SwiftAgent Example App
mainTo run the demonstration application included in the repository, you must use the workspace file located at the root of the repository. This workspace is pre-configured to link against the local development version of the SwiftAgent SDK, enabling immediate testing of SDK changes without updating package dependencies.
Important: Do not open
ExampleApp.xcodeprojdirectly; you must useSwiftAgent.xcworkspaceto ensure the SDK dependencies are correctly resolved.1. Open `SwiftAgent.xcworkspace` (located at the root of the repository) 2. Select the "ExampleApp" scheme in Xcode 3. Choose your target device or simulator 4. Build and runConfigure a Proxy Server for OpenAI
mainTo avoid shipping provider credentials (like OpenAI API keys) directly in your app, point the SDK to a proxy server you control. Use
OpenAIConfiguration.proxy(through:)to set the base URL.SwiftAgent will append the standard
/v1/responsesroute to the provided URL. Your backend should forward requests to OpenAI, attach the secret key, and return the response. The SDK's requests already match the Responses API schema, so no payload reshaping is required on the proxy.let configuration = OpenAIConfiguration.proxy(through: URL(string: "https://api.your-backend.com/proxy")!) let session = OpenAISession( instructions: "You are a helpful assistant.", configuration: configuration )Generate structured outputs
mainTo force the agent to return data in a specific format, use a
SessionSchemaand thegeneratingparameter inrespond(to:generating:).- Define a schema using
@SessionSchema. - Use
@StructuredOutputfor the target type. - Pass the schema to the session and specify the target type (or keypath) during the call.
@SessionSchema struct SessionSchema { @StructuredOutput(WeatherReport.self) var weatherReport } // Usage let session = OpenAISession(schema: SessionSchema(), ...) let response = try await session.respond( to: "What's the weather like in San Francisco?", generating: WeatherReport.self ) print(response.content.temperature)- Define a schema using
Simulate sessions for testing
mainUse
SimulatedSessionto test your application logic without making actual API calls. You can provide aSimulationConfigurationcontaining a sequence of simulated events like.reasoning,.toolRun, and.responseto mimic a full agent interaction.let configuration = SimulationConfiguration(defaultGenerations: [ .reasoning(summary: "Simulated Reasoning"), .toolRun(tool: WeatherToolMock(tool: WeatherTool())), .response(text: "It's sunny!"), ]) let session = SimulatedSession( schema: sessionSchema, instructions: "You are a helpful assistant.", configuration: configuration )