Answers & Citations

Answer questions from your data.

Retrieve relevant information, generate a clear answer, and show the sources that support it.

Primordial answers a question in two stages. It first searches imported files or your collections for relevant text, then gives those retrieved passages to the configured generation model to generate an answer. This is known as retrieval-augmented generation (RAG).

Create a Primordial Client

Import Primordial and create a client. A client’s identifier scopes its stored collections, so use a stable value for the same account or workspace.

import Primordial

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

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

Collections passed to an answer must come from the same PrimordialClient and use the same embedding configuration.

Check Answering Availability

The .answering capability represents the complete grounded-answer workflow. It needs a generation model to write the answer and an embedding model to search your collections (see AI capabilities).

Check its status before showing the feature or asking the user to download anything.

let primordial = PrimordialClient()
            
try await primordial.activate(.evaluationKey("pk_eval_your_key"))
for try await _ in primordial.ai.makeAvailable(.answering) {}

let files = try await primordial.files([fileURL])
let answer = try await primordial.answer(
  "What are the main points?",
  using: files
)

print(answer.text)

isInstalled tells you whether the required AI models are already on the device. isAvailable also checks whether the current device can use it.

When a download is required, use downloadBytes and requiredAvailableBytes to explain the download and storage requirement in your own UI before continuing.

Activate Primordial

Activate the SDK before downloading models, adding searchable data, or asking a question.

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 Answering Capability

After the user agrees to the download, call makeAvailable. Primordial checks storage, downloads missing resources, verifies them, and reports progress as an asynchronous stream.

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

Calling makeAvailable again reuses valid downloads.

On iOS, add .primordialBackgroundDownloads() once to your main SwiftUI scene. See Background downloads. If the user force-quits your app, the download is canceled.

Answer from Files

Pass a set of local .txt or .md URLs and Primordial handles reading, chunking, embeddings, and indexing. No collection name or record setup is required.

let files = try await primordial.files(Set(fileURLs))

let answer = try await primordial.answer(
    "What are the main recommendations?",
    using: files
)

print(answer.text)
if let speed = answer.metrics.tokensPerSecond {
    print("\(speed.formatted(.number.precision(.fractionLength(1)))) tokens/sec")
}
print(answer.citations)

The input is a Set<URL>, so the same URL cannot be passed twice. Each filename, including its extension, is the identifier. Different URLs with the same filename throw an error before import. Files may contain UTF-8 or UTF-16 text up to 10 MiB. Security-scoped access is held only while reading.

let files = try await primordial.files(Set(fileURLs)) { progress in
    status = "\(progress.fileName): \(progress.phase)"
    fraction = progress.fractionCompleted
}

try await files.add(Set(newFileURLs)) { progress in
    fraction = progress.fractionCompleted
}

try await files.replace(Set(updatedFileURLs)) { progress in
    fraction = progress.fractionCompleted
}

let options = PrimordialRetrievalOptions(
    resultLimit: 8,
    contextCharacterLimit: 6_000,
    searchStrategy: .hybrid
)

let answer = try await primordial.answer(
    "Summarize the implementation requirements.",
    using: files,
    options: options
)

File answers use the same retrieval and citation validation as collection answers. Use a collection when you need multiple records, typed filters, field weights, or custom chunking.

Adding accepts only new filenames. Replacing requires existing filenames, prepares every new index first, and deletes the old imported records after switching. If preparation fails, the current files remain available.

let statistics = try await files.statistics(for: fileURL)
print(statistics.chunkCount)

try await files.remove(fileURL)
try await files.removeAll()

Removal deletes Primordial’s imported text, chunks, embeddings, and metadata. It is idempotent and never deletes original source files.

Combine Files and Collections

let notes = try primordial.collection("notes")
let sources = try primordial.sources(
    files: files,
    collections: [notes]
)

let answer = try await primordial.answer(
    "Summarize all relevant information.",
    using: sources
)

Mixed answers retrieve across both source kinds. Each citation identifies either a filename or a collection record through citation.source.

Answer from Your Collection

Primordial answers questions from one or more collections that contain your app’s data. See Store & Search to learn how to create or use collections.

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

Ask Your First Question

Pass your question and choose which collections Primordial should use to answer it

let answer = try await primordial.answer(
    "Why are Swift actors useful?",
    using: [notes]
)

print(answer.text)

answer.text is the response. answer.citations contains the stored sources that were placed in the answer’s context.

Local thinking is off for this default call.

Enable Thinking for an Answer

Local thinking is off by default. Enable the model’s native thinking mode for one answer with thinking: .enabled.

let answer = try await primordial.answer(
    "Which saved design best protects shared state?",
    using: [notes],
    generationOptions: .init(thinking: .enabled)
)

print(answer.text)
print(answer.citations)

.automatic inherits the current local default of off. Use .disabled to state that policy explicitly. Primordial never returns raw model thinking in answer.text.

Thinking and reasoning disclosure are independent. Add reasoning: .summarized only when you also want a separate brief value in answer.reasoningSummary, it does not enable thinking.


Apple Foundation Models does not expose thinking control. Keep thinking: .automatic when the client uses Apple generation.

Show the Supporting Sources

Every citation preserves enough information to connect the answer back to your own record and present a useful source preview.

