1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
|
# Référence : agents, channels et événements
## États d'un agent
| État | Affiché (`en` · `fr`) | Signification |
| --- | --- | --- |
| `stopped` | stopped · arrêté | Aucun processus. L'état initial de chaque agent. |
| `starting` | starting… · démarre… | Le processus est lancé et la poignée de main ACP (`initialize`) est en cours. |
| `ready` | ready · prêt | En marche et inoccupé. |
| `busy` | busy · occupé | En marche et utilisé par au moins une conversation. |
| `error` | error · erreur | Le démarrage a échoué, ou le processus s'est arrêté seul (crash). Le statut porte la raison, suivie des dernières lignes stderr de l'agent en cas d'échec au démarrage. |
`outdated` (affiché « restart to apply changes », ou « à redémarrer » en français) marque un agent en marche dont la définition a changé dans la configuration ; un redémarrage l'applique.
## Transitions
| Depuis | Événement | Vers |
| --- | --- | --- |
| `stopped`, `error` | démarrage demandé (▶ dans le panneau Agents, ou première utilisation) | `starting` |
| `starting` | la poignée de main réussit | `ready` |
| `starting` | la poignée de main échoue, protocole autre qu'ACP v1, délai dépassé (30 s) | `error` |
| `ready` | utilisé | `busy` |
| `busy` | le dernier utilisateur a fini | `ready` |
| `ready`, `busy` | arrêt demandé (■), fermeture de l'application, agent retiré de la configuration | `stopped` |
| `ready` | inutilisé depuis `limits.stopIdleAgentsAfter` (vérifié toutes les 30 s) | `stopped` |
| `ready` | un autre agent doit démarrer et celui-ci est l'agent inoccupé utilisé le moins récemment | `stopped` |
| `ready`, `busy` | le processus s'arrête seul | `error` (jamais redémarré automatiquement) |
| `starting` | arrêt demandé | `stopped` dès la fin du démarrage |
Quand `limits.maxRunningAgents` agents tournent et qu'ils sont tous `busy`, un démarrage est refusé avec le message `too many agents running: N agent(s) busy (…), the limit is N`.
Arrêter un agent ferme son entrée standard, attend 5 secondes, puis tue son groupe de processus (l'agent et ce qu'il a lancé, comme `mm` sous `scripts/agents.sh`).
## Événements du Go vers l'interface
| Événement | Charge | Envoyé quand |
| --- | --- | --- |
| `config:changed` | `{path, valid, problems[]}` | Le fichier de configuration a été enregistré et rechargé (valide ou non). |
| `agent:state` | `{agent, state, error?}` | Un agent change d'état ou sa définition change. |
| `agent:log` | `{agent, line}` | Un agent écrit une ligne sur sa sortie d'erreur, ou une commande de sa sandbox en affiche une (préfixée par `sandbox: `). |
| `files:dropped` | `{target, files[]}` | Des fichiers du système sont déposés sur un élément marqué `data-file-drop-target` ; `target` est son `id`, `files` leurs chemins absolus. |
Les noms et types Go sont dans `internal/events` ; les constantes et types TypeScript sont générés dans `frontend/bindings/events` et `frontend/bindings/…/internal/events`.
## Services appelés par l'interface
| Service | Méthode | Effet |
| --- | --- | --- |
| `AgentService` | `List()` | Tous les agents avec `name, title, icon, description, state, error, outdated, runtime, canStopSandbox, canRemoveSandbox`, dans l'ordre de la configuration. |
| | `Start(name)`, `Stop(name)`, `Restart(name)` | Comme dans les transitions ci-dessus ; une erreur est renvoyée à l'interface. Un démarrage recrée d'abord une sandbox absente si `runtime.lifecycle.create` est défini. |
| | `StopSandbox(name)`, `RemoveSandbox(name)` | Arrête l'agent, puis lance `runtime.lifecycle.stop` ou `remove` (conteneur ou sandbox) ; la sortie va dans le journal de l'agent. |
| | `Logs(name)` | Les 200 dernières lignes stderr de l'agent. |
| `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, historyProblem`. |
| `SystemService` | `Memory()` | `level` (`normal`, `warning`, `critical`), `used` et `total` en octets ; tout vide (`total` à 0) là où la mémoire ne peut pas être mesurée. macOS : le niveau de `kern.memorystatus_vm_pressure_level`, utilisée = mémoire des apps + résidente + compressée (`vm_stat`), comme dans Moniteur d'activité. Linux et Windows : déduit de la mémoire disponible, `warning` sous 20 %, `critical` sous 10 %. Le panneau Log l'appelle toutes les 5 secondes tant qu'il est ouvert (issue #61). |
## Channels (milestones M2 et M3)
Un channel contient une conversation entre l'utilisateur et un ou plusieurs agents. Une conversation directe est un channel à un seul membre, sans nom, dont `direct` vaut le nom de son agent. Les channels, les membres et les messages terminés sont gardés dans l'historique du fichier de configuration (ci-dessous) ; une réponse en cours est gardée quand elle se termine.
### Channels
| Champ | Signification |
| --- | --- |
| `id` | identifiant triable |
| `name`, `description` | 1 à 64 caractères, unique sans tenir compte de la casse ; vide pour une conversation directe |
| `direct` | l'agent d'une conversation directe ; vide pour un channel |
| `members` | noms des agents, dans l'ordre d'arrivée |
### Qui répond à un message
| Le message | Répondu par |
| --- | --- |
| `/agent:commande arguments` (`agent` est un agent de la configuration) | cet agent, qui reçoit `/commande arguments` |
| contient des mentions `@nom` d'agents de la configuration | les agents mentionnés, l'un après l'autre, dans l'ordre des mentions ; un agent mentionné qui n'est pas membre rejoint d'abord le channel |
| sans mention | le dernier agent qui a répondu dans le channel ; avec un seul membre, ce membre |
| sans mention, personne n'a encore répondu, plusieurs membres | personne : message système `no-recipient` |
| tout message d'une conversation directe | son agent ; les mentions y sont du simple texte |
Un message qui commence par `/` (éventuellement après des mentions) est une commande : il est envoyé seul. Tout autre message est envoyé avec les messages du channel que l'agent n'a pas encore vus (au plus les 20 derniers, chacun coupé à 1500 caractères). Un agent répond à un seul channel à la fois ; un channel qui l'attend indique à qui c'est le tour.
### Fichiers joints
Un `$chemin` en début de mot joint le fichier `chemin` du dossier d'un agent (issue #53) : en conversation directe, celui de son agent ; dans un channel, celui du premier agent mentionné avec `@`, sinon celui du premier membre. Un `$` dans du code, après un autre caractère (`5$`), échappé (`\$`) ou suivi d'aucune lettre (`$5`) reste du texte ; la ponctuation qui termine une phrase ne fait pas partie du chemin. Un chemin s'arrête au premier espace.
Chaque agent qui répond reçoit les fichiers avec le message (pas avec une commande slash), sous forme de blocs ACP choisis selon ce qu'il a annoncé :
| Fichier | `promptCapabilities` | Envoyé comme |
| --- | --- | --- |
| image (`image/*`) | `image` | bloc `image` (base64), avec son URI |
| texte, 1 Mo au plus | `embeddedContext` | bloc `resource` avec le texte |
| tout autre texte ou image | — | `resource_link` : URI `file://` + le chemin tel que l'agent le voit, nom, type, taille |
| ni texte ni image | — | non envoyé : `attachment-unavailable` |
Un fichier est lu quand vient le tour de l'agent (5 Mo au plus) ; un fichier illisible donne `attachment-unavailable`, et l'agent répond sans lui.
### Messages
| Champ | Signification |
| --- | --- |
| `id`, `channel`, `createdAt` | identité |
| `agent` | l'auteur d'un message d'agent, ou l'agent dont parle un message système |
| `role` | `user`, `agent` ou `system` |
| `parts` | `text` (Markdown), `thought` (raisonnement), `tool` (un appel d'outil), `plan` (étapes d'un plan) |
| `done` | faux pendant qu'un message d'agent s'affiche |
| `queued` | un message de l'utilisateur qui attend la fin des réponses en cours |
| `stopReason` | comment un message d'agent s'est terminé : `end_turn`, `cancelled`, `max_tokens`, `max_turn_requests`, `refusal` |
| `error` | pourquoi un message d'agent s'est terminé sans raison d'arrêt (l'explication de l'agent), ou `cancelled` pour un message en attente abandonné par Stop |
| `code` | messages système : `agent-joined`, `agent-left`, `no-recipient`, `agent-unavailable` (impossible de le démarrer ou de le joindre ; `error` dit pourquoi), `attachment-unavailable` (un fichier joint n'a pas été envoyé à `agent` ; `error` le nomme et dit pourquoi) |
| `attachments` | messages de l'utilisateur : les fichiers joints avec `$chemin`, sous la forme `{agent, path}` (l'agent dont le dossier le contient). Seule la référence est gardée, jamais le contenu. |
Un appel d'outil a `id, title, kind` (`read, edit, delete, move, search, execute, think, fetch, other`), `status` (`pending, in_progress, completed, failed`), `input` (JSON compact), `output`, `locations`, `diffs` (`path, oldText, newText, newFile`).
### Événements
| Événement | Contenu | Envoyé quand |
| --- | --- | --- |
| `channels:changed` | `{channels[]}` | un channel est créé, renommé ou supprimé, ou ses membres changent |
| `message:updated` | `{channel, message}` | un message est ajouté, ou terminé |
| `message:delta` | `{channel, messageId, index, kind, text?, part?}` | `text` s'ajoute à la partie `index` (texte, raisonnement), ou `part` la remplace (outil, plan) |
| `turn:changed` | `{channel, busy, queued, answering}` | le channel commence ou arrête de répondre, ou le tour d'un autre agent commence ; `queued` messages attendent |
| `permission:requested` | `{id, channel, agent, messageId, toolCall, options[], rejections}` | un agent demande à utiliser un outil ; `rejections` compte les refus déjà donnés pendant ce tour |
| `permission:resolved` | `{id, channel}` | la demande a reçu une réponse, ou a été annulée par Stop |
| `usage:changed` | `{channel, agent, usage: {used, size}}` | un agent a indiqué l'occupation de sa fenêtre de contexte (`size` vaut 0 si elle est inconnue) |
| `commands:changed` | `{agent, commands[]}` | un agent a annoncé ses commandes slash (`name, description, hint`) |
### `ChannelService`
| Méthode | Effet |
| --- | --- |
| `List()` | Tous les channels et conversations directes, du plus ancien au plus récent. |
| `Create(name, description)` | Crée un channel. Un nom déjà pris ou vide est une erreur. |
| `Rename(id, name, description)` | Renomme un channel (pas une conversation directe). |
| `Delete(id)` | Arrête ses réponses, puis supprime le channel et tout son historique. |
| `Clear(id)` | Efface tous les messages d'un channel ou d'une conversation directe et garde son nom, sa description et ses membres. Les sessions des membres sont oubliées (`sessionId` et `lastSeen` vidés) : leur prochain tour ouvre une nouvelle session, qui ne sait rien de ce qui a été effacé ; l'usage est remis à zéro. Refusé tant que le channel répond ou a des messages en attente (`an agent is answering in this channel…`). Envoie `channels:changed`. |
| `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invite ou retire un agent ; le channel affiche `agent-joined` ou `agent-left`. |
| `Direct(agent)` | La conversation directe avec `agent`, créée la première fois. |
| `Send(id, text)` | Ajoute un message de l'utilisateur et renvoie son id ; les agents qui répondent sont démarrés si besoin ; le message attend si le channel répond déjà. Un texte vide ou un channel inconnu sont des erreurs. Chaque `$chemin` joint un fichier du dossier d'un agent (voir [Fichiers joints](#fichiers-joints)) ; plus de 10 est une erreur. |
| `Cancel(id)` | Arrête la réponse en cours (`session/cancel`), les agents qui devaient encore répondre et les messages en attente ; répond « cancelled » aux demandes de permission en attente. |
| `AnswerPermission(id, optionId)` | Répond à une demande en attente avec l'une de ses options. |
| `Conversation(id)` | `channel, messages` (les 100 derniers), `more, busy, queued, answering, usage{agent: …}, permissions`. |
| `Older(id, beforeId)` | `{messages, more}` : les 100 messages précédant `beforeId`. |
| `Commands(agent)` | Les dernières commandes annoncées par l'agent, gardées d'un lancement à l'autre ; vide s'il n'en a jamais annoncé. |
| `Prepare(id)` | Démarre les membres du channel qui n'ont jamais annoncé leurs commandes et ouvre leur session pour ce channel (aucun modèle n'est chargé) ; rend la main une fois les sessions ouvertes, les commandes arrivent par `commands:changed`. Un membre qui répond ailleurs est ignoré. La session sert la première réponse du membre, à qui l'on donne quand même le passé du channel. |
### Historique
| | |
| --- | --- |
| Emplacement | `<dossier de config utilisateur>/sidekicks/data/<16 caractères hexadécimaux>/` : une base Pebble par fichier de configuration (l'empreinte de son chemin absolu). Le dossier de config utilisateur est `~/Library/Application Support` sous macOS, `$XDG_CONFIG_HOME` ou `~/.config` sous Linux. |
| Gardé | les channels, les membres (avec l'id de session ACP de chaque agent et le dernier message qu'il a vu), les messages terminés, les dernières commandes de chaque agent |
| Non gardé | les demandes de permission en attente, les messages en attente, une réponse interrompue par la fermeture |
| Indisponible | si la base ne peut pas être ouverte (un autre Sidekicks utilise le même fichier), l'historique vit en mémoire jusqu'à la fermeture, et `ConfigService.Current().historyProblem` dit pourquoi |
| Après un redémarrage | un agent qui sait faire `session/load` reprend sa session ; sinon, son premier prompt dans un channel contient les derniers messages de ce channel |
## Workspaces (milestone M4)
Le workspace d'un agent est son dossier `workspace`, atteint selon `runtime.type`.
| Runtime | Fichiers | Changements suivis | Terminal | Suppression |
| --- | --- | --- | --- | --- |
| `local` | directement | notifications du système de fichiers (pas dans `.git`, `node_modules`), une demi-seconde après le dernier changement | `$SHELL -l` (sinon bash, sinon sh) dans un pseudo-terminal | vers la corbeille du système (`~/.Trash`, ou la corbeille freedesktop.org) ; définitive si la corbeille est sur un autre disque |
| `container` | `docker exec` dans `workspacePath` (par défaut : le même chemin que `workspace`) | dates et tailles comparées toutes les 2 s | `docker exec -it` : bash s'il existe, sinon sh | définitive |
| `sbx` | `sbx exec` dans `workspacePath` (par défaut : le même chemin que `workspace`, où sbx monte le dossier de l'hôte) | dates et tailles comparées toutes les 2 s | `sbx exec -it` : bash s'il existe, sinon sh | définitive |
Les chemins sont relatifs au dossier et utilisent `/`. Un chemin qui sort du dossier (`..`, absolu, lien symbolique vers l'extérieur) est refusé. Le dossier lui-même ne peut pas être supprimé.
### Fichiers
| Champ | Signification |
| --- | --- |
| `name`, `path` | le nom de l'entrée, et son chemin depuis le dossier |
| `dir` | un dossier |
| `size`, `modTime` | taille en octets, dernière modification |
Un fichier ouvert pour modification a `path, content, version` ; `version` identifie le contenu.
### `WorkspaceService`
| Méthode | Effet |
| --- | --- |
| `Info(agent)` | `{location, problem}` : où est le dossier (`local · /chemin`, `container nom · /chemin`), ou pourquoi il ne peut pas être affiché. |
| `List(agent, dir)` | Les entrées de `dir` (`""` est le dossier), dossiers d'abord, puis par nom. |
| `Read(agent, path)` | Un fichier texte de 1 Mo au plus ; un fichier binaire ou plus gros est une erreur. |
| `Save(agent, path, content, version)` | Écrit le fichier s'il a toujours `version` ; sinon `{conflict: true}` et rien n'est écrit. Une `version` vide écrase. Le fichier est écrit à côté puis déplacé à sa place. |
| `Create(agent, dir, name)` | Crée un fichier vide ; refusé si le nom existe ou contient `/`. |
| `Remove(agent, path)` | Supprime un fichier ou un dossier avec son contenu (voir le tableau ci-dessus). |
| `Attach(agent, hostPath)` | Joint un fichier de cet ordinateur à un message : renvoie son chemin dans le dossier de l'agent, à écrire après `$`. Un fichier déjà dans le dossier est utilisé sur place ; tout autre est copié dans `attachments/` (nom libre, espaces remplacés par `-`, 5 Mo au plus). |
| `ChooseAttachment()` | Affiche le sélecteur de fichier natif, depuis le dossier personnel ; renvoie le chemin choisi, ou `""`. |
| `Image(agent, path)` | Une image du dossier (`.png`, `.jpg`, `.gif`, `.webp`, `.svg`…, 5 Mo au plus) sous forme d'URL `data:`, pour l'aperçu. Tout autre fichier est une erreur. |
| `Watch(agent)`, `Unwatch()` | Suit les changements du dossier d'un agent, annoncés par `workspace:changed`. |
### `TerminalService`
| Méthode | Effet |
| --- | --- |
| `Open(agent, cols, rows)` | Démarre un shell dans le dossier de l'agent ; renvoie son id. |
| `Write(id, data)` | Envoie des frappes. |
| `Resize(id, cols, rows)` | Change la taille, en caractères. |
| `Close(id)` | Raccroche le shell et tout ce qu'il a lancé (les groupes de processus de ses descendants), tâches de fond comprises, puis tue ce qui reste au bout d'une seconde ; dans un conteneur, à l'intérieur du conteneur aussi. |
### Événements
| Événement | Contenu | Envoyé quand |
| --- | --- | --- |
| `workspace:changed` | `{agent}` | des fichiers ont changé dans le dossier suivi |
| `terminal:output` | `{id, data}` | le terminal a affiché quelque chose ; regroupé toutes les 16 ms ou 32 Ko, sans jamais couper un caractère |
| `terminal:exit` | `{id}` | le shell s'est terminé |
## Méthodes Settings de `ConfigService`
| Méthode | Effet |
| --- | --- |
| `Content()` | Le texte du fichier de configuration. |
| `Validate(text)` | Les problèmes et avertissements de `text`, avec `line, column, message, warning`, sans enregistrer. |
| `Save(text)` | Écrit `text` s'il est valide ; sinon renvoie ses problèmes et n'écrit rien. |
| `Schema()` | Le JSON Schema de la configuration. |
| `Open(path)` | Arrête les réponses et tous les agents, ferme l'historique, charge et surveille `path` avec son propre historique, le mémorise. |
| `Choose()` | Affiche le sélecteur de fichier natif et ouvre le fichier choisi ; renvoie `""` en cas d'annulation. |
| `Recent()` | Les fichiers de configuration ouverts depuis l'application, du plus récent au plus ancien (8 au plus). |
| `ReadSandboxEnv(path)` | Lit un fichier d'environnement sbx pour l'assistant : `name, workspace, sandboxFolder, createCommand, stopCommand, removeCommand` (commandes sur une ligne). Un fichier qui n'en est pas un est une erreur. |
| `ChooseSandboxEnv()` | Affiche le sélecteur de fichier natif, depuis le dossier personnel, pour un fichier d'environnement sbx ; renvoie son chemin, ou `""` en cas d'annulation. |
| `ChooseAgentConfig()` | De même, pour le fichier de configuration d'un agent (YAML). |
| `ReadCompose(path)` | Lit un fichier Docker Compose pour l'assistant : `folder`, et pour chaque service `name, container, createCommand, stopCommand, removeCommand`. Un fichier sans services est une erreur. |
| `ChooseCompose()` | Le sélecteur de fichier natif, pour un fichier Compose. |
| `Reset(choice)` | La Danger zone : `settings` (configuration par défaut, préférences oubliées), `history` (celui de cette configuration), `allHistory` (celui de toutes les configurations) ou `everything`. Chaque élément réinitialisé est d'abord gardé en copie datée ; renvoie `{copies[]}`. L'historique est rouvert vide et les channels déclarés sont recréés ; puis `config:changed` est envoyé. |
## Arrêt d'un agent
L'entrée standard de l'agent est fermée ; après 5 s, l'agent et ses enfants reçoivent SIGTERM (le temps pour `scripts/agents.sh` de décharger le modèle) ; 3 s plus tard, SIGKILL.
|