subagent: give a run a goal, turns toward it, and a reason it stopped
`start` takes an optional `goal`. With one set a session stops being a single turn: when a turn ends and nothing has said to stop, the daemon spawns another turn re-prompting the subagent toward that goal, up to `max_turns` (default 5, per-session). Without a goal nothing changes — one turn, one todo, same as before. Four things end a run, each recorded distinctly and reported by `status`: the turn ending with no goal, `goal_reached`, `need_help`, and the turn cap. The last says so out loud rather than stopping quietly — the todo states the harness limit was reached and the goal was never reported reached. Every stop extends the done message rather than replacing it, and lands in the session's report file when it has one. The path is never inferred: it comes from `start`'s `report_file` or from the subagent naming where it wrote. `goal_reached` and `need_help` are the subagent's own, served on a second route (`/signal/mcp`) that carries those two tools and nothing else, so reporting on a run can't become starting one. `goal_reached` is built as a label, never a gate: it is self-reported by a subagent that has just been re-prompted with "you haven't reached the goal", which is exactly the incentive to claim it — the same failure class as a build report asserting the tests pass. Every surface that renders it says so. `need_help` is the blocking signal, and shows in `status` as its own state so a parent polling it sees the block without reading a file. `status` also carries `turn N of M`: with 4330's last-event age, that separates working from wedged from out of turns off one answer. Two bugs the new tests caught: a `tokio::fs::File` was dropped without flushing, so the report line was written to nothing, and the plain idle answer dropped the turn counter. Also documents `await_resume`'s third case — a closed channel with no send, which fails open the same as `Underway` — per argus on #4411. Refs #4403
This commit is contained in:
parent
6e2de33f26
commit
b18348bc9a
10 changed files with 1758 additions and 253 deletions
|
|
@ -24,11 +24,21 @@ infrastructure, not the agent-facing API.
|
|||
Served under the `subagent` MCP server (`mcp__subagent__<tool>`): `start`,
|
||||
`continue`, `status`, `interrupt`.
|
||||
|
||||
A second route on the same port serves the two tools a **subagent** calls
|
||||
about its own run — `goal_reached` and `need_help`. It isn't part of the
|
||||
`subagent` server an agent's own config points at; the daemon writes it
|
||||
into each subagent's `--mcp-config` itself, under `subagent_control`. Two
|
||||
routes rather than six tools on one, so that being able to report on a run
|
||||
never carries the ability to start one: there's no route a subagent holds
|
||||
that `start` is reachable from.
|
||||
|
||||
## State
|
||||
|
||||
In-memory only: what's running now, where each name's session lives, how
|
||||
each name's last turn ended, and when each running turn last produced
|
||||
output. All of it lives only as long as the daemon process does. A daemon
|
||||
each name's last turn ended, when each running turn last produced output,
|
||||
what each session is working toward, how far through its turn budget it
|
||||
is, why its run stopped, and where it writes its report. All of it lives
|
||||
only as long as the daemon process does. A daemon
|
||||
restart stops whatever was running rather than adopting it. The durable
|
||||
record of a subagent's existence is claude's own on-disk session
|
||||
(`hive_claude::SessionStore`), which `continue` reattaches to independent
|
||||
|
|
@ -40,14 +50,68 @@ after a restart has to re-supply `dir` when the session lives anywhere
|
|||
other than the daemon's own working directory — and a restart is the
|
||||
situation you reach for `continue` in most often.
|
||||
|
||||
## Goals, and turns toward them
|
||||
|
||||
`start` takes an optional `goal`. Without one a session is a single turn,
|
||||
exactly as it always was. With one, the daemon keeps the session going:
|
||||
when a turn ends and nothing has said to stop, it starts another turn
|
||||
re-prompting the subagent toward that goal, quoting it verbatim and saying
|
||||
which turn of the budget this is. `max_turns` caps that, defaulting to
|
||||
**5**.
|
||||
|
||||
`status` reports `Turn N of M` for such a session in every state it
|
||||
reaches. Read alongside the last-event age below, it's what separates a
|
||||
subagent that's working from one that's wedged from one that's out of
|
||||
turns — without `ps` and without opening a file.
|
||||
|
||||
Four things stop a run, and each is recorded distinctly, reported by
|
||||
`status`, and appended to the one todo the daemon pushes when the run ends:
|
||||
|
||||
- **the turn ended and there was no goal** — the single-turn case;
|
||||
- **`goal_reached`**, which the subagent calls itself;
|
||||
- **`need_help`**, likewise;
|
||||
- **the turn cap**, which says so rather than stopping quietly: the todo
|
||||
states that the harness limit was reached and the goal was never
|
||||
reported reached, so the work stopped where it had got to.
|
||||
|
||||
A killed or failed turn ends the run too, and keeps the records it already
|
||||
had — see [A killed turn](#a-killed-turn). `interrupt` therefore stops a
|
||||
whole goal run, not just the turn in flight.
|
||||
|
||||
When the session was told where its report goes — `start`'s `report_file`,
|
||||
or the path the subagent names when it signals — the stop reason is
|
||||
appended to that file as well, so the artifact you were going to read
|
||||
anyway also says how the run ended. Nothing is inferred: with no path
|
||||
given, no file is touched.
|
||||
|
||||
## `goal_reached` is a label, not a gate
|
||||
|
||||
Both signals stop the continuation and **extend** the done message. Extend,
|
||||
not replace: the turn's observed end and the reason the run stopped are
|
||||
different facts, and the second never stands in for the first.
|
||||
|
||||
`goal_reached` is **self-reported**, by a subagent that has just been
|
||||
re-prompted with "you haven't reached the goal" — which is precisely the
|
||||
incentive to claim it. It's the same failure class as a build report
|
||||
asserting "done, tests pass": a claim about an artifact, not the artifact.
|
||||
Nothing in this daemon treats it as verification, and every surface that
|
||||
renders it says so. Read the diff and the gate output regardless.
|
||||
|
||||
`need_help` is the blocking signal. It stops the run and shows up in
|
||||
`status` as its own state — blocked, with the subagent's reason — so a
|
||||
parent that polls `status` sees the block without reading anything else.
|
||||
`continue` is how you answer it.
|
||||
|
||||
## Is it working, or is it wedged?
|
||||
|
||||
`status` reporting **running** says a process is tracked, which a wedged
|
||||
subagent satisfies as fully as a busy one. A running answer therefore
|
||||
carries the age of that turn's last event too: seconds means it's working,
|
||||
an age climbing into the minutes with no end-of-turn todo means it's
|
||||
stuck. That one number replaces inferring the same thing from `ps` output
|
||||
and CPU-time deltas.
|
||||
an age climbing into the minutes means it's stuck. That one number
|
||||
replaces inferring the same thing from `ps` output and CPU-time deltas.
|
||||
It resets at each turn's spawn, so on a goal run it describes the turn in
|
||||
flight rather than the run — which is what you want, since a run that's
|
||||
making progress spends several perfectly healthy minutes.
|
||||
|
||||
Every line the subagent's `claude` process writes bumps the timestamp —
|
||||
stream-json events, plain stdout chatter and stderr alike — and what the
|
||||
|
|
@ -135,10 +199,11 @@ Own systemd unit, defined alongside the other per-agent MCP daemons in
|
|||
|
||||
## MCP servers available to a subagent
|
||||
|
||||
A subagent runs with `--strict-mcp-config` and no `--mcp-config` by
|
||||
default — zero MCP servers, full stop; it falls back to claude's own
|
||||
native tools (`Bash`, `WebFetch`, etc.), not the parent's `mcp__bash__*` /
|
||||
`mcp__hyperhive__*` surface. Nothing implicit reaches it: the built-in
|
||||
A subagent runs with `--strict-mcp-config` and, by default, exactly one
|
||||
MCP server: the two-tool `subagent_control` route above. It otherwise
|
||||
falls back to claude's own native tools (`Bash`, `WebFetch`, etc.), not
|
||||
the parent's `mcp__bash__*` / `mcp__hyperhive__*` surface. Nothing
|
||||
implicit reaches it: the built-in
|
||||
hyperhive surface (todos/messaging) isn't an `extraMcpServers` entry at
|
||||
all, and the automatically injected `bash`/`subagent` entries default to excluded
|
||||
too (a subagent can't spawn hive-bash tasks or its own nested subagents
|
||||
|
|
|
|||
Loading…
Reference in a new issue