Poche
Ton agent, sur ton appareil. Hors ligne. Privé. Rien ne quitte ton iPhone.
Agent personnel local : chat, rappels, calendrier, notes, tâches et recherche sémantique — propulsé exclusivement par le modèle Apple Intelligence embarqué. Aucune API. Aucun abonnement. Aucun serveur.
📱 Captures d'écran
| Accueil | Conversation + confirmation | Mode sombre |
|---|---|---|
![]() |
![]() |
![]() |
🧭 Le concept
Poche est un agent personnel qui vit entièrement sur l'iPhone. Le seul moteur génératif de l'application est Apple Foundation Models, le modèle ~3 milliards de paramètres d'Apple Intelligence, exécuté sur le Neural Engine de l'appareil.
La règle fondatrice du projet (voir CLAUDE.md, la source de vérité) :
Si une fonctionnalité ne peut pas être faite on-device, elle n'est pas faite. On ne dégrade pas la promesse pour ajouter une capacité.
Concrètement, c'est un chat en streaming qui sait agir : créer des rappels et des événements (EventKit), enregistrer des notes et des tâches (SwiftData), retrouver tes données par recherche sémantique locale (NLEmbedding) — chaque écriture passant par une carte de confirmation que l'utilisateur valide explicitement.
📊 Métriques
| Métrique | Valeur |
|---|---|
| Fichiers Swift (app + extension) | 41 |
| Fichiers Swift (tests) | 5 |
| Lignes de code (app + extension) | ~2 930 |
| Lignes de code (tests) | ~320 |
| Tests automatisés | 21 / 21 ✓ (5 suites) |
| Outils exposés au modèle | 7 (plafond dur : 8) |
| Fenêtre de contexte gérée | 4 096 tokens (condensation à 70 %, réserve réponse 30 %) |
| Coût des instructions système | ~230 tokens (mesuré, documenté dans le code) |
| Coût fixe par outil (schéma) | ~120 tokens |
| Requêtes réseau dans le chemin IA | 0 — vérifié par un test tripwire |
| Cibles | App iOS + Share Extension + App Intents |
| Objectif premier token | < 400 ms |
🏗 Architecture
Poche/
├─ App/ point d'entrée, gate de disponibilité du modèle
├─ Chat/
│ ├─ UI/ fil, bulles, composeur, jauge de contexte, accueil
│ └─ State/ ChatViewModel, tours de conversation
├─ Agent/
│ ├─ Session/ cycle de vie LanguageModelSession + instructions versionnées
│ ├─ Budget/ tokens, condensation, recyclage ⚠️ cœur du projet
│ ├─ Tools/ un fichier par outil (7 outils)
│ ├─ Confirm/ couche de confirmation ⚠️ non contournable
│ └─ Schemas/ types @Generable
├─ Data/
│ ├─ Store/ SwiftData — notes, tâches, conversations
│ ├─ Search/ index sémantique local (NLEmbedding)
│ └─ Bridges/ EventKit, App Intents
├─ Shared/ boîte d'échange app ↔ extension (groupe d'apps)
├─ Support/ dictée on-device, veille thermique
└─ Design/ thème, marque, fond
PocheShare/ Share Extension (entrée seulement)
PocheTests/ 21 tests, 5 suitesLe principe non négociable
Le modèle ne fait jamais d'effet de bord. Il propose un appel d'outil ; l'application valide, affiche, et n'exécute qu'après confirmation de l'utilisateur.
flowchart LR
U[Utilisateur] -->|message| S[LanguageModelSession]
S -->|appel d'outil| T[Outil<br/>valide les arguments]
T -->|proposition| C[ConfirmCenter<br/>carte de confirmation]
C -->|Confirmer| E[ActionExecutor<br/>seul chemin d'écriture]
C -->|Modifier| U
E --> EK[EventKit]
E --> SD[SwiftData]
E -->|résultat réel| S
Il n'existe aucun chemin de code qui écrit sans traverser Confirm/ — pas de mode
expert, pas de préférence pour le désactiver. Les dates sont résolues et validées en
Swift (jamais par le modèle) et affichées en clair sur la carte.
🧮 Le budget de contexte — le chantier central
4 096 tokens pour tout : instructions système, schémas des outils, historique complet et réponse à venir. Sans gestion, une app de chat locale casse en quelques minutes.
flowchart TD
A[Nouveau message] --> B{Préflight :<br/>estimation ≥ 70 % ?}
B -->|non| C[streamResponse]
B -->|oui| D[Condensation<br/>appel séparé, sortie @Generable]
D --> E[Recyclage : nouvelle session<br/>instructions + résumé + 3 derniers tours]
E --> C
C -->|exceededContextWindowSize| E
C --> F[Réponse streamée<br/>+ jauge discrète mise à jour]
- Mesure continue — estimateur pessimiste (~3 caractères/token), jauge fine dans l'UI, jamais un chiffre.
- À 70 % : condensation — résumé dense et fidèle via un appel séparé (
@Generable, jamais de String à parser). - Recyclage invisible — nouvelle
LanguageModelSessionréamorcée ; l'utilisateur ne voit rien. - Mémoire longue externalisée — les résumés sont persistés dans SwiftData et réinjectés à la demande via
searchMyData. - Filet de sécurité —
exceededContextWindowSize→ recycler, rejouer, ne jamais afficher d'erreur technique.
🧰 Catalogue d'outils (7 / plafond 8)
| Outil | Type | Bridge | Confirmation |
|---|---|---|---|
createReminder |
écriture | EventKit | ✅ |
createCalendarEvent |
écriture | EventKit | ✅ |
saveNote |
écriture | SwiftData | ✅ |
createTask |
écriture | SwiftData | ✅ |
updateTask |
écriture | SwiftData | ✅ |
searchMyData |
lecture | SwiftData + NLEmbedding | — |
getUpcoming |
lecture | EventKit | — |
Chaque outil : arguments @Generable + @Guide, validation hostile (titres tronqués à
80 caractères, dates ISO 8601 résolues en Swift, bornes passé/futur), sortie plafonnée
(~200 tokens) — un outil qui renvoie 30 événements tue la session.
⚠️ Il n'existe aucune API publique pour l'app Notes d'Apple —
saveNoteécrit dans les notes internes de Poche (SwiftData), et son nom ne prétend pas le contraire. Aucun outil de suppression en v1.
🔒 Données et confidentialité
- Tout est local : conversations, notes, tâches, résumés, index de recherche.
- Zéro requête réseau dans le chemin IA — un test automatisé (
NetworkIsolationTests) enregistre un espionURLProtocolet échoue si une seule requête sort pendant l'exercice des outils et de la recherche. - Recherche sémantique via
NLEmbedding(français) — pas de service externe, même pour l'indexation. - Dictée
SFSpeechRecognizeravecrequiresOnDeviceRecognition = true— si l'appareil ne sait pas transcrire localement, le bouton micro n'existe pas. Pas de repli serveur. - Chiffrement au repos via Data Protection. Aucune analytique sur le contenu.
- Un refus des garde-fous est un état normal de l'interface : message neutre, fil intact.
🧪 Tests
xcodebuild test -project Poche.xcodeproj -scheme Poche \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'| Suite | Couverture | Tests |
|---|---|---|
DateResolverTests |
ISO 8601, date seule → 9 h, passé/garbage/hostile/trop loin rejetés | 6 |
ContextBudgetTests |
réserve 30 %, seuil 70 %, prompt plein refusé, estimateur pessimiste | 5 |
CreateReminderToolTests |
l'outil patron : propose sans écrire, args invalides/manquants/hostiles | 6 |
NetworkIsolationTests |
tripwire : 0 requête réseau dans le chemin de l'agent | 1 |
SharedInboxTests |
aller-retour de la boîte partagée, import en notes, boîte absente | 3 |
21 / 21 ✓ — build vert sous Swift 6 concurrence stricte (SWIFT_STRICT_CONCURRENCY=complete).
🔨 Build et distribution
Prérequis
- Xcode 26+ (SDK iOS 26), XcodeGen (
brew install xcodegen) - Pour l'inférence réelle : iPhone 15 Pro ou plus récent (A17 Pro), Apple Intelligence activé
Développement
xcodegen generate # project.yml est la source de vérité du projet Xcode
open Poche.xcodeprojTestFlight
Le build 1.0.0 (1) est uploadé sur App Store Connect (fiche ai.spboucher.poche).
Pour les suivants — bumper CURRENT_PROJECT_VERSION dans project.yml, puis :
xcodegen generate
xcodebuild -project Poche.xcodeproj -scheme Poche \
-destination 'generic/platform=iOS' -archivePath build/Poche.xcarchive \
archive -allowProvisioningUpdates
xcodebuild -exportArchive -archivePath build/Poche.xcarchive \
-exportOptionsPlist build/ExportOptions.plist -exportPath build/export \
-allowProvisioningUpdatesITSAppUsesNonExemptEncryption = false est déjà déclaré : pas de questionnaire de
conformité à chaque build.
Hooks de vérification (DEBUG uniquement)
SIMCTL_CHILD_POCHE_AUTOSEND="Bonjour" \
SIMCTL_CHILD_POCHE_DEMO_THREAD=1 \
SIMCTL_CHILD_POCHE_DEMO_CARD=1 \
xcrun simctl launch booted ai.spboucher.pocheAbsents d'un build release (#if DEBUG).
🚦 États de disponibilité
L'app gère chaque état du modèle avec son propre écran et sa propre action :
| État | Écran | Action |
|---|---|---|
available |
Chat | — |
deviceNotEligible |
Mur définitif, soigné, sans culpabilisation | Jamais de LLM de remplacement |
appleIntelligenceNotEnabled |
Explication | Bouton « Ouvrir Réglages » |
modelNotReady |
Téléchargement en cours | Bouton « Réessayer » |
⚠️ Limitations connues
| Limitation | Détail |
|---|---|
| A17 Pro minimum | Risque commercial principal — à annoncer sur la fiche App Store, pas au premier lancement |
| 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 |
| Pas d'API token count | Le SDK iOS 26 n'expose pas tokenCount(for:) — estimateur pessimiste en attendant (documenté dans TokenEstimator.swift) |
| Modèle ~3B | Bon en extraction/classification/sortie structurée ; la confirmation systématique est ce qui le rend viable en agent |
🗺 Roadmap
- Chat nu : session, streaming, états d'indisponibilité
- Budget de contexte : mesure, condensation, recyclage invisible
-
createReminder+ couche Confirm (l'outil patron) - Stockage local +
searchMyData(mémoire longue) - Reste du catalogue (7 outils, chacun testé)
- Dictée on-device, App Intents, Share Extension
- TestFlight 1.0.0 (1)
- Verrouillage Face ID optionnel à l'ouverture
- Écran de consultation des notes/tâches
- iCloud optionnel, désactivé par défaut, chiffré
-
deleteTask(quand la confiance dans l'agent sera établie)
Simon-Pierre Boucher · contact@spboucher.ai
Aucune API. Aucun abonnement. Aucun serveur. Juste ton iPhone.


