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/llms.txt
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# AgentSwarm
|
|
2
|
+
|
|
3
|
+
> AgentSwarm runs several coding-agent CLIs (Claude Code, Codex, Gemini, Hermes) at once.
|
|
4
|
+
> Each agent gets its own directory and its own tmux session. A single YAML file defines
|
|
5
|
+
> the agents, the command that launches each one, its first prompt, and — as a strict
|
|
6
|
+
> whitelist — which other agents it is allowed to message.
|
|
7
|
+
>
|
|
8
|
+
> This file is the reference for LLMs asked to write or repair an AgentSwarm config.
|
|
9
|
+
> If you are configuring a swarm, everything you need is below. Verify your work with
|
|
10
|
+
> `./swarm.sh validate --show-prompts`, which parses the config and launches nothing.
|
|
11
|
+
|
|
12
|
+
## Mental model
|
|
13
|
+
|
|
14
|
+
- One YAML file describes the whole swarm. `./swarm.sh up` reads it and, for each agent:
|
|
15
|
+
creates `<root>/<name>/`, installs a turn-completion hook in that folder, opens a tmux
|
|
16
|
+
session named `<session_prefix><name>`, runs the agent's `command` inside the folder,
|
|
17
|
+
waits for its input box to become responsive, then types the assembled first prompt in.
|
|
18
|
+
- Agents message each other with `swarm send --to <agent> "text"`. The `swarm` command is
|
|
19
|
+
on their `PATH` and `SWARM_AGENT` identifies them, so no `--from` is needed. Delivery is
|
|
20
|
+
refused unless the recipient appears in the sender's `can_talk_to`.
|
|
21
|
+
- A delivered message is pasted into the recipient's pane as
|
|
22
|
+
`[swarm] message from <sender>:\n<text>`, archived under `.swarm/inbox/<recipient>/`,
|
|
23
|
+
and written to `.swarm/logs/`.
|
|
24
|
+
|
|
25
|
+
## Config file
|
|
26
|
+
|
|
27
|
+
Top level keys: `swarm`, `defaults`, `agent_types`, `agents`, `templates`.
|
|
28
|
+
Only `agents` is required. Relative paths resolve against the config file's directory.
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
swarm:
|
|
32
|
+
name: dev-swarm # label used in prompts/logs; default = config filename
|
|
33
|
+
root: ./workspace # parent of the per-agent folders; default ./workspace
|
|
34
|
+
create_workdirs: true # auto-create missing agent folders; default true
|
|
35
|
+
session_prefix: "" # prepended to tmux session names; default ""
|
|
36
|
+
send_delay_ms: 150 # pause before pasting into a pane
|
|
37
|
+
enter_delay_ms: 250 # pause between pasting and pressing Enter
|
|
38
|
+
max_forward_hops: 3 # auto-forward loop guard
|
|
39
|
+
ready_timeout_ms: 60000 # how long to wait for an agent's input box to respond
|
|
40
|
+
busy_timeout_ms: 900000 # after this, an agent stuck "busy" is treated as idle
|
|
41
|
+
message_format: tagged # tagged | plain; tagged is the default
|
|
42
|
+
max_reply_reminders: 1 # how often to nudge an agent whose reply reached nobody
|
|
43
|
+
resume: false # make `up` reattach to recorded conversations by default
|
|
44
|
+
pane_idle_ms: 2500 # quiet time before a `pane` capture counts as a finished turn
|
|
45
|
+
pane_poll_ms: 700 # pane sampling interval
|
|
46
|
+
pane_scrollback: 400 # lines of scrollback the pane watcher diffs
|
|
47
|
+
tmux_history_limit: 50000 # scrollback per agent pane so you can scroll up when attached; 0 = tmux default
|
|
48
|
+
tmux_mouse: true # enable mouse-wheel scrolling in the panes
|
|
49
|
+
|
|
50
|
+
defaults: # any agent key; applied to agents that omit it
|
|
51
|
+
type: claude
|
|
52
|
+
append_agents_that_you_can_talk_to_prompt: true
|
|
53
|
+
workdir: "{root}/{name}" # optional; see "Working directories" below
|
|
54
|
+
|
|
55
|
+
agent_types: # override built-ins, or define new types
|
|
56
|
+
claude:
|
|
57
|
+
command: "claude --dangerously-skip-permissions"
|
|
58
|
+
capture: hook
|
|
59
|
+
boot_delay_ms: 3000
|
|
60
|
+
|
|
61
|
+
agents:
|
|
62
|
+
- name: developer # REQUIRED. Used as the folder name AND the tmux session name.
|
|
63
|
+
# Must match ^[A-Za-z0-9_][A-Za-z0-9_-]*$
|
|
64
|
+
type: codex # claude | codex | gemini | hermes | any key in agent_types
|
|
65
|
+
command: "codex --yolo" # exact CLI invocation; defaults to the type's command
|
|
66
|
+
can_talk_to: [reviewer] # whitelist. "*" means every other agent. Default: []
|
|
67
|
+
first_prompt: | # typed in after the CLI boots
|
|
68
|
+
You are the DEVELOPER...
|
|
69
|
+
first_prompt_file: ./p.md # alternative to first_prompt (mutually exclusive)
|
|
70
|
+
forward_responses_to: [reviewer] # auto-relay finished turns; must ⊆ can_talk_to
|
|
71
|
+
capture: hook # hook | pane | none | auto (default: from type)
|
|
72
|
+
boot_delay_ms: 3000 # grace period before probing the input box
|
|
73
|
+
ready_probe: true # wait for the input box to echo a token first
|
|
74
|
+
busy_check: true # refuse incoming mail while this agent is mid-turn
|
|
75
|
+
parse_outbound_tags: true # route <swarm-send> blocks written in its replies
|
|
76
|
+
reply_reminder: true # nudge it when it owes a reply but sent nothing
|
|
77
|
+
resume_args: "--resume {session_id}" # appended to command by `up --resume`
|
|
78
|
+
resume_command: "bash -ic 'chy3 --resume {session_id}'" # or replace it entirely
|
|
79
|
+
workdir: ./somewhere # default <root>/<name>; see below
|
|
80
|
+
create_workdir: true # false => a missing workdir is an error
|
|
81
|
+
env: {KEY: value} # extra env vars for its tmux session
|
|
82
|
+
append_agents_that_you_can_talk_to_prompt: true
|
|
83
|
+
in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt: false
|
|
84
|
+
|
|
85
|
+
templates: # override the text AgentSwarm generates
|
|
86
|
+
comms: | # placeholders: {agent} {swarm} {peers} {prefix} {inbox} {workdir}
|
|
87
|
+
You are {agent}. You may message: {peers}.
|
|
88
|
+
task_notice: |
|
|
89
|
+
Stand by. Your real task arrives in the next message.
|
|
90
|
+
reply_reminder: | # placeholders: {agent} {sender} {id} {peers} {problems}
|
|
91
|
+
You owe {sender} an answer to {id}. Send it inside <swarm-send to="{sender}">.
|
|
92
|
+
send_failed: | # placeholders: {agent} {peers} {problems}
|
|
93
|
+
Your message could not be delivered. {problems}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Working directories
|
|
97
|
+
|
|
98
|
+
- Default: each agent gets `<root>/<name>`, created on `up`.
|
|
99
|
+
- `workdir` overrides it. Absolute, relative to the config file, or `~`-prefixed.
|
|
100
|
+
Placeholders: `{name}` `{root}` `{swarm}` `{type}`.
|
|
101
|
+
- `defaults.workdir` applies to every agent that omits it. Include `{name}` to give
|
|
102
|
+
each agent its own folder; omit it to put the whole swarm in one directory.
|
|
103
|
+
- `create_workdirs` (swarm-level) and `create_workdir` (per-agent) control creation.
|
|
104
|
+
Set them to `false` when pointing at real repositories, so a mistyped path is an
|
|
105
|
+
error rather than a new empty folder.
|
|
106
|
+
- Several agents may share one workdir. That is legitimate (pair programming in one
|
|
107
|
+
checkout) but they will overwrite each other's files and interleave git commits,
|
|
108
|
+
so `validate` and `up` print a warning.
|
|
109
|
+
- `root` always holds `.swarm/` (logs, inboxes, the `swarm` shim), even when every
|
|
110
|
+
agent works elsewhere.
|
|
111
|
+
|
|
112
|
+
## Tagged messages (the default wire format)
|
|
113
|
+
|
|
114
|
+
Inbound, an agent sees an envelope it can parse unambiguously:
|
|
115
|
+
|
|
116
|
+
<swarm-message from="lead" to="reviewer" id="m-eb4105" reply-to="m-3f9a1c">
|
|
117
|
+
any number of lines, code blocks, backslashes -- all verbatim
|
|
118
|
+
</swarm-message>
|
|
119
|
+
|
|
120
|
+
Outbound, the agent writes a block **in its reply**; the capture hook routes it when
|
|
121
|
+
the turn ends. There is no shell involved, hence no quoting or escaping to get wrong:
|
|
122
|
+
|
|
123
|
+
<swarm-send to="reviewer" reply-to="m-eb4105">
|
|
124
|
+
multi-line body
|
|
125
|
+
</swarm-send>
|
|
126
|
+
|
|
127
|
+
<swarm-broadcast>
|
|
128
|
+
goes to everyone in the sender's can_talk_to
|
|
129
|
+
</swarm-broadcast>
|
|
130
|
+
|
|
131
|
+
- `id` is generated per message; `reply-to` is optional and used for threading.
|
|
132
|
+
- Tagged sends are permission-checked exactly like `swarm send`, and are queued
|
|
133
|
+
automatically if the recipient is busy.
|
|
134
|
+
- A `<swarm-send to="...">` naming something that is not an agent is discarded with a
|
|
135
|
+
warning (this catches an agent echoing the `AGENT_NAME` placeholder from its prompt).
|
|
136
|
+
- Blocks are removed from the text before `forward_responses_to` runs, so a message
|
|
137
|
+
addressed to one peer is not also auto-forwarded to everybody.
|
|
138
|
+
- `message_format: plain` restores the `[swarm] message from <sender>:` header.
|
|
139
|
+
`parse_outbound_tags: false` stops reading tags out of replies. Both are forced off
|
|
140
|
+
when `capture: none`, since there is nothing to read.
|
|
141
|
+
- With `capture: pane` the tags are recovered from scraped terminal text, so they are
|
|
142
|
+
only as reliable as the scrape. Prefer hook-captured agents for tag routing.
|
|
143
|
+
|
|
144
|
+
## Resuming after a restart
|
|
145
|
+
|
|
146
|
+
- Every captured turn records the agent's conversation id in `<root>/.swarm/sessions.yaml`
|
|
147
|
+
(claude hands it to the Stop hook; codex's is read from the newest rollout file under
|
|
148
|
+
that agent's private `CODEX_HOME/sessions`).
|
|
149
|
+
- `swarm up --resume` rebuilds each agent's command from that id, skips re-sending the
|
|
150
|
+
first prompt, and preserves the agent's queued mail and any reply it still owes.
|
|
151
|
+
- Resume recipes: claude `--resume {session_id}`, codex `resume {session_id}`. Neither
|
|
152
|
+
gemini nor hermes exposes a session id, so they always start fresh (with a warning).
|
|
153
|
+
- `resume_args` is appended to `command`. If the command invokes the CLI through an
|
|
154
|
+
alias or wrapper (`bash -ic chy3`), appending is wrong -- set `resume_command` to the
|
|
155
|
+
full replacement, using `{session_id}` (and optionally `{command}`).
|
|
156
|
+
- Starting an agent fresh removes its stale entry, so a later `--resume` cannot reattach
|
|
157
|
+
to a conversation that agent never ran.
|
|
158
|
+
- `swarm sessions` prints what is recorded and the exact command that would resume it.
|
|
159
|
+
|
|
160
|
+
## Reply reminders
|
|
161
|
+
|
|
162
|
+
A model asked a question will often write the answer as prose and end its turn. That
|
|
163
|
+
prose reaches nobody -- only a `<swarm-send>` block is delivered. So:
|
|
164
|
+
|
|
165
|
+
- When agent B receives a message from agent A (and B may message A back), B is
|
|
166
|
+
recorded as owing a reply.
|
|
167
|
+
- If B's turn ends having sent nothing, it is messaged with the `reply_reminder`
|
|
168
|
+
template: who is waiting, the message id to quote as `reply-to`, and the exact block
|
|
169
|
+
to write.
|
|
170
|
+
- If B *tried* to send but the block was malformed -- unclosed, missing `to`, unknown
|
|
171
|
+
recipient, permission denied -- it gets the `send_failed` template naming each fault.
|
|
172
|
+
- At most `max_reply_reminders` nudges (default 1), then AgentSwarm gives up silently.
|
|
173
|
+
- Anything B delivered counts, including an auto-forward via `forward_responses_to`.
|
|
174
|
+
Reminders are sent from `swarm`, so they never create a reply obligation themselves.
|
|
175
|
+
- `reply_reminder: false` disables it per agent; it is forced off when the agent's
|
|
176
|
+
tags are not parsed (`capture: none` or `message_format: plain`).
|
|
177
|
+
|
|
178
|
+
## The two prompt switches
|
|
179
|
+
|
|
180
|
+
Both are per-agent booleans, both default as shown, and both append to `first_prompt`:
|
|
181
|
+
|
|
182
|
+
- `append_agents_that_you_can_talk_to_prompt` (default `true`) — appends a block naming
|
|
183
|
+
the agents this one may message and showing the `swarm send` / `swarm broadcast` / raw
|
|
184
|
+
`tmux send-keys` syntax. Set it to `false` for an agent with no peers, or when you want
|
|
185
|
+
to write the communication instructions yourself.
|
|
186
|
+
- `in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt` (default `false`) —
|
|
187
|
+
appends a "do not start yet, your task arrives in the next prompt" block. Use it for
|
|
188
|
+
the agent you will drive manually with `swarm send`, so it acknowledges and waits
|
|
189
|
+
instead of inventing work.
|
|
190
|
+
|
|
191
|
+
Final prompt order: `first_prompt`, then the comms block, then the task notice.
|
|
192
|
+
|
|
193
|
+
## capture: how a finished turn is detected
|
|
194
|
+
|
|
195
|
+
| value | works with | how |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `hook` | `claude`, `codex` **only** | the CLI runs a program on turn completion; exact |
|
|
198
|
+
| `pane` | anything | poll the tmux pane and diff it after `pane_idle_ms`; heuristic |
|
|
199
|
+
| `none` | anything | no capture |
|
|
200
|
+
| `auto` | anything | use the agent type's default (this is the default) |
|
|
201
|
+
|
|
202
|
+
- `claude` → a `Stop` hook in `<agent-dir>/.claude/settings.json`; reads the transcript.
|
|
203
|
+
- `codex` → a private `CODEX_HOME` at `<agent-dir>/.codex/` with a `notify` program.
|
|
204
|
+
- `gemini`, `hermes` → no such facility, so they fall back to `pane`. Setting
|
|
205
|
+
`capture: hook` on them (or on a custom type) prints a warning and uses `pane`.
|
|
206
|
+
- `pane` capture is terminal scraping. It filters the echo of incoming messages, but
|
|
207
|
+
spinners and redraws can leak through. When output quality matters, prefer
|
|
208
|
+
`capture: none` and tell the agent in its prompt to call `swarm send` itself.
|
|
209
|
+
|
|
210
|
+
`forward_responses_to` requires `capture` to be `hook` or `pane`; combining it with
|
|
211
|
+
`capture: none` is a config error.
|
|
212
|
+
|
|
213
|
+
## Rules the validator enforces
|
|
214
|
+
|
|
215
|
+
- agent names are unique and match `^[A-Za-z0-9_][A-Za-z0-9_-]*$`
|
|
216
|
+
- every name in `can_talk_to` and `forward_responses_to` refers to a real agent
|
|
217
|
+
- an agent may not list itself in `can_talk_to`
|
|
218
|
+
- `forward_responses_to` ⊆ `can_talk_to`
|
|
219
|
+
- `forward_responses_to` is empty unless capture is enabled
|
|
220
|
+
- `type` is a built-in or defined under `agent_types`
|
|
221
|
+
- `first_prompt` and `first_prompt_file` are not both set
|
|
222
|
+
|
|
223
|
+
## Commands
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
./swarm.sh up [--only a,b] [--restart] [--no-prompt] [--attach]
|
|
227
|
+
./swarm.sh down [--only a,b]
|
|
228
|
+
./swarm.sh restart
|
|
229
|
+
./swarm.sh status
|
|
230
|
+
./swarm.sh attach <agent>
|
|
231
|
+
./swarm.sh send --to <agent> [--from <name>] [--file F] [--queue] [--wait]
|
|
232
|
+
[--wait-timeout S] [--ignore-busy] [--force] "message"
|
|
233
|
+
./swarm.sh broadcast "message"
|
|
234
|
+
./swarm.sh queue <agent> [--clear]
|
|
235
|
+
./swarm.sh idle <agent> [--no-drain]
|
|
236
|
+
./swarm.sh inbox <agent> [-n N]
|
|
237
|
+
./swarm.sh logs [agent] [-n N] [-f]
|
|
238
|
+
./swarm.sh validate [--show-prompts]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Config resolution: `-c PATH`, else `$SWARM_CONFIG`, else `./agents.yaml`, else the
|
|
242
|
+
`agents.yaml` beside `swarm.sh`. `./swarm.sh x.yaml` is shorthand for `up -c x.yaml`.
|
|
243
|
+
|
|
244
|
+
## What an agent sees at runtime
|
|
245
|
+
|
|
246
|
+
Environment inside every agent's tmux session:
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
SWARM_AGENT this agent's name SWARM_CONFIG absolute path to the YAML
|
|
250
|
+
SWARM_PEERS comma-separated can_talk_to SWARM_ROOT the swarm root directory
|
|
251
|
+
SWARM_SESSION its tmux session name SWARM_NAME the swarm's name
|
|
252
|
+
PATH has <root>/.swarm/bin first, providing `swarm`
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Sending, from inside an agent:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
swarm send --to reviewer "src/parse.py is ready for review"
|
|
259
|
+
swarm broadcast "I renamed the config module"
|
|
260
|
+
tmux send-keys -t reviewer -l "raw fallback, skips permissions and logging" \
|
|
261
|
+
&& tmux send-keys -t reviewer Enter
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Loop guard: a forwarded message carries a hop count that increments on each relay and
|
|
265
|
+
stops at `max_forward_hops`. A message you send with `swarm send` resets it to zero.
|
|
266
|
+
|
|
267
|
+
## Worked examples in this repo
|
|
268
|
+
|
|
269
|
+
- `examples/research-swarm.yaml` — hub and spoke: a lead delegates to a scout
|
|
270
|
+
(gemini), an analyst (codex) and a writer; the writer uses a custom `workdir`.
|
|
271
|
+
- `examples/software-company.yaml` — six agents across all four CLIs with a
|
|
272
|
+
restricted comms graph (developers talk to the architect and QA, never to
|
|
273
|
+
each other).
|
|
274
|
+
- `examples/bug-hunt.yaml` — a hands-free pipeline built on `forward_responses_to`:
|
|
275
|
+
reproduce → diagnose → fix → verify.
|
|
276
|
+
- `examples/existing-repo.yaml` — two agents pairing inside one existing checkout,
|
|
277
|
+
using `create_workdirs: false` so a wrong path fails instead of being created.
|
|
278
|
+
|
|
279
|
+
## Recipes
|
|
280
|
+
|
|
281
|
+
**Hub and spoke** — one orchestrator drives specialists, which reply only to it:
|
|
282
|
+
|
|
283
|
+
```yaml
|
|
284
|
+
agents:
|
|
285
|
+
- {name: orchestrator, type: claude, can_talk_to: "*",
|
|
286
|
+
in_first_prompt_append_your_task_will_be_sent_in_the_next_prompt: true}
|
|
287
|
+
- {name: researcher, type: gemini, can_talk_to: [orchestrator]}
|
|
288
|
+
- {name: developer, type: codex, can_talk_to: [orchestrator]}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Pipeline** — each stage hands off to the next, automatically:
|
|
292
|
+
|
|
293
|
+
```yaml
|
|
294
|
+
agents:
|
|
295
|
+
- {name: spec, type: claude, can_talk_to: [build], forward_responses_to: [build]}
|
|
296
|
+
- {name: build, type: codex, can_talk_to: [review], forward_responses_to: [review]}
|
|
297
|
+
- {name: review, type: claude, can_talk_to: []}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Isolated worker** — runs alone, talks to nobody:
|
|
301
|
+
|
|
302
|
+
```yaml
|
|
303
|
+
agents:
|
|
304
|
+
- {name: scribe, type: hermes, can_talk_to: [], capture: none,
|
|
305
|
+
append_agents_that_you_can_talk_to_prompt: false}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Busy agents (backpressure)
|
|
309
|
+
|
|
310
|
+
- An agent is "busy" from the moment a message is submitted to it until its next
|
|
311
|
+
captured turn end. Sending to a busy agent is refused with a message telling the
|
|
312
|
+
sender to retry later, `--queue`, or `--wait`.
|
|
313
|
+
- `--queue` appends to `<root>/.swarm/run/<agent>.queue.jsonl`; the capture hook that
|
|
314
|
+
ends the agent's turn drains one message from it. `--wait` blocks until deliverable.
|
|
315
|
+
`--queue --wait` queues, then blocks until the agent picks the message up.
|
|
316
|
+
- The busy check and the "now busy" write happen inside the recipient's pane lock, so
|
|
317
|
+
two parallel senders can never both see an idle agent and both deliver.
|
|
318
|
+
- Turn state is two counters (`delivered`, `completed`), not a boolean: a hook that
|
|
319
|
+
ends turn N may land after a message delivered during turn N. `completed` is clamped
|
|
320
|
+
to `delivered` on each turn end, because codex folds a queued message into the turn
|
|
321
|
+
already running, and an incrementing counter would drift and wedge the agent "busy".
|
|
322
|
+
- Only works when the agent is captured. `capture: none` => `busy_check` is forced off
|
|
323
|
+
and the agent always accepts mail.
|
|
324
|
+
- `busy_timeout_ms` fails open if a capture never fires; `swarm idle <agent>` clears it.
|
|
325
|
+
- Auto-forwarded responses to a busy peer are queued rather than dropped.
|
|
326
|
+
- Queues self-heal: every turn end sweeps other agents and drains any that have gone
|
|
327
|
+
idle (or gone stale-busy past `busy_timeout_ms`), so a single missed turn-completion
|
|
328
|
+
cannot strand queued mail forever. A capture only fires when `type` matches the CLI
|
|
329
|
+
the `command` actually launches — a `type: codex` agent running `claude` never reports
|
|
330
|
+
turn ends and looks busy forever.
|
|
331
|
+
|
|
332
|
+
## Subagents and concurrency
|
|
333
|
+
|
|
334
|
+
- A subagent inherits `SWARM_AGENT`, so `swarm send` from inside one is attributed to
|
|
335
|
+
the parent agent and checked against the parent's `can_talk_to`.
|
|
336
|
+
- Every write into a pane takes a per-recipient lock (`<root>/.swarm/run/<session>.lock`).
|
|
337
|
+
Without it, parallel senders interleave: a paste and its Enter are two tmux calls, so
|
|
338
|
+
one Enter can submit two concatenated messages and another submit nothing.
|
|
339
|
+
- A message delivered to a busy agent is queued by the CLI and processed after the
|
|
340
|
+
current tool call. It is not lost.
|
|
341
|
+
- `capture: hook` fires when the *agent's* turn ends. Claude `Task` subagents run inside
|
|
342
|
+
the turn, so the hook waits for them. Backgrounded work ends the turn early: the
|
|
343
|
+
interim "I will respond when the subagent finishes" is captured (and forwarded), then
|
|
344
|
+
the real answer is captured when the agent finishes for real. The hop counter stores
|
|
345
|
+
the hop at which the agent last *received* a message, so a second response reuses the
|
|
346
|
+
same hop and the loop guard never suppresses the real answer.
|
|
347
|
+
- Claude records subagent turns in the same transcript with `isSidechain: true`. The
|
|
348
|
+
Stop hook skips them, so subagent chatter is never relayed as the agent's answer.
|
|
349
|
+
- `capture: pane` cannot tell "waiting on a subagent" from "finished": a quiet pane is
|
|
350
|
+
its only signal, so it forwards the interim message and then the real one. Raise
|
|
351
|
+
`pane_idle_ms`, or use `capture: none`.
|
|
352
|
+
|
|
353
|
+
## Verified CLI behaviours (do not "simplify" these away)
|
|
354
|
+
|
|
355
|
+
- The claude `Stop` hook entry must have **no `matcher` key**. With one, the interactive
|
|
356
|
+
TUI never runs the hook (headless `-p` still does, which makes this easy to miss).
|
|
357
|
+
- Claude fires `Stop` *before* flushing the assistant message to the transcript. Read it
|
|
358
|
+
with a short poll, and only consider text after the last user record -- otherwise the
|
|
359
|
+
hook captures nothing, or re-relays the previous turn's reply.
|
|
360
|
+
- Claude asks "do you trust this folder?" in any new directory, even under
|
|
361
|
+
`--dangerously-skip-permissions`. Pre-trust it by setting `hasTrustDialogAccepted`
|
|
362
|
+
for that path in `~/.claude.json` before launching.
|
|
363
|
+
- Long pastes are collapsed into a chip: `[Pasted text #1 +N lines]` (claude),
|
|
364
|
+
`[Pasted Content N chars]` (codex). Delivery checks must recognise both.
|
|
365
|
+
|
|
366
|
+
- Claude Code discards keystrokes for several seconds partway through startup
|
|
367
|
+
(measured on v2.1.205: t=2s landed, t=6s and t=12s were dropped, t=20s landed).
|
|
368
|
+
A fixed `boot_delay_ms` cannot fix this, which is why `ready_probe` types a
|
|
369
|
+
throwaway token until the input box echoes it, and why a paste is verified on
|
|
370
|
+
screen before Enter is pressed.
|
|
371
|
+
- Codex shows a "do you trust this directory?" modal on first run in a new folder;
|
|
372
|
+
Enter answers the modal instead of submitting the prompt. The generated
|
|
373
|
+
`<agent>/.codex/config.toml` pre-trusts the agent's workdir to avoid it.
|
|
374
|
+
- TOML is order-sensitive. In that generated config, `notify` must appear above
|
|
375
|
+
every `[table]` header, or it becomes `projects.<dir>.notify` and codex never
|
|
376
|
+
calls it.
|
|
377
|
+
- Claude collapses a long paste into a `[Pasted text #1 +N lines]` chip, so the
|
|
378
|
+
pasted text never appears verbatim on screen. Delivery checks look for the chip
|
|
379
|
+
as well as for the tail of the pasted text (the head scrolls out of view).
|
|
380
|
+
|
|
381
|
+
## Gotchas
|
|
382
|
+
|
|
383
|
+
- A folder and a tmux session are named after each agent; renaming an agent orphans both.
|
|
384
|
+
- Pointing several agents at one `workdir` is allowed and warned about; they share a
|
|
385
|
+
filesystem and a git index, so they can clobber each other.
|
|
386
|
+
- Two agents with `forward_responses_to` pointing at each other will relay until the hop
|
|
387
|
+
limit stops them. Prefer one-directional forwarding, or none.
|
|
388
|
+
- `--dangerously-skip-permissions` / `--yolo` let agents run tools unsupervised. Point
|
|
389
|
+
`root` at a disposable directory.
|
|
390
|
+
- The first prompt is typed into a live TUI. If AgentSwarm reports "could not confirm
|
|
391
|
+
the text arrived", the CLI is usually stuck behind a modal (login, trust, onboarding);
|
|
392
|
+
attach to the session and look. Raise `ready_timeout_ms` if it is merely slow.
|
|
393
|
+
- PyYAML is used when installed; otherwise a bundled subset parser handles the config.
|
|
394
|
+
It supports block maps/sequences, `|` and `>` scalars, flow `[a, b]` / `{a: 1}`,
|
|
395
|
+
comments and quoting — but not anchors, aliases, tags or multi-document files.
|
|
396
|
+
|
|
397
|
+
## Files
|
|
398
|
+
|
|
399
|
+
- [tests/validate.sh](tests/validate.sh): the full suite. Run it after any change; it
|
|
400
|
+
uses mock agents, so it needs no API key. 48 checks, all must pass.
|
|
401
|
+
- [agents.example.yaml](agents.example.yaml): every option, documented inline.
|
|
402
|
+
- [README.md](README.md): human-facing guide, architecture and troubleshooting.
|
|
403
|
+
- [lib/config.py](lib/config.py): schema, defaults and validation — the source of truth.
|
|
404
|
+
- [lib/swarm.py](lib/swarm.py): tmux orchestration, message routing, capture.
|
|
405
|
+
- [hooks/](hooks/): the Claude `Stop` hook and the Codex `notify` program.
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentainer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Run a configurable swarm of coding-agent CLIs (Claude Code, Codex, Gemini) in tmux and let them talk to each other.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agents",
|
|
7
|
+
"ai",
|
|
8
|
+
"claude",
|
|
9
|
+
"codex",
|
|
10
|
+
"gemini",
|
|
11
|
+
"tmux",
|
|
12
|
+
"orchestration",
|
|
13
|
+
"multi-agent",
|
|
14
|
+
"swarm"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/mehmetcanfarsak/AgentSwarm#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/mehmetcanfarsak/AgentSwarm/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/mehmetcanfarsak/AgentSwarm.git"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "mehmetcanfarsak",
|
|
26
|
+
"type": "commonjs",
|
|
27
|
+
"bin": {
|
|
28
|
+
"agentainer": "bin/agentainer.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin/agentainer.js",
|
|
32
|
+
"lib/*.py",
|
|
33
|
+
"hooks/*.sh",
|
|
34
|
+
"scripts/check-deps.js",
|
|
35
|
+
"swarm.sh",
|
|
36
|
+
"examples/*.yaml",
|
|
37
|
+
"agents.example.yaml",
|
|
38
|
+
"README.md",
|
|
39
|
+
"llms.txt"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"postinstall": "node scripts/check-deps.js",
|
|
43
|
+
"doctor": "node scripts/check-deps.js"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=16"
|
|
47
|
+
},
|
|
48
|
+
"os": [
|
|
49
|
+
"linux",
|
|
50
|
+
"darwin"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Dependency doctor for Agentainer.
|
|
3
|
+
//
|
|
4
|
+
// Runs on `npm install` (postinstall) and on demand via `agentainer doctor`.
|
|
5
|
+
// It only checks the things Agentainer itself needs -- python3 and tmux -- and
|
|
6
|
+
// reports on optional agent CLIs (claude, codex, gemini, ...) without ever
|
|
7
|
+
// treating their absence as an error: a user may only ever run one of them.
|
|
8
|
+
//
|
|
9
|
+
// This script NEVER fails the install. Missing tools are reported with hints;
|
|
10
|
+
// the exit code stays 0 so `npm install -g agentainer` always succeeds.
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
const { spawnSync } = require("child_process");
|
|
14
|
+
const os = require("os");
|
|
15
|
+
|
|
16
|
+
function has(cmd, args) {
|
|
17
|
+
const probe = spawnSync(cmd, args, { stdio: "ignore" });
|
|
18
|
+
return !probe.error && probe.status === 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const platform = os.platform();
|
|
22
|
+
function installHint(pkg) {
|
|
23
|
+
if (platform === "darwin") return `brew install ${pkg}`;
|
|
24
|
+
if (platform === "linux")
|
|
25
|
+
return `sudo apt install ${pkg} (or your distro's package manager)`;
|
|
26
|
+
if (platform === "win32")
|
|
27
|
+
return `${pkg} is not natively supported on Windows; use WSL2`;
|
|
28
|
+
return `install ${pkg} with your package manager`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- Required: Agentainer cannot run without these -------------------------
|
|
32
|
+
const required = [
|
|
33
|
+
{ name: "python3", ok: has("python3", ["--version"]) || has("python", ["--version"]), hint: installHint("python3") },
|
|
34
|
+
{ name: "tmux", ok: has("tmux", ["-V"]), hint: installHint("tmux") },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// --- Optional: at least one agent CLI, but which one is up to the user ------
|
|
38
|
+
const optional = [
|
|
39
|
+
{ name: "claude", label: "Claude Code", ok: has("claude", ["--version"]) },
|
|
40
|
+
{ name: "codex", label: "Codex CLI", ok: has("codex", ["--version"]) },
|
|
41
|
+
{ name: "gemini", label: "Gemini CLI", ok: has("gemini", ["--version"]) },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const missingRequired = required.filter((r) => !r.ok);
|
|
45
|
+
|
|
46
|
+
process.stdout.write("\nAgentainer -- checking dependencies\n");
|
|
47
|
+
process.stdout.write("-----------------------------------\n");
|
|
48
|
+
|
|
49
|
+
for (const r of required) {
|
|
50
|
+
process.stdout.write(` [${r.ok ? "ok" : "--"}] ${r.name}${r.ok ? "" : ` -> ${r.hint}`}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.stdout.write("\n Agent CLIs (install whichever you'll actually use):\n");
|
|
54
|
+
for (const o of optional) {
|
|
55
|
+
process.stdout.write(` [${o.ok ? "ok" : " "}] ${o.name.padEnd(8)} ${o.label}\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (missingRequired.length) {
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
"\n!! Missing required tools: " +
|
|
61
|
+
missingRequired.map((r) => r.name).join(", ") +
|
|
62
|
+
"\n Install them, then re-check with: agentainer doctor\n"
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
const anyAgent = optional.some((o) => o.ok);
|
|
66
|
+
process.stdout.write(
|
|
67
|
+
"\nok Core dependencies satisfied." +
|
|
68
|
+
(anyAgent ? "" : " (No agent CLI detected yet -- install one to start a swarm.)") +
|
|
69
|
+
"\n"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
process.stdout.write("\n");
|
|
73
|
+
|
|
74
|
+
// Always succeed: never abort an npm install over a missing optional (or even
|
|
75
|
+
// required) tool. The launcher re-checks at runtime and fails clearly there.
|
|
76
|
+
process.exit(0);
|
package/swarm.sh
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# AgentSwarm -- launch a configurable swarm of coding agents in tmux.
|
|
4
|
+
#
|
|
5
|
+
# ./swarm.sh up start every agent in agents.yaml
|
|
6
|
+
# ./swarm.sh up -c my-swarm.yaml ...from a different config
|
|
7
|
+
# ./swarm.sh agents.yaml shorthand for `up -c agents.yaml`
|
|
8
|
+
# ./swarm.sh status see who is running
|
|
9
|
+
# ./swarm.sh send --to dev "hi" message an agent
|
|
10
|
+
# ./swarm.sh attach dev jump into an agent's tmux session
|
|
11
|
+
# ./swarm.sh down stop everything
|
|
12
|
+
#
|
|
13
|
+
# See README.md for the full reference, or llms.txt if you are an LLM.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
SWARM_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
export SWARM_HOME
|
|
18
|
+
|
|
19
|
+
PYTHON="${SWARM_PYTHON:-}"
|
|
20
|
+
if [[ -z "$PYTHON" ]]; then
|
|
21
|
+
for candidate in python3 python; do
|
|
22
|
+
if command -v "$candidate" >/dev/null 2>&1; then
|
|
23
|
+
PYTHON="$candidate"
|
|
24
|
+
break
|
|
25
|
+
fi
|
|
26
|
+
done
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [[ -z "$PYTHON" ]]; then
|
|
30
|
+
echo "xx AgentSwarm needs python3 on PATH (or set SWARM_PYTHON)" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
35
|
+
echo "!! tmux was not found on PATH; every command except 'validate' will fail" >&2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# The config is resolved in lib/swarm.py: -c, then $SWARM_CONFIG, then
|
|
39
|
+
# ./agents.yaml, then the agents.yaml next to this script. Deliberately NOT
|
|
40
|
+
# exported here -- a stale SWARM_CONFIG would shadow the config that a hook
|
|
41
|
+
# discovers from the agent's working directory.
|
|
42
|
+
|
|
43
|
+
exec "$PYTHON" "$SWARM_HOME/lib/swarm.py" "$@"
|