Aller au contenu
astorlm
Langue: Français
← Carte

Niveau 10

Des tours tout neufs

Certains travaux sont trop longs pour une seule session. Exécutez-les plutôt en tours : un agent neuf à chaque tour, et la progression notée là où le suivant pourra la trouver.
1/65 Plis du bandonéon :
  • user
  • assistant
  • tool_result
Un canyon large de 36 briques, et une foule qui doit atteindre la sortie. Trop pour l'exécution d'un seul agent : ce travail se fait en tours de 12 briques.

EventBus

Le problème

Certains travaux ne tiennent pas en une exécution : migrer 300 fichiers, traduire tout un catalogue, réparer chaque test en échec d'un dépôt. Chaque étape ajoute un appel d'outil et un résultat à l'historique, et la boucle renvoie tout cela à chaque tour.

C'est Muddle, la session sans fin. L'après-midi, il traîne chaque étape depuis le matin : les requêtes sont énormes, les vieux résultats enterrent les nouveaux, et le modèle se met à refaire un travail déjà fait ou à sauter un travail qu'il avait seulement prévu. Rien ne plante. La qualité s'écoule, tout simplement.

La compaction (niveau 7) ralentit Muddle. Elle ne l'arrête pas : un travail assez long finit par résumer ses propres résumés.

La solution

Ne gardez pas un seul agent en vie pendant tout le travail. Exécutez-le en tours. À chaque tour, votre code lance un nouvel agent avec un historique vide et le même objectif. Il fait une tranche du travail, note où en sont les choses, et se termine. Ensuite, votre code vérifie le travail lui-même et, s'il n'est pas fini, lance le tour suivant.

  • Une seule longue session

    Garder le même agent et le même historique pendant tout le travail.

    Chaque tour renvoie tout depuis le début. Les requêtes s'alourdissent, le modèle a de plus en plus de mal à y trouver ce qui compte, et au-delà de la fenêtre, ça casse.

  • Compacter en route

    La même session, mais en réduisant les vieux messages quand l'historique approche de la limite (niveau 7).

    Ça fait gagner du temps, pas une solution. Chaque compaction perd des détails, et un travail assez long finit par compacter ses propres résumés.

  • Des tours tout neufs

    Découper le travail en tours. Chaque tour est un nouvel agent avec un historique vide. Ce qu'il doit savoir, il le lit dans des fichiers.

    Chaque tour démarre petit et propre. Le prix : chaque tour passe un ou deux échanges à se repérer, et les fichiers doivent dire tout ce qui compte.

L'astuce, c'est que rien d'important ne vit dans l'historique. Le travail est sur le disque (le pont), et une courte note qui dit jusqu'où on est allé aussi (PROGRESS.md). Un nouvel agent n'a pas besoin de se souvenir du tour précédent. Il lui suffit de lire.

On appelle souvent ce pattern la Ralph loop, d'après un one-liner shell qui donnait sans cesse le même prompt à un agent de code. Les agents de code s'en servent pour les longs refactorings, avec l'arbre git et un fichier TODO comme état.

Les personnages

Les mêmes personnages que d'habitude, cette fois dans un canyon.

La trappe votre code
Elle lance un nouvel agent à chaque tour (createIterationAgent) et récupère sa réponse. C'est la boucle autour de la boucle.
L'Astor d'un tour une exécution d'agent
La boucle de l'agent du niveau 2, avec son propre bandonéon. Il commence vide et s'envole quand le tour se termine.
Le pont le travail
Ce que les outils ont modifié sur le disque. Aucun tour ne le jette.
Le panneau PROGRESS.md
Une courte note de chaque tour au suivant : ce qui est fait, ce qui vient ensuite.
DONE? isDone
Votre vérification, entre les tours. Elle mesure le pont, pas ce que le modèle en dit.
LAP 3/5 maxIterations
Le fusible. Si le travail ne passe jamais la vérification, la boucle s'arrête quand même.

Regardez les deux barres du haut. Ce tour, c'est ce que pèse vraiment chaque requête, et elle repart de zéro à chaque tour. 1 session, c'est ce que pèseraient les mêmes requêtes si un seul agent avait fait les trois tours : elle ne redescend jamais.

Le code

