* [2.17.0] Expand agent-native skill with mobile app learnings Major expansion of agent-native-architecture skill based on real-world learnings from building the Every Reader iOS app. New reference documents: - dynamic-context-injection.md: Runtime app state in system prompts - action-parity-discipline.md: Ensuring agents can do what users can - shared-workspace-architecture.md: Agents and users in same data space - agent-native-testing.md: Testing patterns for agent-native apps - mobile-patterns.md: Background execution, permissions, cost awareness Updated references: - architecture-patterns.md: Added Unified Agent Architecture, Agent-to-UI Communication, and Model Tier Selection patterns Enhanced agent-native-reviewer with comprehensive review process covering all new patterns, including mobile-specific verification. Key insight: "The agent should be able to do anything the user can do, through tools that mirror UI capabilities, with full context about the app state." 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * [2.18.0] Add Dynamic Capability Discovery and iCloud sync patterns New patterns in agent-native-architecture skill: - **Dynamic Capability Discovery** - For agent-native apps integrating with external APIs (HealthKit, HomeKit, GraphQL), use a discovery tool (list_*) plus a generic access tool instead of individual tools per endpoint. (Note: Static mapping is fine for constrained agents with limited scope.) - **CRUD Completeness** - Every entity needs create, read, update, AND delete. - **iCloud File Storage** - Use iCloud Documents for shared workspace to get free, automatic multi-device sync without building a sync layer. - **Architecture Review Checklist** - Pushes reviewer findings earlier into design phase. Covers tool design, action parity, UI integration, context. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
20 KiB
Core principle: The agent operates in the same filesystem as the user, not a walled garden.
<why_shared_workspace>
Why Shared Workspace?
The Sandbox Anti-Pattern
Many agent implementations isolate the agent:
┌─────────────────┐ ┌─────────────────┐
│ User Space │ │ Agent Space │
├─────────────────┤ ├─────────────────┤
│ Documents/ │ │ agent_output/ │
│ user_files/ │ ←→ │ temp_files/ │
│ settings.json │sync │ cache/ │
└─────────────────┘ └─────────────────┘
Problems:
- Need a sync layer to move data between spaces
- User can't easily inspect agent work
- Agent can't build on user contributions
- Duplication of state
- Complexity in keeping spaces consistent
The Shared Workspace Pattern
┌─────────────────────────────────────────┐
│ Shared Workspace │
├─────────────────────────────────────────┤
│ Documents/ │
│ ├── Research/ │
│ │ └── {bookId}/ ← Agent writes │
│ │ ├── full_text.txt │
│ │ ├── introduction.md ← User can edit │
│ │ └── sources/ │
│ ├── Chats/ ← Both read/write │
│ └── profile.md ← Agent generates, user refines │
└─────────────────────────────────────────┘
↑ ↑
User Agent
(UI) (Tools)
Benefits:
- Users can inspect, edit, and extend agent work
- Agents can build on user contributions
- No synchronization layer needed
- Complete transparency
- Single source of truth </why_shared_workspace>
<directory_structure>
Designing Your Shared Workspace
Structure by Domain
Organize by what the data represents, not who created it:
Documents/
├── Research/
│ └── {bookId}/
│ ├── full_text.txt # Agent downloads
│ ├── introduction.md # Agent generates, user can edit
│ ├── notes.md # User adds, agent can read
│ └── sources/
│ └── {source}.md # Agent gathers
├── Chats/
│ └── {conversationId}.json # Both read/write
├── Exports/
│ └── {date}/ # Agent generates for user
└── profile.md # Agent generates from photos
Don't Structure by Actor
# BAD - Separates by who created it
Documents/
├── user_created/
│ └── notes.md
├── agent_created/
│ └── research.md
└── system/
└── config.json
This creates artificial boundaries and makes collaboration harder.
Use Conventions for Metadata
If you need to track who created/modified something:
<!-- introduction.md -->
---
created_by: agent
created_at: 2024-01-15
last_modified_by: user
last_modified_at: 2024-01-16
---
# Introduction to Moby Dick
This personalized introduction was generated by your reading assistant
and refined by you on January 16th.
</directory_structure>
<file_tools>
File Tools for Shared Workspace
Give the agent the same file primitives the app uses:
// iOS/Swift implementation
struct FileTools {
static func readFile() -> AgentTool {
tool(
name: "read_file",
description: "Read a file from the user's documents",
parameters: ["path": .string("File path relative to Documents/")],
execute: { params in
let path = params["path"] as! String
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent(path)
let content = try String(contentsOf: fileURL)
return ToolResult(text: content)
}
)
}
static func writeFile() -> AgentTool {
tool(
name: "write_file",
description: "Write a file to the user's documents",
parameters: [
"path": .string("File path relative to Documents/"),
"content": .string("File content")
],
execute: { params in
let path = params["path"] as! String
let content = params["content"] as! String
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent(path)
// Create parent directories if needed
try FileManager.default.createDirectory(
at: fileURL.deletingLastPathComponent(),
withIntermediateDirectories: true
)
try content.write(to: fileURL, atomically: true, encoding: .utf8)
return ToolResult(text: "Wrote \(path)")
}
)
}
static func listFiles() -> AgentTool {
tool(
name: "list_files",
description: "List files in a directory",
parameters: ["path": .string("Directory path relative to Documents/")],
execute: { params in
let path = params["path"] as! String
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let dirURL = documentsURL.appendingPathComponent(path)
let contents = try FileManager.default.contentsOfDirectory(atPath: dirURL.path)
return ToolResult(text: contents.joined(separator: "\n"))
}
)
}
static func searchText() -> AgentTool {
tool(
name: "search_text",
description: "Search for text across files",
parameters: [
"query": .string("Text to search for"),
"path": .string("Directory to search in").optional()
],
execute: { params in
// Implement text search across documents
// Return matching files and snippets
}
)
}
}
TypeScript/Node.js Implementation
const fileTools = [
tool(
"read_file",
"Read a file from the workspace",
{ path: z.string().describe("File path") },
async ({ path }) => {
const content = await fs.readFile(path, 'utf-8');
return { text: content };
}
),
tool(
"write_file",
"Write a file to the workspace",
{
path: z.string().describe("File path"),
content: z.string().describe("File content")
},
async ({ path, content }) => {
await fs.mkdir(dirname(path), { recursive: true });
await fs.writeFile(path, content, 'utf-8');
return { text: `Wrote ${path}` };
}
),
tool(
"list_files",
"List files in a directory",
{ path: z.string().describe("Directory path") },
async ({ path }) => {
const files = await fs.readdir(path);
return { text: files.join('\n') };
}
),
tool(
"append_file",
"Append content to a file",
{
path: z.string().describe("File path"),
content: z.string().describe("Content to append")
},
async ({ path, content }) => {
await fs.appendFile(path, content, 'utf-8');
return { text: `Appended to ${path}` };
}
),
];
</file_tools>
<ui_integration>
UI Integration with Shared Workspace
The UI should observe the same files the agent writes to:
Pattern 1: File-Based Reactivity (iOS)
class ResearchViewModel: ObservableObject {
@Published var researchFiles: [ResearchFile] = []
private var watcher: DirectoryWatcher?
func startWatching(bookId: String) {
let researchPath = documentsURL
.appendingPathComponent("Research")
.appendingPathComponent(bookId)
watcher = DirectoryWatcher(url: researchPath) { [weak self] in
// Reload when agent writes new files
self?.loadResearchFiles(from: researchPath)
}
loadResearchFiles(from: researchPath)
}
}
// SwiftUI automatically updates when files change
struct ResearchView: View {
@StateObject var viewModel = ResearchViewModel()
var body: some View {
List(viewModel.researchFiles) { file in
ResearchFileRow(file: file)
}
}
}
Pattern 2: Shared Data Store
When file-watching isn't practical, use a shared data store:
// Shared service that both UI and agent tools use
class BookLibraryService: ObservableObject {
static let shared = BookLibraryService()
@Published var books: [Book] = []
@Published var analysisRecords: [AnalysisRecord] = []
func addAnalysisRecord(_ record: AnalysisRecord) {
analysisRecords.append(record)
// Persists to shared storage
saveToStorage()
}
}
// Agent tool writes through the same service
tool("publish_to_feed", async ({ bookId, content, headline }) => {
let record = AnalysisRecord(bookId: bookId, content: content, headline: headline)
BookLibraryService.shared.addAnalysisRecord(record)
return { text: "Published to feed" }
})
// UI observes the same service
struct FeedView: View {
@StateObject var library = BookLibraryService.shared
var body: some View {
List(library.analysisRecords) { record in
FeedItemRow(record: record)
}
}
}
Pattern 3: Hybrid (Files + Index)
Use files for content, database for indexing:
Documents/
├── Research/
│ └── book_123/
│ └── introduction.md # Actual content (file)
Database:
├── research_index
│ └── { bookId: "book_123", path: "Research/book_123/introduction.md", ... }
// Agent writes file
await writeFile("Research/\(bookId)/introduction.md", content)
// And updates index
await database.insert("research_index", {
bookId: bookId,
path: "Research/\(bookId)/introduction.md",
title: extractTitle(content),
createdAt: Date()
})
// UI queries index, then reads files
let items = database.query("research_index", where: bookId == "book_123")
for item in items {
let content = readFile(item.path)
// Display...
}
</ui_integration>
<collaboration_patterns>
Agent-User Collaboration Patterns
Pattern: Agent Drafts, User Refines
1. Agent generates introduction.md
2. User opens in Files app or in-app editor
3. User makes refinements
4. Agent can see changes via read_file
5. Future agent work builds on user refinements
The agent's system prompt should acknowledge this:
## Working with User Content
When you create content (introductions, research notes, etc.), the user may
edit it afterward. Always read existing files before modifying them—the user
may have made improvements you should preserve.
If a file exists and has been modified by the user (check the metadata or
compare to your last known version), ask before overwriting.
Pattern: User Seeds, Agent Expands
1. User creates notes.md with initial thoughts
2. User asks: "Research more about this"
3. Agent reads notes.md to understand context
4. Agent adds to notes.md or creates related files
5. User continues building on agent additions
Pattern: Append-Only Collaboration
For chat logs or activity streams:
<!-- activity.md - Both append, neither overwrites -->
## 2024-01-15
**User:** Started reading "Moby Dick"
**Agent:** Downloaded full text and created research folder
**User:** Added highlight about whale symbolism
**Agent:** Found 3 academic sources on whale symbolism in Melville's work
</collaboration_patterns>
<security_considerations>
Security in Shared Workspace
Scope the Workspace
Don't give agents access to the entire filesystem:
// GOOD: Scoped to app's documents
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
tool("read_file", { path }) {
// Path is relative to documents, can't escape
let fileURL = documentsURL.appendingPathComponent(path)
guard fileURL.path.hasPrefix(documentsURL.path) else {
throw ToolError("Invalid path")
}
return try String(contentsOf: fileURL)
}
// BAD: Absolute paths allow escape
tool("read_file", { path }) {
return try String(contentsOf: URL(fileURLWithPath: path)) // Can read /etc/passwd!
}
Protect Sensitive Files
let protectedPaths = [".env", "credentials.json", "secrets/"]
tool("read_file", { path }) {
if protectedPaths.any({ path.contains($0) }) {
throw ToolError("Cannot access protected file")
}
// ...
}
Audit Agent Actions
Log what the agent reads/writes:
func logFileAccess(action: String, path: String, agentId: String) {
logger.info("[\(agentId)] \(action): \(path)")
}
tool("write_file", { path, content }) {
logFileAccess(action: "WRITE", path: path, agentId: context.agentId)
// ...
}
</security_considerations>
## Real-World Example: Every ReaderThe Every Reader app uses shared workspace for research:
Documents/
├── Research/
│ └── book_moby_dick/
│ ├── full_text.txt # Agent downloads from Gutenberg
│ ├── introduction.md # Agent generates, personalized
│ ├── sources/
│ │ ├── whale_symbolism.md # Agent researches
│ │ └── melville_bio.md # Agent researches
│ └── user_notes.md # User can add their own notes
├── Chats/
│ └── 2024-01-15.json # Chat history
└── profile.md # Agent generated from photos
How it works:
- User adds "Moby Dick" to library
- User starts research agent
- Agent downloads full text to
Research/book_moby_dick/full_text.txt - Agent researches and writes to
sources/ - Agent generates
introduction.mdbased on user's reading profile - User can view all files in the app or Files.app
- User can edit
introduction.mdto refine it - Chat agent can read all of this context when answering questions
<icloud_sync>
iCloud File Storage for Multi-Device Sync (iOS)
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 Pattern
// 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
├── Experiments/
│ └── magnesium-sleep/
│ ├── config.json
│ └── log.json
└── Research/
└── {topic}/
└── sources.md
Handling Sync Conflicts
iCloud handles conflicts automatically, but you should design for it:
// Check for conflicts when reading
func readJournalEntry(at url: URL) throws -> JournalEntry {
// iCloud may create .icloud placeholder files for not-yet-downloaded content
if url.pathExtension == "icloud" {
// Trigger download
try FileManager.default.startDownloadingUbiquitousItem(at: url)
throw FileNotYetAvailableError()
}
let data = try Data(contentsOf: url)
return try JSONDecoder().decode(JournalEntry.self, from: data)
}
// For writes, use coordinated file access
func writeJournalEntry(_ entry: JournalEntry, to url: URL) throws {
let coordinator = NSFileCoordinator()
var error: NSError?
coordinator.coordinate(writingItemAt: url, options: .forReplacing, error: &error) { newURL in
let data = try? JSONEncoder().encode(entry)
try? data?.write(to: newURL)
}
if let error = error {
throw error
}
}
What This Enables
- User starts experiment on iPhone → Agent creates
Experiments/sleep-tracking/config.json - User opens app on iPad → Same experiment visible, no sync code needed
- Agent logs observation on iPhone → Syncs to iPad automatically
- User edits journal on iPad → iPhone sees the edit
Entitlements Required
Add to your app's entitlements:
<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 </icloud_sync>
Architecture:
- Single shared directory for agent and user data
- Organized by domain, not by actor
- File tools scoped to workspace (no escape)
- Protected paths for sensitive files
Tools:
read_file- Read any file in workspacewrite_file- Write any file in workspacelist_files- Browse directory structuresearch_text- Find content across files (optional)
UI Integration:
- UI observes same files agent writes
- Changes reflect immediately (file watching or shared store)
- User can edit agent-created files
- Agent reads user modifications before overwriting
Collaboration:
- System prompt acknowledges user may edit files
- Agent checks for user modifications before overwriting
- Metadata tracks who created/modified (optional)
Multi-Device (iOS):
- Use iCloud Documents for shared workspace (free sync)
- Fallback to local Documents if iCloud unavailable
- Handle
.icloudplaceholder files (trigger download) - Use NSFileCoordinator for conflict-safe writes