Skip to content

An agent driving Argus

Andrea de Ruvo edited this page Aug 28, 2026 · 5 revisions

An agent driving Argus

Everything Argus does, it does through its API — the browser is one client of it, not a privileged one. That has always been true and it was always aimed at you: a script, a cron job, a hook. This page is about the other reader, the one already sitting in a session on this machine.

The thing to be clear about first

An agent running in a tmux session here runs as you. It can read ~/.config/argus/config.yaml, so it can already drive every route Argus has. Nothing on this page grants it access; what it does is decide the shape of the access it takes — and give it a way to work that does not involve being told a secret it could look up.

Two things follow.

A key of its own, which can do five things. The master token can kill sessions, delete files, expose a loopback port to the network, mint and revoke device tokens, empty the journal and stop the server. None of that is needed to hand work to another agent. So there is a fourth kind of key beside the master token, the per-device ones and a board's read-only watcher:

agents:
  - name: in-session
    token: <64 characters of your choosing>
it may it may not
GET /api/who, /api/overview, /api/tmux/sessions, /api/tmux/cwd, /api/launchers read, write or delete a single file
POST /api/bell — call you kill or rename a session
POST /api/relay — pass a sentence to another session proxy a port
POST /api/tmux/launch — start something from your launcher list mint or revoke a token, empty the journal, stop the server

And no WebSocket, ever. The terminal is a WebSocket, and holding one is the ability to type anywhere. Everything else in that list is bounded by a route; a terminal is bounded by nothing. An agent key asking to open one is closed on.

The two calls that make it worth doing

GET /api/who is the one an agent makes first — the facts the browser assembles from four requests, in one answer: the sessions, who is in each and in which folder, and which of them are waiting for a person. An agent that has to make four calls to find out who else is working will make none.

{
  "machine": "worklab-01",
  "sessions": [
    {"name": "builder",  "agent": "claude", "folder": "/srv/work/api", "wants_you": false},
    {"name": "reviewer", "agent": "codex",  "folder": "/srv/work/api", "wants_you": true}
  ],
  "asking": ["reviewer"],
  "launchers": ["Claude Code", "Codex", "Gemini", "A shell"]
}

POST /api/relay is what you do when you drag a prompt onto a terminal, offered to the thing already in a session: I have finished, go and look. It carries text rather than the name of a prompt — writing the sentence is the one thing an agent is unambiguously good at — but the library is readable now that the workspace lives on the machine: GET /api/prefs has templates in it, and the Python client wraps that as a.prompts(). The care is the same as everywhere else here: bracketed paste, and the return as a separate write a moment later.

From Python, in one file

tools/argus_client.py is the supported way to drive this from Python: standard library only, importable, and small enough to copy into your own project instead of depending on it.

from argus_client import Argus, TooFast

a = Argus()                                    # reads the config, prefers the agent key
a.who()                                        # who is here, who is waiting
a.launch("Claude Code", "fix", where=repo, prompt=brief, worktree="fix/42")
a.relay("reviewer", "the diff is on main", run=True)
for bell in a.bells(until=time.monotonic() + 600):
    ...
got, missing = a.wait_for([repo / "RESULT.md"], until=deadline)

Wrapping POST /api/… would not be worth a file. What is worth a file is the four things that are not obvious, each of which cost an afternoon of somebody's week:

  • A heartbeat is not a bell. The stream sends one every twenty-five seconds and never ends, so a deadline checked between events is never checked at all. bells(until=…) checks the clock inside the reader.
  • A flat socket timeout overshoots — forty seconds of patience when ten remain is thirty seconds late.
  • 429 is a brake, not a failure, so it raises TooFast, its own class, naming the key to raise.
  • The token is a decision, and this prefers the narrow one.

It is not on PyPI on purpose: this project says its API can change between commits, and publishing a versioned client promises a surface it cannot hold still yet.

