Align skill with canonical Agent-Native Architecture document: ## Core Changes - Restructure SKILL.md with 5 named principles from canonical: - Parity: Agent can do whatever user can do - Granularity: Prefer atomic primitives - Composability: Features are prompts - Emergent Capability: Handle unanticipated requests - Improvement Over Time: Context accumulation - Add "The test" for each principle - Add "Why Now" section (Claude Code origin story) - Update terminology from "prompt-native" to "agent-native" - Add "The Ultimate Test" to success criteria ## New Reference Files - files-universal-interface.md: Why files, organization patterns, context.md pattern, conflict model - from-primitives-to-domain-tools.md: When to add domain tools, graduating to code - agent-execution-patterns.md: Completion signals, partial completion, context limits - product-implications.md: Progressive disclosure, latent demand discovery, approval matrix ## Updated Reference Files - mobile-patterns.md: Add iOS storage architecture (iCloud-first), "needs validation" callouts, on-device vs cloud section - architecture-patterns.md: Update overview to reference 5 principles and cross-link new files ## Anti-Patterns - Add missing anti-patterns: agent as router, build-then-add-agent, request/response thinking, defensive tool design, happy path in code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
872 lines
25 KiB
Markdown
872 lines
25 KiB
Markdown
<overview>
|
|
Mobile is a first-class platform for agent-native apps. It has unique constraints and opportunities. This guide covers why mobile matters, iOS storage architecture, checkpoint/resume patterns, and cost-aware design.
|
|
</overview>
|
|
|
|
<why_mobile>
|
|
## Why Mobile Matters
|
|
|
|
Mobile devices offer unique advantages for agent-native apps:
|
|
|
|
### A File System
|
|
Agents can work with files naturally, using the same primitives that work everywhere else. The filesystem is the universal interface.
|
|
|
|
### Rich Context
|
|
A walled garden you get access to. Health data, location, photos, calendars—context that doesn't exist on desktop or web. This enables deeply personalized agent experiences.
|
|
|
|
### Local Apps
|
|
Everyone has their own copy of the app. This opens opportunities that aren't fully realized yet: apps that modify themselves, fork themselves, evolve per-user. App Store policies constrain some of this today, but the foundation is there.
|
|
|
|
### Cross-Device Sync
|
|
If you use the file system with iCloud, all devices share the same file system. The agent's work on one device appears on all devices—without you having to build a server.
|
|
|
|
### The Challenge
|
|
|
|
**Agents are long-running. Mobile apps are not.**
|
|
|
|
An agent might need 30 seconds, 5 minutes, or an hour to complete a task. But iOS will background your app after seconds of inactivity, and may kill it entirely to reclaim memory. The user might switch apps, take a call, or lock their phone mid-task.
|
|
|
|
This means mobile agent apps need:
|
|
- **Checkpointing** — Saving state so work isn't lost
|
|
- **Resuming** — Picking up where you left off after interruption
|
|
- **Background execution** — Using the limited time iOS gives you wisely
|
|
- **On-device vs. cloud decisions** — What runs locally vs. what needs a server
|
|
</why_mobile>
|
|
|
|
<ios_storage>
|
|
## iOS Storage Architecture
|
|
|
|
> **Needs validation:** This is an approach that works well, but better solutions may exist.
|
|
|
|
For agent-native iOS apps, use iCloud Drive's Documents folder for your shared workspace. This gives you **free, automatic multi-device sync** without building a sync layer or running a server.
|
|
|
|
### Why iCloud Documents?
|
|
|
|
| Approach | Cost | Complexity | Offline | Multi-Device |
|
|
|----------|------|------------|---------|--------------|
|
|
| Custom backend + sync | $$$ | High | Manual | Yes |
|
|
| CloudKit database | Free tier limits | Medium | Manual | Yes |
|
|
| **iCloud Documents** | Free (user's storage) | Low | Automatic | Automatic |
|
|
|
|
iCloud Documents:
|
|
- Uses user's existing iCloud storage (free 5GB, most users have more)
|
|
- Automatic sync across all user's devices
|
|
- Works offline, syncs when online
|
|
- Files visible in Files.app for transparency
|
|
- No server costs, no sync code to maintain
|
|
|
|
### Implementation: iCloud-First with Local Fallback
|
|
|
|
```swift
|
|
// Get the iCloud Documents container
|
|
func iCloudDocumentsURL() -> URL? {
|
|
FileManager.default.url(forUbiquityContainerIdentifier: nil)?
|
|
.appendingPathComponent("Documents")
|
|
}
|
|
|
|
// Your shared workspace lives in iCloud
|
|
class SharedWorkspace {
|
|
let rootURL: URL
|
|
|
|
init() {
|
|
// Use iCloud if available, fall back to local
|
|
if let iCloudURL = iCloudDocumentsURL() {
|
|
self.rootURL = iCloudURL
|
|
} else {
|
|
// Fallback to local Documents (user not signed into iCloud)
|
|
self.rootURL = FileManager.default.urls(
|
|
for: .documentDirectory,
|
|
in: .userDomainMask
|
|
).first!
|
|
}
|
|
}
|
|
|
|
// All file operations go through this root
|
|
func researchPath(for bookId: String) -> URL {
|
|
rootURL.appendingPathComponent("Research/\(bookId)")
|
|
}
|
|
|
|
func journalPath() -> URL {
|
|
rootURL.appendingPathComponent("Journal")
|
|
}
|
|
}
|
|
```
|
|
|
|
### Directory Structure in iCloud
|
|
|
|
```
|
|
iCloud Drive/
|
|
└── YourApp/ # Your app's container
|
|
└── Documents/ # Visible in Files.app
|
|
├── Journal/
|
|
│ ├── user/
|
|
│ │ └── 2025-01-15.md # Syncs across devices
|
|
│ └── agent/
|
|
│ └── 2025-01-15.md # Agent observations sync too
|
|
├── Research/
|
|
│ └── {bookId}/
|
|
│ ├── full_text.txt
|
|
│ └── sources/
|
|
├── Chats/
|
|
│ └── {conversationId}.json
|
|
└── context.md # Agent's accumulated knowledge
|
|
```
|
|
|
|
### Handling iCloud File States
|
|
|
|
iCloud files may not be downloaded locally. Handle this:
|
|
|
|
```swift
|
|
func readFile(at url: URL) throws -> String {
|
|
// iCloud may create .icloud placeholder files
|
|
if url.pathExtension == "icloud" {
|
|
// Trigger download
|
|
try FileManager.default.startDownloadingUbiquitousItem(at: url)
|
|
throw FileNotYetAvailableError()
|
|
}
|
|
|
|
return try String(contentsOf: url, encoding: .utf8)
|
|
}
|
|
|
|
// For writes, use coordinated file access
|
|
func writeFile(_ content: String, to url: URL) throws {
|
|
let coordinator = NSFileCoordinator()
|
|
var error: NSError?
|
|
|
|
coordinator.coordinate(
|
|
writingItemAt: url,
|
|
options: .forReplacing,
|
|
error: &error
|
|
) { newURL in
|
|
try? content.write(to: newURL, atomically: true, encoding: .utf8)
|
|
}
|
|
|
|
if let error = error { throw error }
|
|
}
|
|
```
|
|
|
|
### What iCloud Enables
|
|
|
|
1. **User starts experiment on iPhone** → Agent creates config file
|
|
2. **User opens app on iPad** → Same experiment visible, no sync code needed
|
|
3. **Agent logs observation on iPhone** → Syncs to iPad automatically
|
|
4. **User edits journal on iPad** → iPhone sees the edit
|
|
|
|
### Entitlements Required
|
|
|
|
Add to your app's entitlements:
|
|
|
|
```xml
|
|
<key>com.apple.developer.icloud-container-identifiers</key>
|
|
<array>
|
|
<string>iCloud.com.yourcompany.yourapp</string>
|
|
</array>
|
|
<key>com.apple.developer.icloud-services</key>
|
|
<array>
|
|
<string>CloudDocuments</string>
|
|
</array>
|
|
<key>com.apple.developer.ubiquity-container-identifiers</key>
|
|
<array>
|
|
<string>iCloud.com.yourcompany.yourapp</string>
|
|
</array>
|
|
```
|
|
|
|
### When NOT to Use iCloud Documents
|
|
|
|
- **Sensitive data** - Use Keychain or encrypted local storage instead
|
|
- **High-frequency writes** - iCloud sync has latency; use local + periodic sync
|
|
- **Large media files** - Consider CloudKit Assets or on-demand resources
|
|
- **Shared between users** - iCloud Documents is single-user; use CloudKit for sharing
|
|
</ios_storage>
|
|
|
|
<background_execution>
|
|
## Background Execution & Resumption
|
|
|
|
> **Needs validation:** These patterns work but better solutions may exist.
|
|
|
|
Mobile apps can be suspended or terminated at any time. Agents must handle this gracefully.
|
|
|
|
### The Challenge
|
|
|
|
```
|
|
User starts research agent
|
|
↓
|
|
Agent begins web search
|
|
↓
|
|
User switches to another app
|
|
↓
|
|
iOS suspends your app
|
|
↓
|
|
Agent is mid-execution... what happens?
|
|
```
|
|
|
|
### Checkpoint/Resume Pattern
|
|
|
|
Save agent state before backgrounding, restore on foreground:
|
|
|
|
```swift
|
|
class AgentOrchestrator: ObservableObject {
|
|
@Published var activeSessions: [AgentSession] = []
|
|
|
|
// Called when app is about to background
|
|
func handleAppWillBackground() {
|
|
for session in activeSessions {
|
|
saveCheckpoint(session)
|
|
session.transition(to: .backgrounded)
|
|
}
|
|
}
|
|
|
|
// Called when app returns to foreground
|
|
func handleAppDidForeground() {
|
|
for session in activeSessions where session.state == .backgrounded {
|
|
if let checkpoint = loadCheckpoint(session.id) {
|
|
resumeFromCheckpoint(session, checkpoint)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func saveCheckpoint(_ session: AgentSession) {
|
|
let checkpoint = AgentCheckpoint(
|
|
sessionId: session.id,
|
|
conversationHistory: session.messages,
|
|
pendingToolCalls: session.pendingToolCalls,
|
|
partialResults: session.partialResults,
|
|
timestamp: Date()
|
|
)
|
|
storage.save(checkpoint, for: session.id)
|
|
}
|
|
|
|
private func resumeFromCheckpoint(_ session: AgentSession, _ checkpoint: AgentCheckpoint) {
|
|
session.messages = checkpoint.conversationHistory
|
|
session.pendingToolCalls = checkpoint.pendingToolCalls
|
|
|
|
// Resume execution if there were pending tool calls
|
|
if !checkpoint.pendingToolCalls.isEmpty {
|
|
session.transition(to: .running)
|
|
Task { await executeNextTool(session) }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### State Machine for Agent Lifecycle
|
|
|
|
```swift
|
|
enum AgentState {
|
|
case idle // Not running
|
|
case running // Actively executing
|
|
case waitingForUser // Paused, waiting for user input
|
|
case backgrounded // App backgrounded, state saved
|
|
case completed // Finished successfully
|
|
case failed(Error) // Finished with error
|
|
}
|
|
|
|
class AgentSession: ObservableObject {
|
|
@Published var state: AgentState = .idle
|
|
|
|
func transition(to newState: AgentState) {
|
|
let validTransitions: [AgentState: Set<AgentState>] = [
|
|
.idle: [.running],
|
|
.running: [.waitingForUser, .backgrounded, .completed, .failed],
|
|
.waitingForUser: [.running, .backgrounded],
|
|
.backgrounded: [.running, .completed],
|
|
]
|
|
|
|
guard validTransitions[state]?.contains(newState) == true else {
|
|
logger.warning("Invalid transition: \(state) → \(newState)")
|
|
return
|
|
}
|
|
|
|
state = newState
|
|
}
|
|
}
|
|
```
|
|
|
|
### Background Task Extension (iOS)
|
|
|
|
Request extra time when backgrounded during critical operations:
|
|
|
|
```swift
|
|
class AgentOrchestrator {
|
|
private var backgroundTask: UIBackgroundTaskIdentifier = .invalid
|
|
|
|
func handleAppWillBackground() {
|
|
// Request extra time for saving state
|
|
backgroundTask = UIApplication.shared.beginBackgroundTask { [weak self] in
|
|
self?.endBackgroundTask()
|
|
}
|
|
|
|
// Save all checkpoints
|
|
Task {
|
|
for session in activeSessions {
|
|
await saveCheckpoint(session)
|
|
}
|
|
endBackgroundTask()
|
|
}
|
|
}
|
|
|
|
private func endBackgroundTask() {
|
|
if backgroundTask != .invalid {
|
|
UIApplication.shared.endBackgroundTask(backgroundTask)
|
|
backgroundTask = .invalid
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### User Communication
|
|
|
|
Let users know what's happening:
|
|
|
|
```swift
|
|
struct AgentStatusView: View {
|
|
@ObservedObject var session: AgentSession
|
|
|
|
var body: some View {
|
|
switch session.state {
|
|
case .backgrounded:
|
|
Label("Paused (app in background)", systemImage: "pause.circle")
|
|
.foregroundColor(.orange)
|
|
case .running:
|
|
Label("Working...", systemImage: "ellipsis.circle")
|
|
.foregroundColor(.blue)
|
|
case .waitingForUser:
|
|
Label("Waiting for your input", systemImage: "person.circle")
|
|
.foregroundColor(.green)
|
|
// ...
|
|
}
|
|
}
|
|
}
|
|
```
|
|
</background_execution>
|
|
|
|
<permissions>
|
|
## Permission Handling
|
|
|
|
Mobile agents may need access to system resources. Handle permission requests gracefully.
|
|
|
|
### Common Permissions
|
|
|
|
| Resource | iOS Permission | Use Case |
|
|
|----------|---------------|----------|
|
|
| Photo Library | PHPhotoLibrary | Profile generation from photos |
|
|
| Files | Document picker | Reading user documents |
|
|
| Camera | AVCaptureDevice | Scanning book covers |
|
|
| Location | CLLocationManager | Location-aware recommendations |
|
|
| Network | (automatic) | Web search, API calls |
|
|
|
|
### Permission-Aware Tools
|
|
|
|
Check permissions before executing:
|
|
|
|
```swift
|
|
struct PhotoTools {
|
|
static func readPhotos() -> AgentTool {
|
|
tool(
|
|
name: "read_photos",
|
|
description: "Read photos from the user's photo library",
|
|
parameters: [
|
|
"limit": .number("Maximum photos to read"),
|
|
"dateRange": .string("Date range filter").optional()
|
|
],
|
|
execute: { params, context in
|
|
// Check permission first
|
|
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
|
|
|
|
switch status {
|
|
case .authorized, .limited:
|
|
// Proceed with reading photos
|
|
let photos = await fetchPhotos(params)
|
|
return ToolResult(text: "Found \(photos.count) photos", images: photos)
|
|
|
|
case .denied, .restricted:
|
|
return ToolResult(
|
|
text: "Photo access needed. Please grant permission in Settings → Privacy → Photos.",
|
|
isError: true
|
|
)
|
|
|
|
case .notDetermined:
|
|
return ToolResult(
|
|
text: "Photo permission required. Please try again.",
|
|
isError: true
|
|
)
|
|
|
|
@unknown default:
|
|
return ToolResult(text: "Unknown permission status", isError: true)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Graceful Degradation
|
|
|
|
When permissions aren't granted, offer alternatives:
|
|
|
|
```swift
|
|
func readPhotos() async -> ToolResult {
|
|
let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
|
|
|
switch status {
|
|
case .denied, .restricted:
|
|
// Suggest alternative
|
|
return ToolResult(
|
|
text: """
|
|
I don't have access to your photos. You can either:
|
|
1. Grant access in Settings → Privacy → Photos
|
|
2. Share specific photos directly in our chat
|
|
|
|
Would you like me to help with something else instead?
|
|
""",
|
|
isError: false // Not a hard error, just a limitation
|
|
)
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
### Permission Request Timing
|
|
|
|
Don't request permissions until needed:
|
|
|
|
```swift
|
|
// BAD: Request all permissions at launch
|
|
func applicationDidFinishLaunching() {
|
|
requestPhotoAccess()
|
|
requestCameraAccess()
|
|
requestLocationAccess()
|
|
// User is overwhelmed with permission dialogs
|
|
}
|
|
|
|
// GOOD: Request when the feature is used
|
|
tool("analyze_book_cover", async ({ image }) => {
|
|
// Only request camera access when user tries to scan a cover
|
|
let status = await AVCaptureDevice.requestAccess(for: .video)
|
|
if status {
|
|
return await scanCover(image)
|
|
} else {
|
|
return ToolResult(text: "Camera access needed for book scanning")
|
|
}
|
|
})
|
|
```
|
|
</permissions>
|
|
|
|
<cost_awareness>
|
|
## Cost-Aware Design
|
|
|
|
Mobile users may be on cellular data or concerned about API costs. Design agents to be efficient.
|
|
|
|
### Model Tier Selection
|
|
|
|
Use the cheapest model that achieves the outcome:
|
|
|
|
```swift
|
|
enum ModelTier {
|
|
case fast // claude-3-haiku: ~$0.25/1M tokens
|
|
case balanced // claude-3-sonnet: ~$3/1M tokens
|
|
case powerful // claude-3-opus: ~$15/1M tokens
|
|
|
|
var modelId: String {
|
|
switch self {
|
|
case .fast: return "claude-3-haiku-20240307"
|
|
case .balanced: return "claude-3-sonnet-20240229"
|
|
case .powerful: return "claude-3-opus-20240229"
|
|
}
|
|
}
|
|
}
|
|
|
|
// Match model to task complexity
|
|
let agentConfigs: [AgentType: ModelTier] = [
|
|
.quickLookup: .fast, // "What's in my library?"
|
|
.chatAssistant: .balanced, // General conversation
|
|
.researchAgent: .balanced, // Web search + synthesis
|
|
.profileGenerator: .powerful, // Complex photo analysis
|
|
.introductionWriter: .balanced,
|
|
]
|
|
```
|
|
|
|
### Token Budgets
|
|
|
|
Limit tokens per agent session:
|
|
|
|
```swift
|
|
struct AgentConfig {
|
|
let modelTier: ModelTier
|
|
let maxInputTokens: Int
|
|
let maxOutputTokens: Int
|
|
let maxTurns: Int
|
|
|
|
static let research = AgentConfig(
|
|
modelTier: .balanced,
|
|
maxInputTokens: 50_000,
|
|
maxOutputTokens: 4_000,
|
|
maxTurns: 20
|
|
)
|
|
|
|
static let quickChat = AgentConfig(
|
|
modelTier: .fast,
|
|
maxInputTokens: 10_000,
|
|
maxOutputTokens: 1_000,
|
|
maxTurns: 5
|
|
)
|
|
}
|
|
|
|
class AgentSession {
|
|
var totalTokensUsed: Int = 0
|
|
|
|
func checkBudget() -> Bool {
|
|
if totalTokensUsed > config.maxInputTokens {
|
|
transition(to: .failed(AgentError.budgetExceeded))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
```
|
|
|
|
### Network-Aware Execution
|
|
|
|
Defer heavy operations to WiFi:
|
|
|
|
```swift
|
|
class NetworkMonitor: ObservableObject {
|
|
@Published var isOnWiFi: Bool = false
|
|
@Published var isExpensive: Bool = false // Cellular or hotspot
|
|
|
|
private let monitor = NWPathMonitor()
|
|
|
|
func startMonitoring() {
|
|
monitor.pathUpdateHandler = { [weak self] path in
|
|
DispatchQueue.main.async {
|
|
self?.isOnWiFi = path.usesInterfaceType(.wifi)
|
|
self?.isExpensive = path.isExpensive
|
|
}
|
|
}
|
|
monitor.start(queue: .global())
|
|
}
|
|
}
|
|
|
|
class AgentOrchestrator {
|
|
@ObservedObject var network = NetworkMonitor()
|
|
|
|
func startResearchAgent(for book: Book) async {
|
|
if network.isExpensive {
|
|
// Warn user or defer
|
|
let proceed = await showAlert(
|
|
"Research uses data",
|
|
message: "This will use approximately 1-2 MB of cellular data. Continue?"
|
|
)
|
|
if !proceed { return }
|
|
}
|
|
|
|
// Proceed with research
|
|
await runAgent(ResearchAgent.create(book: book))
|
|
}
|
|
}
|
|
```
|
|
|
|
### Batch API Calls
|
|
|
|
Combine multiple small requests:
|
|
|
|
```swift
|
|
// BAD: Many small API calls
|
|
for book in books {
|
|
await agent.chat("Summarize \(book.title)")
|
|
}
|
|
|
|
// GOOD: Batch into one request
|
|
let bookList = books.map { $0.title }.joined(separator: ", ")
|
|
await agent.chat("Summarize each of these books briefly: \(bookList)")
|
|
```
|
|
|
|
### Caching
|
|
|
|
Cache expensive operations:
|
|
|
|
```swift
|
|
class ResearchCache {
|
|
private var cache: [String: CachedResearch] = [:]
|
|
|
|
func getCachedResearch(for bookId: String) -> CachedResearch? {
|
|
guard let cached = cache[bookId] else { return nil }
|
|
|
|
// Expire after 24 hours
|
|
if Date().timeIntervalSince(cached.timestamp) > 86400 {
|
|
cache.removeValue(forKey: bookId)
|
|
return nil
|
|
}
|
|
|
|
return cached
|
|
}
|
|
|
|
func cacheResearch(_ research: Research, for bookId: String) {
|
|
cache[bookId] = CachedResearch(
|
|
research: research,
|
|
timestamp: Date()
|
|
)
|
|
}
|
|
}
|
|
|
|
// In research tool
|
|
tool("web_search", async ({ query, bookId }) => {
|
|
// Check cache first
|
|
if let cached = cache.getCachedResearch(for: bookId) {
|
|
return ToolResult(text: cached.research.summary, cached: true)
|
|
}
|
|
|
|
// Otherwise, perform search
|
|
let results = await webSearch(query)
|
|
cache.cacheResearch(results, for: bookId)
|
|
return ToolResult(text: results.summary)
|
|
})
|
|
```
|
|
|
|
### Cost Visibility
|
|
|
|
Show users what they're spending:
|
|
|
|
```swift
|
|
struct AgentCostView: View {
|
|
@ObservedObject var session: AgentSession
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading) {
|
|
Text("Session Stats")
|
|
.font(.headline)
|
|
|
|
HStack {
|
|
Label("\(session.turnCount) turns", systemImage: "arrow.2.squarepath")
|
|
Spacer()
|
|
Label(formatTokens(session.totalTokensUsed), systemImage: "text.word.spacing")
|
|
}
|
|
|
|
if let estimatedCost = session.estimatedCost {
|
|
Text("Est. cost: \(estimatedCost, format: .currency(code: "USD"))")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
</cost_awareness>
|
|
|
|
<offline_handling>
|
|
## Offline Graceful Degradation
|
|
|
|
Handle offline scenarios gracefully:
|
|
|
|
```swift
|
|
class ConnectivityAwareAgent {
|
|
@ObservedObject var network = NetworkMonitor()
|
|
|
|
func executeToolCall(_ toolCall: ToolCall) async -> ToolResult {
|
|
// Check if tool requires network
|
|
let requiresNetwork = ["web_search", "web_fetch", "call_api"]
|
|
.contains(toolCall.name)
|
|
|
|
if requiresNetwork && !network.isConnected {
|
|
return ToolResult(
|
|
text: """
|
|
I can't access the internet right now. Here's what I can do offline:
|
|
- Read your library and existing research
|
|
- Answer questions from cached data
|
|
- Write notes and drafts for later
|
|
|
|
Would you like me to try something that works offline?
|
|
""",
|
|
isError: false
|
|
)
|
|
}
|
|
|
|
return await executeOnline(toolCall)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Offline-First Tools
|
|
|
|
Some tools should work entirely offline:
|
|
|
|
```swift
|
|
let offlineTools: Set<String> = [
|
|
"read_file",
|
|
"write_file",
|
|
"list_files",
|
|
"read_library", // Local database
|
|
"search_local", // Local search
|
|
]
|
|
|
|
let onlineTools: Set<String> = [
|
|
"web_search",
|
|
"web_fetch",
|
|
"publish_to_cloud",
|
|
]
|
|
|
|
let hybridTools: Set<String> = [
|
|
"publish_to_feed", // Works offline, syncs later
|
|
]
|
|
```
|
|
|
|
### Queued Actions
|
|
|
|
Queue actions that require connectivity:
|
|
|
|
```swift
|
|
class OfflineQueue: ObservableObject {
|
|
@Published var pendingActions: [QueuedAction] = []
|
|
|
|
func queue(_ action: QueuedAction) {
|
|
pendingActions.append(action)
|
|
persist()
|
|
}
|
|
|
|
func processWhenOnline() {
|
|
network.$isConnected
|
|
.filter { $0 }
|
|
.sink { [weak self] _ in
|
|
self?.processPendingActions()
|
|
}
|
|
}
|
|
|
|
private func processPendingActions() {
|
|
for action in pendingActions {
|
|
Task {
|
|
try await execute(action)
|
|
remove(action)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
</offline_handling>
|
|
|
|
<battery_awareness>
|
|
## Battery-Aware Execution
|
|
|
|
Respect device battery state:
|
|
|
|
```swift
|
|
class BatteryMonitor: ObservableObject {
|
|
@Published var batteryLevel: Float = 1.0
|
|
@Published var isCharging: Bool = false
|
|
@Published var isLowPowerMode: Bool = false
|
|
|
|
var shouldDeferHeavyWork: Bool {
|
|
return batteryLevel < 0.2 && !isCharging
|
|
}
|
|
|
|
func startMonitoring() {
|
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
|
|
|
NotificationCenter.default.addObserver(
|
|
forName: UIDevice.batteryLevelDidChangeNotification,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] _ in
|
|
self?.batteryLevel = UIDevice.current.batteryLevel
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(
|
|
forName: NSNotification.Name.NSProcessInfoPowerStateDidChange,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] _ in
|
|
self?.isLowPowerMode = ProcessInfo.processInfo.isLowPowerModeEnabled
|
|
}
|
|
}
|
|
}
|
|
|
|
class AgentOrchestrator {
|
|
@ObservedObject var battery = BatteryMonitor()
|
|
|
|
func startAgent(_ config: AgentConfig) async {
|
|
if battery.shouldDeferHeavyWork && config.isHeavy {
|
|
let proceed = await showAlert(
|
|
"Low Battery",
|
|
message: "This task uses significant battery. Continue or defer until charging?"
|
|
)
|
|
if !proceed { return }
|
|
}
|
|
|
|
// Adjust model tier based on battery
|
|
let adjustedConfig = battery.isLowPowerMode
|
|
? config.withModelTier(.fast)
|
|
: config
|
|
|
|
await runAgent(adjustedConfig)
|
|
}
|
|
}
|
|
```
|
|
</battery_awareness>
|
|
|
|
<on_device_vs_cloud>
|
|
## On-Device vs. Cloud
|
|
|
|
Understanding what runs where in a mobile agent-native app:
|
|
|
|
| Component | On-Device | Cloud |
|
|
|-----------|-----------|-------|
|
|
| Orchestration | ✅ | |
|
|
| Tool execution | ✅ (file ops, photo access, HealthKit) | |
|
|
| LLM calls | | ✅ (Anthropic API) |
|
|
| Checkpoints | ✅ (local files) | Optional via iCloud |
|
|
| Long-running agents | Limited by iOS | Possible with server |
|
|
|
|
### Implications
|
|
|
|
**Network required for reasoning:**
|
|
- The app needs network connectivity for LLM calls
|
|
- Design tools to degrade gracefully when network is unavailable
|
|
- Consider offline caching for common queries
|
|
|
|
**Data stays local:**
|
|
- File operations happen on device
|
|
- Sensitive data never leaves the device unless explicitly synced
|
|
- Privacy is preserved by default
|
|
|
|
**Long-running agents:**
|
|
For truly long-running agents (hours), consider a server-side orchestrator that can run indefinitely, with the mobile app as a viewer and input mechanism.
|
|
</on_device_vs_cloud>
|
|
|
|
<checklist>
|
|
## Mobile Agent-Native Checklist
|
|
|
|
**iOS Storage:**
|
|
- [ ] iCloud Documents as primary storage (or conscious alternative)
|
|
- [ ] Local Documents fallback when iCloud unavailable
|
|
- [ ] Handle `.icloud` placeholder files (trigger download)
|
|
- [ ] Use NSFileCoordinator for conflict-safe writes
|
|
|
|
**Background Execution:**
|
|
- [ ] Checkpoint/resume implemented for all agent sessions
|
|
- [ ] State machine for agent lifecycle (idle, running, backgrounded, etc.)
|
|
- [ ] Background task extension for critical saves (30 second window)
|
|
- [ ] User-visible status for backgrounded agents
|
|
|
|
**Permissions:**
|
|
- [ ] Permissions requested only when needed, not at launch
|
|
- [ ] Graceful degradation when permissions denied
|
|
- [ ] Clear error messages with Settings deep links
|
|
- [ ] Alternative paths when permissions unavailable
|
|
|
|
**Cost Awareness:**
|
|
- [ ] Model tier matched to task complexity
|
|
- [ ] Token budgets per session
|
|
- [ ] Network-aware (defer heavy work to WiFi)
|
|
- [ ] Caching for expensive operations
|
|
- [ ] Cost visibility to users
|
|
|
|
**Offline Handling:**
|
|
- [ ] Offline-capable tools identified
|
|
- [ ] Graceful degradation for online-only features
|
|
- [ ] Action queue for sync when online
|
|
- [ ] Clear user communication about offline state
|
|
|
|
**Battery Awareness:**
|
|
- [ ] Battery monitoring for heavy operations
|
|
- [ ] Low power mode detection
|
|
- [ ] Defer or downgrade based on battery state
|
|
</checklist>
|