Produzione

Set up Temporal for durable workflows, stream events to UIs, apply adaptive rate limiting, and use system reminders.

Limitazione della velocità del modello

Ogni fornitore di modelli applica limiti di velocità. Se li superate, le vostre richieste falliscono con 429 errori. Peggio ancora: in un’implementazione multi-replica, ogni replica martella l’API in modo indipendente, causando un throttling aggregato invisibile ai singoli processi.

Il problema

Scenario: Si distribuiscono 10 repliche del servizio agente. Ogni replica pensa di avere a disposizione 100K token/minuto. Insieme, inviano 1M di token/minuto, 10 volte la quota effettiva. Il provider applica un throttling aggressivo. Le richieste falliscono in modo casuale su tutte le repliche.

**Senza limitazione della velocità

  • Le richieste falliscono in modo imprevedibile con 429 secondi
  • Nessuna visibilità sulla capacità residua
  • I tentativi peggiorano la congestione
  • L’esperienza dell’utente peggiora sotto carico

Con la limitazione adattiva della velocità:

  • Ogni replica condivide un budget coordinato
  • Le richieste vengono messe in coda finché la capacità non è disponibile
  • Il backoff si propaga in tutto il cluster
  • Degrado graduale invece di guasti

Panoramica

Il pacchetto features/model/middleware fornisce un limitatore di velocità adattativo in stile AIMD che si colloca al confine del client del modello. Stima i costi dei token, blocca i chiamanti finché non è disponibile la capacità e regola automaticamente il suo budget di token al minuto in risposta ai segnali di limitazione della velocità da parte dei provider.

Strategia AIMD

Il limitatore utilizza una strategia Additive Increase / Multiplicative Decrease (AIMD):

EventoAzioneFormula
SuccessoSonda (aumento additivo)TPM += recoveryRate (5% dell’iniziale)
ErrRateLimitedBackoff (diminuzione moltiplicativa)TPM *= 0.5

Il numero effettivo di token al minuto (TPM) è limitato da:

  • Minimo: 10% del TPM iniziale (soglia minima per prevenire l’inedia)
  • Massimo: Il limite massimo configurato maxTPM

Uso di base

Creare un singolo limitatore per processo e avvolgere il client del modello:

import (
    "context"

    "goa.design/goa-ai/features/model/middleware"
    "goa.design/goa-ai/features/model/bedrock"
)

func main() {
    ctx := context.Background()

    // Create the adaptive rate limiter
    // Parameters: context, rmap (nil for local), key, initialTPM, maxTPM
    limiter := middleware.NewAdaptiveRateLimiter(
        ctx,
        nil,     // nil = process-local limiter
        "",      // key (unused when rmap is nil)
        60000,   // initial tokens per minute
        120000,  // maximum tokens per minute
    )

    // Create your underlying model client
    bedrockClient, err := bedrock.NewClient(bedrock.Options{
        Region: "us-east-1",
        Model:  "anthropic.claude-sonnet-4-20250514-v1:0",
    })
    if err != nil {
        panic(err)
    }

    // Wrap with rate limiting middleware
    rateLimitedClient := limiter.Middleware()(bedrockClient)

    // Use rateLimitedClient with your runtime or planners
    rt := runtime.New(
        runtime.WithModelClient("claude", rateLimitedClient),
    )
}

Limitazione della velocità consapevole del cluster

Per le distribuzioni multiprocesso, coordinare la limitazione della velocità tra le istanze usando una mappa replicata di Pulse:

import (
    "context"

    "goa.design/goa-ai/features/model/middleware"
    "goa.design/pulse/rmap"
)

func main() {
    ctx := context.Background()

    // Create a Pulse replicated map backed by Redis
    rm, err := rmap.NewMap(ctx, "rate-limits", rmap.WithRedis(redisClient))
    if err != nil {
        panic(err)
    }

    // Create cluster-aware limiter
    // All processes sharing this map and key coordinate their budgets
    limiter := middleware.NewAdaptiveRateLimiter(
        ctx,
        rm,
        "claude-sonnet",  // shared key for this model
        60000,            // initial TPM
        120000,           // max TPM
    )

    // Wrap your client as before
    rateLimitedClient := limiter.Middleware()(bedrockClient)
}