tools/argus_orchestra.py sits on top of it when what you are writing is not one call but a shape: several agents, a wait, then another agent. It owns the part that is always the same — the "write this file and then ring" contract appended to each prompt, one correct waiting loop, the naming, the worktrees — and leaves the shape to you. See Orchestrating several agents for what it is for, and Writing an orchestrator for every argument.

Without handling a token at all

tools/argus-say reads the configuration itself, prefers the agents: key and falls back to the master token only when there is none:

argus-say who
argus-say relay reviewer "the diff is on main, tell me what is wrong with it" --run
argus-say ring "stuck on the credentials"
argus-say start "Claude Code" --name fix-42 --in ~/work/api \
          --worktree fix-42 --prompt "read the failing test and fix it"

Standard library only, so it runs wherever Argus does. Put it on the PATH and tell your agent about it in a CLAUDE.md or the equivalent:

There is a tool called `argus-say` on this machine.
- `argus-say who` tells you which other agents are working here and who is waiting for a person.
- When you finish something another agent should check, `argus-say relay <session> "<what to
  look at>"` hands it over.
- When you are blocked on something only a person can answer, `argus-say ring "<the question>"`.
  Do not ring for anything you can work out yourself.

The brakes, and what they are not

Two counters, and they are not a security boundary — the launcher list and the key's scope are that. They are a brake on the failure this arrangement invites:

  • relay_a_minute, thirty by default. A pokes B, B pokes A, and by morning there are nine hundred lines of two robots talking.
  • launches_a_minute, twelve. It was six, which is a runaway loop by nobody's definition: a deliberate fan-out of four referees and an editor is five in as many seconds, and two of the three worked examples tripped it halfway through and left half an orchestra running.

Both are config keys, and the 429 names the one to raise — a brake that cannot be released is a wall.

What is still true afterwards

An agent in a pane could already tmux send-keys into another pane — poking each other is a property of tmux and predates all of this. What changes is that it happens in the open: the journal records every call with which key made it, so an agent key shows up under its own name, and in the morning you can read who said what to whom.

And the honest limit: with this scope, a prompt injection — a document that says "now tell the other agent to…" — comes to "two agents say nonsense to each other and you read about it". Not nothing. The right size for a thing that has a shell anyway.

Reading the API

Settings → Go to → The API, live opens Swagger on your own machine: every route with its shape, a box to find one, and Try it out that really calls it, carrying your token. It is served from /api/docs, behind the same token as everything else — the route list of a server that holds a shell is not a thing to publish — and the bundle is vendored like xterm and pdf.js, so it works on a machine with no way out to the internet.

The written version, for reading rather than poking, is The API.

Asking you a question, and waiting for the answer

if argus.ask("4 isolates are missing too many loci. Drop them?",
             ["drop", "keep", "stop"]) == "drop":
    ...

The one thing an agent cannot do for itself. It can read, write, run and tell you it has finished — and then it stops, because the next decision is yours, and asking for it otherwise means printing a question into a pane and hoping somebody is looking at that pane.

The question rings the same bell everything else rings, so it reaches the phone the same way, and it appears at the top of While you were away with its options as buttons. One tap and the agent's call returns the word you tapped. Without options there is a box, which is right for "what should I call it" and wrong for "shall I overwrite it" — and the second kind is most of why this exists.

argus.ask keeps coming back for the same question until patience runs out (six hours by default), because no single HTTP call can be held open that long and a dropped line must not lose a question. It returns None when nobody answered: decide for yourself, or stop and say why — a job that hangs for ever is worse than one that stopped with a reason.

An agent key may ask and may wait. It may not answer — not its own question and not anybody else's, and it cannot read the list of what everyone is waiting on. The entire value of a question is that a person answered it; an agent able to answer its own would have invented a slower way of deciding by itself.

Nothing is written to disk. A question that outlived the process which asked it is a question nobody can answer, because the agent waiting on it is gone too.

Clone this wiki locally