New things to learn — pace (foundational frameworks)
New things to learn — pace (foundational frameworks)
Novel frameworks and algorithms powering an entirely on-device voice+action Mac assistant.
This is the foundational-frameworks page — the external SDKs and algorithms Pace is built on. The rest of the roadmap lives in
README.md, which indexes the themed pages covering Pace’s own architecture (memory, vision, actions, audio, planning, skills, proactive, infra). Concepts explained here are not re-explained elsewhere — themed pages link back with a one-line pointer.
Apple FoundationModels (Apple Intelligence SDK)
- What: In-process, on-device LLM API introduced in iOS/macOS 26 —
SystemLanguageModel,@Generabletyped outputs, no HTTP, no cloud. - Why here: The zero-install first-launch default planner tier and the fast path for pure-knowledge answers, the rolling thread summarizer, and privacy-pinned meeting-note/skill structuring — it is Pace’s on-device brain whenever no LM Studio sidecar is running.
- Gotcha (from code):
SystemLanguageModel.default.availabilitymust be checked at every call site — FM is absent on non-Apple-Intelligence Macs. Three check points:AppleFoundationModelsPlannerClient.swift:84(warmUp guard),PacePlannerTierStore.swift:174(first-launch tier resolution),BuddyPlannerClient.swift:354(factorymakeFoundationModelsPlannerOrFallback). Each has aLocalPlannerClientfallback; missing the check surfaces as a crash or silent no-op on non-eligible hardware. - Source: https://developer.apple.com/documentation/foundationmodels
MCP (Model Context Protocol)
- What: Spec for LLM-to-tool communication over stdio JSON-RPC, so AI apps share a common tool-call dialect instead of hand-rolling integrations.
- Why here: How Pace gets broad third-party integrations (Slack, GitHub, filesystem, fetch…) without rebuilding each one in-app — the planner calls OSS MCP servers through the same approval/observation loop as built-in tools.
- Gotcha (from code): Config is loaded from
~/.config/pace/mcp-servers.jsonat call time — missing server names are caught byPaceToolPreflight(PaceToolPreflight.swift) before approval, not at launch. Catalog installs use atomic temp-file + rename inPaceMCPCatalogInstallerto preserve every user-added entry on each install. - Source: https://modelcontextprotocol.io/specification
WhisperKit (on-device streaming ASR)
- What: Swift package (Argmax) wrapping OpenAI Whisper large-v3-turbo as CoreML for real-time, privacy-safe transcription on Apple Silicon; ~9x realtime on ANE.
- Why here: The on-device ASR upgrade path for long-form / meeting transcription — Apple Speech caps at ~1 minute, so meeting turns route through WhisperKit when
TranscriptionProvider=whisperKit, with nothing leaving the Mac. - Gotcha (from code): WhisperKit is fully wired as of the current codebase (
WhisperKitTranscriptionProvider.isRuntimeAvailable = true, line 37; full streaming session inWhisperKitTranscriptionProvider.swift:56-246). The factory (BuddyTranscriptionProvider.swift) selects it whenTranscriptionProvider=whisperKitandisRuntimeAvailableis true; the Apple Speech fallback only fires when the flag is false. Model must be pre-placed at~/Documents/huggingface/models/argmaxinc/whisperkit-coreml/<model>/—download: falsemeans a missing model throws immediately rather than fetching silently. - Source: https://github.com/argmaxinc/WhisperKit
Kokoro TTS + mlx-audio sidecar
- What: Kokoro-82M is an 82M-parameter Apache-licensed TTS model (StyleTTS2 architecture); mlx-audio serves it as an OpenAI-compatible
/v1/audio/speechloopback sidecar (port 8880, ~150 ms warm latency, 24 kHz output). - Why here: The default TTS voice — natural enough to make the sub-500 ms spoken-reply target feel human — with
AVSpeechSynthesizeras the always-present per-utterance fallback so a missing sidecar costs nothing. - Gotcha (from code): Sidecar endpoint is guarded by
PaceLocalEndpointGuard.resolvedLocalOpenAICompatibleBaseURLatLocalServerTTSClient.swift:51— non-loopback URLs fall back tolocalhost:8880. First synth after boot is 20-30 s while Kokoro’s MLX weights load (LocalServerTTSClient.swift:29-30); the 60 s timeout accounts for this. Any synthesis failure falls through toAVSpeechSynthesizerper-utterance with a 30 s outage memo. - Source: https://huggingface.co/hexgrad/Kokoro-82M
Loopback-guard (TTS self-capture prevention)
- What: Design constraint that ensures audio output from Pace’s own TTS sidecar is never re-captured as microphone input.
- Why here: The enforcement mechanism behind the “0 bytes off this Mac” privacy moat — every model sidecar URL is pinned to loopback so a misconfigured LAN/remote host can’t leak screen or audio data.
- Gotcha (from code): The URL-level guard (
PaceLocalEndpointGuard,LocalServerTTSClient.swift:51) ensures audio synthesis only ever calls a loopback address. The audio pipeline exclusion zone (preventingAVAudioEnginefrom capturingAVSpeechSynthesizeror sidecar output) is architecturally noted but not yet implemented — it is the remaining open trust surface for self-capture prevention. - Source: internal architecture — no single external spec; closest is Apple’s AVAudioSession documentation.
ScreenCaptureKit
- What: Apple’s modern (macOS 13.0+, with enhanced per-display filters on macOS 14.2+) screen-capture framework — replaces
CGWindowListCreateImage, runs in-process, permission-gated. - Why here: Provides both the multi-monitor screenshots the VLM reads for screen-aware actions AND the system-audio track (SCStream audio output) that on-device meeting notes record.
- Gotcha (from code): Pace requires macOS 14.2+ (
CompanionScreenCaptureUtility.swift) for reliable multi-monitor display enumeration. Screen Recording permission must be granted; the app linksWindowPositionManager.swiftto surface the System Settings deep link when the permission is missing. - Source: https://developer.apple.com/documentation/screencapturekit
Accessibility API (AX / NSAccessibility)
- What: macOS accessibility tree API — lets apps read UI element roles/labels and synthesize actions (press, set-value) without CGEvent coordinate clicks.
- Why here: The primary, most-reliable click path (press by role, not by pixel) and the only reversible one — set-value edits are journaled for undo, which raw CGEvent clicks can’t offer.
- Gotcha (from code):
PaceAXTargeter.swiftclimbs fromAXUIElementCopyElementAtPositionup to a pressable role before firingkAXPressAction; if no pressable ancestor is found it returns false andPaceActionExecutorfalls back to CGEvent. AXset-valueedits are journaled in a session-local mutation log and reversible viaUndo.last— the mutation log is the trust surface because the AX API itself has no undo primitive. - Source: https://developer.apple.com/documentation/appkit/nsaccessibility
LM Studio (OpenAI-compatible local server)
- What: Desktop app that serves local LLMs/VLMs behind an OpenAI-compatible HTTP API on
localhost:1234; exposes/v1/chat/completions,/v1/embeddings,/v1/completions. - Why here: The default local planner/VLM host for users without Apple Intelligence — serves the qwen3-30b-a3b planner and the UI-Venus VLM over a loopback OpenAI-compatible API, and the
/v1/embeddingsroute used for memory reranking. - Gotcha (from code):
PaceLocalEndpointGuard.resolvedLocalOpenAICompatibleBaseURL(PaceLocalEndpointGuard.swift:23) validates every planner and VLM URL at construction time — a plist value pointing at a LAN or remote host silently falls back tolocalhost:1234with a printed warning. Two models can be loaded simultaneously via LM Studio Settings → max-loaded-models=2 (planner + VLM) to avoid thrashing. - Source: https://lmstudio.ai/docs/api/openai-api
LM Studio JIT loading + idle-unload keepalive
- What: LM Studio loads a configured model on first request (JIT) rather than at server start, and separately auto-unloads a model after an idle period to free RAM — both save memory but cost latency (a cold-load tax) or correctness (mid-session eviction) if the app doesn’t account for them.
- Why here: Pace pays the cold-load tax once, deliberately, at app launch instead of on the user’s first real turn — and then fights the idle-unload timer for the rest of the session so a long-idle-then-resume conversation doesn’t silently hit a partially-unloaded model.
- Where:
PaceLMStudioModelLoader.swift— launch does a throwaway chat-completion per configured model to trigger JIT load and warm it (comment: “the user’s first PTT turn doesn’t pay the cold-load tax, typically 5-15s on a 14B class model”);startKeepaliveLoopIfNotRunning()(line ~242) pings each model everykeepaliveIntervalSeconds = 60with amax_tokens: 1completion, because “eval runs showed the model state degrading turn-over-turn… LM Studio was partial-unloading between calls”; quit unloads explicitly via thelms unload <model>CLI so 5-20 GB of weights don’t stay resident after Pace exits. - Source: https://lmstudio.ai/docs/api/ttl-and-auto-evict
BM25 (lexical retrieval ranking)
- What: Probabilistic term-frequency/inverse-document-frequency ranking algorithm (Okapi BM25) — fast, no embeddings needed, interpretable scores; standard baseline for sparse retrieval in RAG systems.
- Why here: The production ranking path for all local recall (“what did I do today?”, notes, calendar, screen-watch/app-usage journals) — instant and embedding-free, with an optional embedding rerank layered on the top-K.
- Gotcha (from code):
PaceLocalRetrieval.swiftuses a BM25-style in-memory scorer across all retrieval sources (history, notes, calendar, files, screen-watch, app-usage) before optional embedding re-ranking viaPaceEmbeddingReranker(LM Studio/v1/embeddings). Vector/SQLite-vec retrieval is queued but not yet live — BM25 is the only ranking path in production. - Source: https://en.wikipedia.org/wiki/Okapi_BM25
@Generable typed outputs (constrained decoding via FoundationModels)
- What: Swift macro from the FoundationModels framework that generates a grammar-constrained decoder for a
struct— the model can only produce tokens that form valid JSON matching the schema, so hallucinated field values become structurally impossible. - Why here: Eliminates coordinate hallucination on the Apple FM planner tier — the model picks integer element IDs from the on-screen element list instead of writing raw
[CLICK:x,y]coordinates. - Gotcha (from code):
PaceFMTurnResponse(PaceFMTurnResponse.swift:40) replaces freeform[CLICK:x,y]string tags with integer element IDs (pointAtElementId,clickElementId). The model picks from the on-screen element list or returns -1; it never writes raw coordinates, eliminating the coordinate hallucination failure mode of the string-tag protocol. Trade-off:respond(to:generating:)is non-streaming — the fullspokenTextarrives as one chunk, slightly increasing TTFSW vs the streaming string path. - Source: https://developer.apple.com/documentation/foundationmodels
Constrained decoding / structured outputs (response_format: json_schema)
- What: A grammar-level constraint passed to an OpenAI-compatible
/v1/chat/completionscall —response_format: {"type": "json_schema", "json_schema": {...}}— that forces the decoder to only emit tokens forming valid JSON matching the given schema, instead of hoping the model’s prose happens to contain parseable JSON. - Why here: The planner’s v10 envelope (
spokenText+intentenum + free-formpayload) is pinned this way so LM Studio’s decoder literally cannot emit an unparseable reply — the schema’s enum values and field names are prompt surface the model reads, so a schema that can’t express something becomes a model behavior problem, not just a parsing problem. - Gotcha (from code):
LocalPlannerClient.v10ResponseFormat(LocalPlannerClient.swift:96-115) is the constrained schema used for planner turns. LM Studio’s MLX engine does NOT accept everyresponse_formatvalue, though —LocalVLMClient.swift:366-368documents that the VLM call deliberately sends noresponse_formatat all because MLX returns HTTP 400 on"type": "json_object"(it only accepts"json_schema"with a real schema, or"text"); the VLM path instead relies on a regex-extract fallback to pull JSON out of unstructured prose. Same runtime, two different constraint strategies depending on what the engine will actually accept. - Source: https://platform.openai.com/docs/guides/structured-outputs and https://lmstudio.ai/docs/api/structured-output
Speculative planner race
- What: Running a fast “lite” planner (Apple FM, no VLM) in parallel with the full VLM-fed planner, letting whichever produces its first token first win the TTS stream.
- Why here: The trick that hits sub-500 ms spoken latency on screen turns — the VLM-free Apple FM “lite” planner speaks in ~150 ms while a cold VLM (2-3 s) is still resolving, then the full path supersedes if its first token lands fast enough.
- Gotcha (from code):
PaceSpeculativePlannerRace.raceSpeculative(PaceSpeculativePlannerRace.swift:130) always returns BOTH planners’ complete text (PaceSpeculativeRaceResult.fullPlannerResponseText); action parsing inCompanionManagerexclusively uses the full planner’s text regardless of which path won audio (lines 187-194). Supersede window: 500 ms from lite’s first token AND fewer than 60 spoken chars — past either threshold the lite winner stands to avoid mid-speech jarring cuts. Gate (speculativeRaceShouldFire,CompanionManager.swift:441) requires toggle ON + local VLM configured + Apple FM available + intent is.screenActionor.screenDescription. - Source:
PaceSpeculativePlannerRace.swift,CompanionManager.swift