agentainer 0.1.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 +603 -0
- package/agents.example.yaml +257 -0
- package/bin/agentainer.js +74 -0
- package/examples/bug-hunt.yaml +112 -0
- package/examples/existing-repo.yaml +72 -0
- package/examples/research-swarm.yaml +120 -0
- package/examples/software-company.yaml +152 -0
- package/hooks/claude_stop.sh +17 -0
- package/hooks/codex_notify.sh +16 -0
- package/lib/config.py +641 -0
- package/lib/minyaml.py +376 -0
- package/lib/swarm.py +2277 -0
- package/llms.txt +405 -0
- package/package.json +52 -0
- package/scripts/check-deps.js +76 -0
- package/swarm.sh +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
# Agentainer
|
|
2
|
+
|
|
3
|
+
Run a team of coding agents — **Claude Code, Codex, Gemini CLI, Hermes** — side by
|
|
4
|
+
side in tmux, each in its own directory, each able to message the others only if
|
|
5
|
+
your YAML file says it may.
|
|
6
|
+
|
|
7
|
+
> Formerly **AgentSwarm**. Installed globally, the command is `agentainer`; the
|
|
8
|
+
> repo's `./swarm.sh` remains a drop-in equivalent for local development.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
agents.yaml tmux
|
|
12
|
+
┌────────────┐ ┌──────────────────┐
|
|
13
|
+
│ orchestr. │───────▶│ session: orchestr│──┐
|
|
14
|
+
│ researcher │ │ session: research│ │ swarm send --to developer "..."
|
|
15
|
+
│ developer │ │ session: develop │◀─┘
|
|
16
|
+
│ reviewer │ │ session: reviewer│
|
|
17
|
+
└────────────┘ └──────────────────┘
|
|
18
|
+
│ │
|
|
19
|
+
│ workspace/<agent>/ │ hooks capture each finished turn
|
|
20
|
+
▼ ▼
|
|
21
|
+
one folder per agent messages routed + logged
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
One command starts the swarm: it creates a folder per agent, installs a
|
|
25
|
+
completion hook inside each folder, opens a tmux session per agent, launches
|
|
26
|
+
the agent's CLI, and types each agent's first prompt into it.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- `tmux` (3.0+)
|
|
33
|
+
- `python3` — PyYAML is used if present, otherwise a bundled parser handles the config
|
|
34
|
+
- `node` (16+) — only for the global `agentainer` command; not needed if you run `./swarm.sh` from a clone
|
|
35
|
+
- whichever agent CLIs you reference: `claude`, `codex`, `gemini`, `hermes` — install only the one(s) you actually use
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Global, via npm:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g agentainer
|
|
43
|
+
agentainer doctor # check tmux/python3 are present; report which agent CLIs it found
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`agentainer doctor` verifies the required tools (`tmux`, `python3`) and reports
|
|
47
|
+
which agent CLIs are available — it never fails on a missing *agent* CLI, since
|
|
48
|
+
you may only use one of them.
|
|
49
|
+
|
|
50
|
+
Or from a clone (no npm needed):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/mehmetcanfarsak/AgentSwarm.git && cd AgentSwarm
|
|
54
|
+
./swarm.sh --help # same commands as `agentainer`, straight from the repo
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cp agents.example.yaml agents.yaml
|
|
61
|
+
|
|
62
|
+
agentainer validate # parse the config, print the resolved swarm, launch nothing
|
|
63
|
+
agentainer up # create dirs, install hooks, start tmux, send first prompts
|
|
64
|
+
agentainer status # who is running
|
|
65
|
+
agentainer attach developer
|
|
66
|
+
agentainer down # stop everything
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Give the swarm its actual work:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
./swarm.sh send --to orchestrator "Build a CLI that converts CSV to Parquet."
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Watch the traffic between agents:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
./swarm.sh logs -f # whole swarm, live
|
|
79
|
+
./swarm.sh logs reviewer -n 20 # one agent
|
|
80
|
+
./swarm.sh inbox developer # messages an agent received
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## How it works
|
|
86
|
+
|
|
87
|
+
**One folder + one tmux session per agent.** Agent `developer` gets
|
|
88
|
+
`workspace/developer/` and a tmux session named `developer` (plus any
|
|
89
|
+
`session_prefix`). The agent's CLI is launched inside that folder, so its file
|
|
90
|
+
operations are naturally scoped to it.
|
|
91
|
+
|
|
92
|
+
### Working directories
|
|
93
|
+
|
|
94
|
+
By default every agent gets a fresh folder under `root`, created for you. You can
|
|
95
|
+
override that per agent, or for the whole swarm via `defaults`:
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
swarm:
|
|
99
|
+
root: ./workspace
|
|
100
|
+
create_workdirs: true # auto-create missing folders (default)
|
|
101
|
+
|
|
102
|
+
agents:
|
|
103
|
+
- name: developer # -> ./workspace/developer (created)
|
|
104
|
+
|
|
105
|
+
- name: reviewer
|
|
106
|
+
workdir: ~/projects/acme-api # -> an existing checkout
|
|
107
|
+
create_workdir: false # ...and fail loudly if it is not there
|
|
108
|
+
|
|
109
|
+
- name: scribe
|
|
110
|
+
workdir: "{root}/{name}-notes" # {name} {root} {swarm} {type}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- **`workdir`** may be absolute, relative to the config file, or use `~`.
|
|
114
|
+
- **`create_workdir: false`** turns a missing folder into an error instead of a
|
|
115
|
+
new empty directory — the right setting when you are pointing agents at real
|
|
116
|
+
repositories, where a typo should not silently create `~/projcets/acme-api`.
|
|
117
|
+
- **`defaults.workdir`** applies to every agent that does not override it. With
|
|
118
|
+
a `{name}` placeholder it lays out a folder each; without one, every agent
|
|
119
|
+
shares a single directory.
|
|
120
|
+
- **Sharing is allowed and sometimes the point** (a driver and a navigator in one
|
|
121
|
+
checkout), but agents then overwrite each other's files and interleave commits,
|
|
122
|
+
so `validate` and `up` warn when it happens. See
|
|
123
|
+
[`examples/existing-repo.yaml`](examples/existing-repo.yaml).
|
|
124
|
+
|
|
125
|
+
`root` is still used even when every agent lives elsewhere: it holds `.swarm/`
|
|
126
|
+
with the logs, inboxes and the `swarm` shim.
|
|
127
|
+
|
|
128
|
+
**Prompts are typed in, not piped.** `swarm up` drops each first prompt into the
|
|
129
|
+
agent's input box with a tmux bracketed paste, as one block, then presses Enter.
|
|
130
|
+
That is why multi-line prompts survive intact instead of being submitted line by
|
|
131
|
+
line.
|
|
132
|
+
|
|
133
|
+
Getting that to work reliably against a live TUI took more than a sleep:
|
|
134
|
+
|
|
135
|
+
- **Claude Code silently discards keystrokes for several seconds partway through
|
|
136
|
+
startup.** Measured on v2.1.205: input at t=2s landed, t=6s and t=12s vanished,
|
|
137
|
+
t=20s landed. A fixed `boot_delay_ms` is therefore a coin flip. Before typing,
|
|
138
|
+
AgentSwarm types a throwaway token and waits for the input box to echo it back,
|
|
139
|
+
then erases it (`ready_probe`). Enter is never sent, so nothing is submitted.
|
|
140
|
+
- **Readiness is not monotonic**, so after pasting, AgentSwarm checks that the
|
|
141
|
+
text actually appeared on screen before pressing Enter, and retries if it did
|
|
142
|
+
not. If delivery cannot be confirmed it refuses to press Enter, rather than
|
|
143
|
+
submitting a half-delivered prompt.
|
|
144
|
+
- **Both CLIs open a "do you trust this folder?" modal** on first run in a new
|
|
145
|
+
directory, which would eat the first prompt (Enter answers the dialog). Claude
|
|
146
|
+
does this even under `--dangerously-skip-permissions`. AgentSwarm pre-trusts each
|
|
147
|
+
agent's workdir: for codex in its generated `config.toml`, for claude by adding
|
|
148
|
+
`hasTrustDialogAccepted` for that path in `~/.claude.json`.
|
|
149
|
+
- **Both collapse a long paste into a chip** rather than showing the text —
|
|
150
|
+
`[Pasted text #1 +36 lines]` for claude, `[Pasted Content 2580 chars]` for codex.
|
|
151
|
+
Delivery verification recognises both.
|
|
152
|
+
|
|
153
|
+
**Agents talk in tagged messages.** A message arrives inside an envelope, so the
|
|
154
|
+
agent always knows where it starts, where it ends, and who sent it:
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
<swarm-message from="lead" to="reviewer" id="m-eb4105" reply-to="m-3f9a1c">
|
|
158
|
+
Review finding for ./parse: tokenize() mishandles a trailing backslash.
|
|
159
|
+
|
|
160
|
+
printf 'a\' | ./parse
|
|
161
|
+
</swarm-message>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
To send one, the agent simply **writes a block in its reply**. The capture hook
|
|
165
|
+
reads it when the turn ends and delivers it — no shell, no quoting, so multi-line
|
|
166
|
+
bodies, code blocks and backslashes survive intact:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
<swarm-send to="reviewer" reply-to="m-3f9a1c">
|
|
170
|
+
Please review src/parse.py.
|
|
171
|
+
|
|
172
|
+
printf 'a\' | ./parse
|
|
173
|
+
</swarm-send>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`<swarm-broadcast>` (no `to`) reaches everyone the sender may talk to. The `id`
|
|
177
|
+
and `reply-to` attributes let agents thread a conversation instead of guessing
|
|
178
|
+
which question an answer belongs to. Set `message_format: plain` to go back to the
|
|
179
|
+
old `[swarm] message from <sender>:` header, and `parse_outbound_tags: false` to
|
|
180
|
+
stop reading tags out of replies.
|
|
181
|
+
|
|
182
|
+
Agents can also send from their shell, which is useful mid-task rather than at the
|
|
183
|
+
end of a turn (but then they must quote the text themselves):
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
swarm send --to reviewer "I finished the parser, please review src/parse.py"
|
|
187
|
+
swarm broadcast "heads up: I renamed the config module"
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Either way the message is permission-checked against `can_talk_to`, archived under
|
|
191
|
+
`.swarm/inbox/<recipient>/`, and written to the event log.
|
|
192
|
+
|
|
193
|
+
The raw tmux equivalent also works, and bypasses permissions and logging:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
tmux send-keys -t reviewer -l "your message" && tmux send-keys -t reviewer Enter
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Agents get reminded when their answer goes nowhere.** A model that was asked a
|
|
200
|
+
question will often just *write the answer as prose* and end its turn — and that
|
|
201
|
+
prose reaches nobody, because only a `<swarm-send>` block is delivered. So when an
|
|
202
|
+
agent owes a reply and finishes a turn without sending one, AgentSwarm messages it:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
Your last turn sent no message to anyone, and lead is waiting on your answer to
|
|
206
|
+
message m-c73724.
|
|
207
|
+
...
|
|
208
|
+
<swarm-send to="lead" reply-to="m-c73724">
|
|
209
|
+
your answer here, over as many lines as you need
|
|
210
|
+
</swarm-send>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
If instead the agent *tried* to send but the block was malformed, it gets the
|
|
214
|
+
specific diagnosis — unclosed tag, missing `to`, unknown recipient, permission
|
|
215
|
+
denied — so it can correct itself rather than lose the message silently.
|
|
216
|
+
|
|
217
|
+
It is reminded at most `max_reply_reminders` times (default **1**), then AgentSwarm
|
|
218
|
+
gives up and stops nagging. An agent that auto-forwards via `forward_responses_to`
|
|
219
|
+
is never reminded, since its words did reach someone. Turn it off per agent with
|
|
220
|
+
`reply_reminder: false`.
|
|
221
|
+
|
|
222
|
+
**Conversations survive a restart.** Each time an agent finishes a turn, its
|
|
223
|
+
conversation id is written to `<root>/.swarm/sessions.yaml`:
|
|
224
|
+
|
|
225
|
+
```yaml
|
|
226
|
+
agents:
|
|
227
|
+
lead:
|
|
228
|
+
session_id: "0c2e47e2-5110-4e69-ae45-69d8492d2084"
|
|
229
|
+
type: "claude"
|
|
230
|
+
transcript: "/root/.claude/projects/.../0c2e47e2-....jsonl"
|
|
231
|
+
updated_at: "2026-07-09T21:00:41+00:00"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
If the machine dies, `swarm up --resume` reattaches every agent to its own
|
|
235
|
+
conversation instead of starting a fresh one — it does not re-send the first
|
|
236
|
+
prompt, and it keeps any mail still queued for that agent:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
./swarm.sh sessions # what is recorded, and the command that would resume it
|
|
240
|
+
./swarm.sh up --resume # reattach; agents without a recorded id start fresh
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Claude is resumed with `--resume <id>`, codex with `resume <id>`. Set
|
|
244
|
+
`swarm.resume: true` to make it the default, and `--no-resume` to override.
|
|
245
|
+
Gemini and hermes have no recoverable session id (their turns are scraped from
|
|
246
|
+
the terminal), so they always start a fresh conversation, with a warning.
|
|
247
|
+
|
|
248
|
+
If your command runs the CLI through an alias or wrapper, flags cannot simply be
|
|
249
|
+
appended — give the full recipe:
|
|
250
|
+
|
|
251
|
+
```yaml
|
|
252
|
+
- name: lead
|
|
253
|
+
command: "bash -ic chy3"
|
|
254
|
+
resume_command: "bash -ic 'chy3 --resume {session_id}'"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Permissions are a whitelist.** An agent may only message the agents in its
|
|
258
|
+
`can_talk_to` list. Anything else is refused with an explanatory error that the
|
|
259
|
+
agent sees on its own terminal. Use `can_talk_to: "*"` for "everyone else".
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Capturing what an agent says
|
|
264
|
+
|
|
265
|
+
AgentSwarm needs to know when an agent finishes a turn — both to log it and to
|
|
266
|
+
support auto-forwarding. How it finds out depends on the CLI, and the two
|
|
267
|
+
mechanisms are **not** equally good:
|
|
268
|
+
|
|
269
|
+
| `capture` | Used by | Mechanism | Reliability |
|
|
270
|
+
|---|---|---|---|
|
|
271
|
+
| `hook` | `claude`, `codex` | The CLI runs a program when a turn completes | Exact — the model's final message |
|
|
272
|
+
| `pane` | `gemini`, `hermes` | Poll the tmux pane, diff it once it stops changing | Heuristic — sees rendered text |
|
|
273
|
+
| `none` | any | Nothing is captured | — |
|
|
274
|
+
|
|
275
|
+
- **claude** → a `Stop` hook is written into `<agent-dir>/.claude/settings.json`,
|
|
276
|
+
with **no `matcher` key** (`Stop` is not a tool event, and supplying one stops the
|
|
277
|
+
interactive TUI from ever running the hook). It reads the session transcript.
|
|
278
|
+
Claude fires the hook *before* flushing the assistant message to that transcript,
|
|
279
|
+
so AgentSwarm polls it briefly, and only reads text written after the last user
|
|
280
|
+
message — otherwise a turn would silently capture nothing, or re-relay the
|
|
281
|
+
previous turn's reply.
|
|
282
|
+
- **codex** → the agent gets a private `CODEX_HOME` at `<agent-dir>/.codex/`
|
|
283
|
+
with a `notify` program wired up (your `~/.codex/auth.json` is symlinked in, so
|
|
284
|
+
it stays logged in, and your existing `config.toml` is carried over). The
|
|
285
|
+
generated file keeps `notify` above every `[table]` header -- TOML is
|
|
286
|
+
order-sensitive, and a `notify` written after one silently becomes
|
|
287
|
+
`projects.<dir>.notify`, which codex never calls.
|
|
288
|
+
- **gemini / hermes** → no turn-completion hook exists, so a background watcher
|
|
289
|
+
samples the pane and emits the new text once it has been quiet for
|
|
290
|
+
`pane_idle_ms`. It filters out the terminal's echo of incoming messages, but
|
|
291
|
+
it is still terminal scraping: spinners and redraws can leak in. Prefer having
|
|
292
|
+
these agents call `swarm send` explicitly.
|
|
293
|
+
|
|
294
|
+
Set `capture:` per agent to override the default for its type.
|
|
295
|
+
|
|
296
|
+
### Subagents, parallel work, and busy agents
|
|
297
|
+
|
|
298
|
+
Coding agents spawn subagents and background tasks. Four things follow from that.
|
|
299
|
+
|
|
300
|
+
**A subagent that calls `swarm send` speaks as its parent.** Subagents inherit the
|
|
301
|
+
agent's environment, so `SWARM_AGENT` still names the parent and `can_talk_to` is
|
|
302
|
+
enforced against the parent. That is almost always what you want: the swarm sees
|
|
303
|
+
one `developer`, not five anonymous workers.
|
|
304
|
+
|
|
305
|
+
**Parallel subagents cannot garble each other.** A paste and the Enter that submits
|
|
306
|
+
it are two separate tmux calls, so two senders racing on one pane used to produce
|
|
307
|
+
one Enter submitting two concatenated messages and another submitting nothing.
|
|
308
|
+
Everything that types into a pane now takes a per-recipient lock, so concurrent
|
|
309
|
+
sends queue up instead of interleaving. Different recipients are still messaged in
|
|
310
|
+
parallel.
|
|
311
|
+
|
|
312
|
+
**A message that arrives while an agent is busy is queued, not lost.** The CLIs
|
|
313
|
+
hold it in their input box and process it when the current tool call finishes —
|
|
314
|
+
codex says so out loud (`Messages to be submitted after next tool call`).
|
|
315
|
+
|
|
316
|
+
**An agent that ends its turn saying "I'll respond when the subagent finishes"
|
|
317
|
+
gets captured twice.** With `capture: hook`, the Stop/notify hook fires when the
|
|
318
|
+
*agent's* turn ends. Claude's `Task` subagents run inside the turn, so the hook
|
|
319
|
+
waits for them. But work the agent genuinely backgrounds lets the turn end early,
|
|
320
|
+
and then:
|
|
321
|
+
|
|
322
|
+
- that interim message is captured, and forwarded if `forward_responses_to` is set;
|
|
323
|
+
- when the agent is re-invoked and finishes for real, its answer is captured and
|
|
324
|
+
forwarded too. The hop counter records the hop at which the agent *received* its
|
|
325
|
+
last message, so a second response does not consume an extra hop — the real
|
|
326
|
+
answer is never suppressed by the loop guard.
|
|
327
|
+
|
|
328
|
+
Subagent chatter never leaks: Claude writes subagent turns into the same transcript
|
|
329
|
+
marked `isSidechain: true`, and the hook skips them, so what gets relayed is the
|
|
330
|
+
agent's own final message rather than whatever a subagent happened to say last.
|
|
331
|
+
|
|
332
|
+
With `capture: pane` (gemini, hermes) this breaks down. A quiet pane is the only
|
|
333
|
+
"turn finished" signal there is, so a pause while a subagent works looks exactly
|
|
334
|
+
like a completed turn: the interim "I'll respond when it finishes" is captured and
|
|
335
|
+
forwarded, then the real answer is captured separately. Raise `pane_idle_ms` above
|
|
336
|
+
the longest silence you expect, or set `capture: none` and have the agent call
|
|
337
|
+
`swarm send` when it actually has something to say.
|
|
338
|
+
|
|
339
|
+
### Busy agents and backpressure
|
|
340
|
+
|
|
341
|
+
If `b` gives `a` a task, `a` is mid-turn. When `c` then tries to task `a` as well,
|
|
342
|
+
the message is refused rather than dropped into a working agent's input box:
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
$ swarm send --to a "please review my diff"
|
|
346
|
+
xx a is busy right now (working for 42s on a task from b). Please try again after
|
|
347
|
+
some time, or put your message in the queue and wait for the answer:
|
|
348
|
+
swarm send --to a --queue "..." # delivered automatically when a is free
|
|
349
|
+
swarm send --to a --wait "..." # block here until a is free
|
|
350
|
+
Meanwhile you are free to do other work.
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
So `c` chooses: come back later, `--queue` it and carry on with other work, or
|
|
354
|
+
`--wait` and block. A queued message is delivered by the next capture hook that
|
|
355
|
+
fires for `a`, the moment it goes idle. Agents are told all of this in the
|
|
356
|
+
communication block appended to their first prompt.
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
swarm status # TURN column: idle / busy 42s / untracked, plus QUEUE depth
|
|
360
|
+
swarm queue a # what is waiting for a, and who sent it
|
|
361
|
+
swarm queue a --clear # drop it all
|
|
362
|
+
swarm idle a # force a back to idle, then drain -- if a capture never fired
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**This is safe against parallel senders.** The busy check and the "now busy" write
|
|
366
|
+
happen inside the same per-recipient lock as the paste, so two subagents racing to
|
|
367
|
+
message an idle agent cannot both pass the check — one delivers, the other is told
|
|
368
|
+
it is busy. A flag checked and set separately would let both through.
|
|
369
|
+
|
|
370
|
+
Some honest limits:
|
|
371
|
+
|
|
372
|
+
- Busy tracking needs a "turn finished" signal, so it only works for agents with
|
|
373
|
+
`capture: hook` or `capture: pane`. A `capture: none` agent reports `untracked`
|
|
374
|
+
and always accepts mail.
|
|
375
|
+
- With `capture: pane`, "idle" means "the pane went quiet", which a thinking agent
|
|
376
|
+
can also look like. Backpressure there is a hint, not a guarantee.
|
|
377
|
+
- A turn started by a human typing directly into the pane is not tracked.
|
|
378
|
+
- If a capture never fires (crashed CLI, misconfigured hook), the agent would look
|
|
379
|
+
busy forever. After `busy_timeout_ms` (default 15 minutes) it is treated as idle
|
|
380
|
+
again, with a warning. `swarm idle <agent>` clears it immediately. Any mail queued
|
|
381
|
+
for such an agent is not lost either: whenever some *other* agent finishes a turn,
|
|
382
|
+
AgentSwarm sweeps the now-idle agent's queue and delivers what was stranded — so
|
|
383
|
+
one missed turn-completion cannot wedge a queue permanently.
|
|
384
|
+
- A capture only fires if the agent's `type` matches the CLI its `command` actually
|
|
385
|
+
runs. If you point a `type: codex` agent at a `claude` command (e.g. through an
|
|
386
|
+
alias), it gets codex's `notify` hook, which claude never calls — its turns are
|
|
387
|
+
never detected and it looks busy forever. Set `type` to whatever the command runs.
|
|
388
|
+
- `--force` and `--ignore-busy` deliver anyway. The agent's CLI will queue the
|
|
389
|
+
message and handle it after the current tool call, so nothing is lost — you just
|
|
390
|
+
give up the backpressure.
|
|
391
|
+
|
|
392
|
+
### Auto-forwarding
|
|
393
|
+
|
|
394
|
+
`forward_responses_to` relays an agent's finished turn to other agents without
|
|
395
|
+
it having to ask:
|
|
396
|
+
|
|
397
|
+
```yaml
|
|
398
|
+
- name: researcher
|
|
399
|
+
can_talk_to: [orchestrator, developer]
|
|
400
|
+
forward_responses_to: [orchestrator] # must be a subset of can_talk_to
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Two agents forwarding to each other would ping-pong forever, so every forwarded
|
|
404
|
+
message carries a hop count, and forwarding stops at `max_forward_hops`
|
|
405
|
+
(default 3). A fresh message from you resets the count. Auto-forwarding is
|
|
406
|
+
powerful but chatty — for most swarms it is better to let agents decide when to
|
|
407
|
+
speak, and leave `forward_responses_to` unset.
|
|
408
|
+
|
|
409
|
+
---
|
|
410
|
+
|
|
411
|
+
## Configuration reference
|
|
412
|
+
|
|
413
|
+
Full annotated example: [`agents.example.yaml`](agents.example.yaml).
|
|
414
|
+
Machine-readable summary for agents: [`llms.txt`](llms.txt).
|
|
415
|
+
|
|
416
|
+
### `swarm:`
|
|
417
|
+
|
|
418
|
+
| Key | Default | Meaning |
|
|
419
|
+
|---|---|---|
|
|
420
|
+
| `name` | config filename | Label used in prompts and logs |
|
|
421
|
+
| `root` | `./workspace` | Where per-agent folders are created |
|
|
422
|
+
| `create_workdirs` | `true` | Auto-create missing agent folders |
|
|
423
|
+
| `session_prefix` | `""` | Prepended to every tmux session name |
|
|
424
|
+
| `send_delay_ms` | `150` | Pause before pasting into a pane |
|
|
425
|
+
| `enter_delay_ms` | `250` | Pause between pasting and pressing Enter |
|
|
426
|
+
| `max_forward_hops` | `3` | Auto-forward loop guard |
|
|
427
|
+
| `ready_timeout_ms` | `60000` | How long to wait for an agent's input box to respond |
|
|
428
|
+
| `busy_timeout_ms` | `900000` | After this, a stuck "busy" agent is treated as idle |
|
|
429
|
+
| `message_format` | `tagged` | `tagged` XML-ish envelopes, or `plain` text headers |
|
|
430
|
+
| `max_reply_reminders` | `1` | How often to remind an agent that its reply reached nobody |
|
|
431
|
+
| `resume` | `false` | Make `up` reattach to recorded conversations by default |
|
|
432
|
+
| `pane_idle_ms` | `2500` | Quiet time before a `pane` turn counts as done |
|
|
433
|
+
| `pane_poll_ms` | `700` | Pane sampling interval |
|
|
434
|
+
| `pane_scrollback` | `400` | Lines of scrollback the watcher diffs |
|
|
435
|
+
| `tmux_history_limit` | `50000` | Scrollback kept per agent pane so you can scroll up (`0` = tmux default) |
|
|
436
|
+
| `tmux_mouse` | `true` | Enable mouse-wheel scrolling in the panes |
|
|
437
|
+
|
|
438
|
+
### `agents:`
|
|
439
|
+
|
|
440
|
+
| Key | Default | Meaning |
|
|
441
|
+
|---|---|---|
|
|
442
|
+
| `name` | *required* | Folder name **and** tmux session name |
|
|
443
|
+
| `type` | `claude` | `claude`, `codex`, `gemini`, `hermes`, or one you define |
|
|
444
|
+
| `command` | from type | Exact CLI to run, e.g. `claude --dangerously-skip-permissions` |
|
|
445
|
+
| `can_talk_to` | `[]` | Whitelist of agents it may message; `"*"` for all others |
|
|
446
|
+
| `first_prompt` | `""` | Prompt typed in after the CLI boots |
|
|
447
|
+
| `first_prompt_file` | — | Read the prompt from a file instead |
|
|
448
|
+
| `append_agents_that_you_can_talk_to_prompt` | `true` | Append the "here's who you can message and how" block |
|
|
449
|
+
| `in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt` | `false` | Append "stand by, your task is coming next" |
|
|
450
|
+
| `forward_responses_to` | `[]` | Auto-relay finished turns to these agents |
|
|
451
|
+
| `capture` | from type | `hook`, `pane`, `none`, or `auto` |
|
|
452
|
+
| `boot_delay_ms` | from type | Grace period before probing the input box (not a delivery guarantee) |
|
|
453
|
+
| `ready_probe` | `true` | Wait for the input box to echo a token before typing |
|
|
454
|
+
| `busy_check` | `true` | Refuse incoming messages while this agent is mid-turn |
|
|
455
|
+
| `parse_outbound_tags` | `true` | Route `<swarm-send>` blocks the agent writes in its reply |
|
|
456
|
+
| `reply_reminder` | `true` | Remind it when it owes a reply but sent nothing |
|
|
457
|
+
| `resume_args` | from type | Appended to `command` to resume, e.g. `--resume {session_id}` |
|
|
458
|
+
| `resume_command` | — | Full replacement command when flags can't be appended |
|
|
459
|
+
| `workdir` | `<root>/<name>` | Override the agent's directory (`~`, `{name}`, `{root}`, `{swarm}`, `{type}`) |
|
|
460
|
+
| `create_workdir` | from `create_workdirs` | Create the folder if missing, else error |
|
|
461
|
+
| `env` | `{}` | Extra environment variables for its tmux session |
|
|
462
|
+
|
|
463
|
+
### `agent_types:`
|
|
464
|
+
|
|
465
|
+
Override a built-in launch command, or define a new agent type:
|
|
466
|
+
|
|
467
|
+
```yaml
|
|
468
|
+
agent_types:
|
|
469
|
+
claude:
|
|
470
|
+
command: "claude --dangerously-skip-permissions --model opus"
|
|
471
|
+
aider: # a type of your own
|
|
472
|
+
command: "aider --yes"
|
|
473
|
+
capture: pane # only claude/codex support `hook`
|
|
474
|
+
boot_delay_ms: 4000
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### `defaults:` and `templates:`
|
|
478
|
+
|
|
479
|
+
`defaults:` supplies any agent key for agents that don't set it, including
|
|
480
|
+
`workdir` — useful for putting a whole swarm in one repository. `templates:`
|
|
481
|
+
overrides the text AgentSwarm generates — `comms` and `task_notice` (appended to
|
|
482
|
+
first prompts), plus `reply_reminder` and `send_failed` (the nudges) —
|
|
483
|
+
with `{agent} {swarm} {peers} {prefix} {inbox} {workdir}` available as
|
|
484
|
+
placeholders.
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Examples
|
|
489
|
+
|
|
490
|
+
Ready-to-run swarms in [`examples/`](examples/):
|
|
491
|
+
|
|
492
|
+
| File | Shape | Shows off |
|
|
493
|
+
|---|---|---|
|
|
494
|
+
| [`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 |
|
|
495
|
+
| [`software-company.yaml`](examples/software-company.yaml) | Org chart | Six agents across all four CLIs, with a deliberately restricted comms graph |
|
|
496
|
+
| [`bug-hunt.yaml`](examples/bug-hunt.yaml) | Pipeline | `forward_responses_to` chaining reproduce → diagnose → fix → verify, hands-free |
|
|
497
|
+
| [`existing-repo.yaml`](examples/existing-repo.yaml) | Pairing | Two agents in one **existing** checkout, with `create_workdirs: false` |
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
./swarm.sh validate -c examples/research-swarm.yaml # look before you leap
|
|
501
|
+
./swarm.sh up -c examples/research-swarm.yaml
|
|
502
|
+
./swarm.sh send --to lead "Research the state of WebGPU compute shaders."
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
`existing-repo.yaml` intentionally refuses to start until you point `workdir` at
|
|
506
|
+
a repository that exists.
|
|
507
|
+
|
|
508
|
+
## Commands
|
|
509
|
+
|
|
510
|
+
| Command | Purpose |
|
|
511
|
+
|---|---|
|
|
512
|
+
| `swarm.sh up` | Start the swarm. `--only a,b`, `--restart`, `--resume`, `--no-prompt`, `--attach` |
|
|
513
|
+
| `swarm.sh down` | Kill sessions and watchers. `--only a,b` |
|
|
514
|
+
| `swarm.sh restart` | `down` then `up` |
|
|
515
|
+
| `swarm.sh status` | Table of agents, sessions, capture mode, permissions |
|
|
516
|
+
| `swarm.sh attach <agent>` | Attach to an agent's tmux session |
|
|
517
|
+
| `swarm.sh send --to <agent> "msg"` | Deliver a message (`--from`, `--file`, `--queue`, `--wait`, `--ignore-busy`, `--force`) |
|
|
518
|
+
| `swarm.sh broadcast "msg"` | Message everyone the sender may talk to |
|
|
519
|
+
| `swarm.sh sessions` | Show each agent's recorded conversation id (`--raw`) |
|
|
520
|
+
| `swarm.sh queue <agent>` | Show what is waiting for a busy agent (`--clear`) |
|
|
521
|
+
| `swarm.sh idle <agent>` | Force an agent back to idle, then drain its queue |
|
|
522
|
+
| `swarm.sh inbox <agent>` | Print archived messages |
|
|
523
|
+
| `swarm.sh logs [agent] [-f]` | Event log: prompts, responses, messages |
|
|
524
|
+
| `swarm.sh validate` | Parse the config. `--show-prompts` renders final prompts |
|
|
525
|
+
|
|
526
|
+
`./swarm.sh my-swarm.yaml` is shorthand for `./swarm.sh up -c my-swarm.yaml`.
|
|
527
|
+
`-c` and `$SWARM_CONFIG` both select a config; `-c` wins.
|
|
528
|
+
|
|
529
|
+
## Layout
|
|
530
|
+
|
|
531
|
+
```
|
|
532
|
+
AgentSwarm/
|
|
533
|
+
├── swarm.sh # entrypoint
|
|
534
|
+
├── agents.example.yaml # annotated config
|
|
535
|
+
├── llms.txt # reference for agents configuring this tool
|
|
536
|
+
├── hooks/
|
|
537
|
+
│ ├── claude_stop.sh # Claude Code Stop hook
|
|
538
|
+
│ └── codex_notify.sh # Codex notify program
|
|
539
|
+
├── lib/
|
|
540
|
+
│ ├── swarm.py # tmux orchestration, routing, capture
|
|
541
|
+
│ ├── config.py # schema, defaults, validation
|
|
542
|
+
│ └── minyaml.py # YAML subset parser, used when PyYAML is absent
|
|
543
|
+
├── tests/validate.sh # full suite: mock agents, no model calls
|
|
544
|
+
├── examples/ # research swarm, software company, bug hunt, pairing
|
|
545
|
+
└── workspace/ # created by `up`
|
|
546
|
+
├── <agent>/ # one folder per agent
|
|
547
|
+
└── .swarm/
|
|
548
|
+
├── state.json # what `up` started
|
|
549
|
+
├── sessions.yaml # each agent's conversation id, for `up --resume`
|
|
550
|
+
├── bin/swarm # the `swarm` command agents call
|
|
551
|
+
├── logs/ # <agent>.jsonl + swarm.jsonl
|
|
552
|
+
├── inbox/<agent>/ # archived messages
|
|
553
|
+
└── run/ # watcher pids, hop counters
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## Tests
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
tests/validate.sh
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
48 checks over the real code paths — tmux, hooks, locks, queues, the tag parser,
|
|
563
|
+
sessions and resume — driven by mock agents, so it needs no API key and costs
|
|
564
|
+
nothing. It covers the awkward cases: the check-and-set race between concurrent
|
|
565
|
+
senders, a queued message beating a reply reminder, a subagent's sidechain turn
|
|
566
|
+
being skipped, and a transcript read before Claude has flushed it.
|
|
567
|
+
|
|
568
|
+
## Troubleshooting
|
|
569
|
+
|
|
570
|
+
**"could not confirm the text arrived; NOT pressing Enter".** The agent's input
|
|
571
|
+
box never echoed the prompt, so AgentSwarm refused to submit it. Attach to the
|
|
572
|
+
session to see what state the CLI is in -- usually a modal (login, trust,
|
|
573
|
+
onboarding) is holding focus. Raise `ready_timeout_ms` if the CLI is merely slow.
|
|
574
|
+
|
|
575
|
+
**An agent says it "cannot message" another.** That is the permission check
|
|
576
|
+
doing its job — add the recipient to the sender's `can_talk_to`.
|
|
577
|
+
|
|
578
|
+
**A `pane` agent forwards garbage.** Terminal scraping picked up a redraw. Raise
|
|
579
|
+
`pane_idle_ms`, or set `capture: none` and instruct the agent to call
|
|
580
|
+
`swarm send` itself.
|
|
581
|
+
|
|
582
|
+
**Forwarding stopped with a hop-limit warning.** Two agents were relaying to each
|
|
583
|
+
other. Raise `max_forward_hops`, or break the cycle in `forward_responses_to`.
|
|
584
|
+
|
|
585
|
+
**Nothing captured from a claude agent.** Check `<agent-dir>/.claude/settings.json`
|
|
586
|
+
exists and `.swarm/logs/hooks.log` for errors. If the agent also looks busy forever,
|
|
587
|
+
its `type` probably does not match the CLI its `command` runs (see the capture note
|
|
588
|
+
under [Busy agents](#busy-agents-and-backpressure)).
|
|
589
|
+
|
|
590
|
+
**Can't scroll up in an attached session.** AgentSwarm raises tmux's scrollback to
|
|
591
|
+
`tmux_history_limit` (50000 lines) and turns on `tmux_mouse`, so the wheel scrolls
|
|
592
|
+
the backlog; press `q` to leave copy mode. If your terminal grabs the wheel itself,
|
|
593
|
+
use `Ctrl-b [` then PageUp. Both options are set on the tmux server before sessions
|
|
594
|
+
are created, so a server that was already running keeps its old panes' smaller
|
|
595
|
+
buffer — restart the swarm (or that pane) to pick up the larger one.
|
|
596
|
+
|
|
597
|
+
## A note on flags
|
|
598
|
+
|
|
599
|
+
`claude --dangerously-skip-permissions`, `codex --yolo` and `gemini --yolo` let
|
|
600
|
+
agents act without asking for confirmation. That is usually what you want for an
|
|
601
|
+
unattended swarm, and it means several models are running tools unsupervised in
|
|
602
|
+
these directories. Point `root` somewhere disposable, and don't run a swarm over
|
|
603
|
+
a directory you can't afford to lose.
|