Chat

Build conversations that stay on device.

Create an on-device AI chat, or with your data that your app stores.

A Primordial chat is a reusable, in-memory conversation. It remembers completed turns so people can ask follow-up questions without your app rebuilding the transcript for every request.

You can create a model-only chat with the configured AI generation model, or a grounded chat that searches your collections before every reply and returns citations with the answer.

Create a Primordial Client

Import Primordial and create one client for the account, workspace, or data boundary your chat belongs to. The client owns the configured AI models and coordinates their use across chat, search, and other AI features.

import Primordial

let primordial = PrimordialClient(
    identifier: "account-42" // Optional
)

Use a stable identifier when your app separates data by account or workspace. If it does not, PrimordialClient() uses the stable default identifier.
You can also use Apple Foundation Model or different AI model . See AI Model Setup for details.

Choose the Chat Capability

Check the capability for the kind of conversation you want to offer. This tells your app whether the required AI resources are installed and whether the current device can use them.

Chat mode Capability What it needs
Model-only chat .generation The configured generation model.
Chat with your data .answering The configured generation and embedding models.

let requirement = await primordial.ai.status(
    for: .generation
)

print(requirement.isInstalled)
print(requirement.isAvailable)
print(requirement.downloadBytes)
print(requirement.requiredAvailableBytes)

isInstalled reports whether downloadable resources are already on the device. isAvailable checks whether the current device can use it. Before starting a download, use downloadBytes and requiredAvailableBytes to explain what the app needs and ask the user for permission.

For grounded chat, make the same check with .answering.

Activate Primordial

Activate the SDK before installing models or starting a conversation.

try await primordial.activate(
    .evaluationKey("pk_eval_your_key_here")
)

You can check primordial.isActivated when your UI needs to reflect activation state. Keep the evaluation key out of logs and user-visible error messages.

Prepare Chat on the Device

After the user agrees to any required download, call makeAvailable with the capability you checked. Primordial checks storage, downloads missing resources, verifies them, and reports progress.

for try await progress in primordial.ai.makeAvailable(
    .generation
) {
    print(progress.fractionCompleted)
    print(progress.downloadedBytes)
    print(progress.totalBytes)
    print(progress.phase)
}

Use .answering instead when the chat will use collections. Calling makeAvailable again reuses valid downloads rather than downloading an installed model again.

Do not start a model download automatically when a chat button is tapped. Check status first, show the size and storage requirement in your UI, and let the user decide when to continue.

Start a Model-Only Chat

Create a chat once, then keep that chat instance for the conversation. Calling send adds the user message, generates the next assistant message, and commits both messages to the history when the response succeeds.

let chat = try primordial.chat()

let reply = try await chat.send(
    "Why are Swift actors useful?"
)

print(reply.text)

send returns the completed assistant message. The chat automatically includes earlier completed turns when the next message is sent, so a follow-up can refer to the conversation naturally.

Understand Chat Messages

Every item in the conversation is a PrimordialChatMessage.

Value Meaning
role .user or .assistant.
text The message text committed to the conversation.
citations Sources returned by grounded chat. Model-only assistant messages use an empty array.

print(reply.role)
print(reply.text)
print(reply.citations)

Chat with Your App’s Data

Grounded chat searches one or more Primordial collections before every assistant reply. The retrieved passages help the generation model answer from your app’s current data instead of relying only on its general knowledge.

Following examples demonstrate grounded chat using a research notes collection. You can learn more about collections in Store and Search.

let notes = try primordial.collection("research notes")

Once the collection is ready, create the chat with that collection and send a question.

let chat = try primordial.chat(
    using: [notes]
)

let reply = try await chat.send(
    "Why Primordial makes life so easy?"
)

print(reply.text)
print(reply.citations)

Grounded chat searches its collections again for every turn, including follow-up questions. The collections and answer options are fixed when you create the chat, so create another chat when a conversation needs a different data boundary.

Control What Grounds the Conversation

Use PrimordialAnswerOptions to choose which text fields are searched, influence ranking, filter eligible records, and bound how much retrieved text is supplied to the generation model. Learn more about it in Answers and Citations.

