Agents can hand work to agents: dispatch_agent

A schedule is a time trigger, and only the first step of a pipeline is really
waiting on time — every later step waits on the previous step's result. Modeling
"watch a source -> write a spec -> implement it" as three schedules made each fire
blind: on a quiet day the coding agent still spawned, paid a full model run to find
there was nothing to do, and left an empty run in Activity.

So an agent can start the next step itself. `dispatch_agent` (a tool on the bundled
MCP server, and on the pi bridge through the same --call seam) enqueues an ordinary
spawn command in the agent's own project, tagged requested_by=agent:<id> — so a
handoff is visible in Activity with no new surface to build.

- Project-scoped by construction: project_id is read from the spawn environment and
  never from the tool arguments.
- Bounded rather than gated: MAX_DISPATCH_PER_RUN counts the command rows the agent
  already wrote; MAX_DISPATCH_DEPTH rides in the spawn payload and is recovered by
  spawn._dispatch_depth, so a chain keeps its place across a resume and a cycle
  terminates instead of fanning out.
- New scout and planner roles, with built-in skills (handler-scout, handler-planner,
  handler-dispatch) carrying the judgment code can't: dedupe against a memory-note
  watermark, treat "nothing new" as a complete run, and write a task the receiving
  cold-start agent can act on.
- A scout ending on a clean tree skips the test gate and records the new
  tests_status='skipped' (migration 0017, additive CHECK widening). The gate promises
  `done` means tests passed for the work that shipped; nothing shipped.

Rejected a `condition` field on schedules: "is this paper new and does it matter
here?" is a semantic judgment, so it belongs to a model, not a scheduler column. The
scout is the condition; dispatch is how it reports true — one mechanism that covers
future pipelines too.

426 tests (14 new for dispatch, 3 for the gate exemption).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcbDevyMcJWE6qPA56C7mZ
This commit is contained in:
Claude
2026-08-19 22:46:06 +00:00
parent 52167db085
commit 8541b4b7c0
23 changed files with 945 additions and 33 deletions
+37
View File
@@ -6,6 +6,43 @@ the image workflows publish (plus `latest` from every push to `main`).
## [Unreleased]
### Added — dynamic workflows: agents hand work to agents
Until now the only recurring primitive was a schedule, which fires an unconditional
spawn on a timer. A pipeline (watch a source → write a spec → implement it) had to be
N independent schedules, each firing blind: on a quiet day the coding agent still
spawned, paid a full model run to discover there was no work, and left an empty run in
Activity. Time is the wrong trigger for the later steps — the previous step's *result*
is.
- **`dispatch_agent`**, a new tool on the bundled `handler-memory` MCP server (and on
the pi bridge, via the same `--call` seam). An agent hands work to a fresh agent in
**its own project**`project_id` comes from the spawn environment, never from the
arguments — by enqueuing an ordinary `spawn` command tagged
`requested_by = agent:<id>`. It shows up in Activity like any other command, so
nothing new had to be built to observe it.
- **Guardrails, not an approval queue.** `MAX_DISPATCH_PER_RUN` (default 3) bounds one
run's handoffs, counted off the command rows it already wrote; `MAX_DISPATCH_DEPTH`
(default 3) bounds how far a chain reaches, so a cycle terminates instead of fanning
out. Depth is recovered from the spawn command that created an agent, so it survives
a resume. A refused dispatch tells the agent what to do instead.
- **Two roles — `scout` and `planner`** — with built-in skills (`handler-scout`,
`handler-planner`, `handler-dispatch`) carrying the judgment the code can't: dedupe
against a memory watermark, treat "nothing new" as a complete run, write a task the
receiving agent can act on cold.
- **A quiet scout run is cheap.** A `scout` ending on a clean tree skips `mise run test`
and records the new `tests_status = 'skipped'` (migration `0017_gate_skipped`) — the
gate's promise is that `done` means tests passed *for the work that shipped*, and
nothing shipped. A dirty tree, or any other role, keeps the full gate.
The net effect: one cheap schedule fires the scout; on a quiet day the pipeline costs
one small-model call and stops. Only a scout that actually found something spends a
coding-model run, and the spec it produced travels with the dispatch.
**Rollout:** run migrations (`0017_gate_skipped`, additive — it only widens a CHECK).
The two new settings have working defaults. The three new skills seed themselves on the
next API start, idempotently by name.
### Fixed
- **The project-root checkout is now a ref store, never a working tree Handler moves.**