Quando si usa la limitazione consapevole del cluster:

  • Il backoff si propaga a livello globale: Quando un processo riceve ErrRateLimited, tutti i processi riducono il loro budget
  • Il probing è coordinato: Le richieste riuscite incrementano il budget condiviso
  • Riconciliazione automatica: I processi osservano le modifiche esterne e aggiornano i loro limitatori locali

Stima dei gettoni

Il limitatore stima il costo della richiesta utilizzando una semplice euristica:

  • Conta i caratteri nelle parti di testo e nei risultati degli strumenti di stringa
  • Converte in token utilizzando ~3 caratteri per token
  • Aggiunge un buffer di 500 token per le richieste del sistema e l’overhead del provider

Questa stima è intenzionalmente conservativa per evitare un conteggio insufficiente.

Integrazione con il runtime

Cablare i client a velocità limitata nel runtime di Goa-AI:

// Create limiters for each model you use
claudeLimiter := middleware.NewAdaptiveRateLimiter(ctx, nil, "", 60000, 120000)
gptLimiter := middleware.NewAdaptiveRateLimiter(ctx, nil, "", 90000, 180000)

// Wrap underlying clients
claudeClient := claudeLimiter.Middleware()(bedrockClient)
gptClient := gptLimiter.Middleware()(openaiClient)

// Configure runtime with rate-limited clients
rt := runtime.New(
    runtime.WithEngine(temporalEng),
    runtime.WithModelClient("claude", claudeClient),
    runtime.WithModelClient("gpt-4", gptClient),
)

Cosa succede sotto carico

Livello di trafficoSenza limitatoreCon limitatore
Sotto quotaLe richieste vanno a buon fineLe richieste vanno a buon fine
Alla quota429 fallimenti casualiLe richieste si accodano, poi riescono
Burst sopra la quotaCascata di fallimenti, il provider si bloccaBackoff assorbe il burst, recupero graduale
Sovraccarico prolungatoTutte le richieste fallisconoRichieste in coda con latenza limitata

Parametri di regolazione

ParametroPredefinitoDescrizione
initialTPM(obbligatorio)Budget iniziale di tokens-per-minute
maxTPM(obbligatorio)Massimale per il probing
Floor10% dell’inizialeBudget minimo (previene la fame)
Tasso di recupero5% dell’inizialeIncremento additivo per ogni successo
Fattore di backoff0,5Diminuzione moltiplicativa su 429

Esempio: Con initialTPM=60000, maxTPM=120000:

  • Piano: 6.000 TPM
  • Recupero: +3.000 TPM per lotto riuscito
  • Backoff: dimezzare i TPM correnti su 429

Monitoraggio

Traccia il comportamento del limitatore di velocità con metriche e registri:

// The limiter logs backoff events at WARN level
// Monitor for sustained throttling by tracking:
// - Wait time distribution (how long requests queue)
// - Backoff frequency (how often 429s occur)
// - Current TPM vs. initial TPM

// Example: export current capacity to Prometheus
currentTPM := limiter.CurrentTPM()

Migliori pratiche

  • Un limitatore per modello/provider: Creare limitatori separati per i diversi modelli per isolare i loro budget
  • Impostare un TPM iniziale realistico: Iniziare con il limite di velocità documentato dal provider o con una stima prudente
  • Utilizzare la limitazione consapevole del cluster in produzione: Coordinarsi tra le repliche per evitare il throttling aggregato
  • Monitorare gli eventi di backoff: Registrare o emettere metriche quando si verificano i backoff per rilevare il throttling prolungato
  • Impostare il maxTPM al di sopra di quello iniziale: Lasciare spazio per il probing quando il traffico è al di sotto della quota

Impostazione temporale

Questa sezione tratta l’impostazione di Temporal per i flussi di lavoro degli agenti durevoli negli ambienti di produzione.

