Skip to content

Agent Abilities — Architecture

The Agent screen is a distinct interaction mode from the chat interface. It uses a plan-driven execution model rather than a free-form tool loop.


Data Model

Ability

Sources/Models/Ability.swift

FieldTypePurpose
idUUIDStable identity — defaults use fixed UUIDs so reset/gallery detection works correctly
labelStringShort display name (≤12 chars) shown under the icon
iconStringSF Symbol name
colorHexStringHex color for icon background tint
promptStringFull instruction passed to the agent after variable resolution
planKeyRawString?Optional raw value mapping to AgentAbilityPlanKey
sortOrderIntDisplay order in the drawer
isDefaultBoolWhether this is a factory default (affects reset behavior)
preferredTimeSlotTimeSlot?Optional morning/afternoon/evening hint (used for sorting)

Default abilities use fixed stable UUIDs (A0000001-0000-0000-0000-00000000000X) so AbilityStore can detect them on reinstall and AbilityTemplateGallery can mark them as already installed.

AbilityTemplate

Sources/Models/AbilityTemplate.swift

Read-only catalog used by the template gallery. Maps to Ability via toAbility(sortOrder:). Has no UUID — a new UUID() is generated at install time.

AbilityStore

Sources/Services/AbilityStore.swift

ObservableObject backed by JSON persistence. Exposes:

  • sortedForDisplay() — sorted by sortOrder, with time-slot weighting at runtime
  • update(_:) / remove(id:) / reorder(fromID:toID:) — CRUD
  • resetToDefaults() — restores factory defaults by stable ID

Execution Flow

User taps ability


AbilityVariableResolver.resolve(prompt)
      │  resolves {{today}}, {{name}}, {{next_event}}, {{location}}

AgentViewModel.submitTask(prompt:planKey:)

      ├─ planKey != nil ──► AgentViewModel.abilityPlan(for:date:)
      │                          returns AgentAbilityPlan (ordered tool list)

      └─ planKey == nil ──► agent infers tools from the prompt


AgentViewModel.runAbilityPlan(task:plan:planKey:documents:)

      ├─ executes each tool step (calendar, health, gmail, weather, eta, notes, …)
      ├─ appends AgentStep with status .running → .done
      ├─ collects AgentObservationPayload from rich results


assemblePayload(from:planKey:)
      │  → structured AgentObservationPayload for UI cards

synthesis (cloud or on-device)
      │  → finalAnswer string

AgentAnswerSheetView (fullScreenCover)
      │  shows payload cards + plain text answer + original prompt echo

Plan Keys

AgentAbilityPlanKey (enum in AgentViewModel.swift) maps to structured tool sequences:

KeyTools calledNotes
morningBriefingcalendar, health, weather, gmailParallel execution
calendarTodaycalendarRich calendar payload
meetingsThisWeekcalendarWeek overview
freeSlotsTodaycalendarGap analysis
scheduleForFocuscalendar, habitsFinds deep-work block
healthTodayhealthFull metric grid
sleepLastNighthealth (sleep subset)Sleep-focused
gmailUrgentgmailAction-required only
gmailSummarizegmailFull inbox scan
draftReplygmailReads latest, synthesizes draft
habitsCheckhabitsHabit grid payload
logHabitshabitsRecords an entry
weatherNowweatherCurrent conditions
planDaycalendar, habits, healthMorning synthesis
planAfternooncalendar, habitsAfternoon block synthesis
whatToFocusOncalendar, habits, healthConvergence verdict
trafficToOfficeetaSaved destination from settings
notesSearchnotesCross-references today's calendar
reminderCreateremindersPrompts user, then sets

Variable Resolution

AbilityVariableResolver (Sources/Services/AbilityVariableResolver.swift) resolves template variables in the prompt before the agent sees it.

VariableResolutionCost
Full date stringSync, free
First name from UIDevice.current.nameSync, free
Next calendar event title via CalendarServiceAsync, cheap
Current city via LocationServiceAsync, GPS

Expensive variables are only resolved if the prompt string contains them — no unnecessary fetches.


Answer Sheet

AgentAnswerSheetView (Sources/Views/AgentView.swift:362) renders the result as a .sheet with presentationDetents([.medium, .large]) and presentationDragIndicator(.visible). The system drag indicator and swipe-to-dismiss are handled natively by SwiftUI — no custom gesture or dismiss button needed.

Key behaviors:

  • Echoes the original prompt at the top (SheetRequestBubble) so the sheet is self-contained without context from the main view.
  • Shows during streaming (isStreaming: viewModel.taskState == .synthesizing) — the sheet opens when currentPayload != nil even before synthesis completes.
  • AgentAnswerWidgetView renders rich payload cards (briefing tiles, calendar list, health grid).
  • Falls back to plain text FinalAnswerCard if no structured payload.
  • Opens at .medium detent; user can pull up to .large or pull down to dismiss.

Ability Drawer

AbilityDrawer / AbilityDockView (Sources/Views/AgentView.swift:690+)

The drawer uses three fixed detents: peek (200 pt), half (52% screen height), full (87% screen height). Height is calculated from UIScreen.main.bounds.heightnot GeometryReader — to prevent the overlay from intercepting orb touches.

Drag gesture uses velocity prediction (predictedEndTranslation) with a 0.3 damping factor for natural snapping.

Jiggle mode (iOS home screen style) is per-cell with randomized wobble amount, scale range, and y-shift so icons have distinct personalities. Animation phase is staggered per cell using a stored seed value.


Prompt Design Principles

The default and template prompts follow these rules:

  1. Name the data sources — the agent doesn't guess; it must be told which tools to call.
  2. Request synthesis, not retrieval — "compare to my 7-day average" vs. "show me my HRV."
  3. Specify output format — "three bullets" or "one sentence" constrains verbosity.
  4. End with an action — every prompt should produce something the user can act on.
  5. No vague quantifiers — "recent" is replaced with "last 7 days", "tomorrow morning" maps to 8am.

This makes prompts resilient to model variation — a well-constrained prompt produces a useful answer regardless of which model (local or cloud) executes it.


Custom Ability Sharing

AbilityShareSheet serializes the Ability struct to JSON and wraps it in a .lucidability file via UIActivityViewController. The receiver's app opens the file via a document import handler and calls AbilityStore.install(_:).

Internal — not for distribution