Claude Code and Codex proved coding agents can work. Both run on Python or TypeScript — the default stack for AI tooling.
I’ve been building Swarm, a multi-agent framework in Swift. After a few months with it, I keep running into reasons why Swift makes more sense for this problem than the obvious choice. Here’s what I found.
The concurrency problem nobody talks about
Coding agents juggle multiple tools at once, hold onto long-running sessions, and stream responses — all at the same time. In Python or TypeScript, you end up managing async state, locks, or callbacks. Get it wrong and you either get data races or deadlock, usually at 3am.
Swift has actors. The compiler enforces that only one task can touch mutable state at a time.
public actor ToolRegistry {
private var tools: [String: any AnyJSONTool] = [:]
func register(_ tool: any AnyJSONTool) throws {
guard tools[tool.name] == nil else {
throw ToolRegistryError.duplicateToolName(name: tool.name)
}
tools[tool.name] = tool
}
func execute(toolNamed name: String, arguments: [String: SendableValue]) async throws -> SendableValue {
guard let tool = tools[name] else {
throw AgentError.toolNotFound(name: name)
}
return try await tool.execute(arguments: arguments)
}
}
The compiler knows ToolRegistry is thread-safe. You can’t share it across tasks without await. Data races become compile errors instead of bugs you find in production.
Type-safe tools instead of dictionary soup
In Python or TypeScript, tools typically receive dictionaries. You validate at runtime, or you write separate schema files and hope they stay in sync.
Swift’s SendableValue replaces [String: Any] with something type-safe you can pass across concurrency boundaries:
public enum SendableValue: Sendable, Equatable, Hashable, Codable {
case string(String)
case int(Int)
case double(Double)
case bool(Bool)
case array([SendableValue])
case dictionary([String: SendableValue])
}
It conforms to ExpressibleBy*Literal — you write nested JSON-like structures in Swift syntax:
let json: SendableValue = [
"user": ["name": "Alice", "age": 30],
"active": true,
"scores": [95.5, 87.2]
]
let user: UserInfo = try json.decode()
Tool arguments get checked at compile time for JSON serialization. Call sites decode to typed structs. No dictionary soup.
Macros that write the boilerplate for you
Defining tools in most frameworks means writing schema objects, validation logic, and wrapper code. Swift macros generate this at compile time:
@Tool("Fetches weather for a location")
struct WeatherTool {
@Parameter("City name")
var city: String
@Parameter("Units", oneOf: ["celsius", "fahrenheit"])
var units: String = "fahrenheit"
func execute() async throws -> String {
let temp = try await weatherAPI.fetch(city: city, units: units)
return "\(temp)°"
}
}
The macro generates the name and description properties, the parameters array from @Parameter annotations, a Codable Input struct, the execute(arguments:) wrapper, and Tool and Sendable conformances.
You write the business logic. The framework handles the glue code.
For one-off tools, there’s #Tool:
let greet = #Tool("greet", "Says hello") { (name: String, age: Int) in
"Hello, \(name)! You are \(age)."
}
Protocol composition instead of inheritance chains
Claude Code and Codex use inheritance or class-based composition. Swift’s protocols let you compose behavior without inheritance hierarchies:
public protocol AgentRuntime: Sendable {
nonisolated var name: String { get }
nonisolated var tools: [any AnyJSONTool] { get }
nonisolated var instructions: String { get }
func run(_ input: String, session: (any Session)?, observer: (any AgentObserver)?) async throws -> AgentResult
func stream(_ input: String, session: (any Session)?, observer: (any AgentObserver)?) -> AsyncThrowingStream<AgentEvent, Error>
}
Agent is the main implementation. @AgentActor generates lightweight agents from simple functions. GraphAgent bridges Hive workflows. ObservedAgent wraps any agent with observability — no subclassing required.
// Wrap any agent with logging
let observed = ObservedAgent(wrapped: agent, observer: myObserver)
// AgentActor generates from a simple function
@AgentActor(instructions: "You are a coding assistant")
actor CodeAssistant {
func process(_ input: String) async throws -> String {
// ...
}
}
Phantom types catch mistakes at compile time
Agent context often ends up as string-keyed dictionaries. Swift’s phantom types make these compile-time safe:
extension ContextKey where Value == String {
static let userID = ContextKey("user_id")
static let sessionID = ContextKey("session_id")
}
extension ContextKey where Value == Bool {
static let isAuthenticated = ContextKey("is_authenticated")
}
// Can't set a Bool for userID — compiler error
await context.setTyped(.userID, value: "user-123")
let isAuth: Bool? = await context.getTyped(.isAuthenticated)
Set a string for a bool key, and the compiler refuses to build. No runtime checks needed.
On-device inference on Apple Silicon
Python AI frameworks depend on cloud APIs or barely function locally. Swift talks to Apple’s on-device AI stack directly:
// Uses Apple Neural Engine via Foundation Models when available
let llm = LLM.appleFoundationModels()
// Or OpenRouter for cloud models with routing
let llm = LLM.openRouter(apiKey: key, model: "anthropic/claude-3.5-sonnet") {
$0.providers = [.anthropic, .google]
$0.routeByLatency = true
}
Built-in tools like SemanticCompactorTool use on-device Foundation Models for summarization. No network required.
Sendable — the concurrency contract
Swift 6 introduces strict concurrency checking. Types opt into being shared across tasks by conforming to Sendable.
Swarm’s core types are all Sendable:
public struct AgentResult: Sendable {
public let output: String
public let toolCalls: [ToolCall]
public let iterationCount: Int
public let duration: Duration
public let tokenUsage: TokenUsage?
}
Pass agent results across task boundaries with the compiler verifying safety. No locks, no guesswork.
The workflow model
All of this adds up to a compositional workflow system:
let result = try await Workflow()
.step(researchAgent) // Sequential
.step(writeAgent) // Gets research output
.parallel([bullAgent, bearAgent], merge: .structured)
.repeatUntil(maxIterations: 10) { result in
result.output.contains("FINAL")
}
.run("Climate analysis")
Agents stay small and testable. Workflows compose them. The actor model keeps state safe as things run concurrently.
The tradeoffs
Python and TypeScript dominate AI agents because the ecosystem is there and the tooling works.
Swift brings different tradeoffs:
- Compile-time concurrency safety — data races are impossible, not just unlikely
- Protocol composition — agents are Lego blocks, not inheritance chains
- Macros — less boilerplate, fewer mistakes
- Type safety — tool schemas, context keys, and message types all checked at compile time
- On-device inference — run models on Apple Silicon without cloud dependencies
That said: the ecosystem is smaller, fewer people know Swift in this space, and Apple’s toolchain is Apple’s toolchain. But if you’re already building in Swift, or if you’re tired of runtime concurrency bugs that don’t show up until production — the approach is worth trying.
The code
import Swarm
@Tool("Echoes input back")
struct EchoTool {
@Parameter("Text to echo")
var text: String
func execute() async throws -> String { text }
}
let agent = try Agent("You are helpful.") { EchoTool() }
let result = try await agent("Hello, Swarm!")
Swarm is experimental. APIs will change. If any of this sounds interesting, the repo is linked below.