let options = PrimordialAnswerOptions(
    fields: ["title", "body"],
    weights: ["title": 1.5, "body": 1.0],
    filter: .field(
        "kind",
        .equals(.keyword("guide"))
    ),
    resultLimit: 6,
    contextCharacterLimit: 6_000
)

let chat = try primordial.chat(
    using: [notes],
    options: options
)

let reply = try await chat.send(
"Why Primordial makes life so easy?"
)

Show Grounded Sources

A completed grounded assistant message contains the citations that supported its generated context. Connect them to your source UI so people can inspect the underlying data.

for citation in reply.citations {
    print(citation.collection)
    print(citation.recordID)
    print(citation.field)
    print(citation.excerpt)
    print(citation.fields)
    print(citation.sourceRange)
}

A citation identifies a source supplied to the generation model. It does not prove that every sentence in the reply is correct. Keep important decisions under normal application review.

Stream a Chat Turn

Use stream when the UI should begin showing text before the complete assistant message is ready. Append each fragment as it arrives, then finalize the UI with the completed message.

var visibleText = ""

for try await event in chat.stream(
    "How does that isolation work?"
) {
    switch event {
    case .fragment(let text):
        visibleText += text
        print(visibleText)

    case .completed(let message):
        visibleText = message.text
        print(message.citations)
    }
}

Fragments are provisional. Grounded citations arrive only with the single .completed event, after Primordial has mapped and revalidated the supporting records.

If streaming fails or is cancelled, the partial text is not added to chat history. Commit durable UI state only after receiving .completed.

Inspect and Restore Conversation History

Read messages to inspect every completed user and assistant message in order. Because PrimordialChat is an actor, access its state with await.

let messages = await chat.messages

for message in messages {
    print(message.role)
    print(message.text)
}

You can also begin a chat with existing messages. This is useful after your app restores a conversation from its own storage.

let restoredMessages = [
    PrimordialChatMessage(
        role: .user,
        text: "What is actor isolation?"
    ),
    PrimordialChatMessage(
        role: .assistant,
        text: "Actor isolation protects actor-owned mutable state."
    )
]

let restoredChat = try primordial.chat(
    messages: restoredMessages
)

To restore a grounded conversation, pass the same collection boundary and answer options together with the saved messages.

let restoredGroundedChat = try primordial.chat(
    using: [notes],
    options: options,
    messages: restoredMessages
)

Primordial keeps chat history in memory only. Your app decides whether to persist it, how long to retain it, and how to translate messages into its own durable data model.

Clear or Reset a Chat

clear() removes the current in-memory history. reset() restores the messages supplied when the chat was created.

try await chat.clear()
print(await chat.messages) // []

try await restoredChat.clear()
try await restoredChat.reset()
print(await restoredChat.messages) // restoredMessages

Neither operation changes your collections or deletes conversation data that your app persisted elsewhere.

Keep One Turn Active per Chat

One chat accepts one active send or stream turn at a time. Starting another turn on that same chat before the first finishes returns a resource-conflict error. Separate chat instances keep separate histories and may operate independently.

A user and assistant message enter history together only after the response finishes successfully. Failure or cancellation commits neither message, and a cancelled Swift task remains CancellationError.

Handle Chat Errors

Empty messages, unavailable AI resources, incompatible grounded collections, invalid answer options, overlapping turns, generation failures, and cancellation all fail explicitly.

do {
    let reply = try await chat.send(
        "How should I protect shared state?"
    )
    print(reply.text)
} catch is CancellationError {
    print("Chat cancelled")
} catch let error as PrimordialCollectionError {
    print("Collection error: \(error.localizedDescription)")
} catch let error as PrimordialAIError {
    switch error {
    case .capabilityUnavailable(let requirement):
        print("Download required: \(requirement.downloadBytes) bytes")

    case .invalidQuery(let message),
         .invalidConfiguration(let message),
         .invalidRecord(let message),
         .resourceConflict(let message):
        print(message)

    default:
        print(error.localizedDescription)
    }
}

When .capabilityUnavailable is returned, use its requirement to explain what is missing and ask the user before calling makeAvailable. Do not begin a model download automatically from an error handler.