Aller au contenu
English

Vérifier le travail et réessayer

Utilisez les tests ou une revue pour accepter le travail d’un agent ou demander une nouvelle tentative.

Définissez une tentative et une vérification avec defineLoopTask(). La vérification accepte le résultat ou renvoie des indications pour la tentative suivante. Fixez maxRounds pour arrêter la boucle si aucun résultat ne passe.

import { reportValue } from "./reporter.ts";
import { defineLoopTask, defineWorkflow } from "@elie-laloum/outpost";

const fix = defineLoopTask({
  key: "fix",
  maxRounds: 3,
  attempt: (context, feedback) => ({
    round: context.round,
    feedback: feedback ?? "",
  }),
  check: (_, candidate) =>
    candidate.round === 2
      ? { done: true }
      : { done: false, feedback: "Cover the missing edge case." },
});

const result = await defineWorkflow("verified", [fix]).start();
result.unwrap();
reportValue(result.value(fix));
// Example output: { round: 2, feedback: 'Cover the missing edge case.' }
Glissez pour vous déplacer · Ctrl + molette pour zoomer
100 %
  • EssaiVotre attempt(context, feedback) renvoie un candidat.
    1. Premier tourLa fonction de rappel reçoit undefined comme retour de la vérification.
    2. Tours suivantsLa fonction de rappel reçoit le texte du dernier refus.
    (Étapes)
    • → Vérification : puis
  • VérificationVotre check(context, candidate) renvoie un verdict.
    1. AccepterRenvoyer { done: true }.
    2. RefuserRenvoyer { done: false, feedback } avec un texte.
    (Étapes)
    • → Suite : puis
  • SuiteLe verdict décide.
    1. Tour suivantUn refus alimente l’essai suivant.
    2. TerminéLe candidat accepté devient la valeur de la tâche.
    3. ÉpuiséUn refus au dernier tour fait échouer la tâche. LoopTaskExhausted
    (Étapes)

La première tentative est refusée. La deuxième reçoit les indications de la vérification et produit un résultat accepté. Les options after, condition et cache fonctionnent comme pour les autres tâches.

L’agent travaille dans attempt, les tests tournent dans check, tous deux dans une même sandbox chaude.

Séparez la sandbox, la tentative et la vérification pour faciliter la lecture de chaque étape.

import { createSandbox } from "@elie-laloum/outpost";
import { repository, sandboxProvider, coder } from "./outpost.config.ts";

export function openLoopSandbox() {
  return createSandbox({
    repository,
    sandboxProvider,
    agent: coder,
    branch: { mode: "named", name: "outpost/fix-tests" },
  });
}
import type { Sandbox } from "@elie-laloum/outpost";
import { defineAgentTask } from "@elie-laloum/outpost";

export function codingTask(sandbox: Sandbox, feedback?: string) {
  return defineAgentTask({
    key: "coder",
    sandbox,
    request: () => ({
      brief: { text: `Fix the failing tests and commit.\n${feedback ?? ""}` },
    }),
  });
}
import type { Sandbox, LoopTaskContext } from "@elie-laloum/outpost";
import { codingTask } from "./coding-task.ts";

export function createAttempt(sandbox: Sandbox) {
  return async (context: LoopTaskContext, feedback: string | undefined) => {
    const result = await codingTask(sandbox, feedback).perform(context);
    return { summary: result.text, commits: result.commits.length };
  };
}
import type {
  Sandbox,
  LoopTaskContext,
  LoopCheckResult,
} from "@elie-laloum/outpost";

export function createCheck(sandbox: Sandbox) {
  return async (context: LoopTaskContext): Promise<LoopCheckResult> => {
    const tests = await sandbox.command({
      executable: "npm",
      arguments: ["test"],
      signal: context.signal,
    });
    return tests.status === 0
      ? { done: true }
      : { done: false, feedback: `${tests.stdout}\n${tests.stderr}` };
  };
}
import type { Sandbox } from "@elie-laloum/outpost";
import { defineLoopTask } from "@elie-laloum/outpost";
import { createAttempt } from "./loop-attempt.ts";
import { createCheck } from "./loop-check.ts";

