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.