Pular para o conteúdo
astorlm
Idioma: Português
← Mapa

Nível 16

Agentes proativos

Todos os agentes até aqui esperavam alguém digitar. Um proativo acorda com um timer, dá uma olhada em volta e só fala quando há algo que valha a pena dizer.
1/32 Dobras do bandoneón:
  • user
  • assistant
  • tool_result
A Milonga nasceu hoje de manhã, e a dona dela precisa ir para a escola. Astor cuida dela, com o Oráculo de plantão. Ninguém vai digitar nada: um heartbeat dá uma olhada nela a cada duas horas.

EventBus

O problema

Alguns trabalhos não têm um momento em que uma pessoa pensaria em perguntar: um bichinho que fica com fome enquanto a dona está na escola, um pedido que trava, um servidor que começa a falhar de madrugada. Um agente que só responde quando falam com ele não serve para isso.

O conserto óbvio é um timer que rode o agente de tempos em tempos. Feito sem cuidado, esse é o Cuco Desvairado: cada tick acorda o modelo, cada execução custa tokens e cada execução te manda “tudo certo”. Na terceira mensagem você para de ler, e a que importava fica sem leitura.

A solução

Um heartbeat: um timer que entrega ao agente um prompt fixo, o checkPrompt, como se alguém o tivesse digitado. O que o torna útil em vez de barulhento é o que você coloca em volta desse timer:

  • Conferir antes de acordar

    localCondition

    Código comum que roda a cada tick, antes do modelo: ler um medidor, um arquivo, uma linha. Enquanto ele disser não, o tick não custa nada.

  • Uma execução por vez

    embutido

    Um tick que dispara enquanto a execução anterior ainda está rodando é descartado, não enfileirado. Duas execuções nunca dividem o histórico ao mesmo tempo.

  • Fusíveis

    maxTicks, timeoutMs, runTimeoutMs

    Um orçamento de ticks, de tempo de relógio e de tempo por execução, para que ele termine mesmo que você esqueça de pará-lo.

  • Falar uma vez, e desligar

    stopHeartbeat()

    Mande mensagem para a pessoa só quando algo aconteceu, dizendo como terminou. Quando o trabalho acabar, desligue o heartbeat.

O primeiro faz a maior parte do trabalho. A maioria dos ticks não encontra nada para fazer, e decidir isso não exige um modelo: exige um if. Na animação, passam cinco ticks e só um deles chama o modelo.

O elenco

O mesmo elenco de sempre, dentro de um bichinho de bolso.

O relógio o heartbeat
O sino dele toca a cada duas horas, e fica aceso enquanto o heartbeat está ligado.
O colchete localCondition
Pisca em volta dos corações a cada tick: o seu próprio código lendo os medidores. Nenhum modelo envolvido.
Astor o loop
Cochila no tapetinho até um tick dizer que um medidor está baixo, e aí roda o loop como sempre.
O Oráculo o modelo
Dormindo até Astor levar o checkPrompt até ele.
Os ícones as ferramentas
Status, comida, jogo e a luz de chamada: check_status, feed, play e beep_owner.
A dona a pessoa
Na escola o dia inteiro. Recebe um único bip, e ele já é uma boa notícia.

No painel EventBus, os ticks tranquilos são só o seu código: o astorlm não emite nada para um tick que a sua checagem recusou. heartbeat_tick aparece uma única vez, quando o modelo é de fato acordado.

O código

Com astorlm: passe heartbeat para o agente e ele começa sozinho. As proteções são opções: localCondition, maxTicks e timeoutMs; ticks sobrepostos são descartados para você. O seu app chama stopHeartbeat() quando a dona volta.

Do zero: um timer em volta do loop do nível 2. Uma flag impede que as execuções se sobreponham, um contador e um prazo são os fusíveis, e uma função comum decide se o modelo vai ser chamado ou não.

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

O que observar

  • O heartbeat do astorlm mantém uma única sessão. Cada tick que acorda o modelo soma ao mesmo histórico, então um heartbeat que o acorda com frequência faz o contexto dele, e a fatura, crescerem a cada execução. Mantenha o checkPrompt curto, adicione compactação ou comece cada execução com um agente novo (as versões do zero acima fazem isso).
  • Atenção ao timeout por execução. runTimeoutMs vale 60 segundos por padrão. Uma execução cujas ferramentas esperam algo lento precisa de mais, ou vai ser abortada no meio.
  • Um heartbeat não é um cron job. Ele vive no seu processo: se o processo para, o heartbeat também, e os ticks que ele perdeu não voltam. Para trabalhos que precisam sobreviver a reinícios, deixe um agendador de verdade iniciar o agente, e mantenha as mesmas proteções.
  • Decida o que merece uma mensagem antes de escrever o prompt. “Me avise se precisou fazer algo”, e não “me conte como está indo”. Uma mensagem que não diz nada ensina a pessoa a ignorar a próxima.
  • Agir sozinho também precisa de limites. Ninguém está olhando. Alimentar o bichinho tudo bem; qualquer coisa que não dê para desfazer deveria esperar o sim de uma pessoa.