> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reactor.inc/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Reactor hosts multiple models, each with its own connect slug (modelName) and command/event schema. The catalog of every model — slug, typed SDK package, and links to its schema — is at /model-api-reference/overview. Some models expose one slug per experience (e.g. HappyOyster); always take the slug from the model's own pages, never guess it.
> Fastest path to a working app: `npx create-reactor-app my-app --model=<slug>` scaffolds a complete app with secure auth wired up. Typed TypeScript SDKs are published as @reactor-models/<model>; Python uses the base reactor-sdk package.
> Auth: exchange an API key (rk_...) for a JWT via POST https://api.reactor.inc/tokens from your server. Never put the API key in client-side code.
> Append .md to any docs URL for clean Markdown. Search these docs via the MCP server at https://docs.reactor.inc/mcp.

# Swift SDK types

> Type definitions for the Reactor Swift SDK

Everything here is exported by `import Reactor`.

## `ReactorStatus`

```swift Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
public enum ReactorStatus: String, Sendable, CaseIterable {
    case disconnected   // no session, or the last one ended
    case connecting     // the session is being created or adopted
    case waiting        // the session exists; waiting for the runtime, negotiating transport
    case ready          // transport is up: commands can be sent and tracks flow
}
```

Four states, in the order they happen. Read the current one with
[`reactor.status`](/sdk-reference/swift/reactor#status), or subscribe with
[`onStatus(_:)`](/sdk-reference/swift/reactor#events).

An unrecognised status the library reports is read as `.disconnected` rather than trapping: a client
that cannot understand what the library is telling it should behave as though it has no session,
rather than assume the most capable state it knows.

<Note>
  Several calls require `.ready` and throw [`ReactorError.invalidState`](#reactorerror) otherwise —
  [`uploadFile(at:)`](/sdk-reference/swift/reactor#uploadfile-at),
  [`publish()`](/sdk-reference/swift/track#publish),
  [`pushFrame`](/sdk-reference/swift/track#pushframe-_).
</Note>

***

## `ReactorError`

Thrown when an operation fails, and the same payload [`onError`](/sdk-reference/swift/reactor#events)
delivers — one struct either way, so a failure caught from a call and the event describing it can
never disagree about what happened.

```swift Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
public struct ReactorError: Error, Sendable, Hashable {
    public let code: Code
    public let message: String
    public let recoverable: Bool
    public let status: Int?
    public let operation: String?
    public let retryAfterMS: Double?
    public let timestampMS: Double?
}
```

<ParamField path="code" type="Code">
  A stable, matchable code — see the [table](#error-codes) below, or a code the platform sent for a
  request it rejected. Never empty.
</ParamField>

<ParamField path="message" type="String">
  The human-readable explanation, and the only field guaranteed to be worth printing.
</ParamField>

<ParamField path="recoverable" type="Bool">
  Whether the same call could succeed later. `true` is about the moment — a timeout, a 5xx, a
  transport that dropped — so waiting or reconnecting is worth something. `false` is about the
  request itself. Decided by the core and carried in the payload, never recomputed here.
</ParamField>

<ParamField path="status" type="Int?">
  The HTTP status, when the failure came from one.
</ParamField>

<ParamField path="operation" type="String?">
  Which call failed, e.g. `"connect"`, `"send_command"`. `nil` for a failure not tied to a specific
  call, like a transport that dropped on its own.
</ParamField>

<ParamField path="retryAfterMS" type="Double?">
  A backoff hint, when the platform sent one.
</ParamField>

<ParamField path="timestampMS" type="Double?">
  When this happened. Only ever set on the `onError` event — a thrown error is already happening now,
  so there is nothing this would tell you that catching it does not.
</ParamField>

`Code` is a struct wrapping a `String`, not a closed `enum`: the platform's code list is open-ended — a
command or recording it rejects reports the platform's own code, which this SDK cannot enumerate — so
a closed enum would make every new platform code either a breaking change or an unrepresentable value.
A `~=` overload lets `catch ReactorError.unauthorized` read the way the Python SDK's
`except UnauthorizedError` does, for the codes below:

```swift Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
do {
    try await reactor.connect()
} catch ReactorError.unauthorized {
    token = try await refresh()          // a specific, actionable failure
} catch let error as ReactorError where error.recoverable {
    try await reactor.reconnect()        // a class of failures, by property
}
```

<Note>
  This is the one deliberate divergence from the Python and C++ surfaces, which model this as a class
  hierarchy (`except UnauthorizedError`, `catch (const UnauthorizedError&)`). Swift has no base class
  to catch generically the way those do, and an open code set rules out a closed `enum`, so one struct
  with pattern-matchable static members is the shape that keeps `catch ReactorError.unauthorized`
  reading the same way.
</Note>

### Error codes

| Code                | Static member       | Recoverable | Description                                                                                                                                         |
| ------------------- | ------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_STATE`     | `.invalidState`     | No          | The call is not allowed from the state the client is in — most often one needing a live session, made before `connect()` or after `ready` was lost. |
| `DISCONNECTED`      | `.disconnected`     | Yes         | The connection went away: dropped while a request was in flight, or lost after it had been established. `reconnect()` is the way back.              |
| `NETWORK_ERROR`     | `.networkError`     | Yes         | The request never got a reply — DNS, TLS, a refused socket.                                                                                         |
| `REQUEST_TIMEOUT`   | `.requestTimeout`   | Yes         | Sent, and nothing came back in time.                                                                                                                |
| `TRANSPORT_ERROR`   | `.transportError`   | Yes         | The media transport failed.                                                                                                                         |
| `UNAUTHORIZED`      | `.unauthorized`     | No          | The token is missing, expired, or not scoped for this (HTTP 401 / 403).                                                                             |
| `NOT_FOUND`         | `.notFound`         | No          | No such model, session or upload (HTTP 404).                                                                                                        |
| `CONFLICT`          | `.conflict`         | No          | The session is in a state that does not allow this (HTTP 409) — usually one left orphaned by a run that went away without disconnecting.            |
| `RATE_LIMITED`      | `.rateLimited`      | Yes         | Too many requests (HTTP 429).                                                                                                                       |
| `BAD_REQUEST`       | `.badRequest`       | No          | The request itself was wrong — a 4xx other than the above, or an argument rejected here before it was sent.                                         |
| `SERVER_ERROR`      | `.serverError`      | Yes         | The platform failed, and the same request may work later (HTTP 5xx).                                                                                |
| `VERSION_MISMATCH`  | `.versionMismatch`  | No          | This client and the platform disagree on the protocol (HTTP 426 / 501). Update the package.                                                         |
| `DECODE_FAILED`     | `.decodeFailed`     | No          | A reply arrived and could not be understood.                                                                                                        |
| `SESSION_TERMINAL`  | `.sessionTerminal`  | No          | The session reached a state it cannot leave. Start a new one.                                                                                       |
| `MESSAGE_TOO_LARGE` | `.messageTooLarge`  | No          | The payload exceeds what the data channel accepts. Use `uploadFile(at:)` instead.                                                                   |
| `ABORTED`           | `.aborted`          | No          | The operation was abandoned before it finished.                                                                                                     |
| `RECORDER_DISABLED` | `.recorderDisabled` | No          | A clip/recording request failed because the model's recorder is disabled or has crashed.                                                            |

`.internalError` exists as a fallback for a failure with no better classification, the same role
every other SDK's base error class plays — it never gets a dedicated row here, same as those.

<Note>
  **Codes are open-ended.** A command or control request the model itself rejects reports the
  platform's own code, which this list cannot enumerate — that arrives as a `ReactorError` with
  `code` set to whatever came, round-tripping through `Code`'s `rawValue` unchanged. Match on `code`
  for anything not in the table, and never assume an unrecognised code means the payload was
  malformed.
</Note>

***

## `Subscription`

What every `on*` registration hands back — a token that cancels the handler when it is released.

```swift Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
public final class Subscription: Sendable {
    public func cancel()
}
```

```swift Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
do {
    let status = reactor.onStatus { log($0) }
    // handler live
}   // and cancelled here, once `status` goes out of scope
```

<Warning>
  **It cancels when it is released, not when a block returns.** `_ = reactor.onStatus { print($0) }`
  registers a handler and cancels it on the same line, because nothing holds the token. Store it in a
  property, an array, or anything else that outlives the period you want the handler to fire.
</Warning>

`cancel()` is idempotent and safe from any thread. A handler already running when it is called runs to
completion; it is the next event that does not arrive.

This is the same shape the [C++ SDK's `Subscription`](/sdk-reference/cpp/types#subscription) takes,
for the same reason: two closures cannot be compared, so there is no honest `off(event:handler:)` and
a token is the only removal that works.

***

## `FileRef`

A file the platform is holding, ready to be passed into a command. Returned by
[`uploadFile(at:)`](/sdk-reference/swift/reactor#uploadfile-at) and
[`uploadData(_:name:mimeType:)`](/sdk-reference/swift/reactor#uploaddata-_namemimetype).

```swift Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
public struct FileRef: Sendable, Hashable, Codable {
    public let uploadID: String
    public let name: String
    public let mimeType: String
    public let size: Int
}
```

Handed to [`sendCommand(_:_:uploads:)`](/sdk-reference/swift/reactor#sendcommand-__uploads) as a named
upload rather than embedded in the arguments — the platform resolves the reference on its side, so
the bytes cross the wire once. See [File Uploads](/concepts/file-uploads).

***

## `Clip`

A clip or a full-session recording, once the platform has **accepted** the request — which is not the
same as it being ready. Returned by
[`requestClip(_:)`](/sdk-reference/swift/reactor#requestclip-_) and
[`requestRecording()`](/sdk-reference/swift/reactor#requestrecording).

```swift Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
public struct Clip: Sendable, Hashable {
    public let playlistURL: String
    public let sessionID: String?
    public let kind: String?           // "snap" or "recording"
    public let predictedReadyAtMS: Double?
    public let raw: JSONValue
}
```

`predictedReadyAtMS` is a wall clock plus media seconds, so it is only right for a model generating at
real time — treat it as an anchor for a grace period, never a deadline. Reactor does not host clips:
`playlistURL` names a short-lived HLS playlist, and
[`download(_:to:readyTimeout:progress:)`](/sdk-reference/swift/reactor#download-_toreadytimeoutprogress)
is what fetches and assembles it. See [Recordings](/concepts/recordings).

***

## `DownloadResult` and `DownloadProgress`

Returned by, and passed to the progress callback of,
[`download(_:to:readyTimeout:progress:)`](/sdk-reference/swift/reactor#download-_toreadytimeoutprogress).

```swift Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
public struct DownloadResult: Sendable, Hashable {
    public let path: URL
    public let bytes: Int
    public let segments: Int
}

public struct DownloadProgress: Sendable, Hashable {
    public let done: Int
    public let total: Int
    public var fraction: Double { get }   // done / total, or 0 when total is not known yet
}
```

***

## `CommandReply`

What a model answered a command with. Returned by
[`sendCommand(_:_:uploads:)`](/sdk-reference/swift/reactor#sendcommand-__uploads).

```swift Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
public struct CommandReply: Sendable, Hashable {
    public let type: String?
    public let data: JSONValue?

    public func decode<T: Decodable>(_ type: T.Type) throws -> T
}
```

`nil` where a reply would be is not a failure — [`sendCommand`](/sdk-reference/swift/reactor#sendcommand-__uploads)
itself returns `nil` for that case rather than a `CommandReply` with empty fields. `decode(_:)` throws
[`ReactorError.decodeFailed`](#reactorerror) when `data` does not match the type asked for, or is
absent.

***

## `TokenOptions`

What a token minted with [`Reactor.fetchJWT(apiKey:apiURL:options:local:)`](/sdk-reference/swift/reactor#reactor-fetchjwt-apikeyapiurloptionslocal)
is allowed to do.

```swift Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
public struct TokenOptions: Sendable, Hashable, Encodable {
    public var models: [String]?
    public var maxSessions: Int?
    public var expiresAfter: Int?
}
```

<ParamField path="models" type="[String]?">
  The models this token may reach, as `owner/name`. Left empty, the token carries everything the
  key's roles allow — fine server-to-server, wrong to hand to a client you do not control.
</ParamField>

<ParamField path="maxSessions" type="Int?">
  How many sessions the token may open. Scoped tokens only.
</ParamField>

<ParamField path="expiresAfter" type="Int?">
  How long the token should live, in seconds. The server clamps it.
</ParamField>

The three fields are exactly what the platform's token endpoint accepts, and no others — an
unrecognised key there is an error, so a struct with three fields makes a misspelt option impossible
rather than silently dropped into an unscoped token. See [Authentication](/authentication).

***

## `JSONValue`

A JSON value, for the places where the shape is the model's rather than this SDK's — a command's
arguments, a model's reply.

```swift Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
public enum JSONValue: Sendable, Hashable, Codable {
    case string(String)
    case number(Double)
    case bool(Bool)
    case object([String: JSONValue])
    case array([JSONValue])
    case null
}
```

`ExpressibleBy*Literal` for every case, so a literal reads like plain JSON:

```swift Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
let args: JSONValue = ["prompt": "a red bicycle", "steps": 20, "tags": ["outdoor", "day"]]
```

Reading accessors answer `nil` rather than trapping when the value is not the case asked for:
`stringValue`, `doubleValue`, `intValue` (`nil` for a NaN, an infinity, or a fraction — use
`doubleValue` for those), `boolValue`, `objectValue`, `arrayValue`, and a `subscript(key:)` for
reaching into an object directly.

A caller who has modelled the command should reach for the `Encodable`/`Decodable` overloads on
[`sendCommand`](/sdk-reference/swift/reactor#sendcommand-__uploads) and
[`CommandReply.decode(_:)`](#commandreply) instead, and skip `JSONValue` entirely — this type is for
the ad-hoc case, which is most of a first script.

***

## `Duration`

`requestClip(_:)` and the `readyTimeout` parameter on `download` take Swift's standard library
`Duration` (`.seconds(10)`, `.milliseconds(250)`) rather than a raw `Double` — the same reasoning
that keeps a NaN or an infinite timeout unrepresentable at the call site, since `Duration` is stored
as an exact `(seconds, attoseconds)` pair.
