mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-01 08:01:47 +00:00
190 lines
7.2 KiB
Plaintext
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).
|