Panoramica

Temporal fornisce un’esecuzione duratura per gli agenti Goa-AI. Le esecuzioni degli agenti diventano flussi di lavoro Temporal con cronologia basata sugli eventi. Le chiamate allo strumento diventano attività con tentativi configurabili. Ogni transizione di stato viene conservata. Un worker riavviato riproduce la cronologia e riprende esattamente da dove si era interrotto.

Come funziona la durata

ComponenteRuoloDurabilità
Flusso di lavoroOrchestrazione dell’esecuzione dell’agenteOrigine evento; sopravvive ai riavvii
Attività di pianificazioneChiamata di inferenza LLMRiprova su fallimenti transitori
Execute Tool ActivityInvocazione di uno strumentoPolitiche di ritentamento per strumento
StateCronologia dei turni, risultati degli strumentiPersistenti nella cronologia del flusso di lavoro

Esempio concreto: L’agente chiama un LLM, che restituisce 3 chiamate allo strumento. Due strumenti vengono completati. Il servizio del terzo strumento si blocca.

  • Senza Temporale: L’intera esecuzione fallisce. Si esegue nuovamente l’inferenza ($$$) e si rieseguono i due strumenti che hanno avuto successo.
  • Con Temporale: Si riprova solo lo strumento che si è arrestato. Il flusso di lavoro viene riproposto dalla storia: nessuna nuova chiamata a LLM, nessuna riesecuzione di strumenti completati. Costo: un tentativo, non un riavvio completo.

Cosa sopravvive ai guasti

Scenario di guastoSenza TemporalCon Temporal
Il processo di lavoro si bloccaEsecuzione persa, riavvio da zeroRiproduzione dalla cronologia, continua
La chiamata allo strumento va in time outL’esecuzione fallisce (o gestione manuale)Riprova automatica con backoff
Limite di velocità (429)L’esecuzione non riesceArretra, riprova automaticamente
Partizione di reteAvanzamento parziale persoRiprende dopo la riconnessione
Distribuzione durante l’esecuzioneLe esecuzioni in volo fallisconoI lavoratori si svuotano, i nuovi lavoratori riprendono

Installazione

Opzione 1: Docker (sviluppo)

Una sola riga per lo sviluppo locale:

docker run --rm -d --name temporal-dev -p 7233:7233 temporalio/auto-setup:latest

Opzione 2: Temporalite (Sviluppo)

go install go.temporal.io/server/cmd/temporalite@latest
temporalite start

Opzione 3: Nuvola Temporale (Produzione)

