Install in Claude Code
Copygit clone --depth 1 https://github.com/2FastLabs/agent-squad /tmp/agent-squad-swift && cp -r /tmp/agent-squad-swift/swift ~/.claude/skills/agent-squad-swiftThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# 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:
```swift
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/`.
## 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](https://dakera.ai) 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/…
- **Voice**: two `VoiceAssistant`s 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
plMore from this repository