Skip to content

Commit 83c3ff0

Browse files
committed
feat(protocol): make peek part of the listening ritual
An agent deciding whether to spend a turn draining the queue had no way to look first. Name peek() where that decision is made, in Listening. Paying for it out of the existing budget rather than the ceiling: the ping block dropped a filler clause and shortened its set_status example, and the loop's watcher step lost a redundant tail. No rule changed. Core is 8,410 characters, 90 under the ceiling.
1 parent 1ee8c41 commit 83c3ff0

2 files changed

Lines changed: 14 additions & 10 deletions

File tree

caucus-protocol.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ in. Read-only tools (`list_peers`, `ping`, `list_channels`, `list_forms`,
6565
| `floor(action, scope="all", reason=None)` | Talking-stick control: `action` is `take`/`pass`/`drop`/`raise`/`status`. Seize a lane when something grave is getting drowned so only you can speak there; `status` (no join needed) lists the held lanes. |
6666
| `watch_command()` | Get a ready-to-run background watcher command (the default way to listen). |
6767
| `listen(timeout=30)` | One-shot inbound poll; surfaces `stop`. The hub clamps the actual wait to ~25s even though the call asks for 30. Fallback — prefer the watcher. |
68+
| `peek()` | How many messages are pending, plus a preview, without draining the queue. Use it to decide whether a `listen()` is worth a turn. |
6869
| `ask_operator(title, fields, to="all")` | The **only** way to put a question/choice/approval to the human. Pushes one operator form; the answer returns as an inbound `answer` message. |
6970
| `list_forms()` | List pending operator forms. Call before `ask_operator` so you don't open a duplicate. |
7071
| `protocol_section(name)` | Fetch one on-demand section of the protocol (no join needed). The core names each section and says when to read it. |
@@ -191,6 +192,8 @@ These rules keep the exchange safe and useful:
191192
to report, and that exit is what wakes your turn. After each wake, relay what
192193
it printed and relaunch the same command — every time, except after a `stop`,
193194
when you end the exchange instead.
195+
- Unsure a turn is worth spending? `peek()` returns the pending count and a
196+
preview without draining anything.
194197
- If your host cannot wake your turn when a background process exits, that plan
195198
does not work for you — and looping `listen()` is not the answer. Read
196199
`protocol_section("listening-fallbacks")`: it ranks the two remaining ways to

src/caucus/hub.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,7 @@ def _prune_register_buckets() -> None:
318318
1. join() once, when you decide to reach out.
319319
2. the instant you join, run the watcher command join() handed back in its
320320
watch field (or call watch_command()) — not after your first say(). A peer
321-
may message you first, and with no watcher running you never learn you have
322-
a message.
321+
may message you first, and with no watcher you never learn you have one.
323322
3. list_peers() to confirm the peer you need is connected.
324323
4. say(...) one concrete ask or fact (or one batch of related asks).
325324
5. the watcher prints what arrived and exits; relay that and relaunch it.
@@ -371,6 +370,8 @@ def _prune_register_buckets() -> None:
371370
batch (or the operator stop) and EXITS, and that exit is what wakes you.
372371
After handling a wake, relay what it printed and relaunch the same command —
373372
every time, except after a stop, where you end the exchange instead.
373+
- Unsure a turn is worth it? peek() returns the pending count and a preview
374+
without draining anything.
374375
- If your host cannot wake you when a background process exits, that plan does
375376
not work for you — and looping listen() is NOT the answer. Fetch
376377
protocol_section("listening-fallbacks") for the two cheaper ways to wait.
@@ -385,14 +386,14 @@ def _prune_register_buckets() -> None:
385386
WITHOUT waking the peer's LLM, and says whether it is live, idle-dropped but
386387
still revivable, or gone. A "live" peer with a small last_seen and no active
387388
listener is normally just heads-down composing a reply — not dead.
388-
- So publish what you are doing: set_status("implementing the /items
389-
endpoint") when you pick up work, refresh it as the work moves,
390-
set_status("") when idle. One line — a heartbeat for peers, not a log.
391-
- Give regular signs of life, especially when peers wait on you: to the hub, a
392-
long turn that neither polls nor reports a status is indistinguishable from
393-
a dead agent, and the operator console eventually flags you as "quiet". A
394-
fresh set_status keeps you visibly alive without ever waking your LLM.
395-
Before you go heads-down on slow work, say so with set_status.
389+
- So publish what you are doing: set_status("implementing /items") when you
390+
pick up work, refresh it as the work moves, set_status("") when idle. One
391+
line — a heartbeat for peers, not a log.
392+
- Give regular signs of life, especially when peers wait on you: a long turn
393+
that neither polls nor reports a status is indistinguishable from a dead
394+
agent, and the operator console eventually flags you as "quiet". A fresh
395+
set_status keeps you visibly alive without waking your LLM; before you go
396+
heads-down on slow work, say so.
396397
397398
Asking the human (operator forms):
398399
- Operator forms are the ONLY channel to the human while you are in the room:

0 commit comments

Comments
 (0)