agentainer 0.1.7 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +248 -677
- package/agentainer +16 -18
- package/agentainer.example.yaml +86 -0
- package/bin/agentainer.js +9 -8
- package/examples/brainstorm.yaml +27 -128
- package/examples/bug-hunt.yaml +51 -96
- package/examples/code-review.yaml +73 -0
- package/examples/debate.yaml +16 -90
- package/examples/incident-response.yaml +52 -109
- package/examples/localization.yaml +56 -123
- package/examples/quickstart.yaml +48 -0
- package/examples/research.yaml +25 -0
- package/examples/software-company.yaml +71 -128
- package/examples/tdd-pingpong.yaml +36 -68
- package/examples/writers-room.yaml +49 -111
- package/hooks/claude_stop.sh +5 -3
- package/hooks/codex_notify.sh +4 -3
- package/lib/cli.py +929 -0
- package/lib/config.py +247 -305
- package/lib/hooks.py +246 -0
- package/lib/lock.py +75 -0
- package/lib/log.py +64 -0
- package/lib/mail.py +634 -0
- package/lib/minyaml.py +1 -39
- package/lib/reconcile.py +473 -0
- package/lib/sessions.py +223 -0
- package/lib/supervisor.py +216 -0
- package/lib/telegram.py +372 -0
- package/lib/tmux.py +355 -0
- package/lib/turn.py +159 -0
- package/lib/ui.py +1020 -0
- package/llms.txt +145 -429
- package/package.json +9 -7
- package/scripts/check-deps.js +18 -61
- package/ui/app.js +869 -0
- package/ui/index.html +348 -0
- package/agents.example.yaml +0 -257
- package/examples/code-review-broadcast.yaml +0 -109
- package/examples/existing-repo.yaml +0 -74
- package/examples/multi-language-broadcast.yaml +0 -127
- package/examples/ping-pong.yaml +0 -89
- package/examples/red-team.yaml +0 -117
- package/examples/research-swarm.yaml +0 -129
- package/lib/swarm.py +0 -2461
package/README.md
CHANGED
|
@@ -1,727 +1,298 @@
|
|
|
1
|
-
|
|
2
|
-
<p align="center">
|
|
3
|
-
<img alt="Agentainer — multi-agent orchestrator for AI coding agents" src="https://raw.githubusercontent.com/mehmetcanfarsak/AgentSwarm/main/assets/banner.svg" width="820">
|
|
4
|
-
</p>
|
|
1
|
+
# 🐝 Agentainer
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<img alt="license" src="https://img.shields.io/badge/license-MIT-blue.svg">
|
|
9
|
-
<img alt="zero deps" src="https://img.shields.io/badge/dependencies-zero-brightgreen.svg">
|
|
10
|
-
<img alt="platform" src="https://img.shields.io/badge/platform-linux%20%2F%20macOS-lightgrey.svg">
|
|
11
|
-
<img alt="tests" src="https://img.shields.io/badge/tests-48%20checks%20passing-success.svg">
|
|
12
|
-
</p>
|
|
3
|
+
**Zero-dependency multi-agent orchestrator with a file-based mail model.**
|
|
13
4
|
|
|
14
|
-
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://github.com/mehmetcanfarsak/Agentainer#readme)
|
|
7
|
+
[](https://github.com/mehmetcanfarsak/Agentainer#readme)
|
|
8
|
+
[](https://github.com/mehmetcanfarsak/Agentainer#readme)
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Run a team of <b>Claude Code</b>, <b>OpenAI Codex</b>, <b>Gemini CLI</b>, and <b>Hermes</b> side by side in <b>tmux</b> — each in its own directory, each messaging the others only where your config allows.
|
|
19
|
-
</p>
|
|
10
|
+
> **Formerly AgentSwarm.** The v1 project was renamed and rewritten around a
|
|
11
|
+
> file-based mail model — the old name still resolves to this repo.
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
Agentainer launches AI coding agents (Claude Code, Codex, Gemini, Hermes) each in
|
|
14
|
+
its own `tmux` session and working directory, defined by a single
|
|
15
|
+
`agentainer.yaml`. They talk to each other only where the config's `can_talk_to`
|
|
16
|
+
ACL allows — by **reading a file** to receive and **writing a file** to send.
|
|
24
17
|
|
|
25
|
-
|
|
18
|
+
No runtime dependencies. No API keys required to exercise the mechanics. Agents
|
|
19
|
+
only need to read and write natural-language files; the orchestrator owns all the
|
|
20
|
+
hard logic (routing, ACL, message IDs, threading, read-state, queueing, retries,
|
|
21
|
+
availability, the durable log).
|
|
26
22
|
|
|
27
|
-
<!-- Demo -->
|
|
28
23
|
<p align="center">
|
|
29
|
-
<img
|
|
30
|
-
<br>
|
|
31
|
-
<em>Start a four-agent swarm, check status, hand it a task, and watch the messages route — in four commands.</em>
|
|
24
|
+
<img src="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/banner.svg" alt="Agentainer banner: zero-dependency multi-agent orchestrator with a file-based mail model" width="720"/>
|
|
32
25
|
</p>
|
|
33
26
|
|
|
27
|
+
> v2 replaces v1's "tagged XML envelope emitted inside prose and scraped out of a
|
|
28
|
+
> TUI pane" — that was unreliable across LLMs. The file-based mail model works on
|
|
29
|
+
> nearly every tool-calling model, including weak ones.
|
|
30
|
+
|
|
34
31
|
---
|
|
35
32
|
|
|
36
|
-
##
|
|
33
|
+
## 📑 Contents
|
|
37
34
|
|
|
38
|
-
- [Why Agentainer](#why-agentainer)
|
|
39
|
-
- [How it fits together](#how-it-fits-together)
|
|
40
|
-
- [Requirements](#requirements)
|
|
41
35
|
- [Install](#install)
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
44
|
-
- [
|
|
45
|
-
- [Capturing what an agent says](#capturing-what-an-agent-says)
|
|
46
|
-
- [Subagents, parallel work, and busy agents](#subagents-parallel-work-and-busy-agents)
|
|
47
|
-
- [Configuration reference](#configuration-reference)
|
|
48
|
-
- [Examples](#examples)
|
|
36
|
+
- [Quick start (no API keys)](#quick-start-no-api-keys)
|
|
37
|
+
- [The model in one screen](#the-model-in-one-screen)
|
|
38
|
+
- [Architecture](#architecture)
|
|
49
39
|
- [Commands](#commands)
|
|
50
|
-
- [
|
|
51
|
-
- [
|
|
52
|
-
- [
|
|
53
|
-
- [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
## <span id="why-agentainer"></span>✨ Why Agentainer
|
|
57
|
-
|
|
58
|
-
- **Multi-agent, one command.** Spin up a whole team with `agentainer up` — folders, hooks, tmux sessions, and first prompts, all wired automatically.
|
|
59
|
-
- **Zero runtime dependencies.** Just Python 3 + bash + tmux. No pip installs, no bloated toolchain.
|
|
60
|
-
- **Real ACLs, not vibes.** Agents only message who your YAML `can_talk_to` list permits. `"*"` for everyone, an explicit list for tight graphs.
|
|
61
|
-
- **Survives restarts.** Each turn's conversation id is recorded; `up --resume` reattaches instead of starting cold.
|
|
62
|
-
- **Self-healing routing.** Busy agents queue inbound work, stranded messages get swept, and agents are nudged when a reply reaches nobody.
|
|
63
|
-
- **Any mix of CLIs.** Claude, Codex, Gemini, Hermes — or your own type — in the same swarm.
|
|
64
|
-
|
|
65
|
-
## <span id="how-it-fits-together"></span>🏗️ How It Fits Together
|
|
66
|
-
|
|
67
|
-
*Architecture at a glance: a single `agents.yaml` file maps each AI coding agent to its own tmux session and working directory, while Agentainer routes and logs every message between them under a `can_talk_to` access-control list.*
|
|
68
|
-
|
|
69
|
-
<p align="center">
|
|
70
|
-
<img alt="Agentainer architecture: one YAML file maps each agent to its own tmux session and working directory; Agentainer routes and logs permission-checked messages between them" src="https://raw.githubusercontent.com/mehmetcanfarsak/AgentSwarm/main/assets/architecture.svg" width="900">
|
|
71
|
-
</p>
|
|
72
|
-
|
|
73
|
-
One command starts the swarm: it creates a folder per agent, installs a
|
|
74
|
-
completion hook inside each folder, opens a tmux session per agent, launches
|
|
75
|
-
the CLI, and types each agent's first prompt into it.
|
|
76
|
-
|
|
77
|
-
<details>
|
|
78
|
-
<summary>Plain-text diagram</summary>
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
agents.yaml tmux
|
|
82
|
-
┌────────────┐ ┌──────────────────┐
|
|
83
|
-
│ orchestr. │───────▶│ session: orchestr│──┐
|
|
84
|
-
│ researcher │ │ session: research│ │ swarm send --to developer "..."
|
|
85
|
-
│ developer │ │ session: develop │◀─┘
|
|
86
|
-
│ reviewer │ │ session: reviewer│
|
|
87
|
-
└────────────┘ └──────────────────┘
|
|
88
|
-
│ │
|
|
89
|
-
│ workspace/<agent>/ │ hooks capture each finished turn
|
|
90
|
-
▼ ▼
|
|
91
|
-
one folder per agent messages routed + logged
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
</details>
|
|
40
|
+
- [Key invariants](#key-invariants)
|
|
41
|
+
- [The control-plane UI (P2–P3)](#the-control-plane-ui-p2p3)
|
|
42
|
+
- [Dynamic reconcile (P4)](#dynamic-reconcile-p4)
|
|
43
|
+
- [Project status](#project-status)
|
|
44
|
+
- [FAQ](#faq)
|
|
45
|
+
- [License](#license)
|
|
95
46
|
|
|
96
47
|
---
|
|
97
48
|
|
|
98
|
-
##
|
|
99
|
-
|
|
100
|
-
- **`tmux`** (3.0+)
|
|
101
|
-
- **`python3`** — PyYAML is used if present, otherwise a bundled parser handles the config
|
|
102
|
-
- **`node`** (16+) — only for the global `agentainer` command; not needed if you run `./agentainer` from a clone
|
|
103
|
-
- whichever agent CLIs you reference: `claude`, `codex`, `gemini`, `hermes` — install only the one(s) you actually use
|
|
104
|
-
|
|
105
|
-
## <span id="install"></span>🚀 Install
|
|
106
|
-
|
|
107
|
-
**Global, via npm:**
|
|
49
|
+
## 📦 Install
|
|
108
50
|
|
|
109
51
|
```bash
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
52
|
+
git clone <repo> && cd Agentainer
|
|
53
|
+
npm install # runs the postinstall dep check; no build step
|
|
54
|
+
npm link # puts the `agentainer` bin on your PATH
|
|
55
|
+
agentainer --version
|
|
113
56
|
```
|
|
114
57
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
**Or from a clone (no npm needed):**
|
|
58
|
+
Once linked/installed, the command is just **`agentainer`**. `tmux` is the only
|
|
59
|
+
external runtime. `node` is required for the npm bin wrapper. PyYAML is used *if
|
|
60
|
+
present*; a bundled `minyaml` parser keeps everything working without it.
|
|
120
61
|
|
|
121
|
-
|
|
122
|
-
git clone https://github.com/mehmetcanfarsak/AgentSwarm.git && cd AgentSwarm
|
|
123
|
-
./agentainer --help # same commands as the global `agentainer`, straight from the repo
|
|
124
|
-
```
|
|
62
|
+
## 🚀 Quick start (no API keys)
|
|
125
63
|
|
|
126
|
-
|
|
64
|
+
The bundled quickstart uses `bash` loop "mock agents" so you can watch the
|
|
65
|
+
mailroom route mail safely:
|
|
127
66
|
|
|
128
67
|
```bash
|
|
129
|
-
cp
|
|
130
|
-
|
|
131
|
-
agentainer
|
|
132
|
-
agentainer
|
|
133
|
-
agentainer
|
|
134
|
-
agentainer
|
|
135
|
-
agentainer down # stop everything
|
|
68
|
+
cp examples/quickstart.yaml my-swarm.yaml
|
|
69
|
+
agentainer up -c my-swarm.yaml
|
|
70
|
+
agentainer status -c my-swarm.yaml
|
|
71
|
+
agentainer send -c my-swarm.yaml --to orchestrator "Build a CSV->Parquet CLI."
|
|
72
|
+
agentainer logs -c my-swarm.yaml -f
|
|
73
|
+
agentainer down -c my-swarm.yaml
|
|
136
74
|
```
|
|
137
75
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
```bash
|
|
141
|
-
agentainer send --to orchestrator "Build a CLI that converts CSV to Parquet."
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
Watch the traffic between agents:
|
|
145
|
-
|
|
146
|
-
```bash
|
|
147
|
-
agentainer logs -f # whole swarm, live
|
|
148
|
-
agentainer logs reviewer -n 20 # one agent
|
|
149
|
-
agentainer inbox developer # messages an agent received
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
`agentainer status` gives you the whole swarm at a glance — who is up, who is mid-turn, and how deep each queue is:
|
|
76
|
+
To run real agents, swap each `command` for the actual CLI you installed and drop
|
|
77
|
+
`capture: none` so turns get detected.
|
|
153
78
|
|
|
154
79
|
<p align="center">
|
|
155
|
-
<img alt="
|
|
80
|
+
<img src="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/demo.svg" alt="Terminal cast of the Agentainer quickstart: up, status, send, then logs showing the mailroom routing mail between mock agents" width="700"/>
|
|
156
81
|
</p>
|
|
157
82
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
## <span id="faq"></span>❓ FAQ
|
|
161
|
-
|
|
162
|
-
**What is Agentainer?**
|
|
163
|
-
Agentainer is a zero-dependency multi-agent orchestrator that runs a team of AI coding agents — Claude Code, OpenAI Codex, Google's Gemini CLI, and Hermes — in isolated tmux sessions, each with its own working directory, and routes messages between them under a configurable access-control list. You describe the whole team in one YAML file and start it with a single command.
|
|
164
|
-
|
|
165
|
-
**Which AI coding agents does Agentainer support?**
|
|
166
|
-
Claude Code (`claude`), OpenAI Codex (`codex`), Google's Gemini CLI (`gemini`), and Hermes (`hermes`) out of the box. You can also define your own agent type via `agent_types:` — any CLI that takes input on stdin/tty works, with `capture: pane` or `capture: none`.
|
|
167
|
-
|
|
168
|
-
**Does Agentainer need Docker or any runtime dependencies?**
|
|
169
|
-
No. It is stdlib-only: Python 3, bash, and tmux. PyYAML is used if present, but a bundled parser handles the config without it. Node is only needed for the global `npm` launcher — running `./agentainer` from a clone needs no Node at all.
|
|
170
|
-
|
|
171
|
-
**How do I run multiple coding agents together?**
|
|
172
|
-
Write a YAML config listing your agents (and who may talk to whom), then run `agentainer up`. It creates a folder and a tmux session per agent, installs a turn-completion hook, launches each CLI, and types in the first prompt. `agentainer send --to <agent> "…"` messages any agent; `agentainer validate` previews the resolved swarm without launching anything.
|
|
173
|
-
|
|
174
|
-
**Can agents communicate with each other, and how is that controlled?**
|
|
175
|
-
Yes. Agents exchange tagged `<swarm-message>` / `<swarm-send>` blocks. Messaging is a whitelist: an agent may only reach the agents in its `can_talk_to` list (`"*"` means everyone else); anything else is refused. Auto-forwarding (`forward_responses_to`) can relay finished turns hands-free, bounded by a hop count to prevent loops.
|
|
176
|
-
|
|
177
|
-
**How do I resume a swarm after a crash or reboot?**
|
|
178
|
-
Each finished turn records the agent's conversation id in `.swarm/sessions.yaml`. `agentainer up --resume` reattaches every agent to its own conversation instead of starting cold, keeping any queued mail. Claude resumes with `--resume <id>`, Codex with `resume <id>`; Gemini and Hermes have no recoverable session id and always start fresh (with a warning).
|
|
179
|
-
|
|
180
|
-
**Is it safe to let agents run unattended?**
|
|
181
|
-
Agents run with whatever privileges their CLI is launched with. Flags like `claude --dangerously-skip-permissions`, `codex --yolo`, and `gemini --yolo` let them act without confirmation, which is usually what you want for an unattended swarm — so point `root` at a disposable directory and never run a swarm over a checkout you can't afford to lose.
|
|
182
|
-
|
|
183
|
-
**How is Agentainer different from just opening several terminals?**
|
|
184
|
-
It automates the brittle parts: detecting when each turn finishes (via a Stop/notify hook for Claude/Codex, or pane polling for Gemini/Hermes), typing multi-line prompts reliably into a live TUI, pre-trusting folders so the "trust this directory?" modal doesn't eat the first prompt, enforcing the comms ACL, queueing messages to busy agents, and resuming conversations after a restart.
|
|
83
|
+
## 📬 The model in one screen
|
|
185
84
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
## <span id="how-it-works"></span>🔧 How It Works
|
|
189
|
-
|
|
190
|
-
**One folder + one tmux session per agent.** Agent `developer` gets
|
|
191
|
-
`workspace/developer/` and a tmux session named `developer` (plus any
|
|
192
|
-
`session_prefix`). The agent's CLI is launched inside that folder, so its file
|
|
193
|
-
operations are naturally scoped to it.
|
|
194
|
-
|
|
195
|
-
### Working directories
|
|
196
|
-
|
|
197
|
-
By default every agent gets a fresh folder under `root`, created for you. You can
|
|
198
|
-
override that per agent, or for the whole swarm via `defaults`:
|
|
85
|
+
An agent's entire world is **two verbs** (read a file, write a file) and **four
|
|
86
|
+
folders**:
|
|
199
87
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
- name: reviewer
|
|
209
|
-
workdir: ~/projects/acme-api # -> an existing checkout
|
|
210
|
-
create_workdir: false # ...and fail loudly if it is not there
|
|
211
|
-
|
|
212
|
-
- name: scribe
|
|
213
|
-
workdir: "{root}/{name}-notes" # {name} {root} {swarm} {type}
|
|
214
|
-
```
|
|
88
|
+
| 📁 Folder | 📝 Meaning |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| 📥 `inbox/` | The **one** current unread message (orchestrator releases one at a time). |
|
|
91
|
+
| 📤 `outbox/<name>/` | Write a file here to send to `<name>`. Read `<name>/about.md` for a contact card. |
|
|
92
|
+
| 📖 `read/` | Move a handled message here (best-effort read receipt). |
|
|
93
|
+
| 📨 `sent/` | The agent's own record of delivered mail (orchestrator moves it here). |
|
|
94
|
+
| 🚫 `failed/` | Mail bounced by the orchestrator (ACL violation, etc.), with a `system` explanation. |
|
|
215
95
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
repositories, where a typo should not silently create `~/projcets/acme-api`.
|
|
220
|
-
- **`defaults.workdir`** applies to every agent that does not override it. With
|
|
221
|
-
a `{name}` placeholder it lays out a folder each; without one, every agent
|
|
222
|
-
shares a single directory.
|
|
223
|
-
- **Sharing is allowed and sometimes the point** (a driver and a navigator in one
|
|
224
|
-
checkout), but agents then overwrite each other's files and interleave commits,
|
|
225
|
-
so `validate` and `up` warn when it happens. See
|
|
226
|
-
[`examples/existing-repo.yaml`](examples/existing-repo.yaml).
|
|
227
|
-
- `root` is still used even when every agent lives elsewhere: it holds `.swarm/`
|
|
228
|
-
with the logs, inboxes and the `swarm` shim.
|
|
229
|
-
|
|
230
|
-
**Prompts are typed in, not piped.** `swarm up` drops each first prompt into the
|
|
231
|
-
agent's input box with a tmux bracketed paste, as one block, then presses Enter.
|
|
232
|
-
That is why multi-line prompts survive intact instead of being submitted line by
|
|
233
|
-
line.
|
|
234
|
-
|
|
235
|
-
Getting that to work reliably against a live TUI took more than a sleep:
|
|
236
|
-
|
|
237
|
-
- **Claude Code silently discards keystrokes for several seconds partway through
|
|
238
|
-
startup.** Measured on v2.1.205: input at t=2s landed, t=6s and t=12s vanished,
|
|
239
|
-
t=20s landed. A fixed `boot_delay_ms` is therefore a coin flip. Before typing,
|
|
240
|
-
Agentainer types a throwaway token and waits for the input box to echo it back,
|
|
241
|
-
then erases it (`ready_probe`). Enter is never sent, so nothing is submitted.
|
|
242
|
-
- **Readiness is not monotonic**, so after pasting, Agentainer checks that the
|
|
243
|
-
text actually appeared on screen before pressing Enter, and retries if it did
|
|
244
|
-
not. If delivery cannot be confirmed it refuses to press Enter, rather than
|
|
245
|
-
submitting a half-delivered prompt.
|
|
246
|
-
- **Both CLIs open a "do you trust this folder?" modal** on first run in a new
|
|
247
|
-
directory, which would eat the first prompt (Enter answers the dialog). Claude
|
|
248
|
-
does this even under `--dangerously-skip-permissions`. Agentainer pre-trusts each
|
|
249
|
-
agent's workdir: for codex in its generated `config.toml`, for claude by adding
|
|
250
|
-
`hasTrustDialogAccepted` for that path in `~/.claude.json`.
|
|
251
|
-
- **Both collapse a long paste into a chip** rather than showing the text —
|
|
252
|
-
`[Pasted text #1 +36 lines]` for claude, `[Pasted Content 2580 chars]` for codex.
|
|
253
|
-
Delivery verification recognises both.
|
|
254
|
-
|
|
255
|
-
**Agents talk in tagged messages.** A message arrives inside an envelope, so the
|
|
256
|
-
agent always knows where it starts, where it ends, and who sent it:
|
|
96
|
+
When an agent **stops** with unread mail, the orchestrator sweeps its `outbox`
|
|
97
|
+
and pastes a **nudge** ("you have mail — read it, then move it to `read/`"),
|
|
98
|
+
re-injecting the protocol every time (including the allowed-recipient list).
|
|
257
99
|
|
|
258
|
-
|
|
259
|
-
<swarm-message from="lead" to="reviewer" id="m-eb4105" reply-to="m-3f9a1c">
|
|
260
|
-
Review finding for ./parse: tokenize() mishandles a trailing backslash.
|
|
261
|
-
|
|
262
|
-
printf 'a\' | ./parse
|
|
263
|
-
</swarm-message>
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
To send one, the agent simply **writes a block in its reply**. The capture hook
|
|
267
|
-
reads it when the turn ends and delivers it — no shell, no quoting, so multi-line
|
|
268
|
-
bodies, code blocks and backslashes survive intact:
|
|
269
|
-
|
|
270
|
-
```
|
|
271
|
-
<swarm-send to="reviewer" reply-to="m-3f9a1c">
|
|
272
|
-
Please review src/parse.py.
|
|
273
|
-
|
|
274
|
-
printf 'a\' | ./parse
|
|
275
|
-
</swarm-send>
|
|
276
|
-
```
|
|
100
|
+
## 🏗️ Architecture
|
|
277
101
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
old `[swarm] message from <sender>:` header, and `parse_outbound_tags: false` to
|
|
282
|
-
stop reading tags out of replies.
|
|
102
|
+
<a href="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/architecture.svg">
|
|
103
|
+
<img src="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/architecture.svg" alt="Agentainer architecture: agentainer.yaml drives a CLI core that opens one tmux session per agent; agents read and write mail files that the orchestrator routes" width="720"/>
|
|
104
|
+
</a>
|
|
283
105
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
```bash
|
|
288
|
-
swarm send --to reviewer "I finished the parser, please review src/parse.py"
|
|
289
|
-
swarm broadcast "heads up: I renamed the config module"
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
Either way the message is permission-checked against `can_talk_to`, archived under
|
|
293
|
-
`.swarm/inbox/<recipient>/`, and written to the event log.
|
|
294
|
-
|
|
295
|
-
The raw tmux equivalent also works, and bypasses permissions and logging:
|
|
296
|
-
|
|
297
|
-
```bash
|
|
298
|
-
tmux send-keys -t reviewer -l "your message" && tmux send-keys -t reviewer Enter
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
**Agents get reminded when their answer goes nowhere.** A model that was asked a
|
|
302
|
-
question will often just *write the answer as prose* and end its turn — and that
|
|
303
|
-
prose reaches nobody, because only a `<swarm-send>` block is delivered. So when an
|
|
304
|
-
agent owes a reply and finishes a turn without sending one, Agentainer messages it:
|
|
305
|
-
|
|
306
|
-
```
|
|
307
|
-
Your last turn sent no message to anyone, and lead is waiting on your answer to
|
|
308
|
-
message m-c73724.
|
|
309
|
-
...
|
|
310
|
-
<swarm-send to="lead" reply-to="m-c73724">
|
|
311
|
-
your answer here, over as many lines as you need
|
|
312
|
-
</swarm-send>
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
If instead the agent *tried* to send but the block was malformed, it gets the
|
|
316
|
-
specific diagnosis — unclosed tag, missing `to`, unknown recipient, permission
|
|
317
|
-
denied — so it can correct itself rather than lose the message silently.
|
|
318
|
-
|
|
319
|
-
It is reminded at most `max_reply_reminders` times (default **1**), then Agentainer
|
|
320
|
-
gives up and stops nagging. An agent that auto-forwards via `forward_responses_to`
|
|
321
|
-
is never reminded, since its words did reach someone. Turn it off per agent with
|
|
322
|
-
`reply_reminder: false`.
|
|
323
|
-
|
|
324
|
-
**Conversations survive a restart.** Each time an agent finishes a turn, its
|
|
325
|
-
conversation id is written to `<root>/.swarm/sessions.yaml`:
|
|
326
|
-
|
|
327
|
-
```yaml
|
|
328
|
-
agents:
|
|
329
|
-
lead:
|
|
330
|
-
session_id: "0c2e47e2-5110-4e69-ae45-69d8492d2084"
|
|
331
|
-
type: "claude"
|
|
332
|
-
transcript: "/root/.claude/projects/.../0c2e47e2-....jsonl"
|
|
333
|
-
updated_at: "2026-07-09T21:00:41+00:00"
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
If the machine dies, `swarm up --resume` reattaches every agent to its own
|
|
337
|
-
conversation instead of starting a fresh one — it does not re-send the first
|
|
338
|
-
prompt, and it keeps any mail still queued for that agent:
|
|
339
|
-
|
|
340
|
-
```bash
|
|
341
|
-
agentainer sessions # what is recorded, and the command that would resume it
|
|
342
|
-
agentainer up --resume # reattach; agents without a recorded id start fresh
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
Claude is resumed with `--resume <id>`, codex with `resume <id>`. Set
|
|
346
|
-
`swarm.resume: true` to make it the default, and `--no-resume` to override.
|
|
347
|
-
Gemini and hermes have no recoverable session id (their turns are scraped from
|
|
348
|
-
the terminal), so they always start a fresh conversation, with a warning.
|
|
349
|
-
|
|
350
|
-
If your command runs the CLI through an alias or wrapper, flags cannot simply be
|
|
351
|
-
appended — give the full recipe:
|
|
352
|
-
|
|
353
|
-
```yaml
|
|
354
|
-
- name: lead
|
|
355
|
-
command: "bash -ic chy3"
|
|
356
|
-
resume_command: "bash -ic 'chy3 --resume {session_id}'"
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
**Permissions are a whitelist.** An agent may only message the agents in its
|
|
360
|
-
`can_talk_to` list. Anything else is refused with an explanatory error that the
|
|
361
|
-
agent sees on its own terminal. Use `can_talk_to: "*"` for "everyone else".
|
|
362
|
-
|
|
363
|
-
---
|
|
364
|
-
|
|
365
|
-
## <span id="capturing-what-an-agent-says"></span>🎬 Capturing What An Agent Says
|
|
366
|
-
|
|
367
|
-
Agentainer needs to know when an agent finishes a turn — both to log it and to
|
|
368
|
-
support auto-forwarding. How it finds out depends on the CLI, and the two
|
|
369
|
-
mechanisms are **not** equally good:
|
|
370
|
-
|
|
371
|
-
| `capture` | Used by | Mechanism | Reliability |
|
|
372
|
-
|---|---|---|---|
|
|
373
|
-
| `hook` | `claude`, `codex` | The CLI runs a program when a turn completes | Exact — the model's final message |
|
|
374
|
-
| `pane` | `gemini`, `hermes` | Poll the tmux pane, diff it once it stops changing | Heuristic — sees rendered text |
|
|
375
|
-
| `none` | any | Nothing is captured | — |
|
|
376
|
-
|
|
377
|
-
- **claude** → a `Stop` hook is written into `<agent-dir>/.claude/settings.json`,
|
|
378
|
-
with **no `matcher` key** (`Stop` is not a tool event, and supplying one stops the
|
|
379
|
-
interactive TUI from ever running the hook). It reads the session transcript.
|
|
380
|
-
Claude fires the hook *before* flushing the assistant message to that transcript,
|
|
381
|
-
so Agentainer polls it briefly, and only reads text written after the last user
|
|
382
|
-
message — otherwise a turn would silently capture nothing, or re-relay the
|
|
383
|
-
previous turn's reply.
|
|
384
|
-
- **codex** → the agent gets a private `CODEX_HOME` at `<agent-dir>/.codex/`
|
|
385
|
-
with a `notify` program wired up (your `~/.codex/auth.json` is symlinked in, so
|
|
386
|
-
it stays logged in, and your existing `config.toml` is carried over). The
|
|
387
|
-
generated file keeps `notify` above every `[table]` header -- TOML is
|
|
388
|
-
order-sensitive, and a `notify` written after one silently becomes
|
|
389
|
-
`projects.<dir>.notify`, which codex never calls.
|
|
390
|
-
- **gemini / hermes** → no turn-completion hook exists, so a background watcher
|
|
391
|
-
samples the pane and emits the new text once it has been quiet for
|
|
392
|
-
`pane_idle_ms`. It filters out the terminal's echo of incoming messages, but
|
|
393
|
-
it is still terminal scraping: spinners and redraws can leak in. Prefer having
|
|
394
|
-
these agents call `swarm send` explicitly.
|
|
395
|
-
|
|
396
|
-
Set `capture:` per agent to override the default for its type.
|
|
397
|
-
|
|
398
|
-
### <span id="subagents-parallel-work-and-busy-agents"></span>🧩 Subagents, parallel work, and busy agents
|
|
399
|
-
|
|
400
|
-
Coding agents spawn subagents and background tasks. Four things follow from that.
|
|
401
|
-
|
|
402
|
-
**A subagent that calls `swarm send` speaks as its parent.** Subagents inherit the
|
|
403
|
-
agent's environment, so `SWARM_AGENT` still names the parent and `can_talk_to` is
|
|
404
|
-
enforced against the parent. That is almost always what you want: the swarm sees
|
|
405
|
-
one `developer`, not five anonymous workers.
|
|
406
|
-
|
|
407
|
-
**Parallel subagents cannot garble each other.** A paste and the Enter that submits
|
|
408
|
-
it are two separate tmux calls, so two senders racing on one pane used to produce
|
|
409
|
-
one Enter submitting two concatenated messages and another submitting nothing.
|
|
410
|
-
Everything that types into a pane now takes a per-recipient lock, so concurrent
|
|
411
|
-
sends queue up instead of interleaving. Different recipients are still messaged in
|
|
412
|
-
parallel.
|
|
413
|
-
|
|
414
|
-
**A message that arrives while an agent is busy is queued, not lost.** The CLIs
|
|
415
|
-
hold it in their input box and process it when the current tool call finishes —
|
|
416
|
-
codex says so out loud (`Messages to be submitted after next tool call`).
|
|
417
|
-
|
|
418
|
-
**An agent that ends its turn saying "I'll respond when the subagent finishes"
|
|
419
|
-
gets captured twice.** With `capture: hook`, the Stop/notify hook fires when the
|
|
420
|
-
*agent's* turn ends. Claude's `Task` subagents run inside the turn, so the hook
|
|
421
|
-
waits for them. But work the agent genuinely backgrounds lets the turn end early,
|
|
422
|
-
and then:
|
|
423
|
-
|
|
424
|
-
- that interim message is captured, and forwarded if `forward_responses_to` is set;
|
|
425
|
-
- when the agent is re-invoked and finishes for real, its answer is captured and
|
|
426
|
-
forwarded too. The hop counter records the hop at which the agent *received* its
|
|
427
|
-
last message, so a second response does not consume an extra hop — the real
|
|
428
|
-
answer is never suppressed by the loop guard.
|
|
429
|
-
|
|
430
|
-
Subagent chatter never leaks: Claude writes subagent turns into the same transcript
|
|
431
|
-
marked `isSidechain: true`, and the hook skips them, so what gets relayed is the
|
|
432
|
-
agent's own final message rather than whatever a subagent happened to say last.
|
|
433
|
-
|
|
434
|
-
With `capture: pane` (gemini, hermes) this breaks down. A quiet pane is the only
|
|
435
|
-
"turn finished" signal there is, so a pause while a subagent works looks exactly
|
|
436
|
-
like a completed turn: the interim "I'll respond when it finishes" is captured and
|
|
437
|
-
forwarded, then the real answer is captured separately. Raise `pane_idle_ms` above
|
|
438
|
-
the longest silence you expect, or set `capture: none` and have the agent call
|
|
439
|
-
`swarm send` when it actually has something to say.
|
|
440
|
-
|
|
441
|
-
### Busy agents and backpressure
|
|
442
|
-
|
|
443
|
-
If `b` gives `a` a task, `a` is mid-turn. When `c` then tries to task `a` as well,
|
|
444
|
-
the message is refused rather than dropped into a working agent's input box:
|
|
445
|
-
|
|
446
|
-
```
|
|
447
|
-
$ swarm send --to a "please review my diff"
|
|
448
|
-
xx a is busy right now (working for 42s on a task from b). Please try again after
|
|
449
|
-
some time, or put your message in the queue and wait for the answer:
|
|
450
|
-
swarm send --to a --queue "..." # delivered automatically when a is free
|
|
451
|
-
swarm send --to a --wait "..." # block here until a is free
|
|
452
|
-
Meanwhile you are free to do other work.
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
So `c` chooses: come back later, `--queue` it and carry on with other work, or
|
|
456
|
-
`--wait` and block. A queued message is delivered by the next capture hook that
|
|
457
|
-
fires for `a`, the moment it goes idle. Agents are told all of this in the
|
|
458
|
-
communication block appended to their first prompt.
|
|
459
|
-
|
|
460
|
-
```bash
|
|
461
|
-
swarm status # TURN column: idle / busy 42s / untracked, plus QUEUE depth
|
|
462
|
-
swarm queue a # what is waiting for a, and who sent it
|
|
463
|
-
swarm queue a --clear # drop it all
|
|
464
|
-
swarm idle a # force a back to idle, then drain -- if a capture never fired
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
**This is safe against parallel senders.** The busy check and the "now busy" write
|
|
468
|
-
happen inside the same per-recipient lock as the paste, so two subagents racing to
|
|
469
|
-
message an idle agent cannot both pass the check — one delivers, the other is told
|
|
470
|
-
it is busy. A flag checked and set separately would let both through.
|
|
471
|
-
|
|
472
|
-
Some honest limits:
|
|
473
|
-
|
|
474
|
-
- Busy tracking needs a "turn finished" signal, so it only works for agents with
|
|
475
|
-
`capture: hook` or `capture: pane`. A `capture: none` agent reports `untracked`
|
|
476
|
-
and always accepts mail.
|
|
477
|
-
- With `capture: pane`, "idle" means "the pane went quiet", which a thinking agent
|
|
478
|
-
can also look like. Backpressure there is a hint, not a guarantee.
|
|
479
|
-
- A turn started by a human typing directly into the pane is not tracked.
|
|
480
|
-
- If a capture never fires (crashed CLI, misconfigured hook), the agent would look
|
|
481
|
-
busy forever. After `busy_timeout_ms` (default 15 minutes) it is treated as idle
|
|
482
|
-
again, with a warning. `swarm idle <agent>` clears it immediately. Any mail queued
|
|
483
|
-
for such an agent is not lost either: whenever some *other* agent finishes a turn,
|
|
484
|
-
Agentainer sweeps the now-idle agent's queue and delivers what was stranded — so
|
|
485
|
-
one missed turn-completion cannot wedge a queue permanently.
|
|
486
|
-
- A capture only fires if the agent's `type` matches the CLI its `command` actually
|
|
487
|
-
runs. If you point a `type: codex` agent at a `claude` command (e.g. through an
|
|
488
|
-
alias), it gets codex's `notify` hook, which claude never calls — its turns are
|
|
489
|
-
never detected and it looks busy forever. Set `type` to whatever the command runs.
|
|
490
|
-
- `--force` and `--ignore-busy` deliver anyway. The agent's CLI will queue the
|
|
491
|
-
message and handle it after the current tool call, so nothing is lost — you just
|
|
492
|
-
give up the backpressure.
|
|
493
|
-
|
|
494
|
-
### Auto-forwarding
|
|
495
|
-
|
|
496
|
-
`forward_responses_to` relays an agent's finished turn to other agents without
|
|
497
|
-
it having to ask:
|
|
498
|
-
|
|
499
|
-
```yaml
|
|
500
|
-
- name: researcher
|
|
501
|
-
can_talk_to: [orchestrator, developer]
|
|
502
|
-
forward_responses_to: [orchestrator] # must be a subset of can_talk_to
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
Two agents forwarding to each other would ping-pong forever, so every forwarded
|
|
506
|
-
message carries a hop count, and forwarding stops at `max_forward_hops`
|
|
507
|
-
(default 3). A fresh message from you resets the count. Auto-forwarding is
|
|
508
|
-
powerful but chatty — for most swarms it is better to let agents decide when to
|
|
509
|
-
speak, and leave `forward_responses_to` unset.
|
|
510
|
-
|
|
511
|
-
---
|
|
106
|
+
<details>
|
|
107
|
+
<summary>Plain-text architecture (for terminals & screen readers)</summary>
|
|
512
108
|
|
|
513
|
-
## <span id="configuration-reference"></span>⚙️ Configuration Reference
|
|
514
|
-
|
|
515
|
-
Full annotated example: [`agents.example.yaml`](agents.example.yaml).
|
|
516
|
-
Machine-readable summary for agents: [`llms.txt`](llms.txt).
|
|
517
|
-
|
|
518
|
-
### `swarm:`
|
|
519
|
-
|
|
520
|
-
| Key | Default | Meaning |
|
|
521
|
-
|---|---|---|
|
|
522
|
-
| `name` | config filename | Label used in prompts and logs |
|
|
523
|
-
| `root` | `./workspace` | Where per-agent folders are created |
|
|
524
|
-
| `create_workdirs` | `true` | Auto-create missing agent folders |
|
|
525
|
-
| `session_prefix` | `""` | Prepended to every tmux session name |
|
|
526
|
-
| `send_delay_ms` | `150` | Pause before pasting into a pane |
|
|
527
|
-
| `enter_delay_ms` | `250` | Pause between pasting and pressing Enter |
|
|
528
|
-
| `max_forward_hops` | `3` | Auto-forward loop guard |
|
|
529
|
-
| `ready_timeout_ms` | `60000` | How long to wait for an agent's input box to respond |
|
|
530
|
-
| `busy_timeout_ms` | `900000` | After this, a stuck "busy" agent is treated as idle |
|
|
531
|
-
| `message_format` | `tagged` | `tagged` XML-ish envelopes, or `plain` text headers |
|
|
532
|
-
| `max_reply_reminders` | `1` | How often to remind an agent that its reply reached nobody |
|
|
533
|
-
| `resume` | `false` | Make `up` reattach to recorded conversations by default |
|
|
534
|
-
| `pane_idle_ms` | `2500` | Quiet time before a `pane` turn counts as done |
|
|
535
|
-
| `pane_poll_ms` | `700` | Pane sampling interval |
|
|
536
|
-
| `pane_scrollback` | `400` | Lines of scrollback the watcher diffs |
|
|
537
|
-
| `tmux_history_limit` | `50000` | Scrollback kept per agent pane so you can scroll up (`0` = tmux default) |
|
|
538
|
-
| `tmux_mouse` | `true` | Enable mouse-wheel scrolling in the panes |
|
|
539
|
-
|
|
540
|
-
### `agents:`
|
|
541
|
-
|
|
542
|
-
| Key | Default | Meaning |
|
|
543
|
-
|---|---|---|
|
|
544
|
-
| `name` | *required* | Folder name **and** tmux session name |
|
|
545
|
-
| `type` | `claude` | `claude`, `codex`, `gemini`, `hermes`, or one you define |
|
|
546
|
-
| `command` | from type | Exact CLI to run, e.g. `claude --dangerously-skip-permissions` |
|
|
547
|
-
| `can_talk_to` | `[]` | Whitelist of agents it may message; `"*"` for all others |
|
|
548
|
-
| `first_prompt` | `""` | Prompt typed in after the CLI boots |
|
|
549
|
-
| `first_prompt_file` | — | Read the prompt from a file instead |
|
|
550
|
-
| `append_agents_that_you_can_talk_to_prompt` | `true` | Append the "here's who you can message and how" block |
|
|
551
|
-
| `in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt` | `false` | Append "stand by, your task is coming next" |
|
|
552
|
-
| `forward_responses_to` | `[]` | Auto-relay finished turns to these agents |
|
|
553
|
-
| `capture` | from type | `hook`, `pane`, `none`, or `auto` |
|
|
554
|
-
| `boot_delay_ms` | from type | Grace period before probing the input box (not a delivery guarantee) |
|
|
555
|
-
| `ready_probe` | `true` | Wait for the input box to echo a token before typing |
|
|
556
|
-
| `busy_check` | `true` | Refuse incoming messages while this agent is mid-turn |
|
|
557
|
-
| `parse_outbound_tags` | `true` | Route `<swarm-send>` blocks the agent writes in its reply |
|
|
558
|
-
| `reply_reminder` | `true` | Remind it when it owes a reply but sent nothing |
|
|
559
|
-
| `resume_args` | from type | Appended to `command` to resume, e.g. `--resume {session_id}` |
|
|
560
|
-
| `resume_command` | — | Full replacement command when flags can't be appended |
|
|
561
|
-
| `workdir` | `<root>/<name>` | Override the agent's directory (`~`, `{name}`, `{root}`, `{swarm}`, `{type}`) |
|
|
562
|
-
| `create_workdir` | from `create_workdirs` | Create the folder if missing, else error |
|
|
563
|
-
| `env` | `{}` | Extra environment variables for its tmux session |
|
|
564
|
-
|
|
565
|
-
### `agent_types:`
|
|
566
|
-
|
|
567
|
-
Override a built-in launch command, or define a new agent type:
|
|
568
|
-
|
|
569
|
-
```yaml
|
|
570
|
-
agent_types:
|
|
571
|
-
claude:
|
|
572
|
-
command: "claude --dangerously-skip-permissions --model opus"
|
|
573
|
-
aider: # a type of your own
|
|
574
|
-
command: "aider --yes"
|
|
575
|
-
capture: pane # only claude/codex support `hook`
|
|
576
|
-
boot_delay_ms: 4000
|
|
577
109
|
```
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
## <span id="examples"></span>🧪 Examples
|
|
591
|
-
|
|
592
|
-
Ready-to-run swarms in [`examples/`](examples/):
|
|
593
|
-
|
|
594
|
-
| File | Shape | Shows off |
|
|
595
|
-
|---|---|---|
|
|
596
|
-
| [`research-swarm.yaml`](examples/research-swarm.yaml) | Hub and spoke | A lead delegating to a scout, an analyst and a writer; a custom output folder |
|
|
597
|
-
| [`software-company.yaml`](examples/software-company.yaml) | Org chart | Six agents across all four CLIs, with a deliberately restricted comms graph |
|
|
598
|
-
| [`bug-hunt.yaml`](examples/bug-hunt.yaml) | Pipeline | `forward_responses_to` chaining reproduce → diagnose → fix → verify, hands-free |
|
|
599
|
-
| [`existing-repo.yaml`](examples/existing-repo.yaml) | Pairing | Two agents in one **existing** checkout, with `create_workdirs: false` |
|
|
600
|
-
| [`red-team.yaml`](examples/red-team.yaml) | Adversarial triad | Attacker vs defender arbitrated by a referee, over one **existing** checkout |
|
|
601
|
-
| [`debate.yaml`](examples/debate.yaml) | Debate + judge | Two advocates argue opposite sides; a judge cross-examines and rules |
|
|
602
|
-
| [`writers-room.yaml`](examples/writers-room.yaml) | Hub + review cycle | An editor driving a writer, fact-checker and critic to a finished article |
|
|
603
|
-
| [`incident-response.yaml`](examples/incident-response.yaml) | Incident hub | A commander coordinating investigator/responder/scribe under time pressure |
|
|
604
|
-
| [`tdd-pingpong.yaml`](examples/tdd-pingpong.yaml) | Tight 2-agent loop | Tester writes one failing test, coder makes it pass, in one shared repo |
|
|
605
|
-
| [`brainstorm.yaml`](examples/brainstorm.yaml) | Diverge + converge | Three angled idea-generators fanning out, then a synthesiser ranks them |
|
|
606
|
-
| [`localization.yaml`](examples/localization.yaml) | Pipeline | Translate → review → back-check, auto-forwarded via `forward_responses_to` |
|
|
607
|
-
| [`multi-language-broadcast.yaml`](examples/multi-language-broadcast.yaml) | Broadcast fan-out | One translator broadcasts a source to N language reviewers; a consolidator compiles and broadcasts back |
|
|
608
|
-
| [`code-review-broadcast.yaml`](examples/code-review-broadcast.yaml) | Broadcast hub | One PR broadcast to specialist reviewers (correctness/security/perf); each replies to the coordinator who synthesizes |
|
|
609
|
-
| [`ping-pong.yaml`](examples/ping-pong.yaml) | Round-robin | Two agents trade a unit of work, each broadcast naming who is up next (distinct from tdd-pingpong's fixed `forward_responses_to`) |
|
|
610
|
-
|
|
611
|
-
```bash
|
|
612
|
-
agentainer validate -c examples/research-swarm.yaml # look before you leap
|
|
613
|
-
agentainer up -c examples/research-swarm.yaml
|
|
614
|
-
agentainer send --to lead "Research the state of WebGPU compute shaders."
|
|
110
|
+
agentainer.yaml ──▶ agentainer up ──▶ one tmux session + workdir per agent
|
|
111
|
+
(agents + (CLI core │
|
|
112
|
+
can_talk_to) + supervisor) ▼
|
|
113
|
+
│ agents READ inbox/ WRITE outbox/<name>/
|
|
114
|
+
│ │ │
|
|
115
|
+
│ ▼ ▼
|
|
116
|
+
│ orchestrator routes the mail:
|
|
117
|
+
│ ACL check, message ID, read-state,
|
|
118
|
+
│ queue, durable JSONL log
|
|
119
|
+
▼
|
|
120
|
+
liveness supervisor heartbeat
|
|
121
|
+
(reconciles stale-busy / dead / silent-but-alive)
|
|
615
122
|
```
|
|
123
|
+
</details>
|
|
616
124
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
|
623
|
-
|
|
624
|
-
|
|
|
625
|
-
|
|
|
626
|
-
|
|
|
627
|
-
|
|
|
628
|
-
| `
|
|
629
|
-
|
|
|
630
|
-
|
|
|
631
|
-
|
|
|
632
|
-
| `
|
|
633
|
-
| `
|
|
634
|
-
| `
|
|
635
|
-
| `
|
|
636
|
-
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
125
|
+
## 🛠️ Commands
|
|
126
|
+
|
|
127
|
+
| 🛠️ Command | 📝 Purpose |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| 🔍 `validate` | Resolve and print the config; launch nothing. |
|
|
130
|
+
| 🆙 `up` | Create dirs + mailbox folders, install per-type turn-detection, open one tmux session per agent. |
|
|
131
|
+
| 🔌 `down` | Tear the swarm down. |
|
|
132
|
+
| 🔁 `restart` | Down then up. |
|
|
133
|
+
| 📡 `status` | Show agent/health summary. |
|
|
134
|
+
| 🔗 `attach` | Attach to an agent's tmux session. |
|
|
135
|
+
| ✉️ `send` | Send a `user` message into the swarm (`--to <agent>`). |
|
|
136
|
+
| 🗂️ `sessions` | List recorded tmux sessions (resume info). |
|
|
137
|
+
| ⏳ `queue` | Show pending mail per agent. |
|
|
138
|
+
| 😴 `idle` | List agents currently idle. |
|
|
139
|
+
| 📨 `inbox` | Show current inbox message per agent. |
|
|
140
|
+
| 📟 `logs` | Tail the durable JSONL event log (`-f` to follow). |
|
|
141
|
+
| 🪝 `hook` | Turn-completion entry point (called by `claude`/`codex` stop hooks). |
|
|
142
|
+
| 👀 `watch` | Live-watch the supervisor. |
|
|
143
|
+
| 💗 `supervise` | Run one (or the loop of) supervisor tick(s). |
|
|
144
|
+
| 👤 `user` | Toggle `user` availability. |
|
|
145
|
+
| 🌐 `serve` | Serve the mail-app control-plane UI (threads, settings/agent editing, availability, direct-to-pane) on `127.0.0.1`. |
|
|
146
|
+
| ➕ `add` | Add an agent to the config (YAML) and bring it up immediately. |
|
|
147
|
+
| 🗑️ `remove` | Remove an agent from the config and stop its session. |
|
|
148
|
+
| ✏️ `edit` | Edit an agent's fields in the config (`-s key=value`, repeatable) and reconcile. |
|
|
149
|
+
| 🔧 `reconcile` | Start agents missing from the running set / stop sessions no longer configured. |
|
|
150
|
+
|
|
151
|
+
See `llms.txt` for a machine-readable summary and `ProjectPlan.md` for the full
|
|
152
|
+
design (source of truth).
|
|
642
153
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
├── agents.example.yaml # annotated config
|
|
647
|
-
├── llms.txt # reference for agents configuring this tool
|
|
648
|
-
├── hooks/
|
|
649
|
-
│ ├── claude_stop.sh # Claude Code Stop hook
|
|
650
|
-
│ └── codex_notify.sh # Codex notify program
|
|
651
|
-
├── lib/
|
|
652
|
-
│ ├── swarm.py # tmux orchestration, routing, capture
|
|
653
|
-
│ ├── config.py # schema, defaults, validation
|
|
654
|
-
│ └── minyaml.py # YAML subset parser, used when PyYAML is absent
|
|
655
|
-
├── tests/validate.sh # full suite: mock agents, no model calls
|
|
656
|
-
├── examples/ # research, software company, bug hunt, pairing, red team, debate, writers' room, incident, tdd, brainstorm, l10n, multi-language + code-review (broadcast), ping-pong (round-robin)
|
|
657
|
-
└── workspace/ # created by `up`
|
|
658
|
-
├── <agent>/ # one folder per agent
|
|
659
|
-
└── .swarm/
|
|
660
|
-
├── state.json # what `up` started
|
|
661
|
-
├── sessions.yaml # each agent's conversation id, for `up --resume`
|
|
662
|
-
├── bin/swarm # the `swarm` command agents call
|
|
663
|
-
├── logs/ # <agent>.jsonl + swarm.jsonl
|
|
664
|
-
├── inbox/<agent>/ # archived messages
|
|
665
|
-
└── run/ # watcher pids, hop counters
|
|
666
|
-
```
|
|
154
|
+
<p align="center">
|
|
155
|
+
<img src="https://raw.githubusercontent.com/mehmetcanfarsak/Agentainer/main/assets/screenshot-status.svg" alt="Terminal output of agentainer status and agentainer queue against a running mock swarm" width="700"/>
|
|
156
|
+
</p>
|
|
667
157
|
|
|
668
|
-
##
|
|
158
|
+
## 🔒 Key invariants
|
|
159
|
+
|
|
160
|
+
- 🪶 **Zero runtime dependencies, forever** — Python 3 + bash + tmux; bundled
|
|
161
|
+
`minyaml` fallback; stdlib-only UI (P2+).
|
|
162
|
+
- 🤝 **`can_talk_to` is cooperative, not an OS boundary** — enforced for
|
|
163
|
+
well-behaved agents, documented honestly (Decision D15).
|
|
164
|
+
- 📛 **`user` / `system` are reserved virtual mailboxes.**
|
|
165
|
+
- 💗 **Liveness supervisor heartbeat retained** — no event-only redesign.
|
|
166
|
+
- 🗄️ **Durable JSONL event log is the source of truth** for history (TUIs keep no
|
|
167
|
+
scrollback).
|
|
168
|
+
- 🧪 **100% line coverage** via mock agents (bash loops, no API keys).
|
|
169
|
+
|
|
170
|
+
## 🖥️ The control-plane UI (P2–P3)
|
|
171
|
+
|
|
172
|
+
`agentainer serve -c my-swarm.yaml --port 8080 --token <secret>` starts a
|
|
173
|
+
**zero-dependency** web UI (stdlib `http.server` + a single vanilla-JS page, no
|
|
174
|
+
framework, no build step). It binds `127.0.0.1` by default; any non-loopback bind
|
|
175
|
+
requires a token. The UI is a control plane, so keep it on loopback unless a
|
|
176
|
+
token is supplied.
|
|
177
|
+
|
|
178
|
+
It is a **modern, mobile-friendly mail app** for the swarm:
|
|
179
|
+
|
|
180
|
+
- **Agents overview** — a card per agent with its role, type, and live
|
|
181
|
+
running/idle/busy/unread/queue status.
|
|
182
|
+
- **Mail view** — click an agent to open its correspondence. The left rail lists
|
|
183
|
+
its contacts (the `user`, its peers, `system`) instead of mail folders; pick
|
|
184
|
+
one and every message between the two is reconstructed into a threaded
|
|
185
|
+
conversation (rendered as **markdown**), each with its **delivery status**
|
|
186
|
+
(waiting → delivered → read). Reply to an agent **as the `user`** inline.
|
|
187
|
+
- **Activity + topology** — a global event timeline and a who-talks-to-whom graph.
|
|
188
|
+
- **Direct to the session** — watch each agent's **live tmux pane** and type
|
|
189
|
+
straight into it (bypassing the mailroom) when you need to.
|
|
190
|
+
- **Availability toggle** — flip whether the `user` is available to receive mail
|
|
191
|
+
(off by default: mail is *held*, never bounced) right from the top bar; the
|
|
192
|
+
change is persisted to `agentainer.yaml`.
|
|
193
|
+
- **Settings & agents** — edit swarm settings and **add / edit / delete agents**
|
|
194
|
+
from the UI; every change is written back to `agentainer.yaml` (via the same
|
|
195
|
+
stdlib emitter, so it works with **or without** PyYAML).
|
|
196
|
+
- **Telegram bridge** *(optional)* — configure a bot token + chat id in Settings
|
|
197
|
+
to **mirror** the swarm's mail (all agents or a selected set, plus your own
|
|
198
|
+
mail) to a Telegram chat, and **reply from your phone**: a Telegram message
|
|
199
|
+
reply routes back into the swarm as `user` mail. Stdlib `urllib` only — no new
|
|
200
|
+
dependency; the mirror is best-effort so the network can never stall routing.
|
|
201
|
+
|
|
202
|
+
## 🔄 Dynamic reconcile (P4)
|
|
203
|
+
|
|
204
|
+
No need to tear the whole swarm down to change it:
|
|
669
205
|
|
|
670
206
|
```bash
|
|
671
|
-
|
|
207
|
+
agentainer add dave --type codex --command "codex" --can-talk-to "alice,user" -c my-swarm.yaml
|
|
208
|
+
agentainer edit alice -s can_talk_to="dave,user" -c my-swarm.yaml
|
|
209
|
+
agentainer remove bob -c my-swarm.yaml
|
|
210
|
+
agentainer reconcile -c my-swarm.yaml # start missing agents, stop orphaned sessions
|
|
672
211
|
```
|
|
673
212
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
**
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
213
|
+
`add`/`remove`/`edit` rewrite `agentainer.yaml` with a stdlib-only YAML emitter
|
|
214
|
+
(works with **or without** PyYAML) and then `reconcile` the change into effect.
|
|
215
|
+
|
|
216
|
+
## 📊 Project status
|
|
217
|
+
|
|
218
|
+
- **P1 — Mail runtime (CLI-driven)**: ✅ done.
|
|
219
|
+
- **P2 — UI observability**: ✅ done.
|
|
220
|
+
- **P3 — terminal snapshot + send-from-UI**: ✅ done.
|
|
221
|
+
- **P4 — dynamic reconcile (`add`/`remove`/`edit`/`reconcile`)**: ✅ done.
|
|
222
|
+
|
|
223
|
+
🎉 100% line coverage across all core + UI + reconcile modules, driven entirely by
|
|
224
|
+
mock agents (no API keys).
|
|
225
|
+
|
|
226
|
+
## ❓ FAQ
|
|
227
|
+
|
|
228
|
+
**🤖 What is Agentainer?**
|
|
229
|
+
Agentainer is a zero-dependency orchestrator that runs several AI coding agents
|
|
230
|
+
(Claude Code, Codex, Gemini, Hermes) at once, each in its own `tmux` session and
|
|
231
|
+
working directory. Instead of agents exchanging messages inside their chat prose,
|
|
232
|
+
they communicate through a **file-based mail model**: an agent receives mail by
|
|
233
|
+
reading a file in its `inbox/` and sends mail by writing a file into
|
|
234
|
+
`outbox/<name>/`. The orchestrator owns all the hard logic — routing, access
|
|
235
|
+
control, message IDs, retries, and the durable log — so the agents only deal with
|
|
236
|
+
plain natural-language files.
|
|
237
|
+
|
|
238
|
+
**🧩 Which AI coding agents does Agentainer support?**
|
|
239
|
+
Claude Code (`type: claude`), OpenAI Codex (`type: codex`), Google Gemini CLI
|
|
240
|
+
(`type: gemini`), and Hermes (`type: hermes`). Turn-completion detection is wired
|
|
241
|
+
per `type` (a Stop hook for Claude, a `notify` program for Codex, and pane polling
|
|
242
|
+
for Gemini/Hermes). Any CLI that can read and write files in a tmux pane can be
|
|
243
|
+
slotted in as a new `type`.
|
|
244
|
+
|
|
245
|
+
**🐳 Does Agentainer need Docker or any runtime dependencies?**
|
|
246
|
+
No. The entire runtime is Python 3, `bash`, and `tmux` — nothing to `pip install`.
|
|
247
|
+
PyYAML is used *if present*, but a bundled fallback parser (`minyaml`) keeps
|
|
248
|
+
everything working without it, and CI proves that no-PyYAML path. `node` is only
|
|
249
|
+
needed for the npm launcher, never at swarm runtime. Docker is optional and not
|
|
250
|
+
required.
|
|
251
|
+
|
|
252
|
+
**🚀 How do I run multiple coding agents together?**
|
|
253
|
+
Write one `agentainer.yaml` listing each agent, then run `agentainer up -c
|
|
254
|
+
my-swarm.yaml`. That creates the mailbox folders, installs per-type turn detection,
|
|
255
|
+
and opens one tmux session per agent. `agentainer status` shows the swarm;
|
|
256
|
+
`agentainer send --to <agent> "..."` injects a `user` message; `agentainer down`
|
|
257
|
+
tears it all down. The bundled `examples/quickstart.yaml` runs entirely on
|
|
258
|
+
key-free bash-loop mock agents so you can watch the mailroom route mail with no API
|
|
259
|
+
keys.
|
|
260
|
+
|
|
261
|
+
**💬 Can agents communicate with each other, and how is that controlled?**
|
|
262
|
+
Yes — by writing into each other's `outbox/<name>/` folders. Communication is
|
|
263
|
+
controlled by the `can_talk_to` access-control list in the config: an agent may
|
|
264
|
+
only deliver to names listed there (`"*"` means everyone). The ACL is enforced by
|
|
265
|
+
the orchestrator at routing time — a disallowed send is bounced back as a `system`
|
|
266
|
+
message and filed in `failed/`. It is cooperative, not an OS sandbox (an agent with
|
|
267
|
+
filesystem access *could* bypass it), so it's documented honestly rather than
|
|
268
|
+
presented as a security boundary.
|
|
269
|
+
|
|
270
|
+
**♻️ How do I resume a swarm after a crash or reboot?**
|
|
271
|
+
`agentainer up --resume -c my-swarm.yaml` reattaches recorded Claude and Codex
|
|
272
|
+
conversations via their native resume commands (`claude --resume <id>`,
|
|
273
|
+
`codex resume <id>`); Gemini and Hermes always start fresh with a warning, since
|
|
274
|
+
they have no resume bridge. Conversation ids are recorded in
|
|
275
|
+
`<root>/.agentainer/sessions.yaml` as each agent finishes its first turn.
|
|
276
|
+
|
|
277
|
+
**🛡️ Is it safe to let agents run unattended?**
|
|
278
|
+
The design assumes unattended operation. A liveness **supervisor heartbeat**
|
|
279
|
+
(reconciling stale-busy, dead, and silent-but-alive agents) is always running; a
|
|
280
|
+
per-agent health probe catches the "alive but silent" case the supervisor can't
|
|
281
|
+
otherwise see; and several safeguards prevent a wedged agent from stalling the
|
|
282
|
+
swarm — including auto-releasing the next message after N presentations and a
|
|
283
|
+
rate cap on runaway auto-exchanges. That said, agents can run
|
|
284
|
+
`--dangerously-skip-permissions`, so the control-plane UI binds `127.0.0.1` by
|
|
285
|
+
default and requires a token for any remote bind.
|
|
286
|
+
|
|
287
|
+
**⚖️ How is Agentainer different from just opening several terminals?**
|
|
288
|
+
With several terminals you manually paste prompts, watch for completion, and relay
|
|
289
|
+
messages by hand — and a model that forgets a step silently stalls. Agentainer
|
|
290
|
+
automates the whole loop: it releases exactly one inbox message at a time, detects
|
|
291
|
+
each turn's completion per `type`, sweeps outgoing mail on stop, enforces the
|
|
292
|
+
`can_talk_to` ACL, retries nudges, keeps a durable JSONL event log as the source of
|
|
293
|
+
truth, and runs a supervisor so one stuck agent can't wedge the rest. The models
|
|
294
|
+
only read and write files; all coordination is deterministic orchestrator code.
|
|
724
295
|
|
|
725
296
|
## 📜 License
|
|
726
297
|
|
|
727
|
-
|
|
298
|
+
MIT — see [LICENSE](LICENSE).
|