export function defineFix(sandbox: Sandbox) {
  return defineLoopTask({
    key: "fix-tests",
    maxRounds: 4,
    attempt: createAttempt(sandbox),
    check: createCheck(sandbox),
  });
}

Lancez run-loop.ts : il garde la sandbox ouverte jusqu’à la fin du workflow.

import { reportValue } from "./reporter.ts";
import { openLoopSandbox } from "./loop-sandbox.ts";
import { defineFix } from "./fix-loop.ts";
import { defineWorkflow } from "@elie-laloum/outpost";

await using sandbox = await openLoopSandbox();
export const fix = defineFix(sandbox);
export const result = await defineWorkflow("fix-tests", [fix]).start();
result.unwrap();
reportValue(result.value(fix).summary);
// Example output: Fixed the parser and verified the tests.

coding.perform(context) rattache les tokens et l’annulation de l’agent à la boucle et à son budget. coder sert à exécuter l’appel : seul fix entre dans le workflow. attempt renvoie du JSON, car un checkpoint sauvegarde chaque candidat, même refusé.

Un npm test en échec renvoie un status non nul, et sa sortie devient le retour de la vérification. À vous de choisir où l’envoyer : dans le brief suivant, comme ici, ou dans une conversation poursuivie.

check peut lancer un relecteur et transformer sa réponse typée en verdict. Les deux agents comptent dans le même budget.

import {
  createAgent,
  createClaudeHarness,
  defineJsonResponse,
} from "@elie-laloum/outpost";
import { z } from "zod";

export const reviewer = createAgent({
  harness: createClaudeHarness({ authentication: "account" }),
});
export const review = defineJsonResponse({
  tag: "review",
  schema: z.object({ approved: z.boolean(), feedback: z.string() }),
});
export const brief = {
  text: 'Review the last commit. End with <review>{"approved": false, "feedback": "What to change"}</review>.',
};
import type { Sandbox } from "@elie-laloum/outpost";
import { defineAgentTask } from "@elie-laloum/outpost";
import { reviewer, review, brief } from "./review-settings.ts";

export declare const sandbox: Sandbox;
export function reviewingTask() {
  return defineAgentTask({
    key: "reviewer",
    sandbox,
    request: () => ({ agent: reviewer, response: review, brief }),
  });
}
import type { LoopTaskContext, LoopCheckResult } from "@elie-laloum/outpost";
import { reviewingTask } from "./reviewing-task.ts";

export async function reviewChanges(
  context: LoopTaskContext,
): Promise<LoopCheckResult> {
  const { value } = await reviewingTask().perform(context);
  return value.approved
    ? { done: true }
    : { done: false, feedback: value.feedback };
}

Passez-la comme check: reviewChanges. L’agent de la requête remplace celui de la sandbox pour ce seul dispatch.

Limite ou événementCe qui se passe
maxRoundsPlafonne les tours, reprises comprises.
Tentatives du workflowChaque tour, et chaque rejeu d’un tour interrompu, compte dans budget.attempts.
timeoutMsBorne chaque tour. Il arrête les fonctions de rappel qui respectent context.signal.
Une exceptionFait échouer la tâche aussitôt. Une tâche en boucle n’a pas d’option retry.
La dernière vérification refuseLa tâche échoue avec LoopTaskExhausted, qui porte le dernier feedback. Les dépendants ne s’exécutent pas.

Réparer une CI en échec traite chaque issue dans un script complet.

Avec un checkpoint, chaque candidat est sauvegardé avant check. À la reprise, un candidat sauvegardé passe directement à check, et les tours refusés ne sont pas rejoués.

Un tour interrompu n’est rejoué qu’avec resume: "retry-incomplete", car il a peut-être déjà modifié des fichiers. Modifier maxRounds rend le checkpoint incompatible.

Chaque enregistrement de tâche liste ses rounds, et chaque phase émet un événement de workflow loop. context.idempotencyKey change à chaque tour et à chaque phase, pour dédupliquer les effets comme dans Files de jobs et workers.

API : defineLoopTask · LoopTaskOptions · LoopTaskContext · LoopCheckResult · LoopTaskExhausted · defineAgentTask.