Run competing candidates
Try several candidates with explicit validation, budgets and integration controls.
Race candidates
Section titled “Race candidates”Use speculate() to run several candidates for the same task and validate each result explicitly. Candidates have separate branches and sandboxes; the race returns a winner only when your validation accepts one.
API reference: SpeculationOptions.
For a complete scenario with Codex against Claude Code, see the recipe Let agents compete.
Validate real behaviour
Section titled “Validate real behaviour”validate receives the candidate’s key, its dispatch result and its still-open sandbox. Run your tests there and return true only when they pass: an agent saying it passed is not evidence.
Pass signal to every command. It fires when another candidate wins or the race stops.
The winner’s commit is HEAD read after validate returns. A commit made during validation becomes part of the winner; uncommitted edits do not. A reviewer agent dispatched in validate must therefore not commit: see Let a review agent decide.
Read the result
Section titled “Read the result”API reference: SpeculationResult.
Inspect the candidate results before choosing what to keep.
API reference: SpeculativeCandidateResult and SpeculationResult.
Budget and cleanup
Section titled “Budget and cleanup”- Attempt limitEach candidate start uses one of
budget.attempts. Once reached, no new candidate starts; running ones finish. - Token limitOnce
budget.usageis reached, every running candidate is cancelled. - WinnerRunning candidates are cancelled and waiting ones are skipped.
Tokens used inside validate, such as a reviewer agent’s, do not count in budget. Running candidates can exceed the token limit before their usage is reported. Budgets explains how limits are measured.
Each sandbox is released when its candidate ends. If it does not close within cleanupMs, the candidate reports cleanup: "pending", with its resourceId in a durable race. A durable race with a pending cleanup stays owned: call recoverSpeculation() before the next speculate().
What is kept
Section titled “What is kept”Candidate branches always stay in your repository, winners and losers alike.
A worktree is removed only when it is clean. One with uncommitted, untracked or ignored files, such as node_modules, stays under .outpost/workspaces, and its path is in the candidate’s retainedDirectory. A failed candidate’s worktree is kept too, and durable races keep every candidate’s worktree. Retention and cleanup shows how to remove them.
Check integration before merging
Section titled “Check integration before merging”result.integration tells you whether the winner merges into your checkout’s HEAD, computed with git merge-tree without touching your files or index.
API reference: SpeculationIntegration.
Your checkout can change after the race. Check again right before you merge:
Pass winner.branch and winner.commit. A branch that moved since validation is blocked. The check never merges: run git merge yourself.
Resume after a crash
Section titled “Resume after a crash”Pass durability to speculate(). Attempts, usage, outputs and allocated resources are saved through a transport, and a finished race is returned without running again.
Change version when you change the agents or validate. A saved race whose briefs, budget, provider or version differ is rejected: start it under a new runId.
Durable races need a provider that can find and stop its sandboxes after a crash. Docker and Podman in their default mounted mode can; other providers are rejected unless you implement recovery.
Recover after a crash
Section titled “Recover after a crash”A crashed race stays owned by its coordinator, the process that ran speculate(). Release it before replaying.
An interrupted candidate runs again as a new attempt, on …/<key>/2, from the original commit. Its earlier branch and worktree are listed in result.previousAttempts. Candidates validated before the crash keep their outcome.
Resume after a quota
Section titled “Resume after a quota”A durable race that ends with status quota is not final. Calling speculate() again with the same durability reruns only the candidates a usage or rate limit stopped, as new attempts. result.quota.resetAt gives the reset time when the agent reports it; Quota pauses covers waiting for it.
Limits
Section titled “Limits”speculate()never merges, pushes or opens a pull request.- A
cleanintegration is not a lock: any later change to your checkout makes it stale. cleanupMsbounds the wait, not the provider: apendingsandbox may still run until you reconcile it.- Recovery resumes the race, not an interrupted agent process. A replayed candidate can repeat external effects.
- A durable race needs its worktrees on disk: a remote transport saves the state, not the checkout.
- Durable results must hold JSON values, and a crash mid-run makes usage incomplete: add
budget.attemptsnext to token limits.
API: speculate · SpeculationOptions · SpeculationResult · SpeculativeCandidateResult · SpeculativeValidation · checkSpeculationIntegration · SpeculationDurability · recoverSpeculation.