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.
68
81%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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.
Agent (or GroundedAgent) + an Orchestrator with no classifier. No routing hop.LLMClassifier; the orchestrator routes each turn.GroundedAgent: a Brain calls
tools, an isolated Presenter speaks only from the curated results (it can be a smaller/local model).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.
| 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").
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): /* stream tokens */
case .final(let message): /* the message that was persisted */
case .toolCall, .widget, .thinking, .error: break // .error is a user-facing string
}
}.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/.
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.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, message.mappingText { … } covers the text-only case. SummarizingChatStorage wraps any store and keeps agent context small: on the first fetch that exceeds triggerAt message pairs the user-supplied ChatSummarizer is called and the compressed result is held in an in-memory buffer; subsequent saves append to the buffer and recompress eagerly if needed; fetchAllChats is never intercepted so raw history stays available for analytics — the inner store is never written by the summarizer. OSLogTracer is the default
tracer; wire ProcessingTracer + OTLPExporter to ship traces to Langfuse/LangSmith/Datadog/…VoiceAssistants over a WebSocket — OpenAIVoiceAssistant (single LLM, speaks
directly) and OpenAIGroundedVoiceAssistant (grounded Brain → Presenter). Both are self-sufficient
(own tracer/store/userId/sessionId; with a store, completed turns persist and prior
history seeds on start()), wired to the mic/speaker by RealtimeRuntime. Preferred audio
wiring: ONE VoiceProcessedAudioIO instance passed as both input: and output: — capture and
playback share one voice-processed AVAudioEngine, so the assistant's audio is guaranteed to be
in the echo canceller's reference path. The split MicCapture/AudioPlayback pair also works
(capture is voice-processed by default; the AEC reference is then device-level/route-dependent;
MicCapture(voiceProcessing: nil) = raw capture). All three audio classes take an
AudioSessionPolicy (.managed / .custom / .external for apps that own the
AVAudioSession) and a configureEngine hook exposing the raw AVAudioEngine.
Session tuning on both: transcriptionModel (the user's STT only), turnDetection
(.semanticVAD(eagerness:) / .serverVAD(threshold:…) / .disabled), and sessionOverrides
(deep-merged into the generated session.update last — the escape hatch for unmodeled keys like
audio.input.noise_reduction). On OpenAIVoiceAssistant additionally
reasoning: RealtimeReasoningEffort (.minimal….xhigh, session-wide, reasoning models like
gpt-realtime-2 only) and toolReasoningEffort: [String: RealtimeReasoningEffort] — turn-sticky
escalation: once a turn calls a listed tool, that turn's subsequent responses are created with
the mapped effort (highest wins), so payload synthesis thinks harder while lookups stay fast.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 |
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.nil selection
falls back to the default agent (no confidence threshold)..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.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.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/presenter) is backdated to its response.created receive-time via
SpanHandle.generation(…, startedAt:), 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 SpanHandles need not implement it.events stream is non-throwing; needs
NSMicrophoneUsageDescription; always stop(). In-band failures arrive as
.error(code:message:) — code is the API's machine code (e.g. rate_limit_exceeded),
response_failed when the turn's live response ends with status: "failed" (a late failed
done for a response already cancelled by barge-in is consumed silently), or transport_closed when the
socket dies (then events finishes — the end-of-session signal); message is the
human-readable detail for logs, not for verbatim display. Failures are recorded on the trace
spans they end (turn/session/tool), so they export with status: error instead of vanishing.conversation.item.truncate so the
server drops the unheard audio + transcript from context (OpenAI docs' WebSocket procedure).
Automatic when wired by RealtimeRuntime with the built-in outputs; a custom AudioOutput
gets it by implementing playedMilliseconds() (protocol default returns nil → truncation
is skipped, everything else works). Applies to in-band spoken replies only — the grounded
presenter is out-of-band (conversation: "none"), its items never enter the conversation,
so interrupting it deliberately sends no truncate.start() throws
.voiceProcessingUnavailable (degrade deliberately with MicCapture(voiceProcessing: nil)).
For guaranteed echo cancellation use VoiceProcessedAudioIO and pass the same instance as
input and output (its start()/stop() are idempotent — the runtime calls each twice). The
simulator does no AEC — validate on a device. VP quiets the speaker (counter with
duckingLevel: .min); never enable VP on a playback-only engine. With
sessionPolicy: .external the app must configure and activate its AVAudioSession before
start(), and should use the same policy everywhere.ContentPart Codable keys off case + label names — renaming breaks stored history.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/*.swift/Sources/AgentSquad/ (Core/, Agents/, Core/LLM/,
Core/Tooling/, Core/Tracing/, Runtimes/Realtime/).swift/SKILL.md
31eccfe
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.