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
|
# Reference: agents, channels and events
## Agent states
| State | Shown as | Meaning |
| --- | --- | --- |
| `stopped` | stopped | No process. The initial state of every agent. |
| `starting` | starting… | The process is launched and the ACP handshake (`initialize`) is in progress. |
| `ready` | ready | Running and idle. |
| `busy` | busy | Running and used by at least one conversation. |
| `error` | error | The start failed, or the process exited on its own (crash). The status carries the reason, followed by the agent's last stderr lines for a failed start. |
`outdated` (shown as "restart to apply changes") is set on a running agent whose definition changed in the configuration; a restart applies it.
## Transitions
| From | Event | To |
| --- | --- | --- |
| `stopped`, `error` | start requested (▶ in the Agents panel, or first use) | `starting` |
| `starting` | handshake succeeds | `ready` |
| `starting` | handshake fails, protocol other than ACP v1, timeout (30 s) | `error` |
| `ready` | used | `busy` |
| `busy` | last user done | `ready` |
| `ready`, `busy` | stop requested (■), application quits, agent removed from the configuration | `stopped` |
| `ready` | unused for `limits.stopIdleAgentsAfter` (checked every 30 s) | `stopped` |
| `ready` | another agent must start and this one is the least recently used idle agent | `stopped` |
| `ready`, `busy` | the process exits on its own | `error` (never restarted automatically) |
| `starting` | stop requested | `stopped` once the start ends |
When `limits.maxRunningAgents` agents are running and all are `busy`, a start is refused with the message `too many agents running: N agent(s) busy (…), the limit is N`.
Stopping an agent closes its stdin, waits 5 seconds, then kills its process group (the agent and what it started, such as `mm` under `scripts/agents.sh`).
## Events from Go to the interface
| Event | Payload | Sent when |
| --- | --- | --- |
| `config:changed` | `{path, valid, problems[]}` | The configuration file was saved and reloaded (valid or not). |
| `agent:state` | `{agent, state, error?}` | An agent changes state or its definition changes. |
| `agent:log` | `{agent, line}` | An agent writes a line on its standard error. |
The Go names and types are in `internal/events`; the TypeScript constants and types are generated in `frontend/bindings/events` and `frontend/bindings/…/internal/events`.
## Services called by the interface
| Service | Method | Effect |
| --- | --- | --- |
| `AgentService` | `List()` | Every agent with `name, title, icon, description, state, error, outdated`, in configuration order. |
| | `Start(name)`, `Stop(name)`, `Restart(name)` | As in the transitions above; an error is returned to the interface. |
| | `Logs(name)` | The last 200 stderr lines of the agent. |
| `ConfigService` | `Current()` | `path, source, created, valid, problems[], warnings[], theme, language, historyProblem`. |
## Channels (milestones M2 and M3)
A channel holds a conversation between the user and one or several agents. A direct conversation is a channel with one member, no name, and `direct` set to its agent's name. Channels, members and finished messages are kept in the history of the configuration file (below); a streaming answer is kept once it ends.
### Channels
| Field | Meaning |
| --- | --- |
| `id` | sortable identifier |
| `name`, `description` | 1 to 64 characters, unique whatever the case; empty for a direct conversation |
| `direct` | the agent of a direct conversation; empty for a channel |
| `members` | agent names, in the order they joined |
### Who answers a message
| The message | Answered by |
| --- | --- |
| `/agent:command arguments` (`agent` is a configured agent) | that agent, which receives `/command arguments` |
| contains `@name` mentions of configured agents | the mentioned agents, one after the other, in the order of the mentions; a mentioned agent that is not a member joins first |
| no mention | the last agent who answered in the channel; with a single member, that member |
| no mention and nobody answered yet, several members | nobody: system message `no-recipient` |
| any message in a direct conversation | its agent; mentions are plain text there |
A message starting with `/` (possibly after mentions) is a command: it is sent alone. Any other message is sent with the channel messages the agent has not seen yet (at most the last 20, each cut at 1500 characters). An agent answers one channel at a time; a channel waiting for it shows whose turn it is.
### Messages
| Field | Meaning |
| --- | --- |
| `id`, `channel`, `createdAt` | identity |
| `agent` | the author of an agent message, or the agent a system message is about |
| `role` | `user`, `agent` or `system` |
| `parts` | `text` (Markdown), `thought` (reasoning), `tool` (a tool call), `plan` (plan entries) |
| `done` | false while an agent message streams |
| `queued` | a user message waiting for the current answers |
| `stopReason` | how an agent message ended: `end_turn`, `cancelled`, `max_tokens`, `max_turn_requests`, `refusal` |
| `error` | why an agent message ended without a stop reason (the agent's own explanation), or `cancelled` for a queued message dropped by Stop |
| `code` | system messages: `agent-joined`, `agent-left`, `no-recipient`, `agent-unavailable` (could not start or reach it; `error` says why) |
A tool call has `id, title, kind` (`read, edit, delete, move, search, execute, think, fetch, other`), `status` (`pending, in_progress, completed, failed`), `input` (compact JSON), `output`, `locations`, `diffs` (`path, oldText, newText, newFile`).
### Events
| Event | Payload | Sent when |
| --- | --- | --- |
| `channels:changed` | `{channels[]}` | a channel is created, renamed or deleted, or its members change |
| `message:updated` | `{channel, message}` | a message is added, or finished |
| `message:delta` | `{channel, messageId, index, kind, text?, part?}` | `text` is appended to the part at `index` (text, thought), or `part` replaces it (tool, plan) |
| `turn:changed` | `{channel, busy, queued, answering}` | the channel starts or stops answering, or another agent's turn begins; `queued` messages wait |
| `permission:requested` | `{id, channel, agent, messageId, toolCall, options[], rejections}` | an agent asks to run a tool; `rejections` counts the refusals already given in this turn |
| `permission:resolved` | `{id, channel}` | the request was answered, or cancelled by Stop |
| `usage:changed` | `{channel, agent, usage: {used, size}}` | an agent reported its context window use (`size` is 0 when unknown) |
| `commands:changed` | `{agent, commands[]}` | an agent announced its slash commands (`name, description, hint`) |
### `ChannelService`
| Method | Effect |
| --- | --- |
| `List()` | Every channel and direct conversation, oldest first. |
| `Create(name, description)` | Creates a channel. A taken or empty name is an error. |
| `Rename(id, name, description)` | Renames a channel (not a direct conversation). |
| `Delete(id)` | Stops its answers, then deletes the channel and its whole history. |
| `AddMember(id, agent)`, `RemoveMember(id, agent)` | Invites or removes an agent; the channel shows `agent-joined` or `agent-left`. |
| `Direct(agent)` | The direct conversation with `agent`, created the first time. |
| `Send(id, text)` | Adds a user message and returns its id; the agents that answer are started if needed; the message is queued if the channel is answering. Empty text and unknown channels are errors. |
| `Cancel(id)` | Stops the current answer (`session/cancel`), the agents still to answer and the queued messages; answers pending permission requests "cancelled". |
| `AnswerPermission(id, optionId)` | Answers a pending request with one of its options. |
| `Conversation(id)` | `channel, messages` (the last 100), `more, busy, queued, answering, usage{agent: …}, permissions`. |
| `Older(id, beforeId)` | `{messages, more}`: the 100 messages before `beforeId`. |
| `Commands(agent)` | The commands the agent announced last, kept from one run to the next; empty if it never announced any. |
| `Prepare(id)` | Starts the members of the channel that never announced their commands and opens their session for it (no model is loaded); returns once the sessions are open, the commands arrive as `commands:changed`. A member answering elsewhere is skipped. The session serves the member's first answer, which is still told the channel's past. |
### History
| | |
| --- | --- |
| Location | `<user config dir>/sidekicks/data/<16 hex characters>/`: one Pebble database per configuration file (the hash of its absolute path). `<user config dir>` is `~/Library/Application Support` on macOS, `$XDG_CONFIG_HOME` or `~/.config` on Linux. |
| Kept | channels, members (with each agent's ACP session id and the last message it saw), finished messages, the last commands of each agent |
| Not kept | pending permission requests, queued messages, an answer interrupted by quitting |
| Unavailable | if the database cannot be opened (another Sidekicks uses the same file), the history lives in memory until Sidekicks quits, and `ConfigService.Current().historyProblem` says why |
| After a restart | an agent that supports `session/load` resumes its session; otherwise its first prompt in a channel carries the last messages of that channel |
## Workspaces (milestone M4)
An agent's workspace is its `workspace` folder, reached according to `runtime.type`.
| Runtime | Files | Changes followed | Terminal | Deletion |
| --- | --- | --- | --- | --- |
| `local` | directly | file system notifications (not inside `.git`, `node_modules`), half a second after the last change | `$SHELL -l` (else bash, else sh) in a pseudo-terminal | to the system trash (`~/.Trash`, or the freedesktop.org trash); for good if the trash is on another disk |
| `container` | `docker exec` in `workspacePath` (default: the same path as `workspace`) | dates and sizes compared every 2 s | `docker exec -it`: bash if present, else sh | for good |
| `sbx` | `sbx exec` in `workspacePath` (default: the same path as `workspace`, where sbx mounts the host folder) | dates and sizes compared every 2 s | `sbx exec -it`: bash if present, else sh | for good |
Paths are relative to the folder and use `/`. A path leaving the folder (`..`, absolute, a symbolic link pointing out) is refused. The folder itself cannot be deleted.
### Files
| Field | Meaning |
| --- | --- |
| `name`, `path` | the entry's name, and its path from the folder |
| `dir` | a folder |
| `size`, `modTime` | size in bytes, last change |
A file opened for editing has `path, content, version`; `version` identifies the content.
### `WorkspaceService`
| Method | Effect |
| --- | --- |
| `Info(agent)` | `{location, problem}`: where the folder is (`local · /path`, `container name · /path`), or why it cannot be shown. |
| `List(agent, dir)` | The entries of `dir` (`""` is the folder), folders first, then by name. |
| `Read(agent, path)` | A text file of at most 1 MB; a binary or larger file is an error. |
| `Save(agent, path, content, version)` | Writes the file if it still has `version`; otherwise `{conflict: true}` and nothing is written. An empty `version` overwrites. The file is written next to its place and moved over it. |
| `Create(agent, dir, name)` | Creates an empty file; refused if the name exists or contains `/`. |
| `Remove(agent, path)` | Deletes a file or a folder with its content (see the table above). |
| `Watch(agent)`, `Unwatch()` | Follows the changes of one agent's folder, announced by `workspace:changed`. |
### `TerminalService`
| Method | Effect |
| --- | --- |
| `Open(agent, cols, rows)` | Starts a shell in the agent's folder; returns its id. |
| `Write(id, data)` | Sends keystrokes. |
| `Resize(id, cols, rows)` | Changes the size, in characters. |
| `Close(id)` | Hangs up the shell and everything it started (its descendants' process groups), background jobs included, then kills what is left after a second; in a container, inside the container too. |
### Events
| Event | Payload | Sent when |
| --- | --- | --- |
| `workspace:changed` | `{agent}` | files changed in the folder being watched |
| `terminal:output` | `{id, data}` | the terminal printed something; grouped every 16 ms or 32 KB, never cutting a character |
| `terminal:exit` | `{id}` | the shell exited |
## Settings methods of `ConfigService`
| Method | Effect |
| --- | --- |
| `Content()` | The text of the configuration file. |
| `Validate(text)` | The problems and warnings of `text`, with `line, column, message, warning`, without saving. |
| `Save(text)` | Writes `text` if it is valid; otherwise returns its problems and writes nothing. |
| `Schema()` | The configuration JSON Schema. |
| `Open(path)` | Stops the answers and every agent, closes the history, loads and watches `path` with its own history, remembers it. |
| `Choose()` | Shows the native file dialog and opens the chosen file; returns `""` when cancelled. |
| `Recent()` | The configuration files opened from the application, most recent first (8 at most). |
## Stopping an agent
The agent's stdin is closed; after 5 s the agent and its children get SIGTERM (time for `scripts/agents.sh` to unload the model); after 3 more seconds, SIGKILL.
|