Skip to main content
The research loop is a two-agent optimization campaign. You give the research-coordinator a measurable objective and a budget — “get p95 latency under 200 ms, four rounds” — and it runs the scientific method on a fleet: it proposes falsifiable hypotheses, dispatches one experimenter session per hypothesis to implement and measure a single variant in an isolated sandbox, collates the results into a lab log, and iterates round after round until the objective is met, the budget is spent, or the search stops improving. Nothing ships without a human’s approval.

How it works

  1. A mention starts a campaign. A human mentions @research in Slack with an objective and a budget. The coordinator confirms the three things every campaign needs — a metric and how to measure it, a target or direction, and a budget — then posts the campaign brief as the first threaded reply.
  2. Rounds fan out. Each round the coordinator proposes 2–4 hypotheses and spawns one experimenter session per hypothesis with the auto.sessions.spawn tool. The spawn message is the experiment brief: the exact variant, the measurement protocol, the baseline, and where to report.
  3. Experimenters measure honestly. Each experimenter gets its own sandbox and checkout, measures the baseline with the brief’s exact protocol, implements the one variant, measures again identically, and reports the verdict — confirmed, refuted, or inconclusive — back to the coordinator’s session with auto.sessions.message. It never opens PRs during the loop.
  4. The heartbeat advances the loop. Every ten minutes a cron trigger wakes the coordinator to sweep the fleet: nudge quiet experiments, respawn dead ones once, close out rounds whose results are all in, and dispatch the next round.
  5. The thread is the lab log. After each round the coordinator posts a structured update — every hypothesis with its measured effect, the running best configuration, budget consumed, the next round’s plan. Anyone reading the thread knows exactly where the search stands, and a fresh coordinator session can rebuild the whole campaign state from it.
  6. Convergence hands off to a human. On completion the coordinator posts a final summary; only after a human approves in the thread does it dispatch one last experimenter to turn the winning variant into a real PR.

The configuration

Three files: a shared runtime fragment and the two agents.
.auto/fragments/environments/agent-runtime.yaml
.auto/agents/research-coordinator.yaml
.auto/agents/experimenter.yaml
The example assumes a GitHub connection for the githubApp mounts and a Slack connection named slack.

Walkthrough

The coordinator is a one-slot agent

concurrency: 1 gives the coordinator a single concurrency slot: at most one live session, and deliver triggers with no routeBy resolve to whoever holds it. The three triggers differ only in what happens when the slot is empty: This shape — deliver-to-slot with a spawning mention — is the standard shape for long-horizon orchestrators. Every mention, reply, and tick lands in one session that can hold several campaigns at once, keyed by their Slack threads. Omitting routeBy on a deliver trigger is only legal for concurrency: 1 agents (or when a sibling onUnmatched: spawn trigger claims the slot); see runtime controls.

Fan-out is a tool call, not a trigger

The experimenter has exactly one trigger — a courtesy Slack mention handler — because experimenters are not event-driven. The coordinator creates them directly with the auto.sessions.spawn tool, one per hypothesis, and the spawn message is the entire contract: variant, protocol, baseline, reporting address. Two details make the fan-out robust:
  • Idempotency keys. The prompt derives idempotencyKey from campaign thread + round + hypothesis slug. auto.sessions.spawn returns the existing session instead of creating a duplicate when the same key is replayed — so a heartbeat that re-runs dispatch logic after a crash cannot double-spawn an experiment.
  • Reports flow upward, not sideways. Experimenters report to the coordinator’s session id with auto.sessions.message, which injects the report into the coordinator’s transcript as a message. Experimenters never post to Slack; the coordinator is the single voice of the campaign.
The coordinator supervises with the read-only introspection tools — auto.sessions.list to sweep the fleet, and the auto.sessions.* family to inspect a suspicious session’s conversation and tool calls. See auto tools.

The heartbeat is the control loop

The kind: heartbeat trigger creates one cron schedule (10-minute cadence, timezone defaults to UTC) whose ticks deliver into the slot session with a standing instruction: sweep, nudge, respawn once, close rounds, or do nothing. The tick payload carries {{heartbeat.scheduledAt}} so the log line is self-dating. Because the routing is deliver + onUnmatched: drop, the schedule is inert while no campaign session is live — cron advances campaigns, it never starts them. See cron and webhooks.

State lives in the thread, not the session

The lab log convention makes the campaign crash-proof. Everything that matters — the brief, per-round results, the running best — is posted to the campaign thread, and the initialPrompt tells any fresh session to rebuild from chat.history plus auto.sessions.list before acting. If the coordinator session dies mid-campaign, the next mention spawns a replacement that reads the thread and carries on. Durable state in an external, human-readable artifact beats state in a session’s memory.

Mount asymmetry encodes the division of labor

Both agents mount the same repository with different githubApp capabilities: The coordinator cannot push code no matter what its prompt says — capability boundaries hold even when instructions drift. Its mount also sets depth: 1 for a fast shallow clone, since it only reads. The experimenter can write, but its GitHub tool list is pared to pull_request_read and create_pull_request, and its prompt reserves PR creation for the explicit post-approval instruction.

Reference-only example

Research Coordinator and Experimenter are retired from the managed roster. This page remains a worked optimization-loop reference; there is no current managed entrypoint to import.

Adapt it

  • Replace acme/widgets and slack.
  • The measurement protocol in the experimenter’s brief is where rigor lives: the exact command, warmup, iteration count, and what counts as noise. A loop is only as good as its measurements.
  • The heartbeat cadence bounds how fast rounds close. Ten minutes suits experiments that finish in minutes; use hourly for long benchmarks.
  • Add spend caps (spendCaps.maxPerSessionUsd on the experimenter, dailyUsd on the coordinator) before pointing this at expensive campaigns — the fleet multiplies whatever one session costs.
  • The skeleton is not code-specific: swap the mount and the measurement protocol to run the same loop over eval suites, prompt variants, or document corpora.

Try it

Merge the PR and let GitHub Sync apply the resources, then mention the coordinator with a toy campaign:
Confirm the campaign brief posts in the thread, two experimenter sessions appear in the web app, results arrive back, and the round summary lands in the thread. The coordinator session’s transcript shows the experimenter reports being injected as messages.