Saltar al contenido
astorlm
Idioma: Español
← Mapa

Nivel 16

Agentes proactivos

Todos los agentes hasta ahora esperaban a que alguien escribiera. Uno proactivo se despierta con un temporizador, mira alrededor y solo habla cuando hay algo que valga la pena decir.
1/32 Pliegues del bandoneón:
  • user
  • assistant
  • tool_result
Milonga nació esta mañana, y su dueña tiene que ir a la escuela. Astor la cuida, con el Oráculo de guardia. Nadie va a escribir nada: un heartbeat la revisa cada dos horas.

EventBus

El problema

Algunos trabajos no tienen un momento en el que a una persona se le ocurriría preguntar: una mascota que tiene hambre mientras su dueña está en la escuela, un pedido que se traba, un servidor que empieza a fallar de noche. Un agente que solo responde cuando le hablan no sirve para eso.

El arreglo obvio es un temporizador que ejecute el agente cada tanto. Hecho sin cuidado, ese es el Cucú Desquiciado: cada tick despierta al modelo, cada ejecución cuesta tokens y cada ejecución te manda “todo bien”. Para el tercer mensaje dejas de leerlos, y el que importaba se queda sin leer.

La solución

Un heartbeat: un temporizador que le pasa al agente un prompt fijo, el checkPrompt, como si alguien lo hubiera escrito. Lo que lo hace útil en lugar de ruidoso es lo que pones alrededor de ese temporizador:

  • Revisar antes de despertar

    localCondition

    Código común que corre en cada tick, antes del modelo: leer un indicador, un archivo, una fila. Mientras diga que no, el tick no cuesta nada.

  • Una ejecución a la vez

    incluido

    Un tick que se dispara mientras la ejecución anterior sigue en curso se descarta, no se encola. Dos ejecuciones nunca comparten el historial al mismo tiempo.

  • Fusibles

    maxTicks, timeoutMs, runTimeoutMs

    Un presupuesto de ticks, de tiempo real y de tiempo por ejecución, para que termine aunque te olvides de detenerlo.

  • Hablar una vez, y apagarse

    stopHeartbeat()

    Mándale un mensaje a la persona solo cuando pasó algo, con cómo terminó. Cuando el trabajo se acaba, apaga el heartbeat.

El primero hace la mayor parte del trabajo. La mayoría de los ticks no encuentran nada que hacer, y decidir eso no requiere un modelo: requiere un if. En la animación pasan cinco ticks y solo uno de ellos llama al modelo.

El elenco

El mismo elenco de siempre, dentro de una mascota de bolsillo.

El reloj el heartbeat
Su campana suena cada dos horas, y queda encendida mientras el heartbeat está activo.
El corchete localCondition
Parpadea alrededor de los corazones en cada tick: tu propio código leyendo los indicadores. Sin modelo de por medio.
Astor el bucle
Duerme la siesta en su alfombrita hasta que un tick dice que un indicador está bajo, y entonces corre el bucle como siempre.
El Oráculo el modelo
Dormido hasta que Astor le lleva el checkPrompt.
Los íconos las herramientas
Estado, comida, juego y la luz de llamada: check_status, feed, play y beep_owner.
La dueña la persona
En la escuela todo el día. Recibe un solo bip, y ya es una buena noticia.

En el panel EventBus, los ticks tranquilos son solo tu código: astorlm no emite nada por un tick que tu verificación rechazó. heartbeat_tick aparece una sola vez, cuando de verdad se despierta al modelo.

El código

Con astorlm: pásale heartbeat al agente y arranca solo. Las protecciones son opciones: localCondition, maxTicks y timeoutMs; los ticks que se superponen se descartan por ti. Tu app llama a stopHeartbeat() cuando la dueña vuelve.

Desde cero: un temporizador alrededor del bucle del nivel 2. Una bandera evita que las ejecuciones se superpongan, un contador y una fecha límite son los fusibles, y una función común decide si siquiera se llama al modelo.

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')
})

Qué vigilar

  • El heartbeat de astorlm mantiene una sola sesión. Cada tick que despierta al modelo suma al mismo historial, así que un heartbeat que lo despierta seguido hace crecer su contexto, y su factura, con cada ejecución. Mantén corto el checkPrompt, agrega compactación o arranca cada ejecución con un agente nuevo (las versiones desde cero de arriba lo hacen).
  • Ojo con el timeout por ejecución. runTimeoutMs vale 60 segundos por defecto. Una ejecución cuyas herramientas esperan algo lento necesita más, o se va a abortar a la mitad.
  • Un heartbeat no es un cron job. Vive en tu proceso: si el proceso se detiene, el heartbeat también, y los ticks que se perdió no vuelven. Para trabajos que tienen que sobrevivir a reinicios, deja que un programador de tareas real arranque el agente, y conserva las mismas protecciones.
  • Decide qué merece un mensaje antes de escribir el prompt. “Avísame si tuviste que hacer algo”, no “cuéntame cómo va”. Un mensaje que no dice nada entrena a la persona a ignorar el siguiente.
  • Actuar solo también necesita límites. Nadie está mirando. Darle de comer a la mascota está bien; todo lo que no se pueda deshacer debería esperar el sí de una persona.