Avec astorlm : runGoalLoop prend une fabrique qui renvoie un nouvel agent, votre vérification isDone et un fusible maxIterations. Les outils écrivent dans des fichiers, donc chaque tour retrouve le travail là où le précédent l'a laissé.

À partir de zéro : La boucle du niveau 2, appelée dans un for. L'historique est une variable locale de chaque appel, donc chaque tour démarre vide sans effort.

import { OpenAIProvider, createLocalAgent, runGoalLoop, tool } from 'astorlm'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { z } from 'zod'

// Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
const LLM = { baseURL: 'http://localhost:11434/v1', apiKey: 'YOUR_API_KEY' } // local servers usually ignore the key

// The state lives on disk, not in any history: the bridge, and a progress note.
const GAP = 36
const bridgeLength = (): number => (existsSync('bridge.json') ? JSON.parse(readFileSync('bridge.json', 'utf8')).length : 0)

const readProgress = tool({
  name: 'read_progress',
  description: 'Read PROGRESS.md: what earlier laps built, and where to start.',
  schema: z.object({}),
  execute: async () => (existsSync('PROGRESS.md') ? readFileSync('PROGRESS.md', 'utf8') : 'Nothing built yet.'),
})

const layBricks = tool({
  name: 'lay_bricks',
  description: 'Lay up to 12 bricks of the bridge, starting at brick number "from".',
  schema: z.object({ from: z.number().int().min(1), count: z.number().int().min(1).max(12) }),
  execute: async ({ from, count }) => {
    const to = Math.min(from + count - 1, GAP)
    writeFileSync('bridge.json', JSON.stringify({ length: Math.max(bridgeLength(), to) }))
    return `Laid bricks ${from}-${to}. The bridge is ${bridgeLength()} bricks long.`
  },
})

const writeProgress = tool({
  name: 'write_progress',
  description: 'Overwrite PROGRESS.md with where the bridge stands now, for whoever comes next.',
  schema: z.object({ text: z.string() }),
  execute: async ({ text }) => {
    writeFileSync('PROGRESS.md', `# Progress\n${text}\n`)
    return 'Saved PROGRESS.md.'
  },
})

const result = await runGoalLoop({
  goal: 'Build the bridge to the exit: 36 bricks. Read PROGRESS.md first, lay at most 12 bricks, then update PROGRESS.md.',
  // A NEW agent every lap: empty history, fresh context window. Same tools, same folder.
  createIterationAgent: () =>
    createLocalAgent({
      provider: new OpenAIProvider({ ...LLM, model: 'your-model' }), // e.g. 'llama3.1', 'gpt-4o-mini'
      tools: [readProgress, layBricks, writeProgress],
      maxTurns: 8,
    }),
  // Your code decides when the job is done, by checking the work itself. Not the model's word.
  isDone: () => bridgeLength() >= GAP,
  onIteration: ({ iteration, lastText }) => console.log(`lap ${iteration}: ${lastText}`),
  maxIterations: 5, // the fuse: a goal that never checks out can't run forever
})

console.log(result) // { iterations: 3, done: true, stopReason: 'done', lastText: '…' }

Points de vigilance

  • Vérifiez le travail, pas la réponse. Un « Fini ! » du modèle ne prouve rien. isDone doit regarder le résultat lui-même : lancer les tests, compter les lignes, mesurer le pont. Gardez-le peu coûteux et déterministe, parce qu'il s'exécute après chaque tour.
  • Posez toujours le fusible. Une vérification qui ne peut jamais passer, ou un agent qui défait sans cesse son propre travail, tourne en boucle jusqu'à ce que votre facture l'arrête. maxIterations, et un œil sur la raison pour laquelle il s'est épuisé.
  • Le fichier de progression est le seul passage de relais. Ce qu'il omet, le tour suivant ne le sait pas. Dites à l'agent exactement quoi y écrire : ce qui est fait, ce qui suit, ce qu'il a essayé sans succès.
  • Faites en sorte que chaque étape puisse être rejouée sans risque. Un tour peut mourir à mi-chemin, après le travail mais avant la note. Le tour suivant refera cette tranche, donc la faire deux fois ne doit rien casser.
  • Gardez des tranches petites. Un tour devrait tenir dans une courte exécution. Si une seule tranche a déjà besoin de compaction, les tranches sont trop grosses.