| name | agent-squad-swift |
| description | Use when building or modifying a Swift app that uses the AgentSquad Swift framework — on-device multi-agent orchestration for iOS 16+ / macOS 14+: orchestrator, agents (Agent, GroundedAgent), classifier routing, LLM clients (OpenAI-compatible), tools (native + MCP), tool UIs/widgets, on-device storage, tracing, and realtime voice — built-in types and custom implementations. |
AgentSquad Swift — assistant guide
Protocol-driven, on-device multi-agent framework (Swift 6.2, iOS 16+ / macOS 14+; persistence via
FileChatStorage on iOS 16+, DeviceChatStorage on iOS 17+). This is guidance and
a map — not an API reference. Read the exact signatures from the source (swift/Sources/AgentSquad/)
and the worked recipes from the docs site sources (docs/src/content/docs/swift/); this file tells
you what to use, when, and what to watch out for.
When to use what
- One assistant → an
Agent (or GroundedAgent) + an Orchestrator with no classifier. No routing hop.
- Several specialists → multiple agents + an
LLMClassifier; the orchestrator routes each turn.
- Answers must not drift from data (prices, stock, balances) →
GroundedAgent: a Brain calls
tools, an isolated Presenter speaks only from the curated results (it can be a smaller/local model).
- Voice → a
VoiceAssistant (a peer of the orchestrator, not an agent): OpenAIVoiceAssistant
(single LLM + tools, speaks directly — the spoken analog of Agent) or
OpenAIGroundedVoiceAssistant (Brain → Presenter, can't drift from data — analog of GroundedAgent).
Every component is a Sendable protocol with one built-in implementation — swap in your own anywhere.
Modules (import only what you use)
| Import | Pulls in | Contents |
|---|
AgentSquad | nothing external | protocols, Agent, GroundedAgent, Orchestrator, LLMClassifier, ChatCompletionsClient, DakeraRetriever, FileChatStorage, DeviceChatStorage, InMemoryChatStorage, TransformingChatStorage, SummarizingChatStorage, OSLogTracer, OTLP export |
AgentSquadMCP | MCP Swift SDK | MCPServer (= MCPToolProvider), SDKMCPClient |
AgentSquadAudio | AVFoundation | VoiceProcessedAudioIO (capture+playback, one engine, AEC — the recommended wiring), MicCapture (voice-processed/AEC by default), AudioPlayback, VoiceProcessing, AudioSessionPolicy (needs NSMicrophoneUsageDescription) |
SwiftPM: .package(url: "https://github.com/2FastLabs/agent-squad", branch: "main").
How a turn works
Two peer runtimes share contracts but not a control loop: a turn-based Orchestrator
(classify? → run agent → stream → persist) and a long-lived VoiceAssistant for voice. Either
way you consume an AsyncThrowingStream<AgentEvent, any Error> — the one idiom worth memorizing:
for try await event in orchestrator.route(.text("hello"), userId: "u1", sessionId: "s1") {
switch event {
case .textDelta(let token):
case .final(let message):
case .toolCall, .widget, .thinking, .error: break
}
}
.error carries a user-facing message; real programmer/transport failures throw through the
stream. .final is what the orchestrator persists. Inputs/messages are value types
(AgentInput.text, ConversationMessage, ContentPart, JSONValue) in Sources/AgentSquad/Core/.
The pieces
Orchestrator drives a turn. The classifier is optional — omit it for a single agent.
Agent is one LLM with an internal tool loop. GroundedAgent is two LLMs (Brain + isolated
Presenter) for answers that must stay grounded in tool results. The Presenter never sees chat
history or the Brain's transcript; presenterInput picks .questionAndData (default) or .dataOnly.
ChatCompletionsClient speaks the OpenAI wire — point its baseURL at OpenAI, Azure,
OpenRouter, Groq, or a local Ollama/llama.cpp. Implement LLMClient for anything else.
- Tools come from a
ToolProvider. Built-ins: ToolKit holds native tools — Tool.local
(Swift closure) and Tool.http/Tool.get/.post (declarative HTTP, with a ToolParameter DSL so
you don't hand-write JSON Schema); HTTPToolGroup(baseURL:…) declares one API's shared
config once, then one line per endpoint; MCPServer(url:) connects an MCP server; and
AggregateToolProvider composes any mix behind one seam; DakeraRetriever(namespace:…) is a
ToolProvider backed by a self-hosted Dakera memory server — it exposes a
search_memory tool for grounding (and a direct retrieve(_:) API), talking to Dakera's REST
endpoint over URLSession with no extra dependency. A ToolResult is three-part: text →
the model's context, structuredContent → curator/UI data, ui → an optional widget.
FileChatStorage (JSON files, iOS 16+) and DeviceChatStorage (SwiftData, iOS 17+) persist history on-device; InMemoryChatStorage is a non-persistent, seedable single-conversation store. TransformingChatStorage wraps any store and runs a MessageTransform before each save (PII scrub / redact / drop) — reads pass through, covers the text-only case. wraps any store and keeps agent context small: on the first fetch that exceeds message pairs the user-supplied is called and the compressed result is held in an in-memory buffer; subsequent saves append to the buffer and recompress eagerly if needed; is never intercepted so raw history stays available for analytics — the inner store is never written by the summarizer. is the default
tracer; wire + to ship traces to Langfuse/LangSmith/Datadog/…
Custom implementations
Conform to the protocol and pass your type where the built-in goes. Each seam has a worked example on
its doc page (paths below are under docs/src/content/docs/swift/, published at /agent-squad/swift/…);
signatures live in Sources/AgentSquad/.
| Seam | Protocol | Source · doc |
|---|
| Agent | AgentProtocol | Core/AgentProtocol.swift · agents/custom |
| Classifier | Classifier (return an agent from the passed list, or nil) | Core/Classifier/ · classifiers/custom |
| LLM client | LLMClient | Core/LLMClient.swift · llm/custom |
| Tools | ToolProvider | Core/Tooling/ · tools/custom |
| Tool-output curator | ToolOutputCurator (where you trim oversized output) | Core/Presenter/ · ui/built-in/curators |
| Presenter prompt | PresenterPrompt | Core/Presenter/ · agents/built-in/grounded-agent |
| Storage | ChatStorage | Core/Storage/ · storage/custom |
| Tracing | TraceExporter (easiest) / SpanProcessor / Tracer / Redactor | Core/Tracing/ · tracing/custom |
| Realtime transport | RealtimeTransport | Runtimes/Realtime/ · voice/custom |
| Audio I/O | AudioInput / AudioOutput | Runtimes/Realtime/AudioIO.swift · audio/custom |
Gotchas
maxToolRounds: Agent/GroundedAgent default to 20; the AgentProtocol default is 1. A
custom agent that injects tools but leaves 1 silently disables its tool loop.
- Classifier is optional: no classifier ⇒ no routing hop / no extra model call. A
nil selection
falls back to the default agent (no confidence threshold).
- Persistence: only turns ending in
.final are saved.
ChatCompletionsClient: retries only before the first event; some local runtimes reject
stream_options/unknown body keys — override via extraBody.
JSONValue: whole-number doubles decode to .int; carry large IDs as .string.
- Storage:
FileChatStorage (JSON, iOS 16+, scopes per-call by userId/sessionId/agentId — e.g. sessionId to isolate per match) or DeviceChatStorage (SwiftData, iOS 17+, bound to one userId). Both default to Library/Caches (disposable). InMemoryChatStorage (iOS 16+) is non-persistent and holds one conversation — construct it empty or seeded with a prior conversation to load one into a session. Wrap any store in TransformingChatStorage to scrub/redact before persistence; prefer redacting over returning nil (dropping one side of an exchange can make the store skip its counterpart via the consecutive-same-role guard). Wrap any store in SummarizingChatStorage(wrapping:summarizer:triggerAt:keepLast:) to keep agent context small: the buffer activates lazily on the first qualifying fetch; once active, saves append to it and compress eagerly; fetchAllChats bypasses the buffer entirely.
- Tracing lifecycle: nothing drains the tracer for you — flush on background, shut down on
termination.
OSLogTracer logs no payloads. Redaction hashes ids + clips strings but does not
pattern-scrub PII — supply a custom Redactor for that. A realtime answer generation
(response/) is backdated to its receive-time via
, so the exported span carries the real call latency instead
of a ~0 duration (the Realtime API sends no server-side timing). The overload defaults to stamping
now, so custom s need not implement it.
Go deeper
- Prose & recipes — the Starlight docs under
docs/src/content/docs/swift/ (run the site from
docs/ with npm run dev): quick-start, orchestrator/overview,
agents/built-in/grounded-agent, mcp/overview, ui/overview, storage/built-in/device,
tracing/built-in/otlp-exporter, voice/built-in/openai-voice,
voice/built-in/openai-grounded-voice, guides/*.
- Exact signatures —
swift/Sources/AgentSquad/ (Core/, Agents/, Core/LLM/,
Core/Tooling/, Core/Tracing/, Runtimes/Realtime/).