Registrarsi su [temporal.io] (https://temporal.io) e configurare il client con le credenziali del cloud.

Opzione 4: Self-Hosted (Produzione)

Distribuire Temporal utilizzando Docker Compose o Kubernetes. Vedere la Documentazione di Temporal per le guide alla distribuzione.

Configurazione del runtime

Goa-AI astrae il backend di esecuzione dietro l’interfaccia Engine. È possibile cambiare i motori senza modificare il codice dell’agente:

In-Memory Engine (sviluppo):

// Default: no external dependencies
rt := runtime.New()

Motore temporale (produzione):

import (
    runtimeTemporal "goa.design/goa-ai/runtime/agent/engine/temporal"
    "go.temporal.io/sdk/client"
)

temporalEng, err := runtimeTemporal.New(runtimeTemporal.Options{
    ClientOptions: &client.Options{
        HostPort:  "127.0.0.1:7233",
        Namespace: "default",
    },
    WorkerOptions: runtimeTemporal.WorkerOptions{
        TaskQueue: "orchestrator.chat",
    },
})
if err != nil {
    panic(err)
}
defer temporalEng.Close()

rt := runtime.New(runtime.WithEngine(temporalEng))

Configurazione dei ritiri di attività

Le chiamate agli strumenti sono attività temporali. Configurare i ritiri per ogni set di strumenti nel DSL:

Use("external_apis", func() {
    // Flaky external services: retry aggressively
    ActivityOptions(engine.ActivityOptions{
        Timeout: 30 * time.Second,
        RetryPolicy: engine.RetryPolicy{
            MaxAttempts:        5,
            InitialInterval:    time.Second,
            BackoffCoefficient: 2.0,
        },
    })
    
    Tool("fetch_weather", "Get weather data", func() { /* ... */ })
    Tool("query_database", "Query external DB", func() { /* ... */ })
})

Use("local_compute", func() {
    // Fast local tools: minimal retries
    ActivityOptions(engine.ActivityOptions{
        Timeout: 5 * time.Second,
        RetryPolicy: engine.RetryPolicy{
            MaxAttempts: 2,
        },
    })
    
    Tool("calculate", "Pure computation", func() { /* ... */ })
})

Configurazione del lavoratore

I worker eseguono il polling delle code di attività e l’esecuzione di flussi di lavoro/attività. I worker vengono avviati automaticamente per ogni agente registrato; nella maggior parte dei casi non è necessaria una configurazione manuale dei worker.

Migliori pratiche

  • Usare spazi dei nomi separati per i diversi ambienti (dev, staging, prod)
  • Configurare i criteri di riprova per ogni set di strumenti in base alle caratteristiche di affidabilità
  • Monitorare l’esecuzione del flusso di lavoro utilizzando l’interfaccia utente e gli strumenti di osservabilità di Temporal
  • Impostare timeout appropriati per le attività - bilanciare l’affidabilità rispetto al rilevamento delle sospensioni
  • Usare Temporal Cloud per la produzione per evitare oneri operativi

Interfaccia utente di streaming

Questa sezione mostra come trasmettere gli eventi dell’agente alle interfacce utente in tempo reale, utilizzando l’infrastruttura di streaming di Goa-AI.

Panoramica

Goa-AI espone flussi per-run di eventi tipizzati che possono essere forniti alle interfacce utente tramite:

  • Eventi inviati dal server (SSE)
  • WebSocket
  • Bus di messaggi (Pulse, Redis Streams, ecc.)

Ogni esecuzione del flusso di lavoro ha il proprio flusso; quando gli agenti chiamano altri agenti come strumenti, il runtime avvia le esecuzioni figlio e le collega utilizzando eventi AgentRunStarted e handle RunLink. Le interfacce utente possono sottoscrivere qualsiasi esecuzione in base all’ID e scegliere la quantità di dettagli da rendere.

Interfaccia Stream Sink

Implementare l’interfaccia stream.Sink:

type Sink interface {
    Send(ctx context.Context, event stream.Event) error
    Close(ctx context.Context) error
}

Tipi di evento

Il pacchetto stream definisce tipi di eventi concreti che implementano stream.Event. Quelli più comuni per le interfacce utente sono:

Tipo di eventoDescrizione
AssistantReplyAssistente a pezzi di messaggio (testo in streaming)
PlannerThoughtBlocchi di pensiero del pianificatore (note e ragionamenti strutturati)
ToolStartEsecuzione dello strumento avviata
ToolUpdateAvanzamento dell’esecuzione dello strumento (aggiornamenti previsti del conteggio dei figli)
ToolEndEsecuzione dello strumento completata (risultato, errore, telemetria)
AwaitClarificationIl pianificatore è in attesa di chiarimenti umani
AwaitExternalToolsIl pianificatore è in attesa dei risultati dello strumento esterno
UsageUtilizzo di token per invocazione del modello
WorkflowEsecuzione di aggiornamenti del ciclo di vita e delle fasi
AgentRunStartedCollegamento da una chiamata allo strumento padre a un’esecuzione dell’agente figlio

I trasporti di solito commutano di tipo su stream.Event per la sicurezza in fase di compilazione:

switch e := evt.(type) {
case stream.AssistantReply:
    // e.Data.Text
case stream.PlannerThought:
    // e.Data.Note or structured thinking fields
case stream.ToolStart:
    // e.Data.ToolCallID, e.Data.ToolName, e.Data.Payload
case stream.ToolEnd:
    // e.Data.Result, e.Data.Error, e.Data.ResultPreview
case stream.AgentRunStarted:
    // e.Data.ToolName, e.Data.ToolCallID, e.Data.ChildRunID, e.Data.ChildAgentID
}

Esempio: Lavello SSE

type SSESink struct {
    w http.ResponseWriter
}

func (s *SSESink) Send(ctx context.Context, event stream.Event) error {
    switch e := event.(type) {
    case stream.AssistantReply:
        fmt.Fprintf(s.w, "data: assistant: %s\n\n", e.Data.Text)
    case stream.PlannerThought:
        if e.Data.Note != "" {
            fmt.Fprintf(s.w, "data: thinking: %s\n\n", e.Data.Note)
        }
    case stream.ToolStart:
        fmt.Fprintf(s.w, "data: tool_start: %s\n\n", e.Data.ToolName)
    case stream.ToolEnd:
        fmt.Fprintf(s.w, "data: tool_end: %s status=%v\n\n",
            e.Data.ToolName, e.Data.Error == nil)
    case stream.AgentRunStarted:
        fmt.Fprintf(s.w, "data: agent_run_started: %s child=%s\n\n",
            e.Data.ToolName, e.Data.ChildRunID)
    }
    s.w.(http.Flusher).Flush()
    return nil
}

func (s *SSESink) Close(ctx context.Context) error {
    return nil
}

Abbonamento per esecuzione

Abbonarsi agli eventi di una corsa specifica:

sink := &SSESink{w: w}
stop, err := rt.SubscribeRun(ctx, runID, sink)
if err != nil {
    return err
}
defer stop()

Flusso globale

Per eseguire lo streaming di tutte le corse attraverso un sink globale (ad esempio, Pulse), configurare il runtime con uno stream sink:

rt := runtime.New(
    runtime.WithStream(pulseSink), // or your custom sink
)

Il runtime installa un stream.Subscriber predefinito che:

  • mappa gli eventi hook a valori stream.Event
  • utilizza il default StreamProfile, che emette le risposte dell’assistente, i pensieri del pianificatore, l’avvio/aggiornamento/fine dello strumento, le attese, l’utilizzo, il flusso di lavoro e i collegamenti AgentRunStarted, con le esecuzioni figlie mantenute sui propri flussi

Profili dei flussi

Non tutti i consumatori hanno bisogno di tutti gli eventi. I profili di flusso filtrano gli eventi per pubblici diversi, riducendo il rumore e la larghezza di banda per casi d’uso specifici.

ProfiloCaso d’usoEventi inclusi
UserChatProfile()UI della chat dell’utente finaleRisposte dell’assistente, avvio/fine dello strumento, completamento del flusso di lavoro
AgentDebugProfile()Debug dello sviluppatoreTutto, compresi i pensieri del pianificatore
MetricsProfile()Pipeline di osservabilitàSolo eventi di utilizzo e flusso di lavoro

Utilizzo di profili integrati:

// User-facing chat: replies, tool status, completion
profile := stream.UserChatProfile()

// Debug view: everything including planner thoughts
profile := stream.AgentDebugProfile()

// Metrics pipeline: just usage and workflow events
profile := stream.MetricsProfile()

sub, _ := stream.NewSubscriberWithProfile(sink, profile)

Profili personalizzati:

// Fine-grained control over which events to emit
profile := stream.StreamProfile{
    Assistant:  true,
    Thought:    false,  // Skip planner thinking
    ToolStart:  true,
    ToolUpdate: true,
    ToolEnd:    true,
    Usage:      false,  // Skip usage events
    Workflow:   true,
    RunStarted: true,   // Include agent-run-started links
}

sub, _ := stream.NewSubscriberWithProfile(sink, profile)

I profili personalizzati sono utili quando:

  • Si ha bisogno di eventi specifici per un utente specializzato (ad esempio, il monitoraggio dei progressi)
  • Si desidera ridurre la dimensione del payload per i client mobili
  • Si stanno costruendo pipeline di analisi che necessitano solo di determinati eventi

Avanzato: Ponti di impulsi e flussi

Per le configurazioni di produzione, spesso si desidera:

  • pubblicare eventi su un bus condiviso (ad esempio, Pulse)
  • mantenere flussi per-run su quel bus (un argomento/chiave per run)

Goa-AI fornisce:

  • features/stream/pulse - un’implementazione stream.Sink supportata da Pulse
  • runtime/agent/stream/bridge - helper per collegare il bus hook a qualsiasi sink

Cablaggio tipico:

pulseClient := pulse.NewClient(redisClient)
s, err := pulseSink.NewSink(pulseSink.Options{
    Client: pulseClient,
    StreamIDFunc: func(ev stream.Event) (string, error) {
        if ev.RunID() == "" {
            return "", errors.New("missing run id")
        }
        return fmt.Sprintf("run/%s", ev.RunID()), nil
    },
})
if err != nil { log.Fatal(err) }

rt := runtime.New(
    runtime.WithEngine(eng),
    runtime.WithStream(s),
)

Promemoria del sistema

I modelli vanno alla deriva. Dimenticano le istruzioni. Ignorano un contesto che era chiaro 10 turni fa. Quando il vostro agente esegue compiti di lunga durata, avete bisogno di un modo per iniettare guida dinamica e contestuale senza inquinare la conversazione con l’utente.

Il problema

Scenario: Il vostro agente gestisce un elenco di cose da fare. Dopo 20 turni, l’utente chiede “cosa c’è dopo?”, ma il modello si è allontanato e non ricorda che c’è una cosa in sospeso in corso. È necessario dare una spinta senza che l’utente veda un messaggio imbarazzante del tipo “Promemoria: hai un impegno in corso”.

**Senza promemoria di sistema

  • Si gonfia il prompt di sistema con ogni possibile scenario
  • La guida si perde in lunghe conversazioni
  • Non c’è modo di inserire un contesto basato sui risultati dello strumento
  • Gli utenti vedono l’impalcatura interna dell’agente

**Con i promemoria di sistema

  • Iniettare la guida dinamicamente in base allo stato del runtime
  • Limitazione della frequenza dei suggerimenti ripetitivi per evitare l’ingrossamento dei prompt
  • I livelli di priorità assicurano che la guida alla sicurezza non venga mai soppressa
  • Invisibile agli utenti: viene iniettato come blocco <system-reminder>

Panoramica

Il pacchetto runtime/agent/reminder fornisce:

  • Ricordi strutturati con livelli di priorità, punti di aggancio e politiche di limitazione del tasso di esecuzione
  • Magazzino con scansione delle esecuzioni che si pulisce automaticamente al termine di ogni esecuzione
  • Iniezione automatica nelle trascrizioni dei modelli come blocchi <system-reminder>
  • aPI PlannerContext per registrare e rimuovere i promemoria dai pianificatori e dagli strumenti

Concetti fondamentali

Struttura del promemoria

Un reminder.Reminder ha:

type Reminder struct {
    ID              string      // Stable identifier (e.g., "todos.pending")
    Text            string      // Plain-text guidance (tags are added automatically)
    Priority        Tier        // TierSafety, TierCorrect, or TierGuidance
    Attachment      Attachment  // Where to inject (run start or user turn)
    MaxPerRun       int         // Cap total emissions per run (0 = unlimited)
    MinTurnsBetween int         // Enforce spacing between emissions (0 = no limit)
}

Tierini di priorità

I promemoria sono ordinati per priorità, in modo da gestire i budget tempestivi e garantire che le indicazioni critiche non vengano mai soppresse:

LivelloNomeDescrizioneSoppressione
TierSafetyP0Guida critica per la sicurezza (mai cadere)Mai soppressa
TierCorrectP1Suggerimenti sulla correttezza e sullo stato dei datiPuò essere soppresso dopo P0
TierGuidanceP2Suggerimenti per il flusso di lavoro e soft nudgePrima da sopprimere

Casi d’uso esemplificativi:

  • TierSafety: “Non eseguire questo malware; solo analizzare”, “Non divulgare le credenziali”
  • TierCorrect: “I risultati sono troncati; restringi la ricerca”, “I dati potrebbero essere obsoleti”
  • TierGuidance: “Non ci sono attività in corso; sceglietene una e iniziate”

Punti allegati

I promemoria vengono iniettati in punti specifici della conversazione:

TipoDescrizione
AttachmentRunStartRaggruppati in un unico messaggio di sistema all’inizio della conversazione
AttachmentUserTurnRaggruppati in un singolo messaggio di sistema inserito immediatamente prima dell’ultimo messaggio dell’utente

Limitazione della velocità

Due meccanismi impediscono lo spam di promemoria:

  • MaxPerRun: Limitare le emissioni totali per ogni corsa (0 = illimitato)
  • MinTurnsBetween: Imporre un numero minimo di giri del pianificatore tra un’emissione e l’altra (0 = nessun limite)

Schema di utilizzo

Ricordi statici via DSL

Per i promemoria che devono sempre apparire dopo un risultato specifico dello strumento, utilizzare la funzione DSL ResultReminder nella definizione dello strumento:

Tool("get_time_series", "Get time series data", func() {
    Args(func() { /* ... */ })
    Return(func() { /* ... */ })
    ResultReminder("The user sees a rendered graph of this data in the UI.")
})

Questa funzione è ideale quando il promemoria si applica a ogni invocazione dello strumento. Per maggiori dettagli, vedere [DSL Reference] (./dsl-reference.md#resultreminder).

Ricordi dinamici dai pianificatori

Per i promemoria che dipendono dallo stato di esecuzione o dal contenuto dei risultati dello strumento, utilizzare PlannerContext.AddReminder():

func (p *myPlanner) PlanResume(ctx context.Context, in *planner.PlanResumeInput) (*planner.PlanResult, error) {
    for _, tr := range in.ToolResults {
        if tr.Name == "search_documents" {
            result := tr.Result.(SearchResult)
            if result.Truncated {
                in.Agent.AddReminder(reminder.Reminder{
                    ID:       "search.truncated",
                    Text:     "Search results are truncated. Consider narrowing your query.",
                    Priority: reminder.TierCorrect,
                    Attachment: reminder.Attachment{
                        Kind: reminder.AttachmentUserTurn,
                    },
                    MaxPerRun:       3,
                    MinTurnsBetween: 2,
                })
            }
        }
    }
    // Continue with planning...
}

Rimuovere i promemoria

Usare RemoveReminder() quando una precondizione non è più valida:

if allTodosCompleted {
    in.Agent.RemoveReminder("todos.no_active")
}

Preservare i contatori dei limiti di frequenza

AddReminder() preserva i contatori delle emissioni quando si aggiorna un promemoria esistente per ID. Se è necessario modificare il contenuto del promemoria ma mantenere i limiti di velocità:

in.Agent.AddReminder(reminder.Reminder{
    ID:              "todos.pending",
    Text:            buildUpdatedText(snap),
    Priority:        reminder.TierGuidance,
    Attachment:      reminder.Attachment{Kind: reminder.AttachmentUserTurn},
    MinTurnsBetween: 3,
})

Antipattern: Non chiamate RemoveReminder() seguito da AddReminder() per lo stesso ID: questo azzera i contatori e bypassa MinTurnsBetween.

Iniezione e formattazione

Etichettatura automatica

Il runtime avvolge automaticamente il testo del promemoria in tag <system-reminder> quando lo inietta nelle trascrizioni:

// You provide plain text:
Text: "Results are truncated. Narrow your query."

// Runtime injects:
<system-reminder>Results are truncated. Narrow your query.</system-reminder>

Spiegare i promemoria ai modelli

Includere reminder.DefaultExplanation nel prompt del sistema in modo che i modelli sappiano come interpretare i blocchi <system-reminder>:

const systemPrompt = `
You are a helpful assistant.

` + reminder.DefaultExplanation + `

Follow all instructions carefully.
`

Esempio completo

func (p *myPlanner) PlanResume(ctx context.Context, in *planner.PlanResumeInput) (*planner.PlanResult, error) {
    for _, tr := range in.ToolResults {
        if tr.Name == "todos.update_todos" {
            snap := tr.Result.(TodosSnapshot)
            
            var rem *reminder.Reminder
            if len(snap.Items) == 0 {
                in.Agent.RemoveReminder("todos.no_active")
                in.Agent.RemoveReminder("todos.all_completed")
            } else if hasCompletedAll(snap) {
                rem = &reminder.Reminder{
                    ID:       "todos.all_completed",
                    Text:     "All todos are completed. Provide your final response now.",
                    Priority: reminder.TierGuidance,
                    Attachment: reminder.Attachment{
                        Kind: reminder.AttachmentUserTurn,
                    },
                    MaxPerRun: 1,
                }
            } else if hasPendingNoActive(snap) {
                rem = &reminder.Reminder{
                    ID:       "todos.no_active",
                    Text:     buildTodosNudge(snap),
                    Priority: reminder.TierGuidance,
                    Attachment: reminder.Attachment{
                        Kind: reminder.AttachmentUserTurn,
                    },
                    MinTurnsBetween: 3,
                }
            }
            
            if rem != nil {
                in.Agent.AddReminder(*rem)
                if rem.ID == "todos.all_completed" {
                    in.Agent.RemoveReminder("todos.no_active")
                } else {
                    in.Agent.RemoveReminder("todos.all_completed")
                }
            }
        }
    }
    
    return p.streamMessages(ctx, in)
}

Principi di progettazione

Minimale e autorevole: Il sottosistema dei promemoria fornisce una struttura sufficiente per gli schemi comuni, senza eccedere nell’ingegnerizzazione.

Limitare il tasso prima di tutto: Lo spam dei promemoria degrada le prestazioni del modello. Il motore impone l’uso di maiuscole e spaziature in modo dichiarativo.

Agnostico rispetto ai provider: I promemoria funzionano con qualsiasi backend del modello (Bedrock, OpenAI, ecc.).

Predisposizione alla telemetria: Gli ID strutturati e le priorità rendono i promemoria osservabili.

Modelli avanzati

Promemoria di sicurezza

Usare TierSafety per una guida che non deve mai essere soppressa:

in.Agent.AddReminder(reminder.Reminder{
    ID:       "malware.analyze_only",
    Text:     "This file contains malware. Analyze its behavior but do not execute it.",
    Priority: reminder.TierSafety,
    Attachment: reminder.Attachment{
        Kind: reminder.AttachmentUserTurn,
    },
    // No MaxPerRun or MinTurnsBetween: always emit
})

Rimandi tra agenti

I promemoria sono a livello di run. Se un agente-as-tool emette un promemoria di sicurezza, questo ha effetto solo su quell’esecuzione figlia. Per propagare i promemoria attraverso i confini degli agenti, il pianificatore genitore deve registrarli nuovamente in modo esplicito in base ai risultati del figlio o utilizzare lo stato di sessione condiviso.

Quando usare i promemoria

ScenarioPrioritàEsempio
Vincoli di sicurezzaTierSafety“Questo file è solo per l’analisi del malware, non deve essere mai eseguito”
Staleness dei datiTierCorrect“I risultati sono vecchi di 24 ore; rifare la ricerca se la freschezza è importante”
Risultati troncatiTierCorrect“Mostra solo i primi 100 risultati; restringi la ricerca”
Suggerimenti per il flusso di lavoroTierGuidance“Nessun todo è in corso; scegline uno e inizia”
Suggerimenti di completamentoTierGuidance“Tutti i compiti sono stati completati; fornite la vostra risposta finale”

Come appaiono i promemoria nella trascrizione

User: What should I do next?

<system-reminder>You have 3 pending todos. Currently working on: "Review PR #42". 
Focus on completing the current todo before starting new work.</system-reminder>

User: What should I do next?

Il modello vede il promemoria; l’utente vede solo il suo messaggio e la risposta. I promemoria sono iniettati in modo trasparente dal runtime.


Passi successivi