Aller au contenu
astorlm
Langue: Français
← Carte

Niveau 16

Les agents proactifs

Tous les agents jusqu'ici attendaient que quelqu'un tape. Un agent proactif se réveille avec un minuteur, regarde autour de lui, et ne prend la parole que quand il y a quelque chose qui vaut la peine d'être dit.
1/32 Plis du bandonéon :
  • user
  • assistant
  • tool_result
Milonga a éclos ce matin, et sa propriétaire doit aller à l'école. Astor s'occupe d'elle, avec l'Oracle de garde. Personne ne va rien taper : un heartbeat passe la voir toutes les deux heures.

EventBus

Le problème

Certains travaux n'ont aucun moment où une personne penserait à demander : un animal qui a faim pendant que sa propriétaire est à l'école, une commande qui reste bloquée, un serveur qui se met à flancher la nuit. Un agent qui ne répond que quand on lui parle ne sert à rien dans ces cas-là.

La solution évidente, c'est un minuteur qui lance l'agent de temps en temps. Fait sans soin, c'est le Coucou Détraqué : chaque tick réveille le modèle, chaque exécution coûte des tokens, et chaque exécution vous envoie « tout va bien ». Au troisième message, vous arrêtez de les lire, et celui qui comptait reste sans lecteur.

La solution

Un heartbeat : un minuteur qui donne à l'agent un prompt fixe, le checkPrompt, comme si quelqu'un l'avait tapé. Ce qui le rend utile plutôt que bruyant, c'est ce que vous mettez autour de ce minuteur :

  • Vérifier avant de réveiller

    localCondition

    Du code ordinaire qui s'exécute à chaque tick, avant le modèle : lire une jauge, un fichier, une ligne. Tant qu'il dit non, le tick ne coûte rien.

  • Une exécution à la fois

    intégré

    Un tick qui se déclenche pendant que l'exécution précédente tourne encore est abandonné, pas mis en file. Deux exécutions ne partagent jamais l'historique en même temps.

  • Des fusibles

    maxTicks, timeoutMs, runTimeoutMs

    Un budget de ticks, de temps réel et de temps par exécution, pour qu'il s'arrête même si vous oubliez de le couper.

  • Parler une fois, et s’éteindre

    stopHeartbeat()

    N'envoyer un message à la personne que quand quelque chose s'est passé, avec la façon dont ça s'est terminé. Quand le travail est fini, couper le heartbeat.

Le premier fait l'essentiel du travail. La plupart des ticks ne trouvent rien à faire, et décider cela ne demande pas de modèle : ça demande un if. Dans l'animation, cinq ticks passent et un seul d'entre eux appelle le modèle.

Les personnages

Les mêmes personnages que d'habitude, dans un animal de poche.

L'horloge le heartbeat
Sa cloche sonne toutes les deux heures, et reste allumée tant que le heartbeat est actif.
Le crochet localCondition
Il clignote autour des cœurs à chaque tick : votre propre code qui lit les jauges. Aucun modèle là-dedans.
Astor la boucle
Il fait la sieste sur son tapis jusqu'à ce qu'un tick dise qu'une jauge est basse, puis fait tourner la boucle comme d'habitude.
L'Oracle le modèle
Endormi jusqu'à ce qu'Astor lui apporte le checkPrompt.
Les icônes les outils
Statut, nourriture, jeu et la lumière d'appel : check_status, feed, play et beep_owner.
La propriétaire la personne
À l'école toute la journée. Elle reçoit un seul bip, et c'est déjà une bonne nouvelle.

Dans le panneau EventBus, les ticks calmes ne sont que votre code : astorlm n'émet rien pour un tick que votre vérification a refusé. heartbeat_tick n'apparaît qu'une fois, quand le modèle est vraiment réveillé.

Le code

Avec astorlm : passez heartbeat à l'agent et il démarre tout seul. Les protections sont des options : localCondition, maxTicks et timeoutMs ; les ticks qui se chevauchent sont abandonnés pour vous. Votre app appelle stopHeartbeat() quand la propriétaire revient.

À partir de zéro : un minuteur autour de la boucle du niveau 2. Un drapeau empêche les exécutions de se chevaucher, un compteur et une échéance servent de fusibles, et une simple fonction décide si le modèle est appelé ou non.

import { OpenAIProvider, createLocalAgent, tool } from 'astorlm'
import { z } from 'zod'

const HOUR = 60 * 60_000

const checkStatus = tool({
  name: 'check_status',
  description: 'Open the status screen: hunger and happiness in hearts, and whether the pet is sick or asleep.',
  schema: z.object({}),
  execute: async () => pet.status(), // your code: the pet lives in your app
})

const feed = tool({
  name: 'feed',
  description: 'Feed the pet a meal or a snack. A meal fills hunger; a snack only cheers it up.',
  schema: z.object({ food: z.enum(['meal', 'snack']) }),
  execute: async ({ food }) => pet.feed(food),
})

const play = tool({
  name: 'play',
  description: 'Play the left-or-right game with the pet. Winning fills happiness.',
  schema: z.object({}),
  execute: async () => pet.play(),
})

const beepOwner = tool({
  name: 'beep_owner',
  description: 'Beep the owner with a short message. They are at school: only when something happened.',
  schema: z.object({ text: z.string() }),
  execute: async ({ text }) => {
    await sendPush(text) // your code
    return 'Beeped.'
  },
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  tools: [checkStatus, feed, play, beepOwner],
  maxTurns: 8,
  // Starts on its own as soon as the agent is created. Nobody types anything.
  heartbeat: {
    intervalMs: 2 * HOUR,
    checkPrompt: 'Check on Milonga. Take care of whatever is low, then beep her owner with one line.',
    // Runs on every tick, before the model. While it says no, a tick costs 0 tokens.
    localCondition: () => pet.hunger <= 1 || pet.happy <= 1,
    maxTicks: 6, // the fuses: a school day of ticks at most…
    timeoutMs: 10 * HOUR, // …and of wall-clock time
  },
})

// The owner is home: the app takes over and switches the heartbeat off.
onOwnerHome(() => agent.stopHeartbeat())

// Quiet ticks emit nothing. The ones that wake the model do:
agent.on('event', (event) => {
  if (event.type === 'heartbeat_tick') console.log('heartbeat woke the agent')
})

Points de vigilance

  • Le heartbeat d'astorlm garde une seule session. Chaque tick qui réveille le modèle s'ajoute au même historique, donc un heartbeat qui le réveille souvent fait grossir son contexte, et sa facture, à chaque exécution. Gardez le checkPrompt court, ajoutez de la compaction, ou démarrez chaque exécution sur un agent neuf (c'est ce que font les versions à partir de zéro ci-dessus).
  • Attention au timeout par exécution. runTimeoutMs vaut 60 secondes par défaut. Une exécution dont les outils attendent quelque chose de lent a besoin de plus, sinon elle sera interrompue à mi-chemin.
  • Un heartbeat n'est pas un cron. Il vit dans votre processus : si le processus s'arrête, le heartbeat aussi, et les ticks manqués sont perdus. Pour les travaux qui doivent survivre aux redémarrages, laissez un vrai ordonnanceur lancer l'agent, et gardez les mêmes protections.
  • Décidez de ce qui mérite un message avant d'écrire le prompt. « Préviens-moi si tu as dû faire quelque chose », pas « dis-moi comment ça se passe ». Un message qui ne dit rien apprend à la personne à ignorer le suivant.
  • Agir seul demande aussi des limites. Personne ne regarde. Nourrir l'animal, ça va ; tout ce qui ne peut pas être défait devrait attendre le oui d'une personne.