Use on-device AI to generate, summarize, classify, transform, and extract structured information from text.
Create a Primordial Client
Import Primordial and create a PrimordialClient(). It selects Primordial’s default local generation configuration.
import Primordial
let primordial = PrimordialClient()
You can also use Apple Foundation Model or different AI model . See AI Model Setup for details.
Check Whether Generation Is Available
Text classification, summarization, and extraction or your custom tasks require
.generation capability. Check its status before presenting the feature or asking the user
to download anything.
let status = await primordial.ai.status(for: .generation)
print(status.isInstalled)
print(status.isAvailable)
print(status.downloadBytes)
print(status.requiredAvailableBytes)
isInstalled tells you whether the required AI model is already on the device.
isAvailable also checks whether the current device can use it. Primordial loads an downloaded model when
a collection operation
needs 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 preparing a model or running a generation task.
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 Generation
makeAvailable checks storage,
downloads AI models, verifies it, and reports progress as an asynchronous stream.
for try await progress in primordial.ai.makeAvailable(.generation) {
if let fraction = progress.fractionCompleted {
print("Generation setup: \(Int(fraction * 100))%")
}
print(progress.phase)
print(progress.downloadedBytes as Any)
print(progress.totalBytes as Any)
}
Calling makeAvailable again reuses an existing valid download.
On iOS, add .primordialBackgroundDownloads() once to your main SwiftUI scene. See Background downloads. If the user force-quits your app,
the download is canceled.
Choose the Right Text API
Each API expresses a different application intent. Prefer the focused operation when it matches your
feature, and use task when you want to provide your own prompt.
| API | Use it when | Result |
|---|---|---|
summarize |
You need a concise summary or a bounded bullet list. | String |
streamSummarize |
Your UI should show a summary while it is generated. | Stream of PrimordialTextChunk |
classify |
The output must be exactly one category from your list. | String |
task |
You need a custom transformation or instruction. | PrimordialTaskResult |
streamTask |
You need fragments, optional citations, and a final result. | Stream of PrimordialTaskEvent |
extract |
You want to decode information from text into a Swift type. | Your Decodable type |
Structured task |
You need custom behavior and a typed result, optionally grounded in collections. | Typed PrimordialTaskResult |
Summarize Text
Once .generation is available, the quickest useful task is a summary. Primordial supplies the
task instruction and returns the generated text as a Swift String.
let article = """
Swift actors protect their isolated state from unsynchronized access.
Callers use await when crossing an actor boundary.
"""
let summary = try await primordial.summarize(article)
print(summary)
Use .bulletPoints(maximum:) to create a short, easy-to-scan list.
let concise = try await primordial.summarize(article)
let bullets = try await primordial.summarize(
article,
style: .bulletPoints(maximum: 3)
)
The maximum bullet count must be greater than zero, and the input must contain non-whitespace text.
Classify Text
Pass the complete set of values your application accepts. Primordial returns exactly one of those category strings or throws instead of handing your app an unexpected label.
let sentiment = try await primordial.classify(
"The setup was quick and the results are useful.",
into: ["positive", "neutral", "negative"]
)
switch sentiment {
case "positive": showPositiveState()
case "neutral": showNeutralState()
case "negative": showNegativeState()
default: break // Primordial does not return an unknown category.
}
Categories must be nonempty, unique, and free of empty values. Matching is exact, so choose stable values that your application can handle directly.
Configure Generation
APIs that accept generationOptions can override generation behavior for one request without
changing the client configuration. Use maximumResponseTokens to bound the response and
temperature to control output variability.
let summary = try await primordial.summarize(
article,
generationOptions: .init(
maximumResponseTokens: 256,
temperature: 0.2
)
)
Omitted values inherit the configured defaults. thinking and reasoning are also
generation options. See Thinking Mode and Request a Rationale for their
behavior and model limitations.
Define a Custom Task
Use task for generation that do not have a dedicated API. Build the complete
prompt, including any runtime values, in the order and format the model should receive them.
Primordial sends it as the model prompt and keeps its system instructions separate. Custom tasks accept
the same generationOptions as other generation APIs.
let result = try await primordial.task(
prompt: "Rewrite this text in clear language for a general audience:\n\n\(userText)",
generationOptions: .init(
maximumResponseTokens: 256,
temperature: 0.2
)
)
print(result.output)
if let speed = result.metrics.tokensPerSecond {
print("\(speed.formatted(.number.precision(.fractionLength(1)))) tokens/sec")
}
print(result.citations)
result.output is the generated text. result.metrics reports generation duration and,
when supported by the active engine, exact generated-token count and tokens per second. An ungrounded task has no supporting collection sources,
so its citations array is empty.
Ground a Custom Task in App Data
A custom task can search one or more Primordial collections before generation. This changes the required
capability from .generation to .answering, because the workflow needs both semantic
search and text generation.
let notes = try primordial.collection("notes")
let result = try await primordial.task(
prompt: """
Recommend the next action using only these notes:
\(PrimordialTaskPlaceholder.sources)
Question: \(question)
""",
retrieving: question,
from: [notes],
where: .field("status", .equals(.keyword("open")))
)
print(result.output)
print(result.citations)
A retrieval task prompt must contain PrimordialTaskPlaceholder.sources exactly once.
Primordial searches with retrieving and replaces that placeholder with bounded source context.
The collection must already contain searchable records and belong to the same client. Check and prepare
.answering before running the task. See Store & Search for
collection setup and Answers & Citations for source semantics.
For imported files, pass PrimordialFiles instead of collections. The retrieval query
remains explicit and is used to select the right chunks.
let files = try await primordial.files(Set(fileURLs))
let result = try await primordial.task(
prompt: """
Using only these excerpts:
\(PrimordialTaskPlaceholder.sources)
Create a concise implementation checklist.
""",
retrieving: "implementation requirements and constraints",
from: files
)
print(result.output)
print(result.citations)
let sources = try primordial.sources(
files: files,
collections: [notes]
)
let mixedResult = try await primordial.task(
prompt: prompt,
retrieving: query,
from: sources
)
File and mixed-source tasks use the same search, bounded context assembly, and citation validation as
answers. Their streamTask and typed task(...as:) overloads support streaming and
structured output.
Grounded text tasks return emptySourcesResponse without generation when no sources matches,
including when streaming. Customize it in the retrieval options; see
Empty sources.
Structured task(...as:) calls still generate and validate their typed output.
Stream Generated Text
Streaming lets your UI begin showing text before the complete response is ready. For a summary,
append each nonempty chunk and use isFinal to recognize completion.
var summary = ""
for try await chunk in primordial.streamSummarize(article) {
summary += chunk.text
if chunk.isFinal {
renderFinalSummary(summary)
} else {
renderProvisionalSummary(summary)
}
}
A custom task uses events because it may also return citations. The completed event contains the final assembled output and its final citation set.
var visibleText = ""
for try await event in primordial.streamTask(
prompt: "Rewrite this clearly:\n\n\(userText)"
) {
switch event {
case .fragment(let text):
visibleText += text
renderProvisionalText(visibleText)
case .citation(let citation):
showSource(citation)
case .reasoningSummary(let summary):
showRationale(summary)
case .completed(let result):
visibleText = result.output
renderFinalText(visibleText)
}
}
Streamed fragments are provisional. Commit application state from the completed value, not from a partial fragment. Canceling the consuming Swift task cancels the active generation operation.
Extract Structured Data
Structured output combines a Swift Decodable & Sendable type with a schema that describes the
exact generated shape. Primordial validates the complete output against the schema before decoding it.
struct Contact: Decodable, Sendable {
let name: String
let age: Int?
let email: String
}
let contactSchema = PrimordialOutputSchema(
Contact.self,
schema: .object(
[
"name": .string,
"age": .integer,
"email": .string
],
optional: ["age"]
)
)
let contact = try await primordial.extract(
"Minch is available at minch@example.com.",
as: contactSchema
)
print(contact.name)
print(contact.email)
If the first response does not match the schema, Primordial may make one bounded repair attempt. If the
repaired value is still invalid or cannot be decoded as your Swift type, the operation throws
PrimordialAIError.structuredOutputInvalid.
Use a custom prompt with typed output
Use the structured task overload when extraction alone does not describe the behavior you
need. The result keeps the typed output and any citations together.
let result = try await primordial.task(
prompt: "Normalize these contact details and lowercase the email address:\n\nMinch — MINCH@EXAMPLE.COM",
as: contactSchema
)
let contact: Contact = result.output
Available schema values
| Schema | Expected value |
|---|---|
.string |
A Swift String. |
.oneOf([String]) |
Exactly one string from a nonempty, unique list. |
.integer |
A whole-number value. |
.number |
A finite numeric value. |
.boolean |
A Boolean value. |
.array(schema) |
An array whose elements all match the nested schema. |
.object(fields, optional:) |
An object with declared fields and an optional set of keys that may be omitted. |
Thinking Mode and Request a Rationale
Use thinking mode
Thinking control is available on generation APIs that accept generationOptions, including
summaries, classification, extraction, custom tasks, answers, and chat. Local models use
.automatic by default, which currently leaves thinking off. Use .enabled for a
request that benefits from additional model reasoning, or .disabled to explicitly keep it off.
let summary = try await primordial.summarize(
article,
generationOptions: .init(thinking: .enabled)
)
Primordial never places a model’s raw thinking in returned text or streamed fragments. Thinking control
requires a compatible local model. Apple Foundation Models does not expose this switch, so use
.automatic; explicitly enabling or disabling thinking throws unsupportedFeature.
Request a brief rationale
Use reasoning: .summarized when your UI needs a short explanation alongside a task, answer,
or chat response. After producing the main answer, Primordial makes a separate bounded generation request
based on the visible prompt and answer and returns it in reasoningSummary.
let result = try await primordial.task(
prompt: "Compare these alternatives:\n\n\(userText)",
generationOptions: .init(reasoning: .summarized)
)
print(result.output)
print(result.reasoningSummary as Any)
A reasoning summary is not raw thinking or chain-of-thought. It does not enable thinking mode, and its
additional generation adds latency and token usage. Focused APIs such as summarize,
classify, and extract return only their direct result and do not expose a reasoning
summary.
Handle Generation Errors
Generation fails explicitly when resources are unavailable, input is invalid, a selected feature is not
supported, the context window is exceeded, or structured output cannot be validated. A canceled Swift
task remains CancellationError so your normal cancellation handling still works.
do {
let result = try await primordial.task(
prompt: "Rewrite this clearly:\n\n\(userText)"
)
render(result.output)
} catch is CancellationError {
print("Generation canceled")
} catch let error as PrimordialAIError {
switch error {
case .capabilityUnavailable(let requirement):
print("Missing download: \(requirement.downloadBytes) bytes")
case .invalidQuery(let message),
.invalidConfiguration(let message):
print(message)
case .contextWindowExceeded:
print("The input is too long for the selected model")
case .structuredOutputInvalid:
print("The model did not return the required structure")
default:
print(error.localizedDescription)
}
}