Aller au contenu
English

Exécuter des candidats concurrents

Essayez plusieurs candidats avec une validation, des budgets et une intégration explicites.

Utilisez speculate() pour lancer plusieurs candidats sur une même tâche et valider explicitement chaque résultat. Les candidats ont des branches et des sandboxes séparées ; un gagnant est choisi uniquement si votre vérification l’accepte.

Référence API : SpeculationOptions.

Pour un scénario complet opposant Codex à Claude Code, voir la recette Mettre des agents en concurrence.

import { coder } from "./outpost.config.ts";

export const candidates = ["minimal", "refactor"].map((key) => ({
  key,
  agent: coder,
  request: {
    brief: { text: `Fix the parser with a ${key} change. Test and commit.` },
  },
}));
import type { SpeculationOptions } from "@elie-laloum/outpost";

export const validate: SpeculationOptions["validate"] = async ({
  result,
  sandbox,
  signal,
}) => {
  if (result.commits.length === 0) return false;
  const tests = await sandbox.command({
    executable: "npm",
    arguments: ["test"],
    signal,
  });
  return tests.status === 0;
};
import { reportValue } from "./reporter.ts";
import { speculate } from "@elie-laloum/outpost";
import { repository, sandboxProvider } from "./outpost.config.ts";
import { candidates } from "./candidates.ts";
import { validate } from "./validate.ts";

export const result = await speculate({
  repository,
  sandboxProvider,
  budget: { attempts: 2, usage: { output: 20_000 } },
  candidates,
  validate,
});
reportValue(result.status, result.winner?.branch);
// Example output: winner outpost/speculation/…/codex

validate reçoit la key du candidat, le result de son dispatch et sa sandbox, encore ouverte. Lancez-y vos tests et renvoyez true seulement s’ils passent : un agent qui affirme avoir réussi ne prouve rien.

Passez signal à chaque commande. Il se déclenche quand un autre candidat gagne ou que la course s’arrête.

Le commit du gagnant est le HEAD lu après le retour de validate. Un commit créé pendant la validation fait partie du gagnant ; les modifications non commitées, non. Un agent de revue lancé dans validate ne doit donc pas commiter : voir Laisser un agent de revue trancher.

Référence API : SpeculationResult.

Examinez les résultats des candidats avant de choisir ce que vous souhaitez conserver.

Référence API : SpeculativeCandidateResult et SpeculationResult.

  • Limite de tentativesChaque démarrage de candidat consomme une des budget.attempts. Une fois atteinte, aucun nouveau candidat ne démarre ; ceux en cours terminent.
  • Limite de tokensUne fois budget.usage atteint, tous les candidats en cours sont annulés.
  • GagnantLes candidats en cours sont annulés et ceux en attente sont ignorés.

Les tokens consommés dans validate, par exemple par un agent de revue, ne comptent pas dans budget. Les candidats en cours peuvent dépasser la limite de tokens avant que leur usage soit remonté. Budgets explique comment les limites sont mesurées.

Chaque sandbox est libérée à la fin de son candidat. Si elle ne se ferme pas dans le délai cleanupMs, le candidat indique cleanup: "pending", avec son resourceId dans une course durable. Une course durable dont un nettoyage reste en attente reste possédée : appelez recoverSpeculation() avant le prochain speculate().

Les branches des candidats restent toujours dans votre dépôt, gagnant comme perdants.

Un worktree n’est supprimé que s’il est propre. Un worktree contenant des fichiers non commités, non suivis ou ignorés, comme node_modules, reste sous .outpost/workspaces, et son chemin figure dans le retainedDirectory du candidat. Le worktree d’un candidat en échec est conservé lui aussi, et les courses durables conservent le worktree de chaque candidat. Rétention et nettoyage montre comment les supprimer.

result.integration indique si le gagnant se fusionne dans le HEAD de votre checkout, calculé avec git merge-tree sans toucher à vos fichiers ni à l’index.

Référence API : SpeculationIntegration.

Votre checkout peut changer après la course. Vérifiez à nouveau juste avant de fusionner :

import { checkSpeculationIntegration } from "@elie-laloum/outpost";
import { repository } from "./outpost.config.ts";

export async function canMerge(branch: string, commit?: string) {
  const check = await checkSpeculationIntegration(repository, branch, commit);
  return check.status === "clean";
}

Passez winner.branch et winner.commit. Une branche déplacée depuis la validation donne blocked. La vérification ne fusionne jamais : lancez git merge vous-même.