for citation in answer.citations {
    print(citation.source)
    print(citation.collection)
    print(citation.recordID)
    print(citation.field)
    print(citation.excerpt)
    print(citation.fields)
    print(citation.sourceRange)
}
Citation value Meaning
source A typed .collection or .file source identity.
collection The normalized collection name, or the display filename for an imported file.
recordID Your record identifier, or the filename identifier for an imported file.
field The text field that produced the passage; imported files use content.
excerpt The original stored text of the supporting passage.
fields The record’s typed fields, ready for your source title, metadata, or destination UI.
sourceRange The excerpt’s character range inside the original text field, when a range is available.

Source Names and Excerpts

Primordial includes source names automatically in answers, grounded chat, and retrieval tasks. Imported files use their filename. Collection records prefer a nonempty text value in a field named title. Otherwise, Primordial looks for a single field whose final word is title, such as note_title or noteTitle. If no usable title is found, or several candidates remain, it uses recordID.

Retrieved excerpts from the same collection record and field share one <source> block, including excerpts under different headings. A note's context might look like this:

<source title="Concurrency guide">
heading: Actors
Actors protect isolated mutable state.
</>
Access actor-isolated state through its methods.
</source>

Imported files use <source file="guide.md">. Records without a usable title use <source recordID="record-42">. The heading label identifies a Markdown heading path; custom processor context uses context. Shared headings appear once, and </> separates the retrieved excerpts. Each excerpt keeps its own citation and source range. Literal source tags and excerpt separators are escaped in the prompt; citations retain the original text. Learn more in How Chunking Works.

A citation identifies a source that supported the generated context. It does not prove that every sentence in the answer is correct. Present citations so people can inspect the original data, and keep important decisions under normal application review.

Answer Across Multiple Collections

Use multiple collections when a question should search several datasets. Primordial ranks their matches together without losing collection identity, even when two collections contain the same record ID.

let articles = try primordial.collection("saved articles")

let answer = try await primordial.answer(
    "What have I saved about actor isolation?",
    using: [notes, articles]
)

Every supplied collection must belong to this PrimordialClient and use its embedding configuration. At least one collection is required.

Control Exactly What Grounds Each Answer

PrimordialAnswerOptions lets your app choose the searchable fields, include stored metadata, influence ranking, filter eligible records, and bound how much source context reaches the generation model.

let options = PrimordialAnswerOptions(
    fields: ["title", "body"],
    contextFields: ["author", "modifiedAt"],
    weights: ["title": 1.5, "body": 1.0],
    filter: .all([
        .field("kind", .equals(.keyword("guide"))),
        .field("published", .equals(.bool(true)))
    ]),
    resultLimit: 6,
    contextCharacterLimit: 6_000,
    searchStrategy: .hybrid,
    emptySourcesResponse: "No matching notes found."
)

let answer = try await primordial.answer(
    "How should I protect shared state?",
    using: [notes],
    options: options
)
Option Default What it controls
fields nil The text fields searched. nil searches every text field.
contextFields [] Additional stored fields to include with retrieved passages. They do not change search or ranking.
weights [:] Relative ranking weight for selected text fields.
filter nil A typed filter applied before eligible passages are ranked.
searchStrategy .hybrid Hybrid, semantic-only, or lexical-only source retrieval.
resultLimit 15 The maximum number of supporting passages, from 1 through 64. Several passages may share one source block.
contextCharacterLimit 8_000 The maximum assembled source context, from 64 through 32,000 characters, including names, metadata, headings, and separators.
emptySourcesResponse "I couldn't find anything relevant." The text returned without generation when no sources matches.

Use contextFields for details such as an author or modification date. Missing fields are omitted, and duplicate names are included once. Automatic source names are already available to the model and do not need to be listed again.

When no sources matches for context

Primordial returns emptySourcesResponse text, without calling the generation model or requesting a reasoning summary. Citations are empty. Generation metrics report zero tokens and duration, tokens per second is unavailable.

Set this string on PrimordialAnswerOptions or PrimordialRetrievalOptions to customize the message. It cannot be nil. Streams emit the response as one fragment, followed by completion; an empty string emits only completion.

Stream an Answer

Use streamAnswer when your UI should begin showing text before the complete response is ready. Append each fragment as it arrives, then replace or finalize your UI with the completed answer.

var visibleText = ""

for try await event in primordial.streamAnswer(
    "Why are Swift actors useful?",
    using: [notes]
) {
    switch event {
    case .fragment(let text):
        visibleText += text
        print(visibleText)

    case .reasoningSummary(let summary):
        showRationale(summary)

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

Request reasoning: .summarized when the UI needs a brief rationale in answer.reasoningSummary. Primordial generates it separately and never exposes raw chain-of-thought, hidden prompts, or retrieved source text as reasoning metadata.

Streamed fragments are provisional. Primordial returns citations only with the single .completed event, after it has mapped and revalidated the stored sources. Do not attach citations to partial fragments.

Keep Citations Tied to Current Data

Primordial checks every cited source again when generation finishes. If a record or cited field changed or was removed while the answer was being generated, the operation fails instead of returning stale provenance.

Cancellation also stops the active work without changing your stored collection. A canceled Swift task remains CancellationError.

Handle Answer Errors

Empty questions, missing collections, invalid limits, incompatible collection ownership, missing AI resources, changed sources, and invalid collection filters all fail explicitly.

do {
    let answer = try await primordial.answer(
        "Why are Swift actors useful?",
        using: [notes]
    )
    print(answer.text)
} catch is CancellationError {
    print("Answer cancelled")
} catch let error as PrimordialFileError {
    print("File error: \(error.localizedDescription)")
} 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):
        print(message)

    default:
        print(error.localizedDescription)
    }
}

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