spb/poche Public
Agent personnel 100 % on-device — SwiftUI + Apple Foundation Models + EventKit + SwiftData. Aucune API, aucun serveur.
Swift 100%
1<!--2 ─────────────────────────────────────────────3 Poche — Agent personnel 100 % on-device4 ─────────────────────────────────────────────5 Author : Simon-Pierre Boucher6 Contact : contact@spboucher.ai7 File : README.md8 Purpose : Documentation complète du projet9 ─────────────────────────────────────────────10-->11<div align="center">1213<img src="docs/icon.png" width="96" alt="Icône Poche" />1415# Poche1617**Ton agent, sur ton appareil. Hors ligne. Privé. Rien ne quitte ton iPhone.**1819[](https://developer.apple.com)20[](#-limitations-connues)21[](https://swift.org)22[](https://developer.apple.com/xcode/swiftui/)23[-5E5CE6)](https://developer.apple.com/documentation/foundationmodels)24[](#-donn%C3%A9es-et-confidentialit%C3%A9)25[](#-tests)26[%20upload%C3%A9-0D96F6?logo=apple)](#-build-et-distribution)27[](mailto:contact@spboucher.ai)2829*Agent personnel local : chat, rappels, calendrier, notes, tâches et recherche sémantique —30propulsé exclusivement par le modèle Apple Intelligence embarqué. Aucune API. Aucun abonnement. Aucun serveur.*3132</div>3334---3536## 📱 Captures d'écran3738| Accueil | Conversation + confirmation | Mode sombre |39|:---:|:---:|:---:|40|  |  |  |4142---4344## 🧭 Le concept4546Poche est un agent personnel qui vit **entièrement sur l'iPhone**. Le seul moteur47génératif de l'application est **Apple Foundation Models**, le modèle ~3 milliards de48paramètres d'Apple Intelligence, exécuté sur le Neural Engine de l'appareil.4950La règle fondatrice du projet (voir [`CLAUDE.md`](CLAUDE.md), la source de vérité) :5152> Si une fonctionnalité ne peut pas être faite on-device, **elle n'est pas faite**.53> On ne dégrade pas la promesse pour ajouter une capacité.5455Concrètement, c'est un chat en streaming qui sait **agir** : créer des rappels et des56événements (EventKit), enregistrer des notes et des tâches (SwiftData), retrouver tes57données par recherche sémantique locale (NLEmbedding) — chaque écriture passant par une58**carte de confirmation** que l'utilisateur valide explicitement.5960---6162## 📊 Métriques6364| Métrique | Valeur |65|---|---|66| Fichiers Swift (app + extension) | **41** |67| Fichiers Swift (tests) | **5** |68| Lignes de code (app + extension) | **~2 930** |69| Lignes de code (tests) | **~320** |70| Tests automatisés | **21 / 21 ✓** (5 suites) |71| Outils exposés au modèle | **7** (plafond dur : 8) |72| Fenêtre de contexte gérée | **4 096 tokens** (condensation à 70 %, réserve réponse 30 %) |73| Coût des instructions système | **~230 tokens** (mesuré, documenté dans le code) |74| Coût fixe par outil (schéma) | **~120 tokens** |75| Requêtes réseau dans le chemin IA | **0** — vérifié par un test tripwire |76| Cibles | App iOS + Share Extension + App Intents |77| Objectif premier token | **< 400 ms** |7879---8081## 🏗 Architecture8283```84Poche/85├─ App/ point d'entrée, gate de disponibilité du modèle86├─ Chat/87│ ├─ UI/ fil, bulles, composeur, jauge de contexte, accueil88│ └─ State/ ChatViewModel, tours de conversation89├─ Agent/90│ ├─ Session/ cycle de vie LanguageModelSession + instructions versionnées91│ ├─ Budget/ tokens, condensation, recyclage ⚠️ cœur du projet92│ ├─ Tools/ un fichier par outil (7 outils)93│ ├─ Confirm/ couche de confirmation ⚠️ non contournable94│ └─ Schemas/ types @Generable95├─ Data/96│ ├─ Store/ SwiftData — notes, tâches, conversations97│ ├─ Search/ index sémantique local (NLEmbedding)98│ └─ Bridges/ EventKit, App Intents99├─ Shared/ boîte d'échange app ↔ extension (groupe d'apps)100├─ Support/ dictée on-device, veille thermique101└─ Design/ thème, marque, fond102PocheShare/ Share Extension (entrée seulement)103PocheTests/ 21 tests, 5 suites104```105106### Le principe non négociable107108**Le modèle ne fait jamais d'effet de bord.** Il propose un appel d'outil ; l'application109valide, affiche, et n'exécute qu'après confirmation de l'utilisateur.110111```mermaid112flowchart LR113 U[Utilisateur] -->|message| S[LanguageModelSession]114 S -->|appel d'outil| T[Outil<br/>valide les arguments]115 T -->|proposition| C[ConfirmCenter<br/>carte de confirmation]116 C -->|Confirmer| E[ActionExecutor<br/>seul chemin d'écriture]117 C -->|Modifier| U118 E --> EK[EventKit]119 E --> SD[SwiftData]120 E -->|résultat réel| S121```122123Il n'existe **aucun chemin de code** qui écrit sans traverser `Confirm/` — pas de mode124expert, pas de préférence pour le désactiver. Les dates sont résolues et validées **en125Swift** (jamais par le modèle) et affichées en clair sur la carte.126127---128129## 🧮 Le budget de contexte — le chantier central1301314 096 tokens pour tout : instructions système, schémas des outils, historique complet132et réponse à venir. Sans gestion, une app de chat locale **casse en quelques minutes**.133134```mermaid135flowchart TD136 A[Nouveau message] --> B{Préflight :<br/>estimation ≥ 70 % ?}137 B -->|non| C[streamResponse]138 B -->|oui| D[Condensation<br/>appel séparé, sortie @Generable]139 D --> E[Recyclage : nouvelle session<br/>instructions + résumé + 3 derniers tours]140 E --> C141 C -->|exceededContextWindowSize| E142 C --> F[Réponse streamée<br/>+ jauge discrète mise à jour]143```1441451. **Mesure continue** — estimateur pessimiste (~3 caractères/token), jauge fine dans l'UI, jamais un chiffre.1462. **À 70 % : condensation** — résumé dense et fidèle via un appel séparé (`@Generable`, jamais de String à parser).1473. **Recyclage invisible** — nouvelle `LanguageModelSession` réamorcée ; l'utilisateur ne voit rien.1484. **Mémoire longue externalisée** — les résumés sont persistés dans SwiftData et réinjectés à la demande via `searchMyData`.1495. **Filet de sécurité** — `exceededContextWindowSize` → recycler, rejouer, ne jamais afficher d'erreur technique.150151---152153## 🧰 Catalogue d'outils (7 / plafond 8)154155| Outil | Type | Bridge | Confirmation |156|---|---|---|:---:|157| `createReminder` | écriture | EventKit | ✅ |158| `createCalendarEvent` | écriture | EventKit | ✅ |159| `saveNote` | écriture | SwiftData | ✅ |160| `createTask` | écriture | SwiftData | ✅ |161| `updateTask` | écriture | SwiftData | ✅ |162| `searchMyData` | lecture | SwiftData + NLEmbedding | — |163| `getUpcoming` | lecture | EventKit | — |164165Chaque outil : arguments `@Generable` + `@Guide`, validation hostile (titres tronqués à16680 caractères, dates ISO 8601 résolues en Swift, bornes passé/futur), **sortie plafonnée167(~200 tokens)** — un outil qui renvoie 30 événements tue la session.168169> ⚠️ Il n'existe **aucune API publique pour l'app Notes d'Apple** — `saveNote` écrit dans170> les notes internes de Poche (SwiftData), et son nom ne prétend pas le contraire.171> Aucun outil de suppression en v1.172173---174175## 🔒 Données et confidentialité176177- **Tout est local** : conversations, notes, tâches, résumés, index de recherche.178- **Zéro requête réseau dans le chemin IA** — un test automatisé (`NetworkIsolationTests`)179 enregistre un espion `URLProtocol` et **échoue si une seule requête sort** pendant180 l'exercice des outils et de la recherche.181- Recherche sémantique via `NLEmbedding` (français) — pas de service externe, même pour l'indexation.182- Dictée `SFSpeechRecognizer` avec `requiresOnDeviceRecognition = true` — si l'appareil ne183 sait pas transcrire localement, le bouton micro n'existe pas. Pas de repli serveur.184- Chiffrement au repos via Data Protection. Aucune analytique sur le contenu.185- Un refus des garde-fous est un **état normal de l'interface** : message neutre, fil intact.186187---188189## 🧪 Tests190191```bash192xcodebuild test -project Poche.xcodeproj -scheme Poche \193 -destination 'platform=iOS Simulator,name=iPhone 17 Pro'194```195196| Suite | Couverture | Tests |197|---|---|:---:|198| `DateResolverTests` | ISO 8601, date seule → 9 h, passé/garbage/hostile/trop loin rejetés | 6 |199| `ContextBudgetTests` | réserve 30 %, seuil 70 %, prompt plein refusé, estimateur pessimiste | 5 |200| `CreateReminderToolTests` | l'outil patron : propose sans écrire, args invalides/manquants/hostiles | 6 |201| `NetworkIsolationTests` | tripwire : 0 requête réseau dans le chemin de l'agent | 1 |202| `SharedInboxTests` | aller-retour de la boîte partagée, import en notes, boîte absente | 3 |203204**21 / 21 ✓** — build vert sous Swift 6 concurrence stricte (`SWIFT_STRICT_CONCURRENCY=complete`).205206---207208## 🔨 Build et distribution209210### Prérequis211212- Xcode 26+ (SDK iOS 26), [XcodeGen](https://github.com/yonaskolb/XcodeGen) (`brew install xcodegen`)213- Pour l'inférence réelle : iPhone 15 Pro ou plus récent (A17 Pro), Apple Intelligence activé214215### Développement216217```bash218xcodegen generate # project.yml est la source de vérité du projet Xcode219open Poche.xcodeproj220```221222### TestFlight223224Le build **1.0.0 (1)** est uploadé sur App Store Connect (fiche `ai.spboucher.poche`).225Pour les suivants — bumper `CURRENT_PROJECT_VERSION` dans `project.yml`, puis :226227```bash228xcodegen generate229xcodebuild -project Poche.xcodeproj -scheme Poche \230 -destination 'generic/platform=iOS' -archivePath build/Poche.xcarchive \231 archive -allowProvisioningUpdates232xcodebuild -exportArchive -archivePath build/Poche.xcarchive \233 -exportOptionsPlist build/ExportOptions.plist -exportPath build/export \234 -allowProvisioningUpdates235```236237`ITSAppUsesNonExemptEncryption = false` est déjà déclaré : pas de questionnaire de238conformité à chaque build.239240### Hooks de vérification (DEBUG uniquement)241242```bash243SIMCTL_CHILD_POCHE_AUTOSEND="Bonjour" \244SIMCTL_CHILD_POCHE_DEMO_THREAD=1 \245SIMCTL_CHILD_POCHE_DEMO_CARD=1 \246xcrun simctl launch booted ai.spboucher.poche247```248249Absents d'un build release (`#if DEBUG`).250251---252253## 🚦 États de disponibilité254255L'app gère chaque état du modèle avec son propre écran et sa propre action :256257| État | Écran | Action |258|---|---|---|259| `available` | Chat | — |260| `deviceNotEligible` | Mur définitif, soigné, sans culpabilisation | Jamais de LLM de remplacement |261| `appleIntelligenceNotEnabled` | Explication | Bouton « Ouvrir Réglages » |262| `modelNotReady` | Téléchargement en cours | Bouton « Réessayer » |263264---265266## ⚠️ Limitations connues267268| Limitation | Détail |269|---|---|270| **A17 Pro minimum** | Risque commercial principal — à annoncer sur la fiche App Store, pas au premier lancement |271| **Simulateur** | Sur certains hôtes, le pont Apple Intelligence du simulateur est cassé (`promptTemplateNotFound`) ; l'app l'annonce honnêtement après 2 échecs. Sur appareil réel, tout fonctionne |272| **Pas d'API token count** | Le SDK iOS 26 n'expose pas `tokenCount(for:)` — estimateur pessimiste en attendant (documenté dans `TokenEstimator.swift`) |273| **Modèle ~3B** | Bon en extraction/classification/sortie structurée ; la confirmation systématique est ce qui le rend viable en agent |274275---276277## 🗺 Roadmap278279- [x] Chat nu : session, streaming, états d'indisponibilité280- [x] Budget de contexte : mesure, condensation, recyclage invisible281- [x] `createReminder` + couche Confirm (l'outil patron)282- [x] Stockage local + `searchMyData` (mémoire longue)283- [x] Reste du catalogue (7 outils, chacun testé)284- [x] Dictée on-device, App Intents, Share Extension285- [x] TestFlight 1.0.0 (1)286- [ ] Verrouillage Face ID optionnel à l'ouverture287- [ ] Écran de consultation des notes/tâches288- [ ] iCloud optionnel, désactivé par défaut, chiffré289- [ ] `deleteTask` (quand la confiance dans l'agent sera établie)290291---292293<div align="center">294295**Simon-Pierre Boucher** · [contact@spboucher.ai](mailto:contact@spboucher.ai)296297*Aucune API. Aucun abonnement. Aucun serveur. Juste ton iPhone.*298299</div>300