Skip to main content

Design: Agent Dispatch

Context

Agent dispatch is where Stet's "you read, it writes" promise is kept: a compiled brief (SPEC-0002) becomes a real pull request, produced by an AI agent running locally in a sandbox. It realizes ADR-0003 (pluggable container runtime), ADR-0004 (pluggable harness adapter), and ADR-0005 (declarative YAML workflows), and it closes the loop back to SPEC-0001's Forge (PR creation) and SPEC-0002's Tour rail (narration) and pending-review reply path.

The design centers on three abstractions that must each stay swappable — runtime, harness, workflow — because all three sit on young, fast-moving ecosystems.

Goals / Non-Goals

Goals

  • A ContainerRuntime protocol with Apple container and Docker backends, neither privileged in code.
  • A fresh, isolated sandbox per dispatch: container/VM + worktree + branch-scoped, revocable credential.
  • A Harness protocol with OpenHands, Crush, and Pi adapters, all bring-your-own OpenAI-compatible model.
  • A lightweight local workflow runner executing YAML verb definitions (single- and multi-step).
  • Branch push + PR creation with an attached narration artifact.
  • A readable live transcript and lifecycle state, normalized across harnesses.
  • Per-dispatch cost/usage accounting where available.

Non-Goals

  • Durable/resumable workflow execution (a Temporal-engine strength) — out of scope per ADR-0005; dispatches are re-runnable, not checkpoint-resumable.
  • Remote/cloud execution — the thesis is local (ADR-0003).
  • Authoring verb-pack YAML in-app — v1 ships a curated default pack (SPEC-0002).
  • The markup capture and brief compilation themselves — SPEC-0002.

Decisions

ContainerRuntime

Protocol surface: createSandbox(image, mounts), run(process, stdio), teardown(). The Apple backend embeds the Containerization Swift package in-process (per-container lightweight VM, sub-second start); the Docker backend drives the Docker API/CLI. A RuntimeCapabilities probe detects availability and picks a backend (default: native Apple on capable hardware, else Docker, else a clear error). A base OCI image carries git plus the selected harness's runtime.

Sandbox lifecycle

DispatchSandbox composes a runtime sandbox with an isolated git worktree (created via SwiftGit2 / cloned into the container) and a ScopedCredential limited to the dispatch's branch. Credentials are minted per dispatch from the forge token (SPEC-0001) with the narrowest scope the forge allows, injected into the sandbox as short-lived environment/secret, and revoked on teardown. Concurrent dispatches get fully separate sandboxes.

Harness adapter

Protocol: run(brief step, worktree, model endpoint) -> stream<TranscriptEvent> plus a finalize that yields { branch, commits, narration }. Adapters normalize each harness's invocation and IO:

  • OpenHands — SDK/container image; default harness (most batteries-included).
  • Crush — Go CLI; OpenAI/Anthropic-compatible; MCP + LSP.
  • Pi — BYOK CLI; minimal Read/Write/Edit/Bash core; extensible. Each receives OPENAI_BASE_URL / OPENAI_API_KEY from user config. Transcript events are normalized to a common TranscriptEvent enum so the live view (below) is harness-agnostic.

Workflow runner

A WorkflowRunner loads YAML workflow definitions (shape borrowed from the user's Temporal YAML library — steps, inputs, gating — but executed by a small in-process runner, not Temporal). Each brief verb references a workflow id; the runner sequences steps, each of which renders a templated instruction (from the brief item

  • prior results) and calls the Harness. Validation rejects malformed YAML with a located error. Multi-step example: refactor-ugly → refactor, then add tests, then update docs, gated on tests passing.

PR creation + narration

On success, the runner commits (if the harness hasn't), pushes the branch via the runtime/Forge, and calls Forge.createPullRequest, attaching the Narration artifact (ADR-0008) the harness produced. If the brief targets an existing PR (SPEC-0002 iteration), it commits to that branch and updates the PR instead.

Live transcript + accounting

A DispatchSession exposes lifecycle state (queued → running → openingPR → done | failed) and a stream of normalized transcript events rendered as a narrated feed. Where the model endpoint reports token usage, a UsageMeter accumulates and surfaces per-dispatch cost.

Risks / Trade-offs

  • Two runtimes + three harnesses from day one is real surface area (ADR-0003/ 0004 accepted this for neutrality). Mitigation: keep protocols minimal; add adapters incrementally behind a stable core.
  • Apple Containerization 1.0 edge cases (networking, entitlements) may need workarounds; the Docker backend is the escape hatch.
  • Scoped credentials differ by forge. GitHub and Gitea expose different scoping/expiry; the ScopedCredential abstraction must degrade to the narrowest each forge supports and document the gap.
  • Heterogeneous transcripts. Normalizing three harnesses' output to one event model is ongoing work; unknown events render as raw passthrough.

Migration / Rollout

Order: (1) ContainerRuntime + one backend (Apple on capable dev hardware); (2) DispatchSandbox + worktree + scoped credential; (3) one Harness adapter (OpenHands) end-to-end to a PR; (4) WorkflowRunner single-step, then multi-step; (5) narration attachment + Tour handoff; (6) live transcript + lifecycle; (7) second runtime (Docker); (8) Crush + Pi adapters; (9) cost accounting; (10) follow-up-against-existing-PR path.

Open Questions

  • Base image strategy per harness (prebuilt vs. built on first run) and caching.
  • Exact ScopedCredential capabilities achievable on GitHub vs. Gitea.
  • Shared Brief/Narration schema versioning with SPEC-0002.
  • Concurrency ceiling for simultaneous local dispatches given VM memory footprint.