Memoria e sessioni
Questa guida tratta del modello di trascrizione di Goa-AI, della persistenza della memoria e di come modellare conversazioni a più turni e flussi di lavoro di lunga durata.
Perché le trascrizioni sono importanti
Goa-AI considera la trascrizione la fonte di verità per la conversazione visibile al modello: una sequenza ordinata di messaggi e interazioni con strumenti sufficiente per:
- Ricostruire i payload del provider per ogni chiamata al modello
- Guidare i pianificatori, inclusi nuovi tentativi e riparazione degli strumenti
- Fornire alle interfacce una cronologia accurata
Poiché la trascrizione è autorevole per l’input del modello, non è necessario gestire manualmente:
- elenchi separati di chiamate e risultati precedenti
- strutture ad hoc per lo stato della conversazione
- copie per turno dei messaggi precedenti
Per la cronologia della conversazione, salva e passa solo la trascrizione; Goa-AI e gli adattatori ricostruiscono l’input del provider. Stato dell’esecuzione, annullamento, checkpoint e record immutabili appartengono all’archivio separato del runtime descritto sotto.
Messaggi e parti
Al confine del modello, Goa-AI utilizza i valori model.Message per rappresentare la trascrizione. Ogni messaggio ha un ruolo (user, assistant) e un elenco ordinato di parti:
| Tipo di parte | Descrizione |
|---|---|
ThinkingPart | Contenuto del ragionamento del fornitore (testo in chiaro + firma o byte redatti). Non è rivolto all’utente; è usato per la verifica/riproduzione e per le interfacce utente opzionali di “ragionamento”. |
TextPart | Testo visibile all’utente (domande, risposte, spiegazioni). |
ImagePart | Multimodal image content (bytes or URL/metadata) for providers that support images. |
DocumentPart | Document content (text/bytes/URI/chunks) attached to messages for providers that support document parts. |
CitationsPart | Structured citations metadata produced by providers (for UI display / audit). |
ToolUsePart | Chiamata allo strumento avviata dall’assistente con ID, Name (ID strumento canonico) e Input (carico utile JSON). |
ToolResultPart | Risultato dell’utente/strumento correlato a un uso precedente dello strumento tramite ToolUseID e Content (payload JSON). |
CacheCheckpointPart | Marker for prompt cache boundaries (provider-dependent, not user-facing). |
L’ordine è sacro:
- Il messaggio di un assistente che utilizza uno strumento è tipicamente simile a:
ThinkingPart(se presente), poiTextPartopzionale, poi uno o piùToolUsePart - Un messaggio di risultato utente/strumento contiene tipicamente uno o più
ToolResultPartche fanno riferimento a precedenti ID di utilizzo dello strumento, oltre a contenuto opzionale dell’utente (TextPart,ImagePart,DocumentPart)
Gli adattatori dei provider di Goa-AI (ad esempio, Bedrock Converse) ricodificano queste parti in blocchi specifici del provider senza riordino.
Il contratto di trascrizione
Il contratto di trascrizione di alto livello in Goa-AI è:
- L’applicazione (o il runtime) presenta ogni evento per un’esecuzione in ordine: pensiero dell’assistente, testo, tool_use (ID + args), tool_result dell’utente (tool_use_id + content), messaggi successivi dell’assistente e così via
- Prima di ogni chiamata al modello, il chiamante fornisce l’intera trascrizione** di quella sessione come
[]*model.Message, con l’ultimo elemento che è il nuovo delta (testo dell’utente o risultato dello strumento) - Goa-AI ricodifica la trascrizione nel formato di chat del provider nello stesso ordine
Non esiste un’API separata per la “cronologia degli strumenti”; la trascrizione è la cronologia.
Gli adattatori dei modelli non conservano stato tra una chiamata e l’altra. Ogni
model.Request deve contenere la trascrizione completa pronta per il provider;
un identificatore di run non induce l’adattatore a caricare i messaggi
precedenti. I client pubblici dei modelli validano richiesta e risposta
completa prima che il codice del planner possa osservarle.
Compressione della cronologia
La policy History(...) di un agente può riassumere i turni meno recenti
mantenendo una coda esatta e limitata. I valori CompressAt... stabiliscono
quando avviare il riepilogo; i valori KeepMax... stabiliscono quali turni
completi più recenti restano invariati. Il runtime non tronca mai un turno.
La compressione richiede un HistoryModel configurato. I criteri basati sui
token richiedono inoltre il conteggio esatto fornito dal relativo client del
modello. Bedrock Runtime non può contare richieste con structured output e
alcuni modelli Claude correnti richiedono l’endpoint Mantle separato di AWS.
Vedere Runtime → Policy della cronologia e
Riferimento DSL → History.
Come questo semplifica i pianificatori e le interfacce utente
- Pianificatori: Ricevono la trascrizione corrente in
planner.PlanInput.Messageseplanner.PlanResumeInput.Messages. Possono decidere cosa fare basandosi esclusivamente sui messaggi, senza dover ricorrere a uno stato aggiuntivo. - UI: Possono rendere la cronologia della chat, i nastri degli strumenti e le schede degli agenti dalla stessa trascrizione sottostante che persiste per il modello. Non sono necessarie strutture separate di “log degli strumenti”.
- adattatori Provider: Non indovinano mai quali strumenti sono stati chiamati o quali risultati appartengono a un determinato punto; mappano semplicemente le parti della trascrizione → i blocchi dei provider.
Riproduzione della trascrizione del runtime
Il runtime salva aggiunte canoniche di model.Message nei record ordinati
dell’esecuzione. transcript_messages_seeded contiene i messaggi presenti
prima dell’avvio dell’esecuzione; transcript_messages_appended contiene i
messaggi accettati durante l’esecuzione. I record iniziali ricostruiscono
l’input del modello, ma non vengono pubblicati come una nuova risposta
dell’assistente.
Usa la funzione pubblica di riproduzione quando il recupero o l’ispezione richiede l’esatta sequenza di messaggi pronta per il provider:
import "goa.design/goa-ai/runtime/agent/transcript"
messages, err := transcript.BuildMessagesFromRunLog(ctx, runtimeStore, runID)
if err != nil {
return err
}
BuildMessagesFromRunLog scorre le pagine di
storage.Store.ListRunRecords e riproduce solo i record canonici della
trascrizione nel loro ordine di archiviazione. Se i record sono già caricati,
ReplayRunLogEvents esegue la stessa proiezione. Gli adattatori del provider
mantengono l’ordine delle parti; ValidatePlannerTranscript e
ValidateBedrock consentono di verificare una trascrizione al confine
appropriato.
ValidatePlannerTranscript richiede che ogni gruppo di chiamate a strumenti
dell’assistente sia seguito immediatamente da un solo messaggio utente con
esattamente un risultato per ogni ID di chiamata. Quando il ragionamento è
abilitato, ValidateBedrock richiede inoltre che ogni messaggio dell’assistente
che chiama uno strumento inizi con un ThinkingPart. Nessun validatore modifica
i messaggi.
Questi record servono al recupero e all’ispezione dei workflow. Non sostituiscono la trascrizione di proprietà del prodotto usata per cronologia chat, valutazioni, ricerca, conservazione o eliminazione dei dati dei clienti.
Sessioni, corse e trascrizioni
Goa-AI separa lo stato della conversazione in tre livelli:
Sessione (
SessionID) - una conversazione o un flusso di lavoro nel tempo:- ad esempio, una sessione di chat, un ticket di riparazione, un’attività di ricerca
- Più esecuzioni possono appartenere alla stessa sessione
Esecuzione (
RunID) - un’esecuzione di un agente:- Ogni chiamata a un client agente (
Run/Start) crea un’esecuzione - Le esecuzioni hanno stato, fasi ed etichette
- Ogni chiamata a un client agente (
Transcript - la cronologia completa dei messaggi e delle interazioni con gli strumenti per un’esecuzione:
- Rappresentata come
[]*model.Message - Persistito tramite
memory.Storecome eventi ordinati in memoria
- Rappresentata come
SessionID e TurnID in pratica
Quando si chiama un agente:
store := storageinmem.New()
if _, err := store.CreateSession(ctx, "chat-session-123", time.Now().UTC()); err != nil {
panic(err)
}
rt := runtime.New(store)
client := chat.NewClient(rt)
out, err := client.Run(ctx, "chat-session-123", messages,
runtime.WithTurnID("turn-1"), // optional but recommended for chat
)
SessionID: Raggruppa tutte le corse per una conversazione; spesso viene utilizzato come chiave di ricerca nei runtime records e nei dashboardTurnID: Raggruppa gli eventi per un singolo utente → interazione con l’assistente; opzionale ma utile per le interfacce utente e i log
Le sessioni terminano esplicitamente (ad esempio quando si elimina una conversazione). Una volta terminata una sessione, non devono iniziare nuove run sotto di essa.
Memoria del prodotto e archiviazione del runtime
Goa-AI separa due tipi di dati duraturi perché hanno proprietari diversi:
- La memoria del prodotto contiene la trascrizione e i dati applicativi derivati. Il prodotto decide cosa conservare, mostrare, cercare o eliminare.
- L’archiviazione del runtime contiene lo stato necessario a Goa-AI per eseguire e continuare le esecuzioni: stato della sessione, metadati, checkpoint privati e record immutabili.
Per esempio, un servizio chat conserva conversazione, valutazioni e campi di ricerca nel proprio database. L’archivio del runtime registra che run-42 è iniziata, quale esecuzione figlia ha avviato, se è stato richiesto l’annullamento e come è terminata. Non diventa il database delle trascrizioni.
Archivio della memoria (memory.Store)
Conserva messaggi, chiamate e risultati degli strumenti, note e ragionamento del pianificatore. Da questi eventi si ricostruiscono model.Transcript e messaggi del provider. Il prodotto possiede questi dati visibili al modello.
Archivio del runtime (storage.Store)
L’host fornisce una sola implementazione di storage.Store, proprietaria di tutte le scritture per:
- ambito e stato attivo, terminato o eliminato della sessione
- identità, parentela, etichette, decisione iniziale e stato corrente dell’esecuzione
- byte privati necessari a continuare un’esecuzione sospesa
- record ordinati e immutabili usati per ispezione e provenienza dei prompt
Il runtime richiede questa dipendenza:
store := newRuntimeStore()
rt := runtime.New(store, runtime.WithEngine(eng))
In un’applicazione a processo singolo, store può essere un adattatore locale. In un sistema distribuito, un servizio possiede il database ed espone metodi tipizzati; i worker implementano storage.Store chiamando quel servizio. Servizi distinti non scrivono direttamente nelle stesse raccolte.
Salvare insieme cambiamenti di ciclo di vita e record
Ogni metodo salva stato e record corrispondente nella stessa operazione:
StartRootRunsalva metadati radice e primo record.StartChildRunsalva collegamento al padre, metadati del figlio e primo record. Un nuovo figlio richiede un padre attivo; un retry identico già accettato resta valido dopo l’arresto del padre.StartOneShotRunsalva un’esecuzione senza sessione e il primo record.StartOneShotChildRunsalva insieme il collegamento al padre senza sessione e l’avvio del figlio. Applica la stessa regola del padre attivo e del retry identico.RecordRunCancellationsalva il primo motivo di annullamento e il record.RecordRunSuspensionsalva checkpoint privato, stato sospeso e record.RecordRunTerminalsalva stato finale e record.
Così non può esistere un’esecuzione completata senza record finale, né un checkpoint salvato mentre l’esecuzione appare ancora attiva. I record ordinari usano AppendRunRecord; ListRunRecords e ListSessionRunRecords li leggono tramite un cursor restituito senza modifiche.
Nuovi tentativi esatti
Le activity possono essere ripetute. Una ripetizione esatta riesce e restituisce l’identificatore originale: identità, ora, etichette, chiave e payload, checkpoint, stato e motivo devono essere identici. Qualsiasi differenza produce un conflitto; l’archivio non indovina né sovrascrive. Il primo motivo di annullamento è permanente.
Per ogni avvio, annullamento, sospensione e completamento, lo storage ricorda anche il record esatto scelto dalla prima scrittura riuscita. Ripetere il cambiamento del ciclo di vita con un record diverso produce un conflitto, anche quando lo stato e gli altri campi del ciclo di vita coincidono.
JSON degli eventi persistenti
Il runtime decodifica i payload di RunStarted, RunSuspended, RunCompleted
e ChildRunLinked come un unico valore JSON tipizzato. I campi sconosciuti e
gli ulteriori valori JSON vengono rifiutati. I record esistenti devono
rispettare esattamente queste forme prima che il runtime possa riprodurli o
consegnarli; i dati salvati non compatibili non vengono mai ignorati.
Origine dell’annullamento
Lo storage del runtime distingue una richiesta di annullamento dal motivo per cui termina un’esecuzione:
- Quando un workflow attivo accetta una chiamata esplicita a
CancelRun, salva in un’unica operazione il primo motivo nei metadati dell’esecuzione e un record di tipostorage.CancellationRecordType(runtime.cancellation_intent) con lo stesso motivo. Il successivo recordRunCompletedannullato deve contenere quel medesimo motivo. - Se
StartRootRunoStartChildRunrileva che la sessione è già terminata, l’operazione di avvio salvasession_endednei metadati insieme aRunStartede al recordRunCompletedannullato. Non salva un recordstorage.CancellationRecordType, perché non è stata fatta una richiesta di annullamento separata. - Se il motore dei workflow annulla un’esecuzione senza una richiesta già
registrata, il motivo di annullamento nei metadati rimane vuoto e non esiste
alcun record
storage.CancellationRecordType. Il recordRunCompletedannullato contieneengine_canceled. In questo caso, il campo vuoto ha un significato preciso e non indica dati mancanti.
Queste sono le tre combinazioni valide tra i metadati dell’esecuzione e i record di annullamento. Uno storage persistente deve conservarle esattamente.
Avvio di una continuazione
Una continuazione richiede un’esecuzione precedente esistente con stato
suspended. Il successore deve usare la stessa sessione, lo stesso agente e la
stessa esecuzione padre. Lo storage verifica questi quattro dati nella stessa
transazione che creerebbe il successore. Se un dato non coincide, rifiuta
l’operazione prima di scrivere l’avvio del successore o un collegamento al
padre.
Il record RunStarted del successore conserva PredecessorRunID. RunMeta non
duplica questa relazione. I reader ricostruiscono la cronologia delle
continuazioni dai record che l’hanno stabilita.
Ordine di avvio
Per le esecuzioni radice, il motore accetta il workflow prima che il runtime
scriva nello storage. Non viene creato alcun record pending prima
dell’accettazione del motore. La prima activity durevole del workflow chiama
StartRootRun:
- se la sessione è attiva, lo storage scrive
RunStarted, contrassegna l’esecuzione come attiva e il workflow prosegue; - se la sessione è terminata dopo l’accettazione del workflow, lo storage scrive
comunque
RunStarted, lo fa seguire subito da unRunCompletedannullato e il workflow si ferma prima del pianificatore o degli strumenti.
I workflow figli usano StartChildRun. Lo storage scrive ChildRunLinked sul
padre e poi RunStarted sul figlio. Se la sessione è terminata, scrive anche il
RunCompleted annullato del figlio. Ogni workflow accettato dal motore ha quindi
un record RunStarted, compreso il lavoro fermato perché la sessione era
terminata. Un nuovo figlio richiede un padre attivo. Un retry identico di un
avvio figlio già accettato resta valido dopo l’arresto del padre; un retry
modificato o un nuovo figlio vengono rifiutati. Temporal termina un workflow
figlio se il workflow padre si chiude per primo.
Il lavoro radice senza sessione usa StartOneShotRun: riceve i normali
metadati dell’esecuzione e RunStarted, ma non crea né usa una sessione. Un
agente chiamato come strumento da quell’esecuzione usa StartOneShotChildRun.
Alla prima chiamata, il padre deve esistere, non avere una sessione ed essere
ancora attivo. Lo storage scrive ChildRunLinked sul padre e RunStarted sul
figlio senza sessione in una sola operazione.
Una ripetizione esatta di StartOneShotChildRun riesce anche se il padre è
terminato dopo la prima scrittura, perché il rapporto con il figlio era già
stato accettato. La ripetizione deve usare la stessa identità del figlio e le
stesse chiavi e contenuti di entrambi i record. Una ripetizione modificata
produce un conflitto e non è possibile aggiungere un nuovo figlio dopo il
termine del padre.
Il risultato dell’avvio riporta la decisione originale, non lo stato corrente dell’esecuzione. Ripetere un avvio dopo il completamento restituisce quindi la stessa decisione presa alla prima scrittura.
Ciclo di vita ed eliminazione delle sessioni
L’applicazione host crea, termina e rimuove le sessioni; i worker non lo fanno. Crea prima del lavoro con sessione, termina quando non deve iniziare nuovo lavoro e rimuove definitivamente solo dopo la conclusione di tutte le esecuzioni.
- Terminare impedisce nuovo lavoro, consentendo alle esecuzioni attive di salvare i record finali.
- Eliminare rimuove sessione, esecuzioni, checkpoint e record dopo la conclusione. L’ID resta inutilizzabile.
runtime/agent/storage/inmem espone CreateSession, EndSession e PurgeSession per esempi e test. In produzione li implementa il servizio proprietario del database. Le versioni dei prompt derivano dai record prompt_rendered e dai collegamenti padre-figlio; non esiste una seconda lista che possa divergere.
Migrazione dagli archivi separati
Sono rimossi session.Store, runlog.Store, runtime.WithSessionStore, runtime.WithRunEventStore, i metodi runtime CreateSession, EndSession e PurgeSession, e i package features/session/mongo e features/runlog/mongo.
Implementa un unico storage.Store del package
goa.design/goa-ai/runtime/agent/storage e passalo come primo argomento di
runtime.New. Sposta l’amministrazione delle sessioni nel servizio host
proprietario dei dati. I worker remoti lo chiamano tramite API tipizzata, senza
importarne l’adattatore database.
Prima che il nuovo runtime scriva, i dati esistenti devono rispettare il contratto dello storage integrato. Metadati, checkpoint e record devono supportare le operazioni del ciclo di vita descritte sopra, e i vecchi writer degli storage separati non devono sovrapporsi ai nuovi. L’applicazione host sceglie la procedura di conversione e ripristino per il proprio database e ambiente, quindi distribuisce insieme il proprietario e tutti i worker.
I comandi di consegna del completamento non aggiungono una migrazione dello schema del database e non cambiano alcun formato pubblico. Cambiano però il contratto del codice Go. Aggiorna insieme il runtime e la relativa implementazione dello storage:
- sostituisci ogni chiamata a
Runtime.RepairRunCompletionconRuntime.EnsureRunCompletion; - implementa
LoadSessionStatusin ognistorage.Storepersonalizzato; - configura
Runtime.WithStreamprima di chiamare uno dei comandi di garanzia per una sessione attiva; e - prevedi che l’avvio di un nuovo figlio fallisca dopo l’arresto del padre. Un retry identico di un avvio già accettato dallo storage resta valido.
Anche i record persistenti esistenti devono rispettare il contratto JSON esatto descritto sopra.
Modelli comuni
Sessioni di chat
- Utilizzare un
SessionIDper sessione di chat - Avviare una nuova esecuzione per ogni turno dell’utente o attività
- Conservare la trascrizione del prodotto nel servizio di chat; usare i record del runtime per stato, continuazione e ispezione
Flussi di lavoro di lunga durata
- Usare una esecuzione per ogni workflow accettato dal motore
- Quando un’esecuzione richiede un input esterno, il workflow termina; la risposta avvia una nuova esecuzione nella stessa sessione usando il checkpoint salvato
- Usare
SessionIDper raggruppare flussi di lavoro correlati (ad esempio, per ticket o incidente) - Affidarsi agli eventi
run.PhaseeRunCompletedper il monitoraggio dello stato
Ricerca e cruscotti
- Pagina
storage.StoreperRunIDnelle interfacce di audit e debug - Caricamento delle trascrizioni da
memory.Storesu richiesta per le corse selezionate
Migliori pratiche
Correlare sempre i risultati degli strumenti: Assicurarsi che le implementazioni degli strumenti e i pianificatori conservino gli ID tool_use e mappino i risultati degli strumenti al corretto
ToolUseParttramiteToolResultPart.ToolUseIDUtilizzare schemi forti e descrittivi: Tipi, descrizioni ed esempi ricchi di
Args/Returnnella progettazione di Goa producono carichi utili/risultati più chiari nella trascrizioneLasciare che sia il runtime a gestire lo stato: Evitare di mantenere array paralleli di “cronologia degli strumenti” o fette di “messaggi precedenti” nel pianificatore. Leggere da
PlanInput.Messages/PlanResumeInput.Messagese affidarsi al runtime per aggiungere nuove partiPersistere le trascrizioni una volta, riutilizzarle ovunque: Qualunque sia lo store scelto, trattate la trascrizione come un’infrastruttura riutilizzabile: la stessa trascrizione supporta le chiamate al modello, l’interfaccia della chat, l’interfaccia di debug e l’analisi offline
Indicizzare i campi interrogati di frequente: ID sessione, ID esecuzione, stato per query efficienti
Archiviare le vecchie trascrizioni: Ridurre i costi di archiviazione archiviando le sessioni completate
Prossimi passi
- Produzione - Distribuzione con Temporal, UI in streaming e integrazione del modello
- Runtime - Comprendere il ciclo piano/esecuzione
- Composizione di agenti - Costruire grafi di agenti complessi