Files
tty7/docs/agents/orchestration.mdx
T

190 lines
7.2 KiB
Plaintext

---
title: "Orchestrating agents"
description: "One agent opening a pane for another, waiting on it, and reading the result."
---
Once an agent's status is a thing a program can ask about, one agent can run
another. tty7 gives that loop a primitive instead of leaving it to screen
scraping.
## The loop
```bash
# 1. give the worker a pane
PANE=$(tty7 split --v)
# 2. hand it a task
tty7 send "$PANE" 'claude -p "add tests for the parser"' --enter
# 3. sleep until it needs you or finishes
tty7 wait "$PANE" --until waiting,done --changed --timeout 600
# 4. read what happened
tty7 capture "$PANE" --plain
# 5. clean up
tty7 pane close "$PANE"
```
That is the whole shape. The interesting step is the third.
## `tty7 wait`
```bash
tty7 wait [%PANE] [--until STATE,…] [--changed] [--timeout SECS] [--interval MS]
```
Blocks until the pane reaches one of the states you named.
| Flag | Default | |
|---|---|---|
| `--until` | `waiting,done,exit` | Which states end the wait — see below |
| `--changed` | off | Ignore the state the pane was *already* in — only wake on one it moved into after the wait began |
| `--timeout` | none | Give up after this many seconds, exiting 124 |
| `--interval` | 500 ms | How often to poll |
Exit codes are made for scripts:
| Code | Meaning |
|---|---|
| `0` | A state you asked for was reached |
| `124` | Timed out — the `timeout(1)` convention, so "not yet" is distinguishable from "broken" |
| `1` | The worker died first; the JSON says `"status": "exit"` |
The reply carries the agent's own message and its native session id, so a
wake-up is directly actionable.
### The states
Four of them are the agent's own [status](/agents/status). The other three are
about the pane, because not everything worth waiting on is an agent:
| State | Means |
|---|---|
| `idle` `working` `waiting` `done` | What the agent's hooks last reported |
| `no-agent` | Nothing reports status in this pane — a plain shell, or an agent whose hooks are missing |
| `free` | The foreground command has exited; the pane is back to its bare shell |
| `exit` | The pane is gone. Ends every wait, whether you asked for it or not |
### Waiting on a command instead of an agent
An agent says when it is done. A `cargo test` does not — so for a plain pane the
question is whether anything is still running in front of the shell, which is
what `free` answers:
```bash
tty7 send "$PANE" 'cargo test > /tmp/t.log 2>&1; echo $? > /tmp/t.rc' --enter
tty7 wait "$PANE" --until free --changed --timeout 900
cat /tmp/t.rc /tmp/t.log
```
`free` costs one extra request per poll, so it is only checked when you name it —
and only once the agent ladder has not already answered, so pairing it with
`waiting,done` never costs you a state you asked for.
<Warning>
`free` is read off the process tree, which has two blind spots. A pane whose
own root process *is* the command — what `tty7 run` spawns — looks free the
whole time it runs; wait on `tty7 run` itself instead, it already blocks. And
a backgrounded job (`… &`) keeps the pane busy after the foreground command
has finished.
</Warning>
<Note>
A pane with no agent reports `no-agent`, **not** `idle`. That distinction is
why `--until idle` cannot be used to mean "the command finished" — `idle` is a
thing an agent says about itself, and a busy shell never says it.
</Note>
### Why `--changed` matters
The status the server keeps is a **level, not an event**. `done` stands until
the next turn begins; `waiting` stands until the agent moves again.
So a `wait` issued immediately after a `send` can answer with the *previous*
turn's state, before the worker has even read the input. `--changed` refuses the
state the pane was already in, which is what every round after the first needs.
Without it, the JSON's `stale` flag tells you whether that happened.
`free` has the same problem and a different fix: a shell that goes free → busy →
free ends up where it started, so there is no new state to compare against.
There `--changed` means "something ran while I was watching", which is exactly
what you want in the line after a `send`.
That does mean a command which starts *and* finishes between two polls is never
seen running, and the wait sits there until it times out. If the thing you are
waiting on can be that quick, poll faster (`--interval 100`) or drop `--changed`
and let a sentinel file carry the answer. The timeout says as much when it
happens.
## Answering a prompt
A worker that stops at `waiting` is usually showing something that keystrokes,
not text, are the answer to — a permission prompt driven by the arrow keys, a
menu, a TUI to be dismissed. `send --key` presses keys:
```bash
tty7 wait "$PANE" --until waiting --changed # it needs something
tty7 capture "$PANE" --plain | tail -20 # see what it is asking
tty7 send "$PANE" --key down --key enter # answer it
tty7 send "$PANE" --key C-c # or stop it altogether
```
Keys are delivered as separate events 200 ms apart, so a raw-mode TUI reads a
sequence as a sequence rather than as a paste. The
[full vocabulary](/cli/reference#tty7-send-pane-text-enter-key-key) is in the
reference.
## When an agent never moves
If `tty7 wait` times out and `tty7 agents` shows a status that never changes,
the likely cause is that the agent's status hooks are not installed or are out
of date — the agent is working fine, it just has no way to say so. `tty7 doctor`
reports where every agent's hooks stand, and `tty7 agents` names the specific
one when it can see the gap.
## Watching everything at once
```bash
tty7 agents # every agent on the machine: pane, agent, status, message
tty7 agents --json # the same, parseable
```
If you are an agent yourself, you are in that list too.
## Teaching an agent to do this
tty7 installs nothing into `~/.claude` for it — no switch in **Settings → Integrations** writes a skill, and none ever will. What the agent needs to know ships
in the repository instead, as a skill you install yourself:
```bash
npx skills add l0ng-ai/tty7
```
That covers the pane-driving half — where it is, how to open a pane, send into
one, read one back, and the rules below — see [the agent
skill](/cli/agent-skill). The `wait` step is documented on this page.
A skill rather than a global instruction, on purpose: only its one-line
description rides in context until something reaches for it, so an agent that
never touches another pane pays nothing for it.
## Rules of the road
<Warning>
The panes on a machine are somebody's real work, and some of them are other
agents mid-task. Treat anything you did not create as read-only.
</Warning>
- **Never `send` into a pane you did not open.** Check `tty7 agents` first.
- **Never close a pane, tab, or workspace you did not create.**
- **Never `server stop` or `server restart`.** Every pane on the machine dies
with it.
- **Clean up what you did create** — `tty7 pane close %83` when you are done.
An interrupted `run` leaves its pane behind; `tty7 pane ls --all` shows those,
and `tty7 pane close --orphans` clears them. That last one is a human's
broom, not an agent's: it closes every orphan on the machine, including ones
somebody else abandoned mid-command.
The full agent-facing contract is in [the skill](/cli/agent-skill).