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.height — not 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