Passez durability à speculate(). Tentatives, usage, sorties et ressources allouées sont enregistrés via un transport, et une course terminée est renvoyée sans être relancée.

import { join } from "node:path";
import {
  createLocalTransport,
  type SpeculationDurability,
} from "@elie-laloum/outpost";
import { repository } from "./outpost.config.ts";

export const durability: SpeculationDurability = {
  transporter: createLocalTransport({
    directory: join(repository, ".outpost", "storage"),
  }),
  runId: "parser-race",
  version: "1",
};

Changez version quand vous modifiez les agents ou validate. Une course enregistrée dont les briefs, le budget, le fournisseur ou la version diffèrent est refusée : relancez-la sous un nouveau runId.

Une course durable exige un fournisseur capable de retrouver et d’arrêter ses sandboxes après un arrêt brutal. Docker et Podman dans leur mode monté par défaut en sont capables ; les autres fournisseurs sont refusés, sauf si vous implémentez la récupération.

Une course interrompue par un arrêt brutal reste détenue par son coordinateur, le processus qui a lancé speculate(). Libérez-la avant de la rejouer.

import { reportValue } from "./reporter.ts";
import { createHash } from "node:crypto";
import { join } from "node:path";
import { createLocalTransport, recoverSpeculation } from "@elie-laloum/outpost";
import { repository } from "./outpost.config.ts";
const transporter = createLocalTransport({
  directory: join(repository, ".outpost", "storage"),
});
const key = `speculations/${createHash("sha256").update("parser-race").digest("hex")}.json`;
const saved = await transporter.read(key);
if (saved) {
  reportValue(new TextDecoder().decode(saved.bytes));
  // Example output: {"runId":"parser-race",…}
  await recoverSpeculation({
    transporter,
    runId: "parser-race",
    revision: saved.revision,
    coordinatorStopped: true,
  });
}
Glissez pour vous déplacer · Ctrl + molette pour zoomer
100 %
  • ArrêterTerminer l’ancien coordinateur.
    1. Arrêter le processusUn délai écoulé ou un PID absent ne prouve pas qu’il est arrêté. host
    (Étapes)
    • → Inspecter : puis
  • InspecterLire la course enregistrée.
    1. Lire l’état enregistréGardez sa revision ; le contenu liste le resourceId de chaque candidat. transporter.read()
    (Étapes)
    • → Libérer : puis
  • LibérerAbandonner l’ancienne propriété.
    1. RécupérerÉchoue si la révision a changé depuis votre lecture ; ne supprime rien. recoverSpeculation()
    (Étapes)
    • → Rejouer : puis
  • RejouerRelancer la course.
    1. Autoriser le rejeuMêmes options, avec resume: "retry-incomplete" dans durability. speculate()
    2. RéconcilierLes sandboxes enregistrées sont arrêtées ; les candidats interrompus repartent sur une nouvelle branche. sandbox
    (Étapes)

Un candidat interrompu repart comme une nouvelle tentative, sur …/<key>/2, depuis le commit d’origine. Son ancienne branche et son ancien worktree figurent dans result.previousAttempts. Les candidats validés avant le arrêt brutal gardent leur issue.

Une course durable terminée avec le statut quota n’est pas définitive. Rappeler speculate() avec la même durability relance uniquement les candidats arrêtés par une limite d’usage ou de débit, comme nouvelles tentatives. result.quota.resetAt donne l’heure de réinitialisation quand l’agent la communique ; Pauses sur quota explique comment l’attendre.

  • speculate() ne fusionne jamais, ne pousse rien et n’ouvre aucune pull request.
  • Une intégration clean n’est pas un verrou : toute modification ultérieure de votre checkout la rend obsolète.
  • cleanupMs borne l’attente, pas le fournisseur : une sandbox pending peut encore tourner jusqu’à sa réconciliation.
  • La récupération reprend la course, pas un processus d’agent interrompu. Un candidat rejoué peut répéter des effets externes.
  • Une course durable a besoin de ses worktrees sur disque : un transport distant enregistre l’état, pas le checkout.
  • Les résultats durables doivent contenir des valeurs JSON, et un arrêt brutal en cours d’exécution rend l’usage incomplet : ajoutez budget.attempts aux limites de tokens.

API : speculate · SpeculationOptions · SpeculationResult · SpeculativeCandidateResult · SpeculativeValidation · checkSpeculationIntegration · SpeculationDurability · recoverSpeculation.