bots-garden/sidekickspublic Fork
⑂ main
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekicks.git
git clone ssh://git@rickub.com/bots-garden/sidekicks.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 8ef4097 · on main · k33g · 3d ago
architecture.md · 48 lines · 4.6 KBmarkdown
Blame HistoryOpen raw

Architecture — explanation

What is this about?

Before writing the application, the project fixed how its code is divided, what is stored where, and how the Go side informs the interface. This page tells the story; the precise package list, data model, storage keys and event catalogue are in ADR 0003.

Packages and dependencies

The diagram's source is packages.drawio. Every box exists (v0.1).

Why this split

Three facts drove it:

  • An agent is shared. Alice may be in three channels at once, but she is one process with one model in memory. So one package owns the agent processes (internal/agents) and another owns channels and routing (internal/channels); a channel asks for "a session with Alice", it never starts her.
  • The interface must not know ACP. If the Svelte code handled ACP messages, changing protocol version or agent would ripple into the interface. Everything the interface sees goes through internal/app, as Sidekicks' own calls and events.
  • The configuration can change while agents run. It is loaded, validated and watched in one place (internal/config), which tells the others.

Where Wails stops

Only main.go, at the root of the repository, knows Wails: it creates the window, binds the services of internal/app and plugs the Wails event manager in. Everything below it is plain Go that emits through a small Emitter interface, so the agent manager, the configuration and the services are tested without a window. The main package sits at the root rather than in cmd/ because the build files Wails generates expect it there; fighting the tool would cost more than it gives.

What lives in the YAML and what lives in the database

The YAML file describes what exists: agents, limits, preferences, and channels to create. It is meant to be read and edited by a person. What happens — the channels created from the interface, who joined them, the messages — is stored by the application in Pebble, an embedded key-value store. A channel declared in YAML is created once at startup and then lives in the database like any other, so that editing the YAML never deletes a conversation by accident.

Each configuration file gets its own database: switching from "my work agents" to "my writing agents" never mixes their histories.

Messages are written once, at the end

While an agent answers, the answer lives in memory and the interface receives each fragment. It is written to disk when the turn ends. Writing fifty times a second would wear the disk for nothing; the price is losing an unfinished answer if the application crashes, which is acceptable.

Programs are found as in a terminal

An application opened from the Finder does not inherit the PATH of the user's terminal: macOS gives it /usr/bin:/bin:/usr/sbin:/sbin, so mm, docker or sbx installed with Homebrew or in ~/.local/bin are "not found", although the user just ran them in a terminal. Asking a non-developer to write full paths would be the wrong answer. At startup, before the configuration is read, main.go asks the user's shell for its PATH (internal/shellpath: an interactive login shell, so that it reads the same files as a terminal, with a five-second limit) and puts it in front of its own. The agents inherit it, and the warnings of the configuration ("program not found") are computed with it. When the shell cannot be read, nothing changes and the full path remains the way out.

Errors in plain words

Go, the agents, Docker and sbx report errors for developers (exec: "mm": executable file not found in $PATH). The Go side keeps them as they are: they are precise, the tests check them, and they are what a developer needs in the log. The interface translates the common ones (frontend/src/lib/errors.ts) into a sentence that says what is wrong and what to do, in the interface's language, and keeps the original text folded under Technical details. An unknown error is shown as it is rather than hidden behind a vague sentence.

Rejected alternatives

  • Storing conversations in the YAML file: it would turn a file people edit into a database, and conflict with the application writing to it.
  • One package per Wails service with logic inside: it would mix interface concerns with the domain and make it untestable without a window.

How it relates to the rest

The protocol side is explained in Why ACP, the interface side in The interface stack, and the file format is described in the configuration reference.

 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
# Architecture — explanation

## What is this about?

Before writing the application, the project fixed how its code is divided, what is stored where, and how the Go side informs the interface. This page tells the story; the precise package list, data model, storage keys and event catalogue are in [ADR 0003](../../adr/0003-architecture.md).

![Packages and dependencies](../../diagrams/packages.png)

The diagram's source is [packages.drawio](../../diagrams/packages.drawio). Every box exists (v0.1).

## Why this split

Three facts drove it:

- **An agent is shared.** Alice may be in three channels at once, but she is one process with one model in memory. So one package owns the agent processes (`internal/agents`) and another owns channels and routing (`internal/channels`); a channel asks for "a session with Alice", it never starts her.
- **The interface must not know ACP.** If the Svelte code handled ACP messages, changing protocol version or agent would ripple into the interface. Everything the interface sees goes through `internal/app`, as Sidekicks' own calls and events.
- **The configuration can change while agents run.** It is loaded, validated and watched in one place (`internal/config`), which tells the others.

## Where Wails stops

Only `main.go`, at the root of the repository, knows Wails: it creates the window, binds the services of `internal/app` and plugs the Wails event manager in. Everything below it is plain Go that emits through a small `Emitter` interface, so the agent manager, the configuration and the services are tested without a window. The main package sits at the root rather than in `cmd/` because the build files Wails generates expect it there; fighting the tool would cost more than it gives.

## What lives in the YAML and what lives in the database

The YAML file describes **what exists**: agents, limits, preferences, and channels to create. It is meant to be read and edited by a person. What **happens** — the channels created from the interface, who joined them, the messages — is stored by the application in Pebble, an embedded key-value store. A channel declared in YAML is created once at startup and then lives in the database like any other, so that editing the YAML never deletes a conversation by accident.

Each configuration file gets its own database: switching from "my work agents" to "my writing agents" never mixes their histories.

## Messages are written once, at the end

While an agent answers, the answer lives in memory and the interface receives each fragment. It is written to disk when the turn ends. Writing fifty times a second would wear the disk for nothing; the price is losing an unfinished answer if the application crashes, which is acceptable.

## Programs are found as in a terminal

An application opened from the Finder does not inherit the `PATH` of the user's terminal: macOS gives it `/usr/bin:/bin:/usr/sbin:/sbin`, so `mm`, `docker` or `sbx` installed with Homebrew or in `~/.local/bin` are "not found", although the user just ran them in a terminal. Asking a non-developer to write full paths would be the wrong answer. At startup, before the configuration is read, `main.go` asks the user's shell for its `PATH` (`internal/shellpath`: an interactive login shell, so that it reads the same files as a terminal, with a five-second limit) and puts it in front of its own. The agents inherit it, and the warnings of the configuration ("program not found") are computed with it. When the shell cannot be read, nothing changes and the full path remains the way out.

## Errors in plain words

Go, the agents, Docker and sbx report errors for developers (`exec: "mm": executable file not found in $PATH`). The Go side keeps them as they are: they are precise, the tests check them, and they are what a developer needs in the log. The interface translates the common ones (`frontend/src/lib/errors.ts`) into a sentence that says what is wrong and what to do, in the interface's language, and keeps the original text folded under **Technical details**. An unknown error is shown as it is rather than hidden behind a vague sentence.

## Rejected alternatives

- **Storing conversations in the YAML file**: it would turn a file people edit into a database, and conflict with the application writing to it.
- **One package per Wails service with logic inside**: it would mix interface concerns with the domain and make it untestable without a window.

## How it relates to the rest

The protocol side is explained in [Why ACP](why-acp.md), the interface side in [The interface stack](ui-stack.md), and the file format is described in the [configuration reference](../reference/configuration.md).