Text Generation

On-device text intelligence.

Summarize, classify, transform, stream, and extract structured data with Primordial.

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)
    }
}