SwiftAgent SDK

repository·main·Indexed 42 days ago

https://github.com/swiftedmind/swiftagent

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

Tokens
11.9K
Snippets
34
Records
39
Agent score
53%

What's inside SwiftAgent

  1. Overview of SwiftAgent

    main
    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).
  2. Build type-safe tools using @Generable

    main

    Create tools by conforming to the Tool protocol. Use Apple's @Generable macro on nested Arguments and Output structs 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)
  3. Define a SessionSchema to decode transcripts

    main

    To avoid working with raw GeneratedContent events, use the @SessionSchema macro 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-...",
    )
  4. Use Streaming State Helpers for SwiftUI

    main

    SwiftAgent provides helpers to keep SwiftUI views stable during streaming.

    For ToolRun objects:

    • currentArguments: Returns a partially generated variant of the argument type with an isFinal flag. 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.currentContent for partial projections, and snapshot.contentPhase to 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()
        }
      }
    }
  5. How to use SwiftAgent in a session

    main

    To build an agent, you define a @SessionSchema struct 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)
        }
      }
    }
  6. Use SimulatedSession for Prototyping and Testing

    main

    You can develop and test agents without making live API calls using SimulatedSession. This requires defining a @SessionSchema and providing a SimulationConfiguration that contains mock generations (reasoning, tool runs, and responses).

    To mock tools, implement the MockableTool protocol, which allows you to define mockArguments() and mockOutput() 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!"
  7. Basic Usage with OpenAISession and AnthropicSession

    main

    To get started, initialize a session with instructions and your apiKey. Use the respond(to:) method to send a single-turn prompt and receive an AgentResponse. 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)
  8. Install SwiftAgent via Swift Package Manager

    main

    Add SwiftAgent to your Package.swift dependencies. Depending on which LLM provider you intend to use, you must select the corresponding product: OpenAISession for OpenAI or AnthropicSession for 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")
  9. Run the SwiftAgent Example App

    main

    To 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.xcodeproj directly; you must use SwiftAgent.xcworkspace to 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 run
  10. Configure a Proxy Server for OpenAI

    main

    To 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/responses route 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
    )
  11. Generate structured outputs

    main

    To force the agent to return data in a specific format, use a SessionSchema and the generating parameter in respond(to:generating:).

    1. Define a schema using @SessionSchema.
    2. Use @StructuredOutput for the target type.
    3. 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)
  12. Simulate sessions for testing

    main

    Use SimulatedSession to test your application logic without making actual API calls. You can provide a SimulationConfiguration containing a sequence of simulated events like .reasoning, .toolRun, and .response to 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
    )