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
|
# 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 |
### 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.
|