Core AI model zoo

Local RAG with SpotlightSearchTool behind a third-party model

Verified 2026-06-13 (macOS 27 beta, M4 Max). WWDC26 246 “LLM search using Core Spotlight”. Apple’s SpotlightSearchTool turns the Core Spotlight index into a retrieval tool for a LanguageModelSession. It is a plain FoundationModels.Tool, so it works behind ANY LanguageModel — we ran it behind a Core AI zoo bundle (KitLanguageModel), not the system model. Full round trip (model writes a query → Spotlight searches → grounded answer) passes on the zoo qwen3.5-0.8B and on qwen3-4B. Working example: coreai-kit/Examples/SpotlightChat.

The API (cross-import overlay)

SpotlightSearchTool lives in the _CoreSpotlight_FoundationModels overlay — it materializes when a file imports BOTH CoreSpotlight and FoundationModels. Shape:

import CoreSpotlight
import FoundationModels

let tool = SpotlightSearchTool(configuration: .init(
    sources: [.coreSpotlight(CoreSpotlightSource(searchableIndexDelegate: delegate))],
    guide: SpotlightSearchTool.Guide(level: .focused(.items), format: .compact),
    contactResolver: nil,
    customStages: []))

// Behind YOUR model instead of the system one:
let session = LanguageModelSession(model: kitModel, tools: [tool], instructions: )
let answer = try await session.respond(to: "What did I write about the night hike?")

Does it work behind a third-party model? YES.

The only capability required is .toolCalling — declared by KitLanguageModel for ChatML tokenizers (qwen3 family). The tool’s query GenerationSchema is rendered into the tool prompt; the model emits a parseable tool call; the framework runs the tool and feeds results back. .guidedGeneration is NOT required (the tool does not constrain decoding on the model side), so this works on the GPU-pipelined engine that cannot expose logits. Transcript:

prompt → reasoning → toolCall spotlight_search({"searchTerms":["night hike"]})
       → toolOutput (items) → toolCall fetch_note({"id":"note-003"})
       → toolOutput (body) → grounded answer

The central gotcha: the tool returns metadata, not the body

Even with CoreSpotlightSource(fetchAttributes: [.title, .contentDescription, .keywords]), the toolOutput handed to the model carries only identity attributes — uniqueIdentifier, title, contentType, contentCreationDate, domainIdentifier. contentDescription and keywords do not appear (in .compact or .structured). This is not a Spotlight limitation: a raw CSSearchQuery with the same fetchAttributes returns contentDescription (full body) fine (textContent is index-only — write-only for full-text search, returns nil on read).

Consequence: a model answering from search results alone sees only TITLES and will hallucinate bodies (the system model, asked about a night hike, invented “rained heavily / pack a waterproof jacket”; the real note said the headlamp died — pack spare batteries).

The working pattern: retrieve with Spotlight, hydrate with your own tool

Give the model a second plain Tool that reads the full content from your store by identifier:

struct FetchNoteTool: Tool {
    let name = "fetch_note"
    let description = "Read the full saved text of a note by its identifier."
    @Generable struct Arguments {
        @Guide(description: "The note id from spotlight_search, like note-002.") var id: String
    }
    func call(arguments: Arguments) async throws -> String { store[arguments.id] ?? "not found" }
}
let session = LanguageModelSession(model: kitModel, tools: [spotlightTool, FetchNoteTool()], )

The model chains spotlight_search → ids/titles → fetch_note(id) → body → grounded answer. This mirrors a real app (Spotlight index = lightweight finding aid; full content = your store) and doubles as a multi-tool-orchestration demo on a third-party model. Verified on the system model, zoo qwen3.5-0.8B, and qwen3-4B.

Guidance level is a token gate

.complete guidance injects ~13 k tokens of tool instructions → instant contextSizeExceeded on any 4 k-context model (system or zoo). Ship .focused(.items) + format: .compact for local models. .dynamic(GuidanceProfile) was prompt-sensitive in testing (a model skipped the search and hallucinated) — use deliberately.

Model-choice constraints (Core AI / kit)

CustomStage and the delegate, in this beta